Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/blog/custom-syntax-highlighting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
name: Custom Syntax Highlighting in (Neo)Vim
slug: custom-highlighting
published: 2026-01-17
labels:
- vim
- rust
previewLines: 22
---

*Hopefully this post can be found by someone trying to answer the same questions I was, and maybe save some time.*

Recently while working on closer integration between [Rust and WGSL shader code](/generating-shader-code-1), I looked into what it would take to get syntax highlighting working seamlessly. With a snippet like this:

```rust
fn do_something() -> Option<()> {
// something
None
}

const SHADER_SOURCE: &'static Shader = wgsl!(r#"
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
}

@vertex
fn vs_main(
vertex: ModelVertexData,
instance: InstanceDataWithNormalMatrix,
) -> VertexOutput {
// ...
}
");
```

...we want to be able to edit the "embedded" shader code as seamlessly as possible - it should look like this:

```wgsl
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
}

@vertex
fn vs_main(
vertex: ModelVertexData,
instance: InstanceDataWithNormalMatrix,
) -> VertexOutput {
// ...
}
```

You may have seen this (multiple syntax highlighting languages used in the same file) for things like JSX or HTML with `<style>` tags. One way to accomplish this in modern editors is with [tree-sitter](https://github.com/tree-sitter/tree-sitter), a parser generator library built-in to Neovim and usable in others like VSCode via extensions. Tree-sitter parses the syntax tree of the code in the document, providing a structure for syntax highlighting rules (and others) to operate on.

In this case we want to query for a macro invocation with a particular name (`wgsl`), capturing the string literal argument's contents, and marking them as WGSL for the sake of syntax highlighting. Tree-sitter in Neovim exposes a way to add these custom rules, "injections" ([`:help treesitter-language-injections`](https://neovim.io/doc/user/treesitter.html#_treesitter-language-injections)). There are a number of examples online of setting these up, but I couldn't seem to get the expected results from them and needed to combine details from each:

This [Youtube video from TJ DeVries](https://www.youtube.com/watch?v=v3o9YaHBM4Q) explains the concept well, shows using the tree-sitter playground, and how to operate on Rust - but it's a little outdated now. On my current version of Neovim (v0.11.5), the usage of `:TSPlaygroundToggle` has been replaced with `:InspectTree`. I tested a query in the playground and got the results I wanted, but couldn't get it to load from an injection file `$NVIM_CONFIG_DIR/after/queries/rust/injections.scm` until I added a special comment to the top of the file:

```scheme
;extends

((macro_invocation
macro: [
(scoped_identifier name: (_) @_macro_name)
(identifier) @_macro_name
]
(#eq? @_macro_name "wgsl")
(token_tree (raw_string_literal (string_content) @injection.content))
(#set! injection.language "wgsl"))
)
```

*~/.config/nvim/after/queries/rust/injections.scm*

[This blog post](https://www.josean.com/posts/nvim-treesitter-and-textobjects) has a lot of great information, and mentions the special `; extends` comment. At this point, I have the intended effect when first editing a file that has the matching syntax! ...until it re-parses and is overwritten for some reason, reverting to being treated as a normal Rust string literal. Many examples show setting a "priority" property for injections which seems like exactly what we need, but I couldn't get it to do anything meaningful - nor could I find many people talking about it.

Finally I stumbled on [this Reddit comment](https://www.reddit.com/r/neovim/comments/1iiqwb1/comment/mbg6ivp/) on a post describing my exact problem! As mentioned in the parent comment, `:Inspect` shows two rules matching the node marking it as a string type, which seems to be the issue. The reverting behavior goes away when I add to my Neovim configuration (in this case `after/ftplugin/rust.lua`):

```lua
vim.api.nvim_set_hl(0, '@lsp.type.string.rust', {})
```

Why? It's unsatisfying, but I don't know at this point. I'm just happy to see the expected result, now no longer fleeting.

![A screenshot of code with both Rust and WGSL highlighted](/static/images/blog/custom-syntax-highlighting/success.png)
The fruits of our "labor".

As a cool side effect, this behavior also takes effect when highlighting Rust in other contexts - like the Markdown source for this post.

![A screenshot of Markdown with a Rust code block highlighted, and WGSL properly highlighted within that.](/static/images/blog/custom-syntax-highlighting/markdown.png)
8 changes: 4 additions & 4 deletions src/blog/top-albums-2025.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ I would challenge anyone not to find something unique and compelling here, and i

## [*See You In Heaven*](https://softcult.bandcamp.com/album/see-you-in-heaven) by Softcult

Ok so listen, this is technically an LP re-release of two EPs that came out in 2023 & 2024 - is that against the rules? Probably, but I'm including it anyway so that I can mention *Spoiled* which has been literally on repeat for me, writing this sentence I have to put it on again.
Ok so listen, this is technically an LP re-release of two EPs that came out in 2023 & 2024 - is that against the rules? Probably, but I'm including it anyway so that I can mention *Spoiled* which has been literally on repeat for me, writing this sentence I have to put it on again.

*I'm water, I'm oil, I'm rotten, I'm spoiled*

Expand All @@ -89,7 +89,7 @@ Ok so listen, this is technically an LP re-release of two EPs that came out in 2

<div class="wrapper">

## [*K-Pop Demon Hunters Soundtrack*](https://music.youtube.com/playlist?list=OLAK5uy_mvB1b-5JvguorHZ7EsoKx3jYDq4VDvt04) by... various artists
## [*K-Pop Demon Hunters Soundtrack*](https://music.youtube.com/playlist?list=OLAK5uy_mvB1b-5JvguorHZ7EsoKx3jYDq4VDvt04) by... various artists

The movie slaps and the soundtrack is basically unassailable. Don't @ me (does anyone still say that?)

Expand All @@ -102,7 +102,7 @@ The movie slaps and the soundtrack is basically unassailable. Don't @ me (does a

<div class="wrapper">

## [*2000: In Search Of The Endless Sky*](https://fleshwater.bandcamp.com/album/2000-in-search-of-the-endless-sky) by Fleshwater
## [*2000: In Search Of The Endless Sky*](https://fleshwater.bandcamp.com/album/2000-in-search-of-the-endless-sky) by Fleshwater

I stumbled onto this during a grunge/noise/shoegaze arc that I'm still riding. I can't describe the feeling that *Last Escape* evokes, but it's perfect to me.

Expand All @@ -117,7 +117,7 @@ I stumbled onto this during a grunge/noise/shoegaze arc that I'm still riding. I

<div class="wrapper">

## [*Vaxis - Act III: The Father of Make Believe (New Entities Edition)*](https://music.youtube.com/playlist?list=OLAK5uy_kn6U1AqiBglyYzNbOt2wEqNikOITkT2ss) by Coheed and Cambria
## [*Vaxis - Act III: The Father of Make Believe (New Entities Edition)*](https://music.youtube.com/playlist?list=OLAK5uy_kn6U1AqiBglyYzNbOt2wEqNikOITkT2ss) by Coheed and Cambria

This didn't initially hit me as hard as 2022's [*Vaxis II: A Window of the Waking Mind*](https://music.youtube.com/playlist?list=OLAK5uy_mv2ulNXLwW--0lUT8Iw57XjmlSp55UVSs), but it has definitely grown on me - especially with the added *New Entities* tracks. Coheed and Cambria have been one of my favorite bands for well over a decade and I'm happy to see them continuing to evolve creatively.

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading