# Solidity Language Server > Documentation and benchmarks for solidity-language-server ## Changelog ### v0.1.26 #### Features * File operation behaviors are configurable via: * `fileOperations.templateOnCreate` * `fileOperations.updateImportsOnRename` * `fileOperations.updateImportsOnDelete` * Default file-operation settings are enabled. * Template/scaffolding naming standardized to `templateOnCreate`. #### Fixes * Improve file creation scaffolding flow to avoid missing scaffold content on new files. * Fix duplicate/incorrect scaffold insertion timing during create-file lifecycle. * Improve auto-import completion behavior for top-level symbols and import edit attachment. #### Benchmarks * Added/updated benchmark coverage for file-operation lifecycle flows (`willCreateFiles`, `willRenameFiles`, `willDeleteFiles`) and auto-import scenarios. ### v0.1.25 #### Performance * Replace clone-then-strip with build-filtered-map in `walk_and_extract()` (#132) * `build_filtered_decl()` / `build_filtered_contract()` iterate borrowed node fields and only clone fields that pass the STRIP\_FIELDS filter, skipping heavy subtrees (`body`, `modifiers`, `value`, `overrides`, etc.) * Eliminates 234 MB of transient allocations (629→395 MB total, -37%) * RSS: 310 MB → 254 MB (down from 394 MB pre-optimization) * Pre-size HashMaps with `with_capacity()` in `cache_ids()`, `extract_decl_nodes()`, `build_completion_cache()`, `build_hint_index()`, `build_constructor_index()` (#132) * Remove dead `goto_references()` and `goto_references_with_index()` functions (#132) * Gate `SolcOutput` / `SourceEntry` behind `#[cfg(test)]` (#132) #### Fixes * Fix cross-file references contamination: use `nameLocation` instead of `src` in `resolve_target_location()` so "Find All References" on `IPoolManager manager` returns references to `manager`, not to the `IPoolManager` interface (#131) * Fix non-deterministic hover on inherited contracts: `byte_to_id()` now prefers nodes with `referencedDeclaration` when two nodes share the same span length (#131) #### Memory | State | RSS | vs v0.1.24 | | ------------------- | ---------- | ---------- | | v0.1.24 baseline | 230 MB | — | | Before optimization | 394 MB | +164 MB | | **v0.1.25** | **254 MB** | **+24 MB** | DHAT profiling (poolmanager-t-full.json, 95 files): | Metric | v0.1.24 | v0.1.25 | Delta | | ---------------- | ------- | ------- | --------- | | Total allocated | 629 MB | 395 MB | -37% | | Peak (t-gmax) | 277 MB | 243 MB | -12% | | Retained (t-end) | 60 MB | 60 MB | unchanged | #### Tests * 458 total tests, 0 warnings #### Benchmarks Updated for Shop.sol (all competitors), Pool.sol (v0.1.25 vs v0.1.24), and PoolManager.t.sol (v0.1.25 vs v0.1.24). ### v0.1.24 #### Features * Project-wide source indexing for cross-file references (#115, #119) * Semantic tokens range and delta support * LSP settings configuration (#112) * Benchmark configs with server registry and didChange snapshots (#121) #### Performance * `textDocument/documentLink` returns only import links, not every identifier (#122) * Drop optimizer and conditionally exclude gasEstimates from solc input (#117) #### Fixes * Handle `{value: ...}` / `{gas: ...}` modifier calls in inlay hints and signature help (#125, #116) * Correct signatureHelp cursor positions to inside function call parens * Remap all tests from forge to solc fixture (#123) * CI: checkout submodules so fixture-based tests can find v4-core #### Tests * 466 total tests, 0 warnings #### Benchmark (v4-core Pool.sol) | Method | p95 | | ------------------- | ------ | | initialize | 12.9ms | | completion | 0.4ms | | hover | 23.1ms | | definition | 11.1ms | | references | 19.4ms | | rename | 21.2ms | | inlayHint | 2.8ms | | signatureHelp | 11.7ms | | semanticTokens/full | 3.7ms | Scorecard: **15/18 wins** vs solc, nomicfoundation, juanfranblanco, qiuxiang ### v0.1.23 #### Features * `textDocument/signatureHelp` — shows function signature and active parameter while typing (#110) * Opt-in gas estimates via `@custom:lsp-enable gas-estimates` NatSpec tag (#109) ### v0.1.22 #### Improvements * Use `svm-rs` as a library for solc version management (#106, #105) * `svm::installed_versions()` for discovering installed solc versions * `svm::version_binary()` for resolving solc binary paths * `svm::install()` for auto-installing missing versions (async, native) * No longer shells out to the `svm` CLI or manually walks the filesystem ### v0.1.21 #### Features * Auto-detect solc version from `pragma solidity` and resolve matching binary (#93, #95) * Parses pragma constraints: exact (`0.8.26`), caret (`^0.8.0`), gte (`>=0.8.0`), range (`>=0.6.2 <0.9.0`) * Scans svm-rs and solc-select install directories for matching versions * Auto-installs missing versions via `svm-rs` library * Cross-platform support (macOS, Linux, Windows) * Cached version list (scanned once per session) * Solc version resolution respects both pragma and foundry.toml (#103) * Exact pragmas (`=0.7.6`) always honoured — foundry.toml cannot override * Wildcard pragmas (`^0.8.0`) use foundry.toml version if it satisfies the constraint * No pragma falls back to foundry.toml, then system solc * Foundry config support for compiler settings (#103) * Reads `via_ir`, `optimizer`, `optimizer_runs`, `evm_version` from `foundry.toml` * Passes settings to solc standard JSON (`viaIR`, `optimizer`, `evmVersion`) * Reads `ignored_error_codes` to suppress matching diagnostics * Fixes "Stack too deep" errors for projects requiring `via_ir` (e.g. EkuboProtocol/evm-contracts) * Callsite parameter documentation on hover (#103) * Hovering over arguments in function/event calls shows `@param` doc from the called definition * Uses tree-sitter on the live buffer to find enclosing call and argument index * Resolves via `HintIndex` (exact offset or `(name, arg_count)` fallback) for param name and `decl_id` * Looks up `@param` doc from `DocIndex` or raw NatSpec with `@inheritdoc` resolution * Gas estimates in hover, inlay hints, and code lens (#91, #94) * `GasIndex` built from solc contract output (creation + external/internal costs) * Hover shows gas cost for functions and deploy cost for contracts * Fire icon (🔥) with formatted numbers (e.g. `125,432`) * Use solc directly for AST + diagnostics, 11x faster on large projects (#90) * Use solc userdoc/devdoc for hover documentation (#99) * `DocIndex` built from solc contract output at cache time with pre-resolved `@inheritdoc` * Hover on parameters and return values shows `@param`/`@return` docs from parent function * Works at both declaration site and any usage inside the function body * Structured rendering: notice, `@dev` details, params table, returns table * Typed selectors: `FuncSelector`, `EventSelector`, `Selector` enum, `MethodId` newtype * Replaces raw `String` selectors throughout gas, hover, completion, and inlay hints #### Refactor * Gas inlay hints use tree-sitter positions from the live buffer (#96) * Fixes hints drifting to wrong positions during editing * Function gas hints support opening/closing brace placement (`FN_GAS_HINT_POSITION` constant) * Contract deploy hints show `codeDepositCost` when `totalCost` is infinite * Libraries and interfaces now show deploy cost hints * Remove code lens — gas info is covered by inlay hints and hover (#96) #### Fixes * Improve natspec tag formatting in hover (#98) * `@dev` now renders with a bold `**@dev**` header above italic content * `@custom:` tags and other unknown `@` tags render with bold label and italic content * Bound `foundry.toml` search at git repo root (#89) * Hover works when file has compilation errors (#92) #### Tests * 423 total tests, 0 warnings ### v0.1.20 #### Features * Tree-sitter enhanced goto definition (#66, #79) * Inlay hints v2 — tree-sitter positions + AST semantics (#61, #81) * Respect `foundry.toml` lint ignore and `lint_on_build` config (#84, #87) * Introduce `NodeId`/`FileId` newtypes and shared `SourceLoc` parser (#86) #### Fixes * Goto definition returns wrong result after unsaved edits (#83) * Bound `foundry.toml` search at git repo root (#89) ### v0.1.19 #### Refactor * Rewrite `textDocument/documentSymbol` and `workspace/symbol` to use tree-sitter instead of Forge AST (#77, #78) * Symbols no longer depend on `forge build` — works on any Solidity file immediately * `documentSymbol` reads from text\_cache with disk fallback * `workspace/symbol` scans open files only * Clean up semantic tokens to avoid overriding tree-sitter highlights (#78) * Remove modifiers, builtin types, pragmas, variables, and member expressions from LSP tokens * Prevents `@lsp.typemod.*` priority 126-127 from overriding tree-sitter colors #### Performance * `textDocument/documentSymbol` 3.2x faster (3.24ms → 1.02ms) * `workspace/symbol` 6.4x faster (6.08ms → 0.95ms) #### Features * Add `textDocument/semanticTokens/full` via tree-sitter (#75, #76) #### Notes ##### Symbol kinds The `documentSymbol` response returns hierarchical symbols with the following kind mappings: | Solidity construct | LSP SymbolKind | | ---------------------- | --------------- | | `contract` | CLASS | | `interface` | INTERFACE | | `library` | NAMESPACE | | `function` | FUNCTION | | `constructor` | CONSTRUCTOR | | `fallback` / `receive` | FUNCTION | | `state variable` | FIELD | | `event` | EVENT | | `error` | EVENT | | `modifier` | METHOD | | `struct` | STRUCT | | `struct member` | FIELD | | `enum` | ENUM | | `enum value` | ENUM\_MEMBER | | `using ... for` | PROPERTY | | `type ... is ...` | TYPE\_PARAMETER | | `pragma` | STRING | | `import` | MODULE | Functions include a detail string with parameters and return types (e.g. `(address to, uint256 amount) returns (bool)`). Contracts, structs, and enums are returned as parent symbols with their members nested as children. Top-level declarations (pragma, import, free functions, free structs/enums) appear at root level. ##### Semantic tokens and tree-sitter coexistence LSP semantic tokens in Neovim have higher priority (125-127) than tree-sitter highlights (100). When both emit tokens for the same range, LSP wins. This causes problems when `@lsp.typemod.*` groups fall back to the generic `@lsp` highlight with undesirable colors. The approach taken here: only emit semantic tokens where the LSP adds value that tree-sitter cannot provide. Let tree-sitter handle syntax it already highlights well (builtins, variables, member access, pragmas, modifiers). The LSP focuses on declaration identifiers, parameters, type references, and call targets where semantic knowledge matters. ### v0.1.18 #### Features * Context-sensitive `type(X).` completions (#70) * Typing `type(ContractName).` now shows `creationCode`, `runtimeCode`, `name`, `interfaceId` * Typing `type(IntegerType).` shows `min`, `max` #### Fixes * Skip using-for completions on contract/library/interface names (#71, #72) * `Lock.` no longer returns Pool and SafeCast library functions from `using Pool for *` and `using SafeCast for *` * Using-for completions now only appear when completing on a value of a matching type, not on a contract/library/interface name * Fix SIMD chunk selection skipping past target column in `position_to_byte_offset` (#73, #74) * The SIMD-accelerated position calculation introduced in #68 could pick a chunk boundary past the target column on long lines, returning the wrong byte offset * Go-to-definition on `AlreadyUnlocked` in PoolManager.sol resolved to `revertWith` in CustomRevert.sol instead of the correct `error AlreadyUnlocked()` in IPoolManager.sol #### Performance * SIMD-accelerated position calculation via `lintspec-core` TextIndex (#68) * `position_to_byte_offset` and `byte_offset_to_position` now use a single SIMD pass over 128-byte chunks instead of a full linear scan * Short linear walk (at most 128 bytes) from the nearest chunk to the exact position #### Refactor * Rewrite position conversion to use `lintspec-core` `compute_indices` and `TextIndex` (#64, #68) * Simplify conversion functions, use builtin traits and constructors * Improved identifier validation for Solidity keywords ### v0.1.17 #### Fixes * Simplify diagnostic path matching to fix silent drop (#63) ### v0.1.16 #### Features * Scope-aware completion with inheritance resolution (#57) * Completions are now filtered by the current scope (contract, function, block) * Inherited members from parent contracts are resolved and included * Replaces the previous `fast`/`full` completion mode split with a single unified engine #### Fixes * Use relative path to filter out diagnostics (#55) * Build diagnostic filtering now correctly matches files using relative paths * Fixes cases where diagnostics from dependency files were incorrectly included #### Deprecations * `--completion-mode` flag is deprecated (#59) * The `fast`/`full` split is no longer needed — scope-aware completions are always active #### New Contributors * [@libkakashi](https://github.com/libkakashi) — chore: add Zed editor setup section in docs (#60) #### Other * Refactor build module: simplify diagnostic filtering, extract path comparison helper * Add Zed editor setup section in docs (#60) * 272 total tests, 0 warnings ### v0.1.15 #### Fixes * AST cache now updates when build has warnings but no errors (#41) * The `build_succeeded` check used `diagnostics.is_empty()` which blocked cache updates for files with unused variables or other warnings * Changed to only block on `DiagnosticSeverity::ERROR`, so warnings pass through * Cross-file rename reads from in-memory editor buffers instead of disk (#50) * `rename_symbol` accepts `text_buffers` parameter reflecting unsaved editor state * No more disk writes behind the editor's back * Full `WorkspaceEdit` returned to client for all files (#50) * Previously split edits between client (current file) and server-side `fs::write` (other files) * Now the complete edit set is returned to the client * `nameLocations` index fallback in references (#50) * Nodes without `nameLocations` array now correctly fall through to `nameLocation` or `src` * Stale AST range correction during rename (#50) * `find_identifier_on_line` scans the current line to correct shifted column positions after unsaved edits * All LSP handlers read source from `text_cache` instead of `std::fs::read` (#50) * Respect `includeDeclaration` in `textDocument/references` (#49) * Use cached AST for `workspace/symbol` instead of rebuilding (#46) * Clear caches on `did_close` to free memory (#45) * Encoding-aware UTF-16 position conversion (#39) * Remove document version when publishing diagnostics (#40) * Pass `--ignore-eip-3860` and `--ignored-error-codes 5574` to forge build (#11) #### Features * Announce full version string in LSP `initialize` response (#51) * e.g. `0.1.15+commit.abc1234.macos.aarch64` #### Tests * 10 new regression tests for bugs fixed in #41 and #50 * `tests/build.rs`: warning-only builds succeed, error builds fail, empty diagnostics succeed * `tests/rename.rs`: nameLocations fallback, text\_buffers usage, cross-file WorkspaceEdit, stale AST correction, identifier extraction * Solidity fixture files in `example/` for rename tests (A.sol, B.sol, C.sol, Counter.sol) * 186 total tests, 0 warnings #### New Contributors * [@beeb](https://github.com/beeb) — fix: remove document version when publishing diagnostics (#40), filed issues #32, #33, #34, #35, #36, #37, #38, #41 #### Other * `rustfmt.toml` and `cargo fmt` across codebase (#42) * Benchmark config updated with all implemented LSP methods ### v0.1.14 #### Fixes * `textDocument/definition` and `textDocument/declaration` now return proper range width instead of zero-width ranges (#30) * `goto_bytes()` returns `(file_path, byte_offset, length)` — extracts the length field from `nameLocation` or `src` * `goto_declaration()` computes `end` from `byte_offset + length`, so editors correctly highlight the target symbol * Previously `start == end` in the returned `Location`, making it impossible for the editor to highlight the target #### Tests * 4 new goto range-length tests: `Hooks` (len 5), `Pool` (len 4), `SafeCast` (len 8), Yul external reference (nonzero) * 168 total tests, 0 warnings ### v0.1.13 #### Features * `textDocument/documentLink` — every reference in a file is a clickable link * Import paths link to the imported file (resolves `absolutePath` from AST) * All nodes with `referencedDeclaration` link to their definition via `id_to_location` * Uses pre-indexed `CachedBuild.nodes` — no extra AST traversal at request time #### Fixes * `--version` now shows commit hash when installed from crates.io (reads `.cargo_vcs_info.json` as fallback) #### Tests * 11 document link tests (CI-safe, real fixture data from `pool-manager-ast.json`) * 164 total tests, 0 warnings ### v0.1.12 #### Features * Cross-file "Find All References" — scans all cached AST builds to find usages across files that don't share a build scope * Cross-file "Rename" — renames symbols across all cached builds, not just the current file's dependency tree * `CachedBuild` struct — pre-computes `cache_ids()` once per cache insert instead of N+1 times per request #### Performance * `cache_ids()` no longer called at request time — all node indexing happens on file save * `get_or_fetch_build()` deduplicates cache-miss logic across goto, references, rename, hover, and document symbol handlers #### Tests * 12 new cross-file reference tests (CI-safe, hardcoded AST values from fixture) * 153 total tests, 0 warnings ### v0.1.11 #### Features * `--version` / `-V` flag with full build metadata: version, commit hash, OS, architecture * GPG-signed release checksums for binary verification * `CONTRIBUTING.md` with project structure and development workflow #### Improvements * Remove redundant timestamp from tracing log output * Add `build.rs` to embed git commit hash at compile time * Add `public-key.asc` for release signature verification * Updated README with CLI usage examples, all flags, and verification instructions ### v0.1.10 #### Features * `textDocument/hover` — show Solidity signatures, NatSpec documentation, and selectors on hover * Signature generation for functions, contracts, structs, enums, errors, events, modifiers, variables, UDVTs * NatSpec formatting: `@notice`, `@param`, `@return`, `@dev` rendered as structured markdown * Display `functionSelector`, `errorSelector`, `eventSelector` from AST in hover output * `@inheritdoc` resolution via `functionSelector` matching between implementation and parent interface — correctly handles overloaded functions * 25 hover tests against Uniswap v4 PoolManager AST ### v0.1.8 #### Features * Full completion engine with chain resolution, using-for directives, and type casts (\~1400 lines, 86 tests) * `--completion-mode` flag: `fast` (default) pre-built completions, `full` per-request scope filtering * Dot-completion for structs, contracts, libraries, magic globals (`msg`, `block`, `tx`, `abi`, `type`) * Chain completions through function return types, mappings, type casts * `using-for` directive support (e.g. `PoolKey.toId()`, `BalanceDelta.amount0()`) * Method identifier completions with 4-byte selectors and full signatures * Keyword, global function, ether/time unit completions #### Performance * Arc-based zero-copy AST cache — eliminates 7MB+ deep clones per handler request * Non-blocking completion cache — returns static completions immediately while cache builds in background * `document_symbol` uses `ast_cache` instead of shelling out to `forge ast` on every request * Removed blocking `log_message` from completion handler to fix cancel+re-trigger lag #### Yul * Yul `externalReferences` support for goto-definition and find-references ### v0.1.7 * Yul externalReferences support for goto-definition and find-references * Completion engine with chain resolution, using-for, and type casts ### v0.1.6 * Fix rename in tests * Fix: ignore bytecode size warnings for all sol files * Enable goto definition for import statement strings * Handle ImportDirective nodes in goto definition * Add absolute\_path field to NodeInfo struct ### v0.1.4 * Fix: only update AST cache when build succeeds * Fix: preserve AST cache on file changes to keep go-to-definition working during errors ## Emacs (lsp-mode) Example `lsp-register-client` with full settings: ```elisp [init.el] // [!include ~/snippets/setup/emacs.el] ``` ## Helix Use `languages.toml`. ```toml [languages.toml] // [!include ~/snippets/setup/helix.toml] ``` ## Editor Setup All editor configs support the same server settings. ### Full settings schema ```json [settings.json] { "solidity-language-server": { "inlayHints": { "parameters": true, "gasEstimates": true }, "lint": { "enabled": true, "severity": [], "only": [], "exclude": [] }, "fileOperations": { "templateOnCreate": true, "updateImportsOnRename": true, "updateImportsOnDelete": true } } } ``` ### Lint values Foundry lint config reference (lint\_on\_build): [https://www.getfoundry.sh/config/reference/linter#lint\_on\_build](https://www.getfoundry.sh/config/reference/linter#lint_on_build) * `lint.severity`: `"high"`, `"med"`, `"low"`, `"info"`, `"gas"`, `"code-size"` * `lint.only` / `lint.exclude` rule IDs: * `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` ### Notes * Empty arrays for `severity`, `only`, `exclude` mean "no filter". * Defaults are all enabled (`true`) with empty lint arrays. * For file rename workflows, editors may require explicit save-all to persist buffer edits to disk. ## Neovim Example from your real setup in `~/.config/nvim/lsp`. Server config: ```lua [.config/nvim/lsp/solidity-language-server.lua] // [!include ~/snippets/setup/neovim.lua] ``` LSP capabilities + enable: ```lua [.config/nvim/plugin/lsp.lua] // [!include ~/snippets/setup/neovim-plugin-lsp.lua] ``` ### 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`): ```lua [.config/nvim/lua/plugins/oil.lua] // [!include ~/snippets/setup/neovim-oil.lua] ``` ### Neovim note for rename flows After file renames, if your editor leaves updated imports in modified buffers, run: ```vim :wa ``` This writes all modified buffers so tools reading from disk (for example `forge`) see updated imports. ## Sublime Text (LSP) Add to `LSP.sublime-settings`: ```json [LSP.sublime-settings] // [!include ~/snippets/setup/sublime.json] ``` ## Vim Credit: [https://github.com/leekt/vimsettings/blob/a0166c68dec7f67c391993a9bbf90668577fb929/.vimrc#L108-L137](https://github.com/leekt/vimsettings/blob/a0166c68dec7f67c391993a9bbf90668577fb929/.vimrc#L108-L137) Minimal `vim-lsp` setup in `.vimrc` (no wrapper): ```vim [.vimrc] if executable('solidity-language-server') au User lsp_setup call lsp#register_server({ \ 'name': 'solidity-language-server', \ 'cmd': {server_info -> ['solidity-language-server', '--stdio']}, \ 'root_uri': {server_info -> lsp#utils#path_to_uri( \ empty(lsp#utils#find_nearest_parent_file_directory( \ lsp#utils#get_buffer_path(), ['foundry.toml', '.git'])) \ ? getcwd() \ : lsp#utils#find_nearest_parent_file_directory( \ lsp#utils#get_buffer_path(), ['foundry.toml', '.git']))}, \ 'whitelist': ['solidity'], \ }) endif ``` Optional per-buffer mappings: ```vim [.vimrc] function! s:on_lsp_buffer_enabled() abort setlocal omnifunc=lsp#complete setlocal signcolumn=yes nmap gd (lsp-definition) nmap gr (lsp-references) nmap K (lsp-hover) nmap rn (lsp-rename) nmap [d (lsp-previous-diagnostic) nmap ]d (lsp-next-diagnostic) endfunction ``` ## VS Code Add to `settings.json`: ```json [settings.json] // [!include ~/snippets/setup/vscode.json] ``` ### Example filtered lint setup ```json [settings.json] { "solidity-language-server.lint.severity": ["high", "med"], "solidity-language-server.lint.exclude": ["pascal-case-struct"] } ``` ## Zed Configure LSP settings in Zed: ```json [settings.json] // [!include ~/snippets/setup/zed.json] ``` ## Completions ### What this page covers This page explains the current completion implementation: * how the completion request is routed in `lsp.rs`, * what data structures power completion in `completion.rs`, * how dot-chain/type/member resolution works, * how auto-import tail candidates are appended, * what is covered by tests today. ### Terms used in this page * **`CompletionCache`**: a set of hash maps/vectors built from compiler output and reused per request. * **local cache**: completion cache keyed by current URI (`completion_cache[uri]`). * **root cache**: project-level cache from root build (`ast_cache[root_uri].completion_cache`), used for global tail candidates. * **general completions**: non-dot completion items (keywords, globals, named symbols, units). * **dot completions**: member completions for expressions ending with `.`. * **tail candidates**: top-level importable symbols appended at the end of non-dot completion results (with optional import edits). ### Request flow in `lsp.rs` At `textDocument/completion`: * Read source text from `text_cache` (disk fallback when needed). * Load URI-local cache and root cache (if present). * Use local cache first, root cache as fallback. * Resolve `file_id` from `path_to_file_id` for scope-aware lookup. * For non-dot requests, build tail candidates from top-level importables. * Produce response via the completion handler and return. Important behavior: * If trigger character is `.`, tail candidates are disabled. * If no cache is available, the server asynchronously hydrates from `CachedBuild` when possible. ### The working data model `CompletionCache` is intentionally map-heavy so request-time work is mostly lookups: * `name_to_type`: symbol name -> `typeIdentifier` * `type_to_node`: `typeIdentifier` -> declaration node id * `node_members`: declaration node id -> member completion items * `method_identifiers`: contract node id -> method signature completions (+ selector label details) * `function_return_types`: `(contract_node_id, fn_name)` -> return `typeIdentifier` (for `foo().`) * `using_for`: `typeIdentifier` -> extension methods * `using_for_wildcard`: methods from `using X for *` * `scope_declarations`, `scope_parent`, `scope_ranges`: scope-aware lookup context * `linearized_base_contracts`: inheritance traversal for scope resolution * `top_level_importables_by_name`, `top_level_importables_by_file`: import-on-completion support This is built in `build_completion_cache(...)` from `.sources` AST and optional `.contracts` method identifiers. ### Completion behavior by mode #### Non-dot mode Non-dot requests return prebuilt general completions and then append tail candidates (if any). General completions include: * named AST symbols, * Solidity keywords, * magic globals, * units and type helpers. Tail candidates are appended last so local/scope-aware symbols stay prioritized. #### Dot mode Dot requests parse the expression chain before the cursor (`parse_dot_chain`) and resolve segment-by-segment (`get_chain_completions`): * **Plain segment**: resolve symbol to type. * **Call segment**: use `function_return_types` to continue on return type. * **Index segment**: peel mapping/array value type and continue. Final member set is composed from: * `node_members`, * `method_identifiers`, * `using_for` matches, * `using_for_wildcard`. ### Scope-aware name resolution When resolving a symbol in context, completion walks: * Start at the innermost scope from `(file_id, byte position)`. * Walk outward through `scope_parent`. * Include inherited contracts via `linearized_base_contracts`. * Fall back to global maps (`name_to_type`, `name_to_node_id`) when needed. This is why completion can prioritize locals/params while still finding inherited members. ### Auto-import tail candidates For non-dot mode, root cache can provide importable top-level symbols not declared in the current file. Each tail candidate can carry `additionalTextEdits` to insert an import when selected.\ Candidate extraction intentionally only includes directly declared top-level importables (contracts, structs, enums, UDVTs, free functions, constants) and excludes re-export aliases. ### Why method identifiers are separate AST member lists are useful but do not always carry full external signature detail in completion shape.\ `method_identifiers` from `.contracts` adds: * canonical signature text, * selector metadata in label details, * better external/public method display for contract and interface completions. ### Current limitations / tradeoffs * Completion quality depends on cache freshness; background cache hydration is best-effort. * Dot-chain resolution for very complex expressions is intentionally heuristic, not a full type-check pass. * Tail candidate import edits are only added in non-dot mode by design. ### Test coverage and confidence `tests/completion.rs` is extensive and covers: * scope declarations and scope parent behavior, * AST extraction of declarations by kind, * inheritance-aware scope resolution, * type parsing helpers (`extract_node_id_from_type`, mapping value extraction), * dot-chain parsing and chain resolution behavior, * using-for and wildcard method inclusion, * top-level importable extraction and tail-candidate behavior. This gives strong confidence in cache construction and request-time resolution logic. #### Recommended explicit additions High-value direct additions: * request-level test in `lsp.rs` validating local-cache-first, root-fallback behavior, * request-level test asserting tail candidates are suppressed on `.` trigger, * end-to-end test validating `additionalTextEdits` import insertion behavior through the completion response. ## Go-to-Definition ### Terms used in this page * **`CachedBuild`**: a group of Rust `HashMap`s created from a successful build on disk. The maps store things like `node id -> node info` and `source id -> file path`. It is a snapshot, not the live editor buffer. * **`build_version`**: an integer stored with `CachedBuild` that records which text version produced that snapshot. * **`text_cache`**: an in-memory `HashMap` with the latest editor-buffer content. * **Clean**: the in-memory editor text for a file is at the same version as the last cached successful build for that file (`text_version <= build_version`). * **Dirty**: the in-memory editor text is newer than the cached build (`text_version > build_version`). This usually means unsaved edits or edits not yet reflected in a successful rebuild. * **AST offset path**: direct declaration lookup using AST `src` byte offsets (`goto_declaration_cached`). * **Tree-sitter path**: live-buffer parsing and name/scope-based resolution (`goto_definition_ts`). ### Why this design exists Go-to-definition must work in two very different editor states: 1. **Clean state**: the buffer matches the last successful build. 2. **Dirty state**: the buffer has unsaved or not-yet-built edits. In the clean state, AST byte offsets are reliable and fast.\ In the dirty state, offsets can drift, so name/scope-based resolution is safer. This behavior was introduced to address the dirty-buffer reliability issue reported in [#2](https://github.com/mmsaki/solidity-language-server/issues/2). ### Runtime flow At request time, `textDocument/definition` checks whether the file is clean or dirty, then picks the resolution order. ```mermaid flowchart TD A["Request: textDocument/definition"] --> B["Load source bytes + cursor name"] B --> C{"Dirty file? text_version > build_version"} C -->|No| D["AST offset path: goto_declaration_cached"] D --> E{"Found?"} E -->|Yes| Z["Return Location"] E -->|No| F["Tree-sitter fallback: goto_definition_ts"] C -->|Yes| G["Tree-sitter path: goto_definition_ts"] F --> H{"validate_goto_target(name match)"} G --> H H -->|Yes| Z H -->|No or unresolved| I["AST name path: goto_declaration_by_name"] I --> J{"Found?"} J -->|Yes| Z J -->|No| K["Return null"] ``` ### End-to-end request path :::steps #### Determine source of truth (clean vs dirty) The server compares current text version in `text_cache` to the cached build version.\ If text is newer, the file is treated as dirty and AST byte offsets are considered potentially stale. #### Resolve a candidate from live syntax For dirty files (and as a fallback for clean files), the server parses the live buffer and builds cursor context (identifier, function scope, contract scope), then resolves scope using cached declaration maps and inheritance order. Typical context shape: ```text identifier "totalSupply" -> function_definition "mint" -> contract_declaration "Token" ``` #### Normalize to declaration location After semantic resolution, the server maps the target to concrete declaration ranges in source, including same-file and cross-file targets. When several matches exist, it prefers the best scoped/overload-compatible target. #### Validate and apply fallback policy Tree-sitter targets are validated against the cursor identifier text.\ If validation fails in dirty mode, the server falls back to AST-by-name resolution; in clean mode, it tries AST-by-offset first and then tree-sitter fallback. ::: Once this sequence succeeds, the server returns a single `Location`; otherwise it returns `null`. ### Formatting race guard (related reliability fix) A separate race was fixed so goto/hover read correct text after formatting: 1. `on_change` only writes to `text_cache` when version is not older than what is already cached. 2. Formatting updates `text_cache` immediately before returning edits. ### Current Behavior Summary * **Dirty buffers:** tree-sitter path first, then AST-by-name fallback. * **Clean buffers:** AST-by-offset path first, then tree-sitter fallback. * **Tree-sitter results are validated** against cursor identifier text before return. * **Parameter and local declaration navigation** is supported by tree-sitter declaration scanning. ### Reused Infrastructure | Component | From | Used For | | --------------------------- | ------------- | --------------------------------------------- | | `CompletionCache` | completion.rs | Scope chain, type resolution, inheritance | | `scope_declarations` | completion.rs | Name → type mappings per scope | | `linearized_base_contracts` | completion.rs | C3 inheritance resolution | | `name_to_node_id` | completion.rs | Contract name → scope ID | | `text_cache` | lsp.rs | Live buffer content | | tree-sitter parser | goto.rs | Parse live buffer and find declaration ranges | ### Test Coverage The goal of tests here is to protect behavior, not specific implementation details. Even if internals change, these guarantees should still hold: 1. **Cursor context is extracted correctly** We must reliably detect identifier, function, and contract context from live syntax. Why it matters: wrong context routes resolution to the wrong scope. Representative tests: * `test_cursor_context_state_var` * `test_cursor_context_top_level` * `test_cursor_context_short_param` 2. **Declaration discovery finds valid symbol kinds** The declaration scanner must return the expected declaration nodes (state vars, params, enum values, etc.). Why it matters: if symbol extraction misses a kind, go-to-definition silently fails for that language feature. Representative tests: * `test_find_declarations` * `test_find_declarations_enum_value` * `test_cursor_context_short_param` 3. **Ambiguity resolution picks the intended target** When multiple declarations share a name, selection logic must prefer the correct scope/container. Why it matters: users should not jump to unrelated symbols with the same name. Representative tests: * `test_find_declarations_multiple_contracts` * `test_find_best_declaration_same_contract` This suite is intentionally tied to these guarantees so refactors can change function names or structure without weakening behavior. ## Hover ### What this page covers This page describes the current `textDocument/hover` implementation: * how cursor position resolves to a declaration, * how signature, selector, gas, and docs are assembled, * how `@inheritdoc` and call-site `@param` docs are resolved, * what is covered by tests today. ### Terms used in this page * **`CachedBuild`**: build-time snapshot (from compiler output on disk) stored as hash maps for fast lookups. * **`decl_index`**: `HashMap` with typed declaration nodes used for O(1) declaration lookup. * **`doc_index`**: `HashMap` built from `userdoc` + `devdoc` in compiler output. * **`hint_index`**: prebuilt call-site index used to map a call argument back to `(decl_id, param_name)`. * **selector**: 4-byte function/error selector or 32-byte event topic shown in hover when available. ### Why hover is built this way Hover needs to work for multiple cases with one response: * declaration hover (signature + docs), * parameter hover (show matching `@param` text), * argument-at-callsite hover (show docs for the parameter at that call position), * Yul-linked declarations, * optional gas estimate display. A single source is not enough for all of these. The implementation combines typed AST declarations (`decl_index`), doc metadata (`doc_index`), and call-site mapping (`hint_index`). ### Runtime flow In `src/lsp.rs`, hover is a thin wrapper around: * `hover::hover_info(&cached_build, &uri, position, &source_bytes)` Inside `hover_info`, the flow is: ```mermaid flowchart TD A["hover request"] --> B["resolve byte position"] B --> C["resolve node id (Yul external refs first, else AST span)"] C --> D["follow referencedDeclaration to declaration id"] D --> E["lookup typed declaration in decl_index"] E --> F["build markdown parts"] F --> G["signature / type fallback"] F --> H["selector"] F --> I["optional gas block (sentinel gated)"] F --> J["docs from doc_index or NatSpec fallback"] F --> K["call-site @param doc via hint_index"] G --> Z["return Hover markdown"] H --> Z I --> Z J --> Z K --> Z ``` ### Cursor to declaration Resolution order is: * Try Yul bridge first using `externalReferences`. * Fallback to AST span match using smallest containing node in the current file. * Resolve final declaration id by following `referencedDeclaration` when present. This ensures hover works for both high-level Solidity and inline assembly references. ### Signature and selector For the resolved typed declaration (`DeclNode`): * Signature is built with `build_signature()`. * If no signature is available, hover falls back to `typeString + name`. * Selector is shown when present: * function/public variable/error: 4-byte selector, * event: 32-byte topic hash. ### Documentation source priority Documentation resolution uses this order: * Use compiler-derived doc index entries first (`userdoc`/`devdoc`). * Fall back to declaration-level NatSpec extraction. * For parameter/return variables, fall back to parent declaration param/return docs. `doc_index` is preferred because it already contains structured docs and compiler-resolved inherited docs where available. ### `@inheritdoc` behavior If raw doc text contains `@inheritdoc`, the implementation can resolve parent docs by: * parsing parent name, * reading the declaration selector, * finding the parent in base contracts, * matching by selector in the parent contract declarations. When resolution succeeds, hover renders resolved content instead of raw `@inheritdoc` lines. ### Call-site parameter doc (argument hover) Hover also adds parameter doc when the cursor is on an argument at a call site: * Parse the live buffer and find enclosing call context + argument index. * Resolve callsite semantics through the prebuilt call index. * Resolve parameter identity `(decl_id, param_name)`. * Fetch matching `@param` text from doc index first, then NatSpec fallback. This allows hover to show meaningful parameter descriptions directly at usage sites. ### Gas estimates in hover Gas text is added only when all conditions are true: * gas index is available, * hovered declaration matches function/contract gas lookup, * source includes the gas sentinel comment (`@lsp-enable gas-estimates`) for that declaration region. This is intentional to keep hover lightweight unless explicitly enabled. ### Output shape Hover markdown is assembled from parts in this order: * Signature block (or type fallback). * Selector line when available. * Optional gas section when enabled and resolvable. * Documentation section from doc index or NatSpec fallback. * Optional call-site `@param` detail when applicable. Parts are separated with blank lines for readability. ### Test coverage and confidence The hover module has broad direct tests in `src/hover.rs`: * selector extraction (`function`, `error`, `event`, public variable, no-selector internal), * `@inheritdoc` resolution and formatting behavior, * `DocIndex` construction from real fixture (`poolmanager.json`), * known selector lookup coverage (including overload cases), * parameter doc extraction behavior. This gives good confidence for: * selector correctness, * docs lookup priority, * inherited-doc resolution, * markdown formatting behavior. #### Recommended explicit additions Useful end-to-end additions through LSP boundary: * a request-level hover test verifying full assembled markdown ordering, * a dedicated call-site argument hover test through `lsp.rs` (not just helper-level), * a gas-sentinel hover test that checks both enabled and disabled paths through request flow. ## Imports and Navigation This page is intentionally short and exists to prevent confusion between three related features: 1. **Go-to-definition on symbols** (`Identifier`, `MemberAccess`, etc.) 2. **Go-to-definition on import strings** (`import "./X.sol"`) 3. **Find references** (`textDocument/references`) ### What goes where * Symbol goto and import-string goto are both handled in the goto implementation: * see [`goto.md`](./goto) * Reference collection is handled in the references implementation: * see [`references.md`](./references) ### Important boundary Import strings are treated as **navigation targets**, not symbol references. That means: * `import "./Pool.sol"` can navigate to the file via go-to-definition. * `textDocument/references` does not return import-string literals as references for a declaration. This boundary is deliberate in the current design so references remain declaration/usage based, while import paths remain file-navigation based. ## Inlay Hints ### What this page covers This page documents the current inlay-hint implementation: * parameter hints for calls and emits, * gas hints for functions/contracts, * how live-buffer positions are kept accurate, * how settings filter hint kinds at request time. ### Terms used in this page * **`HintIndex`**: `HashMap` prebuilt from AST at build-cache creation time. * **`HintLookup.by_offset`**: exact `call_start_byte -> CallSite` map (best when offsets are fresh). * **`HintLookup.by_name`**: fallback `(name, arg_count) -> CallSite` map (works when offsets drift). * **`CallSite`**: resolved semantic info for one call shape (`param names`, `skip`, `decl_id`). * **`skip`**: number of leading params to skip for hint labels (mainly `using for` receiver). ### Why this design exists Parameter names come from compiler AST semantics, but cursor/argument positions must follow live edits in the editor.\ A single source cannot solve both well. So the implementation splits responsibilities: * AST snapshot (`HintIndex`) for semantic mapping, * tree-sitter on live buffer for real-time argument positions. ### Runtime flow In `src/lsp.rs::inlay_hint`: * Read source bytes for the requested URI. * Load the cached build snapshot. * Generate raw hints. * Filter parameter hints (`InlayHintKind::PARAMETER`) and gas hints (`InlayHintKind::TYPE`) based on settings. * Return hints, or `None` if empty. Inside `inlay_hints(...)`: ```mermaid flowchart TD A["inlay hint request"] --> B["resolve abs path from URI"] B --> C["load HintLookup from build.hint_index"] C --> D["parse live source with tree-sitter"] D --> E["walk call_expression / emit_statement in requested range"] E --> F["lookup callsite: by_offset then by_name(arg_count)"] F --> G["emit parameter hints at live argument positions"] D --> H["collect gas hints from tree-sitter nodes (if gas index exists)"] G --> I["return raw hints"] H --> I I --> J["lsp.rs filters by settings (parameters / gas estimates)"] ``` ### How callsite mapping is built `build_hint_index(...)` runs once when `CachedBuild` is created. For each source file: * Walk call-like AST nodes (`FunctionCall` and `EmitStatement`). * Resolve declaration via `referencedDeclaration`. * Extract parameter metadata from typed declarations. * Store both exact-offset and name/arity fallback lookup entries. This is why request-time hinting is mostly lookup work, not full AST recomputation. ### Parameter hint behavior Hints are emitted for: * normal function calls, * member calls, * emit statements, * constructor-style `new Contract(args)` when constructor info exists. Special cases handled: * **using-for calls**: `skip = 1` when receiver is implicit and arg count is smaller than param count. * **named-arg struct constructors**: skipped (names are already visible at call site). * **stale offsets**: fallback to `(name, arg_count)` map. ### Gas hint behavior Gas hints are generated from tree-sitter node positions and gas index data.\ They are only shown if: * gas index is non-empty, * `settings.inlay_hints.gas_estimates` is true, * and source-level gas sentinel rules match the declaration region. In request filtering, gas hints are identified by `InlayHintKind::TYPE`. ### Refresh behavior Inlay hint refresh is triggered asynchronously (`tokio::spawn`) in two places: * after successful build/update in `on_change`, * after `did_change_configuration`. This avoids blocking request/diagnostic flow while still asking the client to re-request hints. ### Known tradeoffs * Exact offset matching can drift after edits/formatting; fallback improves resilience but can be less precise for overloaded same-name/same-arity cases. * Request-time accuracy depends on `HintIndex` freshness from the latest successful cached build. * Filtering happens in `lsp.rs`, so `inlay_hints(...)` may generate more hints than ultimately returned. ### Test coverage and confidence `src/inlay_hints.rs` includes strong helper-level coverage: * tree-sitter call/event/name extraction, * call argument indexing and byte-position mapping, * `new` expression handling, * `resolve_callsite_param` behavior (including skip and bounds), * gas sentinel detection helpers. This gives good confidence in the core extraction and lookup mechanics. #### Recommended explicit additions Useful request-level additions: * end-to-end `textDocument/inlayHint` tests that validate settings filtering by kind, * stale-offset overload scenario test through request path, * configuration-change refresh behavior test (ensuring client refresh is triggered). ## Memory Profiling with DHAT ### Why this exists This page documents how to profile heap usage for the current Rust server implementation.\ Use it when you need to answer: * where memory is retained (`t-end`) * where peak memory happens (`t-gmax`) * whether a change improves real server behavior or only micro-benchmarks ### What DHAT measures DHAT records allocation sites and reports: * `tb`: total allocated bytes over process lifetime * `gb`: bytes live at global peak (`t-gmax`) * `eb`: bytes live at process end (`t-end`) `eb` is usually the number to watch for long-lived caches. Useful interpretation for this server: * high `gb` with lower `eb`: transient parsing/index build pressure * high `eb`: retained structures (indexes/caches) are large ### Setup in this repo The server already supports DHAT behind `dhat-heap`: * `Cargo.toml`: optional `dhat` dependency + `dhat-heap` feature * `src/main.rs`: global allocator and profiler guard behind `#[cfg(feature = "dhat-heap")]` Build with release mode: ```bash cargo build --release --features dhat-heap ``` ### Two profiling modes #### 1) Whole-server profiling with lsp-bench (recommended) This captures real behavior: initialize, indexing, requests, and shutdown. ```bash lsp-bench -c benchmarks/dhat-profile.yaml ``` The output file is `dhat-heap.json` (in the server working directory used by the benchmark). Use this when validating real user-facing memory behavior. #### 2) Isolated fixture profiling (`dhat-profile` binary) This is useful when you only want to profile AST/cache construction from one saved `solc` JSON output. ```bash cargo build --release --features dhat-heap --bin dhat-profile ./target/release/dhat-profile poolmanager-t-full.json ``` ### Read results quickly #### Summary numbers ```bash python3 -c " import json,sys d=json.load(open(sys.argv[1])) pps=d['pps'] print('sites:', len(pps)) print('total_mb:', round(sum(pp['tb'] for pp in pps)/1048576,1)) print('peak_mb:', round(sum(pp['gb'] for pp in pps)/1048576,1)) print('end_mb:', round(sum(pp['eb'] for pp in pps)/1048576,1)) " dhat-heap.json ``` #### Focus on our frames ```bash python3 -c " import json,sys d=json.load(open(sys.argv[1])) pps,ftbl=d['pps'],d['ftbl'] keys=['solidity_language_server','lsp::','goto::','completion::','hover::','solc::','inlay_hints::'] rows=[] for pp in pps: frames=[ftbl[f] for f in pp['fs']] if any(k in ' '.join(frames) for k in keys): rows.append((pp,frames)) rows.sort(key=lambda x:x[0]['gb'], reverse=True) for i,(pp,frames) in enumerate(rows[:10],1): print(f\"#{i} peak={pp['gb']/1048576:.1f}MB end={pp['eb']/1048576:.1f}MB total={pp['tb']/1048576:.1f}MB\") print(' ', frames[0]) " dhat-heap.json ``` ### DHAT fields used most often | Field | Meaning | | ----- | --------------------------------------- | | `tb` | total allocated bytes over run | | `tbk` | total allocation count | | `gb` | bytes live at global peak | | `eb` | bytes still live at end | | `fs` | frame indices into frame table (`ftbl`) | ### Interpreting findings for this server In this codebase, large retained memory usually comes from long-lived indexes/caches (for example `CachedBuild`-related maps).\ Large transient memory usually comes from JSON parsing and intermediate allocations during index build. When reviewing a profiling change, compare both: * `RSS` from benchmark reports (external process view) * `DHAT gb/eb` (internal allocation view) If one improves and the other does not, validate whether the change reduced retained structures or only shifted allocation timing. For memory regressions, the usual order is: 1. reproduce with lsp-bench + DHAT 2. identify top `gb` and top `eb` frame groups 3. map groups to concrete data structures in code 4. re-run same benchmark config after patch and compare ### Covered vs not covered Covered here: * how to run DHAT in this repo * how to read and compare main DHAT metrics * how to tie results back to server code paths Not covered here: * full memory-optimization history per release * every historical benchmark table * generic Rust memory-profiling theory beyond DHAT usage in this project ## References ### What this page covers This page documents how `textDocument/references` works in the current implementation: * how the target symbol is resolved from the cursor, * how same-file and cross-file references are collected, * how Yul `externalReferences` are included, * what is covered by tests today, and what still needs explicit tests. If you are looking for import-string navigation (for example `import "./Pool.sol"`), that path belongs to go-to-definition and is documented in [`goto.md`](./goto). ### The working model `references` is built around one core data structure: `CachedBuild`. `CachedBuild` is a snapshot built from successful compiler output on disk. Internally it stores `HashMap`s such as: * `nodes: HashMap>` * `id_to_path_map: HashMap` * `external_refs: HashMap` `NodeInfo` includes fields like `src`, `name_location`, and `referenced_declaration`. At request time, the server uses this snapshot to resolve references quickly, then merges results from other cached builds to get cross-file coverage. ### Request flow in practice In `src/lsp.rs`, the `references` handler does the following: * Load source bytes and get (or build) `CachedBuild` for the URI. * Collect current-build references. * Derive stable target location `(def_abs_path, def_byte_offset)`. * Scan other cached builds for cross-file references to the same target. * Deduplicate by `(uri, start, end)` and return. This is why you get both local and cross-file references in one response when caches are available. ### How target resolution works Inside `references.rs`, resolution follows this order: * Try Yul resolution first (`externalReferences` mapping). * Fall back to AST span match (smallest containing node). * Normalize to declaration target: follow `referencedDeclaration` when present, else use the node id directly. That resolved node ID becomes the target for reference collection. ### Yul references are first-class Yul identifiers inside `assembly {}` do not have normal Solidity node IDs in the same way usage sites do. The implementation bridges this via `InlineAssembly.externalReferences`, which maps Yul `src` ranges to Solidity declaration IDs. During references: * cursor-on-Yul is resolved through `external_refs` before normal AST span matching, * Yul usage locations are also appended back into the result set by matching `decl_id`. This is why references work inside inline assembly rather than only in high-level Solidity syntax. ### Cross-file behavior: stable identity, not unstable node IDs Node IDs are not stable across independent builds.\ Cross-file references therefore do **not** share raw node IDs between builds. Instead, the server derives a stable identity: * declaration file absolute path (`def_abs_path`) * declaration byte offset (`def_byte_offset`) Then each other cached build re-resolves that location locally (`byte_to_id`) and collects matching references in that build. One important detail: resolution prefers `name_location` over `src` for declarations when available, so cross-file matching lands on the symbol name itself rather than a broader declaration span. ### includeDeclaration behavior `includeDeclaration` from the LSP request is honored directly: * when `true`, declaration location is included, * when `false`, only usage locations are returned. This flag is applied in both the current-build pass and cross-file passes. ### What this implementation does not try to do * It does not treat import string literals as references. Import path navigation is handled by go-to-definition logic. * It does not guarantee cross-file completeness when another file has no cached build yet. Cross-file scanning only runs over available entries in `ast_cache`. ### Test coverage and confidence Current tests give good coverage for the core reference architecture: * `tests/cross_file_references.rs` covers stable cross-file target resolution using `(path, byte_offset)`. * `tests/rename.rs` includes `goto_references_cached` behavior that relies on `nameLocation` fallback. * `tests/yul_external_references.rs` covers Yul external-reference indexing and goto/reference mapping behavior. This gives strong confidence in: * target resolution correctness, * cross-file re-resolution strategy, * Yul assembly integration. #### Recommended explicit additions The following are good direct test additions for long-term safety: * `lsp.rs` handler-level test for full merge/dedup behavior across multiple cached builds. * a direct test for `includeDeclaration = false` through the full request path. * a mixed Solidity + Yul reference scenario validated end-to-end through the LSP method boundary. ## Signature Help ### Terms used in this page * **`text_cache`**: in-memory `HashMap` with current editor text. * **`HintIndex`**: lookup maps built from AST that connect callsites to declaration IDs. * **`TsCallContext`**: tree-sitter call context (`name`, `arg_index`, `arg_count`, `is_index_access`, `call_start_byte`). * **Function/event path**: signature help resolved through AST declaration IDs. * **Mapping path**: signature help resolved by scanning AST for a mapping variable with the same name. ### What this feature does `textDocument/signatureHelp` shows: * the active callable signature * the currently active parameter * NatSpec docs when available The server advertises trigger characters `(`, `,`, `[` in `initialize`. | Trigger | Typical case | | ------- | ------------------------- | | `(` | function/event call start | | `,` | move to next argument | | `[` | mapping index access | ### Runtime flow ```mermaid flowchart TD A["Request: textDocument/signatureHelp"] --> B["Read source text (text_cache first)"] B --> C["Build TsCallContext via tree-sitter + text-scan fallback"] C --> D{"Index access? is_index_access"} D -->|No| E["Resolve declaration via HintIndex"] D -->|Yes| F["Find mapping VariableDeclaration by name"] E --> G["Build function/event signature + docs"] F --> H["Build mapping signature + key/value docs"] G --> I["Return SignatureHelp"] H --> I ``` ### Step 1: Find call context The parser path handles both complete and incomplete syntax: * Normal parse path: try `ts_find_call_at_byte()` to locate `call_expression` / `emit_statement` / `array_access`. * Parent walk fallback: walk up from the cursor node until one of those enclosing node kinds is found. * Text-scan fallback: if tree-sitter is incomplete (for example `foo(` while typing), scan text backwards for unmatched `(` or `[` and extract identifier name. This fallback is why signature help still appears while the user is mid-typing. ### Step 2: Resolve target #### Function/event calls Call resolution tries: * exact callsite match: `(call_start_byte, name, arg_count)` * name fallback: `(name, _)` It returns `(decl_id, skip)` where `skip=1` is used for `using-for` receiver adjustment. This matters for calls like: ```solidity using Transaction for uint256; uint256 total = PRICE.addTax(TAX, TAX_BASE); ``` The first declared parameter is the implicit receiver (`PRICE`), so highlighting shifts by `skip`. #### Mapping index access Mapping resolution searches declarations for: * `nodeType == VariableDeclaration` * same variable name * `typeName.nodeType == Mapping` Then it extracts: * key type (`keyType.typeDescriptions.typeString`) * optional key name (`keyName`) * value type (`valueType.typeDescriptions.typeString`) Example AST shape used by this path: ```json { "nodeType": "VariableDeclaration", "name": "_pools", "typeName": { "nodeType": "Mapping", "keyType": { "typeDescriptions": { "typeString": "PoolId" } }, "keyName": "id", "valueType": { "typeDescriptions": { "typeString": "struct Pool.State" } } } } ``` ### Step 3: Build response payload The server builds `SignatureHelp` with byte-offset parameter labels (not plain text labels), so editor highlighting is exact. It also attaches docs: * function/event path: NatSpec `@notice`, `@dev`, `@param` when present * mapping path: key doc and `@returns ` text For `using-for` calls, active parameter is adjusted with `arg_index + skip`. The byte-range labels are important because editors highlight by offset into the full label string, not by matching parameter text. ### Behavior on incomplete code Signature help is intentionally resilient while typing: * `foo(` without closing `)` can still resolve through text-scan fallback. * `orders[` without closing `]` can still resolve through index-scan fallback. * when tree-sitter node shapes are incomplete, fallback scanners keep the feature responsive. ### Current limitations * Overloaded names can fall back to the wrong overload when exact callsite matching is unavailable. * Nested mapping chains (`a[b][c]`) are currently treated as single-step index access for signature rendering. * Builtins/type casts without user declarations (`abi.encode`, `uint256(x)`) do not produce declaration-based signatures. ### Test intent What we need tests to keep guaranteeing: * call/index context is found correctly on complete and incomplete syntax * active parameter index is stable across commas and `using-for` offset cases * mapping signatures include the correct key/value type metadata ### Verify quickly ```bash # standalone benchmark config for signatureHelp lsp-bench -c benchmarks/signature-help.yaml # inspect mapping declarations in a solc AST jq '[.. | objects | select(.nodeType=="VariableDeclaration" and .typeName.nodeType=="Mapping") | {name, keyName:(.typeName.keyName // ""), keyType:.typeName.keyType.typeDescriptions.typeString, valueType:.typeName.valueType.typeDescriptions.typeString}]' poolmanager.json ``` ### Main implementation files * `src/lsp.rs`: request handling and capability declaration * `src/hover.rs`: signature payload construction and docs rendering * `src/inlay_hints.rs`: call-context extraction and text-scan fallback logic ## Symbols ### What this page covers The server exposes two symbol methods: * `textDocument/documentSymbol`: hierarchical symbols for one file * `workspace/symbol`: flat symbol list across files in `text_cache` Both are tree-sitter based and do not require a successful build. This means symbols can work even when the file does not compile. ### Terms used in this page * **Hierarchical symbols**: `DocumentSymbol[]` with parent/child nesting. * **Flat symbols**: `SymbolInformation[]` with `container_name`. * **`text_cache`**: in-memory map of open/known files used as the source text set for workspace symbol queries. ### Runtime flow ```mermaid flowchart TD A["Symbol request"] --> B{"Method"} B -->|documentSymbol| C["Read source (text_cache, then disk fallback)"] B -->|workspace/symbol| D["Collect files from text_cache"] C --> E["tree-sitter parse"] D --> F["tree-sitter parse each file"] E --> G["Build DocumentSymbol tree"] F --> H["Build SymbolInformation list"] ``` `documentSymbol` reads current buffer text first (`text_cache`) and falls back to disk only when needed.\ `workspace/symbol` uses files currently available in `text_cache` for fast, repeated lookups. ### Document symbols (`textDocument/documentSymbol`) The server parses one file and collects top-level nodes. Containers (`contract`, `interface`, `library`, `struct`, `enum`) include children. Flow: * Parse Solidity source with tree-sitter. * Map known top-level node kinds (`contract_declaration`, `function_definition`, `import_directive`, etc.) to symbol builders. * Recurse into container bodies and attach nested children. Function nodes include a `detail` string built from parameter and return nodes. Example: ```solidity function transfer(address to, uint256 amount) external returns (bool) ``` becomes detail: ```text (address to, uint256 amount) returns (bool) ``` ### Workspace symbols (`workspace/symbol`) The server parses each cached file and emits a flat `SymbolInformation` list with `container_name`. This is intentionally cache-scoped for responsiveness; it does not scan every file in the repo on each request. It is a speed/coverage tradeoff: better latency in the editor, but visibility depends on what files are present in cache. ### Symbol kind mapping (current implementation) | Solidity construct | SymbolKind | | ------------------------------ | ---------------- | | `contract` | `CLASS` | | `interface` | `INTERFACE` | | `library` | `NAMESPACE` | | `function` | `FUNCTION` | | `constructor` | `CONSTRUCTOR` | | `fallback` / `receive` | `FUNCTION` | | state variable / struct member | `FIELD` | | `event` / `error` | `EVENT` | | `modifier` | `METHOD` | | `struct` | `STRUCT` | | `enum` | `ENUM` | | enum value | `ENUM_MEMBER` | | `using ... for` | `PROPERTY` | | `type ... is ...` | `TYPE_PARAMETER` | | `pragma` | `STRING` | | `import` | `MODULE` | ### Practical behavior notes * Broken or partially edited files can still return symbols because this path does not depend on `solc`. * `fallback_receive_definition` names are synthesized as `fallback` or `receive` from node text. * Import symbols are rendered as `import ""`. * For nodes that do not expose a `name` field, symbol extraction falls back to the first `identifier` child. ### Tree-sitter node coverage Main node kinds currently mapped: * top-level: `pragma_directive`, `import_directive`, `contract_declaration`, `interface_declaration`, `library_declaration`, `function_definition`, `struct_declaration`, `enum_declaration`, `user_defined_type_definition` * contract body: `function_definition`, `constructor_definition`, `fallback_receive_definition`, `state_variable_declaration`, `event_definition`, `error_declaration`, `modifier_definition`, `struct_declaration`, `enum_declaration`, `using_directive` * nested members: `struct_member`, `enum_value` ### Test intent What tests should keep asserting: * symbol kind mapping remains stable for core Solidity constructs * nested container structure is preserved for `documentSymbol` * flat container labeling remains accurate for `workspace/symbol` ### Verify quickly ```bash lsp-bench -c benchmarks/shop.yaml ``` Check these rows in the generated report: * `textDocument/documentSymbol` * `workspace/symbol` ### Main implementation files * `src/lsp.rs`: symbol request handling * `src/symbols.rs`: parsing, node-kind mapping, and tree/flat symbol generation ## File Rename Imports (`workspace/willRenameFiles`) ### What this feature does When a Solidity file is renamed or moved, the server returns import-path edits so dependent files stay valid. This behavior is controlled by: * `fileOperations.updateImportsOnRename` (default: `true`) ### Terms used in this page * **Rename pre-phase**: `workspace/willRenameFiles` request before filesystem rename. * **Rename post-phase**: `workspace/didRenameFiles` notification after rename. * **`text_cache`**: in-memory file content map used for edit computation and re-indexing. * **Folder rename expansion**: converting one folder rename into concrete file-to-file renames for all discovered `.sol` files under that folder. ### Runtime flow ```mermaid flowchart TD A["workspace/willRenameFiles"] --> B{"updateImportsOnRename enabled?"} B -->|No| Z["Return null"] B -->|Yes| C["Discover source files"] C --> D["Expand folder renames to file renames"] D --> E["Hydrate text_cache for missing files"] E --> F["Compute import edits (tree-sitter import scan)"] F --> G{"Any edits?"} G -->|No| Z G -->|Yes| H["Patch text_cache with same edits"] H --> I["Return WorkspaceEdit"] I --> J["workspace/didRenameFiles"] J --> K["Migrate cache keys old_uri -> new_uri"] K --> L["Invalidate project index + re-index from text_cache snapshot"] ``` This flow has two independent goals: * return correct import edits before the rename is finalized * keep server caches consistent after rename so future requests see the new layout ### What gets updated * Importers of the renamed file: any project file importing the old path gets a text edit for the import string. * The moved file itself (when directory changes): its own relative imports are rewritten from the new location. * Folder renames: folder requests are expanded into per-file renames so nested Solidity files are handled. Two concrete rename classes are handled: * same-directory rename (`Pool.sol` -> `Pools.sol`): importers update, file-local imports usually unchanged * cross-directory move (`src/PoolManager.sol` -> `src/core/PoolManager.sol`): importers update, moved file’s own relative imports can also change ### Why `text_cache` is patched immediately Editors usually apply the returned `WorkspaceEdit` but do not send `didChange` events for every touched file.\ The server applies those edits to `text_cache` itself to keep internal state consistent until saves happen. This avoids stale internal state when files were edited indirectly by rename but were never opened as active buffers. ### Client behavior to expect * The server returns edits. * The editor applies them in buffers. * Disk writes depend on editor/file-manager behavior (`:wa` or autosave may be needed). This is why a rename can look correct in buffers while `forge build` still fails until files are saved. If your editor does not autosave changed buffers after file operations, run `:wa` (Neovim) or equivalent. ### Current settings and defaults ```json { "solidity-language-server": { "fileOperations": { "templateOnCreate": true, "updateImportsOnRename": true, "updateImportsOnDelete": true } } } ``` ### Limitations * Remapped external/library imports are not rewritten (`forge-std/...`, `@openzeppelin/...`, etc.). * Very fast consecutive renames can race with background re-index. * Folder rename behavior depends on what the client sends in `RenameFilesParams`. * The server does not write files to disk itself; it returns edits and maintains in-memory state. ### Debug checklist When a rename appears incomplete: 1. Check LSP logs for `willRenameFiles: ... edit(s)` and `didRenameFiles: re-indexed ...`. 2. Verify editor applied and saved modified buffers. 3. Confirm `fileOperations.updateImportsOnRename` is enabled in client settings. 4. Retry after index completes when running large projects. ### Verify quickly ```bash # feature behavior lsp-bench -c benchmarks/pool.yaml # unit/integration coverage cargo test --release --test file_operations ``` In Neovim logs (`~/.local/state/nvim/lsp.log`), look for: * `willRenameFiles: edit(s) across file(s)` * `didRenameFiles: re-indexed source files` ### Main implementation files * `src/lsp.rs`: request lifecycle, settings gates, cache migration, and re-index orchestration * `src/file_operations.rs`: rename expansion, import scanning, edit generation, and cache patching * `src/links.rs`: tree-sitter import extraction (`ts_find_imports`) * `src/solc.rs`: project re-index from optional in-memory cache content ### Covered vs not covered Covered here: * rename lifecycle (`willRenameFiles` + `didRenameFiles`) * how edits are computed and why cache patching is required * practical save behavior in editors Not covered here: * deep editor plugin configuration matrices * every benchmark variant and historical latency table ## Reference These pages are implementation-deep technical notes for `solidity-language-server`. They explain: * how each LSP feature is implemented in practice, * why certain design choices were made (including tradeoffs), * known limitations and failure modes, * and how Solc output + tree-sitter are combined in the request pipeline. If you want behavior details beyond surface feature docs, start here. * [Go To Definition](/reference/goto) * [References](/reference/references) * [Hover](/reference/hover) * [Completions](/reference/completions) * [Symbols](/reference/symbols) * [Inlay Hints](/reference/inlay-hints) * [Signature Help](/reference/signature-help) * [Will Rename Files](/reference/will-rename-files) * [Imports and References](/reference/imports-references) * [Profiling](/reference/profiling) ### Features * **Go to Definition** / **Go to Declaration** — jump to any symbol across files * **Find References** — all usages of a symbol across the project * **Rename** — project-wide symbol rename with prepare support * **Hover** — signatures, NatSpec docs, function/error/event selectors, `@inheritdoc` resolution * **Completions** — scope-aware with two modes (fast cache vs full recomputation) * **Document Links** — clickable imports, type names, function calls * **Document Symbols** / **Workspace Symbols** — outline and search * **Formatting** — via `forge fmt` * **Diagnostics** — from `solc` and `forge lint` * **Signature Help** — parameter info on function calls, event emits, and mapping access * **Inlay Hints** — parameter names and gas estimates * **File Operations** — `workspace/willCreateFiles` scaffolding + `workspace/willRenameFiles`/`workspace/willDeleteFiles` import edits + `workspace/didCreateFiles`/`workspace/didRenameFiles`/`workspace/didDeleteFiles` cache migration/re-index (`fileOperations.templateOnCreate`, `fileOperations.updateImportsOnRename`, `fileOperations.updateImportsOnDelete`) #### LSP Methods **General** * [x] `initialize` - Server initialization * [x] `initialized` - Server initialized notification * [x] `shutdown` - Server shutdown **Text Synchronization** * [x] `textDocument/didOpen` - Handle file opening * [x] `textDocument/didChange` - Handle file content changes * [x] `textDocument/didSave` - Handle file saving with diagnostics refresh * [x] `textDocument/didClose` - Handle file closing * [x] `textDocument/willSave` - File will save notification * [ ] `textDocument/willSaveWaitUntil` - File will save wait until **Diagnostics** * [x] `textDocument/publishDiagnostics` - Publish compilation errors and warnings via `forge build` * [x] `textDocument/publishDiagnostics` - Publish linting errors and warnings via `forge lint` **Language Features** * [x] `textDocument/definition` - Go to definition * [x] `textDocument/declaration` - Go to declaration * [x] `textDocument/references` - Find all references * [x] `textDocument/documentSymbol` - Document symbol outline (contracts, functions, variables, events, structs, enums, etc.) * [x] `textDocument/prepareRename` - Prepare rename validation * [x] `textDocument/rename` - Rename symbols across files * [x] `textDocument/formatting` - Document formatting * [x] `textDocument/completion` - Code completion * [x] `textDocument/hover` - Hover information * [x] `textDocument/signatureHelp` - Function signature help (functions, events, mappings) * [ ] `textDocument/typeDefinition` - Go to type definition * [ ] `textDocument/implementation` - Go to implementation * [x] `textDocument/documentHighlight` - Document highlighting (read/write classification) * [ ] `textDocument/codeAction` - Code actions (quick fixes, refactoring) * [ ] `textDocument/codeLens` - Code lens * [x] `textDocument/documentLink` - Document links (clickable references and import paths) * [ ] `textDocument/documentColor` - Color information * [ ] `textDocument/colorPresentation` - Color presentation * [ ] `textDocument/rangeFormatting` - Range formatting * [ ] `textDocument/onTypeFormatting` - On-type formatting * [x] `textDocument/foldingRange` - Folding ranges (contracts, functions, structs, enums, blocks, comments, imports) * [x] `textDocument/selectionRange` - Selection ranges * [x] `textDocument/inlayHint` - Inlay hints (parameter names, gas estimates) * [x] `textDocument/semanticTokens` - Semantic tokens * [x] `textDocument/semanticTokens/full` - Full semantic tokens * [x] `textDocument/semanticTokens/range` - Range semantic tokens * [x] `textDocument/semanticTokens/delta` - Delta semantic tokens **Workspace Features** * [x] `workspace/symbol` - Workspace-wide symbol search * [x] `workspace/didChangeConfiguration` - Updates editor settings (inlay hints, lint options) * [x] `workspace/didChangeWatchedFiles` - Acknowledges watched file changes (logs only) * [x] `workspace/didChangeWorkspaceFolders` - Acknowledges workspace folder changes (logs only) * [ ] `workspace/applyEdit` - Inbound handler not implemented (server uses outbound `workspace/applyEdit` to scaffold created files) * [ ] `workspace/executeCommand` - Execute workspace commands (stub implementation) * [x] `workspace/willCreateFiles` - File creation preview (scaffolding for `.sol`, `.t.sol`, `.s.sol`) * [x] `workspace/didCreateFiles` - Post-create scaffold fallback + cache/index refresh * [x] `workspace/willRenameFiles` - File rename preview (import path updates) * [x] `workspace/didRenameFiles` - Post-rename cache migration + background re-index * [x] `workspace/willDeleteFiles` - File deletion preview (removes imports to deleted files) * [x] `workspace/didDeleteFiles` - Post-delete cache cleanup + background re-index **Window Features** * [ ] `window/showMessage` - Show message to user * [ ] `window/showMessageRequest` - Show message request to user * [x] `window/workDoneProgress` - Work done progress ## Docs Development This docs site uses **Bun** as the package manager. ### Install dependencies ```sh bun install ``` ### Run local docs dev server ```sh bun run docs:dev ``` ### Build static docs ```sh bun run docs:build ``` ### Preview built docs ```sh bun run docs:preview ``` ## Quickstart ### Install ```sh cargo install solidity-language-server ``` ### Build (local) ```sh cargo build --release ``` ### Next * [Editor Setup](/setup) * [Features](/docs/features) * [Benchmarks](/benchmarks/overview) ## I Benchmarked 3 Solidity LSP Servers. Here's What I Found If you write Solidity, your editor's language server is doing more work than you think. Every time you open a file, jump to a definition, or hover over a symbol, your LSP server is racing to give you an answer. The question is: how fast? I benchmarked three Solidity LSP servers head-to-head against a real-world codebase — Uniswap V4-core's Pool.sol, 618 lines of production Solidity. No toy examples. No hello-world contracts. Just a file that every serious Solidity developer has encountered. The three servers: * My LSP — solidity-language-server, written in Rust * solc — the Solidity compiler's built-in LSP mode, written in C++ * nomicfoundation — nomicfoundation-solidity-language-server, the Node.js server that ships with the Hardhat VSCode extension Every benchmark ran 10 iterations with 2 warmup rounds. I measured p50, p95, and mean latency. Each server was spawned as a fresh child process, communicating over JSON-RPC via stdio. No caching advantages, no warm state carried over. Equal footing. ### Startup The first thing your editor does is spawn the language server and send an initialize request. This is the time between launching the process and getting a response back. My LSP: 5ms. solc: 121ms. nomicfoundation: 882ms. Solidity language server spawn process benchmark That's not a marginal difference. My server is initialized and ready to work before solc has even finished loading, and nearly 175 times faster than nomicfoundation. Every time you open a workspace, every time your editor restarts, this cost compounds. A server that takes almost a full second just to say hello is a server that's already behind. ### Diagnostics After initialization, the real work begins. I open Pool.sol and wait for the server to publish its first diagnostics — the errors, warnings, and lint results that appear in your editor. solc came in fastest here at 130ms, which makes sense. It's a compiler. Parsing Solidity is literally its primary job. My LSP followed at 410ms, returning 4 diagnostics including forge-lint results for naming conventions. nomicfoundation took 915ms and returned zero diagnostics. Solidity language server diagnostics benchmark. Read that again. nomicfoundation took the longest by a wide margin and had nothing to show for it. No errors, no warnings, no linting. Nearly a full second of your time for an empty result. Solidity language server results from diagnostics. ### Go to Definition This is where things get interesting. Go to Definition is one of the most common actions any developer performs. Click on a symbol, jump to where it's defined. I targeted TickMath at line 103 of Pool.sol. My LSP: 8.5ms. It found the definition and returned the exact location. solc returned an empty array. It technically supports the request but gave no result for this target. nomicfoundation timed out. It never responded. I waited, and it never came back 8.5 milliseconds. That's the time between your click and the answer. At that speed, navigation feels instant — because it effectively is. ### The Features Nobody Else Supports I also benchmarked Go to Declaration, Find References, and Document Symbols. These are bread-and-butter IDE features that developers in every other language take for granted. Go to Declaration: My LSP answered in 8.4ms. solc returned an error — it doesn't support the method. nomicfoundation timed out. Solidity language server go to declaration benchmark. Find References: My LSP returned all references to TickMath across the file in 10.1ms. solc doesn't support it. nomicfoundation timed out. Solidity language server get references benchmark. Document Symbols: My LSP returned the full symbol tree in 8.3ms. solc doesn't support it. nomicfoundation timed out. Solidity language server document symbols benchmark. There's a pattern here. For every feature beyond basic diagnostics, I was the only server that actually returned a result. solc openly declares these methods unsupported. nomicfoundation accepts the requests and then fails silently by timing out. ### What This Means Speed in a language server isn't a vanity metric. It's the difference between an editor that feels alive and one that feels like it's working against you. When Go to Definition takes 8ms, you don't think about it — you just navigate. When it times out, you lose your train of thought. You scroll manually. You grep. You break flow. Solidity developers have tolerated slow tooling for years because the alternatives didn't exist. The compiler's LSP mode covers the basics but stops there. The most widely-used extension in the ecosystem can't reliably handle navigation on a 618-line file. I built my server in Rust because I think Solidity developers deserve the same quality of tooling that Rust, Go, and TypeScript developers already have. Sub-10ms responses aren't a stretch goal. They're the baseline. Try It Yourself The benchmarks are fully reproducible. Clone the repo, build with cargo, and run any subcommand — spawn, diagnostics, definition, declaration, hover, references, documentSymbol. Benchmark source: github.com/mmsaki/lsp-bench (github.com/mmsaki/lsp-bench) The LSP server: github.com/mmsaki/solidity-language-server (github.com/mmsaki/solidity-language-server) The numbers speak for themselves. ### v0.1.17: 6 Solidity LSPs Benchmarked on 13 Methods. Only One Passes All of Them Two days ago I published benchmarks comparing three Solidity LSP servers against Uniswap v4-core's Pool.sol. The results were stark -- my Rust-based server was the only one that could handle basic navigation without timing out. Since then I've expanded the test. v0.1.17 is out, and this time I benchmarked 6 servers across 13 LSP methods against a Foundry project's Shop.sol. ### The servers * solidity-language-server v0.1.17 (Rust) -- new build * solidity-language-server v0.1.16 (Rust) -- previous release * @argotorg solc --lsp 0.8.33 (C++) -- the Solidity compiler * @NomicFoundation nomicfoundation 0.8.25 (Node.js) -- Hardhat VSCode extension * @juanfranblanco vscode-solidity-server 0.0.187 (Node.js) -- Juan Blanco's extension solidity-ls 0.5.4 (Node.js) -- qiuxiang's server The methods tested: initialize, diagnostics, definition, declaration, hover, references, completion, rename, prepareRename, documentSymbol, documentLink, formatting, workspace/symbol. ### Results Both v0.1.16 and v0.1.17 pass all 13 benchmarks. No other server comes close. solc supports 5 methods (initialize, diagnostics, definition, hover, rename). For the other 8 -- references, completion, declaration, prepareRename, documentSymbol, documentLink, formatting, workspace/symbol -- it returns "Unknown method." v0.1.17 capability metrics. All four JS-based servers (nomicfoundation, juanfranblanco, qiuxiang, solidity-ls) timed out on every request after initialize. Diagnostics, definition, hover -- timeout across the board. Not one response. The numbers, v0.1.17: v0.1.17 Benchmark comparison. Memory: \~8-10MB RSS across the board. solc: \~26MB. The JS servers didn't get far enough to measure meaningfully. ### What changed in v0.1.17 The headline fix is diagnostic path matching. In v0.1.16, there was a bug where the path filter used to separate your diagnostics from dependency diagnostics could silently drop results. If your file's relative path didn't match the filter exactly, diagnostics would vanish. No error, no warning -- they just disappeared. v0.1.17 is here. Diagnostics are 1.4x faster (219ms -> 156ms) and the silent drop is gone. This is the kind of bug that doesn't show up in tests but ruins your day in production. You save a file, your editor shows no errors, you deploy, and forge build catches what your LSP missed. ### The bigger picture Two days and three releases separate v0.1.14 (which powered the first article) from v0.1.17. In that span: The completion engine was rewritten from scratch. It's now scope-aware with inheritance resolution -- type self. inside a function and get the actual struct fields from the correct scope, not a flat dump of every symbol. Sub-millisecond, 0.3ms. Cross-file rename was fixed to use in-memory editor buffers. No more writing to disk behind your editor's back. Warning-only builds no longer block the AST cache. If your file has an unused variable warning, your go-to-definition still works. Test count went from 168 to 273. Every fix ships with regression coverage. ### New contributors This project is no longer a solo effort. Two new contributors joined since the last article: (Valentin B.) filed a wave of issues that exposed real bugs -- diagnostics not updating on warning-only builds, encoding-aware position conversion, document version handling -- and then contributed the fixes. The build module refactors that made the v0.1.17 diagnostic fix possible trace directly back to his work. @libkakashi added Zed editor setup documentation, and is now working on something bigger: a Tree-sitter and Solar parser-based version of this LSP. The current server leans on Foundry's AST output. A Solar parser backend would mean faster parsing, no dependency on forge build for the AST, and potentially opening the door to projects that don't use Foundry at all. That work is in progress. ### Why this matters solc is faster on the methods it supports. That's expected -- it's a compiler with the AST in memory. But it only covers 5 of 13 methods. No references, no completion, no document symbols, no formatting, no document links, no workspace search, no declaration, no prepareRename. The JS-based servers cover more methods in theory but can't produce a single response within 5 seconds on a Foundry project. solidity-language-server is the only one that answers all 13 and stays under 17ms on every single one. 273 tests. \~8MB memory. Written in Rust, powered by basically Solidity's JSON AST. ```bash cargo install solidity-language-server # Or grab a pre-built binary from the release (macOS ARM/x86, Linux, Windows). GPG-signed checksums included. Works with Neovim, Zed, and any LSP-compatible editor. solidity Language Server ``` ## v0.1.25: 140 MB Reclaimed, 4x Faster Hover, and Still the Only Server That Passes Everything The last article covered v0.1.17 -- 6 servers, 13 methods, one winner. Since then, eight releases have shipped. v0.1.25 is a different server than the one I wrote about two months ago. The feature set has doubled, the internals have been rewritten, and I've spent the last few releases doing something most people skip: making it use less memory, not more. Here's what happened. ### The memory problem Between v0.1.17 and v0.1.24, I added a lot: a typed Solidity AST module, a declaration index for cross-file references, NatSpec documentation on hover with `@inheritdoc` resolution, gas estimates, signature help, inlay hints. Every feature added data structures. Every data structure retained memory. By v0.1.24, RSS on a 95-file project (Uniswap v4-core PoolManager.t.sol) had grown from 230 MB to 394 MB. That's a 71% regression. Unacceptable for a tool that's supposed to be lightweight. v0.1.25 is the cleanup release. I profiled the server with DHAT, identified where the allocations were going, and systematically eliminated waste. #### What I did **Removed the raw AST from memory.** After building the typed declaration index, the server was retaining the entire JSON AST (a `serde_json::Value` tree) in the `CachedBuild` struct. Tens of megabytes per project, sitting there doing nothing. Dropped it. **Replaced clone-then-strip with build-filtered-map.** The old code cloned every AST node and then stripped fields it didn't need. The new `build_filtered_decl()` and `build_filtered_contract()` functions iterate over borrowed node fields and only copy what passes the filter. This eliminated 234 MB of transient allocations -- from 629 MB total down to 395 MB. A 37% reduction in allocation volume. **Pre-sized every HashMap.** `cache_ids()`, `extract_decl_nodes()`, `build_completion_cache()`, `build_hint_index()`, `build_constructor_index()` -- all of them now use `with_capacity()` based on the known node count. Small change, measurable impact on fragmentation. #### The numbers | Metric | Before | After | Delta | | ---------------------------- | ------- | ------ | -------------------- | | Total allocated | 629 MB | 395 MB | -37% | | Peak memory (t-gmax) | 277 MB | 243 MB | -12% | | RSS observed | 394 MB | 254 MB | -36% | | vs v0.1.24 baseline (230 MB) | +164 MB | +24 MB | **Reclaimed 140 MB** | The remaining +24 MB gap versus the v0.1.24 baseline is real retained data -- 23 MB of `decl_index` structures that power cross-file references, hover docs, and signature help. Those features didn't exist in v0.1.24. The memory cost is the feature cost, not waste. ### Performance: v0.1.25 vs v0.1.24 I benchmarked both releases against Shop.sol (272 lines, single file, Foundry project). 10 iterations, 2 warmup rounds, p95 latency. | Method | v0.1.25 | v0.1.24 | Speedup | | ------------------- | ------- | ------- | -------- | | initialize | 8.6ms | 10.9ms | 1.3x | | definition | 2.3ms | 2.6ms | 1.1x | | declaration | 0.3ms | 1.9ms | **6.3x** | | hover | 1.2ms | 5.0ms | **4.2x** | | references | 0.6ms | 2.2ms | **3.7x** | | completion | 0.3ms | 0.3ms | -- | | rename | 0.9ms | 3.6ms | **4.0x** | | prepareRename | 0.3ms | 0.3ms | -- | | documentSymbol | 1.7ms | 2.1ms | 1.2x | | formatting | 17.0ms | 20.5ms | 1.2x | | semanticTokens/full | 3.6ms | 2.8ms | 0.8x | | workspace/symbol | 1.2ms | 2.1ms | 1.8x | v0.1.25 wins 10 of 13 methods. The highlight is hover: 5.0ms down to 1.2ms. Declaration went from 1.9ms to 0.3ms. Rename from 3.6ms to 0.9ms. These aren't micro-optimizations -- they're the result of removing the raw AST from the hot path and using the typed index directly. The one regression is `semanticTokens/full` (2.8ms to 3.6ms). Tree-sitter parsing overhead increased slightly with the new build pipeline. Worth investigating, but still sub-4ms. ### The competition: 5 servers, 18 methods The full Shop.sol benchmark runs v0.1.25 against four other servers: solc 0.8.26, qiuxiang 0.5.4, juanfranblanco 0.0.187, and nomicfoundation 0.8.25. #### The scorecard | Server | Wins | Out of 18 | | ------------------------------------ | ------ | --------- | | **solidity-language-server v0.1.25** | **15** | **18** | | solc | 1 | 18 | | nomicfoundation | 1 | 18 | | qiuxiang | 0 | 18 | | juanfranblanco | 0 | 18 | solc wins diagnostics (3.4ms vs 74.3ms) because it's the compiler -- parsing is its job, and it doesn't run `forge lint` on top. nomicfoundation wins `textDocument/definition` at 1.6ms, though it resolves to `Shop.sol:21` (the contract declaration) while my server resolves to `Shop.sol:68` (the actual `PRICE` variable definition). A fast wrong answer. #### Method coverage | Method | v0.1.25 | solc | qiuxiang | juanfranblanco | nomicfoundation | | -------------------- | ------- | ------- | -------- | -------------- | --------------- | | initialize | 9.9ms | 311.8ms | 184.9ms | 651.8ms | 849.8ms | | diagnostics | 74.3ms | 3.4ms | 146.1ms | 812.7ms | 546.8ms | | semanticTokens/delta | 1.5ms | error | -- | -- | -- | | definition | 3.5ms | 2.2ms | 20.2ms | 66.2ms | 1.6ms | | declaration | 0.2ms | -- | -- | -- | -- | | hover | 1.2ms | crash | 19.8ms | 69.4ms | 1.6ms | | references | 0.8ms | 2.1ms | 20.7ms | 75.9ms | 1.8ms | | completion | 0.7ms | 2.4ms | 20.2ms | 65.7ms | 34.6ms | | signatureHelp | 0.9ms | -- | empty | empty | empty | | rename | 1.2ms | 2.4ms | 20.6ms | 65.7ms | 1.9ms | | prepareRename | 0.2ms | -- | -- | -- | -- | | documentSymbol | 1.2ms | -- | -- | 14.7ms | 17.4ms | | documentLink | empty | -- | -- | -- | -- | | formatting | 14.1ms | 2.2ms | 20.0ms | 60.4ms | 193.2ms | | inlayHint | 1.5ms | -- | -- | -- | -- | | semanticTokens/full | 1.6ms | error | -- | -- | 15.7ms | | semanticTokens/range | 1.1ms | -- | -- | -- | -- | | workspace/symbol | 1.1ms | -- | -- | timeout | -- | The `--` entries mean the server doesn't support the method. `empty` means it accepted the request but returned nothing. `crash` means solc's hover handler crashed on this input. Eight of 18 methods are only supported by v0.1.25: declaration, prepareRename, signatureHelp, inlayHint, semanticTokens/delta, semanticTokens/range, documentSymbol (sole working response), and workspace/symbol. solc supports 7 methods (initialize, diagnostics, definition, references, completion, rename, formatting) and errors or crashes on the rest. The JS-based servers support more methods in theory but every response is 15-70x slower. ### What shipped between v0.1.17 and v0.1.25 For anyone following along, here's the condensed release log: **v0.1.18** -- Context-sensitive `type(X).` completions. SIMD-accelerated position calculation via `lintspec-core`. Fixed a SIMD chunk boundary bug that was sending goto-definition to the wrong file. **v0.1.19** -- Rewrote `documentSymbol` and `workspace/symbol` to use tree-sitter instead of the Forge AST. Symbols work immediately on file open, no build required. 3.2x and 6.4x faster respectively. **v0.1.20** -- Tree-sitter enhanced goto definition. Inlay hints v2 with tree-sitter positions. `NodeId`/`FileId` newtypes and a shared `SourceLoc` parser. Respect `foundry.toml` lint ignore config. **v0.1.21** -- Auto-detect solc version from `pragma solidity` and resolve matching binary. Foundry.toml support for `via_ir`, optimizer, evm\_version. Gas estimates in hover and inlay hints. Callsite parameter documentation. NatSpec `@inheritdoc` resolution via function selectors. Dropped code lens -- gas info covered by inlay hints and hover. **v0.1.22** -- Use `svm-rs` as a library. No more shelling out to the svm CLI. **v0.1.23** -- `textDocument/signatureHelp` -- shows the active parameter while typing function calls, event emits, and mapping access. **v0.1.24** -- Project-wide source indexing for cross-file references. Semantic tokens range and delta. `documentLink` scoped to imports only (was linking every identifier). Performance: dropped the optimizer and conditionally excluded gasEstimates from solc input. **v0.1.25** -- The memory release. 140 MB reclaimed. 37% fewer allocations. 4x faster hover. That's 8 releases, 290 new tests (from 168 to 458), and a server that went from "fast but basic" to "fast and complete." ### What's next The server still has work ahead. `textDocument/codeAction` (quick fixes), `textDocument/codeLens` (inline "Run Test" for `.t.sol` files), and `window/workDoneProgress` for long-running operations are the next infrastructure targets. `textDocument/documentColor` for hex color literals in NFT contracts is scoped. And there's a VS Code extension in the works -- right now installation is `cargo install` or a pre-built binary, but that's a barrier for anyone who isn't comfortable with the terminal. The bigger architectural question is whether to move diagnostics off `forge build` entirely. The Solar parser integration is in progress -- it already handles lint diagnostics in a stub form. A full Solar backend would mean no Foundry dependency for basic features, faster cold starts, and support for projects that don't use Foundry at all. ### Try it ```bash cargo install solidity-language-server ``` Or grab a pre-built binary from the release page. GPG-signed checksums included. Works with Neovim, Helix, Zed, and any LSP-compatible editor. Benchmark source: github.com/mmsaki/lsp-bench The server: github.com/mmsaki/solidity-language-server ## v0.1.26: File Operations You Can Trust, Cleaner Auto-Import, and Better Bench Coverage v0.1.26 is a workflow release. Not a flashy benchmark-only release, not a refactor release for the sake of internals, and not a one-feature drop. This one closes several editor pain points that show up in daily Solidity work: creating files, renaming files, deleting files, and getting imports updated without weird buffer races. If you’ve ever renamed a Solidity file and then watched Foundry fail because buffers were ahead of disk, this release is aimed directly at that class of problem. ### What shipped #### 1) File operations are now configurable All file-operation behaviors are controlled via settings: * `fileOperations.templateOnCreate` * `fileOperations.updateImportsOnRename` * `fileOperations.updateImportsOnDelete` Defaults are enabled in v0.1.26. That gives you sane behavior out of the box, while still letting teams opt out if they want explicit/manual workflows. Example (Neovim): ```lua settings = { ["solidity-language-server"] = { fileOperations = { templateOnCreate = true, updateImportsOnRename = true, updateImportsOnDelete = true, }, }, } ``` #### 2) `templateOnCreate` is the canonical naming Scaffolding behavior was standardized under `templateOnCreate`. This avoids terminology drift and keeps editor settings consistent across clients. The behavior is the same idea as before, but now the setting is clearer and future-proof. #### 3) Create-file scaffolding lifecycle was fixed A lot of subtle bugs around `willCreateFiles`/`didCreateFiles` are timing bugs between disk, buffer state, and client lifecycle order. v0.1.26 improves this flow so new files reliably receive scaffold content without empty-file races or accidental duplicate insertion. Example scaffold produced for `MyToken.sol`: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract MyToken { } ``` What generated files look like in v0.1.26: `Vault.sol` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Vault { } ``` `Vault.t.sol` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Test} from "forge-std/Test.sol"; contract VaultTest is Test { } ``` `Vault.s.sol` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Script} from "forge-std/Script.sol"; contract VaultScript is Script { } ``` #### 4) Auto-import completion is more reliable Top-level symbol completion and import-edit attachment were improved. The practical result: completions that should add imports are far more predictable, especially in larger projects where candidate selection can be noisy. Example: ```solidity // In A.sol contract A { function f() external {} } // In B.sol (typing `A`) contract B { A a; } ``` Completion now more reliably attaches import edits like: ```solidity import {A} from "./A.sol"; ``` #### 5) Benchmarks expanded around file operations Coverage was added/updated for file operation lifecycle flows: * `workspace/willCreateFiles` * `workspace/willRenameFiles` * `workspace/willDeleteFiles` This matters because these flows are where “works in theory” often diverges from “works in editor under real usage.” Representative lifecycle request shapes: ```json { "method": "workspace/willCreateFiles", "params": { "files": [{ "uri": "file:///.../C.sol" }] } } ``` ```json { "method": "workspace/willRenameFiles", "params": { "files": [{ "oldUri": "file:///.../A.sol", "newUri": "file:///.../AA.sol" }] } } ``` ```json { "method": "workspace/willDeleteFiles", "params": { "files": [{ "uri": "file:///.../A.sol" }] } } ``` ### Why this release matters Most Solidity LSP discussions focus on the obvious methods: definition, hover, references, completion. But the real friction for production users is often file lifecycle correctness: * You rename a file. * Imports are patched in open buffers. * Disk isn’t updated yet. * `forge test`/`forge build` fails. From the LSP side, behavior can be perfectly valid while UX still feels broken unless users understand when to persist edits. That’s why this release also came with practical docs updates around editor workflows (for example, writing all changed buffers after renames). ### Current benchmark snapshot context For the release line around v0.1.26, benchmark summaries now explicitly include file-operation methods in the reporting flow. This is important because file-op methods are often underrepresented in LSP comparisons even though they have high day-to-day impact. ### Upgrade notes If you’re already on `solidity-language-server`, v0.1.26 is a straightforward update: ```sh cargo install solidity-language-server ``` Or download binaries from: * /changelog If your team prefers explicit/manual file operation behavior, you can toggle the new settings accordingly. ### Final note v0.1.26 is about reducing “surprise cost.” When file operations happen, users should get deterministic results. When completions imply auto-import, they should attach edits correctly. And when benchmark claims are made, file-op lifecycle methods should be part of the evidence. That’s what this release delivers. ## Blog * [v0.1.26 Release Post](/blog/v0-1-26-release) * [Article 1](/blog/article1) * [Article 2](/blog/article2) * [Article 3](/blog/article3) * [Article 4](/blog/article4) ## v0.1.26 This release focuses on file lifecycle correctness and import reliability in real Solidity workflows. ### What shipped * Configurable file operations: * `fileOperations.templateOnCreate` * `fileOperations.updateImportsOnRename` * `fileOperations.updateImportsOnDelete` * Improved create-file scaffolding lifecycle behavior. * Improved auto-import completion behavior for top-level symbols. * Expanded benchmark coverage for file operation lifecycle methods. ### Generated file templates `Vault.sol` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Vault { } ``` `Vault.t.sol` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Test} from "forge-std/Test.sol"; contract VaultTest is Test { } ``` `Vault.s.sol` ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import {Script} from "forge-std/Script.sol"; contract VaultScript is Script { } ``` ### Upgrade ```sh cargo install solidity-language-server ``` ## Benchmark Overview All benchmark content below is rendered from markdown in this repository. ### Benchmark reports * [Shop.sol](/benchmarks/reports/shop) * [Pool.sol](/benchmarks/reports/pool) * [PoolManager.t.sol](/benchmarks/reports/poolmanager-t) ### Run locally ```sh lsp-bench -c benchmarks/shop.yaml -s benchmarks/servers.yaml lsp-bench -c benchmarks/pool.yaml -s benchmarks/servers.yaml lsp-bench -c benchmarks/poolmanager-t.yaml -s benchmarks/servers.yaml ``` ### Verify mode ```sh lsp-bench -c benchmarks/ci-verify.yaml -s benchmarks/servers.ci.yaml --verify lsp-bench -c benchmarks/ci-file-ops-verify.yaml -s benchmarks/servers.ci.yaml --verify ```