Skip to main content

solidity_language_server/
lsp.rs

1use crate::completion;
2use crate::goto;
3use crate::hover;
4use crate::inlay_hints;
5use crate::links;
6use crate::references;
7use crate::rename;
8use crate::runner::{ForgeRunner, Runner};
9use crate::semantic_tokens;
10use crate::symbols;
11use crate::utils;
12use std::collections::HashMap;
13use std::sync::Arc;
14use tokio::sync::RwLock;
15use tower_lsp::{Client, LanguageServer, lsp_types::*};
16
17pub struct ForgeLsp {
18    client: Client,
19    compiler: Arc<dyn Runner>,
20    ast_cache: Arc<RwLock<HashMap<String, Arc<goto::CachedBuild>>>>,
21    /// Text cache for opened documents
22    ///
23    /// The key is the file's URI converted to string, and the value is a tuple of (version, content).
24    text_cache: Arc<RwLock<HashMap<String, (i32, String)>>>,
25    completion_cache: Arc<RwLock<HashMap<String, Arc<completion::CompletionCache>>>>,
26}
27
28impl ForgeLsp {
29    pub fn new(client: Client, use_solar: bool) -> Self {
30        let compiler: Arc<dyn Runner> = if use_solar {
31            Arc::new(crate::solar_runner::SolarRunner)
32        } else {
33            Arc::new(ForgeRunner)
34        };
35        let ast_cache = Arc::new(RwLock::new(HashMap::new()));
36        let text_cache = Arc::new(RwLock::new(HashMap::new()));
37        let completion_cache = Arc::new(RwLock::new(HashMap::new()));
38        Self {
39            client,
40            compiler,
41            ast_cache,
42            text_cache,
43            completion_cache,
44        }
45    }
46
47    async fn on_change(&self, params: TextDocumentItem) {
48        let uri = params.uri.clone();
49        let version = params.version;
50
51        let file_path = match uri.to_file_path() {
52            Ok(path) => path,
53            Err(_) => {
54                self.client
55                    .log_message(MessageType::ERROR, "Invalid file URI")
56                    .await;
57                return;
58            }
59        };
60
61        let path_str = match file_path.to_str() {
62            Some(s) => s,
63            None => {
64                self.client
65                    .log_message(MessageType::ERROR, "Invalid file path")
66                    .await;
67                return;
68            }
69        };
70
71        let (lint_result, build_result, ast_result) = tokio::join!(
72            self.compiler.get_lint_diagnostics(&uri),
73            self.compiler.get_build_diagnostics(&uri),
74            self.compiler.ast(path_str)
75        );
76
77        // Only replace cache with new AST if build succeeded (no errors; warnings are OK)
78        let build_succeeded = matches!(&build_result, Ok(diagnostics) if diagnostics.iter().all(|d| d.severity != Some(DiagnosticSeverity::ERROR)));
79
80        if build_succeeded {
81            if let Ok(ast_data) = ast_result {
82                let cached_build = Arc::new(goto::CachedBuild::new(ast_data, version));
83                let mut cache = self.ast_cache.write().await;
84                cache.insert(uri.to_string(), cached_build.clone());
85                drop(cache);
86
87                // Rebuild completion cache in the background; old cache stays usable until replaced
88                let completion_cache = self.completion_cache.clone();
89                let uri_string = uri.to_string();
90                tokio::spawn(async move {
91                    if let Some(sources) = cached_build.ast.get("sources") {
92                        let contracts = cached_build.ast.get("contracts");
93                        let cc = completion::build_completion_cache(sources, contracts);
94                        completion_cache
95                            .write()
96                            .await
97                            .insert(uri_string, Arc::new(cc));
98                    }
99                });
100                self.client
101                    .log_message(MessageType::INFO, "Build successful, AST cache updated")
102                    .await;
103            } else if let Err(e) = ast_result {
104                self.client
105                    .log_message(
106                        MessageType::INFO,
107                        format!("Build succeeded but failed to get AST: {e}"),
108                    )
109                    .await;
110            }
111        } else {
112            // Build has errors - keep the existing cache (don't invalidate)
113            self.client
114                .log_message(
115                    MessageType::INFO,
116                    "Build errors detected, keeping existing AST cache",
117                )
118                .await;
119        }
120
121        // cache text — only if no newer version exists (e.g. from formatting/did_change)
122        {
123            let mut text_cache = self.text_cache.write().await;
124            let uri_str = uri.to_string();
125            let existing_version = text_cache.get(&uri_str).map(|(v, _)| *v).unwrap_or(-1);
126            if version >= existing_version {
127                text_cache.insert(uri_str, (version, params.text));
128            }
129        }
130
131        let mut all_diagnostics = vec![];
132
133        match lint_result {
134            Ok(mut lints) => {
135                self.client
136                    .log_message(
137                        MessageType::INFO,
138                        format!("found {} lint diagnostics", lints.len()),
139                    )
140                    .await;
141                all_diagnostics.append(&mut lints);
142            }
143            Err(e) => {
144                self.client
145                    .log_message(
146                        MessageType::ERROR,
147                        format!("Forge lint diagnostics failed: {e}"),
148                    )
149                    .await;
150            }
151        }
152
153        match build_result {
154            Ok(mut builds) => {
155                self.client
156                    .log_message(
157                        MessageType::INFO,
158                        format!("found {} build diagnostics", builds.len()),
159                    )
160                    .await;
161                all_diagnostics.append(&mut builds);
162            }
163            Err(e) => {
164                self.client
165                    .log_message(
166                        MessageType::WARNING,
167                        format!("Forge build diagnostics failed: {e}"),
168                    )
169                    .await;
170            }
171        }
172
173        // publish diags with no version, so we are sure they get displayed
174        self.client
175            .publish_diagnostics(uri, all_diagnostics, None)
176            .await;
177
178        // Refresh inlay hints after everything is updated
179        if build_succeeded {
180            let client = self.client.clone();
181            tokio::spawn(async move {
182                let _ = client.inlay_hint_refresh().await;
183            });
184        }
185    }
186
187    /// Get a CachedBuild from the cache, or fetch and build one on demand.
188    /// If `insert_on_miss` is true, the freshly-built entry is inserted into the cache
189    /// (used by references handler so cross-file lookups can find it later).
190    ///
191    /// When the entry is in the cache but marked stale (text_cache changed
192    /// since the last build), the text_cache content is flushed to disk and
193    /// the AST is rebuilt so that rename / references work correctly on
194    /// unsaved buffers.
195    async fn get_or_fetch_build(
196        &self,
197        uri: &Url,
198        file_path: &std::path::Path,
199        insert_on_miss: bool,
200    ) -> Option<Arc<goto::CachedBuild>> {
201        let uri_str = uri.to_string();
202
203        // Return cached entry if it exists (stale or not — stale entries are
204        // still usable, positions may be slightly off like goto-definition).
205        {
206            let cache = self.ast_cache.read().await;
207            if let Some(cached) = cache.get(&uri_str) {
208                return Some(cached.clone());
209            }
210        }
211
212        // Cache miss — build the AST from disk.
213        let path_str = file_path.to_str()?;
214        match self.compiler.ast(path_str).await {
215            Ok(data) => {
216                // Built from disk (cache miss) — use version 0; the next
217                // didSave/on_change will stamp the correct version.
218                let build = Arc::new(goto::CachedBuild::new(data, 0));
219                if insert_on_miss {
220                    let mut cache = self.ast_cache.write().await;
221                    cache.insert(uri_str.clone(), build.clone());
222                }
223                Some(build)
224            }
225            Err(e) => {
226                self.client
227                    .log_message(MessageType::ERROR, format!("failed to get AST: {e}"))
228                    .await;
229                None
230            }
231        }
232    }
233
234    /// Get the source bytes for a file, preferring the in-memory text cache
235    /// (which reflects unsaved editor changes) over reading from disk.
236    async fn get_source_bytes(&self, uri: &Url, file_path: &std::path::Path) -> Option<Vec<u8>> {
237        {
238            let text_cache = self.text_cache.read().await;
239            if let Some((_, content)) = text_cache.get(&uri.to_string()) {
240                return Some(content.as_bytes().to_vec());
241            }
242        }
243        match std::fs::read(file_path) {
244            Ok(bytes) => Some(bytes),
245            Err(e) => {
246                self.client
247                    .log_message(MessageType::ERROR, format!("failed to read file: {e}"))
248                    .await;
249                None
250            }
251        }
252    }
253}
254
255#[tower_lsp::async_trait]
256impl LanguageServer for ForgeLsp {
257    async fn initialize(
258        &self,
259        params: InitializeParams,
260    ) -> tower_lsp::jsonrpc::Result<InitializeResult> {
261        // Negotiate position encoding with the client (once, for the session).
262        let client_encodings = params
263            .capabilities
264            .general
265            .as_ref()
266            .and_then(|g| g.position_encodings.as_deref());
267        let encoding = utils::PositionEncoding::negotiate(client_encodings);
268        utils::set_encoding(encoding);
269
270        Ok(InitializeResult {
271            server_info: Some(ServerInfo {
272                name: "Solidity Language Server".to_string(),
273                version: Some(env!("LONG_VERSION").to_string()),
274            }),
275            capabilities: ServerCapabilities {
276                position_encoding: Some(encoding.into()),
277                completion_provider: Some(CompletionOptions {
278                    trigger_characters: Some(vec![".".to_string()]),
279                    resolve_provider: Some(false),
280                    ..Default::default()
281                }),
282                definition_provider: Some(OneOf::Left(true)),
283                declaration_provider: Some(DeclarationCapability::Simple(true)),
284                references_provider: Some(OneOf::Left(true)),
285                rename_provider: Some(OneOf::Right(RenameOptions {
286                    prepare_provider: Some(true),
287                    work_done_progress_options: WorkDoneProgressOptions {
288                        work_done_progress: Some(true),
289                    },
290                })),
291                workspace_symbol_provider: Some(OneOf::Left(true)),
292                document_symbol_provider: Some(OneOf::Left(true)),
293                hover_provider: Some(HoverProviderCapability::Simple(true)),
294                document_link_provider: Some(DocumentLinkOptions {
295                    resolve_provider: Some(false),
296                    work_done_progress_options: WorkDoneProgressOptions {
297                        work_done_progress: None,
298                    },
299                }),
300                document_formatting_provider: Some(OneOf::Left(true)),
301                inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options(
302                    InlayHintOptions {
303                        resolve_provider: Some(false),
304                        work_done_progress_options: WorkDoneProgressOptions {
305                            work_done_progress: None,
306                        },
307                    },
308                ))),
309                semantic_tokens_provider: Some(
310                    SemanticTokensServerCapabilities::SemanticTokensOptions(
311                        SemanticTokensOptions {
312                            legend: semantic_tokens::legend(),
313                            full: Some(SemanticTokensFullOptions::Bool(true)),
314                            range: None,
315                            work_done_progress_options: WorkDoneProgressOptions {
316                                work_done_progress: None,
317                            },
318                        },
319                    ),
320                ),
321                text_document_sync: Some(TextDocumentSyncCapability::Options(
322                    TextDocumentSyncOptions {
323                        will_save: Some(true),
324                        will_save_wait_until: None,
325                        open_close: Some(true),
326                        save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
327                            include_text: Some(true),
328                        })),
329                        change: Some(TextDocumentSyncKind::FULL),
330                    },
331                )),
332                ..ServerCapabilities::default()
333            },
334        })
335    }
336
337    async fn initialized(&self, _: InitializedParams) {
338        self.client
339            .log_message(MessageType::INFO, "lsp server initialized.")
340            .await;
341    }
342
343    async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
344        self.client
345            .log_message(MessageType::INFO, "lsp server shutting down.")
346            .await;
347        Ok(())
348    }
349
350    async fn did_open(&self, params: DidOpenTextDocumentParams) {
351        self.client
352            .log_message(MessageType::INFO, "file opened")
353            .await;
354
355        self.on_change(params.text_document).await
356    }
357
358    async fn did_change(&self, params: DidChangeTextDocumentParams) {
359        self.client
360            .log_message(MessageType::INFO, "file changed")
361            .await;
362
363        // update text cache
364        if let Some(change) = params.content_changes.into_iter().next() {
365            let mut text_cache = self.text_cache.write().await;
366            text_cache.insert(
367                params.text_document.uri.to_string(),
368                (params.text_document.version, change.text),
369            );
370        }
371    }
372
373    async fn did_save(&self, params: DidSaveTextDocumentParams) {
374        self.client
375            .log_message(MessageType::INFO, "file saved")
376            .await;
377
378        let text_content = if let Some(text) = params.text {
379            text
380        } else {
381            // Prefer text_cache (reflects unsaved changes), fall back to disk
382            let cached = {
383                let text_cache = self.text_cache.read().await;
384                text_cache
385                    .get(params.text_document.uri.as_str())
386                    .map(|(_, content)| content.clone())
387            };
388            if let Some(content) = cached {
389                content
390            } else {
391                match std::fs::read_to_string(params.text_document.uri.path()) {
392                    Ok(content) => content,
393                    Err(e) => {
394                        self.client
395                            .log_message(
396                                MessageType::ERROR,
397                                format!("Failed to read file on save: {e}"),
398                            )
399                            .await;
400                        return;
401                    }
402                }
403            }
404        };
405
406        let version = self
407            .text_cache
408            .read()
409            .await
410            .get(params.text_document.uri.as_str())
411            .map(|(version, _)| *version)
412            .unwrap_or_default();
413
414        self.on_change(TextDocumentItem {
415            uri: params.text_document.uri,
416            text: text_content,
417            version,
418            language_id: "".to_string(),
419        })
420        .await;
421    }
422
423    async fn will_save(&self, params: WillSaveTextDocumentParams) {
424        self.client
425            .log_message(
426                MessageType::INFO,
427                format!(
428                    "file will save reason:{:?} {}",
429                    params.reason, params.text_document.uri
430                ),
431            )
432            .await;
433    }
434
435    async fn formatting(
436        &self,
437        params: DocumentFormattingParams,
438    ) -> tower_lsp::jsonrpc::Result<Option<Vec<TextEdit>>> {
439        self.client
440            .log_message(MessageType::INFO, "formatting request")
441            .await;
442
443        let uri = params.text_document.uri;
444        let file_path = match uri.to_file_path() {
445            Ok(path) => path,
446            Err(_) => {
447                self.client
448                    .log_message(MessageType::ERROR, "Invalid file URI for formatting")
449                    .await;
450                return Ok(None);
451            }
452        };
453        let path_str = match file_path.to_str() {
454            Some(s) => s,
455            None => {
456                self.client
457                    .log_message(MessageType::ERROR, "Invalid file path for formatting")
458                    .await;
459                return Ok(None);
460            }
461        };
462
463        // Get original content
464        let original_content = {
465            let text_cache = self.text_cache.read().await;
466            if let Some((_, content)) = text_cache.get(&uri.to_string()) {
467                content.clone()
468            } else {
469                // Fallback to reading file
470                match std::fs::read_to_string(&file_path) {
471                    Ok(content) => content,
472                    Err(_) => {
473                        self.client
474                            .log_message(MessageType::ERROR, "Failed to read file for formatting")
475                            .await;
476                        return Ok(None);
477                    }
478                }
479            }
480        };
481
482        // Get formatted content
483        let formatted_content = match self.compiler.format(path_str).await {
484            Ok(content) => content,
485            Err(e) => {
486                self.client
487                    .log_message(MessageType::WARNING, format!("Formatting failed: {e}"))
488                    .await;
489                return Ok(None);
490            }
491        };
492
493        // If changed, update text_cache with formatted content and return edit
494        if original_content != formatted_content {
495            let end = utils::byte_offset_to_position(&original_content, original_content.len());
496
497            // Update text_cache immediately so goto/hover use the formatted text
498            {
499                let mut text_cache = self.text_cache.write().await;
500                let version = text_cache
501                    .get(&uri.to_string())
502                    .map(|(v, _)| *v)
503                    .unwrap_or(0);
504                text_cache.insert(uri.to_string(), (version, formatted_content.clone()));
505            }
506
507            let edit = TextEdit {
508                range: Range {
509                    start: Position::default(),
510                    end,
511                },
512                new_text: formatted_content,
513            };
514            Ok(Some(vec![edit]))
515        } else {
516            Ok(None)
517        }
518    }
519
520    async fn did_close(&self, params: DidCloseTextDocumentParams) {
521        let uri = params.text_document.uri.to_string();
522        self.ast_cache.write().await.remove(&uri);
523        self.text_cache.write().await.remove(&uri);
524        self.completion_cache.write().await.remove(&uri);
525        self.client
526            .log_message(MessageType::INFO, "file closed, caches cleared.")
527            .await;
528    }
529
530    async fn did_change_configuration(&self, _: DidChangeConfigurationParams) {
531        self.client
532            .log_message(MessageType::INFO, "configuration changed.")
533            .await;
534    }
535    async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {
536        self.client
537            .log_message(MessageType::INFO, "workdspace folders changed.")
538            .await;
539    }
540
541    async fn did_change_watched_files(&self, _: DidChangeWatchedFilesParams) {
542        self.client
543            .log_message(MessageType::INFO, "watched files have changed.")
544            .await;
545    }
546
547    async fn completion(
548        &self,
549        params: CompletionParams,
550    ) -> tower_lsp::jsonrpc::Result<Option<CompletionResponse>> {
551        let uri = params.text_document_position.text_document.uri;
552        let position = params.text_document_position.position;
553
554        let trigger_char = params
555            .context
556            .as_ref()
557            .and_then(|ctx| ctx.trigger_character.as_deref());
558
559        // Get source text — only needed for dot completions (to parse the line)
560        let source_text = {
561            let text_cache = self.text_cache.read().await;
562            if let Some((_, text)) = text_cache.get(&uri.to_string()) {
563                text.clone()
564            } else {
565                match uri.to_file_path() {
566                    Ok(path) => std::fs::read_to_string(&path).unwrap_or_default(),
567                    Err(_) => return Ok(None),
568                }
569            }
570        };
571
572        // Clone the Arc (pointer copy, instant) and drop the lock immediately.
573        let cached: Option<Arc<completion::CompletionCache>> = {
574            let comp_cache = self.completion_cache.read().await;
575            comp_cache.get(&uri.to_string()).cloned()
576        };
577
578        if cached.is_none() {
579            // Spawn background cache build so the next request will have full completions
580            let ast_cache = self.ast_cache.clone();
581            let completion_cache = self.completion_cache.clone();
582            let uri_string = uri.to_string();
583            tokio::spawn(async move {
584                let cached_build = {
585                    let cache = ast_cache.read().await;
586                    match cache.get(&uri_string) {
587                        Some(v) => v.clone(),
588                        None => return,
589                    }
590                };
591                if let Some(sources) = cached_build.ast.get("sources") {
592                    let contracts = cached_build.ast.get("contracts");
593                    let cc = completion::build_completion_cache(sources, contracts);
594                    completion_cache
595                        .write()
596                        .await
597                        .insert(uri_string, Arc::new(cc));
598                }
599            });
600        }
601
602        let cache_ref = cached.as_deref();
603
604        // Look up the AST file_id for scope-aware resolution
605        let file_id = {
606            let uri_path = uri.to_file_path().ok();
607            cache_ref.and_then(|c| {
608                uri_path.as_ref().and_then(|p| {
609                    let path_str = p.to_str()?;
610                    c.path_to_file_id.get(path_str).copied()
611                })
612            })
613        };
614
615        let result =
616            completion::handle_completion(cache_ref, &source_text, position, trigger_char, file_id);
617        Ok(result)
618    }
619
620    async fn goto_definition(
621        &self,
622        params: GotoDefinitionParams,
623    ) -> tower_lsp::jsonrpc::Result<Option<GotoDefinitionResponse>> {
624        self.client
625            .log_message(MessageType::INFO, "got textDocument/definition request")
626            .await;
627
628        let uri = params.text_document_position_params.text_document.uri;
629        let position = params.text_document_position_params.position;
630
631        let file_path = match uri.to_file_path() {
632            Ok(path) => path,
633            Err(_) => {
634                self.client
635                    .log_message(MessageType::ERROR, "Invalid file uri")
636                    .await;
637                return Ok(None);
638            }
639        };
640
641        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
642            Some(bytes) => bytes,
643            None => return Ok(None),
644        };
645
646        let source_text = String::from_utf8_lossy(&source_bytes).to_string();
647
648        // Extract the identifier name under the cursor for tree-sitter validation.
649        let cursor_name = goto::cursor_context(&source_text, position).map(|ctx| ctx.name);
650
651        // Determine if the file is dirty (unsaved edits since last build).
652        // When dirty, AST byte offsets are stale so we prefer tree-sitter.
653        // When clean, AST has proper semantic resolution (scoping, types).
654        let (is_dirty, cached_build) = {
655            let text_version = self
656                .text_cache
657                .read()
658                .await
659                .get(&uri.to_string())
660                .map(|(v, _)| *v)
661                .unwrap_or(0);
662            let cb = self.get_or_fetch_build(&uri, &file_path, false).await;
663            let build_version = cb.as_ref().map(|b| b.build_version).unwrap_or(0);
664            (text_version > build_version, cb)
665        };
666
667        // Validate a tree-sitter result: read the target source and check that
668        // the text at the location matches the cursor identifier. Tree-sitter
669        // resolves by name so a mismatch means it landed on the wrong node.
670        // AST results are NOT validated — the AST can legitimately resolve to a
671        // different name (e.g. `.selector` → error declaration).
672        let validate_ts = |loc: &Location| -> bool {
673            let Some(ref name) = cursor_name else {
674                return true; // can't validate, trust it
675            };
676            let target_src = if loc.uri == uri {
677                Some(source_text.clone())
678            } else {
679                loc.uri
680                    .to_file_path()
681                    .ok()
682                    .and_then(|p| std::fs::read_to_string(&p).ok())
683            };
684            match target_src {
685                Some(src) => goto::validate_goto_target(&src, loc, name),
686                None => true, // can't read target, trust it
687            }
688        };
689
690        if is_dirty {
691            self.client
692                .log_message(MessageType::INFO, "file is dirty, trying tree-sitter first")
693                .await;
694
695            // DIRTY: tree-sitter first (validated) → AST fallback
696            let ts_result = {
697                let comp_cache = self.completion_cache.read().await;
698                let text_cache = self.text_cache.read().await;
699                if let Some(cc) = comp_cache.get(&uri.to_string()) {
700                    goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
701                } else {
702                    None
703                }
704            };
705
706            if let Some(location) = ts_result {
707                if validate_ts(&location) {
708                    self.client
709                        .log_message(
710                            MessageType::INFO,
711                            format!(
712                                "found definition (tree-sitter) at {}:{}",
713                                location.uri, location.range.start.line
714                            ),
715                        )
716                        .await;
717                    return Ok(Some(GotoDefinitionResponse::from(location)));
718                }
719                self.client
720                    .log_message(
721                        MessageType::INFO,
722                        "tree-sitter result failed validation, trying AST fallback",
723                    )
724                    .await;
725            }
726
727            // Tree-sitter failed or didn't validate — try name-based AST lookup.
728            // Instead of matching by byte offset (which is stale on dirty files),
729            // search cached AST nodes whose source text matches the cursor name
730            // and follow their referencedDeclaration.
731            if let Some(ref cb) = cached_build {
732                if let Some(ref name) = cursor_name {
733                    let byte_hint = goto::pos_to_bytes(&source_bytes, position);
734                    if let Some(location) =
735                        goto::goto_declaration_by_name(cb, &uri, name, byte_hint)
736                    {
737                        self.client
738                            .log_message(
739                                MessageType::INFO,
740                                format!(
741                                    "found definition (AST by name) at {}:{}",
742                                    location.uri, location.range.start.line
743                                ),
744                            )
745                            .await;
746                        return Ok(Some(GotoDefinitionResponse::from(location)));
747                    }
748                }
749            }
750        } else {
751            // CLEAN: AST first → tree-sitter fallback (validated)
752            if let Some(ref cb) = cached_build {
753                if let Some(location) =
754                    goto::goto_declaration(&cb.ast, &uri, position, &source_bytes)
755                {
756                    self.client
757                        .log_message(
758                            MessageType::INFO,
759                            format!(
760                                "found definition (AST) at {}:{}",
761                                location.uri, location.range.start.line
762                            ),
763                        )
764                        .await;
765                    return Ok(Some(GotoDefinitionResponse::from(location)));
766                }
767            }
768
769            // AST couldn't resolve — try tree-sitter fallback (validated)
770            let ts_result = {
771                let comp_cache = self.completion_cache.read().await;
772                let text_cache = self.text_cache.read().await;
773                if let Some(cc) = comp_cache.get(&uri.to_string()) {
774                    goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
775                } else {
776                    None
777                }
778            };
779
780            if let Some(location) = ts_result {
781                if validate_ts(&location) {
782                    self.client
783                        .log_message(
784                            MessageType::INFO,
785                            format!(
786                                "found definition (tree-sitter fallback) at {}:{}",
787                                location.uri, location.range.start.line
788                            ),
789                        )
790                        .await;
791                    return Ok(Some(GotoDefinitionResponse::from(location)));
792                }
793                self.client
794                    .log_message(MessageType::INFO, "tree-sitter fallback failed validation")
795                    .await;
796            }
797        }
798
799        self.client
800            .log_message(MessageType::INFO, "no definition found")
801            .await;
802        Ok(None)
803    }
804
805    async fn goto_declaration(
806        &self,
807        params: request::GotoDeclarationParams,
808    ) -> tower_lsp::jsonrpc::Result<Option<request::GotoDeclarationResponse>> {
809        self.client
810            .log_message(MessageType::INFO, "got textDocument/declaration request")
811            .await;
812
813        let uri = params.text_document_position_params.text_document.uri;
814        let position = params.text_document_position_params.position;
815
816        let file_path = match uri.to_file_path() {
817            Ok(path) => path,
818            Err(_) => {
819                self.client
820                    .log_message(MessageType::ERROR, "invalid file uri")
821                    .await;
822                return Ok(None);
823            }
824        };
825
826        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
827            Some(bytes) => bytes,
828            None => return Ok(None),
829        };
830
831        let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
832        let cached_build = match cached_build {
833            Some(cb) => cb,
834            None => return Ok(None),
835        };
836
837        if let Some(location) =
838            goto::goto_declaration(&cached_build.ast, &uri, position, &source_bytes)
839        {
840            self.client
841                .log_message(
842                    MessageType::INFO,
843                    format!(
844                        "found declaration at {}:{}",
845                        location.uri, location.range.start.line
846                    ),
847                )
848                .await;
849            Ok(Some(request::GotoDeclarationResponse::from(location)))
850        } else {
851            self.client
852                .log_message(MessageType::INFO, "no declaration found")
853                .await;
854            Ok(None)
855        }
856    }
857
858    async fn references(
859        &self,
860        params: ReferenceParams,
861    ) -> tower_lsp::jsonrpc::Result<Option<Vec<Location>>> {
862        self.client
863            .log_message(MessageType::INFO, "Got a textDocument/references request")
864            .await;
865
866        let uri = params.text_document_position.text_document.uri;
867        let position = params.text_document_position.position;
868        let file_path = match uri.to_file_path() {
869            Ok(path) => path,
870            Err(_) => {
871                self.client
872                    .log_message(MessageType::ERROR, "Invalid file URI")
873                    .await;
874                return Ok(None);
875            }
876        };
877        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
878            Some(bytes) => bytes,
879            None => return Ok(None),
880        };
881        let cached_build = self.get_or_fetch_build(&uri, &file_path, true).await;
882        let cached_build = match cached_build {
883            Some(cb) => cb,
884            None => return Ok(None),
885        };
886
887        // Get references from the current file's AST
888        let mut locations = references::goto_references(
889            &cached_build.ast,
890            &uri,
891            position,
892            &source_bytes,
893            params.context.include_declaration,
894        );
895
896        // Cross-file: resolve target definition location, then scan other cached ASTs
897        if let Some((def_abs_path, def_byte_offset)) =
898            references::resolve_target_location(&cached_build, &uri, position, &source_bytes)
899        {
900            let cache = self.ast_cache.read().await;
901            for (cached_uri, other_build) in cache.iter() {
902                if *cached_uri == uri.to_string() {
903                    continue;
904                }
905                let other_locations = references::goto_references_for_target(
906                    other_build,
907                    &def_abs_path,
908                    def_byte_offset,
909                    None,
910                    params.context.include_declaration,
911                );
912                locations.extend(other_locations);
913            }
914        }
915
916        // Deduplicate across all caches
917        let mut seen = std::collections::HashSet::new();
918        locations.retain(|loc| {
919            seen.insert((
920                loc.uri.clone(),
921                loc.range.start.line,
922                loc.range.start.character,
923                loc.range.end.line,
924                loc.range.end.character,
925            ))
926        });
927
928        if locations.is_empty() {
929            self.client
930                .log_message(MessageType::INFO, "No references found")
931                .await;
932            Ok(None)
933        } else {
934            self.client
935                .log_message(
936                    MessageType::INFO,
937                    format!("Found {} references", locations.len()),
938                )
939                .await;
940            Ok(Some(locations))
941        }
942    }
943
944    async fn prepare_rename(
945        &self,
946        params: TextDocumentPositionParams,
947    ) -> tower_lsp::jsonrpc::Result<Option<PrepareRenameResponse>> {
948        self.client
949            .log_message(MessageType::INFO, "got textDocument/prepareRename request")
950            .await;
951
952        let uri = params.text_document.uri;
953        let position = params.position;
954
955        let file_path = match uri.to_file_path() {
956            Ok(path) => path,
957            Err(_) => {
958                self.client
959                    .log_message(MessageType::ERROR, "invalid file uri")
960                    .await;
961                return Ok(None);
962            }
963        };
964
965        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
966            Some(bytes) => bytes,
967            None => return Ok(None),
968        };
969
970        if let Some(range) = rename::get_identifier_range(&source_bytes, position) {
971            self.client
972                .log_message(
973                    MessageType::INFO,
974                    format!(
975                        "prepare rename range: {}:{}",
976                        range.start.line, range.start.character
977                    ),
978                )
979                .await;
980            Ok(Some(PrepareRenameResponse::Range(range)))
981        } else {
982            self.client
983                .log_message(MessageType::INFO, "no identifier found for prepare rename")
984                .await;
985            Ok(None)
986        }
987    }
988
989    async fn rename(
990        &self,
991        params: RenameParams,
992    ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
993        self.client
994            .log_message(MessageType::INFO, "got textDocument/rename request")
995            .await;
996
997        let uri = params.text_document_position.text_document.uri;
998        let position = params.text_document_position.position;
999        let new_name = params.new_name;
1000        let file_path = match uri.to_file_path() {
1001            Ok(p) => p,
1002            Err(_) => {
1003                self.client
1004                    .log_message(MessageType::ERROR, "invalid file uri")
1005                    .await;
1006                return Ok(None);
1007            }
1008        };
1009        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1010            Some(bytes) => bytes,
1011            None => return Ok(None),
1012        };
1013
1014        let current_identifier = match rename::get_identifier_at_position(&source_bytes, position) {
1015            Some(id) => id,
1016            None => {
1017                self.client
1018                    .log_message(MessageType::ERROR, "No identifier found at position")
1019                    .await;
1020                return Ok(None);
1021            }
1022        };
1023
1024        if !utils::is_valid_solidity_identifier(&new_name) {
1025            return Err(tower_lsp::jsonrpc::Error::invalid_params(
1026                "new name is not a valid solidity identifier",
1027            ));
1028        }
1029
1030        if new_name == current_identifier {
1031            self.client
1032                .log_message(
1033                    MessageType::INFO,
1034                    "new name is the same as current identifier",
1035                )
1036                .await;
1037            return Ok(None);
1038        }
1039
1040        let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1041        let cached_build = match cached_build {
1042            Some(cb) => cb,
1043            None => return Ok(None),
1044        };
1045        let other_builds: Vec<Arc<goto::CachedBuild>> = {
1046            let cache = self.ast_cache.read().await;
1047            cache
1048                .iter()
1049                .filter(|(key, _)| **key != uri.to_string())
1050                .map(|(_, v)| v.clone())
1051                .collect()
1052        };
1053        let other_refs: Vec<&goto::CachedBuild> = other_builds.iter().map(|v| v.as_ref()).collect();
1054
1055        // Build a map of URI → file content from the text_cache so rename
1056        // verification reads from in-memory buffers (unsaved edits) instead
1057        // of from disk.
1058        let text_buffers: HashMap<String, Vec<u8>> = {
1059            let text_cache = self.text_cache.read().await;
1060            text_cache
1061                .iter()
1062                .map(|(uri, (_, content))| (uri.clone(), content.as_bytes().to_vec()))
1063                .collect()
1064        };
1065
1066        match rename::rename_symbol(
1067            &cached_build,
1068            &uri,
1069            position,
1070            &source_bytes,
1071            new_name,
1072            &other_refs,
1073            &text_buffers,
1074        ) {
1075            Some(workspace_edit) => {
1076                self.client
1077                    .log_message(
1078                        MessageType::INFO,
1079                        format!(
1080                            "created rename edit with {} file(s), {} total change(s)",
1081                            workspace_edit
1082                                .changes
1083                                .as_ref()
1084                                .map(|c| c.len())
1085                                .unwrap_or(0),
1086                            workspace_edit
1087                                .changes
1088                                .as_ref()
1089                                .map(|c| c.values().map(|v| v.len()).sum::<usize>())
1090                                .unwrap_or(0)
1091                        ),
1092                    )
1093                    .await;
1094
1095                // Return the full WorkspaceEdit to the client so the editor
1096                // applies all changes (including cross-file renames) via the
1097                // LSP protocol. This keeps undo working and avoids writing
1098                // files behind the editor's back.
1099                Ok(Some(workspace_edit))
1100            }
1101
1102            None => {
1103                self.client
1104                    .log_message(MessageType::INFO, "No locations found for renaming")
1105                    .await;
1106                Ok(None)
1107            }
1108        }
1109    }
1110
1111    async fn symbol(
1112        &self,
1113        params: WorkspaceSymbolParams,
1114    ) -> tower_lsp::jsonrpc::Result<Option<Vec<SymbolInformation>>> {
1115        self.client
1116            .log_message(MessageType::INFO, "got workspace/symbol request")
1117            .await;
1118
1119        // Collect sources from open files in text_cache
1120        let files: Vec<(Url, String)> = {
1121            let cache = self.text_cache.read().await;
1122            cache
1123                .iter()
1124                .filter(|(uri_str, _)| uri_str.ends_with(".sol"))
1125                .filter_map(|(uri_str, (_, content))| {
1126                    Url::parse(uri_str).ok().map(|uri| (uri, content.clone()))
1127                })
1128                .collect()
1129        };
1130
1131        let mut all_symbols = symbols::extract_workspace_symbols(&files);
1132        if !params.query.is_empty() {
1133            let query = params.query.to_lowercase();
1134            all_symbols.retain(|symbol| symbol.name.to_lowercase().contains(&query));
1135        }
1136        if all_symbols.is_empty() {
1137            self.client
1138                .log_message(MessageType::INFO, "No symbols found")
1139                .await;
1140            Ok(None)
1141        } else {
1142            self.client
1143                .log_message(
1144                    MessageType::INFO,
1145                    format!("found {} symbols", all_symbols.len()),
1146                )
1147                .await;
1148            Ok(Some(all_symbols))
1149        }
1150    }
1151
1152    async fn document_symbol(
1153        &self,
1154        params: DocumentSymbolParams,
1155    ) -> tower_lsp::jsonrpc::Result<Option<DocumentSymbolResponse>> {
1156        self.client
1157            .log_message(MessageType::INFO, "got textDocument/documentSymbol request")
1158            .await;
1159        let uri = params.text_document.uri;
1160        let file_path = match uri.to_file_path() {
1161            Ok(path) => path,
1162            Err(_) => {
1163                self.client
1164                    .log_message(MessageType::ERROR, "invalid file uri")
1165                    .await;
1166                return Ok(None);
1167            }
1168        };
1169
1170        // Read source from text_cache (open files) or disk
1171        let source = {
1172            let cache = self.text_cache.read().await;
1173            cache
1174                .get(&uri.to_string())
1175                .map(|(_, content)| content.clone())
1176        };
1177        let source = match source {
1178            Some(s) => s,
1179            None => match std::fs::read_to_string(&file_path) {
1180                Ok(s) => s,
1181                Err(_) => return Ok(None),
1182            },
1183        };
1184
1185        let symbols = symbols::extract_document_symbols(&source);
1186        if symbols.is_empty() {
1187            self.client
1188                .log_message(MessageType::INFO, "no document symbols found")
1189                .await;
1190            Ok(None)
1191        } else {
1192            self.client
1193                .log_message(
1194                    MessageType::INFO,
1195                    format!("found {} document symbols", symbols.len()),
1196                )
1197                .await;
1198            Ok(Some(DocumentSymbolResponse::Nested(symbols)))
1199        }
1200    }
1201
1202    async fn hover(&self, params: HoverParams) -> tower_lsp::jsonrpc::Result<Option<Hover>> {
1203        self.client
1204            .log_message(MessageType::INFO, "got textDocument/hover request")
1205            .await;
1206
1207        let uri = params.text_document_position_params.text_document.uri;
1208        let position = params.text_document_position_params.position;
1209
1210        let file_path = match uri.to_file_path() {
1211            Ok(path) => path,
1212            Err(_) => {
1213                self.client
1214                    .log_message(MessageType::ERROR, "invalid file uri")
1215                    .await;
1216                return Ok(None);
1217            }
1218        };
1219
1220        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1221            Some(bytes) => bytes,
1222            None => return Ok(None),
1223        };
1224
1225        let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1226        let cached_build = match cached_build {
1227            Some(cb) => cb,
1228            None => return Ok(None),
1229        };
1230
1231        let result = hover::hover_info(&cached_build.ast, &uri, position, &source_bytes);
1232
1233        if result.is_some() {
1234            self.client
1235                .log_message(MessageType::INFO, "hover info found")
1236                .await;
1237        } else {
1238            self.client
1239                .log_message(MessageType::INFO, "no hover info found")
1240                .await;
1241        }
1242
1243        Ok(result)
1244    }
1245
1246    async fn document_link(
1247        &self,
1248        params: DocumentLinkParams,
1249    ) -> tower_lsp::jsonrpc::Result<Option<Vec<DocumentLink>>> {
1250        self.client
1251            .log_message(MessageType::INFO, "got textDocument/documentLink request")
1252            .await;
1253
1254        let uri = params.text_document.uri;
1255        let file_path = match uri.to_file_path() {
1256            Ok(path) => path,
1257            Err(_) => {
1258                self.client
1259                    .log_message(MessageType::ERROR, "invalid file uri")
1260                    .await;
1261                return Ok(None);
1262            }
1263        };
1264
1265        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1266            Some(bytes) => bytes,
1267            None => return Ok(None),
1268        };
1269
1270        let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1271        let cached_build = match cached_build {
1272            Some(cb) => cb,
1273            None => return Ok(None),
1274        };
1275
1276        let result = links::document_links(&cached_build, &uri, &source_bytes);
1277
1278        if result.is_empty() {
1279            self.client
1280                .log_message(MessageType::INFO, "no document links found")
1281                .await;
1282            Ok(None)
1283        } else {
1284            self.client
1285                .log_message(
1286                    MessageType::INFO,
1287                    format!("found {} document links", result.len()),
1288                )
1289                .await;
1290            Ok(Some(result))
1291        }
1292    }
1293
1294    async fn semantic_tokens_full(
1295        &self,
1296        params: SemanticTokensParams,
1297    ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensResult>> {
1298        self.client
1299            .log_message(
1300                MessageType::INFO,
1301                "got textDocument/semanticTokens/full request",
1302            )
1303            .await;
1304
1305        let uri = params.text_document.uri;
1306        let source = {
1307            let cache = self.text_cache.read().await;
1308            cache.get(&uri.to_string()).map(|(_, s)| s.clone())
1309        };
1310
1311        let source = match source {
1312            Some(s) => s,
1313            None => {
1314                // File not open in editor — try reading from disk
1315                let file_path = match uri.to_file_path() {
1316                    Ok(p) => p,
1317                    Err(_) => return Ok(None),
1318                };
1319                match std::fs::read_to_string(&file_path) {
1320                    Ok(s) => s,
1321                    Err(_) => return Ok(None),
1322                }
1323            }
1324        };
1325
1326        let tokens = semantic_tokens::semantic_tokens_full(&source);
1327
1328        Ok(Some(SemanticTokensResult::Tokens(tokens)))
1329    }
1330
1331    async fn inlay_hint(
1332        &self,
1333        params: InlayHintParams,
1334    ) -> tower_lsp::jsonrpc::Result<Option<Vec<InlayHint>>> {
1335        self.client
1336            .log_message(MessageType::INFO, "got textDocument/inlayHint request")
1337            .await;
1338
1339        let uri = params.text_document.uri;
1340        let range = params.range;
1341
1342        let file_path = match uri.to_file_path() {
1343            Ok(path) => path,
1344            Err(_) => {
1345                self.client
1346                    .log_message(MessageType::ERROR, "invalid file uri")
1347                    .await;
1348                return Ok(None);
1349            }
1350        };
1351
1352        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1353            Some(bytes) => bytes,
1354            None => return Ok(None),
1355        };
1356
1357        let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1358        let cached_build = match cached_build {
1359            Some(cb) => cb,
1360            None => return Ok(None),
1361        };
1362
1363        let hints = inlay_hints::inlay_hints(&cached_build, &uri, range, &source_bytes);
1364
1365        if hints.is_empty() {
1366            self.client
1367                .log_message(MessageType::INFO, "no inlay hints found")
1368                .await;
1369            Ok(None)
1370        } else {
1371            self.client
1372                .log_message(
1373                    MessageType::INFO,
1374                    format!("found {} inlay hints", hints.len()),
1375                )
1376                .await;
1377            Ok(Some(hints))
1378        }
1379    }
1380}