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.to_encoding_kind()),
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_line, end_character) =
460                utils::byte_offset_to_position(&original_content, original_content.len());
461            let edit = TextEdit {
462                range: Range {
463                    start: Position {
464                        line: 0,
465                        character: 0,
466                    },
467                    end: Position {
468                        line: end_line,
469                        character: end_character,
470                    },
471                },
472                new_text: formatted_content,
473            };
474            Ok(Some(vec![edit]))
475        } else {
476            Ok(None)
477        }
478    }
479
480    async fn did_close(&self, params: DidCloseTextDocumentParams) {
481        let uri = params.text_document.uri.to_string();
482        self.ast_cache.write().await.remove(&uri);
483        self.text_cache.write().await.remove(&uri);
484        self.completion_cache.write().await.remove(&uri);
485        self.client
486            .log_message(MessageType::INFO, "file closed, caches cleared.")
487            .await;
488    }
489
490    async fn did_change_configuration(&self, _: DidChangeConfigurationParams) {
491        self.client
492            .log_message(MessageType::INFO, "configuration changed.")
493            .await;
494    }
495    async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {
496        self.client
497            .log_message(MessageType::INFO, "workdspace folders changed.")
498            .await;
499    }
500
501    async fn did_change_watched_files(&self, _: DidChangeWatchedFilesParams) {
502        self.client
503            .log_message(MessageType::INFO, "watched files have changed.")
504            .await;
505    }
506
507    async fn completion(
508        &self,
509        params: CompletionParams,
510    ) -> tower_lsp::jsonrpc::Result<Option<CompletionResponse>> {
511        let uri = params.text_document_position.text_document.uri;
512        let position = params.text_document_position.position;
513
514        let trigger_char = params
515            .context
516            .as_ref()
517            .and_then(|ctx| ctx.trigger_character.as_deref());
518
519        // Get source text — only needed for dot completions (to parse the line)
520        let source_text = {
521            let text_cache = self.text_cache.read().await;
522            if let Some((_, text)) = text_cache.get(&uri.to_string()) {
523                text.clone()
524            } else {
525                match uri.to_file_path() {
526                    Ok(path) => std::fs::read_to_string(&path).unwrap_or_default(),
527                    Err(_) => return Ok(None),
528                }
529            }
530        };
531
532        // Clone the Arc (pointer copy, instant) and drop the lock immediately.
533        let cached: Option<Arc<completion::CompletionCache>> = {
534            let comp_cache = self.completion_cache.read().await;
535            comp_cache.get(&uri.to_string()).cloned()
536        };
537
538        if cached.is_none() {
539            // Spawn background cache build so the next request will have full completions
540            let ast_cache = self.ast_cache.clone();
541            let completion_cache = self.completion_cache.clone();
542            let uri_string = uri.to_string();
543            tokio::spawn(async move {
544                let cached_build = {
545                    let cache = ast_cache.read().await;
546                    match cache.get(&uri_string) {
547                        Some(v) => v.clone(),
548                        None => return,
549                    }
550                };
551                if let Some(sources) = cached_build.ast.get("sources") {
552                    let contracts = cached_build.ast.get("contracts");
553                    let cc = completion::build_completion_cache(sources, contracts);
554                    completion_cache
555                        .write()
556                        .await
557                        .insert(uri_string, Arc::new(cc));
558                }
559            });
560        }
561
562        let cache_ref = cached.as_deref();
563
564        // Look up the AST file_id for scope-aware resolution
565        let file_id = {
566            let uri_path = uri.to_file_path().ok();
567            cache_ref.and_then(|c| {
568                uri_path.as_ref().and_then(|p| {
569                    let path_str = p.to_str()?;
570                    c.path_to_file_id.get(path_str).copied()
571                })
572            })
573        };
574
575        let result =
576            completion::handle_completion(cache_ref, &source_text, position, trigger_char, file_id);
577        Ok(result)
578    }
579
580    async fn goto_definition(
581        &self,
582        params: GotoDefinitionParams,
583    ) -> tower_lsp::jsonrpc::Result<Option<GotoDefinitionResponse>> {
584        self.client
585            .log_message(MessageType::INFO, "got textDocument/definition request")
586            .await;
587
588        let uri = params.text_document_position_params.text_document.uri;
589        let position = params.text_document_position_params.position;
590
591        let file_path = match uri.to_file_path() {
592            Ok(path) => path,
593            Err(_) => {
594                self.client
595                    .log_message(MessageType::ERROR, "Invalid file uri")
596                    .await;
597                return Ok(None);
598            }
599        };
600
601        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
602            Some(bytes) => bytes,
603            None => return Ok(None),
604        };
605
606        let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
607        let cached_build = match cached_build {
608            Some(cb) => cb,
609            None => return Ok(None),
610        };
611
612        if let Some(location) =
613            goto::goto_declaration(&cached_build.ast, &uri, position, &source_bytes)
614        {
615            self.client
616                .log_message(
617                    MessageType::INFO,
618                    format!(
619                        "found definition at {}:{}",
620                        location.uri, location.range.start.line
621                    ),
622                )
623                .await;
624            Ok(Some(GotoDefinitionResponse::from(location)))
625        } else {
626            self.client
627                .log_message(MessageType::INFO, "no definition found")
628                .await;
629            Ok(None)
630        }
631    }
632
633    async fn goto_declaration(
634        &self,
635        params: request::GotoDeclarationParams,
636    ) -> tower_lsp::jsonrpc::Result<Option<request::GotoDeclarationResponse>> {
637        self.client
638            .log_message(MessageType::INFO, "got textDocument/declaration request")
639            .await;
640
641        let uri = params.text_document_position_params.text_document.uri;
642        let position = params.text_document_position_params.position;
643
644        let file_path = match uri.to_file_path() {
645            Ok(path) => path,
646            Err(_) => {
647                self.client
648                    .log_message(MessageType::ERROR, "invalid file uri")
649                    .await;
650                return Ok(None);
651            }
652        };
653
654        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
655            Some(bytes) => bytes,
656            None => return Ok(None),
657        };
658
659        let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
660        let cached_build = match cached_build {
661            Some(cb) => cb,
662            None => return Ok(None),
663        };
664
665        if let Some(location) =
666            goto::goto_declaration(&cached_build.ast, &uri, position, &source_bytes)
667        {
668            self.client
669                .log_message(
670                    MessageType::INFO,
671                    format!(
672                        "found declaration at {}:{}",
673                        location.uri, location.range.start.line
674                    ),
675                )
676                .await;
677            Ok(Some(request::GotoDeclarationResponse::from(location)))
678        } else {
679            self.client
680                .log_message(MessageType::INFO, "no declaration found")
681                .await;
682            Ok(None)
683        }
684    }
685
686    async fn references(
687        &self,
688        params: ReferenceParams,
689    ) -> tower_lsp::jsonrpc::Result<Option<Vec<Location>>> {
690        self.client
691            .log_message(MessageType::INFO, "Got a textDocument/references request")
692            .await;
693
694        let uri = params.text_document_position.text_document.uri;
695        let position = params.text_document_position.position;
696        let file_path = match uri.to_file_path() {
697            Ok(path) => path,
698            Err(_) => {
699                self.client
700                    .log_message(MessageType::ERROR, "Invalid file URI")
701                    .await;
702                return Ok(None);
703            }
704        };
705        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
706            Some(bytes) => bytes,
707            None => return Ok(None),
708        };
709        let cached_build = self.get_or_fetch_build(&uri, &file_path, true).await;
710        let cached_build = match cached_build {
711            Some(cb) => cb,
712            None => return Ok(None),
713        };
714
715        // Get references from the current file's AST
716        let mut locations = references::goto_references(
717            &cached_build.ast,
718            &uri,
719            position,
720            &source_bytes,
721            params.context.include_declaration,
722        );
723
724        // Cross-file: resolve target definition location, then scan other cached ASTs
725        if let Some((def_abs_path, def_byte_offset)) =
726            references::resolve_target_location(&cached_build, &uri, position, &source_bytes)
727        {
728            let cache = self.ast_cache.read().await;
729            for (cached_uri, other_build) in cache.iter() {
730                if *cached_uri == uri.to_string() {
731                    continue;
732                }
733                let other_locations = references::goto_references_for_target(
734                    other_build,
735                    &def_abs_path,
736                    def_byte_offset,
737                    None,
738                    params.context.include_declaration,
739                );
740                locations.extend(other_locations);
741            }
742        }
743
744        // Deduplicate across all caches
745        let mut seen = std::collections::HashSet::new();
746        locations.retain(|loc| {
747            seen.insert((
748                loc.uri.clone(),
749                loc.range.start.line,
750                loc.range.start.character,
751                loc.range.end.line,
752                loc.range.end.character,
753            ))
754        });
755
756        if locations.is_empty() {
757            self.client
758                .log_message(MessageType::INFO, "No references found")
759                .await;
760            Ok(None)
761        } else {
762            self.client
763                .log_message(
764                    MessageType::INFO,
765                    format!("Found {} references", locations.len()),
766                )
767                .await;
768            Ok(Some(locations))
769        }
770    }
771
772    async fn prepare_rename(
773        &self,
774        params: TextDocumentPositionParams,
775    ) -> tower_lsp::jsonrpc::Result<Option<PrepareRenameResponse>> {
776        self.client
777            .log_message(MessageType::INFO, "got textDocument/prepareRename request")
778            .await;
779
780        let uri = params.text_document.uri;
781        let position = params.position;
782
783        let file_path = match uri.to_file_path() {
784            Ok(path) => path,
785            Err(_) => {
786                self.client
787                    .log_message(MessageType::ERROR, "invalid file uri")
788                    .await;
789                return Ok(None);
790            }
791        };
792
793        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
794            Some(bytes) => bytes,
795            None => return Ok(None),
796        };
797
798        if let Some(range) = rename::get_identifier_range(&source_bytes, position) {
799            self.client
800                .log_message(
801                    MessageType::INFO,
802                    format!(
803                        "prepare rename range: {}:{}",
804                        range.start.line, range.start.character
805                    ),
806                )
807                .await;
808            Ok(Some(PrepareRenameResponse::Range(range)))
809        } else {
810            self.client
811                .log_message(MessageType::INFO, "no identifier found for prepare rename")
812                .await;
813            Ok(None)
814        }
815    }
816
817    async fn rename(
818        &self,
819        params: RenameParams,
820    ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
821        self.client
822            .log_message(MessageType::INFO, "got textDocument/rename request")
823            .await;
824
825        let uri = params.text_document_position.text_document.uri;
826        let position = params.text_document_position.position;
827        let new_name = params.new_name;
828        let file_path = match uri.to_file_path() {
829            Ok(p) => p,
830            Err(_) => {
831                self.client
832                    .log_message(MessageType::ERROR, "invalid file uri")
833                    .await;
834                return Ok(None);
835            }
836        };
837        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
838            Some(bytes) => bytes,
839            None => return Ok(None),
840        };
841
842        let current_identifier = match rename::get_identifier_at_position(&source_bytes, position) {
843            Some(id) => id,
844            None => {
845                self.client
846                    .log_message(MessageType::ERROR, "No identifier found at position")
847                    .await;
848                return Ok(None);
849            }
850        };
851
852        if !utils::is_valid_solidity_identifier(&new_name) {
853            return Err(tower_lsp::jsonrpc::Error::invalid_params(
854                "new name is not a valid solidity identifier",
855            ));
856        }
857
858        if new_name == current_identifier {
859            self.client
860                .log_message(
861                    MessageType::INFO,
862                    "new name is the same as current identifier",
863                )
864                .await;
865            return Ok(None);
866        }
867
868        let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
869        let cached_build = match cached_build {
870            Some(cb) => cb,
871            None => return Ok(None),
872        };
873        let other_builds: Vec<Arc<goto::CachedBuild>> = {
874            let cache = self.ast_cache.read().await;
875            cache
876                .iter()
877                .filter(|(key, _)| **key != uri.to_string())
878                .map(|(_, v)| v.clone())
879                .collect()
880        };
881        let other_refs: Vec<&goto::CachedBuild> = other_builds.iter().map(|v| v.as_ref()).collect();
882
883        // Build a map of URI → file content from the text_cache so rename
884        // verification reads from in-memory buffers (unsaved edits) instead
885        // of from disk.
886        let text_buffers: HashMap<String, Vec<u8>> = {
887            let text_cache = self.text_cache.read().await;
888            text_cache
889                .iter()
890                .map(|(uri, (_, content))| (uri.clone(), content.as_bytes().to_vec()))
891                .collect()
892        };
893
894        match rename::rename_symbol(
895            &cached_build,
896            &uri,
897            position,
898            &source_bytes,
899            new_name,
900            &other_refs,
901            &text_buffers,
902        ) {
903            Some(workspace_edit) => {
904                self.client
905                    .log_message(
906                        MessageType::INFO,
907                        format!(
908                            "created rename edit with {} file(s), {} total change(s)",
909                            workspace_edit
910                                .changes
911                                .as_ref()
912                                .map(|c| c.len())
913                                .unwrap_or(0),
914                            workspace_edit
915                                .changes
916                                .as_ref()
917                                .map(|c| c.values().map(|v| v.len()).sum::<usize>())
918                                .unwrap_or(0)
919                        ),
920                    )
921                    .await;
922
923                // Return the full WorkspaceEdit to the client so the editor
924                // applies all changes (including cross-file renames) via the
925                // LSP protocol. This keeps undo working and avoids writing
926                // files behind the editor's back.
927                Ok(Some(workspace_edit))
928            }
929
930            None => {
931                self.client
932                    .log_message(MessageType::INFO, "No locations found for renaming")
933                    .await;
934                Ok(None)
935            }
936        }
937    }
938
939    async fn symbol(
940        &self,
941        params: WorkspaceSymbolParams,
942    ) -> tower_lsp::jsonrpc::Result<Option<Vec<SymbolInformation>>> {
943        self.client
944            .log_message(MessageType::INFO, "got workspace/symbol request")
945            .await;
946
947        // Use a cached AST if available (any entry has the full workspace build).
948        // Fall back to a fresh build only on cache miss.
949        let ast_data = {
950            let cache = self.ast_cache.read().await;
951            cache.values().next().map(|cb| cb.ast.clone())
952        };
953        let ast_data = match ast_data {
954            Some(data) => data,
955            None => {
956                let current_dir = std::env::current_dir().ok();
957                if let Some(dir) = current_dir {
958                    let path_str = dir.to_str().unwrap_or(".");
959                    match self.compiler.ast(path_str).await {
960                        Ok(data) => data,
961                        Err(e) => {
962                            self.client
963                                .log_message(
964                                    MessageType::WARNING,
965                                    format!("failed to get ast data: {e}"),
966                                )
967                                .await;
968                            return Ok(None);
969                        }
970                    }
971                } else {
972                    self.client
973                        .log_message(MessageType::ERROR, "could not get current directory")
974                        .await;
975                    return Ok(None);
976                }
977            }
978        };
979
980        let mut all_symbols = symbols::extract_symbols(&ast_data);
981        if !params.query.is_empty() {
982            let query = params.query.to_lowercase();
983            all_symbols.retain(|symbol| symbol.name.to_lowercase().contains(&query));
984        }
985        if all_symbols.is_empty() {
986            self.client
987                .log_message(MessageType::INFO, "No symbols found")
988                .await;
989            Ok(None)
990        } else {
991            self.client
992                .log_message(
993                    MessageType::INFO,
994                    format!("found {} symbol", all_symbols.len()),
995                )
996                .await;
997            Ok(Some(all_symbols))
998        }
999    }
1000
1001    async fn document_symbol(
1002        &self,
1003        params: DocumentSymbolParams,
1004    ) -> tower_lsp::jsonrpc::Result<Option<DocumentSymbolResponse>> {
1005        self.client
1006            .log_message(MessageType::INFO, "got textDocument/documentSymbol request")
1007            .await;
1008        let uri = params.text_document.uri;
1009        let file_path = match uri.to_file_path() {
1010            Ok(path) => path,
1011            Err(_) => {
1012                self.client
1013                    .log_message(MessageType::ERROR, "invalid file uri")
1014                    .await;
1015                return Ok(None);
1016            }
1017        };
1018
1019        let path_str = match file_path.to_str() {
1020            Some(s) => s,
1021            None => {
1022                self.client
1023                    .log_message(MessageType::ERROR, "invalid path")
1024                    .await;
1025                return Ok(None);
1026            }
1027        };
1028        // Use cached AST if available, otherwise fetch fresh
1029        let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1030        let cached_build = match cached_build {
1031            Some(cb) => cb,
1032            None => return Ok(None),
1033        };
1034        let symbols = symbols::extract_document_symbols(&cached_build.ast, path_str);
1035        if symbols.is_empty() {
1036            self.client
1037                .log_message(MessageType::INFO, "no document symbols found")
1038                .await;
1039            Ok(None)
1040        } else {
1041            self.client
1042                .log_message(
1043                    MessageType::INFO,
1044                    format!("found {} document symbols", symbols.len()),
1045                )
1046                .await;
1047            Ok(Some(DocumentSymbolResponse::Nested(symbols)))
1048        }
1049    }
1050
1051    async fn hover(&self, params: HoverParams) -> tower_lsp::jsonrpc::Result<Option<Hover>> {
1052        self.client
1053            .log_message(MessageType::INFO, "got textDocument/hover request")
1054            .await;
1055
1056        let uri = params.text_document_position_params.text_document.uri;
1057        let position = params.text_document_position_params.position;
1058
1059        let file_path = match uri.to_file_path() {
1060            Ok(path) => path,
1061            Err(_) => {
1062                self.client
1063                    .log_message(MessageType::ERROR, "invalid file uri")
1064                    .await;
1065                return Ok(None);
1066            }
1067        };
1068
1069        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1070            Some(bytes) => bytes,
1071            None => return Ok(None),
1072        };
1073
1074        let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1075        let cached_build = match cached_build {
1076            Some(cb) => cb,
1077            None => return Ok(None),
1078        };
1079
1080        let result = hover::hover_info(&cached_build.ast, &uri, position, &source_bytes);
1081
1082        if result.is_some() {
1083            self.client
1084                .log_message(MessageType::INFO, "hover info found")
1085                .await;
1086        } else {
1087            self.client
1088                .log_message(MessageType::INFO, "no hover info found")
1089                .await;
1090        }
1091
1092        Ok(result)
1093    }
1094
1095    async fn document_link(
1096        &self,
1097        params: DocumentLinkParams,
1098    ) -> tower_lsp::jsonrpc::Result<Option<Vec<DocumentLink>>> {
1099        self.client
1100            .log_message(MessageType::INFO, "got textDocument/documentLink request")
1101            .await;
1102
1103        let uri = params.text_document.uri;
1104        let file_path = match uri.to_file_path() {
1105            Ok(path) => path,
1106            Err(_) => {
1107                self.client
1108                    .log_message(MessageType::ERROR, "invalid file uri")
1109                    .await;
1110                return Ok(None);
1111            }
1112        };
1113
1114        let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1115            Some(bytes) => bytes,
1116            None => return Ok(None),
1117        };
1118
1119        let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1120        let cached_build = match cached_build {
1121            Some(cb) => cb,
1122            None => return Ok(None),
1123        };
1124
1125        let result = links::document_links(&cached_build, &uri, &source_bytes);
1126
1127        if result.is_empty() {
1128            self.client
1129                .log_message(MessageType::INFO, "no document links found")
1130                .await;
1131            Ok(None)
1132        } else {
1133            self.client
1134                .log_message(
1135                    MessageType::INFO,
1136                    format!("found {} document links", result.len()),
1137                )
1138                .await;
1139            Ok(Some(result))
1140        }
1141    }
1142}