Skip to main content

solidity_language_server/
lsp.rs

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