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 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 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; });
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}