Skip to main content

solidity_language_server/
lsp.rs

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