Skip to content
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

Make selection an exclusive range #18106

Open
wants to merge 27 commits into
base: main
Choose a base branch
from

Conversation

carlos-zamora
Copy link
Member

@carlos-zamora carlos-zamora commented Oct 23, 2024

Summary of the Pull Request

Selection is generally stored as an inclusive start and end. This PR makes the end exclusive which now allows degenerate selections, namely in mark mode. This also modifies mouse selection to round to the nearest cell boundary (see #5099) and improves word boundaries to be a bit more modern and make sense for degenerate selections (similar to #15787).

References and Relevant Issues

Closes #5099
Closes #13447
Closes #17892

Detailed Description of the Pull Request / Additional comments

  • Buffer, Viewport, and Point
    • Introduced a few new functions here to find word boundaries, delimiter class runs, and glyph boundaries.
      • 📝These new functions should be able to replace a few other functions (i.e. GetWordStart --> GetWordStart2). That migration is going to be a part of Refactor Word Expansion in TextBuffer #4423 to reduce the risk of breaking UIA.
    • Viewport: added a few functions to handle navigating the exclusive bounds (namely allowing RightExclusive as a position for buffer coordinates). This is important for selection to be able to highlight the entire line.
      • 📝BottomInclusiveRightExclusive() will replace EndExclusive in the UIA code
    • Point: iterate_rows_exclusive is similar to iterate_rows, except it has handling for RightExclusive
  • Renderer
    • Use iterate_rows_exclusive for proper handling (this actually fixed a lot of our issues)
    • Remove some workarounds in _drawHighlighted (this is a boundary where we got inclusive coords and made them exclusive, but now we don't need that!)
  • Terminal
    • fix selection marker rendering
    • _ConvertToBufferCell(): add a param to allow for RightExclusive or clamp it to RightInclusive (original behavior). Both are useful!
    • Use new GetWordStart2 and GetWordEnd2 to improve word boundaries and make them feel right now that the selection an exclusive range.
    • Convert a few IsInBounds --> IsInExclusiveBounds for safety and correctness
    • Add TriggerSelection to SelectNewRegion
      • 📝 We normally called TriggerSelection in a different layer, but it turns out, UIA's Select function wouldn't actually update the renderer. Whoops! This fixes that.
  • TermControl
  • UIA
    • TermControlUIAProvider::GetSelectionRange no need to convert from inclusive range to exclusive range anymore!
    • TextBuffer::GetPlainText now works on an exclusive range, so no need to convert the range anymore!

Validation Steps Performed

This fundamental change impacts a lot of scenarios:

  • ✅Rendering selections
  • ✅Selection markers
  • ✅Copy text
  • ✅Session restore
  • ✅Mark mode navigation (i.e. character, word, line, buffer)
  • ✅Mouse selection (i.e. click+drag, shift+click, multi-click, alt+click)
  • ✅Hyperlinks (interaction and rendering)
  • ✅Accessibility (i.e. get selection, movement, text extraction, selecting text)
  • Prev/Next Command/Output (untested)
  • ✅Unit tests
    We should definitely bug bash this to get good coverage.

Follow-ups

  • Refactor Word Expansion in TextBuffer #4423
    • Now that selection and UIA are both exclusive ranges, it should be a lot easier to deduplicate code between selection and UIA. We should be able to remove EndExclusive as well when we do that. This'll also be an opportunity to modernize that code and use more til classes.

src/buffer/out/textBuffer.cpp Fixed Show fixed Hide fixed
src/buffer/out/textBuffer.cpp Fixed Show fixed Hide fixed
src/buffer/out/textBuffer.cpp Fixed Show fixed Hide fixed
src/buffer/out/textBuffer.cpp Fixed Show fixed Hide fixed
@microsoft-github-policy-service microsoft-github-policy-service bot added Issue-Bug It either shouldn't be doing this or needs an investigation. Issue-Task It's a feature request, but it doesn't really need a major design. Area-Accessibility Issues related to accessibility Area-TerminalControl Issues pertaining to the terminal control (input, selection, keybindings, mouse interaction, etc.) Priority-3 A description (P3) Product-Terminal The new Windows Terminal. labels Oct 23, 2024

This comment has been minimized.

This comment has been minimized.

@microsoft-github-policy-service microsoft-github-policy-service bot added the Issue-Feature Complex enough to require an in depth planning process and actual budgeted, scheduled work. label Oct 24, 2024
_fillColorBitmap(row, x1, end, fgColor, bgColor);

// Return early if we couldn't paint the whole region (either this was not the last row, or
// it was the last row but the highlight ends outside of our x range.)
// We will resume from here in the next call.
if (!isFinalRow || hiEnd.x /*inclusive*/ >= x2 /*exclusive*/)
if (!isFinalRow || hiEnd.x > x2)
Copy link
Member

Choose a reason for hiding this comment

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

BTW I think we may have made a mistake here due to my poor understanding of this code. Probably needs to be mulled over once more...

Copy link
Member

Choose a reason for hiding this comment

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

yes, be careful - this is used in TWO places. search highlights and selection rendering. they're both inclusive today. this PR only says it changes one of them.

{
const auto l = static_cast<ptrdiff_t>(_sr.left);
const auto t = static_cast<ptrdiff_t>(_sr.top);
const auto w = static_cast<ptrdiff_t>(std::max(0, _sr.right - _sr.left + 2));
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be +1. BTW is this function really needed? We already have WalkInBounds with the allowEndExclusive parameter that we could set to false no?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, so allowEndExclusive is slightly different. EndExclusive = (Left, BottomExclusive). allowEndExclusive was designed to make WalkInBounds(true) still walk from Left --> RightInclusive, but when we get to the last cell of the bottom inclusive row, we wrap onto the next line (EndExclusive). This made it so that we're able to represent the UIA start/end as an exclusive range, and we'd be able to include the last cell of the buffer by making end = EndExclusive.

In hindsight, I think the right move should've been to make EndExclusive = (RightExclusive, BottomInclusive). This PR makes selection do that and adds these extra APIs. I think it's much cleaner. But yeah, I wouldn't want to port UIA over to this system until it gets reviewed and merged. So I'm making that work a part of #4423 as a follow-up (where we should be able to remove EndExclusive altogether).

As for the +1, I honestly don't remember why I made it +2... you're probably right. I'll make that change and validate.

@carlos-zamora carlos-zamora removed the Priority-3 A description (P3) label Oct 25, 2024
@microsoft-github-policy-service microsoft-github-policy-service bot added the Priority-3 A description (P3) label Oct 25, 2024
Copy link
Member

@DHowett DHowett left a comment

Choose a reason for hiding this comment

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

IOU a review here - thanks for doing it!

@DHowett
Copy link
Member

DHowett commented Nov 4, 2024

However, all of the tests exploded so... I may wait. :P

This comment has been minimized.

Copy link
Member

@DHowett DHowett left a comment

Choose a reason for hiding this comment

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

Have you tested...

  • detecting URls
  • selecting URLs
  • search highlights (with and without regexes)

src/buffer/out/UTextAdapter.cpp Show resolved Hide resolved
src/cascadia/TerminalControl/ControlCore.cpp Outdated Show resolved Hide resolved
@@ -342,9 +342,8 @@ try
const til::rect vp{ _viewport.ToExclusive() };
for (auto&& sp : spans)
{
sp.iterate_rows(bufferWidth, [&](til::CoordType row, til::CoordType min, til::CoordType max) {
sp.iterate_rows_exclusive(bufferWidth, [&](til::CoordType row, til::CoordType min, til::CoordType max) {
Copy link
Member

Choose a reason for hiding this comment

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

careful! this touches conhost code too. ... !

this may make selections not work properly in conhost

Copy link
Member Author

Choose a reason for hiding this comment

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

Testing out Host.exe, this PR does change selection to be exclusive there too. Pivoting the selection is a bit weird now as shift+clicking before the pivot highlights the selected cell, whereas doing so after the pivot highlights up to the cell the pointer is on.

I'm able to polish Conhost to work more nicely too, but is that something we want? Or should I explore how to keep Conhost the same?

@carlos-zamora

This comment was marked as resolved.

@zadjii-msft
Copy link
Member

selecting URLs

Method: Enter mark mode --> Shift+Tab to select the url

Was this fixed? Just wanna know if I should be making my rounds here too

@carlos-zamora
Copy link
Member Author

selecting URLs

Method: Enter mark mode --> Shift+Tab to select the url

Was this fixed? Just wanna know if I should be making my rounds here too

Currently working on it. My goal is to get the following fixed so we can test it in the bug bash on Tuesday:

@carlos-zamora carlos-zamora removed their assignment Nov 15, 2024
@carlos-zamora
Copy link
Member Author

These are fixed now! Verified ConHost using GDI and Atlas. Verified hyperlink detection with cases I mentioned earlier.

@@ -436,7 +436,7 @@ til::point_span Microsoft::Console::ICU::BufferRangeFromMatch(UText* ut, URegula
if (utextAccess(ut, nativeIndexEnd, true))
{
const auto y = accessCurrentRow(ut);
ret.end.x = textBuffer.GetRowByOffset(y).GetTrailingColumnAtCharOffset(ut->chunkOffset);
ret.end.x = textBuffer.GetRowByOffset(y).GetLeadingColumnAtCharOffset(ut->chunkOffset);
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

@lhecker lhecker Nov 16, 2024

Choose a reason for hiding this comment

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

The offsets are exclusive now. This means the chunkOffset now refers to the first character after the matched range. In turn, this means that if there's a wide glyph after the matched range, we want its leading half, because the exclusive end should be exactly 1 past the end (and not 2).

Comment on lines +83 to +96
CharToColumnMapper::CharToColumnMapper(const wchar_t* chars, const uint16_t* charOffsets, ptrdiff_t charsLength, til::CoordType currentColumn, til::CoordType columnCount) noexcept :
_chars{ chars },
_charOffsets{ charOffsets },
_lastCharOffset{ lastCharOffset },
_currentColumn{ currentColumn }
_charsLength{ charsLength },
_currentColumn{ currentColumn },
_columnCount{ columnCount }
{
}

// If given a position (`offset`) inside the ROW's text, this function will return the corresponding column.
// This function in particular returns the glyph's first column.
til::CoordType CharToColumnMapper::GetLeadingColumnAt(ptrdiff_t targetOffset) noexcept
{
targetOffset = clamp(targetOffset, 0, _lastCharOffset);
targetOffset = clamp(targetOffset, 0, _charsLength);
Copy link
Member

Choose a reason for hiding this comment

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

what's all this?

Copy link
Member

@lhecker lhecker Nov 16, 2024

Choose a reason for hiding this comment

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

These changes are necessary so that both accessor functions work reliably with an exclusive target char offset. I suggested to rename the member to make its usage a bit easier to understand, now that it refers to the length and not length-1 anymore. I did this in collaboration with Carlos. 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Accessibility Issues related to accessibility Area-TerminalControl Issues pertaining to the terminal control (input, selection, keybindings, mouse interaction, etc.) Issue-Bug It either shouldn't be doing this or needs an investigation. Issue-Feature Complex enough to require an in depth planning process and actual budgeted, scheduled work. Issue-Task It's a feature request, but it doesn't really need a major design. Priority-3 A description (P3) Product-Terminal The new Windows Terminal.
Projects
None yet
4 participants