Skip to content

Highlight ERB templates as HTML with embedded Ruby - #56

Open
miggyjv wants to merge 2 commits into
warpdotdev:masterfrom
miggyjv:miggyjv/erb-syntax-highlighting
Open

miggyjv wants to merge 2 commits into
warpdotdev:masterfrom
miggyjv:miggyjv/erb-syntax-highlighting

Conversation

@miggyjv

@miggyjv miggyjv commented Nov 10, 2026 •

Copy link
Copy Markdown

Description

Adds syntax highlighting for ERB templates (.erb, .html.erb). The markup outside the tags is highlighted as HTML and the code inside <% %> and <%= %> is highlighted as Ruby. Tag delimiters and <%# %> comments take the comment color, the same color the editor already gives HTML's < and >.

Why this needed more than a new language entry. The highlighter had one grammar and one syntax tree per buffer and no language injection, so a template could only be parsed as one of its two languages. This PR lets a language declare an embedded language:

  • crates/languages: Language gets an optional embedded: EmbeddedLanguage, which names the embedded language and a function that finds the template's tags. erb is registered with HTML as its own grammar and Ruby as its embedded language.
  • crates/syntax_tree: the state keeps SyntaxTrees { host, embedded } per buffer version instead of one Tree. For a template, the host tree parses the text outside the tags and the embedded tree parses the code inside them, both through tree-sitter's included ranges. Edits are applied to both trees and both are reparsed incrementally. Every other language takes the same path with one tree, as before.
  • Highlighting runs the host query, paints the tags with the comment color, clears the code ranges, then runs the embedded query. A host node such as a quoted attribute value can span a tag (class="card <%= kind %>"), so painting the tags also keeps the HTML string color out of the Ruby.
  • app/src/ai/agent: ```erb and ```eruby code blocks in Agent output are highlighted.

Why a scanner and not a tree-sitter ERB grammar. arborium 2.13 has no ERB or embedded-template grammar, and neither does the latest release (2.18.2). The standalone tree-sitter-embedded-template crate compiles C with cc, and arborium's grammars only build for wasm32-unknown-unknown because arborium supplies its own sysroot to its own crates, so an outside C grammar would likely break the wasm jobs. ERB's tag syntax is small, so crates/languages/src/erb.rs finds the tags directly: <% %>, <%= %>, <%== %>, <%- -%>, <%# %>, the <%% and %%> escapes, multi-line tags, and a tag that is still open while you type (it runs to the end of the document, as ERB reads it). No new dependency and no licensing question.

Behavior change: .erb files are now a supported language, so they get an editor language and semantic chunking like .html files do.

Known limitations:

  • Every .erb file is treated as HTML outside its tags, including .js.erb or .yml.erb.
  • The Ruby in all tags is parsed as one program with nothing between the tags, which is how tree-sitter's combined injections work in other editors too. Keywords, strings, symbols and calls are colored correctly, but two tags with no text between them (<% a %><% b %>) read as a b.
  • No ERB-aware indentation or comment toggling. HTML has neither in this editor today.
  • The mechanism is generic, but only ERB uses it here. Vue, Svelte and Markdown fences could use it later.

Linked Issue

Closes #55 (upstream warpdotdev#7901)

  • The linked issue is labeled ready-to-spec or ready-to-implement.
  • Where appropriate, screenshots or a short video of the implementation are included below (especially for user-visible or UI changes).

Upstream warpdotdev#7901 is labeled triaged. It is on the Stanford starter list.

Testing

Automated, all run locally (cargo test -p languages -p syntax_tree, 16 and 22 tests pass; cargo clippy -p languages -p syntax_tree --all-targets --tests -- -D warnings is clean):

  • crates/languages/src/erb_tests.rs: the tag scanner. Each kind of tag, comment tags, multi-line tags, unclosed tags, the escapes, empty tags, plain markup, and multibyte text.
  • crates/languages/src/lib_tests.rs: .erb and .html.erb resolve to ERB, ERB embeds the same cached Ruby language, and all_supported_languages_load_successfully covers the new config.
  • crates/syntax_tree/src/lib_tests.rs: highlights of a fixture view, asserted by color. HTML outside the tags, Ruby inside them, delimiters and comment tags, Ruby keywords in prose staying plain, an attribute string that spans a tag, multibyte text next to a tag, a template with no Ruby, a template that is only tags, an unclosed tag, a visible range that starts and ends inside a tag delimiter, and plain .rb unchanged. Also: the HTML tree has no parse errors because it never sees a tag, the parser's ranges do not leak into the next parse, an incremental reparse after an edit equals a fresh parse, and the range splitting has correct rows and columns.
  • I broke the implementation three ways to check the tests catch it (not resetting the parser's ranges, leaving the tag color on the code, not painting the tags). Each one failed at least one test.
  • Added erb and eruby to test_programming_language_to_extension, which passes (cargo test -p warp --lib test_programming_language_to_extension).

Manual: built warp-oss from this branch and from master and opened the same Rails view in both.

  • I have manually tested my changes locally with ./script/run

I ran the binary from cargo build --bin warp-oss --features gui directly instead of through ./script/run, because cargo-bundle is not installed on my machine.

Screenshots / Videos

Before (master):

erb-before

After (this branch):

erb-after

Agent Mode

  • Warp Agent Mode - This PR was created via Warp's AI Agent Mode

This PR was written with AI assistance (Claude Code). I reviewed the diff, ran the tests and the app myself, and took the screenshots.

CHANGELOG-IMPROVEMENT: Added syntax highlighting for ERB templates (.erb, .html.erb), with HTML outside the tags and Ruby inside them.

🤖 Generated with Claude Code

`.erb` files opened as plain text. They now highlight the markup outside
the tags as HTML and the code inside `<% %>` and `<%= %>` as Ruby. Tag
delimiters and `<%# %>` comments take the comment color.

The highlighter had one grammar and one tree per buffer, so a template
could only be parsed as one of its two languages. A language can now
declare an embedded language and a function that finds its tags. The
syntax tree state then keeps two trees per buffer version: the host tree
parses the text outside the tags and the embedded tree parses the code
inside them, both through tree-sitter's included ranges. Languages
without an embedded language go through the same path with one tree, as
before.

ERB tags are found by a small scanner instead of a tree-sitter grammar.
arborium has no ERB grammar, and a standalone C grammar would need its
own sysroot to build for wasm, which arborium only provides for its own
grammars. ERB's tag syntax is small enough that the scanner covers it:
output, trim and comment tags, the `<%%` and `%%>` escapes, and a tag
left open while typing.

Refs warpdotdev#55

Co-Authored-By: Claude <noreply@anthropic.com>
@warp-factories

Copy link
Copy Markdown

@miggyjv, your GitHub account is not connected to Warp. Connect your GitHub account, then retry GitHub PR opened from warp-terminal-factory.

@miggyjv

miggyjv commented Nov 10, 2026

Copy link
Copy Markdown
Author

/warp-agent-review

@warp-factories

Copy link
Copy Markdown

This PR was generated with Warp.

Comment @warp on this PR to send it follow-up work.

View run View conversation View on Slack

@warp-factories warp-factories Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Overview

Adds ERB (.erb / .html.erb) highlighting by scanning <% %> tags and parsing HTML outside them and Ruby inside them, with erb/eruby fence aliases in Agent output; the design matches #55 and the reported tests and clippy pass. Net position: request changes on comment-rule cleanup before merge.

Verdict

Checks: build pass, tests pass (cargo test -p languages -p syntax_tree: 16 + 21), clippy pass (-D warnings), CI n/a (no checks configured on the branch), visual proof present

Found: 0 critical, 2 important, 2 suggestions, 0 nits

--
Responding as warp-terminal-factory: Open session · View in factory

Comment thread crates/syntax_tree/src/lib.rs Outdated
highlights
}

/// Returns the bytes of the syntax tree that `range` can show. The result is a character wider

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The doc promises "a character wider than range on each side," but the body pads by WIDEST_CHAR (4) buffer bytes and then compares that span to tree-sitter's 0-based included_ranges — a different unit and coordinate space than the comment describes. A later edit that trusts the comment will mis-map the range. Rewrite it to describe the 1-based buffer vs 0-based tree mapping (as char_offset's tree_byte + 1 already does), or drop the padding and do that conversion instead.

Comment thread crates/syntax_tree/src/lib_tests.rs Outdated
#[test]
fn erb_colors_markup_as_html_and_tags_as_ruby() {
check_highlights("show.html.erb", VIEW, |highlights| {
// HTML outside the tags.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These narration comments ("HTML outside the tags", "Ruby inside the tags", ...) only restate the asserts below them, which AGENTS.md's no-line-by-line-narrations rule forbids, and they will drift when the asserts change. Delete them; the test names and assert_eq! lines already carry the meaning.

/// Returns the bytes of the syntax tree that `range` can show. The result is a character wider
/// than `range` on each side, which covers the block marker that the buffer's offsets count and
/// the tree's offsets do not.
fn visible_byte_range(range: &Range<CharOffset>, buffer: &Buffer) -> Range<usize> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

visible_byte_range converts to buffer bytes, pads ±4, and treats the result as tree bytes, so viewport-clipped highlighting is only accidentally correct. Every new test highlights the whole document, so an off-by-one at a range edge would not fail CI. Convert to tree bytes explicitly (buffer_byte.saturating_sub(1)) and add a case that highlights only part of a tag.

Comment thread crates/syntax_tree/src/lib_tests.rs Outdated
);
// The quoted attribute value spans the tag, but its color stays out of the Ruby.
assert_eq!(highlights.color_of("<%= 1 + 2 %>", " + "), None);
assert_ne!(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

assert_ne!(color_of("card_class"), Some(STRING)) still passes if the Ruby overlay is lost entirely and the identifier renders None. The spanning-attribute contract deserves a positive pin: assert the actual expected color rather than "not the HTML string color".

Review feedback on the ERB highlighting.

`visible_byte_range` padded the buffer's byte offsets by four and used
the result as tree offsets, and its comment described a different unit
than the code used. It now converts each end of the range with
`tree_byte`, the inverse of `char_offset`, so no padding is needed.

Every highlight test covered the whole document, so a mistake at the
edge of a range could not fail. A new test highlights a range that
starts and ends inside a tag delimiter and checks that nothing outside
the range is colored.

The spanning-attribute test now asserts the color the Ruby actually
gets instead of only ruling out the HTML string color, and the test
comments that restated their asserts are gone.

Refs warpdotdev#55

Co-Authored-By: Claude <noreply@anthropic.com>
@miggyjv

miggyjv commented Nov 10, 2026

Copy link
Copy Markdown
Author

/warp-agent-review

@warp-factories warp-factories Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Overview

Re-review of e78e56b: the visible range is now mapped to tree bytes explicitly, and all four findings from the previous review are addressed with no new findings. Ready to merge from our side.

Note: HighlightQuery::get_highlighted_chunks still treats buffer bytes as tree bytes — pre-existing, untouched by this PR, and not counted as a finding.

Verdict

Checks: build pass, tests pass (cargo test -p syntax_tree: 22), clippy pass (-D warnings), CI n/a (no checks configured on the branch), visual proof present

Found: 0 critical, 0 important, 0 suggestions, 0 nits

--
Responding as warp-terminal-factory: Open session · View in factory

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ERB (.erb, .html.erb) syntax highlighting (upstream #7901)

1 participant