Skip to content

Commit 0ae04cd

Browse files
authored
Update the inline suggestion rendering to not exceed the max window buffer (#2892)
1 parent 1966609 commit 0ae04cd

File tree

7 files changed

+144
-37
lines changed

7 files changed

+144
-37
lines changed

PSReadLine/Prediction.Views.cs

Lines changed: 82 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,7 @@ private class PredictionInlineView : PredictionViewBase
578578
private uint? _predictorSession;
579579
private string _suggestionText;
580580
private string _lastInputText;
581+
private int _renderedLength;
581582
private bool _alreadyAccepted;
582583

583584
internal string SuggestionText => _suggestionText;
@@ -671,11 +672,59 @@ internal override void RenderSuggestion(List<StringBuilder> consoleBufferLines,
671672
}
672673

673674
int inputLength = _inputText.Length;
675+
int totalLength = _suggestionText.Length;
676+
677+
// Get the maximum buffer cells that could be available to the current command line.
678+
int maxBufferCells = _singleton._console.BufferHeight * _singleton._console.BufferWidth - _singleton._initialX;
679+
bool skipRendering = false;
680+
681+
// Assuming the suggestion text contains wide characters only (1 character takes up 2 buffer cells),
682+
// if it still can fit in the console buffer, then we are all good; otherwise, it is possible that
683+
// it could not fit, and thus more calculation is needed to check if that's really the case.
684+
if (totalLength * 2 > maxBufferCells)
685+
{
686+
int length = SubstringLengthByCells(_suggestionText, maxBufferCells);
687+
if (length <= inputLength)
688+
{
689+
// Even the user input cannot fit in the console buffer without having part of it scrolled up-off the buffer.
690+
// We don't attempt to render the suggestion text in this case.
691+
skipRendering = true;
692+
}
693+
else if (length < totalLength)
694+
{
695+
// The whole suggestion text cannot fit in the console buffer without having part of it scrolled up off the buffer.
696+
// We truncate the end part and append ellipsis.
697+
698+
// We need to truncate 4 buffer cells ealier (just to be safe), so we have enough room to add the ellipsis.
699+
int lenFromEnd = SubstringLengthByCellsFromEnd(_suggestionText, length - 1, countOfCells: 4);
700+
totalLength = length - lenFromEnd;
701+
if (totalLength <= inputLength)
702+
{
703+
// No suggestion left after truncation, so no need to render.
704+
skipRendering = true;
705+
}
706+
}
707+
}
708+
709+
if (skipRendering)
710+
{
711+
_renderedLength = 0;
712+
return;
713+
}
714+
715+
_renderedLength = totalLength;
674716
StringBuilder currentLineBuffer = consoleBufferLines[currentLogicalLine];
675717

676-
currentLineBuffer.Append(_singleton._options._inlinePredictionColor)
677-
.Append(_suggestionText, inputLength, _suggestionText.Length - inputLength)
678-
.Append(VTColorUtils.AnsiReset);
718+
currentLineBuffer
719+
.Append(_singleton._options._inlinePredictionColor)
720+
.Append(_suggestionText, inputLength, _renderedLength - inputLength);
721+
722+
if (_renderedLength < _suggestionText.Length)
723+
{
724+
currentLineBuffer.Append("...");
725+
}
726+
727+
currentLineBuffer.Append(VTColorUtils.AnsiReset);
679728
}
680729

681730
internal override void OnSuggestionAccepted()
@@ -702,34 +751,38 @@ internal override void Clear(bool cursorAtEol)
702751
return;
703752
}
704753

705-
int left, top;
706-
int inputLen = _inputText.Length;
707-
IConsole console = _singleton._console;
708-
709-
if (cursorAtEol)
754+
if (_renderedLength > 0)
710755
{
711-
left = console.CursorLeft;
712-
top = console.CursorTop;
713-
console.BlankRestOfLine();
714-
}
715-
else
716-
{
717-
Point bufferEndPoint = _singleton.ConvertOffsetToPoint(inputLen);
718-
left = bufferEndPoint.X;
719-
top = bufferEndPoint.Y;
720-
_singleton.WriteBlankRestOfLine(left, top);
721-
}
756+
// Clear the suggestion only if we actually rendered it.
757+
int left, top;
758+
int inputLen = _inputText.Length;
759+
IConsole console = _singleton._console;
722760

723-
int bufferWidth = console.BufferWidth;
724-
int columns = LengthInBufferCells(_suggestionText, inputLen, _suggestionText.Length);
761+
if (cursorAtEol)
762+
{
763+
left = console.CursorLeft;
764+
top = console.CursorTop;
765+
console.BlankRestOfLine();
766+
}
767+
else
768+
{
769+
Point bufferEndPoint = _singleton.ConvertOffsetToPoint(inputLen);
770+
left = bufferEndPoint.X;
771+
top = bufferEndPoint.Y;
772+
_singleton.WriteBlankRestOfLine(left, top);
773+
}
725774

726-
int remainingLenInCells = bufferWidth - left;
727-
columns -= remainingLenInCells;
728-
if (columns > 0)
729-
{
730-
int extra = columns % bufferWidth > 0 ? 1 : 0;
731-
int count = columns / bufferWidth + extra;
732-
_singleton.WriteBlankLines(top + 1, count);
775+
int bufferWidth = console.BufferWidth;
776+
int columns = LengthInBufferCells(_suggestionText, inputLen, _renderedLength);
777+
778+
int remainingLenInCells = bufferWidth - left;
779+
columns -= remainingLenInCells;
780+
if (columns > 0)
781+
{
782+
int extra = columns % bufferWidth > 0 ? 1 : 0;
783+
int count = columns / bufferWidth + extra;
784+
_singleton.WriteBlankLines(top + 1, count);
785+
}
733786
}
734787

735788
Reset();
@@ -742,6 +795,7 @@ internal override void Reset()
742795
_predictorId = Guid.Empty;
743796
_predictorSession = null;
744797
_alreadyAccepted = false;
798+
_renderedLength = 0;
745799
}
746800

747801
/// <summary>

PSReadLine/Render.Helper.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,22 @@ private static int SubstringLengthByCells(string text, int start, int countOfCel
122122

123123
for (int i = start; i < text.Length; i++)
124124
{
125-
if (cellLength >= countOfCells)
125+
cellLength += LengthInBufferCells(text[i]);
126+
127+
if (cellLength > countOfCells)
126128
{
127129
return charLength;
128130
}
129131

130-
cellLength += LengthInBufferCells(text[i]);
131132
charLength++;
133+
134+
if (cellLength == countOfCells)
135+
{
136+
return charLength;
137+
}
132138
}
133139

134-
return 0;
140+
return charLength;
135141
}
136142

137143
private static int SubstringLengthByCellsFromEnd(string text, int countOfCells)
@@ -146,16 +152,22 @@ private static int SubstringLengthByCellsFromEnd(string text, int start, int cou
146152

147153
for (int i = start; i >= 0; i--)
148154
{
149-
if (cellLength >= countOfCells)
155+
cellLength += LengthInBufferCells(text[i]);
156+
157+
if (cellLength > countOfCells)
150158
{
151159
return charLength;
152160
}
153161

154-
cellLength += LengthInBufferCells(text[i]);
155162
charLength++;
163+
164+
if (cellLength == countOfCells)
165+
{
166+
return charLength;
167+
}
156168
}
157169

158-
return 0;
170+
return charLength;
159171
}
160172
}
161173
}

PSReadLine/UndoRedo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ private void RemoveEditsAfterUndo()
2020
_edits.RemoveRange(_undoEditIndex, removeCount);
2121
if (_edits.Count < _editGroupStart)
2222
{
23-
// Reset the group start index if any edits before setting the start mark were undone.
23+
// Reset the group start index if any edits after setting the start mark were undone.
2424
_editGroupStart = -1;
2525
}
2626
}

PSReadLine/YankPaste.vi.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ private void SaveLinesToClipboard(int lineIndex, int lineCount)
7171
}
7272

7373
/// <summary>
74-
/// Remove a portion of text from the buffer, save it to the vi register
74+
/// Remove a portion of text from the buffer, save it to the vi register
7575
/// and also save it to the edit list to support undo.
7676
/// </summary>
7777
/// <param name="start"></param>

test/InlinePredictionTest.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,5 +727,28 @@ public void Inline_HistoryAndPluginSource_ExecutionStatus()
727727

728728
Assert.Null(_mockedMethods.lastCommandRunStatus);
729729
}
730+
731+
[SkippableFact]
732+
public void Inline_TruncateVeryLongSuggestion()
733+
{
734+
TestSetup(new TestConsole(width: 10, height: 2, keyboardLayout: _), KeyMode.Cmd);
735+
using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView);
736+
737+
// Truncate long suggestion to make sure the user input is not scrolled up-off the console buffer.
738+
SetHistory(new string('v', 25));
739+
Test("vv", Keys(
740+
'v', CheckThat(() => AssertScreenIs(2,
741+
TokenClassification.Command, 'v',
742+
TokenClassification.InlinePrediction, new string('v', 9),
743+
TokenClassification.InlinePrediction, new string('v', 6) + "...")),
744+
'v', CheckThat(() => AssertScreenIs(2,
745+
TokenClassification.Command, "vv",
746+
TokenClassification.InlinePrediction, new string('v', 8),
747+
TokenClassification.InlinePrediction, new string('v', 6) + "...")),
748+
// Once accepted, the suggestion text should be blanked out.
749+
_.Enter, CheckThat(() => AssertScreenIs(1,
750+
TokenClassification.Command, "vv"))
751+
));
752+
}
730753
}
731754
}

test/MockConsole.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,19 @@ internal TestConsole(dynamic keyboardLayout)
8484
ClearBuffer();
8585
}
8686

87+
internal TestConsole(int width, int height, dynamic keyboardLayout)
88+
{
89+
_keyboardLayout = keyboardLayout;
90+
BackgroundColor = ReadLine.BackgroundColors[0];
91+
ForegroundColor = ReadLine.Colors[0];
92+
CursorLeft = 0;
93+
CursorTop = 0;
94+
_bufferWidth = _windowWidth = width;
95+
_bufferHeight = _windowHeight = height; // big enough to avoid the need to implement scrolling
96+
buffer = new CHAR_INFO[BufferWidth * BufferHeight];
97+
ClearBuffer();
98+
}
99+
87100
internal void Init(object[] items)
88101
{
89102
this.index = 0;

test/UnitTestReadLine.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,11 +537,16 @@ private static string MakeCombinedColor(ConsoleColor fg, ConsoleColor bg)
537537
=> VTColorUtils.AsEscapeSequence(fg) + VTColorUtils.AsEscapeSequence(bg, isBackground: true);
538538

539539
private void TestSetup(KeyMode keyMode, params KeyHandler[] keyHandlers)
540+
{
541+
TestSetup(console: null, keyMode, keyHandlers);
542+
}
543+
544+
private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] keyHandlers)
540545
{
541546
Skip.If(WindowsConsoleFixtureHelper.GetKeyboardLayout() != this.Fixture.Lang,
542547
$"Keyboard layout must be set to {this.Fixture.Lang}");
543548

544-
_console = new TestConsole(_);
549+
_console = console ?? new TestConsole(_);
545550
_mockedMethods = new MockedMethods();
546551
var instance = (PSConsoleReadLine)typeof(PSConsoleReadLine)
547552
.GetField("_singleton", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);

0 commit comments

Comments
 (0)