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