Skip to content

Improve Vue template token captures for theme consistency#119

Open
ryuhei373 wants to merge 6 commits into
zed-extensions:mainfrom
ryuhei373:fix/attribute-equal-punctuation
Open

Improve Vue template token captures for theme consistency#119
ryuhei373 wants to merge 6 commits into
zed-extensions:mainfrom
ryuhei373:fix/attribute-equal-punctuation

Conversation

@ryuhei373
Copy link
Copy Markdown

Summary

This PR refines several token captures in languages/vue/highlights.scm so that themes can render Vue templates more consistently with how the HTML extension renders plain HTML, and with how Vue's official TextMate grammar (used by VSCode) scopes the same characters.

The motivation comes from a theme distinguishing operator vs. punctuation vs. constant vs. attribute name colors. Today the Vue grammar surfaces several characters under a category that doesn't match their syntactic role:

Token Before After
= between attribute and value @operator @punctuation.delimiter
Longhand directive separator : (e.g. v-bind:click) inherits @property @punctuation.delimiter
Shorthand directive prefixes :, @, # @keyword.directive @punctuation.delimiter
directive_argument (e.g. click, class, default) @constant @attribute

Longhand directive names (v-bind, v-on, v-if, ...) remain @keyword.directive. Each change is its own commit with rationale.

Visual Comparison

Before After
image image
image image

Reference: VSCode (Vue official TextMate grammar)

For comparison, the equivalent scopes in vuejs/language-tools's extensions/vscode/syntaxes/vue.tmLanguage.json (pinned to commit 9e97191):

Each shorthand prefix has its own dedicated punctuation scope in the TextMate grammar; since user themes typically style punctuation.* uniformly, treating them all as @punctuation.delimiter in the tree-sitter highlights captures the same intent.

ryuhei373 and others added 4 commits May 3, 2026 14:44
The "=" between attribute name and value is currently captured as
@operator. This causes themes to color it like a programming
operator (e.g., the "+" in arithmetic), which differs from the HTML
extension where the same character is captured as
@punctuation.delimiter.html.

Aligning Vue with HTML produces more consistent rendering across
themes that distinguish operators from punctuation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The argument of a directive ("click" in "@click", "value" in
":value", "default" in "v-slot:default") is the attribute or event
name being targeted. It is more accurately treated as an attribute
name than as a constant.

Vue's official TextMate grammar (used by VSCode) scopes the same
token as entity.other.attribute-name, which is the same scope used
for plain HTML attribute names. This change brings tree-sitter Vue
in line with that convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In the Vue grammar, shorthand prefixes (":", "@", "#") and longhand
directive names ("v-bind", "v-on", "v-if", ...) are both surfaced
under the same node alias (directive_name). Previously the
highlights query painted both with @keyword.directive, which
produced a "keyword" color for shorthand prefixes that are
syntactically punctuation.

Use #match? predicates on the node text to split the two:
- single-character shorthand prefixes -> @punctuation.delimiter
- "v-*" longhand names -> @keyword.directive (unchanged)

Vue's official TextMate grammar treats shorthand prefixes as
punctuation as well.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…iter

In longhand directive syntax ("v-bind:click", "v-on:keydown",
"v-slot:default", ...), the ":" between the directive name and the
argument is a separator. Previously this anonymous token was not
captured explicitly and inherited @Property from the surrounding
directive_attribute, rendering it as an attribute color.

Vue's official TextMate grammar scopes the same character as
punctuation.separator.key-value.html.vue, treating it as
punctuation. Match that intent by capturing the literal ":" as
@punctuation.delimiter.

In the Vue grammar, ":" only appears as an anonymous token in this
position (the shorthand ":" is a directive_shorthand node, handled
separately), so this rule should not affect other locations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cla-bot cla-bot Bot added the cla-signed label May 3, 2026
Copy link
Copy Markdown
Contributor

@MrSubidubi MrSubidubi left a comment

Choose a reason for hiding this comment

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

Thank you for this, overall looks good! Left some comments.

Comment thread languages/vue/highlights.scm Outdated

(directive_name) @keyword.directive
((directive_name) @punctuation.delimiter
(#match? @punctuation.delimiter "^[:@#]$"))
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.

Can we not use any-of here instead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Comment thread languages/vue/highlights.scm Outdated
Comment on lines 33 to 36
"=" @punctuation.delimiter

":" @punctuation.delimiter

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.

Let's make this a list, please

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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


(directive_argument) @constant
((directive_name) @keyword.directive
(#match? @keyword.directive "^v-"))
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.

Do we intentionally not color some directives anymore? Fine with this, but just wanted to check.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@MrSubidubi
I didn't intend to stop coloring them entirely. directive_name still resolves to either a v-* token or one of the shorthand prefixes (:, @, or #), and both are still being captured—the former as @keyword.directive and the latter as @punctuation.delimiter.

If you're referring to the shorthands losing their @keyword.directive highlighting, that part is indeed intentional. It aligns with Vue's official TextMate grammar, which scopes them under punctuation.attribute-shorthand.* rather than as keywords. If I've missed your point, please let me know which specific cases you had in mind so I can take another look.

ryuhei373 added 2 commits May 15, 2026 22:52
Replace the regex-based #match? predicate with #any-of? for the
shorthand prefix check. The capture matches a fixed finite set of
literals (":", "@", "#"), so #any-of? expresses the intent
directly without going through a regex character class and anchors.

Addresses review feedback on zed-extensions#119.
Combine the two single-literal captures for "=" and ":" into a list
form so the shared @punctuation.delimiter capture name only appears
once. This matches the style already used in the same file for the
bracket tokens ("<", ">", "</", "/>") and makes the intent
"these literals share the same role" explicit.

Addresses review feedback on zed-extensions#119.
@ryuhei373 ryuhei373 requested a review from MrSubidubi May 15, 2026 14:04
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.

2 participants