Compare commits

...

8 Commits

Author SHA1 Message Date
Leonard Hecker
d165136852 Fix macro parser 2026-02-16 17:42:59 +01:00
Leonard Hecker
c77b79fb8d Fix passthrough logic 2026-02-13 22:54:35 +01:00
Leonard Hecker
28fe5d81d7 Merge remote-tracking branch 'origin/main' into dev/lhecker/dcs-perf 2026-02-13 21:52:01 +01:00
Leonard Hecker
318328d475 Fix ESC detection, Reduce code size 2026-01-23 15:06:59 +01:00
Leonard Hecker
605ba75cea Remove IsProcessingLastCharacter 2026-01-20 16:37:55 +01:00
Leonard Hecker
7ad3277136 Merge remote-tracking branch 'origin/main' into dev/lhecker/dcs-perf 2026-01-20 16:04:42 +01:00
Leonard Hecker
e51769b072 Fix parsing, tests 2025-12-11 15:00:07 +01:00
Leonard Hecker
c83fbbb89e Optimize DCS performance with batch processing 2025-12-10 16:52:49 +01:00
12 changed files with 572 additions and 498 deletions

View File

@@ -23,7 +23,7 @@ namespace Microsoft::Console::VirtualTerminal
class Microsoft::Console::VirtualTerminal::ITermDispatch class Microsoft::Console::VirtualTerminal::ITermDispatch
{ {
public: public:
using StringHandler = std::function<bool(const wchar_t)>; using StringHandler = std::function<bool(std::wstring_view)>;
enum class OptionalFeature enum class OptionalFeature
{ {

View File

@@ -113,92 +113,96 @@ bool MacroBuffer::InitParser(const size_t macroId, const DispatchTypes::MacroDel
return false; return false;
} }
bool MacroBuffer::ParseDefinition(const wchar_t ch) bool MacroBuffer::ParseDefinition(const std::wstring_view str)
{ {
// Once we receive an ESC, that marks the end of the definition, but if for (const auto ch : str)
// an unterminated repeat is still pending, we should apply that now.
if (ch == AsciiChars::ESC)
{ {
if (_repeatPending && !_applyPendingRepeat()) // Once we receive an ESC, that marks the end of the definition, but if
// an unterminated repeat is still pending, we should apply that now.
if (ch == AsciiChars::ESC)
{
if (_repeatPending && !_applyPendingRepeat())
{
_deleteMacro(_activeMacro());
}
return false;
}
// Any other control characters are just ignored.
if (ch < L' ')
{
continue;
}
// For "text encoded" macros, we'll always be in the ExpectingText state.
// For "hex encoded" macros, we'll typically be alternating between the
// ExpectingHexDigit and ExpectingSecondHexDigit states as we parse the two
// digits of each hex pair. But we also need to deal with repeat sequences,
// which start with `!`, followed by a numeric repeat count, and then a
// range of hex pairs between two `;` characters. When parsing the repeat
// count, we use the ExpectingRepeatCount state, but when parsing the hex
// pairs of the repeat, we just use the regular ExpectingHexDigit states.
auto success = true;
switch (_parseState)
{
case State::ExpectingText:
success = _appendToActiveMacro(ch);
break;
case State::ExpectingHexDigit:
if (_decodeHexDigit(ch))
{
_parseState = State::ExpectingSecondHexDigit;
}
else if (ch == L'!' && !_repeatPending)
{
_parseState = State::ExpectingRepeatCount;
_repeatCount = 0;
}
else if (ch == L';' && _repeatPending)
{
success = _applyPendingRepeat();
}
else
{
success = false;
}
break;
case State::ExpectingSecondHexDigit:
success = _decodeHexDigit(ch) && _appendToActiveMacro(_decodedChar);
_decodedChar = 0;
_parseState = State::ExpectingHexDigit;
break;
case State::ExpectingRepeatCount:
if (ch >= L'0' && ch <= L'9')
{
_repeatCount = _repeatCount * 10 + (ch - L'0');
_repeatCount = std::min<size_t>(_repeatCount, MAX_PARAMETER_VALUE);
}
else if (ch == L';')
{
_repeatPending = true;
_repeatStart = _activeMacro().length();
_parseState = State::ExpectingHexDigit;
}
else
{
success = false;
}
break;
default:
success = false;
break;
}
// If there is an error in the definition, clear everything received so far.
if (!success)
{ {
_deleteMacro(_activeMacro()); _deleteMacro(_activeMacro());
return false;
} }
return false;
} }
return true;
// Any other control characters are just ignored.
if (ch < L' ')
{
return true;
}
// For "text encoded" macros, we'll always be in the ExpectingText state.
// For "hex encoded" macros, we'll typically be alternating between the
// ExpectingHexDigit and ExpectingSecondHexDigit states as we parse the two
// digits of each hex pair. But we also need to deal with repeat sequences,
// which start with `!`, followed by a numeric repeat count, and then a
// range of hex pairs between two `;` characters. When parsing the repeat
// count, we use the ExpectingRepeatCount state, but when parsing the hex
// pairs of the repeat, we just use the regular ExpectingHexDigit states.
auto success = true;
switch (_parseState)
{
case State::ExpectingText:
success = _appendToActiveMacro(ch);
break;
case State::ExpectingHexDigit:
if (_decodeHexDigit(ch))
{
_parseState = State::ExpectingSecondHexDigit;
}
else if (ch == L'!' && !_repeatPending)
{
_parseState = State::ExpectingRepeatCount;
_repeatCount = 0;
}
else if (ch == L';' && _repeatPending)
{
success = _applyPendingRepeat();
}
else
{
success = false;
}
break;
case State::ExpectingSecondHexDigit:
success = _decodeHexDigit(ch) && _appendToActiveMacro(_decodedChar);
_decodedChar = 0;
_parseState = State::ExpectingHexDigit;
break;
case State::ExpectingRepeatCount:
if (ch >= L'0' && ch <= L'9')
{
_repeatCount = _repeatCount * 10 + (ch - L'0');
_repeatCount = std::min<size_t>(_repeatCount, MAX_PARAMETER_VALUE);
}
else if (ch == L';')
{
_repeatPending = true;
_repeatStart = _activeMacro().length();
_parseState = State::ExpectingHexDigit;
}
else
{
success = false;
}
break;
default:
success = false;
break;
}
// If there is an error in the definition, clear everything received so far.
if (!success)
{
_deleteMacro(_activeMacro());
}
return success;
} }
bool MacroBuffer::_decodeHexDigit(const wchar_t ch) noexcept bool MacroBuffer::_decodeHexDigit(const wchar_t ch) noexcept

View File

@@ -44,7 +44,7 @@ namespace Microsoft::Console::VirtualTerminal
void InvokeMacro(const size_t macroId, StateMachine& stateMachine); void InvokeMacro(const size_t macroId, StateMachine& stateMachine);
void ClearMacrosIfInUse(); void ClearMacrosIfInUse();
bool InitParser(const size_t macroId, const DispatchTypes::MacroDeleteControl deleteControl, const DispatchTypes::MacroEncoding encoding); bool InitParser(const size_t macroId, const DispatchTypes::MacroDeleteControl deleteControl, const DispatchTypes::MacroEncoding encoding);
bool ParseDefinition(const wchar_t ch); bool ParseDefinition(std::wstring_view str);
private: private:
bool _decodeHexDigit(const wchar_t ch) noexcept; bool _decodeHexDigit(const wchar_t ch) noexcept;

View File

@@ -42,9 +42,8 @@ size_t SixelParser::MaxColorsForLevel(const VTInt conformanceLevel) noexcept
} }
} }
SixelParser::SixelParser(AdaptDispatch& dispatcher, const StateMachine& stateMachine, const VTInt conformanceLevel) noexcept : SixelParser::SixelParser(AdaptDispatch& dispatcher, const VTInt conformanceLevel) noexcept :
_dispatcher{ dispatcher }, _dispatcher{ dispatcher },
_stateMachine{ stateMachine },
_conformanceLevel{ conformanceLevel }, _conformanceLevel{ conformanceLevel },
_cellSize{ CellSizeForLevel(conformanceLevel) }, _cellSize{ CellSizeForLevel(conformanceLevel) },
_maxColors{ MaxColorsForLevel(conformanceLevel) } _maxColors{ MaxColorsForLevel(conformanceLevel) }
@@ -80,7 +79,7 @@ void SixelParser::SetDisplayMode(const bool enabled) noexcept
} }
} }
std::function<bool(wchar_t)> SixelParser::DefineImage(const VTInt macroParameter, const DispatchTypes::SixelBackground backgroundSelect, const VTParameter backgroundColor) std::function<bool(std::wstring_view)> SixelParser::DefineImage(const VTInt macroParameter, const DispatchTypes::SixelBackground backgroundSelect, const VTParameter backgroundColor)
{ {
if (_initTextBufferBoundaries()) if (_initTextBufferBoundaries())
{ {
@@ -89,8 +88,17 @@ std::function<bool(wchar_t)> SixelParser::DefineImage(const VTInt macroParameter
_initImageBuffer(); _initImageBuffer();
_state = States::Normal; _state = States::Normal;
_parameters.clear(); _parameters.clear();
return [&](const auto ch) { return [&](const std::wstring_view str) {
_parseCommandChar(ch); auto it = str.begin();
const auto end = str.end();
while (it != end)
{
const auto ch = *it++;
_isProcessingLastCharacter = it == end;
_parseCommandChar(ch);
}
return true; return true;
}; };
} }
@@ -554,7 +562,7 @@ void SixelParser::_defineColor(const size_t colorNumber, const COLORREF color)
// If some image content has already been defined at this point, and // If some image content has already been defined at this point, and
// we're processing the last character in the packet, this is likely an // we're processing the last character in the packet, this is likely an
// attempt to animate the palette, so we should flush the image. // attempt to animate the palette, so we should flush the image.
if (_imageWidth > 0 && _stateMachine.IsProcessingLastCharacter()) if (_imageWidth > 0 && _isProcessingLastCharacter)
{ {
_maybeFlushImageBuffer(); _maybeFlushImageBuffer();
} }
@@ -876,7 +884,7 @@ void SixelParser::_maybeFlushImageBuffer(const bool endOfSequence)
const auto currentTime = steady_clock::now(); const auto currentTime = steady_clock::now();
const auto timeSinceLastFlush = duration_cast<milliseconds>(currentTime - _lastFlushTime); const auto timeSinceLastFlush = duration_cast<milliseconds>(currentTime - _lastFlushTime);
const auto linesSinceLastFlush = _imageLineCount - _lastFlushLine; const auto linesSinceLastFlush = _imageLineCount - _lastFlushLine;
if (endOfSequence || timeSinceLastFlush > 500ms || (linesSinceLastFlush <= 1 && _stateMachine.IsProcessingLastCharacter())) if (endOfSequence || timeSinceLastFlush > 500ms || (linesSinceLastFlush <= 1 && _isProcessingLastCharacter))
{ {
_lastFlushTime = currentTime; _lastFlushTime = currentTime;
_lastFlushLine = _imageLineCount; _lastFlushLine = _imageLineCount;

View File

@@ -21,7 +21,6 @@ namespace Microsoft::Console::VirtualTerminal
{ {
class AdaptDispatch; class AdaptDispatch;
class Page; class Page;
class StateMachine;
class SixelParser class SixelParser
{ {
@@ -31,10 +30,10 @@ namespace Microsoft::Console::VirtualTerminal
static til::size CellSizeForLevel(const VTInt conformanceLevel = DefaultConformance) noexcept; static til::size CellSizeForLevel(const VTInt conformanceLevel = DefaultConformance) noexcept;
static size_t MaxColorsForLevel(const VTInt conformanceLevel = DefaultConformance) noexcept; static size_t MaxColorsForLevel(const VTInt conformanceLevel = DefaultConformance) noexcept;
SixelParser(AdaptDispatch& dispatcher, const StateMachine& stateMachine, const VTInt conformanceLevel = DefaultConformance) noexcept; SixelParser(AdaptDispatch& dispatcher, const VTInt conformanceLevel = DefaultConformance) noexcept;
void SoftReset(); void SoftReset();
void SetDisplayMode(const bool enabled) noexcept; void SetDisplayMode(const bool enabled) noexcept;
std::function<bool(wchar_t)> DefineImage(const VTInt macroParameter, const DispatchTypes::SixelBackground backgroundSelect, const VTParameter backgroundColor); std::function<bool(std::wstring_view)> DefineImage(const VTInt macroParameter, const DispatchTypes::SixelBackground backgroundSelect, const VTParameter backgroundColor);
private: private:
// NB: If we want to support more than 256 colors, we'll also need to // NB: If we want to support more than 256 colors, we'll also need to
@@ -49,7 +48,6 @@ namespace Microsoft::Console::VirtualTerminal
}; };
AdaptDispatch& _dispatcher; AdaptDispatch& _dispatcher;
const StateMachine& _stateMachine;
const VTInt _conformanceLevel; const VTInt _conformanceLevel;
void _parseCommandChar(const wchar_t ch); void _parseCommandChar(const wchar_t ch);
@@ -106,6 +104,7 @@ namespace Microsoft::Console::VirtualTerminal
size_t _colorsAvailable = 0; size_t _colorsAvailable = 0;
IndexedPixel _foregroundPixel = {}; IndexedPixel _foregroundPixel = {};
bool _colorTableChanged = false; bool _colorTableChanged = false;
bool _isProcessingLastCharacter = false;
void _initImageBuffer(); void _initImageBuffer();
void _resizeImageBuffer(const til::CoordType requiredHeight); void _resizeImageBuffer(const til::CoordType requiredHeight);

View File

@@ -3787,7 +3787,7 @@ ITermDispatch::StringHandler AdaptDispatch::DefineSixelImage(const VTInt macroPa
// The sixel parser is created on demand. // The sixel parser is created on demand.
if (!_sixelParser) if (!_sixelParser)
{ {
_sixelParser = std::make_unique<SixelParser>(*this, _api.GetStateMachine()); _sixelParser = std::make_unique<SixelParser>(*this);
_sixelParser->SetDisplayMode(_modes.test(Mode::SixelDisplay)); _sixelParser->SetDisplayMode(_modes.test(Mode::SixelDisplay));
} }
return _sixelParser->DefineImage(macroParameter, backgroundSelect, backgroundColor); return _sixelParser->DefineImage(macroParameter, backgroundSelect, backgroundColor);
@@ -3836,34 +3836,37 @@ ITermDispatch::StringHandler AdaptDispatch::DownloadDRCS(const VTInt fontNumber,
return nullptr; return nullptr;
} }
return [=](const auto ch) { return [=](const std::wstring_view str) {
// We pass the data string straight through to the font buffer class for (const auto ch : str)
// until we receive an ESC, indicating the end of the string. At that
// point we can finalize the buffer, and if valid, update the renderer
// with the constructed bit pattern.
if (ch != AsciiChars::ESC)
{ {
_fontBuffer->AddSixelData(ch); // We pass the data string straight through to the font buffer class
} // until we receive an ESC, indicating the end of the string. At that
else if (_fontBuffer->FinalizeSixelData()) // point we can finalize the buffer, and if valid, update the renderer
{ // with the constructed bit pattern.
// We also need to inform the character set mapper of the ID that if (ch != AsciiChars::ESC)
// will map to this font (we only support one font buffer so there
// will only ever be one active dynamic character set).
if (charsetSize == DispatchTypes::CharsetSize::Size96)
{ {
_termOutput.SetDrcs96Designation(_fontBuffer->GetDesignation()); _fontBuffer->AddSixelData(ch);
} }
else else if (_fontBuffer->FinalizeSixelData())
{ {
_termOutput.SetDrcs94Designation(_fontBuffer->GetDesignation()); // We also need to inform the character set mapper of the ID that
} // will map to this font (we only support one font buffer so there
if (_renderer) // will only ever be one active dynamic character set).
{ if (charsetSize == DispatchTypes::CharsetSize::Size96)
const auto bitPattern = _fontBuffer->GetBitPattern(); {
const auto cellSize = _fontBuffer->GetCellSize(); _termOutput.SetDrcs96Designation(_fontBuffer->GetDesignation());
const auto centeringHint = _fontBuffer->GetTextCenteringHint(); }
_renderer->UpdateSoftFont(bitPattern, cellSize, centeringHint); else
{
_termOutput.SetDrcs94Designation(_fontBuffer->GetDesignation());
}
if (_renderer)
{
const auto bitPattern = _fontBuffer->GetBitPattern();
const auto cellSize = _fontBuffer->GetCellSize();
const auto centeringHint = _fontBuffer->GetTextCenteringHint();
_renderer->UpdateSoftFont(bitPattern, cellSize, centeringHint);
}
} }
} }
return true; return true;
@@ -3889,24 +3892,27 @@ void AdaptDispatch::RequestUserPreferenceCharset()
// - a function to parse the character set ID // - a function to parse the character set ID
ITermDispatch::StringHandler AdaptDispatch::AssignUserPreferenceCharset(const DispatchTypes::CharsetSize charsetSize) ITermDispatch::StringHandler AdaptDispatch::AssignUserPreferenceCharset(const DispatchTypes::CharsetSize charsetSize)
{ {
return [this, charsetSize, idBuilder = VTIDBuilder{}](const auto ch) mutable { return [this, charsetSize, idBuilder = VTIDBuilder{}](const std::wstring_view str) mutable {
if (ch >= L'\x20' && ch <= L'\x2f') for (const auto ch : str)
{ {
idBuilder.AddIntermediate(ch); if (ch >= L'\x20' && ch <= L'\x2f')
}
else if (ch >= L'\x30' && ch <= L'\x7e')
{
const auto id = idBuilder.Finalize(ch);
switch (charsetSize)
{ {
case DispatchTypes::CharsetSize::Size94: idBuilder.AddIntermediate(ch);
_termOutput.AssignUserPreferenceCharset(id, false); }
break; else if (ch >= L'\x30' && ch <= L'\x7e')
case DispatchTypes::CharsetSize::Size96: {
_termOutput.AssignUserPreferenceCharset(id, true); const auto id = idBuilder.Finalize(ch);
break; switch (charsetSize)
{
case DispatchTypes::CharsetSize::Size94:
_termOutput.AssignUserPreferenceCharset(id, false);
break;
case DispatchTypes::CharsetSize::Size96:
_termOutput.AssignUserPreferenceCharset(id, true);
break;
}
return false;
} }
return false;
} }
return true; return true;
}; };
@@ -3932,8 +3938,8 @@ ITermDispatch::StringHandler AdaptDispatch::DefineMacro(const VTInt macroId,
if (_macroBuffer->InitParser(macroId, deleteControl, encoding)) if (_macroBuffer->InitParser(macroId, deleteControl, encoding))
{ {
return [&](const auto ch) { return [&](const std::wstring_view str) {
return _macroBuffer->ParseDefinition(ch); return _macroBuffer->ParseDefinition(str);
}; };
} }
@@ -4053,45 +4059,48 @@ void AdaptDispatch::_ReportColorTable(const DispatchTypes::ColorModel colorModel
// - a function to parse the report data. // - a function to parse the report data.
ITermDispatch::StringHandler AdaptDispatch::_RestoreColorTable() ITermDispatch::StringHandler AdaptDispatch::_RestoreColorTable()
{ {
return [this, parameter = VTInt{}, parameters = std::vector<VTParameter>{}](const auto ch) mutable { return [this, parameter = VTInt{}, parameters = std::vector<VTParameter>{}](const std::wstring_view str) mutable {
if (ch >= L'0' && ch <= L'9') for (const auto ch : str)
{ {
parameter *= 10; if (ch >= L'0' && ch <= L'9')
parameter += (ch - L'0'); {
parameter = std::min(parameter, MAX_PARAMETER_VALUE); parameter *= 10;
} parameter += (ch - L'0');
else if (ch == L';') parameter = std::min(parameter, MAX_PARAMETER_VALUE);
{ }
if (parameters.size() < 5) else if (ch == L';')
{
if (parameters.size() < 5)
{
parameters.push_back(parameter);
}
parameter = 0;
}
else if (ch == L'/' || ch == AsciiChars::ESC)
{ {
parameters.push_back(parameter); parameters.push_back(parameter);
} const auto colorParameters = VTParameters{ parameters.data(), parameters.size() };
parameter = 0; const auto colorNumber = colorParameters.at(0).value_or(0);
} if (colorNumber < TextColor::TABLE_SIZE)
else if (ch == L'/' || ch == AsciiChars::ESC)
{
parameters.push_back(parameter);
const auto colorParameters = VTParameters{ parameters.data(), parameters.size() };
const auto colorNumber = colorParameters.at(0).value_or(0);
if (colorNumber < TextColor::TABLE_SIZE)
{
const auto colorModel = DispatchTypes::ColorModel{ colorParameters.at(1) };
const auto x = colorParameters.at(2).value_or(0);
const auto y = colorParameters.at(3).value_or(0);
const auto z = colorParameters.at(4).value_or(0);
if (colorModel == DispatchTypes::ColorModel::HLS)
{ {
SetColorTableEntry(colorNumber, Utils::ColorFromHLS(x, y, z)); const auto colorModel = DispatchTypes::ColorModel{ colorParameters.at(1) };
} const auto x = colorParameters.at(2).value_or(0);
else if (colorModel == DispatchTypes::ColorModel::RGB) const auto y = colorParameters.at(3).value_or(0);
{ const auto z = colorParameters.at(4).value_or(0);
SetColorTableEntry(colorNumber, Utils::ColorFromRGB100(x, y, z)); if (colorModel == DispatchTypes::ColorModel::HLS)
{
SetColorTableEntry(colorNumber, Utils::ColorFromHLS(x, y, z));
}
else if (colorModel == DispatchTypes::ColorModel::RGB)
{
SetColorTableEntry(colorNumber, Utils::ColorFromRGB100(x, y, z));
}
} }
parameters.clear();
parameter = 0;
} }
parameters.clear();
parameter = 0;
} }
return (ch != AsciiChars::ESC); return true;
}; };
} }
@@ -4111,42 +4120,43 @@ ITermDispatch::StringHandler AdaptDispatch::RequestSetting()
// this is the opposite of what is documented in most DEC manuals, which // this is the opposite of what is documented in most DEC manuals, which
// say that 0 is for a valid response, and 1 is for an error. The correct // say that 0 is for a valid response, and 1 is for an error. The correct
// interpretation is documented in the DEC STD 070 reference. // interpretation is documented in the DEC STD 070 reference.
return [this, parameter = VTInt{}, idBuilder = VTIDBuilder{}](const auto ch) mutable { return [this, parameter = VTInt{}, idBuilder = VTIDBuilder{}](const std::wstring_view str) mutable {
const auto isFinal = ch >= L'\x40' && ch <= L'\x7e'; for (const auto ch : str)
if (isFinal)
{ {
const auto id = idBuilder.Finalize(ch); const auto isFinal = ch >= L'\x40' && ch <= L'\x7e';
switch (id) if (isFinal)
{ {
case VTID("m"): const auto id = idBuilder.Finalize(ch);
_ReportSGRSetting(); switch (id)
break; {
case VTID("r"): case VTID("m"):
_ReportDECSTBMSetting(); _ReportSGRSetting();
break; break;
case VTID("s"): case VTID("r"):
_ReportDECSLRMSetting(); _ReportDECSTBMSetting();
break; break;
case VTID(" q"): case VTID("s"):
_ReportDECSCUSRSetting(); _ReportDECSLRMSetting();
break; break;
case VTID("\"q"): case VTID(" q"):
_ReportDECSCASetting(); _ReportDECSCUSRSetting();
break; break;
case VTID("*x"): case VTID("\"q"):
_ReportDECSACESetting(); _ReportDECSCASetting();
break; break;
case VTID(",|"): case VTID("*x"):
_ReportDECACSetting(VTParameter{ parameter }); _ReportDECSACESetting();
break; break;
default: case VTID(",|"):
_ReturnDcsResponse(L"0$r"); _ReportDECACSetting(VTParameter{ parameter });
break; break;
default:
_ReturnDcsResponse(L"0$r");
break;
}
return false;
} }
return false;
}
else
{
// Although we don't yet support any operations with parameter // Although we don't yet support any operations with parameter
// prefixes, it's important that we still parse the prefix and // prefixes, it's important that we still parse the prefix and
// include it in the ID. Otherwise, we'll mistakenly respond to // include it in the ID. Otherwise, we'll mistakenly respond to
@@ -4164,8 +4174,8 @@ ITermDispatch::StringHandler AdaptDispatch::RequestSetting()
parameter += (ch - L'0'); parameter += (ch - L'0');
parameter = std::min(parameter, MAX_PARAMETER_VALUE); parameter = std::min(parameter, MAX_PARAMETER_VALUE);
} }
return true;
} }
return true;
}; };
} }
@@ -4513,126 +4523,129 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation()
VTParameter row{}; VTParameter row{};
VTParameter column{}; VTParameter column{};
}; };
return [&, state = State{}](const auto ch) mutable { return [&, state = State{}](const std::wstring_view str) mutable {
if (numeric.test(state.field)) for (const auto ch : str)
{ {
if (ch >= '0' && ch <= '9') if (numeric.test(state.field))
{ {
state.value *= 10; if (ch >= '0' && ch <= '9')
state.value += (ch - L'0'); {
state.value = std::min(state.value, MAX_PARAMETER_VALUE); state.value *= 10;
state.value += (ch - L'0');
state.value = std::min(state.value, MAX_PARAMETER_VALUE);
}
else if (ch == L';' || ch == AsciiChars::ESC)
{
if (state.field == Field::Row)
{
state.row = state.value;
}
else if (state.field == Field::Column)
{
state.column = state.value;
}
else if (state.field == Field::Page)
{
PagePositionAbsolute(state.value);
}
else if (state.field == Field::GL && state.value <= 3)
{
LockingShift(state.value);
}
else if (state.field == Field::GR && state.value <= 3)
{
LockingShiftRight(state.value);
}
state.value = {};
state.field = static_cast<Field>(state.field + 1);
}
} }
else if (ch == L';' || ch == AsciiChars::ESC) else if (flags.test(state.field))
{ {
if (state.field == Field::Row) // Note that there could potentially be multiple characters in a
// flag field, so we process the flags as soon as they're received.
// But for now we're only interested in the first one, so once the
// state.value is set, we ignore everything else until the `;`.
if (ch >= L'@' && ch <= '~' && !state.value)
{ {
state.row = state.value; state.value = ch;
} if (state.field == Field::SGR)
else if (state.field == Field::Column)
{
state.column = state.value;
}
else if (state.field == Field::Page)
{
PagePositionAbsolute(state.value);
}
else if (state.field == Field::GL && state.value <= 3)
{
LockingShift(state.value);
}
else if (state.field == Field::GR && state.value <= 3)
{
LockingShiftRight(state.value);
}
state.value = {};
state.field = static_cast<Field>(state.field + 1);
}
}
else if (flags.test(state.field))
{
// Note that there could potentially be multiple characters in a
// flag field, so we process the flags as soon as they're received.
// But for now we're only interested in the first one, so once the
// state.value is set, we ignore everything else until the `;`.
if (ch >= L'@' && ch <= '~' && !state.value)
{
state.value = ch;
if (state.field == Field::SGR)
{
const auto page = _pages.ActivePage();
auto attr = page.Attributes();
attr.SetIntense(state.value & 1);
attr.SetUnderlineStyle(state.value & 2 ? UnderlineStyle::SinglyUnderlined : UnderlineStyle::NoUnderline);
attr.SetBlinking(state.value & 4);
attr.SetReverseVideo(state.value & 8);
attr.SetInvisible(state.value & 16);
page.SetAttributes(attr);
}
else if (state.field == Field::Attr)
{
const auto page = _pages.ActivePage();
auto attr = page.Attributes();
attr.SetProtected(state.value & 1);
page.SetAttributes(attr);
}
else if (state.field == Field::Sizes)
{
state.charset96.at(0) = state.value & 1;
state.charset96.at(1) = state.value & 2;
state.charset96.at(2) = state.value & 4;
state.charset96.at(3) = state.value & 8;
}
else if (state.field == Field::Flags)
{
const bool originMode = state.value & 1;
const bool ss2 = state.value & 2;
const bool ss3 = state.value & 4;
const bool delayedEOLWrap = state.value & 8;
// The cursor position is parsed at the start of the sequence,
// but we only set the position once we know the origin mode.
_modes.set(Mode::Origin, originMode);
CursorPosition(state.row, state.column);
// There can only be one single shift applied at a time, so
// we'll just apply the last one that is enabled.
_termOutput.SingleShift(ss3 ? 3 : (ss2 ? 2 : 0));
// The EOL flag will always be reset by the cursor movement
// above, so we only need to worry about setting it.
if (delayedEOLWrap)
{ {
const auto page = _pages.ActivePage(); const auto page = _pages.ActivePage();
page.Cursor().DelayEOLWrap(); auto attr = page.Attributes();
attr.SetIntense(state.value & 1);
attr.SetUnderlineStyle(state.value & 2 ? UnderlineStyle::SinglyUnderlined : UnderlineStyle::NoUnderline);
attr.SetBlinking(state.value & 4);
attr.SetReverseVideo(state.value & 8);
attr.SetInvisible(state.value & 16);
page.SetAttributes(attr);
}
else if (state.field == Field::Attr)
{
const auto page = _pages.ActivePage();
auto attr = page.Attributes();
attr.SetProtected(state.value & 1);
page.SetAttributes(attr);
}
else if (state.field == Field::Sizes)
{
state.charset96.at(0) = state.value & 1;
state.charset96.at(1) = state.value & 2;
state.charset96.at(2) = state.value & 4;
state.charset96.at(3) = state.value & 8;
}
else if (state.field == Field::Flags)
{
const bool originMode = state.value & 1;
const bool ss2 = state.value & 2;
const bool ss3 = state.value & 4;
const bool delayedEOLWrap = state.value & 8;
// The cursor position is parsed at the start of the sequence,
// but we only set the position once we know the origin mode.
_modes.set(Mode::Origin, originMode);
CursorPosition(state.row, state.column);
// There can only be one single shift applied at a time, so
// we'll just apply the last one that is enabled.
_termOutput.SingleShift(ss3 ? 3 : (ss2 ? 2 : 0));
// The EOL flag will always be reset by the cursor movement
// above, so we only need to worry about setting it.
if (delayedEOLWrap)
{
const auto page = _pages.ActivePage();
page.Cursor().DelayEOLWrap();
}
} }
} }
else if (ch == L';')
{
state.value = 0;
state.field = static_cast<Field>(state.field + 1);
}
} }
else if (ch == L';') else if (charset.test(state.field))
{ {
state.value = 0; if (ch >= L' ' && ch <= L'/')
state.field = static_cast<Field>(state.field + 1); {
state.charsetId.AddIntermediate(ch);
}
else if (ch >= L'0' && ch <= L'~')
{
const auto id = state.charsetId.Finalize(ch);
const auto gset = state.field - Field::G0;
if (state.charset96.at(gset))
{
Designate96Charset(gset, id);
}
else
{
Designate94Charset(gset, id);
}
state.charsetId.Clear();
state.field = static_cast<Field>(state.field + 1);
}
} }
} }
else if (charset.test(state.field)) return true;
{
if (ch >= L' ' && ch <= L'/')
{
state.charsetId.AddIntermediate(ch);
}
else if (ch >= L'0' && ch <= L'~')
{
const auto id = state.charsetId.Finalize(ch);
const auto gset = state.field - Field::G0;
if (state.charset96.at(gset))
{
Designate96Charset(gset, id);
}
else
{
Designate94Charset(gset, id);
}
state.charsetId.Clear();
state.field = static_cast<Field>(state.field + 1);
}
}
return (ch != AsciiChars::ESC);
}; };
} }
@@ -4685,30 +4698,33 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreTabStops()
_ClearAllTabStops(); _ClearAllTabStops();
_InitTabStopsForWidth(width); _InitTabStopsForWidth(width);
return [this, width, column = size_t{}](const auto ch) mutable { return [this, width, column = size_t{}](const std::wstring_view str) mutable {
if (ch >= L'0' && ch <= L'9') for (const auto ch : str)
{ {
column *= 10; if (ch >= L'0' && ch <= L'9')
column += (ch - L'0');
column = std::min<size_t>(column, MAX_PARAMETER_VALUE);
}
else if (ch == L'/' || ch == AsciiChars::ESC)
{
// Note that column 1 is always a tab stop, so there is no
// need to record an entry at that offset.
if (column > 1u && column <= static_cast<size_t>(width))
{ {
_tabStopColumns.at(column - 1) = true; column *= 10;
column += (ch - L'0');
column = std::min<size_t>(column, MAX_PARAMETER_VALUE);
}
else if (ch == L'/' || ch == AsciiChars::ESC)
{
// Note that column 1 is always a tab stop, so there is no
// need to record an entry at that offset.
if (column > 1u && column <= static_cast<size_t>(width))
{
_tabStopColumns.at(column - 1) = true;
}
column = 0;
}
else
{
// If we receive an unexpected character, we don't try and
// process any more of the input - we just abort.
return false;
} }
column = 0;
} }
else return true;
{
// If we receive an unexpected character, we don't try and
// process any more of the input - we just abort.
return false;
}
return (ch != AsciiChars::ESC);
}; };
} }

View File

@@ -1847,11 +1847,8 @@ public:
{ {
const auto requestSetting = [=](const std::wstring_view settingId = {}) { const auto requestSetting = [=](const std::wstring_view settingId = {}) {
const auto stringHandler = _pDispatch->RequestSetting(); const auto stringHandler = _pDispatch->RequestSetting();
for (auto ch : settingId) stringHandler(settingId);
{ stringHandler(L"\033"); // String terminator
stringHandler(ch);
}
stringHandler(L'\033'); // String terminator
}; };
Log::Comment(L"Requesting DECSTBM margins (5 to 10)."); Log::Comment(L"Requesting DECSTBM margins (5 to 10).");
@@ -3568,11 +3565,8 @@ public:
{ {
const auto assignCharset = [=](const auto charsetSize, const std::wstring_view charsetId = {}) { const auto assignCharset = [=](const auto charsetSize, const std::wstring_view charsetId = {}) {
const auto stringHandler = _pDispatch->AssignUserPreferenceCharset(charsetSize); const auto stringHandler = _pDispatch->AssignUserPreferenceCharset(charsetSize);
for (auto ch : charsetId) stringHandler(charsetId);
{ stringHandler(L"\033"); // String terminator
stringHandler(ch);
}
stringHandler(L'\033'); // String terminator
}; };
auto& termOutput = _pDispatch->_termOutput; auto& termOutput = _pDispatch->_termOutput;
termOutput.SoftReset(); termOutput.SoftReset();

View File

@@ -20,7 +20,7 @@ namespace Microsoft::Console::VirtualTerminal
class IStateMachineEngine class IStateMachineEngine
{ {
public: public:
using StringHandler = std::function<bool(const wchar_t)>; using StringHandler = std::function<bool(std::wstring_view)>;
virtual ~IStateMachineEngine() = 0; virtual ~IStateMachineEngine() = 0;
IStateMachineEngine(const IStateMachineEngine&) = default; IStateMachineEngine(const IStateMachineEngine&) = default;

View File

@@ -13,17 +13,7 @@ using namespace Microsoft::Console::VirtualTerminal;
//Takes ownership of the pEngine. //Takes ownership of the pEngine.
StateMachine::StateMachine(std::unique_ptr<IStateMachineEngine> engine, const bool isEngineForInput) noexcept : StateMachine::StateMachine(std::unique_ptr<IStateMachineEngine> engine, const bool isEngineForInput) noexcept :
_engine(std::move(engine)), _engine(std::move(engine)),
_isEngineForInput(isEngineForInput), _isEngineForInput(isEngineForInput)
_state(VTStates::Ground),
_trace(Microsoft::Console::VirtualTerminal::ParserTracing()),
_parameters{},
_subParameters{},
_subParameterRanges{},
_parameterLimitOverflowed(false),
_subParameterLimitOverflowed(false),
_subParameterCounter(0),
_oscString{},
_cachedSequence{ std::nullopt }
{ {
// The state machine must always accept C1 controls for the input engine, // The state machine must always accept C1 controls for the input engine,
// otherwise it won't work when the ConPTY terminal has S8C1T enabled. // otherwise it won't work when the ConPTY terminal has S8C1T enabled.
@@ -324,18 +314,6 @@ static constexpr bool _isDcsIndicator(const wchar_t wch) noexcept
return wch == L'P'; // 0x50 return wch == L'P'; // 0x50
} }
// Routine Description:
// - Determines if a character is valid for a DCS pass through sequence.
// Arguments:
// - wch - Character to check.
// Return Value:
// - True if it is. False if it isn't.
static constexpr bool _isDcsPassThroughValid(const wchar_t wch) noexcept
{
// 0x20 - 0x7E
return wch >= AsciiChars::SPC && wch < AsciiChars::DEL;
}
// Routine Description: // Routine Description:
// - Determines if a character is "start of string" beginning // - Determines if a character is "start of string" beginning
// indicator. // indicator.
@@ -655,7 +633,7 @@ void StateMachine::_ActionInterrupt()
if (_state == VTStates::DcsPassThrough) if (_state == VTStates::DcsPassThrough)
{ {
// The ESC signals the end of the data string. // The ESC signals the end of the data string.
_dcsStringHandler(AsciiChars::ESC); _dcsStringHandler(L"\x1b");
_dcsStringHandler = nullptr; _dcsStringHandler = nullptr;
} }
} }
@@ -1799,20 +1777,70 @@ void StateMachine::_EventDcsParam(const wchar_t wch)
// - wch - Character that triggered the event // - wch - Character that triggered the event
// Return Value: // Return Value:
// - <none> // - <none>
void StateMachine::_EventDcsPassThrough(const wchar_t wch) void StateMachine::_EventDcsPassThrough(const wchar_t)
{ {
static constexpr auto isDcsIgnore = [](const wchar_t wch) {
return wch == AsciiChars::DEL;
};
static constexpr auto isDcsTerminator = [](const wchar_t wch) {
return wch == AsciiChars::CAN ||
wch == AsciiChars::SUB ||
wch == AsciiChars::ESC ||
(wch >= 0x80 && wch <= 0x9F);
};
static constexpr auto isDcsActionable = [](const wchar_t wch) {
return wch == AsciiChars::CAN ||
wch == AsciiChars::SUB ||
wch == AsciiChars::ESC ||
wch == AsciiChars::DEL ||
(wch >= 0x80 && wch <= 0x9F);
};
_trace.TraceOnEvent(L"DcsPassThrough"); _trace.TraceOnEvent(L"DcsPassThrough");
if (_isC0Code(wch) || _isDcsPassThroughValid(wch))
// Assuming other functions are correctly implemented,
// we're only called when we have data to process.
assert(_runEnd != 0 && _runEnd <= _currentString.size());
const auto end = _currentString.end();
auto runEnd = _currentString.begin() + (_runEnd - 1);
for (;;)
{ {
if (!_dcsStringHandler(wch)) const auto runBeg = runEnd;
// Process a chunk of non-actionable, passthrough characters
// and exit if we're out of input.
for (; runEnd != end && !isDcsActionable(*runEnd); ++runEnd)
{
}
if (runBeg != runEnd && !_dcsStringHandler({ runBeg, runEnd }))
{ {
_EnterDcsIgnore(); _EnterDcsIgnore();
break;
}
if (runEnd == end)
{
break;
}
// Ignore DEL characters.
for (; runEnd != end && isDcsIgnore(*runEnd); ++runEnd)
{
}
if (runEnd == end)
{
break;
}
// Are we looking at a terminator?
if (isDcsTerminator(*runEnd))
{
break;
} }
} }
else
{ _runEnd = runEnd - _currentString.begin();
_ActionIgnore();
}
} }
// Routine Description: // Routine Description:
@@ -1973,144 +2001,169 @@ bool StateMachine::FlushToTerminal()
// - string - Characters to operate upon // - string - Characters to operate upon
// Return Value: // Return Value:
// - <none> // - <none>
#pragma warning(suppress : 26438) // Avoid 'goto'(es .76).
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).)
void StateMachine::ProcessString(const std::wstring_view string) void StateMachine::ProcessString(const std::wstring_view string)
{ {
size_t i = 0;
_currentString = string; _currentString = string;
_runOffset = 0; _runBeg = 0;
_runSize = 0; _runEnd = 0;
_injections.clear(); _injections.clear();
if (_state != VTStates::Ground) if (_state != VTStates::Ground)
{ {
// Jump straight to where we need to. // Jump straight to where we need to.
#pragma warning(suppress : 26438) // Avoid 'goto'(es .76). #pragma warning(suppress : 26438) // Avoid 'goto' (es.76).
goto processStringLoopVtStart; goto processStringLoopVtStart;
} }
while (i < string.size()) for (;;)
{ {
// Process as much plain text as possible.
{ {
// Pointer arithmetic is perfectly fine for our hot path. // We're starting a new "chunk".
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).) // --> Reset beg(in).
const auto beg = string.data() + i; _runBeg = _runEnd;
const auto len = string.size() - i;
// Ran out of text. Return.
if (_runBeg >= _currentString.size())
{
return;
}
#pragma warning(suppress : 26481) // Don't use pointer arithmetic. Use span instead (bounds.1).
const auto beg = _currentString.data() + _runBeg;
const auto len = _currentString.size() - _runBeg;
const auto it = Microsoft::Console::Utils::FindActionableControlCharacter(beg, len); const auto it = Microsoft::Console::Utils::FindActionableControlCharacter(beg, len);
_runOffset = i; if (it != beg)
_runSize = it - beg;
if (_runSize)
{ {
_runEnd = it - _currentString.data();
_ActionPrintString(_CurrentRun()); _ActionPrintString(_CurrentRun());
_runBeg = _runEnd;
i += _runSize;
_runOffset = i;
_runSize = 0;
} }
} }
// Next, process VT sequences (plural!).
processStringLoopVtStart: processStringLoopVtStart:
if (i >= string.size()) // Ran out of text. Return.
if (_runBeg >= _currentString.size())
{ {
break; return;
} }
for (;;)
do
{ {
_runSize++; // The inner loop deals with 1 VT sequence at a time.
_processingLastCharacter = i + 1 >= string.size(); for (;;)
// If we're processing characters individually, send it to the state machine.
ProcessCharacter(til::at(string, i));
++i;
} while (i < string.size() && _state != VTStates::Ground);
}
// If we're at the end of the string and have remaining un-printed characters,
if (_state != VTStates::Ground)
{
const auto run = _CurrentRun();
auto cacheUnusedRun = true;
// One of the "weird things" in VT input is the case of something like
// <kbd>alt+[</kbd>. In VT, that's encoded as `\x1b[`. However, that's
// also the start of a CSI, and could be the start of a longer sequence,
// there's no way to know for sure. For an <kbd>alt+[</kbd> keypress,
// the parser originally would just sit in the `CsiEntry` state after
// processing it, which would pollute the following keypress (e.g.
// <kbd>alt+[</kbd>, <kbd>A</kbd> would be processed like `\x1b[A`,
// which is _wrong_).
//
// At the same time, input may be broken up arbitrarily, depending on the pipe's
// buffer size, our read-buffer size, the sender's write-buffer size, and more.
// In fact, with the current WSL, input is broken up in 16 byte chunks (Why? :(),
// which breaks up many of our longer sequences, like our Win32InputMode ones.
//
// As a heuristic, this code specifically checks for a trailing Esc or Alt+key.
// If we encountered a win32-input-mode sequence before, we know that our \x1b[?9001h
// request to enable them was successful. While a client may still send \x1b{some char}
// intentionally, it's far more likely now that we're looking at a broken up sequence.
// The most common win32-input-mode is ConPTY itself after all, and we never emit
// \x1b{some char} once it's enabled.
if (_isEngineForInput)
{
const auto win32 = _engine->EncounteredWin32InputModeSequence();
if (!win32 && run.size() <= 2 && run.front() == L'\x1b')
{ {
_EnterGround(); const auto ch = til::at(_currentString, _runEnd);
if (run.size() == 1) ++_runEnd;
{ ProcessCharacter(ch);
_ActionExecute(L'\x1b');
}
else
{
_EnterEscape();
_ActionEscDispatch(run.back());
}
_EnterGround();
// No need to cache the run, since we've dealt with it now.
cacheUnusedRun = false;
}
}
else if (_state == VTStates::SosPmApcString || _state == VTStates::DcsPassThrough || _state == VTStates::DcsIgnore)
{
// There is no need to cache the run if we've reached one of the
// string processing states in the output engine, since that data
// will be dealt with as soon as it is received.
cacheUnusedRun = false;
}
// If the run hasn't been dealt with in one of the cases above, we cache if (_state == VTStates::Ground)
// the partial sequence in case we have to flush the whole thing later. {
if (cacheUnusedRun) break;
{ }
if (!_cachedSequence)
{ // We're not back in ground state, but ran out of text.
_cachedSequence.emplace(std::wstring{}); // --> Incomplete sequence. Clean up at the end.
if (_runEnd >= _currentString.size())
{
_processStringIncompleteSequence();
return;
}
} }
auto& cachedSequence = *_cachedSequence; // The only way we get here is if the above loop ran into the ground state.
cachedSequence.append(run); // This means we're starting a new "chunk".
// --> Reset beg(in).
_runBeg = _runEnd;
// Ran out of text. Return.
if (_runBeg >= _currentString.size())
{
return;
}
// Plain text? Go back to the start, where we process plain text.
if (!Utils::IsActionableFromGround(til::at(_currentString, _runEnd)))
{
break;
}
} }
} }
} }
// Routine Description: void Microsoft::Console::VirtualTerminal::StateMachine::_processStringIncompleteSequence()
// - Determines whether the character being processed is the last in the
// current output fragment, or there are more still to come. Other parts
// of the framework can use this information to work more efficiently.
// Arguments:
// - <none>
// Return Value:
// - True if we're processing the last character. False if not.
bool StateMachine::IsProcessingLastCharacter() const noexcept
{ {
return _processingLastCharacter; const auto run = _CurrentRun();
auto cacheUnusedRun = true;
// One of the "weird things" in VT input is the case of something like
// <kbd>alt+[</kbd>. In VT, that's encoded as `\x1b[`. However, that's
// also the start of a CSI, and could be the start of a longer sequence,
// there's no way to know for sure. For an <kbd>alt+[</kbd> keypress,
// the parser originally would just sit in the `CsiEntry` state after
// processing it, which would pollute the following keypress (e.g.
// <kbd>alt+[</kbd>, <kbd>A</kbd> would be processed like `\x1b[A`,
// which is _wrong_).
//
// At the same time, input may be broken up arbitrarily, depending on the pipe's
// buffer size, our read-buffer size, the sender's write-buffer size, and more.
// In fact, with the current WSL, input is broken up in 16 byte chunks (Why? :(),
// which breaks up many of our longer sequences, like our Win32InputMode ones.
//
// As a heuristic, this code specifically checks for a trailing Esc or Alt+key.
// If we encountered a win32-input-mode sequence before, we know that our \x1b[?9001h
// request to enable them was successful. While a client may still send \x1b{some char}
// intentionally, it's far more likely now that we're looking at a broken up sequence.
// The most common win32-input-mode is ConPTY itself after all, and we never emit
// \x1b{some char} once it's enabled.
if (_isEngineForInput)
{
const auto win32 = _engine->EncounteredWin32InputModeSequence();
if (!win32 && run.size() <= 2 && run.front() == L'\x1b')
{
_EnterGround();
if (run.size() == 1)
{
_ActionExecute(L'\x1b');
}
else
{
_EnterEscape();
_ActionEscDispatch(run.back());
}
_EnterGround();
// No need to cache the run, since we've dealt with it now.
cacheUnusedRun = false;
}
}
else if (_state == VTStates::SosPmApcString || _state == VTStates::DcsPassThrough || _state == VTStates::DcsIgnore)
{
// There is no need to cache the run if we've reached one of the
// string processing states in the output engine, since that data
// will be dealt with as soon as it is received.
cacheUnusedRun = false;
}
// If the run hasn't been dealt with in one of the cases above, we cache
// the partial sequence in case we have to flush the whole thing later.
if (cacheUnusedRun)
{
if (!_cachedSequence)
{
_cachedSequence.emplace(std::wstring{});
}
auto& cachedSequence = *_cachedSequence;
cachedSequence.append(run);
}
} }
void StateMachine::InjectSequence(const InjectionType type) void StateMachine::InjectSequence(const InjectionType type)
{ {
_injections.emplace_back(type, _runOffset + _runSize); _injections.emplace_back(type, _runEnd);
} }
const til::small_vector<Injection, 8>& StateMachine::GetInjections() const noexcept const til::small_vector<Injection, 8>& StateMachine::GetInjections() const noexcept
@@ -2191,8 +2244,8 @@ void StateMachine::_ExecuteCsiCompleteCallback()
// We need to save the state of the string that we're currently // We need to save the state of the string that we're currently
// processing in case the callback injects another string. // processing in case the callback injects another string.
const auto savedCurrentString = _currentString; const auto savedCurrentString = _currentString;
const auto savedRunOffset = _runOffset; const auto savedRunBeg = _runBeg;
const auto savedRunSize = _runSize; const auto savedRunEnd = _runEnd;
// We also need to take ownership of the callback function before // We also need to take ownership of the callback function before
// executing it so there's no risk of it being run more than once. // executing it so there's no risk of it being run more than once.
const auto callback = std::move(_onCsiCompleteCallback); const auto callback = std::move(_onCsiCompleteCallback);
@@ -2200,7 +2253,16 @@ void StateMachine::_ExecuteCsiCompleteCallback()
// Once the callback has returned, we can restore the original state // Once the callback has returned, we can restore the original state
// and continue where we left off. // and continue where we left off.
_currentString = savedCurrentString; _currentString = savedCurrentString;
_runOffset = savedRunOffset; _runBeg = savedRunBeg;
_runSize = savedRunSize; _runEnd = savedRunEnd;
} }
} }
// Construct current run.
//
// Note: We intentionally use this method to create the run lazily for better performance.
// You may find the usage of offset & size unsafe, but under heavy load it shows noticeable performance benefit.
std::wstring_view Microsoft::Console::VirtualTerminal::StateMachine::_CurrentRun() const
{
return _currentString.substr(_runBeg, _runEnd - _runBeg);
}

View File

@@ -81,7 +81,6 @@ namespace Microsoft::Console::VirtualTerminal
void ProcessCharacter(const wchar_t wch); void ProcessCharacter(const wchar_t wch);
void ProcessString(const std::wstring_view string); void ProcessString(const std::wstring_view string);
bool IsProcessingLastCharacter() const noexcept;
void InjectSequence(InjectionType type); void InjectSequence(InjectionType type);
const til::small_vector<Injection, 8>& GetInjections() const noexcept; const til::small_vector<Injection, 8>& GetInjections() const noexcept;
@@ -94,6 +93,8 @@ namespace Microsoft::Console::VirtualTerminal
IStateMachineEngine& Engine() noexcept; IStateMachineEngine& Engine() noexcept;
private: private:
void _processStringIncompleteSequence();
void _ActionExecute(const wchar_t wch); void _ActionExecute(const wchar_t wch);
void _ActionExecuteFromEscape(const wchar_t wch); void _ActionExecuteFromEscape(const wchar_t wch);
void _ActionPrint(const wchar_t wch); void _ActionPrint(const wchar_t wch);
@@ -162,6 +163,7 @@ namespace Microsoft::Console::VirtualTerminal
bool _SafeExecute(TLambda&& lambda); bool _SafeExecute(TLambda&& lambda);
void _ExecuteCsiCompleteCallback(); void _ExecuteCsiCompleteCallback();
std::wstring_view _CurrentRun() const;
enum class VTStates enum class VTStates
{ {
@@ -192,43 +194,30 @@ namespace Microsoft::Console::VirtualTerminal
std::unique_ptr<IStateMachineEngine> _engine; std::unique_ptr<IStateMachineEngine> _engine;
const bool _isEngineForInput; const bool _isEngineForInput;
VTStates _state; VTStates _state = VTStates::Ground;
til::enumset<Mode> _parserMode{ Mode::Ansi }; til::enumset<Mode> _parserMode{ Mode::Ansi };
std::wstring_view _currentString; std::wstring_view _currentString;
size_t _runOffset; size_t _runBeg = 0;
size_t _runSize; size_t _runEnd = 0;
// Construct current run.
//
// Note: We intentionally use this method to create the run lazily for better performance.
// You may find the usage of offset & size unsafe, but under heavy load it shows noticeable performance benefit.
std::wstring_view _CurrentRun() const
{
return _currentString.substr(_runOffset, _runSize);
}
VTIDBuilder _identifier; VTIDBuilder _identifier;
std::vector<VTParameter> _parameters; std::vector<VTParameter> _parameters;
bool _parameterLimitOverflowed;
std::vector<VTParameter> _subParameters; std::vector<VTParameter> _subParameters;
std::vector<std::pair<BYTE /*range start*/, BYTE /*range end*/>> _subParameterRanges; std::vector<std::pair<BYTE /*range start*/, BYTE /*range end*/>> _subParameterRanges;
bool _subParameterLimitOverflowed; bool _parameterLimitOverflowed = false;
BYTE _subParameterCounter; bool _subParameterLimitOverflowed = false;
BYTE _subParameterCounter = 0;
std::wstring _oscString; std::wstring _oscString;
VTInt _oscParameter; VTInt _oscParameter = 0;
IStateMachineEngine::StringHandler _dcsStringHandler; IStateMachineEngine::StringHandler _dcsStringHandler;
std::optional<std::wstring> _cachedSequence; std::optional<std::wstring> _cachedSequence;
til::small_vector<Injection, 8> _injections; til::small_vector<Injection, 8> _injections;
// This is tracked per state machine instance so that separate calls to Process*
// can start and finish a sequence.
bool _processingLastCharacter;
std::function<void()> _onCsiCompleteCallback; std::function<void()> _onCsiCompleteCallback;
}; };
} }

View File

@@ -126,6 +126,7 @@ namespace Microsoft::Console::Utils
// testing easier. // testing easier.
std::wstring_view TrimPaste(std::wstring_view textView) noexcept; std::wstring_view TrimPaste(std::wstring_view textView) noexcept;
bool IsActionableFromGround(const wchar_t wch) noexcept;
const wchar_t* FindActionableControlCharacter(const wchar_t* beg, const size_t len) noexcept; const wchar_t* FindActionableControlCharacter(const wchar_t* beg, const size_t len) noexcept;
// Same deal, but in TerminalPage::_evaluatePathForCwd // Same deal, but in TerminalPage::_evaluatePathForCwd

View File

@@ -1139,7 +1139,8 @@ std::wstring_view Utils::TrimPaste(std::wstring_view textView) noexcept
#pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1). #pragma warning(disable : 26490) // Don't use reinterpret_cast (type.1).
// Returns true for C0 characters and C1 [single-character] CSI. // Returns true for C0 characters and C1 [single-character] CSI.
constexpr bool isActionableFromGround(const wchar_t wch) noexcept #pragma warning(suppress : 26497) // You can attempt to make 'Microsoft::Console::Utils::IsActionableFromGround' constexpr unless it contains any undefined behavior(f.4).
bool Utils::IsActionableFromGround(const wchar_t wch) noexcept
{ {
// This is equivalent to: // This is equivalent to:
// return (wch <= 0x1f) || (wch >= 0x7f && wch <= 0x9f); // return (wch <= 0x1f) || (wch >= 0x7f && wch <= 0x9f);
@@ -1230,7 +1231,7 @@ plainSearch:
#endif #endif
#pragma loop(no_vector) #pragma loop(no_vector)
for (const auto end = beg + len; it < end && !isActionableFromGround(*it); ++it) for (const auto end = beg + len; it < end && !IsActionableFromGround(*it); ++it)
{ {
} }