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