Skip to content

Character-level selections#2199

Open
disconcision wants to merge 17 commits intoprobes-IIIfrom
char-selections
Open

Character-level selections#2199
disconcision wants to merge 17 commits intoprobes-IIIfrom
char-selections

Conversation

@disconcision
Copy link
Copy Markdown
Member

@disconcision disconcision commented Mar 30, 2026

  • Char-level selection precision: Shift+Arrow now grows/shrinks selections one character at a time instead of one token at a time (Alt+Shift+Arrow for token-level). Adds anchor_caret field to Selection.t tracking the anchor end position within a boundary token.
  • Selection operations: Destruct, insert, copy, cut, and paste all work correctly with partial-token selections. Highlight rendering clips to sub-token boundaries.
  • Cross-boundary paste fix: When cutting a selection that spans a delimiter (e.g. = in let...=...in) and pasting back, the re-inserted delimiter now reconnects with its ancestor tile via rescan_parent_shards. This is a pre-existing bug fix also ported to deep-reassociate-clean and probes-III.
File Changes
Selection.re anchor_caret field, push/pop/map/toggle plumbing
Select.re local_by_char grow/shrink, vertical/point-to-point char-level
Zipper.re rescan_parent_shards, trim_selected_text, caret/anchor point fixes, no-progress guard
Insert.re Inner-caret handling for insert-over-selection
Highlight.re Sub-token clipping for selection highlight rendering
Keyboard.re Shift+Arrow to ByChar, Alt+Shift+Arrow to ByToken
Test_Editing.re ~700 lines of new tests

Todo

  • Verify char-level selection with projectors at boundaries
  • Test on PC keyboard layout (Ctrl+Shift for ByToken)

disconcision and others added 12 commits March 28, 2026 18:58
- Add Selection.anchor_caret type (Anchor_outer | Anchor_inner(int))
  to track partial-token boundaries at the anchor end of selections
- Implement Select.local_by_char for character-level grow/shrink
- Split Perform.re dispatch: ByChar → local_by_char, ByToken → local
- Update existing tests that used ByChar for piece-level selection
  to use ByToken instead

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Caret.point: compute from piece origin for Inner carets with selection
  content, avoiding generalized_neighbor which unselects
- selection_anchor_point: compute from token origin using anchor_caret
  offset (Inner(n) always indexes left-to-right)
- shrink_by_char crossover: use directional_unselect towards anchor end
  so caret lands at anchor position, not focus position
- Add 12 passing char selection tests (intra-token, from-inner, shrink)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… bindings (Phases 2b-5)

- Fix crossover in shrink_by_char to put content in right siblings for Inner(an)
- Fix grow_by_char Outer case to use Zipper.select (handles ancestor boundaries)
- Update toggle_focus to swap caret ↔ anchor_caret
- Update directional_unselect to restore anchor_caret and handle Inner references
- Shift+Arrow now selects by char; Alt+Shift+Arrow (Mac) / Ctrl+Shift (PC) by token
- 30 char selection tests all passing, no regressions (445 editing tests pass)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add normalize_char_selection to Zipper.re that splits partial boundary
tokens into exterior (kept) and interior (discarded) portions.

Single-piece selections combine remainders into one token with Inner
caret at the seam, so callers like Insert.go can insert within the
combined token. Multi-piece selections place remainders as separate
pieces in siblings.

Wire normalization into Destruct.destruct and Insert.go before
destroy_selection. Internal callers (replace_shard, etc.) skip
normalization to avoid re-entrancy with incidental Inner carets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add clip_char_selection to Highlight.re: clips first/last row boundaries
  for char-level selections based on anchor_caret and caret Inner offsets
- Inline selection highlight pipeline to insert clipping between row
  computation and SVG generation
- Update Select.vertical and Select.to_point to use local_by_char instead
  of local, giving character-level precision for Shift+Up/Down and mouse
  drag selections

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 13 new tests (52 total) covering:
- String literal selections and delete within strings
- Multi-delimiter tile partial selections (let, in keywords)
- Nested structures (nested let, number literals)
- Delete and insert within number literals
- Grow/shrink round-trips within a single token

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy: selected_text_segment trims boundary tokens to only the selected
portion, so copying a partial token (e.g., "ru" from "true") gives the
correct substring instead of the entire token.

Delimiter jump: decompose_multi_shard_neighbor splits multi-shard tiles
(if-then-else, let-in) into individual shards before growing selection,
and push_raw/grow_selection_raw skip reassembly to prevent matching
shards from merging back together inside selection content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The copy path now prints the full selection content, then substrings
it using character offsets from the boundary carets. This replaces
the old approach that split boundary tokens independently, which
gave wrong results when both boundaries were on the same token
(e.g., selecting "ppl" from "apple" gave "pple").

Adds 7 test_copy tests covering intra-token, prefix, suffix, whole
token, single char, and cross-token copy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…k caret jump

replace_shard_inplace bypasses adj_pos→move(Left) which flattened
ancestors when left siblings were empty during Inner-caret token edits.
shrink_by_char now resets caret to Outer after popping a piece from
selection, preventing stale Inner(n) from being interpreted against
the wrong token. Remove debug logging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix paste-into-token crash in let expressions: create
  put_down_no_reassemble to avoid ancestor tile absorption during
  Inner-caret edits (Relatives.reassemble was merging replaced
  tokens back into multi-shard parent tiles like let...=...in)
- Fix comment insertion creating invalid expressions: add direct
  Secondary piece swap path in replace_shard_inplace, bypassing
  the delete/insert cycle that introduced spurious grout
- Add vertical selection chunkiness (Shift+Option for ByToken)
- Update test expectations for standalone comment grout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 2-6 of char-level selection: anchor_caret field, local_by_char
selection growth/shrink, crossover handling, toggle focus, unselect,
keyboard bindings (Shift+Arrow = ByChar, Alt+Shift = ByToken),
destruct/insert over char selections, copy with trim offsets.

Fix cut-paste across delimiter boundaries: rescan_parent_shards walks
ancestor chain to reconnect fresh-ID delimiters with their parent tile.
Uses in-place absorption (try_absorb) instead of delete_parent to
preserve intermediate ancestors like parens.

Add no-progress guard in do_towards_point to prevent infinite loops
during selection operations.

Cross-boundary paste tests, char-selection tests, string/comment
delimiter preservation tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brings in Alt+N keyboard shortcut, sort-dependent variable highlight
colors, and cross-boundary paste fix (from deep-reassociate-clean merge).

Conflict resolution: kept char-selections Keyboard.re (ByChar for
Shift+Arrow, ByToken for Alt+Shift+Arrow) with probes-III code field
and is_new_slide. Kept char-selections Zipper.re and Test_Editing.re
(superset of probes-III versions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
~refractors=z.refractors.manuals,
z.selection.content,
);
Zipper.trim_selected_text(z, full);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

??

@disconcision disconcision requested a review from cyrus- March 30, 2026 15:41
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Codecov Report

❌ Patch coverage is 68.28645% with 124 lines in your changes missing coverage. Please review.
✅ Project coverage is 51.67%. Comparing base (7d76ee7) to head (f2f5785).

Files with missing lines Patch % Lines
src/web/app/editors/decoration/Highlight.re 0.00% 39 Missing ⚠️
src/haz3lcore/zipper/Zipper.re 81.81% 30 Missing ⚠️
src/haz3lcore/zipper/action/Select.re 80.00% 20 Missing ⚠️
src/web/Keyboard.re 0.00% 14 Missing ⚠️
src/haz3lcore/zipper/Selection.re 77.77% 6 Missing ⚠️
src/haz3lcore/zipper/action/Insert.re 85.71% 4 Missing ⚠️
src/haz3lcore/Editor.re 0.00% 2 Missing ⚠️
src/haz3lcore/zipper/CaretBase.re 33.33% 2 Missing ⚠️
src/haz3lcore/zipper/action/Move.re 33.33% 2 Missing ⚠️
src/web/app/editors/code/CodeWithStatics.re 0.00% 2 Missing ⚠️
... and 3 more
Additional details and impacted files
@@              Coverage Diff               @@
##           probes-III    #2199      +/-   ##
==============================================
+ Coverage       51.39%   51.67%   +0.27%     
==============================================
  Files             254      255       +1     
  Lines           30520    30858     +338     
==============================================
+ Hits            15687    15945     +258     
- Misses          14833    14913      +80     
Files with missing lines Coverage Δ
src/haz3lcore/zipper/action/Destruct.re 83.58% <100.00%> (+0.24%) ⬆️
src/web/exercises/examples/Ex_OddlyRecursive.ml 100.00% <ø> (ø)
...rc/web/exercises/examples/Ex_RecursiveFibonacci.ml 100.00% <ø> (ø)
src/web/exercises/examples/ReverseReverse.ml 100.00% <ø> (ø)
src/haz3lcore/zipper/ZipperBase.re 48.48% <0.00%> (-0.05%) ⬇️
src/haz3lcore/zipper/action/Action.re 0.00% <0.00%> (ø)
src/haz3lcore/zipper/action/Perform.re 54.43% <75.00%> (+3.78%) ⬆️
src/haz3lcore/Editor.re 17.30% <0.00%> (ø)
src/haz3lcore/zipper/CaretBase.re 33.33% <33.33%> (ø)
src/haz3lcore/zipper/action/Move.re 65.55% <33.33%> (ø)
... and 7 more

... and 21 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown
Contributor

Performance Benchmarks

⏳ Running... View workflow

disconcision and others added 4 commits March 31, 2026 03:32
mk_remainder_piece now checks Token.is_secondary and creates
Secondary pieces for comment/whitespace tokens. Previously all
remainders were Tiles, causing edited comments to become invalid
expressions. Updated test expectations: comment remainders no
longer fill expression slots (grout ? appears correctly).

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

cyrus- commented Apr 1, 2026

cutting and pasting the following selection does not round trip
image

@cyrus-
Copy link
Copy Markdown
Member

cyrus- commented Apr 1, 2026

modifier key not working to do token-based selection

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.

2 participants