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