Foundry-native Solidity IDE for VS Code and Cursor.
An LSP-powered extension that gives Solidity the IDE experience that languages like TypeScript and Rust take for granted: reliable navigation, real refactoring, semantic highlighting, call hierarchy, auto-imports, integrated testing, gas profiling, coverage overlays, static analysis, and deep Foundry toolchain integration — in a single extension.
- Why Solidity Workbench?
- Feature overview
- Requirements
- Installation
- Quick start
- Language features in depth
- Foundry integration
- Static analysis
- Configuration
- Architecture
- Development
- Roadmap
- Contributing
- License
Solidity Workbench is built for teams that already live in Foundry. Every feature assumes
forge, cast, anvil, and chisel exist on PATH. The language server and client are
written in TypeScript, run as part of the standard LSP pipeline, and require no Python
runtime.
| Feature | Implementation |
|---|---|
| Semantic syntax highlighting | 20 token types, 10 modifiers, AST-driven |
| Go-to definition / type definition | Cross-file, remapping-aware, solc-enriched overload disambiguation |
| Find all references | Solc declaration-ID graph when warm; inverted index fallback |
| Rename symbol | Workspace-wide for top-level names; scope-aware via solc AST for locals/parameters |
| Hover | Signatures, NatSpec, built-in globals, solc-backed disambiguation |
| Autocompletion | Context-aware: members, keywords, types, snippets, imports, NatSpec tags |
| Auto-import | Code actions for forge-std/..., OpenZeppelin, relative paths |
| Signature help | Parameter hints with NatSpec docs, overload switching, built-ins |
| Inlay hints | Parameter name hints at single-line and multi-line call sites |
| Document / workspace symbols | Outline, Go to Symbol, Quick Open |
| Formatting | Backed by forge fmt — full document and range |
| Code actions | Add SPDX, pin pragma, add visibility, constant/immutable hints, replace tx.origin, implement interface, NatSpec stub, auto-import |
| Code lens | Run Test / Debug, function selectors, event topic0, reference counts, gas estimates |
| Call hierarchy | Incoming/outgoing with receiver-qualifier disambiguation |
| Type hierarchy | Supertypes and subtypes across the is graph |
| Implementation lookup | Interface / virtual method implementations across inheritance trees |
| Folding / selection ranges | AST-backed declaration folds and smart selection expansion |
| Document links | Clickable import paths resolved through Foundry remappings |
| Diagnostics | Fast parser + linter on keystroke; full forge build --json on save |
AST-based rules run on every keystroke with zero false positives on commented-out code.
Suppressible with // solidity-workbench-disable-next-line [rule]:
Security
reentrancy— state writes after external calls (CEI violation)unchecked-call—.call(...)without success checkdangerous-delegatecall— any.delegatecall(...)unprotected-selfdestruct—selfdestructwith no access controlecrecover-zero-check— ecrecover result captured but not zero-checkedunsafe-erc20-call—IERC20.transfer/transferFrom/approvewithoutSafeERC20weak-prng—block.*/blockhashused as randomness (e.g.block.timestamp % N)incorrect-strict-equality—==/!=onblock.*/addr.balance
Correctness
missing-zero-check— address params not checked againstaddress(0)missing-event— state-changing functions that emit no eventsboolean-equality—x == true/x != falsedivide-before-multiply—(a / b) * cprecision lossshadowing-state— parameter / local shadows a state variable
Best Practice / Style
large-literal— magic numbers that should be named constantsempty-block— empty function body (constructor / fallback / receive exempt)payable-fallback— non-payablefallback()with noreceive()func-visibility-explicit— function with no visibility specifiermultiple-pragma— twopragma soliditydirectives in one file
Gas
storage-in-loop— state-var reads insidefor/whileloopsstate-could-be-constant— write-once state var only initialized at declarationstate-could-be-immutable— state var only assigned in the constructor
| Command | Runs |
|---|---|
| Build | forge build |
| Run Tests / Test File / Test at Cursor | forge test with match flags |
| Test Explorer | Tree view, pass/fail, fuzz counterexamples, inline gas |
| Format | forge fmt (on-save supported) |
| Gas Snapshot | forge snapshot with inline decorations and regression deltas |
| Gas Diff | Side-by-side compare of two .gas-snapshot files |
| Coverage | forge coverage --report lcov with line-level gutter decorations |
| Flatten | forge flatten |
| Storage Layout | Interactive webview via forge inspect |
| Inheritance Graph | Webview rendering of the contract is-graph |
| ABI Explorer | Webview for an artifact's functions, events, and errors |
| IR Viewer | Yul (raw / optimized) and EVM assembly via forge inspect, with a function-level TOC, cursor-aware open, auto-refresh on save, and pinned-baseline per-function deltas for iterative optimization |
| Script: Simulate / Broadcast / Resume | forge script with Ledger / Keystore / Interactive signing |
| Deploy Contract | Guided forge create with typed constructor args and verification |
| Verify / Check Verification | forge verify-contract / forge verify-check |
| Debug Test / Script / Transaction | forge test --debug / forge debug in terminal |
- Anvil: Start / stop / fork from any RPC with optional block pin; status in the status bar
- Cast:
sig,4byte,calldata-decode,abi-encode,keccak,to-wei,from-wei,balance,storage - Chisel: Long-lived REPL hosted in a webview with evaluation history (capped at 200 entries, persisted across sessions). Start fresh or forked; send editor selection directly to it.
- Remote Chain UI: ABI-aware webview for
cast callagainst seeded public RPCs (Mainnet, Sepolia, Base, Base Sepolia, Optimism, Arbitrum One, Polygon PoS) or any custom RPC. ABI loaded by paste or by picking a forge artifact underout/.
- Indexer Scaffold — generate a Graph / Ponder / Envio project skeleton from a compiled contract artifact
- Subgraph Scaffold — emit
subgraph.yaml,schema.graphql, and AssemblyScript mappings for an event-only subgraph
Completions, hover docs, and value validation for [profile.default], [fmt], [fuzz],
[invariant], [rpc_endpoints], and [etherscan] keys.
- Built-in linter — 21 AST-based rules in the LSP (see above)
- Slither (optional) — findings surfaced as VS Code diagnostics with severity mapping; auto-runs on save when enabled
- Aderyn (optional) — Cyfrin's Rust analyzer; JSON report parsed and mapped to diagnostics with related-information links between instances; on-demand or on-save
- Wake (optional) — Ackee Blockchain's Python framework;
wake detect all --output-format jsonparsed and mapped to diagnostics, with confidence rating in each message and column-precise ranges when Wake provides them; on-demand or on-save - Mythril (optional) — ConsenSys's symbolic-execution analyzer; runs
myth analyze -o jsonagainst the active file, with a status-bar progress spinner and SWC-registry codes attached to each diagnostic. Per-file rather than per-workspace because symbolic execution is slow; on-save trigger is opt-in.
Static analysis (built-in linter + the syntax-rule warnings: missing-spdx,
floating-pragma, tx-origin, deprecated-selfdestruct) is suppressed on
test/, script/, and lib/ files — those findings are noise on code
that's never deployed or that the user can't modify. solc/forge build
diagnostics still flow through on every tier.
.gas-snapshottree view grouped by contract, with regression deltas (up/down/unchanged) against previous baseline- Inline gas decorations next to every test function
- Line-level coverage via LCOV — covered / uncovered / partial gutter decorations; per-file totals in the status bar
- VS Code >= 1.85.0 (or Cursor on the same engine)
- Foundry on PATH — install via foundryup:
curl -L https://foundry.paradigm.xyz | bash foundryup - Slither (optional) — only if
solidity-workbench.slither.enabledis set totrue
Node.js >= 18 is required only when building from source. The packaged VSIX bundles its own server.
The extension activates automatically in any workspace containing foundry.toml or *.sol files.
Install the published extension from the Open VSX Registry:
- Open the link above and click Install, or search Solidity Workbench in the Extensions view (
ccashwell.solidity-workbench). - CLI (VS Code, Cursor, and other editors that use Open VSX):
code --install-extension ccashwell.solidity-workbench
New releases are published to Open VSX on every v*.*.* tag.
- Download
solidity-workbench-*.vsixfrom the latest GitHub Release. - Install:
Cmd/Ctrl+Shift+P → Extensions: Install from VSIX...
git clone https://github.com/ccashwell/solidity-workbench
cd solidity-workbench
pnpm install
pnpm buildPress F5 in VS Code to launch an Extension Development Host with the extension loaded.
- Open a Foundry project (must contain
foundry.toml). - Open any
.solfile — the extension activates, indexes the workspace, and runsforge build. - Try:
- Hover a symbol for its signature and NatSpec
F12on an import path to jump to the resolved fileShift+F12for Find All ReferencesF2to rename (workspace-wide or scope-aware for locals)Ctrl+Spaceinside a function call for signature help- Click Run Test in the code lens above any
test_*function Cmd/Ctrl+Shift+P→ Solidity Workbench: Gas Snapshot for inline gasCmd/Ctrl+Shift+P→ Solidity Workbench: Run Coverage for line-level decorationsCmd/Ctrl+Shift+P→ Solidity Workbench: Storage Layout Visualization for the slot webview- With your cursor inside a function,
Cmd/Ctrl+Shift+P→ Solidity Workbench: View IR (Yul / Assembly) — the panel opens scrolled to that function's Yul block, then auto-refreshes on save
| Tier | Trigger | Source | Latency |
|---|---|---|---|
| Fast | Every keystroke (debounced) | @solidity-parser/parser + SPDX / pragma / tx.origin checks |
~5–15 ms |
| Lint | Same debounce | AST linter (21 rules) | ~10–30 ms |
| Full | On save | forge build --json mapped via LineIndex (CRLF + UTF-8 safe) |
Hundreds of ms |
- Go to Definition (
F12): symbol index with inheritance chain traversal; solc AST for overload disambiguation - Go to Type Definition: resolves to the declaring contract / struct / enum / UDVT
- Go to Implementation: jumps from interfaces / virtual methods to concrete implementors
- Find References (
Shift+F12): solc declaration-ID lookup when the rich AST is warm, with O(1) inverted-index fallback; includes/excludes declarations per context - Workspace Symbols (
Cmd/Ctrl+T): substring search over all top-level symbols - Document Links: import strings are clickable and resolve through Foundry remappings
- Rename (
F2): workspace-wide for top-level names (contracts, functions, events, errors, structs, enums, modifiers, state variables, UDVTs). Locals and parameters rename scope-aware via the solc AST. Ambiguous renames are refused with an explanation.lib/directories are excluded. - Code actions: add SPDX, pin floating pragmas, add missing visibility, apply constant/immutable hints, replace
tx.origin, generate NatSpec stub, implement interface methods, auto-import unresolved symbols
20 token types with 10 modifiers (declaration, readonly, virtual, override, documentation, defaultLibrary, etc.) for state variables, locals, parameters, functions, modifiers, events, errors, contracts, interfaces, libraries, structs, and enums.
Reference-site tokens use a per-function text scan; collisions between identically-named identifiers in different functions are a known limitation.
Parameter name hints at single-line and multi-line call sites: transfer(›to:‹ alice, ›amount:‹ 100).
Toggle: solidity-workbench.inlayHints.parameterNames.
Triggers on ( and ,. Shows the full signature with visibility, mutability, return types,
NatSpec @notice / @dev / @param docs, overload switching, and built-in signatures for
require, assert, revert, keccak256, sha256, ecrecover, addmod, mulmod, blockhash.
Per function / event / contract:
N references— click opens Find Referencesselector: 0x....— fromforge buildoutput (struct/UDT-accurate), keccak256 fallback; click to copytopic0: 0x....— for events~{gas} gas— from.gas-snapshotRun Test/Debug— in.t.solfilesextends A, B, C— on contracts with base contracts
Incoming and outgoing calls with receiver-qualifier disambiguation — token.transfer()
resolves to the correct contract, not every transfer() in the workspace. State variables,
parameters, this, and super all resolve correctly.
Supertypes and subtypes across the entire is inheritance graph.
All commands available via Cmd/Ctrl+Shift+P → "Solidity Workbench: ...".
- Build —
forge build - Run Tests / Test File / Test at Cursor —
forge testwith--match-test/--match-path - Test Explorer — tree view with pass/fail, fuzz counterexamples, inline gas
- Gas Snapshot —
forge snapshotwith inline decorations and regression tracking - Coverage / Clear Coverage —
forge coverage --report lcovwith line-level coloring - Flatten —
forge flatten - Storage Layout — CLI output or interactive webview
- Simulate / Broadcast / Resume —
forge scriptwith Ledger, Keystore, Interactive, or env-var signing - Deploy Contract — guided
forge createwith artifact picker, typed constructor args, Etherscan verification - Verify Contract / Check Status —
forge verify-contract/forge verify-check
- Anvil: start / stop / fork from any RPC with optional block pin
- Chisel: long-lived REPL hosted in a webview with persistent evaluation history (capped at 200 entries, stored in
globalState); start fresh or forked; send editor selection to it - Remote Chain UI: ABI-aware webview that wraps
cast callagainst seven seeded public RPCs plus a custom-RPC escape hatch; ABI loaded by paste or by picking a forge artifact
Quick picks for sig, 4byte, calldata-decode, abi-encode, keccak, to-wei, from-wei, balance, and storage.
Two paths:
- Terminal fallback:
forge test --debug,forge debug, orforge debug --rpc-url <url> <txHash>for the canonical Foundry TUI. - Native DAP adapter (
type: "solidity-workbench"): step through any EVM trace from VSCode's Run-and-Debug panel. Two ways to feed a trace in:- Live: set
txHash(and optionallyrpcUrl) in launch.json — the adapter spawnscast run --json <txhash>itself. - Saved: set
traceFileto a previously-capturedcast run --json <txhash> > trace.json. Pair either with the contract's forge artifact:- Source-aware step over / step in / step out / continue,
with line breakpoints set in the gutter of any
.solfile. - Internal call stack built from solc's source-map jump markers — every Solidity function call appears as its own DAP frame with its own source position.
- Stack / Memory / Storage scopes in the Variables panel,
with storage entries pretty-printed via the artifact's
storageLayout(socountshows up under its declared name, packed slots surface every entry that occupies them). - Hover evaluation for
stack[N],memory[0xN],storage[<slot>], and bare state-variable identifiers. - Disassembly view of the deployed bytecode through Cancun + post-Cancun opcodes.
- Source-level locals with live values for the enclosing
function's parameters and in-scope locals. Parameters
resolve via Solidity's standard calling convention (slots
[entryStackLen − paramCount, entryStackLen)); locals are tracked incrementally as their declarations execute. Hover-evaluate uses the same lookup so an in-scope identifier resolves to a live value, with locals/params shadowing state-variable names per Solidity's lexical rules. - Multi-contract source resolution and per-depth
internal call stacks: list deployed contracts under
contracts: [{ address, artifact }]in launch.json and the adapter tracks an independent internal-frame stack per EVM call depth — each Solidity function call appears as its own DAP frame whether it's an internal jump or a cross-contract CALL. See the launch.json snippet (Cmd+Shift+P → "Debug: Add Configuration..." → "Solidity Workbench: Debug Foundry Test").
- Source-aware step over / step in / step out / continue,
with line breakpoints set in the gutter of any
- Live: set
Webview that runs forge inspect <Contract> (irOptimized | ir | assembly) and renders the dump
alongside a function-level table of contents parsed from the Yul output. Built for iterative
optimization passes — see in real time how the compiler's optimizer represents your code.
- Variant switcher — flip between optimized Yul, raw Yul, and EVM assembly without reopening. Per-variant scroll position is persisted across switches.
- Function TOC — Yul function names get demangled to their Solidity-level identifier (
fun_increment_15surfaces as increment with the AST id in subscript). Bucketed by intent: External entries / Functions / Getters / Constructor / Modifiers expanded by default; ABI / Storage / Memory / Math / Reverts / Cleanup collapsed. - Cursor-aware open — invoking the command with your cursor inside a Solidity function scrolls the panel directly to that function's Yul block.
- Auto-refresh on save — debounced 600ms, scoped to the same Foundry root, with scroll position preserved per variant so the iterative-edit loop doesn't jerk you back to the top.
- Pin baseline + per-function deltas — capture the current output as a baseline; subsequent
refreshes annotate every TOC entry with
+12/-5/new/gone, plus a header banner with net line delta and grew/shrunk counts. Functions are matched by mangled name (which embeds the source AST id), so the comparison is stable across recompiles. - Assembly variant skips the TOC since it's an opcode listing, not Yul.
Command: Cmd/Ctrl+Shift+P → Solidity Workbench: View IR (Yul / Assembly), or right-click in any
.sol editor.
Twenty-one AST-based rules in the LSP server, running on every keystroke. Suppress per-line:
// solidity-workbench-disable-next-line reentrancy
recipient.call{value: amount}("");
balances[msg.sender] -= amount;Enable with solidity-workbench.slither.enabled = true. Findings appear as VS Code
diagnostics with related-information links. Run on demand or automatically on save.
All settings under solidity-workbench.*:
| Setting | Type | Default | Description |
|---|---|---|---|
foundryPath |
string | "" (PATH) |
Absolute path to forge binary |
formatOnSave |
boolean | true |
Run forge fmt on save |
diagnostics.compileOnSave |
boolean | true |
Run forge build on save for full diagnostics |
diagnostics.debounceMs |
number | 500 |
Debounce for real-time diagnostics (ms) |
slither.enabled |
boolean | false |
Enable Slither on-save analysis |
slither.path |
string | "" |
Absolute path to slither binary |
aderyn.enabled |
boolean | false |
Enable Aderyn on-save analysis |
aderyn.path |
string | "" |
Absolute path to aderyn binary |
wake.enabled |
boolean | false |
Enable Wake on-save analysis |
wake.path |
string | "" |
Absolute path to wake binary |
mythril.enabled |
boolean | false |
Enable Mythril analysis (per-file). Slow; on-save trigger is opt-in |
mythril.path |
string | "" |
Absolute path to myth binary |
inlayHints.parameterNames |
boolean | true |
Parameter name hints at call sites |
gasEstimates.enabled |
boolean | true |
Gas estimate code lens from .gas-snapshot |
test.verbosity |
number | 2 |
forge test verbosity (0–5) |
Settings take effect dynamically without a restart.
┌──────────────────────────────────────────────────────────────┐
│ VS Code / Cursor Extension │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Foundry │ │ Test │ │ Static Analysis │ │
│ │ Commands │ │ Explorer │ │ (Slither + Aderyn) │ │
│ └──────────────┘ └──────────────┘ └────────────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ Gas │ │ Storage │ │ Anvil / Cast / │ │
│ │ Profiler │ │ Layout │ │ Chisel / Deploy │ │
│ └──────────────┘ └──────────────┘ └────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ LSP Client (vscode-languageclient v9) │ │
│ └─────────────────────────────────────────────────────────┘ │
└────────────────────────────┬─────────────────────────────────┘
│ stdio
┌────────────────────────────┴─────────────────────────────────┐
│ Solidity Workbench LSP Server │
│ │
│ ┌───────────────────┐ ┌──────────────────────────────────┐ │
│ │ WorkspaceManager │ │ SolidityParser │ │
│ │ foundry.toml, │ │ @solidity-parser/parser + NatSpec│ │
│ │ remappings, │ │ extraction + error recovery │ │
│ │ multi-root, forge │ └──────────────────────────────────┘ │
│ └───────────────────┘ ┌──────────────────────────────────┐ │
│ │ SymbolIndex + ReferenceIndex │ │
│ │ (inverted index for O(1) refs) │ │
│ └──────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 21 LSP providers: completion, definition, hover, │ │
│ │ diagnostics, semantic-tokens, code-actions, formatting, │ │
│ │ document-symbols, inlay-hints, signature-help, rename, │ │
│ │ code-lens, references, implementation, call-hierarchy, │ │
│ │ type-hierarchy, document-highlight, folding, selection, │ │
│ │ document-links, auto-import │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌───────────────────┐ ┌──────────────────────────────────┐ │
│ │ SolcBridge │ │ LineIndex │ │
│ │ type-resolved │ │ UTF-8 byte offset <-> LSP pos │ │
│ │ AST via forge │ │ │ │
│ └───────────────────┘ └──────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ │ │
┌────┴─────┐ ┌────┴─────┐ ┌─────────┴──────────┐
│ solc │ │ forge │ │ slither / aderyn / │
│(compiler)│ │build/test│ │ wake / mythril │
│ │ │ │ │ (optional) │
└──────────┘ └──────────┘ └────────────────────┘
Dual-AST strategy: a fast parser AST (@solidity-parser/parser) powers keystroke-level
features while a type-resolved solc AST (from forge build --json on save) powers overload
disambiguation, member completion, and scope-aware rename.
See ARCHITECTURE.md for the full design document.
solidity-workbench/
├── packages/
│ ├── common/ Shared types, custom LSP protocol, LCOV parser
│ ├── server/ LSP server — 17 providers, parser, indexes, SolcBridge
│ └── extension/ VS Code client — commands, webviews, test explorer
├── test/fixtures/ Sample Foundry project for integration testing
├── scripts/ Build helpers (icon rasterization)
├── .github/workflows/ CI (Node 18/20/22 matrix) + tag-gated publish
├── ARCHITECTURE.md Design rationale and system diagrams
├── PRODUCTION_GAPS.md Gap tracker (all P1/P2 resolved; P3 backlog)
└── CHANGELOG.md Release notes (Keep a Changelog format)
pnpm install # install workspace dependencies
pnpm build # build common → server → extension (esbuild)
pnpm watch # rebuild on change
pnpm test # ~390 unit tests (node --test)
pnpm lint # ESLint
pnpm format:check # Prettier
pnpm package # produce .vsix
# E2E tests (boots a real VS Code instance)
pnpm --filter solidity-workbench test:e2e- Open this repo in VS Code.
- Press
F5— an Extension Development Host launches with the extension loaded. - Open
test/fixtures/sample-project/(or any Foundry repo). - Use
pnpm watchfor live rebuilds;Ctrl+Rin the host to reload.
~530 unit tests via Node's built-in test runner, plus ~30 E2E tests
via @vscode/test-electron (activation smoke + LSP round-trip
covering hover, workspace symbols including fuzzy subsequence,
references, rename, code actions, and formatting + feature-coverage
spanning LSP providers, webview command registration, the Test
Explorer controller, and a synthetic-trace DAP launch round-trip).
CI runs on Node 18, 20, and 22 with Foundry installed.
Coverage spans: parser (35 tests), symbol/reference indexing, all major providers (completion, definition, hover, rename, references, code actions, auto-import, diagnostics, semantic tokens, call hierarchy, signature help, inlay hints), the 21 linter rules, LCOV parsing, line-index byte-offset conversion, the trigram-indexed fuzzy workspace symbol path, the Aderyn JSON report parser, the subgraph scaffold generator, the chisel output adapter, ABI signature formatters, and text utilities.
All P1 and P2 items are resolved. Aderyn integration, the trigram-backed fuzzy workspace symbols, the subgraph scaffold, the chisel webview, the remote chain UI, the IR Viewer, the Wake bridge, the Mythril bridge, and the native DAP debug adapter all landed in the April–May 2026 sweeps. Remaining P3 enhancements — see PRODUCTION_GAPS.md:
| Item | Effort | Description |
|---|---|---|
| Solar integration | 2–3 weeks | Swap parser hot path to Solar WASM for ~40× speed once Solar publishes a stable LSP |
| More E2E coverage | Ongoing | Coverage decoration rendering, full-trace DAP source resolution against a real forge build, Slither / Aderyn diagnostic round-trip |
Contributions welcome. The codebase follows consistent patterns:
- Providers live in
packages/server/src/providers/— single class withprovide*methods - Extension features live in
packages/extension/src/{commands,views,analysis,test-explorer,debugger}/ - Shared types go in
packages/common/src/types.ts; custom LSP messages inpackages/common/src/protocol.ts - Linter rules plug into
packages/server/src/providers/linter.ts
Run pnpm lint && pnpm format:check && pnpm test before opening a PR. For larger design
changes, open an issue referencing the relevant section of ARCHITECTURE.md.
Zero telemetry. No usage data, crash reports, or identifiers leave your machine. The only
network activity is what you explicitly trigger (forge build, cast, anvil --fork-url, etc.).
The LSP server communicates over stdio and never opens a network socket.
MIT — Copyright (c) 2026 Chris Cashwell
See CHANGELOG.md for release notes.