Neovim
For the full settings schema, see Setup Overview.
Add the following to ~/.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:
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/willCreateFilesworkspace/willRenameFilesworkspace/willDeleteFiles
If these events are not sent, the server cannot apply those file-operation edits.
Example (oil.nvim):
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:
:waThis 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:
-- 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")
endIf you have lspsaga.nvim, it provides a tree UI that renders call hierarchy correctly out of the box:
:Lspsaga incoming_calls
:Lspsaga outgoing_callsSee 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.logClear logs:
> ~/.local/state/nvim/lsp.logTail logs in real time:
tail -f ~/.local/state/nvim/lsp.logEnable 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:
- Verify workspace folders are correct.
:lua for _, c in ipairs(vim.lsp.get_clients()) do print(c.name, vim.inspect(c.workspace_folders)) endYou should see an absolute file:// URI, not file://..
- 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",
},
})- Check the Neovim LSP log.
tail -f ~/.local/state/nvim/lsp.logYou should see workspace/willRenameFiles and then a server message with edit counts.
- Save all modified buffers after rename.
:wawillRenameFiles applies edits to buffers, but your editor may not auto-save all touched files to disk.