From 821ae3af2d311350fbfaa4c77af09858488681e9 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 25 Aug 2023 20:25:39 +0200 Subject: [PATCH] Rewrite COOKED_READ_DATA (#15783) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This massive refactoring has two goals: * Enable us to go beyond UCS-2 support for input editing * Bring clarity into `COOKED_READ_DATA`'s inner workings Unfortunately, over time, knowledge about its exact operation was lost. While the new code is still complex it reduces the amount of code by 4x which will make preserving knowledge hopefully significantly easier. The new implementation is simpler and slower than the old one in a way, because every time the input line is modified it's rewritten to the text buffer from scratch. This however massively simplifies the underlying algorithm and the amount of state that needs to be tracked and results in a significant reduction in code size. It also makes it more robust, because there's less code now that can be incorrect. This "optimization laziness" can be afforded due the recent >10x improvements to `TextBuffer`'s text ingestion performance. For short inputs (<1000 characters) I still expect this implementation to outperform the conhost from the past. It has received one optimization already however: While reading text from the `InputBuffer` we'll now defer writing into the `TextBuffer` until we've stopped reading. This improves the overhead of pasting text from O(n^2) to O(n), which is immediately noticeable for inputs >100kB. Resizing the text buffer still ends up corrupting the input line however, which unfortunately cannot be fixed in `COOKED_READ_DATA`. The issue occurs due to bugs in `TextBuffer::Reflow` itself, as it misplaces the cursor if the prompt is on the last line of the buffer. Closes #1377 Closes #1503 Closes #4628 Closes #4975 Closes #5033 Closes #8008 This commit is required to fix #797 ## Validation Steps Performed * ASCII input ✅ * Chinese input (中文維基百科) ❔ * Resizing the window properly wraps/unwraps wide glyphs ❌ Broken due to `TextBuffer::Reflow` bugs * Surrogate pair input (🙂) ❔ * Resizing the window properly wraps/unwraps surrogate pairs ❌ Broken due to `TextBuffer::Reflow` bugs * In cmd.exe * Create 2 file: "a😊b.txt" and "a😟b.txt" * Press tab: Autocompletes "a😊b.txt" ✅ * Navigate the cursor right past the "a" * Press tab twice: Autocompletes "a😟b.txt" ✅ * Backspace deletes preceding glyphs ✅ * Ctrl+Backspace deletes preceding words ✅ * Escape clears input ✅ * Home navigates to start ✅ * Ctrl+Home deletes text between cursor and start ✅ * End navigates to end ✅ * Ctrl+End deletes text between cursor and end ✅ * Left navigates over previous code points ✅ * Ctrl+Left navigates to previous word-starts ✅ * Right and F1 navigate over next code points ✅ * Pressing right at the end of input copies characters from the previous command ✅ * Ctrl+Right navigates to next word-ends ✅ * Insert toggles overwrite mode ✅ * Delete deletes next code point ✅ * Up and F5 cycle through history ✅ * Doesn't crash with no history ✅ * Stops at first entry ✅ * Down cycles through history ✅ * Doesn't crash with no history ✅ * Stops at last entry ✅ * PageUp retrieves the oldest command ✅ * PageDown retrieves the newest command ✅ * F2 starts "copy to char" prompt ✅ * Escape dismisses prompt ✅ * Typing a character copies text from the previous command up until that character into the current buffer (acts identical to F3, but with automatic character search) ✅ * F3 copies the previous command into the current buffer, starting at the current cursor position, for as many characters as possible ✅ * Doesn't erase trailing text if the current buffer is longer than the previous command ✅ * Puts the cursor at the end of the copied text ✅ * F4 starts "copy from char" prompt ✅ * Escape dismisses prompt ✅ * Erases text between the current cursor position and the first instance of a given char (but not including it) ✅ * F6 inserts Ctrl+Z ✅ * F7 without modifiers starts "command list" prompt ✅ * Escape dismisses prompt ✅ * Minimum size of 40x10 characters ✅ * Width expands to fit the widest history command ✅ * Height expands up to 20 rows with longer histories ✅ * F9 starts "command number" prompt ✅ * Left/Right paste replace the buffer with the given command ✅ * And put cursor at the end of the buffer ✅ * Up/Down navigate selection through history ✅ * Stops at start/end with <10 entries ✅ * Stops at start/end with >20 entries ✅ * Wide text rendering during pagination with >20 entries ✅ * Shift+Up/Down moves history items around ✅ * Home navigates to first entry ✅ * End navigates to last entry ✅ * PageUp navigates by 20 items at a time or to first ✅ * PageDown navigates by 20 items at a time or to last ✅ * Alt+F7 clears command history ✅ * F8 cycles through commands that start with the same text as the current buffer up until the current cursor position ✅ * Doesn't crash with no history ✅ * F9 starts "command number" prompt ✅ * Escape dismisses prompt ✅ * Ignores non-ASCII-decimal characters ✅ * Allows entering between 1 and 5 digits ✅ * Pressing Enter fetches the given command from the history ✅ * Alt+F10 clears doskey aliases ✅ --- .github/actions/spelling/expect/alphabet.txt | 1 + .github/actions/spelling/expect/expect.txt | 13 +- src/buffer/out/textBuffer.cpp | 58 + src/buffer/out/textBuffer.hpp | 2 + .../ConptyRoundtripTests.cpp | 24 +- src/host/CommandListPopup.cpp | 549 ----- src/host/CommandListPopup.hpp | 50 - src/host/CommandNumberPopup.cpp | 195 -- src/host/CommandNumberPopup.hpp | 45 - src/host/CopyFromCharPopup.cpp | 63 - src/host/CopyFromCharPopup.hpp | 29 - src/host/CopyToCharPopup.cpp | 84 - src/host/CopyToCharPopup.hpp | 32 - src/host/_stream.cpp | 352 +--- src/host/_stream.h | 67 +- src/host/alias.cpp | 90 +- src/host/alias.h | 17 +- src/host/cmdline.cpp | 1237 +---------- src/host/cmdline.h | 87 +- src/host/consoleInformation.cpp | 5 + src/host/ft_fuzzer/fuzzmain.cpp | 19 +- src/host/getset.cpp | 14 - src/host/history.cpp | 60 +- src/host/history.h | 9 +- src/host/host-common.vcxitems | 10 - src/host/input.cpp | 4 +- src/host/input.h | 5 - src/host/lib/hostlib.vcxproj.filters | 30 - src/host/misc.cpp | 188 +- src/host/misc.h | 21 - src/host/popup.cpp | 317 --- src/host/popup.h | 82 - src/host/readDataCooked.cpp | 1835 +++++++++-------- src/host/readDataCooked.hpp | 261 ++- src/host/screenInfo.cpp | 103 +- src/host/screenInfo.hpp | 6 +- src/host/scrolling.cpp | 2 +- src/host/selectionInput.cpp | 58 +- src/host/server.h | 18 +- src/host/sources.inc | 5 - src/host/stream.cpp | 89 +- src/host/stream.h | 14 - src/host/tracing.cpp | 7 +- src/host/tracing.hpp | 2 +- src/host/ut_host/AliasTests.cpp | 250 +-- src/host/ut_host/CommandLineTests.cpp | 539 ----- src/host/ut_host/CommandListPopupTests.cpp | 538 ----- src/host/ut_host/CommandNumberPopupTests.cpp | 297 --- src/host/ut_host/CopyFromCharPopupTests.cpp | 165 -- src/host/ut_host/CopyToCharPopupTests.cpp | 243 --- src/host/ut_host/Host.UnitTests.vcxproj | 6 - .../ut_host/Host.UnitTests.vcxproj.filters | 18 - src/host/ut_host/PopupTestHelper.hpp | 84 - src/host/ut_host/ScreenBufferTests.cpp | 20 +- src/host/ut_host/SelectionTests.cpp | 84 - src/host/ut_host/TextBufferTests.cpp | 52 +- src/host/ut_host/sources | 5 - src/interactivity/win32/menu.cpp | 6 - src/interactivity/win32/windowio.cpp | 2 +- 59 files changed, 1410 insertions(+), 7058 deletions(-) delete mode 100644 src/host/CommandListPopup.cpp delete mode 100644 src/host/CommandListPopup.hpp delete mode 100644 src/host/CommandNumberPopup.cpp delete mode 100644 src/host/CommandNumberPopup.hpp delete mode 100644 src/host/CopyFromCharPopup.cpp delete mode 100644 src/host/CopyFromCharPopup.hpp delete mode 100644 src/host/CopyToCharPopup.cpp delete mode 100644 src/host/CopyToCharPopup.hpp delete mode 100644 src/host/popup.cpp delete mode 100644 src/host/popup.h delete mode 100644 src/host/ut_host/CommandLineTests.cpp delete mode 100644 src/host/ut_host/CommandListPopupTests.cpp delete mode 100644 src/host/ut_host/CommandNumberPopupTests.cpp delete mode 100644 src/host/ut_host/CopyFromCharPopupTests.cpp delete mode 100644 src/host/ut_host/CopyToCharPopupTests.cpp delete mode 100644 src/host/ut_host/PopupTestHelper.hpp diff --git a/.github/actions/spelling/expect/alphabet.txt b/.github/actions/spelling/expect/alphabet.txt index b2c7597eee8..97180b72115 100644 --- a/.github/actions/spelling/expect/alphabet.txt +++ b/.github/actions/spelling/expect/alphabet.txt @@ -21,6 +21,7 @@ BBBBCCCCC BBGGRR efg EFG +efgh EFGh KLMNOQQQQQQQQQQ QQQQQQQQQQABCDEFGHIJ diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 2e552912cfa..2826e125a02 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -17,7 +17,6 @@ ADDALIAS ADDREF ADDSTRING ADDTOOL -AEnd AFew AFill AFX @@ -679,7 +678,7 @@ FSINFOCLASS fte Ftm Fullscreens -fullwidth +Fullwidth FUNCTIONCALL fuzzer fuzzmain @@ -923,7 +922,6 @@ itermcolors ITerminal itf Ith -itoa IUI IUnknown ivalid @@ -1102,6 +1100,7 @@ MDs MEASUREITEM megamix memallocator +meme MENUCHAR MENUCONTROL MENUDROPALIGNMENT @@ -1588,7 +1587,6 @@ rgrc rgs rgui rgw -rgwch RIGHTALIGN RIGHTBUTTON riid @@ -1783,7 +1781,6 @@ STDMETHODCALLTYPE STDMETHODIMP STGM stl -stoutapot Stri Stringable STRINGTABLE @@ -1838,7 +1835,6 @@ TBM tchar TCHFORMAT TCI -tcome tcommandline tcommands Tdd @@ -1865,8 +1861,7 @@ testname TESTNULL testpass testpasses -testtestabc -testtesttesttesttest +testtimeout TEXCOORD texel TExpected @@ -2083,7 +2078,6 @@ vtseq vtterm vttest VWX -waaay waitable WANSUNG WANTARROWS @@ -2201,7 +2195,6 @@ wprp wprpi wregex writeback -writechar WRITECONSOLE WRITECONSOLEINPUT WRITECONSOLEOUTPUT diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index f5bfdeda3ce..84336eaf03e 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -416,6 +416,64 @@ size_t TextBuffer::GraphemePrev(const std::wstring_view& chars, size_t position) return til::utf16_iterate_prev(chars, position); } +// Pretend as if `position` is a regular cursor in the TextBuffer. +// This function will then pretend as if you pressed the left/right arrow +// keys `distance` amount of times (negative = left, positive = right). +til::point TextBuffer::NavigateCursor(til::point position, til::CoordType distance) const +{ + const til::CoordType maxX = _width - 1; + const til::CoordType maxY = _height - 1; + auto x = std::clamp(position.x, 0, maxX); + auto y = std::clamp(position.y, 0, maxY); + auto row = &GetRowByOffset(y); + + if (distance < 0) + { + do + { + if (x > 0) + { + x = row->NavigateToPrevious(x); + } + else if (y <= 0) + { + break; + } + else + { + --y; + row = &GetRowByOffset(y); + x = row->GetReadableColumnCount() - 1; + } + } while (++distance != 0); + } + else if (distance > 0) + { + auto rowWidth = row->GetReadableColumnCount(); + + do + { + if (x < rowWidth) + { + x = row->NavigateToNext(x); + } + else if (y >= maxY) + { + break; + } + else + { + ++y; + row = &GetRowByOffset(y); + rowWidth = row->GetReadableColumnCount(); + x = 0; + } + } while (--distance != 0); + } + + return { x, y }; +} + // This function is intended for writing regular "lines" of text as it'll set the wrap flag on the given row. // You can continue calling the function on the same row as long as state.columnEnd < state.columnLimit. void TextBuffer::Write(til::CoordType row, const TextAttribute& attributes, RowWriteState& state) diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index a57474b6b81..360c74aab8c 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -138,6 +138,8 @@ class TextBuffer final static size_t GraphemeNext(const std::wstring_view& chars, size_t position) noexcept; static size_t GraphemePrev(const std::wstring_view& chars, size_t position) noexcept; + til::point NavigateCursor(til::point position, til::CoordType distance) const; + // Text insertion functions void Write(til::CoordType row, const TextAttribute& attributes, RowWriteState& state); void FillRect(const til::rect& rect, const std::wstring_view& fill, const TextAttribute& attributes); diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index 76065f93c95..3c5136b1910 100644 --- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -21,7 +21,6 @@ #include "../host/readDataCooked.hpp" #include "../host/output.h" #include "../host/_stream.h" // For WriteCharsLegacy -#include "../host/cmdline.h" // For WC_INTERACTIVE #include "test/CommonState.hpp" #include "../cascadia/TerminalCore/Terminal.hpp" @@ -3165,20 +3164,6 @@ void ConptyRoundtripTests::NewLinesAtBottomWithBackground() verifyBuffer(*termTb, term->_mutableViewport.ToExclusive()); } -void doWriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view string, DWORD flags = 0) -{ - auto dwNumBytes = string.size() * sizeof(wchar_t); - VERIFY_NT_SUCCESS(WriteCharsLegacy(screenInfo, - string.data(), - string.data(), - string.data(), - &dwNumBytes, - nullptr, - screenInfo.GetTextBuffer().GetCursor().GetPosition().x, - flags, - nullptr)); -} - void ConptyRoundtripTests::WrapNewLineAtBottom() { // The actual bug case is @@ -3220,11 +3205,6 @@ void ConptyRoundtripTests::WrapNewLineAtBottom() return; } - // I've tested this with 0x0, 0x4, 0x80, 0x84, and 0-8, and none of these - // flags seem to make a difference. So we're just assuming 0 here, so we - // don't test a bunch of redundant cases. - const auto writeCharsLegacyMode = 0; - // This test was originally written for // https://github.com/microsoft/terminal/issues/5691 // @@ -3263,7 +3243,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottom() } else if (writingMethod == PrintWithWriteCharsLegacy) { - doWriteCharsLegacy(si, str, writeCharsLegacyMode); + WriteCharsLegacy(si, str, false, nullptr); } }; @@ -3421,7 +3401,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS() } else if (writingMethod == PrintWithWriteCharsLegacy) { - doWriteCharsLegacy(si, str, WC_INTERACTIVE); + WriteCharsLegacy(si, str, true, nullptr); } }; diff --git a/src/host/CommandListPopup.cpp b/src/host/CommandListPopup.cpp deleted file mode 100644 index b530ca2760d..00000000000 --- a/src/host/CommandListPopup.cpp +++ /dev/null @@ -1,549 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "CommandListPopup.hpp" -#include "stream.h" -#include "_stream.h" -#include "cmdline.h" -#include "misc.h" -#include "_output.h" -#include "dbcs.h" -#include "../types/inc/GlyphWidth.hpp" - -#include "../interactivity/inc/ServiceLocator.hpp" - -static constexpr size_t COMMAND_NUMBER_SIZE = 8; // size of command number buffer - -// Routine Description: -// - Calculates what the proposed size of the popup should be, based on the commands in the history -// Arguments: -// - history - the history to look through to measure command sizes -// Return Value: -// - the proposed size of the popup with the history list taken into account -static til::size calculatePopupSize(const CommandHistory& history) -{ - // this is the historical size of the popup, so it is now used as a minimum - const til::size minSize = { 40, 10 }; - - // padding is for the command number listing before a command is printed to the window. - // ex: |10: echo blah - // ^^^^ <- these are the cells that are being accounted for by padding - const size_t padding = 4; - - // find the widest command history item and use it for the width - size_t width = minSize.width; - for (CommandHistory::Index i = 0; i < history.GetNumberOfCommands(); ++i) - { - const auto& historyItem = history.GetNth(i); - width = std::max(width, historyItem.size() + padding); - } - if (width > SHRT_MAX) - { - width = SHRT_MAX; - } - - // calculate height, it can range up to 20 rows - auto height = std::clamp(gsl::narrow(history.GetNumberOfCommands()), minSize.height, 20); - - return { gsl::narrow_cast(width), height }; -} - -CommandListPopup::CommandListPopup(SCREEN_INFORMATION& screenInfo, const CommandHistory& history) : - Popup(screenInfo, calculatePopupSize(history)), - _history{ history }, - _currentCommand{ std::min(history.LastDisplayed, history.GetNumberOfCommands() - 1) } -{ - FAIL_FAST_IF(_currentCommand < 0); - _setBottomIndex(); -} - -[[nodiscard]] NTSTATUS CommandListPopup::_handlePopupKeys(COOKED_READ_DATA& cookedReadData, - const wchar_t wch, - const DWORD modifiers) noexcept -{ - try - { - CommandHistory::Index Index = 0; - const auto shiftPressed = WI_IsFlagSet(modifiers, SHIFT_PRESSED); - switch (wch) - { - case VK_F9: - { - const auto hr = CommandLine::Instance().StartCommandNumberPopup(cookedReadData); - if (S_FALSE == hr) - { - // If we couldn't make the popup, break and go around to read another input character. - break; - } - else - { - return hr; - } - } - case VK_ESCAPE: - CommandLine::Instance().EndCurrentPopup(); - return CONSOLE_STATUS_WAIT_NO_BLOCK; - case VK_UP: - if (shiftPressed) - { - return _swapUp(cookedReadData); - } - else - { - _update(-1); - } - break; - case VK_DOWN: - if (shiftPressed) - { - return _swapDown(cookedReadData); - } - else - { - _update(1); - } - break; - case VK_END: - // Move waaay forward, UpdateCommandListPopup() can handle it. - _update(cookedReadData.History().GetNumberOfCommands()); - break; - case VK_HOME: - // Move waaay back, UpdateCommandListPopup() can handle it. - _update(-cookedReadData.History().GetNumberOfCommands()); - break; - case VK_PRIOR: - _update(-Height()); - break; - case VK_NEXT: - _update(Height()); - break; - case VK_DELETE: - return _deleteSelection(cookedReadData); - case VK_LEFT: - case VK_RIGHT: - Index = _currentCommand; - CommandLine::Instance().EndCurrentPopup(); - SetCurrentCommandLine(cookedReadData, Index); - return CONSOLE_STATUS_WAIT_NO_BLOCK; - default: - break; - } - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -void CommandListPopup::_setBottomIndex() -{ - if (_currentCommand < _history.GetNumberOfCommands() - Height()) - { - _bottomIndex = std::max(_currentCommand, Height() - 1); - } - else - { - _bottomIndex = _history.GetNumberOfCommands() - 1; - } -} - -[[nodiscard]] NTSTATUS CommandListPopup::_deleteSelection(COOKED_READ_DATA& cookedReadData) noexcept -{ - try - { - auto& history = cookedReadData.History(); - history.Remove(_currentCommand); - _setBottomIndex(); - - if (history.GetNumberOfCommands() == 0) - { - // close the popup - return CONSOLE_STATUS_READ_COMPLETE; - } - else if (_currentCommand >= history.GetNumberOfCommands()) - { - _currentCommand = history.GetNumberOfCommands() - 1; - _bottomIndex = _currentCommand; - } - - _drawList(); - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -// Routine Description: -// - moves the selected history item up in the history list -// Arguments: -// - cookedReadData - the read wait object to operate upon -[[nodiscard]] NTSTATUS CommandListPopup::_swapUp(COOKED_READ_DATA& cookedReadData) noexcept -{ - try - { - auto& history = cookedReadData.History(); - - if (history.GetNumberOfCommands() <= 1 || _currentCommand == 0) - { - return STATUS_SUCCESS; - } - history.Swap(_currentCommand, _currentCommand - 1); - _update(-1); - _drawList(); - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -// Routine Description: -// - moves the selected history item down in the history list -// Arguments: -// - cookedReadData - the read wait object to operate upon -[[nodiscard]] NTSTATUS CommandListPopup::_swapDown(COOKED_READ_DATA& cookedReadData) noexcept -{ - try - { - auto& history = cookedReadData.History(); - - if (history.GetNumberOfCommands() <= 1 || _currentCommand == history.GetNumberOfCommands() - 1) - { - return STATUS_SUCCESS; - } - history.Swap(_currentCommand, _currentCommand + 1); - _update(1); - _drawList(); - } - CATCH_LOG(); - return STATUS_SUCCESS; -} - -void CommandListPopup::_handleReturn(COOKED_READ_DATA& cookedReadData) -{ - CommandHistory::Index Index = 0; - auto Status = STATUS_SUCCESS; - DWORD LineCount = 1; - Index = _currentCommand; - CommandLine::Instance().EndCurrentPopup(); - SetCurrentCommandLine(cookedReadData, Index); - cookedReadData.ProcessInput(UNICODE_CARRIAGERETURN, 0, Status); - // complete read - if (cookedReadData.IsEchoInput()) - { - // check for alias - cookedReadData.ProcessAliases(LineCount); - } - - Status = STATUS_SUCCESS; - size_t NumBytes; - if (cookedReadData.BytesRead() > cookedReadData.UserBufferSize() || LineCount > 1) - { - if (LineCount > 1) - { - const wchar_t* Tmp; - for (Tmp = cookedReadData.BufferStartPtr(); *Tmp != UNICODE_LINEFEED; Tmp++) - { - FAIL_FAST_IF(!(Tmp < (cookedReadData.BufferStartPtr() + cookedReadData.BytesRead()))); - } - NumBytes = (Tmp - cookedReadData.BufferStartPtr() + 1) * sizeof(*Tmp); - } - else - { - NumBytes = cookedReadData.UserBufferSize(); - } - - // Copy what we can fit into the user buffer - const auto bytesWritten = cookedReadData.SavePromptToUserBuffer(NumBytes / sizeof(wchar_t)); - - // Store all of the remaining as pending until the next read operation. - cookedReadData.SavePendingInput(NumBytes / sizeof(wchar_t), LineCount > 1); - NumBytes = bytesWritten; - } - else - { - NumBytes = cookedReadData.BytesRead(); - NumBytes = cookedReadData.SavePromptToUserBuffer(NumBytes / sizeof(wchar_t)); - } - - cookedReadData.SetReportedByteCount(NumBytes); -} - -void CommandListPopup::_cycleSelectionToMatchingCommands(COOKED_READ_DATA& cookedReadData, const wchar_t wch) -{ - CommandHistory::Index Index = 0; - if (cookedReadData.History().FindMatchingCommand({ &wch, 1 }, - _currentCommand, - Index, - CommandHistory::MatchOptions::JustLooking)) - { - _update(Index - _currentCommand, true); - } -} - -// Routine Description: -// - This routine handles the command list popup. It returns when we're out of input or the user has selected a command line. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - CONSOLE_STATUS_READ_COMPLETE - user hit return -[[nodiscard]] NTSTATUS CommandListPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto Status = STATUS_SUCCESS; - - for (;;) - { - auto wch = UNICODE_NULL; - auto popupKeys = false; - DWORD modifiers = 0; - - Status = _getUserInput(cookedReadData, popupKeys, modifiers, wch); - if (FAILED_NTSTATUS(Status)) - { - return Status; - } - - if (popupKeys) - { - Status = _handlePopupKeys(cookedReadData, wch, modifiers); - if (Status != STATUS_SUCCESS) - { - return Status; - } - } - else if (wch == UNICODE_CARRIAGERETURN) - { - _handleReturn(cookedReadData); - return CONSOLE_STATUS_READ_COMPLETE; - } - else - { - // cycle through commands that start with the letter of the key pressed - _cycleSelectionToMatchingCommands(cookedReadData, wch); - } - } -} - -void CommandListPopup::_DrawContent() -{ - _drawList(); -} - -// Routine Description: -// - Draws a list of commands for the user to choose from -void CommandListPopup::_drawList() -{ - // draw empty popup - til::point WriteCoord; - WriteCoord.x = _region.left + 1; - WriteCoord.y = _region.top + 1; - size_t lStringLength = Width(); - for (til::CoordType i = 0; i < Height(); ++i) - { - const OutputCellIterator spaces(UNICODE_SPACE, _attributes, lStringLength); - const auto result = _screenInfo.Write(spaces, WriteCoord); - lStringLength = result.GetCellDistance(spaces); - WriteCoord.y += 1; - } - - auto api = Microsoft::Console::Interactivity::ServiceLocator::LocateGlobals().api; - - WriteCoord.y = _region.top + 1; - auto i = std::max(_bottomIndex - Height() + 1, 0); - for (; i <= _bottomIndex; i++) - { - CHAR CommandNumber[COMMAND_NUMBER_SIZE]; - // Write command number to screen. - if (0 != _itoa_s(i, CommandNumber, ARRAYSIZE(CommandNumber), 10)) - { - return; - } - - auto CommandNumberPtr = CommandNumber; - - size_t CommandNumberLength; - if (FAILED(StringCchLengthA(CommandNumberPtr, ARRAYSIZE(CommandNumber), &CommandNumberLength))) - { - return; - } - __assume_bound(CommandNumberLength); - - if (CommandNumberLength + 1 >= ARRAYSIZE(CommandNumber)) - { - return; - } - - CommandNumber[CommandNumberLength] = ':'; - CommandNumber[CommandNumberLength + 1] = ' '; - CommandNumberLength += 2; - if (CommandNumberLength > static_cast(Width())) - { - CommandNumberLength = static_cast(Width()); - } - - WriteCoord.x = _region.left + 1; - - LOG_IF_FAILED(api->WriteConsoleOutputCharacterAImpl(_screenInfo, - { CommandNumberPtr, CommandNumberLength }, - WriteCoord, - CommandNumberLength)); - - // write command to screen - auto command = _history.GetNth(i); - lStringLength = command.size(); - { - auto lTmpStringLength = lStringLength; - auto lPopupLength = static_cast(Width() - CommandNumberLength); - auto lpStr = command.data(); - while (lTmpStringLength--) - { - if (IsGlyphFullWidth(*lpStr++)) - { - lPopupLength -= 2; - } - else - { - lPopupLength--; - } - - if (lPopupLength <= 0) - { - lStringLength -= lTmpStringLength; - if (lPopupLength < 0) - { - lStringLength--; - } - - break; - } - } - } - - WriteCoord.x = gsl::narrow(WriteCoord.x + CommandNumberLength); - size_t used; - LOG_IF_FAILED(api->WriteConsoleOutputCharacterWImpl(_screenInfo, - { command.data(), lStringLength }, - WriteCoord, - used)); - - // write attributes to screen - if (i == _currentCommand) - { - WriteCoord.x = _region.left + 1; - // inverted attributes - lStringLength = Width(); - auto inverted = _attributes; - inverted.Invert(); - - const OutputCellIterator it(inverted, lStringLength); - const auto done = _screenInfo.Write(it, WriteCoord); - - lStringLength = done.GetCellDistance(it); - } - - WriteCoord.y += 1; - } -} - -// Routine Description: -// - For popup lists, will adjust the position of the highlighted item and -// possibly scroll the list if necessary. -// Arguments: -// - originalDelta - The number of lines to move up or down -// - wrap - Down past the bottom or up past the top should wrap the command list -void CommandListPopup::_update(const CommandHistory::Index originalDelta, const bool wrap) -{ - auto delta = originalDelta; - if (delta == 0) - { - return; - } - const auto Size = Height(); - - auto CurCmdNum = _currentCommand; - CommandHistory::Index NewCmdNum = CurCmdNum + delta; - - if (wrap) - { - // Modulo the number of commands to "circle" around if we went off the end. - NewCmdNum %= _history.GetNumberOfCommands(); - } - else - { - if (NewCmdNum >= _history.GetNumberOfCommands()) - { - NewCmdNum = _history.GetNumberOfCommands() - 1; - } - else if (NewCmdNum < 0) - { - NewCmdNum = 0; - } - } - delta = NewCmdNum - CurCmdNum; - - auto Scroll = false; - // determine amount to scroll, if any - if (NewCmdNum <= _bottomIndex - Size) - { - _bottomIndex += delta; - if (_bottomIndex < Size - 1) - { - _bottomIndex = Size - 1; - } - Scroll = true; - } - else if (NewCmdNum > _bottomIndex) - { - _bottomIndex += delta; - if (_bottomIndex >= _history.GetNumberOfCommands()) - { - _bottomIndex = _history.GetNumberOfCommands() - 1; - } - Scroll = true; - } - - // write commands to popup - if (Scroll) - { - _currentCommand = NewCmdNum; - _drawList(); - } - else - { - _updateHighlight(_currentCommand, NewCmdNum); - _currentCommand = NewCmdNum; - } -} - -// Routine Description: -// - Adjusts the highlighted line in a list of commands -// Arguments: -// - OldCurrentCommand - The previous command highlighted -// - NewCurrentCommand - The new command to be highlighted. -void CommandListPopup::_updateHighlight(const CommandHistory::Index OldCurrentCommand, const CommandHistory::Index NewCurrentCommand) -{ - til::CoordType TopIndex; - if (_bottomIndex < Height()) - { - TopIndex = 0; - } - else - { - TopIndex = _bottomIndex - Height() + 1; - } - til::point WriteCoord; - WriteCoord.x = _region.left + 1; - size_t lStringLength = Width(); - - WriteCoord.y = _region.top + 1 + OldCurrentCommand - TopIndex; - - const OutputCellIterator it(_attributes, lStringLength); - const auto done = _screenInfo.Write(it, WriteCoord); - lStringLength = done.GetCellDistance(it); - - // highlight new command - WriteCoord.y = _region.top + 1 + NewCurrentCommand - TopIndex; - - // inverted attributes - auto inverted = _attributes; - inverted.Invert(); - const OutputCellIterator itAttr(inverted, lStringLength); - const auto doneAttr = _screenInfo.Write(itAttr, WriteCoord); - lStringLength = done.GetCellDistance(itAttr); -} diff --git a/src/host/CommandListPopup.hpp b/src/host/CommandListPopup.hpp deleted file mode 100644 index 59bef4c3946..00000000000 --- a/src/host/CommandListPopup.hpp +++ /dev/null @@ -1,50 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CommandListPopup.hpp - -Abstract: -- Popup used for use command list input -- contains code pulled from popup.cpp and cmdline.cpp - -Author: -- Austin Diviness (AustDi) 18-Aug-2018 ---*/ - -#pragma once - -#include "popup.h" - -class CommandListPopup : public Popup -{ -public: - CommandListPopup(SCREEN_INFORMATION& screenInfo, const CommandHistory& history); - - [[nodiscard]] NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; - -protected: - void _DrawContent() override; - -private: - void _drawList(); - void _update(const CommandHistory::Index delta, const bool wrap = false); - void _updateHighlight(const CommandHistory::Index oldCommand, const CommandHistory::Index newCommand); - - void _handleReturn(COOKED_READ_DATA& cookedReadData); - void _cycleSelectionToMatchingCommands(COOKED_READ_DATA& cookedReadData, const wchar_t wch); - void _setBottomIndex(); - [[nodiscard]] NTSTATUS _handlePopupKeys(COOKED_READ_DATA& cookedReadData, const wchar_t wch, const DWORD modifiers) noexcept; - [[nodiscard]] NTSTATUS _deleteSelection(COOKED_READ_DATA& cookedReadData) noexcept; - [[nodiscard]] NTSTATUS _swapUp(COOKED_READ_DATA& cookedReadData) noexcept; - [[nodiscard]] NTSTATUS _swapDown(COOKED_READ_DATA& cookedReadData) noexcept; - - CommandHistory::Index _currentCommand; - CommandHistory::Index _bottomIndex; // number of command displayed on last line of popup - const CommandHistory& _history; - -#ifdef UNIT_TESTING - friend class CommandListPopupTests; -#endif -}; diff --git a/src/host/CommandNumberPopup.cpp b/src/host/CommandNumberPopup.cpp deleted file mode 100644 index 6e990439d70..00000000000 --- a/src/host/CommandNumberPopup.cpp +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "CommandNumberPopup.hpp" - -#include "stream.h" -#include "_stream.h" -#include "cmdline.h" -#include "resource.h" - -#include "../interactivity/inc/ServiceLocator.hpp" - -// 5 digit number for command history -static constexpr size_t COMMAND_NUMBER_LENGTH = 5; - -static constexpr size_t COMMAND_NUMBER_PROMPT_LENGTH = 22; - -CommandNumberPopup::CommandNumberPopup(SCREEN_INFORMATION& screenInfo) : - Popup(screenInfo, { COMMAND_NUMBER_PROMPT_LENGTH + COMMAND_NUMBER_LENGTH, 1 }) -{ - _userInput.reserve(COMMAND_NUMBER_LENGTH); -} - -// Routine Description: -// - handles numerical user input -// Arguments: -// - cookedReadData - read data to operate on -// - wch - digit to handle -void CommandNumberPopup::_handleNumber(COOKED_READ_DATA& cookedReadData, const wchar_t wch) noexcept -{ - if (_userInput.size() < COMMAND_NUMBER_LENGTH) - { - auto CharsToWrite = sizeof(wchar_t); - const auto realAttributes = cookedReadData.ScreenInfo().GetAttributes(); - cookedReadData.ScreenInfo().SetAttributes(_attributes); - size_t NumSpaces; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - _userInput.data(), - _userInput.data() + _userInput.size(), - &wch, - &CharsToWrite, - &NumSpaces, - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr)); - cookedReadData.ScreenInfo().SetAttributes(realAttributes); - try - { - _push(wch); - } - CATCH_LOG(); - } -} - -// Routine Description: -// - handles backspace user input. removes a digit from the user input -// Arguments: -// - cookedReadData - read data to operate on -void CommandNumberPopup::_handleBackspace(COOKED_READ_DATA& cookedReadData) noexcept -{ - if (_userInput.size() > 0) - { - auto CharsToWrite = sizeof(WCHAR); - const auto backspace = UNICODE_BACKSPACE; - const auto realAttributes = cookedReadData.ScreenInfo().GetAttributes(); - cookedReadData.ScreenInfo().SetAttributes(_attributes); - size_t NumSpaces; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - _userInput.data(), - _userInput.data() + _userInput.size(), - &backspace, - &CharsToWrite, - &NumSpaces, - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr)); - cookedReadData.ScreenInfo().SetAttributes(realAttributes); - _pop(); - } -} - -// Routine Description: -// - handles escape user input. cancels the popup -// Arguments: -// - cookedReadData - read data to operate on -void CommandNumberPopup::_handleEscape(COOKED_READ_DATA& cookedReadData) noexcept -{ - CommandLine::Instance().EndAllPopups(); - - // Note that cookedReadData's OriginalCursorPosition is the position before ANY text was entered on the edit line. - // We want to use the position before the cursor was moved for this popup handler specifically, which may - // be *anywhere* in the edit line and will be synchronized with the pointers in the cookedReadData - // structure (BufPtr, etc.) - LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cookedReadData.BeforeDialogCursorPosition(), TRUE)); -} - -// Routine Description: -// - handles return user input. sets the prompt to the history item indicated -// Arguments: -// - cookedReadData - read data to operate on -void CommandNumberPopup::_handleReturn(COOKED_READ_DATA& cookedReadData) noexcept -{ - const auto commandNumber = gsl::narrow(std::min(_parse(), cookedReadData.History().GetNumberOfCommands() - 1)); - - CommandLine::Instance().EndAllPopups(); - SetCurrentCommandLine(cookedReadData, commandNumber); -} - -// Routine Description: -// - This routine handles the command number selection popup. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - CONSOLE_STATUS_READ_COMPLETE - user hit return -[[nodiscard]] NTSTATUS CommandNumberPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto Status = STATUS_SUCCESS; - auto wch = UNICODE_NULL; - auto popupKeys = false; - DWORD modifiers = 0; - - for (;;) - { - Status = _getUserInput(cookedReadData, popupKeys, modifiers, wch); - if (FAILED_NTSTATUS(Status)) - { - return Status; - } - - if (std::iswdigit(wch)) - { - _handleNumber(cookedReadData, wch); - } - else if (wch == UNICODE_BACKSPACE) - { - _handleBackspace(cookedReadData); - } - else if (wch == VK_ESCAPE) - { - _handleEscape(cookedReadData); - break; - } - else if (wch == UNICODE_CARRIAGERETURN) - { - _handleReturn(cookedReadData); - break; - } - } - return CONSOLE_STATUS_WAIT_NO_BLOCK; -} - -void CommandNumberPopup::_DrawContent() -{ - _DrawPrompt(ID_CONSOLE_MSGCMDLINEF9); -} - -// Routine Description: -// - adds single digit number to the popup's number buffer -// Arguments: -// - wch - char of the number to add. must be in the range [L'0', L'9'] -// Note: will throw if wch is out of range -void CommandNumberPopup::_push(const wchar_t wch) -{ - THROW_HR_IF(E_INVALIDARG, !std::iswdigit(wch)); - if (_userInput.size() < COMMAND_NUMBER_LENGTH) - { - _userInput += wch; - } -} - -// Routine Description: -// - removes the last number added to the number buffer -void CommandNumberPopup::_pop() noexcept -{ - if (!_userInput.empty()) - { - _userInput.pop_back(); - } -} - -// Routine Description: -// - get numerical value for the data stored in the number buffer -// Return Value: -// - parsed integer representing the string value found in the number buffer -int CommandNumberPopup::_parse() const noexcept -{ - try - { - return std::stoi(_userInput); - } - catch (...) - { - return 0; - } -} diff --git a/src/host/CommandNumberPopup.hpp b/src/host/CommandNumberPopup.hpp deleted file mode 100644 index 3d0c0c22d81..00000000000 --- a/src/host/CommandNumberPopup.hpp +++ /dev/null @@ -1,45 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CommandNumberPopup.hpp - -Abstract: -- Popup used for use command number input -- contains code pulled from popup.cpp and cmdline.cpp - -Author: -- Austin Diviness (AustDi) 18-Aug-2018 ---*/ - -#pragma once - -#include "popup.h" - -class CommandNumberPopup final : public Popup -{ -public: - explicit CommandNumberPopup(SCREEN_INFORMATION& screenInfo); - - [[nodiscard]] NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; - -protected: - void _DrawContent() override; - -private: - std::wstring _userInput; - - void _handleNumber(COOKED_READ_DATA& cookedReadData, const wchar_t wch) noexcept; - void _handleBackspace(COOKED_READ_DATA& cookedReadData) noexcept; - void _handleEscape(COOKED_READ_DATA& cookedReadData) noexcept; - void _handleReturn(COOKED_READ_DATA& cookedReadData) noexcept; - - void _push(const wchar_t wch); - void _pop() noexcept; - int _parse() const noexcept; - -#ifdef UNIT_TESTING - friend class CommandNumberPopupTests; -#endif -}; diff --git a/src/host/CopyFromCharPopup.cpp b/src/host/CopyFromCharPopup.cpp deleted file mode 100644 index 9b64938225a..00000000000 --- a/src/host/CopyFromCharPopup.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "CopyFromCharPopup.hpp" - -#include "_stream.h" -#include "resource.h" - -static constexpr size_t COPY_FROM_CHAR_PROMPT_LENGTH = 28; - -CopyFromCharPopup::CopyFromCharPopup(SCREEN_INFORMATION& screenInfo) : - Popup(screenInfo, { COPY_FROM_CHAR_PROMPT_LENGTH + 2, 1 }) -{ -} - -// Routine Description: -// - This routine handles the delete from cursor to char popup. It returns when we're out of input or the user has entered a char. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - CONSOLE_STATUS_READ_COMPLETE - user hit return -[[nodiscard]] NTSTATUS CopyFromCharPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept -{ - // get user input - auto Char = UNICODE_NULL; - auto PopupKeys = false; - DWORD modifiers = 0; - auto Status = _getUserInput(cookedReadData, PopupKeys, modifiers, Char); - if (FAILED_NTSTATUS(Status)) - { - return Status; - } - - CommandLine::Instance().EndCurrentPopup(); - - if (PopupKeys && Char == VK_ESCAPE) - { - return CONSOLE_STATUS_WAIT_NO_BLOCK; - } - - const auto span = cookedReadData.SpanAtPointer(); - const auto foundLocation = std::find(std::next(span.begin()), span.end(), Char); - if (foundLocation == span.end()) - { - // char not found, delete everything to the right of the cursor - CommandLine::Instance().DeletePromptAfterCursor(cookedReadData); - } - else - { - // char was found, delete everything between the cursor and it - const auto difference = std::distance(span.begin(), foundLocation); - for (unsigned int i = 0; i < gsl::narrow(difference); ++i) - { - CommandLine::Instance().DeleteFromRightOfCursor(cookedReadData); - } - } - return CONSOLE_STATUS_WAIT_NO_BLOCK; -} - -void CopyFromCharPopup::_DrawContent() -{ - _DrawPrompt(ID_CONSOLE_MSGCMDLINEF4); -} diff --git a/src/host/CopyFromCharPopup.hpp b/src/host/CopyFromCharPopup.hpp deleted file mode 100644 index ee40f09be11..00000000000 --- a/src/host/CopyFromCharPopup.hpp +++ /dev/null @@ -1,29 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CopyFromCharPopup.hpp - -Abstract: -- Popup used for use copying from char input -- contains code pulled from popup.cpp and cmdline.cpp - -Author: -- Austin Diviness (AustDi) 18-Aug-2018 ---*/ - -#pragma once - -#include "popup.h" - -class CopyFromCharPopup final : public Popup -{ -public: - explicit CopyFromCharPopup(SCREEN_INFORMATION& screenInfo); - - [[nodiscard]] NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; - -protected: - void _DrawContent() override; -}; diff --git a/src/host/CopyToCharPopup.cpp b/src/host/CopyToCharPopup.cpp deleted file mode 100644 index 35fc69405ad..00000000000 --- a/src/host/CopyToCharPopup.cpp +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "CopyToCharPopup.hpp" - -#include "stream.h" -#include "_stream.h" -#include "resource.h" - -static constexpr size_t COPY_TO_CHAR_PROMPT_LENGTH = 26; - -CopyToCharPopup::CopyToCharPopup(SCREEN_INFORMATION& screenInfo) : - Popup(screenInfo, { COPY_TO_CHAR_PROMPT_LENGTH + 2, 1 }) -{ -} - -// Routine Description: -// - copies text from the previous command into the current prompt line, up to but not including the first -// instance of wch after the current cookedReadData's cursor position. if wch is not found, nothing is copied. -// Arguments: -// - cookedReadData - the read data to operate on -// - LastCommand - the most recent command run -// - wch - the wchar to copy up to -void CopyToCharPopup::_copyToChar(COOKED_READ_DATA& cookedReadData, const std::wstring_view LastCommand, const wchar_t wch) -{ - // make sure that there it is possible to copy any found text over - if (cookedReadData.InsertionPoint() >= LastCommand.size()) - { - return; - } - - const auto searchStart = std::next(LastCommand.cbegin(), cookedReadData.InsertionPoint() + 1); - auto location = std::find(searchStart, LastCommand.cend(), wch); - - // didn't find wch so copy nothing - if (location == LastCommand.cend()) - { - return; - } - - const auto startIt = std::next(LastCommand.cbegin(), cookedReadData.InsertionPoint()); - const auto endIt = location; - - cookedReadData.Write({ &*startIt, gsl::narrow(std::distance(startIt, endIt)) }); -} - -// Routine Description: -// - This routine handles the delete char popup. It returns when we're out of input or the user has entered a char. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - CONSOLE_STATUS_READ_COMPLETE - user hit return -[[nodiscard]] NTSTATUS CopyToCharPopup::Process(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto wch = UNICODE_NULL; - auto popupKey = false; - DWORD modifiers = 0; - auto Status = _getUserInput(cookedReadData, popupKey, modifiers, wch); - if (FAILED_NTSTATUS(Status)) - { - return Status; - } - - CommandLine::Instance().EndCurrentPopup(); - - if (popupKey && wch == VK_ESCAPE) - { - return CONSOLE_STATUS_WAIT_NO_BLOCK; - } - - // copy up to specified char - const auto lastCommand = cookedReadData.History().GetLastCommand(); - if (!lastCommand.empty()) - { - _copyToChar(cookedReadData, lastCommand, wch); - } - - return CONSOLE_STATUS_WAIT_NO_BLOCK; -} - -void CopyToCharPopup::_DrawContent() -{ - _DrawPrompt(ID_CONSOLE_MSGCMDLINEF2); -} diff --git a/src/host/CopyToCharPopup.hpp b/src/host/CopyToCharPopup.hpp deleted file mode 100644 index a1c490d3b3d..00000000000 --- a/src/host/CopyToCharPopup.hpp +++ /dev/null @@ -1,32 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CopyToCharPopup.hpp - -Abstract: -- Popup used for use copying to char input -- contains code pulled from popup.cpp and cmdline.cpp - -Author: -- Austin Diviness (AustDi) 18-Aug-2018 ---*/ - -#pragma once - -#include "popup.h" - -class CopyToCharPopup final : public Popup -{ -public: - CopyToCharPopup(SCREEN_INFORMATION& screenInfo); - - [[nodiscard]] NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept override; - -protected: - void _DrawContent() override; - -private: - void _copyToChar(COOKED_READ_DATA& cookedReadData, const std::wstring_view LastCommand, const wchar_t wch); -}; diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index cc858f74bef..5ff5c68f5db 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -39,7 +39,7 @@ using Microsoft::Console::VirtualTerminal::StateMachine; // - coordCursor - New location of cursor. // - fKeepCursorVisible - TRUE if changing window origin desirable when hit right edge // Return Value: -void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor, const BOOL fKeepCursorVisible, _Inout_opt_ til::CoordType* psScrollY) +static void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor, const bool interactive, _Inout_opt_ til::CoordType* psScrollY) { const auto bufferSize = screenInfo.GetBufferSize().Dimensions(); if (coordCursor.x < 0) @@ -77,7 +77,7 @@ void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordC { *psScrollY += bufferSize.height - coordCursor.y - 1; } - coordCursor.y += bufferSize.height - coordCursor.y - 1; + coordCursor.y = bufferSize.height - 1; } const auto cursorMovedPastViewport = coordCursor.y > screenInfo.GetViewport().BottomInclusive(); @@ -91,21 +91,19 @@ void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordC LOG_IF_FAILED(screenInfo.SetViewportOrigin(false, WindowOrigin, true)); } - if (fKeepCursorVisible) + if (interactive) { screenInfo.MakeCursorVisible(coordCursor); } - LOG_IF_FAILED(screenInfo.SetCursorPosition(coordCursor, !!fKeepCursorVisible)); + LOG_IF_FAILED(screenInfo.SetCursorPosition(coordCursor, interactive)); } // As the name implies, this writes text without processing its control characters. -static size_t _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const DWORD dwFlags, til::CoordType* const psScrollY, const std::wstring_view& text) +static void _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const std::wstring_view& text, const bool interactive, til::CoordType* psScrollY) { - const auto keepCursorVisible = WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE); const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT); const auto hasAccessibilityEventing = screenInfo.HasAccessibilityEventing(); auto& textBuffer = screenInfo.GetTextBuffer(); - size_t numSpaces = 0; RowWriteState state{ .text = text, @@ -120,8 +118,6 @@ static size_t _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const textBuffer.Write(cursorPosition.y, textBuffer.GetCurrentAttributes(), state); cursorPosition.x = state.columnEnd; - numSpaces += gsl::narrow_cast(state.columnEnd - state.columnBegin); - if (wrapAtEOL && state.columnEnd >= state.columnLimit) { textBuffer.SetWrapForced(cursorPosition.y, true); @@ -132,50 +128,22 @@ static size_t _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const screenInfo.NotifyAccessibilityEventing(state.columnBegin, cursorPosition.y, state.columnEnd - 1, cursorPosition.y); } - AdjustCursorPosition(screenInfo, cursorPosition, keepCursorVisible, psScrollY); + AdjustCursorPosition(screenInfo, cursorPosition, interactive, psScrollY); } - - return numSpaces; } -// Routine Description: -// - This routine writes a string to the screen, processing any embedded -// unicode characters. The string is also copied to the input buffer, if -// the output mode is line mode. -// Arguments: -// - screenInfo - reference to screen buffer information structure. -// - pwchBufferBackupLimit - Pointer to beginning of buffer. -// - pwchBuffer - Pointer to buffer to copy string to. assumed to be at least as long as pwchRealUnicode. -// This pointer is updated to point to the next position in the buffer. -// - pwchRealUnicode - Pointer to string to write. -// - pcb - On input, number of bytes to write. On output, number of bytes written. -// - pcSpaces - On output, the number of spaces consumed by the written characters. -// - dwFlags - -// WC_INTERACTIVE backspace overwrites characters, control characters are expanded (as in, to "^X") -// WC_KEEP_CURSOR_VISIBLE change window origin desirable when hit rt. edge -// Return Value: -// Note: -// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services. -[[nodiscard]] NTSTATUS WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, - _In_range_(<=, pwchBuffer) const wchar_t* const pwchBufferBackupLimit, - _In_ const wchar_t* pwchBuffer, - _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, - _Inout_ size_t* const pcb, - _Out_opt_ size_t* const pcSpaces, - const til::CoordType sOriginalXPosition, - const DWORD dwFlags, - _Inout_opt_ til::CoordType* const psScrollY) -try +// This routine writes a string to the screen while handling control characters. +// `interactive` exists for COOKED_READ_DATA which uses it to transform control characters into visible text like "^X". +// Similarly, `psScrollY` is also used by it to track whether the underlying buffer circled. It requires this information to know where the input line moved to. +void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& text, const bool interactive, til::CoordType* psScrollY) { static constexpr wchar_t tabSpaces[8]{ L' ', L' ', L' ', L' ', L' ', L' ', L' ', L' ' }; auto& textBuffer = screenInfo.GetTextBuffer(); auto& cursor = textBuffer.GetCursor(); - const auto keepCursorVisible = WI_IsFlagSet(dwFlags, WC_KEEP_CURSOR_VISIBLE); const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT); - auto it = pwchRealUnicode; - const auto end = it + *pcb / sizeof(wchar_t); - size_t numSpaces = 0; + auto it = text.begin(); + const auto end = text.end(); // In VT mode, when you have a 120-column terminal you can write 120 columns without the cursor wrapping. // Whenever the cursor is in that 120th column IsDelayedEOLWrap() will return true. I'm not sure why the VT parts @@ -192,7 +160,7 @@ try { pos.x = 0; pos.y++; - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); } } @@ -200,7 +168,7 @@ try // If it's not set, we can just straight up give everything to _writeCharsLegacyUnprocessed. if (WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)) { - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { it, end }); + _writeCharsLegacyUnprocessed(screenInfo, { it, end }, interactive, psScrollY); it = end; } @@ -209,7 +177,7 @@ try const auto nextControlChar = std::find_if(it, end, [](const auto& wch) { return !IS_GLYPH_CHAR(wch); }); if (nextControlChar != it) { - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { it, nextControlChar }); + _writeCharsLegacyUnprocessed(screenInfo, { it, nextControlChar }, interactive, psScrollY); it = nextControlChar; } @@ -218,14 +186,14 @@ try switch (*it) { case UNICODE_NULL: - if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE)) + if (interactive) { break; } - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &tabSpaces[0], 1 }); + _writeCharsLegacyUnprocessed(screenInfo, { &tabSpaces[0], 1 }, interactive, psScrollY); continue; case UNICODE_BELL: - if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE)) + if (interactive) { break; } @@ -233,171 +201,20 @@ try continue; case UNICODE_BACKSPACE: { + // Backspace handling for interactive mode should happen in COOKED_READ_DATA + // where it has full control over the text and can delete it directly. + // Otherwise handling backspacing tabs/whitespace can turn up complex and bug-prone. + assert(!interactive); auto pos = cursor.GetPosition(); - - if (WI_IsFlagClear(dwFlags, WC_INTERACTIVE)) - { - pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(pos.x); - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); - continue; - } - - const auto moveUp = [&]() { - pos.x = -1; - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); - - const auto y = cursor.GetPosition().y; - auto& row = textBuffer.GetMutableRowByOffset(y); - - pos.x = textBuffer.GetSize().RightExclusive(); - pos.y = y; - - if (row.WasDoubleBytePadded()) - { - pos.x--; - numSpaces--; - } - - row.SetWrapForced(false); - row.SetDoubleBytePadded(false); - }; - - // We have to move up early because the tab handling code below needs to be on - // the row of the tab already, so that we can call GetText() for precedingText. - if (pos.x == 0 && pos.y != 0) - { - moveUp(); - } - - til::CoordType glyphCount = 1; - - if (pwchBuffer != pwchBufferBackupLimit) - { - const auto lastChar = pwchBuffer[-1]; - - // Deleting tabs is a bit tricky, because they have a variable width between 1 and 8 spaces, - // are stored as whitespace but are technically distinct from whitespace. - if (lastChar == UNICODE_TAB) - { - const auto precedingText = textBuffer.GetRowByOffset(pos.y).GetText(pos.x - 8, pos.x); - - // First, we measure the amount of spaces that precede the cursor in the text buffer, - // which is generally the amount of spaces that we end up deleting. We do it this way, - // because we don't know what kind of complex mix of wide/narrow glyphs precede the tab. - // Basically, by asking the text buffer we get the size information of the preceding text. - if (precedingText.size() >= 2 && precedingText.back() == L' ') - { - auto textIt = precedingText.rbegin() + 1; - const auto textEnd = precedingText.rend(); - - for (; textIt != textEnd && *textIt == L' '; ++textIt) - { - glyphCount++; - } - } - - // But there's a problem: When you print " \t" it should delete 6 spaces and not 8. - // In other words, we shouldn't delete any actual preceding whitespaces. We can ask - // the "backup" buffer (= preceding text in the commandline) for this information. - // - // backupEnd points to the character immediately preceding the tab (LastChar). - const auto backupEnd = pwchBuffer - 1; - // backupLimit points to how far back we need to search. Even if we have 9000 characters in our command line, - // we'll only need to check a total of 8 whitespaces. "pwchBuffer - pwchBufferBackupLimit" will - // always be at least 1 because that's the \t character in the backup buffer. In other words, - // backupLimit will at a minimum be equal to backupEnd, or precede it by 7 more characters. - const auto backupLimit = pwchBuffer - std::min(8, pwchBuffer - pwchBufferBackupLimit); - // Now count how many spaces precede the \t character. "backupEnd - backupBeg" will be the amount. - auto backupBeg = backupEnd; - for (; backupBeg != backupLimit && backupBeg[-1] == L' '; --backupBeg, --glyphCount) - { - } - - // There's one final problem: A prompt like... - // fputs("foo: ", stdout); - // fgets(buffer, stdin); - // ...has a trailing whitespace in front of our pwchBufferBackupLimit which we should not backspace over. - // sOriginalXPosition stores the start of the prompt at the pwchBufferBackupLimit. - if (backupBeg == pwchBufferBackupLimit) - { - glyphCount = pos.x - sOriginalXPosition; - } - - // Now that we finally know how many columns precede the cursor we can - // subtract the previously determined amount of ' ' from the '\t'. - glyphCount -= gsl::narrow_cast(backupEnd - backupBeg); - - // Can the above code leave glyphCount <= 0? Let's just not find out! - glyphCount = std::max(1, glyphCount); - } - // Control chars in interactive mode were previously written out - // as ^X for instance, so now we also need to delete 2 glyphs. - else if (IS_CONTROL_CHAR(lastChar)) - { - glyphCount = 2; - } - } - - for (;;) - { - // We've already moved up if the cursor was in the first column so - // we need to start off with overwriting the text with whitespace. - // It wouldn't make sense to check the cursor position again already. - { - const auto previousColumn = pos.x; - pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(previousColumn); - - RowWriteState state{ - .text = { &tabSpaces[0], 8 }, - .columnBegin = pos.x, - .columnLimit = previousColumn, - }; - textBuffer.Write(pos.y, textBuffer.GetCurrentAttributes(), state); - numSpaces -= previousColumn - pos.x; - } - - // The cursor movement logic is a little different for the last iteration, so we exit early here. - glyphCount--; - if (glyphCount <= 0) - { - break; - } - - // Otherwise, in case we need to delete 2 or more glyphs, we need to ensure we properly wrap lines back up. - if (pos.x == 0 && pos.y != 0) - { - moveUp(); - } - } - - // After the last iteration the cursor might now be in the first column after a line - // that was previously padded with a whitespace in the last column due to a wide glyph. - // Now that the wide glyph is presumably gone, we can move up a line. - if (pos.x == 0 && pos.y != 0 && textBuffer.GetRowByOffset(pos.y - 1).WasDoubleBytePadded()) - { - moveUp(); - } - else - { - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); - } - - // Notify accessibility to read the backspaced character. - // See GH:12735, MSFT:31748387 - if (screenInfo.HasAccessibilityEventing()) - { - if (const auto pConsoleWindow = ServiceLocator::LocateConsoleWindow()) - { - LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId)); - } - } + pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(pos.x); + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); continue; } case UNICODE_TAB: { const auto pos = cursor.GetPosition(); - const auto tabCount = gsl::narrow_cast(NUMBER_OF_SPACES_IN_TAB(pos.x)); - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &tabSpaces[0], tabCount }); + const auto tabCount = gsl::narrow_cast(8 - (pos.x & 7)); + _writeCharsLegacyUnprocessed(screenInfo, { &tabSpaces[0], tabCount }, interactive, psScrollY); continue; } case UNICODE_LINEFEED: @@ -410,24 +227,25 @@ try textBuffer.GetMutableRowByOffset(pos.y).SetWrapForced(false); pos.y = pos.y + 1; - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); continue; } case UNICODE_CARRIAGERETURN: { auto pos = cursor.GetPosition(); pos.x = 0; - AdjustCursorPosition(screenInfo, pos, keepCursorVisible, psScrollY); + AdjustCursorPosition(screenInfo, pos, interactive, psScrollY); continue; } default: break; } - if (WI_IsFlagSet(dwFlags, WC_INTERACTIVE) && IS_CONTROL_CHAR(*it)) + // In the interactive mode we replace C0 control characters (0x00-0x1f) with ASCII representations like ^C (= 0x03). + if (interactive && *it < L' ') { const wchar_t wchs[2]{ L'^', static_cast(*it + L'@') }; - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &wchs[0], 2 }); + _writeCharsLegacyUnprocessed(screenInfo, { &wchs[0], 2 }, interactive, psScrollY); } else { @@ -439,76 +257,12 @@ try const auto result = MultiByteToWideChar(cp, MB_USEGLYPHCHARS, &ch, 1, &wch, 1); if (result == 1) { - numSpaces += _writeCharsLegacyUnprocessed(screenInfo, dwFlags, psScrollY, { &wch, 1 }); + _writeCharsLegacyUnprocessed(screenInfo, { &wch, 1 }, interactive, psScrollY); } } } } - - if (pcSpaces) - { - *pcSpaces = numSpaces; - } - - return S_OK; } -NT_CATCH_RETURN() - -// Routine Description: -// - This routine writes a string to the screen, processing any embedded -// unicode characters. The string is also copied to the input buffer, if -// the output mode is line mode. -// Arguments: -// - screenInfo - reference to screen buffer information structure. -// - pwchBufferBackupLimit - Pointer to beginning of buffer. -// - pwchBuffer - Pointer to buffer to copy string to. assumed to be at least as long as pwchRealUnicode. -// This pointer is updated to point to the next position in the buffer. -// - pwchRealUnicode - Pointer to string to write. -// - pcb - On input, number of bytes to write. On output, number of bytes written. -// - pcSpaces - On output, the number of spaces consumed by the written characters. -// - dwFlags - -// WC_INTERACTIVE backspace overwrites characters, control characters are expanded (as in, to "^X") -// WC_KEEP_CURSOR_VISIBLE change window origin (viewport) desirable when hit rt. edge -// Return Value: -// Note: -// - This routine does not process tabs and backspace properly. That code will be implemented as part of the line editing services. -[[nodiscard]] NTSTATUS WriteChars(SCREEN_INFORMATION& screenInfo, - _In_range_(<=, pwchBuffer) const wchar_t* const pwchBufferBackupLimit, - _In_ const wchar_t* pwchBuffer, - _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, - _Inout_ size_t* const pcb, - _Out_opt_ size_t* const pcSpaces, - const til::CoordType sOriginalXPosition, - const DWORD dwFlags, - _Inout_opt_ til::CoordType* const psScrollY) -try -{ - if (WI_IsAnyFlagClear(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT)) - { - return WriteCharsLegacy(screenInfo, - pwchBufferBackupLimit, - pwchBuffer, - pwchRealUnicode, - pcb, - pcSpaces, - sOriginalXPosition, - dwFlags, - psScrollY); - } - - auto& machine = screenInfo.GetStateMachine(); - const auto cch = *pcb / sizeof(WCHAR); - - machine.ProcessString({ pwchRealUnicode, cch }); - - if (nullptr != pcSpaces) - { - *pcSpaces = 0; - } - - return STATUS_SUCCESS; -} -NT_CATCH_RETURN() // Routine Description: // - Takes the given text and inserts it into the given screen buffer. @@ -530,23 +284,16 @@ NT_CATCH_RETURN() SCREEN_INFORMATION& screenInfo, bool requiresVtQuirk, std::unique_ptr& waiter) +try { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); if (WI_IsAnyFlagSet(gci.Flags, (CONSOLE_SUSPENDED | CONSOLE_SELECTING | CONSOLE_SCROLLBAR_TRACKING))) { - try - { - waiter = std::make_unique(screenInfo, - pwchBuffer, - *pcbBuffer, - gci.OutputCP, - requiresVtQuirk); - } - catch (...) - { - return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); - } - + waiter = std::make_unique(screenInfo, + pwchBuffer, + *pcbBuffer, + gci.OutputCP, + requiresVtQuirk); return CONSOLE_STATUS_WAIT; } @@ -563,17 +310,20 @@ NT_CATCH_RETURN() restoreVtQuirk.release(); } - const auto& textBuffer = screenInfo.GetTextBuffer(); - return WriteChars(screenInfo, - pwchBuffer, - pwchBuffer, - pwchBuffer, - pcbBuffer, - nullptr, - textBuffer.GetCursor().GetPosition().x, - 0, - nullptr); + const std::wstring_view str{ pwchBuffer, *pcbBuffer / sizeof(WCHAR) }; + + if (WI_IsAnyFlagClear(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT)) + { + WriteCharsLegacy(screenInfo, str, false, nullptr); + } + else + { + screenInfo.GetStateMachine().ProcessString(str); + } + + return STATUS_SUCCESS; } +NT_CATCH_RETURN() // Routine Description: // - This method performs the actual work of attempting to write to the console, converting data types as necessary diff --git a/src/host/_stream.h b/src/host/_stream.h index 58351c86cc1..794591a03f0 100644 --- a/src/host/_stream.h +++ b/src/host/_stream.h @@ -17,76 +17,13 @@ Revision History: #pragma once -#include "../server/IWaitRoutine.h" #include "writeData.hpp" -/*++ -Routine Description: - This routine updates the cursor position. Its input is the non-special - cased new location of the cursor. For example, if the cursor were being - moved one space backwards from the left edge of the screen, the X - coordinate would be -1. This routine would set the X coordinate to - the right edge of the screen and decrement the Y coordinate by one. - -Arguments: - pScreenInfo - Pointer to screen buffer information structure. - coordCursor - New location of cursor. - fKeepCursorVisible - TRUE if changing window origin desirable when hit right edge - -Return Value: ---*/ -void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor, const BOOL fKeepCursorVisible, _Inout_opt_ til::CoordType* psScrollY); - -/*++ -Routine Description: - This routine writes a string to the screen, processing any embedded - unicode characters. The string is also copied to the input buffer, if - the output mode is line mode. - -Arguments: - ScreenInfo - Pointer to screen buffer information structure. - lpBufferBackupLimit - Pointer to beginning of buffer. - lpBuffer - Pointer to buffer to copy string to. assumed to be at least - as long as lpRealUnicodeString. This pointer is updated to point to the - next position in the buffer. - lpRealUnicodeString - Pointer to string to write. - NumBytes - On input, number of bytes to write. On output, number of - bytes written. - NumSpaces - On output, the number of spaces consumed by the written characters. - dwFlags - - WC_INTERACTIVE backspace overwrites characters, control characters are expanded (as in, to "^X") - WC_KEEP_CURSOR_VISIBLE change window origin desirable when hit rt. edge - -Return Value: - -Note: - This routine does not process tabs and backspace properly. That code - will be implemented as part of the line editing services. ---*/ -[[nodiscard]] NTSTATUS WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, - _In_range_(<=, pwchBuffer) const wchar_t* const pwchBufferBackupLimit, - _In_ const wchar_t* pwchBuffer, - _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, - _Inout_ size_t* const pcb, - _Out_opt_ size_t* const pcSpaces, - const til::CoordType sOriginalXPosition, - const DWORD dwFlags, - _Inout_opt_ til::CoordType* const psScrollY); - -// The new entry point for WriteChars to act as an intercept in case we place a Virtual Terminal processor in the way. -[[nodiscard]] NTSTATUS WriteChars(SCREEN_INFORMATION& screenInfo, - _In_range_(<=, pwchBuffer) const wchar_t* const pwchBufferBackupLimit, - _In_ const wchar_t* pwchBuffer, - _In_reads_bytes_(*pcb) const wchar_t* pwchRealUnicode, - _Inout_ size_t* const pcb, - _Out_opt_ size_t* const pcSpaces, - const til::CoordType sOriginalXPosition, - const DWORD dwFlags, - _Inout_opt_ til::CoordType* const psScrollY); +void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& str, bool interactive, til::CoordType* psScrollY); // NOTE: console lock must be held when calling this routine // String has been translated to unicode at this point. -[[nodiscard]] NTSTATUS DoWriteConsole(_In_reads_bytes_(*pcbBuffer) PCWCHAR pwchBuffer, +[[nodiscard]] NTSTATUS DoWriteConsole(_In_reads_bytes_(pcbBuffer) const wchar_t* pwchBuffer, _Inout_ size_t* const pcbBuffer, SCREEN_INFORMATION& screenInfo, bool requiresVtQuirk, diff --git a/src/host/alias.cpp b/src/host/alias.cpp index 85fc3d0875b..a82fa198761 100644 --- a/src/host/alias.cpp +++ b/src/host/alias.cpp @@ -817,32 +817,19 @@ void Alias::s_ClearCmdExeAliases() CATCH_RETURN(); } -// Routine Description: -// - Trims trailing \r\n off of a string -// Arguments: -// - str - String to trim -void Alias::s_TrimTrailingCrLf(std::wstring& str) -{ - const auto trailingCrLfPos = str.find_last_of(UNICODE_CARRIAGERETURN); - if (std::wstring::npos != trailingCrLfPos) - { - str.erase(trailingCrLfPos); - } -} - // Routine Description: // - Tokenizes a string into a collection using space as a separator // Arguments: // - str - String to tokenize // Return Value: // - Collection of tokenized strings -std::deque Alias::s_Tokenize(const std::wstring& str) +std::deque Alias::s_Tokenize(const std::wstring_view str) { std::deque result; size_t prevIndex = 0; auto spaceIndex = str.find(L' '); - while (std::wstring::npos != spaceIndex) + while (std::wstring_view::npos != spaceIndex) { const auto length = spaceIndex - prevIndex; @@ -867,11 +854,11 @@ std::deque Alias::s_Tokenize(const std::wstring& str) // - str - String to split into just args // Return Value: // - Only the arguments part of the string or empty if there are no arguments. -std::wstring Alias::s_GetArgString(const std::wstring& str) +std::wstring Alias::s_GetArgString(const std::wstring_view str) { std::wstring result; auto firstSpace = str.find_first_of(L' '); - if (std::wstring::npos != firstSpace) + if (std::wstring_view::npos != firstSpace) { firstSpace++; if (firstSpace < str.size()) @@ -1126,16 +1113,8 @@ size_t Alias::s_ReplaceMacros(std::wstring& str, // - If we found a matching alias, this will be the processed data // and lineCount is updated to the new number of lines. // - If we didn't match and process an alias, return an empty string. -std::wstring Alias::s_MatchAndCopyAlias(const std::wstring& sourceText, - const std::wstring& exeName, - size_t& lineCount) +std::wstring Alias::s_MatchAndCopyAlias(std::wstring_view sourceText, const std::wstring& exeName, size_t& lineCount) { - // Copy source text into a local for manipulation. - auto sourceCopy = sourceText; - - // Trim trailing \r\n off of sourceCopy if it has one. - s_TrimTrailingCrLf(sourceCopy); - // Check if we have an EXE in the list that matches the request first. auto exeIter = g_aliasData.find(exeName); if (exeIter == g_aliasData.end()) @@ -1152,7 +1131,7 @@ std::wstring Alias::s_MatchAndCopyAlias(const std::wstring& sourceText, } // Tokenize the text by spaces - const auto tokens = s_Tokenize(sourceCopy); + const auto tokens = s_Tokenize(sourceText); // If there are no tokens, return an empty string if (tokens.size() == 0) @@ -1169,14 +1148,14 @@ std::wstring Alias::s_MatchAndCopyAlias(const std::wstring& sourceText, return std::wstring(); } - const auto target = aliasIter->second; + const auto& target = aliasIter->second; if (target.size() == 0) { return std::wstring(); } // Get the string of all parameters as a shorthand for $* later. - const auto allParams = s_GetArgString(sourceCopy); + const auto allParams = s_GetArgString(sourceText); // The final text will be the target but with macros replaced. auto finalText = target; @@ -1185,59 +1164,6 @@ std::wstring Alias::s_MatchAndCopyAlias(const std::wstring& sourceText, return finalText; } -// Routine Description: -// - This routine matches the input string with an alias and copies the alias to the input buffer. -// Arguments: -// - pwchSource - string to match -// - cbSource - length of pwchSource in bytes -// - pwchTarget - where to store matched string -// - cbTargetSize - on input, contains size of pwchTarget. -// - cbTargetWritten - On output, contains length of alias stored in pwchTarget. -// - pwchExe - Name of exe that command is associated with to find related aliases -// - cbExe - Length in bytes of exe name -// - LineCount - aliases can contain multiple commands. $T is the command separator -// Return Value: -// - None. It will just maintain the source as the target if we can't match an alias. -void Alias::s_MatchAndCopyAliasLegacy(_In_reads_bytes_(cbSource) PCWCH pwchSource, - _In_ size_t cbSource, - _Out_writes_bytes_(cbTargetWritten) PWCHAR pwchTarget, - _In_ const size_t cbTargetSize, - size_t& cbTargetWritten, - const std::wstring& exeName, - DWORD& lines) -{ - try - { - std::wstring sourceText(pwchSource, cbSource / sizeof(WCHAR)); - size_t lineCount = lines; - - const auto targetText = s_MatchAndCopyAlias(sourceText, exeName, lineCount); - - // Only return data if the reply was non-empty (we had a match). - if (!targetText.empty()) - { - const auto cchTargetSize = cbTargetSize / sizeof(wchar_t); - - // If the target text will fit in the result buffer, fill out the results. - if (targetText.size() <= cchTargetSize) - { - // Non-null terminated copy into memory space - std::copy_n(targetText.data(), targetText.size(), pwchTarget); - - // Return bytes copied. - cbTargetWritten = gsl::narrow(targetText.size() * sizeof(wchar_t)); - - // Return lines info. - lines = gsl::narrow(lineCount); - } - } - } - catch (...) - { - LOG_HR(wil::ResultFromCaughtException()); - } -} - #ifdef UNIT_TESTING void Alias::s_TestAddAlias(std::wstring& exe, std::wstring& alias, diff --git a/src/host/alias.h b/src/host/alias.h index 1a745c601f7..9c8bc38d534 100644 --- a/src/host/alias.h +++ b/src/host/alias.h @@ -16,22 +16,11 @@ class Alias public: static void s_ClearCmdExeAliases(); - static void s_MatchAndCopyAliasLegacy(_In_reads_bytes_(cbSource) PCWCH pwchSource, - _In_ size_t cbSource, - _Out_writes_bytes_(cbTargetWritten) PWCHAR pwchTarget, - _In_ const size_t cbTargetSize, - size_t& cbTargetWritten, - const std::wstring& exeName, - DWORD& lines); - - static std::wstring s_MatchAndCopyAlias(const std::wstring& sourceText, - const std::wstring& exeName, - size_t& lineCount); + static std::wstring s_MatchAndCopyAlias(std::wstring_view sourceText, const std::wstring& exeName, size_t& lineCount); private: - static void s_TrimTrailingCrLf(std::wstring& str); - static std::deque s_Tokenize(const std::wstring& str); - static std::wstring s_GetArgString(const std::wstring& str); + static std::deque s_Tokenize(const std::wstring_view str); + static std::wstring s_GetArgString(const std::wstring_view str); static size_t s_ReplaceMacros(std::wstring& str, const std::deque& tokens, const std::wstring& fullArgString); diff --git a/src/host/cmdline.cpp b/src/host/cmdline.cpp index 1e1587e37fc..c523ad9ab3c 100644 --- a/src/host/cmdline.cpp +++ b/src/host/cmdline.cpp @@ -2,25 +2,7 @@ // Licensed under the MIT license. #include "precomp.h" - #include "cmdline.h" -#include "popup.h" -#include "CommandNumberPopup.hpp" -#include "CommandListPopup.hpp" -#include "CopyFromCharPopup.hpp" -#include "CopyToCharPopup.hpp" - -#include "_output.h" -#include "output.h" -#include "stream.h" -#include "_stream.h" -#include "dbcs.h" -#include "handle.h" -#include "misc.h" -#include "../types/inc/convert.hpp" -#include "srvinit.h" - -#include "ApiRoutines.h" #include "../interactivity/inc/ServiceLocator.hpp" @@ -29,7 +11,7 @@ using Microsoft::Console::Interactivity::ServiceLocator; // Routine Description: // - Detects Word delimiters -bool IsWordDelim(const wchar_t wch) +bool IsWordDelim(const wchar_t wch) noexcept { // the space character is always a word delimiter. Do not add it to the WordDelimiters global because // that contains the user configurable word delimiters only. @@ -41,1219 +23,24 @@ bool IsWordDelim(const wchar_t wch) return std::ranges::find(delimiters, wch) != delimiters.end(); } -bool IsWordDelim(const std::wstring_view charData) +bool IsWordDelim(const std::wstring_view& charData) noexcept { return charData.size() == 1 && IsWordDelim(charData.front()); } -CommandLine::CommandLine() : - _isVisible{ true } +// Returns a truthy value for delimiters and 0 otherwise. +// The distinction between whitespace and other delimiters allows us to +// implement Windows' inconsistent, but classic, word-wise navigation. +int DelimiterClass(wchar_t wch) noexcept { -} - -CommandLine::~CommandLine() = default; - -CommandLine& CommandLine::Instance() -{ - static CommandLine c; - return c; -} - -bool CommandLine::IsEditLineEmpty() -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - if (!gci.HasPendingCookedRead()) - { - // If the cooked read data pointer is null, there is no edit line data and therefore it's empty. - return true; - } - else if (0 == gci.CookedReadData().VisibleCharCount()) - { - // If we had a valid pointer, but there are no visible characters for the edit line, then it's empty. - // Someone started editing and back spaced the whole line out so it exists, but has no data. - return true; - } - else + if (wch == L' ') { - return false; + return 1; } -} - -void CommandLine::Hide(const bool fUpdateFields) -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - if (!IsEditLineEmpty()) - { - DeleteCommandLine(gci.CookedReadData(), fUpdateFields); - } - _isVisible = false; -} - -void CommandLine::Show() -{ - _isVisible = true; - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - if (!IsEditLineEmpty()) - { - RedrawCommandLine(gci.CookedReadData()); - } -} - -// Routine Description: -// - Returns true if the commandline is currently being displayed. This is false -// after Hide() is called, and before Show() is called again. -// Return Value: -// - true if the commandline should be displayed. Does not take into account -// the echo state of the input. This is only controlled by calls to Hide/Show -bool CommandLine::IsVisible() const noexcept -{ - return _isVisible; -} - -// Routine Description: -// - checks for the presence of a popup -// Return Value: -// - true if popup is present -bool CommandLine::HasPopup() const noexcept -{ - return !_popups.empty(); -} - -// Routine Description: -// - gets the topmost popup -// Arguments: -// Return Value: -// - ref to the topmost popup -Popup& CommandLine::GetPopup() const -{ - return *_popups.front(); -} - -// Routine Description: -// - stops the current popup -void CommandLine::EndCurrentPopup() -{ - if (!_popups.empty()) - { - _popups.front()->End(); - _popups.pop_front(); - } -} - -// Routine Description: -// - stops all popups -void CommandLine::EndAllPopups() -{ - while (!_popups.empty()) - { - _popups.front()->End(); - _popups.pop_front(); - } -} - -void DeleteCommandLine(COOKED_READ_DATA& cookedReadData, const bool fUpdateFields) -{ - auto CharsToWrite = cookedReadData.VisibleCharCount(); - auto coordOriginalCursor = cookedReadData.OriginalCursorPosition(); - const auto coordBufferSize = cookedReadData.ScreenInfo().GetBufferSize().Dimensions(); - - // catch the case where the current command has scrolled off the top of the screen. - if (coordOriginalCursor.y < 0) - { - CharsToWrite += coordBufferSize.width * coordOriginalCursor.y; - CharsToWrite += cookedReadData.OriginalCursorPosition().x; // account for prompt - cookedReadData.OriginalCursorPosition().x = 0; - cookedReadData.OriginalCursorPosition().y = 0; - coordOriginalCursor.x = 0; - coordOriginalCursor.y = 0; - } - - if (!CheckBisectStringW(cookedReadData.BufferStartPtr(), - CharsToWrite, - coordBufferSize.width - cookedReadData.OriginalCursorPosition().x)) - { - CharsToWrite++; - } - - try - { - cookedReadData.ScreenInfo().Write(OutputCellIterator(UNICODE_SPACE, CharsToWrite), coordOriginalCursor); - } - CATCH_LOG(); - - if (fUpdateFields) - { - cookedReadData.Erase(); - } - - LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cookedReadData.OriginalCursorPosition(), true)); -} - -void RedrawCommandLine(COOKED_READ_DATA& cookedReadData) -{ - if (cookedReadData.IsEchoInput()) - { - // Draw the command line - cookedReadData.OriginalCursorPosition() = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - - til::CoordType ScrollY = 0; -#pragma prefast(suppress : 28931, "Status is not unused. It's used in debug assertions.") - auto Status = WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY); - FAIL_FAST_IF_NTSTATUS_FAILED(Status); - - cookedReadData.OriginalCursorPosition().y += ScrollY; - - // Move the cursor back to the right position - auto CursorPosition = cookedReadData.OriginalCursorPosition(); - CursorPosition.x += RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().x, - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint()); - if (CheckBisectStringW(cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint(), - cookedReadData.ScreenInfo().GetBufferSize().Width() - cookedReadData.OriginalCursorPosition().x)) - { - CursorPosition.x++; - } - AdjustCursorPosition(cookedReadData.ScreenInfo(), CursorPosition, TRUE, nullptr); - } -} - -// Routine Description: -// - This routine copies the commandline specified by Index into the cooked read buffer -void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ CommandHistory::Index Index) // index, not command number -{ - DeleteCommandLine(cookedReadData, TRUE); - FAIL_FAST_IF_FAILED(cookedReadData.History().RetrieveNth(Index, - cookedReadData.SpanWholeBuffer(), - cookedReadData.BytesRead())); - FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - } - - const auto CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); - cookedReadData.InsertionPoint() = CharsToWrite; - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); -} - -// Routine Description: -// - This routine handles the command list popup. It puts up the popup, then calls ProcessCommandListInput to get and process input. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - STATUS_SUCCESS - read was fully completed (user hit return) -[[nodiscard]] NTSTATUS CommandLine::_startCommandListPopup(COOKED_READ_DATA& cookedReadData) -{ - if (cookedReadData.HasHistory() && - cookedReadData.History().GetNumberOfCommands()) - { - try - { - auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo(), - cookedReadData.History())); - popup.Draw(); - return popup.Process(cookedReadData); - } - CATCH_RETURN(); - } - else - { - return S_FALSE; - } -} - -// Routine Description: -// - This routine handles the "delete up to this char" popup. It puts up the popup, then calls ProcessCopyFromCharInput to get and process input. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - STATUS_SUCCESS - read was fully completed (user hit return) -[[nodiscard]] NTSTATUS CommandLine::_startCopyFromCharPopup(COOKED_READ_DATA& cookedReadData) -{ - // Delete the current command from cursor position to the - // letter specified by the user. The user is prompted via - // popup to enter a character. - if (cookedReadData.HasHistory()) - { - try - { - auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); - popup.Draw(); - return popup.Process(cookedReadData); - } - CATCH_RETURN(); - } - else - { - return S_FALSE; - } -} - -// Routine Description: -// - This routine handles the "copy up to this char" popup. It puts up the popup, then calls ProcessCopyToCharInput to get and process input. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - STATUS_SUCCESS - read was fully completed (user hit return) -// - S_FALSE - if we couldn't make a popup because we had no commands -[[nodiscard]] NTSTATUS CommandLine::_startCopyToCharPopup(COOKED_READ_DATA& cookedReadData) -{ - // copy the previous command to the current command, up to but - // not including the character specified by the user. the user - // is prompted via popup to enter a character. - if (cookedReadData.HasHistory()) - { - try - { - auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); - popup.Draw(); - return popup.Process(cookedReadData); - } - CATCH_RETURN(); - } - else - { - return S_FALSE; - } -} - -// Routine Description: -// - This routine handles the "enter command number" popup. It puts up the popup, then calls ProcessCommandNumberInput to get and process input. -// Return Value: -// - CONSOLE_STATUS_WAIT - we ran out of input, so a wait block was created -// - STATUS_SUCCESS - read was fully completed (user hit return) -// - S_FALSE - if we couldn't make a popup because we had no commands or it wouldn't fit. -[[nodiscard]] HRESULT CommandLine::StartCommandNumberPopup(COOKED_READ_DATA& cookedReadData) -{ - if (cookedReadData.HasHistory() && - cookedReadData.History().GetNumberOfCommands() && - cookedReadData.ScreenInfo().GetBufferSize().Width() >= Popup::MINIMUM_COMMAND_PROMPT_SIZE + 2) - { - try - { - auto& popup = *_popups.emplace_front(std::make_unique(cookedReadData.ScreenInfo())); - popup.Draw(); - - // Save the original cursor position in case the user cancels out of the dialog - cookedReadData.BeforeDialogCursorPosition() = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - - // Move the cursor into the dialog so the user can type multiple characters for the command number - const auto CursorPosition = popup.GetCursorPosition(); - LOG_IF_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(CursorPosition, TRUE)); - - // Transfer control to the handler routine - return popup.Process(cookedReadData); - } - CATCH_RETURN(); - } - else - { - return S_FALSE; - } -} - -// Routine Description: -// - Process virtual key code and updates the prompt line with the next history element in the direction -// specified by wch -// Arguments: -// - cookedReadData - The cooked read data to operate on -// - searchDirection - Direction in history to search -// Note: -// - May throw exceptions -void CommandLine::_processHistoryCycling(COOKED_READ_DATA& cookedReadData, - const CommandHistory::SearchDirection searchDirection) -{ - // for doskey compatibility, buffer isn't circular. don't do anything if attempting - // to cycle history past the bounds of the history buffer - if (!cookedReadData.HasHistory()) - { - return; - } - else if (searchDirection == CommandHistory::SearchDirection::Previous && cookedReadData.History().AtFirstCommand()) - { - return; - } - else if (searchDirection == CommandHistory::SearchDirection::Next && cookedReadData.History().AtLastCommand()) - { - return; - } - - DeleteCommandLine(cookedReadData, true); - THROW_IF_FAILED(cookedReadData.History().Retrieve(searchDirection, - cookedReadData.SpanWholeBuffer(), - cookedReadData.BytesRead())); - FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - } - const auto CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); - cookedReadData.InsertionPoint() = CharsToWrite; - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); -} - -// Routine Description: -// - Sets the text on the prompt to the oldest run command in the cookedReadData's history -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Note: -// - May throw exceptions -void CommandLine::_setPromptToOldestCommand(COOKED_READ_DATA& cookedReadData) -{ - if (cookedReadData.HasHistory() && cookedReadData.History().GetNumberOfCommands()) - { - DeleteCommandLine(cookedReadData, true); - const short commandNumber = 0; - THROW_IF_FAILED(cookedReadData.History().RetrieveNth(commandNumber, - cookedReadData.SpanWholeBuffer(), - cookedReadData.BytesRead())); - FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - } - auto CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); - cookedReadData.InsertionPoint() = CharsToWrite; - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); - } -} - -// Routine Description: -// - Sets the text on the prompt the most recently run command in cookedReadData's history -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Note: -// - May throw exceptions -void CommandLine::_setPromptToNewestCommand(COOKED_READ_DATA& cookedReadData) -{ - DeleteCommandLine(cookedReadData, true); - if (cookedReadData.HasHistory() && cookedReadData.History().GetNumberOfCommands()) - { - const auto commandNumber = (SHORT)(cookedReadData.History().GetNumberOfCommands() - 1); - THROW_IF_FAILED(cookedReadData.History().RetrieveNth(commandNumber, - cookedReadData.SpanWholeBuffer(), - cookedReadData.BytesRead())); - FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - } - auto CharsToWrite = cookedReadData.BytesRead() / sizeof(WCHAR); - cookedReadData.InsertionPoint() = CharsToWrite; - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CharsToWrite); - } -} - -// Routine Description: -// - Deletes all prompt text to the right of the cursor -// Arguments: -// - cookedReadData - The cooked read data to operate on -void CommandLine::DeletePromptAfterCursor(COOKED_READ_DATA& cookedReadData) noexcept -{ - DeleteCommandLine(cookedReadData, false); - cookedReadData.BytesRead() = cookedReadData.InsertionPoint() * sizeof(WCHAR); - if (cookedReadData.IsEchoInput()) - { - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr)); - } -} - -// Routine Description: -// - Deletes all user input on the prompt to the left of the cursor -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_deletePromptBeforeCursor(COOKED_READ_DATA& cookedReadData) noexcept -{ - DeleteCommandLine(cookedReadData, false); - cookedReadData.BytesRead() -= cookedReadData.InsertionPoint() * sizeof(WCHAR); - cookedReadData.InsertionPoint() = 0; - memmove(cookedReadData.BufferStartPtr(), cookedReadData.BufferCurrentPtr(), cookedReadData.BytesRead()); - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr()); - if (cookedReadData.IsEchoInput()) - { - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr)); - } - return cookedReadData.OriginalCursorPosition(); -} - -// Routine Description: -// - Moves the cursor to the end of the prompt text -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_moveCursorToEndOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept -{ - cookedReadData.InsertionPoint() = cookedReadData.BytesRead() / sizeof(WCHAR); - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + cookedReadData.InsertionPoint()); - til::point cursorPosition; - cursorPosition.x = gsl::narrow(cookedReadData.OriginalCursorPosition().x + cookedReadData.VisibleCharCount()); - cursorPosition.y = cookedReadData.OriginalCursorPosition().y; - - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - if (CheckBisectProcessW(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint(), - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x, - cookedReadData.OriginalCursorPosition().x, - true)) - { - cursorPosition.x++; - } - return cursorPosition; -} - -// Routine Description: -// - Moves the cursor to the start of the user input on the prompt -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_moveCursorToStartOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept -{ - cookedReadData.InsertionPoint() = 0; - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr()); - return cookedReadData.OriginalCursorPosition(); -} - -// Routine Description: -// - Moves the cursor left by a word -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - New cursor position -til::point CommandLine::_moveCursorLeftByWord(COOKED_READ_DATA& cookedReadData) noexcept -{ - PWCHAR LastWord; - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - if (cookedReadData.BufferCurrentPtr() != cookedReadData.BufferStartPtr()) - { - // A bit better word skipping. - LastWord = cookedReadData.BufferCurrentPtr() - 1; - if (LastWord != cookedReadData.BufferStartPtr()) - { - if (*LastWord == L' ') - { - // Skip spaces, until the non-space character is found. - while (--LastWord != cookedReadData.BufferStartPtr()) - { - FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); - if (*LastWord != L' ') - { - break; - } - } - } - if (LastWord != cookedReadData.BufferStartPtr()) - { - if (IsWordDelim(*LastWord)) - { - // Skip WORD_DELIMs until space or non WORD_DELIM is found. - while (--LastWord != cookedReadData.BufferStartPtr()) - { - FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); - if (*LastWord == L' ' || !IsWordDelim(*LastWord)) - { - break; - } - } - } - else - { - // Skip the regular words - while (--LastWord != cookedReadData.BufferStartPtr()) - { - FAIL_FAST_IF(!(LastWord > cookedReadData.BufferStartPtr())); - if (IsWordDelim(*LastWord)) - { - break; - } - } - } - } - FAIL_FAST_IF(!(LastWord >= cookedReadData.BufferStartPtr())); - if (LastWord != cookedReadData.BufferStartPtr()) - { - // LastWord is currently pointing to the last character - // of the previous word, unless it backed up to the beginning - // of the buffer. - // Let's increment LastWord so that it points to the expected - // insertion point. - ++LastWord; - } - cookedReadData.SetBufferCurrentPtr(LastWord); - } - cookedReadData.InsertionPoint() = (cookedReadData.BufferCurrentPtr() - cookedReadData.BufferStartPtr()); - cursorPosition = cookedReadData.OriginalCursorPosition(); - cursorPosition.x = cursorPosition.x + - RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().x, - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint()); - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - if (CheckBisectStringW(cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint() + 1, - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x)) - { - cursorPosition.x++; - } - } - return cursorPosition; -} - -// Routine Description: -// - Moves cursor left by a glyph -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - New cursor position -til::point CommandLine::_moveCursorLeft(COOKED_READ_DATA& cookedReadData) -{ - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - if (cookedReadData.BufferCurrentPtr() != cookedReadData.BufferStartPtr()) - { - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() - 1); - cookedReadData.InsertionPoint()--; - cursorPosition.x = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition().x; - cursorPosition.y = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition().y; - cursorPosition.x = cursorPosition.x - - RetrieveNumberOfSpaces(cookedReadData.OriginalCursorPosition().x, - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint()); - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - if (CheckBisectProcessW(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint() + 2, - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x, - cookedReadData.OriginalCursorPosition().x, - true)) - { - if ((cursorPosition.x == -2) || (cursorPosition.x == -1)) - { - cursorPosition.x--; - } - } - } - return cursorPosition; -} - -// Routine Description: -// - Moves the cursor to the right by a word -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_moveCursorRightByWord(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - if (cookedReadData.InsertionPoint() < (cookedReadData.BytesRead() / sizeof(WCHAR))) - { - auto NextWord = cookedReadData.BufferCurrentPtr(); - - // A bit better word skipping. - auto BufLast = cookedReadData.BufferStartPtr() + cookedReadData.BytesRead() / sizeof(WCHAR); - - FAIL_FAST_IF(!(NextWord < BufLast)); - if (*NextWord == L' ') - { - // If the current character is space, skip to the next non-space character. - while (++NextWord < BufLast) - { - if (*NextWord != L' ') - { - break; - } - } - } - else - { - // Skip the body part. - auto fStartFromDelim = IsWordDelim(*NextWord); - - while (++NextWord < BufLast) - { - if (fStartFromDelim != IsWordDelim(*NextWord)) - { - break; - } - } - - // Skip the space block. - for (; NextWord < BufLast; NextWord++) - { - if (*NextWord != L' ') - { - break; - } - } - } - - cookedReadData.SetBufferCurrentPtr(NextWord); - cookedReadData.InsertionPoint() = (ULONG)(cookedReadData.BufferCurrentPtr() - cookedReadData.BufferStartPtr()); - cursorPosition = cookedReadData.OriginalCursorPosition(); - cursorPosition.x = cursorPosition.x + - RetrieveTotalNumberOfSpaces(cookedReadData.OriginalCursorPosition().x, - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint()); - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - if (CheckBisectStringW(cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint() + 1, - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x)) - { - cursorPosition.x++; - } - } - return cursorPosition; -} - -// Routine Description: -// - Moves the cursor to the right by a glyph -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_moveCursorRight(COOKED_READ_DATA& cookedReadData) noexcept -{ - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - // If not at the end of the line, move cursor position right. - if (cookedReadData.InsertionPoint() < (cookedReadData.BytesRead() / sizeof(WCHAR))) - { - cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - cursorPosition.x = cursorPosition.x + - RetrieveNumberOfSpaces(cookedReadData.OriginalCursorPosition().x, - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint()); - if (CheckBisectProcessW(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint() + 2, - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x, - cookedReadData.OriginalCursorPosition().x, - true)) - { - // Snap cursorPosition.x to sScreenBufferSizeX if it is at the edge of the screen - if (cursorPosition.x == (sScreenBufferSizeX - 1)) - cursorPosition.x = sScreenBufferSizeX; - } - - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); - cookedReadData.InsertionPoint()++; - } - // if at the end of the line, copy a character from the same position in the last command - else if (cookedReadData.HasHistory()) - { - size_t NumSpaces; - const auto LastCommand = cookedReadData.History().GetLastCommand(); - if (!LastCommand.empty() && LastCommand.size() > cookedReadData.InsertionPoint()) - { - *cookedReadData.BufferCurrentPtr() = LastCommand[cookedReadData.InsertionPoint()]; - cookedReadData.BytesRead() += sizeof(WCHAR); - cookedReadData.InsertionPoint()++; - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - auto CharsToWrite = sizeof(WCHAR); - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &CharsToWrite, - &NumSpaces, - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - cookedReadData.VisibleCharCount() += NumSpaces; - // update reported cursor position - if (ScrollY != 0) - { - cursorPosition.x = 0; - cursorPosition.y += ScrollY; - } - else - { - cursorPosition.x += 1; - } - } - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); - } - } - return cursorPosition; -} - -// Routine Description: -// - Place a ctrl-z in the current command line -// Arguments: -// - cookedReadData - The cooked read data to operate on -void CommandLine::_insertCtrlZ(COOKED_READ_DATA& cookedReadData) noexcept -{ - size_t NumSpaces = 0; - - *cookedReadData.BufferCurrentPtr() = (WCHAR)0x1a; // ctrl-z - cookedReadData.BytesRead() += sizeof(WCHAR); - cookedReadData.InsertionPoint()++; - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - auto CharsToWrite = sizeof(WCHAR); - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &CharsToWrite, - &NumSpaces, - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - cookedReadData.VisibleCharCount() += NumSpaces; - } - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + 1); -} - -// Routine Description: -// - Empties the command history for cookedReadData -// Arguments: -// - cookedReadData - The cooked read data to operate on -void CommandLine::_deleteCommandHistory(COOKED_READ_DATA& cookedReadData) noexcept -{ - if (cookedReadData.HasHistory()) - { - cookedReadData.History().Empty(); - cookedReadData.History().Flags |= CommandHistory::CLE_ALLOCATED; - } -} - -// Routine Description: -// - Copy the remainder of the previous command to the current command. -// Arguments: -// - cookedReadData - The cooked read data to operate on -void CommandLine::_fillPromptWithPreviousCommandFragment(COOKED_READ_DATA& cookedReadData) noexcept -{ - if (cookedReadData.HasHistory()) - { - size_t NumSpaces, cchCount; - - const auto LastCommand = cookedReadData.History().GetLastCommand(); - if (!LastCommand.empty() && LastCommand.size() > cookedReadData.InsertionPoint()) - { - cchCount = LastCommand.size() - cookedReadData.InsertionPoint(); - const auto bufferSpan = cookedReadData.SpanAtPointer(); - std::copy_n(LastCommand.cbegin() + cookedReadData.InsertionPoint(), cchCount, bufferSpan.begin()); - cookedReadData.InsertionPoint() += cchCount; - cchCount *= sizeof(WCHAR); - cookedReadData.BytesRead() = std::max(LastCommand.size() * sizeof(wchar_t), cookedReadData.BytesRead()); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cchCount, - &NumSpaces, - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - cookedReadData.VisibleCharCount() += NumSpaces; - } - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferCurrentPtr() + cchCount / sizeof(WCHAR)); - } - } -} - -// Routine Description: -// - Cycles through the stored commands that start with the characters in the current command. -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::_cycleMatchingCommandHistoryToPrompt(COOKED_READ_DATA& cookedReadData) -{ - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - if (cookedReadData.HasHistory()) - { - CommandHistory::Index index; - if (cookedReadData.History().FindMatchingCommand({ cookedReadData.BufferStartPtr(), cookedReadData.InsertionPoint() }, - cookedReadData.History().LastDisplayed, - index, - CommandHistory::MatchOptions::None)) - { - // save cursor position - const auto CurrentPos = cookedReadData.InsertionPoint(); - - DeleteCommandLine(cookedReadData, true); - THROW_IF_FAILED(cookedReadData.History().RetrieveNth(index, - cookedReadData.SpanWholeBuffer(), - cookedReadData.BytesRead())); - FAIL_FAST_IF(!(cookedReadData.BufferStartPtr() == cookedReadData.BufferCurrentPtr())); - if (cookedReadData.IsEchoInput()) - { - til::CoordType ScrollY = 0; - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - cookedReadData.OriginalCursorPosition().y += ScrollY; - cursorPosition.y += ScrollY; - } - - // restore cursor position - cookedReadData.SetBufferCurrentPtr(cookedReadData.BufferStartPtr() + CurrentPos); - cookedReadData.InsertionPoint() = CurrentPos; - FAIL_FAST_IF_NTSTATUS_FAILED(cookedReadData.ScreenInfo().SetCursorPosition(cursorPosition, true)); - } - } - return cursorPosition; -} - -// Routine Description: -// - Deletes a glyph from the right side of the cursor -// Arguments: -// - cookedReadData - The cooked read data to operate on -// Return Value: -// - The new cursor position -til::point CommandLine::DeleteFromRightOfCursor(COOKED_READ_DATA& cookedReadData) noexcept -{ - // save cursor position - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - - if (!cookedReadData.AtEol()) - { - // Delete commandline. - // clang-format off -#pragma prefast(suppress: __WARNING_BUFFER_OVERFLOW, "Not sure why prefast is getting confused here") - // clang-format on - DeleteCommandLine(cookedReadData, false); - - // Delete char. - cookedReadData.BytesRead() -= sizeof(WCHAR); - memmove(cookedReadData.BufferCurrentPtr(), - cookedReadData.BufferCurrentPtr() + 1, - cookedReadData.BytesRead() - (cookedReadData.InsertionPoint() * sizeof(WCHAR))); - - { - auto buf = (PWCHAR)((PBYTE)cookedReadData.BufferStartPtr() + cookedReadData.BytesRead()); - *buf = (WCHAR)' '; - } - - // Write commandline. - if (cookedReadData.IsEchoInput()) - { - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - cookedReadData.BufferStartPtr(), - &cookedReadData.BytesRead(), - &cookedReadData.VisibleCharCount(), - cookedReadData.OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr)); - } - - // restore cursor position - const auto sScreenBufferSizeX = cookedReadData.ScreenInfo().GetBufferSize().Width(); - if (CheckBisectProcessW(cookedReadData.ScreenInfo(), - cookedReadData.BufferStartPtr(), - cookedReadData.InsertionPoint() + 1, - sScreenBufferSizeX - cookedReadData.OriginalCursorPosition().x, - cookedReadData.OriginalCursorPosition().x, - true)) - { - cursorPosition.x++; - } - } - return cursorPosition; -} - -// TODO: [MSFT:4586207] Clean up this mess -- needs helpers. http://osgvsowi/4586207 -// Routine Description: -// - This routine process command line editing keys. -// Return Value: -// - CONSOLE_STATUS_WAIT - CommandListPopup ran out of input -// - CONSOLE_STATUS_READ_COMPLETE - user hit in CommandListPopup -// - STATUS_SUCCESS - everything's cool -[[nodiscard]] NTSTATUS CommandLine::ProcessCommandLine(COOKED_READ_DATA& cookedReadData, - _In_ WCHAR wch, - const DWORD dwKeyState) -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - auto cursorPosition = cookedReadData.ScreenInfo().GetTextBuffer().GetCursor().GetPosition(); - NTSTATUS Status; - - const auto altPressed = WI_IsAnyFlagSet(dwKeyState, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); - const auto ctrlPressed = WI_IsAnyFlagSet(dwKeyState, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); - auto UpdateCursorPosition = false; - switch (wch) - { - case VK_ESCAPE: - DeleteCommandLine(cookedReadData, true); - break; - case VK_DOWN: - try - { - _processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - Status = STATUS_SUCCESS; - } - catch (...) - { - Status = wil::ResultFromCaughtException(); - } - break; - case VK_UP: - case VK_F5: - try - { - _processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - Status = STATUS_SUCCESS; - } - catch (...) - { - Status = wil::ResultFromCaughtException(); - } - break; - case VK_PRIOR: - try - { - _setPromptToOldestCommand(cookedReadData); - Status = STATUS_SUCCESS; - } - catch (...) - { - Status = wil::ResultFromCaughtException(); - } - break; - case VK_NEXT: - try - { - _setPromptToNewestCommand(cookedReadData); - Status = STATUS_SUCCESS; - } - catch (...) - { - Status = wil::ResultFromCaughtException(); - } - break; - case VK_END: - if (ctrlPressed) - { - DeletePromptAfterCursor(cookedReadData); - } - else - { - cursorPosition = _moveCursorToEndOfPrompt(cookedReadData); - UpdateCursorPosition = true; - } - break; - case VK_HOME: - if (ctrlPressed) - { - cursorPosition = _deletePromptBeforeCursor(cookedReadData); - UpdateCursorPosition = true; - } - else - { - cursorPosition = _moveCursorToStartOfPrompt(cookedReadData); - UpdateCursorPosition = true; - } - break; - case VK_LEFT: - if (ctrlPressed) - { - cursorPosition = _moveCursorLeftByWord(cookedReadData); - UpdateCursorPosition = true; - } - else - { - cursorPosition = _moveCursorLeft(cookedReadData); - UpdateCursorPosition = true; - } - break; - case VK_F1: - { - // we don't need to check for end of buffer here because we've - // already done it. - cursorPosition = _moveCursorRight(cookedReadData); - UpdateCursorPosition = true; - break; - } - case VK_RIGHT: - // we don't need to check for end of buffer here because we've - // already done it. - if (ctrlPressed) - { - cursorPosition = _moveCursorRightByWord(cookedReadData); - UpdateCursorPosition = true; - } - else - { - cursorPosition = _moveCursorRight(cookedReadData); - UpdateCursorPosition = true; - } - break; - case VK_F2: - { - Status = _startCopyToCharPopup(cookedReadData); - if (S_FALSE == Status) - { - // We couldn't make the popup, so loop around and read the next character. - break; - } - else - { - return Status; - } - } - case VK_F3: - _fillPromptWithPreviousCommandFragment(cookedReadData); - break; - case VK_F4: - { - Status = _startCopyFromCharPopup(cookedReadData); - if (S_FALSE == Status) - { - // We couldn't display a popup. Go around a loop behind. - break; - } - else - { - return Status; - } - } - case VK_F6: - { - _insertCtrlZ(cookedReadData); - break; - } - case VK_F7: - if (!ctrlPressed && !altPressed) - { - Status = _startCommandListPopup(cookedReadData); - } - else if (altPressed) - { - _deleteCommandHistory(cookedReadData); - } - break; - - case VK_F8: - try - { - cursorPosition = _cycleMatchingCommandHistoryToPrompt(cookedReadData); - UpdateCursorPosition = true; - } - catch (...) - { - Status = wil::ResultFromCaughtException(); - } - break; - case VK_F9: - { - Status = StartCommandNumberPopup(cookedReadData); - if (S_FALSE == Status) - { - // If we couldn't make the popup, break and go around to read another input character. - break; - } - else - { - return Status; - } - } - case VK_F10: - // Alt+F10 clears the aliases for specifically cmd.exe. - if (altPressed) - { - Alias::s_ClearCmdExeAliases(); - } - break; - case VK_INSERT: - cookedReadData.SetInsertMode(!cookedReadData.IsInsertMode()); - cookedReadData.ScreenInfo().SetCursorDBMode(cookedReadData.IsInsertMode() != gci.GetInsertMode()); - break; - case VK_DELETE: - cursorPosition = DeleteFromRightOfCursor(cookedReadData); - UpdateCursorPosition = true; - break; - default: - FAIL_FAST_HR(E_NOTIMPL); - break; - } - - if (UpdateCursorPosition && cookedReadData.IsEchoInput()) + const auto& delimiters = ServiceLocator::LocateGlobals().WordDelimiters; + if (std::find(delimiters.begin(), delimiters.end(), wch) != delimiters.end()) { - AdjustCursorPosition(cookedReadData.ScreenInfo(), cursorPosition, true, nullptr); + return 2; } - - return STATUS_SUCCESS; + return 0; } diff --git a/src/host/cmdline.h b/src/host/cmdline.h index e3e9511ff29..89cdf19b5ca 100644 --- a/src/host/cmdline.h +++ b/src/host/cmdline.h @@ -3,90 +3,9 @@ #pragma once -#include "input.h" #include "screenInfo.hpp" -#include "server.h" - -#include "history.h" -#include "alias.h" -#include "readDataCooked.hpp" -#include "popup.h" - -class CommandLine -{ -public: - ~CommandLine(); - - static CommandLine& Instance(); - - static bool IsEditLineEmpty(); - void Hide(const bool fUpdateFields); - void Show(); - bool IsVisible() const noexcept; - - [[nodiscard]] NTSTATUS ProcessCommandLine(COOKED_READ_DATA& cookedReadData, - _In_ WCHAR wch, - const DWORD dwKeyState); - - [[nodiscard]] HRESULT StartCommandNumberPopup(COOKED_READ_DATA& cookedReadData); - - bool HasPopup() const noexcept; - Popup& GetPopup() const; - - void EndCurrentPopup(); - void EndAllPopups(); - - void DeletePromptAfterCursor(COOKED_READ_DATA& cookedReadData) noexcept; - til::point DeleteFromRightOfCursor(COOKED_READ_DATA& cookedReadData) noexcept; - -protected: - CommandLine(); - - // delete these because we don't want to accidentally get copies of the singleton - CommandLine(const CommandLine&) = delete; - CommandLine& operator=(const CommandLine&) = delete; - - [[nodiscard]] NTSTATUS _startCommandListPopup(COOKED_READ_DATA& cookedReadData); - [[nodiscard]] NTSTATUS _startCopyFromCharPopup(COOKED_READ_DATA& cookedReadData); - [[nodiscard]] NTSTATUS _startCopyToCharPopup(COOKED_READ_DATA& cookedReadData); - - void _processHistoryCycling(COOKED_READ_DATA& cookedReadData, const CommandHistory::SearchDirection searchDirection); - void _setPromptToOldestCommand(COOKED_READ_DATA& cookedReadData); - void _setPromptToNewestCommand(COOKED_READ_DATA& cookedReadData); - til::point _deletePromptBeforeCursor(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _moveCursorToEndOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _moveCursorToStartOfPrompt(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _moveCursorLeftByWord(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _moveCursorLeft(COOKED_READ_DATA& cookedReadData); - til::point _moveCursorRightByWord(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _moveCursorRight(COOKED_READ_DATA& cookedReadData) noexcept; - void _insertCtrlZ(COOKED_READ_DATA& cookedReadData) noexcept; - void _deleteCommandHistory(COOKED_READ_DATA& cookedReadData) noexcept; - void _fillPromptWithPreviousCommandFragment(COOKED_READ_DATA& cookedReadData) noexcept; - til::point _cycleMatchingCommandHistoryToPrompt(COOKED_READ_DATA& cookedReadData); - -#ifdef UNIT_TESTING - friend class CommandLineTests; - friend class CommandNumberPopupTests; -#endif - -private: - std::deque> _popups; - bool _isVisible; -}; - -void DeleteCommandLine(COOKED_READ_DATA& cookedReadData, const bool fUpdateFields); - -void RedrawCommandLine(COOKED_READ_DATA& cookedReadData); - -// Values for WriteChars(), WriteCharsLegacy() dwFlags -#define WC_INTERACTIVE 0x01 -#define WC_KEEP_CURSOR_VISIBLE 0x02 // Word delimiters -bool IsWordDelim(const wchar_t wch); -bool IsWordDelim(const std::wstring_view charData); - -bool IsValidStringBuffer(_In_ bool Unicode, _In_reads_bytes_(Size) PVOID Buffer, _In_ ULONG Size, _In_ ULONG Count, ...); - -void SetCurrentCommandLine(COOKED_READ_DATA& cookedReadData, _In_ CommandHistory::Index Index); +bool IsWordDelim(wchar_t wch) noexcept; +bool IsWordDelim(const std::wstring_view& charData) noexcept; +int DelimiterClass(wchar_t wch) noexcept; diff --git a/src/host/consoleInformation.cpp b/src/host/consoleInformation.cpp index 0b5b6db272e..4722cebe2b9 100644 --- a/src/host/consoleInformation.cpp +++ b/src/host/consoleInformation.cpp @@ -130,6 +130,11 @@ bool CONSOLE_INFORMATION::HasPendingCookedRead() const noexcept return _cookedReadData != nullptr; } +bool CONSOLE_INFORMATION::HasPendingPopup() const noexcept +{ + return _cookedReadData && _cookedReadData->PresentingPopup(); +} + const COOKED_READ_DATA& CONSOLE_INFORMATION::CookedReadData() const noexcept { return *_cookedReadData; diff --git a/src/host/ft_fuzzer/fuzzmain.cpp b/src/host/ft_fuzzer/fuzzmain.cpp index 4b8f04dea6e..cd3708d59ed 100644 --- a/src/host/ft_fuzzer/fuzzmain.cpp +++ b/src/host/ft_fuzzer/fuzzmain.cpp @@ -3,15 +3,13 @@ #include "precomp.h" +#include + #include "../ConsoleArguments.hpp" #include "../srvinit.h" -#include "../../server/Entrypoints.h" +#include "../_stream.h" #include "../../interactivity/inc/ServiceLocator.hpp" -#include "../../server/DeviceHandle.h" #include "../../server/IoThread.h" -#include "../_stream.h" -#include "../getset.h" -#include struct NullDeviceComm : public IDeviceComm { @@ -132,17 +130,8 @@ extern "C" __declspec(dllexport) int LLVMFuzzerTestOneInput(const uint8_t* data, const auto u16String{ til::u8u16(std::string_view{ reinterpret_cast(data), size }) }; til::CoordType scrollY{}; - auto sizeInBytes{ u16String.size() * 2 }; gci.LockConsole(); auto u = wil::scope_exit([&]() { gci.UnlockConsole(); }); - (void)WriteCharsLegacy(gci.GetActiveOutputBuffer(), - u16String.data(), - u16String.data(), - u16String.data(), - &sizeInBytes, - nullptr, - 0, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &scrollY); + WriteCharsLegacy(gci.GetActiveOutputBuffer(), u16String, true, &scrollY); return 0; } diff --git a/src/host/getset.cpp b/src/host/getset.cpp index f2cd485842f..5c2954f4fa4 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -598,13 +598,7 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont const auto requestedBufferSize = til::wrap_coord_size(data.dwSize); if (requestedBufferSize != coordScreenBufferSize) { - auto& commandLine = CommandLine::Instance(); - - commandLine.Hide(FALSE); - LOG_IF_FAILED(context.ResizeScreenBuffer(requestedBufferSize, TRUE)); - - commandLine.Show(); } const auto newBufferSize = context.GetBufferSize().Dimensions(); @@ -655,15 +649,7 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont if (NewSize.width != context.GetViewport().Width() || NewSize.height != context.GetViewport().Height()) { - // GH#1856 - make sure to hide the commandline _before_ we execute - // the resize, and the re-display it after the resize. If we leave - // it displayed, we'll crash during the resize when we try to figure - // out if the bounds of the old commandline fit within the new - // window (it might not). - auto& commandLine = CommandLine::Instance(); - commandLine.Hide(FALSE); context.SetViewportSize(&NewSize); - commandLine.Show(); const auto pWindow = ServiceLocator::LocateConsoleWindow(); if (pWindow != nullptr) diff --git a/src/host/history.cpp b/src/host/history.cpp index dfe1624987a..245e906695f 100644 --- a/src/host/history.cpp +++ b/src/host/history.cpp @@ -167,47 +167,9 @@ const std::vector& CommandHistory::GetCommands() const noexcept return _commands; } -[[nodiscard]] HRESULT CommandHistory::RetrieveNth(const Index index, std::span buffer, size_t& commandSize) +std::wstring_view CommandHistory::Retrieve(const SearchDirection searchDirection) { - LastDisplayed = index; - - try - { - const auto& cmd = _commands.at(index); - if (cmd.size() > buffer.size()) - { - commandSize = buffer.size(); // room for CRLF? - } - else - { - commandSize = cmd.size(); - } - - std::copy_n(cmd.cbegin(), commandSize, buffer.begin()); - - commandSize *= sizeof(wchar_t); - - return S_OK; - } - CATCH_RETURN(); -} - -[[nodiscard]] HRESULT CommandHistory::Retrieve(const SearchDirection searchDirection, - const std::span buffer, - size_t& commandSize) -{ - FAIL_FAST_IF(!(WI_IsFlagSet(Flags, CLE_ALLOCATED))); - - if (_commands.size() == 0) - { - return E_FAIL; - } - - if (_commands.size() == 1) - { - LastDisplayed = 0; - } - else if (searchDirection == SearchDirection::Previous) + if (searchDirection == SearchDirection::Previous) { // if this is the first time for this read that a command has // been retrieved, return the current command. otherwise, return @@ -218,15 +180,27 @@ const std::vector& CommandHistory::GetCommands() const noexcept } else { - _Prev(LastDisplayed); + LastDisplayed--; } } else { - _Next(LastDisplayed); + LastDisplayed++; + } + + return RetrieveNth(LastDisplayed); +} + +std::wstring_view CommandHistory::RetrieveNth(Index index) +{ + if (_commands.empty()) + { + LastDisplayed = 0; + return {}; } - return RetrieveNth(LastDisplayed, buffer, commandSize); + LastDisplayed = std::clamp(index, 0, GetNumberOfCommands() - 1); + return _commands.at(LastDisplayed); } std::wstring_view CommandHistory::GetLastCommand() const diff --git a/src/host/history.h b/src/host/history.h index 943f9475e21..172be22abfd 100644 --- a/src/host/history.h +++ b/src/host/history.h @@ -43,13 +43,8 @@ class CommandHistory [[nodiscard]] HRESULT Add(const std::wstring_view command, const bool suppressDuplicates); - [[nodiscard]] HRESULT Retrieve(const SearchDirection searchDirection, - const std::span buffer, - size_t& commandSize); - - [[nodiscard]] HRESULT RetrieveNth(const Index index, - const std::span buffer, - size_t& commandSize); + std::wstring_view Retrieve(const SearchDirection searchDirection); + std::wstring_view RetrieveNth(Index index); Index GetNumberOfCommands() const; std::wstring_view GetNth(Index index) const; diff --git a/src/host/host-common.vcxitems b/src/host/host-common.vcxitems index 3ff022e82cf..ee482e31a93 100644 --- a/src/host/host-common.vcxitems +++ b/src/host/host-common.vcxitems @@ -3,10 +3,6 @@ - - - - @@ -29,7 +25,6 @@ - Create @@ -64,10 +59,6 @@ - - - - @@ -90,7 +81,6 @@ - diff --git a/src/host/input.cpp b/src/host/input.cpp index d48192daabb..ea13a0c0ef5 100644 --- a/src/host/input.cpp +++ b/src/host/input.cpp @@ -119,7 +119,7 @@ void HandleGenericKeyEvent(INPUT_RECORD event, const bool generateBreak) if (keyEvent.wVirtualKeyCode == 'C' && IsInProcessedInputMode()) { HandleCtrlEvent(CTRL_C_EVENT); - if (gci.PopupCount == 0) + if (!gci.HasPendingPopup()) { gci.pInputBuffer->TerminateRead(WaitTerminationReason::CtrlC); } @@ -135,7 +135,7 @@ void HandleGenericKeyEvent(INPUT_RECORD event, const bool generateBreak) { gci.pInputBuffer->Flush(); HandleCtrlEvent(CTRL_BREAK_EVENT); - if (gci.PopupCount == 0) + if (!gci.HasPendingPopup()) { gci.pInputBuffer->TerminateRead(WaitTerminationReason::CtrlBreak); } diff --git a/src/host/input.h b/src/host/input.h index 7e4509f6a1f..73f22dff777 100644 --- a/src/host/input.h +++ b/src/host/input.h @@ -58,11 +58,6 @@ class INPUT_KEY_INFO ULONG _ulControlKeyState; }; -#define TAB_SIZE 8 -#define TAB_MASK (TAB_SIZE - 1) -// WHY IS THIS NOT POSITION % TAB_SIZE?! -#define NUMBER_OF_SPACES_IN_TAB(POSITION) (TAB_SIZE - ((POSITION)&TAB_MASK)) - // these values are related to GetKeyboardState #define KEY_PRESSED 0x8000 #define KEY_TOGGLED 0x01 diff --git a/src/host/lib/hostlib.vcxproj.filters b/src/host/lib/hostlib.vcxproj.filters index fb42149b626..c108f2d1411 100644 --- a/src/host/lib/hostlib.vcxproj.filters +++ b/src/host/lib/hostlib.vcxproj.filters @@ -159,21 +159,6 @@ Source Files - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - Source Files @@ -329,21 +314,6 @@ Header Files - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - Header Files diff --git a/src/host/misc.cpp b/src/host/misc.cpp index 531ec2a9a63..329328f5672 100644 --- a/src/host/misc.cpp +++ b/src/host/misc.cpp @@ -47,140 +47,6 @@ void SetConsoleCPInfo(const BOOL fOutput) } } -// Routine Description: -// - This routine check bisected on Unicode string end. -// Arguments: -// - pwchBuffer - Pointer to Unicode string buffer. -// - cWords - Number of Unicode string. -// - cBytes - Number of bisect position by byte counts. -// Return Value: -// - TRUE - Bisected character. -// - FALSE - Correctly. -BOOL CheckBisectStringW(_In_reads_bytes_(cBytes) const WCHAR* pwchBuffer, - _In_ size_t cWords, - _In_ size_t cBytes) noexcept -{ - while (cWords && cBytes) - { - if (IsGlyphFullWidth(*pwchBuffer)) - { - if (cBytes < 2) - { - return TRUE; - } - else - { - cWords--; - cBytes -= 2; - pwchBuffer++; - } - } - else - { - cWords--; - cBytes--; - pwchBuffer++; - } - } - - return FALSE; -} - -// Routine Description: -// - This routine check bisected on Unicode string end. -// Arguments: -// - ScreenInfo - reference to screen information structure. -// - pwchBuffer - Pointer to Unicode string buffer. -// - cWords - Number of Unicode string. -// - cBytes - Number of bisect position by byte counts. -// - fPrintableControlChars - TRUE if control characters are being expanded (to ^X) -// Return Value: -// - TRUE - Bisected character. -// - FALSE - Correctly. -BOOL CheckBisectProcessW(const SCREEN_INFORMATION& ScreenInfo, - _In_reads_bytes_(cBytes) const WCHAR* pwchBuffer, - _In_ size_t cWords, - _In_ size_t cBytes, - _In_ til::CoordType sOriginalXPosition, - _In_ BOOL fPrintableControlChars) -{ - if (WI_IsFlagSet(ScreenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT)) - { - while (cWords && cBytes) - { - const auto Char = *pwchBuffer; - if (Char >= UNICODE_SPACE) - { - if (IsGlyphFullWidth(Char)) - { - if (cBytes < 2) - { - return TRUE; - } - else - { - cWords--; - cBytes -= 2; - pwchBuffer++; - sOriginalXPosition += 2; - } - } - else - { - cWords--; - cBytes--; - pwchBuffer++; - sOriginalXPosition++; - } - } - else - { - cWords--; - pwchBuffer++; - switch (Char) - { - case UNICODE_BELL: - if (fPrintableControlChars) - goto CtrlChar; - break; - case UNICODE_BACKSPACE: - case UNICODE_LINEFEED: - case UNICODE_CARRIAGERETURN: - break; - case UNICODE_TAB: - { - size_t TabSize = NUMBER_OF_SPACES_IN_TAB(sOriginalXPosition); - sOriginalXPosition = (til::CoordType)(sOriginalXPosition + TabSize); - if (cBytes < TabSize) - return TRUE; - cBytes -= TabSize; - break; - } - default: - if (fPrintableControlChars) - { - CtrlChar: - if (cBytes < 2) - return TRUE; - cBytes -= 2; - sOriginalXPosition += 2; - } - else - { - cBytes--; - sOriginalXPosition++; - } - } - } - } - return FALSE; - } - else - { - return CheckBisectStringW(pwchBuffer, cWords, cBytes); - } -} - // Routine Description: // - Converts unicode characters to ANSI given a destination codepage // Arguments: @@ -205,18 +71,6 @@ int ConvertToOem(const UINT uiCodePage, return LOG_IF_WIN32_BOOL_FALSE(WideCharToMultiByte(uiCodePage, 0, pwchSource, cchSource, pchTarget, cchTarget, nullptr, nullptr)); } -// Data in the output buffer is the true unicode value. -int ConvertInputToUnicode(const UINT uiCodePage, - _In_reads_(cchSource) const CHAR* const pchSource, - const UINT cchSource, - _Out_writes_(cchTarget) WCHAR* const pwchTarget, - const UINT cchTarget) noexcept -{ - DBGCHARS(("ConvertInputToUnicode %d->U %.*s\n", uiCodePage, cchSource > 10 ? 10 : cchSource, pchSource)); - - return MultiByteToWideChar(uiCodePage, 0, pchSource, cchSource, pwchTarget, cchTarget); -} - // Output data is always translated via the ansi codepage so glyph translation works. int ConvertOutputToUnicode(_In_ UINT uiCodePage, _In_reads_(cchSource) const CHAR* const pchSource, @@ -226,46 +80,6 @@ int ConvertOutputToUnicode(_In_ UINT uiCodePage, { FAIL_FAST_IF(!(cchTarget > 0)); pwchTarget[0] = L'\0'; - DBGCHARS(("ConvertOutputToUnicode %d->U %.*s\n", uiCodePage, cchSource > 10 ? 10 : cchSource, pchSource)); - - if (DoBuffersOverlap(reinterpret_cast(pchSource), - cchSource * sizeof(CHAR), - reinterpret_cast(pwchTarget), - cchTarget * sizeof(WCHAR))) - { - try - { - // buffers overlap so we need to copy one - std::string copyData(pchSource, cchSource); - return MultiByteToWideChar(uiCodePage, MB_USEGLYPHCHARS, copyData.data(), cchSource, pwchTarget, cchTarget); - } - catch (...) - { - return 0; - } - } - else - { - return MultiByteToWideChar(uiCodePage, MB_USEGLYPHCHARS, pchSource, cchSource, pwchTarget, cchTarget); - } -} - -// Routine Description: -// - checks if two buffers overlap -// Arguments: -// - pBufferA - pointer to start of first buffer -// - cbBufferA - size of first buffer, in bytes -// - pBufferB - pointer to start of second buffer -// - cbBufferB - size of second buffer, in bytes -// Return Value: -// - true if buffers overlap, false otherwise -bool DoBuffersOverlap(const BYTE* const pBufferA, - const UINT cbBufferA, - const BYTE* const pBufferB, - const UINT cbBufferB) noexcept -{ - const auto pBufferAEnd = pBufferA + cbBufferA; - const auto pBufferBEnd = pBufferB + cbBufferB; - return (pBufferA <= pBufferB && pBufferAEnd >= pBufferB) || (pBufferB <= pBufferA && pBufferBEnd >= pBufferA); + return MultiByteToWideChar(uiCodePage, MB_USEGLYPHCHARS, pchSource, cchSource, pwchTarget, cchTarget); } diff --git a/src/host/misc.h b/src/host/misc.h index ca90ac23ac8..38f9bce3f46 100644 --- a/src/host/misc.h +++ b/src/host/misc.h @@ -28,35 +28,14 @@ WCHAR CharToWchar(_In_reads_(cch) const char* const pch, const UINT cch); void SetConsoleCPInfo(const BOOL fOutput); -BOOL CheckBisectStringW(_In_reads_bytes_(cBytes) const WCHAR* pwchBuffer, - _In_ size_t cWords, - _In_ size_t cBytes) noexcept; -BOOL CheckBisectProcessW(const SCREEN_INFORMATION& ScreenInfo, - _In_reads_bytes_(cBytes) const WCHAR* pwchBuffer, - _In_ size_t cWords, - _In_ size_t cBytes, - _In_ til::CoordType sOriginalXPosition, - _In_ BOOL fPrintableControlChars); - int ConvertToOem(const UINT uiCodePage, _In_reads_(cchSource) const WCHAR* const pwchSource, const UINT cchSource, _Out_writes_(cchTarget) CHAR* const pchTarget, const UINT cchTarget) noexcept; -int ConvertInputToUnicode(const UINT uiCodePage, - _In_reads_(cchSource) const CHAR* const pchSource, - const UINT cchSource, - _Out_writes_(cchTarget) WCHAR* const pwchTarget, - const UINT cchTarget) noexcept; - int ConvertOutputToUnicode(_In_ UINT uiCodePage, _In_reads_(cchSource) const CHAR* const pchSource, _In_ UINT cchSource, _Out_writes_(cchTarget) WCHAR* pwchTarget, _In_ UINT cchTarget) noexcept; - -bool DoBuffersOverlap(const BYTE* const pBufferA, - const UINT cbBufferA, - const BYTE* const pBufferB, - const UINT cbBufferB) noexcept; diff --git a/src/host/popup.cpp b/src/host/popup.cpp deleted file mode 100644 index b91376eb679..00000000000 --- a/src/host/popup.cpp +++ /dev/null @@ -1,317 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" - -#include "popup.h" - -#include "_output.h" -#include "output.h" - -#include "dbcs.h" -#include "srvinit.h" -#include "stream.h" - -#include "resource.h" - -#include "utils.hpp" - -#include "../interactivity/inc/ServiceLocator.hpp" - -#pragma hdrstop - -using namespace Microsoft::Console::Types; -using Microsoft::Console::Interactivity::ServiceLocator; -// Routine Description: -// - Creates an object representing an interactive popup overlay during cooked mode command line editing. -// - NOTE: Modifies global popup count (and adjusts cursor visibility as appropriate.) -// Arguments: -// - screenInfo - Reference to screen on which the popup should be drawn/overlaid. -// - proposedSize - Suggested size of the popup. May be adjusted based on screen size. -Popup::Popup(SCREEN_INFORMATION& screenInfo, const til::size proposedSize) : - _screenInfo(screenInfo), - _userInputFunction(&Popup::_getUserInputInternal) -{ - _attributes = screenInfo.GetPopupAttributes(); - - const auto size = _CalculateSize(screenInfo, proposedSize); - const auto origin = _CalculateOrigin(screenInfo, size); - - _region.left = origin.x; - _region.top = origin.y; - _region.right = origin.x + size.width - 1; - _region.bottom = origin.y + size.height - 1; - - _oldScreenSize = screenInfo.GetBufferSize().Dimensions(); - - til::inclusive_rect TargetRect; - TargetRect.left = 0; - TargetRect.top = _region.top; - TargetRect.right = _oldScreenSize.width - 1; - TargetRect.bottom = _region.bottom; - - // copy the data into the backup buffer - _oldContents = std::move(screenInfo.ReadRect(Viewport::FromInclusive(TargetRect))); - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto countWas = gci.PopupCount.fetch_add(1ui16); - if (0 == countWas) - { - // If this is the first popup to be shown, stop the cursor from appearing/blinking - screenInfo.GetTextBuffer().GetCursor().SetIsPopupShown(true); - } -} - -// Routine Description: -// - Cleans up a popup object -// - NOTE: Modifies global popup count (and adjusts cursor visibility as appropriate.) -Popup::~Popup() -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto countWas = gci.PopupCount.fetch_sub(1); - if (1 == countWas) - { - // Notify we're done showing popups. - gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().SetIsPopupShown(false); - } -} - -void Popup::Draw() -{ - _DrawBorder(); - _DrawContent(); -} - -// Routine Description: -// - Draws the outlines of the popup area in the screen buffer -void Popup::_DrawBorder() -{ - // fill attributes of top line - til::point WriteCoord; - WriteCoord.x = _region.left; - WriteCoord.y = _region.top; - _screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord); - - // draw upper left corner - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_DOWN_AND_RIGHT, 1), WriteCoord); - - // draw upper bar - WriteCoord.x += 1; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_HORIZONTAL, Width()), WriteCoord); - - // draw upper right corner - WriteCoord.x = _region.right; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_DOWN_AND_LEFT, 1), WriteCoord); - - for (til::CoordType i = 0; i < Height(); i++) - { - WriteCoord.y += 1; - WriteCoord.x = _region.left; - - // fill attributes - _screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord); - - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_VERTICAL, 1), WriteCoord); - - WriteCoord.x = _region.right; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_VERTICAL, 1), WriteCoord); - } - - // Draw bottom line. - // Fill attributes of top line. - WriteCoord.x = _region.left; - WriteCoord.y = _region.bottom; - _screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord); - - // Draw bottom left corner. - WriteCoord.x = _region.left; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_UP_AND_RIGHT, 1), WriteCoord); - - // Draw lower bar. - WriteCoord.x += 1; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_HORIZONTAL, Width()), WriteCoord); - - // draw lower right corner - WriteCoord.x = _region.right; - _screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_UP_AND_LEFT, 1), WriteCoord); -} - -// Routine Description: -// - Draws prompt information in the popup area to tell the user what to enter. -// Arguments: -// - id - Resource ID for string to display to user -void Popup::_DrawPrompt(const UINT id) -{ - auto text = _LoadString(id); - - // Draw empty popup. - til::point WriteCoord; - WriteCoord.x = _region.left + 1; - WriteCoord.y = _region.top + 1; - auto lStringLength = Width(); - for (til::CoordType i = 0; i < Height(); i++) - { - const OutputCellIterator it(UNICODE_SPACE, _attributes, lStringLength); - const auto done = _screenInfo.Write(it, WriteCoord); - lStringLength = done.GetCellDistance(it); - - WriteCoord.y += 1; - } - - WriteCoord.x = _region.left + 1; - WriteCoord.y = _region.top + 1; - - // write prompt to screen - lStringLength = gsl::narrow(text.size()); - if (lStringLength > Width()) - { - text = text.substr(0, Width()); - } - - size_t used; - LOG_IF_FAILED(ServiceLocator::LocateGlobals().api->WriteConsoleOutputCharacterWImpl(_screenInfo, - text, - WriteCoord, - used)); -} - -// Routine Description: -// - Cleans up a popup by restoring the stored buffer information to the region of -// the screen that the popup was covering and frees resources. -void Popup::End() -{ - // restore previous contents to screen - - til::inclusive_rect SourceRect; - SourceRect.left = 0; - SourceRect.top = _region.top; - SourceRect.right = _oldScreenSize.width - 1; - SourceRect.bottom = _region.bottom; - - const auto sourceViewport = Viewport::FromInclusive(SourceRect); - - _screenInfo.WriteRect(_oldContents, sourceViewport.Origin()); -} - -// Routine Description: -// - Helper to calculate the size of the popup. -// Arguments: -// - screenInfo - Screen buffer we will be drawing into -// - proposedSize - The suggested size of the popup that may need to be adjusted to fit -// Return Value: -// - Coordinate size that the popup should consume in the screen buffer -til::size Popup::_CalculateSize(const SCREEN_INFORMATION& screenInfo, const til::size proposedSize) -{ - // determine popup dimensions - auto size = proposedSize; - size.width += 2; // add borders - size.height += 2; // add borders - - const auto viewportSize = screenInfo.GetViewport().Dimensions(); - - size.width = std::min(size.width, viewportSize.width); - size.height = std::min(size.height, viewportSize.height); - - // make sure there's enough room for the popup borders - THROW_HR_IF(E_NOT_SUFFICIENT_BUFFER, size.width < 2 || size.height < 2); - - return size; -} - -// Routine Description: -// - Helper to calculate the origin point (within the screen buffer) for the popup -// Arguments: -// - screenInfo - Screen buffer we will be drawing into -// - size - The size that the popup will consume -// Return Value: -// - Coordinate position of the origin point of the popup -til::point Popup::_CalculateOrigin(const SCREEN_INFORMATION& screenInfo, const til::size size) -{ - const auto viewport = screenInfo.GetViewport(); - - // determine origin. center popup on window - til::point origin; - origin.x = (viewport.Width() - size.width) / 2 + viewport.Left(); - origin.y = (viewport.Height() - size.height) / 2 + viewport.Top(); - return origin; -} - -// Routine Description: -// - Helper to return the width of the popup in columns -// Return Value: -// - Width of popup inside attached screen buffer. -til::CoordType Popup::Width() const noexcept -{ - return _region.right - _region.left - 1; -} - -// Routine Description: -// - Helper to return the height of the popup in columns -// Return Value: -// - Height of popup inside attached screen buffer. -til::CoordType Popup::Height() const noexcept -{ - return _region.bottom - _region.top - 1; -} - -// Routine Description: -// - Helper to get the position on top of some types of popup dialogs where -// we should overlay the cursor for user input. -// Return Value: -// - Coordinate location on the popup where the cursor should be placed. -til::point Popup::GetCursorPosition() const noexcept -{ - til::point CursorPosition; - CursorPosition.x = _region.right - MINIMUM_COMMAND_PROMPT_SIZE; - CursorPosition.y = _region.top + 1; - return CursorPosition; -} - -// Routine Description: -// - changes the function used to gather user input. for allowing custom input during unit tests only -// Arguments: -// - function - function to use to fetch input -void Popup::SetUserInputFunction(UserInputFunction function) noexcept -{ - _userInputFunction = function; -} - -// Routine Description: -// - gets a single char input from the user -// Arguments: -// - cookedReadData - cookedReadData for this popup operation -// - popupKey - on completion, will be true if key was a popup key -// - wch - on completion, the char read from the user -// Return Value: -// - relevant NTSTATUS -[[nodiscard]] NTSTATUS Popup::_getUserInput(COOKED_READ_DATA& cookedReadData, bool& popupKey, DWORD& modifiers, wchar_t& wch) noexcept -{ - return _userInputFunction(cookedReadData, popupKey, modifiers, wch); -} - -// Routine Description: -// - gets a single char input from the user using the InputBuffer -// Arguments: -// - cookedReadData - cookedReadData for this popup operation -// - popupKey - on completion, will be true if key was a popup key -// - wch - on completion, the char read from the user -// Return Value: -// - relevant NTSTATUS -[[nodiscard]] NTSTATUS Popup::_getUserInputInternal(COOKED_READ_DATA& cookedReadData, - bool& popupKey, - DWORD& modifiers, - wchar_t& wch) noexcept -{ - const auto pInputBuffer = cookedReadData.GetInputBuffer(); - auto Status = GetChar(pInputBuffer, - &wch, - true, - nullptr, - &popupKey, - &modifiers); - if (FAILED_NTSTATUS(Status) && Status != CONSOLE_STATUS_WAIT) - { - cookedReadData.BytesRead() = 0; - } - return Status; -} diff --git a/src/host/popup.h b/src/host/popup.h deleted file mode 100644 index 73a91bafe27..00000000000 --- a/src/host/popup.h +++ /dev/null @@ -1,82 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- popup.h - -Abstract: -- This file contains the internal structures and definitions used by command line input and editing. - -Author: -- Therese Stowell (ThereseS) 15-Nov-1991 - -Revision History: -- Mike Griese (migrie) Jan 2018: - Refactored the history and alias functionality into their own files. -- Michael Niksa (miniksa) May 2018: - Separated out popups from the rest of command line functionality. ---*/ - -#pragma once - -#include "readDataCooked.hpp" -#include "screenInfo.hpp" -#include "readDataCooked.hpp" - -class CommandHistory; - -class Popup -{ -public: - static constexpr til::CoordType MINIMUM_COMMAND_PROMPT_SIZE = 5; - - using UserInputFunction = std::function; - - Popup(SCREEN_INFORMATION& screenInfo, const til::size proposedSize); - virtual ~Popup(); - [[nodiscard]] virtual NTSTATUS Process(COOKED_READ_DATA& cookedReadData) noexcept = 0; - - void Draw(); - - void End(); - - til::CoordType Width() const noexcept; - til::CoordType Height() const noexcept; - - til::point GetCursorPosition() const noexcept; - -protected: - // used in test code to alter how the popup fetches use input - void SetUserInputFunction(UserInputFunction function) noexcept; - -#ifdef UNIT_TESTING - friend class CopyFromCharPopupTests; - friend class CopyToCharPopupTests; - friend class CommandNumberPopupTests; - friend class CommandListPopupTests; -#endif - - [[nodiscard]] NTSTATUS _getUserInput(COOKED_READ_DATA& cookedReadData, bool& popupKey, DWORD& modifiers, wchar_t& wch) noexcept; - void _DrawPrompt(const UINT id); - virtual void _DrawContent() = 0; - - til::inclusive_rect _region; // region popup occupies - SCREEN_INFORMATION& _screenInfo; - TextAttribute _attributes; // text attributes - -private: - til::size _CalculateSize(const SCREEN_INFORMATION& screenInfo, const til::size proposedSize); - til::point _CalculateOrigin(const SCREEN_INFORMATION& screenInfo, const til::size size); - - void _DrawBorder(); - - [[nodiscard]] static NTSTATUS _getUserInputInternal(COOKED_READ_DATA& cookedReadData, - bool& popupKey, - DWORD& modifiers, - wchar_t& wch) noexcept; - - OutputCellRect _oldContents; // contains data under popup - til::size _oldScreenSize; - UserInputFunction _userInputFunction; -}; diff --git a/src/host/readDataCooked.cpp b/src/host/readDataCooked.cpp index 75e57b55963..667a445f4b2 100644 --- a/src/host/readDataCooked.cpp +++ b/src/host/readDataCooked.cpp @@ -3,46 +3,42 @@ #include "precomp.h" #include "readDataCooked.hpp" -#include "dbcs.h" + +#include "alias.h" +#include "history.h" +#include "resource.h" #include "stream.h" -#include "misc.h" #include "_stream.h" -#include "inputBuffer.hpp" -#include "cmdline.h" -#include "../types/inc/GlyphWidth.hpp" -#include "../types/inc/convert.hpp" - #include "../interactivity/inc/ServiceLocator.hpp" -#define LINE_INPUT_BUFFER_SIZE (256 * sizeof(WCHAR)) - using Microsoft::Console::Interactivity::ServiceLocator; +// As per https://graphics.stanford.edu/~seander/bithacks.html#IntegerLog10Obvious +constexpr int integerLog10(uint32_t v) +{ + return (v >= 1000000000) ? 9 : + (v >= 100000000) ? 8 : + (v >= 10000000) ? 7 : + (v >= 1000000) ? 6 : + (v >= 100000) ? 5 : + (v >= 10000) ? 4 : + (v >= 1000) ? 3 : + (v >= 100) ? 2 : + (v >= 10) ? 1 : + 0; +} + // Routine Description: // - Constructs cooked read data class to hold context across key presses while a user is modifying their 'input line'. // Arguments: // - pInputBuffer - Buffer that data will be read from. // - pInputReadHandleData - Context stored across calls from the same input handle to return partial data appropriately. // - screenInfo - Output buffer that will be used for 'echoing' the line back to the user so they can see/manipulate it -// - BufferSize - -// - BytesRead - -// - CurrentPosition - -// - BufPtr - -// - BackupLimit - // - UserBufferSize - The byte count of the buffer presented by the client // - UserBuffer - The buffer that was presented by the client for filling with input data on read conclusion/return from server/host. -// - OriginalCursorPosition - -// - NumberOfVisibleChars // - CtrlWakeupMask - Special client parameter to interrupt editing, end the wait, and return control to the client application -// - Echo - -// - InsertMode - -// - Processed - -// - Line - -// - pTempHandle - A handle to the output buffer to prevent it from being destroyed while we're using it to present 'edit line' text. // - initialData - any text data that should be prepopulated into the buffer // - pClientProcess - Attached process handle object -// Return Value: -// - THROW: Throws E_INVALIDARG for invalid pointers. COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData, SCREEN_INFORMATION& screenInfo, @@ -54,242 +50,68 @@ COOKED_READ_DATA::COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, _In_ ConsoleProcessHandle* const pClientProcess) : ReadData(pInputBuffer, pInputReadHandleData), _screenInfo{ screenInfo }, - _bytesRead{ 0 }, - _currentPosition{ 0 }, - _userBufferSize{ UserBufferSize }, - _userBuffer{ UserBuffer }, - _tempHandle{ nullptr }, + _userBuffer{ UserBuffer, UserBufferSize }, _exeName{ exeName }, - _pdwNumBytes{ nullptr }, - - _commandHistory{ CommandHistory::s_Find(pClientProcess) }, - _controlKeyState{ 0 }, + _processHandle{ pClientProcess }, + _history{ CommandHistory::s_Find(pClientProcess) }, _ctrlWakeupMask{ CtrlWakeupMask }, - _visibleCharCount{ 0 }, - _originalCursorPosition{ -1, -1 }, - _beforeDialogCursorPosition{ 0, 0 }, - - _echoInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_ECHO_INPUT) }, - _lineInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_LINE_INPUT) }, - _processedInput{ WI_IsFlagSet(pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) }, - _insertMode{ ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode() }, - _unicode{ false }, - _clientProcess{ pClientProcess } + _insertMode{ ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode() } { #ifndef UNIT_TESTING - THROW_IF_FAILED(screenInfo.GetMainBuffer().AllocateIoHandle(ConsoleHandleData::HandleType::Output, - GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - _tempHandle)); + // The screen buffer instance is basically a reference counted HANDLE given out to the user. + // We need to ensure that it stays alive for the duration of the read. + // Coincidentally this serves another important purpose: It checks whether we're allowed to read from + // the given buffer in the first place. If it's missing the FILE_SHARE_READ flag, we can't read from it. + THROW_IF_FAILED(_screenInfo.AllocateIoHandle(ConsoleHandleData::HandleType::Output, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, _tempHandle)); #endif - // to emulate OS/2 KbdStringIn, we read into our own big buffer - // (256 bytes) until the user types enter. then return as many - // chars as will fit in the user's buffer. - _bufferSize = std::max(UserBufferSize, LINE_INPUT_BUFFER_SIZE); - _buffer = std::make_unique(_bufferSize); - _backupLimit = reinterpret_cast(_buffer.get()); - _bufPtr = reinterpret_cast(_buffer.get()); - - // Initialize the user's buffer to spaces. This is done so that - // moving in the buffer via cursor doesn't do strange things. - std::fill_n(_bufPtr, _bufferSize / sizeof(wchar_t), UNICODE_SPACE); - if (!initialData.empty()) { - memcpy_s(_bufPtr, _bufferSize, initialData.data(), initialData.size() * 2); - - _bytesRead += initialData.size() * 2; - - const auto cchInitialData = initialData.size(); - VisibleCharCount() = cchInitialData; - _bufPtr += cchInitialData; - _currentPosition = cchInitialData; - - OriginalCursorPosition() = screenInfo.GetTextBuffer().GetCursor().GetPosition(); - OriginalCursorPosition().x -= (til::CoordType)_currentPosition; - - const auto sScreenBufferSizeX = screenInfo.GetBufferSize().Width(); - while (OriginalCursorPosition().x < 0) + _buffer.assign(initialData); + _bufferCursor = _buffer.size(); + _bufferDirty = !_buffer.empty(); + + // The console API around `nInitialChars` in `CONSOLE_READCONSOLE_CONTROL` is pretty weird. + // The way it works is that cmd.exe does a ReadConsole() with a `dwCtrlWakeupMask` that includes \t, + // so when you press tab it can autocomplete the prompt based on the available file names. + // The weird part is that it's not us who then prints the autocompletion. It's cmd.exe which calls WriteConsoleW(). + // It then initiates another ReadConsole() where the `nInitialChars` is the amount of chars it wrote via WriteConsoleW(). + // + // In other words, `nInitialChars` is a "trust me bro, I just wrote that in the buffer" API. + // This unfortunately means that the API is inherently broken: ReadConsole() visualizes control + // characters like Ctrl+X as "^X" and WriteConsoleW() doesn't and so the column counts don't match. + // Solving these issues is technically possible, but it's also quite difficult to do so correctly. + // + // But unfortunately (or fortunately) the initial (from the 1990s up to 2023) looked something like that: + // cursor = cursor.GetPosition(); + // cursor.x -= initialData.size(); + // while (cursor.x < 0) + // { + // cursor.x += textBuffer.Width(); + // cursor.y -= 1; + // } + // + // In other words, it assumed that the number of code units in the initial data corresponds 1:1 to + // the column count. This meant that the API never supported tabs for instance (nor wide glyphs). + // The new implementation still doesn't support tabs, but it does fix support for wide glyphs. + // That seemed like a good trade-off. + + // NOTE: You can't just "measure" the length of the string in columns either, because previously written + // wide glyphs might have resulted in padding whitespace in the text buffer (see ROW::WasDoubleBytePadded). + // The alternative to the loop below is counting the number of padding glyphs while iterating backwards. Either approach is fine. + til::CoordType distance = 0; + for (size_t i = 0; i < initialData.size(); i = TextBuffer::GraphemeNext(initialData, i)) { - OriginalCursorPosition().x += sScreenBufferSizeX; - OriginalCursorPosition().y -= 1; + --distance; } - } - - // TODO MSFT:11285829 find a better way to manage the lifetime of this object in relation to gci -} - -// Routine Description: -// - Destructs a read data class. -// - Decrements count of readers waiting on the given handle. -COOKED_READ_DATA::~COOKED_READ_DATA() -{ - CommandLine::Instance().EndAllPopups(); -} - -std::span COOKED_READ_DATA::SpanWholeBuffer() -{ - return std::span{ _backupLimit, (_bufferSize / sizeof(wchar_t)) }; -} - -std::span COOKED_READ_DATA::SpanAtPointer() -{ - auto wholeSpan = SpanWholeBuffer(); - return wholeSpan.subspan(_bufPtr - _backupLimit); -} - -bool COOKED_READ_DATA::HasHistory() const noexcept -{ - return _commandHistory != nullptr; -} - -CommandHistory& COOKED_READ_DATA::History() noexcept -{ - return *_commandHistory; -} - -const size_t& COOKED_READ_DATA::VisibleCharCount() const noexcept -{ - return _visibleCharCount; -} - -size_t& COOKED_READ_DATA::VisibleCharCount() noexcept -{ - return _visibleCharCount; -} - -SCREEN_INFORMATION& COOKED_READ_DATA::ScreenInfo() noexcept -{ - return _screenInfo; -} - -til::point COOKED_READ_DATA::OriginalCursorPosition() const noexcept -{ - return _originalCursorPosition; -} - -til::point& COOKED_READ_DATA::OriginalCursorPosition() noexcept -{ - return _originalCursorPosition; -} - -til::point& COOKED_READ_DATA::BeforeDialogCursorPosition() noexcept -{ - return _beforeDialogCursorPosition; -} - -bool COOKED_READ_DATA::IsEchoInput() const noexcept -{ - return _echoInput; -} - -bool COOKED_READ_DATA::IsInsertMode() const noexcept -{ - return _insertMode; -} - -void COOKED_READ_DATA::SetInsertMode(const bool mode) noexcept -{ - _insertMode = mode; -} - -bool COOKED_READ_DATA::IsUnicode() const noexcept -{ - return _unicode; -} - -// Routine Description: -// - gets the size of the user buffer -// Return Value: -// - the size of the user buffer in bytes -size_t COOKED_READ_DATA::UserBufferSize() const noexcept -{ - return _userBufferSize; -} - -// Routine Description: -// - gets a pointer to the beginning of the prompt storage -// Return Value: -// - pointer to the first char in the internal prompt storage array -wchar_t* COOKED_READ_DATA::BufferStartPtr() noexcept -{ - return _backupLimit; -} - -// Routine Description: -// - gets a pointer to where the next char will be inserted into the prompt storage -// Return Value: -// - pointer to the current insertion point of the prompt storage array -wchar_t* COOKED_READ_DATA::BufferCurrentPtr() noexcept -{ - return _bufPtr; -} - -// Routine Description: -// - Set the location of the next char insert into the prompt storage to be at -// ptr. ptr must point into a valid portion of the internal prompt storage array -// Arguments: -// - ptr - the new char insertion location -void COOKED_READ_DATA::SetBufferCurrentPtr(wchar_t* ptr) noexcept -{ - _bufPtr = ptr; -} - -// Routine Description: -// - gets the number of bytes read so far into the prompt buffer -// Return Value: -// - the number of bytes read -const size_t& COOKED_READ_DATA::BytesRead() const noexcept -{ - return _bytesRead; -} - -// Routine Description: -// - gets the number of bytes read so far into the prompt buffer -// Return Value: -// - the number of bytes read -size_t& COOKED_READ_DATA::BytesRead() noexcept -{ - return _bytesRead; -} - -// Routine Description: -// - gets the index for the current insertion point of the prompt -// Return Value: -// - the index of the current insertion point -const size_t& COOKED_READ_DATA::InsertionPoint() const noexcept -{ - return _currentPosition; -} - -// Routine Description: -// - gets the index for the current insertion point of the prompt -// Return Value: -// - the index of the current insertion point -size_t& COOKED_READ_DATA::InsertionPoint() noexcept -{ - return _currentPosition; -} - -// Routine Description: -// - sets the number of bytes that will be reported when this read block completes its read -// Arguments: -// - count - the number of bytes to report -void COOKED_READ_DATA::SetReportedByteCount(const size_t count) noexcept -{ - FAIL_FAST_IF_NULL(_pdwNumBytes); - *_pdwNumBytes = count; -} -// Routine Description: -// - resets the prompt to be as if it was erased -void COOKED_READ_DATA::Erase() noexcept -{ - _bufPtr = _backupLimit; - _bytesRead = 0; - _currentPosition = 0; - _visibleCharCount = 0; + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + const auto end = cursor.GetPosition(); + const auto beg = textBuffer.NavigateCursor(end, distance); + _distanceCursor = (end.y - beg.y) * textBuffer.GetSize().Width() + end.x - beg.x; + _distanceEnd = _distanceCursor; + } } // Routine Description: @@ -314,24 +136,15 @@ bool COOKED_READ_DATA::Notify(const WaitTerminationReason TerminationReason, _Out_ NTSTATUS* const pReplyStatus, _Out_ size_t* const pNumBytes, _Out_ DWORD* const pControlKeyState, - _Out_ void* const /*pOutputData*/) + _Out_ void* const /*pOutputData*/) noexcept +try { auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // this routine should be called by a thread owning the same - // lock on the same console as we're reading from. - FAIL_FAST_IF(!gci.IsConsoleLocked()); - *pNumBytes = 0; *pControlKeyState = 0; - *pReplyStatus = STATUS_SUCCESS; - FAIL_FAST_IF(_pInputReadHandleData->IsInputPending()); - - // this routine should be called by a thread owning the same lock on the same console as we're reading from. - FAIL_FAST_IF(_pInputReadHandleData->GetReadCount() == 0); - // if ctrl-c or ctrl-break was seen, terminate read. if (WI_IsAnyFlagSet(TerminationReason, (WaitTerminationReason::CtrlC | WaitTerminationReason::CtrlBreak))) { @@ -351,7 +164,6 @@ bool COOKED_READ_DATA::Notify(const WaitTerminationReason TerminationReason, // We must see if we were woken up because the handle is being closed. If // so, we decrement the read count. If it goes to zero, we wake up the // close thread. Otherwise, we wake up any other thread waiting for data. - if (WI_IsFlagSet(TerminationReason, WaitTerminationReason::HandleClosing)) { *pReplyStatus = STATUS_ALERTED; @@ -359,71 +171,23 @@ bool COOKED_READ_DATA::Notify(const WaitTerminationReason TerminationReason, return true; } - // If we get to here, this routine was called either by the input thread - // or a write routine. Both of these callers grab the current console - // lock. - - // MSFT:13994975 This is REALLY weird. - // When we're doing cooked reading for popups, we come through this method - // twice. Once when we press F7 to bring up the popup, then again when we - // press enter to input the selected command. - // The first time, there is no popup, and we go to CookedRead. We pass into - // CookedRead `pNumBytes`, which is passed to us as the address of the - // stack variable dwNumBytes, in ConsoleWaitBlock::Notify. - // CookedRead sets this->_pdwNumBytes to that value, and starts the popup, - // which returns all the way up, and pops the ConsoleWaitBlock::Notify - // stack frame containing the address we're pointing at. - // Then on the second time through this function, we hit this if block, - // because there is a popup to get input from. - // However, pNumBytes is now the address of a different stack frame, and not - // necessarily the same as before (presumably not at all). The - // Callback would try and write the number of bytes read to the - // value in _pdwNumBytes, and then we'd return up to ConsoleWaitBlock::Notify, - // who's dwNumBytes had nothing in it. - // To fix this, when we hit this with a popup, we're going to make sure to - // refresh the value of _pdwNumBytes to the current address we want to put - // the out value into. - // It's still really weird, but limits the potential fallout of changing a - // piece of old spaghetti code. - if (_commandHistory) - { - if (CommandLine::Instance().HasPopup()) - { - // (see above comment, MSFT:13994975) - // Make sure that the popup writes the dwNumBytes to the right place - if (pNumBytes) - { - _pdwNumBytes = pNumBytes; - } - - auto& popup = CommandLine::Instance().GetPopup(); - *pReplyStatus = popup.Process(*this); - if (*pReplyStatus == CONSOLE_STATUS_READ_COMPLETE || - (*pReplyStatus != CONSOLE_STATUS_WAIT && *pReplyStatus != CONSOLE_STATUS_WAIT_NO_BLOCK)) - { - *pReplyStatus = S_OK; - gci.SetCookedReadData(nullptr); - return true; - } - return false; - } - } - - *pReplyStatus = Read(fIsUnicode, *pNumBytes, *pControlKeyState); - if (*pReplyStatus != CONSOLE_STATUS_WAIT) + if (Read(fIsUnicode, *pNumBytes, *pControlKeyState)) { gci.SetCookedReadData(nullptr); return true; } - else - { - return false; - } + + return false; } +NT_CATCH_RETURN() -bool COOKED_READ_DATA::AtEol() const noexcept +void COOKED_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) noexcept { - return _bytesRead == (_currentPosition * 2); + // See the comment in WaitBlock.cpp for more information. + if (_userBuffer.data() == oldBuffer) + { + _userBuffer = { static_cast(newBuffer), _userBuffer.size() }; + } } // Routine Description: @@ -436,633 +200,1096 @@ bool COOKED_READ_DATA::AtEol() const noexcept // - numBytes - On in, the number of bytes available in the client // buffer. On out, the number of bytes consumed in the client buffer. // - controlKeyState - For some types of reads, this is the modifier key state with the last button press. -[[nodiscard]] HRESULT COOKED_READ_DATA::Read(const bool isUnicode, - size_t& numBytes, - ULONG& controlKeyState) noexcept +bool COOKED_READ_DATA::Read(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) { controlKeyState = 0; - auto Status = _readCharInputLoop(isUnicode, numBytes); + const auto done = _readCharInputLoop(); + + // NOTE: Don't call _flushBuffer in a wil::scope_exit/defer. + // It may throw and throwing during an ongoing exception is a bad idea. + _flushBuffer(); - // if the read was completed (status != wait), free the cooked read - // data. also, close the temporary output handle that was opened to - // echo the characters read. - if (Status != CONSOLE_STATUS_WAIT) + if (done) { - Status = _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState); + _handlePostCharInputLoop(isUnicode, numBytes, controlKeyState); } - return Status; + return done; } -void COOKED_READ_DATA::ProcessAliases(DWORD& lineCount) +// Printing wide glyphs at the end of a row results in a forced line wrap and a padding whitespace to be inserted. +// When the text buffer resizes these padding spaces may vanish and the _distanceCursor and _distanceEnd measurements become inaccurate. +// To fix this, this function is called before a resize and will clear the input line. Afterwards, RedrawAfterResize() will restore it. +void COOKED_READ_DATA::EraseBeforeResize() { - Alias::s_MatchAndCopyAliasLegacy(_backupLimit, - _bytesRead, - _backupLimit, - _bufferSize, - _bytesRead, - _exeName, - lineCount); -} + _popupsDone(); -// Routine Description: -// - This method handles the various actions that occur on the edit line like pressing keys left/right/up/down, paging, and -// the final ENTER key press that will end the wait and finally return the data. -// Arguments: -// - pCookedReadData - Pointer to cooked read data information (edit line, client buffer, etc.) -// - wch - The most recently pressed/retrieved character from the input buffer (keystroke) -// - keyState - Modifier keys/state information with the pressed key/character -// - status - The return code to pass to the client -// Return Value: -// - true if read is completed. false if we need to keep waiting and be called again with the user's next keystroke. -bool COOKED_READ_DATA::ProcessInput(const wchar_t wchOrig, - const DWORD keyState, - NTSTATUS& status) -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - size_t NumSpaces = 0; - til::CoordType ScrollY = 0; - size_t NumToWrite; - auto wch = wchOrig; - bool fStartFromDelim; - - status = STATUS_SUCCESS; - if (_bytesRead >= (_bufferSize - (2 * sizeof(WCHAR))) && wch != UNICODE_CARRIAGERETURN && wch != UNICODE_BACKSPACE) + if (_distanceEnd) { - return false; + _unwindCursorPosition(_distanceCursor); + _erase(_distanceEnd); + _unwindCursorPosition(_distanceEnd); + _distanceCursor = 0; + _distanceEnd = 0; } +} - if (_ctrlWakeupMask != 0 && wch < L' ' && (_ctrlWakeupMask & (1 << wch))) +// The counter-part to EraseBeforeResize(). +void COOKED_READ_DATA::RedrawAfterResize() +{ + _markAsDirty(); + _flushBuffer(); +} + +void COOKED_READ_DATA::SetInsertMode(bool insertMode) noexcept +{ + _insertMode = insertMode; +} + +bool COOKED_READ_DATA::IsEmpty() const noexcept +{ + return _buffer.empty() && _popups.empty(); +} + +bool COOKED_READ_DATA::PresentingPopup() const noexcept +{ + return !_popups.empty(); +} + +til::point_span COOKED_READ_DATA::GetBoundaries() const noexcept +{ + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + const auto beg = _offsetPosition(cursor.GetPosition(), -_distanceCursor); + const auto end = _offsetPosition(beg, _distanceEnd); + return { beg, end }; +} + +// _wordPrev and _wordNext implement the classic Windows word-wise cursor movement algorithm, as traditionally used by +// conhost, notepad, Visual Studio and other "old" applications. If you look closely you can see how they're the exact +// same "skip 1 char, skip x, skip not-x", but since the "x" between them is different (non-words for _wordPrev and +// words for _wordNext) it results in the inconsistent feeling that these have compared to more modern algorithms. +// TODO: GH#15787 +size_t COOKED_READ_DATA::_wordPrev(const std::wstring_view& chars, size_t position) +{ + if (position != 0) { - *_bufPtr = wch; - _bytesRead += sizeof(WCHAR); - _bufPtr += 1; - _currentPosition += 1; - _controlKeyState = keyState; - return true; + --position; + while (position != 0 && chars[position] == L' ') + { + --position; + } + + const auto dc = DelimiterClass(chars[position]); + while (position != 0 && DelimiterClass(chars[position - 1]) == dc) + { + --position; + } } + return position; +} - if (wch == EXTKEY_ERASE_PREV_WORD) +size_t COOKED_READ_DATA::_wordNext(const std::wstring_view& chars, size_t position) +{ + if (position < chars.size()) { - wch = UNICODE_BACKSPACE; + ++position; + const auto dc = DelimiterClass(chars[position - 1]); + while (position != chars.size() && dc == DelimiterClass(chars[position])) + { + ++position; + } + while (position != chars.size() && chars[position] == L' ') + { + ++position; + } } + return position; +} + +const std::wstring_view& COOKED_READ_DATA::_newlineSuffix() const noexcept +{ + static constexpr std::wstring_view cr{ L"\r" }; + static constexpr std::wstring_view crlf{ L"\r\n" }; + return WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT) ? crlf : cr; +} - if (AtEol()) +// Reads text off of the InputBuffer and dispatches it to the current popup or otherwise into the _buffer contents. +bool COOKED_READ_DATA::_readCharInputLoop() +{ + for (;;) { - // If at end of line, processing is relatively simple. Just store the character and write it to the screen. - if (wch == UNICODE_BACKSPACE2) + const auto hasPopup = !_popups.empty(); + auto charOrVkey = UNICODE_NULL; + auto commandLineEditingKeys = false; + auto popupKeys = false; + const auto pCommandLineEditingKeys = hasPopup ? nullptr : &commandLineEditingKeys; + const auto pPopupKeys = hasPopup ? &popupKeys : nullptr; + DWORD modifiers = 0; + + const auto status = GetChar(_pInputBuffer, &charOrVkey, true, pCommandLineEditingKeys, pPopupKeys, &modifiers); + if (status == CONSOLE_STATUS_WAIT) { - wch = UNICODE_BACKSPACE; + return false; } + THROW_IF_NTSTATUS_FAILED(status); - if (wch != UNICODE_BACKSPACE || _bufPtr != _backupLimit) + if (hasPopup) + { + const auto wch = static_cast(popupKeys ? 0 : charOrVkey); + const auto vkey = static_cast(popupKeys ? charOrVkey : 0); + if (_popupHandleInput(wch, vkey, modifiers)) + { + return true; + } + } + else { - fStartFromDelim = IsWordDelim(_bufPtr[-1]); + if (commandLineEditingKeys) + { + _handleVkey(charOrVkey, modifiers); + } + else if (_handleChar(charOrVkey, modifiers)) + { + return true; + } + } + } +} + +// Handles character input for _readCharInputLoop() when no popups exist. +bool COOKED_READ_DATA::_handleChar(wchar_t wch, const DWORD modifiers) +{ + // All paths in this function modify the buffer. + + if (_ctrlWakeupMask != 0 && wch < L' ' && (_ctrlWakeupMask & (1 << wch))) + { + _flushBuffer(); + + // The old implementation (all the way since the 90s) overwrote the character at the current cursor position with the given wch. + // But simultaneously it incremented the buffer length, which would have only worked if it was written at the end of the buffer. + // Press tab past the "f" in the string "foo" and you'd get "f\to " (a trailing whitespace; the initial contents of the buffer back then). + // It's unclear whether the original intention was to write at the end of the buffer at all times or to implement an insert mode. + // I went with insert mode. + _buffer.insert(_bufferCursor, 1, wch); + _bufferCursor++; + + _controlKeyState = modifiers; + return true; + } - auto loop = true; - while (loop) + switch (wch) + { + case UNICODE_CARRIAGERETURN: + { + _buffer.append(_newlineSuffix()); + _bufferCursor = _buffer.size(); + _markAsDirty(); + return true; + } + case EXTKEY_ERASE_PREV_WORD: // Ctrl+Backspace + case UNICODE_BACKSPACE: + if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_PROCESSED_INPUT)) + { + size_t pos; + if (wch == EXTKEY_ERASE_PREV_WORD) { - loop = false; - if (_echoInput) - { - NumToWrite = sizeof(WCHAR); - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _bufPtr, - &wch, - &NumToWrite, - &NumSpaces, - _originalCursorPosition.x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY); - if (SUCCEEDED_NTSTATUS(status)) - { - _originalCursorPosition.y += ScrollY; - } - else - { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status); - } - } + pos = _wordPrev(_buffer, _bufferCursor); + } + else + { + pos = TextBuffer::GraphemePrev(_buffer, _bufferCursor); + } - _visibleCharCount += NumSpaces; - if (wch == UNICODE_BACKSPACE && _processedInput) - { - _bytesRead -= sizeof(WCHAR); - // clang-format off -#pragma prefast(suppress: __WARNING_POTENTIAL_BUFFER_OVERFLOW_HIGH_PRIORITY, "This access is fine") - // clang-format on - *_bufPtr = (WCHAR)' '; - _bufPtr -= 1; - _currentPosition -= 1; - - // Repeat until it hits the word boundary - if (wchOrig == EXTKEY_ERASE_PREV_WORD && - _bufPtr != _backupLimit && - fStartFromDelim ^ !IsWordDelim(_bufPtr[-1])) - { - loop = true; - } - } - else + _buffer.erase(pos, _bufferCursor - pos); + _bufferCursor = pos; + _markAsDirty(); + + // Notify accessibility to read the backspaced character. + // See GH:12735, MSFT:31748387 + if (_screenInfo.HasAccessibilityEventing()) + { + if (const auto pConsoleWindow = ServiceLocator::LocateConsoleWindow()) { - *_bufPtr = wch; - _bytesRead += sizeof(WCHAR); - _bufPtr += 1; - _currentPosition += 1; + LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId)); } } + return false; } + // If processed mode is disabled, control characters like backspace are treated like any other character. + break; + default: + break; + } + + if (_insertMode) + { + _buffer.insert(_bufferCursor, 1, wch); } else { - auto CallWrite = true; - const auto sScreenBufferSizeX = _screenInfo.GetBufferSize().Width(); + // TODO GH#15875: If the input grapheme is >1 char, then this will replace >1 grapheme + // --> We should accumulate input text as much as possible and then call _processInput with wstring_view. + const auto nextGraphemeLength = TextBuffer::GraphemeNext(_buffer, _bufferCursor) - _bufferCursor; + _buffer.replace(_bufferCursor, nextGraphemeLength, 1, wch); + } - // processing in the middle of the line is more complex: + _bufferCursor++; + _markAsDirty(); + return false; +} - // calculate new cursor position - // store new char - // clear the current command line from the screen - // write the new command line to the screen - // update the cursor position +// Handles non-character input for _readCharInputLoop() when no popups exist. +void COOKED_READ_DATA::_handleVkey(uint16_t vkey, DWORD modifiers) +{ + const auto ctrlPressed = WI_IsAnyFlagSet(modifiers, LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); + const auto altPressed = WI_IsAnyFlagSet(modifiers, LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); - if (wch == UNICODE_BACKSPACE && _processedInput) + switch (vkey) + { + case VK_ESCAPE: + if (!_buffer.empty()) { - // for backspace, use writechars to calculate the new cursor position. - // this call also sets the cursor to the right position for the - // second call to writechars. - - if (_bufPtr != _backupLimit) + _buffer.clear(); + _bufferCursor = 0; + _markAsDirty(); + } + break; + case VK_HOME: + if (_bufferCursor > 0) + { + if (ctrlPressed) { - fStartFromDelim = IsWordDelim(_bufPtr[-1]); - - auto loop = true; - while (loop) - { - loop = false; - // we call writechar here so that cursor position gets updated - // correctly. we also call it later if we're not at eol so - // that the remainder of the string can be updated correctly. - - if (_echoInput) - { - NumToWrite = sizeof(WCHAR); - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _bufPtr, - &wch, - &NumToWrite, - nullptr, - _originalCursorPosition.x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr); - if (FAILED_NTSTATUS(status)) - { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed %x", status); - } - } - _bytesRead -= sizeof(WCHAR); - _bufPtr -= 1; - _currentPosition -= 1; - memmove(_bufPtr, - _bufPtr + 1, - _bytesRead - (_currentPosition * sizeof(WCHAR))); - { - auto buf = (PWCHAR)((PBYTE)_backupLimit + _bytesRead); - *buf = (WCHAR)' '; - } - NumSpaces = 0; - - // Repeat until it hits the word boundary - if (wchOrig == EXTKEY_ERASE_PREV_WORD && - _bufPtr != _backupLimit && - fStartFromDelim ^ !IsWordDelim(_bufPtr[-1])) - { - loop = true; - } - } + _buffer.erase(0, _bufferCursor); + } + _bufferCursor = 0; + _markAsDirty(); + } + break; + case VK_END: + if (_bufferCursor < _buffer.size()) + { + if (ctrlPressed) + { + _buffer.erase(_bufferCursor); + } + _bufferCursor = _buffer.size(); + _markAsDirty(); + } + break; + case VK_LEFT: + if (_bufferCursor != 0) + { + if (ctrlPressed) + { + _bufferCursor = _wordPrev(_buffer, _bufferCursor); } else { - CallWrite = false; + _bufferCursor = TextBuffer::GraphemePrev(_buffer, _bufferCursor); } + _markAsDirty(); } - else + break; + case VK_F1: + case VK_RIGHT: + if (_bufferCursor != _buffer.size()) { - // store the char - if (wch == UNICODE_CARRIAGERETURN) + if (ctrlPressed && vkey == VK_RIGHT) { - _bufPtr = (PWCHAR)((PBYTE)_backupLimit + _bytesRead); - *_bufPtr = wch; - _bufPtr += 1; - _bytesRead += sizeof(WCHAR); - _currentPosition += 1; + _bufferCursor = _wordNext(_buffer, _bufferCursor); } else { - auto fBisect = false; - - if (_echoInput) - { - if (CheckBisectProcessW(_screenInfo, - _backupLimit, - _currentPosition + 1, - sScreenBufferSizeX - _originalCursorPosition.x, - _originalCursorPosition.x, - TRUE)) - { - fBisect = true; - } - } + _bufferCursor = TextBuffer::GraphemeNext(_buffer, _bufferCursor); + } + _markAsDirty(); + } + else if (_history) + { + // Traditionally pressing right at the end of an input line would paste characters from the previous command. + const auto cmd = _history->GetLastCommand(); + const auto bufferSize = _buffer.size(); + const auto cmdSize = cmd.size(); + size_t bufferBeg = 0; + size_t cmdBeg = 0; + + // We cannot just check if the cmd is longer than the _buffer, because we want to copy graphemes, + // not characters and there's no correlation between the number of graphemes and their byte length. + while (cmdBeg < cmdSize) + { + const auto cmdEnd = TextBuffer::GraphemeNext(cmd, cmdBeg); - if (_insertMode) + if (bufferBeg >= bufferSize) { - memmove(_bufPtr + 1, - _bufPtr, - _bytesRead - (_currentPosition * sizeof(WCHAR))); - _bytesRead += sizeof(WCHAR); + _buffer.append(cmd, cmdBeg, cmdEnd - cmdBeg); + _bufferCursor = _buffer.size(); + _markAsDirty(); + break; } - *_bufPtr = wch; - _bufPtr += 1; - _currentPosition += 1; - // calculate new cursor position - if (_echoInput) - { - NumSpaces = RetrieveNumberOfSpaces(_originalCursorPosition.x, - _backupLimit, - _currentPosition - 1); - if (NumSpaces > 0 && fBisect) - NumSpaces--; - } + bufferBeg = TextBuffer::GraphemeNext(_buffer, bufferBeg); + cmdBeg = cmdEnd; } } - - if (_echoInput && CallWrite) + break; + case VK_INSERT: + _insertMode = !_insertMode; + _screenInfo.SetCursorDBMode(_insertMode != ServiceLocator::LocateGlobals().getConsoleInformation().GetInsertMode()); + _markAsDirty(); + break; + case VK_DELETE: + if (_bufferCursor < _buffer.size()) { - til::point CursorPosition; - - // save cursor position - CursorPosition = _screenInfo.GetTextBuffer().GetCursor().GetPosition(); - CursorPosition.x = (til::CoordType)(CursorPosition.x + NumSpaces); - - // clear the current command line from the screen - // clang-format off -#pragma prefast(suppress: __WARNING_BUFFER_OVERFLOW, "Not sure why prefast doesn't like this call.") - // clang-format on - DeleteCommandLine(*this, FALSE); - - // write the new command line to the screen - NumToWrite = _bytesRead; - - DWORD dwFlags = WC_INTERACTIVE; - if (wch == UNICODE_CARRIAGERETURN) + _buffer.erase(_bufferCursor, TextBuffer::GraphemeNext(_buffer, _bufferCursor) - _bufferCursor); + _markAsDirty(); + } + break; + case VK_UP: + case VK_F5: + if (_history && !_history->AtFirstCommand()) + { + _replaceBuffer(_history->Retrieve(CommandHistory::SearchDirection::Previous)); + } + break; + case VK_DOWN: + if (_history && !_history->AtLastCommand()) + { + _replaceBuffer(_history->Retrieve(CommandHistory::SearchDirection::Next)); + } + break; + case VK_PRIOR: + if (_history && !_history->AtFirstCommand()) + { + _replaceBuffer(_history->RetrieveNth(0)); + } + break; + case VK_NEXT: + if (_history && !_history->AtLastCommand()) + { + _replaceBuffer(_history->RetrieveNth(INT_MAX)); + } + break; + case VK_F2: + if (_history) + { + _popupPush(PopupKind::CopyToChar); + } + break; + case VK_F3: + if (_history) + { + const auto last = _history->GetLastCommand(); + if (last.size() > _bufferCursor) + { + const auto count = last.size() - _bufferCursor; + _buffer.replace(_bufferCursor, count, last, _bufferCursor, count); + _bufferCursor += count; + _markAsDirty(); + } + } + break; + case VK_F4: + // Historically the CopyFromChar popup was constrained to only work when a history exists, + // but I don't see why that should be. It doesn't depend on _history at all. + _popupPush(PopupKind::CopyFromChar); + break; + case VK_F6: + // Don't ask me why but F6 is an alias for ^Z. + _handleChar(0x1a, modifiers); + break; + case VK_F7: + if (!ctrlPressed && !altPressed) + { + if (_history && _history->GetNumberOfCommands()) { - dwFlags |= WC_KEEP_CURSOR_VISIBLE; + _popupPush(PopupKind::CommandList); } - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _backupLimit, - _backupLimit, - &NumToWrite, - &_visibleCharCount, - _originalCursorPosition.x, - dwFlags, - &ScrollY); - if (FAILED_NTSTATUS(status)) + } + else if (altPressed) + { + if (_history) { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status); - _bytesRead = 0; - return true; + _history->Empty(); + _history->Flags |= CommandHistory::CLE_ALLOCATED; } - - // update cursor position - if (wch != UNICODE_CARRIAGERETURN) + } + break; + case VK_F8: + if (_history) + { + CommandHistory::Index index = 0; + const auto prefix = std::wstring_view{ _buffer }.substr(0, _bufferCursor); + if (_history->FindMatchingCommand(prefix, _history->LastDisplayed, index, CommandHistory::MatchOptions::None)) { - if (CheckBisectProcessW(_screenInfo, - _backupLimit, - _currentPosition + 1, - sScreenBufferSizeX - _originalCursorPosition.x, - _originalCursorPosition.x, - TRUE)) - { - if (CursorPosition.x == (sScreenBufferSizeX - 1)) - { - CursorPosition.x++; - } - } - - // adjust cursor position for WriteChars - _originalCursorPosition.y += ScrollY; - CursorPosition.y += ScrollY; - AdjustCursorPosition(_screenInfo, CursorPosition, TRUE, nullptr); + _buffer.assign(_history->RetrieveNth(index)); + _bufferCursor = std::min(_bufferCursor, _buffer.size()); + _markAsDirty(); } } + break; + case VK_F9: + if (_history && _history->GetNumberOfCommands()) + { + _popupPush(PopupKind::CommandNumber); + } + break; + case VK_F10: + // Alt+F10 clears the aliases for specifically cmd.exe. + if (altPressed) + { + Alias::s_ClearCmdExeAliases(); + } + break; + default: + assert(false); // Unrecognized VK. Fix or don't call this function? + break; } +} - // in cooked mode, enter (carriage return) is converted to - // carriage return linefeed (0xda). carriage return is always - // stored at the end of the buffer. - if (wch == UNICODE_CARRIAGERETURN) +// Handles any tasks that need to be completed after the read input loop finishes, +// like handling doskey aliases and converting the input to non-UTF16. +void COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) +{ + auto writer = _userBuffer; + std::wstring_view input{ _buffer }; + size_t lineCount = 1; + + if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT)) { - if (_processedInput) + // The last characters in line-read are a \r or \r\n unless _ctrlWakeupMask was used. + // Neither History nor s_MatchAndCopyAlias want to know about them. + const auto& suffix = _newlineSuffix(); + if (input.ends_with(suffix)) { - if (_bytesRead < _bufferSize) + input.remove_suffix(suffix.size()); + + if (_history) { - *_bufPtr = UNICODE_LINEFEED; - if (_echoInput) - { - NumToWrite = sizeof(WCHAR); - status = WriteCharsLegacy(_screenInfo, - _backupLimit, - _bufPtr, - _bufPtr, - &NumToWrite, - nullptr, - _originalCursorPosition.x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - nullptr); - if (FAILED_NTSTATUS(status)) - { - RIPMSG1(RIP_WARNING, "WriteCharsLegacy failed 0x%x", status); - } - } - _bytesRead += sizeof(WCHAR); - _bufPtr++; - _currentPosition += 1; + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + LOG_IF_FAILED(_history->Add(input, WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP))); } - } - // reset the cursor back to 25% if necessary - if (_lineInput) - { - if (_insertMode != gci.GetInsertMode()) + + Tracing::s_TraceCookedRead(_processHandle, input); + + const auto alias = Alias::s_MatchAndCopyAlias(input, _exeName, lineCount); + if (!alias.empty()) { - // Make cursor small. - LOG_IF_FAILED(CommandLine::Instance().ProcessCommandLine(*this, VK_INSERT, 0)); + _buffer = alias; } - status = STATUS_SUCCESS; - return true; + // NOTE: Even if there's no alias we should restore the trailing \r\n that we removed above. + input = std::wstring_view{ _buffer }; + + // doskey aliases may result in multiple lines of output (for instance `doskey test=echo foo$Techo bar$Techo baz`). + // We need to emit them as multiple cooked reads as well, so that each read completes at a \r\n. + if (lineCount > 1) + { + // ProcessAliases() is supposed to end each line with \r\n. If it doesn't we might as well fail-fast. + const auto firstLineEnd = input.find(UNICODE_LINEFEED) + 1; + input = input.substr(0, std::min(input.size(), firstLineEnd)); + } } } - return false; + const auto inputSizeBefore = input.size(); + _pInputBuffer->Consume(isUnicode, input, writer); + + if (lineCount > 1) + { + // This is a continuation of the above identical if condition. + // We've truncated the `input` slice and now we need to restore it. + const auto inputSizeAfter = input.size(); + const auto amountConsumed = inputSizeBefore - inputSizeAfter; + input = std::wstring_view{ _buffer }; + input = input.substr(std::min(input.size(), amountConsumed)); + GetInputReadHandleData()->SaveMultilinePendingInput(input); + } + else if (!input.empty()) + { + GetInputReadHandleData()->SavePendingInput(input); + } + + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.Flags |= CONSOLE_IGNORE_NEXT_KEYUP; + + // If we previously called SetCursorDBMode() with true, + // this will ensure that the cursor returns to its normal look. + _screenInfo.SetCursorDBMode(false); + + numBytes = _userBuffer.size() - writer.size(); + controlKeyState = _controlKeyState; } -// Routine Description: -// - Writes string to current position in prompt line. can overwrite text to the right of the cursor. -// Arguments: -// - wstr - the string to write -// Return Value: -// - The number of chars written -size_t COOKED_READ_DATA::Write(const std::wstring_view wstr) +// Signals to _flushBuffer() that the contents of _buffer are stale and need to be redrawn. +// ALL _buffer and _bufferCursor changes must be flagged with _markAsDirty(). +// +// By using _bufferDirty to avoid redrawing the buffer unless needed, we turn the amortized time complexity of _readCharInputLoop() +// from O(n^2) (n(n+1)/2 redraws) into O(n). Pasting text would quickly turn into "accidentally quadratic" meme material otherwise. +void COOKED_READ_DATA::_markAsDirty() { - auto end = wstr.end(); - const auto charsRemaining = (_bufferSize / sizeof(wchar_t)) - (_bufPtr - _backupLimit); - if (wstr.size() > charsRemaining) + _bufferDirty = true; +} + +// Draws the contents of _buffer onto the screen. +void COOKED_READ_DATA::_flushBuffer() +{ + // _flushBuffer() is called often and is a good place to assert() that our _bufferCursor is still in bounds. + assert(_bufferCursor <= _buffer.size()); + _bufferCursor = std::min(_bufferCursor, _buffer.size()); + + if (!_bufferDirty) { - end = std::next(wstr.begin(), charsRemaining); + return; } - std::copy(wstr.begin(), end, _bufPtr); - const size_t charsInserted = end - wstr.begin(); - auto bytesInserted = charsInserted * sizeof(wchar_t); - _currentPosition += charsInserted; - _bytesRead += bytesInserted; - - if (IsEchoInput()) + if (WI_IsFlagSet(_pInputBuffer->InputMode, ENABLE_ECHO_INPUT)) { - size_t NumSpaces = 0; - til::CoordType ScrollY = 0; - - FAIL_FAST_IF_NTSTATUS_FAILED(WriteCharsLegacy(ScreenInfo(), - _backupLimit, - _bufPtr, - _bufPtr, - &bytesInserted, - &NumSpaces, - OriginalCursorPosition().x, - WC_INTERACTIVE | WC_KEEP_CURSOR_VISIBLE, - &ScrollY)); - OriginalCursorPosition().y += ScrollY; - VisibleCharCount() += NumSpaces; + _unwindCursorPosition(_distanceCursor); + + const std::wstring_view view{ _buffer }; + const auto distanceBeforeCursor = _writeChars(view.substr(0, _bufferCursor)); + const auto distanceAfterCursor = _writeChars(view.substr(_bufferCursor)); + const auto distanceEnd = distanceBeforeCursor + distanceAfterCursor; + const auto eraseDistance = std::max(0, _distanceEnd - distanceEnd); + + // If the contents of _buffer became shorter we'll have to erase the previously printed contents. + _erase(eraseDistance); + _unwindCursorPosition(distanceAfterCursor + eraseDistance); + + _distanceCursor = distanceBeforeCursor; + _distanceEnd = distanceEnd; } - _bufPtr += charsInserted; - return charsInserted; + _bufferDirty = false; } -// Routine Description: -// - saves data in the prompt buffer to the outgoing user buffer -// Arguments: -// - cch - the number of chars to write to the user buffer -// Return Value: -// - the number of bytes written to the user buffer -size_t COOKED_READ_DATA::SavePromptToUserBuffer(const size_t cch) +// This is just a small helper to fill the next N cells starting at the current cursor position with whitespace. +// The implementation is inefficient for `count`s larger than 7, but such calls are uncommon to happen (namely only when resizing the window). +void COOKED_READ_DATA::_erase(const til::CoordType distance) +{ + if (distance > 0) + { + const std::wstring str(gsl::narrow_cast(distance), L' '); + std::ignore = _writeChars(str); + } +} + +// A helper to write text and calculate the number of cells we've written. +// _unwindCursorPosition then allows us to go that many cells back. Tracking cells instead of explicit +// buffer positions allows us to pay no further mind to whether the buffer scrolled up or not. +til::CoordType COOKED_READ_DATA::_writeChars(const std::wstring_view& text) const { - size_t bytesToWrite = 0; - const auto hr = SizeTMult(cch, sizeof(wchar_t), &bytesToWrite); - if (FAILED(hr)) + if (text.empty()) { return 0; } - memmove(_userBuffer, _backupLimit, bytesToWrite); + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + const auto width = textBuffer.GetSize().Width(); + const auto initialCursorPos = cursor.GetPosition(); + til::CoordType scrollY = 0; + + WriteCharsLegacy(_screenInfo, text, true, &scrollY); + + const auto finalCursorPos = cursor.GetPosition(); + return (finalCursorPos.y - initialCursorPos.y + scrollY) * width + finalCursorPos.x - initialCursorPos.x; +} + +// Moves the given point by the given distance inside the text buffer, as if moving a cursor with the left/right arrow keys. +til::point COOKED_READ_DATA::_offsetPosition(til::point pos, til::CoordType distance) const +{ + const auto size = _screenInfo.GetTextBuffer().GetSize().Dimensions(); + const auto w = static_cast(size.width); + const auto h = static_cast(size.height); + const auto area = w * h; + + auto off = w * pos.y + pos.x; + off += distance; + off = off < 0 ? 0 : (off > area ? area : off); + + return { + gsl::narrow_cast(off % w), + gsl::narrow_cast(off / w), + }; +} + +// This moves the cursor `distance`-many cells back up in the buffer. +// It's intended to be used in combination with _writeChars. +void COOKED_READ_DATA::_unwindCursorPosition(til::CoordType distance) const +{ + if (distance <= 0) + { + // If all the code in this file works correctly, negative distances should not occur. + // If they do occur it would indicate a bug that would need to be fixed urgently. + assert(distance == 0); + return; + } + + const auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto& cursor = textBuffer.GetCursor(); + const auto pos = _offsetPosition(cursor.GetPosition(), -distance); + + std::ignore = _screenInfo.SetCursorPosition(pos, true); + _screenInfo.MakeCursorVisible(pos); +} + +// Just a simple helper to replace the entire buffer contents. +void COOKED_READ_DATA::_replaceBuffer(const std::wstring_view& str) +{ + _buffer.assign(str); + _bufferCursor = _buffer.size(); + _markAsDirty(); +} + +// If the viewport is large enough to fit a popup, this function prepares everything for +// showing the given type. It handles computing the size of the popup, its position, +// backs the affected area up and draws the border and initial contents. +void COOKED_READ_DATA::_popupPush(const PopupKind kind) +try +{ + auto& textBuffer = _screenInfo.GetTextBuffer(); + const auto viewport = _screenInfo.GetViewport(); + const auto viewportOrigin = viewport.Origin(); + const auto viewportSize = viewport.Dimensions(); + + til::size proposedSize; + switch (kind) + { + case PopupKind::CopyToChar: + proposedSize = { 26, 1 }; + break; + case PopupKind::CopyFromChar: + proposedSize = { 28, 1 }; + break; + case PopupKind::CommandNumber: + proposedSize = { 22 + CommandNumberMaxInputLength, 1 }; + break; + case PopupKind::CommandList: + { + const auto& commands = _history->GetCommands(); + const auto commandCount = _history->GetNumberOfCommands(); + + size_t maxStringLength = 0; + for (const auto& c : commands) + { + maxStringLength = std::max(maxStringLength, c.size()); + } + + // Account for the "123: " prefix each line gets. + maxStringLength += integerLog10(commandCount); + maxStringLength += 3; + + // conhost used to draw the command list with a size of 40x10, but at some point it switched over to dynamically + // sizing it depending on the history count and width of the entries. Back then it was implemented with the + // assumption that the code unit count equals the column count, which I kept because I don't want the TextBuffer + // class to expose how wide characters are, any more than necessary. It makes implementing Unicode support + // much harder, because things like combining marks and work zones may cause TextBuffer to end up deciding + // a piece of text has a different size than what you thought it had when measuring it on its own. + proposedSize.width = gsl::narrow_cast(std::clamp(maxStringLength, 40, til::CoordTypeMax)); + proposedSize.height = std::clamp(commandCount, 10, 20); + break; + } + default: + assert(false); + return; + } + + // Subtract 2 because we need to draw the border around our content. We must return early if we're + // unable to do so, otherwise the remaining code fails because the size would be zero/negative. + const til::size contentSize{ + std::min(proposedSize.width, viewportSize.width - 2), + std::min(proposedSize.height, viewportSize.height - 2), + }; + if (!contentSize) + { + return; + } - if (!IsUnicode()) + const auto widthSizeT = gsl::narrow_cast(contentSize.width + 2); + const auto heightSizeT = gsl::narrow_cast(contentSize.height + 2); + const til::point contentOrigin{ + (viewportSize.width - contentSize.width) / 2 + viewportOrigin.x, + (viewportSize.height - contentSize.height) / 2 + viewportOrigin.y, + }; + const til::rect contentRect{ + contentOrigin, + contentSize, + }; + const auto backupRect = Viewport::FromExclusive({ + contentRect.left - 1, + contentRect.top - 1, + contentRect.right + 1, + contentRect.bottom + 1, + }); + + auto& popup = _popups.emplace_back(kind, contentRect, backupRect, std::vector{ widthSizeT * heightSizeT }); + + // Create a backup of the TextBuffer parts we're scribbling over. + // We need to flush the buffer to ensure we capture the latest contents. + // NOTE: This may theoretically modify popup.backupRect (practically unlikely). + _flushBuffer(); + THROW_IF_FAILED(ServiceLocator::LocateGlobals().api->ReadConsoleOutputWImpl(_screenInfo, popup.backup, backupRect, popup.backupRect)); + + // Draw the border around our content and fill it with whitespace to prepare it for future usage. { - try + const auto attributes = _screenInfo.GetPopupAttributes(); + + RowWriteState state{ + .columnBegin = contentRect.left - 1, + .columnLimit = contentRect.right + 1, + }; + + // top line ┌───┐ + std::wstring buffer; + buffer.assign(widthSizeT, L'─'); + buffer.front() = L'┌'; + buffer.back() = L'┐'; + state.text = buffer; + textBuffer.Write(contentRect.top - 1, attributes, state); + + // bottom line └───┘ + buffer.front() = L'└'; + buffer.back() = L'┘'; + state.text = buffer; + textBuffer.Write(contentRect.bottom, attributes, state); + + // middle lines │ │ + buffer.assign(widthSizeT, L' '); + buffer.front() = L'│'; + buffer.back() = L'│'; + for (til::CoordType y = contentRect.top; y < contentRect.bottom; ++y) { - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto wstr = ConvertToW(gci.CP, { _userBuffer, cch }); - const auto copyAmount = std::min(wstr.size() * sizeof(wchar_t), _userBufferSize); - std::copy_n(reinterpret_cast(wstr.data()), copyAmount, _userBuffer); - return copyAmount; + state.text = buffer; + textBuffer.Write(y, attributes, state); } - CATCH_LOG(); } - return bytesToWrite; + + switch (kind) + { + case PopupKind::CopyToChar: + _popupDrawPrompt(popup, ID_CONSOLE_MSGCMDLINEF2); + break; + case PopupKind::CopyFromChar: + _popupDrawPrompt(popup, ID_CONSOLE_MSGCMDLINEF4); + break; + case PopupKind::CommandNumber: + popup.commandNumber.buffer.fill(' '); + popup.commandNumber.bufferSize = 0; + _popupDrawPrompt(popup, ID_CONSOLE_MSGCMDLINEF9); + break; + case PopupKind::CommandList: + popup.commandList.selected = _history->LastDisplayed; + popup.commandList.top = popup.commandList.selected - contentSize.height / 2; + _popupDrawCommandList(popup); + break; + default: + assert(false); + } + + // If this is the first popup to be shown, stop the cursor from appearing/blinking + if (_popups.size() == 1) + { + textBuffer.GetCursor().SetIsPopupShown(true); + } +} +catch (...) +{ + LOG_CAUGHT_EXCEPTION(); + // Using _popupsDone() is a convenient way to restore the buffer contents if anything in this call failed. + // This could technically dismiss an unrelated popup that was already in _popups, but reaching this point is unlikely anyways. + _popupsDone(); } -// Routine Description: -// - saves data in the prompt buffer as pending input -// Arguments: -// - index - the index of what wchar to start the saving -// - multiline - whether the pending input should be saved as multiline or not -void COOKED_READ_DATA::SavePendingInput(const size_t index, const bool multiline) +// Dismisses all current popups at once. Right now we don't need support for just dismissing the topmost popup. +// In fact, there's only a single situation right now where there can be >1 popup: +// Pressing F7 followed by F9 (CommandNumber on top of CommandList). +void COOKED_READ_DATA::_popupsDone() { - auto& inputReadHandleData = *GetInputReadHandleData(); - const std::wstring_view pending{ _backupLimit + index, - BytesRead() / sizeof(wchar_t) - index }; - if (multiline) + while (!_popups.empty()) { - inputReadHandleData.SaveMultilinePendingInput(pending); + auto& popup = _popups.back(); + + // Restore TextBuffer contents. They could be empty if _popupPush() + // threw an exception in the middle of construction. + if (!popup.backup.empty()) + { + [[maybe_unused]] Viewport unused; + LOG_IF_FAILED(ServiceLocator::LocateGlobals().api->WriteConsoleOutputWImpl(_screenInfo, popup.backup, popup.backupRect, unused)); + } + + _popups.pop_back(); } - else + + // Restore cursor blinking. + _screenInfo.GetTextBuffer().GetCursor().SetIsPopupShown(false); +} + +bool COOKED_READ_DATA::_popupHandleInput(wchar_t wch, uint16_t vkey, DWORD modifiers) +{ + if (_popups.empty()) + { + assert(false); // Don't call this function. + return false; + } + + auto& popup = _popups.back(); + switch (popup.kind) { - inputReadHandleData.SavePendingInput(pending); + case PopupKind::CopyToChar: + _popupHandleCopyToCharInput(popup, wch, vkey, modifiers); + return false; + case PopupKind::CopyFromChar: + _popupHandleCopyFromCharInput(popup, wch, vkey, modifiers); + return false; + case PopupKind::CommandNumber: + _popupHandleCommandNumberInput(popup, wch, vkey, modifiers); + return false; + case PopupKind::CommandList: + return _popupHandleCommandListInput(popup, wch, vkey, modifiers); + default: + return false; } } -// Routine Description: -// - saves data in the prompt buffer as pending input -// Arguments: -// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done. -// - numBytes - On in, the number of bytes available in the client -// buffer. On out, the number of bytes consumed in the client buffer. -// Return Value: -// - Status code that indicates success, wait, etc. -[[nodiscard]] NTSTATUS COOKED_READ_DATA::_readCharInputLoop(const bool isUnicode, size_t& numBytes) noexcept +void COOKED_READ_DATA::_popupHandleCopyToCharInput(Popup& /*popup*/, const wchar_t wch, const uint16_t vkey, const DWORD /*modifiers*/) { - auto Status = STATUS_SUCCESS; - - while (_bytesRead < _bufferSize) + if (vkey) { - auto wch = UNICODE_NULL; - auto commandLineEditingKeys = false; - DWORD keyState = 0; - - // This call to GetChar may block. - Status = GetChar(_pInputBuffer, - &wch, - true, - &commandLineEditingKeys, - nullptr, - &keyState); - if (FAILED_NTSTATUS(Status)) + if (vkey == VK_ESCAPE) { - if (Status != CONSOLE_STATUS_WAIT) - { - _bytesRead = 0; - } - break; + _popupsDone(); } + } + else + { + // See PopupKind::CopyToChar for more information about this code. + const auto cmd = _history->GetLastCommand(); + const auto idx = cmd.find(wch, _bufferCursor); - // we should probably set these up in GetChars, but we set them - // up here because the debugger is multi-threaded and calls - // read before outputting the prompt. - - if (_originalCursorPosition.x == -1) + if (idx != decltype(cmd)::npos) { - _originalCursorPosition = _screenInfo.GetTextBuffer().GetCursor().GetPosition(); + // When we enter this if condition it's guaranteed that _bufferCursor must be + // smaller than idx, which in turn implies that it's smaller than cmd.size(). + // As such, calculating length is safe and str.size() == length. + const auto count = idx - _bufferCursor; + _buffer.replace(_bufferCursor, count, cmd, _bufferCursor, count); + _bufferCursor += count; + _markAsDirty(); } - if (commandLineEditingKeys) + _popupsDone(); + } +} + +void COOKED_READ_DATA::_popupHandleCopyFromCharInput(Popup& /*popup*/, const wchar_t wch, const uint16_t vkey, const DWORD /*modifiers*/) +{ + if (vkey) + { + if (vkey == VK_ESCAPE) { - // TODO: this is super weird for command line popups only - _unicode = isUnicode; + _popupsDone(); + } + } + else + { + // See PopupKind::CopyFromChar for more information about this code. + const auto idx = _buffer.find(wch, _bufferCursor); + _buffer.erase(_bufferCursor, std::min(idx, _buffer.size()) - _bufferCursor); + _markAsDirty(); + _popupsDone(); + } +} - _pdwNumBytes = &numBytes; +void COOKED_READ_DATA::_popupHandleCommandNumberInput(Popup& popup, const wchar_t wch, const uint16_t vkey, const DWORD /*modifiers*/) +{ + if (vkey) + { + if (vkey == VK_ESCAPE) + { + _popupsDone(); + } + } + else + { + if (wch == UNICODE_CARRIAGERETURN) + { + popup.commandNumber.buffer[popup.commandNumber.bufferSize++] = L'\0'; + _replaceBuffer(_history->RetrieveNth(std::stoi(popup.commandNumber.buffer.data()))); + _popupsDone(); + return; + } - Status = CommandLine::Instance().ProcessCommandLine(*this, wch, keyState); - if (Status == CONSOLE_STATUS_READ_COMPLETE || Status == CONSOLE_STATUS_WAIT) + if (wch >= L'0' && wch <= L'9') + { + if (popup.commandNumber.bufferSize < CommandNumberMaxInputLength) { - break; + popup.commandNumber.buffer[popup.commandNumber.bufferSize++] = wch; } - if (FAILED_NTSTATUS(Status)) + } + else if (wch == UNICODE_BACKSPACE) + { + if (popup.commandNumber.bufferSize > 0) { - if (Status == CONSOLE_STATUS_WAIT_NO_BLOCK) - { - Status = CONSOLE_STATUS_WAIT; - } - else - { - _bytesRead = 0; - } - break; + popup.commandNumber.buffer[--popup.commandNumber.bufferSize] = L' '; } } else { - if (ProcessInput(wch, keyState, Status)) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.Flags |= CONSOLE_IGNORE_NEXT_KEYUP; - break; - } + return; } + + RowWriteState state{ + .text = { popup.commandNumber.buffer.data(), CommandNumberMaxInputLength }, + .columnBegin = popup.contentRect.right - CommandNumberMaxInputLength, + .columnLimit = popup.contentRect.right, + }; + _screenInfo.GetTextBuffer().Write(popup.contentRect.top, _screenInfo.GetPopupAttributes(), state); } - return Status; } -// Routine Description: -// - handles any tasks that need to be completed after the read input loop finishes -// Arguments: -// - isUnicode - Treat as UCS-2 unicode or use Input CP to convert when done. -// - numBytes - On in, the number of bytes available in the client -// buffer. On out, the number of bytes consumed in the client buffer. -// - controlKeyState - For some types of reads, this is the modifier key state with the last button press. -// Return Value: -// - Status code that indicates success, out of memory, etc. -[[nodiscard]] NTSTATUS COOKED_READ_DATA::_handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept +bool COOKED_READ_DATA::_popupHandleCommandListInput(Popup& popup, const wchar_t wch, const uint16_t vkey, const DWORD modifiers) { - std::span writer{ _userBuffer, _userBufferSize }; - std::wstring_view input{ _backupLimit, _bytesRead / sizeof(wchar_t) }; - DWORD LineCount = 1; + auto& cl = popup.commandList; + + if (wch == UNICODE_CARRIAGERETURN) + { + _buffer.assign(_history->RetrieveNth(cl.selected)); + _popupsDone(); + return _handleChar(UNICODE_CARRIAGERETURN, modifiers); + } - if (_echoInput) + switch (vkey) { - const auto idx = input.find(UNICODE_CARRIAGERETURN); - if (idx != decltype(input)::npos) + case VK_ESCAPE: + _popupsDone(); + return false; + case VK_F9: + _popupPush(PopupKind::CommandNumber); + return false; + case VK_DELETE: + _history->Remove(cl.selected); + if (_history->GetNumberOfCommands() <= 0) { - if (_commandHistory) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - LOG_IF_FAILED(_commandHistory->Add({ _backupLimit, idx }, WI_IsFlagSet(gci.Flags, CONSOLE_HISTORY_NODUP))); - } + _popupsDone(); + return false; + } + break; + case VK_LEFT: + case VK_RIGHT: + _replaceBuffer(_history->RetrieveNth(cl.selected)); + _popupsDone(); + return false; + case VK_UP: + if (WI_IsFlagSet(modifiers, SHIFT_PRESSED)) + { + _history->Swap(cl.selected, cl.selected - 1); + } + // _popupDrawCommandList() clamps all values to valid ranges in `cl`. + cl.selected--; + break; + case VK_DOWN: + if (WI_IsFlagSet(modifiers, SHIFT_PRESSED)) + { + _history->Swap(cl.selected, cl.selected + 1); + } + // _popupDrawCommandList() clamps all values to valid ranges in `cl`. + cl.selected++; + break; + case VK_HOME: + cl.selected = 0; + break; + case VK_END: + // _popupDrawCommandList() clamps all values to valid ranges in `cl`. + cl.selected = INT_MAX; + break; + case VK_PRIOR: + // _popupDrawCommandList() clamps all values to valid ranges in `cl`. + cl.selected -= popup.contentRect.height(); + break; + case VK_NEXT: + // _popupDrawCommandList() clamps all values to valid ranges in `cl`. + cl.selected += popup.contentRect.height(); + break; + default: + return false; + } - Tracing::s_TraceCookedRead(_clientProcess, _backupLimit, base::saturated_cast(idx)); + _popupDrawCommandList(popup); + return false; +} - // Don't be fooled by ProcessAliases only taking one argument. It rewrites multiple - // class members on return, including `_bytesRead`, requiring us to reconstruct `input`. - ProcessAliases(LineCount); - input = { _backupLimit, _bytesRead / sizeof(wchar_t) }; +void COOKED_READ_DATA::_popupDrawPrompt(const Popup& popup, const UINT id) const +{ + const auto text = _LoadString(id); + RowWriteState state{ + .text = text, + .columnBegin = popup.contentRect.left, + .columnLimit = popup.contentRect.right, + }; + _screenInfo.GetTextBuffer().Write(popup.contentRect.top, _screenInfo.GetPopupAttributes(), state); +} - // The exact reasons for this are unclear to me (the one writing this comment), but this code used to - // split the contents of a multiline alias (for instance `doskey test=echo foo$Techo bar$Techo baz`) - // into multiple separate read outputs, ensuring that the client receives them line by line. - // - // This code first truncates the `input` to only contain the first line, so that Consume() below only - // writes that line into the user buffer. We'll later store the remainder in SaveMultilinePendingInput(). - if (LineCount > 1) - { - // ProcessAliases() is supposed to end each line with \r\n. If it doesn't we might as well fail-fast. - const auto firstLineEnd = input.find(UNICODE_LINEFEED) + 1; - input = input.substr(0, std::min(input.size(), firstLineEnd)); - } - } - } +void COOKED_READ_DATA::_popupDrawCommandList(Popup& popup) const +{ + assert(popup.kind == PopupKind::CommandList); - const auto inputSizeBefore = input.size(); - GetInputBuffer()->Consume(isUnicode, input, writer); + auto& cl = popup.commandList; + const auto max = _history->GetNumberOfCommands(); + const auto width = popup.contentRect.narrow_width(); + const auto height = std::min(popup.contentRect.height(), _history->GetNumberOfCommands()); + const auto dirtyHeight = std::max(height, cl.dirtyHeight); - if (LineCount > 1) - { - // This is a continuation of the above identical if condition. - // We've truncated the `input` slice and now we need to restore it. - const auto inputSizeAfter = input.size(); - const auto amountConsumed = inputSizeBefore - inputSizeAfter; - input = { _backupLimit, _bytesRead / sizeof(wchar_t) }; - input = input.substr(std::min(input.size(), amountConsumed)); - GetInputReadHandleData()->SaveMultilinePendingInput(input); - } - else if (!input.empty()) { - GetInputReadHandleData()->SavePendingInput(input); + // The viewport movement of the popup is anchored around the current selection first and foremost. + cl.selected = std::clamp(cl.selected, 0, max - 1); + + // It then lazily follows it when the selection goes out of the viewport. + if (cl.selected < cl.top) + { + cl.top = cl.selected; + } + else if (cl.selected >= cl.top + height) + { + cl.top = cl.selected - height + 1; + } + + cl.top = std::clamp(cl.top, 0, max - height); } - numBytes = _userBufferSize - writer.size(); - controlKeyState = _controlKeyState; - return STATUS_SUCCESS; -} + std::wstring buffer; + buffer.reserve(width * 2 + 4); -void COOKED_READ_DATA::MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) -{ - // See the comment in WaitBlock.cpp for more information. - if (_userBuffer == oldBuffer) + const auto& attrRegular = _screenInfo.GetPopupAttributes(); + auto attrInverted = attrRegular; + attrInverted.Invert(); + + RowWriteState state{ + .columnBegin = popup.contentRect.left, + .columnLimit = popup.contentRect.right, + }; + + for (til::CoordType off = 0; off < dirtyHeight; ++off) { - _userBuffer = static_cast(newBuffer); + const auto y = popup.contentRect.top + off; + const auto historyIndex = cl.top + off; + const auto str = _history->GetNth(historyIndex); + const auto& attr = historyIndex == cl.selected ? attrInverted : attrRegular; + + buffer.clear(); + if (!str.empty()) + { + buffer.append(std::to_wstring(historyIndex)); + buffer.append(L": "); + buffer.append(str); + } + buffer.append(width, L' '); + + state.text = buffer; + _screenInfo.GetTextBuffer().Write(y, attr, state); } + + cl.dirtyHeight = height; } diff --git a/src/host/readDataCooked.hpp b/src/host/readDataCooked.hpp index be2e8645bc1..23f99da94da 100644 --- a/src/host/readDataCooked.hpp +++ b/src/host/readDataCooked.hpp @@ -1,29 +1,5 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- readDataCooked.hpp - -Abstract: -- This file defines the read data structure for reading the command line. -- Cooked reads specifically refer to when the console host acts as a command line on behalf - of another console application (e.g. aliases, command history, completion, line manipulation, etc.) -- The data struct will help store context across multiple calls or in the case of a wait condition. -- Wait conditions happen frequently for cooked reads because they're virtually always waiting for - the user to finish "manipulating" the edit line before hitting enter and submitting the final - result to the client application. -- A cooked read is also limited specifically to string/textual information. Only keyboard-type input applies. -- This can be triggered via ReadConsole A/W and ReadFile A/W calls. - -Author: -- Austin Diviness (AustDi) 1-Mar-2017 -- Michael Niksa (MiNiksa) 1-Mar-2017 - -Revision History: -- Pulled from original authoring by Therese Stowell (ThereseS, 1990) -- Separated from cmdline.h/cmdline.cpp (AustDi, 2017) ---*/ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once @@ -33,132 +9,141 @@ Revision History: class COOKED_READ_DATA final : public ReadData { public: - COOKED_READ_DATA(_In_ InputBuffer* const pInputBuffer, - _In_ INPUT_READ_HANDLE_DATA* const pInputReadHandleData, + COOKED_READ_DATA(_In_ InputBuffer* pInputBuffer, + _In_ INPUT_READ_HANDLE_DATA* pInputReadHandleData, SCREEN_INFORMATION& screenInfo, _In_ size_t UserBufferSize, _In_ char* UserBuffer, _In_ ULONG CtrlWakeupMask, - _In_ const std::wstring_view exeName, - _In_ const std::wstring_view initialData, - _In_ ConsoleProcessHandle* const pClientProcess); + _In_ std::wstring_view exeName, + _In_ std::wstring_view initialData, + _In_ ConsoleProcessHandle* pClientProcess); - ~COOKED_READ_DATA() override; - COOKED_READ_DATA(COOKED_READ_DATA&&) = default; + void MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) noexcept override; - bool AtEol() const noexcept; + bool Notify(WaitTerminationReason TerminationReason, + bool fIsUnicode, + _Out_ NTSTATUS* pReplyStatus, + _Out_ size_t* pNumBytes, + _Out_ DWORD* pControlKeyState, + _Out_ void* pOutputData) noexcept override; - void MigrateUserBuffersOnTransitionToBackgroundWait(const void* oldBuffer, void* newBuffer) override; + bool Read(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); - bool Notify(const WaitTerminationReason TerminationReason, - const bool fIsUnicode, - _Out_ NTSTATUS* const pReplyStatus, - _Out_ size_t* const pNumBytes, - _Out_ DWORD* const pControlKeyState, - _Out_ void* const pOutputData) override; + void EraseBeforeResize(); + void RedrawAfterResize(); - std::span SpanAtPointer(); - std::span SpanWholeBuffer(); - - size_t Write(const std::wstring_view wstr); - - void ProcessAliases(DWORD& lineCount); - - [[nodiscard]] HRESULT Read(const bool isUnicode, - size_t& numBytes, - ULONG& controlKeyState) noexcept; - - bool ProcessInput(const wchar_t wch, - const DWORD keyState, - NTSTATUS& status); - - CommandHistory& History() noexcept; - bool HasHistory() const noexcept; - - const size_t& VisibleCharCount() const noexcept; - size_t& VisibleCharCount() noexcept; - - SCREEN_INFORMATION& ScreenInfo() noexcept; - - til::point OriginalCursorPosition() const noexcept; - til::point& OriginalCursorPosition() noexcept; - - til::point& BeforeDialogCursorPosition() noexcept; - - bool IsEchoInput() const noexcept; - bool IsInsertMode() const noexcept; - void SetInsertMode(const bool mode) noexcept; - bool IsUnicode() const noexcept; - - size_t UserBufferSize() const noexcept; - - wchar_t* BufferStartPtr() noexcept; - wchar_t* BufferCurrentPtr() noexcept; - void SetBufferCurrentPtr(wchar_t* ptr) noexcept; - - const size_t& BytesRead() const noexcept; - size_t& BytesRead() noexcept; - - const size_t& InsertionPoint() const noexcept; - size_t& InsertionPoint() noexcept; - - void SetReportedByteCount(const size_t count) noexcept; - - void Erase() noexcept; - size_t SavePromptToUserBuffer(const size_t cch); - void SavePendingInput(const size_t cch, const bool multiline); - -#if UNIT_TESTING - friend class CommandLineTests; - friend class CopyToCharPopupTests; - friend class CommandNumberPopupTests; - friend class CommandListPopupTests; - friend class PopupTestHelper; -#endif + void SetInsertMode(bool insertMode) noexcept; + bool IsEmpty() const noexcept; + bool PresentingPopup() const noexcept; + til::point_span GetBoundaries() const noexcept; private: - size_t _bufferSize; // size in bytes - size_t _bytesRead; - - // insertion position into the buffer (where the conceptual prompt cursor is) - size_t _currentPosition; // char position, not byte position - - wchar_t* _bufPtr; // current position to insert chars at - - // should be const. the first char of the buffer - wchar_t* _backupLimit; - - size_t _userBufferSize; // doubled size in ansi case - char* _userBuffer; - - size_t* _pdwNumBytes; + static constexpr uint8_t CommandNumberMaxInputLength = 5; + + enum class PopupKind + { + // Copies text from the previous command between the current cursor position and the first instance + // of a given char (but not including it) into the current prompt line at the current cursor position. + // Basically, F3 and this prompt have identical behavior, but the prompt searches for a terminating character. + // + // Let's say your last command was: + // echo hello + // And you type the following with the cursor at "^": + // echo abcd efgh + // ^ + // Then this command, given the char "o" will turn it into + // echo hell efgh + CopyToChar, + // Erases text between the current cursor position and the first instance of a given char (but not including it). + // It's unknown to me why this is was historically called "copy from char" as it conhost never copied anything. + CopyFromChar, + // Let's you choose to replace the current prompt with one from the command history by index. + CommandNumber, + // Let's you choose to replace the current prompt with one from the command history via a + // visual select dialog. Among all the popups this one is the most widely used one by far. + CommandList, + }; + + struct Popup + { + PopupKind kind; + + // The inner rectangle of the popup, excluding the border that we draw. + // In absolute TextBuffer coordinates. + til::rect contentRect; + // The area we've backed up and need to restore when we dismiss the popup. + // It'll practically always be 1 larger than contentRect in all 4 directions. + Microsoft::Console::Types::Viewport backupRect; + // The backed up buffer contents. Uses CHAR_INFO for convenience. + std::vector backup; + + // Using a std::variant would be preferable in modern C++ but is practically equally annoying to use. + union + { + // Used by PopupKind::CommandNumber + struct + { + // Keep 1 char space for the trailing \0 char. + std::array buffer; + uint8_t bufferSize; + } commandNumber; + + // Used by PopupKind::CommandList + struct + { + // Command history index of the first row we draw in the popup. + CommandHistory::Index top; + // Command history index of the currently selected row. + CommandHistory::Index selected; + // Tracks the part of the popup that has previously been drawn and needs to be redrawn in the next paint. + // This becomes relevant when the length of the history changes while the popup is open (= when deleting entries). + til::CoordType dirtyHeight; + } commandList; + }; + }; + + static size_t _wordPrev(const std::wstring_view& chars, size_t position); + static size_t _wordNext(const std::wstring_view& chars, size_t position); + + const std::wstring_view& _newlineSuffix() const noexcept; + bool _readCharInputLoop(); + bool _handleChar(wchar_t wch, DWORD modifiers); + void _handleVkey(uint16_t vkey, DWORD modifiers); + void _handlePostCharInputLoop(bool isUnicode, size_t& numBytes, ULONG& controlKeyState); + void _markAsDirty(); + void _flushBuffer(); + void _erase(til::CoordType distance); + til::CoordType _writeChars(const std::wstring_view& text) const; + til::point _offsetPosition(til::point pos, til::CoordType distance) const; + void _unwindCursorPosition(til::CoordType distance) const; + void _replaceBuffer(const std::wstring_view& str); + + void _popupPush(PopupKind kind); + void _popupsDone(); + void _popupHandleCopyToCharInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); + void _popupHandleCopyFromCharInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); + void _popupHandleCommandNumberInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); + bool _popupHandleCommandListInput(Popup& popup, wchar_t wch, uint16_t vkey, DWORD modifiers); + bool _popupHandleInput(wchar_t wch, uint16_t vkey, DWORD keyState); + void _popupDrawPrompt(const Popup& popup, UINT id) const; + void _popupDrawCommandList(Popup& popup) const; - std::unique_ptr _buffer; + SCREEN_INFORMATION& _screenInfo; + std::span _userBuffer; std::wstring _exeName; + ConsoleProcessHandle* _processHandle = nullptr; + CommandHistory* _history = nullptr; + ULONG _ctrlWakeupMask = 0; + ULONG _controlKeyState = 0; std::unique_ptr _tempHandle; - // TODO MSFT:11285829 make this something other than a deletable pointer - // non-ownership pointer - CommandHistory* _commandHistory; - - ULONG _controlKeyState; - ULONG _ctrlWakeupMask; - size_t _visibleCharCount; // TODO MSFT:11285829 is this cells or glyphs? ie. is a wide char counted as 1 or 2? - SCREEN_INFORMATION& _screenInfo; - - // Note that cookedReadData's _originalCursorPosition is the position before ANY text was entered on the edit line. - til::point _originalCursorPosition; - til::point _beforeDialogCursorPosition; // Currently only used for F9 (ProcessCommandNumberInput) since it's the only pop-up to move the cursor when it starts. - - const bool _echoInput; - const bool _lineInput; - const bool _processedInput; - bool _insertMode; - bool _unicode; - - ConsoleProcessHandle* const _clientProcess; - - [[nodiscard]] NTSTATUS _readCharInputLoop(const bool isUnicode, size_t& numBytes) noexcept; + std::wstring _buffer; + size_t _bufferCursor = 0; + til::CoordType _distanceCursor; + til::CoordType _distanceEnd; + bool _bufferDirty = false; + bool _insertMode = false; - [[nodiscard]] NTSTATUS _handlePostCharInputLoop(const bool isUnicode, size_t& numBytes, ULONG& controlKeyState) noexcept; + std::vector _popups; }; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 048e819a34e..58ac0812d52 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -873,12 +873,7 @@ void SCREEN_INFORMATION::ProcessResizeWindow(const til::rect* const prcClientNew // 1.a In some modes, the screen buffer size needs to change on window size, // so do that first. - // _AdjustScreenBuffer might hide the commandline. If it does so, it'll - // return S_OK instead of S_FALSE. In that case, we'll need to re-show - // the commandline ourselves once the viewport size is updated. - // (See 1.b below) - const auto adjustBufferSizeResult = _AdjustScreenBuffer(prcClientNew); - LOG_IF_FAILED(adjustBufferSizeResult); + LOG_IF_FAILED(_AdjustScreenBuffer(prcClientNew)); // 2. Now calculate how large the new viewport should be til::size coordViewportSize; @@ -888,16 +883,6 @@ void SCREEN_INFORMATION::ProcessResizeWindow(const til::rect* const prcClientNew // The old/new comparison is to figure out which side the window was resized from. _AdjustViewportSize(prcClientNew, prcClientOld, &coordViewportSize); - // 1.b If we did actually change the buffer size, then we need to show the - // commandline again. We hid it during _AdjustScreenBuffer, but we - // couldn't turn it back on until the Viewport was updated to the new - // size. See MSFT:19976291 - if (SUCCEEDED(adjustBufferSizeResult) && adjustBufferSizeResult != S_FALSE) - { - auto& commandLine = CommandLine::Instance(); - commandLine.Show(); - } - // 4. Finally, update the scroll bars. UpdateScrollBars(); @@ -1016,23 +1001,7 @@ void SCREEN_INFORMATION::ProcessResizeWindow(const til::rect* const prcClientNew // Only attempt to modify the buffer if something changed. Expensive operation. if (coordBufferSizeOld != coordBufferSizeNew) { - auto& commandLine = CommandLine::Instance(); - - // TODO: Deleting and redrawing the command line during resizing can cause flickering. See: http://osgvsowi/658439 - // 1. Delete input string if necessary (see menu.c) - commandLine.Hide(FALSE); - - const auto savedCursorVisibility = _textBuffer->GetCursor().IsVisible(); - _textBuffer->GetCursor().SetIsVisible(false); - - // 2. Call the resize screen buffer method (expensive) to redimension the backing buffer (and reflow) LOG_IF_FAILED(ResizeScreenBuffer(coordBufferSizeNew, FALSE)); - - // MSFT:19976291 Don't re-show the commandline here. We need to wait for - // the viewport to also get resized before we can re-show the commandline. - // ProcessResizeWindow will call commandline.Show() for us. - _textBuffer->GetCursor().SetIsVisible(savedCursorVisibility); - // Return S_OK, to indicate we succeeded and actually did something. hr = S_OK; } @@ -1540,8 +1509,17 @@ bool SCREEN_INFORMATION::IsMaximizedY() const // cancel any active selection before resizing or it will not necessarily line up with the new buffer positions Selection::Instance().ClearSelection(); - // cancel any popups before resizing or they will not necessarily line up with new buffer positions - CommandLine::Instance().EndAllPopups(); + if (gci.HasPendingCookedRead()) + { + gci.CookedReadData().EraseBeforeResize(); + } + const auto cookedReadRestore = wil::scope_exit([]() { + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + if (gci.HasPendingCookedRead()) + { + gci.CookedReadData().RedrawAfterResize(); + } + }); const auto fWrapText = gci.GetWrapText(); // GH#3493: Don't reflow the alt buffer. @@ -1937,10 +1915,7 @@ void SCREEN_INFORMATION::_handleDeferredResize(SCREEN_INFORMATION& siMain) // too much work. if (newBufferSize != oldScreenBufferSize) { - auto& commandLine = CommandLine::Instance(); - commandLine.Hide(FALSE); LOG_IF_FAILED(siMain.ResizeScreenBuffer(newBufferSize, TRUE)); - commandLine.Show(); } // Not that the buffer is smaller, actually make sure to resize our @@ -2098,7 +2073,7 @@ bool SCREEN_INFORMATION::_IsInVTMode() const // // Return value: // - This screen buffer's attributes -TextAttribute SCREEN_INFORMATION::GetAttributes() const +const TextAttribute& SCREEN_INFORMATION::GetAttributes() const noexcept { return _textBuffer->GetCurrentAttributes(); } @@ -2109,7 +2084,7 @@ TextAttribute SCREEN_INFORMATION::GetAttributes() const // // Return value: // - This screen buffer's popup attributes -TextAttribute SCREEN_INFORMATION::GetPopupAttributes() const +const TextAttribute& SCREEN_INFORMATION::GetPopupAttributes() const noexcept { return _PopupAttributes; } @@ -2311,56 +2286,6 @@ void SCREEN_INFORMATION::SetTerminalConnection(_In_ VtEngine* const pTtyConnecti } } -// Routine Description: -// - This routine copies a rectangular region from the screen buffer. no clipping is done. -// Arguments: -// - viewport - rectangle in source buffer to copy -// Return Value: -// - output cell rectangle copy of screen buffer data -// Note: -// - will throw exception on error. -OutputCellRect SCREEN_INFORMATION::ReadRect(const Viewport viewport) const -{ - // If the viewport given doesn't fit inside this screen, it's not a valid argument. - THROW_HR_IF(E_INVALIDARG, !GetBufferSize().IsInBounds(viewport)); - - OutputCellRect result(viewport.Height(), viewport.Width()); - const OutputCell paddingCell{ std::wstring_view{ &UNICODE_SPACE, 1 }, {}, GetAttributes() }; - for (til::CoordType rowIndex = 0, height = viewport.Height(); rowIndex < height; ++rowIndex) - { - auto location = viewport.Origin(); - location.y += rowIndex; - - auto data = GetCellLineDataAt(location); - const auto span = result.GetRow(rowIndex); - auto it = span.begin(); - - // Copy row data while there still is data and we haven't run out of rect to store it into. - while (data && it < span.end()) - { - *it++ = *data++; - } - - // Pad out any remaining space. - while (it < span.end()) - { - *it++ = paddingCell; - } - - // if we're clipping a dbcs char then don't include it, add a space instead - if (span.begin()->DbcsAttr() == DbcsAttribute::Trailing) - { - *span.begin() = paddingCell; - } - if (span.rbegin()->DbcsAttr() == DbcsAttribute::Leading) - { - *span.rbegin() = paddingCell; - } - } - - return result; -} - // Routine Description: // - Writes cells to the output buffer at the cursor position. // Arguments: diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index 513c73fac5e..09fca19809b 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -123,8 +123,6 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console static void s_InsertScreenBuffer(_In_ SCREEN_INFORMATION* const pScreenInfo); static void s_RemoveScreenBuffer(_In_ SCREEN_INFORMATION* const pScreenInfo); - OutputCellRect ReadRect(const Microsoft::Console::Types::Viewport location) const; - TextBufferCellIterator GetCellDataAt(const til::point at) const; TextBufferCellIterator GetCellLineDataAt(const til::point at) const; TextBufferCellIterator GetCellDataAt(const til::point at, const Microsoft::Console::Types::Viewport limit) const; @@ -202,8 +200,8 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console SCREEN_INFORMATION& GetActiveBuffer(); const SCREEN_INFORMATION& GetActiveBuffer() const; - TextAttribute GetAttributes() const; - TextAttribute GetPopupAttributes() const; + const TextAttribute& GetAttributes() const noexcept; + const TextAttribute& GetPopupAttributes() const noexcept; void SetAttributes(const TextAttribute& attributes); void SetPopupAttributes(const TextAttribute& popupAttributes); diff --git a/src/host/scrolling.cpp b/src/host/scrolling.cpp index add5a205e80..2e42d620767 100644 --- a/src/host/scrolling.cpp +++ b/src/host/scrolling.cpp @@ -208,7 +208,7 @@ bool Scrolling::s_HandleKeyScrollingEvent(const INPUT_KEY_INFO* const pKeyInfo) const auto VirtualKeyCode = pKeyInfo->GetVirtualKey(); const auto fIsCtrlPressed = pKeyInfo->IsCtrlPressed(); - const auto fIsEditLineEmpty = CommandLine::IsEditLineEmpty(); + const auto fIsEditLineEmpty = !gci.HasPendingCookedRead() || gci.CookedReadData().IsEmpty(); // If escape, enter or ctrl-c, cancel scroll. if (VirtualKeyCode == VK_ESCAPE || diff --git a/src/host/selectionInput.cpp b/src/host/selectionInput.cpp index 69d40b9ae79..f00bbb4b847 100644 --- a/src/host/selectionInput.cpp +++ b/src/host/selectionInput.cpp @@ -8,8 +8,6 @@ #include "../interactivity/inc/ServiceLocator.hpp" #include "../types/inc/convert.hpp" -#include - using namespace Microsoft::Console::Types; using Microsoft::Console::Interactivity::ServiceLocator; // Routine Description: @@ -950,49 +948,27 @@ bool Selection::_HandleMarkModeSelectionNav(const INPUT_KEY_INFO* const pInputKe [[nodiscard]] bool Selection::s_GetInputLineBoundaries(_Out_opt_ til::point* const pcoordInputStart, _Out_opt_ til::point* const pcoordInputEnd) { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto bufferSize = gci.GetActiveOutputBuffer().GetBufferSize(); - - auto& textBuffer = gci.GetActiveOutputBuffer().GetTextBuffer(); - - const auto pendingCookedRead = gci.HasPendingCookedRead(); - const auto isVisible = CommandLine::Instance().IsVisible(); - - // if we have no read data, we have no input line. - if (!pendingCookedRead || gci.CookedReadData().VisibleCharCount() == 0 || !isVisible) - { - return false; - } - - const auto& cookedRead = gci.CookedReadData(); - const auto coordStart = cookedRead.OriginalCursorPosition(); - auto coordEnd = cookedRead.OriginalCursorPosition(); - if (coordEnd.x < 0 && coordEnd.y < 0) + if (gci.HasPendingCookedRead()) { - // if the original cursor position from the input line data is invalid, then the buffer cursor position is the final position - coordEnd = textBuffer.GetCursor().GetPosition(); - } - else - { - // otherwise, we need to add the number of characters in the input line to the original cursor position - bufferSize.MoveInBounds(gsl::narrow(cookedRead.VisibleCharCount()), coordEnd); - } - - // - 1 so the coordinate is on top of the last position of the text, not one past it. - bufferSize.MoveInBounds(-1, coordEnd); - - if (pcoordInputStart != nullptr) - { - pcoordInputStart->x = coordStart.x; - pcoordInputStart->y = coordStart.y; - } - - if (pcoordInputEnd != nullptr) - { - *pcoordInputEnd = coordEnd; + auto boundaries = gci.CookedReadData().GetBoundaries(); + if (boundaries.start < boundaries.end) + { + if (pcoordInputStart != nullptr) + { + *pcoordInputStart = boundaries.start; + } + if (pcoordInputEnd != nullptr) + { + // - 1 so the coordinate is on top of the last position of the text, not one past it. + gci.GetActiveOutputBuffer().GetBufferSize().MoveInBounds(-1, boundaries.end); + *pcoordInputEnd = boundaries.end; + } + return true; + } } - return true; + return false; } // Routine Description: diff --git a/src/host/server.h b/src/host/server.h index ebc2ac10091..3332cc6b383 100644 --- a/src/host/server.h +++ b/src/host/server.h @@ -16,20 +16,17 @@ Revision History: #pragma once +#include "conimeinfo.h" +#include "CursorBlinker.hpp" #include "IIoProvider.hpp" - +#include "readDataCooked.hpp" #include "settings.hpp" - -#include "conimeinfo.h" #include "VtIo.hpp" -#include "CursorBlinker.hpp" - +#include "../audio/midi/MidiAudio.hpp" +#include "../host/RenderData.hpp" #include "../server/ProcessList.h" #include "../server/WaitQueue.h" -#include "../host/RenderData.hpp" -#include "../audio/midi/MidiAudio.hpp" - #include // clang-format off @@ -91,8 +88,6 @@ class CONSOLE_INFORMATION : DWORD Flags = 0; - std::atomic PopupCount = 0; - // the following fields are used for ansi-unicode translation UINT CP = 0; UINT OutputCP = 0; @@ -121,6 +116,7 @@ class CONSOLE_INFORMATION : bool IsInVtIoMode() const; bool HasPendingCookedRead() const noexcept; + bool HasPendingPopup() const noexcept; const COOKED_READ_DATA& CookedReadData() const noexcept; COOKED_READ_DATA& CookedReadData() noexcept; void SetCookedReadData(COOKED_READ_DATA* readData) noexcept; @@ -167,8 +163,6 @@ class CONSOLE_INFORMATION : MidiAudio _midiAudio; }; -#define ConsoleLocked() (ServiceLocator::LocateGlobals()->getConsoleInformation()->ConsoleLock.OwningThread == NtCurrentTeb()->ClientId.UniqueThread) - #define CONSOLE_STATUS_WAIT 0xC0030001 #define CONSOLE_STATUS_READ_COMPLETE 0xC0030002 #define CONSOLE_STATUS_WAIT_NO_BLOCK 0xC0030003 diff --git a/src/host/sources.inc b/src/host/sources.inc index 173dae000cb..bf3b16bcae9 100644 --- a/src/host/sources.inc +++ b/src/host/sources.inc @@ -46,7 +46,6 @@ SOURCES = \ ..\scrolling.cpp \ ..\cmdline.cpp \ ..\CursorBlinker.cpp \ - ..\popup.cpp \ ..\alias.cpp \ ..\history.cpp \ ..\VtIo.cpp \ @@ -89,10 +88,6 @@ SOURCES = \ ..\conareainfo.cpp \ ..\conimeinfo.cpp \ ..\ConsoleArguments.cpp \ - ..\CommandNumberPopup.cpp \ - ..\CommandListPopup.cpp \ - ..\CopyFromCharPopup.cpp \ - ..\CopyToCharPopup.cpp \ ..\VtApiRoutines.cpp \ diff --git a/src/host/stream.cpp b/src/host/stream.cpp index e8dbe5aa2b0..11a7f6a4115 100644 --- a/src/host/stream.cpp +++ b/src/host/stream.cpp @@ -247,93 +247,6 @@ static bool IsCommandLineEditingKey(const KEY_EVENT_RECORD& event) } } -// Routine Description: -// - This routine returns the total number of screen spaces the characters up to the specified character take up. -til::CoordType RetrieveTotalNumberOfSpaces(const til::CoordType sOriginalCursorPositionX, - _In_reads_(ulCurrentPosition) const WCHAR* const pwchBuffer, - _In_ size_t ulCurrentPosition) -{ - auto XPosition = sOriginalCursorPositionX; - til::CoordType NumSpaces = 0; - - for (size_t i = 0; i < ulCurrentPosition; i++) - { - const auto Char = pwchBuffer[i]; - - til::CoordType NumSpacesForChar; - if (Char == UNICODE_TAB) - { - NumSpacesForChar = NUMBER_OF_SPACES_IN_TAB(XPosition); - } - else if (IS_CONTROL_CHAR(Char)) - { - NumSpacesForChar = 2; - } - else if (IsGlyphFullWidth(Char)) - { - NumSpacesForChar = 2; - } - else - { - NumSpacesForChar = 1; - } - XPosition += NumSpacesForChar; - NumSpaces += NumSpacesForChar; - } - - return NumSpaces; -} - -// Routine Description: -// - This routine returns the number of screen spaces the specified character takes up. -til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositionX, - _In_reads_(ulCurrentPosition + 1) const WCHAR* const pwchBuffer, - _In_ size_t ulCurrentPosition) -{ - auto Char = pwchBuffer[ulCurrentPosition]; - if (Char == UNICODE_TAB) - { - til::CoordType NumSpaces = 0; - auto XPosition = sOriginalCursorPositionX; - - for (size_t i = 0; i <= ulCurrentPosition; i++) - { - Char = pwchBuffer[i]; - if (Char == UNICODE_TAB) - { - NumSpaces = NUMBER_OF_SPACES_IN_TAB(XPosition); - } - else if (IS_CONTROL_CHAR(Char)) - { - NumSpaces = 2; - } - else if (IsGlyphFullWidth(Char)) - { - NumSpaces = 2; - } - else - { - NumSpaces = 1; - } - XPosition += NumSpaces; - } - - return NumSpaces; - } - else if (IS_CONTROL_CHAR(Char)) - { - return 2; - } - else if (IsGlyphFullWidth(Char)) - { - return 2; - } - else - { - return 1; - } -} - // Routine Description: // - if we have leftover input, copy as much fits into the user's // buffer and return. we may have multi line input, if a macro @@ -448,7 +361,7 @@ NT_CATCH_RETURN() gci.SetCookedReadData(cookedReadData.get()); bytesRead = buffer.size_bytes(); // This parameter on the way in is the size to read, on the way out, it will be updated to what is actually read. - if (CONSOLE_STATUS_WAIT == cookedReadData->Read(unicode, bytesRead, controlKeyState)) + if (!cookedReadData->Read(unicode, bytesRead, controlKeyState)) { // memory will be cleaned up by wait queue waiter.reset(cookedReadData.release()); diff --git a/src/host/stream.h b/src/host/stream.h index b4c37d82a45..5a03278fc99 100644 --- a/src/host/stream.h +++ b/src/host/stream.h @@ -20,8 +20,6 @@ Revision History: #include "../server/IWaitRoutine.h" #include "readData.hpp" -#define IS_CONTROL_CHAR(wch) ((wch) < L' ') - [[nodiscard]] NTSTATUS GetChar(_Inout_ InputBuffer* const pInputBuffer, _Out_ wchar_t* const pwchOut, const bool Wait, @@ -35,16 +33,4 @@ Revision History: INPUT_READ_HANDLE_DATA& readHandleState, const bool unicode); -// Routine Description: -// - This routine returns the total number of screen spaces the characters up to the specified character take up. -til::CoordType RetrieveTotalNumberOfSpaces(const til::CoordType sOriginalCursorPositionX, - _In_reads_(ulCurrentPosition) const WCHAR* const pwchBuffer, - const size_t ulCurrentPosition); - -// Routine Description: -// - This routine returns the number of screen spaces the specified character takes up. -til::CoordType RetrieveNumberOfSpaces(_In_ til::CoordType sOriginalCursorPositionX, - _In_reads_(ulCurrentPosition + 1) const WCHAR* const pwchBuffer, - _In_ size_t ulCurrentPosition); - VOID UnblockWriteConsole(const DWORD dwReason); diff --git a/src/host/tracing.cpp b/src/host/tracing.cpp index bf54edbed99..998410cf247 100644 --- a/src/host/tracing.cpp +++ b/src/host/tracing.cpp @@ -173,16 +173,17 @@ void Tracing::s_TraceInputRecord(const INPUT_RECORD& inputRecord) } } -void Tracing::s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_reads_(cchCookedBufferLength) const wchar_t* pwchCookedBuffer, _In_ ULONG cchCookedBufferLength) +void Tracing::s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, const std::wstring_view& text) { if (TraceLoggingProviderEnabled(g_hConhostV2EventTraceProvider, 0, TraceKeywords::CookedRead)) { + const auto length = ::base::saturated_cast(text.size()); TraceLoggingWrite( g_hConhostV2EventTraceProvider, "CookedRead", TraceLoggingPid(pConsoleProcessHandle->dwProcessId, "AttachedProcessId"), - TraceLoggingCountedWideString(pwchCookedBuffer, cchCookedBufferLength, "ReadBuffer"), - TraceLoggingULong(cchCookedBufferLength, "ReadBufferLength"), + TraceLoggingCountedWideString(text.data(), length, "ReadBuffer"), + TraceLoggingULong(length, "ReadBufferLength"), TraceLoggingFileTime(pConsoleProcessHandle->GetProcessCreationTime(), "AttachedProcessCreationTime"), TraceLoggingKeyword(TIL_KEYWORD_TRACE), TraceLoggingKeyword(TraceKeywords::CookedRead)); diff --git a/src/host/tracing.hpp b/src/host/tracing.hpp index 8be4bf21b82..03cf423f4d7 100644 --- a/src/host/tracing.hpp +++ b/src/host/tracing.hpp @@ -51,7 +51,7 @@ class Tracing static void s_TraceWindowMessage(const MSG& msg); static void s_TraceInputRecord(const INPUT_RECORD& inputRecord); - static void s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_reads_(cchCookedBufferLength) const wchar_t* pwchCookedBuffer, _In_ ULONG cchCookedBufferLength); + static void s_TraceCookedRead(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, const std::wstring_view& text); static void s_TraceConsoleAttachDetach(_In_ ConsoleProcessHandle* const pConsoleProcessHandle, _In_ bool bIsAttach); static void __stdcall TraceFailure(const wil::FailureInfo& failure) noexcept; diff --git a/src/host/ut_host/AliasTests.cpp b/src/host/ut_host/AliasTests.cpp index 6443dd22f63..cf499bea89e 100644 --- a/src/host/ut_host/AliasTests.cpp +++ b/src/host/ut_host/AliasTests.cpp @@ -111,167 +111,48 @@ class AliasTests // and match to our expected values. std::wstring alias(aliasName); std::wstring exe(exeName); + std::wstring original(originalString); std::wstring target; std::wstring expected; _RetrieveTargetExpectedPair(target, expected); - auto linesExpected = _ReplacePercentWithCRLF(expected); - - std::wstring original(originalString); + const auto linesExpected = _ReplacePercentWithCRLF(expected); Alias::s_TestAddAlias(exe, alias, target); - // Fill classic wchar_t[] buffer for interfacing with the MatchAndCopyAlias function - const auto bufferSize = 160ui16; - auto buffer = std::make_unique(bufferSize); - wcscpy_s(buffer.get(), bufferSize, original.data()); - - const auto cbBuffer = bufferSize * sizeof(wchar_t); - size_t bufferUsed = 0; - DWORD linesActual = 0; - // Run the match and copy function. - Alias::s_MatchAndCopyAliasLegacy(buffer.get(), - wcslen(buffer.get()) * sizeof(wchar_t), - buffer.get(), - cbBuffer, - bufferUsed, - exe, - linesActual); - - // Null terminate buffer for comparison - buffer[bufferUsed / sizeof(wchar_t)] = L'\0'; - - Log::Comment(String().Format(L"Expected: '%s'", expected.data())); - Log::Comment(String().Format(L"Actual : '%s'", buffer.get())); - - VERIFY_ARE_EQUAL(WEX::Common::String(expected.data()), WEX::Common::String(buffer.get())); + size_t linesActual = 0; + const auto actual = Alias::s_MatchAndCopyAlias(original, exe, linesActual); + VERIFY_ARE_EQUAL(expected, actual); VERIFY_ARE_EQUAL(linesExpected, linesActual); } - TEST_METHOD(TestMatchAndCopyTrailingCRLF) - { - const auto pwszSource = L"SourceWithoutCRLF\r\n"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 60; - auto rgwchTarget = std::make_unique(cchTarget); - const auto cbTarget = cchTarget * sizeof(wchar_t); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtesttesttesttest"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - - size_t cbTargetUsed = 0; - DWORD dwLines = 0; - - // Register the wrong alias name before we try. - std::wstring exe(L"exe.exe"); - std::wstring sourceWithoutCRLF(L"SourceWithoutCRLF"); - std::wstring target(L"someTarget"); - Alias::s_TestAddAlias(exe, sourceWithoutCRLF, target); - - const auto targetExpected = target + L"\r\n"; - const auto cbTargetExpected = targetExpected.size() * sizeof(wchar_t); // +2 for \r\n that will be added on replace. - - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - cbTarget, - cbTargetUsed, - exe, - dwLines); - - // Terminate target buffer with \0 for comparison - rgwchTarget[cbTargetUsed] = L'\0'; - - VERIFY_ARE_EQUAL(cbTargetExpected, cbTargetUsed, L"Target bytes should be filled with target size."); - VERIFY_ARE_EQUAL(String(targetExpected.data()), String(rgwchTarget.get(), gsl::narrow(cbTargetUsed / sizeof(wchar_t))), L"Target string should be filled with target data."); - VERIFY_ARE_EQUAL(1u, dwLines, L"Line count should be 1."); - } - TEST_METHOD(TestMatchAndCopyInvalidExeName) { const auto pwszSource = L"Source"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 12; - auto rgwchTarget = std::make_unique(cchTarget); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - - const auto cbTarget = cchTarget * sizeof(wchar_t); - auto cbTargetUsed = cbTarget; - - DWORD dwLines = 0; - const auto dwLinesBefore = dwLines; - + size_t dwLines = 1; std::wstring exeName; - - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - cbTarget, - cbTargetUsed, - exeName, - dwLines); - - VERIFY_ARE_EQUAL(cbTarget, cbTargetUsed, L"Byte count shouldn't have changed with failure."); - VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count shouldn't have changed with failure."); - VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string shouldn't have changed with failure."); + const auto buffer = Alias::s_MatchAndCopyAlias(pwszSource, exeName, dwLines); + VERIFY_IS_TRUE(buffer.empty()); + VERIFY_ARE_EQUAL(1u, dwLines); } TEST_METHOD(TestMatchAndCopyExeNotFound) { const auto pwszSource = L"Source"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 12; - auto rgwchTarget = std::make_unique(cchTarget); - const auto cbTarget = cchTarget * sizeof(wchar_t); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - size_t cbTargetUsed = 0; - const auto cbTargetUsedBefore = cbTargetUsed; - - std::wstring exeName(L"exe.exe"); - - DWORD dwLines = 0; - const auto dwLinesBefore = dwLines; - - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - cbTarget, - cbTargetUsed, - exeName, // we didn't pre-set-up the exe name - dwLines); - - VERIFY_ARE_EQUAL(cbTargetUsedBefore, cbTargetUsed, L"No bytes should have been written."); - VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string should be unmodified."); - VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count should pass through."); + size_t dwLines = 1; + const std::wstring exeName(L"exe.exe"); + const auto buffer = Alias::s_MatchAndCopyAlias(pwszSource, exeName, dwLines); + VERIFY_IS_TRUE(buffer.empty()); + VERIFY_ARE_EQUAL(1u, dwLines); } TEST_METHOD(TestMatchAndCopyAliasNotFound) { const auto pwszSource = L"Source"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 12; - auto rgwchTarget = std::make_unique(cchTarget); - const auto cbTarget = cchTarget * sizeof(wchar_t); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - - size_t cbTargetUsed = 0; - const auto cbTargetUsedBefore = cbTargetUsed; - - DWORD dwLines = 0; - const auto dwLinesBefore = dwLines; + size_t dwLines = 1; // Register the wrong alias name before we try. std::wstring exe(L"exe.exe"); @@ -279,71 +160,15 @@ class AliasTests std::wstring target(L"someTarget"); Alias::s_TestAddAlias(exe, badSource, target); - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - cbTarget, - cbTargetUsed, - exe, - dwLines); - - VERIFY_ARE_EQUAL(cbTargetUsedBefore, cbTargetUsed, L"No bytes should be used if nothing was found."); - VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string should be unmodified."); - VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count should pass through."); - } - - TEST_METHOD(TestMatchAndCopyTargetTooSmall) - { - const auto pwszSource = L"Source"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 12; - auto rgwchTarget = std::make_unique(cchTarget); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - - size_t cbTargetUsed = 0; - const auto cbTargetUsedBefore = cbTargetUsed; - - DWORD dwLines = 0; - const auto dwLinesBefore = dwLines; - - // Register the correct alias name before we try. - std::wstring exe(L"exe.exe"); - std::wstring source(pwszSource); - std::wstring target(L"someTarget"); - Alias::s_TestAddAlias(exe, source, target); - - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - 1, // Make the target size too small - cbTargetUsed, - exe, - dwLines); - - VERIFY_ARE_EQUAL(cbTargetUsedBefore, cbTargetUsed, L"Byte count shouldn't have changed with failure."); - VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count shouldn't have changed with failure."); - VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string shouldn't have changed with failure."); + const auto buffer = Alias::s_MatchAndCopyAlias(pwszSource, exe, dwLines); + VERIFY_IS_TRUE(buffer.empty()); + VERIFY_ARE_EQUAL(1u, dwLines); } TEST_METHOD(TestMatchAndCopyLeadingSpaces) { const auto pwszSource = L" Source"; - const auto cbSource = wcslen(pwszSource) * sizeof(wchar_t); - - const size_t cchTarget = 12; - auto rgwchTarget = std::make_unique(cchTarget); - const auto cbTarget = cchTarget * sizeof(wchar_t); - wcscpy_s(rgwchTarget.get(), cchTarget, L"testtestabc"); - auto rgwchTargetBefore = std::make_unique(cchTarget); - wcscpy_s(rgwchTargetBefore.get(), cchTarget, rgwchTarget.get()); - size_t cbTargetUsed = 0; - const auto cbTargetUsedBefore = cbTargetUsed; - - DWORD dwLines = 0; - const auto dwLinesBefore = dwLines; + size_t dwLines = 1; // Register the correct alias name before we try. std::wstring exe(L"exe.exe"); @@ -352,40 +177,9 @@ class AliasTests Alias::s_TestAddAlias(exe, source, target); // Leading spaces should bypass the alias. This should not match anything. - Alias::s_MatchAndCopyAliasLegacy(pwszSource, - cbSource, - rgwchTarget.get(), - cbTarget, - cbTargetUsed, - exe, - dwLines); - - VERIFY_ARE_EQUAL(cbTargetUsedBefore, cbTargetUsed, L"No bytes should be used if nothing was found."); - VERIFY_ARE_EQUAL(String(rgwchTargetBefore.get(), cchTarget), String(rgwchTarget.get(), cchTarget), L"Target string should be unmodified."); - VERIFY_ARE_EQUAL(dwLinesBefore, dwLines, L"Line count should pass through."); - } - - TEST_METHOD(TrimTrailing) - { - BEGIN_TEST_METHOD_PROPERTIES() - TEST_METHOD_PROPERTY(L"Data:targetExpectedPair", - L"{" - L"bar%=bar," // The character % will be turned into an \r\n - L"bar=bar" - L"}") - END_TEST_METHOD_PROPERTIES() - - std::wstring target; - std::wstring expected; - _RetrieveTargetExpectedPair(target, expected); - - // Substitute %s from metadata into \r\n (since metadata can't hold \r\n) - _ReplacePercentWithCRLF(target); - _ReplacePercentWithCRLF(expected); - - Alias::s_TrimTrailingCrLf(target); - - VERIFY_ARE_EQUAL(String(expected.data()), String(target.data())); + const auto buffer = Alias::s_MatchAndCopyAlias(pwszSource, exe, dwLines); + VERIFY_IS_TRUE(buffer.empty()); + VERIFY_ARE_EQUAL(1u, dwLines); } TEST_METHOD(Tokenize) diff --git a/src/host/ut_host/CommandLineTests.cpp b/src/host/ut_host/CommandLineTests.cpp deleted file mode 100644 index de09dd2e241..00000000000 --- a/src/host/ut_host/CommandLineTests.cpp +++ /dev/null @@ -1,539 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "CommonState.hpp" - -#include "../../interactivity/inc/ServiceLocator.hpp" - -#include "../cmdline.h" - -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; -using Microsoft::Console::Interactivity::ServiceLocator; - -constexpr size_t PROMPT_SIZE = 512; - -class CommandLineTests -{ - std::unique_ptr m_state; - CommandHistory* m_pHistory; - - TEST_CLASS(CommandLineTests); - - TEST_CLASS_SETUP(ClassSetup) - { - m_state = std::make_unique(); - m_state->PrepareGlobalFont(); - return true; - } - - TEST_CLASS_CLEANUP(ClassCleanup) - { - m_state->CleanupGlobalFont(); - return true; - } - - TEST_METHOD_SETUP(MethodSetup) - { - m_state->PrepareGlobalInputBuffer(); - m_state->PrepareGlobalScreenBuffer(); - m_state->PrepareReadHandle(); - m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr); - if (!m_pHistory) - { - return false; - } - // History must be prepared before COOKED_READ (as it uses s_Find to get at it) - m_state->PrepareCookedReadData(); - return true; - } - - TEST_METHOD_CLEANUP(MethodCleanup) - { - CommandHistory::s_Free(nullptr); - m_pHistory = nullptr; - m_state->CleanupCookedReadData(); - m_state->CleanupReadHandle(); - m_state->CleanupGlobalInputBuffer(); - m_state->CleanupGlobalScreenBuffer(); - return true; - } - - void VerifyPromptText(COOKED_READ_DATA& cookedReadData, const std::wstring wstr) - { - const auto span = cookedReadData.SpanWholeBuffer(); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, wstr.size() * sizeof(wchar_t)); - VERIFY_ARE_EQUAL(wstr, (std::wstring_view{ span.data(), cookedReadData._bytesRead / sizeof(wchar_t) })); - } - - void InitCookedReadData(COOKED_READ_DATA& cookedReadData, - CommandHistory* pHistory, - wchar_t* pBuffer, - const size_t cchBuffer) - { - cookedReadData._commandHistory = pHistory; - cookedReadData._userBuffer = reinterpret_cast(pBuffer); - cookedReadData._userBufferSize = cchBuffer * sizeof(wchar_t); - cookedReadData._bufferSize = cchBuffer * sizeof(wchar_t); - cookedReadData._backupLimit = pBuffer; - cookedReadData._bufPtr = pBuffer; - cookedReadData._exeName = L"cmd.exe"; - cookedReadData.OriginalCursorPosition() = { 0, 0 }; - } - - void SetPrompt(COOKED_READ_DATA& cookedReadData, const std::wstring text) - { - std::copy(text.begin(), text.end(), cookedReadData._backupLimit); - cookedReadData._bytesRead = text.size() * sizeof(wchar_t); - cookedReadData._currentPosition = text.size(); - cookedReadData._bufPtr += text.size(); - cookedReadData._visibleCharCount = text.size(); - } - - void MoveCursor(COOKED_READ_DATA& cookedReadData, const size_t column) - { - cookedReadData._currentPosition = column; - cookedReadData._bufPtr = cookedReadData._backupLimit + column; - } - - TEST_METHOD(CanCycleCommandHistory) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); - - auto& commandLine = CommandLine::Instance(); - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - // should not have anything on the prompt - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, 0u); - - // go back one history item - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - VerifyPromptText(cookedReadData, L"echo 3"); - - // try to go to the next history item, prompt shouldn't change - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - VerifyPromptText(cookedReadData, L"echo 3"); - - // go back another - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - VerifyPromptText(cookedReadData, L"echo 2"); - - // go forward - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - VerifyPromptText(cookedReadData, L"echo 3"); - - // go back two - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - VerifyPromptText(cookedReadData, L"echo 1"); - - // make sure we can't go back further - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - VerifyPromptText(cookedReadData, L"echo 1"); - - // can still go forward - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - VerifyPromptText(cookedReadData, L"echo 2"); - } - - TEST_METHOD(CanSetPromptToOldestHistory) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); - - auto& commandLine = CommandLine::Instance(); - commandLine._setPromptToOldestCommand(cookedReadData); - VerifyPromptText(cookedReadData, L"echo 1"); - - // change prompt and go back to oldest - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Next); - commandLine._setPromptToOldestCommand(cookedReadData); - VerifyPromptText(cookedReadData, L"echo 1"); - } - - TEST_METHOD(CanSetPromptToNewestHistory) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); - - auto& commandLine = CommandLine::Instance(); - commandLine._setPromptToNewestCommand(cookedReadData); - VerifyPromptText(cookedReadData, L"echo 3"); - - // change prompt and go back to newest - commandLine._processHistoryCycling(cookedReadData, CommandHistory::SearchDirection::Previous); - commandLine._setPromptToNewestCommand(cookedReadData); - VerifyPromptText(cookedReadData, L"echo 3"); - } - - TEST_METHOD(CanDeletePromptAfterCursor) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - auto& commandLine = CommandLine::Instance(); - // set current cursor position somewhere in the middle of the prompt - MoveCursor(cookedReadData, 4); - commandLine.DeletePromptAfterCursor(cookedReadData); - VerifyPromptText(cookedReadData, L"test"); - } - - TEST_METHOD(CanDeletePromptBeforeCursor) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - // set current cursor position somewhere in the middle of the prompt - MoveCursor(cookedReadData, 5); - auto& commandLine = CommandLine::Instance(); - const auto cursorPos = commandLine._deletePromptBeforeCursor(cookedReadData); - cookedReadData._currentPosition = cursorPos.x; - VerifyPromptText(cookedReadData, L"word blah"); - } - - TEST_METHOD(CanMoveCursorToEndOfPrompt) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - // make sure the cursor is not at the start of the prompt - VERIFY_ARE_NOT_EQUAL(cookedReadData._currentPosition, 0u); - VERIFY_ARE_NOT_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - - // save current position for later checking - const auto expectedCursorPos = cookedReadData._currentPosition; - const auto expectedBufferPos = cookedReadData._bufPtr; - - MoveCursor(cookedReadData, 0); - - auto& commandLine = CommandLine::Instance(); - const auto cursorPos = commandLine._moveCursorToEndOfPrompt(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, gsl::narrow(expectedCursorPos)); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, expectedCursorPos); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, expectedBufferPos); - } - - TEST_METHOD(CanMoveCursorToStartOfPrompt) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - // make sure the cursor is not at the start of the prompt - VERIFY_ARE_NOT_EQUAL(cookedReadData._currentPosition, 0u); - VERIFY_ARE_NOT_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - - auto& commandLine = CommandLine::Instance(); - const auto cursorPos = commandLine._moveCursorToStartOfPrompt(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, 0); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, 0u); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - } - - TEST_METHOD(CanMoveCursorLeftByWord) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - auto& commandLine = CommandLine::Instance(); - // cursor position at beginning of "blah" - til::CoordType expectedPos = 10; - auto cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); - - // move again - expectedPos = 5; // before "word" - cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); - - // move again - expectedPos = 0; // before "test" - cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); - - // try to move again, nothing should happen - cursorPos = commandLine._moveCursorLeftByWord(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, gsl::narrow(expectedPos)); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedPos); - } - - TEST_METHOD(CanMoveCursorLeft) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - const std::wstring expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - // move left from end of prompt text to the beginning of the prompt - auto& commandLine = CommandLine::Instance(); - for (auto it = expected.crbegin(); it != expected.crend(); ++it) - { - const auto cursorPos = commandLine._moveCursorLeft(cookedReadData); - VERIFY_ARE_EQUAL(*cookedReadData._bufPtr, *it); - } - // should now be at the start of the prompt - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, 0u); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - - // try to move left a final time, nothing should change - const auto cursorPos = commandLine._moveCursorLeft(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, 0); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, 0u); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - } - - /* - // TODO MSFT:11285829 tcome back and turn these on once the system cursor isn't needed - TEST_METHOD(CanMoveCursorRightByWord) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto expected = L"test word blah"; - SetPrompt(cookedReadData, expected); - VerifyPromptText(cookedReadData, expected); - - // save current position for later checking - const auto endCursorPos = cookedReadData._currentPosition; - const auto endBufferPos = cookedReadData._bufPtr; - // NOTE: need to initialize the actually cursor and keep it up to date with the changes here. remove - once functions are fixed - // try to move right, nothing should happen - auto expectedPos = endCursorPos; - auto cursorPos = MoveCursorRightByWord(cookedReadData); - VERIFY_ARE_EQUAL(cursorPos.x, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._currentPosition, expectedPos); - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, endBufferPos); - - // move to beginning of prompt and walk to the right by word - } - - TEST_METHOD(CanMoveCursorRight) - { - } - - TEST_METHOD(CanDeleteFromRightOfCursor) - { - } - - */ - - TEST_METHOD(CanInsertCtrlZ) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, nullptr, buffer.get(), PROMPT_SIZE); - - auto& commandLine = CommandLine::Instance(); - commandLine._insertCtrlZ(cookedReadData); - VerifyPromptText(cookedReadData, L"\x1a"); // ctrl-z - } - - TEST_METHOD(CanDeleteCommandHistory) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 1", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 2", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"echo 3", false)); - - auto& commandLine = CommandLine::Instance(); - commandLine._deleteCommandHistory(cookedReadData); - VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands(), 0); - } - - TEST_METHOD(CanFillPromptWithPreviousCommandFragment) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"I'm a little teapot", false)); - SetPrompt(cookedReadData, L"short and stout"); - - auto& commandLine = CommandLine::Instance(); - commandLine._fillPromptWithPreviousCommandFragment(cookedReadData); - VerifyPromptText(cookedReadData, L"short and stoutapot"); - } - - TEST_METHOD(CanCycleMatchingCommandHistory) - { - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - - auto& cookedReadData = ServiceLocator::LocateGlobals().getConsoleInformation().CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - VERIFY_SUCCEEDED(m_pHistory->Add(L"I'm a little teapot", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"short and stout", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"inflammable", false)); - VERIFY_SUCCEEDED(m_pHistory->Add(L"Indestructible", false)); - - SetPrompt(cookedReadData, L"I"); - - auto& commandLine = CommandLine::Instance(); - commandLine._cycleMatchingCommandHistoryToPrompt(cookedReadData); - VerifyPromptText(cookedReadData, L"Indestructible"); - - // make sure we skip to the next "I" history item - commandLine._cycleMatchingCommandHistoryToPrompt(cookedReadData); - VerifyPromptText(cookedReadData, L"I'm a little teapot"); - - // should cycle back to the start of the command history - commandLine._cycleMatchingCommandHistoryToPrompt(cookedReadData); - VerifyPromptText(cookedReadData, L"Indestructible"); - } - - TEST_METHOD(CmdlineCtrlHomeFullwidthChars) - { - Log::Comment(L"Set up buffers, create cooked read data, get screen information."); - auto buffer = std::make_unique(PROMPT_SIZE); - VERIFY_IS_NOT_NULL(buffer.get()); - auto& consoleInfo = ServiceLocator::LocateGlobals().getConsoleInformation(); - auto& screenInfo = consoleInfo.GetActiveOutputBuffer(); - auto& cookedReadData = consoleInfo.CookedReadData(); - InitCookedReadData(cookedReadData, m_pHistory, buffer.get(), PROMPT_SIZE); - - Log::Comment(L"Create Japanese text string and calculate the distance we expect the cursor to move."); - const std::wstring text(L"\x30ab\x30ac\x30ad\x30ae\x30af"); // katakana KA GA KI GI KU - const auto bufferSize = screenInfo.GetBufferSize(); - const auto cursorBefore = screenInfo.GetTextBuffer().GetCursor().GetPosition(); - auto cursorAfterExpected = cursorBefore; - for (size_t i = 0; i < text.length() * 2; i++) - { - bufferSize.IncrementInBounds(cursorAfterExpected); - } - - Log::Comment(L"Write the text into the buffer using the cooked read structures as if it came off of someone's input."); - const auto written = cookedReadData.Write(text); - VERIFY_ARE_EQUAL(text.length(), written); - - Log::Comment(L"Retrieve the position of the cursor after insertion and check that it moved as far as we expected."); - const auto cursorAfter = screenInfo.GetTextBuffer().GetCursor().GetPosition(); - VERIFY_ARE_EQUAL(cursorAfterExpected, cursorAfter); - - Log::Comment(L"Walk through the screen buffer data and ensure that the text we wrote filled the cells up as we expected (2 cells per fullwidth char)"); - { - auto cellIterator = screenInfo.GetCellDataAt(cursorBefore); - for (size_t i = 0; i < text.length() * 2; i++) - { - // Our original string was 5 wide characters which we expected to take 10 cells. - // Therefore each index of the original string will be used twice ( divide by 2 ). - const auto expectedTextValue = text.at(i / 2); - const String expectedText(&expectedTextValue, 1); - - const auto actualTextValue = cellIterator->Chars(); - const String actualText(actualTextValue.data(), gsl::narrow(actualTextValue.size())); - - VERIFY_ARE_EQUAL(expectedText, actualText); - cellIterator++; - } - } - - Log::Comment(L"Now perform the command that is triggered with Ctrl+Home keys normally to erase the entire edit line."); - auto& commandLine = CommandLine::Instance(); - commandLine._deletePromptBeforeCursor(cookedReadData); - - Log::Comment(L"Check that the entire span of the buffer where we had the fullwidth text is now cleared out and full of blanks (nothing left behind)."); - { - auto cursorPos = cursorBefore; - auto cellIterator = screenInfo.GetCellDataAt(cursorPos); - - while (Utils::s_CompareCoords(cursorPos, cursorAfter) < 0) - { - const String expectedText(L"\x20"); // unicode space character - - const auto actualTextValue = cellIterator->Chars(); - const String actualText(actualTextValue.data(), gsl::narrow(actualTextValue.size())); - - VERIFY_ARE_EQUAL(expectedText, actualText); - cellIterator++; - - bufferSize.IncrementInBounds(cursorPos); - } - } - } -}; diff --git a/src/host/ut_host/CommandListPopupTests.cpp b/src/host/ut_host/CommandListPopupTests.cpp deleted file mode 100644 index 342248702db..00000000000 --- a/src/host/ut_host/CommandListPopupTests.cpp +++ /dev/null @@ -1,538 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "CommonState.hpp" - -#include "../../interactivity/inc/ServiceLocator.hpp" - -#include "../CommandListPopup.hpp" -#include "PopupTestHelper.hpp" - -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; -using Microsoft::Console::Interactivity::ServiceLocator; -static constexpr size_t BUFFER_SIZE = 256; -static constexpr UINT s_NumberOfHistoryBuffers = 4; -static constexpr UINT s_HistoryBufferSize = 50; - -class CommandListPopupTests -{ - TEST_CLASS(CommandListPopupTests); - - std::unique_ptr m_state; - CommandHistory* m_pHistory; - - TEST_CLASS_SETUP(ClassSetup) - { - m_state = std::make_unique(); - m_state->PrepareGlobalFont(); - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.SetNumberOfHistoryBuffers(s_NumberOfHistoryBuffers); - gci.SetHistoryBufferSize(s_HistoryBufferSize); - return true; - } - - TEST_CLASS_CLEANUP(ClassCleanup) - { - m_state->CleanupGlobalFont(); - return true; - } - - TEST_METHOD_SETUP(MethodSetup) - { - m_state->PrepareGlobalInputBuffer(); - m_state->PrepareGlobalScreenBuffer(); - m_state->PrepareReadHandle(); - m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr); - // resize command history storage to 50 items so that we don't cycle on accident - // when PopupTestHelper::InitLongHistory() is called. - CommandHistory::s_ResizeAll(50); - if (!m_pHistory) - { - return false; - } - // History must be prepared before COOKED_READ (as it uses s_Find to get at it) - m_state->PrepareCookedReadData(); - return true; - } - - TEST_METHOD_CLEANUP(MethodCleanup) - { - CommandHistory::s_Free(nullptr); - m_pHistory = nullptr; - m_state->CleanupCookedReadData(); - m_state->CleanupReadHandle(); - m_state->CleanupGlobalInputBuffer(); - m_state->CleanupGlobalScreenBuffer(); - return true; - } - - void InitReadData(COOKED_READ_DATA& cookedReadData, - wchar_t* const pBuffer, - const size_t cchBuffer, - const size_t cursorPosition) - { - cookedReadData._bufferSize = cchBuffer * sizeof(wchar_t); - cookedReadData._bufPtr = pBuffer + cursorPosition; - cookedReadData._backupLimit = pBuffer; - cookedReadData.OriginalCursorPosition() = { 0, 0 }; - cookedReadData._bytesRead = cursorPosition * sizeof(wchar_t); - cookedReadData._currentPosition = cursorPosition; - cookedReadData.VisibleCharCount() = cursorPosition; - } - - TEST_METHOD(CanDismiss) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - modifiers = 0; - wch = VK_ESCAPE; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - const std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should not be changed - const std::wstring resultString(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(testString, resultString); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, testString.size() * sizeof(wchar_t)); - - // popup has been dismissed - VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); - } - - TEST_METHOD(UpMovesSelection) - { - // function to simulate user pressing up arrow - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_UP; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - const auto commandNumberBefore = popup._currentCommand; - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved up one line - VERIFY_ARE_EQUAL(commandNumberBefore - 1, popup._currentCommand); - } - - TEST_METHOD(DownMovesSelection) - { - // function to simulate user pressing down arrow - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_DOWN; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - // set the current command selection to the top of the list - popup._currentCommand = 0; - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - const auto commandNumberBefore = popup._currentCommand; - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved down one line - VERIFY_ARE_EQUAL(commandNumberBefore + 1, popup._currentCommand); - } - - TEST_METHOD(EndMovesSelectionToEnd) - { - // function to simulate user pressing end key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_END; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - // set the current command selection to the top of the list - popup._currentCommand = 0; - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved to the bottom line - VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands() - 1, popup._currentCommand); - } - - TEST_METHOD(HomeMovesSelectionToStart) - { - // function to simulate user pressing home key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_HOME; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved to the bottom line - VERIFY_ARE_EQUAL(0, popup._currentCommand); - } - - TEST_METHOD(PageUpMovesSelection) - { - // function to simulate user pressing page up key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_PRIOR; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitLongHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved up a page - VERIFY_ARE_EQUAL(static_cast(m_pHistory->GetNumberOfCommands()) - popup.Height() - 1, popup._currentCommand); - } - - TEST_METHOD(PageDownMovesSelection) - { - // function to simulate user pressing page down key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_NEXT; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitLongHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - // set the current command selection to the top of the list - popup._currentCommand = 0; - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // selection should have moved up a page - VERIFY_ARE_EQUAL(popup.Height(), popup._currentCommand); - } - - TEST_METHOD(SideArrowsFillsPrompt) - { - // function to simulate user pressing right arrow key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - wch = VK_RIGHT; - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - // set the current command selection to the top of the list - popup._currentCommand = 0; - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - // prompt should have history item in prompt - const auto historyItem = m_pHistory->GetLastCommand(); - const std::wstring_view resultText{ buffer, historyItem.size() }; - VERIFY_ARE_EQUAL(historyItem, resultText); - } - - TEST_METHOD(CanLaunchCommandNumberPopup) - { - // function to simulate user pressing F9 - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - wch = VK_F9; - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - auto& commandLine = CommandLine::Instance(); - VERIFY_IS_FALSE(commandLine.HasPopup()); - // should spawn a CommandNumberPopup - auto scopeExit = wil::scope_exit([&]() { commandLine.EndAllPopups(); }); - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT)); - VERIFY_IS_TRUE(commandLine.HasPopup()); - } - - TEST_METHOD(CanDeleteFromCommandHistory) - { - // function to simulate user pressing the delete key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_DELETE; - firstTime = false; - } - else - { - wch = VK_ESCAPE; - } - popupKey = true; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - const auto startHistorySize = m_pHistory->GetNumberOfCommands(); - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - VERIFY_ARE_EQUAL(m_pHistory->GetNumberOfCommands(), startHistorySize - 1); - } - - TEST_METHOD(CanReorderHistoryUp) - { - // function to simulate user pressing shift + up arrow - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto firstTime = true; - if (firstTime) - { - wch = VK_UP; - firstTime = false; - modifiers = SHIFT_PRESSED; - } - else - { - wch = VK_ESCAPE; - modifiers = 0; - } - popupKey = true; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my spout"); - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my handle"); - VERIFY_ARE_EQUAL(m_pHistory->GetNth(2), L"here is my spout"); - } - - TEST_METHOD(CanReorderHistoryDown) - { - // function to simulate user pressing the up arrow, then shift + down arrow, then escape - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static unsigned int count = 0; - if (count == 0) - { - wch = VK_UP; - modifiers = 0; - } - else if (count == 1) - { - wch = VK_DOWN; - modifiers = SHIFT_PRESSED; - } - else - { - wch = VK_ESCAPE; - modifiers = 0; - } - popupKey = true; - ++count; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - PopupTestHelper::InitHistory(*m_pHistory); - CommandListPopup popup{ gci.GetActiveOutputBuffer(), *m_pHistory }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my spout"); - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - VERIFY_ARE_EQUAL(m_pHistory->GetLastCommand(), L"here is my handle"); - VERIFY_ARE_EQUAL(m_pHistory->GetNth(2), L"here is my spout"); - } -}; diff --git a/src/host/ut_host/CommandNumberPopupTests.cpp b/src/host/ut_host/CommandNumberPopupTests.cpp deleted file mode 100644 index 006c36207bd..00000000000 --- a/src/host/ut_host/CommandNumberPopupTests.cpp +++ /dev/null @@ -1,297 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "CommonState.hpp" -#include "PopupTestHelper.hpp" - -#include "../../interactivity/inc/ServiceLocator.hpp" - -#include "../CommandNumberPopup.hpp" -#include "../CommandListPopup.hpp" - -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; -using Microsoft::Console::Interactivity::ServiceLocator; - -static constexpr size_t BUFFER_SIZE = 256; - -class CommandNumberPopupTests -{ - TEST_CLASS(CommandNumberPopupTests); - - std::unique_ptr m_state; - CommandHistory* m_pHistory; - - TEST_CLASS_SETUP(ClassSetup) - { - m_state = std::make_unique(); - m_state->PrepareGlobalFont(); - return true; - } - - TEST_CLASS_CLEANUP(ClassCleanup) - { - m_state->CleanupGlobalFont(); - return true; - } - - TEST_METHOD_SETUP(MethodSetup) - { - m_state->PrepareGlobalInputBuffer(); - m_state->PrepareGlobalScreenBuffer(); - m_state->PrepareReadHandle(); - m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr); - if (!m_pHistory) - { - return false; - } - // History must be prepared before COOKED_READ (as it uses s_Find to get at it) - m_state->PrepareCookedReadData(); - return true; - } - - TEST_METHOD_CLEANUP(MethodCleanup) - { - CommandHistory::s_Free(nullptr); - m_pHistory = nullptr; - m_state->CleanupCookedReadData(); - m_state->CleanupReadHandle(); - m_state->CleanupGlobalInputBuffer(); - m_state->CleanupGlobalScreenBuffer(); - return true; - } - - TEST_METHOD(CanDismiss) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = VK_ESCAPE; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - const std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should not be changed - const std::wstring resultString(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(testString, resultString); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, testString.size() * sizeof(wchar_t)); - - // popup has been dismissed - VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); - } - - TEST_METHOD(CanDismissAllPopups) - { - Log::Comment(L"that that all popups are dismissed when CommandNumberPopup is dismissed"); - // CommandNumberPopup is the only popup that can act as a 2nd popup. make sure that it dismisses all - // popups when exiting - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = VK_ESCAPE; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // add popups to CommandLine - auto& commandLine = CommandLine::Instance(); - commandLine._popups.emplace_front(std::make_unique(gci.GetActiveOutputBuffer(), *m_pHistory)); - commandLine._popups.emplace_front(std::make_unique(gci.GetActiveOutputBuffer())); - auto& numberPopup = *commandLine._popups.front(); - numberPopup.SetUserInputFunction(fn); - - VERIFY_ARE_EQUAL(commandLine._popups.size(), 2u); - - // prepare cookedReadData - const std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(numberPopup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - VERIFY_IS_FALSE(commandLine.HasPopup()); - } - - TEST_METHOD(EmptyInputCountsAsOldestHistory) - { - Log::Comment(L"hitting enter with no input should grab the oldest history item"); - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = false; - wch = UNICODE_CARRIAGERETURN; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should contain the least recent history item - - const auto expected = m_pHistory->GetLastCommand(); - const std::wstring resultString(buffer, buffer + expected.size()); - VERIFY_ARE_EQUAL(expected, resultString); - } - - TEST_METHOD(CanSelectHistoryItem) - { - PopupTestHelper::InitHistory(*m_pHistory); - for (CommandHistory::Index historyIndex = 0; historyIndex < m_pHistory->GetNumberOfCommands(); ++historyIndex) - { - Popup::UserInputFunction fn = [historyIndex](COOKED_READ_DATA& /*cookedReadData*/, - bool& popupKey, - DWORD& modifiers, - wchar_t& wch) { - static auto needReturn = false; - popupKey = false; - modifiers = 0; - if (!needReturn) - { - const auto str = std::to_string(historyIndex); - wch = str.at(0); - needReturn = true; - } - else - { - wch = UNICODE_CARRIAGERETURN; - needReturn = false; - } - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should contain the correct nth history item - - const auto expected = m_pHistory->GetNth(gsl::narrow(historyIndex)); - const std::wstring resultString(buffer, buffer + expected.size()); - VERIFY_ARE_EQUAL(expected, resultString); - } - } - - TEST_METHOD(LargeNumberGrabsNewestHistoryItem) - { - Log::Comment(L"entering a number larger than the number of history items should grab the most recent history item"); - - // simulates user pressing 1, 2, 3, 4, 5, enter - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - static auto num = 1; - popupKey = false; - modifiers = 0; - if (num <= 5) - { - const auto str = std::to_string(num); - wch = str.at(0); - ++num; - } - else - { - wch = UNICODE_CARRIAGERETURN; - } - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should contain the most recent history item - - const auto expected = m_pHistory->GetLastCommand(); - const std::wstring resultString(buffer, buffer + expected.size()); - VERIFY_ARE_EQUAL(expected, resultString); - } - - TEST_METHOD(InputIsLimited) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - CommandNumberPopup popup{ gci.GetActiveOutputBuffer() }; - - // input can't delete past zero number input - popup._pop(); - VERIFY_ARE_EQUAL(popup._parse(), 0); - - // input can only be numbers - VERIFY_THROWS_SPECIFIC(popup._push(L'$'), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); - VERIFY_THROWS_SPECIFIC(popup._push(L'A'), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; }); - - // input can't be more than 5 numbers - popup._push(L'1'); - VERIFY_ARE_EQUAL(popup._parse(), 1); - popup._push(L'2'); - VERIFY_ARE_EQUAL(popup._parse(), 12); - popup._push(L'3'); - VERIFY_ARE_EQUAL(popup._parse(), 123); - popup._push(L'4'); - VERIFY_ARE_EQUAL(popup._parse(), 1234); - popup._push(L'5'); - VERIFY_ARE_EQUAL(popup._parse(), 12345); - // this shouldn't affect the parsed number - popup._push(L'6'); - VERIFY_ARE_EQUAL(popup._parse(), 12345); - // make sure we can delete input correctly - popup._pop(); - VERIFY_ARE_EQUAL(popup._parse(), 1234); - } -}; diff --git a/src/host/ut_host/CopyFromCharPopupTests.cpp b/src/host/ut_host/CopyFromCharPopupTests.cpp deleted file mode 100644 index ccc54172a18..00000000000 --- a/src/host/ut_host/CopyFromCharPopupTests.cpp +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "CommonState.hpp" -#include "PopupTestHelper.hpp" - -#include "../../interactivity/inc/ServiceLocator.hpp" - -#include "../CopyFromCharPopup.hpp" - -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; -using Microsoft::Console::Interactivity::ServiceLocator; - -static constexpr size_t BUFFER_SIZE = 256; - -class CopyFromCharPopupTests -{ - TEST_CLASS(CopyFromCharPopupTests); - - std::unique_ptr m_state; - CommandHistory* m_pHistory; - - TEST_CLASS_SETUP(ClassSetup) - { - m_state = std::make_unique(); - m_state->PrepareGlobalFont(); - return true; - } - - TEST_CLASS_CLEANUP(ClassCleanup) - { - m_state->CleanupGlobalFont(); - return true; - } - - TEST_METHOD_SETUP(MethodSetup) - { - m_state->PrepareGlobalInputBuffer(); - m_state->PrepareGlobalScreenBuffer(); - m_state->PrepareReadHandle(); - m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr); - if (!m_pHistory) - { - return false; - } - // History must be prepared before COOKED_READ (as it uses s_Find to get at it) - m_state->PrepareCookedReadData(); - return true; - } - - TEST_METHOD_CLEANUP(MethodCleanup) - { - CommandHistory::s_Free(nullptr); - m_pHistory = nullptr; - m_state->CleanupCookedReadData(); - m_state->CleanupReadHandle(); - m_state->CleanupGlobalInputBuffer(); - m_state->CleanupGlobalScreenBuffer(); - return true; - } - - TEST_METHOD(CanDismiss) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = VK_ESCAPE; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyFromCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should not be changed - std::wstring resultString(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(testString, resultString); - VERIFY_ARE_EQUAL(cookedReadData.BytesRead(), testString.size() * sizeof(wchar_t)); - - // popup has been dismissed - VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); - } - - TEST_METHOD(DeleteAllWhenCharNotFound) - { - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = false; - wch = L'x'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyFromCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - // move cursor to beginning of prompt text - cookedReadData.InsertionPoint() = 0; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // all text to the right of the cursor should be gone - VERIFY_ARE_EQUAL(cookedReadData.BytesRead(), 0u); - } - - TEST_METHOD(CanDeletePartialLine) - { - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = false; - wch = L'f'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyFromCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - std::wstring testString = L"By the rude bridge that arched the flood"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - // move cursor to index 12 - const size_t index = 12; - cookedReadData.SetBufferCurrentPtr(buffer + index); - cookedReadData.InsertionPoint() = index; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - std::wstring expectedText = L"By the rude flood"; - VERIFY_ARE_EQUAL(cookedReadData.BytesRead(), expectedText.size() * sizeof(wchar_t)); - std::wstring resultText(buffer, buffer + expectedText.size()); - VERIFY_ARE_EQUAL(resultText, expectedText); - } -}; diff --git a/src/host/ut_host/CopyToCharPopupTests.cpp b/src/host/ut_host/CopyToCharPopupTests.cpp deleted file mode 100644 index 320e0b7841a..00000000000 --- a/src/host/ut_host/CopyToCharPopupTests.cpp +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "WexTestClass.h" -#include "../../inc/consoletaeftemplates.hpp" - -#include "CommonState.hpp" -#include "PopupTestHelper.hpp" - -#include "../../interactivity/inc/ServiceLocator.hpp" - -#include "../CopyToCharPopup.hpp" - -using Microsoft::Console::Interactivity::ServiceLocator; -using namespace WEX::Common; -using namespace WEX::Logging; -using namespace WEX::TestExecution; - -static constexpr size_t BUFFER_SIZE = 256; - -class CopyToCharPopupTests -{ - TEST_CLASS(CopyToCharPopupTests); - - std::unique_ptr m_state; - CommandHistory* m_pHistory; - - TEST_CLASS_SETUP(ClassSetup) - { - m_state = std::make_unique(); - m_state->PrepareGlobalFont(); - return true; - } - - TEST_CLASS_CLEANUP(ClassCleanup) - { - m_state->CleanupGlobalFont(); - return true; - } - - TEST_METHOD_SETUP(MethodSetup) - { - m_state->PrepareGlobalInputBuffer(); - m_state->PrepareGlobalScreenBuffer(); - m_state->PrepareReadHandle(); - m_pHistory = CommandHistory::s_Allocate(L"cmd.exe", nullptr); - if (!m_pHistory) - { - return false; - } - // History must be prepared before COOKED_READ (as it uses s_Find to get at it) - m_state->PrepareCookedReadData(); - return true; - } - - TEST_METHOD_CLEANUP(MethodCleanup) - { - CommandHistory::s_Free(nullptr); - m_pHistory = nullptr; - m_state->CleanupCookedReadData(); - m_state->CleanupReadHandle(); - m_state->CleanupGlobalInputBuffer(); - m_state->CleanupGlobalScreenBuffer(); - return true; - } - - TEST_METHOD(CanDismiss) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = VK_ESCAPE; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - const std::wstring testString = L"hello world"; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should not be changed - const std::wstring resultString(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(testString, resultString); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, testString.size() * sizeof(wchar_t)); - - // popup has been dismissed - VERIFY_IS_FALSE(CommandLine::Instance().HasPopup()); - } - - TEST_METHOD(NothingHappensWhenCharNotFound) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = L'x'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0u); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // the buffer should not be changed - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, 0u); - } - - TEST_METHOD(CanCopyToEmptyPrompt) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = L's'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), 0u); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - const std::wstring expectedText = L"here i"; - - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, cookedReadData._backupLimit + expectedText.size()); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, expectedText.size() * sizeof(wchar_t)); - - // make sure that the text matches - const std::wstring resultText(buffer, buffer + expectedText.size()); - VERIFY_ARE_EQUAL(resultText, expectedText); - // make sure that more wasn't copied - VERIFY_ARE_EQUAL(buffer[expectedText.size()], UNICODE_SPACE); - } - - TEST_METHOD(WontCopyTextBeforeCursor) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = L's'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData with a string longer than the previous history - const std::wstring testString = L"Whose woods there are I think I know."; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - const wchar_t* const expectedBufPtr = cookedReadData._bufPtr; - const auto expectedBytesRead = cookedReadData._bytesRead; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - // nothing should have changed - VERIFY_ARE_EQUAL(cookedReadData._bufPtr, expectedBufPtr); - VERIFY_ARE_EQUAL(cookedReadData._bytesRead, expectedBytesRead); - const std::wstring resultText(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(resultText, testString); - // make sure that more wasn't copied - VERIFY_ARE_EQUAL(buffer[testString.size()], UNICODE_SPACE); - } - - TEST_METHOD(CanMergeLine) - { - // function to simulate user pressing escape key - Popup::UserInputFunction fn = [](COOKED_READ_DATA& /*cookedReadData*/, bool& popupKey, DWORD& modifiers, wchar_t& wch) { - popupKey = true; - wch = L's'; - modifiers = 0; - return STATUS_SUCCESS; - }; - - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // prepare popup - CopyToCharPopup popup{ gci.GetActiveOutputBuffer() }; - popup.SetUserInputFunction(fn); - - // prepare cookedReadData with a string longer than the previous history - const std::wstring testString = L"fear "; - wchar_t buffer[BUFFER_SIZE]; - std::fill(std::begin(buffer), std::end(buffer), UNICODE_SPACE); - std::copy(testString.begin(), testString.end(), std::begin(buffer)); - auto& cookedReadData = gci.CookedReadData(); - PopupTestHelper::InitReadData(cookedReadData, buffer, ARRAYSIZE(buffer), testString.size()); - PopupTestHelper::InitHistory(*m_pHistory); - cookedReadData._commandHistory = m_pHistory; - - VERIFY_ARE_EQUAL(popup.Process(cookedReadData), static_cast(CONSOLE_STATUS_WAIT_NO_BLOCK)); - - const std::wstring expectedText = L"fear is"; - const std::wstring resultText(buffer, buffer + testString.size()); - VERIFY_ARE_EQUAL(resultText, testString); - // make sure that more wasn't copied - VERIFY_ARE_EQUAL(buffer[expectedText.size()], UNICODE_SPACE); - } -}; diff --git a/src/host/ut_host/Host.UnitTests.vcxproj b/src/host/ut_host/Host.UnitTests.vcxproj index 36afffca49c..f995dea0a44 100644 --- a/src/host/ut_host/Host.UnitTests.vcxproj +++ b/src/host/ut_host/Host.UnitTests.vcxproj @@ -15,12 +15,7 @@ - - - - - @@ -96,7 +91,6 @@ - diff --git a/src/host/ut_host/Host.UnitTests.vcxproj.filters b/src/host/ut_host/Host.UnitTests.vcxproj.filters index d7045f877f0..4366c712fb9 100644 --- a/src/host/ut_host/Host.UnitTests.vcxproj.filters +++ b/src/host/ut_host/Host.UnitTests.vcxproj.filters @@ -75,27 +75,12 @@ Source Files - - Source Files - - - Source Files - - - Source Files - - - Source Files - Source Files Source Files - - Source Files - Source Files @@ -116,9 +101,6 @@ Header Files - - Header Files - diff --git a/src/host/ut_host/PopupTestHelper.hpp b/src/host/ut_host/PopupTestHelper.hpp deleted file mode 100644 index e427cd9624b..00000000000 --- a/src/host/ut_host/PopupTestHelper.hpp +++ /dev/null @@ -1,84 +0,0 @@ -/*++ - -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- PopupTestHelper.hpp - -Abstract: -- helper functions for unit testing the various popups - -Author(s): -- Austin Diviness (AustDi) 06-Sep-2018 - ---*/ - -#pragma once - -#include "../history.h" -#include "../readDataCooked.hpp" - -class PopupTestHelper final -{ -public: - static void InitReadData(COOKED_READ_DATA& cookedReadData, - wchar_t* const pBuffer, - const size_t cchBuffer, - const size_t cursorPosition) noexcept - { - cookedReadData._bufferSize = cchBuffer * sizeof(wchar_t); - cookedReadData._bufPtr = pBuffer + cursorPosition; - cookedReadData._backupLimit = pBuffer; - cookedReadData.OriginalCursorPosition() = { 0, 0 }; - cookedReadData._bytesRead = cursorPosition * sizeof(wchar_t); - cookedReadData._currentPosition = cursorPosition; - cookedReadData.VisibleCharCount() = cursorPosition; - } - - static void InitHistory(CommandHistory& history) noexcept - { - history.Empty(); - history.Flags |= CommandHistory::CLE_ALLOCATED; - VERIFY_SUCCEEDED(history.Add(L"I'm a little teapot", false)); - VERIFY_SUCCEEDED(history.Add(L"hear me shout", false)); - VERIFY_SUCCEEDED(history.Add(L"here is my handle", false)); - VERIFY_SUCCEEDED(history.Add(L"here is my spout", false)); - VERIFY_ARE_EQUAL(history.GetNumberOfCommands(), 4); - } - - static void InitLongHistory(CommandHistory& history) noexcept - { - history.Empty(); - history.Flags |= CommandHistory::CLE_ALLOCATED; - VERIFY_SUCCEEDED(history.Add(L"Because I could not stop for Death", false)); - VERIFY_SUCCEEDED(history.Add(L"He kindly stopped for me", false)); - VERIFY_SUCCEEDED(history.Add(L"The carriage held but just Ourselves", false)); - VERIFY_SUCCEEDED(history.Add(L"And Immortality", false)); - VERIFY_SUCCEEDED(history.Add(L"~", false)); - VERIFY_SUCCEEDED(history.Add(L"We slowly drove - He knew no haste", false)); - VERIFY_SUCCEEDED(history.Add(L"And I had put away", false)); - VERIFY_SUCCEEDED(history.Add(L"My labor and my leisure too", false)); - VERIFY_SUCCEEDED(history.Add(L"For His Civility", false)); - VERIFY_SUCCEEDED(history.Add(L"~", false)); - VERIFY_SUCCEEDED(history.Add(L"We passed the School, where Children strove", false)); - VERIFY_SUCCEEDED(history.Add(L"At Recess - in the Ring", false)); - VERIFY_SUCCEEDED(history.Add(L"We passed the Fields of Gazing Grain", false)); - VERIFY_SUCCEEDED(history.Add(L"We passed the Setting Sun", false)); - VERIFY_SUCCEEDED(history.Add(L"~", false)); - VERIFY_SUCCEEDED(history.Add(L"Or rather - He passed us,", false)); - VERIFY_SUCCEEDED(history.Add(L"The Dews drew quivering and chill,", false)); - VERIFY_SUCCEEDED(history.Add(L"For only Gossamer, my Gown,", false)); - VERIFY_SUCCEEDED(history.Add(L"My Tippet - only Tulle", false)); - VERIFY_SUCCEEDED(history.Add(L"~", false)); - VERIFY_SUCCEEDED(history.Add(L"We paused before a House that seemed", false)); - VERIFY_SUCCEEDED(history.Add(L"A Swelling of the Ground -", false)); - VERIFY_SUCCEEDED(history.Add(L"The Roof was scarcely visible -", false)); - VERIFY_SUCCEEDED(history.Add(L"The Cornice - in the Ground -", false)); - VERIFY_SUCCEEDED(history.Add(L"~", false)); - VERIFY_SUCCEEDED(history.Add(L"Since then - 'tis Centuries - and yet", false)); - VERIFY_SUCCEEDED(history.Add(L"Feels shorter than the Day", false)); - VERIFY_SUCCEEDED(history.Add(L"~ Emily Dickinson", false)); - VERIFY_ARE_EQUAL(history.GetNumberOfCommands(), 28); - } -}; diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 1dd058f057d..f1e901c91bd 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -2895,15 +2895,11 @@ void ScreenBufferTests::BackspaceDefaultAttrsWriteCharsLegacy() { BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Data:writeSingly", L"{false, true}") - TEST_METHOD_PROPERTY(L"Data:writeCharsLegacyMode", L"{0, 1, 2}") END_TEST_METHOD_PROPERTIES(); bool writeSingly; VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeSingly", writeSingly), L"Write one at a time = true, all at the same time = false"); - DWORD writeCharsLegacyMode; - VERIFY_SUCCEEDED(TestData::TryGetValue(L"writeCharsLegacyMode", writeCharsLegacyMode), L""); - // Created for MSFT:19735050. // Kinda the same as above, but with WriteCharsLegacy instead. // The variable that really breaks this scenario @@ -2931,18 +2927,13 @@ void ScreenBufferTests::BackspaceDefaultAttrsWriteCharsLegacy() if (writeSingly) { - auto str = L"X"; - size_t seqCb = 2; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, writeCharsLegacyMode, nullptr)); - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, writeCharsLegacyMode, nullptr)); - str = L"\x08"; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, writeCharsLegacyMode, nullptr)); + WriteCharsLegacy(si, L"X", false, nullptr); + WriteCharsLegacy(si, L"X", false, nullptr); + WriteCharsLegacy(si, L"\x08", false, nullptr); } else { - const auto str = L"XX\x08"; - size_t seqCb = 6; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, writeCharsLegacyMode, nullptr)); + WriteCharsLegacy(si, L"XX\x08", false, nullptr); } TextAttribute expectedDefaults{}; @@ -7191,8 +7182,7 @@ void ScreenBufferTests::UpdateVirtualBottomWhenCursorMovesBelowIt() Log::Comment(L"Now write several lines of content using WriteCharsLegacy"); const auto content = L"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n"; - auto numBytes = wcslen(content) * sizeof(wchar_t); - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, content, content, content, &numBytes, nullptr, 0, 0, nullptr)); + WriteCharsLegacy(si, content, false, nullptr); Log::Comment(L"Confirm that the cursor position has moved down 10 lines"); const auto newCursorPos = til::point{ initialCursorPos.x, initialCursorPos.y + 10 }; diff --git a/src/host/ut_host/SelectionTests.cpp b/src/host/ut_host/SelectionTests.cpp index 99193049816..f33889992bb 100644 --- a/src/host/ut_host/SelectionTests.cpp +++ b/src/host/ut_host/SelectionTests.cpp @@ -10,7 +10,6 @@ #include "globals.h" #include "selection.hpp" -#include "cmdline.h" #include "../interactivity/inc/ServiceLocator.hpp" @@ -382,89 +381,6 @@ class SelectionInputTests return true; } - TEST_METHOD(TestGetInputLineBoundaries) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - // 80x80 box - const til::CoordType sRowWidth = 80; - - til::inclusive_rect srectEdges; - srectEdges.left = srectEdges.top = 0; - srectEdges.right = srectEdges.bottom = sRowWidth - 1; - - // false when no cooked read data exists - VERIFY_IS_FALSE(gci.HasPendingCookedRead()); - - auto fResult = Selection::s_GetInputLineBoundaries(nullptr, nullptr); - VERIFY_IS_FALSE(fResult); - - // prepare some read data - m_state->PrepareReadHandle(); - auto cleanupReadHandle = wil::scope_exit([&]() { m_state->CleanupReadHandle(); }); - - m_state->PrepareCookedReadData(); - // set up to clean up read data later - auto cleanupCookedRead = wil::scope_exit([&]() { m_state->CleanupCookedReadData(); }); - - auto& readData = gci.CookedReadData(); - - // backup text info position over remainder of text execution duration - auto& textBuffer = gci.GetActiveOutputBuffer().GetTextBuffer(); - til::point coordOldTextInfoPos; - coordOldTextInfoPos.x = textBuffer.GetCursor().GetPosition().x; - coordOldTextInfoPos.y = textBuffer.GetCursor().GetPosition().y; - - // set various cursor positions - readData.OriginalCursorPosition().x = 15; - readData.OriginalCursorPosition().y = 3; - - readData.VisibleCharCount() = 200; - - textBuffer.GetCursor().SetXPosition(35); - textBuffer.GetCursor().SetYPosition(35); - - // try getting boundaries with no pointers. parameters should be fully optional. - fResult = Selection::s_GetInputLineBoundaries(nullptr, nullptr); - VERIFY_IS_TRUE(fResult); - - // now let's get some actual data - til::point coordStart; - til::point coordEnd; - - fResult = Selection::s_GetInputLineBoundaries(&coordStart, &coordEnd); - VERIFY_IS_TRUE(fResult); - - // starting position/boundary should always be where the input line started - VERIFY_ARE_EQUAL(coordStart.x, readData.OriginalCursorPosition().x); - VERIFY_ARE_EQUAL(coordStart.y, readData.OriginalCursorPosition().y); - - // ending position can vary. it's in one of two spots - // 1. If the original cooked cursor was valid (which it was this first time), it's NumberOfVisibleChars ahead. - til::point coordFinalPos; - - const auto cCharsToAdjust = ((til::CoordType)readData.VisibleCharCount() - 1); // then -1 to be on the last piece of text, not past it - - coordFinalPos.x = (readData.OriginalCursorPosition().x + cCharsToAdjust) % sRowWidth; - coordFinalPos.y = readData.OriginalCursorPosition().y + ((readData.OriginalCursorPosition().x + cCharsToAdjust) / sRowWidth); - - VERIFY_ARE_EQUAL(coordEnd.x, coordFinalPos.x); - VERIFY_ARE_EQUAL(coordEnd.y, coordFinalPos.y); - - // 2. if the original cooked cursor is invalid, then it's the text info cursor position - readData.OriginalCursorPosition().x = -1; - readData.OriginalCursorPosition().y = -1; - - fResult = Selection::s_GetInputLineBoundaries(nullptr, &coordEnd); - VERIFY_IS_TRUE(fResult); - - VERIFY_ARE_EQUAL(coordEnd.x, textBuffer.GetCursor().GetPosition().x - 1); // -1 to be on the last piece of text, not past it - VERIFY_ARE_EQUAL(coordEnd.y, textBuffer.GetCursor().GetPosition().y); - - // restore text buffer info position - textBuffer.GetCursor().SetXPosition(coordOldTextInfoPos.x); - textBuffer.GetCursor().SetYPosition(coordOldTextInfoPos.y); - } - TEST_METHOD(TestWordByWordPrevious) { BEGIN_TEST_METHOD_PROPERTIES() diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index 44781d38a72..d52dfc5ae69 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -1554,60 +1554,20 @@ void TextBufferTests::TestBackspaceStringsAPI() // should be the same. std::unique_ptr waiter; - size_t aCb = 2; - VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, false, waiter)); - - size_t seqCb = 6; Log::Comment(NoThrowString().Format( L"Using WriteCharsLegacy, write \\b \\b as a single string.")); - { - const auto str = L"\b \b"; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, 0, nullptr)); - - VERIFY_ARE_EQUAL(cursor.GetPosition().x, x0); - VERIFY_ARE_EQUAL(cursor.GetPosition().y, y0); - - Log::Comment(NoThrowString().Format( - L"Using DoWriteConsole, write \\b \\b as a single string.")); - VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, false, waiter)); - - VERIFY_SUCCEEDED(DoWriteConsole(str, &seqCb, si, false, waiter)); - VERIFY_ARE_EQUAL(cursor.GetPosition().x, x0); - VERIFY_ARE_EQUAL(cursor.GetPosition().y, y0); - } + size_t aCb = 2; + size_t seqCb = 6; + VERIFY_SUCCEEDED(DoWriteConsole(L"a", &aCb, si, false, waiter)); + VERIFY_SUCCEEDED(DoWriteConsole(L"\b \b", &seqCb, si, false, waiter)); + VERIFY_ARE_EQUAL(cursor.GetPosition().x, x0); + VERIFY_ARE_EQUAL(cursor.GetPosition().y, y0); seqCb = 2; - - Log::Comment(NoThrowString().Format( - L"Using DoWriteConsole, write \\b \\b as separate strings.")); - VERIFY_SUCCEEDED(DoWriteConsole(L"a", &seqCb, si, false, waiter)); VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, false, waiter)); VERIFY_SUCCEEDED(DoWriteConsole(L" ", &seqCb, si, false, waiter)); VERIFY_SUCCEEDED(DoWriteConsole(L"\b", &seqCb, si, false, waiter)); - - VERIFY_ARE_EQUAL(cursor.GetPosition().x, x0); - VERIFY_ARE_EQUAL(cursor.GetPosition().y, y0); - - Log::Comment(NoThrowString().Format( - L"Using WriteCharsLegacy, write \\b \\b as separate strings.")); - { - const auto str = L"a"; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, 0, nullptr)); - } - { - const auto str = L"\b"; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, 0, nullptr)); - } - { - const auto str = L" "; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, 0, nullptr)); - } - { - const auto str = L"\b"; - VERIFY_NT_SUCCESS(WriteCharsLegacy(si, str, str, str, &seqCb, nullptr, cursor.GetPosition().x, 0, nullptr)); - } - VERIFY_ARE_EQUAL(cursor.GetPosition().x, x0); VERIFY_ARE_EQUAL(cursor.GetPosition().y, y0); } diff --git a/src/host/ut_host/sources b/src/host/ut_host/sources index 161939e3c73..9b47dded30d 100644 --- a/src/host/ut_host/sources +++ b/src/host/ut_host/sources @@ -37,11 +37,6 @@ SOURCES = \ ConptyOutputTests.cpp \ ViewportTests.cpp \ ConsoleArgumentsTests.cpp \ - CommandLineTests.cpp \ - CommandListPopupTests.cpp \ - CommandNumberPopupTests.cpp \ - CopyFromCharPopupTests.cpp \ - CopyToCharPopupTests.cpp \ ObjectTests.cpp \ DefaultResource.rc \ diff --git a/src/interactivity/win32/menu.cpp b/src/interactivity/win32/menu.cpp index 205a0135498..7b59887daaf 100644 --- a/src/interactivity/win32/menu.cpp +++ b/src/interactivity/win32/menu.cpp @@ -507,13 +507,7 @@ void Menu::s_PropertiesUpdate(PCONSOLE_STATE_INFO pStateInfo) if (coordBuffer.width != coordScreenBufferSize.width || coordBuffer.height != coordScreenBufferSize.height) { - const auto pCommandLine = &CommandLine::Instance(); - - pCommandLine->Hide(FALSE); - LOG_IF_FAILED(ScreenInfo.ResizeScreenBuffer(coordBuffer, TRUE)); - - pCommandLine->Show(); } // Finally, restrict window size to the maximum possible size for the given buffer now that it's processed. diff --git a/src/interactivity/win32/windowio.cpp b/src/interactivity/win32/windowio.cpp index 6331a6590f7..5866400a730 100644 --- a/src/interactivity/win32/windowio.cpp +++ b/src/interactivity/win32/windowio.cpp @@ -397,7 +397,7 @@ void HandleKeyEvent(const HWND hWnd, } } // we need to check if there is an active popup because otherwise they won't be able to receive shift+key events - if (pSelection->s_IsValidKeyboardLineSelection(&inputKeyInfo) && IsInProcessedInputMode() && gci.PopupCount.load() == 0) + if (pSelection->s_IsValidKeyboardLineSelection(&inputKeyInfo) && IsInProcessedInputMode() && !gci.HasPendingPopup()) { if (!bKeyDown || pSelection->HandleKeyboardLineSelectionEvent(&inputKeyInfo)) {