Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Neovim

For the full settings schema, see Setup Overview.

Add the following to ~/.config/nvim/lsp/solidity-language-server.lua:

~/.config/nvim/lsp/solidity-language-server.lua
return {
  name = "Solidity Language Server",
  cmd = { "solidity-language-server", "--stdio" },
  filetypes = { "solidity" },
  root_markers = { "foundry.toml", ".git" },
  capabilities = {
    textDocument = {
      semanticTokens = {
        multilineTokenSupport = true,
      },
    },
    workspace = {
      fileOperations = {
        willCreate = true,
        didCreate = true,
        willRename = true,
        didRename = true,
        willDelete = true,
        didDelete = true,
      },
    },
  },
  settings = {
    ["solidity-language-server"] = {
      inlayHints = {
        -- Show parameter name hints on function/event/struct calls.
        parameters = true,
        -- Show gas cost hints on functions annotated with
        -- `/// @custom:lsp-enable gas-estimates`.
        gasEstimates = true,
      },
      lint = {
        -- Master toggle for forge lint diagnostics.
        enabled = true,
        -- Filter lints by severity. Empty = all severities.
        -- Values: "high", "med", "low", "info", "gas", "code-size"
        severity = {},
        -- Run only specific lint rules by ID. Empty = all rules.
        -- Values: "incorrect-shift", "unchecked-call", "erc20-unchecked-transfer",
        --   "divide-before-multiply", "unsafe-typecast", "pascal-case-struct",
        --   "mixed-case-function", "mixed-case-variable", "screaming-snake-case-const",
        --   "screaming-snake-case-immutable", "unused-import", "unaliased-plain-import",
        --   "named-struct-fields", "unsafe-cheatcode", "asm-keccak256", "custom-errors",
        --   "unwrapped-modifier-logic"
        only = {},
        -- Suppress specific lint rule IDs from diagnostics.
        exclude = {},
      },
      fileOperations = {
        -- Auto-generate scaffold for new .sol files.
        -- Set to false to disable scaffold generation.
        templateOnCreate = true,
        -- Auto-update imports via workspace/willRenameFiles.
        updateImportsOnRename = true,
        -- Auto-remove imports via workspace/willDeleteFiles.
        updateImportsOnDelete = true,
      },
      projectIndex = {
        fullProjectScan = true,
        -- Persistent cache mode: "v2"
        cacheMode = "v2",
        -- Aggressive scoped dirty-sync: reindex only reverse-import affected files.
        -- Falls back to full reindex if scoped compile fails.
        incrementalEditReindex = false,
      },
    },
  },
  on_attach = function(client, bufnr)
    vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
 
    -- autoformat
    vim.api.nvim_create_autocmd("BufWritePost", {
      callback = function()
        vim.lsp.buf.format()
      end,
    })
 
    -- completions autotrigger
    vim.lsp.completion.enable(true, client.id, bufnr, {
      autotrigger = true,
      convert = function(item)
        return { abbr = item.label:gsub('%b()', '') }
      end,
    })
 
    -- completion trigger list
    for _, char in ipairs({ "(", ",", "[" }) do
      vim.keymap.set("i", char, function()
        vim.api.nvim_feedkeys(char, "n", false)
        vim.defer_fn(vim.lsp.buf.signature_help, 50)
      end, { buffer = bufnr })
    end
  end,
}

Add the following to ~/.config/nvim/plugin/solidity.lua:

~/.config/nvim/plugin/solidity.lua
vim.lsp.enable("solidity-language-server")

File manager requirement

For templateOnCreate, rename import updates, and delete import updates to work, your file manager must send LSP file operation events:

  • workspace/willCreateFiles
  • workspace/willRenameFiles
  • workspace/willDeleteFiles

If these events are not sent, the server cannot apply those file-operation edits.

Example (oil.nvim):

~/.config/nvim/lua/plugins/oil.lua
require("oil").setup({
  lsp_file_methods = {
    enabled = true,
    timeout_ms = 1000,
    autosave_changes = "all",
  },
})

Neovim note for rename flows

After file renames, if your editor leaves updated imports in modified buffers, run:

:wa

This writes all modified buffers so tools reading from disk (for example forge) see updated imports.

Call Hierarchy

Neovim has built-in call hierarchy support (vim.lsp.buf.incoming_calls(), vim.lsp.buf.outgoing_calls()). However, the default quickfix rendering can be confusing because it mixes the target file name with call-site line numbers.

Add custom handlers to jump to call-site expressions (fromRanges) with just the function name in quickfix text:

~/.config/nvim/plugin/lsp.lua
-- Custom call hierarchy handlers: jump to the call-site expression
-- (fromRanges) rather than the caller/callee function definition.
vim.lsp.handlers["callHierarchy/incomingCalls"] = function(_, result, ctx)
  if not result or vim.tbl_isempty(result) then
    vim.notify("No incoming calls found", vim.log.levels.INFO)
    return
  end
  local items = {}
  for _, call in ipairs(result) do
    local caller = call.from
    local filename = vim.uri_to_fname(caller.uri)
    -- Each fromRange is a call-site expression inside the caller.
    for _, range in ipairs(call.fromRanges or {}) do
      table.insert(items, {
        filename = filename,
        lnum = range.start.line + 1,
        col = range.start.character + 1,
        text = caller.name,
      })
    end
  end
  table.sort(items, function(a, b)
    if a.filename ~= b.filename then return a.filename < b.filename end
    if a.lnum ~= b.lnum then return a.lnum < b.lnum end
    return a.col < b.col
  end)
  vim.fn.setqflist({}, " ", { title = "Incoming Calls", items = items })
  vim.cmd("copen")
end
 
vim.lsp.handlers["callHierarchy/outgoingCalls"] = function(_, result, ctx)
  if not result or vim.tbl_isempty(result) then
    vim.notify("No outgoing calls found", vim.log.levels.INFO)
    return
  end
  -- fromRanges are in the caller item file, not the callee definition file.
  local caller_uri = ctx.params and ctx.params.item and ctx.params.item.uri
  local caller_file = caller_uri and vim.uri_to_fname(caller_uri)
    or vim.api.nvim_buf_get_name(ctx.bufnr)
  local items = {}
  for _, call in ipairs(result) do
    local callee = call.to
    for _, range in ipairs(call.fromRanges or {}) do
      table.insert(items, {
        filename = caller_file,
        lnum = range.start.line + 1,
        col = range.start.character + 1,
        text = callee.name,
      })
    end
  end
  table.sort(items, function(a, b)
    if a.lnum ~= b.lnum then return a.lnum < b.lnum end
    return a.col < b.col
  end)
  vim.fn.setqflist({}, " ", { title = "Outgoing Calls", items = items })
  vim.cmd("copen")
end

If you have lspsaga.nvim, it provides a tree UI that renders call hierarchy correctly out of the box:

:Lspsaga incoming_calls
:Lspsaga outgoing_calls

See the Call Hierarchy reference for full details on what gets tracked and known limitations.

Debugging

Neovim LSP logs are written to:

~/.local/state/nvim/lsp.log

Clear logs:

> ~/.local/state/nvim/lsp.log

Tail logs in real time:

tail -f ~/.local/state/nvim/lsp.log

Enable LSP log levels from Neovim:

" info level
:lua vim.lsp.set_log_level("info")
" trace/debug level
:lua vim.lsp.set_log_level("trace")

Lint options reference

Lint options (severity, rule IDs, only, exclude) are documented in Setup Overview → Lint values.

FAQ

Renaming a .sol file doesn't update imports in Neovim

When you rename a file through a file explorer plugin (for example oil.nvim), the server should update import paths in other files. If this is not happening:

  1. Verify workspace folders are correct.
:lua for _, c in ipairs(vim.lsp.get_clients()) do print(c.name, vim.inspect(c.workspace_folders)) end

You should see an absolute file:// URI, not file://..

  1. Verify your file explorer sends workspace/willRenameFiles.

For oil.nvim, make sure LSP file methods are enabled:

require("oil").setup({
  lsp_file_methods = {
    enabled = true,
    timeout_ms = 1000,
    autosave_changes = "unmodified",
  },
})
  1. Check the Neovim LSP log.
tail -f ~/.local/state/nvim/lsp.log

You should see workspace/willRenameFiles and then a server message with edit counts.

  1. Save all modified buffers after rename.
:wa

willRenameFiles applies edits to buffers, but your editor may not auto-save all touched files to disk.