-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rewrite how marks are stored & add reflow #16937
Changes from 24 commits
80a0ffb
f88ee30
0bfe603
ed79c94
ec1eb90
ef86ff0
65f7b34
d0aaf82
0b39a61
3aa16d9
c28f8c0
c6e5934
e8aa067
73cd1b3
68f8994
f1ff258
bc5a638
5f0933e
f78dec3
6afd08f
ca50d96
ad3e599
43b64f4
43d6fe9
9f792fa
e573896
11d82a2
3cf6a3c
e0fa1c3
4da4d1f
5617282
351208b
dc21f31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/*++ | ||
Copyright (c) Microsoft Corporation | ||
Licensed under the MIT license. | ||
|
||
Module Name: | ||
- marks.hpp | ||
|
||
Abstract: | ||
- Definitions for types that are used for "scroll marks" and shell integration | ||
in the buffer. | ||
- Scroll marks are identified by the existence of "ScrollbarData" on a ROW in the buffer. | ||
- Shell integration will then also markup the buffer with special | ||
TextAttributes, to identify regions of text as the Prompt, the Command, the | ||
Output, etc. | ||
- MarkExtents are used to abstract away those regions of text, so a caller | ||
doesn't need to iterate over the buffer themselves. | ||
--*/ | ||
|
||
#pragma once | ||
|
||
enum class MarkCategory : uint8_t | ||
{ | ||
Default = 0, | ||
Error = 1, | ||
Warning = 2, | ||
Success = 3, | ||
Prompt = 4 | ||
}; | ||
|
||
// This is the data that's stored on each ROW, to suggest that there's something | ||
// interesting on this row to show in the scrollbar. Also used in conjunction | ||
// with shell integration - when a prompt is added through shell integration, | ||
// we'll also add a scrollbar mark as a quick "bookmark" to the start of that | ||
// command. | ||
struct ScrollbarData | ||
{ | ||
MarkCategory category{ MarkCategory::Default }; | ||
|
||
// Scrollbar marks may have been given a color, or not. | ||
std::optional<til::color> color; | ||
|
||
// Prompts without an exit code haven't had a matching FTCS CommandEnd | ||
// called yet. Any value other than 0 is an error. | ||
std::optional<uint32_t> exitCode; | ||
// Future consideration: stick the literal command as a string on here, if | ||
// we were given it with the 633;E sequence. | ||
}; | ||
|
||
// Helper struct for describing the bounds of a command and it's output, | ||
// * The Prompt is between the start & end | ||
// * The Command is between the end & commandEnd | ||
// * The Output is between the commandEnd & outputEnd | ||
// | ||
// These are not actually stored in the buffer. The buffer can produce them for | ||
// callers, to make reasoning about regions of the buffer easier. | ||
struct MarkExtents | ||
{ | ||
// Data from the row | ||
ScrollbarData data; | ||
|
||
til::point start; | ||
til::point end; // exclusive | ||
std::optional<til::point> commandEnd; | ||
std::optional<til::point> outputEnd; | ||
|
||
// MarkCategory category{ MarkCategory::Info }; | ||
// Other things we may want to think about in the future are listed in | ||
// GH#11000 | ||
|
||
bool HasCommand() const noexcept | ||
{ | ||
return commandEnd.has_value() && *commandEnd != end; | ||
} | ||
bool HasOutput() const noexcept | ||
{ | ||
return outputEnd.has_value() && *outputEnd != *commandEnd; | ||
} | ||
std::pair<til::point, til::point> GetExtent() const | ||
{ | ||
til::point realEnd{ til::coalesce_value(outputEnd, commandEnd, end) }; | ||
return std::make_pair(til::point{ start }, realEnd); | ||
} | ||
}; | ||
|
||
// Another helper, for when callers would like to know just about the data of | ||
// the scrollbar, but don't actually need all the extents of prompts. | ||
struct ScrollMark | ||
{ | ||
til::CoordType row{ 0 }; | ||
ScrollbarData data; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1181,3 +1181,36 @@ CharToColumnMapper ROW::_createCharToColumnMapper(ptrdiff_t offset) const noexce | |
const auto guessedColumn = gsl::narrow_cast<til::CoordType>(clamp(offset, 0, _columnCount)); | ||
return CharToColumnMapper{ _chars.data(), _charOffsets.data(), lastChar, guessedColumn }; | ||
} | ||
|
||
const std::optional<ScrollbarData>& ROW::GetScrollbarData() const noexcept | ||
{ | ||
return _promptData; | ||
} | ||
void ROW::SetScrollbarData(std::optional<ScrollbarData> data) noexcept | ||
{ | ||
_promptData = data; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The If I'm correct, then simply adding it to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (This is my only remaining important concern. The memory layout is IMO not optimal for perf, but we can noodle on that later.) |
||
} | ||
|
||
void ROW::StartPrompt() noexcept | ||
{ | ||
if (!_promptData.has_value()) | ||
{ | ||
_promptData = ScrollbarData{ | ||
.category = MarkCategory::Prompt, | ||
.color = std::nullopt, | ||
.exitCode = std::nullopt, | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI, and I'm writing this just because I think it's interesting, as I mentioned elsewhere, As Dustin said, an template<typename T>
struct optional {
union { T data }; // union keeps it uninitialized even if it has a constructor
bool initialized = false;
}; The problem with
takes the initial value by reference, This means that constructing a mov BYTE PTR [rcx], 4 ; .category = 4
mov BYTE PTR [rcx+8], 0 ; .color.initialized = false
mov BYTE PTR [rcx+16], 0 ; .exitCode.initialized = false However, constructing a ; $T1 == the current function stack
mov BYTE PTR $T1[rsp], 4 ; stack: .category = 4
mov BYTE PTR $T1[rsp+8], 0 ; stack: .color.initialized = false
movups xmm0, XMMWORD PTR $T1[rsp] ; stack -> register
mov BYTE PTR $T1[rsp+16], 0 ; stack: .exitCode.initialized = false
mov eax, DWORD PTR $T1[rsp+16] ; stack -> register
movups XMMWORD PTR [rcx], xmm0 ; register -> ROW (.category/.color)
mov DWORD PTR [rcx+16], eax ; register -> ROW (.exitCode)
mov BYTE PTR [rcx+20], 1 ; ._promptData.initialized = true In other words, MSVC constructs the However, fret not, because the C++ consortium has solved this problem
std::optional<ScrollbarData>{
std::in_place,
MarkCategory::Prompt,
std::nullopt,
std::nullopt,
}; Just kidding: mov BYTE PTR $T1[rsp], 4
mov BYTE PTR $T1[rsp+8], 0
movups xmm0, XMMWORD PTR $T1[rsp]
mov BYTE PTR $T1[rsp+16], 0
mov BYTE PTR $T1[rsp+20], 1
movsd xmm1, QWORD PTR $T1[rsp+16]
movups XMMWORD PTR [rcx], xmm0
movsd QWORD PTR [rcx+16], xmm1 This occurs because fixing the construction doesn't fix the existence of its move constructor. That one is still implemented in the STL and thus relies on the compiler optimizing it which no compiler does consistently correct. The actual proper fix is to use this: if (!_promptData.has_value())
{
_promptData.emplace(MarkCategory::Prompt);
} which results in this: cmp BYTE PTR [rcx+20], 0 ; _promptData.has_value()
jne SHORT $LN8@test ; if (!...) goto ret;
mov BYTE PTR [rcx], 4 ; .category = 4
mov BYTE PTR [rcx+8], 0 ; .color.initialized = false
mov BYTE PTR [rcx+16], 0 ; .exitCode.initialized = false
mov BYTE PTR [rcx+20], 1 ; ._promptData.initialized = true
$LN8@test:
ret 0 While using |
||
} | ||
} | ||
|
||
void ROW::EndOutput(std::optional<unsigned int> error) noexcept | ||
{ | ||
if (_promptData.has_value()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. gut check me - There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean, technically, yes. But I think the one caller of this ( |
||
{ | ||
_promptData->exitCode = error; | ||
if (error.has_value()) | ||
{ | ||
_promptData->category = *error == 0 ? MarkCategory::Success : MarkCategory::Error; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
#include "LineRendition.hpp" | ||
#include "OutputCell.hpp" | ||
#include "OutputCellIterator.hpp" | ||
#include "Marks.hpp" | ||
|
||
class ROW; | ||
class TextBuffer; | ||
|
@@ -167,6 +168,11 @@ class ROW final | |
auto AttrBegin() const noexcept { return _attr.begin(); } | ||
auto AttrEnd() const noexcept { return _attr.end(); } | ||
|
||
const std::optional<ScrollbarData>& GetScrollbarData() const noexcept; | ||
void SetScrollbarData(std::optional<ScrollbarData> data) noexcept; | ||
void StartPrompt() noexcept; | ||
void EndOutput(std::optional<unsigned int> error) noexcept; | ||
|
||
#ifdef UNIT_TESTING | ||
friend constexpr bool operator==(const ROW& a, const ROW& b) noexcept; | ||
friend class RowTests; | ||
|
@@ -299,6 +305,8 @@ class ROW final | |
bool _wrapForced = false; | ||
// Occurs when the user runs out of text to support a double byte character and we're forced to the next line | ||
bool _doubleBytePadded = false; | ||
|
||
std::optional<ScrollbarData> _promptData = std::nullopt; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lhecker this will grow ROW by 12+ bytes (considering padding for alignment after @zadjii-msft if you want to save the cost of this in conhost, you can use the preprocessor version of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something else we discussed: struct ScrollbarData
{
til::color color;
uint32_t exitCode = 0;
MarkCategory category{ MarkCategory::Default };
bool valid = false;
bool hasColor = false;
bool hasExitCode = false;
};
struct ROW {
// ...
ScrollbarData _promptData;
}; to avoid some overhead There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While I'd agree that The current
|
||
}; | ||
|
||
#ifdef UNIT_TESTING | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -38,6 +38,16 @@ enum class UnderlineStyle | |||||
Max = DashedUnderlined | ||||||
}; | ||||||
|
||||||
// We only need a few bits, but uint8_t is two bytes anyways, and apparently | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
fwiw since uint8_t is unequivocally one byte, we should explain why it's two ^ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm gonna leave it as a u16 unfortunately due to the |
||||||
// doesn't satisfy std::has_unique_object_representations_v | ||||||
enum class MarkKind : uint16_t | ||||||
{ | ||||||
None = 0, | ||||||
Prompt = 1, | ||||||
Command = 2, | ||||||
Output = 3, | ||||||
}; | ||||||
|
||||||
class TextAttribute final | ||||||
{ | ||||||
public: | ||||||
|
@@ -46,7 +56,8 @@ class TextAttribute final | |||||
_foreground{}, | ||||||
_background{}, | ||||||
_hyperlinkId{ 0 }, | ||||||
_underlineColor{} | ||||||
_underlineColor{}, | ||||||
_markKind{ MarkKind::None } | ||||||
{ | ||||||
} | ||||||
|
||||||
|
@@ -55,7 +66,8 @@ class TextAttribute final | |||||
_foreground{ gsl::at(s_legacyForegroundColorMap, wLegacyAttr & FG_ATTRS) }, | ||||||
_background{ gsl::at(s_legacyBackgroundColorMap, (wLegacyAttr & BG_ATTRS) >> 4) }, | ||||||
_hyperlinkId{ 0 }, | ||||||
_underlineColor{} | ||||||
_underlineColor{}, | ||||||
_markKind{ MarkKind::None } | ||||||
{ | ||||||
} | ||||||
|
||||||
|
@@ -66,7 +78,8 @@ class TextAttribute final | |||||
_foreground{ rgbForeground }, | ||||||
_background{ rgbBackground }, | ||||||
_hyperlinkId{ 0 }, | ||||||
_underlineColor{ rgbUnderline } | ||||||
_underlineColor{ rgbUnderline }, | ||||||
_markKind{ MarkKind::None } | ||||||
{ | ||||||
} | ||||||
|
||||||
|
@@ -75,7 +88,8 @@ class TextAttribute final | |||||
_foreground{ foreground }, | ||||||
_background{ background }, | ||||||
_hyperlinkId{ hyperlinkId }, | ||||||
_underlineColor{ underlineColor } | ||||||
_underlineColor{ underlineColor }, | ||||||
_markKind{ MarkKind::None } | ||||||
{ | ||||||
} | ||||||
|
||||||
|
@@ -135,6 +149,15 @@ class TextAttribute final | |||||
return _attrs; | ||||||
} | ||||||
|
||||||
constexpr void SetMarkAttributes(const MarkKind attrs) noexcept | ||||||
{ | ||||||
_markKind = attrs; | ||||||
} | ||||||
constexpr MarkKind GetMarkAttributes() const noexcept | ||||||
{ | ||||||
return _markKind; | ||||||
} | ||||||
|
||||||
bool IsHyperlink() const noexcept; | ||||||
|
||||||
TextColor GetForeground() const noexcept; | ||||||
|
@@ -202,6 +225,7 @@ class TextAttribute final | |||||
TextColor _foreground; // sizeof: 4, alignof: 1 | ||||||
TextColor _background; // sizeof: 4, alignof: 1 | ||||||
TextColor _underlineColor; // sizeof: 4, alignof: 1 | ||||||
MarkKind _markKind; // sizeof: 2, cause I guess a uint8_t is 2B? | ||||||
zadjii-msft marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
#ifdef UNIT_TESTING | ||||||
friend class TextBufferTests; | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: is this necessary?
start
is already the right type