Skip to content

spec: Shift+Click extends terminal text selection (#9963)#10026

Open
lonexreb wants to merge 1 commit intowarpdotdev:masterfrom
lonexreb:spec/9963-shift-click-extend-selection
Open

spec: Shift+Click extends terminal text selection (#9963)#10026
lonexreb wants to merge 1 commit intowarpdotdev:masterfrom
lonexreb:spec/9963-shift-click-extend-selection

Conversation

@lonexreb
Copy link
Copy Markdown
Contributor

@lonexreb lonexreb commented May 4, 2026

Adds a product+tech spec for #9963: support the standard terminal interaction where Shift+Click extends an existing selection from its original anchor to the clicked point.

Files

  • `specs/GH9963/product.md` (98 lines) — V1 scope, user experience for forward/backward extension, 8 testable behavior invariants
  • `specs/GH9963/tech.md` (136 lines) — implementation outline, mouse-down handler branch, end-to-end flow

Total: 234 insertions. No code changes — this is a spec PR.

Why this is small

The selection model in `app/src/terminal/model/blocks/selection.rs` already represents selections as `head`/`tail` anchor pairs with orientation-agnostic `start_anchor()` / `end_anchor()` getters. Extension is mechanically just "move the head to the click point" — the existing struct handles the orientation-flip case (when the click is before the original anchor) without any new logic.

The implementation surface:

  1. One new branch in the terminal-view mouse-down handler: if `modifiers.shift` AND a selection exists, extend instead of replace.
  2. One small mutator on `Selection` (`set_head_to`) if not already public.
  3. Zero new infrastructure — re-uses the existing `ModelChanged` event, so shared-session broadcasting and "Copy on Select" interactions ride for free.

V1 scope

  • Forward extension: existing selection `(5,10)→(8,30)`, Shift+Click at `(12,5)` → selection becomes `(5,10)→(12,5)`.
  • Backward extension: existing selection `(8,20)→(12,5)`, Shift+Click at `(5,10)` → selection covers `(5,10)→(8,20)` (orientation flips internally; user-visible bounds adjust).
  • Anchor persists across scroll (buffer-coordinates, not viewport).
  • A non-Shift click resets the anchor; subsequent Shift+Clicks extend from the new anchor.
  • Smart-selection (double-click) sets the anchor to the smart-selected unit's start.

Out of V1 (tracked as follow-ups)

  • Shift+Drag to extend an in-progress selection (V1 handles only the discrete click case; existing drag-select behavior is unchanged).
  • Shift+Click parity in the editor / Markdown viewer / settings surfaces (different selection state machines).
  • Shift+Click in rectangular-selection mode (separate semantics).
  • Keyboard-only selection extension in terminal output (Shift+Arrow).

Why this spec

  • Issue is unclaimed (verified via `gh search prs`).
  • Reporter explicitly identifies this as parity with mainstream terminals.
  • Implementation is genuinely small — the data model already supports orientation-agnostic anchor mutation.
  • Aligns with my track record of small, V1-bounded specs.

Open questions for maintainers

  1. Visual cursor feedback during Shift+hover. Spec recommends none (no Shift-specific cursor change). Confirm.
  2. Click-then-Shift-Click latency / debounce. Spec recommends none (match macOS Terminal). Confirm.
  3. Existing `Selection::set_head_to` (or equivalent). If a public mutator already exists, use it; if not, add one.

Happy to iterate further.

Adds product.md and tech.md for issue warpdotdev#9963: support the standard
terminal interaction where Shift+Click extends an existing selection
from its original anchor to the clicked point. Today, Shift+Click
clears or restarts the selection — a regression from every other
terminal emulator.

V1 scope:
- Holding Shift while clicking inside terminal output extends from the
  existing selection's anchor (not its head). Forward and backward
  extension both supported via the existing `Selection::start_anchor`
  / `end_anchor` getters' orientation-agnostic resolution.
- Degenerate cases handled: Shift+Click with no existing selection
  starts a single-point selection (equivalent to a non-Shift click).
- Anchor persistence across scroll: BlockAnchor encodes buffer
  position, not viewport position — invariant 9 holds for free.
- One small mutator on Selection (set_head_to) if not already public,
  plus a single new branch in the terminal-view mouse-down handler.

Out of V1 (tracked as follow-ups):
- Shift+Drag to extend an in-progress selection
- Shift+Click parity in editor / Markdown viewer / settings surfaces
- Shift+Click in rectangular-selection mode
- Keyboard-only selection extension (Shift+Arrow in terminal output)

8 testable behavior invariants. Implementation surface is genuinely
small: one branch in the mouse-down handler, one mutator on Selection
if missing, no new infrastructure. Reuses existing model events so
shared-session broadcasting and "Copy on Select" interactions ride
the existing path.
@cla-bot cla-bot Bot added the cla-signed label May 4, 2026
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 4, 2026

@lonexreb

I'm starting a first review of this spec-only pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@github-actions github-actions Bot added the external-contributor Indicates that a PR has been opened by someone outside the Warp team. label May 4, 2026
Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

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

Overview

This PR adds product and tech specs for terminal Shift+Click selection extension. The desired user behavior is clear, but the tech spec currently directs implementers to mutate the wrong selection endpoint and has several internal inconsistencies around Shift+Drag scope, endpoint side handling, and scroll-persistence validation.

Concerns

  • The implementation guidance inverts the existing head/tail semantics, which would preserve the wrong anchor and break the core feature.
  • V1 scope is inconsistent for Shift+Drag/active-drag behavior.
  • Anchor persistence across scroll is claimed as an invariant but is not actually listed or tested.

Verdict

Found: 1 critical, 3 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread specs/GH9963/tech.md
}
```

The "original anchor" is the `tail` of the existing selection in today's model — the field that wasn't moved by the most recent mouse drag. If the existing selection's `head` is currently before `tail` in document order (i.e. the user dragged backward last time), `set_head_to(click_point)` may flip the orientation; the existing `Selection`'s `start_anchor()` / `end_anchor()` getters resolve the user-visible "from" and "to" points correctly regardless.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚨 [CRITICAL] The current BlockListSelection model treats head as the original click and tail as the endpoint moved by update_selection, so this directs implementers to move the wrong endpoint. Rewrite the spec to preserve head and update tail for Shift+Click.

Comment thread specs/GH9963/product.md

## Non-goals (V1 — explicitly deferred to follow-ups)

- **Shift+Click during an active drag.** V1 handles the discrete click case; a drag started with Shift held continues to extend until release, but the spec does not formalise drag-extension behavior. Existing drag-select continues to work as today.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] This says active-drag Shift behavior is out of scope, then says a drag started with Shift continues to extend until release, while the tech follow-ups say Shift+Drag stays unchanged. Pick one V1 behavior so implementation and tests are unambiguous.

Comment thread specs/GH9963/tech.md
}
```

The `side` field on `BlockAnchor` (left vs right of the column) is preserved; for click-to-extend, keeping the existing head side is the right default — Shift+Click at a column doesn't change the half-column the selection ends on.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] The new endpoint side should come from the Shift+Click hit test, matching the drag update path, not from the previous endpoint. Preserving a stale side makes cell inclusivity depend on the old selection rather than the click location.

Comment thread specs/GH9963/tech.md

### 3. Anchor persistence across scroll

The `BlockAnchor` already encodes a buffer position (block index + grid point), not a viewport position. Scrolling the buffer between the original click-drag and the Shift+Click does NOT invalidate the anchor — invariant 9 in product.md (anchor persistence across scroll) holds for free.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ [IMPORTANT] product.md has only eight invariants and anchor persistence is listed as an open question, so this V1 requirement has no mapped verification path. Add it as a numbered invariant and a test row, or remove the invariant claim.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed external-contributor Indicates that a PR has been opened by someone outside the Warp team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant