Skip to content

highlighter: Clip stale injection ranges to char boundaries before slicing - #3317

Merged
huacnlee merged 1 commit into
mainfrom
highlighter-stale-injection-range-slice
Sep 29, 2026
Merged

huacnlee merged 1 commit into
mainfrom
highlighter-stale-injection-range-slice

Conversation

@huacnlee

Copy link
Copy Markdown
Member

Summary

Fixes #3315.

A syntax tree can be stale relative to the current text — after a sync parse times out, or after SyntaxHighlighter::edit_tree — so byte ranges taken from it can start or end inside a multi-byte character. 0.7.0 made the tree-sitter read callback byte-safe, but two helpers still sliced the Rope directly at those ranges:

  • markdown_inline_range_has_trigger panicked with byte index is not on a char boundary (no bounds or boundary check). It now clips the range to the text's char boundaries and length before scanning.
  • captured_injection_language checked bounds but not char boundaries. A range that no longer lands on char boundaries cannot name a language, so it now returns None.

No public API changes.

Test

🤖 Generated with Claude Code

…icing

A syntax tree can be stale relative to the current text, so injection ranges
taken from it may start or end inside a multi-byte character. Slicing the
`Rope` there panicked in `markdown_inline_range_has_trigger`.

- `markdown_inline_range_has_trigger` clips the range to the text's char
  boundaries (and length) before scanning.
- `captured_injection_language` returns `None` for a range that does not land
  on char boundaries.

Fixes #3315

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@huacnlee
huacnlee enabled auto-merge (squash) September 29, 2026 11:03
@huacnlee
huacnlee merged commit 4f861e8 into main Sep 29, 2026
11 checks passed
@huacnlee
huacnlee deleted the highlighter-stale-injection-range-slice branch September 29, 2026 11:10
trancong12102 added a commit to aislopware/gpui-kit that referenced this pull request Sep 29, 2026
…bridge#3315)

longbridge#3317 clips the injection ranges that longbridge#3315 found inside multi-byte
characters, which ends the panic but leaves where those ranges came from.
They came from a tree parsed incrementally from a tree that never took the
change: `edit_tree(None, text)` swapped the text and kept the tree, and the
next parse reused its untouched nodes at their old offsets. The result is
not a stale moment but a wrong tree that every later incremental parse
builds on (a heading kept at 0..9 where the text has it at 0..12), so the
colours stay wrong until an edit happens to cover it.

A change without an edit now drops the tree: `edit_tree(None, _)` clears it
like it already cleared the injection layers, and `update` without edits
parses from nothing instead of from the old tree under a made-up edit that
inserted the whole text at 0 (with a (0, 0) end point). The parse with edits
takes the old tree only when there is one, rather than parsing "" first on
every update to stand in for a missing tree. longbridge#3317's clipping stays as a
guard; with this, the ranges it clips come only from a correctly edited tree.

Tests: test_a_change_without_an_edit_is_parsed_afresh compares the tree after
both paths with a fresh parse (fails before: atx_heading 0..9 vs 0..12);
test_stale_tree_styles_snap_to_char_boundaries now makes its stale tree the
way the editor does, a timed-out parse of an edit.
trancong12102 added a commit to aislopware/gpui-kit that referenced this pull request Sep 30, 2026
…bridge#3315)

longbridge#3317 clips the injection ranges that longbridge#3315 found inside multi-byte
characters, which ends the panic but leaves where those ranges came from.
They came from a tree parsed incrementally from a tree that never took the
change: `edit_tree(None, text)` swapped the text and kept the tree, and the
next parse reused its untouched nodes at their old offsets. The result is
not a stale moment but a wrong tree that every later incremental parse
builds on (a heading kept at 0..9 where the text has it at 0..12), so the
colours stay wrong until an edit happens to cover it.

A change without an edit now drops the tree: `edit_tree(None, _)` clears it
like it already cleared the injection layers, and `update` without edits
parses from nothing instead of from the old tree under a made-up edit that
inserted the whole text at 0 (with a (0, 0) end point). The parse with edits
takes the old tree only when there is one, rather than parsing "" first on
every update to stand in for a missing tree. longbridge#3317's clipping stays as a
guard; with this, the ranges it clips come only from a correctly edited tree.

Tests: test_a_change_without_an_edit_is_parsed_afresh compares the tree after
both paths with a fresh parse (fails before: atx_heading 0..9 vs 0..12);
test_stale_tree_styles_snap_to_char_boundaries now makes its stale tree the
way the editor does, a timed-out parse of an edit.
trancong12102 added a commit to aislopware/gpui-kit that referenced this pull request Sep 30, 2026
…bridge#3315)

longbridge#3317 clips the injection ranges that longbridge#3315 found inside multi-byte
characters, which ends the panic but leaves where those ranges came from.
They came from a tree parsed incrementally from a tree that never took the
change: `edit_tree(None, text)` swapped the text and kept the tree, and the
next parse reused its untouched nodes at their old offsets. The result is
not a stale moment but a wrong tree that every later incremental parse
builds on (a heading kept at 0..9 where the text has it at 0..12), so the
colours stay wrong until an edit happens to cover it.

A change without an edit now drops the tree: `edit_tree(None, _)` clears it
like it already cleared the injection layers, and `update` without edits
parses from nothing instead of from the old tree under a made-up edit that
inserted the whole text at 0 (with a (0, 0) end point). The parse with edits
takes the old tree only when there is one, rather than parsing "" first on
every update to stand in for a missing tree. longbridge#3317's clipping stays as a
guard; with this, the ranges it clips come only from a correctly edited tree.

Tests: test_a_change_without_an_edit_is_parsed_afresh compares the tree after
both paths with a fresh parse (fails before: atx_heading 0..9 vs 0..12);
test_stale_tree_styles_snap_to_char_boundaries now makes its stale tree the
way the editor does, a timed-out parse of an edit.
trancong12102 added a commit to aislopware/gpui-kit that referenced this pull request Oct 1, 2026
…bridge#3315)

longbridge#3317 clips the injection ranges that longbridge#3315 found inside multi-byte
characters, which ends the panic but leaves where those ranges came from.
They came from a tree parsed incrementally from a tree that never took the
change: `edit_tree(None, text)` swapped the text and kept the tree, and the
next parse reused its untouched nodes at their old offsets. The result is
not a stale moment but a wrong tree that every later incremental parse
builds on (a heading kept at 0..9 where the text has it at 0..12), so the
colours stay wrong until an edit happens to cover it.

A change without an edit now drops the tree: `edit_tree(None, _)` clears it
like it already cleared the injection layers, and `update` without edits
parses from nothing instead of from the old tree under a made-up edit that
inserted the whole text at 0 (with a (0, 0) end point). The parse with edits
takes the old tree only when there is one, rather than parsing "" first on
every update to stand in for a missing tree. longbridge#3317's clipping stays as a
guard; with this, the ranges it clips come only from a correctly edited tree.

Tests: test_a_change_without_an_edit_is_parsed_afresh compares the tree after
both paths with a fresh parse (fails before: atx_heading 0..9 vs 0..12);
test_stale_tree_styles_snap_to_char_boundaries now makes its stale tree the
way the editor does, a timed-out parse of an edit.
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.

panics on mid-UTF-8-character ranges(highlighter.rs:280)

1 participant