From b66f56a4e7d3413ec45e832ac3e05239869c8db7 Mon Sep 17 00:00:00 2001 From: Andre Vianna Date: Wed, 18 Sep 2024 11:06:56 -0400 Subject: [PATCH 1/8] Add CancelCommand and Bind it to EscapeKey. Also added unit tests for that. --- .../Commands/CancelCommandTests.cs | 25 ++ .../Commands/InsertCommandTests.cs | 48 +-- src/RadLine.Tests/LineEditorTests.cs | 286 ++++++++++-------- .../Utilities/TestInputSource.cs | 170 ++++++----- src/RadLine/Commands/CancelCommand.cs | 10 + src/RadLine/Internal/InputBuffer.cs | 262 ++++++++-------- src/RadLine/KeyBindingsExtensions.cs | 147 ++++----- src/RadLine/LineBuffer.cs | 14 +- src/RadLine/LineEditor.cs | 5 +- src/RadLine/LineEditorContext.cs | 8 +- ...ianna_L000783_2024-09-18.11_02_30.coverage | Bin 0 -> 132635 bytes 11 files changed, 525 insertions(+), 450 deletions(-) create mode 100644 src/RadLine.Tests/Commands/CancelCommandTests.cs create mode 100644 src/RadLine/Commands/CancelCommand.cs create mode 100644 src/TestResults/42faa95d-7ac9-4f60-aad0-41a32909b4c0/andrevianna_L000783_2024-09-18.11_02_30.coverage diff --git a/src/RadLine.Tests/Commands/CancelCommandTests.cs b/src/RadLine.Tests/Commands/CancelCommandTests.cs new file mode 100644 index 0000000..62c642b --- /dev/null +++ b/src/RadLine.Tests/Commands/CancelCommandTests.cs @@ -0,0 +1,25 @@ +using Shouldly; +using Xunit; + +namespace RadLine.Tests +{ + public sealed class CancelCommandTests + { + [Fact] + public void Should_Cancel_Input() + { + // Given + var buffer = new LineBuffer("Foo"); + var context = new LineEditorContext(buffer); + buffer.Insert("Bar"); + var command = new CancelCommand(); + + // When + command.Execute(context); + + // Then + buffer.Content.ShouldBe("Foo"); + buffer.Position.ShouldBe(3); + } + } +} diff --git a/src/RadLine.Tests/Commands/InsertCommandTests.cs b/src/RadLine.Tests/Commands/InsertCommandTests.cs index c417e76..39e328d 100644 --- a/src/RadLine.Tests/Commands/InsertCommandTests.cs +++ b/src/RadLine.Tests/Commands/InsertCommandTests.cs @@ -1,24 +1,24 @@ -using Shouldly; -using Xunit; - -namespace RadLine.Tests -{ - public sealed class InsertCommandTests - { - [Fact] - public void Should_Insert_Text_At_Position() - { - // Given - var buffer = new LineBuffer("Foo"); - var context = new LineEditorContext(buffer); - var command = new InsertCommand('l'); - - // When - command.Execute(context); - - // Then - buffer.Content.ShouldBe("Fool"); - buffer.Position.ShouldBe(4); - } - } -} +using Shouldly; +using Xunit; + +namespace RadLine.Tests +{ + public sealed class InsertCommandTests + { + [Fact] + public void Should_Insert_Text_At_Position() + { + // Given + var buffer = new LineBuffer("Foo"); + var context = new LineEditorContext(buffer); + var command = new InsertCommand('l'); + + // When + command.Execute(context); + + // Then + buffer.Content.ShouldBe("Fool"); + buffer.Position.ShouldBe(4); + } + } +} diff --git a/src/RadLine.Tests/LineEditorTests.cs b/src/RadLine.Tests/LineEditorTests.cs index ef6594f..efac4bc 100644 --- a/src/RadLine.Tests/LineEditorTests.cs +++ b/src/RadLine.Tests/LineEditorTests.cs @@ -1,133 +1,153 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Shouldly; -using Spectre.Console.Testing; -using Xunit; - -namespace RadLine.Tests -{ - public sealed class LineEditorTests - { - [Fact] - public async Task Should_Return_Entered_Text_When_Pressing_Enter() - { - // Given - var editor = new LineEditor( - new TestConsole(), - new TestInputSource() - .Push("Patrik") - .PushEnter()); - - // When - var result = await editor.ReadLine(CancellationToken.None); - - // Then - result.ShouldBe("Patrik"); - } - - [Fact] - public async Task Should_Add_New_Line_When_Pressing_Shift_And_Enter() - { - // Given - var editor = new LineEditor( - new TestConsole(), - new TestInputSource() - .Push("Patrik") - .PushNewLine() - .Push("Svensson") - .PushEnter()) - { - MultiLine = true, - }; - - // When - var result = await editor.ReadLine(CancellationToken.None); - - // Then - result.ShouldBe($"Patrik{Environment.NewLine}Svensson"); - } - - [Fact] - public async Task Should_Move_To_Previous_Item_In_History() - { - // Given - var editor = new LineEditor( - new TestConsole(), - new TestInputSource() - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .PushEnter()); - - editor.History.Add("Foo"); - editor.History.Add("Bar"); - editor.History.Add("Baz"); - - // When - var result = await editor.ReadLine(CancellationToken.None); - - // Then - result.ShouldBe("Foo"); - } - - [Fact] - public async Task Should_Move_To_Next_Item_In_History() - { - // Given - var editor = new LineEditor( - new TestConsole(), - new TestInputSource() - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.DownArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.DownArrow, ConsoleModifiers.Control) - .PushEnter()); - - editor.History.Add("Foo"); - editor.History.Add("Bar"); - editor.History.Add("Baz"); - - // When - var result = await editor.ReadLine(CancellationToken.None); - - // Then - result.ShouldBe("Baz"); - } - - [Fact] - public async Task Should_Add_Entered_Text_To_History() - { - // Given - var input = new TestInputSource(); - var editor = new LineEditor(new TestConsole(), input); - input.Push("Patrik").PushEnter(); - await editor.ReadLine(CancellationToken.None); - - // When - input.Push(ConsoleKey.UpArrow, ConsoleModifiers.Control).PushEnter(); - var result = await editor.ReadLine(CancellationToken.None); - - // Then - result.ShouldBe("Patrik"); - } - - [Fact] - public async Task Should_Not_Add_Entered_Text_To_History_If_Its_The_Same_As_The_Last_Entry() - { - // Given - var input = new TestInputSource(); - var editor = new LineEditor(new TestConsole(), input); - input.Push("Patrik").PushNewLine().Push("Svensson").PushEnter(); - await editor.ReadLine(CancellationToken.None); - - // When - input.Push("Patrik").PushNewLine().Push("Svensson").PushEnter(); - var result = await editor.ReadLine(CancellationToken.None); - - // Then - editor.History.Count.ShouldBe(1); - } - } -} +using System; +using System.Threading; +using System.Threading.Tasks; +using Shouldly; +using Spectre.Console.Testing; +using Xunit; + +namespace RadLine.Tests +{ + public sealed class LineEditorTests + { + [Fact] + public async Task Should_Return_Original_Text_When_Pressing_Escape() + { + // Given + var editor = new LineEditor( + new TestConsole(), + new TestInputSource() + .Push("Bar") + .PushEscape()) + { + Text = "Foo", + }; + + // When + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe("Foo"); + } + + [Fact] + public async Task Should_Return_Entered_Text_When_Pressing_Enter() + { + // Given + var editor = new LineEditor( + new TestConsole(), + new TestInputSource() + .Push("Patrik") + .PushEnter()); + + // When + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe("Patrik"); + } + + [Fact] + public async Task Should_Add_New_Line_When_Pressing_Shift_And_Enter() + { + // Given + var editor = new LineEditor( + new TestConsole(), + new TestInputSource() + .Push("Patrik") + .PushNewLine() + .Push("Svensson") + .PushEnter()) + { + MultiLine = true, + }; + + // When + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe($"Patrik{Environment.NewLine}Svensson"); + } + + [Fact] + public async Task Should_Move_To_Previous_Item_In_History() + { + // Given + var editor = new LineEditor( + new TestConsole(), + new TestInputSource() + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .PushEnter()); + + editor.History.Add("Foo"); + editor.History.Add("Bar"); + editor.History.Add("Baz"); + + // When + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe("Foo"); + } + + [Fact] + public async Task Should_Move_To_Next_Item_In_History() + { + // Given + var editor = new LineEditor( + new TestConsole(), + new TestInputSource() + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.DownArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.DownArrow, ConsoleModifiers.Control) + .PushEnter()); + + editor.History.Add("Foo"); + editor.History.Add("Bar"); + editor.History.Add("Baz"); + + // When + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe("Baz"); + } + + [Fact] + public async Task Should_Add_Entered_Text_To_History() + { + // Given + var input = new TestInputSource(); + var editor = new LineEditor(new TestConsole(), input); + input.Push("Patrik").PushEnter(); + await editor.ReadLine(CancellationToken.None); + + // When + input.Push(ConsoleKey.UpArrow, ConsoleModifiers.Control).PushEnter(); + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe("Patrik"); + } + + [Fact] + public async Task Should_Not_Add_Entered_Text_To_History_If_Its_The_Same_As_The_Last_Entry() + { + // Given + var input = new TestInputSource(); + var editor = new LineEditor(new TestConsole(), input); + input.Push("Patrik").PushNewLine().Push("Svensson").PushEnter(); + await editor.ReadLine(CancellationToken.None); + + // When + input.Push("Patrik").PushNewLine().Push("Svensson").PushEnter(); + var result = await editor.ReadLine(CancellationToken.None); + + // Then + editor.History.Count.ShouldBe(1); + } + } +} diff --git a/src/RadLine.Tests/Utilities/TestInputSource.cs b/src/RadLine.Tests/Utilities/TestInputSource.cs index c814b70..d9bf8fe 100644 --- a/src/RadLine.Tests/Utilities/TestInputSource.cs +++ b/src/RadLine.Tests/Utilities/TestInputSource.cs @@ -1,82 +1,88 @@ -using System; -using System.Collections.Generic; - -namespace RadLine.Tests -{ - public sealed class TestInputSource : IInputSource - { - private readonly Queue _input; - - public bool ByPassProcessing => true; - - public TestInputSource() - { - _input = new Queue(); - } - - public TestInputSource Push(string input) - { - if (input is null) - { - throw new ArgumentNullException(nameof(input)); - } - - foreach (var character in input) - { - Push(character); - } - - return this; - } - - public TestInputSource Push(char input) - { - var control = char.IsUpper(input); - _input.Enqueue(new ConsoleKeyInfo(input, (ConsoleKey)input, false, false, control)); - return this; - } - - public TestInputSource Push(ConsoleKey input) - { - _input.Enqueue(new ConsoleKeyInfo((char)input, input, false, false, false)); - return this; - } - - public TestInputSource PushNewLine() - { - Push(ConsoleKey.Enter, ConsoleModifiers.Shift); - return this; - } - - public TestInputSource PushEnter() - { - Push(ConsoleKey.Enter); - return this; - } - - public TestInputSource Push(ConsoleKey input, ConsoleModifiers modifiers) - { - var shift = modifiers.HasFlag(ConsoleModifiers.Shift); - var control = modifiers.HasFlag(ConsoleModifiers.Control); - var alt = modifiers.HasFlag(ConsoleModifiers.Alt); - - _input.Enqueue(new ConsoleKeyInfo((char)0, input, shift, alt, control)); - return this; - } - - public bool IsKeyAvailable() - { - return _input.Count > 0; - } - - ConsoleKeyInfo IInputSource.ReadKey() - { - if (_input.Count == 0) - { - throw new InvalidOperationException("No keys available"); - } - - return _input.Dequeue(); - } - } -} +using System; +using System.Collections.Generic; + +namespace RadLine.Tests +{ + public sealed class TestInputSource : IInputSource + { + private readonly Queue _input; + + public bool ByPassProcessing => true; + + public TestInputSource() + { + _input = new Queue(); + } + + public TestInputSource Push(string input) + { + if (input is null) + { + throw new ArgumentNullException(nameof(input)); + } + + foreach (var character in input) + { + Push(character); + } + + return this; + } + + public TestInputSource Push(char input) + { + var control = char.IsUpper(input); + _input.Enqueue(new ConsoleKeyInfo(input, (ConsoleKey)input, false, false, control)); + return this; + } + + public TestInputSource Push(ConsoleKey input) + { + _input.Enqueue(new ConsoleKeyInfo((char)input, input, false, false, false)); + return this; + } + + public TestInputSource PushNewLine() + { + Push(ConsoleKey.Enter, ConsoleModifiers.Shift); + return this; + } + + public TestInputSource PushEnter() + { + Push(ConsoleKey.Enter); + return this; + } + + public TestInputSource PushEscape() + { + Push(ConsoleKey.Escape); + return this; + } + + public TestInputSource Push(ConsoleKey input, ConsoleModifiers modifiers) + { + var shift = modifiers.HasFlag(ConsoleModifiers.Shift); + var control = modifiers.HasFlag(ConsoleModifiers.Control); + var alt = modifiers.HasFlag(ConsoleModifiers.Alt); + + _input.Enqueue(new ConsoleKeyInfo((char)0, input, shift, alt, control)); + return this; + } + + public bool IsKeyAvailable() + { + return _input.Count > 0; + } + + ConsoleKeyInfo IInputSource.ReadKey() + { + if (_input.Count == 0) + { + throw new InvalidOperationException("No keys available"); + } + + return _input.Dequeue(); + } + } +} diff --git a/src/RadLine/Commands/CancelCommand.cs b/src/RadLine/Commands/CancelCommand.cs new file mode 100644 index 0000000..6117fc6 --- /dev/null +++ b/src/RadLine/Commands/CancelCommand.cs @@ -0,0 +1,10 @@ +namespace RadLine; + +public sealed class CancelCommand : LineEditorCommand +{ + public override void Execute(LineEditorContext context) + { + context.Buffer.Reset(); + context.Submit(SubmitAction.Cancel); + } +} \ No newline at end of file diff --git a/src/RadLine/Internal/InputBuffer.cs b/src/RadLine/Internal/InputBuffer.cs index 7480070..64e4506 100644 --- a/src/RadLine/Internal/InputBuffer.cs +++ b/src/RadLine/Internal/InputBuffer.cs @@ -1,131 +1,131 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace RadLine -{ - internal sealed class InputBuffer - { - private readonly IInputSource _source; - private readonly Queue _queue; - private KeyBinding? _newLineBinding; - private KeyBinding? _submitBinding; - - public InputBuffer(IInputSource source) - { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _queue = new Queue(); - } - - public void Initialize(KeyBindings bindings) - { - bindings.TryFindKeyBindings(out _newLineBinding); - bindings.TryFindKeyBindings(out _submitBinding); - } - - public async Task ReadKey(bool multiline, CancellationToken cancellationToken) - { - if (_queue.Count > 0) - { - return _queue.Dequeue(); - } - - // Wait for the user to enter a key - var key = await ReadKeyFromSource(wait: true, cancellationToken); - if (key == null) - { - return null; - } - else - { - _queue.Enqueue(key.Value); - } - - if (_source.IsKeyAvailable()) - { - // Read all remaining keys from the buffer - await ReadRemainingKeys(multiline, cancellationToken); - } - - // Got something? - if (_queue.Count > 0) - { - return _queue.Dequeue(); - } - - return null; - } - - private async Task ReadRemainingKeys(bool multiline, CancellationToken cancellationToken) - { - var keys = new Queue(); - - while (true) - { - var key = await ReadKeyFromSource(wait: false, cancellationToken); - if (key == null) - { - break; - } - - keys.Enqueue(key.Value); - } - - if (keys.Count > 0) - { - // Process the input when we're somewhat sure that - // the input has been automated in some fashion, - // and the editor support multiline. The input source - // can bypass this kind of behavior, so we need to check - // it as well to see if we should do any processing. - var shouldProcess = multiline && keys.Count >= 5 && !_source.ByPassProcessing; - - while (keys.Count > 0) - { - var key = keys.Dequeue(); - - if (shouldProcess && _submitBinding != null && _newLineBinding != null) - { - // Is the key trying to submit? - if (_submitBinding.Equals(key)) - { - // Insert a new line instead - key = _newLineBinding.AsConsoleKeyInfo(); - } - } - - _queue.Enqueue(key); - } - } - } - - private async Task ReadKeyFromSource(bool wait, CancellationToken cancellationToken) - { - if (wait) - { - while (true) - { - if (cancellationToken.IsCancellationRequested) - { - return null; - } - - if (_source.IsKeyAvailable()) - { - break; - } - - await Task.Delay(5, cancellationToken).ConfigureAwait(false); - } - } - - if (_source.IsKeyAvailable()) - { - return _source.ReadKey(); - } - - return null; - } - } -} +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace RadLine +{ + internal sealed class InputBuffer + { + private readonly IInputSource _source; + private readonly Queue _queue; + private KeyBinding? _newLineBinding; + private KeyBinding? _submitBinding; + + public InputBuffer(IInputSource source) + { + _source = source ?? throw new ArgumentNullException(nameof(source)); + _queue = new Queue(); + } + + public void Initialize(KeyBindings bindings) + { + bindings.TryFindKeyBindings(out _newLineBinding); + bindings.TryFindKeyBindings(out _submitBinding); + } + + public async Task ReadKey(bool multiline, CancellationToken cancellationToken) + { + if (_queue.Count > 0) + { + return _queue.Dequeue(); + } + + // Wait for the user to enter a key + var key = await ReadKeyFromSource(wait: true, cancellationToken); + if (key == null) + { + return null; + } + else + { + _queue.Enqueue(key.Value); + } + + if (_source.IsKeyAvailable()) + { + // Read all remaining keys from the buffer + await ReadRemainingKeys(multiline, cancellationToken); + } + + // Got something? + if (_queue.Count > 0) + { + return _queue.Dequeue(); + } + + return null; + } + + private async Task ReadRemainingKeys(bool multiline, CancellationToken cancellationToken) + { + var keys = new Queue(); + + while (true) + { + var key = await ReadKeyFromSource(wait: false, cancellationToken); + if (key == null) + { + break; + } + + keys.Enqueue(key.Value); + } + + if (keys.Count > 0) + { + // Process the input when we're somewhat sure that + // the input has been automated in some fashion, + // and the editor support multiline. The input source + // can bypass this kind of behavior, so we need to check + // it as well to see if we should do any processing. + var shouldProcess = multiline && keys.Count >= 5 && !_source.ByPassProcessing; + + while (keys.Count > 0) + { + var key = keys.Dequeue(); + + if (shouldProcess && _submitBinding != null && _newLineBinding != null) + { + // Is the key trying to submit? + if (_submitBinding.Equals(key)) + { + // Insert a new line instead + key = _newLineBinding.AsConsoleKeyInfo(); + } + } + + _queue.Enqueue(key); + } + } + } + + private async Task ReadKeyFromSource(bool wait, CancellationToken cancellationToken) + { + if (wait) + { + while (true) + { + if (cancellationToken.IsCancellationRequested) + { + return null; + } + + if (_source.IsKeyAvailable()) + { + break; + } + + await Task.Delay(5, cancellationToken).ConfigureAwait(false); + } + } + + if (_source.IsKeyAvailable()) + { + return _source.ReadKey(); + } + + return null; + } + } +} diff --git a/src/RadLine/KeyBindingsExtensions.cs b/src/RadLine/KeyBindingsExtensions.cs index a8be21a..4095cad 100644 --- a/src/RadLine/KeyBindingsExtensions.cs +++ b/src/RadLine/KeyBindingsExtensions.cs @@ -1,73 +1,74 @@ -using System; - -namespace RadLine -{ - public static class KeyBindingsExtensions - { - public static void AddDefault(this KeyBindings bindings) - { - bindings.Add(ConsoleKey.Tab, () => new AutoCompleteCommand(AutoComplete.Next)); - bindings.Add(ConsoleKey.Tab, ConsoleModifiers.Control, () => new AutoCompleteCommand(AutoComplete.Previous)); - - bindings.Add(ConsoleKey.Backspace); - bindings.Add(ConsoleKey.Delete); - bindings.Add(ConsoleKey.Home); - bindings.Add(ConsoleKey.End); - bindings.Add(ConsoleKey.UpArrow); - bindings.Add(ConsoleKey.DownArrow); - bindings.Add(ConsoleKey.PageUp); - bindings.Add(ConsoleKey.PageDown); - bindings.Add(ConsoleKey.LeftArrow); - bindings.Add(ConsoleKey.RightArrow); - bindings.Add(ConsoleKey.LeftArrow, ConsoleModifiers.Control); - bindings.Add(ConsoleKey.RightArrow, ConsoleModifiers.Control); - bindings.Add(ConsoleKey.UpArrow, ConsoleModifiers.Control); - bindings.Add(ConsoleKey.DownArrow, ConsoleModifiers.Control); - bindings.Add(ConsoleKey.Enter); - bindings.Add(ConsoleKey.Enter, ConsoleModifiers.Shift); - } - - public static void Add(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers? modifiers = null) - where TCommand : LineEditorCommand, new() - { - if (bindings is null) - { - throw new ArgumentNullException(nameof(bindings)); - } - - bindings.Add(new KeyBinding(key, modifiers), () => new TCommand()); - } - - public static void Add(this KeyBindings bindings, ConsoleKey key, Func func) - where TCommand : LineEditorCommand - { - if (bindings is null) - { - throw new ArgumentNullException(nameof(bindings)); - } - - bindings.Add(new KeyBinding(key), () => func()); - } - - public static void Add(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers modifiers, Func func) - where TCommand : LineEditorCommand - { - if (bindings is null) - { - throw new ArgumentNullException(nameof(bindings)); - } - - bindings.Add(new KeyBinding(key, modifiers), () => func()); - } - - public static void Remove(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers? modifiers = null) - { - if (bindings is null) - { - throw new ArgumentNullException(nameof(bindings)); - } - - bindings.Remove(new KeyBinding(key, modifiers)); - } - } -} +using System; + +namespace RadLine +{ + public static class KeyBindingsExtensions + { + public static void AddDefault(this KeyBindings bindings) + { + bindings.Add(ConsoleKey.Tab, () => new AutoCompleteCommand(AutoComplete.Next)); + bindings.Add(ConsoleKey.Tab, ConsoleModifiers.Control, () => new AutoCompleteCommand(AutoComplete.Previous)); + + bindings.Add(ConsoleKey.Backspace); + bindings.Add(ConsoleKey.Delete); + bindings.Add(ConsoleKey.Home); + bindings.Add(ConsoleKey.End); + bindings.Add(ConsoleKey.UpArrow); + bindings.Add(ConsoleKey.DownArrow); + bindings.Add(ConsoleKey.PageUp); + bindings.Add(ConsoleKey.PageDown); + bindings.Add(ConsoleKey.LeftArrow); + bindings.Add(ConsoleKey.RightArrow); + bindings.Add(ConsoleKey.LeftArrow, ConsoleModifiers.Control); + bindings.Add(ConsoleKey.RightArrow, ConsoleModifiers.Control); + bindings.Add(ConsoleKey.UpArrow, ConsoleModifiers.Control); + bindings.Add(ConsoleKey.DownArrow, ConsoleModifiers.Control); + bindings.Add(ConsoleKey.Escape); + bindings.Add(ConsoleKey.Enter); + bindings.Add(ConsoleKey.Enter, ConsoleModifiers.Shift); + } + + public static void Add(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers? modifiers = null) + where TCommand : LineEditorCommand, new() + { + if (bindings is null) + { + throw new ArgumentNullException(nameof(bindings)); + } + + bindings.Add(new KeyBinding(key, modifiers), () => new TCommand()); + } + + public static void Add(this KeyBindings bindings, ConsoleKey key, Func func) + where TCommand : LineEditorCommand + { + if (bindings is null) + { + throw new ArgumentNullException(nameof(bindings)); + } + + bindings.Add(new KeyBinding(key), () => func()); + } + + public static void Add(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers modifiers, Func func) + where TCommand : LineEditorCommand + { + if (bindings is null) + { + throw new ArgumentNullException(nameof(bindings)); + } + + bindings.Add(new KeyBinding(key, modifiers), () => func()); + } + + public static void Remove(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers? modifiers = null) + { + if (bindings is null) + { + throw new ArgumentNullException(nameof(bindings)); + } + + bindings.Remove(new KeyBinding(key, modifiers)); + } + } +} diff --git a/src/RadLine/LineBuffer.cs b/src/RadLine/LineBuffer.cs index da7df64..7bab1fd 100644 --- a/src/RadLine/LineBuffer.cs +++ b/src/RadLine/LineBuffer.cs @@ -6,11 +6,13 @@ namespace RadLine { public sealed class LineBuffer { + private string _initialContent; private string _buffer; private int _position; public int Position => _position; public int Length => _buffer.Length; + public string InitialContent => _initialContent; public string Content => _buffer; public bool AtBeginning => Position == 0; @@ -76,8 +78,9 @@ public bool IsAtEndOfWord public int CursorPosition => _position; public LineBuffer(string? content = null) - { - _buffer = content ?? string.Empty; + { + _initialContent = content ?? string.Empty; + _buffer = _initialContent; _position = _buffer.Length; } @@ -88,6 +91,7 @@ public LineBuffer(LineBuffer buffer) throw new ArgumentNullException(nameof(buffer)); } + _initialContent = buffer.InitialContent; _buffer = buffer.Content; _position = _buffer.Length; } @@ -115,6 +119,12 @@ public void Insert(string text) _buffer = _buffer.Insert(_position, text); } + public void Reset() + { + _buffer = _initialContent; + _position = _buffer.Length; + } + public int Clear(int index, int count) { if (index < 0) diff --git a/src/RadLine/LineEditor.cs b/src/RadLine/LineEditor.cs index 765abdd..e166461 100644 --- a/src/RadLine/LineEditor.cs +++ b/src/RadLine/LineEditor.cs @@ -139,8 +139,9 @@ public static bool IsSupported(IAnsiConsole console) _history.Add(state.GetBuffers()); } - // Return all the lines - return cancelled ? null : state.Text; + // If cancelled return initial text otherwise + // return text from current state + return cancelled ? Text : state.Text; } private async Task<(LineBuffer Buffer, SubmitAction Result)> ReadLine( diff --git a/src/RadLine/LineEditorContext.cs b/src/RadLine/LineEditorContext.cs index f0d7c16..d64bd9b 100644 --- a/src/RadLine/LineEditorContext.cs +++ b/src/RadLine/LineEditorContext.cs @@ -14,11 +14,13 @@ public sealed class LineEditorContext : IServiceProvider public LineEditorContext(LineBuffer buffer, IServiceProvider? provider = null) { _state = new Dictionary(StringComparer.OrdinalIgnoreCase); - _provider = provider; - - Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); + _provider = provider; + Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); + InitialText = buffer.Content; } + public string? InitialText { get; } + public object? GetService(Type serviceType) { if (_provider != null) diff --git a/src/TestResults/42faa95d-7ac9-4f60-aad0-41a32909b4c0/andrevianna_L000783_2024-09-18.11_02_30.coverage b/src/TestResults/42faa95d-7ac9-4f60-aad0-41a32909b4c0/andrevianna_L000783_2024-09-18.11_02_30.coverage new file mode 100644 index 0000000000000000000000000000000000000000..73cf6f316881298a90c3a1128d66f0a5a158e88d GIT binary patch literal 132635 zcmdR%1%MPs*2jBbS=|*|9QD7%X8*6ab(kMg++W%3*tTQ^ z(V!!V3ybc>^n5zE?5|pI8>%4blI{@Co@*oGnsSMRlU_^Y1&szjxfm^~tuGcrw#oX| zWj$YhVB|3)Z~VAJkA1HEZGCQ8#TbT}aFG{*;zgSLZL1>vH_|(vtTr8`-0j5;ih1MX zelhfSKauaO|HDBqWPP2Nk}u;#Fq=AhVll6p^6;OE>c&@J1p6+ z7yXKRJ*i;_ie9s}_45Y)m#P4#jdfYxMIEpvqd%u+K&q5#fL~6wkX>+0h7kD;>q4s0 zB8Q?v#M@J}e(6=tmX}<-(~Uz;9Wnf{v#)-!$3Er0(W`0yo7;mQiDYUpH0Tq|3?>9C zgXO^@{dZ2VG{_4^1dD?u!GfSrug%vhtAb_vX_bB|40e;xO8qonKaG{&yyWi|%4?xK z3W9OL@L-DSU7qwR2nGi$^#7SbFTK`D)=TBd!CKX{O0_H%nG~#6{i{^hQW0ucoh+?P z)?O$wU(qZ{_JOEpZWB?{L!w%s3=sQr^?_L7%fwcz|Ky3D#d|=0{`tHU=~9gi@rH4Z zRe8S3OEMzfHf9R*baAkkW*~E?Aeg53iFJ(VvS81Qs3&d{c^;OGcaEY9GrlyL@%WsW zyiLS9Qu$mS%vHool+%5Zk%x0T9`Cl1+wqEZcg+QkLI3RCH7&Q!pVf^OkJW|wvp_!= zip-NvO7d`CG1I2#RaPv1XKvDaW-v@_u8#9XV&VE2u5sDdMOVWFwZ{Bis64OK{0(OX zE5(kYth&kmWkg_elPfd7Nz<^J17* zf;n1UmMK=&9`;(q8t&HSDfYrFYsB{d(3}oeK;oREh?%u3_22lc-8S|S;kwR#VR*1c zJ0MnHa+%u2rO(|N|4>fTkEiVGM7Yu|wX?=v$)3cszRNZdKWD%-njh@=7ieCuPM!%@ z7IO^dr1fFjJBM=x#T;_y_u)DU7HL#CF5=@cE3Gg8)OkEL9>h9N@y}1jyU319Bc2?}83WCwPIvAPk$pRhcoJ+IsXNRZtX6w%{R`-Sa|Aywt!nBzF zsd*B=KA)^?avhfK8lgC=QTiEb>t}khR`~D=fpy+pq5Jl)+s0bY9y{CJh0&VVOSN)x zmYth*|2-*f&iwP)b=N(tgsd#-XVz`wPDr?m4X@7QyMb*p;`lK+M)QSh1&+*k#5?@W zGlKhfthlVcBa&+__x1Kxge$e<&fIOMU!DI`tDGDC?03V8v#xF1R}E8=BQa0wcDCd2 zpNc)(9ps&oF?J2QX#Nx@@1xS@_dhj%!Zjp)bsN7;L^ehD>^bwXK4c&L|9|_85y{nL zsrs}^|7X?9TvvA7CbA#S;M6^9))_{FYtXqFYxT5kBIfwnWwh3eRf?L~!}UOTA0mCO zPuNBx&vq{@J<@IC9z?dIGRop-@!iw%yY=^V*;eMM8v8<>-P!q1){I!4Tv3*57Q0=Z zyZTMt-7el8`tcV%Yn~mob961SBIula zCo_FkrC-zklRaQ{;a$Cz>WNz!i`$EBN%w4r+vv;XF!#6OSriX1E4 z6-^y0^dY=Y%3abGHcIj1ecQOd&v6~zv&+0kls^B{kKli57slB&+ja9e@nw*9=9x6PeQan_J+<9amv8aPF>g1cG8 z=|}o~wts3qr`~HxUoWA4Qx8xEgA%l7nE`qlE*-|1!BiF00N50Sawj+4Q<%EXiRK2pyYWL_L_F)u@e&`A-O^^#wsxinM~Zh7xf3!^M}&KC zeB)0draz7GPwg$U?~zmQ{j>L6q`Tj%^zPGwqH=tc#-)wYKh+nv6CI^!S7~L6N4f2; zsnKbW?5Ke27hi12-{=v|Ak zk#nniGBk6)nSN!m^=Cx%P94uq6lYKTcRv#pjd7ml@5(Ial=L0%-S|^5k_9ccJ#ytniQfC6Cs9xaX+b z^Lp1V{n!6Kn0wfJ?K`x8c>hK}-Z=E8U(S8`^&UCNFZ&q0RWh0_V%{N|zkzt$mG55e z++IKZ^1?59w;$dAvrFcGI_djy^M5(D&Tn7e_WE6KT#*yxD#IiHws_f_koQ&g`N0j% z-kan zBL1#^|DPu<&YLi}`YRuQH@d;1(brx&=%KcQE*tT2>+#oJcz2_mpqwHe@wa7rjE=(M z<@$yug(sH&^0I3$pRuy%Z^xeZ@WNqbx*hk|SJ&;iQ;Sok{&er;oS^*Qn(HxIIrwa| z|46*@;>Rza_ulkoKMlBW@3EJ(KkvQM*StDw!eawgUH4MmcV8QScTP}25pV9fx%g|x zq@rtGf6N6F_N!UuqV^ZJ{Pn=9J3ji%*dOXIzrM>2UoC7s?88op&9`v`( zj~#Um@kjgEk3UqqvEKOoKKkyZ8?UQ3{jTlqYO${1)&74BJnMuz?z{NfUvq*=f6JU; zhspclJ0(}^4Xt*2OuBpUpS$03W`p0p{OhPYCsmj^{=N05lS48xz>P=VxO$JLUjJkM9@k&`?$NIo^n7LU{kcc}e#hGje|_(R z8;^SDo-1>LDvEq-oT<6Dx$*tZV~vk~1=<$_vg6fKJz*dTo<7brSBWHu~IR}3(-?;nkSGx^ecxTHyE^OMj@6ww; zDtqfSjd~3pUA^sLGoQbsuu@J?Ly>Nc^KSgODQ?8U&4ZiYH+Eo;@)s}dc>fE1st+7+ z`+qCke#}XST{7#wYahR1Sx!)ME6vWyI=<)XT7swA{24u=T#p|nz^GQdX_w)QG-N>M;!h6Tt=f=8i;2@d{bb+UPHOXUgWE1|eD@L8Klfeb zi#yF8HT3G7ptd4Tf6i#r?~2S!<}Q5K#N{^I^B`PHhR@0|8^SvxtghL7qt}zaHv03H zBRapl;JE4KcAogn%+D{cw*1kh&&{uVa--$-cKPn)k8*-K>cz0lUL7*?o-H!h+ib5z zqpmaNUjvukT&?w8zg~3h+m)_*sAb2)8~yw0_I-OFv*h$<<96R8C#b8aH}~$Di-wWk zaPCLP-b;@TyVjU-FKMWb$L!u zPyN~atEJ+O!ekw(;S=DCHadfTdA#MY6NU~r_0D1cy8X|a?kYR!`Rj+gHE;Pfi|%^n z=X zosW3)`bYcSKjt}`42q(#Cb=*{OM!gjCtkXdkv^LtL4xAC;xb2#{<58?Y6ovop5YU(0D6l zHe0;BYsm~CtMSZsTX)mr*RQy7#sQ`7n9}Q!@^4>v`|XXNTz0`}&1SUx`O-h@=i~%U z6lveBFgvqF$}!Ko!o0Jc8vVP*4D363P^&$MJ^TB@F>_|!e&VfbkG$d4>rW{A^5J6- z+Ux!Eb97H~OWbc9o%~b*yG1t_!)t@^^V=I+KPR5}_Jh}bcT&&es=w4_{c|V$^!n;y zpZ4GVT0QOYP8((tiL|IX4tqF9_+HmM_v2m1kDsludNix#`5&-Wi3iSdVb|o=D%|9pVz(m z#_bgjXj=Qpmv7#`@ogs`e&XPd_RI;IE4mJw9i5wBzIA!nv!_hHso98wDz3h6hrNGa zdh}m~r#GA4`Gy}FKla((pFFWjyN(u$WAo1$+2Yt(j^6Il`muNG9<%JMgBI5r@mQr( z$6WDvrw5+9Wzf0PcbPctvIlkdq2(6MQFL{S&ZK*%@4IN-sROSrx96PwzA4=(_n5ph z|Jvovj#mx({JYFaoS>EB=(feiDjG+)hh!}XKM#F|+F-)i)J`vGenKI6m>hvx)s6xHUhq~X4fC!WG@2Xpf)n!Bh^G{z3^zO>KC zD*Jz1Ki}^%?4=329CFj%y?*Gobe}R^o_h8%#n@IccG>JX=k7}}qCB&+#5BCl ziDs?-bGMzo?t!mP>^tL;V+UTiaL4aQ9NMApNoSt&`L!?a`rY=wEXfJlZIP^H?mnV> z;3>MM;`^g`c50tw2BR@ndGPSQ*RQ(p>6|x5bZj{P_~UPQYy9~S?{?SC)7p1#-2RN) z^Af>?ydY_M2b8xxlJC*w7xUe}HW}Yf=X=*MUw0dPuZ3q-_ugaKSe?Av6@E)7cUHOM zj`G!^ia2T9zl5XQ-*%k4gD{kRzT@1L!ygU(=jR0hruQdvd5?0EtXcBv!aZfMKu2l( zo!D$gSo-Qz@?OtKjsp>MUx{c@q9r~!cW>w&I!wlUuyF30z){Xr9y=3^{ALslIJWqx{O8m@@Y@BZKOlb@4E@M3Lk8{Qo&W-?olzwr~BpYY!;hec?7o}asAD%CRqa9`L zh>CNrTJdiy!yhe>F)y-l#ycBlJmIXN)PoX_Zc&}%X{vSSl7v2$Jzsg;uH6_|J zwGZ-=@5s4(yz!rB1iNUBE9q{seR3Sh%dRr|iPBGc2A?e>?`TUIxrfh@!BOUi<8xJJ zo}tXWsWSZIobiV*kWmlHnl??AjZZheP>MfFyN>UsGVP*GB7CuNl;h3$^E1Z*+Ed1S zqg{TkZ`wxc`0grGKUyQg_fUB|8Ooezeo{}IGk@SqWcZ`CWsWaZxt4gRhq1k2a7wzL#;7 z{6{(Yhm-&LGV0-H@{f{#$M;s5`cdL>e2vP)kJ4Y}5%t75^98^EA{bl&07j56-bn{1rg8(;3>D~um1EiXe^2NJ;*&4Z-nXuaE0q@rxgTn$#!yDl4kQZ;_{TRAyTODTd} z8F8Y-No4S{GV+OXE@1v<+;0DLc-{5jmPCQ4j>Tj;t=CePf1M99;Y znaNePZ`;uG!SO7V4rredATk#r<8+V=PH!1kls)~yGI+GN;kb5c5d@fi$~{CL^nJ_p zs!CC>vOPl_?^8}d>uyA&eR4e7r&jV^XZ~@1sPW9cj#57{&Fb94IW&=}68!LS9>LEK@Ty`!mY%UgZR|Pvr4lInHZt zhPGCDz4^!ak;XH7yGlL9s;QBq-9hd#@}hUuBXMPWdby4ER;iI1quosA6VCCNa{_ATc#B$7{d}x3tqjT6P|We@ zxub4`bIzbH6#qnUruu=^R9}u0VP2+=nCx-3(cXve=_pB$XoCniqd9G>^6~a_)rb@WPLT2YoPnK&J-R`?!D^{TC+gL$ainF>OXm2|KG7+bA|*=4*?)4Rp&eu= z$?%W!lZ~^l<#jad?W5``rr*dtMU=T%qi8Pro=ayoR@P&fUxj9*q{t^NGjn7KQHJ6HJWr50PnSY$0ZJgPdJ}XPL)J&9V?a4hyuWq^Z zzjD!B$o%|5e7=?rXq&kgU-KQ0uKAticdq$ouKBgq8?3%6J5OF)ZUn39PFVOJzk5e^ zWA78vK1WWxSB^(}#j0|?`N#PM#xr|cpq^q4)yReN+H$?EscX05d_E#RmN@}&uvW%< z?Rd0zjO9h^9w=3)JuJ4l;@t&3mXq&Yt-c!e;J<6Bg zrRGnM$PmhLdzp-S;{0;sZUskK4-&yD^&ZPp<5!3Wv*T+bL*{(kJuciOdrt}va4kHN6#=iMvHjn8RkUw4{NGEUM){X zdAkk7-X`rX?WXlJH=qULV;txY+E#Xr4DR}h(kI8G=NA00HQzYD&N$~B@`7@g)N$@$ z(Jo5;iQo?P9n*O`_j(ap-cqx?_%onJ;M(S_LWC$0I?h>z9-;ZN8)U@Fein{018$UM z<2Mpd$3e1Ply>hm&c2RuLBpBT zL(OB&HFNG0EtPzgXZbew>5(eQbKu^d&jrUvhnj4OSL_RaBV-+IXBK^gv;eVe6zVXal! zL-N>ieXE^u7T>f!5)H>dypN7Y``AZ*51W5xA6Ke>m{y3~N948T`dB&no?rIQcV*5< z+SWC6Mvoki_NcS`9yR~W9v!aUU_DjYWAaMKYA4@$T&4H$hUoo`3A!g6{^c`18O6QX z@NenycSg9ftC8GoZ0wU?@jfI1TH~64BSZO2YJoHn9I1G)UW(~)kutJQ#cHLEvbf>D z1~ppmtY`bwR7v}h8xSG=h@SfudGLgB&ITM=D1CLD)enYFkv%D+o;ZKX!=E?T00e4Uyb~GqRixuv6W!MpWkFfi2gPD zjeTw}U#lNiPpS26!}Z74wK4&5vb*FOf!?4)Wbe!1(RG4eI!>mXHH+}*dce->0~z&% zM?v(s@25;9P1*WT9;}=F^n5LUjWhf_HlIftpL~`*e5Qm??)KB_lio+34Vj|#f~Vg3 zOb&nFi#xEXZ;%|Im4Cw?o14e70X?9f@pZ-V==#w{ejk}XGmJc<8PlHGsF;M566caVK8gSV8y(WbI*WN@7plKn-)HRB151U5jU@vUgCtWEM8 z33z%c`fW^`vs>8Cvyb5VgwHgDdyMdN+3_`^TtHmRD@Mv0(K}9=-lFvO-!lAJOW-J3 zbDTAW?4v_u-^uWg^ZyuU?VujCgY0`5{&D_;ajs^F6AiQcqcV&QRDypLCCjyxUG@~L z8}1%CYf1S2O{(PXYOK=Jh8xdT`GER4>S%)~P@-_WtjhETWxoC-!=K|2j*>0Mse^2x z%>Vz&@F!dNqtx@W3=VTfJ&seR9+Y~1k>L-+ALTgwRR)i*WQm|f8_jgtAa&}0@}Ot+ z^h_h)=DtERj2$=M-#dSc=989)hvSDFpybnWW&-h}Pq7eMBe9s+oVBSNHH5##tq)2kj!OX`KBY z?V@3>eo>yV;Yw32d6m{HO_KfOJ91Kg!8+Tg%(8#3yZCw1_vlz$tdhiq5|`twQ1k;G zC)>{aIqKo)7+G!O%nRy4!~WH1t1QSytABOmrGN6^sc`Piq^^4_SULT>C*mWU2&jWK znc3ddl>Jm9sIOMA?bT9UYpH5cOFWatH(D%|v76;s%~y=sa+ujF_0eiMS%E3znh2Vy zMNIp=+f8%Hv2qqOa~J7PHkddV7^f1Ee+@WvkA#KZGE zys2^WNStVxqZP^zHdZ-mCNExXp=$!}Y{tiZ!*ea}U0CcnQZb-q^2q8+1Ss)2&TEW0 z%9uAd|2S{q;Vq5F_g9Wb_g8&Xe=GBk^VS~T#>4ZC=c!KON9kKz<4skjzi2ofcW5jy z9U-~xO!x8vV*~i9f1iH(MW2GQA0`X5~XIt-Cb&KQ2{q=0W z&$x2E=)Q+2e*UN*5FIm`@p48V9H-2zL$w(QdYFHl_w?{y9^Tt{e8=rLD>f@3$`RDZ z{G&T-;_2(*{XD$ChYv8$zKi~%oCO>w7H8a@8EF382g4s7EF0wEgFSqRhY$7eVIDr* z!$)}dNDt>r<(UuhevS6IyT10i<#8Kt|G&827md7JyCIY zmL;A2l>^$Rx9(aIB_79_d5kW~Y;c?zz-&OdC+9e2INDV<*6Pnw8IDeqjq~vF9zMat zCwllK51;JeQ;auNY{ZGu=N*m5SM)^ighmjXq*0tIFZQ1ki{A5(et(O5M=<>x#yA6& z?7hVHLFSq`I(Y{&^P5iw>m611o+?+!`MMjE?He$%#~5END+ctGyhN|wcGk~38E4gC z6-Ajxj+0a75z76BY3A?ta47T3ajwGfMTg3!n}3||Y@EFz^`lc{Gdz5zhwoyX^Az=< z#OZka_(%lR+N+nc$r}Gz`fbkp19VryeTy9TDtQNSWyW4(ig@@fNYOhGso&%pDBBok z8#ihQzhiClMz(1{9~fEs!3d&^pySb9ALsd9%|Fg(8>g=5%Heo)h0yLGm}CBNKG(zN zdH8(edDbq*c9$(Te|}~x z(N3}z9=@k>e5fDARvITZILcY0&^T?vQQ~pDtIFgD9VJ_3{&BwA!}l`YO}|V1DE04c zyr*%LoUSq6OJ(Li%KT3RpD2Zx)|K3~^5WG&kt3aVKo{zm<;j)Z{$CvJl>+*~sP>i7 zAC%rY-cMz+hVt4z<{#(#diZ`GzQ6JQRzJ$9I6gpSMjh=fJHY%0s!Tj6{s$T#Y#fd9 zA^MqmP+~sF{5h9U4~qZ6#)qj)9?)>q+jP*eDcez_eu%u7%lk)0-L1y)E6J_1x^L)w z5WQ!MU;348=3T1r{jvecb4+o4H}CAyB_M;$w~;dPjFM-^Il>tiG+(yP{6`x{;fEUE z!8kfwc9@4B?%_vx_<9dN(!-DP@S}~7wK!4c;V~Y5tcM@x;m3RU2_Al;ho5A8g2jol z`k(CKrx>4T{wV&ZdiZI^*^@E8D7iS@!_V;WGmYzLOxA<4?>x)H&o<7r2JNC8+vj-r zxyEOhKgt>XJP$wL_)POhSx+3_MP=3>w1eyd^PiN*t};gn+Cg@^`L9r!cu@TBFuu|_8s&xhnR-z6*>{@% zDwU}R#s4njt5qfsXgJ#CI_e5uwv$HtZh3JG?hrZKkFfW8_>0c`y*l3YS(|+J?f=)# zuTwzpmbHI6ovJ!1Zc8Aq8@jvuZva}Z@0 z^Qig9`D4b{n?E{8_PB>XVf;w*M{CF&KU(GOWawDgljeVnag_c#eynk{UxXj0GV!C- z|CH5pf^js;Ptwn{i?)JnRd|*vggeIeB-E&?%;V3f5F3F^zfIAUu5;8wEMF0i;bfkZ;oH0GV!Ct z?>PM&CPUdBzhd?9Grx}xk-h5SuX*_E#xK|J(k?n$_J)VQ>EUk~zsl-CIaj>x;qMr~ zT0X2ZXt>r+>!ke{rnUB6dDYV^<05MCrZ1Y7{5(rj#RX{ z>{H{ntBgO2zvFkROq^&3*=Oc|k8zYE^>YvZ!o$Dx@UM*DYxSe-68>fUK9z|RB~Hib z4@Vq2R`#{|^E2aw4w8N2;olm6P(IX;_LKeF!@o2Bkolvu{~r(k-ot+|{;1W1a-{y~ z;XfIFOg_v{G(1woML|CJXtSo|GZ=NG?Ik@@8A z=Da)6A|OVNv?pcs1f?gAKczAwfwqnCXN;ruBm7z8Xo2i!t3S?vG5(zSqb+2PzhE3? z2K;LNasEH!FPcBvQ1+YgmyDy#RL5Udxw;JPFZ|#$U5~(BZP6 zRG8;DPk4AKr2*0-Yo)ZT@kdWBe`iN1MqUf7>`3l41?6@LbI zbB^}%0qv9Jf60gvB}&J?R+;Rh-DTCx{~O~dqgdVex5iOM%kh6xmZ7a>HOxQGYa0Jv zKD3Ln?$+|~?Tr5*AL2*DEVt;Q^+Ps8S*|TF9fpd)J(-#Pz1r{-if*NizW>bo6VbmI z6@OPZ5zt0swNXjd!?cey5wuqn*i1!IM_yG$$7Ov7Io`@Y_5OB^Vnr9vL5+akkpo7a z(LmXS)HP1e;V7fzcr}$7ZM2!Jp7}Fl;3#!DUc(qVR94^oPN|g;|)|MH|S(p3-gcjmc}{K$v+w% zC0A%(V!LQ=wvtz#UTI#;=d-gNC(-xPOE!yI1;oKDA{L@WiPrIEDw7SgudKEC$9WqM z&-d`Q#@T;UKT5V8XE#E&(KfPn<{#(njk7<-ALWeM!8q$*Z5fIc7^hDBQD(B^`6@GW z(M7V3<{#&sJiN1qck%G9#@kuDC@Wbv5ASZgz4@c`&+!f_b7Y|XWj)M4&U+d!kPrPu zsi&9mjw+KMl>9hO4%wNZ6J@>4pP$JQI!xBb!}}WVEFbDed&&BFcz@$MY?A&cV>ZCU z2O4K(pnjCT5AyKA#yN}Nk1`jB81JcaWf{u+9BRC`aWu*qJL*C6WW&s#V~2WB{D&Lw zr*e528mHav`UPSSFT>+`Kk2x?0r`((evo$uMcGdB4B+OC?igkI2|9PGW|lC zD6zmrRVJe-nI3EYql}|F$;Ns3cn_aoe6-br zG6EAle3J3)esCdoR( zJH+h%$V4Kjt5&f&YI%yaocg?u8+ShI!85A-#fXx$PtCN?@!Qmt^*Rycscmeo+TT%L z`ltT6{jY1EyAFJMl{*fb-hQconrXkPjP}tEvZ)@P2s$VxY@XuUNnVUdYG2Mbpk#h4 zb4g-L1k_G!>?%1&qdFMXVlTCi%~uT5rFob z$_CU&^z2xi>O2|D@$k7GKF`DFd-wtmU+Ce9;2vcF+f7+mBrnFkOVJF^exAuZFOAOn z8_|`Na}&o->T|-Ix~|p@h>vX6k+GtqoPBmPUe`FvO7A%POwLGdzq#1_xpIP|4P}l~ zhNBZ?yPJQU@8RJ~j1w#MptWRC-pn}lFOB#|`7#e*9^veTsh=~l<9RC6F3LH;@un)X z>Y(gTS6Dr9zNhhK@}VA-dR7{zJ!kxGp>fs@{83i6RUW?Dcq{Wqso(L|DibGKAlu9Q z<9u)9?6YYXrJgm$^HpY^py8Q+wzk>WV$Jro@+z%Ys%fXfCv&6g=wSWqz6oQ~kN=#2 zy2%h%Jw$*Kf#V%iCSsHf?qmLOzOQkzKs{*KlY`U?YhVB4%a@FoX6kaNZ-BkDxU-9 zdn0+*GW=9*L9&<2L|IGbDJS9T!|KAfCh^^3?hIW}REmyn_=INo9?7PUZk2#ukQ=TS z=qpOE9Ov9gZcs+|K=bGN5sorX9p~DN`Hl{e9c2D-ez5U=@}VA-J-YebX%}VvIMn3v(RDC)|Ui7J4QJ?r;@{;U~$_CU$kH*Pp5v4`P$E!?_&^+0(=Fgc1jxq}zpQtjk z0A&_9KFK)RNOqjnv!lw?gL39P-uP6Ni3bfcFigG2mMH@#$g5296+Vf*myX`llYX0i z43-P1k1R}=(JD%-j_<58QKB7XCz^kppJaT7`J-gV@tG=<9hB@izKd~`?3`@%%vPCp zQL=N2@i{6JCmLpFoic+hS9VU7m;UKo#Ql?{mZ8DP)z7VEC0ogJ0_rAH^JPSU5`p6j zR3>7Sqy04VUuYbqhmJ2&nR?K$Cugb`*b4RJbp1AG)v8)O=0{fI_&ZdadPXV}P&575 zT}JyT?K`gXOR^tmXW1F%ALnNp*IJYGM;R%{m#R$tXxP7J)Hke9{X0v)4V|F(3|8vu zCOqSErgGo9mpW5r+b6s0ifq3T-y;|2XRg#&={S$>t;4?wSx;-;hFUAmH}g~u=x?6- zTS-P%Q2OsUGshV#&e`VAF$71smw1kcpX=f0dHDGreu43-)-Fohj#pEe;}~TYISzBi z@qMAy!+xFk(e|>7jBjTg<(zi0@!BerA2ggbRdo8tR%zB;A}{^Zw}`m=357y~pMp$jxR z*xnkUTja&AsGF^&+~Z?hvVGq~Vb)x?NghS4f5jQc;*U(3fY``*a~VBA>4D=dR3;PX zIN7b{ALqAu`0d79>UXIh4KwtFvV*NrhVGCT$7}txeL?YNZSi|sHlQ7P%M~=yphV+% zzRL6##qUn@Z)+T79y;DmW#%C|M0S_?$NAmH3*dHBN~ z{)mS^>fw)h_~Rb_gmHEx#E&v>9q*?y^A?>Xd(!;l{3#EA+IWBcF7>0-|BUg0#?g_o zXFdEmgRaaVoREpyB$G(_80C*}hs|K9m=$Q4`zQq<`;u@y-r>|Jin) zGi`CM4Dte^Xr?I2$%qvtR>xuV8J#Kn$o%8{V-Nqt!#_1nThxzg)eS!L@XtN`3lIO& zI9EEUye8CN(8|JpdaVmQjW_>FOP#c-5$;#=d)H#o}i{cq#UKRC*9 z^PO>azi^b}=0C={a)G1N^SyEAEgYquAB=P50!Me1{pjI88LwmhDC7HI5C7SCUGqn0 z%YO0jUyXA-5Dz*__CF8*&3JwDN6G*1#v2$%S?~Wa-q1KYMfRtM|7Dy#GIJLVkID5K zPHaCNlic*+)m`nF%ywl^+#@sddgJfgBW%u^P%(Myr{WUkr z%Zt@<%s)6cHhjgpIip@as9;&(J_K1n$%5k@RAy9AM!ur?7Z^twb;sH5GwSG2Staw2 z^UB7%$cOq-R?;dSUe&{^8E2PI{Af6e?X_TF2WS+l%ZpbR{DY&Iz7Ax5r)lQOvGvBi zLQuo<7QaexJbIPDxYsm)ZDW$-g);7r>u^bqJ33TW%lzYfJL8Nslq)QIvH^^oDchHE?@_0KGc_2u3ni|f@||a9V78)_0q>Sya$r$mHDkRneUmVt|l{Q z2;YsoIY)cBfPOQY>?z0&N_HF{qB5g`;@80ZhZ;v2b;sGWaI~Ym*3kUpypeIP0jM8k zht~69Kg~ zS8c?XXPPff1h=R?>|nLoR9>Y-Q_paX(W=7VWm}c}o~z=vS1O<;+9RKhO<8@Ld3Yjt zP%&VKD30dxqVKhe`p%PwsqbJ9XXL76Y~izj#mxgX6Bm14`j4_2B!VZ^HnvXfx3Kn8 zPmYzmMe3qOcGJu{G|c>qY7IM7Ew+>wnNK}i$devJ)Iu*?@6sO>LCiQ0+R-9Hkz|yQoY(s3vjH$Nb~GuZQ-yXX*Ee-9tv;RB8L(C?BrG#tT8GOuLP5gtC$!$%osPrxXk;VAs1lwe0{ z6h_M{p;uDp$LL=n+njv@H8)qy<7C`FM7g?21b?V)>?pOry}Wq$C-q5<=u>R*Cn}0} zzA76~D{W7ZF~?Ajf*p)cG>)>yJ3dKea)q+vA7lP;KGyhT^G93D9OqsO^`LEJ!BSOyAHv*+lc-S!FoNJac@8%FJVQuxyg~$N6OAv*bfPXm{BZZIS)Fz^|E0!Jb`3L(FH@O}pkYQHRW`6=m64h9 z(m(Ck*&!Du_ciQ@=h<$jxHC(iYq0kAwDwTib9|-B^amX&+r|9je3tP-`4Bf6#{HRM z#*S0myUMGSUa1m`JNmRxabqtXP+N1=wwH`HQQCBTZk*g`b>6hn%i@y`e3HW`Y+Sgh^X~psVR3<8vndbQZ#!-%qxmM2sDibXlM%zM> zVkaotdGcajR@HeY^*!WVJFH4x(}$(v=Cpc1eMEn-j8;)vb^H*Oi5_hrn{WQ>jHATk z_@OFuoS^J_7nuJMDpL>29&4e8FEYN~{88$6{799lAEo}?%wOw9vVJsQw%GWw#?g7Q z-93B{4`1TpOO0!mC)-7P%a(cgat~i&Tr)A5e>5ERD>UNRi5m4i<<(TL)GK;^cZ%*3 zWbOb`^I)!C=N?VXlL0f4_|Hk4EfM5O8t(XsWSDW99Mgpf1K}a{2cjE4?0S=#`t;0(T1|M#?Lp7 z(yrqds7$+PU0IYjH{MOUkJWR5%2B?rhwmTZllzbj({7U0! zlwYNvi3cUm&Yz!~8*eK;%IblWH^GI>Dh@A1ZOHjYk}o#5do8oxz8zXv%tSQc%$O&kj98{ALBT9^pb2Je*+FW+F`N#P=#yRS!2MzoByZVXg+>?8* zycqiiMSXR5H;Xgwo72-W0d3P$))^v1iO_M@7kY|v{e7PK$NBjleu42i`d#Wr!wfVW zq|ud~rVLytuO@n>RZ#}Q-NQ_BO2D1t6B)io7# z`MJ?JePJ9?@_&=@4#rXV&BhrE>OrZ;@y;sKUzFV4V*XuJ#vjH1R^uE?aCErrHV?nu z!|(9$J3ag^55L=ZSH(acQ2Kk1@gBxe=J~zGS^4ot$;Evhe!uZP^5Hl_!{h&+L25;& zU3TsR^5RHJo&7le`8(SDwW`$J_pN>0HwlP@PPW^-1rFN z=s?*M9{!|kl$HE_5C6dU9QojnQorMKRi=KF`ad-P1uEl@vVwf% z;U61cZ2leH!x?;oW-oT8X7IQ2;yBMwo57jSEu_CMxV2|<{eZX_os}}8M~U9? zLX{aUl=H^F%|Fh+Grr3FQAXGC)haU^P-feI%zv%Q)Q>WozxVJTjPGOqDD^wOugcVq zQvZ+Ue}Kx=k20Ho^6>u}Kg9e|>UVsd%G8gtw*PGYasG>k|LWoY^YGt{A8PHQHDrz- zrZV#hZ6N#I{MV~YoM>~|AI6VVnLMEM>rdkc8%Nv7{_=1x0GTJpm_N!g9TlbJmC!4xr+Kn}axnefgy?q~Wj?`0oh?;oEm>RB0%;=XuU4_M z)pCxtocivxv6`Dpw3}O8bX~qFtx_MYGA2z;`8g2`SBseLn&y_VR%_b3B)e)g$ZR#+ zo#zernR;F89Hm6T6+voRUkl zqbw0DQd`&sYOlPt*RrUe>Fvc=@Qr`ZR`mUb4QGV5U4$HqMA$tU;R;2EU8o2vScIu> z!s8je_;(#_N@l2$cI(MFvd}KFM6gC}Vi&31iq>xHqReD(H#0+SreyOkes*ZzMcLJ& z?3Il2U`2>stSBp4l&KXc+xN7`pMl%(xKJl8*Ozh5L6d7`aHLwrE>X*st>x6r@wJ?p zq3oW;&kb$62s>MZJ*0`?Bt?f^stBuCgsG=HX_t4N`Fq*n_W~Ao)u1j~I$YL_FLFQ{zeH+A@y{RrQ_5eE-o#CSM zcuRjP{b-HN3Z30n>6^m1Ke|%mw?M~3{HgiT$!`){nY@pRN4S1+SCwy2*W{GzNh zwah=xw=>?%{80^2P}{@n80V}<{V2cVcnjm`AX#1WCl~mm8GS04x{Gj9~5lkDZ=YV8aY8Ez@7dzQ}-Qy|L*ssQw%}-$ z2q(Mjx=}_l&-~-Osd3In)WZtM%xmW1%{{z@hqv_bRvzBk!`paxzK6H<@OB>F-orb1 zc!7s^G~QD=WE@eOalrhV6 zS&cEf9+43hN>q+NsxqVhw5*z}i}}ZSSL4sfhkDSkcc%=~yO6SL)w^!;(!a?2)T!@f z+gd&Bt_JDh^D-hsiOlgARHlco$>?DZ^N;hM#$T5Y^`K!7Ul^u)p|b1L!(Q^zzsTM9 z)OShy-)-I8QS7ZI>El~6Vnd0|@wZi`j~~kDV;}R6^S;JEk`MKuVIM0E*SplR>($48 z^3uP^D7x>e-g-UkZ;^d!k)cH9_-889!>?uZaDe&8`9R~}$cG-HVGl>Cci0W;;UIaj zFKJ(N6mDwIGWQOHwWn~O@+$7?-zXrGmWt#%8S$dT>-c|Err&5=*kMm(3KHS4c82{1gM>(T9{$G{Z`=Fd%Mw)+|k23zN`J-IXk2d~4 z<0xms?T!Co9PKRI!NbQG|I_?Y{Kp#qOJ(wd68|{kiA2cJC@-y_=@&}B#+!efPcWWq z{^&6IPxSCf#><*NN**Q~uV5S{{wW^5qw$L7kK#YocqRQze^L6ilkv*>nfZn?e`Xr5 zqMwdvWjE@0+C^Sn^y>8F{yFoTcsFhB&pfu+TWy{1?k(<-+AW}u9I4eU z6DXN*yrzC8b0~YNS>_+-yL$L+51-@VbB))sc2P#m@$K|8BaAYG=9zz8^GDe`&NtrB zI66Rlfrl?N-o*UT7V=+YyqR&7dsVv`Z)qImerS|8)6a13x-X9SNBJHRUfcZX*AnBc ztbUX{INn-6lLxeebgB8r`7+~etR9qlmPgv<>Wes=N>_OJo*urJ@pe`}O8kzuH;$5@ zC?^ka^0T+q!_QH^Hp0mduT%d%#yjX|<|j(O952w%Q$IRLdZdRRWqg47qZ~&^8y{>O z?IwPVhaYQvi20-S9Sv9XgLS(GyG1Md@$%9Okx!LJKl{WT+Ui;F+=N%b zT%+(bO6EHv)XS(=l(jGo@0=g0ma$va`Ux4WbAPSS-k&IW>(oo@m1Oy*%=tucqFTmo zQ|l*M>z#}4w57M6{v8RK-=Ppc+i_oEuI_U4S@`HKk#A2ZSr&=V^@C%L{;&&41m~y^ z*zM}aN%ATuI-nRM<=_8C$Gv^9>37UZ(!&Y?vC~72#N@t1x|8f=<5i8L%wEU2HsB~j zr^!w+|2RL@I4cnTD66UCtPF5;g6uT&kMq+#{0!qYtsb`ddV@bn7}kG`)oYS67`20lQOioGq_1qILA?F+NGf-&siA z9gTnY&#_{?pGbSi!l5@a(DE6JxD*- zM8AKjq-T#R0a4Jm?lNLUiPiBQD$_TV_YSTy|2V(ecu(_3nH7%nyUYrd9n3Z6ALrM4 z_;ntBz46{wKgzo7ILA6WLbSE)2J`QyGW|k{^G4%b?Z8oH=}jJfv+;rQA#Z3n#y4wx zv3oVfx5%rsUTLUVnmX@it_IOpqq~BVtpPa!?J}xEWJHG&o#R7Qrk5!9zHT-DIKR#K zF!@jq8us#G^$@#Hy}Vss9E+)MnqehNeFuH^vwr$}w(;FzseoD;F-C_NQD)8^9-atZ zP(0ZEis?>ym6LbsJ>u{i7I^2z{r$m`#8ob!X5w3;rDv@eI9(3)BIh3RAD-(jTt#m6!ypcQh)&WCw~HkUnQJiY^RoFkWc zin5|SZ2s(2s0R(_W3AEJKgk|cpB|AHD{DpVu=z_N>E{C9F}2}7R12t;KCm00ZIreh zudgz(qFie~YX0#ZiR00o2dl?p<{#&e8*d^X>PIzr!4n?-r13oY;E%G(I?fr26%wVc zr_8^Z%J`!kYmPHc%yE=BpEmy%D&vph?>O}`Rw%3JGv?3F&N$0FYn*k8dQkHFoN>zJ z84btyP>nD4kjD6Vc_s7;b1nMW{*oNiIRUlNPFopmqO|Ea>kcuZtVNDT_cY|q@eV3e z4;sdPhT_H^R_rgRH8S73Xnv-j*|+8$&XVM(T|mU-C%yx6oShH(K^fy0&7XA(wV#j}E+K9#Hb& zIQbeRL;0Og&7Yt1jkAV*=HZ_k-(Eh93(B~BVSERbnQtib?Mvfhjia0&zA`?}IEugH z<5i}9bb{<(<{#%@d-ylT`CZ~cssCH!lZ>O})$z$HmzSa8I(Ce@h&`fp?BDurqHkIB z&MD7GML&6(xzpX)vkWC!pPB|l!n~goOE1=cP~oQ^M4k$$0VWeM|N zWE^F_momP)%Jd86++Nzlb3D9^hvypK!|F$g)A1!L6DL|AD{KBsjiba_&iD$Ii4$dA zEN^^I$}*Jsp9m&s>A@aVV-@68DfxH&2!0;^=SjMB2>t5qgaw3)1; z`R`>MB@>SCtumQF8_O!0KW8cGLEBLF@G2f&)x)b9-`DC#iO2E%R3;v@EoJlH-#E(1 z)G&Uq%EXD1mzu^8F^*EdKRAjx&i#LKiE;+7XZ~?s-#9BB^`PXwf$^Hg(SEXq9^S~q8yjZ@rhb&& zND~jwGhW;LQTpY09hK=9%8J|6{Nuctapnc>q8xM0J-mg7xAgE<9^Tq`18Wy0e#aZC zO#J9bcU>(`Nw&85AWgOJw3dahxayKNih&7 z8fN?*WgB}^8Sf)6{oAUuzqmQi4TvUR(Xhf2GfK>kS5ukXq9bK}%|Fijd3b*hA7Gp# zg?7;}S5?O<3fWW2)j)aapRTOkH(_v(kMD+BV#<4Sd_E`r{tGjazW~qowTJJ?tqDIqhXdh zDMQ%P%F-}-mDVe*>~56HRr+1i@ZJ6mfBqqQ1aE4VasuKYOPqCy7A0E8*}sqtl+|>& z`A5&P+|w9goUh(x@@k(c156;m&Vz;Z|Qykf6NrA4UR%=!)k#{za z`Fe*je9|R7bDKV6Y6nC|F6a+^K+Fn1NBpCFdk-HI z;be_^n#jf)=S&*|~CTq61{(%s>(QdK{=AWlB{wV$v zjWa*sXq3}ew;SuK@+9+*o?Y>uY`mq)^b4h5Q;gI9JQ+$GJ9_w351(N?diJKzGmU4S zb-&TP#h%yP-9=src~{r&H2pb)0a_<^)&IN6I%!?Y*Pd~(W-XtLbSoKWSk}GpXZN&0 zf$AEd=N00sS0Zq&bykaAWGJ~y1m7th>;=U%%VKJ%dff9NtF?aecLEC49^XZ_OvhIE zxyUtYD~z=J7L3%zu+Aby!$^Nnl-P@kbXP@MR)V{qc{NqG z`4;EJ$?T^7S`MQ`&coNjPmS^Uv25+fN1$9l#2j^4Wm9%GvyF2d07sdnj@MP0qX6Yj z*BtYY^SK^A&p4|O^`Okm`5wN&cyx6lokw6iQ@^j7Qgn5S_+sO%L)3#(&+ZBKB3_hu9cMjcWYKoACFUPp4;k^L9=^itZCf?>p5?t_tBNa-+`|vLf-`9%tw9+cyTN#Ol@~W0h(e?4>rCcbtJZ zW1^ff9gmJYS=ihB<9v;AW&}M#>5=1%I2l9pWoykp&iC=~eT|cG>PMBPU_TGv-^14# zXQZeHr7wqi_+iFdnm2}7ly&)R<9$>vFGDNH62T)A^lpvp4IPQ+ z$%`46{YY$H>?3i6&X_!_82tpjJ34!19Gwk=xsD7)Lq!Z#BMF zWyS?1|F;?6*Erf%cDsk)VSGRHN2$m0{Z%F&lzQ$o|2V(P!|yhJkbajqQSy49@q<-n zzM&OliJ+g-ioLC);X!$EJeT}v;QYi6ntKJ|{etjJRjB=Kc&_T1aWr%Y9<}KDt8Jn~ ziO%r>D$^U3_#QL=IDg#uVDm@GuH!>gCcEfl*%Rg;=TCb0^Ts)ns0VE-d%?qBG(ODy zQD&Lr!&PQ>qg*+?Wd5U6rhYVE_Oge+VtllG$PY??Uo}2RWjM+#c+L0@D#Ot{+3Ozu zhH>3$OZubK@3^K-GEQ_?*_-Ac=WluV+aCUohrjFL?|Jz9#&@(hQS$HjRF%m;S|Izt z{HGa5XUabG@Q*zFV-Nqt!#_1X-P%Q29UY&cGUJE#l6_|WasIh+V#Xh359s(Tl^e;> z5wb7LpRvOq#s5p=vsETPDDiw{e6GrHl==BD&CLpD!Qg zC(3;L-uMEQ%gfMk^}1M9V()A9`bl1;^h!(ZligqAb9bQJ)pcj(FT4{Fzsu$BcSU#H z`MT%kc=U4#+}8`gYr=Pzpj1FKO%x4zVy!_LkN`0D z9a+BNcn{@-6_=~Vm72>uF`rJ|`r9dE{ml#hRBP?jS~(f{Mai$@FvbIA{2a#zj@F8C z{NU94m(|bD%pi?#KT4h)C$}7*Xa`w&^QUe&+A+fU zT^O3oxB1sKj`o#R^zcf?shfVG#9Y}pYXmt)$zc`ajB5)S+DcZ{!>bu@C?D!UnGe-H zyoPbsF8op2b(|xGIMJ4}n&!{(hd+vcE#rAAGnY|f)wgd=)h94yUusadlNakqMIA-q z^U`kh2=5@awX5$T$sNvo+vgM}k2>xcaz*a$XDrb=u~z#8v{3&K@9uGQrr&KGka1L& z3W$T<1-)%!+F4rL!?l8_2iU*Vi#qbsKaGdG{}^vEyx&Nl;%i~H@Ak}BwzBc!^}Ba+ z)s5fD%MFNu$r7j_Op7{`fu)8*G=ddvC*^s zvO%UJq-{Jr5gemlVZW+p`SQ{~odNq5-EZc5)!f_*&uZD9gme4b4V}}vYSm$Fh_CMD z0$S#L{FsdMFv@w@@yAu>yp6JNXlwp)-p=?F=8q=#RK}mw&+K>50$F7Sk&NafLegWHzMWjEz9doF36T)rqHMwA#Ge@SKf zgtn4(GXFU5Z2V>ON6CTXuc%BeQP$-y<{#%>jlU`%+C{0SoAK9FCQdZWWubv5$TsFgXHFIXtKG}RnMwBQ~_AvgQ%Jd4QSB}50GTA{3WIfG4 z&U+dEKt9xiQcrK=@2U((Tg&=*cwgfm%7-}7Fq>nPN$f9Wv!A@kW^`t{Z>8IExeV9h z%v{nsx%^y4j3_boH~yu{^c5usj(?>xxkQ<11I$0p2O8I|Hdzl!J%fyYVI0ku4fgOM z#)O73PDAEPq;LaE>Ju_{wPNjA@ejlA9+tb5WdU&CSuk!HK9=?}{?`@p9O8hACJ3d=w<}=DUWsUjI zQyGr-l&v*B-#FS;wvX`z#!=?Uz8=1x@rCA(a@_6j;RhIBWd11a9%y_w<0xzLK^}gv z@x}5X4=8y!#Q5$iGma>0$2t!`)WZ++@WVa)2;+OGE#g6m$MGd9lYg|iY`ys}RT+O2 z|09hrQ#m=0rQ>8rdHB&DevF47>*2>4U#_-@2PGcISEx+>?HO#CSMKiB-%s*FF1|9QsuQMs}VWuI`q@gc@h*2N2q z?`It4xVzB9FY@q?Gjk1S=f?z*a zsavaP9ks=}_d@HeTdS?xR$H~MTCLT7+NyPY&+nf9AvZVogg^l8r#?P6znt9nod5aX zbI+YGo+n)!;MW;nYvpn3$xewZw~NV z0{qqhzr*-B`m8zFVE1-y;heC^hwkO zr!JmfsAXb?lehO-`E|zegQWWd`~l+^S$VuvdN9Br3h>_;zr@yq(}$j4s%3HkXAb+Y zl~3|Vj9+HuakgF0FV`~LIzB*p)XFFMV*&oS@n7n1sSn;)@_fBK1+ zEx=zlezmm=-(B+j8Z9$cq{8`kBwgnPVua^pIG@M|1`k= z8Q}jL;GY@4U!O&L@dD{{;}00eM@s(+@P7yR7XkidfPWR>|1tic>QG<88K1v4{xdDZ zaq{{*;}2=Mj)b${`EK~2u_rbd)3MI%HG@xmw*Im0X8-O^;R^rMTg8t}G8Z>IW>Jra zRt#O=X_p6HYx3M@%^yAM)Fd0OYt?;qr0JesJxW&x`7VbgB8zp5IX!w>VZP{OF}^!$ za;59Z+=at4FZ|8a=z95N9hJ?{GEc-z)}1Rnfm5XH>{{ht7e=2pS2fSceAs4@UOAYf z7daXbB^7<#%zL2~A zvNl5w-=*VsgInI(z0R^{3a9pZ`^b(Tojq>KyKB4mzu@KezdUJ5ZYFm-)S73xczR{Aa=Bs_rPq4Ug4+G} zJNUs-Z=KrUxAQ;!_PDFsE~?w*;C=p8y#AV#`?PGFmm575)Mu+^Ncy$q^!mNu?2GGP zUAFYASW zwN&HMZC+!azq7RUEmcMkU!He!S+US^+dTmoum8Nx16R-Z=KP6&8qj?1dv{$q@zUQM zarvOq?+)yI<`*Z=o;XS`xvH)DZ{6`YE#l)9fB(Is72j&@_5tPJ`4_K!amV_NAHDFQ z_3s^dLC4wa_rK?ib3gyK{aL?S+3A~qzw`RxhxW*g_EJS#HTwQ)OZ=|Q^lYxz^Nw4l z&KiB)Kkm4wq|5quny-51)z)qPaQ}zbtZP-b?;ks^&yDt-+opE$dq>l2cT?Y&&ab^- z{>-12?)|rBlS{ik^~Ucy6+gf3pEq}!c*Ao8Mn9Yz-5WV#tH!S47{!)Wy(eSSTkz@1 zZy$gEm<0_w-LmkNvM*+Sblu~36@7lwiW#d9ys>EYtdGCV$xP1E`D#gf5C)DY8(Qnk z|7@MD<6a}44{*;9EYNwY8TyHAi@!asOzoeoZHjpZa}f8gf_%*;tG|z~U&ZW%*%NaM zH;-aY1##5zdit3;7~WN)EP{FU(K7pXIP*6+-d$qv31=P($JCVSIo%NAagRF0Hw?waSmqVG(BQ8+PXGin5S^Bv& zGgJScAVNRTUwltw<#ODKT(M`lA8Tv%S>32Tv~5p`7T~mvWq5Om7~%ApYqz_v65hgo zS65E?y(KtKU((m`JPD4|mn_5E*fLIE(${d7DT8;G8c1opq49m>l*jWWa)a_o&ao7{ zgG3u~*QR={R9i{ek!@^rw?tpYXN2^a->1?ijkJAjdcIiI7F0KSO9k2QdjLn&2cF!5 z$aFX@_af?KG(5fqwbsvME}Upm9`7PGmEc4NjuZJkBzSyFp*J{!O5r~R;`EOK5tSzo;5hx$T4Fu%cr)^~3GlYY zi8JfL=^xI7u%0C6GvMR_?Zs)Y?v_@SkZ6`|jNfjLk+0^xxN|fn&?z|f;UdM5ikBX6=y`{^PH--sGXzuvi~Oj z)Cxz`5}w?mx)~>aUTit+Qz#!F@nAiSC+7@3jmPH<#Jg93?`u3h64L*@1H8yMTPE$o zX_ww*pngEAeU_S2>-LGh7_L2uo5eA2DANpWj$&T^v_zTS8pX_EzJ50-nm@Q>CEiZY z`_49Xq*_&<>=f;{nLDvhr|vj)r?zk(cbr`8Yn;&vh7+6pB+AD}tvvnQ&p5GX{kV(G zMQ!yxMv}&vY=6-lY4?rKqYc!R&asLN&w{vPDb6SH6VaI!wY|Y6j(DqTc}{eXaMo%= z3#e6rq$^{p6>6rM6^pQ)?YfE2qAzMvpWNt~UFySDK%{UDy3rLTIMIgV)OCLep4$LxYy;M9W8g~#)R zv38*G_%=$*JZD>EyT%7dgCy3Ix^^8SU6+x5Q+8hGutiRMj9%?l>^aw zaO&eZvmfe%(@Vo7$}{`nb8*TKm*A{dp^v=1yi`i#BaAc4U_CgWeSk!H_EW?YcfFe1 zPUDKCPt1-KWh>k{)~nOhw;T!9tVgra8GmX(YpDTF4fssBhPkN5ID4m25}at#68uPM zw3Noj7$-`U$7%UkiSlY<^trfec~31O?bEXdisnj9V=b@vZa(%|u)T}M6ojUZCr~^(NJnyAt>Wq_v^c&^(mEbsSIZT2xD_}jiYs)2S z2hzBfog_+I%E#Fu8fT|zOW=)&HQ#HnernNIq82!{@O(clQ)^sR(zz>Fo?e3EwB~S$ z_4Jom5AIsCL4StwRNx5FT2j}IJ(GL`X4+O(`THI#mg1fh<~uVb&X6pr;aF8Wx{6I} z21v99r!~|cULw(Ye2`Qo!3RokoDqGB1g8ufXTQrh1|K59akho2QW~FTe5joAI8mQ2 zQJ&ADp16zp9__WSkXot04AC8>hS7N=H*%F|&dA$CcwTmDbe_ECxy0vCn-LPV!Ksbs z?1QL1-cFh+Q9j9M8E4PHdT`gOBDDy$)*sFmttGXrbe^#^cD5z{o}j|$tY`UKy=vOx zI@&Y37O?N3rZ_eAoP7^1#@k49BwC#0M;T|_r##Ny-*d+OT_t>kbhN~Jl6k(Sy4K+_8DZ zYiCOmv-?f;+P02qZPnAof~|tK;k3>3Mp`C5I9uK_iP*)D8;FPJO|;B9@E+1J66KTp zSmRCQtRL?sEte>tw~8hf28d*dz|QwQd1qyBHVP zc6lnf;?+^|Z-Pd7f9;fQoEaWxHr&--zK6By65Bd@w1-wFGt~2VhB7BO!OAE3YU5-s z(ZgNzqRvo<=)de5QI|pf9k@h}>&Kk;NRGwj-;um+Gl*IdgWV9{*`aLott!-IPl=Y`j5ofH&9zKs;N7K@B+9pt;5b=vvINhQ;CO*_ ziUe;X!Eue|(YLd}S>|(bJ~t;bS5ZQp6sglhT>L9uC8>B-xN^MmsvWQRnyMSMI?;E? zPmbNJbSiCR_d9^Q@7$4pmeKxViT2~P-}3=lCSEwPKS!ec zKnafXH|I+5K@uDvAe|?{he~jKgmk_HFO}dpN31`U;FO_0xa*J3>Mm5MuDC$7wv@ak zRkQy1K6|20^lStzA1%>xoR-s8_*jWZ;q=6X5`3Hl$JvtBN$~L!96v<5NJ`@u8=okr zJU&_anMC;{zr^^Va?0cM+@%ub50j`rPW^K-%hhX0+jRCaky_ID#FpglpeVB$?Urs) zm3uILEyZ1JNp5YF;HrbWr-u7$=4f9rL)w1ZVeJfYA$mthL=Pu=o=?#-(Z=ch%O%QB zmEd@>^m7S5O@ia}{uNRh|Aq0Ha?0b3ufLQiKSzS&>cY%=34WABJ#p8|zt^u(H}&$B zqFJd~qL=-%MqGj9%2(QT#}*sUVutQqbNhF{uIi6<)T)jYpY2k2&snCecu(n966IlD z1zNt!c>G91JBrNJ#&?rb9;f^@66;CwYmM(NCziO2W$k>8Z<2aCdz~m-3^T&`{SkhP z@p15X9hKEJld6NWZ4h6?4f3uXvZ!z9ZPCaqg0~7LfE?!dYvbT!vBqg&setnz~ zEA9Tf4%N)&__`{usk%9y%}`fb%3O#R;k3x}LM;;+oH^EQ66L!|aGa>!F2U&?IL_bP zA*JyA;bDpL zhf8prTz*7?Pm$m_{80%$O@ia}!(&n!f86+VIrYI^KXmP=D^F5ib-@#&^h29qKe)R` z=!Ib#JGkD)d4PEmqx0P8u7~n#irXX7)R`8~mS`DH%RFcHM1*i6{iHq97l}G9F?DLtQoYBFV zTf14v_nG(E4DBK+$4Imur}dsM*D_JW>5bn>ls{gA<3#=U68r=SjuZ9gq%{7#@zruZ z7w28OdLk;Acp1yp8mV1V3AX;|0>I zQX2n*@pI(V6L*n+M^Q)nEAoF7CEo`m@;w=OcifTo?1ldx#F}meoi?-Q$cW^VAigfu z?age4_7lBz5>dj5lIIs|nP}tvq}L?MUn;?Idf{~m{&NYA!~Z0u@jn~CLeA&nu6LSr z(m7#ifO_W*QO5n^P4v!4W$(;rtMC!v&Ptx2Va~%A!_ko2KZiysHw$;i|C|@#ES@_L z@O|`5t>|5fMD9w72;oGCn82@+h%`>myeYx2mf$$s&07-uIth-`Gk=lbH%f3E{DM0T~T_h|0a4YnYo+l9k&k|sD9>J(|rBQ|9RfVjf3BRf6;Q{ zcAG?eaN^_n9a<*NIQ{jWM0t*$;CP<&z69rJ0glrzf0f|(N^l(hft1GoX8b-m^}$_# zovyw@gVa?Yiqcx{0sMhFH!!qM!ZkTzxg?S zQA{3@({`M;d;XY~i6LGjeIyaX$0azEzTnUb>A^w@P`13VB(i{9NnHH2yE+ugj?q z?t1KN^%feU&ic0~J+|K_ddz2XaNZi`j=+`CTN+nroM-`$76f<5v?Uw zZ%V`jCnlb=M*0mchUbukw*t8`VB=Hosv)d`+FQ2YtoK7D_*6VFVxSw1^Dj9 zJIaYW?&4mg?m{CKcY2SwC(rEqtgje+3Rdm8U;9H(FQGG1gHXZ_6s zyoK>Ta{2{#{jx@Vf<~!dT8h#ydk03Niv5xv|7{&{>PQ6pTT5_S;`#pinTX>=ge`^g z1B~NDJkR(b<2Vs#3!?l`<2Vs#3xW?bju-0ZHUZw&_;5K9$6dr-=R7c2JWXZ%N29@i`@%2n2%yE{pPbj-5v=iN^Xe9p(8f_8Ho}$_#Ck8EY->q4v;XoECfD zOUqSm(PXMfj|&ueB7jE?i7 zxdg|M>EmI~DzmL=X1=v*it0?Oa9ZX0R4tQjM@h76ft644g#o@Oz!wMjk^o;C;L8I1 zm;gW4_*`oj&iJ!Dz*iWbXXSCq9~a=q8=r6Gamud@@KweaS$UkT;)DQS9pGyM{1oF$ zZT&cNfl~u~t?^|lLq6ax%irvt$)H1(#WYfd4eWF9`4p1ALuv?en5~;$+uF0e-RZ z305Ab{LcdX65|J1d7Sc>2KZ&h53%w%*?W0_|2)922=J?nPqOvnWbf4hex32jDns_- zE_+w@)HW!IWN%cdg6KDC*?YuhXRklz+p?LfKO4e$&N+2|9)7%LvGX*-bN@q}m!F?^ zbBp-wci$@WzggxEi_cn%4^DhMKV8d=_BgZOJFWZ~#_>VYT>-wq_?cE7A0^!#;P(Xh zuLJyE<7e6Wac0Z+1^E2|{y=~~YW!SVKTiE03-HGS{D}a6()f9{ew^*)sQ`aEz<(Ry z&l|tM){hrTF9i6D0sc~eziRwKt&{BwciY#xeRb_$D%1A$no2c`ev`I+4c_eSYn--% zrP>oM)?7M1qfQ?_{1@=5+yfd3`H z-wyD1jO)@-^tm{>^KO8@7vS#)_}`6_FSHA1?Eg5xKQ+!cK`!7f7hdeG>ui$dh}q9n zYR~95X}K`8hPg0CIl=vtT+2UF=Op82398P9^0#XEv=;h z6St;P1L;dEpX6T!_j3{Iz`r$4)Ts|no_rVJnH^j^lI3#(yq576wtk$PuN~lZ zjdOfXp19+5j^G>h(b-&Sy7FWfl~M)ue8sG2Yv=x(WjdCgsh<`{=RotcA6yol@2U9A zdR6a@=etjNLo{Q@3_T+eJ~LFP{aXBa(Iu)4b*ig6(VJ}LcxNdmv!`l}W~k;3tmch2 z);v^C=EmE<71WPbkcSh^4~tsSS}i~`)ryAJ3eJ?dJLki#sQ8A4AMTqTwtuXNqK`Fm zRbs9^qB5qF;*4e~*1M`yRjLj{Ae^YVaP<}n3aN|8@o40RikfA+nY3(Ii zjnh8Q+20T)yg+JW<@1f>Y$0s}yq)omRvt&~1AHIj^a!7elNB8TJU_rY26!jqg|>cN zhvS*f#=9BE`%47@UKrqAjCa@H5>K3X=45*I)h9_uDT}(Q6npXHv)>$H@D3dJJv3?8 z%YVq()G$N4$R-_tMXkqaz2`+*rdM&YshgG8#uag#Z0a81J&Y@iQF)wf>KWj@jB{+x z=i;NKeFMCAfENXLALGTgew=LTYkYs>ID9|j1C8TkS-$}9AK=BtIR`?P;V#Q=S4hy& z%Ch}ciYyzN7^}8+miaSB@tHE$0o)VmjB?DMnPF_-(XC;I2#}A%B;tecOew^|A5aTnATY1GM;y77#M1YqWUtr~NvT90z zA8CA{mB&X*Qv-ZjfKLzb8OE2``f;*qrtzi5ari9b#~R1Uw%GwbC%}&~zFdD>U&4uV zPNql)J4lhq9<5RhM1NR&((fZLi3EAMQX*bB@$&oxEi>BTpM_mx$3yq&><#FwT}pV1iSs$ zM3C4JK}|2B2;!rn2=1=k3euh-yUHRseVa$H`o~m0*4_VWVoel@HAg>WEAC=FNO4BT zE7m7itVdLeb;a#`OOF?P+_r2hZ?^@bWb#O3OSg6+L;R*l#1D7zo21yGm5SeLi{B4@ zYyUnYNsNdjvsogEyGSlk6wxY0a*aju|Ib$cea4(<5_4v1#2hE)J7mV@=_pvzj3!%4 vWEXM1Er9RxiqBal=~y~n*Jk`Yg*Rr-mv~ Date: Wed, 18 Sep 2024 11:23:37 -0400 Subject: [PATCH 2/8] Correct mixed EOL char in the file --- src/RadLine/LineBuffer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RadLine/LineBuffer.cs b/src/RadLine/LineBuffer.cs index 7bab1fd..74c03ac 100644 --- a/src/RadLine/LineBuffer.cs +++ b/src/RadLine/LineBuffer.cs @@ -78,7 +78,7 @@ public bool IsAtEndOfWord public int CursorPosition => _position; public LineBuffer(string? content = null) - { + { _initialContent = content ?? string.Empty; _buffer = _initialContent; _position = _buffer.Length; @@ -124,7 +124,7 @@ public void Reset() _buffer = _initialContent; _position = _buffer.Length; } - + public int Clear(int index, int count) { if (index < 0) From beec0281e32c5f18e4c2284e219df520473b3991 Mon Sep 17 00:00:00 2001 From: Andre Vianna Date: Wed, 18 Sep 2024 11:33:18 -0400 Subject: [PATCH 3/8] Correct mixed EOL char in the file --- src/RadLine/Commands/InsertCommand.cs | 72 +++++++++++++-------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/RadLine/Commands/InsertCommand.cs b/src/RadLine/Commands/InsertCommand.cs index 3008b04..14a2df1 100644 --- a/src/RadLine/Commands/InsertCommand.cs +++ b/src/RadLine/Commands/InsertCommand.cs @@ -1,36 +1,36 @@ -namespace RadLine -{ - public sealed class InsertCommand : LineEditorCommand - { - private readonly char? _character; - private readonly string? _text; - - public InsertCommand(char character) - { - _character = character; - _text = null; - } - - public InsertCommand(string text) - { - _text = text ?? string.Empty; - _character = null; - } - - public override void Execute(LineEditorContext context) - { - var buffer = context.Buffer; - - if (_character != null) - { - buffer.Insert(_character.Value); - buffer.Move(buffer.Position + 1); - } - else if (_text != null) - { - buffer.Insert(_text); - buffer.Move(buffer.Position + _text.Length); - } - } - } -} +namespace RadLine +{ + public sealed class InsertCommand : LineEditorCommand + { + private readonly char? _character; + private readonly string? _text; + + public InsertCommand(char character) + { + _character = character; + _text = null; + } + + public InsertCommand(string text) + { + _text = text ?? string.Empty; + _character = null; + } + + public override void Execute(LineEditorContext context) + { + var buffer = context.Buffer; + + if (_character != null) + { + buffer.Insert(_character.Value); + buffer.Move(buffer.Position + 1); + } + else if (_text != null) + { + buffer.Insert(_text); + buffer.Move(buffer.Position + _text.Length); + } + } + } +} From 8a5b3ec97c7007cfe29c4a8a7356ebc147458c6b Mon Sep 17 00:00:00 2001 From: Andre Vianna Date: Wed, 18 Sep 2024 11:42:20 -0400 Subject: [PATCH 4/8] Correct mixed EOL char in the file --- src/RadLine/Commands/InsertCommand.cs | 72 +-- src/RadLine/LineBuffer.cs | 364 +++++++-------- src/RadLine/LineEditor.cs | 650 +++++++++++++------------- 3 files changed, 543 insertions(+), 543 deletions(-) diff --git a/src/RadLine/Commands/InsertCommand.cs b/src/RadLine/Commands/InsertCommand.cs index 14a2df1..3008b04 100644 --- a/src/RadLine/Commands/InsertCommand.cs +++ b/src/RadLine/Commands/InsertCommand.cs @@ -1,36 +1,36 @@ -namespace RadLine -{ - public sealed class InsertCommand : LineEditorCommand - { - private readonly char? _character; - private readonly string? _text; - - public InsertCommand(char character) - { - _character = character; - _text = null; - } - - public InsertCommand(string text) - { - _text = text ?? string.Empty; - _character = null; - } - - public override void Execute(LineEditorContext context) - { - var buffer = context.Buffer; - - if (_character != null) - { - buffer.Insert(_character.Value); - buffer.Move(buffer.Position + 1); - } - else if (_text != null) - { - buffer.Insert(_text); - buffer.Move(buffer.Position + _text.Length); - } - } - } -} +namespace RadLine +{ + public sealed class InsertCommand : LineEditorCommand + { + private readonly char? _character; + private readonly string? _text; + + public InsertCommand(char character) + { + _character = character; + _text = null; + } + + public InsertCommand(string text) + { + _text = text ?? string.Empty; + _character = null; + } + + public override void Execute(LineEditorContext context) + { + var buffer = context.Buffer; + + if (_character != null) + { + buffer.Insert(_character.Value); + buffer.Move(buffer.Position + 1); + } + else if (_text != null) + { + buffer.Insert(_text); + buffer.Move(buffer.Position + _text.Length); + } + } + } +} diff --git a/src/RadLine/LineBuffer.cs b/src/RadLine/LineBuffer.cs index 74c03ac..26f58d4 100644 --- a/src/RadLine/LineBuffer.cs +++ b/src/RadLine/LineBuffer.cs @@ -1,182 +1,182 @@ -using System; -using System.Globalization; -using System.Linq; - -namespace RadLine -{ - public sealed class LineBuffer - { - private string _initialContent; - private string _buffer; - private int _position; - - public int Position => _position; - public int Length => _buffer.Length; - public string InitialContent => _initialContent; - public string Content => _buffer; - - public bool AtBeginning => Position == 0; - public bool AtEnd => Position == Content.Length; - - public bool IsAtCharacter - { - get - { - if (Length == 0) - { - return false; - } - - if (AtEnd) - { - return false; - } - - return !char.IsWhiteSpace(_buffer[_position]); - } - } - - public bool IsAtBeginningOfWord - { - get - { - if (Length == 0) - { - return false; - } - - if (_position == 0) - { - return !char.IsWhiteSpace(_buffer[0]); - } - - return char.IsWhiteSpace(_buffer[_position - 1]); - } - } - - public bool IsAtEndOfWord - { - get - { - if (Length == 0) - { - return false; - } - - if (_position == 0) - { - return false; - } - - return !char.IsWhiteSpace(_buffer[_position - 1]); - } - } - - // TODO: Right now, this only returns the position in the line buffer. - // This is OK for western alphabets and most emojis which consist - // of a single surrogate pair, but everything else will be wrong. - public int CursorPosition => _position; - - public LineBuffer(string? content = null) - { - _initialContent = content ?? string.Empty; - _buffer = _initialContent; - _position = _buffer.Length; - } - - public LineBuffer(LineBuffer buffer) - { - if (buffer is null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - _initialContent = buffer.InitialContent; - _buffer = buffer.Content; - _position = _buffer.Length; - } - - public bool Move(int position) - { - if (position == _position) - { - return false; - } - - var movingLeft = position < _position; - _position = MoveToPosition(position, movingLeft); - - return true; - } - - public void Insert(char character) - { - _buffer = _buffer.Insert(_position, character.ToString()); - } - - public void Insert(string text) - { - _buffer = _buffer.Insert(_position, text); - } - - public void Reset() - { - _buffer = _initialContent; - _position = _buffer.Length; - } - - public int Clear(int index, int count) - { - if (index < 0) - { - return 0; - } - - if (index > _buffer.Length - 1) - { - return 0; - } - - var length = _buffer.Length; - _buffer = _buffer.Remove(Math.Max(0, index), Math.Min(count, _buffer.Length - index)); - return Math.Max(length - _buffer.Length, 0); - } - - private int MoveToPosition(int position, bool movingLeft) - { - if (position <= 0) - { - return 0; - } - else if (position >= _buffer.Length) - { - return _buffer.Length; - } - - var indices = StringInfo.ParseCombiningCharacters(_buffer); - - if (movingLeft) - { - foreach (var e in indices.Reverse()) - { - if (e <= position) - { - return e; - } - } - } - else - { - foreach (var e in indices) - { - if (e >= position) - { - return e; - } - } - } - - throw new InvalidOperationException("Could not find position in buffer"); - } - } -} +using System; +using System.Globalization; +using System.Linq; + +namespace RadLine +{ + public sealed class LineBuffer + { + private readonly string _initialContent; + private string _buffer; + private int _position; + + public int Position => _position; + public int Length => _buffer.Length; + public string InitialContent => _initialContent; + public string Content => _buffer; + + public bool AtBeginning => Position == 0; + public bool AtEnd => Position == Content.Length; + + public bool IsAtCharacter + { + get + { + if (Length == 0) + { + return false; + } + + if (AtEnd) + { + return false; + } + + return !char.IsWhiteSpace(_buffer[_position]); + } + } + + public bool IsAtBeginningOfWord + { + get + { + if (Length == 0) + { + return false; + } + + if (_position == 0) + { + return !char.IsWhiteSpace(_buffer[0]); + } + + return char.IsWhiteSpace(_buffer[_position - 1]); + } + } + + public bool IsAtEndOfWord + { + get + { + if (Length == 0) + { + return false; + } + + if (_position == 0) + { + return false; + } + + return !char.IsWhiteSpace(_buffer[_position - 1]); + } + } + + // TODO: Right now, this only returns the position in the line buffer. + // This is OK for western alphabets and most emojis which consist + // of a single surrogate pair, but everything else will be wrong. + public int CursorPosition => _position; + + public LineBuffer(string? content = null) + { + _initialContent = content ?? string.Empty; + _buffer = _initialContent; + _position = _buffer.Length; + } + + public LineBuffer(LineBuffer buffer) + { + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + _initialContent = buffer.InitialContent; + _buffer = buffer.Content; + _position = _buffer.Length; + } + + public bool Move(int position) + { + if (position == _position) + { + return false; + } + + var movingLeft = position < _position; + _position = MoveToPosition(position, movingLeft); + + return true; + } + + public void Insert(char character) + { + _buffer = _buffer.Insert(_position, character.ToString()); + } + + public void Insert(string text) + { + _buffer = _buffer.Insert(_position, text); + } + + public void Reset() + { + _buffer = _initialContent; + _position = _buffer.Length; + } + + public int Clear(int index, int count) + { + if (index < 0) + { + return 0; + } + + if (index > _buffer.Length - 1) + { + return 0; + } + + var length = _buffer.Length; + _buffer = _buffer.Remove(Math.Max(0, index), Math.Min(count, _buffer.Length - index)); + return Math.Max(length - _buffer.Length, 0); + } + + private int MoveToPosition(int position, bool movingLeft) + { + if (position <= 0) + { + return 0; + } + else if (position >= _buffer.Length) + { + return _buffer.Length; + } + + var indices = StringInfo.ParseCombiningCharacters(_buffer); + + if (movingLeft) + { + foreach (var e in indices.Reverse()) + { + if (e <= position) + { + return e; + } + } + } + else + { + foreach (var e in indices) + { + if (e >= position) + { + return e; + } + } + } + + throw new InvalidOperationException("Could not find position in buffer"); + } + } +} diff --git a/src/RadLine/LineEditor.cs b/src/RadLine/LineEditor.cs index e166461..1ac892f 100644 --- a/src/RadLine/LineEditor.cs +++ b/src/RadLine/LineEditor.cs @@ -1,325 +1,325 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Spectre.Console; -using Spectre.Console.Advanced; - -namespace RadLine -{ - public sealed class LineEditor : IHighlighterAccessor - { - private readonly IInputSource _source; - private readonly IServiceProvider? _provider; - private readonly IAnsiConsole _console; - private readonly LineEditorRenderer _renderer; - private readonly LineEditorHistory _history; - private readonly InputBuffer _input; - - public KeyBindings KeyBindings { get; } - public bool MultiLine { get; init; } = false; - public string Text { get; init; } = string.Empty; - - public ILineEditorPrompt Prompt { get; init; } = new LineEditorPrompt("[yellow]>[/]"); - public ITextCompletion? Completion { get; init; } - public IHighlighter? Highlighter { get; init; } - public ILineEditorHistory History => _history; - - public ILineDecorationRenderer? LineDecorationRenderer { get; init; } - - public LineEditor(IAnsiConsole? terminal = null, IInputSource? source = null, IServiceProvider? provider = null) - { - _console = terminal ?? AnsiConsole.Console; - _source = source ?? new DefaultInputSource(_console); - _provider = provider; - _renderer = new LineEditorRenderer(_console, this); - _history = new LineEditorHistory(); - _input = new InputBuffer(_source); - - KeyBindings = new KeyBindings(); - KeyBindings.AddDefault(); - } - - public static bool IsSupported(IAnsiConsole console) - { - if (console is null) - { - throw new ArgumentNullException(nameof(console)); - } - - return - console.Profile.Out.IsTerminal && - console.Profile.Capabilities.Ansi && - console.Profile.Capabilities.Interactive; - } - - public async Task ReadLine(CancellationToken cancellationToken) - { - var cancelled = false; - var state = new LineEditorState(Prompt, Text); - - _history.Reset(); - _input.Initialize(KeyBindings); - _renderer.Refresh(state); - - while (true) - { - var result = await ReadLine(state, cancellationToken).ConfigureAwait(false); - - if (result.Result == SubmitAction.Cancel) - { - cancelled = true; - break; - } - else if (result.Result == SubmitAction.Submit) - { - break; - } - else if (result.Result == SubmitAction.PreviousHistory) - { - if (_history.MovePrevious(state) && !SetContent(state, _history.Current)) - { - continue; - } - } - else if (result.Result == SubmitAction.NextHistory) - { - if (_history.MoveNext() && !SetContent(state, _history.Current)) - { - continue; - } - } - else if (result.Result == SubmitAction.NewLine && MultiLine && state.IsLastLine) - { - // Add a new line - state.AddLine(); - - // Refresh - var builder = new StringBuilder(); - builder.Append("\u001b[?25l"); // Hide cursor - _renderer.AnsiBuilder.MoveDown(builder, state); - _renderer.AnsiBuilder.BuildRefresh(builder, state); - builder.Append("\u001b[?25h"); // Show cursor - _console.WriteAnsi(builder.ToString()); - } - else if (result.Result == SubmitAction.MoveUp && MultiLine) - { - MoveUp(state); - } - else if (result.Result == SubmitAction.MoveDown && MultiLine) - { - MoveDown(state); - } - else if (result.Result == SubmitAction.MoveFirst && MultiLine) - { - MoveFirst(state); - } - else if (result.Result == SubmitAction.MoveLast && MultiLine) - { - MoveLast(state); - } - } - - _renderer.RenderLine(state, cursorPosition: 0); - - // Move the cursor to the last line - while (state.MoveDown()) - { - _console.Cursor.MoveDown(); - } - - // Moving the cursor won't work here if we're at - // the bottom of the screen, so let's insert a new line. - _console.WriteLine(); - - // Add the current state to the history - if (!state.IsEmpty) - { - _history.Add(state.GetBuffers()); - } - - // If cancelled return initial text otherwise - // return text from current state - return cancelled ? Text : state.Text; - } - - private async Task<(LineBuffer Buffer, SubmitAction Result)> ReadLine( - LineEditorState state, - CancellationToken cancellationToken) - { - var provider = new DefaultServiceProvider(_provider); - provider.RegisterOptional(Completion); - var context = new LineEditorContext(state.Buffer, provider); - - while (true) - { - if (cancellationToken.IsCancellationRequested) - { - return (state.Buffer, SubmitAction.Cancel); - } - - // Get command - var command = default(LineEditorCommand); - var key = await _input.ReadKey(MultiLine, cancellationToken).ConfigureAwait(false); - if (key != null) - { - if (key.Value.KeyChar != 0 && !char.IsControl(key.Value.KeyChar)) - { - command = new InsertCommand(key.Value.KeyChar); - } - else - { - command = KeyBindings.GetCommand(key.Value.Key, key.Value.Modifiers); - } - } - - // Execute command - if (command != null) - { - context.Execute(command); - } - - // Time to exit? - if (context.Result != null) - { - return (state.Buffer, context.Result.Value); - } - - // Render the line - _renderer.RenderLine(state); - LineDecorationRenderer?.RenderLineDecoration(state.Buffer); - } - } - - private void MoveUp(LineEditorState state) - { - Move(state, () => - { - if (state.MoveUp()) - { - _console.Cursor.MoveUp(); - } - }); - } - - private void MoveDown(LineEditorState state) - { - Move(state, () => - { - if (state.MoveDown()) - { - _console.Cursor.MoveDown(); - } - }); - } - - private void MoveFirst(LineEditorState state) - { - Move(state, () => - { - while (state.MoveUp()) - { - _console.Cursor.MoveUp(); - } - }); - } - - private void MoveLast(LineEditorState state) - { - Move(state, () => - { - while (state.MoveDown()) - { - _console.Cursor.MoveDown(); - } - }); - } - - private void Move(LineEditorState state, Action action) - { - using (_console.HideCursor()) - { - if (state.LineCount > _console.Profile.Height) - { - // Get the current position - var position = state.Buffer.Position; - - // Refresh everything - action(); - _renderer.Refresh(state); - - // Re-render the current line at the correct position - state.Buffer.Move(position); - _renderer.RenderLine(state); - } - else - { - // Get the current position - var position = state.Buffer.Position; - - // Reset the line - _renderer.RenderLine(state, cursorPosition: 0); - action(); - - // Render the current line at the correct position - state.Buffer.Move(position); - _renderer.RenderLine(state); - } - } - } - - private bool SetContent(LineEditorState state, IList? lines) - { - // Nothing to set? - if (lines == null || lines.Count == 0) - { - return false; - } - - var builder = new StringBuilder(); - - // Clearing the current lines will - // move the cursor to the top. - _renderer.AnsiBuilder.BuildClear(builder, state); - - // Remove all lines - state.RemoveAllLines(); - - // Hide the cursor - builder.Append("\u001b[?25l"); - - // Add all the lines - foreach (var line in lines) - { - state.AddLine(line.Content); - } - - // Make room for all the lines - var first = true; - foreach (var line in lines) - { - var shouldAddNewLine = true; - if (first) - { - shouldAddNewLine = false; - first = false; - } - - if (shouldAddNewLine) - { - _renderer.AnsiBuilder.MoveDown(builder, state); - } - } - - _renderer.AnsiBuilder.BuildRefresh(builder, state); - - // Show the cursor again - builder.Append("\u001b[?25h"); - - // Flush - _console.WriteAnsi(builder.ToString()); - return true; - } - } -} +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Spectre.Console; +using Spectre.Console.Advanced; + +namespace RadLine +{ + public sealed class LineEditor : IHighlighterAccessor + { + private readonly IInputSource _source; + private readonly IServiceProvider? _provider; + private readonly IAnsiConsole _console; + private readonly LineEditorRenderer _renderer; + private readonly LineEditorHistory _history; + private readonly InputBuffer _input; + + public KeyBindings KeyBindings { get; } + public bool MultiLine { get; init; } = false; + public string Text { get; init; } = string.Empty; + + public ILineEditorPrompt Prompt { get; init; } = new LineEditorPrompt("[yellow]>[/]"); + public ITextCompletion? Completion { get; init; } + public IHighlighter? Highlighter { get; init; } + public ILineEditorHistory History => _history; + + public ILineDecorationRenderer? LineDecorationRenderer { get; init; } + + public LineEditor(IAnsiConsole? terminal = null, IInputSource? source = null, IServiceProvider? provider = null) + { + _console = terminal ?? AnsiConsole.Console; + _source = source ?? new DefaultInputSource(_console); + _provider = provider; + _renderer = new LineEditorRenderer(_console, this); + _history = new LineEditorHistory(); + _input = new InputBuffer(_source); + + KeyBindings = new KeyBindings(); + KeyBindings.AddDefault(); + } + + public static bool IsSupported(IAnsiConsole console) + { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + return + console.Profile.Out.IsTerminal && + console.Profile.Capabilities.Ansi && + console.Profile.Capabilities.Interactive; + } + + public async Task ReadLine(CancellationToken cancellationToken) + { + var cancelled = false; + var state = new LineEditorState(Prompt, Text); + + _history.Reset(); + _input.Initialize(KeyBindings); + _renderer.Refresh(state); + + while (true) + { + var result = await ReadLine(state, cancellationToken).ConfigureAwait(false); + + if (result.Result == SubmitAction.Cancel) + { + cancelled = true; + break; + } + else if (result.Result == SubmitAction.Submit) + { + break; + } + else if (result.Result == SubmitAction.PreviousHistory) + { + if (_history.MovePrevious(state) && !SetContent(state, _history.Current)) + { + continue; + } + } + else if (result.Result == SubmitAction.NextHistory) + { + if (_history.MoveNext() && !SetContent(state, _history.Current)) + { + continue; + } + } + else if (result.Result == SubmitAction.NewLine && MultiLine && state.IsLastLine) + { + // Add a new line + state.AddLine(); + + // Refresh + var builder = new StringBuilder(); + builder.Append("\u001b[?25l"); // Hide cursor + _renderer.AnsiBuilder.MoveDown(builder, state); + _renderer.AnsiBuilder.BuildRefresh(builder, state); + builder.Append("\u001b[?25h"); // Show cursor + _console.WriteAnsi(builder.ToString()); + } + else if (result.Result == SubmitAction.MoveUp && MultiLine) + { + MoveUp(state); + } + else if (result.Result == SubmitAction.MoveDown && MultiLine) + { + MoveDown(state); + } + else if (result.Result == SubmitAction.MoveFirst && MultiLine) + { + MoveFirst(state); + } + else if (result.Result == SubmitAction.MoveLast && MultiLine) + { + MoveLast(state); + } + } + + _renderer.RenderLine(state, cursorPosition: 0); + + // Move the cursor to the last line + while (state.MoveDown()) + { + _console.Cursor.MoveDown(); + } + + // Moving the cursor won't work here if we're at + // the bottom of the screen, so let's insert a new line. + _console.WriteLine(); + + // Add the current state to the history + if (!state.IsEmpty) + { + _history.Add(state.GetBuffers()); + } + + // If cancelled return initial text otherwise + // return text from current state + return cancelled ? Text : state.Text; + } + + private async Task<(LineBuffer Buffer, SubmitAction Result)> ReadLine( + LineEditorState state, + CancellationToken cancellationToken) + { + var provider = new DefaultServiceProvider(_provider); + provider.RegisterOptional(Completion); + var context = new LineEditorContext(state.Buffer, provider); + + while (true) + { + if (cancellationToken.IsCancellationRequested) + { + return (state.Buffer, SubmitAction.Cancel); + } + + // Get command + var command = default(LineEditorCommand); + var key = await _input.ReadKey(MultiLine, cancellationToken).ConfigureAwait(false); + if (key != null) + { + if (key.Value.KeyChar != 0 && !char.IsControl(key.Value.KeyChar)) + { + command = new InsertCommand(key.Value.KeyChar); + } + else + { + command = KeyBindings.GetCommand(key.Value.Key, key.Value.Modifiers); + } + } + + // Execute command + if (command != null) + { + context.Execute(command); + } + + // Time to exit? + if (context.Result != null) + { + return (state.Buffer, context.Result.Value); + } + + // Render the line + _renderer.RenderLine(state); + LineDecorationRenderer?.RenderLineDecoration(state.Buffer); + } + } + + private void MoveUp(LineEditorState state) + { + Move(state, () => + { + if (state.MoveUp()) + { + _console.Cursor.MoveUp(); + } + }); + } + + private void MoveDown(LineEditorState state) + { + Move(state, () => + { + if (state.MoveDown()) + { + _console.Cursor.MoveDown(); + } + }); + } + + private void MoveFirst(LineEditorState state) + { + Move(state, () => + { + while (state.MoveUp()) + { + _console.Cursor.MoveUp(); + } + }); + } + + private void MoveLast(LineEditorState state) + { + Move(state, () => + { + while (state.MoveDown()) + { + _console.Cursor.MoveDown(); + } + }); + } + + private void Move(LineEditorState state, Action action) + { + using (_console.HideCursor()) + { + if (state.LineCount > _console.Profile.Height) + { + // Get the current position + var position = state.Buffer.Position; + + // Refresh everything + action(); + _renderer.Refresh(state); + + // Re-render the current line at the correct position + state.Buffer.Move(position); + _renderer.RenderLine(state); + } + else + { + // Get the current position + var position = state.Buffer.Position; + + // Reset the line + _renderer.RenderLine(state, cursorPosition: 0); + action(); + + // Render the current line at the correct position + state.Buffer.Move(position); + _renderer.RenderLine(state); + } + } + } + + private bool SetContent(LineEditorState state, IList? lines) + { + // Nothing to set? + if (lines == null || lines.Count == 0) + { + return false; + } + + var builder = new StringBuilder(); + + // Clearing the current lines will + // move the cursor to the top. + _renderer.AnsiBuilder.BuildClear(builder, state); + + // Remove all lines + state.RemoveAllLines(); + + // Hide the cursor + builder.Append("\u001b[?25l"); + + // Add all the lines + foreach (var line in lines) + { + state.AddLine(line.Content); + } + + // Make room for all the lines + var first = true; + foreach (var line in lines) + { + var shouldAddNewLine = true; + if (first) + { + shouldAddNewLine = false; + first = false; + } + + if (shouldAddNewLine) + { + _renderer.AnsiBuilder.MoveDown(builder, state); + } + } + + _renderer.AnsiBuilder.BuildRefresh(builder, state); + + // Show the cursor again + builder.Append("\u001b[?25h"); + + // Flush + _console.WriteAnsi(builder.ToString()); + return true; + } + } +} From bb2ffabc0b1177b587c37c76756345865b977143 Mon Sep 17 00:00:00 2001 From: Andre Vianna Date: Wed, 18 Sep 2024 11:46:36 -0400 Subject: [PATCH 5/8] Correct mixed EOL char in the file --- src/RadLine.Tests/Commands/InsertCommandTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RadLine.Tests/Commands/InsertCommandTests.cs b/src/RadLine.Tests/Commands/InsertCommandTests.cs index 39e328d..468c21d 100644 --- a/src/RadLine.Tests/Commands/InsertCommandTests.cs +++ b/src/RadLine.Tests/Commands/InsertCommandTests.cs @@ -21,4 +21,4 @@ public void Should_Insert_Text_At_Position() buffer.Position.ShouldBe(4); } } -} +} \ No newline at end of file From dd6d39600e68de4ddc1dd6de6ecc32828ade0a31 Mon Sep 17 00:00:00 2001 From: Andre Vianna Date: Wed, 18 Sep 2024 11:48:09 -0400 Subject: [PATCH 6/8] Correct mixed EOL char in the file --- src/RadLine.Tests/Commands/CancelCommandTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RadLine.Tests/Commands/CancelCommandTests.cs b/src/RadLine.Tests/Commands/CancelCommandTests.cs index 62c642b..ddd7cc4 100644 --- a/src/RadLine.Tests/Commands/CancelCommandTests.cs +++ b/src/RadLine.Tests/Commands/CancelCommandTests.cs @@ -22,4 +22,4 @@ public void Should_Cancel_Input() buffer.Position.ShouldBe(3); } } -} +} \ No newline at end of file From bb1d4b5302949c3508b2c82994269b9d0b93abbb Mon Sep 17 00:00:00 2001 From: Andre Vianna Date: Wed, 18 Sep 2024 12:02:11 -0400 Subject: [PATCH 7/8] Correct mixed EOL char --- .../Commands/CancelCommandTests.cs | 48 +- .../Commands/InsertCommandTests.cs | 46 +- src/RadLine.Tests/LineEditorTests.cs | 306 ++++----- .../Utilities/TestInputSource.cs | 176 ++--- src/RadLine/Commands/CancelCommand.cs | 18 +- src/RadLine/Internal/InputBuffer.cs | 262 +++---- src/RadLine/KeyBindingsExtensions.cs | 148 ++-- src/RadLine/LineBuffer.cs | 364 +++++----- src/RadLine/LineEditor.cs | 650 +++++++++--------- src/RadLine/LineEditorContext.cs | 6 +- 10 files changed, 1012 insertions(+), 1012 deletions(-) diff --git a/src/RadLine.Tests/Commands/CancelCommandTests.cs b/src/RadLine.Tests/Commands/CancelCommandTests.cs index ddd7cc4..66b181f 100644 --- a/src/RadLine.Tests/Commands/CancelCommandTests.cs +++ b/src/RadLine.Tests/Commands/CancelCommandTests.cs @@ -1,25 +1,25 @@ -using Shouldly; -using Xunit; - -namespace RadLine.Tests -{ - public sealed class CancelCommandTests - { - [Fact] - public void Should_Cancel_Input() - { - // Given - var buffer = new LineBuffer("Foo"); - var context = new LineEditorContext(buffer); - buffer.Insert("Bar"); - var command = new CancelCommand(); - - // When - command.Execute(context); - - // Then - buffer.Content.ShouldBe("Foo"); - buffer.Position.ShouldBe(3); - } - } +using Shouldly; +using Xunit; + +namespace RadLine.Tests +{ + public sealed class CancelCommandTests + { + [Fact] + public void Should_Cancel_Input() + { + // Given + var buffer = new LineBuffer("Foo"); + var context = new LineEditorContext(buffer); + buffer.Insert("Bar"); + var command = new CancelCommand(); + + // When + command.Execute(context); + + // Then + buffer.Content.ShouldBe("Foo"); + buffer.Position.ShouldBe(3); + } + } } \ No newline at end of file diff --git a/src/RadLine.Tests/Commands/InsertCommandTests.cs b/src/RadLine.Tests/Commands/InsertCommandTests.cs index 468c21d..cec7949 100644 --- a/src/RadLine.Tests/Commands/InsertCommandTests.cs +++ b/src/RadLine.Tests/Commands/InsertCommandTests.cs @@ -1,24 +1,24 @@ -using Shouldly; -using Xunit; - -namespace RadLine.Tests -{ - public sealed class InsertCommandTests - { - [Fact] - public void Should_Insert_Text_At_Position() - { - // Given - var buffer = new LineBuffer("Foo"); - var context = new LineEditorContext(buffer); - var command = new InsertCommand('l'); - - // When - command.Execute(context); - - // Then - buffer.Content.ShouldBe("Fool"); - buffer.Position.ShouldBe(4); - } - } +using Shouldly; +using Xunit; + +namespace RadLine.Tests +{ + public sealed class InsertCommandTests + { + [Fact] + public void Should_Insert_Text_At_Position() + { + // Given + var buffer = new LineBuffer("Foo"); + var context = new LineEditorContext(buffer); + var command = new InsertCommand('l'); + + // When + command.Execute(context); + + // Then + buffer.Content.ShouldBe("Fool"); + buffer.Position.ShouldBe(4); + } + } } \ No newline at end of file diff --git a/src/RadLine.Tests/LineEditorTests.cs b/src/RadLine.Tests/LineEditorTests.cs index efac4bc..accc556 100644 --- a/src/RadLine.Tests/LineEditorTests.cs +++ b/src/RadLine.Tests/LineEditorTests.cs @@ -1,153 +1,153 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Shouldly; -using Spectre.Console.Testing; -using Xunit; - -namespace RadLine.Tests -{ - public sealed class LineEditorTests - { - [Fact] - public async Task Should_Return_Original_Text_When_Pressing_Escape() - { - // Given - var editor = new LineEditor( - new TestConsole(), - new TestInputSource() - .Push("Bar") - .PushEscape()) - { - Text = "Foo", - }; - - // When - var result = await editor.ReadLine(CancellationToken.None); - - // Then - result.ShouldBe("Foo"); - } - - [Fact] - public async Task Should_Return_Entered_Text_When_Pressing_Enter() - { - // Given - var editor = new LineEditor( - new TestConsole(), - new TestInputSource() - .Push("Patrik") - .PushEnter()); - - // When - var result = await editor.ReadLine(CancellationToken.None); - - // Then - result.ShouldBe("Patrik"); - } - - [Fact] - public async Task Should_Add_New_Line_When_Pressing_Shift_And_Enter() - { - // Given - var editor = new LineEditor( - new TestConsole(), - new TestInputSource() - .Push("Patrik") - .PushNewLine() - .Push("Svensson") - .PushEnter()) - { - MultiLine = true, - }; - - // When - var result = await editor.ReadLine(CancellationToken.None); - - // Then - result.ShouldBe($"Patrik{Environment.NewLine}Svensson"); - } - - [Fact] - public async Task Should_Move_To_Previous_Item_In_History() - { - // Given - var editor = new LineEditor( - new TestConsole(), - new TestInputSource() - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .PushEnter()); - - editor.History.Add("Foo"); - editor.History.Add("Bar"); - editor.History.Add("Baz"); - - // When - var result = await editor.ReadLine(CancellationToken.None); - - // Then - result.ShouldBe("Foo"); - } - - [Fact] - public async Task Should_Move_To_Next_Item_In_History() - { - // Given - var editor = new LineEditor( - new TestConsole(), - new TestInputSource() - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.DownArrow, ConsoleModifiers.Control) - .Push(ConsoleKey.DownArrow, ConsoleModifiers.Control) - .PushEnter()); - - editor.History.Add("Foo"); - editor.History.Add("Bar"); - editor.History.Add("Baz"); - - // When - var result = await editor.ReadLine(CancellationToken.None); - - // Then - result.ShouldBe("Baz"); - } - - [Fact] - public async Task Should_Add_Entered_Text_To_History() - { - // Given - var input = new TestInputSource(); - var editor = new LineEditor(new TestConsole(), input); - input.Push("Patrik").PushEnter(); - await editor.ReadLine(CancellationToken.None); - - // When - input.Push(ConsoleKey.UpArrow, ConsoleModifiers.Control).PushEnter(); - var result = await editor.ReadLine(CancellationToken.None); - - // Then - result.ShouldBe("Patrik"); - } - - [Fact] - public async Task Should_Not_Add_Entered_Text_To_History_If_Its_The_Same_As_The_Last_Entry() - { - // Given - var input = new TestInputSource(); - var editor = new LineEditor(new TestConsole(), input); - input.Push("Patrik").PushNewLine().Push("Svensson").PushEnter(); - await editor.ReadLine(CancellationToken.None); - - // When - input.Push("Patrik").PushNewLine().Push("Svensson").PushEnter(); - var result = await editor.ReadLine(CancellationToken.None); - - // Then - editor.History.Count.ShouldBe(1); - } - } -} +using System; +using System.Threading; +using System.Threading.Tasks; +using Shouldly; +using Spectre.Console.Testing; +using Xunit; + +namespace RadLine.Tests +{ + public sealed class LineEditorTests + { + [Fact] + public async Task Should_Return_Original_Text_When_Pressing_Escape() + { + // Given + var editor = new LineEditor( + new TestConsole(), + new TestInputSource() + .Push("Bar") + .PushEscape()) + { + Text = "Foo", + }; + + // When + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe("Foo"); + } + + [Fact] + public async Task Should_Return_Entered_Text_When_Pressing_Enter() + { + // Given + var editor = new LineEditor( + new TestConsole(), + new TestInputSource() + .Push("Patrik") + .PushEnter()); + + // When + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe("Patrik"); + } + + [Fact] + public async Task Should_Add_New_Line_When_Pressing_Shift_And_Enter() + { + // Given + var editor = new LineEditor( + new TestConsole(), + new TestInputSource() + .Push("Patrik") + .PushNewLine() + .Push("Svensson") + .PushEnter()) + { + MultiLine = true, + }; + + // When + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe($"Patrik{Environment.NewLine}Svensson"); + } + + [Fact] + public async Task Should_Move_To_Previous_Item_In_History() + { + // Given + var editor = new LineEditor( + new TestConsole(), + new TestInputSource() + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .PushEnter()); + + editor.History.Add("Foo"); + editor.History.Add("Bar"); + editor.History.Add("Baz"); + + // When + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe("Foo"); + } + + [Fact] + public async Task Should_Move_To_Next_Item_In_History() + { + // Given + var editor = new LineEditor( + new TestConsole(), + new TestInputSource() + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.UpArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.DownArrow, ConsoleModifiers.Control) + .Push(ConsoleKey.DownArrow, ConsoleModifiers.Control) + .PushEnter()); + + editor.History.Add("Foo"); + editor.History.Add("Bar"); + editor.History.Add("Baz"); + + // When + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe("Baz"); + } + + [Fact] + public async Task Should_Add_Entered_Text_To_History() + { + // Given + var input = new TestInputSource(); + var editor = new LineEditor(new TestConsole(), input); + input.Push("Patrik").PushEnter(); + await editor.ReadLine(CancellationToken.None); + + // When + input.Push(ConsoleKey.UpArrow, ConsoleModifiers.Control).PushEnter(); + var result = await editor.ReadLine(CancellationToken.None); + + // Then + result.ShouldBe("Patrik"); + } + + [Fact] + public async Task Should_Not_Add_Entered_Text_To_History_If_Its_The_Same_As_The_Last_Entry() + { + // Given + var input = new TestInputSource(); + var editor = new LineEditor(new TestConsole(), input); + input.Push("Patrik").PushNewLine().Push("Svensson").PushEnter(); + await editor.ReadLine(CancellationToken.None); + + // When + input.Push("Patrik").PushNewLine().Push("Svensson").PushEnter(); + var result = await editor.ReadLine(CancellationToken.None); + + // Then + editor.History.Count.ShouldBe(1); + } + } +} diff --git a/src/RadLine.Tests/Utilities/TestInputSource.cs b/src/RadLine.Tests/Utilities/TestInputSource.cs index d9bf8fe..9dc16d3 100644 --- a/src/RadLine.Tests/Utilities/TestInputSource.cs +++ b/src/RadLine.Tests/Utilities/TestInputSource.cs @@ -1,88 +1,88 @@ -using System; -using System.Collections.Generic; - -namespace RadLine.Tests -{ - public sealed class TestInputSource : IInputSource - { - private readonly Queue _input; - - public bool ByPassProcessing => true; - - public TestInputSource() - { - _input = new Queue(); - } - - public TestInputSource Push(string input) - { - if (input is null) - { - throw new ArgumentNullException(nameof(input)); - } - - foreach (var character in input) - { - Push(character); - } - - return this; - } - - public TestInputSource Push(char input) - { - var control = char.IsUpper(input); - _input.Enqueue(new ConsoleKeyInfo(input, (ConsoleKey)input, false, false, control)); - return this; - } - - public TestInputSource Push(ConsoleKey input) - { - _input.Enqueue(new ConsoleKeyInfo((char)input, input, false, false, false)); - return this; - } - - public TestInputSource PushNewLine() - { - Push(ConsoleKey.Enter, ConsoleModifiers.Shift); - return this; - } - - public TestInputSource PushEnter() - { - Push(ConsoleKey.Enter); - return this; - } - - public TestInputSource PushEscape() - { - Push(ConsoleKey.Escape); - return this; - } - - public TestInputSource Push(ConsoleKey input, ConsoleModifiers modifiers) - { - var shift = modifiers.HasFlag(ConsoleModifiers.Shift); - var control = modifiers.HasFlag(ConsoleModifiers.Control); - var alt = modifiers.HasFlag(ConsoleModifiers.Alt); - - _input.Enqueue(new ConsoleKeyInfo((char)0, input, shift, alt, control)); - return this; - } - - public bool IsKeyAvailable() - { - return _input.Count > 0; - } - - ConsoleKeyInfo IInputSource.ReadKey() - { - if (_input.Count == 0) - { - throw new InvalidOperationException("No keys available"); - } - - return _input.Dequeue(); - } - } -} +using System; +using System.Collections.Generic; + +namespace RadLine.Tests +{ + public sealed class TestInputSource : IInputSource + { + private readonly Queue _input; + + public bool ByPassProcessing => true; + + public TestInputSource() + { + _input = new Queue(); + } + + public TestInputSource Push(string input) + { + if (input is null) + { + throw new ArgumentNullException(nameof(input)); + } + + foreach (var character in input) + { + Push(character); + } + + return this; + } + + public TestInputSource Push(char input) + { + var control = char.IsUpper(input); + _input.Enqueue(new ConsoleKeyInfo(input, (ConsoleKey)input, false, false, control)); + return this; + } + + public TestInputSource Push(ConsoleKey input) + { + _input.Enqueue(new ConsoleKeyInfo((char)input, input, false, false, false)); + return this; + } + + public TestInputSource PushNewLine() + { + Push(ConsoleKey.Enter, ConsoleModifiers.Shift); + return this; + } + + public TestInputSource PushEnter() + { + Push(ConsoleKey.Enter); + return this; + } + + public TestInputSource PushEscape() + { + Push(ConsoleKey.Escape); + return this; + } + + public TestInputSource Push(ConsoleKey input, ConsoleModifiers modifiers) + { + var shift = modifiers.HasFlag(ConsoleModifiers.Shift); + var control = modifiers.HasFlag(ConsoleModifiers.Control); + var alt = modifiers.HasFlag(ConsoleModifiers.Alt); + + _input.Enqueue(new ConsoleKeyInfo((char)0, input, shift, alt, control)); + return this; + } + + public bool IsKeyAvailable() + { + return _input.Count > 0; + } + + ConsoleKeyInfo IInputSource.ReadKey() + { + if (_input.Count == 0) + { + throw new InvalidOperationException("No keys available"); + } + + return _input.Dequeue(); + } + } +} diff --git a/src/RadLine/Commands/CancelCommand.cs b/src/RadLine/Commands/CancelCommand.cs index 6117fc6..fa1a1e5 100644 --- a/src/RadLine/Commands/CancelCommand.cs +++ b/src/RadLine/Commands/CancelCommand.cs @@ -1,10 +1,10 @@ -namespace RadLine; - -public sealed class CancelCommand : LineEditorCommand -{ - public override void Execute(LineEditorContext context) - { - context.Buffer.Reset(); - context.Submit(SubmitAction.Cancel); - } +namespace RadLine; + +public sealed class CancelCommand : LineEditorCommand +{ + public override void Execute(LineEditorContext context) + { + context.Buffer.Reset(); + context.Submit(SubmitAction.Cancel); + } } \ No newline at end of file diff --git a/src/RadLine/Internal/InputBuffer.cs b/src/RadLine/Internal/InputBuffer.cs index 64e4506..7480070 100644 --- a/src/RadLine/Internal/InputBuffer.cs +++ b/src/RadLine/Internal/InputBuffer.cs @@ -1,131 +1,131 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace RadLine -{ - internal sealed class InputBuffer - { - private readonly IInputSource _source; - private readonly Queue _queue; - private KeyBinding? _newLineBinding; - private KeyBinding? _submitBinding; - - public InputBuffer(IInputSource source) - { - _source = source ?? throw new ArgumentNullException(nameof(source)); - _queue = new Queue(); - } - - public void Initialize(KeyBindings bindings) - { - bindings.TryFindKeyBindings(out _newLineBinding); - bindings.TryFindKeyBindings(out _submitBinding); - } - - public async Task ReadKey(bool multiline, CancellationToken cancellationToken) - { - if (_queue.Count > 0) - { - return _queue.Dequeue(); - } - - // Wait for the user to enter a key - var key = await ReadKeyFromSource(wait: true, cancellationToken); - if (key == null) - { - return null; - } - else - { - _queue.Enqueue(key.Value); - } - - if (_source.IsKeyAvailable()) - { - // Read all remaining keys from the buffer - await ReadRemainingKeys(multiline, cancellationToken); - } - - // Got something? - if (_queue.Count > 0) - { - return _queue.Dequeue(); - } - - return null; - } - - private async Task ReadRemainingKeys(bool multiline, CancellationToken cancellationToken) - { - var keys = new Queue(); - - while (true) - { - var key = await ReadKeyFromSource(wait: false, cancellationToken); - if (key == null) - { - break; - } - - keys.Enqueue(key.Value); - } - - if (keys.Count > 0) - { - // Process the input when we're somewhat sure that - // the input has been automated in some fashion, - // and the editor support multiline. The input source - // can bypass this kind of behavior, so we need to check - // it as well to see if we should do any processing. - var shouldProcess = multiline && keys.Count >= 5 && !_source.ByPassProcessing; - - while (keys.Count > 0) - { - var key = keys.Dequeue(); - - if (shouldProcess && _submitBinding != null && _newLineBinding != null) - { - // Is the key trying to submit? - if (_submitBinding.Equals(key)) - { - // Insert a new line instead - key = _newLineBinding.AsConsoleKeyInfo(); - } - } - - _queue.Enqueue(key); - } - } - } - - private async Task ReadKeyFromSource(bool wait, CancellationToken cancellationToken) - { - if (wait) - { - while (true) - { - if (cancellationToken.IsCancellationRequested) - { - return null; - } - - if (_source.IsKeyAvailable()) - { - break; - } - - await Task.Delay(5, cancellationToken).ConfigureAwait(false); - } - } - - if (_source.IsKeyAvailable()) - { - return _source.ReadKey(); - } - - return null; - } - } -} +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace RadLine +{ + internal sealed class InputBuffer + { + private readonly IInputSource _source; + private readonly Queue _queue; + private KeyBinding? _newLineBinding; + private KeyBinding? _submitBinding; + + public InputBuffer(IInputSource source) + { + _source = source ?? throw new ArgumentNullException(nameof(source)); + _queue = new Queue(); + } + + public void Initialize(KeyBindings bindings) + { + bindings.TryFindKeyBindings(out _newLineBinding); + bindings.TryFindKeyBindings(out _submitBinding); + } + + public async Task ReadKey(bool multiline, CancellationToken cancellationToken) + { + if (_queue.Count > 0) + { + return _queue.Dequeue(); + } + + // Wait for the user to enter a key + var key = await ReadKeyFromSource(wait: true, cancellationToken); + if (key == null) + { + return null; + } + else + { + _queue.Enqueue(key.Value); + } + + if (_source.IsKeyAvailable()) + { + // Read all remaining keys from the buffer + await ReadRemainingKeys(multiline, cancellationToken); + } + + // Got something? + if (_queue.Count > 0) + { + return _queue.Dequeue(); + } + + return null; + } + + private async Task ReadRemainingKeys(bool multiline, CancellationToken cancellationToken) + { + var keys = new Queue(); + + while (true) + { + var key = await ReadKeyFromSource(wait: false, cancellationToken); + if (key == null) + { + break; + } + + keys.Enqueue(key.Value); + } + + if (keys.Count > 0) + { + // Process the input when we're somewhat sure that + // the input has been automated in some fashion, + // and the editor support multiline. The input source + // can bypass this kind of behavior, so we need to check + // it as well to see if we should do any processing. + var shouldProcess = multiline && keys.Count >= 5 && !_source.ByPassProcessing; + + while (keys.Count > 0) + { + var key = keys.Dequeue(); + + if (shouldProcess && _submitBinding != null && _newLineBinding != null) + { + // Is the key trying to submit? + if (_submitBinding.Equals(key)) + { + // Insert a new line instead + key = _newLineBinding.AsConsoleKeyInfo(); + } + } + + _queue.Enqueue(key); + } + } + } + + private async Task ReadKeyFromSource(bool wait, CancellationToken cancellationToken) + { + if (wait) + { + while (true) + { + if (cancellationToken.IsCancellationRequested) + { + return null; + } + + if (_source.IsKeyAvailable()) + { + break; + } + + await Task.Delay(5, cancellationToken).ConfigureAwait(false); + } + } + + if (_source.IsKeyAvailable()) + { + return _source.ReadKey(); + } + + return null; + } + } +} diff --git a/src/RadLine/KeyBindingsExtensions.cs b/src/RadLine/KeyBindingsExtensions.cs index 4095cad..8949b7e 100644 --- a/src/RadLine/KeyBindingsExtensions.cs +++ b/src/RadLine/KeyBindingsExtensions.cs @@ -1,74 +1,74 @@ -using System; - -namespace RadLine -{ - public static class KeyBindingsExtensions - { - public static void AddDefault(this KeyBindings bindings) - { - bindings.Add(ConsoleKey.Tab, () => new AutoCompleteCommand(AutoComplete.Next)); - bindings.Add(ConsoleKey.Tab, ConsoleModifiers.Control, () => new AutoCompleteCommand(AutoComplete.Previous)); - - bindings.Add(ConsoleKey.Backspace); - bindings.Add(ConsoleKey.Delete); - bindings.Add(ConsoleKey.Home); - bindings.Add(ConsoleKey.End); - bindings.Add(ConsoleKey.UpArrow); - bindings.Add(ConsoleKey.DownArrow); - bindings.Add(ConsoleKey.PageUp); - bindings.Add(ConsoleKey.PageDown); - bindings.Add(ConsoleKey.LeftArrow); - bindings.Add(ConsoleKey.RightArrow); - bindings.Add(ConsoleKey.LeftArrow, ConsoleModifiers.Control); - bindings.Add(ConsoleKey.RightArrow, ConsoleModifiers.Control); - bindings.Add(ConsoleKey.UpArrow, ConsoleModifiers.Control); - bindings.Add(ConsoleKey.DownArrow, ConsoleModifiers.Control); - bindings.Add(ConsoleKey.Escape); - bindings.Add(ConsoleKey.Enter); - bindings.Add(ConsoleKey.Enter, ConsoleModifiers.Shift); - } - - public static void Add(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers? modifiers = null) - where TCommand : LineEditorCommand, new() - { - if (bindings is null) - { - throw new ArgumentNullException(nameof(bindings)); - } - - bindings.Add(new KeyBinding(key, modifiers), () => new TCommand()); - } - - public static void Add(this KeyBindings bindings, ConsoleKey key, Func func) - where TCommand : LineEditorCommand - { - if (bindings is null) - { - throw new ArgumentNullException(nameof(bindings)); - } - - bindings.Add(new KeyBinding(key), () => func()); - } - - public static void Add(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers modifiers, Func func) - where TCommand : LineEditorCommand - { - if (bindings is null) - { - throw new ArgumentNullException(nameof(bindings)); - } - - bindings.Add(new KeyBinding(key, modifiers), () => func()); - } - - public static void Remove(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers? modifiers = null) - { - if (bindings is null) - { - throw new ArgumentNullException(nameof(bindings)); - } - - bindings.Remove(new KeyBinding(key, modifiers)); - } - } -} +using System; + +namespace RadLine +{ + public static class KeyBindingsExtensions + { + public static void AddDefault(this KeyBindings bindings) + { + bindings.Add(ConsoleKey.Tab, () => new AutoCompleteCommand(AutoComplete.Next)); + bindings.Add(ConsoleKey.Tab, ConsoleModifiers.Control, () => new AutoCompleteCommand(AutoComplete.Previous)); + + bindings.Add(ConsoleKey.Backspace); + bindings.Add(ConsoleKey.Delete); + bindings.Add(ConsoleKey.Home); + bindings.Add(ConsoleKey.End); + bindings.Add(ConsoleKey.UpArrow); + bindings.Add(ConsoleKey.DownArrow); + bindings.Add(ConsoleKey.PageUp); + bindings.Add(ConsoleKey.PageDown); + bindings.Add(ConsoleKey.LeftArrow); + bindings.Add(ConsoleKey.RightArrow); + bindings.Add(ConsoleKey.LeftArrow, ConsoleModifiers.Control); + bindings.Add(ConsoleKey.RightArrow, ConsoleModifiers.Control); + bindings.Add(ConsoleKey.UpArrow, ConsoleModifiers.Control); + bindings.Add(ConsoleKey.DownArrow, ConsoleModifiers.Control); + bindings.Add(ConsoleKey.Escape); + bindings.Add(ConsoleKey.Enter); + bindings.Add(ConsoleKey.Enter, ConsoleModifiers.Shift); + } + + public static void Add(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers? modifiers = null) + where TCommand : LineEditorCommand, new() + { + if (bindings is null) + { + throw new ArgumentNullException(nameof(bindings)); + } + + bindings.Add(new KeyBinding(key, modifiers), () => new TCommand()); + } + + public static void Add(this KeyBindings bindings, ConsoleKey key, Func func) + where TCommand : LineEditorCommand + { + if (bindings is null) + { + throw new ArgumentNullException(nameof(bindings)); + } + + bindings.Add(new KeyBinding(key), () => func()); + } + + public static void Add(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers modifiers, Func func) + where TCommand : LineEditorCommand + { + if (bindings is null) + { + throw new ArgumentNullException(nameof(bindings)); + } + + bindings.Add(new KeyBinding(key, modifiers), () => func()); + } + + public static void Remove(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers? modifiers = null) + { + if (bindings is null) + { + throw new ArgumentNullException(nameof(bindings)); + } + + bindings.Remove(new KeyBinding(key, modifiers)); + } + } +} diff --git a/src/RadLine/LineBuffer.cs b/src/RadLine/LineBuffer.cs index 26f58d4..4d1dbb8 100644 --- a/src/RadLine/LineBuffer.cs +++ b/src/RadLine/LineBuffer.cs @@ -1,182 +1,182 @@ -using System; -using System.Globalization; -using System.Linq; - -namespace RadLine -{ - public sealed class LineBuffer - { - private readonly string _initialContent; - private string _buffer; - private int _position; - - public int Position => _position; - public int Length => _buffer.Length; - public string InitialContent => _initialContent; - public string Content => _buffer; - - public bool AtBeginning => Position == 0; - public bool AtEnd => Position == Content.Length; - - public bool IsAtCharacter - { - get - { - if (Length == 0) - { - return false; - } - - if (AtEnd) - { - return false; - } - - return !char.IsWhiteSpace(_buffer[_position]); - } - } - - public bool IsAtBeginningOfWord - { - get - { - if (Length == 0) - { - return false; - } - - if (_position == 0) - { - return !char.IsWhiteSpace(_buffer[0]); - } - - return char.IsWhiteSpace(_buffer[_position - 1]); - } - } - - public bool IsAtEndOfWord - { - get - { - if (Length == 0) - { - return false; - } - - if (_position == 0) - { - return false; - } - - return !char.IsWhiteSpace(_buffer[_position - 1]); - } - } - - // TODO: Right now, this only returns the position in the line buffer. - // This is OK for western alphabets and most emojis which consist - // of a single surrogate pair, but everything else will be wrong. - public int CursorPosition => _position; - - public LineBuffer(string? content = null) - { - _initialContent = content ?? string.Empty; - _buffer = _initialContent; - _position = _buffer.Length; - } - - public LineBuffer(LineBuffer buffer) - { - if (buffer is null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - _initialContent = buffer.InitialContent; - _buffer = buffer.Content; - _position = _buffer.Length; - } - - public bool Move(int position) - { - if (position == _position) - { - return false; - } - - var movingLeft = position < _position; - _position = MoveToPosition(position, movingLeft); - - return true; - } - - public void Insert(char character) - { - _buffer = _buffer.Insert(_position, character.ToString()); - } - - public void Insert(string text) - { - _buffer = _buffer.Insert(_position, text); - } - - public void Reset() - { - _buffer = _initialContent; - _position = _buffer.Length; - } - - public int Clear(int index, int count) - { - if (index < 0) - { - return 0; - } - - if (index > _buffer.Length - 1) - { - return 0; - } - - var length = _buffer.Length; - _buffer = _buffer.Remove(Math.Max(0, index), Math.Min(count, _buffer.Length - index)); - return Math.Max(length - _buffer.Length, 0); - } - - private int MoveToPosition(int position, bool movingLeft) - { - if (position <= 0) - { - return 0; - } - else if (position >= _buffer.Length) - { - return _buffer.Length; - } - - var indices = StringInfo.ParseCombiningCharacters(_buffer); - - if (movingLeft) - { - foreach (var e in indices.Reverse()) - { - if (e <= position) - { - return e; - } - } - } - else - { - foreach (var e in indices) - { - if (e >= position) - { - return e; - } - } - } - - throw new InvalidOperationException("Could not find position in buffer"); - } - } -} +using System; +using System.Globalization; +using System.Linq; + +namespace RadLine +{ + public sealed class LineBuffer + { + private readonly string _initialContent; + private string _buffer; + private int _position; + + public int Position => _position; + public int Length => _buffer.Length; + public string InitialContent => _initialContent; + public string Content => _buffer; + + public bool AtBeginning => Position == 0; + public bool AtEnd => Position == Content.Length; + + public bool IsAtCharacter + { + get + { + if (Length == 0) + { + return false; + } + + if (AtEnd) + { + return false; + } + + return !char.IsWhiteSpace(_buffer[_position]); + } + } + + public bool IsAtBeginningOfWord + { + get + { + if (Length == 0) + { + return false; + } + + if (_position == 0) + { + return !char.IsWhiteSpace(_buffer[0]); + } + + return char.IsWhiteSpace(_buffer[_position - 1]); + } + } + + public bool IsAtEndOfWord + { + get + { + if (Length == 0) + { + return false; + } + + if (_position == 0) + { + return false; + } + + return !char.IsWhiteSpace(_buffer[_position - 1]); + } + } + + // TODO: Right now, this only returns the position in the line buffer. + // This is OK for western alphabets and most emojis which consist + // of a single surrogate pair, but everything else will be wrong. + public int CursorPosition => _position; + + public LineBuffer(string? content = null) + { + _initialContent = content ?? string.Empty; + _buffer = _initialContent; + _position = _buffer.Length; + } + + public LineBuffer(LineBuffer buffer) + { + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + _initialContent = buffer.InitialContent; + _buffer = buffer.Content; + _position = _buffer.Length; + } + + public bool Move(int position) + { + if (position == _position) + { + return false; + } + + var movingLeft = position < _position; + _position = MoveToPosition(position, movingLeft); + + return true; + } + + public void Insert(char character) + { + _buffer = _buffer.Insert(_position, character.ToString()); + } + + public void Insert(string text) + { + _buffer = _buffer.Insert(_position, text); + } + + public void Reset() + { + _buffer = _initialContent; + _position = _buffer.Length; + } + + public int Clear(int index, int count) + { + if (index < 0) + { + return 0; + } + + if (index > _buffer.Length - 1) + { + return 0; + } + + var length = _buffer.Length; + _buffer = _buffer.Remove(Math.Max(0, index), Math.Min(count, _buffer.Length - index)); + return Math.Max(length - _buffer.Length, 0); + } + + private int MoveToPosition(int position, bool movingLeft) + { + if (position <= 0) + { + return 0; + } + else if (position >= _buffer.Length) + { + return _buffer.Length; + } + + var indices = StringInfo.ParseCombiningCharacters(_buffer); + + if (movingLeft) + { + foreach (var e in indices.Reverse()) + { + if (e <= position) + { + return e; + } + } + } + else + { + foreach (var e in indices) + { + if (e >= position) + { + return e; + } + } + } + + throw new InvalidOperationException("Could not find position in buffer"); + } + } +} diff --git a/src/RadLine/LineEditor.cs b/src/RadLine/LineEditor.cs index 1ac892f..e166461 100644 --- a/src/RadLine/LineEditor.cs +++ b/src/RadLine/LineEditor.cs @@ -1,325 +1,325 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Spectre.Console; -using Spectre.Console.Advanced; - -namespace RadLine -{ - public sealed class LineEditor : IHighlighterAccessor - { - private readonly IInputSource _source; - private readonly IServiceProvider? _provider; - private readonly IAnsiConsole _console; - private readonly LineEditorRenderer _renderer; - private readonly LineEditorHistory _history; - private readonly InputBuffer _input; - - public KeyBindings KeyBindings { get; } - public bool MultiLine { get; init; } = false; - public string Text { get; init; } = string.Empty; - - public ILineEditorPrompt Prompt { get; init; } = new LineEditorPrompt("[yellow]>[/]"); - public ITextCompletion? Completion { get; init; } - public IHighlighter? Highlighter { get; init; } - public ILineEditorHistory History => _history; - - public ILineDecorationRenderer? LineDecorationRenderer { get; init; } - - public LineEditor(IAnsiConsole? terminal = null, IInputSource? source = null, IServiceProvider? provider = null) - { - _console = terminal ?? AnsiConsole.Console; - _source = source ?? new DefaultInputSource(_console); - _provider = provider; - _renderer = new LineEditorRenderer(_console, this); - _history = new LineEditorHistory(); - _input = new InputBuffer(_source); - - KeyBindings = new KeyBindings(); - KeyBindings.AddDefault(); - } - - public static bool IsSupported(IAnsiConsole console) - { - if (console is null) - { - throw new ArgumentNullException(nameof(console)); - } - - return - console.Profile.Out.IsTerminal && - console.Profile.Capabilities.Ansi && - console.Profile.Capabilities.Interactive; - } - - public async Task ReadLine(CancellationToken cancellationToken) - { - var cancelled = false; - var state = new LineEditorState(Prompt, Text); - - _history.Reset(); - _input.Initialize(KeyBindings); - _renderer.Refresh(state); - - while (true) - { - var result = await ReadLine(state, cancellationToken).ConfigureAwait(false); - - if (result.Result == SubmitAction.Cancel) - { - cancelled = true; - break; - } - else if (result.Result == SubmitAction.Submit) - { - break; - } - else if (result.Result == SubmitAction.PreviousHistory) - { - if (_history.MovePrevious(state) && !SetContent(state, _history.Current)) - { - continue; - } - } - else if (result.Result == SubmitAction.NextHistory) - { - if (_history.MoveNext() && !SetContent(state, _history.Current)) - { - continue; - } - } - else if (result.Result == SubmitAction.NewLine && MultiLine && state.IsLastLine) - { - // Add a new line - state.AddLine(); - - // Refresh - var builder = new StringBuilder(); - builder.Append("\u001b[?25l"); // Hide cursor - _renderer.AnsiBuilder.MoveDown(builder, state); - _renderer.AnsiBuilder.BuildRefresh(builder, state); - builder.Append("\u001b[?25h"); // Show cursor - _console.WriteAnsi(builder.ToString()); - } - else if (result.Result == SubmitAction.MoveUp && MultiLine) - { - MoveUp(state); - } - else if (result.Result == SubmitAction.MoveDown && MultiLine) - { - MoveDown(state); - } - else if (result.Result == SubmitAction.MoveFirst && MultiLine) - { - MoveFirst(state); - } - else if (result.Result == SubmitAction.MoveLast && MultiLine) - { - MoveLast(state); - } - } - - _renderer.RenderLine(state, cursorPosition: 0); - - // Move the cursor to the last line - while (state.MoveDown()) - { - _console.Cursor.MoveDown(); - } - - // Moving the cursor won't work here if we're at - // the bottom of the screen, so let's insert a new line. - _console.WriteLine(); - - // Add the current state to the history - if (!state.IsEmpty) - { - _history.Add(state.GetBuffers()); - } - - // If cancelled return initial text otherwise - // return text from current state - return cancelled ? Text : state.Text; - } - - private async Task<(LineBuffer Buffer, SubmitAction Result)> ReadLine( - LineEditorState state, - CancellationToken cancellationToken) - { - var provider = new DefaultServiceProvider(_provider); - provider.RegisterOptional(Completion); - var context = new LineEditorContext(state.Buffer, provider); - - while (true) - { - if (cancellationToken.IsCancellationRequested) - { - return (state.Buffer, SubmitAction.Cancel); - } - - // Get command - var command = default(LineEditorCommand); - var key = await _input.ReadKey(MultiLine, cancellationToken).ConfigureAwait(false); - if (key != null) - { - if (key.Value.KeyChar != 0 && !char.IsControl(key.Value.KeyChar)) - { - command = new InsertCommand(key.Value.KeyChar); - } - else - { - command = KeyBindings.GetCommand(key.Value.Key, key.Value.Modifiers); - } - } - - // Execute command - if (command != null) - { - context.Execute(command); - } - - // Time to exit? - if (context.Result != null) - { - return (state.Buffer, context.Result.Value); - } - - // Render the line - _renderer.RenderLine(state); - LineDecorationRenderer?.RenderLineDecoration(state.Buffer); - } - } - - private void MoveUp(LineEditorState state) - { - Move(state, () => - { - if (state.MoveUp()) - { - _console.Cursor.MoveUp(); - } - }); - } - - private void MoveDown(LineEditorState state) - { - Move(state, () => - { - if (state.MoveDown()) - { - _console.Cursor.MoveDown(); - } - }); - } - - private void MoveFirst(LineEditorState state) - { - Move(state, () => - { - while (state.MoveUp()) - { - _console.Cursor.MoveUp(); - } - }); - } - - private void MoveLast(LineEditorState state) - { - Move(state, () => - { - while (state.MoveDown()) - { - _console.Cursor.MoveDown(); - } - }); - } - - private void Move(LineEditorState state, Action action) - { - using (_console.HideCursor()) - { - if (state.LineCount > _console.Profile.Height) - { - // Get the current position - var position = state.Buffer.Position; - - // Refresh everything - action(); - _renderer.Refresh(state); - - // Re-render the current line at the correct position - state.Buffer.Move(position); - _renderer.RenderLine(state); - } - else - { - // Get the current position - var position = state.Buffer.Position; - - // Reset the line - _renderer.RenderLine(state, cursorPosition: 0); - action(); - - // Render the current line at the correct position - state.Buffer.Move(position); - _renderer.RenderLine(state); - } - } - } - - private bool SetContent(LineEditorState state, IList? lines) - { - // Nothing to set? - if (lines == null || lines.Count == 0) - { - return false; - } - - var builder = new StringBuilder(); - - // Clearing the current lines will - // move the cursor to the top. - _renderer.AnsiBuilder.BuildClear(builder, state); - - // Remove all lines - state.RemoveAllLines(); - - // Hide the cursor - builder.Append("\u001b[?25l"); - - // Add all the lines - foreach (var line in lines) - { - state.AddLine(line.Content); - } - - // Make room for all the lines - var first = true; - foreach (var line in lines) - { - var shouldAddNewLine = true; - if (first) - { - shouldAddNewLine = false; - first = false; - } - - if (shouldAddNewLine) - { - _renderer.AnsiBuilder.MoveDown(builder, state); - } - } - - _renderer.AnsiBuilder.BuildRefresh(builder, state); - - // Show the cursor again - builder.Append("\u001b[?25h"); - - // Flush - _console.WriteAnsi(builder.ToString()); - return true; - } - } -} +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Spectre.Console; +using Spectre.Console.Advanced; + +namespace RadLine +{ + public sealed class LineEditor : IHighlighterAccessor + { + private readonly IInputSource _source; + private readonly IServiceProvider? _provider; + private readonly IAnsiConsole _console; + private readonly LineEditorRenderer _renderer; + private readonly LineEditorHistory _history; + private readonly InputBuffer _input; + + public KeyBindings KeyBindings { get; } + public bool MultiLine { get; init; } = false; + public string Text { get; init; } = string.Empty; + + public ILineEditorPrompt Prompt { get; init; } = new LineEditorPrompt("[yellow]>[/]"); + public ITextCompletion? Completion { get; init; } + public IHighlighter? Highlighter { get; init; } + public ILineEditorHistory History => _history; + + public ILineDecorationRenderer? LineDecorationRenderer { get; init; } + + public LineEditor(IAnsiConsole? terminal = null, IInputSource? source = null, IServiceProvider? provider = null) + { + _console = terminal ?? AnsiConsole.Console; + _source = source ?? new DefaultInputSource(_console); + _provider = provider; + _renderer = new LineEditorRenderer(_console, this); + _history = new LineEditorHistory(); + _input = new InputBuffer(_source); + + KeyBindings = new KeyBindings(); + KeyBindings.AddDefault(); + } + + public static bool IsSupported(IAnsiConsole console) + { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + return + console.Profile.Out.IsTerminal && + console.Profile.Capabilities.Ansi && + console.Profile.Capabilities.Interactive; + } + + public async Task ReadLine(CancellationToken cancellationToken) + { + var cancelled = false; + var state = new LineEditorState(Prompt, Text); + + _history.Reset(); + _input.Initialize(KeyBindings); + _renderer.Refresh(state); + + while (true) + { + var result = await ReadLine(state, cancellationToken).ConfigureAwait(false); + + if (result.Result == SubmitAction.Cancel) + { + cancelled = true; + break; + } + else if (result.Result == SubmitAction.Submit) + { + break; + } + else if (result.Result == SubmitAction.PreviousHistory) + { + if (_history.MovePrevious(state) && !SetContent(state, _history.Current)) + { + continue; + } + } + else if (result.Result == SubmitAction.NextHistory) + { + if (_history.MoveNext() && !SetContent(state, _history.Current)) + { + continue; + } + } + else if (result.Result == SubmitAction.NewLine && MultiLine && state.IsLastLine) + { + // Add a new line + state.AddLine(); + + // Refresh + var builder = new StringBuilder(); + builder.Append("\u001b[?25l"); // Hide cursor + _renderer.AnsiBuilder.MoveDown(builder, state); + _renderer.AnsiBuilder.BuildRefresh(builder, state); + builder.Append("\u001b[?25h"); // Show cursor + _console.WriteAnsi(builder.ToString()); + } + else if (result.Result == SubmitAction.MoveUp && MultiLine) + { + MoveUp(state); + } + else if (result.Result == SubmitAction.MoveDown && MultiLine) + { + MoveDown(state); + } + else if (result.Result == SubmitAction.MoveFirst && MultiLine) + { + MoveFirst(state); + } + else if (result.Result == SubmitAction.MoveLast && MultiLine) + { + MoveLast(state); + } + } + + _renderer.RenderLine(state, cursorPosition: 0); + + // Move the cursor to the last line + while (state.MoveDown()) + { + _console.Cursor.MoveDown(); + } + + // Moving the cursor won't work here if we're at + // the bottom of the screen, so let's insert a new line. + _console.WriteLine(); + + // Add the current state to the history + if (!state.IsEmpty) + { + _history.Add(state.GetBuffers()); + } + + // If cancelled return initial text otherwise + // return text from current state + return cancelled ? Text : state.Text; + } + + private async Task<(LineBuffer Buffer, SubmitAction Result)> ReadLine( + LineEditorState state, + CancellationToken cancellationToken) + { + var provider = new DefaultServiceProvider(_provider); + provider.RegisterOptional(Completion); + var context = new LineEditorContext(state.Buffer, provider); + + while (true) + { + if (cancellationToken.IsCancellationRequested) + { + return (state.Buffer, SubmitAction.Cancel); + } + + // Get command + var command = default(LineEditorCommand); + var key = await _input.ReadKey(MultiLine, cancellationToken).ConfigureAwait(false); + if (key != null) + { + if (key.Value.KeyChar != 0 && !char.IsControl(key.Value.KeyChar)) + { + command = new InsertCommand(key.Value.KeyChar); + } + else + { + command = KeyBindings.GetCommand(key.Value.Key, key.Value.Modifiers); + } + } + + // Execute command + if (command != null) + { + context.Execute(command); + } + + // Time to exit? + if (context.Result != null) + { + return (state.Buffer, context.Result.Value); + } + + // Render the line + _renderer.RenderLine(state); + LineDecorationRenderer?.RenderLineDecoration(state.Buffer); + } + } + + private void MoveUp(LineEditorState state) + { + Move(state, () => + { + if (state.MoveUp()) + { + _console.Cursor.MoveUp(); + } + }); + } + + private void MoveDown(LineEditorState state) + { + Move(state, () => + { + if (state.MoveDown()) + { + _console.Cursor.MoveDown(); + } + }); + } + + private void MoveFirst(LineEditorState state) + { + Move(state, () => + { + while (state.MoveUp()) + { + _console.Cursor.MoveUp(); + } + }); + } + + private void MoveLast(LineEditorState state) + { + Move(state, () => + { + while (state.MoveDown()) + { + _console.Cursor.MoveDown(); + } + }); + } + + private void Move(LineEditorState state, Action action) + { + using (_console.HideCursor()) + { + if (state.LineCount > _console.Profile.Height) + { + // Get the current position + var position = state.Buffer.Position; + + // Refresh everything + action(); + _renderer.Refresh(state); + + // Re-render the current line at the correct position + state.Buffer.Move(position); + _renderer.RenderLine(state); + } + else + { + // Get the current position + var position = state.Buffer.Position; + + // Reset the line + _renderer.RenderLine(state, cursorPosition: 0); + action(); + + // Render the current line at the correct position + state.Buffer.Move(position); + _renderer.RenderLine(state); + } + } + } + + private bool SetContent(LineEditorState state, IList? lines) + { + // Nothing to set? + if (lines == null || lines.Count == 0) + { + return false; + } + + var builder = new StringBuilder(); + + // Clearing the current lines will + // move the cursor to the top. + _renderer.AnsiBuilder.BuildClear(builder, state); + + // Remove all lines + state.RemoveAllLines(); + + // Hide the cursor + builder.Append("\u001b[?25l"); + + // Add all the lines + foreach (var line in lines) + { + state.AddLine(line.Content); + } + + // Make room for all the lines + var first = true; + foreach (var line in lines) + { + var shouldAddNewLine = true; + if (first) + { + shouldAddNewLine = false; + first = false; + } + + if (shouldAddNewLine) + { + _renderer.AnsiBuilder.MoveDown(builder, state); + } + } + + _renderer.AnsiBuilder.BuildRefresh(builder, state); + + // Show the cursor again + builder.Append("\u001b[?25h"); + + // Flush + _console.WriteAnsi(builder.ToString()); + return true; + } + } +} diff --git a/src/RadLine/LineEditorContext.cs b/src/RadLine/LineEditorContext.cs index d64bd9b..84a2f6e 100644 --- a/src/RadLine/LineEditorContext.cs +++ b/src/RadLine/LineEditorContext.cs @@ -14,13 +14,13 @@ public sealed class LineEditorContext : IServiceProvider public LineEditorContext(LineBuffer buffer, IServiceProvider? provider = null) { _state = new Dictionary(StringComparer.OrdinalIgnoreCase); - _provider = provider; - Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); + _provider = provider; + Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); InitialText = buffer.Content; } public string? InitialText { get; } - + public object? GetService(Type serviceType) { if (_provider != null) From f9b792c0a046b29c369efa0345972b33f396bb6b Mon Sep 17 00:00:00 2001 From: Andre Vianna Date: Wed, 18 Sep 2024 12:08:03 -0400 Subject: [PATCH 8/8] Correct End of Line missing empty line --- src/RadLine.Tests/Commands/CancelCommandTests.cs | 2 +- src/RadLine.Tests/Commands/InsertCommandTests.cs | 2 +- src/RadLine/Commands/CancelCommand.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RadLine.Tests/Commands/CancelCommandTests.cs b/src/RadLine.Tests/Commands/CancelCommandTests.cs index 66b181f..beb9c8d 100644 --- a/src/RadLine.Tests/Commands/CancelCommandTests.cs +++ b/src/RadLine.Tests/Commands/CancelCommandTests.cs @@ -22,4 +22,4 @@ public void Should_Cancel_Input() buffer.Position.ShouldBe(3); } } -} \ No newline at end of file +} diff --git a/src/RadLine.Tests/Commands/InsertCommandTests.cs b/src/RadLine.Tests/Commands/InsertCommandTests.cs index cec7949..c417e76 100644 --- a/src/RadLine.Tests/Commands/InsertCommandTests.cs +++ b/src/RadLine.Tests/Commands/InsertCommandTests.cs @@ -21,4 +21,4 @@ public void Should_Insert_Text_At_Position() buffer.Position.ShouldBe(4); } } -} \ No newline at end of file +} diff --git a/src/RadLine/Commands/CancelCommand.cs b/src/RadLine/Commands/CancelCommand.cs index fa1a1e5..284f6b4 100644 --- a/src/RadLine/Commands/CancelCommand.cs +++ b/src/RadLine/Commands/CancelCommand.cs @@ -7,4 +7,4 @@ public override void Execute(LineEditorContext context) context.Buffer.Reset(); context.Submit(SubmitAction.Cancel); } -} \ No newline at end of file +}