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

Caches

This page explains the full cache architecture used by the language server.

Cache layers

The server uses two layers:

  • In-memory caches: live state held on ForgeLsp behind Arc<RwLock<…>>. Used to answer all LSP requests without disk I/O.
  • On-disk cache (v2): warm-start data persisted under .solidity-language-server/. Allows fast restart without a full recompile.

In-memory caches

All in-memory caches are fields on ForgeLsp in src/lsp.rs.

ast_cache — the primary index

Arc<RwLock<HashMap<DocumentUri, Arc<CachedBuild>>>>

The central store. Two kinds of entries coexist using the same map:

  • Per-file entries — key is DocumentUri("file:///…/A.sol"). Created by on_change() after each successful single-file build.
  • Project-level entry — key is DocumentUri(project_cache_key()) (the workspace root URI). Created by the background project-index worker. Used for cross-file features (references, rename, goto across files).

Entries are never evicted by age. They are removed explicitly on didDeleteFiles, solidity.clearCache, solidity.reindex, and initialized() full-rebuild. A failed build leaves the previous entry intact.

text_cache

Arc<RwLock<HashMap<DocumentUri, (i32, String)>>>

Key = DocumentUri. Value = (version, content). Stores the live editor buffer text. Updated on didOpen, didChange, didSave. All LSP handlers read from here so they see unsaved edits without a disk read. Version guard: only updates when incoming_version >= stored_version to prevent older updates from overwriting newer ones.

completion_cache

Arc<RwLock<HashMap<DocumentUri, Arc<CompletionCache>>>>

Key = DocumentUri. Populated immediately after a successful build in on_change() by sharing the Arc<CompletionCache> that was already pre-built inside CachedBuild::new(). This is a convenience shortcut so the completion handler can access the completion index without locking the whole ast_cache. Evicted on didDeleteFiles.

semantic_token_cache

Arc<RwLock<HashMap<DocumentUri, (String, Vec<SemanticToken>)>>>

Key = DocumentUri. Value = (result_id, token_list). The result_id is a monotonically increasing string from semantic_token_id: Arc<AtomicU64>. Populated on textDocument/semanticTokens/full; used for delta computation on textDocument/semanticTokens/full/delta. Evicted on didDeleteFiles.

path_interner

Arc<RwLock<PathInterner>>

Project-wide file ID interner. Solc assigns file IDs sequentially based on input order, so the same file gets different IDs in different compilations. PathInterner assigns deterministic canonical IDs by mapping (file_id, id_to_path_map) pairs to stable FileId values.

Used during CachedBuild::new() to rewrite all src strings (offset:length:fileId format) in NodeInfo fields to use canonical IDs. Also seeds from persisted id_to_path_map during warm-load via from_reference_index(). Sub-caches (library sub-projects) pass None for the interner since they have isolated ID spaces.

Key methods:

  • intern(path) -> FileId — returns existing canonical ID or assigns the next one
  • resolve(file_id) -> Option<&str> — look up path by canonical ID
  • build_remap(solc_id_to_path_map) -> HashMap<u64, FileId> — builds a translation table from solc IDs to canonical IDs for a given compilation

Process-static: INSTALLED_VERSIONS

static OnceLock<Mutex<Vec<SemVer>>> in src/solc.rs.

Caches the list of solc versions installed by svm-rs. Populated lazily on first access via get_installed_versions(). Explicitly invalidated after a successful svm install via invalidate_installed_versions(). Never visible to LSP clients.


CachedBuild — field reference

CachedBuild in src/goto.rs is the pre-built AST index. The raw solc JSON is consumed in CachedBuild::new() and then dropped — nothing is retained in raw form. CachedBuild::new() accepts an optional &mut PathInterner — when provided, all src strings in NodeInfo fields are canonicalized through the interner so file IDs are deterministic across compilations.

FieldTypeBuilt byPurpose
nodesHashMap<AbsPath, HashMap<NodeId, NodeInfo>>cache_ids() in goto.rsTwo-level map: abs_path → node_id → NodeInfo. AbsPath is a typed wrapper over path strings. Core of goto-definition, references, rename.
path_to_absHashMap<RelPath, AbsPath>cache_ids()Maps solc-relative path → absolute path. Needed because solc outputs relative keys.
external_refsHashMap<SrcLocation, NodeId>cache_ids()SrcLocation("offset:length:fileId") → declaration NodeId for Yul externalReferences.
id_to_path_mapHashMap<SolcFileId, String>CachedBuild::new()Source file id → relative path. SolcFileId wraps the stringified solc id ("0", "34", …).
decl_indexHashMap<NodeId, DeclNode>solc_ast::extract_decl_nodes()Typed declaration lookup: function, variable, contract, event, error, struct, enum, modifier, UDVT. Keyed by NodeId.
node_id_to_source_pathHashMap<NodeId, AbsPath>solc_ast::extract_decl_nodes()O(1): declaration node id → source file absolute path. Avoids O(N) walk.
hint_indexHashMap<AbsPath, HintLookup>inlay_hints::build_hint_index()Keyed by AbsPath. Each HintLookup has by-offset and by-(name, arg_count) sub-indexes for resolving parameter names at call sites.
doc_indexHashMap<DocKey, DocEntry>hover::build_doc_index()Merged userdoc/devdoc. Keyed by 4-byte selector, 32-byte event topic, or "path:Name".
completion_cacheArc<CompletionCache>completion::build_completion_cache()Full completion index (see below). Wrapped in Arc so it can be shared into ForgeLsp.completion_cache without cloning.
build_versioni32Set from LSP document versionThe didChange/didOpen version that produced this build. Used to detect dirty files (text_version > build_version).
content_hashu64DefaultHasher on source textHash of the source text at compile time. Compared in on_change() to skip rebuilds when content is identical (format-on-save guard). 0 for project-index builds.
base_function_implementationHashMap<NodeId, Vec<NodeId>>build_base_function_implementation() in goto.rsBidirectional interface ↔ implementation equivalence. Maps each function to its equivalent IDs from baseFunctions/baseModifiers. Used by call hierarchy (incoming calls expands targets to include interface variants) and references (finds callers via interface-typed references). Populated on both fresh builds and warm loads.
qualifier_refsHashMap<NodeId, Vec<NodeId>>build_qualifier_refs() in goto.rsMaps container declaration ID (contract/library/interface) to IdentifierPath node IDs that use it as a qualifier prefix in qualified type paths (e.g., Pool in Pool.State). Used by references and rename to include qualifier usages.

NodeInfo stored per-node

  • srcSrcLocation ("offset:length:fileId" string wrapper)
  • name_location — optional nameLocation src for declarations
  • name_locationsVec<String> for IdentifierPath
  • referenced_declaration — optional declaration node id this node refers to
  • node_type — e.g. "FunctionDefinition"
  • member_location — for MemberAccess expressions
  • absolute_path — for ImportDirective nodes
  • base_functionsVec<NodeId> from the AST baseFunctions/baseModifiers array. Lists the parent interface/abstract function IDs that this function overrides. Used by build_base_function_implementation() to construct the bidirectional equivalence index. Empty for functions that don't override anything.
  • scope — optional NodeId; the AST scope field pointing to the containing declaration (contract, library, interface, function). Used by build_qualifier_refs() to resolve the container in qualified type paths like Pool.State.

Warm-loaded cache limitation

When a CachedBuild is reconstructed from the on-disk v2 cache via from_reference_index(), only nodes, path_to_abs, external_refs, id_to_path_map, and qualifier_refs are populated. decl_index, hint_index, doc_index, and completion_cache are all empty.

However, base_function_implementation IS populated on warm load — build_base_function_implementation() is called after from_reference_index() because it only depends on nodes (which is always available). This means call hierarchy incoming calls with interface ↔ implementation equivalence works immediately on warm start.

This means after a warm-load, cross-file goto-definition, references, call hierarchy, and implementation all work immediately. Hover docs, parameter inlay hints, and completions are not available until the first full solc_project_index() run completes. This is logged at startup and is by design: the warm-load prioritizes low latency for navigation features.


CompletionCache — field reference (src/completion.rs)

FieldPurpose
namesFlat list of all named identifiers (unscoped)
name_to_typeSymbolNameTypeIdentifier; used for dot-completion
type_to_nodeTypeIdentifier → defining node id
node_membersstruct/contract/enum/library member completion items
name_to_node_idSymbolName (contract/library/interface name) → node id
method_identifiers4-byte selectors from evm.methodIdentifiers
function_return_types(contract_id, fn_name) → return typeIdentifier (for foo().)
using_forTypeIdentifier → library functions via using X for T
using_for_wildcardFunctions from using X for *
general_completionsPre-built non-dot completion list (AST names + keywords + globals + units)
scope_declarationsDeclarations per scope node
scope_parentScope tree for upward scope walks
scope_rangesAll scope byte ranges sorted by span size (innermost-first)
path_to_file_idRelPath → AST file id (for scope-aware lookup)
linearized_base_contractsC3 linearization per contract (for inherited member lookup)
contract_kinds"contract", "interface", or "library"
top_level_importables_by_nameImport-on-completion: SymbolName → candidates
top_level_importables_by_fileSame data keyed by RelPath (for incremental invalidation)

On-disk cache (v2)

Source: src/project_cache.rs

Location: <project_root>/.solidity-language-server/

Files

PathContent
.solidity-language-server/.gitignore* — ensures the entire directory is gitignored
.solidity-language-server/solidity-lsp-schema-v2.jsonMain index file (PersistedReferenceCacheV2)
.solidity-language-server/reference-index-v2/<keccak256_hex>.jsonOne shard per source file (PersistedFileShardV2)
.solidity-language-server/last-solc-input.jsonMost recent solc --standard-json input payload (debug/repro aid), overwritten on each solc run

The .gitignore is written automatically the first time the cache directory is created.

Index file schema (PersistedReferenceCacheV2)

FieldPurpose
schema_versionMust equal 3 to be loaded
project_rootAbsolute path to the project root
config_fingerprintKeccak256 of config inputs (see below)
file_hashesBTreeMap<rel_path, keccak256_hex> — current content hashes at save time
file_hash_historyBTreeMap<rel_path, Vec<String>> — up to 8 historical hashes per file (reserved for future use; not currently read during load)
path_to_absRelative → absolute path map
id_to_path_mapSource file id → relative path
external_refsYul external reference entries
node_shardsBTreeMap<rel_path, shard_filename>

Shard files (PersistedFileShardV2)

Each shard stores one source file's node entries:

{
  "abs_path": "/path/to/file.sol",
  "entries": [{ "id": 12345, "info": { "src": "0:100:0", ... } }]
}

Shard filename = keccak256(rel_path_bytes) as hex + .json. All writes use write-to-tmp + fs::rename() for atomicity.


Cache invalidation

Config fingerprint

config_fingerprint is a Keccak256 hash of the inputs that affect the reference index. Any change triggers a full cache miss and clean rebuild:

InputWhy it matters
lsp_versionAny server binary upgrade rebuilds from scratch
solc_versionDifferent compiler → different AST node IDs
remappingsAffects which files are resolved
evm_versionCan change which code paths solc parses
sources_dir / libsDetermines the set of source files in scope
via_irYul IR pipeline can produce different AST node IDs

Optimizer settings (optimizer, optimizer_runs) are intentionally excluded — they affect bytecode but not the AST shape or node IDs.

Per-file content hashes

Each file has an independent Keccak256 content hash in file_hashes. A fingerprint match but changed file hash triggers a scoped reconcile for only those files, without discarding the whole cache.

In-memory content hash (CachedBuild.content_hash)

Separate from the on-disk hash. Uses DefaultHasher (not keccak256) applied to the saved text in on_change(). Only used for within-session idempotency: if the incoming text hash matches the last build's hash, the build is skipped entirely. Not used for on-disk cache validation.


Startup (warm load)

On initialized(), the server tries to load the v2 cache:

The warm-load CachedBuild (from from_reference_index()) has only navigation data. After the scoped or full recompile, decl_index, hint_index, doc_index, and completion_cache become available.


During editing: two async workers

On each didSave, the server may launch two independent background workers:

Fast-path upsert worker (350ms debounce)

Drains project_cache_upsert_files (the saved file's abs path). Reads the authoritative root-key CachedBuild from ast_cache (the merged build with correctly remapped global file IDs), then calls upsert_reference_cache_v2_with_report():

  • Reads and parses the existing index file (for file_hashes and node_shards of unchanged files).
  • Writes only the changed per-file shards.
  • Serializes path_to_abs, id_to_path_map, and external_refs directly from the in-memory merged build, ensuring the disk cache always mirrors the authoritative in-memory state.

This keeps the on-disk cache data fresh between full project reindexes without introducing file ID remapping drift.

Full sync worker (700ms debounce)

Set when project_cache_dirty is true (after rename/delete/reindex events). Drains project_cache_changed_files, computes compute_reverse_import_closure() (files that import the changed files), and runs solc_project_index_scoped() to recompile the affected closure. Result is merged and persisted via save_reference_cache_with_report().

Single-flight guards: both workers have _running / _pending atomics so at most one of each runs at a time. If a save arrives while a worker is already running, _pending is set and the worker loops once more after finishing before stopping.


Commands

The server exposes two workspace/executeCommand commands for cache management. Both are advertised during initialize.

solidity.clearCache

Deletes the entire .solidity-language-server/ directory on disk and wipes the in-memory ast_cache entry for the current project root. The next file save or open triggers a clean rebuild from scratch.

Use this when the cache is corrupt, after a major foundry config change, or when you want to verify a fresh index.

nvim:
vim.lsp.buf.execute_command({ command = "solidity.clearCache" })
VS Code / Cursor:
vscode.commands.executeCommand("solidity.clearCache");

Helix (:lsp-execute-command solidity.clearCache — requires Helix ≥ 24.03)

Returns { "success": true } on success, or a JSON-RPC InternalError if the directory could not be removed.

solidity.reindex

Evicts the in-memory ast_cache entry for the current project root and sets project_cache_dirty so the background sync worker triggers a fresh scoped index build. The on-disk cache is left intact, so the warm-load will be fast.

Important: if no file is saved after calling solidity.reindex, no reindex actually runs. The command arms the dirty flag; the sync worker is triggered on the next didSave.

nvim:
vim.lsp.buf.execute_command({ command = "solidity.reindex" })
VS Code / Cursor:
vscode.commands.executeCommand("solidity.reindex");

Returns { "success": true }.

Comparison

solidity.clearCachesolidity.reindex
Deletes .solidity-language-server/ on diskYesNo
Clears in-memory ast_cacheYesYes
Triggers background reindexYes (from scratch)Yes (warm from disk)
Speed of next index buildSlow (full recompile)Fast (warm load)
Use whenCache corrupt / config changedIndex feels stale

Settings that affect cache behavior

Under projectIndex in editor settings:

SettingPurpose
cacheMode"v2" or "auto" (alias for "v2"). V2 is the only active mode.
incrementalEditReindexEnable the scoped affected-file reindex path on save
incrementalEditReindexThresholdRatio gate: if affected-file fraction exceeds this, fall back to full reindex
fullProjectScanRun a full project index on the first successful single-file build (default: true)

See the setup schema in Setup Overview.


Main implementation files

FileRole
src/goto.rsCachedBuild, NodeInfo, cache_ids(), from_reference_index(), remap_src_canonical(), canonicalize_node_info(), build_qualifier_refs(), resolve_qualifier_goto(), find_node_info()
src/references.rsdedup_locations(), resolve_qualifier_target(), collect_qualifier_references(), goto_references_for_target(), resolve_target_location()
src/project_cache.rsAll on-disk v2 persistence: save, load, upsert, shard logic
src/lsp.rsOwns all in-memory caches; drives lifecycle via LSP events; both async workers
src/completion.rsCompletionCache, build_completion_cache()
src/inlay_hints.rsHintIndex, HintLookup, build_hint_index()
src/hover.rsDocIndex, DocEntry, build_doc_index()
src/types.rsPathInterner, NodeId, FileId, SrcLocation, AbsPath, RelPath newtypes
src/solc.rsINSTALLED_VERSIONS static cache; solc_ast(), solc_project_index()