DEV Community

Ionut Gabriel Marisescu
Ionut Gabriel Marisescu

Posted on

How to setup VueJs in Neovim (January 2025)

Hello, I had issues setting up neovim with vuejs plugins, so I am sharing the knowledge on how I got it to work.

This article assumes basic knowledge on how to add and modify your plugins with LazyVim.

I am using kickstart.nvim.

Packages that we will use : blink.cmp for autocompletion, typescript-tools for the typescript LSP, neovim/nvim-lspconfig to set up the LSP, and Mason so we can install volar.

1) Typescript-tools setup:

return {
  'pmizio/typescript-tools.nvim',
  dependencies = { 'nvim-lua/plenary.nvim', 'neovim/nvim-lspconfig' },
  opts = {},
  ft = { 'javascript', 'javascriptreact', 'typescript', 'typescriptreact', 'vue' },
  config = function()
    require('typescript-tools').setup {
      on_attach = function(client, bufnr)
        client.server_capabilities.documentFormattingProvider = false
        client.server_capabilities.documentRangeFormattingProvider = false
      end,
      filetypes = {
        'javascript',
        'javascriptreact',
        'typescript',
        'typescriptreact',
        'vue',
      },
      settings = {
        tsserver_plugins = {
          '@vue/typescript-plugin',
        },
        jsx_close_tag = {
          enable = true,
          filetypes = { 'javascriptreact', 'typescriptreact' },
        },
      },
    }
  end,
}
Enter fullscreen mode Exit fullscreen mode

This is typescript-tools. It is tsserver, but faster.

In the tsserver_plugins table, we are calling @vue/typescript-plugin.

The recent versions of Volar, it embeds itself in the typescript LSP, providing it with capabilities.

We have to actually install this plugin.
Ideally on a per project basis, I installed it globally as of right now.
npm i -g @vue/typescript-plugin

2) LspConfig:
We are using neovim/nvim-lspconfig to set up the lsp functionalities.

 return {
    'neovim/nvim-lspconfig',
    dependencies = {
      { 'williamboman/mason.nvim', config = true },
      'williamboman/mason-lspconfig.nvim',
      'WhoIsSethDaniel/mason-tool-installer.nvim',
      { 'j-hui/fidget.nvim', opts = {} },
      'saghen/blink.cmp',
    },
    config = function()
      local capabilities = vim.lsp.protocol.make_client_capabilities()
      capabilities = require('blink.cmp').get_lsp_capabilities(capabilities)

      local servers = {
        volar = { 'vue' },
        lua_ls = {
          settings = {
            Lua = {
              completion = { callSnippet = 'Replace' },
            },
          },
        },
      }

      require('mason').setup()

      local ensure_installed = vim.tbl_keys(servers)
      -- vim.list_extend(ensure_installed, { 'stylua' })
      require('mason-tool-installer').setup { ensure_installed = ensure_installed }

      -- Setup mason-lspconfig
      require('mason-lspconfig').setup {
        ensure_installed = ensure_installed,
        handlers = {
          function(server_name)
            local opts = servers[server_name] or {}
            opts.capabilities = vim.tbl_deep_extend('force', {}, capabilities, opts.capabilities or {})
            require('lspconfig')[server_name].setup(opts)
          end,
        },
      }

      -- Setup LspAttach autocmd for keymaps and additional functionality
      vim.api.nvim_create_autocmd('LspAttach', {
        group = vim.api.nvim_create_augroup('kickstart-lsp-attach', { clear = true }),
        callback = function(event)
          local map = function(keys, func, desc, mode)
            mode = mode or 'n'
            vim.keymap.set(mode, keys, func, { buffer = event.buf, desc = 'LSP: ' .. desc })
          end

          map('gd', require('telescope.builtin').lsp_definitions, '[G]oto [D]efinition')
          map('gr', require('telescope.builtin').lsp_references, '[G]oto [R]eferences')
          map('gI', require('telescope.builtin').lsp_implementations, '[G]oto [I]mplementation')
          map('<leader>D', require('telescope.builtin').lsp_type_definitions, 'Type [D]efinition')
          map('<leader>ds', require('telescope.builtin').lsp_document_symbols, '[D]ocument [S]ymbols')
          map('<leader>ws', require('telescope.builtin').lsp_dynamic_workspace_symbols, '[W]orkspace [S]ymbols')
          map('<leader>cr', vim.lsp.buf.rename, '[R]e[n]ame')
          map('<leader>ca', vim.lsp.buf.code_action, '[C]ode [A]ction', { 'n', 'x' })
          map('gD', vim.lsp.buf.declaration, '[G]oto [D]eclaration')

          local client = vim.lsp.get_client_by_id(event.data.client_id)
          if client and client.supports_method(vim.lsp.protocol.Methods.textDocument_documentHighlight) then
            local highlight_group = vim.api.nvim_create_augroup('kickstart-lsp-highlight', { clear = false })
            vim.api.nvim_create_autocmd({ 'CursorHold', 'CursorHoldI' }, 
{
              buffer = event.buf,
              group = highlight_group,
              callback = vim.lsp.buf.document_highlight,
            })
            vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI'}, {
              buffer = event.buf,
              group = highlight_group,
              callback = vim.lsp.buf.clear_references,
            })
          end
        end,
      })
    end,
  },
Enter fullscreen mode Exit fullscreen mode

The most important thing here is that we are passing blink.cmp and that we are setting up volar as our LSP for .vue files.

Open up neovim and install volar.

:MasonInstall vue-language-server
Enter fullscreen mode Exit fullscreen mode

Restart Neovim and this should be it. You should have all the functionalities, go to definition, go to reference, lsp errors etc.

Image description

Autocomplete config

If you have a functional autocomplete plugin, ignore this.
I am going to share my config for blink.cmp in case you need one.

return {
    'saghen/blink.cmp',
    -- dependencies = { 'rafamadriz/friendly-snippets', 'onsails/lspkind.nvim' },
    dependencies = { 'onsails/lspkind.nvim' },

    version = '*',
    ---@module 'blink.cmp'
    ---
    ---@type blink.cmp.Config

    opts = {
      keymap = {
        ['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' },
        ['<C-e>'] = { 'hide', 'fallback' },
        ['<CR>'] = { 'accept', 'fallback' },

        ['<Tab>'] = {
          function(cmp)
            return cmp.select_next()
          end,
          'snippet_forward',
          'fallback',
        },
        ['<S-Tab>'] = {
          function(cmp)
            return cmp.select_prev()
          end,
          'snippet_backward',
          'fallback',
        },

        ['<Up>'] = { 'select_prev', 'fallback' },
        ['<Down>'] = { 'select_next', 'fallback' },
        ['<C-p>'] = { 'select_prev', 'fallback' },
        ['<C-n>'] = { 'select_next', 'fallback' },
        ['<C-up>'] = { 'scroll_documentation_up', 'fallback' },
        ['<C-down>'] = { 'scroll_documentation_down', 'fallback' },
      },

      appearance = {
        use_nvim_cmp_as_default = true,
        nerd_font_variant = 'mono',
      },

      completion = {
        accept = {
          auto_brackets = {
            enabled = true,
          },
        },

        list = {
          selection = function(ctx)
            return ctx.mode == 'cmdline' and 'auto_insert' or 'preselect'
          end,
        },
        --
        menu = {
          border = 'rounded',

          cmdline_position = function()
            if vim.g.ui_cmdline_pos ~= nil then
              local pos = vim.g.ui_cmdline_pos -- (1, 0)-indexed
              return { pos[1] - 1, pos[2] }
            end
            local height = (vim.o.cmdheight == 0) and 1 or vim.o.cmdheight
            return { vim.o.lines - height, 0 }
          end,

          draw = {
            columns = {
              { 'kind_icon', 'label', gap = 1 },
              { 'kind' },
            },
            components = {
              kind_icon = {
                text = function(item)
                  local kind = require('lspkind').symbol_map[item.kind] or ''
                  return kind .. ' '
                end,
                highlight = 'CmpItemKind',
              },
              label = {
                text = function(item)
                  return item.label
                end,
                highlight = 'CmpItemAbbr',
              },
              kind = {
                text = function(item)
                  return item.kind
                end,
                highlight = 'CmpItemKind',
              },
            },
          },
        },
      },
      sources = {
        default = { 'lsp', 'path', 'buffer', 'snippets' },
        cmdline = {},
        providers = {
          lsp = {
            min_keyword_length = 2, -- Number of characters to trigger porvider
            score_offset = 0, -- Boost/penalize the score of the items
          },
          path = {
            min_keyword_length = 0,
          },
          snippets = {
            min_keyword_length = 2,
          },
          buffer = {
            min_keyword_length = 2,
          },
        },
      },
    },
  },
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
codemonster240 profile image
CodeMonster

This was actually really helpful. I usually use a CDN, but use Neovim for when I get tired of VS Code's UI. I Googled how to install Vue.js with Neovim, but all I got was a bunch of tutorials on how to use Vue. Actually, because Neovim is so complicated to use, I'll just use VS code with a CDN.

Wait, I can just use a CDN in Neovim too. :\

Collapse
 
ionut_gabrielmarisescu_f profile image
Ionut Gabriel Marisescu

I also found very few resources on how to get vuejs to work. I was kinda surprised.
I love the clean UI of neovim and vim motions. Only 2 things that are not negotiable imo.