Skip to content

Implements vertical multi-caret support#133

Merged
tig merged 18 commits into
developfrom
claude/spec-pr-125-reimplementation-rVWsC
May 17, 2026
Merged

Implements vertical multi-caret support#133
tig merged 18 commits into
developfrom
claude/spec-pr-125-reimplementation-rVWsC

Conversation

@tig
Copy link
Copy Markdown
Member

@tig tig commented May 15, 2026

Update — implementation landed (commit dc835c6). This PR now contains the
vertical multi-caret implementation, ported tests, and docs in addition to the spec.
Single PR (spec + tests + code). Closes #124; supersedes #125.

Key bindings

Gesture Action Notes
Ctrl+Alt+↑ Add a caret on the line above the topmost caret VS Code parity; sticky visual column (tabs / short lines / word-wrap aware); no-op past line 1
Ctrl+Alt+↓ Add a caret on the line below the bottommost caret VS Code parity; sticky visual column; no-op past last line
Alt + LeftButton drag Build a vertical column of carets (press row = primary) Alt, not VS Code's Shift+Alt — Windows Terminal eats Shift+drag for block-select; carets only
Ctrl+Click Toggle an additional caret at the click (unchanged) Re-confirmed: a pre-press PositionReport+Ctrl no longer hijacks the primary
Tab / Shift+Tab Indent / un-indent at every caret, one undo step Each caret computes its own tab stop so the column stays aligned across repeats
Esc Collapse to the primary caret, refresh sticky column Continued ↑/↓ navigation is not trapped by where the block was

Ctrl+Alt+↑/↓ is a per-platform [ConfigurationProperty] PlatformKeyBinding in
Editor.DefaultKeyBindings — fully user-overridable via View.ViewKeyBindings config,
no editor-specific fallback chord (macOS same chord pending real-terminal validation,
DEC-006); registered via AddCommand + the configurable path, not an inline if.
The mouse column-drag uses Alt (not VS Code's Shift+Alt): a TUI runs inside a
terminal that reserves Shift+drag for its own forced/block text selection, so
Shift+Alt+drag never reaches the editor. The mouse modifier is not yet
user-configurable — tracked upstream by
gui-cs/Terminal.Gui#4888 (configurable
MouseBindings), to be prioritized.

Validation

  • 12 integration tests ported from PR Add vertically aligned multi-caret creation via Alt+Up/Down and Alt+Drag #125, re-keyed to the VS Code chords — failing-first
    on develop
    , green after implementation — plus 1 visual-line-cache unit test.
  • Full suites green: Tests 437/437, IntegrationTests 187/187; dotnet format + jb
    cleanup clean; 0 warnings.
  • OD-1 (primary caret disappears after exit): not reproducible on develop — kept
    Primary_Caret_Is_Visible_After_Exiting_MultiCaret as a regression guard.
  • OD-3 resolved: ClearAdditionalCarets stays public (DEC-007).

No new public Editor API (R8). Docs updated: specs/decisions.md, specs/public-api.md,
Docs/Help/keyboard-reference.md, Docs/Help/multi-caret.md.


Original spec-PR description

Summary

Adds specs/vertical-multi-caret/spec.md. No code changes.

PR #125 ships the right user-facing feature set — Alt+Up / Alt+Down to create vertically-aligned carets, Alt+Drag to create a column, Tab at every caret, normalization so the primary moving onto an additional caret doesn't duplicate edits, and Ctrl+Click after a vertical block landing where the user clicked — but the maintainer rejected it as hacky after a stream of regressions kept popping out under each fix:

@copilot this is basically non-functional. The first time I use it, it basically works. But then all cursor/caret management is messed up.

…and four follow-ups, ending with the unresolved "primary caret disappears after exiting multi mode" that Copilot couldn't reproduce.

This spec treats PR #125's tests as the executable contract and re-derives the requirements / file scope / definition of done from black-box behavior, not from the patch. The intended workflow is: open a new branch from develop, port the PR #125 tests verbatim, confirm they fail, then implement against this spec.

What the spec covers

  • 12 user scenarios extracted from the PR test set, plus the unresolved "caret disappears" bug as Scenario 12 / OD-1.
  • 17 requirements (FR-001 … FR-017) covering Alt+Up/Down, Alt+Drag, sticky visual column (incl. tabs and wrap), normalization on every caret-offset / document-change boundary, Tab-at-all-carets with single-undo, Esc semantics, Ctrl+Click reorder defense, primary-caret visibility post-exit, and the visual-line cache invalidation defect Scenario 10 exposes.
  • Out-of-scope: column-selection variants (Alt+Shift+arrow), find/replace across multi-caret, ted UI menu items, WordWrap-toggle reflow.
  • Three open decisions: OD-1 (caret-disappears repro), OD-2 (Alt+Shift+arrow reservation), OD-3 (whether ClearAdditionalCarets should drop to internal per R9).

What this PR is not

Test plan

https://claude.ai/code/session_014uBfGKbouM7qvzSShb3WCc


Generated by Claude Code

claude and others added 7 commits May 15, 2026 18:41
PR #125 ships the right user-facing feature set (Alt+Up/Down for
vertically-aligned carets, Alt+Drag for column-of-carets, Tab at all
carets, normalization on primary-caret moves, Ctrl+Click after vertical
block) but the implementation accreted around maintainer-reported
regressions and is rejected as throwaway. This spec captures the
behavior from the PR's tests, lifts the failing-first regressions as
acceptance criteria, and flags the open caret-disappears-after-exit bug
as OD-1 to be reproduced before the spec moves to Ready.
Cross-walk every user-facing behavior in the spec against VS Code and
VS 2022/2026. Call out the three intentional divergences (Alt vs Ctrl+Alt
vs Alt+Shift for add-caret-above/below; Alt+drag carets-only vs
Shift+Alt+drag column-select; Ctrl+Click vs Alt+Click for caret-at-click)
with rationale. Add OD-4 (keybinding choice) and OD-5 (Alt+Click alias).
Resolve OD-4 in favor of matching VS Code chords (modulo terminal
incompat): Ctrl+Alt+Up/Down for add-caret-above/below, Shift+Alt+drag
modifier for the column-drag. Comparison table rows that previously
diverged on chords now match. The Shift+Alt+drag-produces-carets-only
divergence stays (column-select-per-row is the follow-up); Ctrl+Click
vs Alt+Click stays as a separate open question.

Rename requirements from cryptic FR-### identifiers to human-readable
labels (e.g. "Add caret above", "Caret normalization", "Cache
invalidation on offset shift"). Open Decisions and scenarios are
referenced by name throughout. Tests renamed from AltDown_* /
AltDrag_* to CtrlAltDown_* / ShiftAltDrag_*.

Add Resolved Decisions section capturing the keybinding choice and a
new open question for whether ted should ship an alternate-chord
fallback for terminals/WMs that grab Ctrl+Alt+arrow.
Resolve the alternate-chord open question: no editor-specific fallback.
Keys are fully adjustable via TG keybindings, so pre-ship per-platform
defaults the TG-standard way — a [ConfigurationProperty]
DefaultKeyBindings populated from a PlatformKeyBinding record
(All/Windows/Linux/Macos), user-overridable through View.ViewKeyBindings
config. Windows/Linux default to the VS Code chord (Ctrl+Alt+Up/Down);
the macOS default is a wiring-time empirical question (Terminal.app /
iTerm2 swallow Cmd and may not deliver Ctrl+Alt+arrow) tracked as a
remaining open decision. References TG docfx/docs/keyboard.md for the
binding-layer model and Bind.AllPlus() helper.
Implements specs/vertical-multi-caret/spec.md — the re-spec of the
rejected prototype PR #125, closing issue #124.

Behaviour (VS Code chords, per DEC-006):
- Ctrl+Alt+CursorUp / Ctrl+Alt+CursorDown add a caret above the topmost
  / below the bottommost caret at the sticky visual column (tabs, short
  lines, and word-wrap aware). Crossing a document bound is a no-op.
- Shift+Alt + LeftButton drag builds a vertical column of carets
  (carets only; per-row selection is the documented follow-up).
- Tab / Shift+Tab insert at every caret in one undo scope, each caret
  computing its own tab stop so the column stays aligned across presses.

Design (addresses the maintainer's "hacky" rejection of #125):
- Keybindings registered as Editor-local Command ids via AddCommand and
  bound through the configurable [ConfigurationProperty]
  Editor.DefaultKeyBindings (PlatformKeyBinding) — no inline if-chain in
  OnKeyDownNotHandled, no fallback chord.
- AddAdditionalCaretAt (the only add path) dedupes by construction;
  NormalizeAdditionalCarets (the only invariant-trim path) runs on every
  primary move and document change; ToggleCaretAt rewritten on top.
- Editor.Mouse.cs replaces the three suppress-until-release booleans with
  a single DragMode state; Ctrl-modified drags can no longer hijack the
  primary caret (Ctrl+Click reorder defence).
- Visual-line cache: a same-line-count edit that shifts absolute offsets
  now invalidates downstream cached lines (lineDelta == 0 &&
  offsetDelta != 0) — fixes the "Tab twice desyncs" defect; pinned by a
  unit test in EditorVisualLineCacheTests.
- Vertical placement reuses the single-caret visual-column / wrap-map
  primitives (GetVisualColumnForOffset, TryGetVerticalOffset).

Tests: 12 integration tests ported from the PR #125 set, re-keyed to the
VS Code chords (failing-first on develop, green now) + 1 cache unit test.
Full suites green (Tests 437/437, IntegrationTests 187/187); dotnet
format + jb cleanup clean.

OD-1 (primary caret disappears after exit) confirmed not reproducible on
develop; kept Primary_Caret_Is_Visible_After_Exiting_MultiCaret as a
regression guard. OD-3 resolved: ClearAdditionalCarets stays public
(DEC-007). Docs (decisions, public-api, ted help) updated; stray CDATA
wrapper stripped from the spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Windows Terminal (and the xterm family it emulates) reserves Shift+drag
as the user's forced text-selection override while an app has mouse mode
on, and Alt turns that into a rectangular/block selection. So the spec's
Shift+Alt+drag column gesture was being consumed by the terminal's own
block-select and never reaching the editor. Alt+drag is forwarded.

- Editor.Mouse.cs: column-drag press now keys on `alt` alone (was
  `shift && alt`); DragMode.ColumnCarets doc updated.
- EditorMouseTests: ShiftAltDrag_Adds_Vertically_Aligned_Carets renamed
  to AltDrag_Adds_Vertically_Aligned_Carets; press/drag flags drop Shift.
- Spec: authoritative Amendment block + normative refs updated to Alt;
  DEC-006 amended; public-api / Docs/Help (keyboard-reference,
  multi-caret) updated. VS-Code-describing text left accurate.

Keyboard chords (Ctrl+Alt+Up/Down) unchanged. Making the mouse modifier
user-configurable (to opt back into Shift+Alt parity) is tracked upstream
by gui-cs/Terminal.Gui#4888 — to be prioritized.

Full suites green: Tests 437/437, IntegrationTests 187/187; dotnet
format + jb cleanup clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tig tig marked this pull request as ready for review May 17, 2026 00:53
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c90ea243d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Terminal.Gui.Editor/Editor.Indentation.cs
tig and others added 2 commits May 16, 2026 19:23
Codex review (P1) caught that the multi-caret path only covered Tab:
Unindent() still computed removals from the primary caret's line only,
so Shift+Tab in a vertical multi-caret session unindented one line and
left the other carets untouched — breaking the indent/unindent symmetry
the spec requires ("Tab and Shift+Tab honor HasMultipleCarets"; Files in
Scope names MultiCaretUnindent).

- Editor.MultiCaret.cs: add MultiCaretUnindent() — removes one
  indentation unit from every distinct caret line (deduped), high-offset
  -first, in a single RunUpdate scope; reuses
  TextUtilities.GetSingleIndentationSegment like single-caret Unindent.
- Editor.Indentation.cs: Unindent() routes to MultiCaretUnindent() when
  HasMultipleCarets (symmetric with InsertTab).
- Test: ShiftTab_Unindents_At_All_Carets_In_One_Undo_Step — confirmed
  failing-first ("ab\n\tab\n\tab"), green after the fix; also asserts a
  single Ctrl+Z restores all lines (one undo step).

Full suites green: Tests 437/437, IntegrationTests 188/188; dotnet
format + jb cleanup clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tig

This comment was marked as resolved.

@tig tig changed the title spec: vertical multi-caret (Alt+Up/Down, Alt+Drag) — re-spec of PR #125 Implements vertical multi-caret support May 17, 2026
@tig
Copy link
Copy Markdown
Member Author

tig commented May 17, 2026

Scope note: column / box selection (Alt+drag producing a selection per row) is intentionally deferred — this PR ships carets-only, per the spec's Out of Scope / Intentional Divergences #1 and DEC-006. Architectural review confirmed deferral is additive (nothing here needs undoing). Tracked, with the concrete code seams and the pre-existing additional-caret-selection rendering gap, in #139.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c286837bbd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Terminal.Gui.Editor/Editor.Indentation.cs
Comment thread src/Terminal.Gui.Editor/Editor.Indentation.cs
@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

This comment was marked as resolved.

Copilot finished work on behalf of tig May 17, 2026 04:58
Per maintainer review (bespoke (Command)1001/1002 casts are a no-no; the
correct fix is upstream in TG's Command enum), filed
gui-cs/Terminal.Gui#5318 requesting proper Command.InsertCaretAbove /
.InsertCaretBelow members.

- Editor.Commands.cs: comment above the const ids now marks them an
  explicitly TEMPORARY workaround tracked by TG#5318, with the removal
  plan (replace with real enum members + drop the block once TG#5318
  ships and $(TerminalGuiVersion) is bumped). "Bespoke command values
  are not an acceptable end state."
- specs/decisions.md DEC-006: same, cross-linked to TG#5318.

Comment/doc only — no code-behavior change. Pure Tests suite green
(454/454) on the merged canonical state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TG#5318 shipped Command.InsertCaretAbove / .InsertCaretBelow (TG PR
#5319, in Terminal.Gui 2.1.1-develop.98). Remove the sanctioned
short-term magic-int workaround now that the upstream fix is available:

- Directory.Build.props: pin $(TerminalGuiVersion) 2.1.1-develop.59 →
  2.1.1-develop.98 (the develop build containing #5319).
- Editor.Commands.cs: delete the TEMPORARY WORKAROUND comment block and
  the (Command)1001/1002 const casts; bind/AddCommand against the real
  Command.InsertCaretAbove / Command.InsertCaretBelow members.
- specs/decisions.md DEC-006: mark the Command-enum debt RESOLVED
  (2026-05-17), cross-link TG#5319 and the parked design issue TG#5320.

Keybindings now round-trip through config by readable name
("InsertCaretAbove") instead of the bare number "1001", satisfying the
gui-cs/Editor side of TG#5318's acceptance criterion.

Verified on 2.1.1-develop.98: restore + build clean (0 warnings),
Tests 454/454, IntegrationTests 207/207. `dotnet format
--verify-no-changes` clean. (jb cleanupcode hit the known local
profile-discovery flakiness the Stop hook documents; no style drift —
the edit is mechanical and dotnet format is the deterministic gate.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29200ed1bc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Terminal.Gui.Editor/Editor.Indentation.cs
tig and others added 2 commits May 16, 2026 23:25
Underline rendered poorly and inconsistently across terminals for the
multi-caret indicator. Switch the MultiCaretRenderer caret attribute to
TextStyle.Blink | TextStyle.Reverse — far more legible and reliably
supported — and bring everything that described the old style into line:

- MultiCaretRenderer.cs: rewrite the now-contradictory rationale comment
  and the XML-doc summary (was "underlined + blinking").
- EditorRenderingTests / EditorMouseTests: update the expected caret
  Attribute, the positive HasFlag assertion to Blink|Reverse, the
  negative assertions to "NOT Reverse", and rename
  MultiCaret_Renders_Underline_Blink_Attribute_On_Text →
  ..._Reverse_Blink_... .
- Docs/Help/multi-caret.md, selection.md: "inverted-attribute cells" →
  "blinking, reverse-video cells"; also corrected the renderer interface
  name (IBackgroundRenderer → the actual IOverlayRenderer).

Tests 454/454, IntegrationTests 207/207, dotnet format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings in develop: code-themes #134/#132 (xshd colorizer wired through TG
Scheme code-token VisualRoles), TG pin .95→.98, Editor.MultiCaret.cs
whitespace fix, HighlightingColor Role equality.

Conflict: tests/.../EditorRenderingTests.cs — resolved by keeping our
renamed MultiCaret_Renders_Reverse_Blink_Attribute_On_Text (the
reverse-video caret change develop never had) and DROPPING the orphaned
UseThemeBackground_Defaults_To_True test: develop removed the
UseThemeBackground property in 04315ea ([#132] colorizer rework) with no
replacement boolean, so the test referenced dead surface (it only
survived on this branch via the earlier intermediate develop merge).

Build clean; Tests 460/460; IntegrationTests 205/205; dotnet format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7685378b4b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Terminal.Gui.Editor/Editor.MultiCaret.cs Outdated
…odex P1)

InsertTab()/Unindent() short-circuited to the multi-caret path the moment
HasMultipleCarets was true — before the multi-line-selection branch — so
a multi-line selection coexisting with an extra caret was silently
REPLACED by a single tab (data loss) on Tab, and on Shift+Tab only the
caret-hosting lines unindented (the block was ignored).

Fix in Editor.MultiCaret.cs: classify each caret (primary uses the
editor selection; additional uses its own anchor) via the new
TryGetCaretSelectionRange / RangeSpansMultipleLines / LinesInRange
helpers. A multi-line selection block-indents / block-unindents every
line it touches; a single-line selection is type-over-replaced; a
point caret gets a tab / line-unindent. All edits dedupe block lines
and apply strictly high-offset-first in one RunUpdate scope (one undo
step). A multi-line selection is never replaced/deleted by a tab.

- specs/vertical-multi-caret/spec.md: amended the "Tab at every caret"
  requirement (was under-specified — "replacement, if a per-caret
  selection is active"), added the "Tab with a multi-line selection
  plus an extra caret" scenario, recorded the Resolved Decision.
- New EditorMultiCaretIndentTests: Tab and Shift+Tab with a multi-line
  primary selection + a point caret; assert block-indent (not delete)
  and single-step undo. These failed before the fix.

Tests 460/460; IntegrationTests 207/207; dotnet format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tig tig merged commit b235943 into develop May 17, 2026
8 checks passed
@tig tig deleted the claude/spec-pr-125-reimplementation-rVWsC branch May 17, 2026 07:03
tig added a commit that referenced this pull request May 17, 2026
Loose end carried forward from PR #133 (now merged): multi-caret
Tab/Shift+Tab block-indent calls ClearAdditionalCaretSelections() and
collapses the primary selection post-edit, whereas single-caret
IndentSelectedLines preserves it. Cosmetic while carets are point-only,
but load-bearing once per-row selections become a first-class column
primitive — so it belongs to the column/box selection-per-row
("multi-select") follow-up.

- specs/vertical-multi-caret/spec.md: the Out-of-Scope "Column / box
  selection" bullet now names it "the multi-select PR" and lists
  selection-preservation parity (primary + per-caret, mirroring
  SetSelectionRangePreservingDirection, with a regression test) as a
  REQUIRED deliverable of that PR.
- specs/multi-caret/spec.md: Out of Scope cross-references the full
  requirement so it's discoverable from the canonical multi-caret spec.

Spec/docs only — no code change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tig added a commit that referenced this pull request May 17, 2026
…rvey

All four beta features (find-and-replace #104, clipboard #107, word-wrap
#106, multi-caret #105) plus vertical-multi-caret #133 and syntax-theme
Phase 2 #134 have merged since the 2026-05-13 plan snapshot. Bring the
spec set back in line with develop:

- Per-feature status lines -> Done with PR/issue refs (clipboard,
  multi-caret, find-and-replace, word-wrap, vertical-multi-caret,
  syntax-theme).
- plan.md rescoped to the beta endgame: new "Done (beta)" table, a
  "Remaining for beta" that reflects no feature work is left (only
  external/verification/cut items), evidence-checked DoD boxes,
  refreshed dependency graph and header.
- decisions.md: OPEN-005 annotated as settled by syntax-theme; add
  OPEN-006 (single-line/input-mode decision).
- codex-autonomous-sprint.md: flag stale work pool.
- New specs/textview-parity-gap/spec.md: survey of TextView
  capabilities Editor lacks (autocomplete, overwrite mode,
  single-line mode, kill-ring, context menu) with dispositions;
  wired into plan.md follow-ups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Editor should support vertically aligned carets

3 participants