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