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