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