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