Skip to main content

solidity_language_server/
lsp.rs

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