1use crate::completion;
2use crate::config::{self, FoundryConfig, LintConfig, Settings};
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 std::sync::atomic::{AtomicU64, Ordering};
16use tokio::sync::RwLock;
17use tower_lsp::{Client, LanguageServer, lsp_types::*};
18
19pub struct ForgeLsp {
20 client: Client,
21 compiler: Arc<dyn Runner>,
22 ast_cache: Arc<RwLock<HashMap<String, Arc<goto::CachedBuild>>>>,
23 text_cache: Arc<RwLock<HashMap<String, (i32, String)>>>,
27 completion_cache: Arc<RwLock<HashMap<String, Arc<completion::CompletionCache>>>>,
28 lint_config: Arc<RwLock<LintConfig>>,
30 foundry_config: Arc<RwLock<FoundryConfig>>,
32 client_capabilities: Arc<RwLock<Option<ClientCapabilities>>>,
34 settings: Arc<RwLock<Settings>>,
36 use_solc: bool,
38 semantic_token_cache: Arc<RwLock<HashMap<String, (String, Vec<SemanticToken>)>>>,
41 semantic_token_id: Arc<AtomicU64>,
43 root_uri: Arc<RwLock<Option<Url>>>,
45 project_indexed: Arc<std::sync::atomic::AtomicBool>,
47}
48
49impl ForgeLsp {
50 pub fn new(client: Client, use_solar: bool, use_solc: bool) -> Self {
51 let compiler: Arc<dyn Runner> = if use_solar {
52 Arc::new(crate::solar_runner::SolarRunner)
53 } else {
54 Arc::new(ForgeRunner)
55 };
56 let ast_cache = Arc::new(RwLock::new(HashMap::new()));
57 let text_cache = Arc::new(RwLock::new(HashMap::new()));
58 let completion_cache = Arc::new(RwLock::new(HashMap::new()));
59 let lint_config = Arc::new(RwLock::new(LintConfig::default()));
60 let foundry_config = Arc::new(RwLock::new(FoundryConfig::default()));
61 let client_capabilities = Arc::new(RwLock::new(None));
62 let settings = Arc::new(RwLock::new(Settings::default()));
63 Self {
64 client,
65 compiler,
66 ast_cache,
67 text_cache,
68 completion_cache,
69 lint_config,
70 foundry_config,
71 client_capabilities,
72 settings,
73 use_solc,
74 semantic_token_cache: Arc::new(RwLock::new(HashMap::new())),
75 semantic_token_id: Arc::new(AtomicU64::new(0)),
76 root_uri: Arc::new(RwLock::new(None)),
77 project_indexed: Arc::new(std::sync::atomic::AtomicBool::new(false)),
78 }
79 }
80
81 async fn on_change(&self, params: TextDocumentItem) {
82 let uri = params.uri.clone();
83 let version = params.version;
84
85 let file_path = match uri.to_file_path() {
86 Ok(path) => path,
87 Err(_) => {
88 self.client
89 .log_message(MessageType::ERROR, "Invalid file URI")
90 .await;
91 return;
92 }
93 };
94
95 let path_str = match file_path.to_str() {
96 Some(s) => s,
97 None => {
98 self.client
99 .log_message(MessageType::ERROR, "Invalid file path")
100 .await;
101 return;
102 }
103 };
104
105 let (should_lint, lint_settings) = {
107 let lint_cfg = self.lint_config.read().await;
108 let settings = self.settings.read().await;
109 let enabled = lint_cfg.should_lint(&file_path) && settings.lint.enabled;
110 let ls = settings.lint.clone();
111 (enabled, ls)
112 };
113
114 let (lint_result, build_result, ast_result) = if self.use_solc {
118 let foundry_cfg = self.foundry_config.read().await.clone();
119 let solc_future = crate::solc::solc_ast(path_str, &foundry_cfg, Some(&self.client));
120
121 if should_lint {
122 let (lint, solc) = tokio::join!(
123 self.compiler.get_lint_diagnostics(&uri, &lint_settings),
124 solc_future
125 );
126 match solc {
127 Ok(data) => {
128 self.client
129 .log_message(
130 MessageType::INFO,
131 "solc: AST + diagnostics from single run",
132 )
133 .await;
134 let content = tokio::fs::read_to_string(&file_path)
136 .await
137 .unwrap_or_default();
138 let build_diags = crate::build::build_output_to_diagnostics(
139 &data,
140 &file_path,
141 &content,
142 &foundry_cfg.ignored_error_codes,
143 );
144 (Some(lint), Ok(build_diags), Ok(data))
145 }
146 Err(e) => {
147 self.client
148 .log_message(
149 MessageType::WARNING,
150 format!("solc failed, falling back to forge: {e}"),
151 )
152 .await;
153 let (build, ast) = tokio::join!(
154 self.compiler.get_build_diagnostics(&uri),
155 self.compiler.ast(path_str)
156 );
157 (Some(lint), build, ast)
158 }
159 }
160 } else {
161 self.client
162 .log_message(
163 MessageType::INFO,
164 format!("skipping lint for ignored file: {path_str}"),
165 )
166 .await;
167 match solc_future.await {
168 Ok(data) => {
169 self.client
170 .log_message(
171 MessageType::INFO,
172 "solc: AST + diagnostics from single run",
173 )
174 .await;
175 let content = tokio::fs::read_to_string(&file_path)
176 .await
177 .unwrap_or_default();
178 let build_diags = crate::build::build_output_to_diagnostics(
179 &data,
180 &file_path,
181 &content,
182 &foundry_cfg.ignored_error_codes,
183 );
184 (None, Ok(build_diags), Ok(data))
185 }
186 Err(e) => {
187 self.client
188 .log_message(
189 MessageType::WARNING,
190 format!("solc failed, falling back to forge: {e}"),
191 )
192 .await;
193 let (build, ast) = tokio::join!(
194 self.compiler.get_build_diagnostics(&uri),
195 self.compiler.ast(path_str)
196 );
197 (None, build, ast)
198 }
199 }
200 }
201 } else {
202 if should_lint {
204 let (lint, build, ast) = tokio::join!(
205 self.compiler.get_lint_diagnostics(&uri, &lint_settings),
206 self.compiler.get_build_diagnostics(&uri),
207 self.compiler.ast(path_str)
208 );
209 (Some(lint), build, ast)
210 } else {
211 self.client
212 .log_message(
213 MessageType::INFO,
214 format!("skipping lint for ignored file: {path_str}"),
215 )
216 .await;
217 let (build, ast) = tokio::join!(
218 self.compiler.get_build_diagnostics(&uri),
219 self.compiler.ast(path_str)
220 );
221 (None, build, ast)
222 }
223 };
224
225 let build_succeeded = matches!(&build_result, Ok(diagnostics) if diagnostics.iter().all(|d| d.severity != Some(DiagnosticSeverity::ERROR)));
227
228 if build_succeeded {
229 if let Ok(ast_data) = ast_result {
230 let cached_build = Arc::new(goto::CachedBuild::new(ast_data, version));
231 let mut cache = self.ast_cache.write().await;
232 cache.insert(uri.to_string(), cached_build.clone());
233 drop(cache);
234
235 let completion_cache = self.completion_cache.clone();
237 let uri_string = uri.to_string();
238 tokio::spawn(async move {
239 if let Some(sources) = cached_build.ast.get("sources") {
240 let contracts = cached_build.ast.get("contracts");
241 let cc = completion::build_completion_cache(sources, contracts);
242 completion_cache
243 .write()
244 .await
245 .insert(uri_string, Arc::new(cc));
246 }
247 });
248 self.client
249 .log_message(MessageType::INFO, "Build successful, AST cache updated")
250 .await;
251 } else if let Err(e) = ast_result {
252 self.client
253 .log_message(
254 MessageType::INFO,
255 format!("Build succeeded but failed to get AST: {e}"),
256 )
257 .await;
258 }
259 } else {
260 self.client
262 .log_message(
263 MessageType::INFO,
264 "Build errors detected, keeping existing AST cache",
265 )
266 .await;
267 }
268
269 {
271 let mut text_cache = self.text_cache.write().await;
272 let uri_str = uri.to_string();
273 let existing_version = text_cache.get(&uri_str).map(|(v, _)| *v).unwrap_or(-1);
274 if version >= existing_version {
275 text_cache.insert(uri_str, (version, params.text));
276 }
277 }
278
279 let mut all_diagnostics = vec![];
280
281 if let Some(lint_result) = lint_result {
282 match lint_result {
283 Ok(mut lints) => {
284 if !lint_settings.exclude.is_empty() {
286 lints.retain(|d| {
287 if let Some(NumberOrString::String(code)) = &d.code {
288 !lint_settings.exclude.iter().any(|ex| ex == code)
289 } else {
290 true
291 }
292 });
293 }
294 self.client
295 .log_message(
296 MessageType::INFO,
297 format!("found {} lint diagnostics", lints.len()),
298 )
299 .await;
300 all_diagnostics.append(&mut lints);
301 }
302 Err(e) => {
303 self.client
304 .log_message(
305 MessageType::ERROR,
306 format!("Forge lint diagnostics failed: {e}"),
307 )
308 .await;
309 }
310 }
311 }
312
313 match build_result {
314 Ok(mut builds) => {
315 self.client
316 .log_message(
317 MessageType::INFO,
318 format!("found {} build diagnostics", builds.len()),
319 )
320 .await;
321 all_diagnostics.append(&mut builds);
322 }
323 Err(e) => {
324 self.client
325 .log_message(
326 MessageType::WARNING,
327 format!("Forge build diagnostics failed: {e}"),
328 )
329 .await;
330 }
331 }
332
333 let is_test_or_script = path_str.ends_with(".t.sol") || path_str.ends_with(".s.sol");
341 if build_succeeded
342 && self.use_solc
343 && !is_test_or_script
344 && !self
345 .project_indexed
346 .load(std::sync::atomic::Ordering::Relaxed)
347 {
348 self.project_indexed
349 .store(true, std::sync::atomic::Ordering::Relaxed);
350 let foundry_config = self.foundry_config.read().await.clone();
351 let root_uri = self.root_uri.read().await.clone();
352
353 let cache_key = root_uri.as_ref().map(|u| u.to_string());
355
356 if let Some(cache_key) = cache_key {
357 if foundry_config
358 .root
359 .join(&foundry_config.sources_dir)
360 .is_dir()
361 {
362 match crate::solc::solc_project_index(&foundry_config, Some(&self.client)).await
363 {
364 Ok(ast_data) => {
365 let cached_build = Arc::new(crate::goto::CachedBuild::new(ast_data, 0));
366 let source_count = cached_build.nodes.len();
367 self.ast_cache.write().await.insert(cache_key, cached_build);
368 self.client
369 .log_message(
370 MessageType::INFO,
371 format!("project index: cached {} source files", source_count),
372 )
373 .await;
374 }
375 Err(e) => {
376 self.client
377 .log_message(
378 MessageType::WARNING,
379 format!("project index failed: {e}"),
380 )
381 .await;
382 }
383 }
384 } else {
385 self.client
386 .log_message(
387 MessageType::INFO,
388 format!(
389 "project index: {}/{} not found, skipping",
390 foundry_config.root.display(),
391 foundry_config.sources_dir
392 ),
393 )
394 .await;
395 }
396 }
397 }
398
399 self.client
401 .publish_diagnostics(uri, all_diagnostics, None)
402 .await;
403
404 if build_succeeded {
406 let client = self.client.clone();
407 tokio::spawn(async move {
408 let _ = client.inlay_hint_refresh().await;
409 });
410 }
411 }
412
413 async fn get_or_fetch_build(
422 &self,
423 uri: &Url,
424 file_path: &std::path::Path,
425 insert_on_miss: bool,
426 ) -> Option<Arc<goto::CachedBuild>> {
427 let uri_str = uri.to_string();
428
429 {
432 let cache = self.ast_cache.read().await;
433 if let Some(cached) = cache.get(&uri_str) {
434 return Some(cached.clone());
435 }
436 }
437
438 if !insert_on_miss {
442 return None;
443 }
444
445 let path_str = file_path.to_str()?;
447 let ast_result = if self.use_solc {
448 let foundry_cfg = self.foundry_config.read().await.clone();
449 match crate::solc::solc_ast(path_str, &foundry_cfg, Some(&self.client)).await {
450 Ok(data) => Ok(data),
451 Err(_) => self.compiler.ast(path_str).await,
452 }
453 } else {
454 self.compiler.ast(path_str).await
455 };
456 match ast_result {
457 Ok(data) => {
458 let build = Arc::new(goto::CachedBuild::new(data, 0));
461 let mut cache = self.ast_cache.write().await;
462 cache.insert(uri_str.clone(), build.clone());
463 Some(build)
464 }
465 Err(e) => {
466 self.client
467 .log_message(MessageType::ERROR, format!("failed to get AST: {e}"))
468 .await;
469 None
470 }
471 }
472 }
473
474 async fn get_source_bytes(&self, uri: &Url, file_path: &std::path::Path) -> Option<Vec<u8>> {
477 {
478 let text_cache = self.text_cache.read().await;
479 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
480 return Some(content.as_bytes().to_vec());
481 }
482 }
483 match std::fs::read(file_path) {
484 Ok(bytes) => Some(bytes),
485 Err(e) => {
486 self.client
487 .log_message(MessageType::ERROR, format!("failed to read file: {e}"))
488 .await;
489 None
490 }
491 }
492 }
493}
494
495#[tower_lsp::async_trait]
496impl LanguageServer for ForgeLsp {
497 async fn initialize(
498 &self,
499 params: InitializeParams,
500 ) -> tower_lsp::jsonrpc::Result<InitializeResult> {
501 {
503 let mut caps = self.client_capabilities.write().await;
504 *caps = Some(params.capabilities.clone());
505 }
506
507 if let Some(init_opts) = ¶ms.initialization_options {
509 let s = config::parse_settings(init_opts);
510 self.client
511 .log_message(
512 MessageType::INFO,
513 format!(
514 "settings: inlayHints.parameters={}, inlayHints.gasEstimates={}, lint.enabled={}, lint.severity={:?}, lint.only={:?}, lint.exclude={:?}",
515 s.inlay_hints.parameters, s.inlay_hints.gas_estimates, s.lint.enabled, s.lint.severity, s.lint.only, s.lint.exclude,
516 ),
517 )
518 .await;
519 let mut settings = self.settings.write().await;
520 *settings = s;
521 }
522
523 if let Some(uri) = params.root_uri.as_ref() {
525 let mut root = self.root_uri.write().await;
526 *root = Some(uri.clone());
527 }
528
529 if let Some(root_uri) = params
531 .root_uri
532 .as_ref()
533 .and_then(|uri| uri.to_file_path().ok())
534 {
535 let lint_cfg = config::load_lint_config(&root_uri);
536 self.client
537 .log_message(
538 MessageType::INFO,
539 format!(
540 "loaded foundry.toml lint config: lint_on_build={}, ignore_patterns={}",
541 lint_cfg.lint_on_build,
542 lint_cfg.ignore_patterns.len()
543 ),
544 )
545 .await;
546 let mut config = self.lint_config.write().await;
547 *config = lint_cfg;
548
549 let foundry_cfg = config::load_foundry_config(&root_uri);
550 self.client
551 .log_message(
552 MessageType::INFO,
553 format!(
554 "loaded foundry.toml project config: solc_version={:?}, remappings={}",
555 foundry_cfg.solc_version,
556 foundry_cfg.remappings.len()
557 ),
558 )
559 .await;
560 if foundry_cfg.via_ir {
561 self.client
562 .log_message(
563 MessageType::WARNING,
564 "via_ir is enabled in foundry.toml — gas estimate inlay hints are disabled to avoid slow compilation",
565 )
566 .await;
567 }
568 let mut fc = self.foundry_config.write().await;
569 *fc = foundry_cfg;
570 }
571
572 let client_encodings = params
574 .capabilities
575 .general
576 .as_ref()
577 .and_then(|g| g.position_encodings.as_deref());
578 let encoding = utils::PositionEncoding::negotiate(client_encodings);
579 utils::set_encoding(encoding);
580
581 Ok(InitializeResult {
582 server_info: Some(ServerInfo {
583 name: "Solidity Language Server".to_string(),
584 version: Some(env!("LONG_VERSION").to_string()),
585 }),
586 capabilities: ServerCapabilities {
587 position_encoding: Some(encoding.into()),
588 completion_provider: Some(CompletionOptions {
589 trigger_characters: Some(vec![".".to_string()]),
590 resolve_provider: Some(false),
591 ..Default::default()
592 }),
593 signature_help_provider: Some(SignatureHelpOptions {
594 trigger_characters: Some(vec![
595 "(".to_string(),
596 ",".to_string(),
597 "[".to_string(),
598 ]),
599 retrigger_characters: None,
600 work_done_progress_options: WorkDoneProgressOptions {
601 work_done_progress: None,
602 },
603 }),
604 definition_provider: Some(OneOf::Left(true)),
605 declaration_provider: Some(DeclarationCapability::Simple(true)),
606 references_provider: Some(OneOf::Left(true)),
607 rename_provider: Some(OneOf::Right(RenameOptions {
608 prepare_provider: Some(true),
609 work_done_progress_options: WorkDoneProgressOptions {
610 work_done_progress: Some(true),
611 },
612 })),
613 workspace_symbol_provider: Some(OneOf::Left(true)),
614 document_symbol_provider: Some(OneOf::Left(true)),
615 hover_provider: Some(HoverProviderCapability::Simple(true)),
616 document_link_provider: Some(DocumentLinkOptions {
617 resolve_provider: Some(false),
618 work_done_progress_options: WorkDoneProgressOptions {
619 work_done_progress: None,
620 },
621 }),
622 document_formatting_provider: Some(OneOf::Left(true)),
623 code_lens_provider: None,
624 inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options(
625 InlayHintOptions {
626 resolve_provider: Some(false),
627 work_done_progress_options: WorkDoneProgressOptions {
628 work_done_progress: None,
629 },
630 },
631 ))),
632 semantic_tokens_provider: Some(
633 SemanticTokensServerCapabilities::SemanticTokensOptions(
634 SemanticTokensOptions {
635 legend: semantic_tokens::legend(),
636 full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
637 range: Some(true),
638 work_done_progress_options: WorkDoneProgressOptions {
639 work_done_progress: None,
640 },
641 },
642 ),
643 ),
644 text_document_sync: Some(TextDocumentSyncCapability::Options(
645 TextDocumentSyncOptions {
646 will_save: Some(true),
647 will_save_wait_until: None,
648 open_close: Some(true),
649 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
650 include_text: Some(true),
651 })),
652 change: Some(TextDocumentSyncKind::FULL),
653 },
654 )),
655 ..ServerCapabilities::default()
656 },
657 })
658 }
659
660 async fn initialized(&self, _: InitializedParams) {
661 self.client
662 .log_message(MessageType::INFO, "lsp server initialized.")
663 .await;
664
665 let supports_dynamic = self
667 .client_capabilities
668 .read()
669 .await
670 .as_ref()
671 .and_then(|caps| caps.workspace.as_ref())
672 .and_then(|ws| ws.did_change_watched_files.as_ref())
673 .and_then(|dcwf| dcwf.dynamic_registration)
674 .unwrap_or(false);
675
676 if supports_dynamic {
677 let registration = Registration {
678 id: "foundry-toml-watcher".to_string(),
679 method: "workspace/didChangeWatchedFiles".to_string(),
680 register_options: Some(
681 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
682 watchers: vec![
683 FileSystemWatcher {
684 glob_pattern: GlobPattern::String("**/foundry.toml".to_string()),
685 kind: Some(WatchKind::all()),
686 },
687 FileSystemWatcher {
688 glob_pattern: GlobPattern::String("**/remappings.txt".to_string()),
689 kind: Some(WatchKind::all()),
690 },
691 ],
692 })
693 .unwrap(),
694 ),
695 };
696
697 if let Err(e) = self.client.register_capability(vec![registration]).await {
698 self.client
699 .log_message(
700 MessageType::WARNING,
701 format!("failed to register foundry.toml watcher: {e}"),
702 )
703 .await;
704 } else {
705 self.client
706 .log_message(MessageType::INFO, "registered foundry.toml file watcher")
707 .await;
708 }
709 }
710 }
711
712 async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
713 self.client
714 .log_message(MessageType::INFO, "lsp server shutting down.")
715 .await;
716 Ok(())
717 }
718
719 async fn did_open(&self, params: DidOpenTextDocumentParams) {
720 self.client
721 .log_message(MessageType::INFO, "file opened")
722 .await;
723
724 self.on_change(params.text_document).await
725 }
726
727 async fn did_change(&self, params: DidChangeTextDocumentParams) {
728 self.client
729 .log_message(MessageType::INFO, "file changed")
730 .await;
731
732 if let Some(change) = params.content_changes.into_iter().next() {
734 let mut text_cache = self.text_cache.write().await;
735 text_cache.insert(
736 params.text_document.uri.to_string(),
737 (params.text_document.version, change.text),
738 );
739 }
740 }
741
742 async fn did_save(&self, params: DidSaveTextDocumentParams) {
743 self.client
744 .log_message(MessageType::INFO, "file saved")
745 .await;
746
747 let text_content = if let Some(text) = params.text {
748 text
749 } else {
750 let cached = {
752 let text_cache = self.text_cache.read().await;
753 text_cache
754 .get(params.text_document.uri.as_str())
755 .map(|(_, content)| content.clone())
756 };
757 if let Some(content) = cached {
758 content
759 } else {
760 match std::fs::read_to_string(params.text_document.uri.path()) {
761 Ok(content) => content,
762 Err(e) => {
763 self.client
764 .log_message(
765 MessageType::ERROR,
766 format!("Failed to read file on save: {e}"),
767 )
768 .await;
769 return;
770 }
771 }
772 }
773 };
774
775 let version = self
776 .text_cache
777 .read()
778 .await
779 .get(params.text_document.uri.as_str())
780 .map(|(version, _)| *version)
781 .unwrap_or_default();
782
783 self.on_change(TextDocumentItem {
784 uri: params.text_document.uri,
785 text: text_content,
786 version,
787 language_id: "".to_string(),
788 })
789 .await;
790 }
791
792 async fn will_save(&self, params: WillSaveTextDocumentParams) {
793 self.client
794 .log_message(
795 MessageType::INFO,
796 format!(
797 "file will save reason:{:?} {}",
798 params.reason, params.text_document.uri
799 ),
800 )
801 .await;
802 }
803
804 async fn formatting(
805 &self,
806 params: DocumentFormattingParams,
807 ) -> tower_lsp::jsonrpc::Result<Option<Vec<TextEdit>>> {
808 self.client
809 .log_message(MessageType::INFO, "formatting request")
810 .await;
811
812 let uri = params.text_document.uri;
813 let file_path = match uri.to_file_path() {
814 Ok(path) => path,
815 Err(_) => {
816 self.client
817 .log_message(MessageType::ERROR, "Invalid file URI for formatting")
818 .await;
819 return Ok(None);
820 }
821 };
822 let path_str = match file_path.to_str() {
823 Some(s) => s,
824 None => {
825 self.client
826 .log_message(MessageType::ERROR, "Invalid file path for formatting")
827 .await;
828 return Ok(None);
829 }
830 };
831
832 let original_content = {
834 let text_cache = self.text_cache.read().await;
835 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
836 content.clone()
837 } else {
838 match std::fs::read_to_string(&file_path) {
840 Ok(content) => content,
841 Err(_) => {
842 self.client
843 .log_message(MessageType::ERROR, "Failed to read file for formatting")
844 .await;
845 return Ok(None);
846 }
847 }
848 }
849 };
850
851 let formatted_content = match self.compiler.format(path_str).await {
853 Ok(content) => content,
854 Err(e) => {
855 self.client
856 .log_message(MessageType::WARNING, format!("Formatting failed: {e}"))
857 .await;
858 return Ok(None);
859 }
860 };
861
862 if original_content != formatted_content {
864 let end = utils::byte_offset_to_position(&original_content, original_content.len());
865
866 {
868 let mut text_cache = self.text_cache.write().await;
869 let version = text_cache
870 .get(&uri.to_string())
871 .map(|(v, _)| *v)
872 .unwrap_or(0);
873 text_cache.insert(uri.to_string(), (version, formatted_content.clone()));
874 }
875
876 let edit = TextEdit {
877 range: Range {
878 start: Position::default(),
879 end,
880 },
881 new_text: formatted_content,
882 };
883 Ok(Some(vec![edit]))
884 } else {
885 Ok(None)
886 }
887 }
888
889 async fn did_close(&self, params: DidCloseTextDocumentParams) {
890 let uri = params.text_document.uri.to_string();
891 self.ast_cache.write().await.remove(&uri);
892 self.text_cache.write().await.remove(&uri);
893 self.completion_cache.write().await.remove(&uri);
894 self.client
895 .log_message(MessageType::INFO, "file closed, caches cleared.")
896 .await;
897 }
898
899 async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
900 let s = config::parse_settings(¶ms.settings);
901 self.client
902 .log_message(
903 MessageType::INFO,
904 format!(
905 "settings updated: inlayHints.parameters={}, inlayHints.gasEstimates={}, lint.enabled={}, lint.severity={:?}, lint.only={:?}, lint.exclude={:?}",
906 s.inlay_hints.parameters, s.inlay_hints.gas_estimates, s.lint.enabled, s.lint.severity, s.lint.only, s.lint.exclude,
907 ),
908 )
909 .await;
910 let mut settings = self.settings.write().await;
911 *settings = s;
912
913 let client = self.client.clone();
915 tokio::spawn(async move {
916 let _ = client.inlay_hint_refresh().await;
917 });
918 }
919 async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {
920 self.client
921 .log_message(MessageType::INFO, "workdspace folders changed.")
922 .await;
923 }
924
925 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
926 self.client
927 .log_message(MessageType::INFO, "watched files have changed.")
928 .await;
929
930 for change in ¶ms.changes {
932 let path = match change.uri.to_file_path() {
933 Ok(p) => p,
934 Err(_) => continue,
935 };
936
937 let filename = path.file_name().and_then(|n| n.to_str());
938
939 if filename == Some("foundry.toml") {
940 let lint_cfg = config::load_lint_config_from_toml(&path);
941 self.client
942 .log_message(
943 MessageType::INFO,
944 format!(
945 "reloaded foundry.toml lint config: lint_on_build={}, ignore_patterns={}",
946 lint_cfg.lint_on_build,
947 lint_cfg.ignore_patterns.len()
948 ),
949 )
950 .await;
951 let mut lc = self.lint_config.write().await;
952 *lc = lint_cfg;
953
954 let foundry_cfg = config::load_foundry_config_from_toml(&path);
955 self.client
956 .log_message(
957 MessageType::INFO,
958 format!(
959 "reloaded foundry.toml project config: solc_version={:?}, remappings={}",
960 foundry_cfg.solc_version,
961 foundry_cfg.remappings.len()
962 ),
963 )
964 .await;
965 if foundry_cfg.via_ir {
966 self.client
967 .log_message(
968 MessageType::WARNING,
969 "via_ir is enabled in foundry.toml — gas estimate inlay hints are disabled to avoid slow compilation",
970 )
971 .await;
972 }
973 let mut fc = self.foundry_config.write().await;
974 *fc = foundry_cfg;
975 break;
976 }
977
978 if filename == Some("remappings.txt") {
979 self.client
980 .log_message(
981 MessageType::INFO,
982 "remappings.txt changed, config may need refresh",
983 )
984 .await;
985 }
988 }
989 }
990
991 async fn completion(
992 &self,
993 params: CompletionParams,
994 ) -> tower_lsp::jsonrpc::Result<Option<CompletionResponse>> {
995 let uri = params.text_document_position.text_document.uri;
996 let position = params.text_document_position.position;
997
998 let trigger_char = params
999 .context
1000 .as_ref()
1001 .and_then(|ctx| ctx.trigger_character.as_deref());
1002
1003 let source_text = {
1005 let text_cache = self.text_cache.read().await;
1006 if let Some((_, text)) = text_cache.get(&uri.to_string()) {
1007 text.clone()
1008 } else {
1009 match uri.to_file_path() {
1010 Ok(path) => std::fs::read_to_string(&path).unwrap_or_default(),
1011 Err(_) => return Ok(None),
1012 }
1013 }
1014 };
1015
1016 let cached: Option<Arc<completion::CompletionCache>> = {
1018 let comp_cache = self.completion_cache.read().await;
1019 comp_cache.get(&uri.to_string()).cloned()
1020 };
1021
1022 if cached.is_none() {
1023 let ast_cache = self.ast_cache.clone();
1025 let completion_cache = self.completion_cache.clone();
1026 let uri_string = uri.to_string();
1027 tokio::spawn(async move {
1028 let cached_build = {
1029 let cache = ast_cache.read().await;
1030 match cache.get(&uri_string) {
1031 Some(v) => v.clone(),
1032 None => return,
1033 }
1034 };
1035 if let Some(sources) = cached_build.ast.get("sources") {
1036 let contracts = cached_build.ast.get("contracts");
1037 let cc = completion::build_completion_cache(sources, contracts);
1038 completion_cache
1039 .write()
1040 .await
1041 .insert(uri_string, Arc::new(cc));
1042 }
1043 });
1044 }
1045
1046 let cache_ref = cached.as_deref();
1047
1048 let file_id = {
1050 let uri_path = uri.to_file_path().ok();
1051 cache_ref.and_then(|c| {
1052 uri_path.as_ref().and_then(|p| {
1053 let path_str = p.to_str()?;
1054 c.path_to_file_id.get(path_str).copied()
1055 })
1056 })
1057 };
1058
1059 let result =
1060 completion::handle_completion(cache_ref, &source_text, position, trigger_char, file_id);
1061 Ok(result)
1062 }
1063
1064 async fn goto_definition(
1065 &self,
1066 params: GotoDefinitionParams,
1067 ) -> tower_lsp::jsonrpc::Result<Option<GotoDefinitionResponse>> {
1068 self.client
1069 .log_message(MessageType::INFO, "got textDocument/definition request")
1070 .await;
1071
1072 let uri = params.text_document_position_params.text_document.uri;
1073 let position = params.text_document_position_params.position;
1074
1075 let file_path = match uri.to_file_path() {
1076 Ok(path) => path,
1077 Err(_) => {
1078 self.client
1079 .log_message(MessageType::ERROR, "Invalid file uri")
1080 .await;
1081 return Ok(None);
1082 }
1083 };
1084
1085 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1086 Some(bytes) => bytes,
1087 None => return Ok(None),
1088 };
1089
1090 let source_text = String::from_utf8_lossy(&source_bytes).to_string();
1091
1092 let cursor_name = goto::cursor_context(&source_text, position).map(|ctx| ctx.name);
1094
1095 let (is_dirty, cached_build) = {
1099 let text_version = self
1100 .text_cache
1101 .read()
1102 .await
1103 .get(&uri.to_string())
1104 .map(|(v, _)| *v)
1105 .unwrap_or(0);
1106 let cb = self.get_or_fetch_build(&uri, &file_path, false).await;
1107 let build_version = cb.as_ref().map(|b| b.build_version).unwrap_or(0);
1108 (text_version > build_version, cb)
1109 };
1110
1111 let validate_ts = |loc: &Location| -> bool {
1117 let Some(ref name) = cursor_name else {
1118 return true; };
1120 let target_src = if loc.uri == uri {
1121 Some(source_text.clone())
1122 } else {
1123 loc.uri
1124 .to_file_path()
1125 .ok()
1126 .and_then(|p| std::fs::read_to_string(&p).ok())
1127 };
1128 match target_src {
1129 Some(src) => goto::validate_goto_target(&src, loc, name),
1130 None => true, }
1132 };
1133
1134 if is_dirty {
1135 self.client
1136 .log_message(MessageType::INFO, "file is dirty, trying tree-sitter first")
1137 .await;
1138
1139 let ts_result = {
1141 let comp_cache = self.completion_cache.read().await;
1142 let text_cache = self.text_cache.read().await;
1143 if let Some(cc) = comp_cache.get(&uri.to_string()) {
1144 goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
1145 } else {
1146 None
1147 }
1148 };
1149
1150 if let Some(location) = ts_result {
1151 if validate_ts(&location) {
1152 self.client
1153 .log_message(
1154 MessageType::INFO,
1155 format!(
1156 "found definition (tree-sitter) at {}:{}",
1157 location.uri, location.range.start.line
1158 ),
1159 )
1160 .await;
1161 return Ok(Some(GotoDefinitionResponse::from(location)));
1162 }
1163 self.client
1164 .log_message(
1165 MessageType::INFO,
1166 "tree-sitter result failed validation, trying AST fallback",
1167 )
1168 .await;
1169 }
1170
1171 if let Some(ref cb) = cached_build
1176 && let Some(ref name) = cursor_name
1177 {
1178 let byte_hint = goto::pos_to_bytes(&source_bytes, position);
1179 if let Some(location) = goto::goto_declaration_by_name(cb, &uri, name, byte_hint) {
1180 self.client
1181 .log_message(
1182 MessageType::INFO,
1183 format!(
1184 "found definition (AST by name) at {}:{}",
1185 location.uri, location.range.start.line
1186 ),
1187 )
1188 .await;
1189 return Ok(Some(GotoDefinitionResponse::from(location)));
1190 }
1191 }
1192 } else {
1193 if let Some(ref cb) = cached_build
1195 && let Some(location) =
1196 goto::goto_declaration(&cb.ast, &uri, position, &source_bytes)
1197 {
1198 self.client
1199 .log_message(
1200 MessageType::INFO,
1201 format!(
1202 "found definition (AST) at {}:{}",
1203 location.uri, location.range.start.line
1204 ),
1205 )
1206 .await;
1207 return Ok(Some(GotoDefinitionResponse::from(location)));
1208 }
1209
1210 let ts_result = {
1212 let comp_cache = self.completion_cache.read().await;
1213 let text_cache = self.text_cache.read().await;
1214 if let Some(cc) = comp_cache.get(&uri.to_string()) {
1215 goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
1216 } else {
1217 None
1218 }
1219 };
1220
1221 if let Some(location) = ts_result {
1222 if validate_ts(&location) {
1223 self.client
1224 .log_message(
1225 MessageType::INFO,
1226 format!(
1227 "found definition (tree-sitter fallback) at {}:{}",
1228 location.uri, location.range.start.line
1229 ),
1230 )
1231 .await;
1232 return Ok(Some(GotoDefinitionResponse::from(location)));
1233 }
1234 self.client
1235 .log_message(MessageType::INFO, "tree-sitter fallback failed validation")
1236 .await;
1237 }
1238 }
1239
1240 self.client
1241 .log_message(MessageType::INFO, "no definition found")
1242 .await;
1243 Ok(None)
1244 }
1245
1246 async fn goto_declaration(
1247 &self,
1248 params: request::GotoDeclarationParams,
1249 ) -> tower_lsp::jsonrpc::Result<Option<request::GotoDeclarationResponse>> {
1250 self.client
1251 .log_message(MessageType::INFO, "got textDocument/declaration request")
1252 .await;
1253
1254 let uri = params.text_document_position_params.text_document.uri;
1255 let position = params.text_document_position_params.position;
1256
1257 let file_path = match uri.to_file_path() {
1258 Ok(path) => path,
1259 Err(_) => {
1260 self.client
1261 .log_message(MessageType::ERROR, "invalid file uri")
1262 .await;
1263 return Ok(None);
1264 }
1265 };
1266
1267 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1268 Some(bytes) => bytes,
1269 None => return Ok(None),
1270 };
1271
1272 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1273 let cached_build = match cached_build {
1274 Some(cb) => cb,
1275 None => return Ok(None),
1276 };
1277
1278 if let Some(location) =
1279 goto::goto_declaration(&cached_build.ast, &uri, position, &source_bytes)
1280 {
1281 self.client
1282 .log_message(
1283 MessageType::INFO,
1284 format!(
1285 "found declaration at {}:{}",
1286 location.uri, location.range.start.line
1287 ),
1288 )
1289 .await;
1290 Ok(Some(request::GotoDeclarationResponse::from(location)))
1291 } else {
1292 self.client
1293 .log_message(MessageType::INFO, "no declaration found")
1294 .await;
1295 Ok(None)
1296 }
1297 }
1298
1299 async fn references(
1300 &self,
1301 params: ReferenceParams,
1302 ) -> tower_lsp::jsonrpc::Result<Option<Vec<Location>>> {
1303 self.client
1304 .log_message(MessageType::INFO, "Got a textDocument/references request")
1305 .await;
1306
1307 let uri = params.text_document_position.text_document.uri;
1308 let position = params.text_document_position.position;
1309 let file_path = match uri.to_file_path() {
1310 Ok(path) => path,
1311 Err(_) => {
1312 self.client
1313 .log_message(MessageType::ERROR, "Invalid file URI")
1314 .await;
1315 return Ok(None);
1316 }
1317 };
1318 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1319 Some(bytes) => bytes,
1320 None => return Ok(None),
1321 };
1322 let cached_build = self.get_or_fetch_build(&uri, &file_path, true).await;
1323 let cached_build = match cached_build {
1324 Some(cb) => cb,
1325 None => return Ok(None),
1326 };
1327
1328 let mut locations = references::goto_references(
1330 &cached_build.ast,
1331 &uri,
1332 position,
1333 &source_bytes,
1334 params.context.include_declaration,
1335 );
1336
1337 if let Some((def_abs_path, def_byte_offset)) =
1339 references::resolve_target_location(&cached_build, &uri, position, &source_bytes)
1340 {
1341 let cache = self.ast_cache.read().await;
1342 for (cached_uri, other_build) in cache.iter() {
1343 if *cached_uri == uri.to_string() {
1344 continue;
1345 }
1346 let other_locations = references::goto_references_for_target(
1347 other_build,
1348 &def_abs_path,
1349 def_byte_offset,
1350 None,
1351 params.context.include_declaration,
1352 );
1353 locations.extend(other_locations);
1354 }
1355 }
1356
1357 let mut seen = std::collections::HashSet::new();
1359 locations.retain(|loc| {
1360 seen.insert((
1361 loc.uri.clone(),
1362 loc.range.start.line,
1363 loc.range.start.character,
1364 loc.range.end.line,
1365 loc.range.end.character,
1366 ))
1367 });
1368
1369 if locations.is_empty() {
1370 self.client
1371 .log_message(MessageType::INFO, "No references found")
1372 .await;
1373 Ok(None)
1374 } else {
1375 self.client
1376 .log_message(
1377 MessageType::INFO,
1378 format!("Found {} references", locations.len()),
1379 )
1380 .await;
1381 Ok(Some(locations))
1382 }
1383 }
1384
1385 async fn prepare_rename(
1386 &self,
1387 params: TextDocumentPositionParams,
1388 ) -> tower_lsp::jsonrpc::Result<Option<PrepareRenameResponse>> {
1389 self.client
1390 .log_message(MessageType::INFO, "got textDocument/prepareRename request")
1391 .await;
1392
1393 let uri = params.text_document.uri;
1394 let position = params.position;
1395
1396 let file_path = match uri.to_file_path() {
1397 Ok(path) => path,
1398 Err(_) => {
1399 self.client
1400 .log_message(MessageType::ERROR, "invalid file uri")
1401 .await;
1402 return Ok(None);
1403 }
1404 };
1405
1406 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1407 Some(bytes) => bytes,
1408 None => return Ok(None),
1409 };
1410
1411 if let Some(range) = rename::get_identifier_range(&source_bytes, position) {
1412 self.client
1413 .log_message(
1414 MessageType::INFO,
1415 format!(
1416 "prepare rename range: {}:{}",
1417 range.start.line, range.start.character
1418 ),
1419 )
1420 .await;
1421 Ok(Some(PrepareRenameResponse::Range(range)))
1422 } else {
1423 self.client
1424 .log_message(MessageType::INFO, "no identifier found for prepare rename")
1425 .await;
1426 Ok(None)
1427 }
1428 }
1429
1430 async fn rename(
1431 &self,
1432 params: RenameParams,
1433 ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
1434 self.client
1435 .log_message(MessageType::INFO, "got textDocument/rename request")
1436 .await;
1437
1438 let uri = params.text_document_position.text_document.uri;
1439 let position = params.text_document_position.position;
1440 let new_name = params.new_name;
1441 let file_path = match uri.to_file_path() {
1442 Ok(p) => p,
1443 Err(_) => {
1444 self.client
1445 .log_message(MessageType::ERROR, "invalid file uri")
1446 .await;
1447 return Ok(None);
1448 }
1449 };
1450 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1451 Some(bytes) => bytes,
1452 None => return Ok(None),
1453 };
1454
1455 let current_identifier = match rename::get_identifier_at_position(&source_bytes, position) {
1456 Some(id) => id,
1457 None => {
1458 self.client
1459 .log_message(MessageType::ERROR, "No identifier found at position")
1460 .await;
1461 return Ok(None);
1462 }
1463 };
1464
1465 if !utils::is_valid_solidity_identifier(&new_name) {
1466 return Err(tower_lsp::jsonrpc::Error::invalid_params(
1467 "new name is not a valid solidity identifier",
1468 ));
1469 }
1470
1471 if new_name == current_identifier {
1472 self.client
1473 .log_message(
1474 MessageType::INFO,
1475 "new name is the same as current identifier",
1476 )
1477 .await;
1478 return Ok(None);
1479 }
1480
1481 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1482 let cached_build = match cached_build {
1483 Some(cb) => cb,
1484 None => return Ok(None),
1485 };
1486 let other_builds: Vec<Arc<goto::CachedBuild>> = {
1487 let cache = self.ast_cache.read().await;
1488 cache
1489 .iter()
1490 .filter(|(key, _)| **key != uri.to_string())
1491 .map(|(_, v)| v.clone())
1492 .collect()
1493 };
1494 let other_refs: Vec<&goto::CachedBuild> = other_builds.iter().map(|v| v.as_ref()).collect();
1495
1496 let text_buffers: HashMap<String, Vec<u8>> = {
1500 let text_cache = self.text_cache.read().await;
1501 text_cache
1502 .iter()
1503 .map(|(uri, (_, content))| (uri.clone(), content.as_bytes().to_vec()))
1504 .collect()
1505 };
1506
1507 match rename::rename_symbol(
1508 &cached_build,
1509 &uri,
1510 position,
1511 &source_bytes,
1512 new_name,
1513 &other_refs,
1514 &text_buffers,
1515 ) {
1516 Some(workspace_edit) => {
1517 self.client
1518 .log_message(
1519 MessageType::INFO,
1520 format!(
1521 "created rename edit with {} file(s), {} total change(s)",
1522 workspace_edit
1523 .changes
1524 .as_ref()
1525 .map(|c| c.len())
1526 .unwrap_or(0),
1527 workspace_edit
1528 .changes
1529 .as_ref()
1530 .map(|c| c.values().map(|v| v.len()).sum::<usize>())
1531 .unwrap_or(0)
1532 ),
1533 )
1534 .await;
1535
1536 Ok(Some(workspace_edit))
1541 }
1542
1543 None => {
1544 self.client
1545 .log_message(MessageType::INFO, "No locations found for renaming")
1546 .await;
1547 Ok(None)
1548 }
1549 }
1550 }
1551
1552 async fn symbol(
1553 &self,
1554 params: WorkspaceSymbolParams,
1555 ) -> tower_lsp::jsonrpc::Result<Option<Vec<SymbolInformation>>> {
1556 self.client
1557 .log_message(MessageType::INFO, "got workspace/symbol request")
1558 .await;
1559
1560 let files: Vec<(Url, String)> = {
1562 let cache = self.text_cache.read().await;
1563 cache
1564 .iter()
1565 .filter(|(uri_str, _)| uri_str.ends_with(".sol"))
1566 .filter_map(|(uri_str, (_, content))| {
1567 Url::parse(uri_str).ok().map(|uri| (uri, content.clone()))
1568 })
1569 .collect()
1570 };
1571
1572 let mut all_symbols = symbols::extract_workspace_symbols(&files);
1573 if !params.query.is_empty() {
1574 let query = params.query.to_lowercase();
1575 all_symbols.retain(|symbol| symbol.name.to_lowercase().contains(&query));
1576 }
1577 if all_symbols.is_empty() {
1578 self.client
1579 .log_message(MessageType::INFO, "No symbols found")
1580 .await;
1581 Ok(None)
1582 } else {
1583 self.client
1584 .log_message(
1585 MessageType::INFO,
1586 format!("found {} symbols", all_symbols.len()),
1587 )
1588 .await;
1589 Ok(Some(all_symbols))
1590 }
1591 }
1592
1593 async fn document_symbol(
1594 &self,
1595 params: DocumentSymbolParams,
1596 ) -> tower_lsp::jsonrpc::Result<Option<DocumentSymbolResponse>> {
1597 self.client
1598 .log_message(MessageType::INFO, "got textDocument/documentSymbol request")
1599 .await;
1600 let uri = params.text_document.uri;
1601 let file_path = match uri.to_file_path() {
1602 Ok(path) => path,
1603 Err(_) => {
1604 self.client
1605 .log_message(MessageType::ERROR, "invalid file uri")
1606 .await;
1607 return Ok(None);
1608 }
1609 };
1610
1611 let source = {
1613 let cache = self.text_cache.read().await;
1614 cache
1615 .get(&uri.to_string())
1616 .map(|(_, content)| content.clone())
1617 };
1618 let source = match source {
1619 Some(s) => s,
1620 None => match std::fs::read_to_string(&file_path) {
1621 Ok(s) => s,
1622 Err(_) => return Ok(None),
1623 },
1624 };
1625
1626 let symbols = symbols::extract_document_symbols(&source);
1627 if symbols.is_empty() {
1628 self.client
1629 .log_message(MessageType::INFO, "no document symbols found")
1630 .await;
1631 Ok(None)
1632 } else {
1633 self.client
1634 .log_message(
1635 MessageType::INFO,
1636 format!("found {} document symbols", symbols.len()),
1637 )
1638 .await;
1639 Ok(Some(DocumentSymbolResponse::Nested(symbols)))
1640 }
1641 }
1642
1643 async fn hover(&self, params: HoverParams) -> tower_lsp::jsonrpc::Result<Option<Hover>> {
1644 self.client
1645 .log_message(MessageType::INFO, "got textDocument/hover request")
1646 .await;
1647
1648 let uri = params.text_document_position_params.text_document.uri;
1649 let position = params.text_document_position_params.position;
1650
1651 let file_path = match uri.to_file_path() {
1652 Ok(path) => path,
1653 Err(_) => {
1654 self.client
1655 .log_message(MessageType::ERROR, "invalid file uri")
1656 .await;
1657 return Ok(None);
1658 }
1659 };
1660
1661 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1662 Some(bytes) => bytes,
1663 None => return Ok(None),
1664 };
1665
1666 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1667 let cached_build = match cached_build {
1668 Some(cb) => cb,
1669 None => return Ok(None),
1670 };
1671
1672 let result = hover::hover_info(
1673 &cached_build.ast,
1674 &uri,
1675 position,
1676 &source_bytes,
1677 &cached_build.gas_index,
1678 &cached_build.doc_index,
1679 &cached_build.hint_index,
1680 );
1681
1682 if result.is_some() {
1683 self.client
1684 .log_message(MessageType::INFO, "hover info found")
1685 .await;
1686 } else {
1687 self.client
1688 .log_message(MessageType::INFO, "no hover info found")
1689 .await;
1690 }
1691
1692 Ok(result)
1693 }
1694
1695 async fn signature_help(
1696 &self,
1697 params: SignatureHelpParams,
1698 ) -> tower_lsp::jsonrpc::Result<Option<SignatureHelp>> {
1699 self.client
1700 .log_message(MessageType::INFO, "got textDocument/signatureHelp request")
1701 .await;
1702
1703 let uri = params.text_document_position_params.text_document.uri;
1704 let position = params.text_document_position_params.position;
1705
1706 let file_path = match uri.to_file_path() {
1707 Ok(path) => path,
1708 Err(_) => {
1709 self.client
1710 .log_message(MessageType::ERROR, "invalid file uri")
1711 .await;
1712 return Ok(None);
1713 }
1714 };
1715
1716 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1717 Some(bytes) => bytes,
1718 None => return Ok(None),
1719 };
1720
1721 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1722 let cached_build = match cached_build {
1723 Some(cb) => cb,
1724 None => return Ok(None),
1725 };
1726
1727 let result = hover::signature_help(
1728 &cached_build.ast,
1729 &source_bytes,
1730 position,
1731 &cached_build.hint_index,
1732 &cached_build.doc_index,
1733 );
1734
1735 Ok(result)
1736 }
1737
1738 async fn document_link(
1739 &self,
1740 params: DocumentLinkParams,
1741 ) -> tower_lsp::jsonrpc::Result<Option<Vec<DocumentLink>>> {
1742 self.client
1743 .log_message(MessageType::INFO, "got textDocument/documentLink request")
1744 .await;
1745
1746 let uri = params.text_document.uri;
1747 let file_path = match uri.to_file_path() {
1748 Ok(path) => path,
1749 Err(_) => {
1750 self.client
1751 .log_message(MessageType::ERROR, "invalid file uri")
1752 .await;
1753 return Ok(None);
1754 }
1755 };
1756
1757 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1758 Some(bytes) => bytes,
1759 None => return Ok(None),
1760 };
1761
1762 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1763 let cached_build = match cached_build {
1764 Some(cb) => cb,
1765 None => return Ok(None),
1766 };
1767
1768 let result = links::document_links(&cached_build, &uri, &source_bytes);
1769
1770 if result.is_empty() {
1771 self.client
1772 .log_message(MessageType::INFO, "no document links found")
1773 .await;
1774 Ok(None)
1775 } else {
1776 self.client
1777 .log_message(
1778 MessageType::INFO,
1779 format!("found {} document links", result.len()),
1780 )
1781 .await;
1782 Ok(Some(result))
1783 }
1784 }
1785
1786 async fn semantic_tokens_full(
1787 &self,
1788 params: SemanticTokensParams,
1789 ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensResult>> {
1790 self.client
1791 .log_message(
1792 MessageType::INFO,
1793 "got textDocument/semanticTokens/full request",
1794 )
1795 .await;
1796
1797 let uri = params.text_document.uri;
1798 let source = {
1799 let cache = self.text_cache.read().await;
1800 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
1801 };
1802
1803 let source = match source {
1804 Some(s) => s,
1805 None => {
1806 let file_path = match uri.to_file_path() {
1808 Ok(p) => p,
1809 Err(_) => return Ok(None),
1810 };
1811 match std::fs::read_to_string(&file_path) {
1812 Ok(s) => s,
1813 Err(_) => return Ok(None),
1814 }
1815 }
1816 };
1817
1818 let mut tokens = semantic_tokens::semantic_tokens_full(&source);
1819
1820 let id = self.semantic_token_id.fetch_add(1, Ordering::Relaxed);
1822 let result_id = id.to_string();
1823 tokens.result_id = Some(result_id.clone());
1824
1825 {
1826 let mut cache = self.semantic_token_cache.write().await;
1827 cache.insert(uri.to_string(), (result_id, tokens.data.clone()));
1828 }
1829
1830 Ok(Some(SemanticTokensResult::Tokens(tokens)))
1831 }
1832
1833 async fn semantic_tokens_range(
1834 &self,
1835 params: SemanticTokensRangeParams,
1836 ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensRangeResult>> {
1837 self.client
1838 .log_message(
1839 MessageType::INFO,
1840 "got textDocument/semanticTokens/range request",
1841 )
1842 .await;
1843
1844 let uri = params.text_document.uri;
1845 let range = params.range;
1846 let source = {
1847 let cache = self.text_cache.read().await;
1848 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
1849 };
1850
1851 let source = match source {
1852 Some(s) => s,
1853 None => {
1854 let file_path = match uri.to_file_path() {
1855 Ok(p) => p,
1856 Err(_) => return Ok(None),
1857 };
1858 match std::fs::read_to_string(&file_path) {
1859 Ok(s) => s,
1860 Err(_) => return Ok(None),
1861 }
1862 }
1863 };
1864
1865 let tokens =
1866 semantic_tokens::semantic_tokens_range(&source, range.start.line, range.end.line);
1867
1868 Ok(Some(SemanticTokensRangeResult::Tokens(tokens)))
1869 }
1870
1871 async fn semantic_tokens_full_delta(
1872 &self,
1873 params: SemanticTokensDeltaParams,
1874 ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensFullDeltaResult>> {
1875 self.client
1876 .log_message(
1877 MessageType::INFO,
1878 "got textDocument/semanticTokens/full/delta request",
1879 )
1880 .await;
1881
1882 let uri = params.text_document.uri;
1883 let previous_result_id = params.previous_result_id;
1884
1885 let source = {
1886 let cache = self.text_cache.read().await;
1887 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
1888 };
1889
1890 let source = match source {
1891 Some(s) => s,
1892 None => {
1893 let file_path = match uri.to_file_path() {
1894 Ok(p) => p,
1895 Err(_) => return Ok(None),
1896 };
1897 match std::fs::read_to_string(&file_path) {
1898 Ok(s) => s,
1899 Err(_) => return Ok(None),
1900 }
1901 }
1902 };
1903
1904 let mut new_tokens = semantic_tokens::semantic_tokens_full(&source);
1905
1906 let id = self.semantic_token_id.fetch_add(1, Ordering::Relaxed);
1908 let new_result_id = id.to_string();
1909 new_tokens.result_id = Some(new_result_id.clone());
1910
1911 let uri_str = uri.to_string();
1912
1913 let old_tokens = {
1915 let cache = self.semantic_token_cache.read().await;
1916 cache
1917 .get(&uri_str)
1918 .filter(|(rid, _)| *rid == previous_result_id)
1919 .map(|(_, tokens)| tokens.clone())
1920 };
1921
1922 {
1924 let mut cache = self.semantic_token_cache.write().await;
1925 cache.insert(uri_str, (new_result_id.clone(), new_tokens.data.clone()));
1926 }
1927
1928 match old_tokens {
1929 Some(old) => {
1930 let edits = semantic_tokens::compute_delta(&old, &new_tokens.data);
1932 Ok(Some(SemanticTokensFullDeltaResult::TokensDelta(
1933 SemanticTokensDelta {
1934 result_id: Some(new_result_id),
1935 edits,
1936 },
1937 )))
1938 }
1939 None => {
1940 Ok(Some(SemanticTokensFullDeltaResult::Tokens(new_tokens)))
1942 }
1943 }
1944 }
1945
1946 async fn inlay_hint(
1947 &self,
1948 params: InlayHintParams,
1949 ) -> tower_lsp::jsonrpc::Result<Option<Vec<InlayHint>>> {
1950 self.client
1951 .log_message(MessageType::INFO, "got textDocument/inlayHint request")
1952 .await;
1953
1954 let uri = params.text_document.uri;
1955 let range = params.range;
1956
1957 let file_path = match uri.to_file_path() {
1958 Ok(path) => path,
1959 Err(_) => {
1960 self.client
1961 .log_message(MessageType::ERROR, "invalid file uri")
1962 .await;
1963 return Ok(None);
1964 }
1965 };
1966
1967 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1968 Some(bytes) => bytes,
1969 None => return Ok(None),
1970 };
1971
1972 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1973 let cached_build = match cached_build {
1974 Some(cb) => cb,
1975 None => return Ok(None),
1976 };
1977
1978 let mut hints = inlay_hints::inlay_hints(&cached_build, &uri, range, &source_bytes);
1979
1980 let settings = self.settings.read().await;
1982 if !settings.inlay_hints.parameters {
1983 hints.retain(|h| h.kind != Some(InlayHintKind::PARAMETER));
1984 }
1985 if !settings.inlay_hints.gas_estimates {
1986 hints.retain(|h| h.kind != Some(InlayHintKind::TYPE));
1987 }
1988
1989 if hints.is_empty() {
1990 self.client
1991 .log_message(MessageType::INFO, "no inlay hints found")
1992 .await;
1993 Ok(None)
1994 } else {
1995 self.client
1996 .log_message(
1997 MessageType::INFO,
1998 format!("found {} inlay hints", hints.len()),
1999 )
2000 .await;
2001 Ok(Some(hints))
2002 }
2003 }
2004}