Skip to content

language: Only invalidate injections owned by the reparsed layer - #62437

Open
Artin0123 wants to merge 3 commits into
zed-industries:mainfrom
Artin0123:keep-sibling-injection-layers
Open

language: Only invalidate injections owned by the reparsed layer#62437
Artin0123 wants to merge 3 commits into
zed-industries:mainfrom
Artin0123:keep-sibling-injection-layers

Conversation

@Artin0123

@Artin0123 Artin0123 commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Objective

Syntax highlighting inside nested language injections can be lost permanently after editing a nearby line. It only comes back after editing the affected line again or restarting Zed; saving the file does not help.

This is most visible with inline HTML in Markdown, where every paragraph and list item is a separate injection layer:

  • Typing in one list item removes the highlighting of the inline HTML in the items above and below it.
  • Typing at the end of an HTML block removes the highlighting of the inline HTML in the paragraph on the next line.
output.mp4

After a layer is reparsed, SyntaxMap registers its changed rows, expanded by one row in each direction, as invalidated regions for the next nesting depth. Nothing checks that the layers those regions discard actually belong to the reparsed layer. A layer only recomputes its injections when its own content changed, so a neighbouring layer whose text was untouched skips get_injections, and the child layer discarded on its behalf is never recreated.

Solution

Record which layer each ChangedRegion belongs to, and only invalidate layers nested inside of it. A layer owns exactly the injections inside its own range, and it is the only layer that will recreate them.

Two narrower fixes were tried first and are not sufficient:

Change Sibling layers on adjacent rows Layer starting where the reparsed layer ends ITERATIONS=300 random edits
none broken broken passes
clamp the expanded ranges to the reparsed layer's range fixed broken passes
bias the invalidated region's endpoints inwards fixed fixed fails
Only invalidate layers owned by the reparsed layer (this PR) fixed fixed passes

Clamping leaves the second case, because the clamped range and the abutting layer still touch at the same offset. Biasing the endpoints inwards cannot distinguish a child that legitimately begins at that offset from an unrelated layer that does, so it keeps stale layers alive: with that variant, a 300-seed sweep of test_random_syntax_map_edits_rust_macros ends up with three layers surviving where a fresh parse has one. Ownership distinguishes the two: a child is nested inside the owner, an abutting layer is not.

Ownership subsumes clamping, so the expanded ranges themselves are unchanged from main.

#62370 changes splice_included_ranges, which is a different function and a different bug: it produces a wrong range list within a single layer, so a full reparse repairs it. The tests added here fail with it applied.

Testing

  • test_injections_are_preserved_when_a_sibling_layer_is_reparsed edits one Markdown list item and asserts that the inline HTML of the adjacent items keeps its highlighting, for both comments and tags.
  • test_injections_are_preserved_when_an_abutting_layer_is_reparsed edits the last line of an HTML block and asserts that the HTML injected into the following paragraph keeps its highlighting.
  • cargo test -p language (154 tests)
  • ITERATIONS=300 cargo test -p language test_random_syntax_map_edits

Self-Review Checklist:

  • I've reviewed my own diff for quality, security, and reliability
  • Unsafe blocks (if any) have justifying comments
  • The content adheres to Zed's UI standards (UX/UI and icon guidelines)
  • Tests cover the new/changed behavior
  • Performance impact has been considered and is acceptable

Release Notes:

  • Fixed syntax highlighting inside nested language injections being lost after editing a nearby line, such as inline HTML in Markdown.

@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Aug 10, 2026

@dinocosta dinocosta left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hey @Artin0123 🙂

Thank you for suggesting these changes! Was taking a look at the code and I'm suspecting it might be a duplicate of or, at least, attempt to fix the same issue as #62370 .

I'm going to close this Pull Request because of that, but feel free to let me know if there's any detail I might be missing that is contemplated in this Pull Request but not on #62370 . In case that's something that could be easily added to #62370 , feel free to comment there.

Thanks!

@dinocosta dinocosta closed this Aug 11, 2026
@Artin0123

Copy link
Copy Markdown
Contributor Author

@dinocosta Thanks for taking a look, and sorry for the confusion — the symptom in both reports
does look identical ("HTML highlighting in Markdown disappears"), but I believe these
are two independent bugs, and #62370 does not cover any part of this one.

To make sure I wasn't guessing, I measured it. I checked out a single base commit
(b579623, the parent of #62370) and a single test file, and then swapped only
crates/language/src/syntax_map.rs between four versions: neither fix, #62370 only,
this PR only, and both. That way the only variable is the fix itself. Each scenario
starts from a Markdown buffer with the HTML injection queries loaded, applies the
edit, and reports which ranges still carry the comment/tag highlight.

Scenario neither #62370 only this PR only both
A. - one <!--first-->\n- two <!--second-->\n- three <!--third-->, type a space at the end of the first item broken broken fixed fixed
B. same buffer, type a space at the end of the second item broken broken fixed fixed
C. two-line paragraph followed by an HTML block, type a space inside the block comment broken broken fixed fixed
D. hello <em>, then type </em> directly after it (#62127) broken fixed broken fixed
E. hello <!--a-->, then type <!--b--> directly after it broken fixed broken fixed

In the "#62370 only" column, the output for A/B/C is byte-for-byte identical to the
unfixed column: the comments on the neighbouring lines still lose their highlight.
And this PR alone does not fix D/E. Only both together fix all five.

That is because the two changes are in different functions and address different
mistakes:

  • language: Preserve adjacent HTML ranges while editing Markdown #62370 fixes splice_included_ranges, where a new injection range whose start
    equals an existing range's end replaces that existing range. This is a wrong
    range list within a single layer, which is why a full reparse (saving the file)
    restores the highlighting.
  • This PR fixes the invalidation range in reparse_with_ranges. The changed ranges
    are expanded by one row in each direction to catch injection changes, but they are
    not clamped to the layer's own bounds, so they spill onto adjacent rows and discard
    the injections belonging to sibling layers. Those siblings have empty
    changed_ranges (their text was not touched), so they skip get_injections and
    never recreate the discarded layers. This is why saving does not help here —
    only restarting Zed, or editing the affected line itself, brings the highlighting
    back.

The user-visible difference is exactly that: #62127 recovers on save, this one does
not. The regression test added here (test_injections_are_preserved_when_a_sibling_layer_is_reparsed)
fails on top of #62370.

Would you mind reopening this PR? Happy to rebase it onto current main first, or to
adjust the approach if clamping to the layer's range isn't how you'd like to solve it.

@dinocosta

dinocosta commented Aug 17, 2026

Copy link
Copy Markdown
Member

Thank you for taking a thorough look @Artin0123 ! I'll reopen this then 🙂

In the meantime, and maybe the existing test already does this, would you mind adding or updating existing tests to cover the scenarios you've listed that are only fixed by the changes in this Pull Request and not by #62370 ? Thanks!

P.S.: Applied the are:tree-sitter label so a maintainer can eventually take a look at these changes!

@dinocosta dinocosta reopened this Aug 17, 2026
@dinocosta dinocosta added the area:tree-sitter Syntax highlighting and tree-sitter label Aug 17, 2026
@Artin0123

Copy link
Copy Markdown
Contributor Author

Thanks for reopening, @dinocosta! 🎉

I think this is already covered by the test in this PR: test_injections_are_preserved_when_a_sibling_layer_is_reparsed edits one Markdown list item and asserts that the HTML comments in the adjacent items keep their highlighting, which is scenario B from the table.

A and C should come down to the same clamp in reparse_with_ranges: A is the same setup with the space typed on the first row (and since the invalidated range is expanded one row in each direction, editing the middle item already asserts both neighbours), and C only swaps the list items for a paragraph plus an HTML block, so separate tests for them would mostly duplicate the existing one.

If you'd still like them covered explicitly, I'm happy to add the extra edits and assertions, or to restructure the test if there's a shape you'd prefer.

The lost highlighting is not specific to HTML comments: any highlight
produced by the injected layer disappears, so assert an inline tag in an
adjacent list item as well.
Clamping the expanded ranges to the reparsed layer's own range still let the
invalidation discard the injections of a layer that merely begins where the
reparsed layer ends, such as the paragraph following an HTML block in Markdown.
That layer is not reparsed, so its discarded injections are never recreated.

Record which layer each changed region belongs to and skip layers that are not
nested inside of it. This subsumes the clamping, so the expanded ranges are back
to their original form.
@Artin0123 Artin0123 changed the title language: Keep sibling injection layers alive when reparsing a layer language: Only invalidate injections owned by the reparsed layer Aug 29, 2026
@Artin0123

Copy link
Copy Markdown
Contributor Author

Pushed two more commits and rewrote the description, which now stands on its own.

The regression test now asserts an inline tag in addition to the comments, because the loss is not specific to HTML comments — a plain <b> in an adjacent list item goes the same way.

More importantly, the clamping this PR did before only fixes part of the problem. Testing more reports of the same symptom turned up a case it misses: a layer that begins exactly where the reparsed layer ends. Typing at the end of an HTML block still discards the HTML injected into the paragraph on the next line, because the clamped range and that layer touch at the same offset.

The fix now records which layer each changed region belongs to and only invalidates layers nested inside of it, which subsumes the clamping and covers both cases. The description has the comparison with the alternatives I ruled out.

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

Labels

area:tree-sitter Syntax highlighting and tree-sitter cla-signed The user has signed the Contributor License Agreement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants