[Conhost] Fix off-by-1 errors for search and color selection foreground - #20519
[Conhost] Fix off-by-1 errors for search and color selection foreground#20519Carlos Zamora (carlos-zamora) wants to merge 1 commit into
Conversation
| // GH#20152: srSelectionRect.right is inclusive, but CopyRequest::end is | ||
| // exclusive, so it must be adjusted by one | ||
| const auto req = TextBuffer::CopyRequest::FromConfig(textBuffer, | ||
| til::point{ _d->srSelectionRect.left, _d->srSelectionRect.top }, | ||
| til::point{ _d->srSelectionRect.right, _d->srSelectionRect.bottom }, | ||
| til::point{ _d->srSelectionRect.right + 1, _d->srSelectionRect.bottom }, |
There was a problem hiding this comment.
Investigate.
srSelectionRect.rightis already inclusive --> right side of the character- CopyRequest::end expects exclusive
Why do we need to add 1, if we're on the right side of the character already.
| // beg and end coordinates are inclusive | ||
| til::point beg; | ||
| til::point end; | ||
| til::point beg; // inclusive | ||
| til::point end; // exclusive |
There was a problem hiding this comment.
Investigate.
- when did this occur? How long has this been a problem?
Carlos Zamora (carlos-zamora)
left a comment
There was a problem hiding this comment.
TODO: Have GHCP do a full audit of logic and comments
Resulting report using GHCP - Opus 5 - 1M context - Max reasoningAudit: the #18106 "exclusive selection range" effortRepository: Scope & methodTraced the full change surface of #18106 (
Layers reviewed: Findings A, B, and C were empirically proven by building
🔴 The root cause — fix this first
// Searches through the entire (committed) text buffer for `needle` ...
// The end coordinates of the returned ranges are considered inclusive. // ← WRONG
std::optional<std::vector<til::point_span>> TextBuffer::SearchText(...) const
// Searches through the given rows [rowBeg,rowEnd) for `needle` ...
// While the end coordinates of the returned ranges are considered inclusive, // ← WRONG
// the [rowBeg,rowEnd) range is half-open.#18106 changed the underlying - // Returns an inclusive point range given a text start and end position.
+ // Returns a half-open [beg,end) range given a text start and end position.
...
- auto nativeIndexEnd = uregex_end64(re, 0, &status);
- // The parameters are given as a half-open [beg,end) range, but the point_span we return in closed [beg,end].
- nativeIndexEnd--;
+ const auto nativeIndexEnd = uregex_end64(re, 0, &status);
...
- ret.end.x = ...GetTrailingColumnAtCharOffset(ut->chunkOffset);
+ ret.end.x = ...GetLeadingColumnAtCharOffset(ut->chunkOffset);…but the public Fix: change both comments to "half-open Real bugsA.
|
| Location | Problem |
|---|---|
buffer/out/textBuffer.cpp:3066, :3073 |
Root cause above — says SearchText results have an inclusive end; they're exclusive. |
inc/til/point.h:286-287 |
"At the time of writing there's a push to make selections have an exclusive end coordinate, so the interpretation of end might change soon (making this comment potentially outdated)." — the push landed in #18106. |
inc/til/point.h:305-306 |
"begX and begY are inclusive coordinates" — typo, should be begX and endX. And "because point_span itself also uses inclusive coordinates" is no longer true. |
host/selectionState.cpp:200 |
GetSelectionAnchors header says "begin and end (inclusive) anchor positions", but #19259 deliberately made the body return an exclusive end. Directly contradicts the code 25 lines below it. |
cascadia/TerminalCore/TerminalSelection.cpp:608-609 |
"No need to undo a move! We'll decrement in the next step anyways." — that DecrementInBounds was deleted by #18106. The end is now intentionally exclusive; the justification describes a step that no longer exists. |
buffer/out/textBuffer.hpp:202 |
The pending PR's til::point end; // exclusive is under-specified: end.x is exclusive but end.y is used inclusively (for (auto iRow = req.beg.y; iRow <= req.end.y; ++iRow)). Spell this out — a one-word comment invites exactly the mistake being fixed. |
buffer/out/textBuffer.cpp:1666-1667 vs :1727-1728 |
GetTextRects documents both ends inclusive; GetTextSpans documents its end exclusive — yet GetTextSpans's block-selection path delegates to GetTextRects. It happens to work (verified), but nothing says so. |
Simple improvements
i. til::point_span::iterate_rows now has zero callers.
The pending PR converts the last one (host/selectionInput.cpp:704) to iterate_rows_exclusive. Delete the inclusive version, then rename iterate_rows_exclusive → iterate_rows. This structurally removes the ability to make this class of mistake again — the highest-leverage cleanup available.
ii. Viewport::CompareInExclusiveBounds has zero callers.
Either delete it or wire it into Terminal.cpp:1555 (finding H). Also note an internal inconsistency: it uses Width() as the row stride while WalkInExclusiveBounds uses Width() + 1, so the two disagree on whether {w, y} and {0, y+1} are the same position. (Compare's stride is right for measuring cell distance; just be aware Compare(a,b) == 0 does not imply a == b.)
iii. AtlasEngine::_invalidateSpans passes til::CoordTypeMax as the width.
renderer/atlas/AtlasEngine.api.cpp:92
sp.iterate_rows_exclusive(til::CoordTypeMax, [&](til::CoordType row, til::CoordType beg, til::CoordType end) {
const auto shift = buffer.GetLineRendition(row) != LineRendition::SingleWidth ? 1 : 0;
end <<= shift; // INT32_MAX << 1 → signed-overflow UBThe function already has buffer, so it can pass the real width. Consequences today: non-final rows get end == INT32_MAX; on double-width rows end <<= 1 is UB, the rect collapses to {} via operator&, and invalidatedRows.start is forced to 0. Benign (over-invalidation) and it pre-dates #18106, but it's adjacent and cheap. The sentinel also defeats iterate_rows_exclusive's ax == w normalization. renderer/base/renderer.cpp:702 does it correctly with bufferWidth.
Confirmed correct (no action needed)
The pending commit ebde93363 is sound. Specifically verified:
search.cpp—DecrementInBoundsis genuinely the right choice here (notDecrementInExclusiveBounds): both{w,y}and{0,y+1}correctly map to{w-1,y}, whereas the exclusive variant would map{0,y+1}→{w,y}. The added comment is accurate.selectionInput.cpp—right + 1and theiterate_rows_exclusiveswitch are both correct (modulo finding B, which is pre-existing).UiaTextRangeBase::FindText— removing theIncrementInBoundsis correct;hitEnd <= _endnow compares like-for-like.- Existing
SelectionTests+SearchTestspass, including the newTestColorSelectionSearchAndColorAllMatches.
Also verified correct under the new convention:
renderer/base/renderer.cpp:702(TriggerSelection) andAtlasEngine::_drawHighlighted— properly exclusive; the redundantmax += 1was correctly removed.TextBuffer::_ExpandTextRow— happens to be correct for a half-open[left, right)rect (checkingcell[right] == Trailingand incrementing is exactly right whenrightis exclusive).- Conhost block-selection anchor math in
Selection::_RegenerateSelectionSpans/GetSelectionAnchors— the two blocks are logically equivalent and produce correct exclusive ends for all four drag directions. ControlCore::_selectSpan,ControlCore::ClearSearch,SelectionStartForRendering/SelectionEndForRendering,_ConvertToBufferCell,GetHyperlinkAtBufferPosition,_getPatterns.- GDI / UIA renderers — consume pre-computed
til::rects; no inclusive↔exclusive math.
Suggested fix order
SearchTextcomments — the root cause; prevents the next recurrence.- B — user-visible data loss on copy, and the pending PR widens its reach.
- D, E/F/G — user-visible (Select All, accessibility).
- A, C, H — latent / edge-case correctness.
- Remaining stale comments.
- Dead-code cleanup (i, ii) — ideally fold i into this PR while the last
iterate_rowscaller is already being touched.
This all still needs to be reviewed and verified, but I wanted to post it here so that it doesn't get lost.
Summary of the Pull Request
Fixes a number of off-by-one errors in conhost. Specifically, the issues were with search, color selection (foreground), and the UIA find text API.
To minimize risk and make a small, concentrated change, I tried making targeted fixes with concise comments explaining why the change is needed. An earlier approach was to be more explicit about inclusive/exclusive coordinates using strict typing, but that seemed more harmful than helpful.
References and Relevant Issues
#18106
Validation Steps Performed
echo foo foo fooreg add "HKCU\Console" /v EnableColorSelection /t REG_DWORD /d 1 /fecho foo foo fooPR Checklist
TestColorSelectionSearchAndColorAllMatches)