Skip to content

Commit c8b645c

Browse files
Implement EnableColorSelection
As described in microsoft#9583, this change implements the legacy conhost "EnableColorSelection" feature. @zadjii-msft was super nice and provided the outline/plumbing (WinRT classes and such) as a hackathon-type project (thank you!)--a "SelectionColor" runtimeclass, a ColorSelection method on the ControlCore runtimeclass, associated plumbing through the layers; plus the action-and-args plumbing to allow hooking up a basic "ColorSelection" action, which allows you to put actions in your settings JSON like so: ```json { "command": { "action": "experimental.colorSelection", "foreground": "#0f3" }, "keys": "alt+4" }, ``` On top of that foundation, I added a couple of things: * The ability to specify indexes for colors, in addition to RGB and RRGGBB colors. - It's a bit hacky, because there are some conversions that fight against sneaking an "I'm an index" flag in the alpha channel. * A new "matchMode" parameter on the action, allowing you to say if you want to only color the current selection ("0") or all matches ("1"). - I made it an int, because I'd like to enable at least one other "match mode" later, but it requires me/someone to fix up search.cpp to handle regex first. - Search used an old UIA "ColorSelection" method which was previously `E_NOTIMPL`, but is now wired up. Don't know what/if anything else uses this. * An uber-toggle setting, "EnableColorSelection", which allows you to set a single `bool` in your settings JSON, to light up all the keybindings you would expect from the legacy "EnableColorSelection" feature: - alt+[0..9]: color foreground - alt+shift+[0..9]: color foreground, all matches - ctrl+[0..9]: color background - ctrl+shift+[0..9]: color background, all matches * A few of the actions cannot be properly invoked via their keybindings, due to microsoft#13124. `*!*` But they work if you do them from the command palette. * If you have "`EnableColorSelection : true`" in your settings JSON, but then specify a different action in your JSON that uses the same key binding as a color selection keybinding, your custom one wins, which I think is the right thing. * I fixed what I think was a bug in search.cpp, which also affected the legacy EnableColorSelection feature: due to a non-inclusive coordinate comparison, you were not allowed to color a single character; but I see no reason why that should be disallowed. Now you can make all your `X`s red if you like. "Soft" spots: * I was a bit surprised at some of the helpers I had to provide in textBuffer.cpp. Perhaps there are existing methods that I didn't find? * Localization? Because there are so many (40!) actions, I went to some trouble to try to provide nice command/arg descriptions. But I don't know how localization works…
1 parent bbc570d commit c8b645c

27 files changed

+1057
-98
lines changed

.github/actions/spelling/allow/apis.txt

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ istream
8686
IStringable
8787
ITab
8888
ITaskbar
89+
itow
8990
IUri
9091
IVirtual
9192
KEYSELECT

src/buffer/out/LineRendition.hpp

+7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ constexpr til::inclusive_rect ScreenToBufferLine(const til::inclusive_rect& line
2828
return { line.Left >> scale, line.Top, line.Right >> scale, line.Bottom };
2929
}
3030

31+
constexpr til::point ScreenToBufferLine(const til::point& line, const LineRendition lineRendition)
32+
{
33+
// Use shift right to quickly divide the Left and Right by 2 for double width lines.
34+
const auto scale = lineRendition == LineRendition::SingleWidth ? 0 : 1;
35+
return { line.X >> scale, line.Y };
36+
}
37+
3138
constexpr til::inclusive_rect BufferToScreenLine(const til::inclusive_rect& line, const LineRendition lineRendition)
3239
{
3340
// Use shift left to quickly multiply the Left and Right by 2 for double width lines.

src/buffer/out/search.cpp

+5-8
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,14 @@ void Search::Select() const
106106
}
107107

108108
// Routine Description:
109-
// - In console host, we take the found word and apply the given color to it in the screen buffer
110-
// - In Windows Terminal, we just select the found word, but we do not modify the buffer
109+
// - Applies the supplied TextAttribute to the current search result.
111110
// Arguments:
112-
// - ulAttr - The legacy color attribute to apply to the word
111+
// - attr - The attribute to apply to the result
113112
void Search::Color(const TextAttribute attr) const
114113
{
115-
// Only select if we've found something.
116-
if (_coordSelStart != _coordSelEnd)
117-
{
118-
_uiaData.ColorSelection(_coordSelStart, _coordSelEnd, attr);
119-
}
114+
// Note that _coordSelStart may be equal to _coordSelEnd (but it's an inclusive
115+
// selection: if they are equal, it means we are applying to a single character).
116+
_uiaData.ColorSelection(_coordSelStart, _coordSelEnd, attr);
120117
}
121118

122119
// Routine Description:

src/buffer/out/textBuffer.cpp

+138-1
Original file line numberDiff line numberDiff line change
@@ -1611,7 +1611,7 @@ bool TextBuffer::MoveToPreviousGlyph(til::point& pos, std::optional<til::point>
16111611
// - bufferCoordinates: when enabled, treat the coordinates as relative to
16121612
// the buffer rather than the screen.
16131613
// Return Value:
1614-
// - the delimiter class for the given char
1614+
// - One or more rects corresponding to the selection area
16151615
const std::vector<til::inclusive_rect> TextBuffer::GetTextRects(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const
16161616
{
16171617
std::vector<til::inclusive_rect> textRects;
@@ -1660,6 +1660,72 @@ const std::vector<til::inclusive_rect> TextBuffer::GetTextRects(til::point start
16601660
return textRects;
16611661
}
16621662

1663+
// Method Description:
1664+
// - Computes the span(s) for the given selection
1665+
// - If not a blockSelection, returns a single span (start - end)
1666+
// - Else if a blockSelection, returns spans corresponding to each line in the block selection
1667+
// Arguments:
1668+
// - start: beginning of the text region of interest (inclusive)
1669+
// - end: the other end of the text region of interest (inclusive)
1670+
// - blockSelection: when enabled, get spans for each line covered by the block
1671+
// - bufferCoordinates: when enabled, treat the coordinates as relative to
1672+
// the buffer rather than the screen.
1673+
// Return Value:
1674+
// - one or more sets of start-end coordinates
1675+
const std::vector<std::tuple<til::point, til::point>> TextBuffer::GetTextSpans(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const
1676+
{
1677+
std::vector<std::tuple<til::point, til::point>> textSpans;
1678+
1679+
if (blockSelection)
1680+
{
1681+
// If blockSelection, this is effectively the same operation as GetTextRects, but
1682+
// expressed in til::point coordinates.
1683+
auto rects = GetTextRects(start, end, /*blockSelection*/ true, bufferCoordinates);
1684+
textSpans.reserve(rects.size());
1685+
1686+
for (auto rect : rects)
1687+
{
1688+
til::point first = { rect.Left, rect.Top };
1689+
til::point second = { rect.Right, rect.Bottom };
1690+
auto span = std::make_tuple(first, second);
1691+
textSpans.emplace_back(span);
1692+
}
1693+
}
1694+
else
1695+
{
1696+
const auto bufferSize = GetSize();
1697+
1698+
// (0,0) is the top-left of the screen
1699+
// the physically "higher" coordinate is closer to the top-left
1700+
// the physically "lower" coordinate is closer to the bottom-right
1701+
auto [higherCoord, lowerCoord] = start <= end ?
1702+
std::make_tuple(start, end) :
1703+
std::make_tuple(end, start);
1704+
1705+
textSpans.reserve(1);
1706+
1707+
// If we were passed screen coordinates, convert the given range into
1708+
// equivalent buffer offsets, taking line rendition into account.
1709+
if (!bufferCoordinates)
1710+
{
1711+
higherCoord = ScreenToBufferLine(higherCoord, GetLineRendition(higherCoord.Y));
1712+
lowerCoord = ScreenToBufferLine(lowerCoord, GetLineRendition(lowerCoord.Y));
1713+
}
1714+
1715+
til::inclusive_rect asRect = { higherCoord.X, higherCoord.Y, lowerCoord.X, lowerCoord.Y };
1716+
_ExpandTextRow(asRect);
1717+
higherCoord.X = asRect.Left;
1718+
higherCoord.Y = asRect.Top;
1719+
lowerCoord.X = asRect.Right;
1720+
lowerCoord.Y = asRect.Bottom;
1721+
1722+
auto span = std::make_tuple(higherCoord, lowerCoord);
1723+
textSpans.emplace_back(span);
1724+
}
1725+
1726+
return textSpans;
1727+
}
1728+
16631729
// Method Description:
16641730
// - Expand the selection row according to include wide glyphs fully
16651731
// - this is particularly useful for box selections (ALT + selection)
@@ -1832,6 +1898,77 @@ const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF,
18321898
return data;
18331899
}
18341900

1901+
size_t TextBuffer::SpanLength(const til::point coordStart, const til::point coordEnd) const
1902+
{
1903+
assert((coordEnd.Y > coordStart.Y) ||
1904+
((coordEnd.Y == coordStart.Y) && (coordEnd.X >= coordStart.X)));
1905+
1906+
// Note that this could also be computed using CompareInBounds, but that function
1907+
// seems disfavored lately.
1908+
//
1909+
// CompareInBounds version:
1910+
//
1911+
// const auto bufferSize = GetSize();
1912+
// // Note that we negate because CompareInBounds is backwards from what we are trying to calculate.
1913+
// auto length = - bufferSize.CompareInBounds(coordStart, coordEnd);
1914+
// length += 1; // because we need "inclusive" behavior.
1915+
1916+
const auto rowSize = gsl::narrow<SHORT>(GetRowByOffset(0).size());
1917+
1918+
size_t length = ((size_t)(coordEnd.Y - coordStart.Y)) * rowSize;
1919+
length += ((size_t)coordEnd.X - coordStart.X) + 1; // "+1" is because we need "inclusive" behavior
1920+
1921+
return length;
1922+
}
1923+
1924+
// Routine Description:
1925+
// - Retrieves the plain text data between the specified coordinates.
1926+
// Arguments:
1927+
// - trimTrailingWhitespace - remove the trailing whitespace at the end of the result.
1928+
// - start - where to start getting text (should be at or prior to "end")
1929+
// - end - where to end getting text
1930+
// Return Value:
1931+
// - Just the text.
1932+
const std::wstring TextBuffer::GetPlainText(const bool trimTrailingWhitespace,
1933+
const til::point& start,
1934+
const til::point& end) const
1935+
{
1936+
std::wstring text;
1937+
// TODO: should I put in protections for start coming before end?
1938+
auto spanLength = SpanLength(start, end);
1939+
text.reserve(spanLength);
1940+
1941+
auto it = GetCellDataAt(start);
1942+
1943+
// copy char data into the string buffer, skipping trailing bytes
1944+
// TODO: is using spanLength like this the right way to do it?
1945+
while (it && ((spanLength) > 0))
1946+
{
1947+
const auto& cell = *it;
1948+
spanLength--;
1949+
1950+
if (!cell.DbcsAttr().IsTrailing())
1951+
{
1952+
const auto chars = cell.Chars();
1953+
text.append(chars);
1954+
}
1955+
#pragma warning(suppress : 26444)
1956+
// TODO GH 2675: figure out why there's custom construction/destruction happening here
1957+
it++;
1958+
}
1959+
1960+
if (trimTrailingWhitespace)
1961+
{
1962+
// remove the spaces at the end (aka trim the trailing whitespace)
1963+
while (!text.empty() && text.back() == UNICODE_SPACE)
1964+
{
1965+
text.pop_back();
1966+
}
1967+
}
1968+
1969+
return text;
1970+
}
1971+
18351972
// Routine Description:
18361973
// - Generates a CF_HTML compliant structure based on the passed in text and color data
18371974
// Arguments:

src/buffer/out/textBuffer.hpp

+7
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ class TextBuffer final
166166
bool MoveToPreviousGlyph(til::point& pos, std::optional<til::point> limitOptional = std::nullopt) const;
167167

168168
const std::vector<til::inclusive_rect> GetTextRects(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const;
169+
const std::vector<std::tuple<til::point, til::point>> GetTextSpans(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const;
169170

170171
void AddHyperlinkToMap(std::wstring_view uri, uint16_t id);
171172
std::wstring GetHyperlinkUriFromId(uint16_t id) const;
@@ -182,12 +183,18 @@ class TextBuffer final
182183
std::vector<std::vector<COLORREF>> BkAttr;
183184
};
184185

186+
size_t SpanLength(const til::point coordStart, const til::point coordEnd) const;
187+
185188
const TextAndColor GetText(const bool includeCRLF,
186189
const bool trimTrailingWhitespace,
187190
const std::vector<til::inclusive_rect>& textRects,
188191
std::function<std::pair<COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors = nullptr,
189192
const bool formatWrappedRows = false) const;
190193

194+
const std::wstring GetPlainText(const bool trimTrailingWhitespace,
195+
const til::point& start,
196+
const til::point& end) const;
197+
191198
static std::string GenHTML(const TextAndColor& rows,
192199
const int fontHeightPoints,
193200
const std::wstring_view fontFaceName,

src/cascadia/TerminalApp/AppActionHandlers.cpp

+15
Original file line numberDiff line numberDiff line change
@@ -1122,4 +1122,19 @@ namespace winrt::TerminalApp::implementation
11221122
args.Handled(handled);
11231123
}
11241124
}
1125+
1126+
void TerminalPage::_HandleColorSelection(const IInspectable& /*sender*/,
1127+
const ActionEventArgs& args)
1128+
{
1129+
if (args)
1130+
{
1131+
if (const auto& realArgs = args.ActionArgs().try_as<ColorSelectionArgs>())
1132+
{
1133+
const auto res = _ApplyToActiveControls([&](auto& control) {
1134+
control.ColorSelection(realArgs.Foreground(), realArgs.Background(), realArgs.MatchMode());
1135+
});
1136+
args.Handled(res);
1137+
}
1138+
}
1139+
}
11251140
}

src/cascadia/TerminalControl/ControlCore.cpp

+54
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "../../renderer/atlas/AtlasEngine.h"
1717
#include "../../renderer/dx/DxRenderer.hpp"
1818

19+
#include "SelectionColor.g.cpp"
1920
#include "ControlCore.g.cpp"
2021

2122
using namespace ::Microsoft::Console::Types;
@@ -2094,4 +2095,57 @@ namespace winrt::Microsoft::Terminal::Control::implementation
20942095
}
20952096
}
20962097
}
2098+
2099+
void ControlCore::ColorSelection(Control::SelectionColor fg, Control::SelectionColor bg, uint32_t matchMode)
2100+
{
2101+
if (HasSelection())
2102+
{
2103+
auto pForeground = winrt::get_self<implementation::SelectionColor>(fg);
2104+
auto pBackground = winrt::get_self<implementation::SelectionColor>(bg);
2105+
2106+
TextColor foregroundAsTextColor;
2107+
TextColor backgroundAsTextColor;
2108+
2109+
if (pForeground)
2110+
{
2111+
auto colorFg = pForeground->Color();
2112+
if (colorFg.a == 1)
2113+
{
2114+
// We're dealing with indexed colors.
2115+
foregroundAsTextColor.SetIndex(colorFg.r, false);
2116+
}
2117+
else
2118+
{
2119+
foregroundAsTextColor.SetColor(colorFg);
2120+
}
2121+
}
2122+
2123+
if (pBackground)
2124+
{
2125+
auto colorBg = pBackground->Color();
2126+
if (colorBg.a == 1)
2127+
{
2128+
// We're dealing with indexed colors.
2129+
backgroundAsTextColor.SetIndex(colorBg.r, false);
2130+
}
2131+
else
2132+
{
2133+
backgroundAsTextColor.SetColor(colorBg);
2134+
}
2135+
}
2136+
2137+
TextAttribute attr;
2138+
attr.SetForeground(foregroundAsTextColor);
2139+
attr.SetBackground(backgroundAsTextColor);
2140+
2141+
_terminal->ColorSelection(attr, matchMode);
2142+
_terminal->ClearSelection();
2143+
if (matchMode > 0)
2144+
{
2145+
// TODO: can this be scoped down further?
2146+
// one problem is that at this point on the stack, we don't know what changed
2147+
_renderer->TriggerRedrawAll();
2148+
}
2149+
}
2150+
}
20972151
}

src/cascadia/TerminalControl/ControlCore.h

+26
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
#pragma once
1717

18+
#include "SelectionColor.g.h"
1819
#include "ControlCore.g.h"
1920
#include "ControlSettings.h"
2021
#include "../../audio/midi/MidiAudio.hpp"
@@ -40,6 +41,28 @@ public: \
4041

4142
namespace winrt::Microsoft::Terminal::Control::implementation
4243
{
44+
struct SelectionColor : SelectionColorT<SelectionColor>
45+
{
46+
SelectionColor() = default;
47+
WINRT_PROPERTY(uint32_t, TextColor);
48+
49+
public:
50+
til::color Color() const
51+
{
52+
if (_TextColor & 0xff000000)
53+
{
54+
// We indicate that this is an indexed color by setting alpha to 1:
55+
return til::color(gsl::narrow_cast<uint8_t>(_TextColor), 0, 0, 1);
56+
}
57+
else
58+
{
59+
return til::color(static_cast<uint8_t>((_TextColor & 0xff000000) >> 24),
60+
static_cast<uint8_t>((_TextColor & 0x00ff0000) >> 16),
61+
static_cast<uint8_t>((_TextColor & 0x0000ff00) >> 8));
62+
}
63+
};
64+
};
65+
4366
struct ControlCore : ControlCoreT<ControlCore>
4467
{
4568
public:
@@ -104,6 +127,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
104127

105128
::Microsoft::Console::Types::IUiaData* GetUiaData() const;
106129

130+
void ColorSelection(Control::SelectionColor fg, Control::SelectionColor bg, uint32_t matchMode);
131+
107132
void Close();
108133

109134
#pragma region ICoreState
@@ -335,5 +360,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
335360

336361
namespace winrt::Microsoft::Terminal::Control::factory_implementation
337362
{
363+
BASIC_FACTORY(SelectionColor);
338364
BASIC_FACTORY(ControlCore);
339365
}

src/cascadia/TerminalControl/ControlCore.idl

+9
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ namespace Microsoft.Terminal.Control
5353
Boolean EndAtRightBoundary;
5454
};
5555

56+
[default_interface] runtimeclass SelectionColor
57+
{
58+
SelectionColor();
59+
// UInt32 to literally be a TextColor from buffer/out/TextColor.h
60+
UInt32 TextColor;
61+
}
62+
5663
[default_interface] runtimeclass ControlCore : ICoreState
5764
{
5865
ControlCore(IControlSettings settings,
@@ -132,6 +139,8 @@ namespace Microsoft.Terminal.Control
132139
void AdjustOpacity(Double Opacity, Boolean relative);
133140
void WindowVisibilityChanged(Boolean showOrHide);
134141

142+
void ColorSelection(SelectionColor fg, SelectionColor bg, UInt32 matchMode);
143+
135144
event FontSizeChangedEventArgs FontSizeChanged;
136145

137146
event Windows.Foundation.TypedEventHandler<Object, CopyToClipboardEventArgs> CopyToClipboard;

0 commit comments

Comments
 (0)