Skip to main content

solidity_language_server/
lsp.rs

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