Clip the "current commandline" at the cursor position (#17781)

This is particularly relevant to pwsh with the "ghost text" enabled. In
that scenario, pwsh writes out the predicted command to the right of the
cursor. With `showSuggestions(useCommandline=true)`, we'd auto-include
that text in the filter, and that was effectively useless.

This instead defaults us to not use anything to the right of the cursor
(inclusive) for what we consider "the current commandline"

closes #17772
This commit is contained in:
Mike Griese 2024-08-23 14:24:34 -05:00 committed by GitHub
parent ef960558b3
commit cd8c12586b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 87 additions and 6 deletions

View File

@ -23,6 +23,12 @@
"name": "Upload package to nuget feed",
"icon": "\uE898",
"description": "Go download a .nupkg, put it in ~/Downloads, and use this to push to our private feed."
},
{
"input": "runut /name:**\u001b[D",
"name": "Run a test",
"icon": "",
"description": "Enter the name of a test to run"
}
]
}

View File

@ -3266,23 +3266,30 @@ MarkExtents TextBuffer::_scrollMarkExtentForRow(const til::CoordType rowOffset,
return mark;
}
std::wstring TextBuffer::_commandForRow(const til::CoordType rowOffset, const til::CoordType bottomInclusive) const
std::wstring TextBuffer::_commandForRow(const til::CoordType rowOffset,
const til::CoordType bottomInclusive,
const bool clipAtCursor) const
{
std::wstring commandBuilder;
MarkKind lastMarkKind = MarkKind::Prompt;
const auto cursorPosition = GetCursor().GetPosition();
for (auto y = rowOffset; y <= bottomInclusive; y++)
{
const bool onCursorRow = clipAtCursor && y == cursorPosition.y;
// Now we need to iterate over text attributes. We need to find a
// segment of Prompt attributes, we'll skip those. Then there should be
// Command attributes. Collect up all of those, till we get to the next
// Output attribute.
const auto& row = GetRowByOffset(y);
const auto runs = row.Attributes().runs();
auto x = 0;
for (const auto& [attr, length] : runs)
{
const auto nextX = gsl::narrow_cast<uint16_t>(x + length);
auto nextX = gsl::narrow_cast<uint16_t>(x + length);
if (onCursorRow)
{
nextX = std::min(nextX, gsl::narrow_cast<uint16_t>(cursorPosition.x));
}
const auto markKind{ attr.GetMarkAttributes() };
if (markKind != lastMarkKind)
{
@ -3302,6 +3309,10 @@ std::wstring TextBuffer::_commandForRow(const til::CoordType rowOffset, const ti
}
// advance to next run of text
x = nextX;
if (onCursorRow && x == cursorPosition.x)
{
return commandBuilder;
}
}
// we went over all the runs in this row, but we're not done yet. Keep iterating on the next row.
}
@ -3325,7 +3336,7 @@ std::wstring TextBuffer::CurrentCommand() const
// This row did start a prompt! Find the prompt that starts here.
// Presumably, no rows below us will have prompts, so pass in the last
// row with text as the bottom
return _commandForRow(promptY, _estimateOffsetOfLastCommittedRow());
return _commandForRow(promptY, _estimateOffsetOfLastCommittedRow(), true);
}
return L"";
}

View File

@ -326,7 +326,7 @@ private:
til::point _GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const;
void _PruneHyperlinks();
std::wstring _commandForRow(const til::CoordType rowOffset, const til::CoordType bottomInclusive) const;
std::wstring _commandForRow(const til::CoordType rowOffset, const til::CoordType bottomInclusive, const bool clipAtCursor = false) const;
MarkExtents _scrollMarkExtentForRow(const til::CoordType rowOffset, const til::CoordType bottomInclusive) const;
bool _createPromptMarkIfNeeded();

View File

@ -2291,7 +2291,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
// If the very last thing in the list of recent commands, is exactly the
// same as the current command, then let's not include it in the
// history. It's literally the thing the user has typed, RIGHT now.
if (!commands.empty() && commands.back() == trimmedCurrentCommand)
// (also account for the fact that the cursor may be in the middle of a commandline)
if (!commands.empty() &&
!trimmedCurrentCommand.empty() &&
std::wstring_view{ commands.back() }.substr(0, trimmedCurrentCommand.size()) == trimmedCurrentCommand)
{
commands.pop_back();
}

View File

@ -41,6 +41,8 @@ namespace ControlUnitTests
TEST_METHOD(TestSelectCommandSimple);
TEST_METHOD(TestSelectOutputSimple);
TEST_METHOD(TestCommandContext);
TEST_METHOD(TestCommandContextWithPwshGhostText);
TEST_METHOD(TestSelectOutputScrolling);
TEST_METHOD(TestSelectOutputExactWrap);
@ -556,6 +558,61 @@ namespace ControlUnitTests
}
}
void ControlCoreTests::TestCommandContextWithPwshGhostText()
{
auto [settings, conn] = _createSettingsAndConnection();
Log::Comment(L"Create ControlCore object");
auto core = createCore(*settings, *conn);
VERIFY_IS_NOT_NULL(core);
_standardInit(core);
Log::Comment(L"Print some text");
_writePrompt(conn, L"C:\\Windows");
conn->WriteInput(winrt_wstring_to_array_view(L"Foo-bar"));
conn->WriteInput(winrt_wstring_to_array_view(L"\x1b]133;C\x7"));
conn->WriteInput(winrt_wstring_to_array_view(L"\r\n"));
conn->WriteInput(winrt_wstring_to_array_view(L"This is some text \r\n"));
conn->WriteInput(winrt_wstring_to_array_view(L"with varying amounts \r\n"));
conn->WriteInput(winrt_wstring_to_array_view(L"of whitespace \r\n"));
_writePrompt(conn, L"C:\\Windows");
Log::Comment(L"Check the command context");
const WEX::TestExecution::DisableVerifyExceptions disableExceptionsScope;
{
auto historyContext{ core->CommandHistory() };
VERIFY_ARE_EQUAL(1u, historyContext.History().Size());
VERIFY_ARE_EQUAL(L"", historyContext.CurrentCommandline());
}
Log::Comment(L"Write 'BarBar' to the command...");
conn->WriteInput(winrt_wstring_to_array_view(L"BarBar"));
{
auto historyContext{ core->CommandHistory() };
// BarBar shouldn't be in the history, it should be the current command
VERIFY_ARE_EQUAL(1u, historyContext.History().Size());
VERIFY_ARE_EQUAL(L"BarBar", historyContext.CurrentCommandline());
}
Log::Comment(L"then move the cursor to the left");
// This emulates the state the buffer is in when pwsh does it's "ghost
// text" thing. We don't want to include all that ghost text in the
// current commandline.
conn->WriteInput(winrt_wstring_to_array_view(L"\x1b[D"));
conn->WriteInput(winrt_wstring_to_array_view(L"\x1b[D"));
{
auto historyContext{ core->CommandHistory() };
VERIFY_ARE_EQUAL(1u, historyContext.History().Size());
// The current commandline is only the text to the left of the cursor
auto curr{ historyContext.CurrentCommandline() };
VERIFY_ARE_EQUAL(4u, curr.size());
VERIFY_ARE_EQUAL(L"BarB", curr);
}
}
void ControlCoreTests::TestSelectOutputScrolling()
{
auto [settings, conn] = _createSettingsAndConnection();

View File

@ -8337,6 +8337,10 @@ void ScreenBufferTests::SimpleMarkCommand()
void ScreenBufferTests::SimpleWrappedCommand()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
END_TEST_METHOD_PROPERTIES()
auto& g = ServiceLocator::LocateGlobals();
auto& gci = g.getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();