Motivation
The wat-lsp project has matured with solid native/WASM dual-build support. To enable supporting additional tree-sitter languages in the future, we should split the project into a reusable LSP framework crate and a WAT language consumer crate. This maximizes code sharing so future languages get document management, debouncing, incremental parsing, protocol handling, and WASM support for free.
The current codebase already has good layering (~30% is framework-ready), but ~70% is WAT-specific and deeply entangled with WAT grammar node kinds, instruction semantics, and symbol structures.
Proposed Architecture: 3-Crate Workspace
wat-lsp/
├── Cargo.toml # [workspace] root
├── crates/
│ ├── ts-lsp/ # Reusable tree-sitter LSP framework
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs
│ │ ├── language.rs # Language trait (the key abstraction)
│ │ ├── server.rs # LspServer<L> (generic tower-lsp Backend)
│ │ ├── wasm_host.rs # WasmLspHost<L> (generic WASM wrapper)
│ │ ├── core/
│ │ │ ├── mod.rs
│ │ │ └── types.rs # Position, Range, Diagnostic, HoverResult, CompletionItem, etc.
│ │ ├── ts_facade.rs # Tree-sitter native/WASM abstraction
│ │ └── native/
│ │ ├── mod.rs
│ │ └── adapters.rs # Core type → tower-lsp type conversions
│ │
│ └── wat/ # WAT language implementation
│ ├── Cargo.toml
│ ├── build.rs # Doc gen + grammar compilation
│ └── src/
│ ├── lib.rs # WatLanguage struct + Language impl
│ ├── symbols.rs
│ ├── parser.rs
│ ├── utils.rs
│ ├── instruction_metadata.rs
│ ├── docs.rs
│ ├── symbol_lookup.rs
│ ├── tree_sitter_bindings.rs
│ ├── wast_parser.rs
│ ├── test_utils.rs
│ ├── diagnostics_core/ # All WAT semantic analysis
│ ├── diagnostics/ # Native diagnostic wrappers
│ └── features/ # All WAT feature implementations
│
├── src/
│ ├── main.rs # Binary: LspServer<WatLanguage>::run()
│ └── wasm/
│ └── api.rs # #[wasm_bindgen] WatLSP wrapping WasmLspHost<WatLanguage>
├── grammars/ # Stays at root (referenced by wat crate's build.rs)
├── packages/ # Stays at root
└── benches/ # Stays at root
The Language Trait
Defined in crates/ts-lsp/src/language.rs. This is the contract between framework and consumer.
pub trait Language: Send + Sync + 'static {
type SymbolTable: Default + Clone + Send + Sync;
// Identity
fn name(&self) -> &str;
fn diagnostic_source(&self) -> &str;
fn completion_trigger_characters(&self) -> Vec<String>;
fn signature_help_trigger_characters(&self) -> Vec<String>;
// Tree-sitter setup (native)
#[cfg(feature = "native")]
fn ts_language(&self) -> tree_sitter::Language;
#[cfg(feature = "native")]
fn create_parser(&self) -> tree_sitter::Parser;
// Symbol extraction
fn extract_symbols(&self, tree: &Tree, source: &str) -> Result<Self::SymbolTable, String>;
// Features (all use protocol-independent core types)
fn hover(&self, doc: &str, symbols: &Self::SymbolTable, tree: &Tree, pos: Position) -> Option<HoverResult>;
fn completion(&self, doc: &str, symbols: &Self::SymbolTable, pos: Position) -> Vec<CompletionItem>;
fn definition(&self, ...) -> Option<Range> { None } // optional
fn references(&self, ...) -> Vec<Range> { vec![] } // optional
fn document_symbols(&self, ...) -> Vec<DocumentSymbolInfo> { vec![] }
fn folding_ranges(&self, ...) -> Vec<FoldingRangeResult> { vec![] }
fn signature_help(&self, ...) -> Option<SignatureHelp> { None }
// Diagnostics
fn immediate_diagnostics(&self, tree: &Tree, source: &str, symbols: &Self::SymbolTable) -> Vec<Diagnostic>;
fn debounced_diagnostics(&self, ...) -> Vec<Diagnostic> { vec![] } // optional
// Rename (optional)
fn prepare_rename(&self, ...) -> Option<Range> { None }
fn validate_rename(&self, new_name: &str) -> Result<(), String> { Ok(()) }
}
WAT implements every method by delegating to its existing _core functions.
Generic Server (LspServer<L: Language>)
crates/ts-lsp/src/server.rs — The current main.rs Backend generalized. Handles:
DashMap<String, (String, L::SymbolTable, Tree)> document state
- Debounced validation with cancellation (
watch::channel)
- Incremental text editing + tree-sitter
InputEdit
- All
LanguageServer trait methods delegating to self.language.hover(), etc.
- Core type → tower-lsp type conversion via existing
From impls
- Rename flow:
prepare_rename → validate_rename → references → WorkspaceEdit
The main.rs binary becomes ~5 lines:
fn main() { ts_lsp::run_server(wat::WatLanguage::new()) }
What Moves Where
To ts-lsp (framework):
src/core/types.rs — as-is
src/ts_facade.rs — minus wat_language()/create_parser() (those go to wat)
src/native/adapters.rs — as-is
src/main.rs Backend logic → generalized into server.rs
src/wasm/api.rs generic parts → wasm_host.rs
src/utils.rs generic fns: position_to_byte, apply_text_edit, node_to_range, get_line_at_position
node_copy!/node_clone! macros (tree-sitter abstraction, not WAT-specific)
To wat (language):
- Everything else:
symbols.rs, parser.rs, utils.rs (WAT parts), instruction_metadata.rs, docs.rs, symbol_lookup.rs, tree_sitter_bindings.rs, wast_parser.rs, diagnostics_core/, diagnostics/, all features/
Stays in root crate:
src/main.rs (thin binary)
src/wasm/api.rs (thin #[wasm_bindgen] concrete wrapper)
src/bin/*.rs (CLI tools)
benches/, tests/
Migration Order (Incremental)
Each phase should compile and pass tests before proceeding.
Phase 1: Workspace skeleton
- Convert
Cargo.toml to [workspace] with members
- Create
crates/ts-lsp/ and crates/wat/ with minimal Cargo.toml
- Root crate depends on both; all code stays in root initially
- Verify
cargo build --features native and cargo test --features native pass
Phase 2: Extract core types to ts-lsp
- Move
src/core/types.rs → crates/ts-lsp/src/core/types.rs
- Move
src/native/adapters.rs → crates/ts-lsp/src/native/adapters.rs
- Root and
wat crate re-import from ts_lsp::core::types
- Add
FoldingRangeResult/FoldingRangeKind to core types (currently in features/folding)
Phase 3: Extract ts_facade to ts-lsp
- Move
src/ts_facade.rs → crates/ts-lsp/src/ts_facade.rs
- Move generic utils (
position_to_byte, apply_text_edit, node_to_range, node_copy!/node_clone!) to ts-lsp
- Remove WAT-specific functions (
wat_language, create_parser) from facade
Phase 4: Define Language trait in ts-lsp
- Add
crates/ts-lsp/src/language.rs
- Just the trait definition, no implementors yet
Phase 5: Move WAT code to wat crate
- Largest phase — move module by module, testing after each:
symbols.rs
instruction_metadata.rs + docs.rs + build.rs
parser.rs + symbol_lookup.rs
utils.rs (WAT-specific parts)
tree_sitter_bindings.rs + wast_parser.rs
diagnostics_core/
diagnostics/
features/ (all of them)
test_utils.rs
Phase 6: Implement Language for WatLanguage
- Create
WatLanguage struct in crates/wat/src/lib.rs
- Implement trait by delegating to existing functions
Phase 7: Extract generic server
- Move
Backend logic from src/main.rs → crates/ts-lsp/src/server.rs as LspServer<L>
- Replace all direct WAT calls with
self.language.*() dispatch
- Root
src/main.rs becomes LspServer::<WatLanguage>::run()
Phase 8: Extract generic WASM host
- Move generic WASM logic →
crates/ts-lsp/src/wasm_host.rs as WasmLspHost<L>
- Root
src/wasm/api.rs becomes thin #[wasm_bindgen] wrapper over WasmLspHost<WatLanguage>
Feature Flags
Propagate through workspace:
ts-lsp: native (tower-lsp, tokio, tree-sitter, dashmap), wasm (wasm-bindgen, web-tree-sitter-sg, js-sys)
wat: native → ts-lsp/native + wast + regex, wasm → ts-lsp/wasm
- Root:
native → wat/native + ts-lsp/native + clap, wasm → wat/wasm + ts-lsp/wasm
Verification
cargo build --features native — native build compiles
cargo test --features native — all existing tests pass
cargo build --features wasm --target wasm32-unknown-unknown — WASM build compiles
- Start native LSP and exercise hover, completion, definition, references, rename, diagnostics
- Load WASM module in browser and verify hover/completion/diagnostics work
Motivation
The wat-lsp project has matured with solid native/WASM dual-build support. To enable supporting additional tree-sitter languages in the future, we should split the project into a reusable LSP framework crate and a WAT language consumer crate. This maximizes code sharing so future languages get document management, debouncing, incremental parsing, protocol handling, and WASM support for free.
The current codebase already has good layering (~30% is framework-ready), but ~70% is WAT-specific and deeply entangled with WAT grammar node kinds, instruction semantics, and symbol structures.
Proposed Architecture: 3-Crate Workspace
The Language Trait
Defined in
crates/ts-lsp/src/language.rs. This is the contract between framework and consumer.WAT implements every method by delegating to its existing
_corefunctions.Generic Server (
LspServer<L: Language>)crates/ts-lsp/src/server.rs— The currentmain.rsBackend generalized. Handles:DashMap<String, (String, L::SymbolTable, Tree)>document statewatch::channel)InputEditLanguageServertrait methods delegating toself.language.hover(), etc.Fromimplsprepare_rename→validate_rename→references→WorkspaceEditThe
main.rsbinary becomes ~5 lines:What Moves Where
To
ts-lsp(framework):src/core/types.rs— as-issrc/ts_facade.rs— minuswat_language()/create_parser()(those go towat)src/native/adapters.rs— as-issrc/main.rsBackend logic → generalized intoserver.rssrc/wasm/api.rsgeneric parts →wasm_host.rssrc/utils.rsgeneric fns:position_to_byte,apply_text_edit,node_to_range,get_line_at_positionnode_copy!/node_clone!macros (tree-sitter abstraction, not WAT-specific)To
wat(language):symbols.rs,parser.rs,utils.rs(WAT parts),instruction_metadata.rs,docs.rs,symbol_lookup.rs,tree_sitter_bindings.rs,wast_parser.rs,diagnostics_core/,diagnostics/, allfeatures/Stays in root crate:
src/main.rs(thin binary)src/wasm/api.rs(thin#[wasm_bindgen]concrete wrapper)src/bin/*.rs(CLI tools)benches/,tests/Migration Order (Incremental)
Each phase should compile and pass tests before proceeding.
Phase 1: Workspace skeleton
Cargo.tomlto[workspace]withmemberscrates/ts-lsp/andcrates/wat/with minimal Cargo.tomlcargo build --features nativeandcargo test --features nativepassPhase 2: Extract core types to
ts-lspsrc/core/types.rs→crates/ts-lsp/src/core/types.rssrc/native/adapters.rs→crates/ts-lsp/src/native/adapters.rswatcrate re-import fromts_lsp::core::typesFoldingRangeResult/FoldingRangeKindto core types (currently infeatures/folding)Phase 3: Extract
ts_facadetots-lspsrc/ts_facade.rs→crates/ts-lsp/src/ts_facade.rsposition_to_byte,apply_text_edit,node_to_range,node_copy!/node_clone!) tots-lspwat_language,create_parser) from facadePhase 4: Define
Languagetrait ints-lspcrates/ts-lsp/src/language.rsPhase 5: Move WAT code to
watcratesymbols.rsinstruction_metadata.rs+docs.rs+build.rsparser.rs+symbol_lookup.rsutils.rs(WAT-specific parts)tree_sitter_bindings.rs+wast_parser.rsdiagnostics_core/diagnostics/features/(all of them)test_utils.rsPhase 6: Implement
LanguageforWatLanguageWatLanguagestruct incrates/wat/src/lib.rsPhase 7: Extract generic server
Backendlogic fromsrc/main.rs→crates/ts-lsp/src/server.rsasLspServer<L>self.language.*()dispatchsrc/main.rsbecomesLspServer::<WatLanguage>::run()Phase 8: Extract generic WASM host
crates/ts-lsp/src/wasm_host.rsasWasmLspHost<L>src/wasm/api.rsbecomes thin#[wasm_bindgen]wrapper overWasmLspHost<WatLanguage>Feature Flags
Propagate through workspace:
ts-lsp:native(tower-lsp, tokio, tree-sitter, dashmap),wasm(wasm-bindgen, web-tree-sitter-sg, js-sys)wat:native→ts-lsp/native+ wast + regex,wasm→ts-lsp/wasmnative→wat/native+ts-lsp/native+ clap,wasm→wat/wasm+ts-lsp/wasmVerification
cargo build --features native— native build compilescargo test --features native— all existing tests passcargo build --features wasm --target wasm32-unknown-unknown— WASM build compiles