Skip to main content

solidity_language_server/
lsp.rs

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