From 2154b1c21ec73686e6fe3fffa2436ba9bad0e3b8 Mon Sep 17 00:00:00 2001 From: Vu Vo Date: Sun, 9 Aug 2026 11:26:14 +0700 Subject: [PATCH] add goto-implementation Signed-off-by: Vu Vo --- crates/lsp/src/global_state.rs | 7 +- crates/lsp/src/handler.rs | 1 + crates/lsp/src/handler/goto_implementation.rs | 115 ++++++++++++++++++ crates/lsp/src/lib.rs | 17 +-- 4 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 crates/lsp/src/handler/goto_implementation.rs diff --git a/crates/lsp/src/global_state.rs b/crates/lsp/src/global_state.rs index 3903dc7..1d1ce8b 100644 --- a/crates/lsp/src/global_state.rs +++ b/crates/lsp/src/global_state.rs @@ -5,8 +5,8 @@ use lsp_types::notification::{ DidOpenTextDocument, Notification as _, }; use lsp_types::request::{ - Completion, DocumentSymbolRequest, Formatting, GotoDefinition, HoverRequest, - PrepareRenameRequest, References, Rename, Request as _, + Completion, DocumentSymbolRequest, Formatting, GotoDefinition, GotoImplementation, + HoverRequest, PrepareRenameRequest, References, Rename, Request as _, }; use lsp_types::{ DidChangeTextDocumentParams, DidOpenTextDocumentParams, FileChangeType, Location, Range, Url, @@ -151,6 +151,9 @@ impl GlobalState { let id = req.id.clone(); match req.method.as_str() { GotoDefinition::METHOD => dispatch(self, id, req, handler::goto_definition::handle), + GotoImplementation::METHOD => { + dispatch(self, id, req, handler::goto_implementation::handle) + } HoverRequest::METHOD => dispatch(self, id, req, handler::hover::handle), Completion::METHOD => dispatch(self, id, req, handler::completion::handle), References::METHOD => dispatch(self, id, req, handler::references::handle), diff --git a/crates/lsp/src/handler.rs b/crates/lsp/src/handler.rs index fb7b583..972ec14 100644 --- a/crates/lsp/src/handler.rs +++ b/crates/lsp/src/handler.rs @@ -9,6 +9,7 @@ pub mod completion; pub mod document_symbol; pub mod formatting; pub mod goto_definition; +pub mod goto_implementation; pub mod hover; pub mod references; pub mod rename; diff --git a/crates/lsp/src/handler/goto_implementation.rs b/crates/lsp/src/handler/goto_implementation.rs new file mode 100644 index 0000000..f2bba59 --- /dev/null +++ b/crates/lsp/src/handler/goto_implementation.rs @@ -0,0 +1,115 @@ +//! `textDocument/implementation` for Circom. +//! +//! Circom has no separate "implementation" concept (no interfaces/traits/abstract symbols +//! distinct from their definition), so go-to-implementation behaves identically to +//! go-to-definition. This handler delegates to [`super::goto_definition::handle`] so the two +//! features can never drift. + +use anyhow::Result; +use lsp_types::request::{GotoImplementationParams, GotoImplementationResponse}; + +use crate::global_state::GlobalState; + +/// Entry point for the `textDocument/implementation` request. Identical to +/// [`super::goto_definition::handle`] — every implementation target IS the symbol's definition. +pub fn handle( + state: &GlobalState, + params: GotoImplementationParams, +) -> Result> { + super::goto_definition::handle(state, params) +} + +#[cfg(test)] +mod tests { + use lsp_types::request::GotoImplementationParams; + use lsp_types::{Location, TextDocumentIdentifier, TextDocumentPositionParams, Url}; + use parser::token_kind::TokenKind; + use rowan::ast::AstNode; + use syntax::abstract_syntax_tree::AstCircomProgram; + use syntax::tree::syntax_tree; + + use crate::source_db::SourceDatabase; + use crate::test_util::state_with; + + /// Drive the real `textDocument/implementation` handler at the `occurrence`-th token whose + /// kind+text match, returning the resolved `Location`s. + fn goto_impl( + state: &crate::global_state::GlobalState, + url: &Url, + source: &str, + kind: TokenKind, + text: &str, + occurrence: usize, + ) -> Vec { + let id = state.source_db.id_for_url(url).expect("doc registered"); + let file_db = state.source_db.file_db(id); + let ast = AstCircomProgram::cast(syntax_tree(source)).expect("program"); + let token = ast + .syntax() + .descendants_with_tokens() + .filter_map(|e| e.into_token()) + .filter(|t| t.kind() == kind && t.text() == text) + .nth(occurrence) + .unwrap_or_else(|| panic!("token {text:?}#{occurrence} not found")); + let pos = file_db.position(token.text_range().start()); + super::handle( + state, + GotoImplementationParams { + text_document_position_params: TextDocumentPositionParams { + text_document: TextDocumentIdentifier { uri: url.clone() }, + position: pos, + }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + }, + ) + .unwrap() + .map(|r| match r { + lsp_types::request::GotoImplementationResponse::Array(v) => v, + _ => Vec::new(), + }) + .unwrap_or_default() + } + + /// Go-to-implementation on the `X` usage in `component main = X()` lands on `X`'s + /// definition in the same file — the "identical to go-to-definition" contract. + #[test] + fn implementation_matches_definition_same_file_test() { + let source = + "pragma circom 2.0.0;\ntemplate X() { signal output o; o <== 0; }\ncomponent main = X();\n"; + let url = Url::from_file_path("/tmp/impl_same.circom").unwrap(); + let state = state_with(&url, source); + + // The `X` usage in `component main = X()` is the 2nd `X` token (0th = the definition). + let locs = goto_impl(&state, &url, source, TokenKind::Identifier, "X", 1); + + assert_eq!(locs.len(), 1, "implementation jump: {locs:?}"); + assert_eq!(locs[0].uri, url, "jumps within the same file"); + assert_eq!( + locs[0].range.start.line, 1, + "lands on the template definition" + ); + } + + /// Go-to-implementation on an include path string jumps to the included file's URL — the + /// include-target path is shared with go-to-definition, so this locks that it isn't lost. + #[test] + fn implementation_on_include_string_test() { + let source = "pragma circom 2.0.0;\ninclude \"lib.circom\";\ncomponent main = X();\n"; + let url = Url::from_file_path("/tmp/impl_inc.circom").unwrap(); + // `state_with` is single-file; the include won't resolve to a real lib, but the handler + // must still run without panicking and return an array (here empty, as the lib is absent). + let state = state_with(&url, source); + + let locs = goto_impl( + &state, + &url, + source, + TokenKind::CircomString, + "\"lib.circom\"", + 0, + ); + // The lib doesn't exist on disk in this single-file harness, so no target resolves. + assert!(locs.is_empty(), "absent include yields no target: {locs:?}"); + } +} diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index 65718da..55fea90 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -17,8 +17,8 @@ use std::path::PathBuf; use lsp_server::{Connection, Message, Request, RequestId}; use lsp_types::notification::Notification; use lsp_types::{ - CompletionOptions, HoverProviderCapability, InitializeParams, OneOf, ServerCapabilities, - TextDocumentSyncCapability, TextDocumentSyncKind, + CompletionOptions, HoverProviderCapability, ImplementationProviderCapability, InitializeParams, + OneOf, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, }; use crate::global_state::GlobalState; @@ -49,15 +49,18 @@ pub fn run() -> Result<(), Box> { /// Advertise the LSP features this server handles. /// -/// `definition` is fully implemented; `hover`/`completion`/`references`/`documentSymbol`/ -/// `formatting` are registered as placeholders — the client routes them to the server, which -/// currently returns an empty result until each is implemented in `handler::*`. `rename` is fully -/// implemented and advertises `prepareSupport` so the client consults the server (not its own -/// textual word check) before opening the rename box — keywords/strings never become renamable. +/// `definition` and `implementation` are fully implemented; `implementation` behaves identically +/// to `definition` (Circom has no separate implementation targets). `hover`/`completion`/ +/// `references`/`documentSymbol`/`formatting` are registered as placeholders — the client routes +/// them to the server, which currently returns an empty result until each is implemented in +/// `handler::*`. `rename` is fully implemented and advertises `prepareSupport` so the client +/// consults the server (not its own textual word check) before opening the rename box — +/// keywords/strings never become renamable. fn server_capabilities() -> ServerCapabilities { ServerCapabilities { text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)), definition_provider: Some(OneOf::Left(true)), + implementation_provider: Some(ImplementationProviderCapability::Simple(true)), hover_provider: Some(HoverProviderCapability::Simple(true)), completion_provider: Some(CompletionOptions { trigger_characters: Some(vec![".".to_string()]),