1use crate::completion;
2use crate::config::{self, FoundryConfig, LintConfig, Settings};
3use crate::file_operations;
4use crate::folding;
5use crate::goto;
6use crate::highlight;
7use crate::hover;
8use crate::inlay_hints;
9use crate::links;
10use crate::references;
11use crate::rename;
12use crate::runner::{ForgeRunner, Runner};
13use crate::selection;
14use crate::semantic_tokens;
15use crate::symbols;
16use crate::utils;
17use std::collections::{HashMap, HashSet};
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU64, Ordering};
20use tokio::sync::RwLock;
21use tower_lsp::{Client, LanguageServer, lsp_types::*};
22
23type SemanticTokenCache = HashMap<String, (String, Vec<SemanticToken>)>;
25
26pub struct ForgeLsp {
27 client: Client,
28 compiler: Arc<dyn Runner>,
29 ast_cache: Arc<RwLock<HashMap<String, Arc<goto::CachedBuild>>>>,
30 text_cache: Arc<RwLock<HashMap<String, (i32, String)>>>,
34 completion_cache: Arc<RwLock<HashMap<String, Arc<completion::CompletionCache>>>>,
35 lint_config: Arc<RwLock<LintConfig>>,
37 foundry_config: Arc<RwLock<FoundryConfig>>,
39 client_capabilities: Arc<RwLock<Option<ClientCapabilities>>>,
41 settings: Arc<RwLock<Settings>>,
43 use_solc: bool,
45 semantic_token_cache: Arc<RwLock<SemanticTokenCache>>,
47 semantic_token_id: Arc<AtomicU64>,
49 root_uri: Arc<RwLock<Option<Url>>>,
51 project_indexed: Arc<std::sync::atomic::AtomicBool>,
53 pending_create_scaffold: Arc<RwLock<HashSet<String>>>,
56}
57
58impl ForgeLsp {
59 pub fn new(client: Client, use_solar: bool, use_solc: bool) -> Self {
60 let compiler: Arc<dyn Runner> = if use_solar {
61 Arc::new(crate::solar_runner::SolarRunner)
62 } else {
63 Arc::new(ForgeRunner)
64 };
65 let ast_cache = Arc::new(RwLock::new(HashMap::new()));
66 let text_cache = Arc::new(RwLock::new(HashMap::new()));
67 let completion_cache = Arc::new(RwLock::new(HashMap::new()));
68 let lint_config = Arc::new(RwLock::new(LintConfig::default()));
69 let foundry_config = Arc::new(RwLock::new(FoundryConfig::default()));
70 let client_capabilities = Arc::new(RwLock::new(None));
71 let settings = Arc::new(RwLock::new(Settings::default()));
72 Self {
73 client,
74 compiler,
75 ast_cache,
76 text_cache,
77 completion_cache,
78 lint_config,
79 foundry_config,
80 client_capabilities,
81 settings,
82 use_solc,
83 semantic_token_cache: Arc::new(RwLock::new(HashMap::new())),
84 semantic_token_id: Arc::new(AtomicU64::new(0)),
85 root_uri: Arc::new(RwLock::new(None)),
86 project_indexed: Arc::new(std::sync::atomic::AtomicBool::new(false)),
87 pending_create_scaffold: Arc::new(RwLock::new(HashSet::new())),
88 }
89 }
90
91 async fn foundry_config_for_file(&self, file_path: &std::path::Path) -> FoundryConfig {
99 config::load_foundry_config(file_path)
100 }
101
102 async fn on_change(&self, params: TextDocumentItem) {
103 let uri = params.uri.clone();
104 let version = params.version;
105
106 let file_path = match uri.to_file_path() {
107 Ok(path) => path,
108 Err(_) => {
109 self.client
110 .log_message(MessageType::ERROR, "Invalid file URI")
111 .await;
112 return;
113 }
114 };
115
116 let path_str = match file_path.to_str() {
117 Some(s) => s,
118 None => {
119 self.client
120 .log_message(MessageType::ERROR, "Invalid file path")
121 .await;
122 return;
123 }
124 };
125
126 let (should_lint, lint_settings) = {
128 let lint_cfg = self.lint_config.read().await;
129 let settings = self.settings.read().await;
130 let enabled = lint_cfg.should_lint(&file_path) && settings.lint.enabled;
131 let ls = settings.lint.clone();
132 (enabled, ls)
133 };
134
135 let (lint_result, build_result, ast_result) = if self.use_solc {
139 let foundry_cfg = self.foundry_config_for_file(&file_path).await;
140 let solc_future = crate::solc::solc_ast(path_str, &foundry_cfg, Some(&self.client));
141
142 if should_lint {
143 let (lint, solc) = tokio::join!(
144 self.compiler.get_lint_diagnostics(&uri, &lint_settings),
145 solc_future
146 );
147 match solc {
148 Ok(data) => {
149 self.client
150 .log_message(
151 MessageType::INFO,
152 "solc: AST + diagnostics from single run",
153 )
154 .await;
155 let content = tokio::fs::read_to_string(&file_path)
157 .await
158 .unwrap_or_default();
159 let build_diags = crate::build::build_output_to_diagnostics(
160 &data,
161 &file_path,
162 &content,
163 &foundry_cfg.ignored_error_codes,
164 );
165 (Some(lint), Ok(build_diags), Ok(data))
166 }
167 Err(e) => {
168 self.client
169 .log_message(
170 MessageType::WARNING,
171 format!("solc failed, falling back to forge: {e}"),
172 )
173 .await;
174 let (build, ast) = tokio::join!(
175 self.compiler.get_build_diagnostics(&uri),
176 self.compiler.ast(path_str)
177 );
178 (Some(lint), build, ast)
179 }
180 }
181 } else {
182 self.client
183 .log_message(
184 MessageType::INFO,
185 format!("skipping lint for ignored file: {path_str}"),
186 )
187 .await;
188 match solc_future.await {
189 Ok(data) => {
190 self.client
191 .log_message(
192 MessageType::INFO,
193 "solc: AST + diagnostics from single run",
194 )
195 .await;
196 let content = tokio::fs::read_to_string(&file_path)
197 .await
198 .unwrap_or_default();
199 let build_diags = crate::build::build_output_to_diagnostics(
200 &data,
201 &file_path,
202 &content,
203 &foundry_cfg.ignored_error_codes,
204 );
205 (None, Ok(build_diags), Ok(data))
206 }
207 Err(e) => {
208 self.client
209 .log_message(
210 MessageType::WARNING,
211 format!("solc failed, falling back to forge: {e}"),
212 )
213 .await;
214 let (build, ast) = tokio::join!(
215 self.compiler.get_build_diagnostics(&uri),
216 self.compiler.ast(path_str)
217 );
218 (None, build, ast)
219 }
220 }
221 }
222 } else {
223 if should_lint {
225 let (lint, build, ast) = tokio::join!(
226 self.compiler.get_lint_diagnostics(&uri, &lint_settings),
227 self.compiler.get_build_diagnostics(&uri),
228 self.compiler.ast(path_str)
229 );
230 (Some(lint), build, ast)
231 } else {
232 self.client
233 .log_message(
234 MessageType::INFO,
235 format!("skipping lint for ignored file: {path_str}"),
236 )
237 .await;
238 let (build, ast) = tokio::join!(
239 self.compiler.get_build_diagnostics(&uri),
240 self.compiler.ast(path_str)
241 );
242 (None, build, ast)
243 }
244 };
245
246 let build_succeeded = matches!(&build_result, Ok(diagnostics) if diagnostics.iter().all(|d| d.severity != Some(DiagnosticSeverity::ERROR)));
248
249 if build_succeeded {
250 if let Ok(ast_data) = ast_result {
251 let cached_build = Arc::new(goto::CachedBuild::new(ast_data, version));
252 let mut cache = self.ast_cache.write().await;
253 cache.insert(uri.to_string(), cached_build.clone());
254 drop(cache);
255
256 {
258 let mut cc = self.completion_cache.write().await;
259 cc.insert(uri.to_string(), cached_build.completion_cache.clone());
260 }
261 self.client
262 .log_message(MessageType::INFO, "Build successful, AST cache updated")
263 .await;
264 } else if let Err(e) = ast_result {
265 self.client
266 .log_message(
267 MessageType::INFO,
268 format!("Build succeeded but failed to get AST: {e}"),
269 )
270 .await;
271 }
272 } else {
273 self.client
275 .log_message(
276 MessageType::INFO,
277 "Build errors detected, keeping existing AST cache",
278 )
279 .await;
280 }
281
282 {
284 let mut text_cache = self.text_cache.write().await;
285 let uri_str = uri.to_string();
286 let existing_version = text_cache.get(&uri_str).map(|(v, _)| *v).unwrap_or(-1);
287 if version >= existing_version {
288 text_cache.insert(uri_str, (version, params.text));
289 }
290 }
291
292 let mut all_diagnostics = vec![];
293
294 if let Some(lint_result) = lint_result {
295 match lint_result {
296 Ok(mut lints) => {
297 if !lint_settings.exclude.is_empty() {
299 lints.retain(|d| {
300 if let Some(NumberOrString::String(code)) = &d.code {
301 !lint_settings.exclude.iter().any(|ex| ex == code)
302 } else {
303 true
304 }
305 });
306 }
307 self.client
308 .log_message(
309 MessageType::INFO,
310 format!("found {} lint diagnostics", lints.len()),
311 )
312 .await;
313 all_diagnostics.append(&mut lints);
314 }
315 Err(e) => {
316 self.client
317 .log_message(
318 MessageType::ERROR,
319 format!("Forge lint diagnostics failed: {e}"),
320 )
321 .await;
322 }
323 }
324 }
325
326 match build_result {
327 Ok(mut builds) => {
328 self.client
329 .log_message(
330 MessageType::INFO,
331 format!("found {} build diagnostics", builds.len()),
332 )
333 .await;
334 all_diagnostics.append(&mut builds);
335 }
336 Err(e) => {
337 self.client
338 .log_message(
339 MessageType::WARNING,
340 format!("Forge build diagnostics failed: {e}"),
341 )
342 .await;
343 }
344 }
345
346 for diag in &mut all_diagnostics {
350 if diag.message.is_empty() {
351 diag.message = "Unknown issue".to_string();
352 }
353 }
354
355 self.client
357 .publish_diagnostics(uri, all_diagnostics, None)
358 .await;
359
360 if build_succeeded {
362 let client = self.client.clone();
363 tokio::spawn(async move {
364 let _ = client.inlay_hint_refresh().await;
365 });
366 }
367
368 if build_succeeded
374 && self.use_solc
375 && !self
376 .project_indexed
377 .load(std::sync::atomic::Ordering::Relaxed)
378 {
379 self.project_indexed
380 .store(true, std::sync::atomic::Ordering::Relaxed);
381 let foundry_config = self.foundry_config.read().await.clone();
382 let root_uri = self.root_uri.read().await.clone();
383 let cache_key = root_uri.as_ref().map(|u| u.to_string());
384 let ast_cache = self.ast_cache.clone();
385 let client = self.client.clone();
386
387 tokio::spawn(async move {
388 let Some(cache_key) = cache_key else {
389 return;
390 };
391 if !foundry_config.root.is_dir() {
392 client
393 .log_message(
394 MessageType::INFO,
395 format!(
396 "project index: {} not found, skipping",
397 foundry_config.root.display(),
398 ),
399 )
400 .await;
401 return;
402 }
403
404 let token = NumberOrString::String("solidity/projectIndex".to_string());
406 let _ = client
407 .send_request::<request::WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
408 token: token.clone(),
409 })
410 .await;
411
412 client
414 .send_notification::<notification::Progress>(ProgressParams {
415 token: token.clone(),
416 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(
417 WorkDoneProgressBegin {
418 title: "Indexing project".to_string(),
419 message: Some("Discovering source files...".to_string()),
420 cancellable: Some(false),
421 percentage: None,
422 },
423 )),
424 })
425 .await;
426
427 match crate::solc::solc_project_index(&foundry_config, Some(&client), None).await {
428 Ok(ast_data) => {
429 let cached_build = Arc::new(crate::goto::CachedBuild::new(ast_data, 0));
430 let source_count = cached_build.nodes.len();
431 ast_cache.write().await.insert(cache_key, cached_build);
432 client
433 .log_message(
434 MessageType::INFO,
435 format!("project index: cached {} source files", source_count),
436 )
437 .await;
438
439 client
441 .send_notification::<notification::Progress>(ProgressParams {
442 token: token.clone(),
443 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
444 WorkDoneProgressEnd {
445 message: Some(format!(
446 "Indexed {} source files",
447 source_count
448 )),
449 },
450 )),
451 })
452 .await;
453 }
454 Err(e) => {
455 client
456 .log_message(MessageType::WARNING, format!("project index failed: {e}"))
457 .await;
458
459 client
461 .send_notification::<notification::Progress>(ProgressParams {
462 token: token.clone(),
463 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
464 WorkDoneProgressEnd {
465 message: Some("Indexing failed".to_string()),
466 },
467 )),
468 })
469 .await;
470 }
471 }
472 });
473 }
474 }
475
476 async fn get_or_fetch_build(
485 &self,
486 uri: &Url,
487 file_path: &std::path::Path,
488 insert_on_miss: bool,
489 ) -> Option<Arc<goto::CachedBuild>> {
490 let uri_str = uri.to_string();
491
492 {
495 let cache = self.ast_cache.read().await;
496 if let Some(cached) = cache.get(&uri_str) {
497 return Some(cached.clone());
498 }
499 }
500
501 if !insert_on_miss {
505 return None;
506 }
507
508 let path_str = file_path.to_str()?;
510 let ast_result = if self.use_solc {
511 let foundry_cfg = self.foundry_config_for_file(&file_path).await;
512 match crate::solc::solc_ast(path_str, &foundry_cfg, Some(&self.client)).await {
513 Ok(data) => Ok(data),
514 Err(_) => self.compiler.ast(path_str).await,
515 }
516 } else {
517 self.compiler.ast(path_str).await
518 };
519 match ast_result {
520 Ok(data) => {
521 let build = Arc::new(goto::CachedBuild::new(data, 0));
524 let mut cache = self.ast_cache.write().await;
525 cache.insert(uri_str.clone(), build.clone());
526 Some(build)
527 }
528 Err(e) => {
529 self.client
530 .log_message(MessageType::ERROR, format!("failed to get AST: {e}"))
531 .await;
532 None
533 }
534 }
535 }
536
537 async fn get_source_bytes(&self, uri: &Url, file_path: &std::path::Path) -> Option<Vec<u8>> {
540 {
541 let text_cache = self.text_cache.read().await;
542 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
543 return Some(content.as_bytes().to_vec());
544 }
545 }
546 match std::fs::read(file_path) {
547 Ok(bytes) => Some(bytes),
548 Err(e) => {
549 if e.kind() == std::io::ErrorKind::NotFound {
550 self.client
553 .log_message(
554 MessageType::INFO,
555 format!("file not found yet (transient): {e}"),
556 )
557 .await;
558 } else {
559 self.client
560 .log_message(MessageType::ERROR, format!("failed to read file: {e}"))
561 .await;
562 }
563 None
564 }
565 }
566 }
567}
568
569fn update_imports_on_delete_enabled(settings: &crate::config::Settings) -> bool {
570 settings.file_operations.update_imports_on_delete
571}
572
573#[tower_lsp::async_trait]
574impl LanguageServer for ForgeLsp {
575 async fn initialize(
576 &self,
577 params: InitializeParams,
578 ) -> tower_lsp::jsonrpc::Result<InitializeResult> {
579 {
581 let mut caps = self.client_capabilities.write().await;
582 *caps = Some(params.capabilities.clone());
583 }
584
585 if let Some(init_opts) = ¶ms.initialization_options {
587 let s = config::parse_settings(init_opts);
588 self.client
589 .log_message(
590 MessageType::INFO,
591 format!(
592 "settings: inlayHints.parameters={}, inlayHints.gasEstimates={}, lint.enabled={}, lint.severity={:?}, lint.only={:?}, lint.exclude={:?}, fileOperations.templateOnCreate={}, fileOperations.updateImportsOnRename={}, fileOperations.updateImportsOnDelete={}",
593 s.inlay_hints.parameters, s.inlay_hints.gas_estimates, s.lint.enabled, s.lint.severity, s.lint.only, s.lint.exclude, s.file_operations.template_on_create, s.file_operations.update_imports_on_rename, s.file_operations.update_imports_on_delete,
594 ),
595 )
596 .await;
597 let mut settings = self.settings.write().await;
598 *settings = s;
599 }
600
601 if let Some(uri) = params.root_uri.as_ref() {
603 let mut root = self.root_uri.write().await;
604 *root = Some(uri.clone());
605 }
606
607 if let Some(root_uri) = params
609 .root_uri
610 .as_ref()
611 .and_then(|uri| uri.to_file_path().ok())
612 {
613 let lint_cfg = config::load_lint_config(&root_uri);
614 self.client
615 .log_message(
616 MessageType::INFO,
617 format!(
618 "loaded foundry.toml lint config: lint_on_build={}, ignore_patterns={}",
619 lint_cfg.lint_on_build,
620 lint_cfg.ignore_patterns.len()
621 ),
622 )
623 .await;
624 let mut config = self.lint_config.write().await;
625 *config = lint_cfg;
626
627 let foundry_cfg = config::load_foundry_config(&root_uri);
628 self.client
629 .log_message(
630 MessageType::INFO,
631 format!(
632 "loaded foundry.toml project config: solc_version={:?}, remappings={}",
633 foundry_cfg.solc_version,
634 foundry_cfg.remappings.len()
635 ),
636 )
637 .await;
638 if foundry_cfg.via_ir {
639 self.client
640 .log_message(
641 MessageType::WARNING,
642 "via_ir is enabled in foundry.toml — gas estimate inlay hints are disabled to avoid slow compilation",
643 )
644 .await;
645 }
646 let mut fc = self.foundry_config.write().await;
647 *fc = foundry_cfg;
648 }
649
650 let client_encodings = params
652 .capabilities
653 .general
654 .as_ref()
655 .and_then(|g| g.position_encodings.as_deref());
656 let encoding = utils::PositionEncoding::negotiate(client_encodings);
657 utils::set_encoding(encoding);
658
659 Ok(InitializeResult {
660 server_info: Some(ServerInfo {
661 name: "Solidity Language Server".to_string(),
662 version: Some(env!("LONG_VERSION").to_string()),
663 }),
664 capabilities: ServerCapabilities {
665 position_encoding: Some(encoding.into()),
666 completion_provider: Some(CompletionOptions {
667 trigger_characters: Some(vec![".".to_string()]),
668 resolve_provider: Some(false),
669 ..Default::default()
670 }),
671 signature_help_provider: Some(SignatureHelpOptions {
672 trigger_characters: Some(vec![
673 "(".to_string(),
674 ",".to_string(),
675 "[".to_string(),
676 ]),
677 retrigger_characters: None,
678 work_done_progress_options: WorkDoneProgressOptions {
679 work_done_progress: None,
680 },
681 }),
682 definition_provider: Some(OneOf::Left(true)),
683 declaration_provider: Some(DeclarationCapability::Simple(true)),
684 references_provider: Some(OneOf::Left(true)),
685 rename_provider: Some(OneOf::Right(RenameOptions {
686 prepare_provider: Some(true),
687 work_done_progress_options: WorkDoneProgressOptions {
688 work_done_progress: Some(true),
689 },
690 })),
691 workspace_symbol_provider: Some(OneOf::Left(true)),
692 document_symbol_provider: Some(OneOf::Left(true)),
693 document_highlight_provider: Some(OneOf::Left(true)),
694 hover_provider: Some(HoverProviderCapability::Simple(true)),
695 document_link_provider: Some(DocumentLinkOptions {
696 resolve_provider: Some(false),
697 work_done_progress_options: WorkDoneProgressOptions {
698 work_done_progress: None,
699 },
700 }),
701 document_formatting_provider: Some(OneOf::Left(true)),
702 code_lens_provider: None,
703 folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
704 selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
705 inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options(
706 InlayHintOptions {
707 resolve_provider: Some(false),
708 work_done_progress_options: WorkDoneProgressOptions {
709 work_done_progress: None,
710 },
711 },
712 ))),
713 semantic_tokens_provider: Some(
714 SemanticTokensServerCapabilities::SemanticTokensOptions(
715 SemanticTokensOptions {
716 legend: semantic_tokens::legend(),
717 full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
718 range: Some(true),
719 work_done_progress_options: WorkDoneProgressOptions {
720 work_done_progress: None,
721 },
722 },
723 ),
724 ),
725 text_document_sync: Some(TextDocumentSyncCapability::Options(
726 TextDocumentSyncOptions {
727 will_save: Some(true),
728 will_save_wait_until: None,
729 open_close: Some(true),
730 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
731 include_text: Some(true),
732 })),
733 change: Some(TextDocumentSyncKind::FULL),
734 },
735 )),
736 workspace: Some(WorkspaceServerCapabilities {
737 workspace_folders: None,
738 file_operations: Some(WorkspaceFileOperationsServerCapabilities {
739 will_rename: Some(FileOperationRegistrationOptions {
740 filters: vec![
741 FileOperationFilter {
743 scheme: Some("file".to_string()),
744 pattern: FileOperationPattern {
745 glob: "**/*.sol".to_string(),
746 matches: Some(FileOperationPatternKind::File),
747 options: None,
748 },
749 },
750 FileOperationFilter {
752 scheme: Some("file".to_string()),
753 pattern: FileOperationPattern {
754 glob: "**".to_string(),
755 matches: Some(FileOperationPatternKind::Folder),
756 options: None,
757 },
758 },
759 ],
760 }),
761 did_rename: Some(FileOperationRegistrationOptions {
762 filters: vec![
763 FileOperationFilter {
764 scheme: Some("file".to_string()),
765 pattern: FileOperationPattern {
766 glob: "**/*.sol".to_string(),
767 matches: Some(FileOperationPatternKind::File),
768 options: None,
769 },
770 },
771 FileOperationFilter {
772 scheme: Some("file".to_string()),
773 pattern: FileOperationPattern {
774 glob: "**".to_string(),
775 matches: Some(FileOperationPatternKind::Folder),
776 options: None,
777 },
778 },
779 ],
780 }),
781 will_delete: Some(FileOperationRegistrationOptions {
782 filters: vec![
783 FileOperationFilter {
784 scheme: Some("file".to_string()),
785 pattern: FileOperationPattern {
786 glob: "**/*.sol".to_string(),
787 matches: Some(FileOperationPatternKind::File),
788 options: None,
789 },
790 },
791 FileOperationFilter {
792 scheme: Some("file".to_string()),
793 pattern: FileOperationPattern {
794 glob: "**".to_string(),
795 matches: Some(FileOperationPatternKind::Folder),
796 options: None,
797 },
798 },
799 ],
800 }),
801 did_delete: Some(FileOperationRegistrationOptions {
802 filters: vec![
803 FileOperationFilter {
804 scheme: Some("file".to_string()),
805 pattern: FileOperationPattern {
806 glob: "**/*.sol".to_string(),
807 matches: Some(FileOperationPatternKind::File),
808 options: None,
809 },
810 },
811 FileOperationFilter {
812 scheme: Some("file".to_string()),
813 pattern: FileOperationPattern {
814 glob: "**".to_string(),
815 matches: Some(FileOperationPatternKind::Folder),
816 options: None,
817 },
818 },
819 ],
820 }),
821 will_create: Some(FileOperationRegistrationOptions {
822 filters: vec![FileOperationFilter {
823 scheme: Some("file".to_string()),
824 pattern: FileOperationPattern {
825 glob: "**/*.sol".to_string(),
826 matches: Some(FileOperationPatternKind::File),
827 options: None,
828 },
829 }],
830 }),
831 did_create: Some(FileOperationRegistrationOptions {
832 filters: vec![FileOperationFilter {
833 scheme: Some("file".to_string()),
834 pattern: FileOperationPattern {
835 glob: "**/*.sol".to_string(),
836 matches: Some(FileOperationPatternKind::File),
837 options: None,
838 },
839 }],
840 }),
841 ..Default::default()
842 }),
843 }),
844 ..ServerCapabilities::default()
845 },
846 })
847 }
848
849 async fn initialized(&self, _: InitializedParams) {
850 self.client
851 .log_message(MessageType::INFO, "lsp server initialized.")
852 .await;
853
854 let supports_dynamic = self
856 .client_capabilities
857 .read()
858 .await
859 .as_ref()
860 .and_then(|caps| caps.workspace.as_ref())
861 .and_then(|ws| ws.did_change_watched_files.as_ref())
862 .and_then(|dcwf| dcwf.dynamic_registration)
863 .unwrap_or(false);
864
865 if supports_dynamic {
866 let registration = Registration {
867 id: "foundry-toml-watcher".to_string(),
868 method: "workspace/didChangeWatchedFiles".to_string(),
869 register_options: Some(
870 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
871 watchers: vec![
872 FileSystemWatcher {
873 glob_pattern: GlobPattern::String("**/foundry.toml".to_string()),
874 kind: Some(WatchKind::all()),
875 },
876 FileSystemWatcher {
877 glob_pattern: GlobPattern::String("**/remappings.txt".to_string()),
878 kind: Some(WatchKind::all()),
879 },
880 ],
881 })
882 .unwrap(),
883 ),
884 };
885
886 if let Err(e) = self.client.register_capability(vec![registration]).await {
887 self.client
888 .log_message(
889 MessageType::WARNING,
890 format!("failed to register foundry.toml watcher: {e}"),
891 )
892 .await;
893 } else {
894 self.client
895 .log_message(MessageType::INFO, "registered foundry.toml file watcher")
896 .await;
897 }
898 }
899
900 if self.use_solc {
904 self.project_indexed
905 .store(true, std::sync::atomic::Ordering::Relaxed);
906 let foundry_config = self.foundry_config.read().await.clone();
907 let root_uri = self.root_uri.read().await.clone();
908 let cache_key = root_uri.as_ref().map(|u| u.to_string());
909 let ast_cache = self.ast_cache.clone();
910 let client = self.client.clone();
911
912 tokio::spawn(async move {
913 let Some(cache_key) = cache_key else {
914 return;
915 };
916 if !foundry_config.root.is_dir() {
917 client
918 .log_message(
919 MessageType::INFO,
920 format!(
921 "project index: {} not found, skipping eager index",
922 foundry_config.root.display(),
923 ),
924 )
925 .await;
926 return;
927 }
928
929 let token = NumberOrString::String("solidity/projectIndex".to_string());
930 let _ = client
931 .send_request::<request::WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
932 token: token.clone(),
933 })
934 .await;
935
936 client
937 .send_notification::<notification::Progress>(ProgressParams {
938 token: token.clone(),
939 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(
940 WorkDoneProgressBegin {
941 title: "Indexing project".to_string(),
942 message: Some("Discovering source files...".to_string()),
943 cancellable: Some(false),
944 percentage: None,
945 },
946 )),
947 })
948 .await;
949
950 match crate::solc::solc_project_index(&foundry_config, Some(&client), None).await {
951 Ok(ast_data) => {
952 let cached_build = Arc::new(crate::goto::CachedBuild::new(ast_data, 0));
953 let source_count = cached_build.nodes.len();
954 ast_cache.write().await.insert(cache_key, cached_build);
955 client
956 .log_message(
957 MessageType::INFO,
958 format!(
959 "project index (eager): cached {} source files",
960 source_count
961 ),
962 )
963 .await;
964
965 client
966 .send_notification::<notification::Progress>(ProgressParams {
967 token: token.clone(),
968 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
969 WorkDoneProgressEnd {
970 message: Some(format!(
971 "Indexed {} source files",
972 source_count
973 )),
974 },
975 )),
976 })
977 .await;
978 }
979 Err(e) => {
980 client
981 .log_message(
982 MessageType::WARNING,
983 format!("project index (eager): failed: {e}"),
984 )
985 .await;
986
987 client
988 .send_notification::<notification::Progress>(ProgressParams {
989 token,
990 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
991 WorkDoneProgressEnd {
992 message: Some(format!("Index failed: {e}")),
993 },
994 )),
995 })
996 .await;
997 }
998 }
999 });
1000 }
1001 }
1002
1003 async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
1004 self.client
1005 .log_message(MessageType::INFO, "lsp server shutting down.")
1006 .await;
1007 Ok(())
1008 }
1009
1010 async fn did_open(&self, params: DidOpenTextDocumentParams) {
1011 self.client
1012 .log_message(MessageType::INFO, "file opened")
1013 .await;
1014
1015 let mut td = params.text_document;
1016 let template_on_create = self
1017 .settings
1018 .read()
1019 .await
1020 .file_operations
1021 .template_on_create;
1022
1023 let should_attempt_scaffold = template_on_create
1026 && td.text.chars().all(|ch| ch.is_whitespace())
1027 && td.uri.scheme() == "file"
1028 && td
1029 .uri
1030 .to_file_path()
1031 .ok()
1032 .and_then(|p| p.extension().map(|e| e == "sol"))
1033 .unwrap_or(false);
1034
1035 if should_attempt_scaffold {
1036 let uri_str = td.uri.to_string();
1037 let create_flow_pending = {
1038 let pending = self.pending_create_scaffold.read().await;
1039 pending.contains(&uri_str)
1040 };
1041 if create_flow_pending {
1042 self.client
1043 .log_message(
1044 MessageType::INFO,
1045 format!(
1046 "didOpen: skip scaffold for {} (didCreateFiles scaffold pending)",
1047 uri_str
1048 ),
1049 )
1050 .await;
1051 } else {
1052 let cache_has_content = {
1053 let tc = self.text_cache.read().await;
1054 tc.get(&uri_str)
1055 .map_or(false, |(_, c)| c.chars().any(|ch| !ch.is_whitespace()))
1056 };
1057
1058 if !cache_has_content {
1059 let file_has_content = td.uri.to_file_path().ok().is_some_and(|p| {
1060 std::fs::read_to_string(&p)
1061 .map_or(false, |c| c.chars().any(|ch| !ch.is_whitespace()))
1062 });
1063
1064 if !file_has_content {
1065 let solc_version = self.foundry_config.read().await.solc_version.clone();
1066 if let Some(scaffold) =
1067 file_operations::generate_scaffold(&td.uri, solc_version.as_deref())
1068 {
1069 let end = utils::byte_offset_to_position(&td.text, td.text.len());
1070 let edit = WorkspaceEdit {
1071 changes: Some(HashMap::from([(
1072 td.uri.clone(),
1073 vec![TextEdit {
1074 range: Range {
1075 start: Position::default(),
1076 end,
1077 },
1078 new_text: scaffold.clone(),
1079 }],
1080 )])),
1081 document_changes: None,
1082 change_annotations: None,
1083 };
1084 if self
1085 .client
1086 .apply_edit(edit)
1087 .await
1088 .as_ref()
1089 .is_ok_and(|r| r.applied)
1090 {
1091 td.text = scaffold;
1092 self.client
1093 .log_message(
1094 MessageType::INFO,
1095 format!("didOpen: scaffolded empty file {}", uri_str),
1096 )
1097 .await;
1098 }
1099 }
1100 }
1101 }
1102 }
1103 }
1104
1105 self.on_change(td).await
1106 }
1107
1108 async fn did_change(&self, params: DidChangeTextDocumentParams) {
1109 self.client
1110 .log_message(MessageType::INFO, "file changed")
1111 .await;
1112
1113 if let Some(change) = params.content_changes.into_iter().next() {
1115 let has_substantive_content = change.text.chars().any(|ch| !ch.is_whitespace());
1116 let mut text_cache = self.text_cache.write().await;
1117 text_cache.insert(
1118 params.text_document.uri.to_string(),
1119 (params.text_document.version, change.text),
1120 );
1121 drop(text_cache);
1122
1123 if has_substantive_content {
1124 self.pending_create_scaffold
1125 .write()
1126 .await
1127 .remove(params.text_document.uri.as_str());
1128 }
1129 }
1130 }
1131
1132 async fn did_save(&self, params: DidSaveTextDocumentParams) {
1133 self.client
1134 .log_message(MessageType::INFO, "file saved")
1135 .await;
1136
1137 let mut text_content = if let Some(text) = params.text {
1138 text
1139 } else {
1140 let cached = {
1142 let text_cache = self.text_cache.read().await;
1143 text_cache
1144 .get(params.text_document.uri.as_str())
1145 .map(|(_, content)| content.clone())
1146 };
1147 if let Some(content) = cached {
1148 content
1149 } else {
1150 match std::fs::read_to_string(params.text_document.uri.path()) {
1151 Ok(content) => content,
1152 Err(e) => {
1153 self.client
1154 .log_message(
1155 MessageType::ERROR,
1156 format!("Failed to read file on save: {e}"),
1157 )
1158 .await;
1159 return;
1160 }
1161 }
1162 }
1163 };
1164
1165 let uri_str = params.text_document.uri.to_string();
1169 let template_on_create = self
1170 .settings
1171 .read()
1172 .await
1173 .file_operations
1174 .template_on_create;
1175 let needs_recover_scaffold = {
1176 let pending = self.pending_create_scaffold.read().await;
1177 template_on_create
1178 && pending.contains(&uri_str)
1179 && !text_content.chars().any(|ch| !ch.is_whitespace())
1180 };
1181 if needs_recover_scaffold {
1182 let solc_version = self.foundry_config.read().await.solc_version.clone();
1183 if let Some(scaffold) = file_operations::generate_scaffold(
1184 ¶ms.text_document.uri,
1185 solc_version.as_deref(),
1186 ) {
1187 let end = utils::byte_offset_to_position(&text_content, text_content.len());
1188 let edit = WorkspaceEdit {
1189 changes: Some(HashMap::from([(
1190 params.text_document.uri.clone(),
1191 vec![TextEdit {
1192 range: Range {
1193 start: Position::default(),
1194 end,
1195 },
1196 new_text: scaffold.clone(),
1197 }],
1198 )])),
1199 document_changes: None,
1200 change_annotations: None,
1201 };
1202 if self
1203 .client
1204 .apply_edit(edit)
1205 .await
1206 .as_ref()
1207 .is_ok_and(|r| r.applied)
1208 {
1209 text_content = scaffold.clone();
1210 let version = self
1211 .text_cache
1212 .read()
1213 .await
1214 .get(params.text_document.uri.as_str())
1215 .map(|(v, _)| *v)
1216 .unwrap_or_default();
1217 self.text_cache
1218 .write()
1219 .await
1220 .insert(uri_str.clone(), (version, scaffold));
1221 self.pending_create_scaffold.write().await.remove(&uri_str);
1222 self.client
1223 .log_message(
1224 MessageType::INFO,
1225 format!("didSave: recovered scaffold for {}", uri_str),
1226 )
1227 .await;
1228 }
1229 }
1230 }
1231
1232 let version = self
1233 .text_cache
1234 .read()
1235 .await
1236 .get(params.text_document.uri.as_str())
1237 .map(|(version, _)| *version)
1238 .unwrap_or_default();
1239
1240 self.on_change(TextDocumentItem {
1241 uri: params.text_document.uri,
1242 text: text_content,
1243 version,
1244 language_id: "".to_string(),
1245 })
1246 .await;
1247 }
1248
1249 async fn will_save(&self, params: WillSaveTextDocumentParams) {
1250 self.client
1251 .log_message(
1252 MessageType::INFO,
1253 format!(
1254 "file will save reason:{:?} {}",
1255 params.reason, params.text_document.uri
1256 ),
1257 )
1258 .await;
1259 }
1260
1261 async fn formatting(
1262 &self,
1263 params: DocumentFormattingParams,
1264 ) -> tower_lsp::jsonrpc::Result<Option<Vec<TextEdit>>> {
1265 self.client
1266 .log_message(MessageType::INFO, "formatting request")
1267 .await;
1268
1269 let uri = params.text_document.uri;
1270 let file_path = match uri.to_file_path() {
1271 Ok(path) => path,
1272 Err(_) => {
1273 self.client
1274 .log_message(MessageType::ERROR, "Invalid file URI for formatting")
1275 .await;
1276 return Ok(None);
1277 }
1278 };
1279 let path_str = match file_path.to_str() {
1280 Some(s) => s,
1281 None => {
1282 self.client
1283 .log_message(MessageType::ERROR, "Invalid file path for formatting")
1284 .await;
1285 return Ok(None);
1286 }
1287 };
1288
1289 let original_content = {
1291 let text_cache = self.text_cache.read().await;
1292 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1293 content.clone()
1294 } else {
1295 match std::fs::read_to_string(&file_path) {
1297 Ok(content) => content,
1298 Err(_) => {
1299 self.client
1300 .log_message(MessageType::ERROR, "Failed to read file for formatting")
1301 .await;
1302 return Ok(None);
1303 }
1304 }
1305 }
1306 };
1307
1308 let formatted_content = match self.compiler.format(path_str).await {
1310 Ok(content) => content,
1311 Err(e) => {
1312 self.client
1313 .log_message(MessageType::WARNING, format!("Formatting failed: {e}"))
1314 .await;
1315 return Ok(None);
1316 }
1317 };
1318
1319 if original_content != formatted_content {
1321 let end = utils::byte_offset_to_position(&original_content, original_content.len());
1322
1323 {
1325 let mut text_cache = self.text_cache.write().await;
1326 let version = text_cache
1327 .get(&uri.to_string())
1328 .map(|(v, _)| *v)
1329 .unwrap_or(0);
1330 text_cache.insert(uri.to_string(), (version, formatted_content.clone()));
1331 }
1332
1333 let edit = TextEdit {
1334 range: Range {
1335 start: Position::default(),
1336 end,
1337 },
1338 new_text: formatted_content,
1339 };
1340 Ok(Some(vec![edit]))
1341 } else {
1342 Ok(None)
1343 }
1344 }
1345
1346 async fn did_close(&self, params: DidCloseTextDocumentParams) {
1347 let uri = params.text_document.uri.to_string();
1348 self.ast_cache.write().await.remove(&uri);
1349 self.text_cache.write().await.remove(&uri);
1350 self.completion_cache.write().await.remove(&uri);
1351 self.client
1352 .log_message(MessageType::INFO, "file closed, caches cleared.")
1353 .await;
1354 }
1355
1356 async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
1357 let s = config::parse_settings(¶ms.settings);
1358 self.client
1359 .log_message(
1360 MessageType::INFO,
1361 format!(
1362 "settings updated: inlayHints.parameters={}, inlayHints.gasEstimates={}, lint.enabled={}, lint.severity={:?}, lint.only={:?}, lint.exclude={:?}, fileOperations.templateOnCreate={}, fileOperations.updateImportsOnRename={}, fileOperations.updateImportsOnDelete={}",
1363 s.inlay_hints.parameters, s.inlay_hints.gas_estimates, s.lint.enabled, s.lint.severity, s.lint.only, s.lint.exclude, s.file_operations.template_on_create, s.file_operations.update_imports_on_rename, s.file_operations.update_imports_on_delete,
1364 ),
1365 )
1366 .await;
1367 let mut settings = self.settings.write().await;
1368 *settings = s;
1369
1370 let client = self.client.clone();
1372 tokio::spawn(async move {
1373 let _ = client.inlay_hint_refresh().await;
1374 });
1375 }
1376 async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {
1377 self.client
1378 .log_message(MessageType::INFO, "workdspace folders changed.")
1379 .await;
1380 }
1381
1382 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
1383 self.client
1384 .log_message(MessageType::INFO, "watched files have changed.")
1385 .await;
1386
1387 for change in ¶ms.changes {
1389 let path = match change.uri.to_file_path() {
1390 Ok(p) => p,
1391 Err(_) => continue,
1392 };
1393
1394 let filename = path.file_name().and_then(|n| n.to_str());
1395
1396 if filename == Some("foundry.toml") {
1397 let lint_cfg = config::load_lint_config_from_toml(&path);
1398 self.client
1399 .log_message(
1400 MessageType::INFO,
1401 format!(
1402 "reloaded foundry.toml lint config: lint_on_build={}, ignore_patterns={}",
1403 lint_cfg.lint_on_build,
1404 lint_cfg.ignore_patterns.len()
1405 ),
1406 )
1407 .await;
1408 let mut lc = self.lint_config.write().await;
1409 *lc = lint_cfg;
1410
1411 let foundry_cfg = config::load_foundry_config_from_toml(&path);
1412 self.client
1413 .log_message(
1414 MessageType::INFO,
1415 format!(
1416 "reloaded foundry.toml project config: solc_version={:?}, remappings={}",
1417 foundry_cfg.solc_version,
1418 foundry_cfg.remappings.len()
1419 ),
1420 )
1421 .await;
1422 if foundry_cfg.via_ir {
1423 self.client
1424 .log_message(
1425 MessageType::WARNING,
1426 "via_ir is enabled in foundry.toml — gas estimate inlay hints are disabled to avoid slow compilation",
1427 )
1428 .await;
1429 }
1430 let mut fc = self.foundry_config.write().await;
1431 *fc = foundry_cfg;
1432 break;
1433 }
1434
1435 if filename == Some("remappings.txt") {
1436 self.client
1437 .log_message(
1438 MessageType::INFO,
1439 "remappings.txt changed, config may need refresh",
1440 )
1441 .await;
1442 }
1445 }
1446 }
1447
1448 async fn completion(
1449 &self,
1450 params: CompletionParams,
1451 ) -> tower_lsp::jsonrpc::Result<Option<CompletionResponse>> {
1452 let uri = params.text_document_position.text_document.uri;
1453 let position = params.text_document_position.position;
1454
1455 let trigger_char = params
1456 .context
1457 .as_ref()
1458 .and_then(|ctx| ctx.trigger_character.as_deref());
1459
1460 let source_text = {
1462 let text_cache = self.text_cache.read().await;
1463 if let Some((_, text)) = text_cache.get(&uri.to_string()) {
1464 text.clone()
1465 } else {
1466 match uri.to_file_path() {
1467 Ok(path) => std::fs::read_to_string(&path).unwrap_or_default(),
1468 Err(_) => return Ok(None),
1469 }
1470 }
1471 };
1472
1473 let local_cached: Option<Arc<completion::CompletionCache>> = {
1475 let comp_cache = self.completion_cache.read().await;
1476 comp_cache.get(&uri.to_string()).cloned()
1477 };
1478
1479 let root_cached: Option<Arc<completion::CompletionCache>> = {
1481 let root_key = self.root_uri.read().await.as_ref().map(|u| u.to_string());
1482 match root_key {
1483 Some(root_key) => {
1484 let ast_cache = self.ast_cache.read().await;
1485 ast_cache
1486 .get(&root_key)
1487 .map(|root_build| root_build.completion_cache.clone())
1488 }
1489 None => None,
1490 }
1491 };
1492
1493 let cached = local_cached.or(root_cached.clone());
1495
1496 if cached.is_none() {
1497 let ast_cache = self.ast_cache.clone();
1499 let completion_cache = self.completion_cache.clone();
1500 let uri_string = uri.to_string();
1501 tokio::spawn(async move {
1502 let cached_build = {
1503 let cache = ast_cache.read().await;
1504 match cache.get(&uri_string) {
1505 Some(v) => v.clone(),
1506 None => return,
1507 }
1508 };
1509 completion_cache
1510 .write()
1511 .await
1512 .insert(uri_string, cached_build.completion_cache.clone());
1513 });
1514 }
1515
1516 let cache_ref = cached.as_deref();
1517
1518 let file_id = {
1520 let uri_path = uri.to_file_path().ok();
1521 cache_ref.and_then(|c| {
1522 uri_path.as_ref().and_then(|p| {
1523 let path_str = p.to_str()?;
1524 c.path_to_file_id.get(path_str).copied()
1525 })
1526 })
1527 };
1528
1529 let current_file_path = uri
1530 .to_file_path()
1531 .ok()
1532 .and_then(|p| p.to_str().map(|s| s.to_string()));
1533
1534 let tail_candidates = if trigger_char == Some(".") {
1535 vec![]
1536 } else {
1537 root_cached.as_deref().map_or_else(Vec::new, |c| {
1538 completion::top_level_importable_completion_candidates(
1539 c,
1540 current_file_path.as_deref(),
1541 &source_text,
1542 )
1543 })
1544 };
1545
1546 let result = completion::handle_completion_with_tail_candidates(
1547 cache_ref,
1548 &source_text,
1549 position,
1550 trigger_char,
1551 file_id,
1552 tail_candidates,
1553 );
1554 Ok(result)
1555 }
1556
1557 async fn goto_definition(
1558 &self,
1559 params: GotoDefinitionParams,
1560 ) -> tower_lsp::jsonrpc::Result<Option<GotoDefinitionResponse>> {
1561 self.client
1562 .log_message(MessageType::INFO, "got textDocument/definition request")
1563 .await;
1564
1565 let uri = params.text_document_position_params.text_document.uri;
1566 let position = params.text_document_position_params.position;
1567
1568 let file_path = match uri.to_file_path() {
1569 Ok(path) => path,
1570 Err(_) => {
1571 self.client
1572 .log_message(MessageType::ERROR, "Invalid file uri")
1573 .await;
1574 return Ok(None);
1575 }
1576 };
1577
1578 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1579 Some(bytes) => bytes,
1580 None => return Ok(None),
1581 };
1582
1583 let source_text = String::from_utf8_lossy(&source_bytes).to_string();
1584
1585 let cursor_name = goto::cursor_context(&source_text, position).map(|ctx| ctx.name);
1587
1588 let (is_dirty, cached_build) = {
1592 let text_version = self
1593 .text_cache
1594 .read()
1595 .await
1596 .get(&uri.to_string())
1597 .map(|(v, _)| *v)
1598 .unwrap_or(0);
1599 let cb = self.get_or_fetch_build(&uri, &file_path, false).await;
1600 let build_version = cb.as_ref().map(|b| b.build_version).unwrap_or(0);
1601 (text_version > build_version, cb)
1602 };
1603
1604 let validate_ts = |loc: &Location| -> bool {
1610 let Some(ref name) = cursor_name else {
1611 return true; };
1613 let target_src = if loc.uri == uri {
1614 Some(source_text.clone())
1615 } else {
1616 loc.uri
1617 .to_file_path()
1618 .ok()
1619 .and_then(|p| std::fs::read_to_string(&p).ok())
1620 };
1621 match target_src {
1622 Some(src) => goto::validate_goto_target(&src, loc, name),
1623 None => true, }
1625 };
1626
1627 if is_dirty {
1628 self.client
1629 .log_message(MessageType::INFO, "file is dirty, trying tree-sitter first")
1630 .await;
1631
1632 let ts_result = {
1634 let comp_cache = self.completion_cache.read().await;
1635 let text_cache = self.text_cache.read().await;
1636 if let Some(cc) = comp_cache.get(&uri.to_string()) {
1637 goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
1638 } else {
1639 None
1640 }
1641 };
1642
1643 if let Some(location) = ts_result {
1644 if validate_ts(&location) {
1645 self.client
1646 .log_message(
1647 MessageType::INFO,
1648 format!(
1649 "found definition (tree-sitter) at {}:{}",
1650 location.uri, location.range.start.line
1651 ),
1652 )
1653 .await;
1654 return Ok(Some(GotoDefinitionResponse::from(location)));
1655 }
1656 self.client
1657 .log_message(
1658 MessageType::INFO,
1659 "tree-sitter result failed validation, trying AST fallback",
1660 )
1661 .await;
1662 }
1663
1664 if let Some(ref cb) = cached_build
1669 && let Some(ref name) = cursor_name
1670 {
1671 let byte_hint = goto::pos_to_bytes(&source_bytes, position);
1672 if let Some(location) = goto::goto_declaration_by_name(cb, &uri, name, byte_hint) {
1673 self.client
1674 .log_message(
1675 MessageType::INFO,
1676 format!(
1677 "found definition (AST by name) at {}:{}",
1678 location.uri, location.range.start.line
1679 ),
1680 )
1681 .await;
1682 return Ok(Some(GotoDefinitionResponse::from(location)));
1683 }
1684 }
1685 } else {
1686 if let Some(ref cb) = cached_build
1688 && let Some(location) =
1689 goto::goto_declaration_cached(cb, &uri, position, &source_bytes)
1690 {
1691 self.client
1692 .log_message(
1693 MessageType::INFO,
1694 format!(
1695 "found definition (AST) at {}:{}",
1696 location.uri, location.range.start.line
1697 ),
1698 )
1699 .await;
1700 return Ok(Some(GotoDefinitionResponse::from(location)));
1701 }
1702
1703 let ts_result = {
1705 let comp_cache = self.completion_cache.read().await;
1706 let text_cache = self.text_cache.read().await;
1707 if let Some(cc) = comp_cache.get(&uri.to_string()) {
1708 goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
1709 } else {
1710 None
1711 }
1712 };
1713
1714 if let Some(location) = ts_result {
1715 if validate_ts(&location) {
1716 self.client
1717 .log_message(
1718 MessageType::INFO,
1719 format!(
1720 "found definition (tree-sitter fallback) at {}:{}",
1721 location.uri, location.range.start.line
1722 ),
1723 )
1724 .await;
1725 return Ok(Some(GotoDefinitionResponse::from(location)));
1726 }
1727 self.client
1728 .log_message(MessageType::INFO, "tree-sitter fallback failed validation")
1729 .await;
1730 }
1731 }
1732
1733 self.client
1734 .log_message(MessageType::INFO, "no definition found")
1735 .await;
1736 Ok(None)
1737 }
1738
1739 async fn goto_declaration(
1740 &self,
1741 params: request::GotoDeclarationParams,
1742 ) -> tower_lsp::jsonrpc::Result<Option<request::GotoDeclarationResponse>> {
1743 self.client
1744 .log_message(MessageType::INFO, "got textDocument/declaration request")
1745 .await;
1746
1747 let uri = params.text_document_position_params.text_document.uri;
1748 let position = params.text_document_position_params.position;
1749
1750 let file_path = match uri.to_file_path() {
1751 Ok(path) => path,
1752 Err(_) => {
1753 self.client
1754 .log_message(MessageType::ERROR, "invalid file uri")
1755 .await;
1756 return Ok(None);
1757 }
1758 };
1759
1760 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1761 Some(bytes) => bytes,
1762 None => return Ok(None),
1763 };
1764
1765 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1766 let cached_build = match cached_build {
1767 Some(cb) => cb,
1768 None => return Ok(None),
1769 };
1770
1771 if let Some(location) =
1772 goto::goto_declaration_cached(&cached_build, &uri, position, &source_bytes)
1773 {
1774 self.client
1775 .log_message(
1776 MessageType::INFO,
1777 format!(
1778 "found declaration at {}:{}",
1779 location.uri, location.range.start.line
1780 ),
1781 )
1782 .await;
1783 Ok(Some(request::GotoDeclarationResponse::from(location)))
1784 } else {
1785 self.client
1786 .log_message(MessageType::INFO, "no declaration found")
1787 .await;
1788 Ok(None)
1789 }
1790 }
1791
1792 async fn references(
1793 &self,
1794 params: ReferenceParams,
1795 ) -> tower_lsp::jsonrpc::Result<Option<Vec<Location>>> {
1796 self.client
1797 .log_message(MessageType::INFO, "Got a textDocument/references request")
1798 .await;
1799
1800 let uri = params.text_document_position.text_document.uri;
1801 let position = params.text_document_position.position;
1802 let file_path = match uri.to_file_path() {
1803 Ok(path) => path,
1804 Err(_) => {
1805 self.client
1806 .log_message(MessageType::ERROR, "Invalid file URI")
1807 .await;
1808 return Ok(None);
1809 }
1810 };
1811 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1812 Some(bytes) => bytes,
1813 None => return Ok(None),
1814 };
1815 let cached_build = self.get_or_fetch_build(&uri, &file_path, true).await;
1816 let cached_build = match cached_build {
1817 Some(cb) => cb,
1818 None => return Ok(None),
1819 };
1820
1821 let mut locations = references::goto_references_cached(
1823 &cached_build,
1824 &uri,
1825 position,
1826 &source_bytes,
1827 None,
1828 params.context.include_declaration,
1829 );
1830
1831 if let Some((def_abs_path, def_byte_offset)) =
1833 references::resolve_target_location(&cached_build, &uri, position, &source_bytes)
1834 {
1835 let cache = self.ast_cache.read().await;
1836 for (cached_uri, other_build) in cache.iter() {
1837 if *cached_uri == uri.to_string() {
1838 continue;
1839 }
1840 let other_locations = references::goto_references_for_target(
1841 other_build,
1842 &def_abs_path,
1843 def_byte_offset,
1844 None,
1845 params.context.include_declaration,
1846 );
1847 locations.extend(other_locations);
1848 }
1849 }
1850
1851 let mut seen = std::collections::HashSet::new();
1853 locations.retain(|loc| {
1854 seen.insert((
1855 loc.uri.clone(),
1856 loc.range.start.line,
1857 loc.range.start.character,
1858 loc.range.end.line,
1859 loc.range.end.character,
1860 ))
1861 });
1862
1863 if locations.is_empty() {
1864 self.client
1865 .log_message(MessageType::INFO, "No references found")
1866 .await;
1867 Ok(None)
1868 } else {
1869 self.client
1870 .log_message(
1871 MessageType::INFO,
1872 format!("Found {} references", locations.len()),
1873 )
1874 .await;
1875 Ok(Some(locations))
1876 }
1877 }
1878
1879 async fn prepare_rename(
1880 &self,
1881 params: TextDocumentPositionParams,
1882 ) -> tower_lsp::jsonrpc::Result<Option<PrepareRenameResponse>> {
1883 self.client
1884 .log_message(MessageType::INFO, "got textDocument/prepareRename request")
1885 .await;
1886
1887 let uri = params.text_document.uri;
1888 let position = params.position;
1889
1890 let file_path = match uri.to_file_path() {
1891 Ok(path) => path,
1892 Err(_) => {
1893 self.client
1894 .log_message(MessageType::ERROR, "invalid file uri")
1895 .await;
1896 return Ok(None);
1897 }
1898 };
1899
1900 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1901 Some(bytes) => bytes,
1902 None => return Ok(None),
1903 };
1904
1905 if let Some(range) = rename::get_identifier_range(&source_bytes, position) {
1906 self.client
1907 .log_message(
1908 MessageType::INFO,
1909 format!(
1910 "prepare rename range: {}:{}",
1911 range.start.line, range.start.character
1912 ),
1913 )
1914 .await;
1915 Ok(Some(PrepareRenameResponse::Range(range)))
1916 } else {
1917 self.client
1918 .log_message(MessageType::INFO, "no identifier found for prepare rename")
1919 .await;
1920 Ok(None)
1921 }
1922 }
1923
1924 async fn rename(
1925 &self,
1926 params: RenameParams,
1927 ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
1928 self.client
1929 .log_message(MessageType::INFO, "got textDocument/rename request")
1930 .await;
1931
1932 let uri = params.text_document_position.text_document.uri;
1933 let position = params.text_document_position.position;
1934 let new_name = params.new_name;
1935 let file_path = match uri.to_file_path() {
1936 Ok(p) => p,
1937 Err(_) => {
1938 self.client
1939 .log_message(MessageType::ERROR, "invalid file uri")
1940 .await;
1941 return Ok(None);
1942 }
1943 };
1944 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
1945 Some(bytes) => bytes,
1946 None => return Ok(None),
1947 };
1948
1949 let current_identifier = match rename::get_identifier_at_position(&source_bytes, position) {
1950 Some(id) => id,
1951 None => {
1952 self.client
1953 .log_message(MessageType::ERROR, "No identifier found at position")
1954 .await;
1955 return Ok(None);
1956 }
1957 };
1958
1959 if !utils::is_valid_solidity_identifier(&new_name) {
1960 return Err(tower_lsp::jsonrpc::Error::invalid_params(
1961 "new name is not a valid solidity identifier",
1962 ));
1963 }
1964
1965 if new_name == current_identifier {
1966 self.client
1967 .log_message(
1968 MessageType::INFO,
1969 "new name is the same as current identifier",
1970 )
1971 .await;
1972 return Ok(None);
1973 }
1974
1975 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
1976 let cached_build = match cached_build {
1977 Some(cb) => cb,
1978 None => return Ok(None),
1979 };
1980 let other_builds: Vec<Arc<goto::CachedBuild>> = {
1981 let cache = self.ast_cache.read().await;
1982 cache
1983 .iter()
1984 .filter(|(key, _)| **key != uri.to_string())
1985 .map(|(_, v)| v.clone())
1986 .collect()
1987 };
1988 let other_refs: Vec<&goto::CachedBuild> = other_builds.iter().map(|v| v.as_ref()).collect();
1989
1990 let text_buffers: HashMap<String, Vec<u8>> = {
1994 let text_cache = self.text_cache.read().await;
1995 text_cache
1996 .iter()
1997 .map(|(uri, (_, content))| (uri.clone(), content.as_bytes().to_vec()))
1998 .collect()
1999 };
2000
2001 match rename::rename_symbol(
2002 &cached_build,
2003 &uri,
2004 position,
2005 &source_bytes,
2006 new_name,
2007 &other_refs,
2008 &text_buffers,
2009 ) {
2010 Some(workspace_edit) => {
2011 self.client
2012 .log_message(
2013 MessageType::INFO,
2014 format!(
2015 "created rename edit with {} file(s), {} total change(s)",
2016 workspace_edit
2017 .changes
2018 .as_ref()
2019 .map(|c| c.len())
2020 .unwrap_or(0),
2021 workspace_edit
2022 .changes
2023 .as_ref()
2024 .map(|c| c.values().map(|v| v.len()).sum::<usize>())
2025 .unwrap_or(0)
2026 ),
2027 )
2028 .await;
2029
2030 Ok(Some(workspace_edit))
2035 }
2036
2037 None => {
2038 self.client
2039 .log_message(MessageType::INFO, "No locations found for renaming")
2040 .await;
2041 Ok(None)
2042 }
2043 }
2044 }
2045
2046 async fn symbol(
2047 &self,
2048 params: WorkspaceSymbolParams,
2049 ) -> tower_lsp::jsonrpc::Result<Option<Vec<SymbolInformation>>> {
2050 self.client
2051 .log_message(MessageType::INFO, "got workspace/symbol request")
2052 .await;
2053
2054 let files: Vec<(Url, String)> = {
2056 let cache = self.text_cache.read().await;
2057 cache
2058 .iter()
2059 .filter(|(uri_str, _)| uri_str.ends_with(".sol"))
2060 .filter_map(|(uri_str, (_, content))| {
2061 Url::parse(uri_str).ok().map(|uri| (uri, content.clone()))
2062 })
2063 .collect()
2064 };
2065
2066 let mut all_symbols = symbols::extract_workspace_symbols(&files);
2067 if !params.query.is_empty() {
2068 let query = params.query.to_lowercase();
2069 all_symbols.retain(|symbol| symbol.name.to_lowercase().contains(&query));
2070 }
2071 if all_symbols.is_empty() {
2072 self.client
2073 .log_message(MessageType::INFO, "No symbols found")
2074 .await;
2075 Ok(None)
2076 } else {
2077 self.client
2078 .log_message(
2079 MessageType::INFO,
2080 format!("found {} symbols", all_symbols.len()),
2081 )
2082 .await;
2083 Ok(Some(all_symbols))
2084 }
2085 }
2086
2087 async fn document_symbol(
2088 &self,
2089 params: DocumentSymbolParams,
2090 ) -> tower_lsp::jsonrpc::Result<Option<DocumentSymbolResponse>> {
2091 self.client
2092 .log_message(MessageType::INFO, "got textDocument/documentSymbol request")
2093 .await;
2094 let uri = params.text_document.uri;
2095 let file_path = match uri.to_file_path() {
2096 Ok(path) => path,
2097 Err(_) => {
2098 self.client
2099 .log_message(MessageType::ERROR, "invalid file uri")
2100 .await;
2101 return Ok(None);
2102 }
2103 };
2104
2105 let source = {
2107 let cache = self.text_cache.read().await;
2108 cache
2109 .get(&uri.to_string())
2110 .map(|(_, content)| content.clone())
2111 };
2112 let source = match source {
2113 Some(s) => s,
2114 None => match std::fs::read_to_string(&file_path) {
2115 Ok(s) => s,
2116 Err(_) => return Ok(None),
2117 },
2118 };
2119
2120 let symbols = symbols::extract_document_symbols(&source);
2121 if symbols.is_empty() {
2122 self.client
2123 .log_message(MessageType::INFO, "no document symbols found")
2124 .await;
2125 Ok(None)
2126 } else {
2127 self.client
2128 .log_message(
2129 MessageType::INFO,
2130 format!("found {} document symbols", symbols.len()),
2131 )
2132 .await;
2133 Ok(Some(DocumentSymbolResponse::Nested(symbols)))
2134 }
2135 }
2136
2137 async fn document_highlight(
2138 &self,
2139 params: DocumentHighlightParams,
2140 ) -> tower_lsp::jsonrpc::Result<Option<Vec<DocumentHighlight>>> {
2141 self.client
2142 .log_message(
2143 MessageType::INFO,
2144 "got textDocument/documentHighlight request",
2145 )
2146 .await;
2147
2148 let uri = params.text_document_position_params.text_document.uri;
2149 let position = params.text_document_position_params.position;
2150
2151 let source = {
2152 let cache = self.text_cache.read().await;
2153 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
2154 };
2155
2156 let source = match source {
2157 Some(s) => s,
2158 None => {
2159 let file_path = match uri.to_file_path() {
2160 Ok(p) => p,
2161 Err(_) => return Ok(None),
2162 };
2163 match std::fs::read_to_string(&file_path) {
2164 Ok(s) => s,
2165 Err(_) => return Ok(None),
2166 }
2167 }
2168 };
2169
2170 let highlights = highlight::document_highlights(&source, position);
2171
2172 if highlights.is_empty() {
2173 self.client
2174 .log_message(MessageType::INFO, "no document highlights found")
2175 .await;
2176 Ok(None)
2177 } else {
2178 self.client
2179 .log_message(
2180 MessageType::INFO,
2181 format!("found {} document highlights", highlights.len()),
2182 )
2183 .await;
2184 Ok(Some(highlights))
2185 }
2186 }
2187
2188 async fn hover(&self, params: HoverParams) -> tower_lsp::jsonrpc::Result<Option<Hover>> {
2189 self.client
2190 .log_message(MessageType::INFO, "got textDocument/hover request")
2191 .await;
2192
2193 let uri = params.text_document_position_params.text_document.uri;
2194 let position = params.text_document_position_params.position;
2195
2196 let file_path = match uri.to_file_path() {
2197 Ok(path) => path,
2198 Err(_) => {
2199 self.client
2200 .log_message(MessageType::ERROR, "invalid file uri")
2201 .await;
2202 return Ok(None);
2203 }
2204 };
2205
2206 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
2207 Some(bytes) => bytes,
2208 None => return Ok(None),
2209 };
2210
2211 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
2212 let cached_build = match cached_build {
2213 Some(cb) => cb,
2214 None => return Ok(None),
2215 };
2216
2217 let result = hover::hover_info(&cached_build, &uri, position, &source_bytes);
2218
2219 if result.is_some() {
2220 self.client
2221 .log_message(MessageType::INFO, "hover info found")
2222 .await;
2223 } else {
2224 self.client
2225 .log_message(MessageType::INFO, "no hover info found")
2226 .await;
2227 }
2228
2229 Ok(result)
2230 }
2231
2232 async fn signature_help(
2233 &self,
2234 params: SignatureHelpParams,
2235 ) -> tower_lsp::jsonrpc::Result<Option<SignatureHelp>> {
2236 self.client
2237 .log_message(MessageType::INFO, "got textDocument/signatureHelp request")
2238 .await;
2239
2240 let uri = params.text_document_position_params.text_document.uri;
2241 let position = params.text_document_position_params.position;
2242
2243 let file_path = match uri.to_file_path() {
2244 Ok(path) => path,
2245 Err(_) => {
2246 self.client
2247 .log_message(MessageType::ERROR, "invalid file uri")
2248 .await;
2249 return Ok(None);
2250 }
2251 };
2252
2253 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
2254 Some(bytes) => bytes,
2255 None => return Ok(None),
2256 };
2257
2258 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
2259 let cached_build = match cached_build {
2260 Some(cb) => cb,
2261 None => return Ok(None),
2262 };
2263
2264 let result = hover::signature_help(&cached_build, &source_bytes, position);
2265
2266 Ok(result)
2267 }
2268
2269 async fn document_link(
2270 &self,
2271 params: DocumentLinkParams,
2272 ) -> tower_lsp::jsonrpc::Result<Option<Vec<DocumentLink>>> {
2273 self.client
2274 .log_message(MessageType::INFO, "got textDocument/documentLink request")
2275 .await;
2276
2277 let uri = params.text_document.uri;
2278 let file_path = match uri.to_file_path() {
2279 Ok(path) => path,
2280 Err(_) => {
2281 self.client
2282 .log_message(MessageType::ERROR, "invalid file uri")
2283 .await;
2284 return Ok(None);
2285 }
2286 };
2287
2288 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
2289 Some(bytes) => bytes,
2290 None => return Ok(None),
2291 };
2292
2293 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
2294 let cached_build = match cached_build {
2295 Some(cb) => cb,
2296 None => return Ok(None),
2297 };
2298
2299 let result = links::document_links(&cached_build, &uri, &source_bytes);
2300
2301 if result.is_empty() {
2302 self.client
2303 .log_message(MessageType::INFO, "no document links found")
2304 .await;
2305 Ok(None)
2306 } else {
2307 self.client
2308 .log_message(
2309 MessageType::INFO,
2310 format!("found {} document links", result.len()),
2311 )
2312 .await;
2313 Ok(Some(result))
2314 }
2315 }
2316
2317 async fn semantic_tokens_full(
2318 &self,
2319 params: SemanticTokensParams,
2320 ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensResult>> {
2321 self.client
2322 .log_message(
2323 MessageType::INFO,
2324 "got textDocument/semanticTokens/full request",
2325 )
2326 .await;
2327
2328 let uri = params.text_document.uri;
2329 let source = {
2330 let cache = self.text_cache.read().await;
2331 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
2332 };
2333
2334 let source = match source {
2335 Some(s) => s,
2336 None => {
2337 let file_path = match uri.to_file_path() {
2339 Ok(p) => p,
2340 Err(_) => return Ok(None),
2341 };
2342 match std::fs::read_to_string(&file_path) {
2343 Ok(s) => s,
2344 Err(_) => return Ok(None),
2345 }
2346 }
2347 };
2348
2349 let mut tokens = semantic_tokens::semantic_tokens_full(&source);
2350
2351 let id = self.semantic_token_id.fetch_add(1, Ordering::Relaxed);
2353 let result_id = id.to_string();
2354 tokens.result_id = Some(result_id.clone());
2355
2356 {
2357 let mut cache = self.semantic_token_cache.write().await;
2358 cache.insert(uri.to_string(), (result_id, tokens.data.clone()));
2359 }
2360
2361 Ok(Some(SemanticTokensResult::Tokens(tokens)))
2362 }
2363
2364 async fn semantic_tokens_range(
2365 &self,
2366 params: SemanticTokensRangeParams,
2367 ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensRangeResult>> {
2368 self.client
2369 .log_message(
2370 MessageType::INFO,
2371 "got textDocument/semanticTokens/range request",
2372 )
2373 .await;
2374
2375 let uri = params.text_document.uri;
2376 let range = params.range;
2377 let source = {
2378 let cache = self.text_cache.read().await;
2379 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
2380 };
2381
2382 let source = match source {
2383 Some(s) => s,
2384 None => {
2385 let file_path = match uri.to_file_path() {
2386 Ok(p) => p,
2387 Err(_) => return Ok(None),
2388 };
2389 match std::fs::read_to_string(&file_path) {
2390 Ok(s) => s,
2391 Err(_) => return Ok(None),
2392 }
2393 }
2394 };
2395
2396 let tokens =
2397 semantic_tokens::semantic_tokens_range(&source, range.start.line, range.end.line);
2398
2399 Ok(Some(SemanticTokensRangeResult::Tokens(tokens)))
2400 }
2401
2402 async fn semantic_tokens_full_delta(
2403 &self,
2404 params: SemanticTokensDeltaParams,
2405 ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensFullDeltaResult>> {
2406 self.client
2407 .log_message(
2408 MessageType::INFO,
2409 "got textDocument/semanticTokens/full/delta request",
2410 )
2411 .await;
2412
2413 let uri = params.text_document.uri;
2414 let previous_result_id = params.previous_result_id;
2415
2416 let source = {
2417 let cache = self.text_cache.read().await;
2418 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
2419 };
2420
2421 let source = match source {
2422 Some(s) => s,
2423 None => {
2424 let file_path = match uri.to_file_path() {
2425 Ok(p) => p,
2426 Err(_) => return Ok(None),
2427 };
2428 match std::fs::read_to_string(&file_path) {
2429 Ok(s) => s,
2430 Err(_) => return Ok(None),
2431 }
2432 }
2433 };
2434
2435 let mut new_tokens = semantic_tokens::semantic_tokens_full(&source);
2436
2437 let id = self.semantic_token_id.fetch_add(1, Ordering::Relaxed);
2439 let new_result_id = id.to_string();
2440 new_tokens.result_id = Some(new_result_id.clone());
2441
2442 let uri_str = uri.to_string();
2443
2444 let old_tokens = {
2446 let cache = self.semantic_token_cache.read().await;
2447 cache
2448 .get(&uri_str)
2449 .filter(|(rid, _)| *rid == previous_result_id)
2450 .map(|(_, tokens)| tokens.clone())
2451 };
2452
2453 {
2455 let mut cache = self.semantic_token_cache.write().await;
2456 cache.insert(uri_str, (new_result_id.clone(), new_tokens.data.clone()));
2457 }
2458
2459 match old_tokens {
2460 Some(old) => {
2461 let edits = semantic_tokens::compute_delta(&old, &new_tokens.data);
2463 Ok(Some(SemanticTokensFullDeltaResult::TokensDelta(
2464 SemanticTokensDelta {
2465 result_id: Some(new_result_id),
2466 edits,
2467 },
2468 )))
2469 }
2470 None => {
2471 Ok(Some(SemanticTokensFullDeltaResult::Tokens(new_tokens)))
2473 }
2474 }
2475 }
2476
2477 async fn folding_range(
2478 &self,
2479 params: FoldingRangeParams,
2480 ) -> tower_lsp::jsonrpc::Result<Option<Vec<FoldingRange>>> {
2481 self.client
2482 .log_message(MessageType::INFO, "got textDocument/foldingRange request")
2483 .await;
2484
2485 let uri = params.text_document.uri;
2486
2487 let source = {
2488 let cache = self.text_cache.read().await;
2489 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
2490 };
2491
2492 let source = match source {
2493 Some(s) => s,
2494 None => {
2495 let file_path = match uri.to_file_path() {
2496 Ok(p) => p,
2497 Err(_) => return Ok(None),
2498 };
2499 match std::fs::read_to_string(&file_path) {
2500 Ok(s) => s,
2501 Err(_) => return Ok(None),
2502 }
2503 }
2504 };
2505
2506 let ranges = folding::folding_ranges(&source);
2507
2508 if ranges.is_empty() {
2509 self.client
2510 .log_message(MessageType::INFO, "no folding ranges found")
2511 .await;
2512 Ok(None)
2513 } else {
2514 self.client
2515 .log_message(
2516 MessageType::INFO,
2517 format!("found {} folding ranges", ranges.len()),
2518 )
2519 .await;
2520 Ok(Some(ranges))
2521 }
2522 }
2523
2524 async fn selection_range(
2525 &self,
2526 params: SelectionRangeParams,
2527 ) -> tower_lsp::jsonrpc::Result<Option<Vec<SelectionRange>>> {
2528 self.client
2529 .log_message(MessageType::INFO, "got textDocument/selectionRange request")
2530 .await;
2531
2532 let uri = params.text_document.uri;
2533
2534 let source = {
2535 let cache = self.text_cache.read().await;
2536 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
2537 };
2538
2539 let source = match source {
2540 Some(s) => s,
2541 None => {
2542 let file_path = match uri.to_file_path() {
2543 Ok(p) => p,
2544 Err(_) => return Ok(None),
2545 };
2546 match std::fs::read_to_string(&file_path) {
2547 Ok(s) => s,
2548 Err(_) => return Ok(None),
2549 }
2550 }
2551 };
2552
2553 let ranges = selection::selection_ranges(&source, ¶ms.positions);
2554
2555 if ranges.is_empty() {
2556 self.client
2557 .log_message(MessageType::INFO, "no selection ranges found")
2558 .await;
2559 Ok(None)
2560 } else {
2561 self.client
2562 .log_message(
2563 MessageType::INFO,
2564 format!("found {} selection ranges", ranges.len()),
2565 )
2566 .await;
2567 Ok(Some(ranges))
2568 }
2569 }
2570
2571 async fn inlay_hint(
2572 &self,
2573 params: InlayHintParams,
2574 ) -> tower_lsp::jsonrpc::Result<Option<Vec<InlayHint>>> {
2575 self.client
2576 .log_message(MessageType::INFO, "got textDocument/inlayHint request")
2577 .await;
2578
2579 let uri = params.text_document.uri;
2580 let range = params.range;
2581
2582 let file_path = match uri.to_file_path() {
2583 Ok(path) => path,
2584 Err(_) => {
2585 self.client
2586 .log_message(MessageType::ERROR, "invalid file uri")
2587 .await;
2588 return Ok(None);
2589 }
2590 };
2591
2592 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
2593 Some(bytes) => bytes,
2594 None => return Ok(None),
2595 };
2596
2597 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
2598 let cached_build = match cached_build {
2599 Some(cb) => cb,
2600 None => return Ok(None),
2601 };
2602
2603 let mut hints = inlay_hints::inlay_hints(&cached_build, &uri, range, &source_bytes);
2604
2605 let settings = self.settings.read().await;
2607 if !settings.inlay_hints.parameters {
2608 hints.retain(|h| h.kind != Some(InlayHintKind::PARAMETER));
2609 }
2610 if !settings.inlay_hints.gas_estimates {
2611 hints.retain(|h| h.kind != Some(InlayHintKind::TYPE));
2612 }
2613
2614 if hints.is_empty() {
2615 self.client
2616 .log_message(MessageType::INFO, "no inlay hints found")
2617 .await;
2618 Ok(None)
2619 } else {
2620 self.client
2621 .log_message(
2622 MessageType::INFO,
2623 format!("found {} inlay hints", hints.len()),
2624 )
2625 .await;
2626 Ok(Some(hints))
2627 }
2628 }
2629
2630 async fn will_rename_files(
2631 &self,
2632 params: RenameFilesParams,
2633 ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
2634 self.client
2635 .log_message(
2636 MessageType::INFO,
2637 format!("workspace/willRenameFiles: {} file(s)", params.files.len()),
2638 )
2639 .await;
2640 if !self
2641 .settings
2642 .read()
2643 .await
2644 .file_operations
2645 .update_imports_on_rename
2646 {
2647 self.client
2648 .log_message(
2649 MessageType::INFO,
2650 "willRenameFiles: updateImportsOnRename disabled",
2651 )
2652 .await;
2653 return Ok(None);
2654 }
2655
2656 let config = self.foundry_config.read().await.clone();
2658 let project_root = config.root.clone();
2659 let source_files: Vec<String> = tokio::task::spawn_blocking(move || {
2660 crate::solc::discover_source_files(&config)
2661 .into_iter()
2662 .filter_map(|p| p.to_str().map(String::from))
2663 .collect()
2664 })
2665 .await
2666 .unwrap_or_default();
2667
2668 if source_files.is_empty() {
2669 self.client
2670 .log_message(
2671 MessageType::WARNING,
2672 "willRenameFiles: no source files found",
2673 )
2674 .await;
2675 return Ok(None);
2676 }
2677
2678 let raw_renames: Vec<(std::path::PathBuf, std::path::PathBuf)> = params
2680 .files
2681 .iter()
2682 .filter_map(|fr| {
2683 let old_uri = Url::parse(&fr.old_uri).ok()?;
2684 let new_uri = Url::parse(&fr.new_uri).ok()?;
2685 let old_path = old_uri.to_file_path().ok()?;
2686 let new_path = new_uri.to_file_path().ok()?;
2687 Some((old_path, new_path))
2688 })
2689 .collect();
2690
2691 let renames = file_operations::expand_folder_renames(&raw_renames, &source_files);
2692
2693 if renames.is_empty() {
2694 return Ok(None);
2695 }
2696
2697 self.client
2698 .log_message(
2699 MessageType::INFO,
2700 format!(
2701 "willRenameFiles: {} rename(s) after folder expansion",
2702 renames.len()
2703 ),
2704 )
2705 .await;
2706
2707 let files_to_read: Vec<(String, String)> = {
2710 let tc = self.text_cache.read().await;
2711 source_files
2712 .iter()
2713 .filter_map(|fs_path| {
2714 let uri = Url::from_file_path(fs_path).ok()?;
2715 let uri_str = uri.to_string();
2716 if tc.contains_key(&uri_str) {
2717 None
2718 } else {
2719 Some((uri_str, fs_path.clone()))
2720 }
2721 })
2722 .collect()
2723 };
2724
2725 if !files_to_read.is_empty() {
2726 let loaded: Vec<(String, String)> = tokio::task::spawn_blocking(move || {
2727 files_to_read
2728 .into_iter()
2729 .filter_map(|(uri_str, fs_path)| {
2730 let content = std::fs::read_to_string(&fs_path).ok()?;
2731 Some((uri_str, content))
2732 })
2733 .collect()
2734 })
2735 .await
2736 .unwrap_or_default();
2737
2738 let mut tc = self.text_cache.write().await;
2739 for (uri_str, content) in loaded {
2740 tc.entry(uri_str).or_insert((0, content));
2741 }
2742 }
2743
2744 let text_cache = self.text_cache.clone();
2749 let result = {
2750 let tc = text_cache.read().await;
2751 let get_source_bytes = |fs_path: &str| -> Option<Vec<u8>> {
2752 let uri = Url::from_file_path(fs_path).ok()?;
2753 let (_, content) = tc.get(&uri.to_string())?;
2754 Some(content.as_bytes().to_vec())
2755 };
2756
2757 file_operations::rename_imports(
2758 &source_files,
2759 &renames,
2760 &project_root,
2761 &get_source_bytes,
2762 )
2763 };
2764
2765 let stats = &result.stats;
2767 if stats.read_failures > 0 || stats.pathdiff_failures > 0 || stats.duplicate_renames > 0 {
2768 self.client
2769 .log_message(
2770 MessageType::WARNING,
2771 format!(
2772 "willRenameFiles stats: read_failures={}, pathdiff_failures={}, \
2773 duplicate_renames={}, no_parent={}, no_op_skips={}, dedup_skips={}",
2774 stats.read_failures,
2775 stats.pathdiff_failures,
2776 stats.duplicate_renames,
2777 stats.no_parent,
2778 stats.no_op_skips,
2779 stats.dedup_skips,
2780 ),
2781 )
2782 .await;
2783 }
2784
2785 let all_edits = result.edits;
2786
2787 if all_edits.is_empty() {
2788 self.client
2789 .log_message(MessageType::INFO, "willRenameFiles: no import edits needed")
2790 .await;
2791 return Ok(None);
2792 }
2793
2794 {
2796 let mut tc = self.text_cache.write().await;
2797 let patched = file_operations::apply_edits_to_cache(&all_edits, &mut tc);
2798 self.client
2799 .log_message(
2800 MessageType::INFO,
2801 format!("willRenameFiles: patched {} cached file(s)", patched),
2802 )
2803 .await;
2804 }
2805
2806 let total_edits: usize = all_edits.values().map(|v| v.len()).sum();
2807 self.client
2808 .log_message(
2809 MessageType::INFO,
2810 format!(
2811 "willRenameFiles: {} edit(s) across {} file(s)",
2812 total_edits,
2813 all_edits.len()
2814 ),
2815 )
2816 .await;
2817
2818 Ok(Some(WorkspaceEdit {
2819 changes: Some(all_edits),
2820 document_changes: None,
2821 change_annotations: None,
2822 }))
2823 }
2824
2825 async fn did_rename_files(&self, params: RenameFilesParams) {
2826 self.client
2827 .log_message(
2828 MessageType::INFO,
2829 format!("workspace/didRenameFiles: {} file(s)", params.files.len()),
2830 )
2831 .await;
2832
2833 let raw_uri_pairs: Vec<(Url, Url)> = params
2835 .files
2836 .iter()
2837 .filter_map(|fr| {
2838 let old_uri = Url::parse(&fr.old_uri).ok()?;
2839 let new_uri = Url::parse(&fr.new_uri).ok()?;
2840 Some((old_uri, new_uri))
2841 })
2842 .collect();
2843
2844 let file_renames = {
2845 let tc = self.text_cache.read().await;
2846 let cache_paths: Vec<std::path::PathBuf> = tc
2847 .keys()
2848 .filter_map(|k| Url::parse(k).ok())
2849 .filter_map(|u| u.to_file_path().ok())
2850 .collect();
2851 drop(tc);
2852
2853 let cfg = self.foundry_config.read().await.clone();
2856 let discovered_paths =
2857 tokio::task::spawn_blocking(move || crate::solc::discover_source_files(&cfg))
2858 .await
2859 .unwrap_or_default();
2860
2861 let mut all_paths: HashSet<std::path::PathBuf> = discovered_paths.into_iter().collect();
2862 all_paths.extend(cache_paths);
2863 let all_paths: Vec<std::path::PathBuf> = all_paths.into_iter().collect();
2864
2865 file_operations::expand_folder_renames_from_paths(&raw_uri_pairs, &all_paths)
2866 };
2867
2868 self.client
2869 .log_message(
2870 MessageType::INFO,
2871 format!(
2872 "didRenameFiles: migrating {} cache entry/entries",
2873 file_renames.len()
2874 ),
2875 )
2876 .await;
2877
2878 {
2882 let mut tc = self.text_cache.write().await;
2883 for (old_key, new_key) in &file_renames {
2884 if let Some(entry) = tc.remove(old_key) {
2885 tc.insert(new_key.clone(), entry);
2886 }
2887 }
2888 }
2889 {
2890 let mut ac = self.ast_cache.write().await;
2891 for (old_key, _) in &file_renames {
2892 ac.remove(old_key);
2893 }
2894 }
2895 {
2896 let mut cc = self.completion_cache.write().await;
2897 for (old_key, _) in &file_renames {
2898 cc.remove(old_key);
2899 }
2900 }
2901
2902 let root_key = self.root_uri.read().await.as_ref().map(|u| u.to_string());
2905 if let Some(ref key) = root_key {
2906 self.ast_cache.write().await.remove(key);
2907 }
2908
2909 let foundry_config = self.foundry_config.read().await.clone();
2910 let ast_cache = self.ast_cache.clone();
2911 let client = self.client.clone();
2912 let text_cache_snapshot = self.text_cache.read().await.clone();
2916
2917 tokio::spawn(async move {
2918 let Some(cache_key) = root_key else {
2919 return;
2920 };
2921 match crate::solc::solc_project_index(
2922 &foundry_config,
2923 Some(&client),
2924 Some(&text_cache_snapshot),
2925 )
2926 .await
2927 {
2928 Ok(ast_data) => {
2929 let cached_build = Arc::new(crate::goto::CachedBuild::new(ast_data, 0));
2930 let source_count = cached_build.nodes.len();
2931 ast_cache.write().await.insert(cache_key, cached_build);
2932 client
2933 .log_message(
2934 MessageType::INFO,
2935 format!("didRenameFiles: re-indexed {} source files", source_count),
2936 )
2937 .await;
2938 }
2939 Err(e) => {
2940 client
2941 .log_message(
2942 MessageType::WARNING,
2943 format!("didRenameFiles: re-index failed: {e}"),
2944 )
2945 .await;
2946 }
2947 }
2948 });
2949 }
2950
2951 async fn will_delete_files(
2952 &self,
2953 params: DeleteFilesParams,
2954 ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
2955 self.client
2956 .log_message(
2957 MessageType::INFO,
2958 format!("workspace/willDeleteFiles: {} file(s)", params.files.len()),
2959 )
2960 .await;
2961 if !update_imports_on_delete_enabled(&*self.settings.read().await) {
2962 self.client
2963 .log_message(
2964 MessageType::INFO,
2965 "willDeleteFiles: updateImportsOnDelete disabled",
2966 )
2967 .await;
2968 return Ok(None);
2969 }
2970
2971 let config = self.foundry_config.read().await.clone();
2972 let project_root = config.root.clone();
2973 let source_files: Vec<String> = tokio::task::spawn_blocking(move || {
2974 crate::solc::discover_source_files(&config)
2975 .into_iter()
2976 .filter_map(|p| p.to_str().map(String::from))
2977 .collect()
2978 })
2979 .await
2980 .unwrap_or_default();
2981
2982 if source_files.is_empty() {
2983 self.client
2984 .log_message(
2985 MessageType::WARNING,
2986 "willDeleteFiles: no source files found",
2987 )
2988 .await;
2989 return Ok(None);
2990 }
2991
2992 let raw_deletes: Vec<std::path::PathBuf> = params
2993 .files
2994 .iter()
2995 .filter_map(|fd| Url::parse(&fd.uri).ok())
2996 .filter_map(|u| u.to_file_path().ok())
2997 .collect();
2998
2999 let deletes = file_operations::expand_folder_deletes(&raw_deletes, &source_files);
3000 if deletes.is_empty() {
3001 return Ok(None);
3002 }
3003
3004 self.client
3005 .log_message(
3006 MessageType::INFO,
3007 format!(
3008 "willDeleteFiles: {} delete target(s) after folder expansion",
3009 deletes.len()
3010 ),
3011 )
3012 .await;
3013
3014 let files_to_read: Vec<(String, String)> = {
3015 let tc = self.text_cache.read().await;
3016 source_files
3017 .iter()
3018 .filter_map(|fs_path| {
3019 let uri = Url::from_file_path(fs_path).ok()?;
3020 let uri_str = uri.to_string();
3021 if tc.contains_key(&uri_str) {
3022 None
3023 } else {
3024 Some((uri_str, fs_path.clone()))
3025 }
3026 })
3027 .collect()
3028 };
3029
3030 if !files_to_read.is_empty() {
3031 let loaded: Vec<(String, String)> = tokio::task::spawn_blocking(move || {
3032 files_to_read
3033 .into_iter()
3034 .filter_map(|(uri_str, fs_path)| {
3035 let content = std::fs::read_to_string(&fs_path).ok()?;
3036 Some((uri_str, content))
3037 })
3038 .collect()
3039 })
3040 .await
3041 .unwrap_or_default();
3042
3043 let mut tc = self.text_cache.write().await;
3044 for (uri_str, content) in loaded {
3045 tc.entry(uri_str).or_insert((0, content));
3046 }
3047 }
3048
3049 let result = {
3050 let tc = self.text_cache.read().await;
3051 let get_source_bytes = |fs_path: &str| -> Option<Vec<u8>> {
3052 let uri = Url::from_file_path(fs_path).ok()?;
3053 let (_, content) = tc.get(&uri.to_string())?;
3054 Some(content.as_bytes().to_vec())
3055 };
3056
3057 file_operations::delete_imports(
3058 &source_files,
3059 &deletes,
3060 &project_root,
3061 &get_source_bytes,
3062 )
3063 };
3064
3065 let stats = &result.stats;
3066 if stats.read_failures > 0
3067 || stats.statement_range_failures > 0
3068 || stats.duplicate_deletes > 0
3069 {
3070 self.client
3071 .log_message(
3072 MessageType::WARNING,
3073 format!(
3074 "willDeleteFiles stats: read_failures={}, statement_range_failures={}, \
3075 duplicate_deletes={}, no_parent={}, dedup_skips={}",
3076 stats.read_failures,
3077 stats.statement_range_failures,
3078 stats.duplicate_deletes,
3079 stats.no_parent,
3080 stats.dedup_skips,
3081 ),
3082 )
3083 .await;
3084 }
3085
3086 let all_edits = result.edits;
3087 if all_edits.is_empty() {
3088 self.client
3089 .log_message(
3090 MessageType::INFO,
3091 "willDeleteFiles: no import-removal edits needed",
3092 )
3093 .await;
3094 return Ok(None);
3095 }
3096
3097 {
3098 let mut tc = self.text_cache.write().await;
3099 let patched = file_operations::apply_edits_to_cache(&all_edits, &mut tc);
3100 self.client
3101 .log_message(
3102 MessageType::INFO,
3103 format!("willDeleteFiles: patched {} cached file(s)", patched),
3104 )
3105 .await;
3106 }
3107
3108 let total_edits: usize = all_edits.values().map(|v| v.len()).sum();
3109 self.client
3110 .log_message(
3111 MessageType::INFO,
3112 format!(
3113 "willDeleteFiles: {} edit(s) across {} file(s)",
3114 total_edits,
3115 all_edits.len()
3116 ),
3117 )
3118 .await;
3119
3120 Ok(Some(WorkspaceEdit {
3121 changes: Some(all_edits),
3122 document_changes: None,
3123 change_annotations: None,
3124 }))
3125 }
3126
3127 async fn did_delete_files(&self, params: DeleteFilesParams) {
3128 self.client
3129 .log_message(
3130 MessageType::INFO,
3131 format!("workspace/didDeleteFiles: {} file(s)", params.files.len()),
3132 )
3133 .await;
3134
3135 let raw_delete_uris: Vec<Url> = params
3136 .files
3137 .iter()
3138 .filter_map(|fd| Url::parse(&fd.uri).ok())
3139 .collect();
3140
3141 let deleted_paths = {
3142 let tc = self.text_cache.read().await;
3143 let cache_paths: Vec<std::path::PathBuf> = tc
3144 .keys()
3145 .filter_map(|k| Url::parse(k).ok())
3146 .filter_map(|u| u.to_file_path().ok())
3147 .collect();
3148 drop(tc);
3149
3150 let cfg = self.foundry_config.read().await.clone();
3151 let discovered_paths =
3152 tokio::task::spawn_blocking(move || crate::solc::discover_source_files(&cfg))
3153 .await
3154 .unwrap_or_default();
3155
3156 let mut all_paths: HashSet<std::path::PathBuf> = discovered_paths.into_iter().collect();
3157 all_paths.extend(cache_paths);
3158 let all_paths: Vec<std::path::PathBuf> = all_paths.into_iter().collect();
3159
3160 file_operations::expand_folder_deletes_from_paths(&raw_delete_uris, &all_paths)
3161 };
3162
3163 let mut deleted_keys: HashSet<String> = HashSet::new();
3164 let mut deleted_uris: Vec<Url> = Vec::new();
3165 for path in deleted_paths {
3166 if let Ok(uri) = Url::from_file_path(&path) {
3167 deleted_keys.insert(uri.to_string());
3168 deleted_uris.push(uri);
3169 }
3170 }
3171 if deleted_keys.is_empty() {
3172 return;
3173 }
3174
3175 self.client
3176 .log_message(
3177 MessageType::INFO,
3178 format!(
3179 "didDeleteFiles: deleting {} cache/diagnostic entry(ies)",
3180 deleted_keys.len()
3181 ),
3182 )
3183 .await;
3184
3185 for uri in &deleted_uris {
3186 self.client
3187 .publish_diagnostics(uri.clone(), vec![], None)
3188 .await;
3189 }
3190
3191 let mut removed_text = 0usize;
3192 let mut removed_ast = 0usize;
3193 let mut removed_completion = 0usize;
3194 let mut removed_semantic = 0usize;
3195 let mut removed_pending_create = 0usize;
3196 {
3197 let mut tc = self.text_cache.write().await;
3198 for key in &deleted_keys {
3199 if tc.remove(key).is_some() {
3200 removed_text += 1;
3201 }
3202 }
3203 }
3204 {
3205 let mut ac = self.ast_cache.write().await;
3206 for key in &deleted_keys {
3207 if ac.remove(key).is_some() {
3208 removed_ast += 1;
3209 }
3210 }
3211 }
3212 {
3213 let mut cc = self.completion_cache.write().await;
3214 for key in &deleted_keys {
3215 if cc.remove(key).is_some() {
3216 removed_completion += 1;
3217 }
3218 }
3219 }
3220 {
3221 let mut sc = self.semantic_token_cache.write().await;
3222 for key in &deleted_keys {
3223 if sc.remove(key).is_some() {
3224 removed_semantic += 1;
3225 }
3226 }
3227 }
3228 {
3229 let mut pending = self.pending_create_scaffold.write().await;
3230 for key in &deleted_keys {
3231 if pending.remove(key) {
3232 removed_pending_create += 1;
3233 }
3234 }
3235 }
3236 self.client
3237 .log_message(
3238 MessageType::INFO,
3239 format!(
3240 "didDeleteFiles: removed caches text={} ast={} completion={} semantic={} pendingCreate={}",
3241 removed_text,
3242 removed_ast,
3243 removed_completion,
3244 removed_semantic,
3245 removed_pending_create,
3246 ),
3247 )
3248 .await;
3249
3250 let root_key = self.root_uri.read().await.as_ref().map(|u| u.to_string());
3251 if let Some(ref key) = root_key {
3252 self.ast_cache.write().await.remove(key);
3253 }
3254
3255 let foundry_config = self.foundry_config.read().await.clone();
3256 let ast_cache = self.ast_cache.clone();
3257 let client = self.client.clone();
3258 let text_cache_snapshot = self.text_cache.read().await.clone();
3259
3260 tokio::spawn(async move {
3261 let Some(cache_key) = root_key else {
3262 return;
3263 };
3264 match crate::solc::solc_project_index(
3265 &foundry_config,
3266 Some(&client),
3267 Some(&text_cache_snapshot),
3268 )
3269 .await
3270 {
3271 Ok(ast_data) => {
3272 let cached_build = Arc::new(crate::goto::CachedBuild::new(ast_data, 0));
3273 let source_count = cached_build.nodes.len();
3274 ast_cache.write().await.insert(cache_key, cached_build);
3275 client
3276 .log_message(
3277 MessageType::INFO,
3278 format!("didDeleteFiles: re-indexed {} source files", source_count),
3279 )
3280 .await;
3281 }
3282 Err(e) => {
3283 client
3284 .log_message(
3285 MessageType::WARNING,
3286 format!("didDeleteFiles: re-index failed: {e}"),
3287 )
3288 .await;
3289 }
3290 }
3291 });
3292 }
3293
3294 async fn will_create_files(
3295 &self,
3296 params: CreateFilesParams,
3297 ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
3298 self.client
3299 .log_message(
3300 MessageType::INFO,
3301 format!("workspace/willCreateFiles: {} file(s)", params.files.len()),
3302 )
3303 .await;
3304 if !self
3305 .settings
3306 .read()
3307 .await
3308 .file_operations
3309 .template_on_create
3310 {
3311 self.client
3312 .log_message(
3313 MessageType::INFO,
3314 "willCreateFiles: templateOnCreate disabled",
3315 )
3316 .await;
3317 return Ok(None);
3318 }
3319 self.client
3320 .log_message(
3321 MessageType::INFO,
3322 "willCreateFiles: skipping pre-create edits; scaffolding via didCreateFiles",
3323 )
3324 .await;
3325 Ok(None)
3326 }
3327
3328 async fn did_create_files(&self, params: CreateFilesParams) {
3329 self.client
3330 .log_message(
3331 MessageType::INFO,
3332 format!("workspace/didCreateFiles: {} file(s)", params.files.len()),
3333 )
3334 .await;
3335 if !self
3336 .settings
3337 .read()
3338 .await
3339 .file_operations
3340 .template_on_create
3341 {
3342 self.client
3343 .log_message(
3344 MessageType::INFO,
3345 "didCreateFiles: templateOnCreate disabled",
3346 )
3347 .await;
3348 return;
3349 }
3350
3351 let config = self.foundry_config.read().await;
3352 let solc_version = config.solc_version.clone();
3353 drop(config);
3354
3355 let mut apply_edits: HashMap<Url, Vec<TextEdit>> = HashMap::new();
3360 let mut staged_content: HashMap<String, String> = HashMap::new();
3361 let mut created_uris: Vec<String> = Vec::new();
3362 {
3363 let tc = self.text_cache.read().await;
3364 for file_create in ¶ms.files {
3365 let uri = match Url::parse(&file_create.uri) {
3366 Ok(u) => u,
3367 Err(_) => continue,
3368 };
3369 let uri_str = uri.to_string();
3370
3371 let open_has_content = tc
3372 .get(&uri_str)
3373 .map_or(false, |(_, c)| c.chars().any(|ch| !ch.is_whitespace()));
3374 let path = match uri.to_file_path() {
3375 Ok(p) => p,
3376 Err(_) => continue,
3377 };
3378 let disk_has_content = std::fs::read_to_string(&path)
3379 .map_or(false, |c| c.chars().any(|ch| !ch.is_whitespace()));
3380
3381 if open_has_content {
3384 self.client
3385 .log_message(
3386 MessageType::INFO,
3387 format!(
3388 "didCreateFiles: skip {} (open buffer already has content)",
3389 uri_str
3390 ),
3391 )
3392 .await;
3393 continue;
3394 }
3395
3396 if disk_has_content {
3398 self.client
3399 .log_message(
3400 MessageType::INFO,
3401 format!(
3402 "didCreateFiles: skip {} (disk file already has content)",
3403 uri_str
3404 ),
3405 )
3406 .await;
3407 continue;
3408 }
3409
3410 let content =
3411 match file_operations::generate_scaffold(&uri, solc_version.as_deref()) {
3412 Some(s) => s,
3413 None => continue,
3414 };
3415
3416 staged_content.insert(uri_str, content.clone());
3417 created_uris.push(uri.to_string());
3418
3419 apply_edits.entry(uri).or_default().push(TextEdit {
3420 range: Range {
3421 start: Position {
3422 line: 0,
3423 character: 0,
3424 },
3425 end: Position {
3426 line: 0,
3427 character: 0,
3428 },
3429 },
3430 new_text: content,
3431 });
3432 }
3433 }
3434
3435 if !apply_edits.is_empty() {
3436 {
3437 let mut pending = self.pending_create_scaffold.write().await;
3438 for uri in &created_uris {
3439 pending.insert(uri.clone());
3440 }
3441 }
3442
3443 let edit = WorkspaceEdit {
3444 changes: Some(apply_edits.clone()),
3445 document_changes: None,
3446 change_annotations: None,
3447 };
3448 self.client
3449 .log_message(
3450 MessageType::INFO,
3451 format!(
3452 "didCreateFiles: scaffolding {} empty file(s) via workspace/applyEdit",
3453 apply_edits.len()
3454 ),
3455 )
3456 .await;
3457 let apply_result = self.client.apply_edit(edit).await;
3458 let applied = apply_result.as_ref().is_ok_and(|r| r.applied);
3459
3460 if applied {
3461 let mut tc = self.text_cache.write().await;
3462 for (uri_str, content) in staged_content {
3463 tc.insert(uri_str, (0, content));
3464 }
3465 } else {
3466 if let Ok(resp) = &apply_result {
3467 self.client
3468 .log_message(
3469 MessageType::WARNING,
3470 format!(
3471 "didCreateFiles: applyEdit rejected (no disk fallback): {:?}",
3472 resp.failure_reason
3473 ),
3474 )
3475 .await;
3476 } else if let Err(e) = &apply_result {
3477 self.client
3478 .log_message(
3479 MessageType::WARNING,
3480 format!("didCreateFiles: applyEdit failed (no disk fallback): {e}"),
3481 )
3482 .await;
3483 }
3484 }
3485 }
3486
3487 for file_create in ¶ms.files {
3491 let Ok(uri) = Url::parse(&file_create.uri) else {
3492 continue;
3493 };
3494 let (version, content) = {
3495 let tc = self.text_cache.read().await;
3496 match tc.get(&uri.to_string()) {
3497 Some((v, c)) => (*v, c.clone()),
3498 None => continue,
3499 }
3500 };
3501 if !content.chars().any(|ch| !ch.is_whitespace()) {
3502 continue;
3503 }
3504 self.on_change(TextDocumentItem {
3505 uri,
3506 version,
3507 text: content,
3508 language_id: "solidity".to_string(),
3509 })
3510 .await;
3511 }
3512
3513 let root_key = self.root_uri.read().await.as_ref().map(|u| u.to_string());
3515 if let Some(ref key) = root_key {
3516 self.ast_cache.write().await.remove(key);
3517 }
3518
3519 let foundry_config = self.foundry_config.read().await.clone();
3520 let ast_cache = self.ast_cache.clone();
3521 let client = self.client.clone();
3522 let text_cache_snapshot = self.text_cache.read().await.clone();
3523
3524 tokio::spawn(async move {
3525 let Some(cache_key) = root_key else {
3526 return;
3527 };
3528 match crate::solc::solc_project_index(
3529 &foundry_config,
3530 Some(&client),
3531 Some(&text_cache_snapshot),
3532 )
3533 .await
3534 {
3535 Ok(ast_data) => {
3536 let cached_build = Arc::new(crate::goto::CachedBuild::new(ast_data, 0));
3537 let source_count = cached_build.nodes.len();
3538 ast_cache.write().await.insert(cache_key, cached_build);
3539 client
3540 .log_message(
3541 MessageType::INFO,
3542 format!("didCreateFiles: re-indexed {} source files", source_count),
3543 )
3544 .await;
3545 }
3546 Err(e) => {
3547 client
3548 .log_message(
3549 MessageType::WARNING,
3550 format!("didCreateFiles: re-index failed: {e}"),
3551 )
3552 .await;
3553 }
3554 }
3555 });
3556 }
3557}
3558
3559#[cfg(test)]
3560mod tests {
3561 use super::update_imports_on_delete_enabled;
3562
3563 #[test]
3564 fn update_imports_on_delete_enabled_defaults_true() {
3565 let s = crate::config::Settings::default();
3566 assert!(update_imports_on_delete_enabled(&s));
3567 }
3568
3569 #[test]
3570 fn update_imports_on_delete_enabled_respects_false() {
3571 let mut s = crate::config::Settings::default();
3572 s.file_operations.update_imports_on_delete = false;
3573 assert!(!update_imports_on_delete_enabled(&s));
3574 }
3575}