diff --git a/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs b/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs index cd7691c6a..e7ebfa27e 100644 --- a/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs +++ b/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs @@ -84,7 +84,7 @@ public IReadOnlyList CollectSuggestions(IShellState shellState) shellState = shellState ?? throw new ArgumentNullException(nameof(shellState)); string line = shellState.InputManager.GetCurrentBuffer(); - TParseResult parseResult = _parser.Parse(line, shellState.ConsoleManager.CaretPosition); + TParseResult parseResult = _parser.Parse(line, shellState.InputManager.CaretPosition); HashSet suggestions = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (ICommand command in _commands) @@ -139,7 +139,7 @@ public async Task ExecuteCommandAsync(IShellState shellState, CancellationToken private async Task ExecuteCommandInternalAsync(IShellState shellState, CancellationToken cancellationToken) { string line = shellState.InputManager.GetCurrentBuffer(); - TParseResult parseResult = _parser.Parse(line, shellState.ConsoleManager.CaretPosition); + TParseResult parseResult = _parser.Parse(line, shellState.InputManager.CaretPosition); if (!string.IsNullOrWhiteSpace(parseResult.CommandText)) { @@ -171,7 +171,7 @@ public void OnReady(IShellState shellState) if (!_isReady && !shellState.IsExiting) { _onReady(shellState); - shellState.ConsoleManager.ResetCommandStart(); + shellState.InputManager.ResetInput(); _isReady = true; } } diff --git a/src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs b/src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs index 5e50ca603..05a858f1a 100644 --- a/src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs +++ b/src/Microsoft.Repl/ConsoleHandling/ConsoleManager.cs @@ -14,17 +14,6 @@ public class ConsoleManager : IConsoleManager public Point Caret => new Point(Console.CursorLeft, Console.CursorTop); - public Point CommandStart - { - get - { - Point c = Caret; - return new Point(c.X - CaretPosition % Console.BufferWidth, c.Y - CaretPosition / Console.BufferWidth); - } - } - - public int CaretPosition { get; private set; } - public bool IsKeyAvailable => Console.KeyAvailable; public bool IsCaretVisible @@ -35,95 +24,88 @@ public bool IsCaretVisible public ConsoleManager() { - Error = new Writable(CaretUpdateScope, Reporter.Error); + Error = new Writable(Reporter.Error); Console.CancelKeyPress += OnCancelKeyPress; } public void Clear() { - using (CaretUpdateScope()) - { - Console.Clear(); - ResetCommandStart(); - } + Console.Clear(); } public void MoveCaret(int positions) { - using (CaretUpdateScope()) + if (positions == 0) { - if (positions == 0) - { - return; - } + return; + } - int bufferWidth = Console.BufferWidth; - int cursorTop = Console.CursorTop; - int cursorLeft = Console.CursorLeft; + int bufferWidth = Console.BufferWidth; + int cursorTop = Console.CursorTop; + int cursorLeft = Console.CursorLeft; - while (positions < 0 && CaretPosition > 0) + while (positions < 0) + { + if (-positions > bufferWidth) { - if (-positions > bufferWidth) + if (cursorTop == 0) { - if (cursorTop == 0) - { - cursorLeft = 0; - positions = 0; - } - else - { - positions += bufferWidth; - --cursorTop; - } + cursorLeft = 0; + positions = 0; } else { - int remaining = cursorLeft + positions; - - if (remaining >= 0) - { - cursorLeft = remaining; - } - else if (cursorTop == 0) - { - cursorLeft = 0; - } - else - { - --cursorTop; - cursorLeft = bufferWidth + remaining; - } + positions += bufferWidth; + --cursorTop; + } + } + else + { + int remaining = cursorLeft + positions; - positions = 0; + if (remaining >= 0) + { + cursorLeft = remaining; } + else if (cursorTop == 0) + { + cursorLeft = 0; + } + else + { + --cursorTop; + cursorLeft = bufferWidth + remaining; + } + + positions = 0; } + } - while (positions > 0) + while (positions > 0) + { + if (positions > bufferWidth) + { + positions -= bufferWidth; + ++cursorTop; + } + else { - if (positions > bufferWidth) + int spaceLeftOnLine = bufferWidth - cursorLeft - 1; + if (positions > spaceLeftOnLine) { - positions -= bufferWidth; ++cursorTop; + cursorLeft = positions - spaceLeftOnLine - 1; } else { - int spaceLeftOnLine = bufferWidth - cursorLeft - 1; - if (positions > spaceLeftOnLine) - { - ++cursorTop; - cursorLeft = positions - spaceLeftOnLine - 1; - } - else - { - cursorLeft += positions; - } - - positions = 0; + cursorLeft += positions; } - } - Console.SetCursorPosition(cursorLeft, cursorTop); + positions = 0; + } } + + Console.SetCursorPosition(cursorLeft, cursorTop); } public ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) @@ -143,33 +125,19 @@ public ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) } } - public void ResetCommandStart() - { - CaretPosition = 0; - } - public void Write(char c) { - using (CaretUpdateScope()) - { - Reporter.Output.Write(c); - } + Reporter.Output.Write(c); } public void Write(string s) { - using (CaretUpdateScope()) - { - Reporter.Output.Write(s); - } + Reporter.Output.Write(s); } public void WriteLine() { - using (CaretUpdateScope()) - { - Reporter.Output.WriteLine(); - } + Reporter.Output.WriteLine(); } public void WriteLine(string s) @@ -179,10 +147,7 @@ public void WriteLine(string s) return; } - using (CaretUpdateScope()) - { - Reporter.Output.WriteLine(s); - } + Reporter.Output.WriteLine(s); } public IDisposable AddBreakHandler(Action onBreak) @@ -192,18 +157,6 @@ public IDisposable AddBreakHandler(Action onBreak) return result; } - private IDisposable CaretUpdateScope() - { - Point currentCaret = Caret; - return new Disposable(() => - { - Point c = Caret; - int y = c.Y - currentCaret.Y; - int x = c.X - currentCaret.X; - CaretPosition += y * Console.BufferWidth + x; - }); - } - private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; diff --git a/src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs b/src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs index 6bf85a91e..b72cc12c4 100644 --- a/src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs +++ b/src/Microsoft.Repl/ConsoleHandling/IConsoleManager.cs @@ -10,10 +10,6 @@ public interface IConsoleManager : IWritable { Point Caret { get; } - Point CommandStart { get; } - - int CaretPosition { get; } - #pragma warning disable CA1716 // Identifiers should not match keywords IWritable Error { get; } #pragma warning restore CA1716 // Identifiers should not match keywords @@ -26,8 +22,6 @@ public interface IConsoleManager : IWritable ConsoleKeyInfo ReadKey(CancellationToken cancellationToken); - void ResetCommandStart(); - IDisposable AddBreakHandler(Action onBreak); bool AllowOutputRedirection { get; } diff --git a/src/Microsoft.Repl/ConsoleHandling/Writable.cs b/src/Microsoft.Repl/ConsoleHandling/Writable.cs index 1b32e939f..d5ee7fd1e 100644 --- a/src/Microsoft.Repl/ConsoleHandling/Writable.cs +++ b/src/Microsoft.Repl/ConsoleHandling/Writable.cs @@ -1,18 +1,14 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; - namespace Microsoft.Repl.ConsoleHandling { internal class Writable : IWritable { - private readonly Func _caretUpdater; private readonly Reporter _reporter; - public Writable(Func caretUpdater, Reporter reporter) + public Writable(Reporter reporter) { - _caretUpdater = caretUpdater; _reporter = reporter; } @@ -24,34 +20,22 @@ public bool IsCaretVisible public void Write(char c) { - using (_caretUpdater()) - { - _reporter.Write(c); - } + _reporter.Write(c); } public void Write(string s) { - using (_caretUpdater()) - { - _reporter.Write(s); - } + _reporter.Write(s); } public void WriteLine() { - using (_caretUpdater()) - { - _reporter.WriteLine(); - } + _reporter.WriteLine(); } public void WriteLine(string s) { - using (_caretUpdater()) - { - _reporter.WriteLine(s); - } + _reporter.WriteLine(s); } } } diff --git a/src/Microsoft.Repl/IShellState.cs b/src/Microsoft.Repl/IShellState.cs index 1941fa65f..d1d3a20a9 100644 --- a/src/Microsoft.Repl/IShellState.cs +++ b/src/Microsoft.Repl/IShellState.cs @@ -21,5 +21,7 @@ public interface IShellState ISuggestionManager SuggestionManager { get; } bool IsExiting { get; set; } + + void MoveCarets(int positions); } } diff --git a/src/Microsoft.Repl/Input/IInputManager.cs b/src/Microsoft.Repl/Input/IInputManager.cs index e8e98b2d5..98db2ad78 100644 --- a/src/Microsoft.Repl/Input/IInputManager.cs +++ b/src/Microsoft.Repl/Input/IInputManager.cs @@ -11,6 +11,8 @@ public interface IInputManager { bool IsOverwriteMode { get; set; } + int CaretPosition { get; } + IInputManager RegisterKeyHandler(ConsoleKey key, AsyncKeyPressHandler handler); IInputManager RegisterKeyHandler(ConsoleKey key, ConsoleModifiers modifiers, AsyncKeyPressHandler handler); @@ -28,5 +30,7 @@ public interface IInputManager void RemoveCurrentCharacter(IShellState state); void Clear(IShellState state); + + void MoveCaret(int positions); } } diff --git a/src/Microsoft.Repl/Input/InputManager.cs b/src/Microsoft.Repl/Input/InputManager.cs index d70695afa..c8bcff5c3 100644 --- a/src/Microsoft.Repl/Input/InputManager.cs +++ b/src/Microsoft.Repl/Input/InputManager.cs @@ -18,6 +18,24 @@ public class InputManager : IInputManager public bool IsOverwriteMode { get; set; } + public int CaretPosition { get; private set; } + + public void MoveCaret(int positions) + { + if (CaretPosition + positions < 0) + { + CaretPosition = 0; + } + else if (CaretPosition + positions > _inputBuffer.Count) + { + CaretPosition = _inputBuffer.Count; + } + else + { + CaretPosition += positions; + } + } + public void Clear(IShellState state) { SetInput(state, string.Empty); @@ -70,18 +88,15 @@ public void RemoveCurrentCharacter(IShellState state) { state = state ?? throw new ArgumentNullException(nameof(state)); - int caret = state.ConsoleManager.CaretPosition; - - if (caret == _inputBuffer.Count) + if (CaretPosition == _inputBuffer.Count) { return; } List update = _inputBuffer.ToList(); - update.RemoveAt(caret); + update.RemoveAt(CaretPosition); state.ConsoleManager.IsCaretVisible = false; SetInput(state, update); - state.ConsoleManager.MoveCaret(caret - state.ConsoleManager.CaretPosition); state.ConsoleManager.IsCaretVisible = true; } @@ -89,17 +104,15 @@ public void RemovePreviousCharacter(IShellState state) { state = state ?? throw new ArgumentNullException(nameof(state)); - int caret = state.ConsoleManager.CaretPosition; - if (caret == 0) + if (CaretPosition == 0) { return; } List update = _inputBuffer.ToList(); - update.RemoveAt(caret - 1); + update.RemoveAt(CaretPosition - 1); state.ConsoleManager.IsCaretVisible = false; - SetInput(state, update, false); - state.ConsoleManager.MoveCaret(caret - state.ConsoleManager.CaretPosition - 1); + SetInput(state, update); state.ConsoleManager.IsCaretVisible = true; } @@ -115,6 +128,7 @@ public void SetInput(IShellState state, string input) public void ResetInput() { _inputBuffer.Clear(); + CaretPosition = 0; } private string _ttyState; @@ -163,7 +177,7 @@ private void RestoreTtyState() } } - private void SetInput(IShellState state, IReadOnlyList input, bool moveCaret = true) + private void SetInput(IShellState state, IReadOnlyList input) { bool oldCaretVisibility = state.ConsoleManager.IsCaretVisible; state.ConsoleManager.IsCaretVisible = false; @@ -173,7 +187,7 @@ private void SetInput(IShellState state, IReadOnlyList input, bool moveCar { } - state.ConsoleManager.MoveCaret(-state.ConsoleManager.CaretPosition + lastCommonPosition); + state.ConsoleManager.MoveCaret(-CaretPosition + lastCommonPosition); string str = new string(input.Skip(lastCommonPosition).ToArray()); int trailing = _inputBuffer.Count - input.Count; @@ -184,13 +198,15 @@ private void SetInput(IShellState state, IReadOnlyList input, bool moveCar state.ConsoleManager.Write(str); - if (trailing > 0 && moveCaret) + _inputBuffer.Clear(); + _inputBuffer.AddRange(input); + + if (trailing > 0) { state.ConsoleManager.MoveCaret(-trailing); } - _inputBuffer.Clear(); - _inputBuffer.AddRange(input); + CaretPosition = _inputBuffer.Count; if (oldCaretVisibility) { @@ -234,11 +250,13 @@ public async Task StartAsync(IShellState state, CancellationToken cancellationTo //TODO: Verify on a mac whether these are still needed if (keyPress.Key == ConsoleKey.A) { - state.ConsoleManager.MoveCaret(-state.ConsoleManager.CaretPosition); + state.ConsoleManager.MoveCaret(-CaretPosition); + CaretPosition = 0; } else if (keyPress.Key == ConsoleKey.E) { - state.ConsoleManager.MoveCaret(_inputBuffer.Count - state.ConsoleManager.CaretPosition); + state.ConsoleManager.MoveCaret(_inputBuffer.Count - CaretPosition); + CaretPosition = _inputBuffer.Count; } } //TODO: Register these like regular commands @@ -252,7 +270,7 @@ public async Task StartAsync(IShellState state, CancellationToken cancellationTo //Move back a word if (keyPress.Key == ConsoleKey.B) { - int i = state.ConsoleManager.CaretPosition - 1; + int i = CaretPosition - 1; if (i < 0) { @@ -272,13 +290,14 @@ public async Task StartAsync(IShellState state, CancellationToken cancellationTo if (i > -1) { - state.ConsoleManager.MoveCaret(i - state.ConsoleManager.CaretPosition); + state.ConsoleManager.MoveCaret(i - CaretPosition); + CaretPosition = i; } } //Move forward a word else if (keyPress.Key == ConsoleKey.F) { - int i = state.ConsoleManager.CaretPosition + 1; + int i = CaretPosition + 1; if (i >= _inputBuffer.Count) { @@ -296,7 +315,8 @@ public async Task StartAsync(IShellState state, CancellationToken cancellationTo --i; } - state.ConsoleManager.MoveCaret(i - state.ConsoleManager.CaretPosition); + state.ConsoleManager.MoveCaret(i - CaretPosition); + CaretPosition = i; } } else @@ -319,25 +339,27 @@ public async Task StartAsync(IShellState state, CancellationToken cancellationTo continue; } - if (state.ConsoleManager.CaretPosition == _inputBuffer.Count) + if (CaretPosition == _inputBuffer.Count) { _inputBuffer.Add(keyPress.KeyChar); state.ConsoleManager.Write(keyPress.KeyChar); + MoveCaret(1); } else if (IsOverwriteMode) { - _inputBuffer[state.ConsoleManager.CaretPosition] = keyPress.KeyChar; + _inputBuffer[CaretPosition] = keyPress.KeyChar; state.ConsoleManager.Write(keyPress.KeyChar); + MoveCaret(1); } else { state.ConsoleManager.IsCaretVisible = false; - _inputBuffer.Insert(state.ConsoleManager.CaretPosition, keyPress.KeyChar); - int currentCaretPosition = state.ConsoleManager.CaretPosition; - string s = new string(_inputBuffer.ToArray(), state.ConsoleManager.CaretPosition, _inputBuffer.Count - state.ConsoleManager.CaretPosition); + _inputBuffer.Insert(CaretPosition, keyPress.KeyChar); + int currentCaretPosition = CaretPosition; + string s = new string(_inputBuffer.ToArray(), CaretPosition, _inputBuffer.Count - CaretPosition); state.ConsoleManager.Write(s); - state.ConsoleManager.MoveCaret(currentCaretPosition - state.ConsoleManager.CaretPosition + 1); state.ConsoleManager.IsCaretVisible = true; + MoveCaret(1); } } } @@ -352,7 +374,7 @@ private void FlushInput(IShellState state, ref List presses) { string str = new string(presses.Select(x => x.KeyChar).ToArray()); - if (state.ConsoleManager.CaretPosition == _inputBuffer.Count) + if (CaretPosition == _inputBuffer.Count) { _inputBuffer.AddRange(str); state.ConsoleManager.Write(str); @@ -361,9 +383,9 @@ private void FlushInput(IShellState state, ref List presses) { for (int i = 0; i < str.Length; ++i) { - if (state.ConsoleManager.CaretPosition + i < _inputBuffer.Count) + if (CaretPosition + i < _inputBuffer.Count) { - _inputBuffer[state.ConsoleManager.CaretPosition + i] = str[i]; + _inputBuffer[CaretPosition + i] = str[i]; } else { @@ -377,13 +399,14 @@ private void FlushInput(IShellState state, ref List presses) else { state.ConsoleManager.IsCaretVisible = false; - _inputBuffer.InsertRange(state.ConsoleManager.CaretPosition, str); - int currentCaretPosition = state.ConsoleManager.CaretPosition; - string s = new string(_inputBuffer.ToArray(), state.ConsoleManager.CaretPosition, _inputBuffer.Count - state.ConsoleManager.CaretPosition); + _inputBuffer.InsertRange(CaretPosition, str); + int currentCaretPosition = CaretPosition; + string s = new string(_inputBuffer.ToArray(), CaretPosition, _inputBuffer.Count - CaretPosition); state.ConsoleManager.Write(s); - state.ConsoleManager.MoveCaret(currentCaretPosition - state.ConsoleManager.CaretPosition + str.Length); + state.ConsoleManager.MoveCaret(currentCaretPosition - CaretPosition + str.Length); state.ConsoleManager.IsCaretVisible = true; } + MoveCaret(str.Length); presses = null; } diff --git a/src/Microsoft.Repl/Input/KeyHandlers.cs b/src/Microsoft.Repl/Input/KeyHandlers.cs index fdd403105..fba79d4d7 100644 --- a/src/Microsoft.Repl/Input/KeyHandlers.cs +++ b/src/Microsoft.Repl/Input/KeyHandlers.cs @@ -89,7 +89,7 @@ public static void RegisterDefaultKeyHandlers(IInputManager inputManager) private static Task End(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken) { - state.ConsoleManager.MoveCaret(state.InputManager.GetCurrentBuffer().Length - state.ConsoleManager.CaretPosition); + state.MoveCarets(state.InputManager.GetCurrentBuffer().Length - state.InputManager.CaretPosition); return Task.CompletedTask; } @@ -97,7 +97,7 @@ public static Task Home(ConsoleKeyInfo keyInfo, IShellState state, CancellationT { state = state ?? throw new ArgumentNullException(nameof(state)); - state.ConsoleManager.MoveCaret(-state.ConsoleManager.CaretPosition); + state.MoveCarets(-state.InputManager.CaretPosition); return Task.CompletedTask; } @@ -105,16 +105,16 @@ public static Task LeftArrow(ConsoleKeyInfo keyInfo, IShellState state, Cancella { state = state ?? throw new ArgumentNullException(nameof(state)); - if (state.ConsoleManager.CaretPosition > 0) + if (state.InputManager.CaretPosition > 0) { if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) { - state.ConsoleManager.MoveCaret(-1); + state.MoveCarets(-1); } else { string line = state.InputManager.GetCurrentBuffer(); - ICoreParseResult parseResult = state.CommandDispatcher.Parser.Parse(line, state.ConsoleManager.CaretPosition); + ICoreParseResult parseResult = state.CommandDispatcher.Parser.Parse(line, state.InputManager.CaretPosition); int targetSection = parseResult.SelectedSection - (parseResult.CaretPositionWithinSelectedSection > 0 ? 0 : 1); if (targetSection < 0) @@ -123,7 +123,7 @@ public static Task LeftArrow(ConsoleKeyInfo keyInfo, IShellState state, Cancella } int desiredPosition = parseResult.SectionStartLookup[targetSection]; - state.ConsoleManager.MoveCaret(desiredPosition - state.ConsoleManager.CaretPosition); + state.MoveCarets(desiredPosition - state.InputManager.CaretPosition); } } @@ -136,25 +136,25 @@ public static Task RightArrow(ConsoleKeyInfo keyInfo, IShellState state, Cancell string line = state.InputManager.GetCurrentBuffer(); - if (state.ConsoleManager.CaretPosition < line.Length) + if (state.InputManager.CaretPosition < line.Length) { if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control)) { - state.ConsoleManager.MoveCaret(1); + state.MoveCarets(1); } else { - ICoreParseResult parseResult = state.CommandDispatcher.Parser.Parse(line, state.ConsoleManager.CaretPosition); + ICoreParseResult parseResult = state.CommandDispatcher.Parser.Parse(line, state.InputManager.CaretPosition); int targetSection = parseResult.SelectedSection + 1; if (targetSection >= parseResult.Sections.Count) { - state.ConsoleManager.MoveCaret(line.Length - state.ConsoleManager.CaretPosition); + state.MoveCarets(line.Length - state.InputManager.CaretPosition); } else { int desiredPosition = parseResult.SectionStartLookup[targetSection]; - state.ConsoleManager.MoveCaret(desiredPosition - state.ConsoleManager.CaretPosition); + state.MoveCarets(desiredPosition - state.InputManager.CaretPosition); } } } diff --git a/src/Microsoft.Repl/Scripting/ScriptExecutor.cs b/src/Microsoft.Repl/Scripting/ScriptExecutor.cs index 71b5db53a..bd71c8d12 100644 --- a/src/Microsoft.Repl/Scripting/ScriptExecutor.cs +++ b/src/Microsoft.Repl/Scripting/ScriptExecutor.cs @@ -45,7 +45,6 @@ public async Task ExecuteScriptAsync(IShellState shellState, IEnumerable } dispatcher.OnReady(shellState); - shellState.ConsoleManager.ResetCommandStart(); shellState.InputManager.SetInput(shellState, commandText); await dispatcher.ExecuteCommandAsync(shellState, cancellationToken).ConfigureAwait(false); } diff --git a/src/Microsoft.Repl/ShellState.cs b/src/Microsoft.Repl/ShellState.cs index cb94bc63a..c48d803de 100644 --- a/src/Microsoft.Repl/ShellState.cs +++ b/src/Microsoft.Repl/ShellState.cs @@ -30,5 +30,11 @@ public ShellState(ICommandDispatcher commandDispatcher, ISuggestionManager sugge public bool IsExiting { get; set; } public ISuggestionManager SuggestionManager { get; } + + public void MoveCarets(int positions) + { + ConsoleManager.MoveCaret(positions); + InputManager.MoveCaret(positions); + } } } diff --git a/src/Microsoft.Repl/Suggestions/SuggestionManager.cs b/src/Microsoft.Repl/Suggestions/SuggestionManager.cs index d1bbdad46..6096f9ff1 100644 --- a/src/Microsoft.Repl/Suggestions/SuggestionManager.cs +++ b/src/Microsoft.Repl/Suggestions/SuggestionManager.cs @@ -18,7 +18,7 @@ public void NextSuggestion(IShellState shellState) shellState = shellState ?? throw new ArgumentNullException(nameof(shellState)); string line = shellState.InputManager.GetCurrentBuffer(); - ICoreParseResult parseResult = shellState.CommandDispatcher.Parser.Parse(line, shellState.ConsoleManager.CaretPosition); + ICoreParseResult parseResult = shellState.CommandDispatcher.Parser.Parse(line, shellState.InputManager.CaretPosition); string currentSuggestion; //Check to see if we're continuing before querying for suggestions again @@ -61,7 +61,7 @@ public void PreviousSuggestion(IShellState shellState) shellState = shellState ?? throw new ArgumentNullException(nameof(shellState)); string line = shellState.InputManager.GetCurrentBuffer(); - ICoreParseResult parseResult = shellState.CommandDispatcher.Parser.Parse(line, shellState.ConsoleManager.CaretPosition); + ICoreParseResult parseResult = shellState.CommandDispatcher.Parser.Parse(line, shellState.InputManager.CaretPosition); string currentSuggestion; //Check to see if we're continuing before querying for suggestions again diff --git a/test/Microsoft.HttpRepl.Fakes/LoggingConsoleManagerDecorator.cs b/test/Microsoft.HttpRepl.Fakes/LoggingConsoleManagerDecorator.cs index f81d71d51..e2ee72ae1 100644 --- a/test/Microsoft.HttpRepl.Fakes/LoggingConsoleManagerDecorator.cs +++ b/test/Microsoft.HttpRepl.Fakes/LoggingConsoleManagerDecorator.cs @@ -25,10 +25,6 @@ public LoggingConsoleManagerDecorator(IConsoleManager console) #region IConsoleManager public Point Caret => _baseConsole.Caret; - public Point CommandStart => _baseConsole.CommandStart; - - public int CaretPosition => _baseConsole.CaretPosition; - public IWritable Error => _baseConsole.Error; public bool IsKeyAvailable => _baseConsole.IsKeyAvailable; @@ -58,11 +54,6 @@ public ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) return _baseConsole.ReadKey(cancellationToken); } - public void ResetCommandStart() - { - _baseConsole.ResetCommandStart(); - } - public void Write(char c) { _log.Append(c); diff --git a/test/Microsoft.HttpRepl.Fakes/MockConsoleManager.cs b/test/Microsoft.HttpRepl.Fakes/MockConsoleManager.cs index 75bd0ab0a..0e8440e53 100644 --- a/test/Microsoft.HttpRepl.Fakes/MockConsoleManager.cs +++ b/test/Microsoft.HttpRepl.Fakes/MockConsoleManager.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Text; using System.Threading; using Microsoft.Repl.ConsoleHandling; @@ -11,13 +12,14 @@ namespace Microsoft.HttpRepl.Fakes public class MockConsoleManager : IConsoleManager { private CancellationTokenSource _cancellationTokenSource; - private ConsoleKeyInfo _consoleKeyInfo; + private List _consoleKeyInfo; + private int _nextKeyIndex; private StringBuilder _outputTracking = new StringBuilder(); - public MockConsoleManager(ConsoleKeyInfo consoleKeyInfo, CancellationTokenSource cancellationTokenSource) + public MockConsoleManager(IEnumerable consoleKeyInfo, CancellationTokenSource cancellationTokenSource) { _cancellationTokenSource = cancellationTokenSource; - _consoleKeyInfo = consoleKeyInfo; + _consoleKeyInfo = new List(consoleKeyInfo); } public MockConsoleManager() @@ -35,7 +37,7 @@ public MockConsoleManager() public IWritable Error => new MockWritable(); - public bool IsKeyAvailable => throw new NotImplementedException(); + public bool IsKeyAvailable => _nextKeyIndex < _consoleKeyInfo.Count; public void Clear() { @@ -48,8 +50,13 @@ public void MoveCaret(int offset) public ConsoleKeyInfo ReadKey(CancellationToken cancellationToken) { - _cancellationTokenSource.Cancel(); - return _consoleKeyInfo; + ConsoleKeyInfo currentKeyInfo = _nextKeyIndex < _consoleKeyInfo.Count ? _consoleKeyInfo[_nextKeyIndex] : new ConsoleKeyInfo(); + _nextKeyIndex++; + if (_nextKeyIndex >= _consoleKeyInfo.Count) + { + _cancellationTokenSource.Cancel(); + } + return currentKeyInfo; } public void ResetCommandStart() diff --git a/test/Microsoft.HttpRepl.Fakes/MockInputManager.cs b/test/Microsoft.HttpRepl.Fakes/MockInputManager.cs index 8fce785c0..9500685da 100644 --- a/test/Microsoft.HttpRepl.Fakes/MockInputManager.cs +++ b/test/Microsoft.HttpRepl.Fakes/MockInputManager.cs @@ -13,11 +13,18 @@ public class MockInputManager : IInputManager { private string _inputBuffer; + public int CaretPosition { get; private set; } + public MockInputManager(string inputBuffer) { _inputBuffer = inputBuffer; } + public void MoveCaret(int positions) + { + + } + public bool IsOverwriteMode { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public IInputManager RegisterKeyHandler(ConsoleKey key, AsyncKeyPressHandler handler) diff --git a/test/Microsoft.HttpRepl.Fakes/MockedShellState.cs b/test/Microsoft.HttpRepl.Fakes/MockedShellState.cs index 97b2e7546..18012d730 100644 --- a/test/Microsoft.HttpRepl.Fakes/MockedShellState.cs +++ b/test/Microsoft.HttpRepl.Fakes/MockedShellState.cs @@ -46,5 +46,11 @@ public MockedShellState() public ISuggestionManager SuggestionManager => _shellState.SuggestionManager; public bool IsExiting { get => _shellState.IsExiting; set => _shellState.IsExiting = value; } + + public void MoveCarets(int positions) + { + ConsoleManager?.MoveCaret(positions); + InputManager?.MoveCaret(positions); + } } } diff --git a/test/Microsoft.Repl.Tests/ShellTests.cs b/test/Microsoft.Repl.Tests/ShellTests.cs index 28cfcbb48..b23ac9e0a 100644 --- a/test/Microsoft.Repl.Tests/ShellTests.cs +++ b/test/Microsoft.Repl.Tests/ShellTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.HttpRepl.Fakes; @@ -28,6 +29,28 @@ public async Task RunAsync_WithUpArrowKeyPress_UpdatesCurrentBufferWithPreviousC // Verify the input buffer has previous command after the UpArrow key press event Assert.Equal(previousCommand, shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(previousCommand.Length, shell.ShellState.InputManager.CaretPosition); + } + + [Fact] + public async Task RunAsync_WithUpArrowKeyPressWithLongPreviousCommand_UpdatesCurrentBufferWithPreviousCommand() + { + string previousCommand = "connect \"https://localhost:44366/\" --base \"https://localhost:44366/api/v2/\" --openapi \"https://localhost:44366/openapi/v2/openapi.yaml\""; + ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', + key: ConsoleKey.UpArrow, + shift: false, + alt: false, + control: false); + Shell shell = CreateShell(consoleKeyInfo, + previousCommand: previousCommand, + nextCommand: null, + out CancellationTokenSource cancellationTokenSource); + + await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); + + // Verify the input buffer has previous command after the UpArrow key press event + Assert.Equal(previousCommand, shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(previousCommand.Length, shell.ShellState.InputManager.CaretPosition); } [Fact] @@ -46,11 +69,13 @@ public async Task RunAsync_WithUpArrowKeyPress_VerifyInputBufferContentsBeforeAn // Verify the input buffer is empty before the UpArrow key press event Assert.Equal(string.Empty, shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(0, shell.ShellState.InputManager.CaretPosition); await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); // Verify the input buffer has previous command after the UpArrow key press event Assert.Equal(previousCommand, shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(previousCommand.Length, shell.ShellState.InputManager.CaretPosition); } [Fact] @@ -71,29 +96,28 @@ public async Task RunAsync_WithDownArrowKeyPress_UpdatesCurrentBufferWithNextCom // Verify the input buffer has next command after the DownArrow key press event Assert.Equal(nextCommand, shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(nextCommand.Length, shell.ShellState.InputManager.CaretPosition); } [Fact] public async Task RunAsync_WithDeleteKeyPress_DeletesCurrentCharacterInTheInputBuffer() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.Delete, - shift: false, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + ConsoleKeyInfo[] keys = new[] + { + GetConsoleKeyInfo('g'), + GetConsoleKeyInfo('e'), + GetConsoleKeyInfo('t'), + GetConsoleKeyInfo(ConsoleKey.LeftArrow), + GetConsoleKeyInfo(ConsoleKey.Delete) + }; + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferTextBeforeKeyPress = "get"; string inputBufferTextAfterKeyPress = "ge"; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferTextBeforeKeyPress); - - shellState.ConsoleManager.MoveCaret(2); - await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); // Verify the input buffer contents after Delete key press event @@ -103,24 +127,22 @@ public async Task RunAsync_WithDeleteKeyPress_DeletesCurrentCharacterInTheInputB [Fact] public async Task RunAsync_WithBackspaceKeyPress_DeletesPreviousCharacterInTheInputBuffer() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.Backspace, - shift: false, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + ConsoleKeyInfo[] keys = new[] + { + GetConsoleKeyInfo('g'), + GetConsoleKeyInfo('e'), + GetConsoleKeyInfo('t'), + GetConsoleKeyInfo(ConsoleKey.LeftArrow), + GetConsoleKeyInfo(ConsoleKey.Backspace) + }; + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferTextBeforeKeyPress = "get"; string inputBufferTextAfterKeyPress = "gt"; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferTextBeforeKeyPress); - - shellState.ConsoleManager.MoveCaret(2); - await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); // Verify the input buffer contents after Backspace key press event @@ -130,22 +152,20 @@ public async Task RunAsync_WithBackspaceKeyPress_DeletesPreviousCharacterInTheIn [Fact] public async Task RunAsync_WithEscapeKeyPress_UpdatesInputBufferWithEmptyString() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.Escape, - shift: false, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + ConsoleKeyInfo[] keys = new[] +{ + GetConsoleKeyInfo('g'), + GetConsoleKeyInfo('e'), + GetConsoleKeyInfo('t'), + GetConsoleKeyInfo(ConsoleKey.Escape), + }; + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferTextBeforeKeyPress = "get"; string inputBufferTextAfterKeyPress = string.Empty; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferTextBeforeKeyPress); - await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); // Verify the input buffer contents after Escape key press event @@ -155,22 +175,21 @@ public async Task RunAsync_WithEscapeKeyPress_UpdatesInputBufferWithEmptyString( [Fact] public async Task RunAsync_WithCtrlUKeyPress_UpdatesInputBufferWithEmptyString() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.U, - shift: false, - alt: false, - control: true); - Shell shell = CreateShell(consoleKeyInfo, + ConsoleKeyInfo[] keys = new[] + { + GetConsoleKeyInfo('g'), + GetConsoleKeyInfo('e'), + GetConsoleKeyInfo('t'), + new(keyChar: '\0', key: ConsoleKey.U, shift: false, alt: false, control: true), + }; + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferTextBeforeKeyPress = "get"; string inputBufferTextAfterKeyPress = string.Empty; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferTextBeforeKeyPress); - await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); // Verify the input buffer contents after Ctrl + U key press event @@ -199,21 +218,21 @@ public async Task RunAsync_WithInsertKeyPress_FlipsIsOverwriteModeInInputManager [Fact] public async Task RunAsync_WithUnhandledKeyPress_DoesNothing() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.F1, - shift: false, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + ConsoleKeyInfo[] keys = new[] + { + GetConsoleKeyInfo('g'), + GetConsoleKeyInfo('e'), + GetConsoleKeyInfo('t'), + GetConsoleKeyInfo(ConsoleKey.F1), + }; + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); string inputBufferText = "get"; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferText); - await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); // Verify the input buffer contents after F1 key press event @@ -223,23 +242,20 @@ public async Task RunAsync_WithUnhandledKeyPress_DoesNothing() [Fact] public async Task RunAsync_WithTabKeyPress_UpdatesInputBufferWithFirstEntryFromSuggestionList() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.Tab, - shift: false, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + ConsoleKeyInfo[] keys = new[] + { + GetConsoleKeyInfo('c'), + GetConsoleKeyInfo(ConsoleKey.Tab), + }; + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferTextBeforeKeyPress = "c"; string inputBufferTextAfterKeyPress = "cd"; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferTextBeforeKeyPress); - - DefaultCommandDispatcher defaultCommandDispatcher = shellState.CommandDispatcher as DefaultCommandDispatcher; + DefaultCommandDispatcher defaultCommandDispatcher = shell.ShellState.CommandDispatcher as DefaultCommandDispatcher; string cdCommandName = "cd"; defaultCommandDispatcher.AddCommand(new MockCommand(cdCommandName)); string clearCommandName = "clear"; @@ -249,28 +265,26 @@ public async Task RunAsync_WithTabKeyPress_UpdatesInputBufferWithFirstEntryFromS // Verify the input buffer contents after tab key press event Assert.Equal(inputBufferTextAfterKeyPress, shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(inputBufferTextAfterKeyPress.Length, shell.ShellState.InputManager.CaretPosition); } [Fact] public async Task RunAsync_WithShiftTabKeyPress_UpdatesInputBufferWithLastEntryFromSuggestionList() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.Tab, - shift: true, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + ConsoleKeyInfo[] keys = new[] + { + GetConsoleKeyInfo('c'), + new(keyChar: '\0', key: ConsoleKey.Tab, shift: true, alt: false, control: false), + }; + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferTextBeforeKeyPress = "c"; string inputBufferTextAfterKeyPress = "clear"; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferTextBeforeKeyPress); - - DefaultCommandDispatcher defaultCommandDispatcher = shellState.CommandDispatcher as DefaultCommandDispatcher; + DefaultCommandDispatcher defaultCommandDispatcher = shell.ShellState.CommandDispatcher as DefaultCommandDispatcher; string cdCommandName = "cd"; defaultCommandDispatcher.AddCommand(new MockCommand(cdCommandName)); string clearCommandName = "clear"; @@ -280,285 +294,274 @@ public async Task RunAsync_WithShiftTabKeyPress_UpdatesInputBufferWithLastEntryF // Verify the input buffer contents after Shift + Tab key press event Assert.Equal(inputBufferTextAfterKeyPress, shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(inputBufferTextAfterKeyPress.Length, shell.ShellState.InputManager.CaretPosition); } [Fact] public async Task RunAsync_WithTabKeyPressAndNoSuggestions_DoesNothing() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.Tab, - shift: false, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + ConsoleKeyInfo[] keys = new[] + { + GetConsoleKeyInfo('z'), + GetConsoleKeyInfo(ConsoleKey.Tab), + }; + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferTextBeforeKeyPress = "z"; - - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferTextBeforeKeyPress); + string inputBufferTextAfterKeyPress = "z"; await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); // Verify the input buffer contents after tab key press event - Assert.Equal(inputBufferTextBeforeKeyPress, shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(inputBufferTextAfterKeyPress, shell.ShellState.InputManager.GetCurrentBuffer()); } [Fact] public async Task RunAsync_WithShiftTabKeyPressAndNoSuggestions_DoesNothing() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.Tab, - shift: true, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + ConsoleKeyInfo[] keys = new[] + { + GetConsoleKeyInfo('z'), + new(keyChar: '\0', key: ConsoleKey.Tab, shift: true, alt: false, control: false) + }; + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferTextBeforeKeyPress = "z"; - - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferTextBeforeKeyPress); + string inputBufferTextAfterKeyPress = "z"; await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); // Verify the input buffer contents after Shift + Tab key press event - Assert.Equal(inputBufferTextBeforeKeyPress, shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(inputBufferTextAfterKeyPress, shell.ShellState.InputManager.GetCurrentBuffer()); } [Fact] public async Task RunAsync_WithEnterKeyPress_UpdatesInputBufferWithEmptyString() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.Enter, - shift: false, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + string input = "set base \"https://localhost:44366/\""; + List keys = GetConsoleKeyInfo(input); + keys.Add(new(keyChar: '\0', key: ConsoleKey.Enter, shift: false, alt: false, control: false)); + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferContents = "set base \"https://localhost:44366/\""; - - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferContents); - await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); Assert.Equal(string.Empty, shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(0, shell.ShellState.InputManager.CaretPosition); } [Fact] public async Task RunAsync_WithLeftArrowKeyPress_VerifyCaretPositionWasUpdated() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.LeftArrow, - shift: false, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + string input = "set base \"https://localhost:44366/\""; + List keys = GetConsoleKeyInfo(input); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferContents = "set base \"https://localhost:44366/\""; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferContents); - - shellState.ConsoleManager.MoveCaret(3); await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); - Assert.Equal(2, shellState.ConsoleManager.CaretPosition); + Assert.Equal(input.Length - 1, shellState.InputManager.CaretPosition); } [Fact] public async Task RunAsync_WithControlLeftArrowKeyPress_VerifyCaretPositionWasUpdated() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.LeftArrow, - shift: false, - alt: false, - control: true); - Shell shell = CreateShell(consoleKeyInfo, + string input = "set base \"https://localhost:44366/\""; + List keys = GetConsoleKeyInfo(input); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: true)); + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferContents = "set base \"https://localhost:44366/\""; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferContents); - - shellState.ConsoleManager.MoveCaret(7); await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); - Assert.Equal(4, shellState.ConsoleManager.CaretPosition); + Assert.Equal(9, shellState.InputManager.CaretPosition); } [Fact] public async Task RunAsync_WithRightArrowKeyPress_VerifyCaretPositionWasUpdated() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.RightArrow, - shift: false, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + string input = "set base \"https://localhost:44366/\""; + List keys = GetConsoleKeyInfo(input); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.RightArrow, shift: false, alt: false, control: false)); + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferContents = "set base \"https://localhost:44366/\""; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferContents); - - shellState.ConsoleManager.MoveCaret(3); await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); - Assert.Equal(4, shellState.ConsoleManager.CaretPosition); + Assert.Equal(input.Length - 2, shellState.InputManager.CaretPosition); } [Fact] public async Task RunAsync_WithControlRightArrowKeyPress_VerifyCaretPositionWasUpdated() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.RightArrow, - shift: false, - alt: false, - control: true); - Shell shell = CreateShell(consoleKeyInfo, + string input = "set base \"https://localhost:44366/\""; + List keys = GetConsoleKeyInfo(input); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.RightArrow, shift: false, alt: false, control: true)); + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferContents = "set base \"https://localhost:44366/\""; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferContents); - - shellState.ConsoleManager.MoveCaret(4); await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); - Assert.Equal(9, shellState.ConsoleManager.CaretPosition); + Assert.Equal(input.Length, shellState.InputManager.CaretPosition); } [Fact] public async Task RunAsync_WithHomeKeyPress_VerifyCaretPositionWasUpdated() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.Home, - shift: false, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + string input = "set base \"https://localhost:44366/\""; + List keys = GetConsoleKeyInfo(input); + keys.Add(new(keyChar: '\0', key: ConsoleKey.Home, shift: false, alt: false, control: false)); + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferContents = "set base \"https://localhost:44366/\""; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferContents); - - shellState.ConsoleManager.MoveCaret(3); await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); - Assert.Equal(0, shellState.ConsoleManager.CaretPosition); + Assert.Equal(0, shellState.InputManager.CaretPosition); } [Fact] public async Task RunAsync_WithCtrlAKeyPress_VerifyCaretPositionWasUpdated() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.A, - shift: false, - alt: false, - control: true); - Shell shell = CreateShell(consoleKeyInfo, + string input = "set base \"https://localhost:44366/\""; + List keys = GetConsoleKeyInfo(input); + keys.Add(new(keyChar: '\0', key: ConsoleKey.A, shift: false, alt: false, control: true)); + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferContents = "set base \"https://localhost:44366/\""; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferContents); - - shellState.ConsoleManager.MoveCaret(3); await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); - Assert.Equal(0, shellState.ConsoleManager.CaretPosition); + Assert.Equal(0, shellState.InputManager.CaretPosition); } [Fact] public async Task RunAsync_WithEndKeyPress_VerifyCaretPositionWasUpdated() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.End, - shift: false, - alt: false, - control: false); - Shell shell = CreateShell(consoleKeyInfo, + string input = "set base \"https://localhost:44366/\""; + List keys = GetConsoleKeyInfo(input); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.End, shift: false, alt: false, control: false)); + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferContents = "set base \"https://localhost:44366/\""; - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferContents); - - shellState.ConsoleManager.MoveCaret(3); await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); - Assert.Equal(35, shellState.ConsoleManager.CaretPosition); + Assert.Equal(input.Length, shellState.InputManager.CaretPosition); } [Fact] public async Task RunAsync_WithCtrlEKeyPress_VerifyCaretPositionWasUpdated() { - ConsoleKeyInfo consoleKeyInfo = new ConsoleKeyInfo(keyChar: '\0', - key: ConsoleKey.E, - shift: false, - alt: false, - control: true); - Shell shell = CreateShell(consoleKeyInfo, + string input = "set base \"https://localhost:44366/\""; + List keys = GetConsoleKeyInfo(input); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.LeftArrow, shift: false, alt: false, control: false)); + keys.Add(new(keyChar: '\0', key: ConsoleKey.E, shift: false, alt: false, control: true)); + + Shell shell = CreateShell(keys, previousCommand: null, nextCommand: null, out CancellationTokenSource cancellationTokenSource); - string inputBufferContents = "set base \"https://localhost:44366/\""; + await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); - IShellState shellState = shell.ShellState; - shellState.InputManager.SetInput(shellState, inputBufferContents); + Assert.Equal(input.Length, shell.ShellState.InputManager.CaretPosition); + } + + [Fact] + public async Task RunAsync_WithInvalidCommand_VerifyInputBufferIsCleared() + { + string input = "this is an invalid command\n"; + List keys = GetConsoleKeyInfo(input); + + Shell shell = CreateShell(keys, + previousCommand: null, + nextCommand: null, + out CancellationTokenSource cancellationTokenSource); + + await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); + + Assert.Empty(shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(0, shell.ShellState.InputManager.CaretPosition); + } + + [Fact] + public async Task RunAsync_WithTwoInvalidCommandsAndTwoUpArrows_VerifyInputBufferIsCorrect() + { + string commandOne = "longer invalid command text"; + string commandTwo = "small invalid cmd"; + string input = $"{commandOne}\n{commandTwo}\n"; + List keys = GetConsoleKeyInfo(input); + keys.Add(GetConsoleKeyInfo(ConsoleKey.UpArrow)); + keys.Add(GetConsoleKeyInfo(ConsoleKey.UpArrow)); - shellState.ConsoleManager.MoveCaret(3); + Shell shell = CreateShell(keys, out CancellationTokenSource cancellationTokenSource); await shell.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false); - Assert.Equal(35, shellState.ConsoleManager.CaretPosition); + Assert.Equal(commandOne,shell.ShellState.InputManager.GetCurrentBuffer()); + Assert.Equal(commandOne.Length, shell.ShellState.InputManager.CaretPosition); } - private Shell CreateShell(ConsoleKeyInfo consoleKeyInfo, + private static Shell CreateShell(IEnumerable consoleKeyInfo, string previousCommand, string nextCommand, out CancellationTokenSource cancellationTokenSource) { - var defaultCommandDispatcher = DefaultCommandDispatcher.Create(x => { }, new object()); + DefaultCommandDispatcher defaultCommandDispatcher = DefaultCommandDispatcher.Create(x => { }, new object()); cancellationTokenSource = new CancellationTokenSource(); MockConsoleManager mockConsoleManager = new MockConsoleManager(consoleKeyInfo, cancellationTokenSource); @@ -575,5 +578,66 @@ private Shell CreateShell(ConsoleKeyInfo consoleKeyInfo, return new Shell(shellState); } + + private static Shell CreateShell(IEnumerable consoleKeyInfo, out CancellationTokenSource cancellationTokenSource) + { + DefaultCommandDispatcher defaultCommandDispatcher = DefaultCommandDispatcher.Create(x => { }, new object()); + + cancellationTokenSource = new CancellationTokenSource(); + MockConsoleManager mockConsoleManager = new MockConsoleManager(consoleKeyInfo, cancellationTokenSource); + + ShellState shellState = new ShellState(defaultCommandDispatcher, + consoleManager: mockConsoleManager); + + return new Shell(shellState); + } + + private static Shell CreateShell(ConsoleKeyInfo consoleKeyInfo, string previousCommand, string nextCommand, out CancellationTokenSource cancellationTokenSource) + { + return CreateShell(new ConsoleKeyInfo[] { consoleKeyInfo }, previousCommand, nextCommand, out cancellationTokenSource); + } + + /// + /// Converts a string into the series of ConsoleKeyInfo instances that would be used + /// to type the string in the console. + /// + private static List GetConsoleKeyInfo(string text) + { + List keys = new(); + + text = text.Replace("\r\n", "\n"); + + foreach (char c in text) + { + ConsoleKeyInfo consoleKeyInfo = GetConsoleKeyInfo(c); + keys.Add(consoleKeyInfo); + } + + return keys; + } + + /// + /// Builds a ConsoleKeyInfo object with the specified console key, the null keyChar ('\0') and no modifiers. + /// Intended for non-printable keystrokes. + /// + private static ConsoleKeyInfo GetConsoleKeyInfo(ConsoleKey key) => new('\0', key, shift: false, alt: false, control: false); + + /// + /// Builds a ConsoleKeyInfo that could produce the specified character + /// + private static ConsoleKeyInfo GetConsoleKeyInfo(char keyChar) + { + return keyChar switch + { + >= 'a' and <= 'z' => new(keyChar: keyChar, key: (ConsoleKey)(keyChar - 32), shift: false, alt: false, control: false), + >= 'A' and <= 'Z' => new(keyChar: keyChar, key: (ConsoleKey)keyChar, shift: true, alt: false, control: false), + >= '0' and <= '9' or ' ' => new(keyChar: keyChar, key: (ConsoleKey)keyChar, shift: false, alt: false, control: false), + ':' => new(keyChar: keyChar, key: ConsoleKey.Oem1, shift: true, alt: false, control: false), + '/' => new(keyChar: keyChar, key: ConsoleKey.Oem2, shift: false, alt: false, control: false), + '"' => new(keyChar: keyChar, key: ConsoleKey.Oem7, shift: true, alt: false, control: false), + '\n' => new(keyChar: '\r', key: ConsoleKey.Enter, shift: false, alt: false, control: false), + _ => throw new InvalidOperationException($"Test setup does not support '{keyChar}' yet."), + }; + } } }