Skip to main content

solidity_language_server/
lsp.rs

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