1use crate::completion;
2use crate::config::{self, FoundryConfig, LintConfig};
3use crate::goto;
4use crate::hover;
5use crate::inlay_hints;
6use crate::links;
7use crate::references;
8use crate::rename;
9use crate::runner::{ForgeRunner, Runner};
10use crate::semantic_tokens;
11use crate::symbols;
12use crate::utils;
13use std::collections::HashMap;
14use std::sync::Arc;
15use tokio::sync::RwLock;
16use tower_lsp::{Client, LanguageServer, lsp_types::*};
17
18pub struct ForgeLsp {
19 client: Client,
20 compiler: Arc<dyn Runner>,
21 ast_cache: Arc<RwLock<HashMap<String, Arc<goto::CachedBuild>>>>,
22 text_cache: Arc<RwLock<HashMap<String, (i32, String)>>>,
26 completion_cache: Arc<RwLock<HashMap<String, Arc<completion::CompletionCache>>>>,
27 lint_config: Arc<RwLock<LintConfig>>,
29 foundry_config: Arc<RwLock<FoundryConfig>>,
31 client_capabilities: Arc<RwLock<Option<ClientCapabilities>>>,
33 use_solc: bool,
35}
36
37impl ForgeLsp {
38 pub fn new(client: Client, use_solar: bool, use_solc: bool) -> Self {
39 let compiler: Arc<dyn Runner> = if use_solar {
40 Arc::new(crate::solar_runner::SolarRunner)
41 } else {
42 Arc::new(ForgeRunner)
43 };
44 let ast_cache = Arc::new(RwLock::new(HashMap::new()));
45 let text_cache = Arc::new(RwLock::new(HashMap::new()));
46 let completion_cache = Arc::new(RwLock::new(HashMap::new()));
47 let lint_config = Arc::new(RwLock::new(LintConfig::default()));
48 let foundry_config = Arc::new(RwLock::new(FoundryConfig::default()));
49 let client_capabilities = Arc::new(RwLock::new(None));
50 Self {
51 client,
52 compiler,
53 ast_cache,
54 text_cache,
55 completion_cache,
56 lint_config,
57 foundry_config,
58 client_capabilities,
59 use_solc,
60 }
61 }
62
63 async fn on_change(&self, params: TextDocumentItem) {
64 let uri = params.uri.clone();
65 let version = params.version;
66
67 let file_path = match uri.to_file_path() {
68 Ok(path) => path,
69 Err(_) => {
70 self.client
71 .log_message(MessageType::ERROR, "Invalid file URI")
72 .await;
73 return;
74 }
75 };
76
77 let path_str = match file_path.to_str() {
78 Some(s) => s,
79 None => {
80 self.client
81 .log_message(MessageType::ERROR, "Invalid file path")
82 .await;
83 return;
84 }
85 };
86
87 let should_lint = {
89 let lint_cfg = self.lint_config.read().await;
90 lint_cfg.should_lint(&file_path)
91 };
92
93 let (lint_result, build_result, ast_result) = if self.use_solc {
97 let foundry_cfg = self.foundry_config.read().await.clone();
98 let solc_future = crate::solc::solc_ast(path_str, &foundry_cfg, Some(&self.client));
99
100 if should_lint {
101 let (lint, solc) =
102 tokio::join!(self.compiler.get_lint_diagnostics(&uri), solc_future);
103 match solc {
104 Ok(data) => {
105 self.client
106 .log_message(
107 MessageType::INFO,
108 "solc: AST + diagnostics from single run",
109 )
110 .await;
111 let content = tokio::fs::read_to_string(&file_path)
113 .await
114 .unwrap_or_default();
115 let build_diags = crate::build::build_output_to_diagnostics(
116 &data,
117 &file_path,
118 &content,
119 &foundry_cfg.ignored_error_codes,
120 );
121 (Some(lint), Ok(build_diags), Ok(data))
122 }
123 Err(e) => {
124 self.client
125 .log_message(
126 MessageType::WARNING,
127 format!("solc failed, falling back to forge: {e}"),
128 )
129 .await;
130 let (build, ast) = tokio::join!(
131 self.compiler.get_build_diagnostics(&uri),
132 self.compiler.ast(path_str)
133 );
134 (Some(lint), build, ast)
135 }
136 }
137 } else {
138 self.client
139 .log_message(
140 MessageType::INFO,
141 format!("skipping lint for ignored file: {path_str}"),
142 )
143 .await;
144 match solc_future.await {
145 Ok(data) => {
146 self.client
147 .log_message(
148 MessageType::INFO,
149 "solc: AST + diagnostics from single run",
150 )
151 .await;
152 let content = tokio::fs::read_to_string(&file_path)
153 .await
154 .unwrap_or_default();
155 let build_diags = crate::build::build_output_to_diagnostics(
156 &data,
157 &file_path,
158 &content,
159 &foundry_cfg.ignored_error_codes,
160 );
161 (None, Ok(build_diags), Ok(data))
162 }
163 Err(e) => {
164 self.client
165 .log_message(
166 MessageType::WARNING,
167 format!("solc failed, falling back to forge: {e}"),
168 )
169 .await;
170 let (build, ast) = tokio::join!(
171 self.compiler.get_build_diagnostics(&uri),
172 self.compiler.ast(path_str)
173 );
174 (None, build, ast)
175 }
176 }
177 }
178 } else {
179 if should_lint {
181 let (lint, build, ast) = tokio::join!(
182 self.compiler.get_lint_diagnostics(&uri),
183 self.compiler.get_build_diagnostics(&uri),
184 self.compiler.ast(path_str)
185 );
186 (Some(lint), build, ast)
187 } else {
188 self.client
189 .log_message(
190 MessageType::INFO,
191 format!("skipping lint for ignored file: {path_str}"),
192 )
193 .await;
194 let (build, ast) = tokio::join!(
195 self.compiler.get_build_diagnostics(&uri),
196 self.compiler.ast(path_str)
197 );
198 (None, build, ast)
199 }
200 };
201
202 let build_succeeded = matches!(&build_result, Ok(diagnostics) if diagnostics.iter().all(|d| d.severity != Some(DiagnosticSeverity::ERROR)));
204
205 if build_succeeded {
206 if let Ok(ast_data) = ast_result {
207 let cached_build = Arc::new(goto::CachedBuild::new(ast_data, version));
208 let mut cache = self.ast_cache.write().await;
209 cache.insert(uri.to_string(), cached_build.clone());
210 drop(cache);
211
212 let completion_cache = self.completion_cache.clone();
214 let uri_string = uri.to_string();
215 tokio::spawn(async move {
216 if let Some(sources) = cached_build.ast.get("sources") {
217 let contracts = cached_build.ast.get("contracts");
218 let cc = completion::build_completion_cache(sources, contracts);
219 completion_cache
220 .write()
221 .await
222 .insert(uri_string, Arc::new(cc));
223 }
224 });
225 self.client
226 .log_message(MessageType::INFO, "Build successful, AST cache updated")
227 .await;
228 } else if let Err(e) = ast_result {
229 self.client
230 .log_message(
231 MessageType::INFO,
232 format!("Build succeeded but failed to get AST: {e}"),
233 )
234 .await;
235 }
236 } else {
237 self.client
239 .log_message(
240 MessageType::INFO,
241 "Build errors detected, keeping existing AST cache",
242 )
243 .await;
244 }
245
246 {
248 let mut text_cache = self.text_cache.write().await;
249 let uri_str = uri.to_string();
250 let existing_version = text_cache.get(&uri_str).map(|(v, _)| *v).unwrap_or(-1);
251 if version >= existing_version {
252 text_cache.insert(uri_str, (version, params.text));
253 }
254 }
255
256 let mut all_diagnostics = vec![];
257
258 if let Some(lint_result) = lint_result {
259 match lint_result {
260 Ok(mut lints) => {
261 self.client
262 .log_message(
263 MessageType::INFO,
264 format!("found {} lint diagnostics", lints.len()),
265 )
266 .await;
267 all_diagnostics.append(&mut lints);
268 }
269 Err(e) => {
270 self.client
271 .log_message(
272 MessageType::ERROR,
273 format!("Forge lint diagnostics failed: {e}"),
274 )
275 .await;
276 }
277 }
278 }
279
280 match build_result {
281 Ok(mut builds) => {
282 self.client
283 .log_message(
284 MessageType::INFO,
285 format!("found {} build diagnostics", builds.len()),
286 )
287 .await;
288 all_diagnostics.append(&mut builds);
289 }
290 Err(e) => {
291 self.client
292 .log_message(
293 MessageType::WARNING,
294 format!("Forge build diagnostics failed: {e}"),
295 )
296 .await;
297 }
298 }
299
300 self.client
302 .publish_diagnostics(uri, all_diagnostics, None)
303 .await;
304
305 if build_succeeded {
307 let client = self.client.clone();
308 tokio::spawn(async move {
309 let _ = client.inlay_hint_refresh().await;
310 });
311 }
312 }
313
314 async fn get_or_fetch_build(
323 &self,
324 uri: &Url,
325 file_path: &std::path::Path,
326 insert_on_miss: bool,
327 ) -> Option<Arc<goto::CachedBuild>> {
328 let uri_str = uri.to_string();
329
330 {
333 let cache = self.ast_cache.read().await;
334 if let Some(cached) = cache.get(&uri_str) {
335 return Some(cached.clone());
336 }
337 }
338
339 if !insert_on_miss {
343 return None;
344 }
345
346 let path_str = file_path.to_str()?;
348 let ast_result = if self.use_solc {
349 let foundry_cfg = self.foundry_config.read().await.clone();
350 match crate::solc::solc_ast(path_str, &foundry_cfg, Some(&self.client)).await {
351 Ok(data) => Ok(data),
352 Err(_) => self.compiler.ast(path_str).await,
353 }
354 } else {
355 self.compiler.ast(path_str).await
356 };
357 match ast_result {
358 Ok(data) => {
359 let build = Arc::new(goto::CachedBuild::new(data, 0));
362 let mut cache = self.ast_cache.write().await;
363 cache.insert(uri_str.clone(), build.clone());
364 Some(build)
365 }
366 Err(e) => {
367 self.client
368 .log_message(MessageType::ERROR, format!("failed to get AST: {e}"))
369 .await;
370 None
371 }
372 }
373 }
374
375 async fn get_source_bytes(&self, uri: &Url, file_path: &std::path::Path) -> Option<Vec<u8>> {
378 {
379 let text_cache = self.text_cache.read().await;
380 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
381 return Some(content.as_bytes().to_vec());
382 }
383 }
384 match std::fs::read(file_path) {
385 Ok(bytes) => Some(bytes),
386 Err(e) => {
387 self.client
388 .log_message(MessageType::ERROR, format!("failed to read file: {e}"))
389 .await;
390 None
391 }
392 }
393 }
394}
395
396#[tower_lsp::async_trait]
397impl LanguageServer for ForgeLsp {
398 async fn initialize(
399 &self,
400 params: InitializeParams,
401 ) -> tower_lsp::jsonrpc::Result<InitializeResult> {
402 {
404 let mut caps = self.client_capabilities.write().await;
405 *caps = Some(params.capabilities.clone());
406 }
407
408 if let Some(root_uri) = params
410 .root_uri
411 .as_ref()
412 .and_then(|uri| uri.to_file_path().ok())
413 {
414 let lint_cfg = config::load_lint_config(&root_uri);
415 self.client
416 .log_message(
417 MessageType::INFO,
418 format!(
419 "loaded foundry.toml lint config: lint_on_build={}, ignore_patterns={}",
420 lint_cfg.lint_on_build,
421 lint_cfg.ignore_patterns.len()
422 ),
423 )
424 .await;
425 let mut config = self.lint_config.write().await;
426 *config = lint_cfg;
427
428 let foundry_cfg = config::load_foundry_config(&root_uri);
429 self.client
430 .log_message(
431 MessageType::INFO,
432 format!(
433 "loaded foundry.toml project config: solc_version={:?}, remappings={}",
434 foundry_cfg.solc_version,
435 foundry_cfg.remappings.len()
436 ),
437 )
438 .await;
439 let mut fc = self.foundry_config.write().await;
440 *fc = foundry_cfg;
441 }
442
443 let client_encodings = params
445 .capabilities
446 .general
447 .as_ref()
448 .and_then(|g| g.position_encodings.as_deref());
449 let encoding = utils::PositionEncoding::negotiate(client_encodings);
450 utils::set_encoding(encoding);
451
452 Ok(InitializeResult {
453 server_info: Some(ServerInfo {
454 name: "Solidity Language Server".to_string(),
455 version: Some(env!("LONG_VERSION").to_string()),
456 }),
457 capabilities: ServerCapabilities {
458 position_encoding: Some(encoding.into()),
459 completion_provider: Some(CompletionOptions {
460 trigger_characters: Some(vec![".".to_string()]),
461 resolve_provider: Some(false),
462 ..Default::default()
463 }),
464 signature_help_provider: Some(SignatureHelpOptions {
465 trigger_characters: Some(vec![
466 "(".to_string(),
467 ",".to_string(),
468 "[".to_string(),
469 ]),
470 retrigger_characters: None,
471 work_done_progress_options: WorkDoneProgressOptions {
472 work_done_progress: None,
473 },
474 }),
475 definition_provider: Some(OneOf::Left(true)),
476 declaration_provider: Some(DeclarationCapability::Simple(true)),
477 references_provider: Some(OneOf::Left(true)),
478 rename_provider: Some(OneOf::Right(RenameOptions {
479 prepare_provider: Some(true),
480 work_done_progress_options: WorkDoneProgressOptions {
481 work_done_progress: Some(true),
482 },
483 })),
484 workspace_symbol_provider: Some(OneOf::Left(true)),
485 document_symbol_provider: Some(OneOf::Left(true)),
486 hover_provider: Some(HoverProviderCapability::Simple(true)),
487 document_link_provider: Some(DocumentLinkOptions {
488 resolve_provider: Some(false),
489 work_done_progress_options: WorkDoneProgressOptions {
490 work_done_progress: None,
491 },
492 }),
493 document_formatting_provider: Some(OneOf::Left(true)),
494 code_lens_provider: None,
495 inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options(
496 InlayHintOptions {
497 resolve_provider: Some(false),
498 work_done_progress_options: WorkDoneProgressOptions {
499 work_done_progress: None,
500 },
501 },
502 ))),
503 semantic_tokens_provider: Some(
504 SemanticTokensServerCapabilities::SemanticTokensOptions(
505 SemanticTokensOptions {
506 legend: semantic_tokens::legend(),
507 full: Some(SemanticTokensFullOptions::Bool(true)),
508 range: None,
509 work_done_progress_options: WorkDoneProgressOptions {
510 work_done_progress: None,
511 },
512 },
513 ),
514 ),
515 text_document_sync: Some(TextDocumentSyncCapability::Options(
516 TextDocumentSyncOptions {
517 will_save: Some(true),
518 will_save_wait_until: None,
519 open_close: Some(true),
520 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
521 include_text: Some(true),
522 })),
523 change: Some(TextDocumentSyncKind::FULL),
524 },
525 )),
526 ..ServerCapabilities::default()
527 },
528 })
529 }
530
531 async fn initialized(&self, _: InitializedParams) {
532 self.client
533 .log_message(MessageType::INFO, "lsp server initialized.")
534 .await;
535
536 let supports_dynamic = self
538 .client_capabilities
539 .read()
540 .await
541 .as_ref()
542 .and_then(|caps| caps.workspace.as_ref())
543 .and_then(|ws| ws.did_change_watched_files.as_ref())
544 .and_then(|dcwf| dcwf.dynamic_registration)
545 .unwrap_or(false);
546
547 if supports_dynamic {
548 let registration = Registration {
549 id: "foundry-toml-watcher".to_string(),
550 method: "workspace/didChangeWatchedFiles".to_string(),
551 register_options: Some(
552 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
553 watchers: vec![
554 FileSystemWatcher {
555 glob_pattern: GlobPattern::String("**/foundry.toml".to_string()),
556 kind: Some(WatchKind::all()),
557 },
558 FileSystemWatcher {
559 glob_pattern: GlobPattern::String("**/remappings.txt".to_string()),
560 kind: Some(WatchKind::all()),
561 },
562 ],
563 })
564 .unwrap(),
565 ),
566 };
567
568 if let Err(e) = self.client.register_capability(vec![registration]).await {
569 self.client
570 .log_message(
571 MessageType::WARNING,
572 format!("failed to register foundry.toml watcher: {e}"),
573 )
574 .await;
575 } else {
576 self.client
577 .log_message(MessageType::INFO, "registered foundry.toml file watcher")
578 .await;
579 }
580 }
581 }
582
583 async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
584 self.client
585 .log_message(MessageType::INFO, "lsp server shutting down.")
586 .await;
587 Ok(())
588 }
589
590 async fn did_open(&self, params: DidOpenTextDocumentParams) {
591 self.client
592 .log_message(MessageType::INFO, "file opened")
593 .await;
594
595 self.on_change(params.text_document).await
596 }
597
598 async fn did_change(&self, params: DidChangeTextDocumentParams) {
599 self.client
600 .log_message(MessageType::INFO, "file changed")
601 .await;
602
603 if let Some(change) = params.content_changes.into_iter().next() {
605 let mut text_cache = self.text_cache.write().await;
606 text_cache.insert(
607 params.text_document.uri.to_string(),
608 (params.text_document.version, change.text),
609 );
610 }
611 }
612
613 async fn did_save(&self, params: DidSaveTextDocumentParams) {
614 self.client
615 .log_message(MessageType::INFO, "file saved")
616 .await;
617
618 let text_content = if let Some(text) = params.text {
619 text
620 } else {
621 let cached = {
623 let text_cache = self.text_cache.read().await;
624 text_cache
625 .get(params.text_document.uri.as_str())
626 .map(|(_, content)| content.clone())
627 };
628 if let Some(content) = cached {
629 content
630 } else {
631 match std::fs::read_to_string(params.text_document.uri.path()) {
632 Ok(content) => content,
633 Err(e) => {
634 self.client
635 .log_message(
636 MessageType::ERROR,
637 format!("Failed to read file on save: {e}"),
638 )
639 .await;
640 return;
641 }
642 }
643 }
644 };
645
646 let version = self
647 .text_cache
648 .read()
649 .await
650 .get(params.text_document.uri.as_str())
651 .map(|(version, _)| *version)
652 .unwrap_or_default();
653
654 self.on_change(TextDocumentItem {
655 uri: params.text_document.uri,
656 text: text_content,
657 version,
658 language_id: "".to_string(),
659 })
660 .await;
661 }
662
663 async fn will_save(&self, params: WillSaveTextDocumentParams) {
664 self.client
665 .log_message(
666 MessageType::INFO,
667 format!(
668 "file will save reason:{:?} {}",
669 params.reason, params.text_document.uri
670 ),
671 )
672 .await;
673 }
674
675 async fn formatting(
676 &self,
677 params: DocumentFormattingParams,
678 ) -> tower_lsp::jsonrpc::Result<Option<Vec<TextEdit>>> {
679 self.client
680 .log_message(MessageType::INFO, "formatting request")
681 .await;
682
683 let uri = params.text_document.uri;
684 let file_path = match uri.to_file_path() {
685 Ok(path) => path,
686 Err(_) => {
687 self.client
688 .log_message(MessageType::ERROR, "Invalid file URI for formatting")
689 .await;
690 return Ok(None);
691 }
692 };
693 let path_str = match file_path.to_str() {
694 Some(s) => s,
695 None => {
696 self.client
697 .log_message(MessageType::ERROR, "Invalid file path for formatting")
698 .await;
699 return Ok(None);
700 }
701 };
702
703 let original_content = {
705 let text_cache = self.text_cache.read().await;
706 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
707 content.clone()
708 } else {
709 match std::fs::read_to_string(&file_path) {
711 Ok(content) => content,
712 Err(_) => {
713 self.client
714 .log_message(MessageType::ERROR, "Failed to read file for formatting")
715 .await;
716 return Ok(None);
717 }
718 }
719 }
720 };
721
722 let formatted_content = match self.compiler.format(path_str).await {
724 Ok(content) => content,
725 Err(e) => {
726 self.client
727 .log_message(MessageType::WARNING, format!("Formatting failed: {e}"))
728 .await;
729 return Ok(None);
730 }
731 };
732
733 if original_content != formatted_content {
735 let end = utils::byte_offset_to_position(&original_content, original_content.len());
736
737 {
739 let mut text_cache = self.text_cache.write().await;
740 let version = text_cache
741 .get(&uri.to_string())
742 .map(|(v, _)| *v)
743 .unwrap_or(0);
744 text_cache.insert(uri.to_string(), (version, formatted_content.clone()));
745 }
746
747 let edit = TextEdit {
748 range: Range {
749 start: Position::default(),
750 end,
751 },
752 new_text: formatted_content,
753 };
754 Ok(Some(vec![edit]))
755 } else {
756 Ok(None)
757 }
758 }
759
760 async fn did_close(&self, params: DidCloseTextDocumentParams) {
761 let uri = params.text_document.uri.to_string();
762 self.ast_cache.write().await.remove(&uri);
763 self.text_cache.write().await.remove(&uri);
764 self.completion_cache.write().await.remove(&uri);
765 self.client
766 .log_message(MessageType::INFO, "file closed, caches cleared.")
767 .await;
768 }
769
770 async fn did_change_configuration(&self, _: DidChangeConfigurationParams) {
771 self.client
772 .log_message(MessageType::INFO, "configuration changed.")
773 .await;
774 }
775 async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {
776 self.client
777 .log_message(MessageType::INFO, "workdspace folders changed.")
778 .await;
779 }
780
781 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
782 self.client
783 .log_message(MessageType::INFO, "watched files have changed.")
784 .await;
785
786 for change in ¶ms.changes {
788 let path = match change.uri.to_file_path() {
789 Ok(p) => p,
790 Err(_) => continue,
791 };
792
793 let filename = path.file_name().and_then(|n| n.to_str());
794
795 if filename == Some("foundry.toml") {
796 let lint_cfg = config::load_lint_config_from_toml(&path);
797 self.client
798 .log_message(
799 MessageType::INFO,
800 format!(
801 "reloaded foundry.toml lint config: lint_on_build={}, ignore_patterns={}",
802 lint_cfg.lint_on_build,
803 lint_cfg.ignore_patterns.len()
804 ),
805 )
806 .await;
807 let mut lc = self.lint_config.write().await;
808 *lc = lint_cfg;
809
810 let foundry_cfg = config::load_foundry_config_from_toml(&path);
811 self.client
812 .log_message(
813 MessageType::INFO,
814 format!(
815 "reloaded foundry.toml project config: solc_version={:?}, remappings={}",
816 foundry_cfg.solc_version,
817 foundry_cfg.remappings.len()
818 ),
819 )
820 .await;
821 let mut fc = self.foundry_config.write().await;
822 *fc = foundry_cfg;
823 break;
824 }
825
826 if filename == Some("remappings.txt") {
827 self.client
828 .log_message(
829 MessageType::INFO,
830 "remappings.txt changed, config may need refresh",
831 )
832 .await;
833 }
836 }
837 }
838
839 async fn completion(
840 &self,
841 params: CompletionParams,
842 ) -> tower_lsp::jsonrpc::Result<Option<CompletionResponse>> {
843 let uri = params.text_document_position.text_document.uri;
844 let position = params.text_document_position.position;
845
846 let trigger_char = params
847 .context
848 .as_ref()
849 .and_then(|ctx| ctx.trigger_character.as_deref());
850
851 let source_text = {
853 let text_cache = self.text_cache.read().await;
854 if let Some((_, text)) = text_cache.get(&uri.to_string()) {
855 text.clone()
856 } else {
857 match uri.to_file_path() {
858 Ok(path) => std::fs::read_to_string(&path).unwrap_or_default(),
859 Err(_) => return Ok(None),
860 }
861 }
862 };
863
864 let cached: Option<Arc<completion::CompletionCache>> = {
866 let comp_cache = self.completion_cache.read().await;
867 comp_cache.get(&uri.to_string()).cloned()
868 };
869
870 if cached.is_none() {
871 let ast_cache = self.ast_cache.clone();
873 let completion_cache = self.completion_cache.clone();
874 let uri_string = uri.to_string();
875 tokio::spawn(async move {
876 let cached_build = {
877 let cache = ast_cache.read().await;
878 match cache.get(&uri_string) {
879 Some(v) => v.clone(),
880 None => return,
881 }
882 };
883 if let Some(sources) = cached_build.ast.get("sources") {
884 let contracts = cached_build.ast.get("contracts");
885 let cc = completion::build_completion_cache(sources, contracts);
886 completion_cache
887 .write()
888 .await
889 .insert(uri_string, Arc::new(cc));
890 }
891 });
892 }
893
894 let cache_ref = cached.as_deref();
895
896 let file_id = {
898 let uri_path = uri.to_file_path().ok();
899 cache_ref.and_then(|c| {
900 uri_path.as_ref().and_then(|p| {
901 let path_str = p.to_str()?;
902 c.path_to_file_id.get(path_str).copied()
903 })
904 })
905 };
906
907 let result =
908 completion::handle_completion(cache_ref, &source_text, position, trigger_char, file_id);
909 Ok(result)
910 }
911
912 async fn goto_definition(
913 &self,
914 params: GotoDefinitionParams,
915 ) -> tower_lsp::jsonrpc::Result<Option<GotoDefinitionResponse>> {
916 self.client
917 .log_message(MessageType::INFO, "got textDocument/definition request")
918 .await;
919
920 let uri = params.text_document_position_params.text_document.uri;
921 let position = params.text_document_position_params.position;
922
923 let file_path = match uri.to_file_path() {
924 Ok(path) => path,
925 Err(_) => {
926 self.client
927 .log_message(MessageType::ERROR, "Invalid file uri")
928 .await;
929 return Ok(None);
930 }
931 };
932
933 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
934 Some(bytes) => bytes,
935 None => return Ok(None),
936 };
937
938 let source_text = String::from_utf8_lossy(&source_bytes).to_string();
939
940 let cursor_name = goto::cursor_context(&source_text, position).map(|ctx| ctx.name);
942
943 let (is_dirty, cached_build) = {
947 let text_version = self
948 .text_cache
949 .read()
950 .await
951 .get(&uri.to_string())
952 .map(|(v, _)| *v)
953 .unwrap_or(0);
954 let cb = self.get_or_fetch_build(&uri, &file_path, false).await;
955 let build_version = cb.as_ref().map(|b| b.build_version).unwrap_or(0);
956 (text_version > build_version, cb)
957 };
958
959 let validate_ts = |loc: &Location| -> bool {
965 let Some(ref name) = cursor_name else {
966 return true; };
968 let target_src = if loc.uri == uri {
969 Some(source_text.clone())
970 } else {
971 loc.uri
972 .to_file_path()
973 .ok()
974 .and_then(|p| std::fs::read_to_string(&p).ok())
975 };
976 match target_src {
977 Some(src) => goto::validate_goto_target(&src, loc, name),
978 None => true, }
980 };
981
982 if is_dirty {
983 self.client
984 .log_message(MessageType::INFO, "file is dirty, trying tree-sitter first")
985 .await;
986
987 let ts_result = {
989 let comp_cache = self.completion_cache.read().await;
990 let text_cache = self.text_cache.read().await;
991 if let Some(cc) = comp_cache.get(&uri.to_string()) {
992 goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
993 } else {
994 None
995 }
996 };
997
998 if let Some(location) = ts_result {
999 if validate_ts(&location) {
1000 self.client
1001 .log_message(
1002 MessageType::INFO,
1003 format!(
1004 "found definition (tree-sitter) at {}:{}",
1005 location.uri, location.range.start.line
1006 ),
1007 )
1008 .await;
1009 return Ok(Some(GotoDefinitionResponse::from(location)));
1010 }
1011 self.client
1012 .log_message(
1013 MessageType::INFO,
1014 "tree-sitter result failed validation, trying AST fallback",
1015 )
1016 .await;
1017 }
1018
1019 if let Some(ref cb) = cached_build
1024 && let Some(ref name) = cursor_name
1025 {
1026 let byte_hint = goto::pos_to_bytes(&source_bytes, position);
1027 if let Some(location) = goto::goto_declaration_by_name(cb, &uri, name, byte_hint) {
1028 self.client
1029 .log_message(
1030 MessageType::INFO,
1031 format!(
1032 "found definition (AST by name) at {}:{}",
1033 location.uri, location.range.start.line
1034 ),
1035 )
1036 .await;
1037 return Ok(Some(GotoDefinitionResponse::from(location)));
1038 }
1039 }
1040 } else {
1041 if let Some(ref cb) = cached_build
1043 && let Some(location) =
1044 goto::goto_declaration(&cb.ast, &uri, position, &source_bytes)
1045 {
1046 self.client
1047 .log_message(
1048 MessageType::INFO,
1049 format!(
1050 "found definition (AST) at {}:{}",
1051 location.uri, location.range.start.line
1052 ),
1053 )
1054 .await;
1055 return Ok(Some(GotoDefinitionResponse::from(location)));
1056 }
1057
1058 let ts_result = {
1060 let comp_cache = self.completion_cache.read().await;
1061 let text_cache = self.text_cache.read().await;
1062 if let Some(cc) = comp_cache.get(&uri.to_string()) {
1063 goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
1064 } else {
1065 None
1066 }
1067 };
1068
1069 if let Some(location) = ts_result {
1070 if validate_ts(&location) {
1071 self.client
1072 .log_message(
1073 MessageType::INFO,
1074 format!(
1075 "found definition (tree-sitter fallback) at {}:{}",
1076 location.uri, location.range.start.line
1077 ),
1078 )
1079 .await;
1080 return Ok(Some(GotoDefinitionResponse::from(location)));
1081 }
1082 self.client
1083 .log_message(MessageType::INFO, "tree-sitter fallback failed validation")
1084 .await;
1085 }
1086 }
1087
1088 self.client
1089 .log_message(MessageType::INFO, "no definition found")
1090 .await;
1091 Ok(None)
1092 }
1093
1094 async fn goto_declaration(
1095 &self,
1096 params: request::GotoDeclarationParams,
1097 ) -> tower_lsp::jsonrpc::Result<Option<request::GotoDeclarationResponse>> {
1098 self.client
1099 .log_message(MessageType::INFO, "got textDocument/declaration request")
1100 .await;
1101
1102 let uri = params.text_document_position_params.text_document.uri;
1103 let position = params.text_document_position_params.position;
1104
1105 let file_path = match uri.to_file_path() {
1106 Ok(path) => path,
1107 Err(_) => {
1108 self.client
1109 .log_message(MessageType::ERROR, "invalid file uri")
1110 .await;
1111 return Ok(None);
1112 }
1113 };
1114
1115 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1116 Some(bytes) => bytes,
1117 None => return Ok(None),
1118 };
1119
1120 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1121 let cached_build = match cached_build {
1122 Some(cb) => cb,
1123 None => return Ok(None),
1124 };
1125
1126 if let Some(location) =
1127 goto::goto_declaration(&cached_build.ast, &uri, position, &source_bytes)
1128 {
1129 self.client
1130 .log_message(
1131 MessageType::INFO,
1132 format!(
1133 "found declaration at {}:{}",
1134 location.uri, location.range.start.line
1135 ),
1136 )
1137 .await;
1138 Ok(Some(request::GotoDeclarationResponse::from(location)))
1139 } else {
1140 self.client
1141 .log_message(MessageType::INFO, "no declaration found")
1142 .await;
1143 Ok(None)
1144 }
1145 }
1146
1147 async fn references(
1148 &self,
1149 params: ReferenceParams,
1150 ) -> tower_lsp::jsonrpc::Result<Option<Vec<Location>>> {
1151 self.client
1152 .log_message(MessageType::INFO, "Got a textDocument/references request")
1153 .await;
1154
1155 let uri = params.text_document_position.text_document.uri;
1156 let position = params.text_document_position.position;
1157 let file_path = match uri.to_file_path() {
1158 Ok(path) => path,
1159 Err(_) => {
1160 self.client
1161 .log_message(MessageType::ERROR, "Invalid file URI")
1162 .await;
1163 return Ok(None);
1164 }
1165 };
1166 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1167 Some(bytes) => bytes,
1168 None => return Ok(None),
1169 };
1170 let cached_build = self.get_or_fetch_build(&uri, &file_path, true).await;
1171 let cached_build = match cached_build {
1172 Some(cb) => cb,
1173 None => return Ok(None),
1174 };
1175
1176 let mut locations = references::goto_references(
1178 &cached_build.ast,
1179 &uri,
1180 position,
1181 &source_bytes,
1182 params.context.include_declaration,
1183 );
1184
1185 if let Some((def_abs_path, def_byte_offset)) =
1187 references::resolve_target_location(&cached_build, &uri, position, &source_bytes)
1188 {
1189 let cache = self.ast_cache.read().await;
1190 for (cached_uri, other_build) in cache.iter() {
1191 if *cached_uri == uri.to_string() {
1192 continue;
1193 }
1194 let other_locations = references::goto_references_for_target(
1195 other_build,
1196 &def_abs_path,
1197 def_byte_offset,
1198 None,
1199 params.context.include_declaration,
1200 );
1201 locations.extend(other_locations);
1202 }
1203 }
1204
1205 let mut seen = std::collections::HashSet::new();
1207 locations.retain(|loc| {
1208 seen.insert((
1209 loc.uri.clone(),
1210 loc.range.start.line,
1211 loc.range.start.character,
1212 loc.range.end.line,
1213 loc.range.end.character,
1214 ))
1215 });
1216
1217 if locations.is_empty() {
1218 self.client
1219 .log_message(MessageType::INFO, "No references found")
1220 .await;
1221 Ok(None)
1222 } else {
1223 self.client
1224 .log_message(
1225 MessageType::INFO,
1226 format!("Found {} references", locations.len()),
1227 )
1228 .await;
1229 Ok(Some(locations))
1230 }
1231 }
1232
1233 async fn prepare_rename(
1234 &self,
1235 params: TextDocumentPositionParams,
1236 ) -> tower_lsp::jsonrpc::Result<Option<PrepareRenameResponse>> {
1237 self.client
1238 .log_message(MessageType::INFO, "got textDocument/prepareRename request")
1239 .await;
1240
1241 let uri = params.text_document.uri;
1242 let position = params.position;
1243
1244 let file_path = match uri.to_file_path() {
1245 Ok(path) => path,
1246 Err(_) => {
1247 self.client
1248 .log_message(MessageType::ERROR, "invalid file uri")
1249 .await;
1250 return Ok(None);
1251 }
1252 };
1253
1254 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1255 Some(bytes) => bytes,
1256 None => return Ok(None),
1257 };
1258
1259 if let Some(range) = rename::get_identifier_range(&source_bytes, position) {
1260 self.client
1261 .log_message(
1262 MessageType::INFO,
1263 format!(
1264 "prepare rename range: {}:{}",
1265 range.start.line, range.start.character
1266 ),
1267 )
1268 .await;
1269 Ok(Some(PrepareRenameResponse::Range(range)))
1270 } else {
1271 self.client
1272 .log_message(MessageType::INFO, "no identifier found for prepare rename")
1273 .await;
1274 Ok(None)
1275 }
1276 }
1277
1278 async fn rename(
1279 &self,
1280 params: RenameParams,
1281 ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
1282 self.client
1283 .log_message(MessageType::INFO, "got textDocument/rename request")
1284 .await;
1285
1286 let uri = params.text_document_position.text_document.uri;
1287 let position = params.text_document_position.position;
1288 let new_name = params.new_name;
1289 let file_path = match uri.to_file_path() {
1290 Ok(p) => p,
1291 Err(_) => {
1292 self.client
1293 .log_message(MessageType::ERROR, "invalid file uri")
1294 .await;
1295 return Ok(None);
1296 }
1297 };
1298 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1299 Some(bytes) => bytes,
1300 None => return Ok(None),
1301 };
1302
1303 let current_identifier = match rename::get_identifier_at_position(&source_bytes, position) {
1304 Some(id) => id,
1305 None => {
1306 self.client
1307 .log_message(MessageType::ERROR, "No identifier found at position")
1308 .await;
1309 return Ok(None);
1310 }
1311 };
1312
1313 if !utils::is_valid_solidity_identifier(&new_name) {
1314 return Err(tower_lsp::jsonrpc::Error::invalid_params(
1315 "new name is not a valid solidity identifier",
1316 ));
1317 }
1318
1319 if new_name == current_identifier {
1320 self.client
1321 .log_message(
1322 MessageType::INFO,
1323 "new name is the same as current identifier",
1324 )
1325 .await;
1326 return Ok(None);
1327 }
1328
1329 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1330 let cached_build = match cached_build {
1331 Some(cb) => cb,
1332 None => return Ok(None),
1333 };
1334 let other_builds: Vec<Arc<goto::CachedBuild>> = {
1335 let cache = self.ast_cache.read().await;
1336 cache
1337 .iter()
1338 .filter(|(key, _)| **key != uri.to_string())
1339 .map(|(_, v)| v.clone())
1340 .collect()
1341 };
1342 let other_refs: Vec<&goto::CachedBuild> = other_builds.iter().map(|v| v.as_ref()).collect();
1343
1344 let text_buffers: HashMap<String, Vec<u8>> = {
1348 let text_cache = self.text_cache.read().await;
1349 text_cache
1350 .iter()
1351 .map(|(uri, (_, content))| (uri.clone(), content.as_bytes().to_vec()))
1352 .collect()
1353 };
1354
1355 match rename::rename_symbol(
1356 &cached_build,
1357 &uri,
1358 position,
1359 &source_bytes,
1360 new_name,
1361 &other_refs,
1362 &text_buffers,
1363 ) {
1364 Some(workspace_edit) => {
1365 self.client
1366 .log_message(
1367 MessageType::INFO,
1368 format!(
1369 "created rename edit with {} file(s), {} total change(s)",
1370 workspace_edit
1371 .changes
1372 .as_ref()
1373 .map(|c| c.len())
1374 .unwrap_or(0),
1375 workspace_edit
1376 .changes
1377 .as_ref()
1378 .map(|c| c.values().map(|v| v.len()).sum::<usize>())
1379 .unwrap_or(0)
1380 ),
1381 )
1382 .await;
1383
1384 Ok(Some(workspace_edit))
1389 }
1390
1391 None => {
1392 self.client
1393 .log_message(MessageType::INFO, "No locations found for renaming")
1394 .await;
1395 Ok(None)
1396 }
1397 }
1398 }
1399
1400 async fn symbol(
1401 &self,
1402 params: WorkspaceSymbolParams,
1403 ) -> tower_lsp::jsonrpc::Result<Option<Vec<SymbolInformation>>> {
1404 self.client
1405 .log_message(MessageType::INFO, "got workspace/symbol request")
1406 .await;
1407
1408 let files: Vec<(Url, String)> = {
1410 let cache = self.text_cache.read().await;
1411 cache
1412 .iter()
1413 .filter(|(uri_str, _)| uri_str.ends_with(".sol"))
1414 .filter_map(|(uri_str, (_, content))| {
1415 Url::parse(uri_str).ok().map(|uri| (uri, content.clone()))
1416 })
1417 .collect()
1418 };
1419
1420 let mut all_symbols = symbols::extract_workspace_symbols(&files);
1421 if !params.query.is_empty() {
1422 let query = params.query.to_lowercase();
1423 all_symbols.retain(|symbol| symbol.name.to_lowercase().contains(&query));
1424 }
1425 if all_symbols.is_empty() {
1426 self.client
1427 .log_message(MessageType::INFO, "No symbols found")
1428 .await;
1429 Ok(None)
1430 } else {
1431 self.client
1432 .log_message(
1433 MessageType::INFO,
1434 format!("found {} symbols", all_symbols.len()),
1435 )
1436 .await;
1437 Ok(Some(all_symbols))
1438 }
1439 }
1440
1441 async fn document_symbol(
1442 &self,
1443 params: DocumentSymbolParams,
1444 ) -> tower_lsp::jsonrpc::Result<Option<DocumentSymbolResponse>> {
1445 self.client
1446 .log_message(MessageType::INFO, "got textDocument/documentSymbol request")
1447 .await;
1448 let uri = params.text_document.uri;
1449 let file_path = match uri.to_file_path() {
1450 Ok(path) => path,
1451 Err(_) => {
1452 self.client
1453 .log_message(MessageType::ERROR, "invalid file uri")
1454 .await;
1455 return Ok(None);
1456 }
1457 };
1458
1459 let source = {
1461 let cache = self.text_cache.read().await;
1462 cache
1463 .get(&uri.to_string())
1464 .map(|(_, content)| content.clone())
1465 };
1466 let source = match source {
1467 Some(s) => s,
1468 None => match std::fs::read_to_string(&file_path) {
1469 Ok(s) => s,
1470 Err(_) => return Ok(None),
1471 },
1472 };
1473
1474 let symbols = symbols::extract_document_symbols(&source);
1475 if symbols.is_empty() {
1476 self.client
1477 .log_message(MessageType::INFO, "no document symbols found")
1478 .await;
1479 Ok(None)
1480 } else {
1481 self.client
1482 .log_message(
1483 MessageType::INFO,
1484 format!("found {} document symbols", symbols.len()),
1485 )
1486 .await;
1487 Ok(Some(DocumentSymbolResponse::Nested(symbols)))
1488 }
1489 }
1490
1491 async fn hover(&self, params: HoverParams) -> tower_lsp::jsonrpc::Result<Option<Hover>> {
1492 self.client
1493 .log_message(MessageType::INFO, "got textDocument/hover request")
1494 .await;
1495
1496 let uri = params.text_document_position_params.text_document.uri;
1497 let position = params.text_document_position_params.position;
1498
1499 let file_path = match uri.to_file_path() {
1500 Ok(path) => path,
1501 Err(_) => {
1502 self.client
1503 .log_message(MessageType::ERROR, "invalid file uri")
1504 .await;
1505 return Ok(None);
1506 }
1507 };
1508
1509 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1510 Some(bytes) => bytes,
1511 None => return Ok(None),
1512 };
1513
1514 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1515 let cached_build = match cached_build {
1516 Some(cb) => cb,
1517 None => return Ok(None),
1518 };
1519
1520 let result = hover::hover_info(
1521 &cached_build.ast,
1522 &uri,
1523 position,
1524 &source_bytes,
1525 &cached_build.gas_index,
1526 &cached_build.doc_index,
1527 &cached_build.hint_index,
1528 );
1529
1530 if result.is_some() {
1531 self.client
1532 .log_message(MessageType::INFO, "hover info found")
1533 .await;
1534 } else {
1535 self.client
1536 .log_message(MessageType::INFO, "no hover info found")
1537 .await;
1538 }
1539
1540 Ok(result)
1541 }
1542
1543 async fn signature_help(
1544 &self,
1545 params: SignatureHelpParams,
1546 ) -> tower_lsp::jsonrpc::Result<Option<SignatureHelp>> {
1547 self.client
1548 .log_message(MessageType::INFO, "got textDocument/signatureHelp request")
1549 .await;
1550
1551 let uri = params.text_document_position_params.text_document.uri;
1552 let position = params.text_document_position_params.position;
1553
1554 let file_path = match uri.to_file_path() {
1555 Ok(path) => path,
1556 Err(_) => {
1557 self.client
1558 .log_message(MessageType::ERROR, "invalid file uri")
1559 .await;
1560 return Ok(None);
1561 }
1562 };
1563
1564 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1565 Some(bytes) => bytes,
1566 None => return Ok(None),
1567 };
1568
1569 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1570 let cached_build = match cached_build {
1571 Some(cb) => cb,
1572 None => return Ok(None),
1573 };
1574
1575 let result = hover::signature_help(
1576 &cached_build.ast,
1577 &source_bytes,
1578 position,
1579 &cached_build.hint_index,
1580 &cached_build.doc_index,
1581 );
1582
1583 Ok(result)
1584 }
1585
1586 async fn document_link(
1587 &self,
1588 params: DocumentLinkParams,
1589 ) -> tower_lsp::jsonrpc::Result<Option<Vec<DocumentLink>>> {
1590 self.client
1591 .log_message(MessageType::INFO, "got textDocument/documentLink request")
1592 .await;
1593
1594 let uri = params.text_document.uri;
1595 let file_path = match uri.to_file_path() {
1596 Ok(path) => path,
1597 Err(_) => {
1598 self.client
1599 .log_message(MessageType::ERROR, "invalid file uri")
1600 .await;
1601 return Ok(None);
1602 }
1603 };
1604
1605 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1606 Some(bytes) => bytes,
1607 None => return Ok(None),
1608 };
1609
1610 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1611 let cached_build = match cached_build {
1612 Some(cb) => cb,
1613 None => return Ok(None),
1614 };
1615
1616 let result = links::document_links(&cached_build, &uri, &source_bytes);
1617
1618 if result.is_empty() {
1619 self.client
1620 .log_message(MessageType::INFO, "no document links found")
1621 .await;
1622 Ok(None)
1623 } else {
1624 self.client
1625 .log_message(
1626 MessageType::INFO,
1627 format!("found {} document links", result.len()),
1628 )
1629 .await;
1630 Ok(Some(result))
1631 }
1632 }
1633
1634 async fn semantic_tokens_full(
1635 &self,
1636 params: SemanticTokensParams,
1637 ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensResult>> {
1638 self.client
1639 .log_message(
1640 MessageType::INFO,
1641 "got textDocument/semanticTokens/full request",
1642 )
1643 .await;
1644
1645 let uri = params.text_document.uri;
1646 let source = {
1647 let cache = self.text_cache.read().await;
1648 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
1649 };
1650
1651 let source = match source {
1652 Some(s) => s,
1653 None => {
1654 let file_path = match uri.to_file_path() {
1656 Ok(p) => p,
1657 Err(_) => return Ok(None),
1658 };
1659 match std::fs::read_to_string(&file_path) {
1660 Ok(s) => s,
1661 Err(_) => return Ok(None),
1662 }
1663 }
1664 };
1665
1666 let tokens = semantic_tokens::semantic_tokens_full(&source);
1667
1668 Ok(Some(SemanticTokensResult::Tokens(tokens)))
1669 }
1670
1671 async fn inlay_hint(
1672 &self,
1673 params: InlayHintParams,
1674 ) -> tower_lsp::jsonrpc::Result<Option<Vec<InlayHint>>> {
1675 self.client
1676 .log_message(MessageType::INFO, "got textDocument/inlayHint request")
1677 .await;
1678
1679 let uri = params.text_document.uri;
1680 let range = params.range;
1681
1682 let file_path = match uri.to_file_path() {
1683 Ok(path) => path,
1684 Err(_) => {
1685 self.client
1686 .log_message(MessageType::ERROR, "invalid file uri")
1687 .await;
1688 return Ok(None);
1689 }
1690 };
1691
1692 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1693 Some(bytes) => bytes,
1694 None => return Ok(None),
1695 };
1696
1697 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1698 let cached_build = match cached_build {
1699 Some(cb) => cb,
1700 None => return Ok(None),
1701 };
1702
1703 let hints = inlay_hints::inlay_hints(&cached_build, &uri, range, &source_bytes);
1704
1705 if hints.is_empty() {
1706 self.client
1707 .log_message(MessageType::INFO, "no inlay hints found")
1708 .await;
1709 Ok(None)
1710 } else {
1711 self.client
1712 .log_message(
1713 MessageType::INFO,
1714 format!("found {} inlay hints", hints.len()),
1715 )
1716 .await;
1717 Ok(Some(hints))
1718 }
1719 }
1720}