Skip to content

Commit 8e94596

Browse files
lheckerjs324
authored andcommitted
Fix UIA and marks regressions due to cooked read (microsoft#16105)
The initial cooked read (= conhost readline) rewrite had two flaws: * Using viewport scrolls under ConPTY to avoid emitting newlines resulted in various bugs around marks, coloring, etc. It's still somewhat unclear why this happened, but the next issue is related and much worse. * Rewriting the input line every time causes problems with accessibility tools, as they'll re-announce unchanged parts again and again. The solution to these is to simply stop writing the unchanged parts of the prompt. To do this, code was added to measure the size of text without actually inserting them into the buffer. Since this meant that the "interactive" mode of `WriteCharsLegacy` would need to be duplicated for the new code, I instead moved those parts into `COOKED_READ_DATA`. That way we can now have the interactive transform of the prompt (= Ctrl+C -> ^C) and the two text functions (measure text & actually write text) are now agnostic to this transformation. Closes microsoft#16034 Closes microsoft#16044 ## Validation Steps Performed * A vision impaired user checked it out and it seemed fine ✅
1 parent 9db88af commit 8e94596

File tree

11 files changed

+492
-240
lines changed

11 files changed

+492
-240
lines changed

.github/actions/spelling/expect/expect.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1776,6 +1776,7 @@ stdafx
17761776
STDAPI
17771777
stdc
17781778
stdcpp
1779+
STDEXT
17791780
STDMETHODCALLTYPE
17801781
STDMETHODIMP
17811782
STGM

src/buffer/out/textBuffer.cpp

+82
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,88 @@ size_t TextBuffer::GraphemePrev(const std::wstring_view& chars, size_t position)
416416
return til::utf16_iterate_prev(chars, position);
417417
}
418418

419+
// Ever wondered how much space a piece of text needs before inserting it? This function will tell you!
420+
// It fundamentally behaves identical to the various ROW functions around `RowWriteState`.
421+
//
422+
// Set `columnLimit` to the amount of space that's available (e.g. `buffer_width - cursor_position.x`)
423+
// and it'll return the amount of characters that fit into this space. The out parameter `columns`
424+
// will contain the amount of columns this piece of text has actually used.
425+
//
426+
// Just like with `RowWriteState` one special case is when not all text fits into the given space:
427+
// In that case, this function always returns exactly `columnLimit`. This distinction is important when "inserting"
428+
// a wide glyph but there's only 1 column left. That 1 remaining column is supposed to be padded with whitespace.
429+
size_t TextBuffer::FitTextIntoColumns(const std::wstring_view& chars, til::CoordType columnLimit, til::CoordType& columns) noexcept
430+
{
431+
columnLimit = std::max(0, columnLimit);
432+
433+
const auto beg = chars.begin();
434+
const auto end = chars.end();
435+
auto it = beg;
436+
const auto asciiEnd = beg + std::min(chars.size(), gsl::narrow_cast<size_t>(columnLimit));
437+
438+
// ASCII fast-path: 1 char always corresponds to 1 column.
439+
for (; it != asciiEnd && *it < 0x80; ++it)
440+
{
441+
}
442+
443+
const auto dist = gsl::narrow_cast<size_t>(it - beg);
444+
auto col = gsl::narrow_cast<til::CoordType>(dist);
445+
446+
if (it == asciiEnd) [[likely]]
447+
{
448+
columns = col;
449+
return dist;
450+
}
451+
452+
// Unicode slow-path where we need to count text and columns separately.
453+
for (;;)
454+
{
455+
auto ptr = &*it;
456+
const auto wch = *ptr;
457+
size_t len = 1;
458+
459+
col++;
460+
461+
// Even in our slow-path we can avoid calling IsGlyphFullWidth if the current character is ASCII.
462+
// It also allows us to skip the surrogate pair decoding at the same time.
463+
if (wch >= 0x80)
464+
{
465+
if (til::is_surrogate(wch))
466+
{
467+
const auto it2 = it + 1;
468+
if (til::is_leading_surrogate(wch) && it2 != end && til::is_trailing_surrogate(*it2))
469+
{
470+
len = 2;
471+
}
472+
else
473+
{
474+
ptr = &UNICODE_REPLACEMENT;
475+
}
476+
}
477+
478+
col += IsGlyphFullWidth({ ptr, len });
479+
}
480+
481+
// If we ran out of columns, we need to always return `columnLimit` and not `cols`,
482+
// because if we tried inserting a wide glyph into just 1 remaining column it will
483+
// fail to fit, but that remaining column still has been used up. When the caller sees
484+
// `columns == columnLimit` they will line-wrap and continue inserting into the next row.
485+
if (col > columnLimit)
486+
{
487+
columns = columnLimit;
488+
return gsl::narrow_cast<size_t>(it - beg);
489+
}
490+
491+
// But if we simply ran out of text we just need to return the actual number of columns.
492+
it += len;
493+
if (it == end)
494+
{
495+
columns = col;
496+
return chars.size();
497+
}
498+
}
499+
}
500+
419501
// Pretend as if `position` is a regular cursor in the TextBuffer.
420502
// This function will then pretend as if you pressed the left/right arrow
421503
// keys `distance` amount of times (negative = left, positive = right).

src/buffer/out/textBuffer.hpp

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ class TextBuffer final
137137

138138
static size_t GraphemeNext(const std::wstring_view& chars, size_t position) noexcept;
139139
static size_t GraphemePrev(const std::wstring_view& chars, size_t position) noexcept;
140+
static size_t FitTextIntoColumns(const std::wstring_view& chars, til::CoordType columnLimit, til::CoordType& columns) noexcept;
140141

141142
til::point NavigateCursor(til::point position, til::CoordType distance) const;
142143

src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp

+2-2
Original file line numberDiff line numberDiff line change
@@ -3243,7 +3243,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottom()
32433243
}
32443244
else if (writingMethod == PrintWithWriteCharsLegacy)
32453245
{
3246-
WriteCharsLegacy(si, str, false, nullptr);
3246+
WriteCharsLegacy(si, str, nullptr);
32473247
}
32483248
};
32493249

@@ -3401,7 +3401,7 @@ void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS()
34013401
}
34023402
else if (writingMethod == PrintWithWriteCharsLegacy)
34033403
{
3404-
WriteCharsLegacy(si, str, false, nullptr);
3404+
WriteCharsLegacy(si, str, nullptr);
34053405
}
34063406
};
34073407

src/common.build.pre.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@
133133
We didn't and it breaks WRL and large parts of conhost code.
134134
-->
135135
<DisableSpecificWarnings>4201;4312;4467;5105;26434;26445;26456;%(DisableSpecificWarnings)</DisableSpecificWarnings>
136-
<PreprocessorDefinitions>_WINDOWS;EXTERNAL_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
136+
<PreprocessorDefinitions>_WINDOWS;EXTERNAL_BUILD;_SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING;%(PreprocessorDefinitions)</PreprocessorDefinitions>
137137
<SDLCheck>true</SDLCheck>
138138
<PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
139139
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>

src/host/_stream.cpp

+33-79
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ using Microsoft::Console::VirtualTerminal::StateMachine;
3939
// - coordCursor - New location of cursor.
4040
// - fKeepCursorVisible - TRUE if changing window origin desirable when hit right edge
4141
// Return Value:
42-
static void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor, const bool interactive, _Inout_opt_ til::CoordType* psScrollY)
42+
static void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor, _Inout_opt_ til::CoordType* psScrollY)
4343
{
4444
const auto bufferSize = screenInfo.GetBufferSize().Dimensions();
4545
if (coordCursor.x < 0)
@@ -70,42 +70,19 @@ static void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point
7070

7171
if (coordCursor.y >= bufferSize.height)
7272
{
73-
const auto vtIo = ServiceLocator::LocateGlobals().getConsoleInformation().GetVtIo();
74-
const auto renderer = ServiceLocator::LocateGlobals().pRender;
75-
const auto needsConPTYWorkaround = interactive && vtIo->IsUsingVt();
7673
auto& buffer = screenInfo.GetTextBuffer();
77-
const auto isActiveBuffer = buffer.IsActiveBuffer();
74+
buffer.IncrementCircularBuffer(buffer.GetCurrentAttributes());
7875

79-
// ConPTY translates scrolling into newlines. We don't want that during cooked reads (= "cmd.exe prompts")
80-
// because the entire prompt is supposed to fit into the VT viewport, with no scrollback. If we didn't do that,
81-
// any prompt larger than the viewport will cause >1 lines to be added to scrollback, even if typing backspaces.
82-
// You can test this by removing this branch, launch Windows Terminal, and fill the entire viewport with text in cmd.exe.
83-
if (needsConPTYWorkaround)
76+
if (buffer.IsActiveBuffer())
8477
{
85-
buffer.SetAsActiveBuffer(false);
86-
buffer.IncrementCircularBuffer(buffer.GetCurrentAttributes());
87-
buffer.SetAsActiveBuffer(isActiveBuffer);
88-
89-
if (isActiveBuffer && renderer)
78+
if (const auto notifier = ServiceLocator::LocateAccessibilityNotifier())
9079
{
91-
renderer->TriggerRedrawAll();
80+
notifier->NotifyConsoleUpdateScrollEvent(0, -1);
9281
}
93-
}
94-
else
95-
{
96-
buffer.IncrementCircularBuffer(buffer.GetCurrentAttributes());
97-
98-
if (isActiveBuffer)
82+
if (const auto renderer = ServiceLocator::LocateGlobals().pRender)
9983
{
100-
if (const auto notifier = ServiceLocator::LocateAccessibilityNotifier())
101-
{
102-
notifier->NotifyConsoleUpdateScrollEvent(0, -1);
103-
}
104-
if (renderer)
105-
{
106-
static constexpr til::point delta{ 0, -1 };
107-
renderer->TriggerScroll(&delta);
108-
}
84+
static constexpr til::point delta{ 0, -1 };
85+
renderer->TriggerScroll(&delta);
10986
}
11087
}
11188

@@ -128,15 +105,11 @@ static void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point
128105
LOG_IF_FAILED(screenInfo.SetViewportOrigin(false, WindowOrigin, true));
129106
}
130107

131-
if (interactive)
132-
{
133-
screenInfo.MakeCursorVisible(coordCursor);
134-
}
135-
LOG_IF_FAILED(screenInfo.SetCursorPosition(coordCursor, interactive));
108+
LOG_IF_FAILED(screenInfo.SetCursorPosition(coordCursor, false));
136109
}
137110

138111
// As the name implies, this writes text without processing its control characters.
139-
static void _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const std::wstring_view& text, const bool interactive, til::CoordType* psScrollY)
112+
void _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const std::wstring_view& text, til::CoordType* psScrollY)
140113
{
141114
const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT);
142115
const auto hasAccessibilityEventing = screenInfo.HasAccessibilityEventing();
@@ -165,18 +138,19 @@ static void _writeCharsLegacyUnprocessed(SCREEN_INFORMATION& screenInfo, const s
165138
screenInfo.NotifyAccessibilityEventing(state.columnBegin, cursorPosition.y, state.columnEnd - 1, cursorPosition.y);
166139
}
167140

168-
AdjustCursorPosition(screenInfo, cursorPosition, interactive, psScrollY);
141+
AdjustCursorPosition(screenInfo, cursorPosition, psScrollY);
169142
}
170143
}
171144

172145
// This routine writes a string to the screen while handling control characters.
173146
// `interactive` exists for COOKED_READ_DATA which uses it to transform control characters into visible text like "^X".
174147
// 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.
175-
void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& text, const bool interactive, til::CoordType* psScrollY)
148+
void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& text, til::CoordType* psScrollY)
176149
{
177150
static constexpr wchar_t tabSpaces[8]{ L' ', L' ', L' ', L' ', L' ', L' ', L' ', L' ' };
178151

179152
auto& textBuffer = screenInfo.GetTextBuffer();
153+
const auto width = textBuffer.GetSize().Width();
180154
auto& cursor = textBuffer.GetCursor();
181155
const auto wrapAtEOL = WI_IsFlagSet(screenInfo.OutputMode, ENABLE_WRAP_AT_EOL_OUTPUT);
182156
auto it = text.begin();
@@ -197,15 +171,15 @@ void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& t
197171
{
198172
pos.x = 0;
199173
pos.y++;
200-
AdjustCursorPosition(screenInfo, pos, interactive, psScrollY);
174+
AdjustCursorPosition(screenInfo, pos, psScrollY);
201175
}
202176
}
203177

204178
// If ENABLE_PROCESSED_OUTPUT is set we search for C0 control characters and handle them like backspace, tab, etc.
205-
// If it's not set, we can just straight up give everything to _writeCharsLegacyUnprocessed.
179+
// If it's not set, we can just straight up give everything to WriteCharsLegacyUnprocessed.
206180
if (WI_IsFlagClear(screenInfo.OutputMode, ENABLE_PROCESSED_OUTPUT))
207181
{
208-
_writeCharsLegacyUnprocessed(screenInfo, { it, end }, interactive, psScrollY);
182+
_writeCharsLegacyUnprocessed(screenInfo, { it, end }, psScrollY);
209183
it = end;
210184
}
211185

@@ -214,7 +188,7 @@ void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& t
214188
const auto nextControlChar = std::find_if(it, end, [](const auto& wch) { return !IS_GLYPH_CHAR(wch); });
215189
if (nextControlChar != it)
216190
{
217-
_writeCharsLegacyUnprocessed(screenInfo, { it, nextControlChar }, interactive, psScrollY);
191+
_writeCharsLegacyUnprocessed(screenInfo, { it, nextControlChar }, psScrollY);
218192
it = nextControlChar;
219193
}
220194

@@ -223,35 +197,24 @@ void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& t
223197
switch (*it)
224198
{
225199
case UNICODE_NULL:
226-
if (interactive)
227-
{
228-
break;
229-
}
230-
_writeCharsLegacyUnprocessed(screenInfo, { &tabSpaces[0], 1 }, interactive, psScrollY);
200+
_writeCharsLegacyUnprocessed(screenInfo, { &tabSpaces[0], 1 }, psScrollY);
231201
continue;
232202
case UNICODE_BELL:
233-
if (interactive)
234-
{
235-
break;
236-
}
237203
std::ignore = screenInfo.SendNotifyBeep();
238204
continue;
239205
case UNICODE_BACKSPACE:
240206
{
241-
// Backspace handling for interactive mode should happen in COOKED_READ_DATA
242-
// where it has full control over the text and can delete it directly.
243-
// Otherwise handling backspacing tabs/whitespace can turn up complex and bug-prone.
244-
assert(!interactive);
245207
auto pos = cursor.GetPosition();
246208
pos.x = textBuffer.GetRowByOffset(pos.y).NavigateToPrevious(pos.x);
247-
AdjustCursorPosition(screenInfo, pos, interactive, psScrollY);
209+
AdjustCursorPosition(screenInfo, pos, psScrollY);
248210
continue;
249211
}
250212
case UNICODE_TAB:
251213
{
252214
const auto pos = cursor.GetPosition();
253-
const auto tabCount = gsl::narrow_cast<size_t>(8 - (pos.x & 7));
254-
_writeCharsLegacyUnprocessed(screenInfo, { &tabSpaces[0], tabCount }, interactive, psScrollY);
215+
const auto remaining = width - pos.x;
216+
const auto tabCount = gsl::narrow_cast<size_t>(std::min(remaining, 8 - (pos.x & 7)));
217+
_writeCharsLegacyUnprocessed(screenInfo, { &tabSpaces[0], tabCount }, psScrollY);
255218
continue;
256219
}
257220
case UNICODE_LINEFEED:
@@ -264,38 +227,29 @@ void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& t
264227

265228
textBuffer.GetMutableRowByOffset(pos.y).SetWrapForced(false);
266229
pos.y = pos.y + 1;
267-
AdjustCursorPosition(screenInfo, pos, interactive, psScrollY);
230+
AdjustCursorPosition(screenInfo, pos, psScrollY);
268231
continue;
269232
}
270233
case UNICODE_CARRIAGERETURN:
271234
{
272235
auto pos = cursor.GetPosition();
273236
pos.x = 0;
274-
AdjustCursorPosition(screenInfo, pos, interactive, psScrollY);
237+
AdjustCursorPosition(screenInfo, pos, psScrollY);
275238
continue;
276239
}
277240
default:
278241
break;
279242
}
280243

281-
// In the interactive mode we replace C0 control characters (0x00-0x1f) with ASCII representations like ^C (= 0x03).
282-
if (interactive && *it < L' ')
244+
// As a special favor to incompetent apps that attempt to display control chars,
245+
// convert to corresponding OEM Glyph Chars
246+
const auto cp = ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP;
247+
const auto ch = gsl::narrow_cast<char>(*it);
248+
wchar_t wch = 0;
249+
const auto result = MultiByteToWideChar(cp, MB_USEGLYPHCHARS, &ch, 1, &wch, 1);
250+
if (result == 1)
283251
{
284-
const wchar_t wchs[2]{ L'^', static_cast<wchar_t>(*it + L'@') };
285-
_writeCharsLegacyUnprocessed(screenInfo, { &wchs[0], 2 }, interactive, psScrollY);
286-
}
287-
else
288-
{
289-
// As a special favor to incompetent apps that attempt to display control chars,
290-
// convert to corresponding OEM Glyph Chars
291-
const auto cp = ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP;
292-
const auto ch = gsl::narrow_cast<char>(*it);
293-
wchar_t wch = 0;
294-
const auto result = MultiByteToWideChar(cp, MB_USEGLYPHCHARS, &ch, 1, &wch, 1);
295-
if (result == 1)
296-
{
297-
_writeCharsLegacyUnprocessed(screenInfo, { &wch, 1 }, interactive, psScrollY);
298-
}
252+
_writeCharsLegacyUnprocessed(screenInfo, { &wch, 1 }, psScrollY);
299253
}
300254
}
301255
}
@@ -358,7 +312,7 @@ try
358312

359313
if (WI_IsAnyFlagClear(screenInfo.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT))
360314
{
361-
WriteCharsLegacy(screenInfo, str, false, nullptr);
315+
WriteCharsLegacy(screenInfo, str, nullptr);
362316
}
363317
else
364318
{

src/host/_stream.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Revision History:
1919

2020
#include "writeData.hpp"
2121

22-
void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& str, bool interactive, til::CoordType* psScrollY);
22+
void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& str, til::CoordType* psScrollY);
2323

2424
// NOTE: console lock must be held when calling this routine
2525
// String has been translated to unicode at this point.

src/host/ft_fuzzer/fuzzmain.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,6 @@ extern "C" __declspec(dllexport) int LLVMFuzzerTestOneInput(const uint8_t* data,
132132
til::CoordType scrollY{};
133133
gci.LockConsole();
134134
auto u = wil::scope_exit([&]() { gci.UnlockConsole(); });
135-
WriteCharsLegacy(gci.GetActiveOutputBuffer(), u16String, true, &scrollY);
135+
WriteCharsLegacy(gci.GetActiveOutputBuffer(), u16String, &scrollY);
136136
return 0;
137137
}

0 commit comments

Comments
 (0)