You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #124 / PR #133 (vertical-multi-caret). PR #133 ships carets-only: Alt+drag and Ctrl+Alt+Up/Down create a vertical column of bare carets. This issue tracks the deferred column / box selection (the natural next step) and a pre-existing rendering gap it depends on.
Parity intent: the end-user behavior of column/box selection must match VS Code. Only input gestures may differ, and only where a terminal makes VS Code's chord physically impossible. Every behavioral deviation is enumerated, categorized, and justified in the Behavioral parity section below — nothing deviates silently.
The spec explicitly scoped this out and deferred it (specs/vertical-multi-caret/spec.md → Out of Scope and Intentional Divergences #1; specs/decisions.md DEC-006). The architectural review on PR #133 confirmed deferral is additive — nothing in PR #133 needs undoing.
(a) Column-select during drag
Alt+drag should optionally produce a selection per row (VS Code's Shift+Alt+drag semantics — typing replaces the column), not just carets.
The per-caret edit pipeline already supports this — every MultiCaret* op (MultiCaretInsert/DeleteLeft/DeleteRight/NewLine/InsertTab/Unindent) already branches on caret.SelectionAnchor → _document.Replace(...). So "type to replace a column" is free once selection anchors are set. The work is placement + rendering. Concrete seams (all additive):
Editor.MultiCaret.csAddAdditionalCaretAt(int offset) → add an overload AddAdditionalCaretAt(int offset, int selectionAnchorOffset) that also sets CaretInfo.SelectionAnchor (the field already exists). Preserves the PR Implements vertical multi-caret support #133 DoD invariant ("only AddAdditionalCaretAt/NormalizeAdditionalCarets mutate _additionalCarets") via overload, not violation.
Editor.MultiCaret.csSetVerticalCaretsFromViewRows(anchorViewRow, activeViewRow, viewColumn) → thread an active column through (anchor column = press X, already stored in _columnDragAnchor; active column = current drag X). Per row, set a selection from anchor-col offset to active-col offset instead of a bare caret. The rebuild-from-scratch-per-drag-event design already in place is exactly right for live widen/narrow.
Editor.csTryGetVerticalOffset(...) → call it twice per target row (anchor column and active column) to form each row's selection range. Reused as-is. Short rows clamp to line end (see parity D-rules); the visual columns stay sticky/virtual so re-widening re-extends them — no padding is written to the document.
Editor.Mouse.cs — same DragMode.ColumnCarets state; _columnDragAnchor already captures the full press point. No new drag state. (Decide interaction with the open Alt+Click add-caret alias spec decision — see deviation D2.)
Keyboard column-select path — register Editor-local commands for column-select up/down/left/right/page and bind them to Ctrl+Shift+Alt+Arrow / PgUp/PgDn (mac Cmd+…) through the configurable Editor.DefaultKeyBindings, exactly as Ctrl+Alt+↑/↓ is bound. The handler reuses the same TryGetVerticalOffset + sticky-virtual-column machinery as the drag path (anchor column fixed at the primary's column when the gesture starts; active column moves with each keypress), so it shares the row-selection builder with the mouse path. Command-id note: like InsertCaretAbove/Below, these have no Command enum member — register via the same sanctioned path PR Implements vertical multi-caret support #133 used post-TG#5318 (real members if added, else tracked by Need mechanism for view-defined Commands - full routing/config/bubbling/bridging/localization parity Terminal.Gui#5320); do not cast magic ints.
NormalizeAdditionalCarets / ClearAdditionalCarets / Esc — unchanged (offset-keyed dedupe still valid for distinct-line column rows; clearing a CaretInfo already clears its SelectionAnchor).
Modifier stays Alt (not Shift+Alt) for the terminal-compat reasons in DEC-006; configurable mouse modifiers tracked upstream by gui-cs/Terminal.Gui#4888.
Rendering/MultiCaretRenderer.cs paints additional caret cells (reverse-video + blink, post-PR #133) only. It does not paint additional carets' selection spans. Additional-caret SelectionAnchor is consumed by the edit pipeline but never drawn — so the multi-caret spec's FR-005 ("paints all carets and selections") is only partially true today, independent of vertical multi-caret. Any additional-caret selection (column-select, or future per-caret selection gestures) is invisible until this is fixed.
Add a per-caret selection IBackgroundRenderer (or extend the existing selection renderer to also iterate _additionalCarets whose SelectionAnchor is set), scoped per visual-line segment like MultiCaretRenderer already does for word-wrap.
This is a multi-caret completeness item; (a) cannot be user-visible without it.
Surfaced while fixing the Codex P1 in PR #133: multi-caret Tab / Shift+Tab block-indent calls ClearAdditionalCaretSelections () and collapses the primary selection after the edit, whereas the single-caret IndentSelectedLines path preserves it (via SetSelectionRangePreservingDirection). This is cosmetic only while additional carets are point-only — but (a) makes per-row selections a first-class column primitive, at which point a column selection vanishing the instant the user presses Tab is a real defect. It also depends on (b) (the selection must render to be observable). Hence it belongs to this PR.
Required: multi-caret Tab / Shift+Tab preserves the primary selection and every per-caret selection across the block-indent, mirroring IndentSelectedLines' SetSelectionRangePreservingDirection behavior per caret. Add a regression test alongside tests/Terminal.Gui.Editor.IntegrationTests/EditorMultiCaretIndentTests.cs. Tracked in spec: specs/vertical-multi-caret/spec.md § Out of Scope → Column / box selection (listed as a required deliverable of this PR) and specs/multi-caret/spec.md § Out of Scope.
Behavioral parity with VS Code (and explicit deviations)
Rule: any column/box-select end-user behavior not listed in the Deviations table below must behave exactly as VS Code does. If implementation forces a new deviation, it must be added to the table (with category + rationale) before merge — never shipped silently.
Must match VS Code (behavior, not keys)
Zero-width vs. ranged. A vertical drag with no horizontal extent ⇒ a bare caret per row (already shipped, PR Implements vertical multi-caret support #133). A drag with horizontal extent ⇒ one selection per row. (VS Code: identical.)
Typing replaces the column. Typing (or a 1-line paste) over a ranged column replaces each row's selection; over a zero-width column it inserts at each caret. Exactly one undo step for the whole column edit. (VS Code: identical.)
Bidirectional / reversed box. Dragging the active column to the left of the anchor selects leftward on every row (caret on the low side); dragging back across the anchor flips direction; widen/narrow tracks the pointer live. (VS Code: identical.)
Short lines clamp, never pad. On rows shorter than the box, the per-row selection/caret clamps to the line's real end. No padding or virtual spaces are written to the document. The anchor and active visual columns are sticky (reuse the vertical-caret sticky-virtual-column machinery) so widening later re-extends the short rows. (VS Code: identical — the box column is virtual; the buffer is untouched until the user types.)
Esc / plain click collapses to the primary caret / clears the block. (VS Code: Esc collapses multi-cursor — same effect.)
Keyboard column-select.Ctrl+Shift+Alt+↑/↓/←/→ and Ctrl+Shift+Alt+PgUp/PgDn create/extend a column selection from the keyboard, matching VS Code's Cursor Column Select commands (mac: Cmd+Shift+Alt+…). In scope and must match VS Code — TG implements the Kitty keyboard protocol (progressive enhancement / CSI-u), which disambiguates and delivers these four-modifier chords to the app. On a terminal that does not negotiate the protocol the chord is simply unavailable and the binding is user-overridable — the same environmental bound that applies to every advanced chord (and to the chords PR Implements vertical multi-caret support #133 already ships); it is not a designed behavioral deviation.
Deviations — where & why (each MUST be documented in Docs/Help/multi-caret.md and the spec Comparison table)
#
Behavior
VS Code
This editor
Why
Category
D1
Start column/box select (mouse)
Shift+Alt+drag
Alt+drag
Windows Terminal / the xterm family reserve Shift+drag for the terminal's own forced/rectangular selection while an app has mouse mode on, so Shift+Alt+drag never reaches the app; Alt+drag is forwarded. End-user capability is identical; only the modifier differs.
Pre-existing Editor binding; and Alt is now the column-drag modifier (D1), so any future Alt+Click alias must be disambiguated from Alt+drag by a movement threshold. Capability (click to add a caret) is preserved; only the modifier differs.
Terminal incompatibility (knock-on of D1) + binding history; Alt+Click alias is an open spec decision
WITHDRAWN — not a deviation; in scope, matches VS Code
Originally listed as terminal-incompat. Corrected: TG implements the Kitty keyboard protocol, so this chord is deliverable to the TUI. Keyboard column-select is now a Must match VS Code item (see above), not a deviation. Legacy terminals lacking the protocol: chord unavailable / user-rebindable — an environmental bound shared by all advanced chords (including those PR #133 ships), not a product deviation. ID kept (not renumbered) to preserve references.
Not a deviation — reclassified after the Kitty-protocol correction
D4
Column Selection Mode (sticky toggle; ordinary click/arrows do column-select until turned off; Selection menu + status-bar indicator)
Supported
Out of scope
A modal input state with menu/status-bar UI is far larger than the drag gesture and not required for column-edit parity.
Scope — separate issue if wanted
D5
Multi-cursor clipboard paste distribution (editor.multiCursorPaste: N clipboard lines → N cursors one-per-cursor; else full at each)
"spread" by default
Deferred — separate follow-up, not blocking this PR
Clipboard distribution is orthogonal to creating/rendering the column selection (this issue's focus). Typing/replace parity is in scope; paste-distribution is its own behavior and must be tracked so VS Code column-edit parity is eventually complete — not silently dropped.
Scope — must be filed as its own tracked follow-up
Definition of done
Alt+drag column select: zero-width ⇒ caret per row, ranged ⇒ selection per row; typing / 1-line paste replaces each row; one undo step — behavior matches VS Code.
Bidirectional/reversed box: dragging left of, and back across, the anchor column matches VS Code (per-row direction follows the pointer; live widen/narrow).
Short rows clamp to line end with no document padding; anchor & active visual columns stay sticky so re-widening re-extends short rows (matches VS Code's virtual-column box).
Additional-caret selections render (fixes (b); regression test at the rendering boundary).
Multi-caret Tab/Shift+Tab preserves the primary and per-caret selections (fixes (c); parity with single-caret IndentSelectedLines; carried forward from PR Implements vertical multi-caret support #133) — regression test added.
Esc collapses to the primary caret; plain click clears (matches VS Code).
Keyboard column-select (Ctrl+Shift+Alt+Arrow / PgUp/PgDn, mac Cmd+…) creates/extends a column selection matching VS Code, bound via Editor.DefaultKeyBindings, delivered through TG's Kitty-keyboard-protocol support; commands registered via the post-TG#5318 sanctioned path (no magic-int casts). If split out for sizing it is a tracked follow-up, never a silent drop.
Each real deviation D1, D2, D4, D5 is documented in Docs/Help/multi-caret.mdand the specs/vertical-multi-caret/spec.md "Comparison with VS Code" table; the D3 row records why keyboard column-select is not a deviation (Kitty protocol). No undocumented behavioral deviation ships.
D5 (multi-cursor paste distribution) is filed as its own tracked follow-up issue.
examples/ted demonstrates it end-to-end (R9 — keybindings discoverable, help text updated).
Failing-first integration tests; full suites green; dotnet format + jb cleanup clean.
Docs: specs/vertical-multi-caret/spec.md (move column-select out of Out of Scope; add D1–D5 to the Comparison/Deviations table), specs/multi-caret/spec.md, Docs/Help/multi-caret.md, specs/public-api.md if surface changes.
Summary
Follow-up to #124 / PR #133 (
vertical-multi-caret). PR #133 ships carets-only:Alt+drag andCtrl+Alt+Up/Downcreate a vertical column of bare carets. This issue tracks the deferred column / box selection (the natural next step) and a pre-existing rendering gap it depends on.Parity intent: the end-user behavior of column/box selection must match VS Code. Only input gestures may differ, and only where a terminal makes VS Code's chord physically impossible. Every behavioral deviation is enumerated, categorized, and justified in the Behavioral parity section below — nothing deviates silently.
The spec explicitly scoped this out and deferred it (
specs/vertical-multi-caret/spec.md→ Out of Scope and Intentional Divergences #1;specs/decisions.mdDEC-006). The architectural review on PR #133 confirmed deferral is additive — nothing in PR #133 needs undoing.(a) Column-select during drag
Alt+drag should optionally produce a selection per row (VS Code'sShift+Alt+drag semantics — typing replaces the column), not just carets.The per-caret edit pipeline already supports this — every
MultiCaret*op (MultiCaretInsert/DeleteLeft/DeleteRight/NewLine/InsertTab/Unindent) already branches oncaret.SelectionAnchor→_document.Replace(...). So "type to replace a column" is free once selection anchors are set. The work is placement + rendering. Concrete seams (all additive):Editor.MultiCaret.csAddAdditionalCaretAt(int offset)→ add an overloadAddAdditionalCaretAt(int offset, int selectionAnchorOffset)that also setsCaretInfo.SelectionAnchor(the field already exists). Preserves the PR Implements vertical multi-caret support #133 DoD invariant ("onlyAddAdditionalCaretAt/NormalizeAdditionalCaretsmutate_additionalCarets") via overload, not violation.Editor.MultiCaret.csSetVerticalCaretsFromViewRows(anchorViewRow, activeViewRow, viewColumn)→ thread an active column through (anchor column = press X, already stored in_columnDragAnchor; active column = current drag X). Per row, set a selection from anchor-col offset to active-col offset instead of a bare caret. The rebuild-from-scratch-per-drag-event design already in place is exactly right for live widen/narrow.Editor.csTryGetVerticalOffset(...)→ call it twice per target row (anchor column and active column) to form each row's selection range. Reused as-is. Short rows clamp to line end (see parity D-rules); the visual columns stay sticky/virtual so re-widening re-extends them — no padding is written to the document.Editor.Mouse.cs— sameDragMode.ColumnCaretsstate;_columnDragAnchoralready captures the full press point. No new drag state. (Decide interaction with the open Alt+Click add-caret alias spec decision — see deviation D2.)Editor-local commands for column-select up/down/left/right/page and bind them toCtrl+Shift+Alt+Arrow/PgUp/PgDn(macCmd+…) through the configurableEditor.DefaultKeyBindings, exactly asCtrl+Alt+↑/↓is bound. The handler reuses the sameTryGetVerticalOffset+ sticky-virtual-column machinery as the drag path (anchor column fixed at the primary's column when the gesture starts; active column moves with each keypress), so it shares the row-selection builder with the mouse path. Command-id note: likeInsertCaretAbove/Below, these have noCommandenum member — register via the same sanctioned path PR Implements vertical multi-caret support #133 used post-TG#5318 (real members if added, else tracked by Need mechanism for view-defined Commands - full routing/config/bubbling/bridging/localization parity Terminal.Gui#5320); do not cast magic ints.NormalizeAdditionalCarets/ClearAdditionalCarets/ Esc — unchanged (offset-keyed dedupe still valid for distinct-line column rows; clearing aCaretInfoalready clears itsSelectionAnchor).Modifier stays
Alt(notShift+Alt) for the terminal-compat reasons in DEC-006; configurable mouse modifiers tracked upstream by gui-cs/Terminal.Gui#4888.(b) Render additional-caret selections (pre-existing gap)
Rendering/MultiCaretRenderer.cspaints additional caret cells (reverse-video + blink, post-PR #133) only. It does not paint additional carets' selection spans. Additional-caretSelectionAnchoris consumed by the edit pipeline but never drawn — so the multi-caret spec's FR-005 ("paints all carets and selections") is only partially true today, independent of vertical multi-caret. Any additional-caret selection (column-select, or future per-caret selection gestures) is invisible until this is fixed.IBackgroundRenderer(or extend the existing selection renderer to also iterate_additionalCaretswhoseSelectionAnchoris set), scoped per visual-line segment likeMultiCaretRendereralready does for word-wrap.(c) Carried-forward: selection-preservation parity for multi-caret Tab/Shift+Tab (PR #133 loose end)
Surfaced while fixing the Codex P1 in PR #133: multi-caret
Tab/Shift+Tabblock-indent callsClearAdditionalCaretSelections ()and collapses the primary selection after the edit, whereas the single-caretIndentSelectedLinespath preserves it (viaSetSelectionRangePreservingDirection). This is cosmetic only while additional carets are point-only — but (a) makes per-row selections a first-class column primitive, at which point a column selection vanishing the instant the user pressesTabis a real defect. It also depends on (b) (the selection must render to be observable). Hence it belongs to this PR.Required: multi-caret
Tab/Shift+Tabpreserves the primary selection and every per-caret selection across the block-indent, mirroringIndentSelectedLines'SetSelectionRangePreservingDirectionbehavior per caret. Add a regression test alongsidetests/Terminal.Gui.Editor.IntegrationTests/EditorMultiCaretIndentTests.cs. Tracked in spec:specs/vertical-multi-caret/spec.md§ Out of Scope → Column / box selection (listed as a required deliverable of this PR) andspecs/multi-caret/spec.md§ Out of Scope.Behavioral parity with VS Code (and explicit deviations)
Rule: any column/box-select end-user behavior not listed in the Deviations table below must behave exactly as VS Code does. If implementation forces a new deviation, it must be added to the table (with category + rationale) before merge — never shipped silently.
Must match VS Code (behavior, not keys)
Esccollapses multi-cursor — same effect.)Ctrl+Alt+↑/↓add-caret-above/below already matches VS Code'seditor.action.insertCursorAbove/Below(shipped PR Implements vertical multi-caret support #133). No deviation.Ctrl+Shift+Alt+↑/↓/←/→andCtrl+Shift+Alt+PgUp/PgDncreate/extend a column selection from the keyboard, matching VS Code's Cursor Column Select commands (mac:Cmd+Shift+Alt+…). In scope and must match VS Code — TG implements the Kitty keyboard protocol (progressive enhancement / CSI-u), which disambiguates and delivers these four-modifier chords to the app. On a terminal that does not negotiate the protocol the chord is simply unavailable and the binding is user-overridable — the same environmental bound that applies to every advanced chord (and to the chords PR Implements vertical multi-caret support #133 already ships); it is not a designed behavioral deviation.Deviations — where & why (each MUST be documented in
Docs/Help/multi-caret.mdand the spec Comparison table)Shift+Alt+dragAlt+dragShift+drag for the terminal's own forced/rectangular selection while an app has mouse mode on, soShift+Alt+drag never reaches the app;Alt+drag is forwarded. End-user capability is identical; only the modifier differs.Alt+ClickCtrl+Click (existing)Altis now the column-drag modifier (D1), so any futureAlt+Click alias must be disambiguated fromAlt+drag by a movement threshold. Capability (click to add a caret) is preserved; only the modifier differs.Alt+Click alias is an open spec decisionD3Ctrl+Shift+Alt+Arrow/PgUp/PgDn)editor.multiCursorPaste: N clipboard lines → N cursors one-per-cursor; else full at each)Definition of done
Alt+drag column select: zero-width ⇒ caret per row, ranged ⇒ selection per row; typing / 1-line paste replaces each row; one undo step — behavior matches VS Code.Tab/Shift+Tabpreserves the primary and per-caret selections (fixes (c); parity with single-caretIndentSelectedLines; carried forward from PR Implements vertical multi-caret support #133) — regression test added.Ctrl+Shift+Alt+Arrow/PgUp/PgDn, macCmd+…) creates/extends a column selection matching VS Code, bound viaEditor.DefaultKeyBindings, delivered through TG's Kitty-keyboard-protocol support; commands registered via the post-TG#5318 sanctioned path (no magic-int casts). If split out for sizing it is a tracked follow-up, never a silent drop.Docs/Help/multi-caret.mdand thespecs/vertical-multi-caret/spec.md"Comparison with VS Code" table; the D3 row records why keyboard column-select is not a deviation (Kitty protocol). No undocumented behavioral deviation ships.examples/teddemonstrates it end-to-end (R9 — keybindings discoverable, help text updated).dotnet format+jbcleanup clean.specs/vertical-multi-caret/spec.md(move column-select out of Out of Scope; add D1–D5 to the Comparison/Deviations table),specs/multi-caret/spec.md,Docs/Help/multi-caret.md,specs/public-api.mdif surface changes.Refs: #124, PR #133,
specs/vertical-multi-caret/spec.md,specs/multi-caret/spec.md,specs/decisions.mdDEC-006, gui-cs/Terminal.Gui#4888.🤖 Generated with Claude Code