4c53c595e7
This PR adds support for the VT line rendition attributes, which allow for double-width and double-height line renditions. These renditions are enabled with the `DECDWL` (double-width line) and `DECDHL` (double-height line) escape sequences. Both reset to the default rendition with the `DECSWL` (single-width line) escape sequence. For now this functionality is only supported by the GDI renderer in conhost. There are a lot of changes, so this is just a general overview of the main areas affected. Previously it was safe to assume that the screen had a fixed width, at least for a given point in time. But now we need to deal with the possibility of different lines have different widths, so all the functions that are constrained by the right border (text wrapping, cursor movement operations, and sequences like `EL` and `ICH`) now need to lookup the width of the active line in order to behave correctly. Similarly it used to be safe to assume that buffer and screen coordinates were the same thing, but that is no longer true. Lots of places now need to translate back and forth between coordinate systems dependent on the line rendition. This includes clipboard handling, the conhost color selection and search, accessibility location tracking and screen reading, IME editor positioning, "snapping" the viewport, and of course all the rendering calculations. For the rendering itself, I've had to introduce a new `PrepareLineTransform` method that the render engines can use to setup the necessary transform matrix for a given line rendition. This is also now used to handle the horizontal viewport offset, since that could no longer be achieved just by changing the target coordinates (on a double width line, the viewport offset may be halfway through a character). I've also had to change the renderer's existing `InvalidateCursor` method to take a `SMALL_RECT` rather than a `COORD`, to allow for the cursor being a variable width. Technically this was already a problem, because the cursor could occupy two screen cells when over a double-width character, but now it can be anything between one and four screen cells (e.g. a double-width character on the double-width line). In terms of architectural changes, there is now a new `lineRendition` field in the `ROW` class that keeps track of the line rendition for each row, and several new methods in the `ROW` and `TextBuffer` classes for manipulating that state. This includes a few helper methods for handling the various issues discussed above, e.g. position clamping and translating between coordinate systems. ## Validation Steps Performed I've manually confirmed all the double-width and double-height tests in _Vttest_ are now working as expected, and the _VT100 Torture Test_ now renders correctly (at least the line rendition aspects). I've also got my own test scripts that check many of the line rendition boundary cases and have confirmed that those are now passing. I've manually tested as many areas of the conhost UI that I could think of, that might be affected by line rendition, including things like searching, selection, copying, and color highlighting. For accessibility, I've confirmed that the _Magnifier_ and _Narrator_ correctly handle double-width lines. And I've also tested the Japanese IME, which while not perfect, is at least useable. Closes #7865
289 lines
13 KiB
C++
289 lines
13 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include <precomp.h>
|
|
|
|
#include "telemetry.hpp"
|
|
|
|
#pragma warning(push)
|
|
#pragma warning(disable : 26494) // _Tlgdata uninitialized from TraceLoggingWrite
|
|
#pragma warning(disable : 26477) // Use nullptr instead of NULL or 0 from TraceLoggingWrite
|
|
#pragma warning(disable : 26485) // _Tlgdata, no array to pointer decay from TraceLoggingWrite
|
|
#pragma warning(disable : 26446) // Prefer gsl::at over unchecked subscript from TraceLoggingLevel
|
|
#pragma warning(disable : 26482) // Only index to arrays with constant expressions from TraceLoggingLevel
|
|
|
|
TRACELOGGING_DEFINE_PROVIDER(g_hConsoleVirtTermParserEventTraceProvider,
|
|
"Microsoft.Windows.Console.VirtualTerminal.Parser",
|
|
// {c9ba2a84-d3ca-5e19-2bd6-776a0910cb9d}
|
|
(0xc9ba2a84, 0xd3ca, 0x5e19, 0x2b, 0xd6, 0x77, 0x6a, 0x09, 0x10, 0xcb, 0x9d));
|
|
|
|
using namespace Microsoft::Console::VirtualTerminal;
|
|
|
|
#pragma warning(push)
|
|
// Disable 4351 so we can initialize the arrays to 0 without a warning.
|
|
#pragma warning(disable : 4351)
|
|
TermTelemetry::TermTelemetry() noexcept :
|
|
_uiTimesUsedCurrent(0),
|
|
_uiTimesFailedCurrent(0),
|
|
_uiTimesFailedOutsideRangeCurrent(0),
|
|
_uiTimesUsed(),
|
|
_uiTimesFailed(),
|
|
_uiTimesFailedOutsideRange(0),
|
|
_activityId(),
|
|
_fShouldWriteFinalLog(false)
|
|
{
|
|
TraceLoggingRegister(g_hConsoleVirtTermParserEventTraceProvider);
|
|
|
|
// Create a random activityId just in case it doesn't get set later in SetActivityId().
|
|
EventActivityIdControl(EVENT_ACTIVITY_CTRL_CREATE_ID, &_activityId);
|
|
}
|
|
#pragma warning(pop)
|
|
|
|
TermTelemetry::~TermTelemetry()
|
|
{
|
|
try
|
|
{
|
|
WriteFinalTraceLog();
|
|
TraceLoggingUnregister(g_hConsoleVirtTermParserEventTraceProvider);
|
|
}
|
|
CATCH_LOG()
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Logs the usage of a particular VT100 code.
|
|
//
|
|
// Arguments:
|
|
// - code - VT100 code.
|
|
// Return Value:
|
|
// - <none>
|
|
void TermTelemetry::Log(const Codes code) noexcept
|
|
{
|
|
// Initially we wanted to pass over a string (ex. "CUU") and use a dictionary data type to hold the counts.
|
|
// However we would have to search through the dictionary every time we called this method, so we decided
|
|
// to use an array which has very quick access times.
|
|
// The downside is we have to create an enum type, and then convert them to strings when we finally
|
|
// send out the telemetry, but the upside is we should have very good performance.
|
|
_uiTimesUsed[code]++;
|
|
_uiTimesUsedCurrent++;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Logs a particular VT100 escape code failed or was unsupported.
|
|
//
|
|
// Arguments:
|
|
// - code - VT100 code.
|
|
// Return Value:
|
|
// - <none>
|
|
void TermTelemetry::LogFailed(const wchar_t wch) noexcept
|
|
{
|
|
if (wch > CHAR_MAX)
|
|
{
|
|
_uiTimesFailedOutsideRange++;
|
|
_uiTimesFailedOutsideRangeCurrent++;
|
|
}
|
|
else
|
|
{
|
|
// Even though we pass over a wide character, we only care about the ASCII single byte character.
|
|
_uiTimesFailed[wch]++;
|
|
_uiTimesFailedCurrent++;
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Gets and resets the total count of codes used.
|
|
//
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - total number.
|
|
unsigned int TermTelemetry::GetAndResetTimesUsedCurrent() noexcept
|
|
{
|
|
const auto temp = _uiTimesUsedCurrent;
|
|
_uiTimesUsedCurrent = 0;
|
|
return temp;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Gets and resets the total count of codes failed.
|
|
//
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - total number.
|
|
unsigned int TermTelemetry::GetAndResetTimesFailedCurrent() noexcept
|
|
{
|
|
const auto temp = _uiTimesFailedCurrent;
|
|
_uiTimesFailedCurrent = 0;
|
|
return temp;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Gets and resets the total count of codes failed outside the valid range.
|
|
//
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - total number.
|
|
unsigned int TermTelemetry::GetAndResetTimesFailedOutsideRangeCurrent() noexcept
|
|
{
|
|
const auto temp = _uiTimesFailedOutsideRangeCurrent;
|
|
_uiTimesFailedOutsideRangeCurrent = 0;
|
|
return temp;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Lets us know whether we should write the final log. Typically set true when the console has been
|
|
// interacted with, to help reduce the amount of telemetry we're sending.
|
|
//
|
|
// Arguments:
|
|
// - writeLog - true if we should write the log.
|
|
// Return Value:
|
|
// - <none>
|
|
void TermTelemetry::SetShouldWriteFinalLog(const bool writeLog) noexcept
|
|
{
|
|
_fShouldWriteFinalLog = writeLog;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Sets the activity Id, so we can match our events with other providers (such as Microsoft.Windows.Console.Host).
|
|
//
|
|
// Arguments:
|
|
// - activityId - Pointer to Guid to set our activity Id to.
|
|
// Return Value:
|
|
// - <none>
|
|
void TermTelemetry::SetActivityId(const GUID* activityId) noexcept
|
|
{
|
|
_activityId = *activityId;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Writes the final log of all the telemetry collected. The primary reason to send back a final log instead
|
|
// of individual events is to reduce the amount of telemetry being sent and potentially overloading our servers.
|
|
//
|
|
// Arguments:
|
|
// - code - VT100 code.
|
|
// Return Value:
|
|
// - <none>
|
|
void TermTelemetry::WriteFinalTraceLog() const
|
|
{
|
|
if (_fShouldWriteFinalLog)
|
|
{
|
|
// Determine if we've logged any VT100 sequences at all.
|
|
bool fLoggedSequence = (_uiTimesFailedOutsideRange > 0);
|
|
|
|
if (!fLoggedSequence)
|
|
{
|
|
for (int n = 0; n < ARRAYSIZE(_uiTimesUsed); n++)
|
|
{
|
|
if (_uiTimesUsed[n] > 0)
|
|
{
|
|
fLoggedSequence = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!fLoggedSequence)
|
|
{
|
|
for (int n = 0; n < ARRAYSIZE(_uiTimesFailed); n++)
|
|
{
|
|
if (_uiTimesFailed[n] > 0)
|
|
{
|
|
fLoggedSequence = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only send telemetry if we've logged some VT100 sequences. This should help reduce the amount of unnecessary
|
|
// telemetry being sent.
|
|
if (fLoggedSequence)
|
|
{
|
|
// I could use the TraceLoggingUIntArray, but then we would have to know the order of the enums on the backend.
|
|
// So just log each enum count separately with its string representation which makes it more human readable.
|
|
// Set the related activity to NULL since we aren't using it.
|
|
TraceLoggingWriteActivity(g_hConsoleVirtTermParserEventTraceProvider,
|
|
"ControlCodesUsed",
|
|
&_activityId,
|
|
NULL,
|
|
TraceLoggingUInt32(_uiTimesUsed[CUU], "CUU"),
|
|
TraceLoggingUInt32(_uiTimesUsed[CUD], "CUD"),
|
|
TraceLoggingUInt32(_uiTimesUsed[CUF], "CUF"),
|
|
TraceLoggingUInt32(_uiTimesUsed[CUB], "CUB"),
|
|
TraceLoggingUInt32(_uiTimesUsed[CNL], "CNL"),
|
|
TraceLoggingUInt32(_uiTimesUsed[CPL], "CPL"),
|
|
TraceLoggingUInt32(_uiTimesUsed[CHA], "CHA"),
|
|
TraceLoggingUInt32(_uiTimesUsed[CUP], "CUP"),
|
|
TraceLoggingUInt32(_uiTimesUsed[ED], "ED"),
|
|
TraceLoggingUInt32(_uiTimesUsed[EL], "EL"),
|
|
TraceLoggingUInt32(_uiTimesUsed[SGR], "SGR"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECSC], "DECSC"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECRC], "DECRC"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECSET], "DECSET"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECRST], "DECRST"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECKPAM], "DECKPAM"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECKPNM], "DECKPNM"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DSR], "DSR"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DA], "DA"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DA2], "DA2"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DA3], "DA3"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECREQTPARM], "DECREQTPARM"),
|
|
TraceLoggingUInt32(_uiTimesUsed[VPA], "VPA"),
|
|
TraceLoggingUInt32(_uiTimesUsed[HPR], "HPR"),
|
|
TraceLoggingUInt32(_uiTimesUsed[VPR], "VPR"),
|
|
TraceLoggingUInt32(_uiTimesUsed[ICH], "ICH"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DCH], "DCH"),
|
|
TraceLoggingUInt32(_uiTimesUsed[IL], "IL"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DL], "DL"),
|
|
TraceLoggingUInt32(_uiTimesUsed[SU], "SU"),
|
|
TraceLoggingUInt32(_uiTimesUsed[SD], "SD"),
|
|
TraceLoggingUInt32(_uiTimesUsed[ANSISYSSC], "ANSISYSSC"),
|
|
TraceLoggingUInt32(_uiTimesUsed[ANSISYSRC], "ANSISYSRC"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECSTBM], "DECSTBM"),
|
|
TraceLoggingUInt32(_uiTimesUsed[NEL], "NEL"),
|
|
TraceLoggingUInt32(_uiTimesUsed[IND], "IND"),
|
|
TraceLoggingUInt32(_uiTimesUsed[RI], "RI"),
|
|
TraceLoggingUInt32(_uiTimesUsed[OSCWT], "OscWindowTitle"),
|
|
TraceLoggingUInt32(_uiTimesUsed[HTS], "HTS"),
|
|
TraceLoggingUInt32(_uiTimesUsed[CHT], "CHT"),
|
|
TraceLoggingUInt32(_uiTimesUsed[CBT], "CBT"),
|
|
TraceLoggingUInt32(_uiTimesUsed[TBC], "TBC"),
|
|
TraceLoggingUInt32(_uiTimesUsed[ECH], "ECH"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DesignateG0], "DesignateG0"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DesignateG1], "DesignateG1"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DesignateG2], "DesignateG2"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DesignateG3], "DesignateG3"),
|
|
TraceLoggingUInt32(_uiTimesUsed[LS2], "LS2"),
|
|
TraceLoggingUInt32(_uiTimesUsed[LS3], "LS3"),
|
|
TraceLoggingUInt32(_uiTimesUsed[LS1R], "LS1R"),
|
|
TraceLoggingUInt32(_uiTimesUsed[LS2R], "LS2R"),
|
|
TraceLoggingUInt32(_uiTimesUsed[LS3R], "LS3R"),
|
|
TraceLoggingUInt32(_uiTimesUsed[SS2], "SS2"),
|
|
TraceLoggingUInt32(_uiTimesUsed[SS3], "SS3"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DOCS], "DOCS"),
|
|
TraceLoggingUInt32(_uiTimesUsed[HVP], "HVP"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECSTR], "DECSTR"),
|
|
TraceLoggingUInt32(_uiTimesUsed[RIS], "RIS"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECSCUSR], "DECSCUSR"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DTTERM_WM], "DTTERM_WM"),
|
|
TraceLoggingUInt32(_uiTimesUsed[OSCCT], "OscColorTable"),
|
|
TraceLoggingUInt32(_uiTimesUsed[OSCSCC], "OscSetCursorColor"),
|
|
TraceLoggingUInt32(_uiTimesUsed[OSCRCC], "OscResetCursorColor"),
|
|
TraceLoggingUInt32(_uiTimesUsed[OSCFG], "OscForegroundColor"),
|
|
TraceLoggingUInt32(_uiTimesUsed[OSCBG], "OscBackgroundColor"),
|
|
TraceLoggingUInt32(_uiTimesUsed[OSCSCB], "OscSetClipboard"),
|
|
TraceLoggingUInt32(_uiTimesUsed[REP], "REP"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECSWL], "DECSWL"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECDWL], "DECDWL"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECDHL], "DECDHL"),
|
|
TraceLoggingUInt32(_uiTimesUsed[DECALN], "DECALN"),
|
|
TraceLoggingUInt32(_uiTimesUsed[XTPUSHSGR], "XTPUSHSGR"),
|
|
TraceLoggingUInt32(_uiTimesUsed[XTPOPSGR], "XTPOPSGR"),
|
|
TraceLoggingUInt32Array(_uiTimesFailed, ARRAYSIZE(_uiTimesFailed), "Failed"),
|
|
TraceLoggingUInt32(_uiTimesFailedOutsideRange, "FailedOutsideRange"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma warning(pop)
|