[SOLVED] Vue 3 + TypeScript + Inlay Hint support in NeoVim
Dan Walsh

Dan Walsh @danwalsh

About: Developer by day, music producer by night. Gaming nerd. Comic enthusiast. All-round good guy. Not as grumpy as everyone says.

Location:
Melbourne, Australia
Joined:
Dec 11, 2020

[SOLVED] Vue 3 + TypeScript + Inlay Hint support in NeoVim

Publish Date: Nov 19 '24
9 9

It would be an understatement to say that getting stable Vue 3 language server support in NeoVim over the last 9-12 months has been smooth sailing for me, as evidenced by this lengthy GitHub issue: volar v2 no longer works.

I'd almost given up on finding "the right" solution. Everywhere I looked, others had "solved" the issue in different ways: they were using the vtsls TypeScript LSP wrapper instead of ts_ls, or they were using coc.nvim, or the now-archived null-ls.nvim. But nowhere could I simply find a reliable configuration for a Vue language server configured natively in NeoVim.

But today, I've finally got it working, and it looks like it should be for good. Massive thanks to Johnson Chu and GR3YH4TT3R93. 👏

Now I can finally say:

Frodo from Lord of the Rings saying,

TLDR, just gimme the config so I can write some sweet Vue code with that LSP-goodness

Since you asked so nicely, sure. Here are the volar and ts_ls server configs for nvim-lspconfig. If you're using something like kickstart.nvim, you'll want to add these entries to your servers table:

local servers = {
  -- Vue 3        
  volar = {},
  -- TypeScript
  ts_ls = {
    filetypes = { 'typescript', 'javascript', 'javascriptreact', 'typescriptreact', 'vue' },
    init_options = {
      plugins = {
        {
          name = '@vue/typescript-plugin',
          location = vim.fn.stdpath 'data' .. '/mason/packages/vue-language-server/node_modules/@vue/language-server',
          languages = { 'vue' },
        },
      },
    },
  },
  -- ...
}
Enter fullscreen mode Exit fullscreen mode

And for bonus points, here's the config if you want inlay hints. For this to work we need to disable hybrid mode:

local servers = {
  -- Vue 3        
  volar = {
    init_options = {
      vue = {
        hybridMode = false,
      },
    },
    settings = {
      typescript = {
        inlayHints = {
          enumMemberValues = {
            enabled = true,
          },
          functionLikeReturnTypes = {
            enabled = true,
          },
          propertyDeclarationTypes = {
            enabled = true,
          },
          parameterTypes = {
            enabled = true,
            suppressWhenArgumentMatchesName = true,
          },
          variableTypes = {
            enabled = true,
          },
        },
      },
    },
  },
  -- TypeScript
  ts_ls = {
    init_options = {
      plugins = {
        {
          name = '@vue/typescript-plugin',
          location = vim.fn.stdpath 'data' .. '/mason/packages/vue-language-server/node_modules/@vue/language-server',
          languages = { 'vue' },
        },
      },
    },
    settings = {
      typescript = {
        tsserver = {
          useSyntaxServer = false,
        },
        inlayHints = {
          includeInlayParameterNameHints = 'all',
          includeInlayParameterNameHintsWhenArgumentMatchesName = true,
          includeInlayFunctionParameterTypeHints = true,
          includeInlayVariableTypeHints = true,
          includeInlayVariableTypeHintsWhenTypeMatchesName = true,
          includeInlayPropertyDeclarationTypeHints = true,
          includeInlayFunctionLikeReturnTypeHints = true,
          includeInlayEnumMemberValueHints = true,
        },
      },
    },
  },
  -- ...
}
Enter fullscreen mode Exit fullscreen mode

Note: this assumes you are using mason.nvim to install and manage your LSPs.

With that in place, you should be able to do all the good things: code completion, go to definitions, rename symbols across files, and tell your co-workers you use NeoVim btw.

Winning

So how did we get here?

I'm glad you asked.

The previous Vue language server versions Vetur and Volar v1 were both troubled with memory duplication issues and the additional complexity of requiring the TypeScript Vue Plugin.

Both the TypeScript language server and the Vue language server were creating duplicate TypeScript abstract syntax trees (AST) in memory, which scaled with the size of your project.

While the TypeScript Vue Plugin had neat features like renaming symbols across .ts and .vue files, it added yet another layer of memory usage.

All of this lead to performance issues and tooling trade-offs, amounting to a sub-optimal development environment.

The solution at last

However, through much hard work and determination, Volar's author Johnson Chu delivered Volar v2 and a new "Hybrid mode" which solves these problems!

Johnson has a great write up on the history of these issues, including some useful visual aids. I would highly recommend you read it.

And it was GR3YH4TT3R93's support and patience in this GitHub issue thread, helping find the right configuration to support not just the newly released Volar v2, but inlay hint support as well.

Comments 9 total

  • DowarDev
    DowarDevDec 30, 2024

    Thanks mate
    LazyVim stopped working for my Vue projects a while ago, so I had to switch to VSCode.
    I was missing vim.

  • Anish Kumar
    Anish KumarJan 5, 2025

    can you provide for react js ? mine typescript suggestion are too much slow

  • Gaëtan Pinot
    Gaëtan PinotJan 27, 2025

    thanks

  • Александр Костарев
    Александр КостаревFeb 8, 2025

    Just registered to offer my thanks. With best regards from Kazan

  • Tom Szwaja
    Tom SzwajaApr 17, 2025

    This got me up and running. Thank You!

    I was pretty happy to see it working since my config seems to be using a different structure from the kickstart one. Freebie.

    In case anyone else is using require('mason-lspconfig').setup_handlers() (maybe because they copied it from Josean :}), I've set it up here.

  • Sam W
    Sam WMay 13, 2025

    Thank you!

    Not using Kickstart, but this is otherwise exactly what I was looking for.

    It didn't work at first, but I tried printing the concatenated string so I could check the location and when it didn't print I realised my setup function wasn't actually running because I'd put it in the wrong place.

    I saw a version of this which uses an npm global-installed Vue language server, but I prefer this code pointing to the Mason-installed one and I wouldn't have arrived at it without your help.

  • Ignacio Badell
    Ignacio BadellJun 25, 2025

    Just in case anyone is reading this and it doesn't work:

    volar has been renamed to vue_ls, so in the servers table you should update the name from volar to vue_ls

    • Harun Sheikhali
      Harun SheikhaliJul 17, 2025

      this doesn't work either with kickstart.nvim..

      1. volar was changed to vue_ls.
      2. vue_ls doesn't work work with ts_ls .. it expects vtsls
      3. vue-language-server recently updated to 3.0.x and its not backward compatible so we have to downgrade to 2.x
      • GR3YH4TT3R93
        GR3YH4TT3R93Jul 18, 2025

        Hi, it's GR3YH4TT3R93 from the GH issue, I don't use kickstart.nvim but depending on your neovim version, this is the native nvim-0.10.0+ way of configuring vue_ls-3.0.x and vtsls:

        (BONUS: includes inlayHints for Typescript AND Vue)

        Place this in ~/.config/nvim/after/lsp/vue_ls.lua

        (or for kickstart.nvim copy the content within return {} and paste it in local servers = {} within the proper vue_ls = {} and vtsls = {} in your kickstart.nvim config e.g. local servers = { vue_ls = { settings = { vue = { inlayHints = { ... } } } } })

        ---@module "vim.lsp.client"
        ---@class vim.lsp.ClientConfig
        return {
          settings = {
            vue = {
              inlayHints = {
                destructuredProps = {
                  enabled = true,
                },
                inlineHandlerLoading = {
                  enabled = true,
                },
                missingProps = {
                  enabled = true,
                },
                optionsWrapper = {
                  enabled = true,
                },
                vBindShorthand = {
                  enabled = true,
                },
              },
            },
          },
        }
        
        Enter fullscreen mode Exit fullscreen mode

        if using pre-0.10.0, put with the rest of your lspconfig setups

        require("lspconfig").volar.setup({
          settings = {
            vue = {
              inlayHints = {
                destructuredProps = {
                  enabled = true,
                },
                inlineHandlerLoading = {
                  enabled = true,
                },
                missingProps = {
                  enabled = true,
                },
                optionsWrapper = {
                  enabled = true,
                },
                vBindShorthand = {
                  enabled = true,
                },
              },
            },
          },
        })
        
        Enter fullscreen mode Exit fullscreen mode

        and for vtsls:

        (again, placed in ~/.config/nvim/after/lsp/vtsls.lua)

        ---@module "vim.lsp.client"
        ---@class vim.lsp.ClientConfig
        return {
          filetypes = {
            "javascript",
            "javascriptreact",
            "typescript",
            "typescriptreact",
            "vue",
          },
          settings = {
            vtsls = {
              tsserver = {
                globalPlugins = {
                  {
                    name = "@vue/typescript-plugin",
                    location = vim.fn.stdpath("data")
                      .. "/mason/packages/vue-language-server/node_modules/@vue/language-server",
                    languages = { "vue" },
                    configNamespace = "typescript",
                  },
                },
              },
            },
            typescript = {
              inlayHints = {
                enumMemberValues = {
                  enabled = true,
                },
                functionLikeReturnTypes = {
                  enabled = true,
                },
                parameterNames = { enabled = "all" },
                parameterTypes = {
                  enabled = true,
                  suppressWhenArgumentMatchesName = true,
                },
                propertyDeclarationTypes = {
                  enabled = true,
                },
                variableTypes = {
                  enabled = true,
                },
              },
            },
          },
        }
        
        Enter fullscreen mode Exit fullscreen mode
Add comment