nvim-vtsls

Plugin to help utilize capabilities of vtsls.

NOTE: This plugin is not needed to work with vtsls. It simply offers some extra helper commands and optional improvements. Any server related issue should go up to the upstream.

Usage

Setup server

Through nvim-lspconfig:

require("lspconfig.configs").vtsls = require("vtsls").lspconfig -- set default server config, optional but recommended

-- If the lsp setup is taken over by other plugin, it is the same to call the counterpart setup function
require("lspconfig").vtsls.setup({ --[[ your custom server config here ]] })

Execute commands

:VtsExec {command}

Rename file/folder and update import paths

:VtsRename {from} {to}

Config

This is OPTIONAL. All the fields are also optional.

require('vtsls').config({
  -- customize handlers for commands
  handlers = {
    source_definition = function(err, locations) end,
    file_references = function(err, locations) end,
    code_action = function(err, actions) end,
  },
  -- automatically trigger renaming of extracted symbol
  refactor_auto_rename = true,
  refactor_move_to_file = {
    -- If dressing.nvim is installed, telescope will be used for selection prompt. Use this to customize
    -- the opts for telescope picker.
    telescope_opts = function(items, default) end,
  }
})

Commands

name description
restart_tsserver This not restart vtsls itself, but restart the underlying tsserver.
open_tsserver_log It will open prompt if logging has not been enabled.
reload_projects
select_ts_version Select version of ts either from workspace or global.
goto_project_config Open tsconfig.json.
goto_source_definition Go to the source definition instead of typings.
file_references Show references of the current file.
rename_file Rename the current file and update all the related paths in the project.
organize_imports
sort_imports
remove_unused_imports
fix_all
remove_unused
add_missing_imports
source_actions Pick applicable source actions (same as above)

API

require('vtsls').commands[any_command_name](bufnr, on_resolve, on_reject)
require('vtsls').commands.goto_source_definition(winnr, on_resolve, on_reject) -- goto_source_definition requires winnr
require('vtsls').rename(old_name, new_name, on_resolve, on_reject) -- rename file or folder

-- These callbacks are useful if you want to promisify the command functions to write async code.
function on_resolve() end -- after handler called
function on_reject(msg_or_err) end -- in case any error happens

Other useful snippets

Common settings to enable inlay hints
{
  settings = {
    typescript = {
      inlayHints = {
        parameterNames = { enabled = "literals" },
        parameterTypes = { enabled = true },
        variableTypes = { enabled = true },
        propertyDeclarationTypes = { enabled = true },
        functionLikeReturnTypes = { enabled = true },
        enumMemberValues = { enabled = true },
      }
    },
  }
}
Handler for codelens command
vim.lsp.commands["editor.action.showReferences"] = function(command, ctx)
  local locations = command.arguments[3]
  local client = vim.lsp.get_client_by_id(ctx.client_id)
  if locations and #locations > 0 then
    local items = vim.lsp.util.locations_to_items(locations, client.offset_encoding)
    vim.fn.setloclist(0, {}, " ", { title = "References", items = items, context = ctx })
    vim.api.nvim_command("lopen")
  end
end

Then executing vim.lsp.codelens.run() will open up a quickfix window for references shown by the lens.

Integration to nvim-tree.lua for automatic renamed paths update

Excellent replacement for manually calling :VtsExec rename_file or :VtsRename.

You have two ways. You can use nvim-lsp-file-operations. It has integration with nvim and neo tree. Or you can use the following snippet. It also works for any server supporting workspace/didRenameFiles notification.

local path_sep = package.config:sub(1, 1)

local function trim_sep(path)
  return path:gsub(path_sep .. "$", "")
end

local function uri_from_path(path)
  return vim.uri_from_fname(trim_sep(path))
end

local function is_sub_path(path, folder)
  path = trim_sep(path)
  folder = trim_sep(folder)
  if path == folder then
    return true
  else
    return path:sub(1, #folder + 1) == folder .. path_sep
  end
end

local function check_folders_contains(folders, path)
  for _, folder in pairs(folders) do
    if is_sub_path(path, folder.name) then
      return true
    end
  end
  return false
end

local function match_file_operation_filter(filter, name, type)
  if filter.scheme and filter.scheme ~= "file" then
    -- we do not support uri scheme other than file
    return false
  end
  local pattern = filter.pattern
  local matches = pattern.matches

  if type ~= matches then
    return false
  end

  local regex_str = vim.fn.glob2regpat(pattern.glob)
  if vim.tbl_get(pattern, "options", "ignoreCase") then
    regex_str = "\\c" .. regex_str
  end
  return vim.regex(regex_str):match_str(name) ~= nil
end

local api = require("nvim-tree.api")
api.events.subscribe(api.events.Event.NodeRenamed, function(data)
  local stat = vim.loop.fs_stat(data.new_name)
  if not stat then
    return
  end
  local type = ({ file = "file", directory = "folder" })[stat.type]
  local clients = vim.lsp.get_clients({})
  for _, client in ipairs(clients) do
    if check_folders_contains(client.workspace_folders, data.old_name) then
      local filters = vim.tbl_get(client.server_capabilities, "workspace", "fileOperations", "didRename", "filters")
        or {}
      for _, filter in pairs(filters) do
        if
          match_file_operation_filter(filter, data.old_name, type)
          and match_file_operation_filter(filter, data.new_name, type)
        then
          client.notify(
            "workspace/didRenameFiles",
            { files = { { oldUri = uri_from_path(data.old_name), newUri = uri_from_path(data.new_name) } } }
          )
        end
      end
    end
  end
end)

Credits