solidity_language_server/
lsp.rs

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