Skip to content

Commit

Permalink
Implement EnableColorSelection
Browse files Browse the repository at this point in the history
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…
  • Loading branch information
jazzdelightsme committed Jul 5, 2022
1 parent 478c2c3 commit c1ffaa4
Show file tree
Hide file tree
Showing 26 changed files with 989 additions and 25 deletions.
7 changes: 7 additions & 0 deletions src/buffer/out/LineRendition.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ constexpr til::inclusive_rect ScreenToBufferLine(const til::inclusive_rect& line
return { line.Left >> scale, line.Top, line.Right >> scale, line.Bottom };
}

constexpr til::point ScreenToBufferLine(const til::point& line, const LineRendition lineRendition)
{
// Use shift right to quickly divide the Left and Right by 2 for double width lines.
const auto scale = lineRendition == LineRendition::SingleWidth ? 0 : 1;
return { line.X >> scale, line.Y };
}

constexpr til::inclusive_rect BufferToScreenLine(const til::inclusive_rect& line, const LineRendition lineRendition)
{
// Use shift left to quickly multiply the Left and Right by 2 for double width lines.
Expand Down
13 changes: 5 additions & 8 deletions src/buffer/out/search.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,14 @@ void Search::Select() const
}

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

// Routine Description:
Expand Down
140 changes: 139 additions & 1 deletion src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1611,7 +1611,7 @@ bool TextBuffer::MoveToPreviousGlyph(til::point& pos, std::optional<til::point>
// - bufferCoordinates: when enabled, treat the coordinates as relative to
// the buffer rather than the screen.
// Return Value:
// - the delimiter class for the given char
// - One or more rects corresponding to the selection area
const std::vector<til::inclusive_rect> TextBuffer::GetTextRects(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const
{
std::vector<til::inclusive_rect> textRects;
Expand Down Expand Up @@ -1660,6 +1660,73 @@ const std::vector<til::inclusive_rect> TextBuffer::GetTextRects(til::point start
return textRects;
}


// Method Description:
// - Computes the span(s) for the given selection
// - If not a blockSelection, returns a single span (start - end)
// - Else if a blockSelection, returns spans corresponding to each line in the block selection
// Arguments:
// - start: beginning of the text region of interest (inclusive)
// - end: the other end of the text region of interest (inclusive)
// - blockSelection: when enabled, get spans for each line covered by the block
// - bufferCoordinates: when enabled, treat the coordinates as relative to
// the buffer rather than the screen.
// Return Value:
// - one or more sets of start-end coordinates
const std::vector<std::tuple<til::point, til::point>> TextBuffer::GetTextSpans(til::point start, til::point end, bool blockSelection, bool bufferCoordinates) const
{
std::vector<std::tuple<til::point, til::point>> textSpans;

if (blockSelection)
{
// If blockSelection, this is effectively the same operation as GetTextRects, but
// expressed in til::point coordinates.
auto rects = GetTextRects(start, end, /*blockSelection*/ true, bufferCoordinates);
textSpans.reserve(rects.size());

for (auto rect : rects)
{
til::point first = { rect.Left, rect.Top };
til::point second = { rect.Right, rect.Bottom };
auto span = std::make_tuple(first, second);
textSpans.emplace_back(span);
}
}
else
{
const auto bufferSize = GetSize();

// (0,0) is the top-left of the screen
// the physically "higher" coordinate is closer to the top-left
// the physically "lower" coordinate is closer to the bottom-right
auto [higherCoord, lowerCoord] = start <= end ?
std::make_tuple(start, end) :
std::make_tuple(end, start);

textSpans.reserve(1);

// If we were passed screen coordinates, convert the given range into
// equivalent buffer offsets, taking line rendition into account.
if (!bufferCoordinates)
{
higherCoord = ScreenToBufferLine(higherCoord, GetLineRendition(higherCoord.Y));
lowerCoord = ScreenToBufferLine(lowerCoord, GetLineRendition(lowerCoord.Y));
}

til::inclusive_rect asRect = { higherCoord.X, higherCoord.Y, lowerCoord.X, lowerCoord.Y };
_ExpandTextRow(asRect);
higherCoord.X = asRect.Left;
higherCoord.Y = asRect.Top;
lowerCoord.X = asRect.Right;
lowerCoord.Y = asRect.Bottom;

auto span = std::make_tuple(higherCoord, lowerCoord);
textSpans.emplace_back(span);
}

return textSpans;
}

// Method Description:
// - Expand the selection row according to include wide glyphs fully
// - this is particularly useful for box selections (ALT + selection)
Expand Down Expand Up @@ -1832,6 +1899,77 @@ const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF,
return data;
}

size_t TextBuffer::SpanLength(const til::point coordStart, const til::point coordEnd) const
{
assert((coordEnd.Y > coordStart.Y) ||
((coordEnd.Y == coordStart.Y) && (coordEnd.X >= coordStart.X)));

// Note that this could also be computed using CompareInBounds, but that function
// seems disfavored lately.
//
// CompareInBounds version:
//
// const auto bufferSize = GetSize();
// // Note that we negate because CompareInBounds is backwards from what we are trying to calculate.
// auto length = - bufferSize.CompareInBounds(coordStart, coordEnd);
// length += 1; // because we need "inclusive" behavior.

const auto rowSize = gsl::narrow<SHORT>(GetRowByOffset(0).size());

size_t length = (coordEnd.Y - coordStart.Y) * rowSize;
length += coordEnd.X - coordStart.X + 1; // "+1" is because we need "inclusive" behavior

return length;
}

// Routine Description:
// - Retrieves the plain text data between the specified coordinates.
// Arguments:
// - trimTrailingWhitespace - remove the trailing whitespace at the end of the result.
// - start - where to start getting text (should be at or prior to "end")
// - end - where to end getting text
// Return Value:
// - Just the text.
const std::wstring TextBuffer::GetPlainText(const bool trimTrailingWhitespace,
const til::point& start,
const til::point& end) const
{
std::wstring text;
// TODO: should I put in protections for start coming before end?
auto spanLength = SpanLength(start, end);
text.reserve(spanLength);

auto it = GetCellDataAt(start);

// copy char data into the string buffer, skipping trailing bytes
// TODO: is using spanLength like this the right way to do it?
while (it && ((spanLength) > 0))
{
const auto& cell = *it;
spanLength--;

if (!cell.DbcsAttr().IsTrailing())
{
const auto chars = cell.Chars();
text.append(chars);
}
#pragma warning(suppress : 26444)
// TODO GH 2675: figure out why there's custom construction/destruction happening here
it++;
}

if (trimTrailingWhitespace)
{
// remove the spaces at the end (aka trim the trailing whitespace)
while (!text.empty() && text.back() == UNICODE_SPACE)
{
text.pop_back();
}
}

return text;
}

// Routine Description:
// - Generates a CF_HTML compliant structure based on the passed in text and color data
// Arguments:
Expand Down
8 changes: 8 additions & 0 deletions src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ class TextBuffer final
bool MoveToPreviousGlyph(til::point& pos, std::optional<til::point> limitOptional = std::nullopt) const;

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

void AddHyperlinkToMap(std::wstring_view uri, uint16_t id);
std::wstring GetHyperlinkUriFromId(uint16_t id) const;
Expand All @@ -182,12 +183,19 @@ class TextBuffer final
std::vector<std::vector<COLORREF>> BkAttr;
};


size_t SpanLength(const til::point coordStart, const til::point coordEnd) const;

const TextAndColor GetText(const bool includeCRLF,
const bool trimTrailingWhitespace,
const std::vector<til::inclusive_rect>& textRects,
std::function<std::pair<COLORREF, COLORREF>(const TextAttribute&)> GetAttributeColors = nullptr,
const bool formatWrappedRows = false) const;

const std::wstring GetPlainText(const bool trimTrailingWhitespace,
const til::point& start,
const til::point& end) const;

static std::string GenHTML(const TextAndColor& rows,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
Expand Down
15 changes: 15 additions & 0 deletions src/cascadia/TerminalApp/AppActionHandlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1122,4 +1122,19 @@ namespace winrt::TerminalApp::implementation
args.Handled(handled);
}
}

void TerminalPage::_HandleColorSelection(const IInspectable& /*sender*/,
const ActionEventArgs& args)
{
if (args)
{
if (const auto& realArgs = args.ActionArgs().try_as<ColorSelectionArgs>())
{
const auto res = _ApplyToActiveControls([&](auto& control) {
control.ColorSelection(realArgs.Foreground(), realArgs.Background(), realArgs.MatchMode());
});
args.Handled(res);
}
}
}
}
54 changes: 54 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "../../renderer/atlas/AtlasEngine.h"
#include "../../renderer/dx/DxRenderer.hpp"

#include "SelectionColor.g.cpp"
#include "ControlCore.g.cpp"

using namespace ::Microsoft::Console::Types;
Expand Down Expand Up @@ -2093,4 +2094,57 @@ namespace winrt::Microsoft::Terminal::Control::implementation
}
}
}

void ControlCore::ColorSelection(Control::SelectionColor fg, Control::SelectionColor bg, uint32_t matchMode)
{
if (HasSelection())
{
auto pfg = winrt::get_self<implementation::SelectionColor>(fg);
auto pbg = winrt::get_self<implementation::SelectionColor>(bg);

TextColor tcfg;
TextColor tcbg;

if (pfg)
{
auto colorFg = pfg->Color();
if (colorFg.a == 1)
{
// We're dealing with indexed colors.
tcfg.SetIndex(colorFg.r, false);
}
else
{
tcfg.SetColor(colorFg);
}
}

if (pbg)
{
auto colorBg = pbg->Color();
if (colorBg.a == 1)
{
// We're dealing with indexed colors.
tcbg.SetIndex(colorBg.r, false);
}
else
{
tcbg.SetColor(colorBg);
}
}

TextAttribute attr;
attr.SetForeground(tcfg);
attr.SetBackground(tcbg);

_terminal->ColorSelection(attr, matchMode);
_terminal->ClearSelection();
if (matchMode > 0)
{
// TODO: can this be scoped down further?
// one problem is that at this point on the stack, we don't know what changed
_renderer->TriggerRedrawAll();
}
}
}
}
26 changes: 26 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#pragma once

#include "SelectionColor.g.h"
#include "ControlCore.g.h"
#include "ControlSettings.h"
#include "../../audio/midi/MidiAudio.hpp"
Expand All @@ -40,6 +41,28 @@ public: \

namespace winrt::Microsoft::Terminal::Control::implementation
{
struct SelectionColor : SelectionColorT<SelectionColor>
{
SelectionColor() = default;
WINRT_PROPERTY(uint32_t, TextColor);

public:
til::color Color() const
{
if (_TextColor & 0xff000000)
{
// We indicate that this is an indexed color by setting alpha to 1:
return til::color(gsl::narrow_cast<uint8_t>(_TextColor), 0, 0, 1);
}
else
{
return til::color(static_cast<uint8_t>((_TextColor & 0xff000000) >> 24),
static_cast<uint8_t>((_TextColor & 0x00ff0000) >> 16),
static_cast<uint8_t>((_TextColor & 0x0000ff00) >> 8));
}
};
};

struct ControlCore : ControlCoreT<ControlCore>
{
public:
Expand Down Expand Up @@ -104,6 +127,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation

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

void ColorSelection(Control::SelectionColor fg, Control::SelectionColor bg, uint32_t matchMode);

void Close();

#pragma region ICoreState
Expand Down Expand Up @@ -335,5 +360,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation

namespace winrt::Microsoft::Terminal::Control::factory_implementation
{
BASIC_FACTORY(SelectionColor);
BASIC_FACTORY(ControlCore);
}
9 changes: 9 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.idl
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ namespace Microsoft.Terminal.Control
Boolean EndAtRightBoundary;
};

[default_interface] runtimeclass SelectionColor
{
SelectionColor();
// UInt32 to literally be a TextColor from buffer/out/TextColor.h
UInt32 TextColor;
}

[default_interface] runtimeclass ControlCore : ICoreState
{
ControlCore(IControlSettings settings,
Expand Down Expand Up @@ -132,6 +139,8 @@ namespace Microsoft.Terminal.Control
void AdjustOpacity(Double Opacity, Boolean relative);
void WindowVisibilityChanged(Boolean showOrHide);

void ColorSelection(SelectionColor fg, SelectionColor bg, UInt32 matchMode);

event FontSizeChangedEventArgs FontSizeChanged;

event Windows.Foundation.TypedEventHandler<Object, CopyToClipboardEventArgs> CopyToClipboard;
Expand Down
4 changes: 4 additions & 0 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3044,4 +3044,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return _core.ScrollMarks();
}

void TermControl::ColorSelection(Control::SelectionColor fg, Control::SelectionColor bg, uint32_t matchMode)
{
_core.ColorSelection(fg, bg, matchMode);
}
}
Loading

2 comments on commit c1ffaa4

@github-actions
Copy link

@github-actions github-actions bot commented on c1ffaa4 Jul 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@check-spelling-bot Report

Unrecognized words, please review:

  • itow
  • pbg
  • tcbg
  • tcfg
Previously acknowledged words that are now absent azurewebsites Checkin condev Consolescreen css cxcy DCompile debolden deconstructed DECRST DECRSTS devicefamily dxp errno FARPROC GETKEYSTATE guardxfg HPAINTBUFFER HPROPSHEETPAGE iconify ipa LLVM LPCHARSETINFO MAPVIRTUALKEY MSDL mspartners ned newcursor nlength NOWAIT PENDTASKMSG pgorepro pgort PGU Poli PPORT PSMALL redistributable SOURCESDIRECTORY Timeline timelines toolbars unintense UWA UWAs VKKEYSCAN wddmconrenderer wdx windev WResult xfg
To accept these unrecognized words as correct (and remove the previously acknowledged and now absent words), run the following commands

... in a clone of the git@github.com:jazzdelightsme/terminal.git repository
on the user/danthom/wip_ecs branch:

update_files() {
perl -e '
my @expect_files=qw('".github/actions/spelling/expect/alphabet.txt
.github/actions/spelling/expect/expect.txt
.github/actions/spelling/expect/web.txt"');
@ARGV=@expect_files;
my @stale=qw('"$patch_remove"');
my $re=join "|", @stale;
my $suffix=".".time();
my $previous="";
sub maybe_unlink { unlink($_[0]) if $_[0]; }
while (<>) {
if ($ARGV ne $old_argv) { maybe_unlink($previous); $previous="$ARGV$suffix"; rename($ARGV, $previous); open(ARGV_OUT, ">$ARGV"); select(ARGV_OUT); $old_argv = $ARGV; }
next if /^(?:$re)(?:(?:\r|\n)*$| .*)/; print;
}; maybe_unlink($previous);'
perl -e '
my $new_expect_file=".github/actions/spelling/expect/c1ffaa4bfb9ee73bce18911502c8b0876f4574b4.txt";
use File::Path qw(make_path);
use File::Basename qw(dirname);
make_path (dirname($new_expect_file));
open FILE, q{<}, $new_expect_file; chomp(my @words = <FILE>); close FILE;
my @add=qw('"$patch_add"');
my %items; @items{@words} = @words x (1); @items{@add} = @add x (1);
@words = sort {lc($a)."-".$a cmp lc($b)."-".$b} keys %items;
open FILE, q{>}, $new_expect_file; for my $word (@words) { print FILE "$word\n" if $word =~ /\w/; };
close FILE;
system("git", "add", $new_expect_file);
'
}

comment_json=$(mktemp)
curl -L -s -S \
  --header "Content-Type: application/json" \
  "https://api.github.com/repos/jazzdelightsme/terminal/comments/77694366" > "$comment_json"
comment_body=$(mktemp)
jq -r .body < "$comment_json" > $comment_body
rm $comment_json

patch_remove=$(perl -ne 'next unless s{^</summary>(.*)</details>$}{$1}; print' < "$comment_body")
  

patch_add=$(perl -e '$/=undef;
$_=<>;
s{<details>.*}{}s;
s{^#.*}{};
s{\n##.*}{};
s{(?:^|\n)\s*\*}{}g;
s{\s+}{ }g;
print' < "$comment_body")
  
update_files
rm $comment_body
git add -u
✏️ Contributor please read this

By default the command suggestion will generate a file named based on your commit. That's generally ok as long as you add the file to your commit. Someone can reorganize it later.

⚠️ The command is written for posix shells. You can copy the contents of each perl command excluding the outer ' marks and dropping any '"/"' quotation mark pairs into a file and then run perl file.pl from the root of the repository to run the code. Alternatively, you can manually insert the items...

If the listed items are:

  • ... misspelled, then please correct them instead of using the command.
  • ... names, please add them to .github/actions/spelling/allow/names.txt.
  • ... APIs, you can add them to a file in .github/actions/spelling/allow/.
  • ... just things you're using, please add them to an appropriate file in .github/actions/spelling/expect/.
  • ... tokens you only need in one place and shouldn't generally be used, you can add an item in an appropriate file in .github/actions/spelling/patterns/.

See the README.md in each directory for more information.

🔬 You can test your commits without appending to a PR by creating a new branch with that extra change and pushing it to your fork. The check-spelling action will run in response to your push -- it doesn't require an open pull request. By using such a branch, you can limit the number of typos your peers see you make. 😉

🗜️ If you see a bunch of garbage

If it relates to a ...

well-formed pattern

See if there's a pattern that would match it.

If not, try writing one and adding it to a patterns/{file}.txt.

Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

Note that patterns can't match multiline strings.

binary-ish string

Please add a file path to the excludes.txt file instead of just accepting the garbage.

File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

@github-actions
Copy link

@github-actions github-actions bot commented on c1ffaa4 Jul 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@check-spelling-bot Report

Unrecognized words, please review:

  • itow
  • pbg
  • tcbg
  • tcfg
Previously acknowledged words that are now absent azurewebsites Checkin condev Consolescreen css cxcy DCompile debolden deconstructed DECRST DECRSTS devicefamily dxp errno FARPROC GETKEYSTATE guardxfg HPAINTBUFFER HPROPSHEETPAGE iconify ipa LLVM LPCHARSETINFO MAPVIRTUALKEY MSDL mspartners ned newcursor nlength NOWAIT PENDTASKMSG pgorepro pgort PGU Poli PPORT PSMALL redistributable SOURCESDIRECTORY Timeline timelines toolbars unintense UWA UWAs VKKEYSCAN wddmconrenderer wdx windev WResult xfg
To accept these unrecognized words as correct (and remove the previously acknowledged and now absent words), run the following commands

... in a clone of the git@github.com:jazzdelightsme/terminal.git repository
on the dev/danthom/enableColorSelection branch:

update_files() {
perl -e '
my @expect_files=qw('".github/actions/spelling/expect/alphabet.txt
.github/actions/spelling/expect/expect.txt
.github/actions/spelling/expect/web.txt"');
@ARGV=@expect_files;
my @stale=qw('"$patch_remove"');
my $re=join "|", @stale;
my $suffix=".".time();
my $previous="";
sub maybe_unlink { unlink($_[0]) if $_[0]; }
while (<>) {
if ($ARGV ne $old_argv) { maybe_unlink($previous); $previous="$ARGV$suffix"; rename($ARGV, $previous); open(ARGV_OUT, ">$ARGV"); select(ARGV_OUT); $old_argv = $ARGV; }
next if /^(?:$re)(?:(?:\r|\n)*$| .*)/; print;
}; maybe_unlink($previous);'
perl -e '
my $new_expect_file=".github/actions/spelling/expect/c1ffaa4bfb9ee73bce18911502c8b0876f4574b4.txt";
use File::Path qw(make_path);
use File::Basename qw(dirname);
make_path (dirname($new_expect_file));
open FILE, q{<}, $new_expect_file; chomp(my @words = <FILE>); close FILE;
my @add=qw('"$patch_add"');
my %items; @items{@words} = @words x (1); @items{@add} = @add x (1);
@words = sort {lc($a)."-".$a cmp lc($b)."-".$b} keys %items;
open FILE, q{>}, $new_expect_file; for my $word (@words) { print FILE "$word\n" if $word =~ /\w/; };
close FILE;
system("git", "add", $new_expect_file);
'
}

comment_json=$(mktemp)
curl -L -s -S \
  --header "Content-Type: application/json" \
  "https://api.github.com/repos/jazzdelightsme/terminal/comments/77694428" > "$comment_json"
comment_body=$(mktemp)
jq -r .body < "$comment_json" > $comment_body
rm $comment_json

patch_remove=$(perl -ne 'next unless s{^</summary>(.*)</details>$}{$1}; print' < "$comment_body")
  

patch_add=$(perl -e '$/=undef;
$_=<>;
s{<details>.*}{}s;
s{^#.*}{};
s{\n##.*}{};
s{(?:^|\n)\s*\*}{}g;
s{\s+}{ }g;
print' < "$comment_body")
  
update_files
rm $comment_body
git add -u
✏️ Contributor please read this

By default the command suggestion will generate a file named based on your commit. That's generally ok as long as you add the file to your commit. Someone can reorganize it later.

⚠️ The command is written for posix shells. You can copy the contents of each perl command excluding the outer ' marks and dropping any '"/"' quotation mark pairs into a file and then run perl file.pl from the root of the repository to run the code. Alternatively, you can manually insert the items...

If the listed items are:

  • ... misspelled, then please correct them instead of using the command.
  • ... names, please add them to .github/actions/spelling/allow/names.txt.
  • ... APIs, you can add them to a file in .github/actions/spelling/allow/.
  • ... just things you're using, please add them to an appropriate file in .github/actions/spelling/expect/.
  • ... tokens you only need in one place and shouldn't generally be used, you can add an item in an appropriate file in .github/actions/spelling/patterns/.

See the README.md in each directory for more information.

🔬 You can test your commits without appending to a PR by creating a new branch with that extra change and pushing it to your fork. The check-spelling action will run in response to your push -- it doesn't require an open pull request. By using such a branch, you can limit the number of typos your peers see you make. 😉

🗜️ If you see a bunch of garbage

If it relates to a ...

well-formed pattern

See if there's a pattern that would match it.

If not, try writing one and adding it to a patterns/{file}.txt.

Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

Note that patterns can't match multiline strings.

binary-ish string

Please add a file path to the excludes.txt file instead of just accepting the garbage.

File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

Please sign in to comment.