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 definition_provider: Some(OneOf::Left(true)),
465 declaration_provider: Some(DeclarationCapability::Simple(true)),
466 references_provider: Some(OneOf::Left(true)),
467 rename_provider: Some(OneOf::Right(RenameOptions {
468 prepare_provider: Some(true),
469 work_done_progress_options: WorkDoneProgressOptions {
470 work_done_progress: Some(true),
471 },
472 })),
473 workspace_symbol_provider: Some(OneOf::Left(true)),
474 document_symbol_provider: Some(OneOf::Left(true)),
475 hover_provider: Some(HoverProviderCapability::Simple(true)),
476 document_link_provider: Some(DocumentLinkOptions {
477 resolve_provider: Some(false),
478 work_done_progress_options: WorkDoneProgressOptions {
479 work_done_progress: None,
480 },
481 }),
482 document_formatting_provider: Some(OneOf::Left(true)),
483 code_lens_provider: None,
484 inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options(
485 InlayHintOptions {
486 resolve_provider: Some(false),
487 work_done_progress_options: WorkDoneProgressOptions {
488 work_done_progress: None,
489 },
490 },
491 ))),
492 semantic_tokens_provider: Some(
493 SemanticTokensServerCapabilities::SemanticTokensOptions(
494 SemanticTokensOptions {
495 legend: semantic_tokens::legend(),
496 full: Some(SemanticTokensFullOptions::Bool(true)),
497 range: None,
498 work_done_progress_options: WorkDoneProgressOptions {
499 work_done_progress: None,
500 },
501 },
502 ),
503 ),
504 text_document_sync: Some(TextDocumentSyncCapability::Options(
505 TextDocumentSyncOptions {
506 will_save: Some(true),
507 will_save_wait_until: None,
508 open_close: Some(true),
509 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
510 include_text: Some(true),
511 })),
512 change: Some(TextDocumentSyncKind::FULL),
513 },
514 )),
515 ..ServerCapabilities::default()
516 },
517 })
518 }
519
520 async fn initialized(&self, _: InitializedParams) {
521 self.client
522 .log_message(MessageType::INFO, "lsp server initialized.")
523 .await;
524
525 let supports_dynamic = self
527 .client_capabilities
528 .read()
529 .await
530 .as_ref()
531 .and_then(|caps| caps.workspace.as_ref())
532 .and_then(|ws| ws.did_change_watched_files.as_ref())
533 .and_then(|dcwf| dcwf.dynamic_registration)
534 .unwrap_or(false);
535
536 if supports_dynamic {
537 let registration = Registration {
538 id: "foundry-toml-watcher".to_string(),
539 method: "workspace/didChangeWatchedFiles".to_string(),
540 register_options: Some(
541 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
542 watchers: vec![
543 FileSystemWatcher {
544 glob_pattern: GlobPattern::String("**/foundry.toml".to_string()),
545 kind: Some(WatchKind::all()),
546 },
547 FileSystemWatcher {
548 glob_pattern: GlobPattern::String("**/remappings.txt".to_string()),
549 kind: Some(WatchKind::all()),
550 },
551 ],
552 })
553 .unwrap(),
554 ),
555 };
556
557 if let Err(e) = self.client.register_capability(vec![registration]).await {
558 self.client
559 .log_message(
560 MessageType::WARNING,
561 format!("failed to register foundry.toml watcher: {e}"),
562 )
563 .await;
564 } else {
565 self.client
566 .log_message(MessageType::INFO, "registered foundry.toml file watcher")
567 .await;
568 }
569 }
570 }
571
572 async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
573 self.client
574 .log_message(MessageType::INFO, "lsp server shutting down.")
575 .await;
576 Ok(())
577 }
578
579 async fn did_open(&self, params: DidOpenTextDocumentParams) {
580 self.client
581 .log_message(MessageType::INFO, "file opened")
582 .await;
583
584 self.on_change(params.text_document).await
585 }
586
587 async fn did_change(&self, params: DidChangeTextDocumentParams) {
588 self.client
589 .log_message(MessageType::INFO, "file changed")
590 .await;
591
592 if let Some(change) = params.content_changes.into_iter().next() {
594 let mut text_cache = self.text_cache.write().await;
595 text_cache.insert(
596 params.text_document.uri.to_string(),
597 (params.text_document.version, change.text),
598 );
599 }
600 }
601
602 async fn did_save(&self, params: DidSaveTextDocumentParams) {
603 self.client
604 .log_message(MessageType::INFO, "file saved")
605 .await;
606
607 let text_content = if let Some(text) = params.text {
608 text
609 } else {
610 let cached = {
612 let text_cache = self.text_cache.read().await;
613 text_cache
614 .get(params.text_document.uri.as_str())
615 .map(|(_, content)| content.clone())
616 };
617 if let Some(content) = cached {
618 content
619 } else {
620 match std::fs::read_to_string(params.text_document.uri.path()) {
621 Ok(content) => content,
622 Err(e) => {
623 self.client
624 .log_message(
625 MessageType::ERROR,
626 format!("Failed to read file on save: {e}"),
627 )
628 .await;
629 return;
630 }
631 }
632 }
633 };
634
635 let version = self
636 .text_cache
637 .read()
638 .await
639 .get(params.text_document.uri.as_str())
640 .map(|(version, _)| *version)
641 .unwrap_or_default();
642
643 self.on_change(TextDocumentItem {
644 uri: params.text_document.uri,
645 text: text_content,
646 version,
647 language_id: "".to_string(),
648 })
649 .await;
650 }
651
652 async fn will_save(&self, params: WillSaveTextDocumentParams) {
653 self.client
654 .log_message(
655 MessageType::INFO,
656 format!(
657 "file will save reason:{:?} {}",
658 params.reason, params.text_document.uri
659 ),
660 )
661 .await;
662 }
663
664 async fn formatting(
665 &self,
666 params: DocumentFormattingParams,
667 ) -> tower_lsp::jsonrpc::Result<Option<Vec<TextEdit>>> {
668 self.client
669 .log_message(MessageType::INFO, "formatting request")
670 .await;
671
672 let uri = params.text_document.uri;
673 let file_path = match uri.to_file_path() {
674 Ok(path) => path,
675 Err(_) => {
676 self.client
677 .log_message(MessageType::ERROR, "Invalid file URI for formatting")
678 .await;
679 return Ok(None);
680 }
681 };
682 let path_str = match file_path.to_str() {
683 Some(s) => s,
684 None => {
685 self.client
686 .log_message(MessageType::ERROR, "Invalid file path for formatting")
687 .await;
688 return Ok(None);
689 }
690 };
691
692 let original_content = {
694 let text_cache = self.text_cache.read().await;
695 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
696 content.clone()
697 } else {
698 match std::fs::read_to_string(&file_path) {
700 Ok(content) => content,
701 Err(_) => {
702 self.client
703 .log_message(MessageType::ERROR, "Failed to read file for formatting")
704 .await;
705 return Ok(None);
706 }
707 }
708 }
709 };
710
711 let formatted_content = match self.compiler.format(path_str).await {
713 Ok(content) => content,
714 Err(e) => {
715 self.client
716 .log_message(MessageType::WARNING, format!("Formatting failed: {e}"))
717 .await;
718 return Ok(None);
719 }
720 };
721
722 if original_content != formatted_content {
724 let end = utils::byte_offset_to_position(&original_content, original_content.len());
725
726 {
728 let mut text_cache = self.text_cache.write().await;
729 let version = text_cache
730 .get(&uri.to_string())
731 .map(|(v, _)| *v)
732 .unwrap_or(0);
733 text_cache.insert(uri.to_string(), (version, formatted_content.clone()));
734 }
735
736 let edit = TextEdit {
737 range: Range {
738 start: Position::default(),
739 end,
740 },
741 new_text: formatted_content,
742 };
743 Ok(Some(vec![edit]))
744 } else {
745 Ok(None)
746 }
747 }
748
749 async fn did_close(&self, params: DidCloseTextDocumentParams) {
750 let uri = params.text_document.uri.to_string();
751 self.ast_cache.write().await.remove(&uri);
752 self.text_cache.write().await.remove(&uri);
753 self.completion_cache.write().await.remove(&uri);
754 self.client
755 .log_message(MessageType::INFO, "file closed, caches cleared.")
756 .await;
757 }
758
759 async fn did_change_configuration(&self, _: DidChangeConfigurationParams) {
760 self.client
761 .log_message(MessageType::INFO, "configuration changed.")
762 .await;
763 }
764 async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {
765 self.client
766 .log_message(MessageType::INFO, "workdspace folders changed.")
767 .await;
768 }
769
770 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
771 self.client
772 .log_message(MessageType::INFO, "watched files have changed.")
773 .await;
774
775 for change in ¶ms.changes {
777 let path = match change.uri.to_file_path() {
778 Ok(p) => p,
779 Err(_) => continue,
780 };
781
782 let filename = path.file_name().and_then(|n| n.to_str());
783
784 if filename == Some("foundry.toml") {
785 let lint_cfg = config::load_lint_config_from_toml(&path);
786 self.client
787 .log_message(
788 MessageType::INFO,
789 format!(
790 "reloaded foundry.toml lint config: lint_on_build={}, ignore_patterns={}",
791 lint_cfg.lint_on_build,
792 lint_cfg.ignore_patterns.len()
793 ),
794 )
795 .await;
796 let mut lc = self.lint_config.write().await;
797 *lc = lint_cfg;
798
799 let foundry_cfg = config::load_foundry_config_from_toml(&path);
800 self.client
801 .log_message(
802 MessageType::INFO,
803 format!(
804 "reloaded foundry.toml project config: solc_version={:?}, remappings={}",
805 foundry_cfg.solc_version,
806 foundry_cfg.remappings.len()
807 ),
808 )
809 .await;
810 let mut fc = self.foundry_config.write().await;
811 *fc = foundry_cfg;
812 break;
813 }
814
815 if filename == Some("remappings.txt") {
816 self.client
817 .log_message(
818 MessageType::INFO,
819 "remappings.txt changed, config may need refresh",
820 )
821 .await;
822 }
825 }
826 }
827
828 async fn completion(
829 &self,
830 params: CompletionParams,
831 ) -> tower_lsp::jsonrpc::Result<Option<CompletionResponse>> {
832 let uri = params.text_document_position.text_document.uri;
833 let position = params.text_document_position.position;
834
835 let trigger_char = params
836 .context
837 .as_ref()
838 .and_then(|ctx| ctx.trigger_character.as_deref());
839
840 let source_text = {
842 let text_cache = self.text_cache.read().await;
843 if let Some((_, text)) = text_cache.get(&uri.to_string()) {
844 text.clone()
845 } else {
846 match uri.to_file_path() {
847 Ok(path) => std::fs::read_to_string(&path).unwrap_or_default(),
848 Err(_) => return Ok(None),
849 }
850 }
851 };
852
853 let cached: Option<Arc<completion::CompletionCache>> = {
855 let comp_cache = self.completion_cache.read().await;
856 comp_cache.get(&uri.to_string()).cloned()
857 };
858
859 if cached.is_none() {
860 let ast_cache = self.ast_cache.clone();
862 let completion_cache = self.completion_cache.clone();
863 let uri_string = uri.to_string();
864 tokio::spawn(async move {
865 let cached_build = {
866 let cache = ast_cache.read().await;
867 match cache.get(&uri_string) {
868 Some(v) => v.clone(),
869 None => return,
870 }
871 };
872 if let Some(sources) = cached_build.ast.get("sources") {
873 let contracts = cached_build.ast.get("contracts");
874 let cc = completion::build_completion_cache(sources, contracts);
875 completion_cache
876 .write()
877 .await
878 .insert(uri_string, Arc::new(cc));
879 }
880 });
881 }
882
883 let cache_ref = cached.as_deref();
884
885 let file_id = {
887 let uri_path = uri.to_file_path().ok();
888 cache_ref.and_then(|c| {
889 uri_path.as_ref().and_then(|p| {
890 let path_str = p.to_str()?;
891 c.path_to_file_id.get(path_str).copied()
892 })
893 })
894 };
895
896 let result =
897 completion::handle_completion(cache_ref, &source_text, position, trigger_char, file_id);
898 Ok(result)
899 }
900
901 async fn goto_definition(
902 &self,
903 params: GotoDefinitionParams,
904 ) -> tower_lsp::jsonrpc::Result<Option<GotoDefinitionResponse>> {
905 self.client
906 .log_message(MessageType::INFO, "got textDocument/definition request")
907 .await;
908
909 let uri = params.text_document_position_params.text_document.uri;
910 let position = params.text_document_position_params.position;
911
912 let file_path = match uri.to_file_path() {
913 Ok(path) => path,
914 Err(_) => {
915 self.client
916 .log_message(MessageType::ERROR, "Invalid file uri")
917 .await;
918 return Ok(None);
919 }
920 };
921
922 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
923 Some(bytes) => bytes,
924 None => return Ok(None),
925 };
926
927 let source_text = String::from_utf8_lossy(&source_bytes).to_string();
928
929 let cursor_name = goto::cursor_context(&source_text, position).map(|ctx| ctx.name);
931
932 let (is_dirty, cached_build) = {
936 let text_version = self
937 .text_cache
938 .read()
939 .await
940 .get(&uri.to_string())
941 .map(|(v, _)| *v)
942 .unwrap_or(0);
943 let cb = self.get_or_fetch_build(&uri, &file_path, false).await;
944 let build_version = cb.as_ref().map(|b| b.build_version).unwrap_or(0);
945 (text_version > build_version, cb)
946 };
947
948 let validate_ts = |loc: &Location| -> bool {
954 let Some(ref name) = cursor_name else {
955 return true; };
957 let target_src = if loc.uri == uri {
958 Some(source_text.clone())
959 } else {
960 loc.uri
961 .to_file_path()
962 .ok()
963 .and_then(|p| std::fs::read_to_string(&p).ok())
964 };
965 match target_src {
966 Some(src) => goto::validate_goto_target(&src, loc, name),
967 None => true, }
969 };
970
971 if is_dirty {
972 self.client
973 .log_message(MessageType::INFO, "file is dirty, trying tree-sitter first")
974 .await;
975
976 let ts_result = {
978 let comp_cache = self.completion_cache.read().await;
979 let text_cache = self.text_cache.read().await;
980 if let Some(cc) = comp_cache.get(&uri.to_string()) {
981 goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
982 } else {
983 None
984 }
985 };
986
987 if let Some(location) = ts_result {
988 if validate_ts(&location) {
989 self.client
990 .log_message(
991 MessageType::INFO,
992 format!(
993 "found definition (tree-sitter) at {}:{}",
994 location.uri, location.range.start.line
995 ),
996 )
997 .await;
998 return Ok(Some(GotoDefinitionResponse::from(location)));
999 }
1000 self.client
1001 .log_message(
1002 MessageType::INFO,
1003 "tree-sitter result failed validation, trying AST fallback",
1004 )
1005 .await;
1006 }
1007
1008 if let Some(ref cb) = cached_build
1013 && let Some(ref name) = cursor_name
1014 {
1015 let byte_hint = goto::pos_to_bytes(&source_bytes, position);
1016 if let Some(location) = goto::goto_declaration_by_name(cb, &uri, name, byte_hint) {
1017 self.client
1018 .log_message(
1019 MessageType::INFO,
1020 format!(
1021 "found definition (AST by name) at {}:{}",
1022 location.uri, location.range.start.line
1023 ),
1024 )
1025 .await;
1026 return Ok(Some(GotoDefinitionResponse::from(location)));
1027 }
1028 }
1029 } else {
1030 if let Some(ref cb) = cached_build
1032 && let Some(location) =
1033 goto::goto_declaration(&cb.ast, &uri, position, &source_bytes)
1034 {
1035 self.client
1036 .log_message(
1037 MessageType::INFO,
1038 format!(
1039 "found definition (AST) at {}:{}",
1040 location.uri, location.range.start.line
1041 ),
1042 )
1043 .await;
1044 return Ok(Some(GotoDefinitionResponse::from(location)));
1045 }
1046
1047 let ts_result = {
1049 let comp_cache = self.completion_cache.read().await;
1050 let text_cache = self.text_cache.read().await;
1051 if let Some(cc) = comp_cache.get(&uri.to_string()) {
1052 goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
1053 } else {
1054 None
1055 }
1056 };
1057
1058 if let Some(location) = ts_result {
1059 if validate_ts(&location) {
1060 self.client
1061 .log_message(
1062 MessageType::INFO,
1063 format!(
1064 "found definition (tree-sitter fallback) at {}:{}",
1065 location.uri, location.range.start.line
1066 ),
1067 )
1068 .await;
1069 return Ok(Some(GotoDefinitionResponse::from(location)));
1070 }
1071 self.client
1072 .log_message(MessageType::INFO, "tree-sitter fallback failed validation")
1073 .await;
1074 }
1075 }
1076
1077 self.client
1078 .log_message(MessageType::INFO, "no definition found")
1079 .await;
1080 Ok(None)
1081 }
1082
1083 async fn goto_declaration(
1084 &self,
1085 params: request::GotoDeclarationParams,
1086 ) -> tower_lsp::jsonrpc::Result<Option<request::GotoDeclarationResponse>> {
1087 self.client
1088 .log_message(MessageType::INFO, "got textDocument/declaration request")
1089 .await;
1090
1091 let uri = params.text_document_position_params.text_document.uri;
1092 let position = params.text_document_position_params.position;
1093
1094 let file_path = match uri.to_file_path() {
1095 Ok(path) => path,
1096 Err(_) => {
1097 self.client
1098 .log_message(MessageType::ERROR, "invalid file uri")
1099 .await;
1100 return Ok(None);
1101 }
1102 };
1103
1104 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1105 Some(bytes) => bytes,
1106 None => return Ok(None),
1107 };
1108
1109 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1110 let cached_build = match cached_build {
1111 Some(cb) => cb,
1112 None => return Ok(None),
1113 };
1114
1115 if let Some(location) =
1116 goto::goto_declaration(&cached_build.ast, &uri, position, &source_bytes)
1117 {
1118 self.client
1119 .log_message(
1120 MessageType::INFO,
1121 format!(
1122 "found declaration at {}:{}",
1123 location.uri, location.range.start.line
1124 ),
1125 )
1126 .await;
1127 Ok(Some(request::GotoDeclarationResponse::from(location)))
1128 } else {
1129 self.client
1130 .log_message(MessageType::INFO, "no declaration found")
1131 .await;
1132 Ok(None)
1133 }
1134 }
1135
1136 async fn references(
1137 &self,
1138 params: ReferenceParams,
1139 ) -> tower_lsp::jsonrpc::Result<Option<Vec<Location>>> {
1140 self.client
1141 .log_message(MessageType::INFO, "Got a textDocument/references request")
1142 .await;
1143
1144 let uri = params.text_document_position.text_document.uri;
1145 let position = params.text_document_position.position;
1146 let file_path = match uri.to_file_path() {
1147 Ok(path) => path,
1148 Err(_) => {
1149 self.client
1150 .log_message(MessageType::ERROR, "Invalid file URI")
1151 .await;
1152 return Ok(None);
1153 }
1154 };
1155 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1156 Some(bytes) => bytes,
1157 None => return Ok(None),
1158 };
1159 let cached_build = self.get_or_fetch_build(&uri, &file_path, true).await;
1160 let cached_build = match cached_build {
1161 Some(cb) => cb,
1162 None => return Ok(None),
1163 };
1164
1165 let mut locations = references::goto_references(
1167 &cached_build.ast,
1168 &uri,
1169 position,
1170 &source_bytes,
1171 params.context.include_declaration,
1172 );
1173
1174 if let Some((def_abs_path, def_byte_offset)) =
1176 references::resolve_target_location(&cached_build, &uri, position, &source_bytes)
1177 {
1178 let cache = self.ast_cache.read().await;
1179 for (cached_uri, other_build) in cache.iter() {
1180 if *cached_uri == uri.to_string() {
1181 continue;
1182 }
1183 let other_locations = references::goto_references_for_target(
1184 other_build,
1185 &def_abs_path,
1186 def_byte_offset,
1187 None,
1188 params.context.include_declaration,
1189 );
1190 locations.extend(other_locations);
1191 }
1192 }
1193
1194 let mut seen = std::collections::HashSet::new();
1196 locations.retain(|loc| {
1197 seen.insert((
1198 loc.uri.clone(),
1199 loc.range.start.line,
1200 loc.range.start.character,
1201 loc.range.end.line,
1202 loc.range.end.character,
1203 ))
1204 });
1205
1206 if locations.is_empty() {
1207 self.client
1208 .log_message(MessageType::INFO, "No references found")
1209 .await;
1210 Ok(None)
1211 } else {
1212 self.client
1213 .log_message(
1214 MessageType::INFO,
1215 format!("Found {} references", locations.len()),
1216 )
1217 .await;
1218 Ok(Some(locations))
1219 }
1220 }
1221
1222 async fn prepare_rename(
1223 &self,
1224 params: TextDocumentPositionParams,
1225 ) -> tower_lsp::jsonrpc::Result<Option<PrepareRenameResponse>> {
1226 self.client
1227 .log_message(MessageType::INFO, "got textDocument/prepareRename request")
1228 .await;
1229
1230 let uri = params.text_document.uri;
1231 let position = params.position;
1232
1233 let file_path = match uri.to_file_path() {
1234 Ok(path) => path,
1235 Err(_) => {
1236 self.client
1237 .log_message(MessageType::ERROR, "invalid file uri")
1238 .await;
1239 return Ok(None);
1240 }
1241 };
1242
1243 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1244 Some(bytes) => bytes,
1245 None => return Ok(None),
1246 };
1247
1248 if let Some(range) = rename::get_identifier_range(&source_bytes, position) {
1249 self.client
1250 .log_message(
1251 MessageType::INFO,
1252 format!(
1253 "prepare rename range: {}:{}",
1254 range.start.line, range.start.character
1255 ),
1256 )
1257 .await;
1258 Ok(Some(PrepareRenameResponse::Range(range)))
1259 } else {
1260 self.client
1261 .log_message(MessageType::INFO, "no identifier found for prepare rename")
1262 .await;
1263 Ok(None)
1264 }
1265 }
1266
1267 async fn rename(
1268 &self,
1269 params: RenameParams,
1270 ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
1271 self.client
1272 .log_message(MessageType::INFO, "got textDocument/rename request")
1273 .await;
1274
1275 let uri = params.text_document_position.text_document.uri;
1276 let position = params.text_document_position.position;
1277 let new_name = params.new_name;
1278 let file_path = match uri.to_file_path() {
1279 Ok(p) => p,
1280 Err(_) => {
1281 self.client
1282 .log_message(MessageType::ERROR, "invalid file uri")
1283 .await;
1284 return Ok(None);
1285 }
1286 };
1287 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1288 Some(bytes) => bytes,
1289 None => return Ok(None),
1290 };
1291
1292 let current_identifier = match rename::get_identifier_at_position(&source_bytes, position) {
1293 Some(id) => id,
1294 None => {
1295 self.client
1296 .log_message(MessageType::ERROR, "No identifier found at position")
1297 .await;
1298 return Ok(None);
1299 }
1300 };
1301
1302 if !utils::is_valid_solidity_identifier(&new_name) {
1303 return Err(tower_lsp::jsonrpc::Error::invalid_params(
1304 "new name is not a valid solidity identifier",
1305 ));
1306 }
1307
1308 if new_name == current_identifier {
1309 self.client
1310 .log_message(
1311 MessageType::INFO,
1312 "new name is the same as current identifier",
1313 )
1314 .await;
1315 return Ok(None);
1316 }
1317
1318 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1319 let cached_build = match cached_build {
1320 Some(cb) => cb,
1321 None => return Ok(None),
1322 };
1323 let other_builds: Vec<Arc<goto::CachedBuild>> = {
1324 let cache = self.ast_cache.read().await;
1325 cache
1326 .iter()
1327 .filter(|(key, _)| **key != uri.to_string())
1328 .map(|(_, v)| v.clone())
1329 .collect()
1330 };
1331 let other_refs: Vec<&goto::CachedBuild> = other_builds.iter().map(|v| v.as_ref()).collect();
1332
1333 let text_buffers: HashMap<String, Vec<u8>> = {
1337 let text_cache = self.text_cache.read().await;
1338 text_cache
1339 .iter()
1340 .map(|(uri, (_, content))| (uri.clone(), content.as_bytes().to_vec()))
1341 .collect()
1342 };
1343
1344 match rename::rename_symbol(
1345 &cached_build,
1346 &uri,
1347 position,
1348 &source_bytes,
1349 new_name,
1350 &other_refs,
1351 &text_buffers,
1352 ) {
1353 Some(workspace_edit) => {
1354 self.client
1355 .log_message(
1356 MessageType::INFO,
1357 format!(
1358 "created rename edit with {} file(s), {} total change(s)",
1359 workspace_edit
1360 .changes
1361 .as_ref()
1362 .map(|c| c.len())
1363 .unwrap_or(0),
1364 workspace_edit
1365 .changes
1366 .as_ref()
1367 .map(|c| c.values().map(|v| v.len()).sum::<usize>())
1368 .unwrap_or(0)
1369 ),
1370 )
1371 .await;
1372
1373 Ok(Some(workspace_edit))
1378 }
1379
1380 None => {
1381 self.client
1382 .log_message(MessageType::INFO, "No locations found for renaming")
1383 .await;
1384 Ok(None)
1385 }
1386 }
1387 }
1388
1389 async fn symbol(
1390 &self,
1391 params: WorkspaceSymbolParams,
1392 ) -> tower_lsp::jsonrpc::Result<Option<Vec<SymbolInformation>>> {
1393 self.client
1394 .log_message(MessageType::INFO, "got workspace/symbol request")
1395 .await;
1396
1397 let files: Vec<(Url, String)> = {
1399 let cache = self.text_cache.read().await;
1400 cache
1401 .iter()
1402 .filter(|(uri_str, _)| uri_str.ends_with(".sol"))
1403 .filter_map(|(uri_str, (_, content))| {
1404 Url::parse(uri_str).ok().map(|uri| (uri, content.clone()))
1405 })
1406 .collect()
1407 };
1408
1409 let mut all_symbols = symbols::extract_workspace_symbols(&files);
1410 if !params.query.is_empty() {
1411 let query = params.query.to_lowercase();
1412 all_symbols.retain(|symbol| symbol.name.to_lowercase().contains(&query));
1413 }
1414 if all_symbols.is_empty() {
1415 self.client
1416 .log_message(MessageType::INFO, "No symbols found")
1417 .await;
1418 Ok(None)
1419 } else {
1420 self.client
1421 .log_message(
1422 MessageType::INFO,
1423 format!("found {} symbols", all_symbols.len()),
1424 )
1425 .await;
1426 Ok(Some(all_symbols))
1427 }
1428 }
1429
1430 async fn document_symbol(
1431 &self,
1432 params: DocumentSymbolParams,
1433 ) -> tower_lsp::jsonrpc::Result<Option<DocumentSymbolResponse>> {
1434 self.client
1435 .log_message(MessageType::INFO, "got textDocument/documentSymbol request")
1436 .await;
1437 let uri = params.text_document.uri;
1438 let file_path = match uri.to_file_path() {
1439 Ok(path) => path,
1440 Err(_) => {
1441 self.client
1442 .log_message(MessageType::ERROR, "invalid file uri")
1443 .await;
1444 return Ok(None);
1445 }
1446 };
1447
1448 let source = {
1450 let cache = self.text_cache.read().await;
1451 cache
1452 .get(&uri.to_string())
1453 .map(|(_, content)| content.clone())
1454 };
1455 let source = match source {
1456 Some(s) => s,
1457 None => match std::fs::read_to_string(&file_path) {
1458 Ok(s) => s,
1459 Err(_) => return Ok(None),
1460 },
1461 };
1462
1463 let symbols = symbols::extract_document_symbols(&source);
1464 if symbols.is_empty() {
1465 self.client
1466 .log_message(MessageType::INFO, "no document symbols found")
1467 .await;
1468 Ok(None)
1469 } else {
1470 self.client
1471 .log_message(
1472 MessageType::INFO,
1473 format!("found {} document symbols", symbols.len()),
1474 )
1475 .await;
1476 Ok(Some(DocumentSymbolResponse::Nested(symbols)))
1477 }
1478 }
1479
1480 async fn hover(&self, params: HoverParams) -> tower_lsp::jsonrpc::Result<Option<Hover>> {
1481 self.client
1482 .log_message(MessageType::INFO, "got textDocument/hover request")
1483 .await;
1484
1485 let uri = params.text_document_position_params.text_document.uri;
1486 let position = params.text_document_position_params.position;
1487
1488 let file_path = match uri.to_file_path() {
1489 Ok(path) => path,
1490 Err(_) => {
1491 self.client
1492 .log_message(MessageType::ERROR, "invalid file uri")
1493 .await;
1494 return Ok(None);
1495 }
1496 };
1497
1498 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1499 Some(bytes) => bytes,
1500 None => return Ok(None),
1501 };
1502
1503 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1504 let cached_build = match cached_build {
1505 Some(cb) => cb,
1506 None => return Ok(None),
1507 };
1508
1509 let result = hover::hover_info(
1510 &cached_build.ast,
1511 &uri,
1512 position,
1513 &source_bytes,
1514 &cached_build.gas_index,
1515 &cached_build.doc_index,
1516 &cached_build.hint_index,
1517 );
1518
1519 if result.is_some() {
1520 self.client
1521 .log_message(MessageType::INFO, "hover info found")
1522 .await;
1523 } else {
1524 self.client
1525 .log_message(MessageType::INFO, "no hover info found")
1526 .await;
1527 }
1528
1529 Ok(result)
1530 }
1531
1532 async fn document_link(
1533 &self,
1534 params: DocumentLinkParams,
1535 ) -> tower_lsp::jsonrpc::Result<Option<Vec<DocumentLink>>> {
1536 self.client
1537 .log_message(MessageType::INFO, "got textDocument/documentLink request")
1538 .await;
1539
1540 let uri = params.text_document.uri;
1541 let file_path = match uri.to_file_path() {
1542 Ok(path) => path,
1543 Err(_) => {
1544 self.client
1545 .log_message(MessageType::ERROR, "invalid file uri")
1546 .await;
1547 return Ok(None);
1548 }
1549 };
1550
1551 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1552 Some(bytes) => bytes,
1553 None => return Ok(None),
1554 };
1555
1556 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1557 let cached_build = match cached_build {
1558 Some(cb) => cb,
1559 None => return Ok(None),
1560 };
1561
1562 let result = links::document_links(&cached_build, &uri, &source_bytes);
1563
1564 if result.is_empty() {
1565 self.client
1566 .log_message(MessageType::INFO, "no document links found")
1567 .await;
1568 Ok(None)
1569 } else {
1570 self.client
1571 .log_message(
1572 MessageType::INFO,
1573 format!("found {} document links", result.len()),
1574 )
1575 .await;
1576 Ok(Some(result))
1577 }
1578 }
1579
1580 async fn semantic_tokens_full(
1581 &self,
1582 params: SemanticTokensParams,
1583 ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensResult>> {
1584 self.client
1585 .log_message(
1586 MessageType::INFO,
1587 "got textDocument/semanticTokens/full request",
1588 )
1589 .await;
1590
1591 let uri = params.text_document.uri;
1592 let source = {
1593 let cache = self.text_cache.read().await;
1594 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
1595 };
1596
1597 let source = match source {
1598 Some(s) => s,
1599 None => {
1600 let file_path = match uri.to_file_path() {
1602 Ok(p) => p,
1603 Err(_) => return Ok(None),
1604 };
1605 match std::fs::read_to_string(&file_path) {
1606 Ok(s) => s,
1607 Err(_) => return Ok(None),
1608 }
1609 }
1610 };
1611
1612 let tokens = semantic_tokens::semantic_tokens_full(&source);
1613
1614 Ok(Some(SemanticTokensResult::Tokens(tokens)))
1615 }
1616
1617 async fn inlay_hint(
1618 &self,
1619 params: InlayHintParams,
1620 ) -> tower_lsp::jsonrpc::Result<Option<Vec<InlayHint>>> {
1621 self.client
1622 .log_message(MessageType::INFO, "got textDocument/inlayHint request")
1623 .await;
1624
1625 let uri = params.text_document.uri;
1626 let range = params.range;
1627
1628 let file_path = match uri.to_file_path() {
1629 Ok(path) => path,
1630 Err(_) => {
1631 self.client
1632 .log_message(MessageType::ERROR, "invalid file uri")
1633 .await;
1634 return Ok(None);
1635 }
1636 };
1637
1638 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1639 Some(bytes) => bytes,
1640 None => return Ok(None),
1641 };
1642
1643 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1644 let cached_build = match cached_build {
1645 Some(cb) => cb,
1646 None => return Ok(None),
1647 };
1648
1649 let hints = inlay_hints::inlay_hints(&cached_build, &uri, range, &source_bytes);
1650
1651 if hints.is_empty() {
1652 self.client
1653 .log_message(MessageType::INFO, "no inlay hints found")
1654 .await;
1655 Ok(None)
1656 } else {
1657 self.client
1658 .log_message(
1659 MessageType::INFO,
1660 format!("found {} inlay hints", hints.len()),
1661 )
1662 .await;
1663 Ok(Some(hints))
1664 }
1665 }
1666}