Skip to content

Instantly share code, notes, and snippets.

@swarn
Last active April 23, 2024 06:31
Show Gist options
  • Star 111 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save swarn/fb37d9eefe1bc616c2a7e476c0bc0316 to your computer and use it in GitHub Desktop.
Save swarn/fb37d9eefe1bc616c2a7e476c0bc0316 to your computer and use it in GitHub Desktop.
Using semantic highlighting in neovim

Semantic Highlighting in Neovim

What is Semantic Highlighting?

And, how is it different than treesitter highlighting? Here's a small example:

treesitter and lsp highlights

In C++, treesitter will highlight member variable declarations with @property and names in the parameter list as @parameter. But when they are used inside the function body, treesitter can't tell the difference between them, so they are all just blue @variable identifiers. Semantic highlighting uses an LSP (clangd, in this case) to show more accurate highlights.

Being able to tell the difference with a glance is useful:

a possible error

You know immediately — without seeing any other code — that something strange is going on with z. Maybe it's just poorly named, or maybe it's shadowing another variable.

Semantic highlighting can do much more. Here's another C++ example that highlights functions and variables by scope:

highlighting scopes

Seeing variable scope at a glance is so useful, many C++ projects use conventions like "prefix member variables with m_." But there isn't a universal convention, and even if there was, people would make mistakes. If you use semantic highlighting, you can simply assign a specific color to member variables.

Highlighting variables by scope is only one option! Instead, you could choose to highlight mutable variables, or async functions, or anything else that an LSP tells you about your code. You probably care about different properties for each language you write in.

Treesitter and semantic highlighting work great together! Treesitter is a fast, in-process parser. It understands the structure of your code, and it will always handle most of the highlighting. An LSP can add more — or more accurate — highlights for some parts of your code, but it is a slower, separate process.

Default Highlighting

Tokens to Highlights

An LSP server that supports semantic highlighting sends "tokens" to the LSP client. A token is data that describes a piece of text. Each token has a type, and zero or more modifiers.

For this C++ code:

//        Let's look at this token ↓
int function(int const p) { return p; }

The LSP tells us that p has a token with type parameter and two modifiers: readonly and functionScope. The default highlighting will apply five highlights to p:

  • @lsp.type.parameter.cpp
  • @lsp.mod.readonly.cpp
  • @lsp.mod.functionScope.cpp
  • @lsp.typemod.parameter.readonly.cpp
  • @lsp.typemod.parameter.functionScope.cpp

In general, it applies:

  • @lsp.type.<type>.<ft> highlight for each token
  • @lsp.mod.<mod>.<ft> highlight for each modifier of each token
  • @lsp.typemod.<type>.<mod>.<ft> highlights for each modifier of each token

You can use the :Inspect command to see what semantic highlights are being applied to your code.

Changing Highlights

Most of these highlight groups will be undefined, so they won't change the appearance of your code. To make parameters purple:

hi @lsp.type.parameter guifg=Purple

Or, with equivalent lua:

vim.api.nvim_set_hl(0, '@lsp.type.parameter', { fg='Purple' })

Just like treesitter highlights, if there is no specific-to-C++ @lsp.type.parameter.cpp group, it will fall back to the @lsp.type.parameter group.

Then, if you want everything which is read-only to be italic:

hi @lsp.mod.readonly gui=italic

If you only want parameters which are read-only to be italic:

hi @lsp.typemod.parameter.readonly gui=italic

To make sure your changes persist after changing colorschemes, wrap them in an autocommand that will reapply them after each colorscheme change:

vim.api.nvim_create_autocmd('ColorScheme', {
  callback = function ()
    vim.api.nvim_set_hl(0, '@lsp.type.parameter', { fg='Purple' })
    vim.api.nvim_set_hl(0, '@lsp.mod.readonly', { italic=true })
  end
})

Be careful to create the autocommand before calling :colorscheme in your init.

The C++ scopes example above can be created with a handful of highlights:

hi @lsp.type.class      guifg=Aqua
hi @lsp.type.function   guifg=Yellow
hi @lsp.type.method     guifg=Green
hi @lsp.type.parameter  guifg=Purple
hi @lsp.type.variable   guifg=Blue
hi @lsp.type.property   guifg=Green

hi @lsp.typemod.function.classScope  guifg=Orange
hi @lsp.typemod.variable.classScope  guifg=Orange
hi @lsp.typemod.variable.fileScope   guifg=Orange
hi @lsp.typemod.variable.globalScope guifg=Red

You probably want to use nicer colors than these!

If your colorscheme doesn't define @lsp.* groups yet, but it does define treesitter highlights, you might find it useful to link the semantic groups to the treesitter groups to get consistent colors:

local links = {
  ['@lsp.type.namespace'] = '@namespace',
  ['@lsp.type.type'] = '@type',
  ['@lsp.type.class'] = '@type',
  ['@lsp.type.enum'] = '@type',
  ['@lsp.type.interface'] = '@type',
  ['@lsp.type.struct'] = '@structure',
  ['@lsp.type.parameter'] = '@parameter',
  ['@lsp.type.variable'] = '@variable',
  ['@lsp.type.property'] = '@property',
  ['@lsp.type.enumMember'] = '@constant',
  ['@lsp.type.function'] = '@function',
  ['@lsp.type.method'] = '@method',
  ['@lsp.type.macro'] = '@macro',
  ['@lsp.type.decorator'] = '@function',
}
for newgroup, oldgroup in pairs(links) do
  vim.api.nvim_set_hl(0, newgroup, { link = oldgroup, default = true })
end

Disabling Highlights

You can disable semantic highlighting by clearing the semantic highlighting groups.

For example, maybe you don't like the semantic highlighting of functions in lua. Disable it with:

vim.api.nvim_set_hl(0, '@lsp.type.function.lua', {})

Or, you can disable all semantic highlights by clearing all the groups:

for _, group in ipairs(vim.fn.getcompletion("@lsp", "highlight")) do
  vim.api.nvim_set_hl(0, group, {})
end

Using LspTokenUpdate for Complex Highlighting

You can apply custom highlights based on semantic tokens using the LspTokenUpdate event. This event is triggered every time a visible token is updated. You can write code to inspect the token, then apply a highlight with the vim.lsp.semantic_tokens.highlight_token function. Here are a few examples:

Highlighting Based on More Than One Modifier

What if I want all global variables that aren't read-only to get a special highlight? I can check the modifiers for the semantic tokens and use whatever logic I want:

vim.api.nvim_create_autocmd("LspTokenUpdate", {
  callback = function(args)
    local token = args.data.token
    if
      token.type == "variable"
      and token.modifiers.globalScope
      and not token.modifiers.readonly
    then
      vim.lsp.semantic_tokens.highlight_token(
        token, args.buf, args.data.client_id, "MyMutableGlobalHL")
    end
  end,
})

vim.api.nvim_set_hl(0, 'MyMutableGlobalHL', { fg = 'red' })

By default, this highlight is higher priority than the standard LSP highlights.

Dealing with Ambiguity

Imagine I have these highlights:

hi @lsp.typemod.variable.globalScope     guifg=Red
hi @lsp.typemod.variable.defaultLibrary  guifg=Green

And I have the following c++:

std::cout << "Hello";

The semantic highlights applied to cout will be:

  • @lsp.type.variable.cpp, priority: 125
  • @lsp.mod.defaultLibrary.cpp, priority: 126
  • @lsp.mod.globalScope.cpp, priority: 126
  • @lsp.typemod.variable.defaultLibrary.cpp, priority: 127
  • @lsp.typemod.variable.globalScope.cpp, priority: 127

There are two different highlights (the last two) with the same priority and different colors. Because of that, there's no way to tell whether cout will be red or green.

One way to fix that is to make sure you use composable highlights. If globalScope is red and defaultLibrary is underlined, then cout will be both red and underlined.

Another alternative is use a callback to apply the highlight you want at a higher priority:

vim.api.nvim_create_autocmd("LspTokenUpdate", {
  callback = function(args)
    local token = args.data.token
    if token.type == "variable" and token.modifiers.defaultLibrary then
      vim.lsp.semantic_tokens.highlight_token(
        token, args.buf, args.data.client_id, "@lsp.mod.defaultLibrary")
    end
  end,
})

Complex Highlighting

You can write highlighting logic that uses more than just the token type and modifiers. Here's an example that highlights variable names written in ALL_CAPS that aren't constant:

local function show_unconst_caps(args)
  local token = args.data.token
  if token.type ~= "variable" or token.modifiers.readonly then return end

  local text = vim.api.nvim_buf_get_text(
    args.buf, token.line, token.start_col, token.line, token.end_col, {})[1]
  if text ~= string.upper(text) then return end

  vim.lsp.semantic_tokens.highlight_token(
    token, args.buf, args.data.client_id, "Error")
end

vim.api.nvim_create_autocmd("LspTokenUpdate", {
  callback = show_unconst_caps,
})

Controlling When Highlights are Applied

The previous example, which highlighted mutable variables, only makes sense for languages that have some way of marking variables as readonly, like const in C++ and Typescript. In languages like Lua or Python, where there is no readonly, that highlight won't work correctly.

Thankfully, there are many ways to control how the highlights are applied:

  • :h autocmd-pattern explains how you can filter autocommands based on file name:

    vim.api.nvim_create_autocmd("LspTokenUpdate", {
      pattern = {"*.cpp", "*.hpp"},
      callback = show_unconst_caps,
    })
  • :h LspTokenUpdate tells you that the client_id is in the args, so you can just return early if it's not an LSP server you want to highlight:

    local function show_unconst_caps(args)
      local client = vim.lsp.get_client_by_id(args.data.client_id)
      if client.name ~= "clangd" then return end
    
      local token = args.data.token
      -- etc
    end
  • You can create buffer-local autocommands (:h autocmd-buflocal) whenever an LSP client attaches to a buffer:

    require('lspconfig').clangd.setup {
      on_attach = function(client, buffer)
        vim.api.nvim_create_autocmd("LspTokenUpdate", {
          buffer = buffer,
          callback = show_unconst_caps,
        })
    
        -- other on_attach logic
      end
    }
    
  • You can also create buffer-local autocommands inside an :h LspAttach event callback:

    vim.api.nvim_create_autocmd("LspAttach", {
      callback = function(args)
        local client = vim.lsp.get_client_by_id(args.data.client_id)
        if client.name ~= "clangd" then return end
    
        vim.api.nvim_create_autocmd("LspTokenUpdate", {
          buffer = args.buf,
          callback = show_unconst_caps,
        })
      end
    })
@swarn
Copy link
Author

swarn commented Apr 26, 2023

@cryptomilk, I agree that having clangd show preprocessor-disabled code is very useful! My point was that clangd isn't highlighting comments; instead, it's using the comment token for this other purpose.

I'm not aware of any LSP that that is better at identifying comments than treesitter1. That's why I don't think LSP comment highlighting is useful. For my own colors, I have @lsp.type.comment cleared, which which means those treesitter highlights don't get overwritten by a LSP (like lua_ls) that does comment highlighting. Also, I have @lsp.type.comment.cpp linked to Comment for the specific case of clangd. I think this gets the same effect you're describing without dealing with highlight priorities.

Footnotes

  1. I'd be interested if anyone has a counter-example!

@cryptomilk
Copy link

It would be possible to add an autocmd which in LspTokenUpdate event checks for lsp comment and sets extmarks with higher priority. It doesn't work right now due to a bug.

However I went down the route and enabled @lsp.type.comment.c which works so far ...

@xbladesub
Copy link

xbladesub commented Jun 3, 2023

this code fixes my colorscheme, but rust constants are still being highlighted as variables

  local links = {
    ['@lsp.type.namespace'] = '@namespace',
    ['@lsp.type.type'] = '@type',
    ['@lsp.type.class'] = '@type',
    ['@lsp.type.enum'] = '@type',
    ['@lsp.type.interface'] = '@type',
    ['@lsp.type.struct'] = '@structure',
    ['@lsp.type.parameter'] = '@parameter',
    ['@lsp.type.variable'] = '@variable',
    ['@lsp.type.property'] = '@property',
    ['@lsp.type.enumMember'] = '@constant',
    ['@lsp.type.function'] = '@function',
    ['@lsp.type.method'] = '@method',
    ['@lsp.type.macro'] = '@macro',
    ['@lsp.type.decorator'] = '@function',
  }
  for newgroup, oldgroup in pairs(links) do
    vim.api.nvim_set_hl(0, newgroup, { link = oldgroup, default = true })
  end

@swarn
Copy link
Author

swarn commented Jun 4, 2023

@xbladesub, I don't have a rust LSP set up, so everything I write here is a guess.

If you position your cursor over a constant name and use :Inspect, I bet you will see that it has both the @lsp.type.variable highlight, and at a higher priority, the @lsp.typemod.variable.readonly highlight. However, the latter highlight is probably not defined (it shows as "links to @lsp").

If that's the case, then you need to define a @lsp.typemod.variable.readonly highlight as described above. This highlight will be higher priority for constants, and override the @variable highlight.

@xbladesub
Copy link

@xbladesub, I don't have a rust LSP set up, so everything I write here is a guess.

If you position your cursor over a constant name and use :Inspect, I bet you will see that it has both the @lsp.type.variable highlight, and at a higher priority, the @lsp.typemod.variable.readonly highlight. However, the latter highlight is probably not defined (it shows as "links to @lsp").

If that's the case, then you need to define a @lsp.typemod.variable.readonly highlight as described above. This highlight will be higher priority for constants, and override the @variable highlight.

  -- Hide semantic highlights for functions
    vim.api.nvim_set_hl(0, '@lsp.type.function', {})
    -- Hide all semantic highlights
    for _, group in ipairs(vim.fn.getcompletion("@lsp", "highlight")) do
     vim.api.nvim_set_hl(0, group, {})
    end

This code solved my issue

@benzwt
Copy link

benzwt commented Jul 3, 2023

It would be possible to add an autocmd which in LspTokenUpdate event checks for lsp comment and sets extmarks with higher priority. It doesn't work right now due to a bug.

However I went down the route and enabled @lsp.type.comment.c which works so far ...

I am new to nvim , would you share your settings ? I can't live without #if 0 #endif

@swarn
Copy link
Author

swarn commented Jul 3, 2023

In ~/.config/nvim/init.lua, add

local function link_comment()
  vim.api.nvim_set_hl(0, "@lsp.type.comment.c", { link = "Comment" })
  vim.api.nvim_set_hl(0, "@lsp.type.comment.cpp", { link = "Comment" })
end

-- Add the lsp comment highlights now, and again later if we change colorschemes.
link_comment()
vim.api.nvim_create_autocmd("ColorScheme", {
  callback = link_comment,
})

@benzwt
Copy link

benzwt commented Jul 3, 2023

In ~/.config/nvim/init.lua, add

local function link_comment()
  vim.api.nvim_set_hl(0, "@lsp.type.comment.c", { link = "Comment" })
  vim.api.nvim_set_hl(0, "@lsp.type.comment.cpp", { link = "Comment" })
end

-- Add the lsp comment highlights now, and again later if we change colorschemes.
link_comment()
vim.api.nvim_create_autocmd("ColorScheme", {
  callback = link_comment,
})

Thanks!

@dhoboy
Copy link

dhoboy commented Jul 19, 2023

Thank you so much @swarn for helping us all out with this. I was going crazy with my highlights before I found your reddit post and this gist ❤️

@tan-wei
Copy link

tan-wei commented Jul 27, 2023

Thanks for this great gist!

But I have a question about a given semantic group. Is there any way to use multiple colors for a given semantic? I am from vs-code which uses ccls as LSP, as here showing, it could be set to use various colors to a given semantic group. For example, for functions, it could choose a random color from the given set. It is quite useful to show various variables (and for the same variable, its color is the same). Maybe my requirement is strange, but I still want to simulate this in Neovim.

Thanks very much.

@swarn
Copy link
Author

swarn commented Jul 27, 2023

Your requirement is not strange at all. There are two different kinds of semantic highlighting;

  • The idea of semantic highlighting is much older than LSP servers and vs-code. Originally, the idea — described in this article, but long predating it — was to make each variable a different color, just like you're looking for. The ccls docs call this "rainbow semantic highlight."
  • The newer, LSP-based semantic highlighting is instead coloring each variable according to some property (like read-only, or local, etc.).

So what you want is the older "rainbow semantic highlighting." The LSP-based highlighting described in this gist is probably not helpful to you. This plugin seems to do what you want, but I have not tried it.

@tan-wei
Copy link

tan-wei commented Jul 28, 2023

@swarn Thanks for your detailed reply.

So I can understand it that way:

  • Semanic highlighting with LSP just does: coloring a group of token whose property is the same with the same one, whchi means all tokens in a given group will be colored with the same color
  • Rainbow semantic highlighting does: coloring the same variable with the same one, which means the tokens in a given group will be colored with different colors

@swarn
Copy link
Author

swarn commented Jul 28, 2023

It is confusing! When people say "semantic highlighting," they can mean either of the two. I have only seen the phrase "rainbow semantic highlighting" on the wiki you linked, but I like it.

So, the first kind of semantic highlighting is easy with neovim, but rainbow semantic highlighting will probably need some kind of plugin.

@tan-wei
Copy link

tan-wei commented Jul 28, 2023

Truly, the "rainbow semantic highlighting" is awesome. For example (credit: C++ 17 - The Complete Guide by Nicolai M. Josuttis):
图片

Obviously:

  1. It is semantic highlighting
  2. For the same variable, they have the same color, but every kind of token group will have different colors

Maybe we need some other plugins (maybe there isn't any now?), or maybe some extra features are needed for neovim to implement this? I don't know.

@swarn
Copy link
Author

swarn commented Jul 29, 2023

The semantic highlighting currently built-in to neovim won't do the rainbow highlighting. However, it should be possible to write a plugin which does so using the current features provided by neovim. Maybe someone already has. Plugins like vim-illuminate can already use LSP or treesitter to highlight all instances of a single variable, so the ability to identify the variable (using a mechanism smarter than regex match) is there.

@mellery451
Copy link

clangd added a new dedicated protocol (message) for inactive region handling. In theory this should make it possible to have syntax highlights as well as dimming of these regions, but I haven't figured out how to make it all work yet.

@jdrouhard
Copy link

That inactive regions stuff uses a different protocol from semantic tokens. It requires advertising support for an off-protocol client capability and implementing a handler for a server-side push notification. This would all be separate from the semantic token stuff in neovim core.

I’m interested in this feature too, so I may work on something simple over the weekend to see if I can make it work.

@swarn
Copy link
Author

swarn commented Sep 23, 2023

Oh, that's interesting. Comments in that thread discuss default-disabling the current approach in clangd 17 and removing it in 18.

@jdrouhard
Copy link

jdrouhard commented Sep 24, 2023

@mellery451 @swarn - The following adds support for this if you want to use the new protocol extension in clangd 17. You just need to hook up the capabilities and the handler function in whatever way you normally do that (lspconfig or manually or whatnot). Dimming existing colors (from syntax or treesitter) isn't possible as that is a neovim limitation, but you can certainly set a different background color to keep treesitter/syntax highlighting of disabled regions but still have visual separation. I've played with CursorColumn and DiagnosticUnnecessary as highlight groups with this. The former changes background color, latter is typically linked to "Comment" (darkens/changes text color).

expand for code snippet
local inactive_ns = vim.api.nvim_create_namespace('inactive_regions')

local inactive_regions_update = function(_, message, _, _)
  local uri = message.textDocument.uri
  local fname = vim.uri_to_fname(uri)
  local ranges = message.regions
  if #ranges == 0 and vim.fn.bufexists(fname) == 0 then
    return
  end

  local bufnr = vim.fn.bufadd(fname)
  if not bufnr then
    return
  end

  vim.api.nvim_buf_clear_namespace(bufnr, inactive_ns, 0, -1)

  for _, range in ipairs(ranges) do
    local lnum = range.start.line
    local end_lnum = range['end'].line

    vim.api.nvim_buf_set_extmark(bufnr, inactive_ns, lnum, 0, {
      line_hl_group = 'ColorColumn', -- or whatever hl group you want
      hl_eol = true,
      end_row = end_lnum,
      priority = vim.highlight.priorities.treesitter - 1, -- or whatever priority
    })
  end
end

local M = {}

M.handlers = {
  ['textDocument/inactiveRegions'] = inactive_regions_update
}

M.capabilities = {
  textDocument = {
    inactiveRegionsCapabilities = {
      inactiveRegions = true,
    },
  },
}

return M

@swarn
Copy link
Author

swarn commented Sep 24, 2023

Nice!

After a testing a while, would it be reasonable to make this a PR to lspconfig?

I guess the trickiest part might be the highlighting — maybe using a highlight ClangdInactive, default linked to DiagnosticUnnecceary?

@jdrouhard
Copy link

Maybe, not sure what their stance is on protocol extensions (vs just minimal working configs). There is a plugin out there that adds support for most of the other clangd extensions where this might be a better fit.

@mellery451
Copy link

mellery451 commented Sep 25, 2023

@jdrouhard thanks for that code snippet. I'm having no luck getting the handler to actually be called even though I'm seeing inactiveRegionProvider set to true in the clangd startup - I'll have to keep digging. Adding this to nvim-lspconfig seems reasonable to me - there are lots of protocol/server specific things in there already.

EDIT: I got it working (thanks again @jdrouhard ). I had to change line_hl_group to just hl_group to get multiline blocks to show with background color..I'm not sure why, but it seems to work better for me. I also set end_row = end_lnum + 1

@jdrouhard
Copy link

Could be something new in neovim 0.10 that isn’t in a released version yet (I follow HEAD). Glad you got it working!

@swarn
Copy link
Author

swarn commented Sep 26, 2023

I hope it can be in lspconfig! When the neovim semantic highlighting first came out, the lack of highlighting for clangd's comment-but-actually-unused tokens was, by far, the biggest issue. Adding the new inactive handler in lspconfig seems like the best away to avoid several years worth of "I just upgraded clangd, why is the inactive highlight not working anymore?"

@swarn
Copy link
Author

swarn commented Oct 9, 2023

I got around to updating to clangd 17 and used this without problem. Thanks @jdrouhard!

I see that if you don't change your capabilities, clangd 17 will still use the Comment tokens. Looking at the PR for the feature, I see that there was discussion of removing this in clangd 18, but I didn't see any conclusions.

@ajoino
Copy link

ajoino commented Dec 10, 2023

Hi Swarn, thanks for writing this guide, it's an incredible resource.

However, I have an issue which I can't figure out from the information given.
I'm using dockerls for Dockerfiles, and it provides semantic highlighting.
However, it overwrites the treesitter injected bash highlighting, which is annoying to say the least.
I've managed to turn off the semantic highlighting for all tokens of type @lsp.type.parameter.dockerfile, but this also disables the highlighting in certain other areas.

So my question is this: Is it possible to disable semantic highlighting for all injected languages in a file?

@swarn
Copy link
Author

swarn commented Dec 11, 2023

Hi @ajoino . That's a good question. I think that a fix is possible, but hacky.

Do I understand correctly that dockerls uses @lsp.type.parameter for the region of bash code? If so, yeah, disabling that highlight removes useful highlights elsewhere.

The mechanism for customization is vim.lsp.semantic_tokens.highlight_token(), but that only has the ability to add a highlight, not disable the default highlight. IIRC, there was some discussion of that feature at the time semantic highlighting was added, but it didn't make it in.

As a workaround, you could use an LspTokenUpdate function that checks if 1) the token is type parameter, and 2) if the underlying treesitter language is bash. If so, it could re-apply the treesitter highlight at higher priority. That's probably easier said than done; the underlying treesitter captures probably don't line up 1:1 with the dockerls tokens.

@jdrouhard , if you have a moment, do you have any thoughts on this? Does it require an issue in the neovim repo?

@ajoino
Copy link

ajoino commented Dec 11, 2023

@swarn thanks for the quick reply.

Indeed, it seems dockerls uses @lsp.type.parameter.dockerfile (Does the language name at the end have any meaning here?) for almost everything after a dockerfile command that doesn't have a special meaning in the dockerfile specs.
This is perhaps a bit too eager on the part of the language server, but

As a workaround, you could use an LspTokenUpdate function that checks if 1) the token is type parameter, and 2) if the underlying treesitter language is bash. If so, it could re-apply the treesitter highlight at higher priority. That's probably easier said than done; the underlying treesitter captures probably don't line up 1:1 with the dockerls tokens.

This is essentially what I've been trying to do but I'm too new at treesitter and the lua config to know where to start. The hack is working for now, but for my own learning could you tell me which command I would use to check the underlying treesitter language?

@swarn
Copy link
Author

swarn commented Dec 11, 2023

The language name at the end allows specialization. If there's no @lsp.type.parameter.dockerfile highlight defined, neovim uses @lsp.type.parameter. So you can define a general parameter highlight and override it with per-language settings.

I'm not a treesitter expert, but I imagine a combination of vim.treesitter.get_captures_at_position() and vim.treesitter.get_node() give you what you need. The capture metadata includes language. The get_node() function by default will ignore injected languages, which is useful here: the entire injection will be something like code_fence_content (this is what it is in markdown; I don't have dockerls.

@jdrouhard
Copy link

jdrouhard commented Dec 12, 2023

I’m not sure it’s possible to opt out entirely of a specific token type not getting extmarks applied. I fought hard for this when we were initially pushing this feature through but some of the core maintainers were strongly against too much customization. I’d have preferred some kind of “interceptor” callback that lets a user decide what to do with tokens or fallback to a default if the callback didn’t deal with it. Instead, we always get defaults and a callback that lets you do additional stuff with a token.

What you could try to do is just disable @lsp.type.parameter.dockerfile (see https://gist.github.com/swarn/fb37d9eefe1bc616c2a7e476c0bc0316#disabling-highlights). Then add an LspTokenUpdate handler that adds a different highlight group for them at a lower priority than treesitter. Finally, you’d have to make sure you theme your new group to the same settings as @lsp.type.parameter.dockerfile was linked to or set as.

This wouldn’t totally disable the highlights in the injected regions, but it would at least allow treesitter highlights to still show up. The “base” would basically become the lsp parameter highlight group instead of the normal base colors. If your LspTokenUpdate handler is fancy enough, it could detect being in a treesitter injection region and just not do anything. Not 100% sure how to do that but @swarn is probably on the right track.

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