From 0e01f8a13a6de9f3fc95bcd3e68b5c4ba900509a Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 21 Sep 2021 14:08:14 -0700 Subject: [PATCH 1/3] Fix a typo --- PSReadLine/UndoRedo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSReadLine/UndoRedo.cs b/PSReadLine/UndoRedo.cs index 3e5ab8860..1e3f9dc0a 100644 --- a/PSReadLine/UndoRedo.cs +++ b/PSReadLine/UndoRedo.cs @@ -20,7 +20,7 @@ private void RemoveEditsAfterUndo() _edits.RemoveRange(_undoEditIndex, removeCount); if (_edits.Count < _editGroupStart) { - // Reset the group start index if any edits before setting the start mark were undone. + // Reset the group start index if any edits after setting the start mark were undone. _editGroupStart = -1; } } From 681eef70004934644a854072afd158c16a65db64 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 21 Sep 2021 23:29:39 -0700 Subject: [PATCH 2/3] Fix line ending --- PSReadLine/YankPaste.vi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSReadLine/YankPaste.vi.cs b/PSReadLine/YankPaste.vi.cs index e9bc622ab..727fec872 100644 --- a/PSReadLine/YankPaste.vi.cs +++ b/PSReadLine/YankPaste.vi.cs @@ -71,7 +71,7 @@ private void SaveLinesToClipboard(int lineIndex, int lineCount) } /// - /// Remove a portion of text from the buffer, save it to the vi register + /// Remove a portion of text from the buffer, save it to the vi register /// and also save it to the edit list to support undo. /// /// From e2d154e27306a31b71db3f264c3d2468100957d4 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 23 Sep 2021 13:29:49 -0700 Subject: [PATCH 3/3] Update the inline suggestion rendering to not exceed the max window buffer --- PSReadLine/Prediction.Views.cs | 110 ++++++++++++++++++++++++--------- PSReadLine/Render.Helper.cs | 24 +++++-- test/InlinePredictionTest.cs | 23 +++++++ test/MockConsole.cs | 13 ++++ test/UnitTestReadLine.cs | 7 ++- 5 files changed, 142 insertions(+), 35 deletions(-) diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index ed9a4bf93..e61d396cd 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -578,6 +578,7 @@ private class PredictionInlineView : PredictionViewBase private uint? _predictorSession; private string _suggestionText; private string _lastInputText; + private int _renderedLength; private bool _alreadyAccepted; internal string SuggestionText => _suggestionText; @@ -671,11 +672,59 @@ internal override void RenderSuggestion(List consoleBufferLines, } int inputLength = _inputText.Length; + int totalLength = _suggestionText.Length; + + // Get the maximum buffer cells that could be available to the current command line. + int maxBufferCells = _singleton._console.BufferHeight * _singleton._console.BufferWidth - _singleton._initialX; + bool skipRendering = false; + + // Assuming the suggestion text contains wide characters only (1 character takes up 2 buffer cells), + // if it still can fit in the console buffer, then we are all good; otherwise, it is possible that + // it could not fit, and thus more calculation is needed to check if that's really the case. + if (totalLength * 2 > maxBufferCells) + { + int length = SubstringLengthByCells(_suggestionText, maxBufferCells); + if (length <= inputLength) + { + // Even the user input cannot fit in the console buffer without having part of it scrolled up-off the buffer. + // We don't attempt to render the suggestion text in this case. + skipRendering = true; + } + else if (length < totalLength) + { + // The whole suggestion text cannot fit in the console buffer without having part of it scrolled up off the buffer. + // We truncate the end part and append ellipsis. + + // We need to truncate 4 buffer cells ealier (just to be safe), so we have enough room to add the ellipsis. + int lenFromEnd = SubstringLengthByCellsFromEnd(_suggestionText, length - 1, countOfCells: 4); + totalLength = length - lenFromEnd; + if (totalLength <= inputLength) + { + // No suggestion left after truncation, so no need to render. + skipRendering = true; + } + } + } + + if (skipRendering) + { + _renderedLength = 0; + return; + } + + _renderedLength = totalLength; StringBuilder currentLineBuffer = consoleBufferLines[currentLogicalLine]; - currentLineBuffer.Append(_singleton._options._inlinePredictionColor) - .Append(_suggestionText, inputLength, _suggestionText.Length - inputLength) - .Append(VTColorUtils.AnsiReset); + currentLineBuffer + .Append(_singleton._options._inlinePredictionColor) + .Append(_suggestionText, inputLength, _renderedLength - inputLength); + + if (_renderedLength < _suggestionText.Length) + { + currentLineBuffer.Append("..."); + } + + currentLineBuffer.Append(VTColorUtils.AnsiReset); } internal override void OnSuggestionAccepted() @@ -702,34 +751,38 @@ internal override void Clear(bool cursorAtEol) return; } - int left, top; - int inputLen = _inputText.Length; - IConsole console = _singleton._console; - - if (cursorAtEol) + if (_renderedLength > 0) { - left = console.CursorLeft; - top = console.CursorTop; - console.BlankRestOfLine(); - } - else - { - Point bufferEndPoint = _singleton.ConvertOffsetToPoint(inputLen); - left = bufferEndPoint.X; - top = bufferEndPoint.Y; - _singleton.WriteBlankRestOfLine(left, top); - } + // Clear the suggestion only if we actually rendered it. + int left, top; + int inputLen = _inputText.Length; + IConsole console = _singleton._console; - int bufferWidth = console.BufferWidth; - int columns = LengthInBufferCells(_suggestionText, inputLen, _suggestionText.Length); + if (cursorAtEol) + { + left = console.CursorLeft; + top = console.CursorTop; + console.BlankRestOfLine(); + } + else + { + Point bufferEndPoint = _singleton.ConvertOffsetToPoint(inputLen); + left = bufferEndPoint.X; + top = bufferEndPoint.Y; + _singleton.WriteBlankRestOfLine(left, top); + } - int remainingLenInCells = bufferWidth - left; - columns -= remainingLenInCells; - if (columns > 0) - { - int extra = columns % bufferWidth > 0 ? 1 : 0; - int count = columns / bufferWidth + extra; - _singleton.WriteBlankLines(top + 1, count); + int bufferWidth = console.BufferWidth; + int columns = LengthInBufferCells(_suggestionText, inputLen, _renderedLength); + + int remainingLenInCells = bufferWidth - left; + columns -= remainingLenInCells; + if (columns > 0) + { + int extra = columns % bufferWidth > 0 ? 1 : 0; + int count = columns / bufferWidth + extra; + _singleton.WriteBlankLines(top + 1, count); + } } Reset(); @@ -742,6 +795,7 @@ internal override void Reset() _predictorId = Guid.Empty; _predictorSession = null; _alreadyAccepted = false; + _renderedLength = 0; } /// diff --git a/PSReadLine/Render.Helper.cs b/PSReadLine/Render.Helper.cs index 4880f9d8e..d99313621 100644 --- a/PSReadLine/Render.Helper.cs +++ b/PSReadLine/Render.Helper.cs @@ -122,16 +122,22 @@ private static int SubstringLengthByCells(string text, int start, int countOfCel for (int i = start; i < text.Length; i++) { - if (cellLength >= countOfCells) + cellLength += LengthInBufferCells(text[i]); + + if (cellLength > countOfCells) { return charLength; } - cellLength += LengthInBufferCells(text[i]); charLength++; + + if (cellLength == countOfCells) + { + return charLength; + } } - return 0; + return charLength; } private static int SubstringLengthByCellsFromEnd(string text, int countOfCells) @@ -146,16 +152,22 @@ private static int SubstringLengthByCellsFromEnd(string text, int start, int cou for (int i = start; i >= 0; i--) { - if (cellLength >= countOfCells) + cellLength += LengthInBufferCells(text[i]); + + if (cellLength > countOfCells) { return charLength; } - cellLength += LengthInBufferCells(text[i]); charLength++; + + if (cellLength == countOfCells) + { + return charLength; + } } - return 0; + return charLength; } } } diff --git a/test/InlinePredictionTest.cs b/test/InlinePredictionTest.cs index 9bc0fc729..c594acebb 100644 --- a/test/InlinePredictionTest.cs +++ b/test/InlinePredictionTest.cs @@ -727,5 +727,28 @@ public void Inline_HistoryAndPluginSource_ExecutionStatus() Assert.Null(_mockedMethods.lastCommandRunStatus); } + + [SkippableFact] + public void Inline_TruncateVeryLongSuggestion() + { + TestSetup(new TestConsole(width: 10, height: 2, keyboardLayout: _), KeyMode.Cmd); + using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView); + + // Truncate long suggestion to make sure the user input is not scrolled up-off the console buffer. + SetHistory(new string('v', 25)); + Test("vv", Keys( + 'v', CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, 'v', + TokenClassification.InlinePrediction, new string('v', 9), + TokenClassification.InlinePrediction, new string('v', 6) + "...")), + 'v', CheckThat(() => AssertScreenIs(2, + TokenClassification.Command, "vv", + TokenClassification.InlinePrediction, new string('v', 8), + TokenClassification.InlinePrediction, new string('v', 6) + "...")), + // Once accepted, the suggestion text should be blanked out. + _.Enter, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "vv")) + )); + } } } diff --git a/test/MockConsole.cs b/test/MockConsole.cs index 81a2b232a..415d79d4a 100644 --- a/test/MockConsole.cs +++ b/test/MockConsole.cs @@ -84,6 +84,19 @@ internal TestConsole(dynamic keyboardLayout) ClearBuffer(); } + internal TestConsole(int width, int height, dynamic keyboardLayout) + { + _keyboardLayout = keyboardLayout; + BackgroundColor = ReadLine.BackgroundColors[0]; + ForegroundColor = ReadLine.Colors[0]; + CursorLeft = 0; + CursorTop = 0; + _bufferWidth = _windowWidth = width; + _bufferHeight = _windowHeight = height; // big enough to avoid the need to implement scrolling + buffer = new CHAR_INFO[BufferWidth * BufferHeight]; + ClearBuffer(); + } + internal void Init(object[] items) { this.index = 0; diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index 4a5ba561e..47f239d42 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -537,11 +537,16 @@ private static string MakeCombinedColor(ConsoleColor fg, ConsoleColor bg) => VTColorUtils.AsEscapeSequence(fg) + VTColorUtils.AsEscapeSequence(bg, isBackground: true); private void TestSetup(KeyMode keyMode, params KeyHandler[] keyHandlers) + { + TestSetup(console: null, keyMode, keyHandlers); + } + + private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] keyHandlers) { Skip.If(WindowsConsoleFixtureHelper.GetKeyboardLayout() != this.Fixture.Lang, $"Keyboard layout must be set to {this.Fixture.Lang}"); - _console = new TestConsole(_); + _console = console ?? new TestConsole(_); _mockedMethods = new MockedMethods(); var instance = (PSConsoleReadLine)typeof(PSConsoleReadLine) .GetField("_singleton", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);