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::types::DocumentUri;
17use crate::types::ErrorCode;
18use crate::utils;
19use std::collections::{HashMap, HashSet};
20use std::path::{Component, Path, PathBuf};
21use std::sync::Arc;
22use std::sync::atomic::{AtomicU64, Ordering};
23use tokio::sync::RwLock;
24use tower_lsp::{Client, LanguageServer, lsp_types::*};
25
26type SemanticTokenCache = HashMap<DocumentUri, (String, Vec<SemanticToken>)>;
28
29const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
33
34async fn check_for_updates(client: Client) {
36 let url = "https://api.github.com/repos/asyncswap/solidity-language-server/releases/latest";
37
38 let http = match reqwest::Client::builder()
39 .user_agent("solidity-language-server")
40 .timeout(std::time::Duration::from_secs(5))
41 .build()
42 {
43 Ok(c) => c,
44 Err(_) => return,
45 };
46
47 let resp = match http.get(url).send().await {
48 Ok(r) if r.status().is_success() => r,
49 _ => return,
50 };
51
52 let body: serde_json::Value = match resp.json().await {
53 Ok(v) => v,
54 Err(_) => return,
55 };
56
57 let tag = match body.get("tag_name").and_then(|v| v.as_str()) {
58 Some(t) => t.strip_prefix('v').unwrap_or(t),
59 None => return,
60 };
61
62 let latest = match semver::Version::parse(tag) {
63 Ok(v) => v,
64 Err(_) => return,
65 };
66
67 let current = match semver::Version::parse(CURRENT_VERSION) {
68 Ok(v) => v,
69 Err(_) => return,
70 };
71
72 if latest > current {
73 client
74 .show_message(
75 MessageType::INFO,
76 format!(
77 "Solidity Language Server v{latest} is available (current: v{current}). \
78 Update: cargo install solidity-language-server"
79 ),
80 )
81 .await;
82 }
83}
84
85type CompletionSourceText = (Arc<completion::CompletionCache>, Arc<str>);
87
88#[derive(Clone)]
89pub struct ForgeLsp {
90 client: Client,
91 compiler: Arc<dyn Runner>,
92 ast_cache: Arc<RwLock<HashMap<DocumentUri, Arc<goto::CachedBuild>>>>,
93 text_cache: Arc<RwLock<HashMap<DocumentUri, (i32, String)>>>,
97 completion_cache: Arc<RwLock<HashMap<DocumentUri, Arc<completion::CompletionCache>>>>,
98 completion_source_text: Arc<RwLock<HashMap<DocumentUri, CompletionSourceText>>>,
103 lint_config: Arc<RwLock<LintConfig>>,
105 foundry_config: Arc<RwLock<FoundryConfig>>,
107 client_capabilities: Arc<RwLock<Option<ClientCapabilities>>>,
109 settings: Arc<RwLock<Settings>>,
111 use_solc: bool,
113 semantic_token_cache: Arc<RwLock<SemanticTokenCache>>,
115 semantic_token_id: Arc<AtomicU64>,
117 root_uri: Arc<RwLock<Option<Url>>>,
119 project_indexed: Arc<std::sync::atomic::AtomicBool>,
121 project_cache_dirty: Arc<std::sync::atomic::AtomicBool>,
124 project_cache_sync_running: Arc<std::sync::atomic::AtomicBool>,
126 project_cache_sync_pending: Arc<std::sync::atomic::AtomicBool>,
128 project_cache_force_full_rebuild: Arc<std::sync::atomic::AtomicBool>,
133 project_cache_upsert_running: Arc<std::sync::atomic::AtomicBool>,
135 project_cache_upsert_pending: Arc<std::sync::atomic::AtomicBool>,
137 project_cache_changed_files: Arc<RwLock<HashSet<String>>>,
140 project_cache_upsert_files: Arc<RwLock<HashSet<String>>>,
142 pending_create_scaffold: Arc<RwLock<HashSet<DocumentUri>>>,
145 settings_from_init: Arc<std::sync::atomic::AtomicBool>,
149 did_save_workers: Arc<
155 RwLock<HashMap<DocumentUri, tokio::sync::watch::Sender<Option<DidSaveTextDocumentParams>>>>,
156 >,
157 code_action_db: Arc<HashMap<ErrorCode, crate::code_actions::CodeActionDef>>,
159 sub_caches: Arc<RwLock<Vec<Arc<goto::CachedBuild>>>>,
170 sub_caches_loading: Arc<std::sync::atomic::AtomicBool>,
172 path_interner: Arc<RwLock<crate::types::PathInterner>>,
180 cross_file_diag_uris: Arc<RwLock<HashSet<Url>>>,
184}
185
186fn spawn_load_lib_sub_caches_task(
192 foundry_config: crate::config::FoundryConfig,
193 sub_caches: Arc<RwLock<Vec<Arc<goto::CachedBuild>>>>,
194 loading_flag: Arc<std::sync::atomic::AtomicBool>,
195 path_interner: Arc<RwLock<crate::types::PathInterner>>,
196 client: Client,
197) {
198 if loading_flag
200 .compare_exchange(
201 false,
202 true,
203 std::sync::atomic::Ordering::SeqCst,
204 std::sync::atomic::Ordering::SeqCst,
205 )
206 .is_err()
207 {
208 return;
209 }
210 tokio::spawn(async move {
211 let cfg = foundry_config.clone();
212 let discovered = tokio::task::spawn_blocking(move || {
213 crate::project_cache::discover_lib_sub_projects(&cfg)
214 })
215 .await
216 .unwrap_or_else(|_| crate::project_cache::DiscoveredLibs {
217 cached: Vec::new(),
218 uncached: Vec::new(),
219 });
220
221 let sub_caches_start = std::time::Instant::now();
223 spawn_and_collect_sub_cache_builds(&discovered.uncached, &client, &path_interner).await;
224
225 let cfg2 = foundry_config.clone();
227 let all_cached =
228 tokio::task::spawn_blocking(move || crate::project_cache::discover_lib_caches(&cfg2))
229 .await
230 .unwrap_or_default();
231
232 if all_cached.is_empty() {
233 emit_sub_caches_loaded(&client, 0, 0, sub_caches_start.elapsed().as_secs_f64()).await;
234 loading_flag.store(false, std::sync::atomic::Ordering::SeqCst);
235 return;
236 }
237
238 let mut loaded = Vec::new();
239 for sub_root in &all_cached {
240 let root = sub_root.clone();
241 let build =
242 tokio::task::spawn_blocking(move || crate::project_cache::load_lib_cache(&root))
243 .await
244 .ok()
245 .flatten();
246 if let Some(build) = build {
247 {
250 let mut interner = path_interner.write().await;
251 for path in build.id_to_path_map.values() {
252 interner.intern(path);
253 }
254 }
255 loaded.push(Arc::new(build));
256 }
257 }
258
259 let count = loaded.len();
260 let total: usize = loaded.iter().map(|b| b.nodes.len()).sum();
261 let elapsed = sub_caches_start.elapsed().as_secs_f64();
262
263 if !loaded.is_empty() {
264 client
265 .log_message(
266 MessageType::INFO,
267 format!(
268 "sub-caches: loaded {} lib caches ({} total sources, {:.1}s total)",
269 count, total, elapsed,
270 ),
271 )
272 .await;
273 *sub_caches.write().await = loaded;
274 }
275
276 emit_sub_caches_loaded(&client, count, total, elapsed).await;
277 loading_flag.store(false, std::sync::atomic::Ordering::SeqCst);
278 });
279}
280
281async fn spawn_and_collect_sub_cache_builds(
288 roots: &[std::path::PathBuf],
289 client: &Client,
290 path_interner: &Arc<RwLock<crate::types::PathInterner>>,
291) {
292 if roots.is_empty() {
293 return;
294 }
295 let max_parallel = std::thread::available_parallelism()
296 .map(|n| n.get())
297 .unwrap_or(4);
298 let semaphore = Arc::new(tokio::sync::Semaphore::new(max_parallel));
299 client
300 .log_message(
301 MessageType::INFO,
302 format!(
303 "sub-cache: building {} libs (max {max_parallel} parallel)",
304 roots.len()
305 ),
306 )
307 .await;
308 let mut join_set = tokio::task::JoinSet::new();
309 for sub_root in roots {
310 let sub_name = sub_root
311 .file_name()
312 .map(|n| n.to_string_lossy().into_owned())
313 .unwrap_or_else(|| sub_root.display().to_string());
314 let sub_config =
315 crate::config::load_foundry_config_from_toml(&sub_root.join("foundry.toml"));
316 let sem = semaphore.clone();
317 join_set.spawn(async move {
318 let _permit = sem.acquire().await.expect("semaphore closed");
319 let sub_start = std::time::Instant::now();
320 let result = crate::solc::solc_project_index_ast_only(&sub_config, None).await;
321 let elapsed = sub_start.elapsed().as_secs_f64();
322 (sub_name, sub_config, result, elapsed)
323 });
324 }
325
326 while let Some(join_result) = join_set.join_next().await {
327 let Ok((sub_name, sub_config, result, elapsed)) = join_result else {
328 continue;
329 };
330 match result {
331 Ok(ast_data) => {
332 let mut interner = path_interner.write().await;
333 let build = crate::goto::CachedBuild::new(ast_data, 0, Some(&mut interner));
334 drop(interner);
335 let source_count = build.nodes.len();
336 if source_count == 0 {
337 client
338 .log_message(
339 MessageType::WARNING,
340 format!("sub-cache: {sub_name} produced 0 sources"),
341 )
342 .await;
343 continue;
344 }
345 let cfg_for_save = sub_config.clone();
346 let build_for_save = build.clone();
347 let _ = tokio::task::spawn_blocking(move || {
348 crate::project_cache::save_reference_cache_with_report(
349 &cfg_for_save,
350 &build_for_save,
351 None,
352 )
353 })
354 .await;
355 }
356 Err(e) => {
357 client
358 .log_message(
359 MessageType::WARNING,
360 format!("sub-cache: {sub_name} failed ({elapsed:.1}s): {e}"),
361 )
362 .await;
363 }
364 }
365 }
366}
367
368async fn emit_sub_caches_loaded(client: &Client, count: usize, total: usize, elapsed: f64) {
371 let token = NumberOrString::String("solidity/subCachesLoaded".to_string());
372 let _ = client
373 .send_request::<request::WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
374 token: token.clone(),
375 })
376 .await;
377 client
378 .send_notification::<notification::Progress>(ProgressParams {
379 token: token.clone(),
380 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(WorkDoneProgressBegin {
381 title: "Sub-caches loaded".to_string(),
382 message: Some(format!(
383 "{count} lib caches ({total} sources) in {elapsed:.1}s",
384 )),
385 cancellable: Some(false),
386 percentage: None,
387 })),
388 })
389 .await;
390 client
391 .send_notification::<notification::Progress>(ProgressParams {
392 token,
393 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(WorkDoneProgressEnd {
394 message: Some(format!("Loaded {count} lib caches ({total} sources)",)),
395 })),
396 })
397 .await;
398}
399
400impl ForgeLsp {
401 pub fn new(client: Client, use_solar: bool, use_solc: bool) -> Self {
402 let compiler: Arc<dyn Runner> = if use_solar {
403 Arc::new(crate::solar_runner::SolarRunner)
404 } else {
405 Arc::new(ForgeRunner)
406 };
407 let ast_cache = Arc::new(RwLock::new(HashMap::new()));
408 let text_cache = Arc::new(RwLock::new(HashMap::new()));
409 let completion_cache = Arc::new(RwLock::new(HashMap::new()));
410 let completion_source_text = Arc::new(RwLock::new(HashMap::new()));
411 let lint_config = Arc::new(RwLock::new(LintConfig::default()));
412 let foundry_config = Arc::new(RwLock::new(FoundryConfig::default()));
413 let client_capabilities = Arc::new(RwLock::new(None));
414 let settings = Arc::new(RwLock::new(Settings::default()));
415 Self {
416 client,
417 compiler,
418 ast_cache,
419 text_cache,
420 completion_cache,
421 completion_source_text,
422 lint_config,
423 foundry_config,
424 client_capabilities,
425 settings,
426 use_solc,
427 semantic_token_cache: Arc::new(RwLock::new(HashMap::new())),
428 semantic_token_id: Arc::new(AtomicU64::new(0)),
429 root_uri: Arc::new(RwLock::new(None)),
430 project_indexed: Arc::new(std::sync::atomic::AtomicBool::new(false)),
431 project_cache_dirty: Arc::new(std::sync::atomic::AtomicBool::new(false)),
432 project_cache_sync_running: Arc::new(std::sync::atomic::AtomicBool::new(false)),
433 project_cache_sync_pending: Arc::new(std::sync::atomic::AtomicBool::new(false)),
434 project_cache_force_full_rebuild: Arc::new(std::sync::atomic::AtomicBool::new(false)),
435 project_cache_upsert_running: Arc::new(std::sync::atomic::AtomicBool::new(false)),
436 project_cache_upsert_pending: Arc::new(std::sync::atomic::AtomicBool::new(false)),
437 project_cache_changed_files: Arc::new(RwLock::new(HashSet::new())),
438 project_cache_upsert_files: Arc::new(RwLock::new(HashSet::new())),
439 pending_create_scaffold: Arc::new(RwLock::new(HashSet::new())),
440 settings_from_init: Arc::new(std::sync::atomic::AtomicBool::new(false)),
441 did_save_workers: Arc::new(RwLock::new(HashMap::new())),
442 code_action_db: Arc::new(crate::code_actions::load()),
443 sub_caches: Arc::new(RwLock::new(Vec::new())),
444 sub_caches_loading: Arc::new(std::sync::atomic::AtomicBool::new(false)),
445 path_interner: Arc::new(RwLock::new(crate::types::PathInterner::new())),
446 cross_file_diag_uris: Arc::new(RwLock::new(HashSet::new())),
447 }
448 }
449
450 async fn foundry_config_for_file(&self, file_path: &std::path::Path) -> FoundryConfig {
458 config::load_foundry_config(file_path)
459 }
460
461 async fn project_cache_key(&self) -> Option<String> {
466 if let Some(uri) = self.root_uri.read().await.as_ref() {
467 return Some(uri.to_string());
468 }
469
470 let mut root = self.foundry_config.read().await.root.clone();
471 if !root.is_absolute()
472 && let Ok(cwd) = std::env::current_dir()
473 {
474 root = cwd.join(root);
475 }
476 if !root.is_dir() {
477 root = root.parent()?.to_path_buf();
478 }
479 Url::from_directory_path(root).ok().map(|u| u.to_string())
480 }
481
482 fn spawn_load_lib_sub_caches(&self) {
491 let foundry_config = self.foundry_config.clone();
492 let sub_caches = self.sub_caches.clone();
493 let loading_flag = self.sub_caches_loading.clone();
494 let path_interner = self.path_interner.clone();
495 let client = self.client.clone();
496 tokio::spawn(async move {
497 let cfg = foundry_config.read().await.clone();
498 spawn_load_lib_sub_caches_task(cfg, sub_caches, loading_flag, path_interner, client);
499 });
500 }
501
502 async fn invalidate_lib_sub_caches_if_affected(&self, changed_paths: &[std::path::PathBuf]) {
506 let config = self.foundry_config.read().await.clone();
507 let affected = changed_paths.iter().any(|p| {
508 config
509 .libs
510 .iter()
511 .any(|lib_name| p.starts_with(config.root.join(lib_name)))
512 });
513 if affected {
514 self.sub_caches.write().await.clear();
515 self.spawn_load_lib_sub_caches();
516 }
517 }
518
519 async fn ensure_project_cached_build(&self) -> Option<Arc<goto::CachedBuild>> {
524 let root_key = self.project_cache_key().await?;
525 if let Some(existing) = self.ast_cache.read().await.get(&root_key).cloned() {
526 self.spawn_load_lib_sub_caches();
528 return Some(existing);
529 }
530
531 let settings = self.settings.read().await.clone();
532 if !self.use_solc || !settings.project_index.full_project_scan {
533 return None;
534 }
535
536 let foundry_config = self.foundry_config.read().await.clone();
537 if !foundry_config.root.is_dir() {
538 return None;
539 }
540
541 let cache_mode = settings.project_index.cache_mode.clone();
542 let cfg_for_load = foundry_config.clone();
543 let load_res = tokio::task::spawn_blocking(move || {
544 crate::project_cache::load_reference_cache_with_report(&cfg_for_load, cache_mode, true)
545 })
546 .await;
547
548 let Ok(report) = load_res else {
549 return None;
550 };
551 let build = report.build?;
552
553 let source_count = build.nodes.len();
554 let complete = report.complete;
555 let duration_ms = report.duration_ms;
556 let reused = report.file_count_reused;
557 let hashed = report.file_count_hashed;
558 let arc = Arc::new(build);
559 self.ast_cache
560 .write()
561 .await
562 .insert(root_key.clone().into(), arc.clone());
563 self.project_indexed
564 .store(true, std::sync::atomic::Ordering::Relaxed);
565 self.client
566 .log_message(
567 MessageType::INFO,
568 format!(
569 "references warm-load: project cache loaded (sources={}, reused_files={}/{}, complete={}, duration={}ms)",
570 source_count, reused, hashed, complete, duration_ms
571 ),
572 )
573 .await;
574
575 self.spawn_load_lib_sub_caches();
577
578 if complete {
579 return Some(arc);
580 }
581
582 let cfg_for_diff = foundry_config.clone();
585 let changed = tokio::task::spawn_blocking(move || {
586 crate::project_cache::changed_files_since_v2_cache(&cfg_for_diff, true)
587 })
588 .await
589 .ok()
590 .and_then(Result::ok)
591 .unwrap_or_default();
592
593 if changed.is_empty() {
594 return Some(arc);
595 }
596
597 let remappings = crate::solc::resolve_remappings(&foundry_config).await;
598 let cfg_for_plan = foundry_config.clone();
599 let changed_for_plan = changed.clone();
600 let remappings_for_plan = remappings.clone();
601 let affected_set = tokio::task::spawn_blocking(move || {
602 compute_reverse_import_closure(&cfg_for_plan, &changed_for_plan, &remappings_for_plan)
603 })
604 .await
605 .ok()
606 .unwrap_or_default();
607 let mut affected_files: Vec<PathBuf> = affected_set.into_iter().collect();
608 if affected_files.is_empty() {
609 affected_files = changed;
610 }
611
612 let text_cache_snapshot = self.text_cache.read().await.clone();
613 match crate::solc::solc_project_index_scoped(
614 &foundry_config,
615 Some(&self.client),
616 Some(&text_cache_snapshot),
617 &affected_files,
618 )
619 .await
620 {
621 Ok(ast_data) => {
622 let scoped_build = Arc::new(crate::goto::CachedBuild::new(
623 ast_data,
624 0,
625 Some(&mut *self.path_interner.write().await),
626 ));
627 let mut merge_error: Option<String> = None;
628 let merged = {
629 let mut cache = self.ast_cache.write().await;
630 let merged = if let Some(existing) = cache.get(&root_key).cloned() {
631 let mut merged = (*existing).clone();
632 match merge_scoped_cached_build(&mut merged, (*scoped_build).clone()) {
633 Ok(_) => Arc::new(merged),
634 Err(e) => {
635 merge_error = Some(e);
636 scoped_build.clone()
637 }
638 }
639 } else {
640 scoped_build.clone()
641 };
642 cache.insert(root_key.clone().into(), merged.clone());
643 merged
644 };
645 if let Some(e) = merge_error {
646 self.client
647 .log_message(
648 MessageType::WARNING,
649 format!(
650 "references warm-load reconcile: merge failed, using scoped build: {}",
651 e
652 ),
653 )
654 .await;
655 }
656
657 let cfg_for_save = foundry_config.clone();
658 let build_for_save = (*merged).clone();
659 let save_res = tokio::task::spawn_blocking(move || {
660 crate::project_cache::save_reference_cache_with_report(
661 &cfg_for_save,
662 &build_for_save,
663 None,
664 )
665 })
666 .await;
667 if let Ok(Ok(report)) = save_res {
668 self.client
669 .log_message(
670 MessageType::INFO,
671 format!(
672 "references warm-load reconcile: saved cache (affected={}, hashed_files={}, duration={}ms)",
673 affected_files.len(),
674 report.file_count_hashed,
675 report.duration_ms
676 ),
677 )
678 .await;
679 }
680 Some(merged)
681 }
682 Err(e) => {
683 self.client
684 .log_message(
685 MessageType::WARNING,
686 format!(
687 "references warm-load reconcile: scoped reindex failed: {}",
688 e
689 ),
690 )
691 .await;
692 Some(arc)
693 }
694 }
695 }
696
697 async fn flush_project_cache_to_disk(&self, reason: &str) {
701 if !self.use_solc || !self.settings.read().await.project_index.full_project_scan {
702 return;
703 }
704 let Some(root_key) = self.project_cache_key().await else {
705 return;
706 };
707 let build = {
708 let cache = self.ast_cache.read().await;
709 cache.get(&root_key).cloned()
710 };
711 let Some(build) = build else {
712 return;
713 };
714
715 let foundry_config = self.foundry_config.read().await.clone();
716 let build_for_save = (*build).clone();
717 let res = tokio::task::spawn_blocking(move || {
718 crate::project_cache::save_reference_cache_with_report(
719 &foundry_config,
720 &build_for_save,
721 None,
722 )
723 })
724 .await;
725
726 match res {
727 Ok(Ok(report)) => {
728 self.client
729 .log_message(
730 MessageType::INFO,
731 format!(
732 "project cache flush ({}): saved hashed_files={}, duration={}ms",
733 reason, report.file_count_hashed, report.duration_ms
734 ),
735 )
736 .await;
737 }
738 Ok(Err(e)) => {
739 self.client
740 .log_message(
741 MessageType::WARNING,
742 format!("project cache flush ({}) failed: {}", reason, e),
743 )
744 .await;
745 }
746 Err(e) => {
747 self.client
748 .log_message(
749 MessageType::WARNING,
750 format!("project cache flush ({}) task failed: {}", reason, e),
751 )
752 .await;
753 }
754 }
755 }
756
757 async fn on_change(&self, params: TextDocumentItem) {
758 let uri = params.uri.clone();
759 let version = params.version;
760
761 let file_path = match uri.to_file_path() {
762 Ok(path) => path,
763 Err(_) => {
764 self.client
765 .log_message(MessageType::ERROR, "Invalid file URI")
766 .await;
767 return;
768 }
769 };
770
771 let path_str = match file_path.to_str() {
772 Some(s) => s,
773 None => {
774 self.client
775 .log_message(MessageType::ERROR, "Invalid file path")
776 .await;
777 return;
778 }
779 };
780
781 self.client
785 .publish_diagnostics(uri.clone(), vec![], None)
786 .await;
787
788 let (should_lint, lint_settings) = {
790 let lint_cfg = self.lint_config.read().await;
791 let settings = self.settings.read().await;
792 let enabled = lint_cfg.should_lint(&file_path) && settings.lint.enabled;
793 let ls = settings.lint.clone();
794 (enabled, ls)
795 };
796
797 let (lint_result, build_result, ast_result) = if self.use_solc {
800 let foundry_cfg = self.foundry_config_for_file(&file_path).await;
801 let solc_future = crate::solc::solc_ast(
804 path_str,
805 &foundry_cfg,
806 Some(&self.client),
807 Some(¶ms.text),
808 );
809
810 if should_lint {
811 let (lint, solc) = tokio::join!(
812 self.compiler.get_lint_diagnostics(&uri, &lint_settings),
813 solc_future
814 );
815 match solc {
816 Ok(data) => {
817 let build_diags = crate::build::build_output_to_diagnostics(
821 &data,
822 &file_path,
823 ¶ms.text,
824 &foundry_cfg.ignored_error_codes,
825 );
826 (Some(lint), Ok(build_diags), Ok(data))
827 }
828 Err(e) => {
829 self.client
830 .log_message(
831 MessageType::WARNING,
832 format!("solc failed, falling back to forge build: {e}"),
833 )
834 .await;
835 let (build, ast) = tokio::join!(
836 self.compiler.get_build_diagnostics(&uri),
837 self.compiler.ast(path_str)
838 );
839 (Some(lint), build, ast)
840 }
841 }
842 } else {
843 self.client
844 .log_message(
845 MessageType::INFO,
846 format!("skipping lint for ignored file: {path_str}"),
847 )
848 .await;
849 match solc_future.await {
850 Ok(data) => {
851 let build_diags = crate::build::build_output_to_diagnostics(
852 &data,
853 &file_path,
854 ¶ms.text,
855 &foundry_cfg.ignored_error_codes,
856 );
857 (None, Ok(build_diags), Ok(data))
858 }
859 Err(e) => {
860 self.client
861 .log_message(
862 MessageType::WARNING,
863 format!("solc failed, falling back to forge build: {e}"),
864 )
865 .await;
866 let (build, ast) = tokio::join!(
867 self.compiler.get_build_diagnostics(&uri),
868 self.compiler.ast(path_str)
869 );
870 (None, build, ast)
871 }
872 }
873 }
874 } else {
875 if should_lint {
877 let (lint, build, ast) = tokio::join!(
878 self.compiler.get_lint_diagnostics(&uri, &lint_settings),
879 self.compiler.get_build_diagnostics(&uri),
880 self.compiler.ast(path_str)
881 );
882 (Some(lint), build, ast)
883 } else {
884 self.client
885 .log_message(
886 MessageType::INFO,
887 format!("skipping lint for ignored file: {path_str}"),
888 )
889 .await;
890 let (build, ast) = tokio::join!(
891 self.compiler.get_build_diagnostics(&uri),
892 self.compiler.ast(path_str)
893 );
894 (None, build, ast)
895 }
896 };
897
898 let has_file_local_errors = matches!(
908 &build_result,
909 Ok(diagnostics) if diagnostics.iter().any(|d| d.severity == Some(DiagnosticSeverity::ERROR))
910 );
911 let has_solc_errors = ast_result.as_ref().is_ok_and(|data| {
912 data.get("errors")
913 .and_then(|v| v.as_array())
914 .is_some_and(|errs| {
915 errs.iter().any(|e| {
916 e.get("severity")
917 .and_then(|s| s.as_str())
918 .is_some_and(|s| s == "error")
919 })
920 })
921 });
922 let build_succeeded = !has_file_local_errors && !has_solc_errors;
923
924 let cross_file_diags = if has_solc_errors {
928 if let Ok(ref data) = ast_result {
929 let cfg = self.foundry_config_for_file(&file_path).await;
930 crate::build::cross_file_error_diagnostics(
931 data,
932 &file_path,
933 &cfg.root,
934 &cfg.ignored_error_codes,
935 )
936 } else {
937 std::collections::HashMap::new()
938 }
939 } else {
940 std::collections::HashMap::new()
941 };
942
943 if build_succeeded {
944 if let Ok(ast_data) = ast_result {
945 let sources_empty = ast_data
949 .get("sources")
950 .and_then(|v| v.as_object())
951 .is_none_or(|m| m.is_empty());
952
953 if sources_empty {
954 self.client
955 .log_message(
956 MessageType::INFO,
957 "Build produced empty AST, keeping existing cache",
958 )
959 .await;
960 } else {
961 let cached_build = goto::CachedBuild::new(
962 ast_data,
963 version,
964 Some(&mut *self.path_interner.write().await),
965 );
966 let cached_build = Arc::new(cached_build);
967 let mut cache = self.ast_cache.write().await;
968 cache.insert(uri.to_string().into(), cached_build.clone());
969 drop(cache);
970
971 {
973 let mut cc = self.completion_cache.write().await;
974 cc.insert(
975 uri.to_string().into(),
976 cached_build.completion_cache.clone(),
977 );
978 }
979 self.completion_source_text.write().await.insert(
980 uri.to_string().into(),
981 (
982 cached_build.completion_cache.clone(),
983 Arc::from(params.text.as_str()),
984 ),
985 );
986 }
987 } else if let Err(e) = ast_result {
988 self.client
989 .log_message(
990 MessageType::INFO,
991 format!("Build succeeded but failed to get AST: {e}"),
992 )
993 .await;
994 }
995 } else {
996 let reason = if has_solc_errors && !has_file_local_errors {
999 "Cross-file compilation errors detected, keeping existing AST cache"
1000 } else {
1001 "Build errors detected, keeping existing AST cache"
1002 };
1003 self.client.log_message(MessageType::INFO, reason).await;
1004 }
1005
1006 {
1008 let mut text_cache = self.text_cache.write().await;
1009 let uri_str = uri.to_string();
1010 let existing_version = text_cache.get(&uri_str).map(|(v, _)| *v).unwrap_or(-1);
1011 if version >= existing_version {
1012 text_cache.insert(uri_str.into(), (version, params.text));
1013 }
1014 }
1015
1016 let mut all_diagnostics = vec![];
1017
1018 if let Some(lint_result) = lint_result {
1019 match lint_result {
1020 Ok(mut lints) => {
1021 if !lint_settings.exclude.is_empty() {
1023 lints.retain(|d| {
1024 if let Some(NumberOrString::String(code)) = &d.code {
1025 !lint_settings.exclude.iter().any(|ex| ex == code)
1026 } else {
1027 true
1028 }
1029 });
1030 }
1031 if !lints.is_empty() {
1032 self.client
1033 .log_message(
1034 MessageType::INFO,
1035 format!("found {} lint diagnostics", lints.len()),
1036 )
1037 .await;
1038 }
1039 all_diagnostics.append(&mut lints);
1040 }
1041 Err(e) => {
1042 self.client
1043 .log_message(
1044 MessageType::ERROR,
1045 format!("Forge lint diagnostics failed: {e}"),
1046 )
1047 .await;
1048 }
1049 }
1050 }
1051
1052 match build_result {
1053 Ok(mut builds) => {
1054 if !builds.is_empty() {
1055 self.client
1056 .log_message(
1057 MessageType::INFO,
1058 format!("found {} build diagnostics", builds.len()),
1059 )
1060 .await;
1061 }
1062 all_diagnostics.append(&mut builds);
1063 }
1064 Err(e) => {
1065 self.client
1066 .log_message(
1067 MessageType::WARNING,
1068 format!("Forge build diagnostics failed: {e}"),
1069 )
1070 .await;
1071 }
1072 }
1073
1074 for diag in &mut all_diagnostics {
1078 if diag.message.is_empty() {
1079 diag.message = "Unknown issue".to_string();
1080 }
1081 }
1082
1083 self.client
1085 .publish_diagnostics(uri, all_diagnostics, None)
1086 .await;
1087
1088 {
1091 let mut prev_uris = self.cross_file_diag_uris.write().await;
1092 let mut new_uris = HashSet::new();
1093
1094 for (abs_path, diags) in &cross_file_diags {
1095 if let Ok(file_uri) = Url::from_file_path(abs_path) {
1096 self.client
1097 .publish_diagnostics(file_uri.clone(), diags.clone(), None)
1098 .await;
1099 new_uris.insert(file_uri);
1100 }
1101 }
1102
1103 for stale_uri in prev_uris.difference(&new_uris) {
1106 self.client
1107 .publish_diagnostics(stale_uri.clone(), vec![], None)
1108 .await;
1109 }
1110
1111 *prev_uris = new_uris;
1112 }
1113
1114 if build_succeeded {
1116 let client = self.client.clone();
1117 tokio::spawn(async move {
1118 let _ = client.inlay_hint_refresh().await;
1119 });
1120 }
1121
1122 if build_succeeded
1128 && self.use_solc
1129 && self.settings.read().await.project_index.full_project_scan
1130 && !self
1131 .project_indexed
1132 .load(std::sync::atomic::Ordering::Relaxed)
1133 {
1134 let cache_mode = self.settings.read().await.project_index.cache_mode.clone();
1135 self.project_indexed
1136 .store(true, std::sync::atomic::Ordering::Relaxed);
1137 let foundry_config = self.foundry_config.read().await.clone();
1138 let cache_key = self.project_cache_key().await;
1139 let ast_cache = self.ast_cache.clone();
1140 let client = self.client.clone();
1141 let path_interner = self.path_interner.clone();
1142
1143 tokio::spawn(async move {
1144 let Some(cache_key) = cache_key else {
1145 return;
1146 };
1147 if !foundry_config.root.is_dir() {
1148 client
1149 .log_message(
1150 MessageType::INFO,
1151 format!(
1152 "project index: {} not found, skipping",
1153 foundry_config.root.display(),
1154 ),
1155 )
1156 .await;
1157 return;
1158 }
1159
1160 let token = NumberOrString::String("solidity/projectIndex".to_string());
1162 let _ = client
1163 .send_request::<request::WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
1164 token: token.clone(),
1165 })
1166 .await;
1167
1168 client
1170 .send_notification::<notification::Progress>(ProgressParams {
1171 token: token.clone(),
1172 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(
1173 WorkDoneProgressBegin {
1174 title: "Indexing project".to_string(),
1175 message: Some("Discovering source files...".to_string()),
1176 cancellable: Some(false),
1177 percentage: None,
1178 },
1179 )),
1180 })
1181 .await;
1182
1183 let cfg_for_load = foundry_config.clone();
1185 let cache_mode_for_load = cache_mode.clone();
1186 let load_res = tokio::task::spawn_blocking(move || {
1187 crate::project_cache::load_reference_cache_with_report(
1188 &cfg_for_load,
1189 cache_mode_for_load,
1190 true,
1191 )
1192 })
1193 .await;
1194 match load_res {
1195 Ok(report) => {
1196 if let Some(cached_build) = report.build {
1197 let source_count = cached_build.nodes.len();
1198 ast_cache
1199 .write()
1200 .await
1201 .insert(cache_key.clone().into(), Arc::new(cached_build));
1202 client
1203 .log_message(
1204 MessageType::INFO,
1205 format!(
1206 "project index: cache load hit (sources={}, reused_files={}/{}, complete={}, duration={}ms)",
1207 source_count,
1208 report.file_count_reused,
1209 report.file_count_hashed,
1210 report.complete,
1211 report.duration_ms
1212 ),
1213 )
1214 .await;
1215 if report.complete {
1216 client
1217 .send_notification::<notification::Progress>(ProgressParams {
1218 token: token.clone(),
1219 value: ProgressParamsValue::WorkDone(
1220 WorkDoneProgress::End(WorkDoneProgressEnd {
1221 message: Some(format!(
1222 "Loaded {} source files from cache",
1223 source_count
1224 )),
1225 }),
1226 ),
1227 })
1228 .await;
1229 return;
1230 }
1231 }
1232
1233 client
1234 .log_message(
1235 MessageType::INFO,
1236 format!(
1237 "project index: cache load miss/partial (reason={}, reused_files={}/{}, duration={}ms)",
1238 report
1239 .miss_reason
1240 .unwrap_or_else(|| "unknown".to_string()),
1241 report.file_count_reused,
1242 report.file_count_hashed,
1243 report.duration_ms
1244 ),
1245 )
1246 .await;
1247 }
1248 Err(e) => {
1249 client
1250 .log_message(
1251 MessageType::WARNING,
1252 format!("project index: cache load task failed: {e}"),
1253 )
1254 .await;
1255 }
1256 }
1257
1258 match crate::solc::solc_project_index(&foundry_config, Some(&client), None).await {
1259 Ok(ast_data) => {
1260 let mut new_build = crate::goto::CachedBuild::new(
1261 ast_data,
1262 0,
1263 Some(&mut *path_interner.write().await),
1264 );
1265 if let Some(prev) = ast_cache.read().await.get(&cache_key) {
1268 new_build.merge_missing_from(prev);
1269 }
1270 let source_count = new_build.nodes.len();
1271 let cached_build = Arc::new(new_build);
1272 let build_for_save = (*cached_build).clone();
1273 ast_cache
1274 .write()
1275 .await
1276 .insert(cache_key.clone().into(), cached_build);
1277 client
1278 .log_message(
1279 MessageType::INFO,
1280 format!("project index: cached {} source files", source_count),
1281 )
1282 .await;
1283
1284 let cfg_for_save = foundry_config.clone();
1285 let client_for_save = client.clone();
1286 tokio::spawn(async move {
1287 let res = tokio::task::spawn_blocking(move || {
1288 crate::project_cache::save_reference_cache_with_report(
1289 &cfg_for_save,
1290 &build_for_save,
1291 None,
1292 )
1293 })
1294 .await;
1295 match res {
1296 Ok(Ok(report)) => {
1297 client_for_save
1298 .log_message(
1299 MessageType::INFO,
1300 format!(
1301 "project index: cache save complete (hashed_files={}, duration={}ms)",
1302 report.file_count_hashed, report.duration_ms
1303 ),
1304 )
1305 .await;
1306 }
1307 Ok(Err(e)) => {
1308 client_for_save
1309 .log_message(
1310 MessageType::WARNING,
1311 format!("project index: failed to persist cache: {e}"),
1312 )
1313 .await;
1314 }
1315 Err(e) => {
1316 client_for_save
1317 .log_message(
1318 MessageType::WARNING,
1319 format!("project index: cache save task failed: {e}"),
1320 )
1321 .await;
1322 }
1323 }
1324 });
1325
1326 client
1328 .send_notification::<notification::Progress>(ProgressParams {
1329 token: token.clone(),
1330 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
1331 WorkDoneProgressEnd {
1332 message: Some(format!(
1333 "Indexed {} source files",
1334 source_count
1335 )),
1336 },
1337 )),
1338 })
1339 .await;
1340 }
1341 Err(e) => {
1342 client
1343 .log_message(MessageType::WARNING, format!("project index failed: {e}"))
1344 .await;
1345
1346 client
1348 .send_notification::<notification::Progress>(ProgressParams {
1349 token: token.clone(),
1350 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
1351 WorkDoneProgressEnd {
1352 message: Some("Indexing failed".to_string()),
1353 },
1354 )),
1355 })
1356 .await;
1357 }
1358 }
1359 });
1360 }
1361 }
1362
1363 async fn get_or_fetch_build(
1372 &self,
1373 uri: &Url,
1374 file_path: &std::path::Path,
1375 insert_on_miss: bool,
1376 ) -> Option<Arc<goto::CachedBuild>> {
1377 let uri_str = uri.to_string();
1378
1379 {
1382 let cache = self.ast_cache.read().await;
1383 if let Some(cached) = cache.get(&uri_str) {
1384 return Some(cached.clone());
1385 }
1386 }
1387
1388 if !insert_on_miss {
1392 return None;
1393 }
1394
1395 let path_str = file_path.to_str()?;
1398 let ast_result = if self.use_solc {
1399 let foundry_cfg = self.foundry_config_for_file(file_path).await;
1400 let cached_text = {
1403 let tc = self.text_cache.read().await;
1404 tc.get(&uri_str).map(|(_, c)| c.clone())
1405 };
1406 match crate::solc::solc_ast(
1407 path_str,
1408 &foundry_cfg,
1409 Some(&self.client),
1410 cached_text.as_deref(),
1411 )
1412 .await
1413 {
1414 Ok(data) => Ok(data),
1415 Err(_) => self.compiler.ast(path_str).await,
1416 }
1417 } else {
1418 self.compiler.ast(path_str).await
1419 };
1420 match ast_result {
1421 Ok(data) => {
1422 let build = Arc::new(goto::CachedBuild::new(
1425 data,
1426 0,
1427 Some(&mut *self.path_interner.write().await),
1428 ));
1429 let mut cache = self.ast_cache.write().await;
1430 cache.insert(uri_str.clone().into(), build.clone());
1431 Some(build)
1432 }
1433 Err(e) => {
1434 self.client
1435 .log_message(MessageType::ERROR, format!("failed to get AST: {e}"))
1436 .await;
1437 None
1438 }
1439 }
1440 }
1441
1442 async fn get_source_bytes(&self, uri: &Url, file_path: &std::path::Path) -> Option<Vec<u8>> {
1445 {
1446 let text_cache = self.text_cache.read().await;
1447 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1448 return Some(content.as_bytes().to_vec());
1449 }
1450 }
1451 match std::fs::read(file_path) {
1452 Ok(bytes) => Some(bytes),
1453 Err(e) => {
1454 if e.kind() == std::io::ErrorKind::NotFound {
1455 self.client
1458 .log_message(
1459 MessageType::INFO,
1460 format!("file not found yet (transient): {e}"),
1461 )
1462 .await;
1463 } else {
1464 self.client
1465 .log_message(MessageType::ERROR, format!("failed to read file: {e}"))
1466 .await;
1467 }
1468 None
1469 }
1470 }
1471 }
1472}
1473
1474fn update_imports_on_delete_enabled(settings: &crate::config::Settings) -> bool {
1475 settings.file_operations.update_imports_on_delete
1476}
1477
1478fn start_or_mark_project_cache_sync_pending(
1479 pending: &std::sync::atomic::AtomicBool,
1480 running: &std::sync::atomic::AtomicBool,
1481) -> bool {
1482 pending.store(true, Ordering::Release);
1483 running
1484 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1485 .is_ok()
1486}
1487
1488fn take_project_cache_sync_pending(pending: &std::sync::atomic::AtomicBool) -> bool {
1489 pending.swap(false, Ordering::AcqRel)
1490}
1491
1492fn stop_project_cache_sync_worker_or_reclaim(
1493 pending: &std::sync::atomic::AtomicBool,
1494 running: &std::sync::atomic::AtomicBool,
1495) -> bool {
1496 running.store(false, Ordering::Release);
1497 pending.load(Ordering::Acquire)
1498 && running
1499 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1500 .is_ok()
1501}
1502
1503fn try_claim_project_cache_dirty(dirty: &std::sync::atomic::AtomicBool) -> bool {
1504 dirty
1505 .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire)
1506 .is_ok()
1507}
1508
1509fn start_or_mark_project_cache_upsert_pending(
1510 pending: &std::sync::atomic::AtomicBool,
1511 running: &std::sync::atomic::AtomicBool,
1512) -> bool {
1513 pending.store(true, Ordering::Release);
1514 running
1515 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1516 .is_ok()
1517}
1518
1519fn take_project_cache_upsert_pending(pending: &std::sync::atomic::AtomicBool) -> bool {
1520 pending.swap(false, Ordering::AcqRel)
1521}
1522
1523fn stop_project_cache_upsert_worker_or_reclaim(
1524 pending: &std::sync::atomic::AtomicBool,
1525 running: &std::sync::atomic::AtomicBool,
1526) -> bool {
1527 running.store(false, Ordering::Release);
1528 pending.load(Ordering::Acquire)
1529 && running
1530 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
1531 .is_ok()
1532}
1533
1534fn lexical_normalize(path: &Path) -> PathBuf {
1535 let mut out = PathBuf::new();
1536 for comp in path.components() {
1537 match comp {
1538 Component::CurDir => {}
1539 Component::ParentDir => {
1540 out.pop();
1541 }
1542 Component::RootDir => out.push(comp.as_os_str()),
1543 Component::Prefix(_) => out.push(comp.as_os_str()),
1544 Component::Normal(seg) => out.push(seg),
1545 }
1546 }
1547 out
1548}
1549
1550fn resolve_import_spec_to_abs(
1551 project_root: &Path,
1552 importer_abs: &Path,
1553 import_path: &str,
1554 remappings: &[String],
1555) -> Option<PathBuf> {
1556 if import_path.starts_with("./") || import_path.starts_with("../") {
1557 let base = importer_abs.parent()?;
1558 return Some(lexical_normalize(&base.join(import_path)));
1559 }
1560
1561 for remap in remappings {
1562 let mut it = remap.splitn(2, '=');
1563 let prefix = it.next().unwrap_or_default();
1564 let target = it.next().unwrap_or_default();
1565 if prefix.is_empty() || target.is_empty() {
1566 continue;
1567 }
1568 if import_path.starts_with(prefix) {
1569 let suffix = import_path.strip_prefix(prefix).unwrap_or_default();
1570 return Some(lexical_normalize(
1571 &project_root.join(format!("{target}{suffix}")),
1572 ));
1573 }
1574 }
1575
1576 Some(lexical_normalize(&project_root.join(import_path)))
1577}
1578
1579fn compute_reverse_import_closure(
1580 config: &FoundryConfig,
1581 changed_abs: &[PathBuf],
1582 remappings: &[String],
1583) -> HashSet<PathBuf> {
1584 let source_files = crate::solc::discover_source_files(config);
1585 let mut reverse_edges: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
1586
1587 for importer in &source_files {
1588 let Ok(bytes) = std::fs::read(importer) else {
1589 continue;
1590 };
1591 for imp in links::ts_find_imports(&bytes) {
1592 let Some(imported_abs) =
1593 resolve_import_spec_to_abs(&config.root, importer, &imp.path, remappings)
1594 else {
1595 continue;
1596 };
1597 if !imported_abs.starts_with(&config.root) {
1598 continue;
1599 }
1600 reverse_edges
1601 .entry(imported_abs)
1602 .or_default()
1603 .insert(importer.clone());
1604 }
1605 }
1606
1607 let mut affected: HashSet<PathBuf> = HashSet::new();
1608 let mut queue: std::collections::VecDeque<PathBuf> = std::collections::VecDeque::new();
1609
1610 for path in changed_abs {
1611 if !path.starts_with(&config.root) {
1612 continue;
1613 }
1614 let normalized = lexical_normalize(path);
1615 if affected.insert(normalized.clone()) {
1616 queue.push_back(normalized);
1617 }
1618 }
1619
1620 while let Some(current) = queue.pop_front() {
1621 if let Some(importers) = reverse_edges.get(¤t) {
1622 for importer in importers {
1623 if affected.insert(importer.clone()) {
1624 queue.push_back(importer.clone());
1625 }
1626 }
1627 }
1628 }
1629
1630 let source_set: HashSet<PathBuf> = source_files.into_iter().collect();
1632 affected
1633 .into_iter()
1634 .filter(|p| source_set.contains(p) && p.is_file())
1635 .collect()
1636}
1637
1638fn src_file_id(src: &str) -> Option<&str> {
1639 src.rsplit(':').next().filter(|id| !id.is_empty())
1640}
1641
1642fn doc_key_path(key: &hover::DocKey) -> Option<&str> {
1643 match key {
1644 hover::DocKey::Contract(k) | hover::DocKey::StateVar(k) | hover::DocKey::Method(k) => {
1645 k.split_once(':').map(|(path, _)| path)
1646 }
1647 hover::DocKey::Func(_) | hover::DocKey::Event(_) => None,
1648 }
1649}
1650
1651fn merge_scoped_cached_build(
1652 existing: &mut goto::CachedBuild,
1653 scoped: goto::CachedBuild,
1654) -> Result<usize, String> {
1655 let affected_paths: HashSet<String> = scoped.nodes.keys().map(|p| p.to_string()).collect();
1656 if affected_paths.is_empty() {
1657 return Ok(0);
1658 }
1659 let affected_abs_paths: HashSet<crate::types::AbsPath> =
1660 scoped.path_to_abs.values().cloned().collect();
1661
1662 for scoped_id in scoped.decl_index.keys() {
1665 if existing.decl_index.contains_key(scoped_id)
1666 && let Some(path) = existing.node_id_to_source_path.get(scoped_id)
1667 && !affected_abs_paths.contains(path)
1668 {
1669 return Err(format!(
1670 "decl id collision for id={} in unaffected path {}",
1671 scoped_id, path
1672 ));
1673 }
1674 }
1675
1676 let old_id_to_path = existing.id_to_path_map.clone();
1681 existing.external_refs.retain(|src, _| {
1682 src_file_id(src.as_str())
1683 .and_then(|fid| old_id_to_path.get(fid))
1684 .map(|path| !affected_paths.contains(path))
1685 .unwrap_or(true)
1686 });
1687 existing
1688 .nodes
1689 .retain(|path, _| !affected_paths.contains(path.as_str()));
1690 existing
1691 .path_to_abs
1692 .retain(|path, _| !affected_paths.contains(path.as_str()));
1693 existing
1694 .id_to_path_map
1695 .retain(|_, path| !affected_paths.contains(path));
1696
1697 existing
1698 .node_id_to_source_path
1699 .retain(|_, path| !affected_abs_paths.contains(path));
1700 existing
1701 .decl_index
1702 .retain(|id, _| match existing.node_id_to_source_path.get(id) {
1703 Some(path) => !affected_abs_paths.contains(path),
1704 None => true,
1705 });
1706 existing
1707 .hint_index
1708 .retain(|abs_path, _| !affected_abs_paths.contains(abs_path));
1709 existing.doc_index.retain(|k, _| {
1710 doc_key_path(k)
1711 .map(|p| !affected_paths.contains(p))
1712 .unwrap_or(true)
1713 });
1714 existing.nodes.extend(scoped.nodes);
1715 existing.path_to_abs.extend(scoped.path_to_abs);
1716 existing.external_refs.extend(scoped.external_refs);
1717 existing.id_to_path_map.extend(scoped.id_to_path_map);
1718 existing.decl_index.extend(scoped.decl_index);
1719 existing
1720 .node_id_to_source_path
1721 .extend(scoped.node_id_to_source_path);
1722 existing.hint_index.extend(scoped.hint_index);
1723 existing.doc_index.extend(scoped.doc_index);
1724
1725 Ok(affected_paths.len())
1726}
1727
1728async fn run_did_save(this: ForgeLsp, params: DidSaveTextDocumentParams) {
1734 this.client
1735 .log_message(MessageType::INFO, "file saved")
1736 .await;
1737
1738 let mut text_content = if let Some(text) = params.text {
1739 text
1740 } else {
1741 let cached = {
1743 let text_cache = this.text_cache.read().await;
1744 text_cache
1745 .get(params.text_document.uri.as_str())
1746 .map(|(_, content)| content.clone())
1747 };
1748 if let Some(content) = cached {
1749 content
1750 } else {
1751 match std::fs::read_to_string(params.text_document.uri.path()) {
1752 Ok(content) => content,
1753 Err(e) => {
1754 this.client
1755 .log_message(
1756 MessageType::ERROR,
1757 format!("Failed to read file on save: {e}"),
1758 )
1759 .await;
1760 return;
1761 }
1762 }
1763 }
1764 };
1765
1766 let uri_str = params.text_document.uri.to_string();
1770 let template_on_create = this
1771 .settings
1772 .read()
1773 .await
1774 .file_operations
1775 .template_on_create;
1776 let needs_recover_scaffold = {
1777 let pending = this.pending_create_scaffold.read().await;
1778 template_on_create
1779 && pending.contains(&uri_str)
1780 && !text_content.chars().any(|ch| !ch.is_whitespace())
1781 };
1782 if needs_recover_scaffold {
1783 let solc_version = this.foundry_config.read().await.solc_version.clone();
1784 if let Some(scaffold) =
1785 file_operations::generate_scaffold(¶ms.text_document.uri, solc_version.as_deref())
1786 {
1787 let end = utils::byte_offset_to_position(&text_content, text_content.len());
1788 let edit = WorkspaceEdit {
1789 changes: Some(HashMap::from([(
1790 params.text_document.uri.clone(),
1791 vec![TextEdit {
1792 range: Range {
1793 start: Position::default(),
1794 end,
1795 },
1796 new_text: scaffold.clone(),
1797 }],
1798 )])),
1799 document_changes: None,
1800 change_annotations: None,
1801 };
1802 if this
1803 .client
1804 .apply_edit(edit)
1805 .await
1806 .as_ref()
1807 .is_ok_and(|r| r.applied)
1808 {
1809 text_content = scaffold.clone();
1810 let version = this
1811 .text_cache
1812 .read()
1813 .await
1814 .get(params.text_document.uri.as_str())
1815 .map(|(v, _)| *v)
1816 .unwrap_or_default();
1817 this.text_cache
1818 .write()
1819 .await
1820 .insert(uri_str.clone().into(), (version, scaffold));
1821 this.pending_create_scaffold.write().await.remove(&uri_str);
1822 this.client
1823 .log_message(
1824 MessageType::INFO,
1825 format!("didSave: recovered scaffold for {}", uri_str),
1826 )
1827 .await;
1828 }
1829 }
1830 }
1831
1832 let version = this
1833 .text_cache
1834 .read()
1835 .await
1836 .get(params.text_document.uri.as_str())
1837 .map(|(version, _)| *version)
1838 .unwrap_or_default();
1839
1840 let saved_uri = params.text_document.uri.clone();
1841 if let Ok(saved_file_path) = saved_uri.to_file_path() {
1842 let saved_abs = saved_file_path.to_string_lossy().to_string();
1843 this.project_cache_changed_files
1844 .write()
1845 .await
1846 .insert(saved_abs.clone());
1847 this.project_cache_upsert_files
1848 .write()
1849 .await
1850 .insert(saved_abs);
1851 }
1852 this.on_change(TextDocumentItem {
1853 uri: saved_uri.clone(),
1854 text: text_content,
1855 version,
1856 language_id: "".to_string(),
1857 })
1858 .await;
1859
1860 let settings_snapshot = this.settings.read().await.clone();
1861
1862 if this.use_solc
1868 && settings_snapshot.project_index.full_project_scan
1869 && matches!(
1870 settings_snapshot.project_index.cache_mode,
1871 crate::config::ProjectIndexCacheMode::V2 | crate::config::ProjectIndexCacheMode::Auto
1872 )
1873 && start_or_mark_project_cache_upsert_pending(
1874 &this.project_cache_upsert_pending,
1875 &this.project_cache_upsert_running,
1876 )
1877 {
1878 let upsert_files = this.project_cache_upsert_files.clone();
1879 let ast_cache = this.ast_cache.clone();
1880 let client = this.client.clone();
1881 let running_flag = this.project_cache_upsert_running.clone();
1882 let pending_flag = this.project_cache_upsert_pending.clone();
1883 let foundry_config = this.foundry_config.read().await.clone();
1884 let root_key = this.project_cache_key().await;
1885
1886 tokio::spawn(async move {
1887 loop {
1888 tokio::time::sleep(std::time::Duration::from_millis(350)).await;
1889
1890 if !take_project_cache_upsert_pending(&pending_flag) {
1891 if stop_project_cache_upsert_worker_or_reclaim(&pending_flag, &running_flag) {
1892 continue;
1893 }
1894 break;
1895 }
1896
1897 let changed_paths: Vec<String> = {
1898 let mut paths = upsert_files.write().await;
1899 paths.drain().collect()
1900 };
1901 if changed_paths.is_empty() {
1902 continue;
1903 }
1904
1905 let Some(ref rk) = root_key else {
1910 continue;
1911 };
1912 let Some(root_build) = ast_cache.read().await.get(rk).cloned() else {
1913 continue;
1914 };
1915
1916 let cfg = foundry_config.clone();
1917 let build = (*root_build).clone();
1918 let changed = changed_paths.clone();
1919
1920 let res = tokio::task::spawn_blocking(move || {
1921 crate::project_cache::upsert_reference_cache_v2_with_report(
1922 &cfg, &build, &changed,
1923 )
1924 })
1925 .await;
1926
1927 match res {
1928 Ok(Ok(report)) => {
1929 client
1930 .log_message(
1931 MessageType::INFO,
1932 format!(
1933 "project cache v2 upsert (debounced): touched_files={}, duration={}ms",
1934 report.file_count_hashed, report.duration_ms
1935 ),
1936 )
1937 .await;
1938 }
1939 Ok(Err(e)) => {
1940 client
1941 .log_message(
1942 MessageType::WARNING,
1943 format!("project cache v2 upsert: {e}"),
1944 )
1945 .await;
1946 }
1947 Err(e) => {
1948 client
1949 .log_message(
1950 MessageType::WARNING,
1951 format!("project cache v2 upsert task failed: {e}"),
1952 )
1953 .await;
1954 }
1955 }
1956 }
1957 });
1958 }
1959
1960 if this.use_solc
1963 && settings_snapshot.project_index.full_project_scan
1964 && this.project_cache_dirty.load(Ordering::Acquire)
1965 && start_or_mark_project_cache_sync_pending(
1966 &this.project_cache_sync_pending,
1967 &this.project_cache_sync_running,
1968 )
1969 {
1970 let foundry_config = this.foundry_config.read().await.clone();
1971 let root_key = this.project_cache_key().await;
1972 let ast_cache = this.ast_cache.clone();
1973 let text_cache = this.text_cache.clone();
1974 let client = this.client.clone();
1975 let dirty_flag = this.project_cache_dirty.clone();
1976 let running_flag = this.project_cache_sync_running.clone();
1977 let pending_flag = this.project_cache_sync_pending.clone();
1978 let changed_files = this.project_cache_changed_files.clone();
1979 let aggressive_scoped = settings_snapshot.project_index.incremental_edit_reindex;
1980 let force_full_rebuild_flag = this.project_cache_force_full_rebuild.clone();
1981 let path_interner = this.path_interner.clone();
1982
1983 tokio::spawn(async move {
1984 loop {
1985 tokio::time::sleep(std::time::Duration::from_millis(700)).await;
1987
1988 if !take_project_cache_sync_pending(&pending_flag) {
1989 if stop_project_cache_sync_worker_or_reclaim(&pending_flag, &running_flag) {
1990 continue;
1991 }
1992 break;
1993 }
1994
1995 if !try_claim_project_cache_dirty(&dirty_flag) {
1996 continue;
1997 }
1998
1999 let Some(cache_key) = &root_key else {
2000 dirty_flag.store(true, Ordering::Release);
2001 continue;
2002 };
2003 if !foundry_config.root.is_dir() {
2004 dirty_flag.store(true, Ordering::Release);
2005 client
2006 .log_message(
2007 MessageType::WARNING,
2008 format!(
2009 "didSave cache sync: invalid project root {}, deferring",
2010 foundry_config.root.display()
2011 ),
2012 )
2013 .await;
2014 continue;
2015 }
2016
2017 let mut scoped_ok = false;
2018
2019 let force_full = force_full_rebuild_flag.swap(false, Ordering::AcqRel);
2023
2024 if aggressive_scoped && !force_full {
2025 let changed_abs: Vec<PathBuf> = {
2026 let mut changed = changed_files.write().await;
2027
2028 changed.drain().map(PathBuf::from).collect::<Vec<PathBuf>>()
2029 };
2030 if !changed_abs.is_empty() {
2031 let remappings = crate::solc::resolve_remappings(&foundry_config).await;
2032 let cfg_for_plan = foundry_config.clone();
2033 let changed_for_plan = changed_abs.clone();
2034 let remappings_for_plan = remappings.clone();
2035 let plan_res = tokio::task::spawn_blocking(move || {
2036 compute_reverse_import_closure(
2037 &cfg_for_plan,
2038 &changed_for_plan,
2039 &remappings_for_plan,
2040 )
2041 })
2042 .await;
2043
2044 let affected_files = match plan_res {
2045 Ok(set) => set.into_iter().collect::<Vec<PathBuf>>(),
2046 Err(_) => Vec::new(),
2047 };
2048 if !affected_files.is_empty() {
2049 client
2050 .log_message(
2051 MessageType::INFO,
2052 format!(
2053 "didSave cache sync: aggressive scoped reindex (affected={})",
2054 affected_files.len(),
2055 ),
2056 )
2057 .await;
2058
2059 let text_cache_snapshot = text_cache.read().await.clone();
2060 match crate::solc::solc_project_index_scoped(
2061 &foundry_config,
2062 Some(&client),
2063 Some(&text_cache_snapshot),
2064 &affected_files,
2065 )
2066 .await
2067 {
2068 Ok(ast_data) => {
2069 let scoped_build = Arc::new(crate::goto::CachedBuild::new(
2070 ast_data,
2071 0,
2072 Some(&mut *path_interner.write().await),
2073 ));
2074 let source_count = scoped_build.nodes.len();
2075 enum ScopedApply {
2076 Merged { affected_count: usize },
2077 Stored,
2078 Failed(String),
2079 }
2080 let apply_outcome = {
2081 let mut cache = ast_cache.write().await;
2082 if let Some(existing) = cache.get(cache_key).cloned() {
2083 let mut merged = (*existing).clone();
2084 match merge_scoped_cached_build(
2085 &mut merged,
2086 (*scoped_build).clone(),
2087 ) {
2088 Ok(affected_count) => {
2089 cache.insert(
2090 cache_key.clone().into(),
2091 Arc::new(merged),
2092 );
2093 ScopedApply::Merged { affected_count }
2094 }
2095 Err(e) => ScopedApply::Failed(e),
2096 }
2097 } else {
2098 cache.insert(cache_key.clone().into(), scoped_build);
2099 ScopedApply::Stored
2100 }
2101 };
2102
2103 match apply_outcome {
2104 ScopedApply::Merged { affected_count } => {
2105 client
2106 .log_message(
2107 MessageType::INFO,
2108 format!(
2109 "didSave cache sync: scoped merge applied (scoped_sources={}, affected_paths={})",
2110 source_count, affected_count
2111 ),
2112 )
2113 .await;
2114 scoped_ok = true;
2115 }
2116 ScopedApply::Stored => {
2117 client
2118 .log_message(
2119 MessageType::INFO,
2120 format!(
2121 "didSave cache sync: scoped cache stored (scoped_sources={})",
2122 source_count
2123 ),
2124 )
2125 .await;
2126 scoped_ok = true;
2127 }
2128 ScopedApply::Failed(e) => {
2129 client
2130 .log_message(
2131 MessageType::WARNING,
2132 format!(
2133 "didSave cache sync: scoped merge rejected, will retry scoped on next save: {e}"
2134 ),
2135 )
2136 .await;
2137 dirty_flag.store(true, Ordering::Release);
2138 }
2139 }
2140 }
2141 Err(e) => {
2142 client
2143 .log_message(
2144 MessageType::WARNING,
2145 format!(
2146 "didSave cache sync: scoped reindex failed, will retry scoped on next save: {e}"
2147 ),
2148 )
2149 .await;
2150 dirty_flag.store(true, Ordering::Release);
2151 }
2152 }
2153 } else {
2154 client
2155 .log_message(
2156 MessageType::INFO,
2157 "didSave cache sync: no affected files from scoped planner",
2158 )
2159 .await;
2160 }
2161 }
2162 }
2163
2164 if scoped_ok {
2165 continue;
2166 }
2167 if aggressive_scoped {
2168 continue;
2169 }
2170
2171 client
2172 .log_message(
2173 MessageType::INFO,
2174 "didSave cache sync: rebuilding project index from disk",
2175 )
2176 .await;
2177
2178 match crate::solc::solc_project_index(&foundry_config, Some(&client), None).await {
2179 Ok(ast_data) => {
2180 let mut new_build = crate::goto::CachedBuild::new(
2181 ast_data,
2182 0,
2183 Some(&mut *path_interner.write().await),
2184 );
2185 if let Some(prev) = ast_cache.read().await.get(cache_key) {
2186 new_build.merge_missing_from(prev);
2187 }
2188 let source_count = new_build.nodes.len();
2189 let cached_build = Arc::new(new_build);
2190 let build_for_save = (*cached_build).clone();
2191 ast_cache
2192 .write()
2193 .await
2194 .insert(cache_key.clone().into(), cached_build);
2195
2196 let cfg_for_save = foundry_config.clone();
2197 let save_res = tokio::task::spawn_blocking(move || {
2198 crate::project_cache::save_reference_cache_with_report(
2199 &cfg_for_save,
2200 &build_for_save,
2201 None,
2202 )
2203 })
2204 .await;
2205
2206 match save_res {
2207 Ok(Ok(report)) => {
2208 changed_files.write().await.clear();
2209 client
2210 .log_message(
2211 MessageType::INFO,
2212 format!(
2213 "didSave cache sync: persisted cache (sources={}, hashed_files={}, duration={}ms)",
2214 source_count, report.file_count_hashed, report.duration_ms
2215 ),
2216 )
2217 .await;
2218 }
2219 Ok(Err(e)) => {
2220 dirty_flag.store(true, Ordering::Release);
2221 client
2222 .log_message(
2223 MessageType::WARNING,
2224 format!(
2225 "didSave cache sync: persist failed, will retry: {e}"
2226 ),
2227 )
2228 .await;
2229 }
2230 Err(e) => {
2231 dirty_flag.store(true, Ordering::Release);
2232 client
2233 .log_message(
2234 MessageType::WARNING,
2235 format!(
2236 "didSave cache sync: save task failed, will retry: {e}"
2237 ),
2238 )
2239 .await;
2240 }
2241 }
2242 }
2243 Err(e) => {
2244 dirty_flag.store(true, Ordering::Release);
2245 client
2246 .log_message(
2247 MessageType::WARNING,
2248 format!("didSave cache sync: re-index failed, will retry: {e}"),
2249 )
2250 .await;
2251 }
2252 }
2253 }
2254 });
2255 }
2256}
2257
2258#[tower_lsp::async_trait]
2259impl LanguageServer for ForgeLsp {
2260 async fn initialize(
2261 &self,
2262 params: InitializeParams,
2263 ) -> tower_lsp::jsonrpc::Result<InitializeResult> {
2264 {
2266 let mut caps = self.client_capabilities.write().await;
2267 *caps = Some(params.capabilities.clone());
2268 }
2269
2270 if let Some(init_opts) = ¶ms.initialization_options {
2272 let s = config::parse_settings(init_opts);
2273 self.client
2274 .log_message(
2275 MessageType::INFO,
2276 format!(
2277 "settings: inlayHints.parameters={}, lint.enabled={}, lint.severity={:?}, lint.only={:?}, lint.exclude={:?}, fileOperations.templateOnCreate={}, fileOperations.updateImportsOnRename={}, fileOperations.updateImportsOnDelete={}, projectIndex.fullProjectScan={}, projectIndex.cacheMode={:?}, projectIndex.incrementalEditReindex={}",
2278 s.inlay_hints.parameters, 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, s.project_index.full_project_scan, s.project_index.cache_mode, s.project_index.incremental_edit_reindex,
2279 ),
2280 )
2281 .await;
2282 let mut settings = self.settings.write().await;
2283 *settings = s;
2284 self.settings_from_init
2285 .store(true, std::sync::atomic::Ordering::Relaxed);
2286 }
2287
2288 if let Some(uri) = params.root_uri.as_ref() {
2290 let mut root = self.root_uri.write().await;
2291 *root = Some(uri.clone());
2292 }
2293
2294 if let Some(root_uri) = params
2296 .root_uri
2297 .as_ref()
2298 .and_then(|uri| uri.to_file_path().ok())
2299 {
2300 let lint_cfg = config::load_lint_config(&root_uri);
2301 self.client
2302 .log_message(
2303 MessageType::INFO,
2304 format!(
2305 "loaded foundry.toml lint config: lint_on_build={}, ignore_patterns={}",
2306 lint_cfg.lint_on_build,
2307 lint_cfg.ignore_patterns.len()
2308 ),
2309 )
2310 .await;
2311 let mut config = self.lint_config.write().await;
2312 *config = lint_cfg;
2313
2314 let foundry_cfg = config::load_foundry_config(&root_uri);
2315 self.client
2316 .log_message(
2317 MessageType::INFO,
2318 format!(
2319 "loaded foundry.toml: solc={}, remappings={}",
2320 foundry_cfg.solc_version.as_deref().unwrap_or("auto"),
2321 foundry_cfg.remappings.len()
2322 ),
2323 )
2324 .await;
2325 let mut fc = self.foundry_config.write().await;
2326 *fc = foundry_cfg;
2327 }
2328
2329 let client_encodings = params
2331 .capabilities
2332 .general
2333 .as_ref()
2334 .and_then(|g| g.position_encodings.as_deref());
2335 let encoding = utils::PositionEncoding::negotiate(client_encodings);
2336 utils::set_encoding(encoding);
2337
2338 Ok(InitializeResult {
2339 server_info: Some(ServerInfo {
2340 name: "Solidity Language Server".to_string(),
2341 version: Some(env!("LONG_VERSION").to_string()),
2342 }),
2343 capabilities: ServerCapabilities {
2344 position_encoding: Some(encoding.into()),
2345 completion_provider: Some(CompletionOptions {
2346 trigger_characters: Some(vec![
2347 ".".to_string(),
2348 "\"".to_string(),
2349 "'".to_string(),
2350 "/".to_string(),
2351 ]),
2352 resolve_provider: Some(false),
2353 ..Default::default()
2354 }),
2355 signature_help_provider: Some(SignatureHelpOptions {
2356 trigger_characters: Some(vec![
2357 "(".to_string(),
2358 ",".to_string(),
2359 "[".to_string(),
2360 ]),
2361 retrigger_characters: None,
2362 work_done_progress_options: WorkDoneProgressOptions {
2363 work_done_progress: None,
2364 },
2365 }),
2366 definition_provider: Some(OneOf::Left(true)),
2367 declaration_provider: Some(DeclarationCapability::Simple(true)),
2368 implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
2369 references_provider: Some(OneOf::Left(true)),
2370 rename_provider: Some(OneOf::Right(RenameOptions {
2371 prepare_provider: Some(true),
2372 work_done_progress_options: WorkDoneProgressOptions {
2373 work_done_progress: Some(true),
2374 },
2375 })),
2376 workspace_symbol_provider: Some(OneOf::Left(true)),
2377 document_symbol_provider: Some(OneOf::Left(true)),
2378 document_highlight_provider: Some(OneOf::Left(true)),
2379 hover_provider: Some(HoverProviderCapability::Simple(true)),
2380 document_link_provider: Some(DocumentLinkOptions {
2381 resolve_provider: Some(false),
2382 work_done_progress_options: WorkDoneProgressOptions {
2383 work_done_progress: None,
2384 },
2385 }),
2386 document_formatting_provider: Some(OneOf::Left(true)),
2387 code_action_provider: Some(CodeActionProviderCapability::Options(
2388 CodeActionOptions {
2389 code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
2390 resolve_provider: Some(false),
2391 work_done_progress_options: WorkDoneProgressOptions {
2392 work_done_progress: None,
2393 },
2394 },
2395 )),
2396 call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)),
2397 code_lens_provider: None,
2398 folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
2399 selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
2400 inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options(
2401 InlayHintOptions {
2402 resolve_provider: Some(false),
2403 work_done_progress_options: WorkDoneProgressOptions {
2404 work_done_progress: None,
2405 },
2406 },
2407 ))),
2408 semantic_tokens_provider: Some(
2409 SemanticTokensServerCapabilities::SemanticTokensOptions(
2410 SemanticTokensOptions {
2411 legend: semantic_tokens::legend(),
2412 full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
2413 range: Some(true),
2414 work_done_progress_options: WorkDoneProgressOptions {
2415 work_done_progress: None,
2416 },
2417 },
2418 ),
2419 ),
2420 text_document_sync: Some(TextDocumentSyncCapability::Options(
2421 TextDocumentSyncOptions {
2422 will_save: Some(true),
2423 will_save_wait_until: None,
2424 open_close: Some(true),
2425 save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
2426 include_text: Some(true),
2427 })),
2428 change: Some(TextDocumentSyncKind::FULL),
2429 },
2430 )),
2431 workspace: Some(WorkspaceServerCapabilities {
2432 workspace_folders: None,
2433 file_operations: Some(WorkspaceFileOperationsServerCapabilities {
2434 will_rename: Some(FileOperationRegistrationOptions {
2435 filters: vec![
2436 FileOperationFilter {
2438 scheme: Some("file".to_string()),
2439 pattern: FileOperationPattern {
2440 glob: "**/*.sol".to_string(),
2441 matches: Some(FileOperationPatternKind::File),
2442 options: None,
2443 },
2444 },
2445 FileOperationFilter {
2447 scheme: Some("file".to_string()),
2448 pattern: FileOperationPattern {
2449 glob: "**".to_string(),
2450 matches: Some(FileOperationPatternKind::Folder),
2451 options: None,
2452 },
2453 },
2454 ],
2455 }),
2456 did_rename: Some(FileOperationRegistrationOptions {
2457 filters: vec![
2458 FileOperationFilter {
2459 scheme: Some("file".to_string()),
2460 pattern: FileOperationPattern {
2461 glob: "**/*.sol".to_string(),
2462 matches: Some(FileOperationPatternKind::File),
2463 options: None,
2464 },
2465 },
2466 FileOperationFilter {
2467 scheme: Some("file".to_string()),
2468 pattern: FileOperationPattern {
2469 glob: "**".to_string(),
2470 matches: Some(FileOperationPatternKind::Folder),
2471 options: None,
2472 },
2473 },
2474 ],
2475 }),
2476 will_delete: Some(FileOperationRegistrationOptions {
2477 filters: vec![
2478 FileOperationFilter {
2479 scheme: Some("file".to_string()),
2480 pattern: FileOperationPattern {
2481 glob: "**/*.sol".to_string(),
2482 matches: Some(FileOperationPatternKind::File),
2483 options: None,
2484 },
2485 },
2486 FileOperationFilter {
2487 scheme: Some("file".to_string()),
2488 pattern: FileOperationPattern {
2489 glob: "**".to_string(),
2490 matches: Some(FileOperationPatternKind::Folder),
2491 options: None,
2492 },
2493 },
2494 ],
2495 }),
2496 did_delete: Some(FileOperationRegistrationOptions {
2497 filters: vec![
2498 FileOperationFilter {
2499 scheme: Some("file".to_string()),
2500 pattern: FileOperationPattern {
2501 glob: "**/*.sol".to_string(),
2502 matches: Some(FileOperationPatternKind::File),
2503 options: None,
2504 },
2505 },
2506 FileOperationFilter {
2507 scheme: Some("file".to_string()),
2508 pattern: FileOperationPattern {
2509 glob: "**".to_string(),
2510 matches: Some(FileOperationPatternKind::Folder),
2511 options: None,
2512 },
2513 },
2514 ],
2515 }),
2516 will_create: Some(FileOperationRegistrationOptions {
2517 filters: vec![FileOperationFilter {
2518 scheme: Some("file".to_string()),
2519 pattern: FileOperationPattern {
2520 glob: "**/*.sol".to_string(),
2521 matches: Some(FileOperationPatternKind::File),
2522 options: None,
2523 },
2524 }],
2525 }),
2526 did_create: Some(FileOperationRegistrationOptions {
2527 filters: vec![FileOperationFilter {
2528 scheme: Some("file".to_string()),
2529 pattern: FileOperationPattern {
2530 glob: "**/*.sol".to_string(),
2531 matches: Some(FileOperationPatternKind::File),
2532 options: None,
2533 },
2534 }],
2535 }),
2536 }),
2537 }),
2538 execute_command_provider: Some(ExecuteCommandOptions {
2539 commands: vec![
2540 "solidity.clearCache".to_string(),
2541 "solidity.reindex".to_string(),
2542 ],
2543 work_done_progress_options: WorkDoneProgressOptions {
2544 work_done_progress: None,
2545 },
2546 }),
2547 ..ServerCapabilities::default()
2548 },
2549 })
2550 }
2551
2552 async fn initialized(&self, _: InitializedParams) {
2553 self.client
2554 .log_message(MessageType::INFO, "lsp server initialized.")
2555 .await;
2556
2557 let supports_dynamic = self
2559 .client_capabilities
2560 .read()
2561 .await
2562 .as_ref()
2563 .and_then(|caps| caps.workspace.as_ref())
2564 .and_then(|ws| ws.did_change_watched_files.as_ref())
2565 .and_then(|dcwf| dcwf.dynamic_registration)
2566 .unwrap_or(false);
2567
2568 if supports_dynamic {
2569 let registration = Registration {
2570 id: "foundry-toml-watcher".to_string(),
2571 method: "workspace/didChangeWatchedFiles".to_string(),
2572 register_options: Some(
2573 serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
2574 watchers: vec![
2575 FileSystemWatcher {
2576 glob_pattern: GlobPattern::String("**/foundry.toml".to_string()),
2577 kind: Some(WatchKind::all()),
2578 },
2579 FileSystemWatcher {
2580 glob_pattern: GlobPattern::String("**/remappings.txt".to_string()),
2581 kind: Some(WatchKind::all()),
2582 },
2583 ],
2584 })
2585 .unwrap(),
2586 ),
2587 };
2588
2589 if let Err(e) = self.client.register_capability(vec![registration]).await {
2590 self.client
2591 .log_message(
2592 MessageType::WARNING,
2593 format!("failed to register foundry.toml watcher: {e}"),
2594 )
2595 .await;
2596 } else {
2597 self.client
2598 .log_message(MessageType::INFO, "registered foundry.toml file watcher")
2599 .await;
2600 }
2601 }
2602
2603 if !self
2608 .settings_from_init
2609 .load(std::sync::atomic::Ordering::Relaxed)
2610 {
2611 let supports_config = self
2612 .client_capabilities
2613 .read()
2614 .await
2615 .as_ref()
2616 .and_then(|caps| caps.workspace.as_ref())
2617 .and_then(|ws| ws.configuration)
2618 .unwrap_or(false);
2619
2620 if supports_config {
2621 match self
2622 .client
2623 .configuration(vec![ConfigurationItem {
2624 scope_uri: None,
2625 section: Some("solidity-language-server".to_string()),
2626 }])
2627 .await
2628 {
2629 Ok(values) => {
2630 if let Some(val) = values.into_iter().next()
2631 && !val.is_null()
2632 {
2633 let s = config::parse_settings(&val);
2634 self.client
2635 .log_message(
2636 MessageType::INFO,
2637 format!(
2638 "settings (workspace/configuration): lint.enabled={}, lint.exclude={:?}, projectIndex.fullProjectScan={}, projectIndex.cacheMode={:?}",
2639 s.lint.enabled, s.lint.exclude, s.project_index.full_project_scan, s.project_index.cache_mode,
2640 ),
2641 )
2642 .await;
2643 let mut settings = self.settings.write().await;
2644 *settings = s;
2645 }
2646 }
2647 Err(e) => {
2648 self.client
2649 .log_message(
2650 MessageType::WARNING,
2651 format!("workspace/configuration request failed: {e}"),
2652 )
2653 .await;
2654 }
2655 }
2656 }
2657 }
2658
2659 if self.settings.read().await.check_for_updates {
2661 let client = self.client.clone();
2662 tokio::spawn(check_for_updates(client));
2663 }
2664
2665 if self.use_solc && self.settings.read().await.project_index.full_project_scan {
2669 let cache_mode = self.settings.read().await.project_index.cache_mode.clone();
2670 self.project_indexed
2671 .store(true, std::sync::atomic::Ordering::Relaxed);
2672 let foundry_config = self.foundry_config.read().await.clone();
2673 let cache_key = self.project_cache_key().await;
2674 let ast_cache = self.ast_cache.clone();
2675 let client = self.client.clone();
2676 let sub_caches_arc = self.sub_caches.clone();
2677 let sub_caches_loading_flag = self.sub_caches_loading.clone();
2678 let path_interner = self.path_interner.clone();
2679
2680 tokio::spawn(async move {
2681 let Some(cache_key) = cache_key else {
2682 return;
2683 };
2684 if !foundry_config.root.is_dir() {
2685 client
2686 .log_message(
2687 MessageType::INFO,
2688 format!(
2689 "project index: {} not found, skipping eager index",
2690 foundry_config.root.display(),
2691 ),
2692 )
2693 .await;
2694 return;
2695 }
2696
2697 let token = NumberOrString::String("solidity/projectIndex".to_string());
2698 let _ = client
2699 .send_request::<request::WorkDoneProgressCreate>(WorkDoneProgressCreateParams {
2700 token: token.clone(),
2701 })
2702 .await;
2703
2704 client
2705 .send_notification::<notification::Progress>(ProgressParams {
2706 token: token.clone(),
2707 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(
2708 WorkDoneProgressBegin {
2709 title: "Indexing project".to_string(),
2710 message: Some("Discovering source files...".to_string()),
2711 cancellable: Some(false),
2712 percentage: None,
2713 },
2714 )),
2715 })
2716 .await;
2717
2718 {
2723 let cfg_for_discover = foundry_config.clone();
2724 let all_files = tokio::task::spawn_blocking(move || {
2725 crate::solc::discover_source_files_with_libs(&cfg_for_discover)
2726 })
2727 .await
2728 .unwrap_or_default();
2729 let mut interner = path_interner.write().await;
2730 for file in &all_files {
2731 if let Some(path_str) = file.to_str() {
2732 interner.intern(path_str);
2733 }
2734 }
2735 }
2736
2737 let cfg_for_load = foundry_config.clone();
2739 let cache_mode_for_load = cache_mode.clone();
2740 let load_res = tokio::task::spawn_blocking(move || {
2741 crate::project_cache::load_reference_cache_with_report(
2742 &cfg_for_load,
2743 cache_mode_for_load,
2744 true,
2745 )
2746 })
2747 .await;
2748 match load_res {
2749 Ok(report) => {
2750 if let Some(cached_build) = report.build {
2751 let source_count = cached_build.nodes.len();
2752 ast_cache
2753 .write()
2754 .await
2755 .insert(cache_key.clone().into(), Arc::new(cached_build));
2756 client
2757 .log_message(
2758 MessageType::INFO,
2759 format!(
2760 "loaded {source_count} sources from cache ({}ms)",
2761 report.duration_ms
2762 ),
2763 )
2764 .await;
2765 if report.complete {
2766 spawn_load_lib_sub_caches_task(
2769 foundry_config.clone(),
2770 sub_caches_arc.clone(),
2771 sub_caches_loading_flag.clone(),
2772 path_interner.clone(),
2773 client.clone(),
2774 );
2775 client
2777 .send_notification::<notification::Progress>(ProgressParams {
2778 token: token.clone(),
2779 value: ProgressParamsValue::WorkDone(
2780 WorkDoneProgress::End(WorkDoneProgressEnd {
2781 message: Some(format!(
2782 "Loaded {} source files from cache",
2783 source_count
2784 )),
2785 }),
2786 ),
2787 })
2788 .await;
2789 let token2 =
2802 NumberOrString::String("solidity/projectIndexFull".to_string());
2803 let _ = client
2804 .send_request::<request::WorkDoneProgressCreate>(
2805 WorkDoneProgressCreateParams {
2806 token: token2.clone(),
2807 },
2808 )
2809 .await;
2810 client
2811 .send_notification::<notification::Progress>(ProgressParams {
2812 token: token2.clone(),
2813 value: ProgressParamsValue::WorkDone(
2814 WorkDoneProgress::Begin(WorkDoneProgressBegin {
2815 title: "Full project index".to_string(),
2816 message: Some(format!(
2817 "Restored {} source files from cache",
2818 source_count,
2819 )),
2820 cancellable: Some(false),
2821 percentage: None,
2822 }),
2823 ),
2824 })
2825 .await;
2826 client
2827 .send_notification::<notification::Progress>(ProgressParams {
2828 token: token2,
2829 value: ProgressParamsValue::WorkDone(
2830 WorkDoneProgress::End(WorkDoneProgressEnd {
2831 message: Some(format!(
2832 "Restored {} source files from cache",
2833 source_count,
2834 )),
2835 }),
2836 ),
2837 })
2838 .await;
2839 return;
2840 }
2841 }
2842
2843 client
2844 .log_message(
2845 MessageType::INFO,
2846 "no cached index found, building from source",
2847 )
2848 .await;
2849 }
2850 Err(e) => {
2851 client
2852 .log_message(MessageType::WARNING, format!("cache load failed: {e}"))
2853 .await;
2854 }
2855 }
2856
2857 let remappings = crate::solc::resolve_remappings(&foundry_config).await;
2873 let cfg_for_src = foundry_config.clone();
2874 let remappings_for_src = remappings.clone();
2875 let src_files = tokio::task::spawn_blocking(move || {
2876 crate::solc::discover_src_only_closure(&cfg_for_src, &remappings_for_src)
2877 })
2878 .await
2879 .unwrap_or_default();
2880
2881 let cfg_for_full = foundry_config.clone();
2884 let remappings_for_full = remappings.clone();
2885 let full_files = tokio::task::spawn_blocking(move || {
2886 crate::solc::discover_compilation_closure(&cfg_for_full, &remappings_for_full)
2887 })
2888 .await
2889 .unwrap_or_default();
2890
2891 let src_count = src_files.len();
2892 let full_count = full_files.len();
2893
2894 let phase1_start = std::time::Instant::now();
2896 client
2897 .send_notification::<notification::Progress>(ProgressParams {
2898 token: token.clone(),
2899 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Report(
2900 WorkDoneProgressReport {
2901 message: Some(format!("Compiling {} src files...", src_count,)),
2902 cancellable: Some(false),
2903 percentage: None,
2904 },
2905 )),
2906 })
2907 .await;
2908
2909 let phase1_ok = match crate::solc::solc_project_index_scoped(
2910 &foundry_config,
2911 Some(&client),
2912 None,
2913 &src_files,
2914 )
2915 .await
2916 {
2917 Ok(ast_data) => {
2918 let mut new_build = crate::goto::CachedBuild::new(
2919 ast_data,
2920 0,
2921 Some(&mut *path_interner.write().await),
2922 );
2923 if let Some(prev) = ast_cache.read().await.get(&cache_key) {
2924 new_build.merge_missing_from(prev);
2925 }
2926 let source_count = new_build.nodes.len();
2927 ast_cache
2928 .write()
2929 .await
2930 .insert(cache_key.clone().into(), Arc::new(new_build));
2931 client
2932 .log_message(
2933 MessageType::INFO,
2934 format!(
2935 "project index: phase 1 complete — {} source files indexed in {:.1}s",
2936 source_count,
2937 phase1_start.elapsed().as_secs_f64(),
2938 ),
2939 )
2940 .await;
2941
2942 client
2944 .send_notification::<notification::Progress>(ProgressParams {
2945 token: token.clone(),
2946 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
2947 WorkDoneProgressEnd {
2948 message: Some(format!(
2949 "Indexed {} source files (full index in background)",
2950 source_count,
2951 )),
2952 },
2953 )),
2954 })
2955 .await;
2956 true
2957 }
2958 Err(e) => {
2959 client
2960 .log_message(
2961 MessageType::WARNING,
2962 format!("project index: phase 1 failed: {e}"),
2963 )
2964 .await;
2965 false
2967 }
2968 };
2969
2970 let phase2_foundry_config = foundry_config.clone();
2976 let phase2_client = client.clone();
2977 let phase2_cache_key = cache_key.clone();
2978 let phase2_ast_cache = ast_cache.clone();
2979 let phase2_path_interner = path_interner.clone();
2980 let phase2_sub_caches = sub_caches_arc.clone();
2981 let phase2_loading_flag = sub_caches_loading_flag.clone();
2982 let phase2 = async move {
2983 let phase2_start = std::time::Instant::now();
2984 let token2 = NumberOrString::String("solidity/projectIndexFull".to_string());
2986 let _ = phase2_client
2987 .send_request::<request::WorkDoneProgressCreate>(
2988 WorkDoneProgressCreateParams {
2989 token: token2.clone(),
2990 },
2991 )
2992 .await;
2993 phase2_client
2994 .send_notification::<notification::Progress>(ProgressParams {
2995 token: token2.clone(),
2996 value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(
2997 WorkDoneProgressBegin {
2998 title: "Full project index".to_string(),
2999 message: Some(format!(
3000 "Compiling {} files (src + test + script)...",
3001 full_count,
3002 )),
3003 cancellable: Some(false),
3004 percentage: None,
3005 },
3006 )),
3007 })
3008 .await;
3009
3010 match crate::solc::solc_project_index_scoped(
3011 &phase2_foundry_config,
3012 Some(&phase2_client),
3013 None,
3014 &full_files,
3015 )
3016 .await
3017 {
3018 Ok(ast_data) => {
3019 let mut new_build = crate::goto::CachedBuild::new(
3020 ast_data,
3021 0,
3022 Some(&mut *phase2_path_interner.write().await),
3023 );
3024 let phase2_compiled = new_build.nodes.len();
3031 if let Some(prev) = phase2_ast_cache.read().await.get(&phase2_cache_key)
3035 {
3036 new_build.merge_missing_from(prev);
3037 }
3038 let source_count = new_build.nodes.len();
3039 let cached_build = Arc::new(new_build);
3040 let build_for_save = (*cached_build).clone();
3041 phase2_ast_cache
3042 .write()
3043 .await
3044 .insert(phase2_cache_key.clone().into(), cached_build);
3045 let phase2_message = if phase2_compiled == 0 {
3046 format!(
3047 "project index: phase 2 produced no ASTs in {:.1}s — solc likely errored on the batch (cache retains {} merged sources from phase 1)",
3048 phase2_start.elapsed().as_secs_f64(),
3049 source_count,
3050 )
3051 } else {
3052 format!(
3053 "project index: phase 2 complete — {} sources compiled this round, {} total in cache, in {:.1}s",
3054 phase2_compiled,
3055 source_count,
3056 phase2_start.elapsed().as_secs_f64(),
3057 )
3058 };
3059 phase2_client
3060 .log_message(MessageType::INFO, phase2_message)
3061 .await;
3062
3063 spawn_load_lib_sub_caches_task(
3065 phase2_foundry_config.clone(),
3066 phase2_sub_caches,
3067 phase2_loading_flag,
3068 phase2_path_interner,
3069 phase2_client.clone(),
3070 );
3071
3072 let cfg_for_save = phase2_foundry_config.clone();
3074 let client_for_save = phase2_client.clone();
3075 tokio::spawn(async move {
3076 let res = tokio::task::spawn_blocking(move || {
3077 crate::project_cache::save_reference_cache_with_report(
3078 &cfg_for_save,
3079 &build_for_save,
3080 None,
3081 )
3082 })
3083 .await;
3084 match res {
3085 Ok(Ok(_report)) => {}
3086 Ok(Err(e)) => {
3087 client_for_save
3088 .log_message(
3089 MessageType::WARNING,
3090 format!("project index: cache save failed: {e}"),
3091 )
3092 .await;
3093 }
3094 Err(e) => {
3095 client_for_save
3096 .log_message(
3097 MessageType::WARNING,
3098 format!(
3099 "project index: cache save task failed: {e}"
3100 ),
3101 )
3102 .await;
3103 }
3104 }
3105 });
3106
3107 phase2_client
3108 .send_notification::<notification::Progress>(ProgressParams {
3109 token: token2,
3110 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
3111 WorkDoneProgressEnd {
3112 message: Some(format!(
3113 "Indexed {} source files in {:.1}s",
3114 source_count,
3115 phase2_start.elapsed().as_secs_f64(),
3116 )),
3117 },
3118 )),
3119 })
3120 .await;
3121 }
3122 Err(e) => {
3123 phase2_client
3124 .log_message(
3125 MessageType::WARNING,
3126 format!("project index: phase 2 failed: {e}"),
3127 )
3128 .await;
3129 phase2_client
3130 .send_notification::<notification::Progress>(ProgressParams {
3131 token: token2,
3132 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
3133 WorkDoneProgressEnd {
3134 message: Some(format!("Full index failed: {e}",)),
3135 },
3136 )),
3137 })
3138 .await;
3139 }
3140 }
3141 };
3142
3143 if phase1_ok {
3144 tokio::spawn(phase2);
3146 } else {
3147 phase2.await;
3150
3151 client
3154 .send_notification::<notification::Progress>(ProgressParams {
3155 token,
3156 value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
3157 WorkDoneProgressEnd {
3158 message: Some("Index complete (phase 1 skipped)".to_string()),
3159 },
3160 )),
3161 })
3162 .await;
3163 }
3164 });
3165 }
3166 }
3167
3168 async fn execute_command(
3169 &self,
3170 params: ExecuteCommandParams,
3171 ) -> tower_lsp::jsonrpc::Result<Option<serde_json::Value>> {
3172 match params.command.as_str() {
3173 "solidity.clearCache" => {
3185 let root = self.foundry_config.read().await.root.clone();
3186 let cache_dir = crate::project_cache::cache_dir(&root);
3187
3188 let disk_result = if cache_dir.exists() {
3190 std::fs::remove_dir_all(&cache_dir).map_err(|e| format!("{e}"))
3191 } else {
3192 Ok(())
3193 };
3194
3195 self.ast_cache.write().await.clear();
3197 self.completion_cache.write().await.clear();
3198 self.completion_source_text.write().await.clear();
3199 self.sub_caches.write().await.clear();
3200 self.semantic_token_cache.write().await.clear();
3201 *self.path_interner.write().await = crate::types::PathInterner::new();
3202
3203 match disk_result {
3204 Ok(()) => {
3205 self.client
3206 .show_message(
3207 MessageType::INFO,
3208 format!(
3209 "Cache cleared: {} removed, all in-memory caches reset",
3210 cache_dir.display()
3211 ),
3212 )
3213 .await;
3214 Ok(Some(serde_json::json!({ "success": true })))
3215 }
3216 Err(e) => {
3217 self.client
3218 .show_message(
3219 MessageType::ERROR,
3220 format!("solidity.clearCache: failed to remove cache dir: {e}"),
3221 )
3222 .await;
3223 Err(tower_lsp::jsonrpc::Error {
3224 code: tower_lsp::jsonrpc::ErrorCode::InternalError,
3225 message: std::borrow::Cow::Owned(e),
3226 data: None,
3227 })
3228 }
3229 }
3230 }
3231
3232 "solidity.reindex" => {
3244 if let Some(root_key) = self.project_cache_key().await {
3245 self.ast_cache.write().await.remove(&root_key);
3246 }
3247 self.project_cache_dirty
3248 .store(true, std::sync::atomic::Ordering::Relaxed);
3249 self.project_cache_force_full_rebuild
3253 .store(true, std::sync::atomic::Ordering::Release);
3254
3255 if start_or_mark_project_cache_sync_pending(
3260 &self.project_cache_sync_pending,
3261 &self.project_cache_sync_running,
3262 ) {
3263 let foundry_config = self.foundry_config.read().await.clone();
3264 let root_key = self.project_cache_key().await;
3265 let ast_cache = self.ast_cache.clone();
3266 let client = self.client.clone();
3267 let dirty_flag = self.project_cache_dirty.clone();
3268 let running_flag = self.project_cache_sync_running.clone();
3269 let pending_flag = self.project_cache_sync_pending.clone();
3270 let changed_files = self.project_cache_changed_files.clone();
3271 let force_full_rebuild_flag = self.project_cache_force_full_rebuild.clone();
3272 let path_interner = self.path_interner.clone();
3273
3274 tokio::spawn(async move {
3275 loop {
3276 tokio::time::sleep(std::time::Duration::from_millis(700)).await;
3277
3278 if !take_project_cache_sync_pending(&pending_flag) {
3279 if stop_project_cache_sync_worker_or_reclaim(
3280 &pending_flag,
3281 &running_flag,
3282 ) {
3283 continue;
3284 }
3285 break;
3286 }
3287
3288 if !try_claim_project_cache_dirty(&dirty_flag) {
3289 continue;
3290 }
3291
3292 let Some(cache_key) = &root_key else {
3293 dirty_flag.store(true, Ordering::Release);
3294 continue;
3295 };
3296 if !foundry_config.root.is_dir() {
3297 dirty_flag.store(true, Ordering::Release);
3298 client
3299 .log_message(
3300 MessageType::WARNING,
3301 format!(
3302 "solidity.reindex cache sync: invalid project root {}, deferring",
3303 foundry_config.root.display()
3304 ),
3305 )
3306 .await;
3307 continue;
3308 }
3309
3310 client
3311 .log_message(
3312 MessageType::INFO,
3313 "solidity.reindex: rebuilding project index from disk",
3314 )
3315 .await;
3316
3317 match crate::solc::solc_project_index(
3318 &foundry_config,
3319 Some(&client),
3320 None,
3321 )
3322 .await
3323 {
3324 Ok(ast_data) => {
3325 let mut new_build = crate::goto::CachedBuild::new(
3326 ast_data,
3327 0,
3328 Some(&mut *path_interner.write().await),
3329 );
3330 if let Some(prev) = ast_cache.read().await.get(cache_key) {
3331 new_build.merge_missing_from(prev);
3332 }
3333 let source_count = new_build.nodes.len();
3334 let cached_build = Arc::new(new_build);
3335 let build_for_save = (*cached_build).clone();
3336 ast_cache
3337 .write()
3338 .await
3339 .insert(cache_key.clone().into(), cached_build);
3340
3341 let cfg_for_save = foundry_config.clone();
3342 let save_res = tokio::task::spawn_blocking(move || {
3343 crate::project_cache::save_reference_cache_with_report(
3344 &cfg_for_save,
3345 &build_for_save,
3346 None,
3347 )
3348 })
3349 .await;
3350
3351 match save_res {
3352 Ok(Ok(report)) => {
3353 changed_files.write().await.clear();
3354 force_full_rebuild_flag.store(false, Ordering::Release);
3357 client
3358 .log_message(
3359 MessageType::INFO,
3360 format!(
3361 "solidity.reindex: persisted cache (sources={}, hashed_files={}, duration={}ms)",
3362 source_count, report.file_count_hashed, report.duration_ms
3363 ),
3364 )
3365 .await;
3366 }
3367 Ok(Err(e)) => {
3368 dirty_flag.store(true, Ordering::Release);
3369 client
3370 .log_message(
3371 MessageType::WARNING,
3372 format!(
3373 "solidity.reindex: persist failed, will retry: {e}"
3374 ),
3375 )
3376 .await;
3377 }
3378 Err(e) => {
3379 dirty_flag.store(true, Ordering::Release);
3380 client
3381 .log_message(
3382 MessageType::WARNING,
3383 format!(
3384 "solidity.reindex: save task failed, will retry: {e}"
3385 ),
3386 )
3387 .await;
3388 }
3389 }
3390 }
3391 Err(e) => {
3392 dirty_flag.store(true, Ordering::Release);
3393 client
3394 .log_message(
3395 MessageType::WARNING,
3396 format!(
3397 "solidity.reindex: re-index failed, will retry: {e}"
3398 ),
3399 )
3400 .await;
3401 }
3402 }
3403
3404 if stop_project_cache_sync_worker_or_reclaim(
3405 &pending_flag,
3406 &running_flag,
3407 ) {
3408 continue;
3409 }
3410 break;
3411 }
3412 });
3413 }
3414
3415 self.client
3416 .log_message(
3417 MessageType::INFO,
3418 "solidity.reindex: in-memory cache evicted, background reindex triggered",
3419 )
3420 .await;
3421 Ok(Some(serde_json::json!({ "success": true })))
3422 }
3423
3424 _ => Err(tower_lsp::jsonrpc::Error::method_not_found()),
3425 }
3426 }
3427
3428 async fn shutdown(&self) -> tower_lsp::jsonrpc::Result<()> {
3429 self.flush_project_cache_to_disk("shutdown").await;
3430 self.client
3431 .log_message(MessageType::INFO, "lsp server shutting down.")
3432 .await;
3433 Ok(())
3434 }
3435
3436 async fn did_open(&self, params: DidOpenTextDocumentParams) {
3437 self.client
3438 .log_message(MessageType::INFO, "file opened")
3439 .await;
3440
3441 let mut td = params.text_document;
3442 let template_on_create = self
3443 .settings
3444 .read()
3445 .await
3446 .file_operations
3447 .template_on_create;
3448
3449 let should_attempt_scaffold = template_on_create
3452 && td.text.chars().all(|ch| ch.is_whitespace())
3453 && td.uri.scheme() == "file"
3454 && td
3455 .uri
3456 .to_file_path()
3457 .ok()
3458 .and_then(|p| p.extension().map(|e| e == "sol"))
3459 .unwrap_or(false);
3460
3461 if should_attempt_scaffold {
3462 let uri_str = td.uri.to_string();
3463 let create_flow_pending = {
3464 let pending = self.pending_create_scaffold.read().await;
3465 pending.contains(&uri_str)
3466 };
3467 if create_flow_pending {
3468 self.client
3469 .log_message(
3470 MessageType::INFO,
3471 format!(
3472 "didOpen: skip scaffold for {} (didCreateFiles scaffold pending)",
3473 uri_str
3474 ),
3475 )
3476 .await;
3477 } else {
3478 let cache_has_content = {
3479 let tc = self.text_cache.read().await;
3480 tc.get(&uri_str)
3481 .is_some_and(|(_, c)| c.chars().any(|ch| !ch.is_whitespace()))
3482 };
3483
3484 if !cache_has_content {
3485 let file_has_content = td.uri.to_file_path().ok().is_some_and(|p| {
3486 std::fs::read_to_string(&p)
3487 .is_ok_and(|c| c.chars().any(|ch| !ch.is_whitespace()))
3488 });
3489
3490 if !file_has_content {
3491 let solc_version = self.foundry_config.read().await.solc_version.clone();
3492 if let Some(scaffold) =
3493 file_operations::generate_scaffold(&td.uri, solc_version.as_deref())
3494 {
3495 let end = utils::byte_offset_to_position(&td.text, td.text.len());
3496 let edit = WorkspaceEdit {
3497 changes: Some(HashMap::from([(
3498 td.uri.clone(),
3499 vec![TextEdit {
3500 range: Range {
3501 start: Position::default(),
3502 end,
3503 },
3504 new_text: scaffold.clone(),
3505 }],
3506 )])),
3507 document_changes: None,
3508 change_annotations: None,
3509 };
3510 if self
3511 .client
3512 .apply_edit(edit)
3513 .await
3514 .as_ref()
3515 .is_ok_and(|r| r.applied)
3516 {
3517 td.text = scaffold;
3518 self.client
3519 .log_message(
3520 MessageType::INFO,
3521 format!("didOpen: scaffolded empty file {}", uri_str),
3522 )
3523 .await;
3524 }
3525 }
3526 }
3527 }
3528 }
3529 }
3530
3531 self.on_change(td).await
3532 }
3533
3534 async fn did_change(&self, params: DidChangeTextDocumentParams) {
3535 self.client
3536 .log_message(MessageType::INFO, "file changed")
3537 .await;
3538
3539 if let Some(change) = params.content_changes.into_iter().next() {
3541 let has_substantive_content = change.text.chars().any(|ch| !ch.is_whitespace());
3542 let mut text_cache = self.text_cache.write().await;
3543 text_cache.insert(
3544 params.text_document.uri.to_string().into(),
3545 (params.text_document.version, change.text),
3546 );
3547 drop(text_cache);
3548
3549 if has_substantive_content {
3550 self.pending_create_scaffold
3551 .write()
3552 .await
3553 .remove(params.text_document.uri.as_str());
3554 }
3555 }
3556 }
3557
3558 async fn did_save(&self, params: DidSaveTextDocumentParams) {
3559 let uri_key = params.text_document.uri.to_string();
3564
3565 {
3567 let workers = self.did_save_workers.read().await;
3568 if let Some(tx) = workers.get(&uri_key) {
3569 if tx.send(Some(params.clone())).is_ok() {
3572 return;
3573 }
3574 }
3575 }
3576
3577 let (tx, mut rx) = tokio::sync::watch::channel(Some(params));
3580 rx.mark_changed();
3585 self.did_save_workers
3586 .write()
3587 .await
3588 .insert(uri_key.into(), tx);
3589
3590 let this = self.clone();
3591 tokio::spawn(async move {
3592 loop {
3593 if rx.changed().await.is_err() {
3595 break;
3598 }
3599 let params = match rx.borrow_and_update().clone() {
3600 Some(p) => p,
3601 None => continue,
3602 };
3603 run_did_save(this.clone(), params).await;
3604 }
3605 });
3606 }
3607
3608 async fn will_save(&self, params: WillSaveTextDocumentParams) {
3609 self.client
3610 .log_message(
3611 MessageType::INFO,
3612 format!(
3613 "file will save reason:{:?} {}",
3614 params.reason, params.text_document.uri
3615 ),
3616 )
3617 .await;
3618 }
3619
3620 async fn formatting(
3621 &self,
3622 params: DocumentFormattingParams,
3623 ) -> tower_lsp::jsonrpc::Result<Option<Vec<TextEdit>>> {
3624 self.client
3625 .log_message(MessageType::INFO, "formatting request")
3626 .await;
3627
3628 let uri = params.text_document.uri;
3629 let file_path = match uri.to_file_path() {
3630 Ok(path) => path,
3631 Err(_) => {
3632 self.client
3633 .log_message(MessageType::ERROR, "Invalid file URI for formatting")
3634 .await;
3635 return Ok(None);
3636 }
3637 };
3638 let path_str = match file_path.to_str() {
3639 Some(s) => s,
3640 None => {
3641 self.client
3642 .log_message(MessageType::ERROR, "Invalid file path for formatting")
3643 .await;
3644 return Ok(None);
3645 }
3646 };
3647
3648 let original_content = {
3650 let text_cache = self.text_cache.read().await;
3651 if let Some((_, content)) = text_cache.get(&uri.to_string()) {
3652 content.clone()
3653 } else {
3654 match std::fs::read_to_string(&file_path) {
3656 Ok(content) => content,
3657 Err(_) => {
3658 self.client
3659 .log_message(MessageType::ERROR, "Failed to read file for formatting")
3660 .await;
3661 return Ok(None);
3662 }
3663 }
3664 }
3665 };
3666
3667 let formatted_content = match self.compiler.format(path_str).await {
3669 Ok(content) => content,
3670 Err(e) => {
3671 self.client
3672 .log_message(MessageType::WARNING, format!("Formatting failed: {e}"))
3673 .await;
3674 return Ok(None);
3675 }
3676 };
3677
3678 if original_content != formatted_content {
3680 let end = utils::byte_offset_to_position(&original_content, original_content.len());
3681
3682 {
3684 let mut text_cache = self.text_cache.write().await;
3685 let version = text_cache
3686 .get(&uri.to_string())
3687 .map(|(v, _)| *v)
3688 .unwrap_or(0);
3689 text_cache.insert(uri.to_string().into(), (version, formatted_content.clone()));
3690 }
3691
3692 let edit = TextEdit {
3693 range: Range {
3694 start: Position::default(),
3695 end,
3696 },
3697 new_text: formatted_content,
3698 };
3699 Ok(Some(vec![edit]))
3700 } else {
3701 Ok(None)
3702 }
3703 }
3704
3705 async fn did_close(&self, params: DidCloseTextDocumentParams) {
3706 self.flush_project_cache_to_disk("didClose").await;
3707 let uri = params.text_document.uri.to_string();
3708 self.ast_cache.write().await.remove(&uri);
3709 self.text_cache.write().await.remove(&uri);
3710 self.completion_cache.write().await.remove(&uri);
3711 self.completion_source_text.write().await.remove(&uri);
3712 self.client
3713 .log_message(MessageType::INFO, "file closed, caches cleared.")
3714 .await;
3715 }
3716
3717 async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
3718 let s = config::parse_settings(¶ms.settings);
3719 self.client
3720 .log_message(
3721 MessageType::INFO,
3722 format!(
3723 "settings updated: inlayHints.parameters={}, lint.enabled={}, lint.severity={:?}, lint.only={:?}, lint.exclude={:?}, fileOperations.templateOnCreate={}, fileOperations.updateImportsOnRename={}, fileOperations.updateImportsOnDelete={}, projectIndex.fullProjectScan={}, projectIndex.cacheMode={:?}, projectIndex.incrementalEditReindex={}",
3724 s.inlay_hints.parameters, 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, s.project_index.full_project_scan, s.project_index.cache_mode, s.project_index.incremental_edit_reindex,
3725 ),
3726 )
3727 .await;
3728 let mut settings = self.settings.write().await;
3729 *settings = s;
3730
3731 let client = self.client.clone();
3733 tokio::spawn(async move {
3734 let _ = client.inlay_hint_refresh().await;
3735 });
3736 }
3737 async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {
3738 self.client
3739 .log_message(MessageType::INFO, "workdspace folders changed.")
3740 .await;
3741 }
3742
3743 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
3744 self.client
3745 .log_message(MessageType::INFO, "watched files have changed.")
3746 .await;
3747
3748 for change in ¶ms.changes {
3750 let path = match change.uri.to_file_path() {
3751 Ok(p) => p,
3752 Err(_) => continue,
3753 };
3754
3755 let filename = path.file_name().and_then(|n| n.to_str());
3756
3757 if filename == Some("foundry.toml") {
3758 let lint_cfg = config::load_lint_config_from_toml(&path);
3759 self.client
3760 .log_message(
3761 MessageType::INFO,
3762 format!(
3763 "reloaded foundry.toml lint config: lint_on_build={}, ignore_patterns={}",
3764 lint_cfg.lint_on_build,
3765 lint_cfg.ignore_patterns.len()
3766 ),
3767 )
3768 .await;
3769 let mut lc = self.lint_config.write().await;
3770 *lc = lint_cfg;
3771
3772 let foundry_cfg = config::load_foundry_config_from_toml(&path);
3773 self.client
3774 .log_message(
3775 MessageType::INFO,
3776 format!(
3777 "reloaded foundry.toml: solc={}, remappings={}",
3778 foundry_cfg.solc_version.as_deref().unwrap_or("auto"),
3779 foundry_cfg.remappings.len()
3780 ),
3781 )
3782 .await;
3783 if foundry_cfg.via_ir {
3784 self.client
3785 .log_message(
3786 MessageType::WARNING,
3787 "via_ir is enabled in foundry.toml — gas estimate inlay hints are disabled to avoid slow compilation",
3788 )
3789 .await;
3790 }
3791 let mut fc = self.foundry_config.write().await;
3792 *fc = foundry_cfg;
3793 break;
3794 }
3795
3796 if filename == Some("remappings.txt") {
3797 self.client
3798 .log_message(
3799 MessageType::INFO,
3800 "remappings.txt changed, config may need refresh",
3801 )
3802 .await;
3803 }
3806 }
3807 }
3808
3809 async fn completion(
3810 &self,
3811 params: CompletionParams,
3812 ) -> tower_lsp::jsonrpc::Result<Option<CompletionResponse>> {
3813 let uri = params.text_document_position.text_document.uri;
3814 let position = params.text_document_position.position;
3815
3816 let trigger_char = params
3817 .context
3818 .as_ref()
3819 .and_then(|ctx| ctx.trigger_character.as_deref());
3820
3821 let source_text = {
3823 let text_cache = self.text_cache.read().await;
3824 if let Some((_, text)) = text_cache.get(&uri.to_string()) {
3825 text.clone()
3826 } else {
3827 match uri.to_file_path() {
3828 Ok(path) => std::fs::read_to_string(&path).unwrap_or_default(),
3829 Err(_) => return Ok(None),
3830 }
3831 }
3832 };
3833
3834 let local_cached: Option<Arc<completion::CompletionCache>> = {
3836 let comp_cache = self.completion_cache.read().await;
3837 comp_cache.get(&uri.to_string()).cloned()
3838 };
3839
3840 let root_cached: Option<Arc<completion::CompletionCache>> = {
3842 let root_key = self.project_cache_key().await;
3843 match root_key {
3844 Some(root_key) => {
3845 let ast_cache = self.ast_cache.read().await;
3846 ast_cache
3847 .get(&root_key)
3848 .map(|root_build| root_build.completion_cache.clone())
3849 }
3850 None => None,
3851 }
3852 };
3853
3854 let cached = local_cached.or(root_cached.clone());
3856
3857 if cached.is_none() {
3858 let ast_cache = self.ast_cache.clone();
3860 let completion_cache = self.completion_cache.clone();
3861 let uri_string = uri.to_string();
3862 tokio::spawn(async move {
3863 let cached_build = {
3864 let cache = ast_cache.read().await;
3865 match cache.get(&uri_string) {
3866 Some(v) => v.clone(),
3867 None => return,
3868 }
3869 };
3870 completion_cache
3871 .write()
3872 .await
3873 .insert(uri_string.into(), cached_build.completion_cache.clone());
3874 });
3875 }
3876
3877 let cache_ref = cached.as_deref();
3878
3879 let compiled_text: Option<Arc<str>> = match cached.as_ref() {
3881 Some(c) => self
3882 .completion_source_text
3883 .read()
3884 .await
3885 .get(&uri.to_string())
3886 .filter(|(src_cache, _)| Arc::ptr_eq(src_cache, c))
3887 .map(|(_, text)| text.clone()),
3888 None => None,
3889 };
3890
3891 let file_id = {
3893 let uri_path = uri.to_file_path().ok();
3894 cache_ref.and_then(|c| {
3895 uri_path.as_ref().and_then(|p| {
3896 let path_str = p.to_str()?;
3897 c.path_to_file_id.get(path_str).copied()
3898 })
3899 })
3900 };
3901
3902 let current_file_path = uri
3903 .to_file_path()
3904 .ok()
3905 .and_then(|p| p.to_str().map(|s| s.to_string()));
3906
3907 let check_pos = if matches!(trigger_char, Some("\"") | Some("'")) {
3918 Position {
3919 line: position.line,
3920 character: position.character.saturating_add(1),
3921 }
3922 } else {
3923 position
3924 };
3925
3926 if let Some(asm_range) =
3931 links::ts_cursor_in_assembly_flags(source_text.as_bytes(), check_pos)
3932 {
3933 let text_edit = CompletionTextEdit::Edit(TextEdit {
3934 range: Range {
3935 start: Position {
3936 line: position.line,
3937 character: asm_range.start.character,
3938 },
3939 end: Position {
3940 line: position.line,
3941 character: check_pos.character,
3942 },
3943 },
3944 new_text: "memory-safe".to_string(),
3945 });
3946 let item = CompletionItem {
3947 label: "memory-safe".to_string(),
3948 kind: Some(CompletionItemKind::VALUE),
3949 detail: Some("Solidity assembly dialect".to_string()),
3950 filter_text: Some("memory-safe".to_string()),
3951 text_edit: Some(text_edit),
3952 ..Default::default()
3953 };
3954 return Ok(Some(CompletionResponse::List(CompletionList {
3955 is_incomplete: false,
3956 items: vec![item],
3957 })));
3958 }
3959
3960 if let Some(import_range) =
3967 links::ts_cursor_in_import_string(source_text.as_bytes(), check_pos)
3968 {
3969 if let Ok(current_file) = uri.to_file_path() {
3970 let foundry_cfg = self.foundry_config.read().await.clone();
3971 let project_root = foundry_cfg.root.clone();
3972 let remappings = crate::solc::resolve_remappings(&foundry_cfg).await;
3973 let typed_range = Some((
3976 position.line,
3977 import_range.start.character,
3978 check_pos.character,
3979 ));
3980 let items = completion::all_sol_import_paths(
3981 ¤t_file,
3982 &project_root,
3983 &remappings,
3984 typed_range,
3985 );
3986 return Ok(Some(CompletionResponse::List(CompletionList {
3987 is_incomplete: true,
3988 items,
3989 })));
3990 }
3991 return Ok(None);
3992 }
3993
3994 if matches!(trigger_char, Some("\"") | Some("'")) {
3998 return Ok(None);
3999 }
4000
4001 let tail_candidates = if trigger_char == Some(".") {
4002 vec![]
4003 } else {
4004 root_cached.as_deref().map_or_else(Vec::new, |c| {
4005 completion::top_level_importable_completion_candidates(
4006 c,
4007 current_file_path.as_deref(),
4008 &source_text,
4009 )
4010 })
4011 };
4012
4013 let result = completion::handle_completion_with_tail_candidates(
4014 cache_ref,
4015 &source_text,
4016 position,
4017 trigger_char,
4018 file_id,
4019 compiled_text.as_deref(),
4020 tail_candidates,
4021 );
4022 Ok(result)
4023 }
4024
4025 async fn goto_definition(
4026 &self,
4027 params: GotoDefinitionParams,
4028 ) -> tower_lsp::jsonrpc::Result<Option<GotoDefinitionResponse>> {
4029 self.client
4030 .log_message(MessageType::INFO, "got textDocument/definition request")
4031 .await;
4032
4033 let uri = params.text_document_position_params.text_document.uri;
4034 let position = params.text_document_position_params.position;
4035
4036 let file_path = match uri.to_file_path() {
4037 Ok(path) => path,
4038 Err(_) => {
4039 self.client
4040 .log_message(MessageType::ERROR, "Invalid file uri")
4041 .await;
4042 return Ok(None);
4043 }
4044 };
4045
4046 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
4047 Some(bytes) => bytes,
4048 None => return Ok(None),
4049 };
4050
4051 let source_text = String::from_utf8_lossy(&source_bytes).to_string();
4052
4053 {
4058 let imports = crate::links::ts_find_imports(&source_bytes);
4059 if let Some(imp) = imports.iter().find(|imp| {
4060 let r = &imp.inner_range;
4061 position >= r.start && position <= r.end
4062 }) {
4063 let foundry_cfg = self.foundry_config_for_file(&file_path).await;
4064 let remappings = crate::solc::resolve_remappings(&foundry_cfg).await;
4065 if let Some(abs) = resolve_import_spec_to_abs(
4066 &foundry_cfg.root,
4067 &file_path,
4068 &imp.path,
4069 &remappings,
4070 ) && abs.exists()
4071 && let Ok(target_uri) = Url::from_file_path(&abs)
4072 {
4073 let location = Location {
4074 uri: target_uri,
4075 range: Range::default(), };
4077 self.client
4078 .log_message(
4079 MessageType::INFO,
4080 format!("found definition (import path) at {}", location.uri),
4081 )
4082 .await;
4083 return Ok(Some(GotoDefinitionResponse::from(location)));
4084 }
4085 }
4086 }
4087
4088 {
4094 let identifier = crate::rename::get_identifier_at_position(&source_bytes, position);
4095 if let Some(ref ident) = identifier {
4096 let alias_names = crate::rename::ts_find_alias_names(&source_bytes);
4097 if alias_names.contains(ident.as_str())
4098 && let Some(decl_range) =
4099 crate::rename::ts_find_alias_declaration(&source_bytes, ident)
4100 {
4101 let location = Location {
4102 uri: uri.clone(),
4103 range: decl_range,
4104 };
4105 self.client
4106 .log_message(
4107 MessageType::INFO,
4108 format!(
4109 "found definition (alias declaration) for '{}' at {}:{}",
4110 ident, location.uri, decl_range.start.line
4111 ),
4112 )
4113 .await;
4114 return Ok(Some(GotoDefinitionResponse::from(location)));
4115 }
4116 }
4117 }
4118
4119 let cursor_name = goto::cursor_context(&source_text, position).map(|ctx| ctx.name);
4121
4122 let (is_dirty, cached_build) = {
4126 let text_version = self
4127 .text_cache
4128 .read()
4129 .await
4130 .get(&uri.to_string())
4131 .map(|(v, _)| *v)
4132 .unwrap_or(0);
4133 let cb = self.get_or_fetch_build(&uri, &file_path, false).await;
4134 let build_version = cb.as_ref().map(|b| b.build_version).unwrap_or(0);
4135 (text_version > build_version, cb)
4136 };
4137
4138 let validate_ts = |loc: &Location| -> bool {
4144 let Some(ref name) = cursor_name else {
4145 return true; };
4147 let target_src = if loc.uri == uri {
4148 Some(source_text.clone())
4149 } else {
4150 loc.uri
4151 .to_file_path()
4152 .ok()
4153 .and_then(|p| std::fs::read_to_string(&p).ok())
4154 };
4155 match target_src {
4156 Some(src) => goto::validate_goto_target(&src, loc, name),
4157 None => true, }
4159 };
4160
4161 if is_dirty {
4162 self.client
4163 .log_message(MessageType::INFO, "file is dirty, trying tree-sitter first")
4164 .await;
4165
4166 let ts_result = {
4168 let comp_cache = self.completion_cache.read().await;
4169 let text_cache = self.text_cache.read().await;
4170 if let Some(cc) = comp_cache.get(&uri.to_string()) {
4171 goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
4172 } else {
4173 None
4174 }
4175 };
4176
4177 if let Some(location) = ts_result {
4178 if validate_ts(&location) {
4179 self.client
4180 .log_message(
4181 MessageType::INFO,
4182 format!(
4183 "found definition (tree-sitter) at {}:{}",
4184 location.uri, location.range.start.line
4185 ),
4186 )
4187 .await;
4188 return Ok(Some(GotoDefinitionResponse::from(location)));
4189 }
4190 self.client
4191 .log_message(
4192 MessageType::INFO,
4193 "tree-sitter result failed validation, trying AST fallback",
4194 )
4195 .await;
4196 }
4197
4198 if let Some(ref cb) = cached_build
4203 && let Some(ref name) = cursor_name
4204 {
4205 let byte_hint = goto::pos_to_bytes(&source_bytes, position);
4206 if let Some(location) = goto::goto_declaration_by_name(cb, &uri, name, byte_hint) {
4207 self.client
4208 .log_message(
4209 MessageType::INFO,
4210 format!(
4211 "found definition (AST by name) at {}:{}",
4212 location.uri, location.range.start.line
4213 ),
4214 )
4215 .await;
4216 return Ok(Some(GotoDefinitionResponse::from(location)));
4217 }
4218 }
4219 } else {
4220 if let Some(ref cb) = cached_build
4222 && let Some(location) =
4223 goto::goto_declaration_cached(cb, &uri, position, &source_bytes)
4224 {
4225 self.client
4226 .log_message(
4227 MessageType::INFO,
4228 format!(
4229 "found definition (AST) at {}:{}",
4230 location.uri, location.range.start.line
4231 ),
4232 )
4233 .await;
4234 return Ok(Some(GotoDefinitionResponse::from(location)));
4235 }
4236
4237 let ts_result = {
4239 let comp_cache = self.completion_cache.read().await;
4240 let text_cache = self.text_cache.read().await;
4241 if let Some(cc) = comp_cache.get(&uri.to_string()) {
4242 goto::goto_definition_ts(&source_text, position, &uri, cc, &text_cache)
4243 } else {
4244 None
4245 }
4246 };
4247
4248 if let Some(location) = ts_result {
4249 if validate_ts(&location) {
4250 self.client
4251 .log_message(
4252 MessageType::INFO,
4253 format!(
4254 "found definition (tree-sitter fallback) at {}:{}",
4255 location.uri, location.range.start.line
4256 ),
4257 )
4258 .await;
4259 return Ok(Some(GotoDefinitionResponse::from(location)));
4260 }
4261 self.client
4262 .log_message(MessageType::INFO, "tree-sitter fallback failed validation")
4263 .await;
4264 }
4265 }
4266
4267 self.client
4268 .log_message(MessageType::INFO, "no definition found")
4269 .await;
4270 Ok(None)
4271 }
4272
4273 async fn goto_declaration(
4274 &self,
4275 params: request::GotoDeclarationParams,
4276 ) -> tower_lsp::jsonrpc::Result<Option<request::GotoDeclarationResponse>> {
4277 self.client
4278 .log_message(MessageType::INFO, "got textDocument/declaration request")
4279 .await;
4280
4281 let uri = params.text_document_position_params.text_document.uri;
4282 let position = params.text_document_position_params.position;
4283
4284 let file_path = match uri.to_file_path() {
4285 Ok(path) => path,
4286 Err(_) => {
4287 self.client
4288 .log_message(MessageType::ERROR, "invalid file uri")
4289 .await;
4290 return Ok(None);
4291 }
4292 };
4293
4294 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
4295 Some(bytes) => bytes,
4296 None => return Ok(None),
4297 };
4298
4299 {
4301 let identifier = crate::rename::get_identifier_at_position(&source_bytes, position);
4302 if let Some(ref ident) = identifier {
4303 let alias_names = crate::rename::ts_find_alias_names(&source_bytes);
4304 if alias_names.contains(ident.as_str())
4305 && let Some(decl_range) =
4306 crate::rename::ts_find_alias_declaration(&source_bytes, ident)
4307 {
4308 let location = Location {
4309 uri: uri.clone(),
4310 range: decl_range,
4311 };
4312 self.client
4313 .log_message(
4314 MessageType::INFO,
4315 format!(
4316 "found declaration (alias) for '{}' at line {}",
4317 ident, decl_range.start.line
4318 ),
4319 )
4320 .await;
4321 return Ok(Some(request::GotoDeclarationResponse::from(location)));
4322 }
4323 }
4324 }
4325
4326 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
4327 let cached_build = match cached_build {
4328 Some(cb) => cb,
4329 None => return Ok(None),
4330 };
4331
4332 if let Some(location) =
4333 goto::goto_declaration_cached(&cached_build, &uri, position, &source_bytes)
4334 {
4335 self.client
4336 .log_message(
4337 MessageType::INFO,
4338 format!(
4339 "found declaration at {}:{}",
4340 location.uri, location.range.start.line
4341 ),
4342 )
4343 .await;
4344 Ok(Some(request::GotoDeclarationResponse::from(location)))
4345 } else {
4346 self.client
4347 .log_message(MessageType::INFO, "no declaration found")
4348 .await;
4349 Ok(None)
4350 }
4351 }
4352
4353 async fn goto_implementation(
4354 &self,
4355 params: request::GotoImplementationParams,
4356 ) -> tower_lsp::jsonrpc::Result<Option<request::GotoImplementationResponse>> {
4357 self.client
4358 .log_message(MessageType::INFO, "got textDocument/implementation request")
4359 .await;
4360
4361 let uri = params.text_document_position_params.text_document.uri;
4362 let position = params.text_document_position_params.position;
4363
4364 let file_path = match uri.to_file_path() {
4365 Ok(path) => path,
4366 Err(_) => {
4367 self.client
4368 .log_message(MessageType::ERROR, "invalid file uri")
4369 .await;
4370 return Ok(None);
4371 }
4372 };
4373
4374 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
4375 Some(bytes) => bytes,
4376 None => return Ok(None),
4377 };
4378
4379 let is_alias = {
4384 let ident = crate::rename::get_identifier_at_position(&source_bytes, position);
4385 if let Some(ref name) = ident {
4386 let alias_names = crate::rename::ts_find_alias_names(&source_bytes);
4387 alias_names.contains(name.as_str())
4388 } else {
4389 false
4390 }
4391 };
4392
4393 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
4394
4395 if is_alias {
4400 let build = match &cached_build {
4403 Some(cb) => Some(cb.clone()),
4404 None => self.get_or_fetch_build(&uri, &file_path, true).await,
4405 };
4406 if let Some(ref cb) = build {
4407 let byte_pos = goto::pos_to_bytes(&source_bytes, position);
4408
4409 let lookup_position = if let Some(foreign_byte) =
4415 crate::rename::ts_alias_foreign_byte_offset(&source_bytes, byte_pos)
4416 {
4417 goto::bytes_to_pos(&source_bytes, foreign_byte).unwrap_or(position)
4419 } else {
4420 position
4422 };
4423
4424 if let Some(location) =
4425 goto::goto_declaration_cached(cb, &uri, lookup_position, &source_bytes)
4426 {
4427 let ident = crate::rename::get_identifier_at_position(&source_bytes, position)
4428 .unwrap_or_default();
4429 self.client
4430 .log_message(
4431 MessageType::INFO,
4432 format!(
4433 "found implementation (alias → original) for '{}' at {}:{}",
4434 ident, location.uri, location.range.start.line
4435 ),
4436 )
4437 .await;
4438 return Ok(Some(request::GotoImplementationResponse::Scalar(location)));
4439 }
4440 }
4441 }
4442
4443 let cached_build = match cached_build {
4444 Some(cb) => cb,
4445 None => return Ok(None),
4446 };
4447
4448 let byte_position = goto::pos_to_bytes(&source_bytes, position);
4449 let abs_path = uri.as_ref().strip_prefix("file://").unwrap_or(uri.as_ref());
4450
4451 let (target_id, target_decl_abs, target_decl_offset) =
4455 match references::byte_to_id(&cached_build.nodes, abs_path, byte_position) {
4456 Some(id) => {
4457 let resolved = cached_build
4458 .nodes
4459 .get(abs_path)
4460 .and_then(|f| f.get(&id))
4461 .and_then(|info| info.referenced_declaration)
4462 .unwrap_or(id);
4463
4464 let (decl_abs, decl_offset) = references::resolve_target_location(
4466 &cached_build,
4467 &uri,
4468 position,
4469 &source_bytes,
4470 )
4471 .unwrap_or_else(|| (abs_path.to_string(), byte_position));
4472
4473 (resolved, decl_abs, decl_offset)
4474 }
4475 None => return Ok(None),
4476 };
4477
4478 let project_build = self.ensure_project_cached_build().await;
4480 let sub_caches = self.sub_caches.read().await;
4481
4482 let mut builds: Vec<&goto::CachedBuild> = vec![&cached_build];
4483 if let Some(ref pb) = project_build {
4484 builds.push(pb);
4485 }
4486 for sc in sub_caches.iter() {
4487 builds.push(sc);
4488 }
4489
4490 let mut locations: Vec<Location> = Vec::new();
4495 let mut seen_positions: Vec<(String, u32, u32)> = Vec::new(); for build in &builds {
4498 let local_target =
4500 references::byte_to_id(&build.nodes, &target_decl_abs, target_decl_offset).or_else(
4501 || {
4502 if build.nodes.values().any(|f| f.contains_key(&target_id)) {
4504 Some(target_id)
4505 } else {
4506 None
4507 }
4508 },
4509 );
4510
4511 let Some(local_id) = local_target else {
4512 continue;
4513 };
4514
4515 let Some(impls) = build.base_function_implementation.get(&local_id) else {
4517 continue;
4518 };
4519
4520 for &impl_id in impls {
4521 if let Some(loc) =
4522 references::id_to_location(&build.nodes, &build.id_to_path_map, impl_id)
4523 {
4524 let key = (
4526 loc.uri.to_string(),
4527 loc.range.start.line,
4528 loc.range.start.character,
4529 );
4530 if !seen_positions.contains(&key) {
4531 seen_positions.push(key);
4532 locations.push(loc);
4533 }
4534 }
4535 }
4536 }
4537
4538 if locations.is_empty() {
4541 if let Some(location) =
4542 goto::goto_declaration_cached(&cached_build, &uri, position, &source_bytes)
4543 {
4544 self.client
4545 .log_message(
4546 MessageType::INFO,
4547 "no implementations found, falling back to definition",
4548 )
4549 .await;
4550 return Ok(Some(request::GotoImplementationResponse::Scalar(location)));
4551 }
4552
4553 self.client
4554 .log_message(MessageType::INFO, "no implementations found")
4555 .await;
4556 return Ok(None);
4557 }
4558
4559 self.client
4560 .log_message(
4561 MessageType::INFO,
4562 format!("found {} implementation(s)", locations.len()),
4563 )
4564 .await;
4565
4566 if locations.len() == 1 {
4567 Ok(Some(request::GotoImplementationResponse::Scalar(
4568 locations.into_iter().next().unwrap(),
4569 )))
4570 } else {
4571 Ok(Some(request::GotoImplementationResponse::Array(locations)))
4572 }
4573 }
4574
4575 async fn references(
4576 &self,
4577 params: ReferenceParams,
4578 ) -> tower_lsp::jsonrpc::Result<Option<Vec<Location>>> {
4579 self.client
4580 .log_message(MessageType::INFO, "Got a textDocument/references request")
4581 .await;
4582
4583 let uri = params.text_document_position.text_document.uri;
4584 let position = params.text_document_position.position;
4585 let file_path = match uri.to_file_path() {
4586 Ok(path) => path,
4587 Err(_) => {
4588 self.client
4589 .log_message(MessageType::ERROR, "Invalid file URI")
4590 .await;
4591 return Ok(None);
4592 }
4593 };
4594 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
4595 Some(bytes) => bytes,
4596 None => return Ok(None),
4597 };
4598
4599 {
4611 let cursor_byte = crate::goto::pos_to_bytes(&source_bytes, position);
4612 let identifier = crate::rename::get_identifier_at_position(&source_bytes, position);
4613
4614 let is_alias = if let Some(alias_name) =
4615 crate::rename::ts_alias_local_name_at_cursor(&source_bytes, cursor_byte)
4616 {
4617 Some(alias_name)
4619 } else if let Some(ref ident) = identifier {
4620 let alias_names = crate::rename::ts_find_alias_names(&source_bytes);
4623 if alias_names.contains(ident.as_str()) {
4624 Some(ident.clone())
4625 } else {
4626 None
4627 }
4628 } else {
4629 None
4630 };
4631
4632 if let Some(alias_name) = is_alias {
4633 let locations = crate::rename::ts_collect_identifier_locations(
4634 &source_bytes,
4635 &uri,
4636 &alias_name,
4637 );
4638 self.client
4639 .log_message(
4640 MessageType::INFO,
4641 format!(
4642 "Found {} references for alias '{}'",
4643 locations.len(),
4644 alias_name
4645 ),
4646 )
4647 .await;
4648 return Ok(Some(locations));
4649 }
4650 }
4651
4652 let file_build = self.get_or_fetch_build(&uri, &file_path, true).await;
4653 let file_build = match file_build {
4654 Some(cb) => cb,
4655 None => return Ok(None),
4656 };
4657 let mut project_build = self.ensure_project_cached_build().await;
4658 let current_abs = file_path.to_string_lossy().to_string();
4659 if self.use_solc
4660 && self.settings.read().await.project_index.full_project_scan
4661 && project_build
4662 .as_ref()
4663 .is_some_and(|b| !b.nodes.contains_key(current_abs.as_str()))
4664 {
4665 let foundry_config = self.foundry_config_for_file(&file_path).await;
4666 let remappings = crate::solc::resolve_remappings(&foundry_config).await;
4667 let changed = vec![PathBuf::from(¤t_abs)];
4668 let cfg_for_plan = foundry_config.clone();
4669 let remappings_for_plan = remappings.clone();
4670 let affected_set = tokio::task::spawn_blocking(move || {
4671 compute_reverse_import_closure(&cfg_for_plan, &changed, &remappings_for_plan)
4672 })
4673 .await
4674 .ok()
4675 .unwrap_or_default();
4676 let mut affected_files: Vec<PathBuf> = affected_set.into_iter().collect();
4677 if affected_files.is_empty() {
4678 affected_files.push(PathBuf::from(¤t_abs));
4679 }
4680 let text_cache_snapshot = self.text_cache.read().await.clone();
4681 match crate::solc::solc_project_index_scoped(
4682 &foundry_config,
4683 Some(&self.client),
4684 Some(&text_cache_snapshot),
4685 &affected_files,
4686 )
4687 .await
4688 {
4689 Ok(ast_data) => {
4690 let scoped_build = Arc::new(crate::goto::CachedBuild::new(
4691 ast_data,
4692 0,
4693 Some(&mut *self.path_interner.write().await),
4694 ));
4695 if let Some(root_key) = self.project_cache_key().await {
4696 let merged = {
4697 let mut cache = self.ast_cache.write().await;
4698 let merged = if let Some(existing) = cache.get(&root_key).cloned() {
4699 let mut merged = (*existing).clone();
4700 match merge_scoped_cached_build(
4701 &mut merged,
4702 (*scoped_build).clone(),
4703 ) {
4704 Ok(_) => Arc::new(merged),
4705 Err(_) => scoped_build.clone(),
4706 }
4707 } else {
4708 scoped_build.clone()
4709 };
4710 cache.insert(root_key.into(), merged.clone());
4711 merged
4712 };
4713 project_build = Some(merged);
4714 } else {
4715 project_build = Some(scoped_build);
4716 }
4717 self.client
4718 .log_message(
4719 MessageType::INFO,
4720 format!(
4721 "references warm-refresh: scoped reindex applied (affected={})",
4722 affected_files.len()
4723 ),
4724 )
4725 .await;
4726 }
4727 Err(e) => {
4728 self.client
4729 .log_message(
4730 MessageType::WARNING,
4731 format!("references warm-refresh: scoped reindex failed: {e}"),
4732 )
4733 .await;
4734 }
4735 }
4736 }
4737
4738 let mut locations = references::goto_references_cached(
4741 &file_build,
4742 &uri,
4743 position,
4744 &source_bytes,
4745 None,
4746 params.context.include_declaration,
4747 );
4748
4749 if let Some((def_abs_path, def_byte_offset)) =
4755 references::resolve_target_location(&file_build, &uri, position, &source_bytes)
4756 {
4757 if let Some(project_build) = project_build {
4758 let other_locations = references::goto_references_for_target(
4759 &project_build,
4760 &def_abs_path,
4761 def_byte_offset,
4762 None,
4763 params.context.include_declaration,
4764 Some(¤t_abs),
4765 );
4766 locations.extend(other_locations);
4767 }
4768
4769 let sub_caches = self.sub_caches.read().await;
4774 for sub_cache in sub_caches.iter() {
4775 let sub_locations = references::goto_references_for_target(
4776 sub_cache,
4777 &def_abs_path,
4778 def_byte_offset,
4779 None,
4780 params.context.include_declaration,
4781 None,
4782 );
4783 locations.extend(sub_locations);
4784 }
4785
4786 let project_root_key = self.project_cache_key().await;
4802 let other_builds: Vec<Arc<goto::CachedBuild>> = {
4803 let cache = self.ast_cache.read().await;
4804 cache
4805 .iter()
4806 .filter(|(key, _)| {
4807 key.as_str() != uri.to_string()
4808 && project_root_key.as_deref() != Some(key.as_str())
4809 })
4810 .map(|(_, v)| v.clone())
4811 .collect()
4812 };
4813 for other in &other_builds {
4814 let extra = references::goto_references_for_target(
4815 other,
4816 &def_abs_path,
4817 def_byte_offset,
4818 None,
4819 params.context.include_declaration,
4820 Some(¤t_abs),
4821 );
4822 locations.extend(extra);
4823 }
4824 }
4825
4826 locations = references::dedup_locations(locations);
4830
4831 self.client
4832 .log_message(
4833 MessageType::INFO,
4834 format!("Found {} references", locations.len()),
4835 )
4836 .await;
4837 Ok(Some(locations))
4838 }
4839
4840 async fn prepare_rename(
4841 &self,
4842 params: TextDocumentPositionParams,
4843 ) -> tower_lsp::jsonrpc::Result<Option<PrepareRenameResponse>> {
4844 self.client
4845 .log_message(MessageType::INFO, "got textDocument/prepareRename request")
4846 .await;
4847
4848 let uri = params.text_document.uri;
4849 let position = params.position;
4850
4851 let file_path = match uri.to_file_path() {
4852 Ok(path) => path,
4853 Err(_) => {
4854 self.client
4855 .log_message(MessageType::ERROR, "invalid file uri")
4856 .await;
4857 return Ok(None);
4858 }
4859 };
4860
4861 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
4862 Some(bytes) => bytes,
4863 None => return Ok(None),
4864 };
4865
4866 if let Some(range) = rename::get_identifier_range(&source_bytes, position) {
4867 self.client
4868 .log_message(
4869 MessageType::INFO,
4870 format!(
4871 "prepare rename range: {}:{}",
4872 range.start.line, range.start.character
4873 ),
4874 )
4875 .await;
4876 Ok(Some(PrepareRenameResponse::Range(range)))
4877 } else {
4878 self.client
4879 .log_message(MessageType::INFO, "no identifier found for prepare rename")
4880 .await;
4881 Ok(None)
4882 }
4883 }
4884
4885 async fn rename(
4886 &self,
4887 params: RenameParams,
4888 ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
4889 self.client
4890 .log_message(MessageType::INFO, "got textDocument/rename request")
4891 .await;
4892
4893 let uri = params.text_document_position.text_document.uri;
4894 let position = params.text_document_position.position;
4895 let new_name = params.new_name;
4896 let file_path = match uri.to_file_path() {
4897 Ok(p) => p,
4898 Err(_) => {
4899 self.client
4900 .log_message(MessageType::ERROR, "invalid file uri")
4901 .await;
4902 return Ok(None);
4903 }
4904 };
4905 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
4906 Some(bytes) => bytes,
4907 None => return Ok(None),
4908 };
4909
4910 let current_identifier = match rename::get_identifier_at_position(&source_bytes, position) {
4911 Some(id) => id,
4912 None => {
4913 self.client
4914 .log_message(MessageType::ERROR, "No identifier found at position")
4915 .await;
4916 return Ok(None);
4917 }
4918 };
4919
4920 if !utils::is_valid_solidity_identifier(&new_name) {
4921 return Err(tower_lsp::jsonrpc::Error::invalid_params(
4922 "new name is not a valid solidity identifier",
4923 ));
4924 }
4925
4926 if new_name == current_identifier {
4927 self.client
4928 .log_message(
4929 MessageType::INFO,
4930 "new name is the same as current identifier",
4931 )
4932 .await;
4933 return Ok(None);
4934 }
4935
4936 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
4937 let cached_build = match cached_build {
4938 Some(cb) => cb,
4939 None => return Ok(None),
4940 };
4941 let other_builds: Vec<Arc<goto::CachedBuild>> = {
4942 let cache = self.ast_cache.read().await;
4943 cache
4944 .iter()
4945 .filter(|(key, _)| key.as_str() != uri.to_string())
4946 .map(|(_, v)| v.clone())
4947 .collect()
4948 };
4949 let other_refs: Vec<&goto::CachedBuild> = other_builds.iter().map(|v| v.as_ref()).collect();
4950
4951 let text_buffers: HashMap<String, Vec<u8>> = {
4955 let text_cache = self.text_cache.read().await;
4956 text_cache
4957 .iter()
4958 .map(|(uri, (_, content))| (uri.to_string(), content.as_bytes().to_vec()))
4959 .collect()
4960 };
4961
4962 match rename::rename_symbol(
4963 &cached_build,
4964 &uri,
4965 position,
4966 &source_bytes,
4967 new_name,
4968 &other_refs,
4969 &text_buffers,
4970 ) {
4971 Some(workspace_edit) => {
4972 self.client
4973 .log_message(
4974 MessageType::INFO,
4975 format!(
4976 "created rename edit with {} file(s), {} total change(s)",
4977 workspace_edit
4978 .changes
4979 .as_ref()
4980 .map(|c| c.len())
4981 .unwrap_or(0),
4982 workspace_edit
4983 .changes
4984 .as_ref()
4985 .map(|c| c.values().map(|v| v.len()).sum::<usize>())
4986 .unwrap_or(0)
4987 ),
4988 )
4989 .await;
4990
4991 Ok(Some(workspace_edit))
4996 }
4997
4998 None => {
4999 self.client
5000 .log_message(MessageType::INFO, "No locations found for renaming")
5001 .await;
5002 Ok(None)
5003 }
5004 }
5005 }
5006
5007 async fn symbol(
5008 &self,
5009 params: WorkspaceSymbolParams,
5010 ) -> tower_lsp::jsonrpc::Result<Option<Vec<SymbolInformation>>> {
5011 self.client
5012 .log_message(MessageType::INFO, "got workspace/symbol request")
5013 .await;
5014
5015 let files: Vec<(Url, String)> = {
5017 let cache = self.text_cache.read().await;
5018 cache
5019 .iter()
5020 .filter(|(uri_str, _)| uri_str.ends_with(".sol"))
5021 .filter_map(|(uri_str, (_, content))| {
5022 Url::parse(uri_str).ok().map(|uri| (uri, content.clone()))
5023 })
5024 .collect()
5025 };
5026
5027 let mut all_symbols = symbols::extract_workspace_symbols(&files);
5028 if !params.query.is_empty() {
5029 let query = params.query.to_lowercase();
5030 all_symbols.retain(|symbol| symbol.name.to_lowercase().contains(&query));
5031 }
5032 self.client
5033 .log_message(
5034 MessageType::INFO,
5035 format!("found {} symbols", all_symbols.len()),
5036 )
5037 .await;
5038 Ok(Some(all_symbols))
5039 }
5040
5041 async fn document_symbol(
5042 &self,
5043 params: DocumentSymbolParams,
5044 ) -> tower_lsp::jsonrpc::Result<Option<DocumentSymbolResponse>> {
5045 self.client
5046 .log_message(MessageType::INFO, "got textDocument/documentSymbol request")
5047 .await;
5048 let uri = params.text_document.uri;
5049 let file_path = match uri.to_file_path() {
5050 Ok(path) => path,
5051 Err(_) => {
5052 self.client
5053 .log_message(MessageType::ERROR, "invalid file uri")
5054 .await;
5055 return Ok(None);
5056 }
5057 };
5058
5059 let source = {
5061 let cache = self.text_cache.read().await;
5062 cache
5063 .get(&uri.to_string())
5064 .map(|(_, content)| content.clone())
5065 };
5066 let source = match source {
5067 Some(s) => s,
5068 None => match std::fs::read_to_string(&file_path) {
5069 Ok(s) => s,
5070 Err(_) => return Ok(None),
5071 },
5072 };
5073
5074 let symbols = symbols::extract_document_symbols(&source);
5075 self.client
5076 .log_message(
5077 MessageType::INFO,
5078 format!("found {} document symbols", symbols.len()),
5079 )
5080 .await;
5081 Ok(Some(DocumentSymbolResponse::Nested(symbols)))
5082 }
5083
5084 async fn document_highlight(
5085 &self,
5086 params: DocumentHighlightParams,
5087 ) -> tower_lsp::jsonrpc::Result<Option<Vec<DocumentHighlight>>> {
5088 self.client
5089 .log_message(
5090 MessageType::INFO,
5091 "got textDocument/documentHighlight request",
5092 )
5093 .await;
5094
5095 let uri = params.text_document_position_params.text_document.uri;
5096 let position = params.text_document_position_params.position;
5097
5098 let source = {
5099 let cache = self.text_cache.read().await;
5100 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
5101 };
5102
5103 let source = match source {
5104 Some(s) => s,
5105 None => {
5106 let file_path = match uri.to_file_path() {
5107 Ok(p) => p,
5108 Err(_) => return Ok(None),
5109 };
5110 match std::fs::read_to_string(&file_path) {
5111 Ok(s) => s,
5112 Err(_) => return Ok(None),
5113 }
5114 }
5115 };
5116
5117 let highlights = highlight::document_highlights(&source, position);
5118 self.client
5119 .log_message(
5120 MessageType::INFO,
5121 format!("found {} document highlights", highlights.len()),
5122 )
5123 .await;
5124 Ok(Some(highlights))
5125 }
5126
5127 async fn hover(&self, params: HoverParams) -> tower_lsp::jsonrpc::Result<Option<Hover>> {
5128 self.client
5129 .log_message(MessageType::INFO, "got textDocument/hover request")
5130 .await;
5131
5132 let uri = params.text_document_position_params.text_document.uri;
5133 let position = params.text_document_position_params.position;
5134
5135 let file_path = match uri.to_file_path() {
5136 Ok(path) => path,
5137 Err(_) => {
5138 self.client
5139 .log_message(MessageType::ERROR, "invalid file uri")
5140 .await;
5141 return Ok(None);
5142 }
5143 };
5144
5145 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
5146 Some(bytes) => bytes,
5147 None => return Ok(None),
5148 };
5149
5150 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
5151 let cached_build = match cached_build {
5152 Some(cb) => cb,
5153 None => return Ok(None),
5154 };
5155
5156 let compiled_text: Option<Arc<str>> = self
5159 .completion_source_text
5160 .read()
5161 .await
5162 .get(&uri.to_string())
5163 .filter(|(src_cache, _)| Arc::ptr_eq(src_cache, &cached_build.completion_cache))
5164 .map(|(_, text)| text.clone());
5165
5166 let result = hover::hover_info(
5167 &cached_build,
5168 &uri,
5169 position,
5170 &source_bytes,
5171 compiled_text.as_deref(),
5172 );
5173
5174 if result.is_some() {
5175 self.client
5176 .log_message(MessageType::INFO, "hover info found")
5177 .await;
5178 } else {
5179 self.client
5180 .log_message(MessageType::INFO, "no hover info found")
5181 .await;
5182 }
5183
5184 Ok(result)
5185 }
5186
5187 async fn signature_help(
5188 &self,
5189 params: SignatureHelpParams,
5190 ) -> tower_lsp::jsonrpc::Result<Option<SignatureHelp>> {
5191 self.client
5192 .log_message(MessageType::INFO, "got textDocument/signatureHelp request")
5193 .await;
5194
5195 let uri = params.text_document_position_params.text_document.uri;
5196 let position = params.text_document_position_params.position;
5197
5198 let file_path = match uri.to_file_path() {
5199 Ok(path) => path,
5200 Err(_) => {
5201 self.client
5202 .log_message(MessageType::ERROR, "invalid file uri")
5203 .await;
5204 return Ok(None);
5205 }
5206 };
5207
5208 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
5209 Some(bytes) => bytes,
5210 None => return Ok(None),
5211 };
5212
5213 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
5214 let cached_build = match cached_build {
5215 Some(cb) => cb,
5216 None => return Ok(None),
5217 };
5218
5219 let result = hover::signature_help(&cached_build, &source_bytes, position);
5220
5221 Ok(result)
5222 }
5223
5224 async fn document_link(
5225 &self,
5226 params: DocumentLinkParams,
5227 ) -> tower_lsp::jsonrpc::Result<Option<Vec<DocumentLink>>> {
5228 self.client
5229 .log_message(MessageType::INFO, "got textDocument/documentLink request")
5230 .await;
5231
5232 let uri = params.text_document.uri;
5233 let file_path = match uri.to_file_path() {
5234 Ok(path) => path,
5235 Err(_) => {
5236 self.client
5237 .log_message(MessageType::ERROR, "invalid file uri")
5238 .await;
5239 return Ok(None);
5240 }
5241 };
5242
5243 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
5244 Some(bytes) => bytes,
5245 None => return Ok(None),
5246 };
5247
5248 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
5249 let cached_build = match cached_build {
5250 Some(cb) => cb,
5251 None => return Ok(None),
5252 };
5253
5254 let result = links::document_links(&cached_build, &uri, &source_bytes);
5255 self.client
5256 .log_message(
5257 MessageType::INFO,
5258 format!("found {} document links", result.len()),
5259 )
5260 .await;
5261 Ok(Some(result))
5262 }
5263
5264 async fn semantic_tokens_full(
5265 &self,
5266 params: SemanticTokensParams,
5267 ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensResult>> {
5268 self.client
5269 .log_message(
5270 MessageType::INFO,
5271 "got textDocument/semanticTokens/full request",
5272 )
5273 .await;
5274
5275 let uri = params.text_document.uri;
5276 let source = {
5277 let cache = self.text_cache.read().await;
5278 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
5279 };
5280
5281 let source = match source {
5282 Some(s) => s,
5283 None => {
5284 let file_path = match uri.to_file_path() {
5286 Ok(p) => p,
5287 Err(_) => return Ok(None),
5288 };
5289 match std::fs::read_to_string(&file_path) {
5290 Ok(s) => s,
5291 Err(_) => return Ok(None),
5292 }
5293 }
5294 };
5295
5296 let mut tokens = semantic_tokens::semantic_tokens_full(&source);
5297
5298 let id = self.semantic_token_id.fetch_add(1, Ordering::Relaxed);
5300 let result_id = id.to_string();
5301 tokens.result_id = Some(result_id.clone());
5302
5303 {
5304 let mut cache = self.semantic_token_cache.write().await;
5305 cache.insert(uri.to_string().into(), (result_id, tokens.data.clone()));
5306 }
5307
5308 Ok(Some(SemanticTokensResult::Tokens(tokens)))
5309 }
5310
5311 async fn semantic_tokens_range(
5312 &self,
5313 params: SemanticTokensRangeParams,
5314 ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensRangeResult>> {
5315 self.client
5316 .log_message(
5317 MessageType::INFO,
5318 "got textDocument/semanticTokens/range request",
5319 )
5320 .await;
5321
5322 let uri = params.text_document.uri;
5323 let range = params.range;
5324 let source = {
5325 let cache = self.text_cache.read().await;
5326 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
5327 };
5328
5329 let source = match source {
5330 Some(s) => s,
5331 None => {
5332 let file_path = match uri.to_file_path() {
5333 Ok(p) => p,
5334 Err(_) => return Ok(None),
5335 };
5336 match std::fs::read_to_string(&file_path) {
5337 Ok(s) => s,
5338 Err(_) => return Ok(None),
5339 }
5340 }
5341 };
5342
5343 let tokens =
5344 semantic_tokens::semantic_tokens_range(&source, range.start.line, range.end.line);
5345
5346 Ok(Some(SemanticTokensRangeResult::Tokens(tokens)))
5347 }
5348
5349 async fn semantic_tokens_full_delta(
5350 &self,
5351 params: SemanticTokensDeltaParams,
5352 ) -> tower_lsp::jsonrpc::Result<Option<SemanticTokensFullDeltaResult>> {
5353 self.client
5354 .log_message(
5355 MessageType::INFO,
5356 "got textDocument/semanticTokens/full/delta request",
5357 )
5358 .await;
5359
5360 let uri = params.text_document.uri;
5361 let previous_result_id = params.previous_result_id;
5362
5363 let source = {
5364 let cache = self.text_cache.read().await;
5365 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
5366 };
5367
5368 let source = match source {
5369 Some(s) => s,
5370 None => {
5371 let file_path = match uri.to_file_path() {
5372 Ok(p) => p,
5373 Err(_) => return Ok(None),
5374 };
5375 match std::fs::read_to_string(&file_path) {
5376 Ok(s) => s,
5377 Err(_) => return Ok(None),
5378 }
5379 }
5380 };
5381
5382 let mut new_tokens = semantic_tokens::semantic_tokens_full(&source);
5383
5384 let id = self.semantic_token_id.fetch_add(1, Ordering::Relaxed);
5386 let new_result_id = id.to_string();
5387 new_tokens.result_id = Some(new_result_id.clone());
5388
5389 let uri_str = uri.to_string();
5390
5391 let old_tokens = {
5393 let cache = self.semantic_token_cache.read().await;
5394 cache
5395 .get(&uri_str)
5396 .filter(|(rid, _)| *rid == previous_result_id)
5397 .map(|(_, tokens)| tokens.clone())
5398 };
5399
5400 {
5402 let mut cache = self.semantic_token_cache.write().await;
5403 cache.insert(
5404 uri_str.into(),
5405 (new_result_id.clone(), new_tokens.data.clone()),
5406 );
5407 }
5408
5409 match old_tokens {
5410 Some(old) => {
5411 let edits = semantic_tokens::compute_delta(&old, &new_tokens.data);
5413 Ok(Some(SemanticTokensFullDeltaResult::TokensDelta(
5414 SemanticTokensDelta {
5415 result_id: Some(new_result_id),
5416 edits,
5417 },
5418 )))
5419 }
5420 None => {
5421 Ok(Some(SemanticTokensFullDeltaResult::Tokens(new_tokens)))
5423 }
5424 }
5425 }
5426
5427 async fn folding_range(
5428 &self,
5429 params: FoldingRangeParams,
5430 ) -> tower_lsp::jsonrpc::Result<Option<Vec<FoldingRange>>> {
5431 self.client
5432 .log_message(MessageType::INFO, "got textDocument/foldingRange request")
5433 .await;
5434
5435 let uri = params.text_document.uri;
5436
5437 let source = {
5438 let cache = self.text_cache.read().await;
5439 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
5440 };
5441
5442 let source = match source {
5443 Some(s) => s,
5444 None => {
5445 let file_path = match uri.to_file_path() {
5446 Ok(p) => p,
5447 Err(_) => return Ok(None),
5448 };
5449 match std::fs::read_to_string(&file_path) {
5450 Ok(s) => s,
5451 Err(_) => return Ok(None),
5452 }
5453 }
5454 };
5455
5456 let ranges = folding::folding_ranges(&source);
5457 self.client
5458 .log_message(
5459 MessageType::INFO,
5460 format!("found {} folding ranges", ranges.len()),
5461 )
5462 .await;
5463 Ok(Some(ranges))
5464 }
5465
5466 async fn selection_range(
5467 &self,
5468 params: SelectionRangeParams,
5469 ) -> tower_lsp::jsonrpc::Result<Option<Vec<SelectionRange>>> {
5470 self.client
5471 .log_message(MessageType::INFO, "got textDocument/selectionRange request")
5472 .await;
5473
5474 let uri = params.text_document.uri;
5475
5476 let source = {
5477 let cache = self.text_cache.read().await;
5478 cache.get(&uri.to_string()).map(|(_, s)| s.clone())
5479 };
5480
5481 let source = match source {
5482 Some(s) => s,
5483 None => {
5484 let file_path = match uri.to_file_path() {
5485 Ok(p) => p,
5486 Err(_) => return Ok(None),
5487 };
5488 match std::fs::read_to_string(&file_path) {
5489 Ok(s) => s,
5490 Err(_) => return Ok(None),
5491 }
5492 }
5493 };
5494
5495 let ranges = selection::selection_ranges(&source, ¶ms.positions);
5496 self.client
5497 .log_message(
5498 MessageType::INFO,
5499 format!("found {} selection ranges", ranges.len()),
5500 )
5501 .await;
5502 Ok(Some(ranges))
5503 }
5504
5505 async fn inlay_hint(
5506 &self,
5507 params: InlayHintParams,
5508 ) -> tower_lsp::jsonrpc::Result<Option<Vec<InlayHint>>> {
5509 self.client
5510 .log_message(MessageType::INFO, "got textDocument/inlayHint request")
5511 .await;
5512
5513 let uri = params.text_document.uri;
5514 let range = params.range;
5515
5516 let file_path = match uri.to_file_path() {
5517 Ok(path) => path,
5518 Err(_) => {
5519 self.client
5520 .log_message(MessageType::ERROR, "invalid file uri")
5521 .await;
5522 return Ok(None);
5523 }
5524 };
5525
5526 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
5527 Some(bytes) => bytes,
5528 None => return Ok(None),
5529 };
5530
5531 let cached_build = self.get_or_fetch_build(&uri, &file_path, false).await;
5532 let cached_build = match cached_build {
5533 Some(cb) => cb,
5534 None => return Ok(None),
5535 };
5536
5537 let mut hints = inlay_hints::inlay_hints(&cached_build, &uri, range, &source_bytes);
5538
5539 let settings = self.settings.read().await;
5541 if !settings.inlay_hints.parameters {
5542 hints.retain(|h| h.kind != Some(InlayHintKind::PARAMETER));
5543 }
5544 self.client
5545 .log_message(
5546 MessageType::INFO,
5547 format!("found {} inlay hints", hints.len()),
5548 )
5549 .await;
5550
5551 Ok(Some(hints))
5557 }
5558
5559 async fn code_action(
5560 &self,
5561 params: CodeActionParams,
5562 ) -> tower_lsp::jsonrpc::Result<Option<CodeActionResponse>> {
5563 use crate::code_actions::FixKind;
5564
5565 let uri = ¶ms.text_document.uri;
5566
5567 let source: Option<String> = if let Ok(path) = uri.to_file_path() {
5569 self.get_source_bytes(uri, &path)
5570 .await
5571 .map(|b| String::from_utf8_lossy(&b).into_owned())
5572 } else {
5573 None
5574 };
5575
5576 let db = &self.code_action_db;
5577 let mut actions: Vec<CodeActionOrCommand> = Vec::new();
5578
5579 for diag in ¶ms.context.diagnostics {
5580 if let Some(NumberOrString::String(s)) = &diag.code
5582 && s == "unused-import"
5583 {
5584 if let Some(edit) = source.as_deref().and_then(|src| {
5585 goto::code_action_edit(
5586 src,
5587 diag.range,
5588 goto::CodeActionKind::DeleteNodeByKind {
5589 node_kind: "import_directive",
5590 },
5591 )
5592 }) {
5593 let mut changes = HashMap::new();
5594 changes.insert(uri.clone(), vec![edit]);
5595 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
5596 title: "Remove unused import".to_string(),
5597 kind: Some(CodeActionKind::QUICKFIX),
5598 diagnostics: Some(vec![diag.clone()]),
5599 edit: Some(WorkspaceEdit {
5600 changes: Some(changes),
5601 ..Default::default()
5602 }),
5603 is_preferred: Some(true),
5604 ..Default::default()
5605 }));
5606 }
5607 continue;
5608 }
5609
5610 let code: ErrorCode = match &diag.code {
5612 Some(NumberOrString::String(s)) => match s.parse() {
5613 Ok(n) => ErrorCode(n),
5614 Err(_) => continue,
5615 },
5616 _ => continue,
5617 };
5618
5619 if let Some(def) = db.get(&code) {
5621 let edit_opt: Option<TextEdit> = match &def.fix {
5623 FixKind::Insert { text, anchor: _ } => {
5624 goto::code_action_edit(
5626 source.as_deref().unwrap_or(""),
5627 diag.range,
5628 goto::CodeActionKind::InsertAtFileStart { text },
5629 )
5630 }
5631
5632 FixKind::ReplaceToken {
5633 replacement,
5634 walk_to,
5635 } => source.as_deref().and_then(|src| {
5636 goto::code_action_edit(
5637 src,
5638 diag.range,
5639 goto::CodeActionKind::ReplaceToken {
5640 replacement,
5641 walk_to: walk_to.as_deref(),
5642 },
5643 )
5644 }),
5645
5646 FixKind::DeleteToken => source.as_deref().and_then(|src| {
5647 goto::code_action_edit(src, diag.range, goto::CodeActionKind::DeleteToken)
5648 }),
5649
5650 FixKind::DeleteNode { node_kind } => {
5651 if node_kind == "variable_declaration_statement" {
5653 source.as_deref().and_then(|src| {
5654 goto::code_action_edit(
5655 src,
5656 diag.range,
5657 goto::CodeActionKind::DeleteLocalVar,
5658 )
5659 })
5660 } else {
5661 None
5662 }
5663 }
5664
5665 FixKind::DeleteChildNode {
5666 walk_to,
5667 child_kinds,
5668 } => {
5669 let ck: Vec<&str> = child_kinds.iter().map(|s| s.as_str()).collect();
5670 source.as_deref().and_then(|src| {
5671 goto::code_action_edit(
5672 src,
5673 diag.range,
5674 goto::CodeActionKind::DeleteChildNode {
5675 walk_to,
5676 child_kinds: &ck,
5677 },
5678 )
5679 })
5680 }
5681
5682 FixKind::ReplaceChildNode {
5683 walk_to,
5684 child_kind,
5685 replacement,
5686 } => source.as_deref().and_then(|src| {
5687 goto::code_action_edit(
5688 src,
5689 diag.range,
5690 goto::CodeActionKind::ReplaceChildNode {
5691 walk_to,
5692 child_kind,
5693 replacement,
5694 },
5695 )
5696 }),
5697
5698 FixKind::InsertBeforeNode {
5699 walk_to,
5700 before_child,
5701 text,
5702 } => {
5703 let bc: Vec<&str> = before_child.iter().map(|s| s.as_str()).collect();
5704 source.as_deref().and_then(|src| {
5705 goto::code_action_edit(
5706 src,
5707 diag.range,
5708 goto::CodeActionKind::InsertBeforeNode {
5709 walk_to,
5710 before_child: &bc,
5711 text,
5712 },
5713 )
5714 })
5715 }
5716
5717 FixKind::Custom => None,
5719 };
5720
5721 if let Some(edit) = edit_opt {
5722 let mut changes = HashMap::new();
5723 changes.insert(uri.clone(), vec![edit]);
5724 actions.push(CodeActionOrCommand::CodeAction(CodeAction {
5725 title: def.title.clone(),
5726 kind: Some(CodeActionKind::QUICKFIX),
5727 diagnostics: Some(vec![diag.clone()]),
5728 edit: Some(WorkspaceEdit {
5729 changes: Some(changes),
5730 ..Default::default()
5731 }),
5732 is_preferred: Some(true),
5733 ..Default::default()
5734 }));
5735 continue; }
5737
5738 if !matches!(def.fix, FixKind::Custom) {
5741 continue;
5742 }
5743 }
5744
5745 #[allow(clippy::match_single_binding)]
5749 match code {
5750 _ => {}
5755 }
5756 }
5757
5758 Ok(Some(actions))
5759 }
5760
5761 async fn will_rename_files(
5762 &self,
5763 params: RenameFilesParams,
5764 ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
5765 self.client
5766 .log_message(
5767 MessageType::INFO,
5768 format!("workspace/willRenameFiles: {} file(s)", params.files.len()),
5769 )
5770 .await;
5771 if !self
5772 .settings
5773 .read()
5774 .await
5775 .file_operations
5776 .update_imports_on_rename
5777 {
5778 self.client
5779 .log_message(
5780 MessageType::INFO,
5781 "willRenameFiles: updateImportsOnRename disabled",
5782 )
5783 .await;
5784 return Ok(None);
5785 }
5786
5787 let config = self.foundry_config.read().await.clone();
5789 let project_root = config.root.clone();
5790 let source_files: Vec<String> = tokio::task::spawn_blocking(move || {
5791 crate::solc::discover_source_files(&config)
5792 .into_iter()
5793 .filter_map(|p| p.to_str().map(String::from))
5794 .collect()
5795 })
5796 .await
5797 .unwrap_or_default();
5798
5799 if source_files.is_empty() {
5800 self.client
5801 .log_message(
5802 MessageType::WARNING,
5803 "willRenameFiles: no source files found",
5804 )
5805 .await;
5806 return Ok(None);
5807 }
5808
5809 let raw_renames: Vec<(std::path::PathBuf, std::path::PathBuf)> = params
5811 .files
5812 .iter()
5813 .filter_map(|fr| {
5814 let old_uri = Url::parse(&fr.old_uri).ok()?;
5815 let new_uri = Url::parse(&fr.new_uri).ok()?;
5816 let old_path = old_uri.to_file_path().ok()?;
5817 let new_path = new_uri.to_file_path().ok()?;
5818 Some((old_path, new_path))
5819 })
5820 .collect();
5821
5822 let renames = file_operations::expand_folder_renames(&raw_renames, &source_files);
5823
5824 if renames.is_empty() {
5825 return Ok(None);
5826 }
5827
5828 self.client
5829 .log_message(
5830 MessageType::INFO,
5831 format!(
5832 "willRenameFiles: {} rename(s) after folder expansion",
5833 renames.len()
5834 ),
5835 )
5836 .await;
5837
5838 let files_to_read: Vec<(String, String)> = {
5841 let tc = self.text_cache.read().await;
5842 source_files
5843 .iter()
5844 .filter_map(|fs_path| {
5845 let uri = Url::from_file_path(fs_path).ok()?;
5846 let uri_str = uri.to_string();
5847 if tc.contains_key(&uri_str) {
5848 None
5849 } else {
5850 Some((uri_str, fs_path.clone()))
5851 }
5852 })
5853 .collect()
5854 };
5855
5856 if !files_to_read.is_empty() {
5857 let loaded: Vec<(String, String)> = tokio::task::spawn_blocking(move || {
5858 files_to_read
5859 .into_iter()
5860 .filter_map(|(uri_str, fs_path)| {
5861 let content = std::fs::read_to_string(&fs_path).ok()?;
5862 Some((uri_str, content))
5863 })
5864 .collect()
5865 })
5866 .await
5867 .unwrap_or_default();
5868
5869 let mut tc = self.text_cache.write().await;
5870 for (uri_str, content) in loaded {
5871 tc.entry(uri_str.into()).or_insert((0, content));
5872 }
5873 }
5874
5875 let text_cache = self.text_cache.clone();
5880 let result = {
5881 let tc = text_cache.read().await;
5882 let get_source_bytes = |fs_path: &str| -> Option<Vec<u8>> {
5883 let uri = Url::from_file_path(fs_path).ok()?;
5884 let (_, content) = tc.get(&uri.to_string())?;
5885 Some(content.as_bytes().to_vec())
5886 };
5887
5888 file_operations::rename_imports(
5889 &source_files,
5890 &renames,
5891 &project_root,
5892 &get_source_bytes,
5893 )
5894 };
5895
5896 let stats = &result.stats;
5898 if stats.read_failures > 0 || stats.pathdiff_failures > 0 || stats.duplicate_renames > 0 {
5899 self.client
5900 .log_message(
5901 MessageType::WARNING,
5902 format!(
5903 "willRenameFiles stats: read_failures={}, pathdiff_failures={}, \
5904 duplicate_renames={}, no_parent={}, no_op_skips={}, dedup_skips={}",
5905 stats.read_failures,
5906 stats.pathdiff_failures,
5907 stats.duplicate_renames,
5908 stats.no_parent,
5909 stats.no_op_skips,
5910 stats.dedup_skips,
5911 ),
5912 )
5913 .await;
5914 }
5915
5916 let all_edits = result.edits;
5917
5918 if all_edits.is_empty() {
5919 self.client
5920 .log_message(MessageType::INFO, "willRenameFiles: no import edits needed")
5921 .await;
5922 return Ok(None);
5923 }
5924
5925 {
5927 let mut tc = self.text_cache.write().await;
5928 let patched = file_operations::apply_edits_to_cache(&all_edits, &mut tc);
5929 self.client
5930 .log_message(
5931 MessageType::INFO,
5932 format!("willRenameFiles: patched {} cached file(s)", patched),
5933 )
5934 .await;
5935 }
5936
5937 let total_edits: usize = all_edits.values().map(|v| v.len()).sum();
5938 self.client
5939 .log_message(
5940 MessageType::INFO,
5941 format!(
5942 "willRenameFiles: {} edit(s) across {} file(s)",
5943 total_edits,
5944 all_edits.len()
5945 ),
5946 )
5947 .await;
5948
5949 Ok(Some(WorkspaceEdit {
5950 changes: Some(all_edits),
5951 document_changes: None,
5952 change_annotations: None,
5953 }))
5954 }
5955
5956 async fn did_rename_files(&self, params: RenameFilesParams) {
5957 self.client
5958 .log_message(
5959 MessageType::INFO,
5960 format!("workspace/didRenameFiles: {} file(s)", params.files.len()),
5961 )
5962 .await;
5963 self.project_cache_dirty.store(true, Ordering::Release);
5964 {
5965 let mut changed = self.project_cache_changed_files.write().await;
5966 for file in ¶ms.files {
5967 if let Ok(old_uri) = Url::parse(&file.old_uri)
5968 && let Ok(old_path) = old_uri.to_file_path()
5969 {
5970 changed.insert(old_path.to_string_lossy().to_string());
5971 }
5972 if let Ok(new_uri) = Url::parse(&file.new_uri)
5973 && let Ok(new_path) = new_uri.to_file_path()
5974 {
5975 changed.insert(new_path.to_string_lossy().to_string());
5976 }
5977 }
5978 }
5979
5980 let raw_uri_pairs: Vec<(Url, Url)> = params
5982 .files
5983 .iter()
5984 .filter_map(|fr| {
5985 let old_uri = Url::parse(&fr.old_uri).ok()?;
5986 let new_uri = Url::parse(&fr.new_uri).ok()?;
5987 Some((old_uri, new_uri))
5988 })
5989 .collect();
5990
5991 let file_renames = {
5992 let tc = self.text_cache.read().await;
5993 let cache_paths: Vec<std::path::PathBuf> = tc
5994 .keys()
5995 .filter_map(|k| Url::parse(k).ok())
5996 .filter_map(|u| u.to_file_path().ok())
5997 .collect();
5998 drop(tc);
5999
6000 let cfg = self.foundry_config.read().await.clone();
6003 let discovered_paths =
6004 tokio::task::spawn_blocking(move || crate::solc::discover_source_files(&cfg))
6005 .await
6006 .unwrap_or_default();
6007
6008 let mut all_paths: HashSet<std::path::PathBuf> = discovered_paths.into_iter().collect();
6009 all_paths.extend(cache_paths);
6010 let all_paths: Vec<std::path::PathBuf> = all_paths.into_iter().collect();
6011
6012 file_operations::expand_folder_renames_from_paths(&raw_uri_pairs, &all_paths)
6013 };
6014
6015 self.client
6016 .log_message(
6017 MessageType::INFO,
6018 format!(
6019 "didRenameFiles: migrating {} cache entry/entries",
6020 file_renames.len()
6021 ),
6022 )
6023 .await;
6024
6025 {
6029 let mut tc = self.text_cache.write().await;
6030 for (old_key, new_key) in &file_renames {
6031 if let Some(entry) = tc.remove(old_key) {
6032 tc.insert(new_key.clone().into(), entry);
6033 }
6034 }
6035 }
6036 {
6037 let mut ac = self.ast_cache.write().await;
6038 for (old_key, _) in &file_renames {
6039 ac.remove(old_key);
6040 }
6041 }
6042 {
6043 let mut cc = self.completion_cache.write().await;
6044 for (old_key, _) in &file_renames {
6045 cc.remove(old_key);
6046 }
6047 }
6048 {
6049 let mut cst = self.completion_source_text.write().await;
6050 for (old_key, _) in &file_renames {
6051 cst.remove(old_key);
6052 }
6053 }
6054 {
6055 let mut sc = self.semantic_token_cache.write().await;
6056 for (old_key, _) in &file_renames {
6057 sc.remove(old_key);
6058 }
6059 }
6060 {
6061 let mut pending = self.pending_create_scaffold.write().await;
6062 for (old_key, _) in &file_renames {
6063 pending.remove(old_key);
6064 }
6065 }
6066
6067 {
6069 let affected_paths: Vec<std::path::PathBuf> = file_renames
6070 .iter()
6071 .flat_map(|(old_key, new_key)| {
6072 let mut paths = Vec::new();
6073 if let Ok(u) = Url::parse(old_key)
6074 && let Ok(p) = u.to_file_path()
6075 {
6076 paths.push(p);
6077 }
6078 if let Ok(u) = Url::parse(new_key)
6079 && let Ok(p) = u.to_file_path()
6080 {
6081 paths.push(p);
6082 }
6083 paths
6084 })
6085 .collect();
6086 self.invalidate_lib_sub_caches_if_affected(&affected_paths)
6087 .await;
6088 }
6089
6090 let root_key = self.project_cache_key().await;
6094
6095 let foundry_config = self.foundry_config.read().await.clone();
6096 let ast_cache = self.ast_cache.clone();
6097 let client = self.client.clone();
6098 let path_interner = self.path_interner.clone();
6099 let text_cache_snapshot = self.text_cache.read().await.clone();
6103
6104 tokio::spawn(async move {
6105 let Some(cache_key) = root_key else {
6106 return;
6107 };
6108 match crate::solc::solc_project_index(
6109 &foundry_config,
6110 Some(&client),
6111 Some(&text_cache_snapshot),
6112 )
6113 .await
6114 {
6115 Ok(ast_data) => {
6116 let cached_build = Arc::new(crate::goto::CachedBuild::new(
6117 ast_data,
6118 0,
6119 Some(&mut *path_interner.write().await),
6120 ));
6121 let source_count = cached_build.nodes.len();
6122 ast_cache
6123 .write()
6124 .await
6125 .insert(cache_key.into(), cached_build);
6126 client
6127 .log_message(
6128 MessageType::INFO,
6129 format!("didRenameFiles: re-indexed {} source files", source_count),
6130 )
6131 .await;
6132 }
6133 Err(e) => {
6134 client
6135 .log_message(
6136 MessageType::WARNING,
6137 format!("didRenameFiles: re-index failed: {e}"),
6138 )
6139 .await;
6140 }
6141 }
6142 });
6143 }
6144
6145 async fn will_delete_files(
6146 &self,
6147 params: DeleteFilesParams,
6148 ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
6149 self.client
6150 .log_message(
6151 MessageType::INFO,
6152 format!("workspace/willDeleteFiles: {} file(s)", params.files.len()),
6153 )
6154 .await;
6155 if !update_imports_on_delete_enabled(&*self.settings.read().await) {
6156 self.client
6157 .log_message(
6158 MessageType::INFO,
6159 "willDeleteFiles: updateImportsOnDelete disabled",
6160 )
6161 .await;
6162 return Ok(None);
6163 }
6164
6165 let config = self.foundry_config.read().await.clone();
6166 let project_root = config.root.clone();
6167 let source_files: Vec<String> = tokio::task::spawn_blocking(move || {
6168 crate::solc::discover_source_files(&config)
6169 .into_iter()
6170 .filter_map(|p| p.to_str().map(String::from))
6171 .collect()
6172 })
6173 .await
6174 .unwrap_or_default();
6175
6176 if source_files.is_empty() {
6177 self.client
6178 .log_message(
6179 MessageType::WARNING,
6180 "willDeleteFiles: no source files found",
6181 )
6182 .await;
6183 return Ok(None);
6184 }
6185
6186 let raw_deletes: Vec<std::path::PathBuf> = params
6187 .files
6188 .iter()
6189 .filter_map(|fd| Url::parse(&fd.uri).ok())
6190 .filter_map(|u| u.to_file_path().ok())
6191 .collect();
6192
6193 let deletes = file_operations::expand_folder_deletes(&raw_deletes, &source_files);
6194 if deletes.is_empty() {
6195 return Ok(None);
6196 }
6197
6198 self.client
6199 .log_message(
6200 MessageType::INFO,
6201 format!(
6202 "willDeleteFiles: {} delete target(s) after folder expansion",
6203 deletes.len()
6204 ),
6205 )
6206 .await;
6207
6208 let files_to_read: Vec<(String, String)> = {
6209 let tc = self.text_cache.read().await;
6210 source_files
6211 .iter()
6212 .filter_map(|fs_path| {
6213 let uri = Url::from_file_path(fs_path).ok()?;
6214 let uri_str = uri.to_string();
6215 if tc.contains_key(&uri_str) {
6216 None
6217 } else {
6218 Some((uri_str, fs_path.clone()))
6219 }
6220 })
6221 .collect()
6222 };
6223
6224 if !files_to_read.is_empty() {
6225 let loaded: Vec<(String, String)> = tokio::task::spawn_blocking(move || {
6226 files_to_read
6227 .into_iter()
6228 .filter_map(|(uri_str, fs_path)| {
6229 let content = std::fs::read_to_string(&fs_path).ok()?;
6230 Some((uri_str, content))
6231 })
6232 .collect()
6233 })
6234 .await
6235 .unwrap_or_default();
6236
6237 let mut tc = self.text_cache.write().await;
6238 for (uri_str, content) in loaded {
6239 tc.entry(uri_str.into()).or_insert((0, content));
6240 }
6241 }
6242
6243 let result = {
6244 let tc = self.text_cache.read().await;
6245 let get_source_bytes = |fs_path: &str| -> Option<Vec<u8>> {
6246 let uri = Url::from_file_path(fs_path).ok()?;
6247 let (_, content) = tc.get(&uri.to_string())?;
6248 Some(content.as_bytes().to_vec())
6249 };
6250
6251 file_operations::delete_imports(
6252 &source_files,
6253 &deletes,
6254 &project_root,
6255 &get_source_bytes,
6256 )
6257 };
6258
6259 let stats = &result.stats;
6260 if stats.read_failures > 0
6261 || stats.statement_range_failures > 0
6262 || stats.duplicate_deletes > 0
6263 {
6264 self.client
6265 .log_message(
6266 MessageType::WARNING,
6267 format!(
6268 "willDeleteFiles stats: read_failures={}, statement_range_failures={}, \
6269 duplicate_deletes={}, no_parent={}, dedup_skips={}",
6270 stats.read_failures,
6271 stats.statement_range_failures,
6272 stats.duplicate_deletes,
6273 stats.no_parent,
6274 stats.dedup_skips,
6275 ),
6276 )
6277 .await;
6278 }
6279
6280 let all_edits = result.edits;
6281 if all_edits.is_empty() {
6282 self.client
6283 .log_message(
6284 MessageType::INFO,
6285 "willDeleteFiles: no import-removal edits needed",
6286 )
6287 .await;
6288 return Ok(None);
6289 }
6290
6291 {
6292 let mut tc = self.text_cache.write().await;
6293 let patched = file_operations::apply_edits_to_cache(&all_edits, &mut tc);
6294 self.client
6295 .log_message(
6296 MessageType::INFO,
6297 format!("willDeleteFiles: patched {} cached file(s)", patched),
6298 )
6299 .await;
6300 }
6301
6302 let total_edits: usize = all_edits.values().map(|v| v.len()).sum();
6303 self.client
6304 .log_message(
6305 MessageType::INFO,
6306 format!(
6307 "willDeleteFiles: {} edit(s) across {} file(s)",
6308 total_edits,
6309 all_edits.len()
6310 ),
6311 )
6312 .await;
6313
6314 Ok(Some(WorkspaceEdit {
6315 changes: Some(all_edits),
6316 document_changes: None,
6317 change_annotations: None,
6318 }))
6319 }
6320
6321 async fn did_delete_files(&self, params: DeleteFilesParams) {
6322 self.client
6323 .log_message(
6324 MessageType::INFO,
6325 format!("workspace/didDeleteFiles: {} file(s)", params.files.len()),
6326 )
6327 .await;
6328 self.project_cache_dirty.store(true, Ordering::Release);
6329 {
6330 let mut changed = self.project_cache_changed_files.write().await;
6331 for file in ¶ms.files {
6332 if let Ok(uri) = Url::parse(&file.uri)
6333 && let Ok(path) = uri.to_file_path()
6334 {
6335 changed.insert(path.to_string_lossy().to_string());
6336 }
6337 }
6338 }
6339
6340 let raw_delete_uris: Vec<Url> = params
6341 .files
6342 .iter()
6343 .filter_map(|fd| Url::parse(&fd.uri).ok())
6344 .collect();
6345
6346 let deleted_paths = {
6347 let tc = self.text_cache.read().await;
6348 let cache_paths: Vec<std::path::PathBuf> = tc
6349 .keys()
6350 .filter_map(|k| Url::parse(k).ok())
6351 .filter_map(|u| u.to_file_path().ok())
6352 .collect();
6353 drop(tc);
6354
6355 let cfg = self.foundry_config.read().await.clone();
6356 let discovered_paths =
6357 tokio::task::spawn_blocking(move || crate::solc::discover_source_files(&cfg))
6358 .await
6359 .unwrap_or_default();
6360
6361 let mut all_paths: HashSet<std::path::PathBuf> = discovered_paths.into_iter().collect();
6362 all_paths.extend(cache_paths);
6363 let all_paths: Vec<std::path::PathBuf> = all_paths.into_iter().collect();
6364
6365 file_operations::expand_folder_deletes_from_paths(&raw_delete_uris, &all_paths)
6366 };
6367
6368 let mut deleted_keys: HashSet<String> = HashSet::new();
6369 let mut deleted_uris: Vec<Url> = Vec::new();
6370 for path in deleted_paths {
6371 if let Ok(uri) = Url::from_file_path(&path) {
6372 deleted_keys.insert(uri.to_string());
6373 deleted_uris.push(uri);
6374 }
6375 }
6376 if deleted_keys.is_empty() {
6377 return;
6378 }
6379
6380 self.client
6381 .log_message(
6382 MessageType::INFO,
6383 format!(
6384 "didDeleteFiles: deleting {} cache/diagnostic entry(ies)",
6385 deleted_keys.len()
6386 ),
6387 )
6388 .await;
6389
6390 for uri in &deleted_uris {
6391 self.client
6392 .publish_diagnostics(uri.clone(), vec![], None)
6393 .await;
6394 }
6395
6396 let mut removed_text = 0usize;
6397 let mut removed_ast = 0usize;
6398 let mut removed_completion = 0usize;
6399 let mut removed_semantic = 0usize;
6400 let mut removed_pending_create = 0usize;
6401 {
6402 let mut tc = self.text_cache.write().await;
6403 for key in &deleted_keys {
6404 if tc.remove(key).is_some() {
6405 removed_text += 1;
6406 }
6407 }
6408 }
6409 {
6410 let mut ac = self.ast_cache.write().await;
6411 for key in &deleted_keys {
6412 if ac.remove(key).is_some() {
6413 removed_ast += 1;
6414 }
6415 }
6416 }
6417 {
6418 let mut cc = self.completion_cache.write().await;
6419 for key in &deleted_keys {
6420 if cc.remove(key).is_some() {
6421 removed_completion += 1;
6422 }
6423 }
6424 }
6425 {
6426 let mut cst = self.completion_source_text.write().await;
6427 for key in &deleted_keys {
6428 cst.remove(key);
6429 }
6430 }
6431 {
6432 let mut sc = self.semantic_token_cache.write().await;
6433 for key in &deleted_keys {
6434 if sc.remove(key).is_some() {
6435 removed_semantic += 1;
6436 }
6437 }
6438 }
6439 {
6440 let mut pending = self.pending_create_scaffold.write().await;
6441 for key in &deleted_keys {
6442 if pending.remove(key) {
6443 removed_pending_create += 1;
6444 }
6445 }
6446 }
6447 self.client
6448 .log_message(
6449 MessageType::INFO,
6450 format!(
6451 "didDeleteFiles: removed caches text={} ast={} completion={} semantic={} pendingCreate={}",
6452 removed_text,
6453 removed_ast,
6454 removed_completion,
6455 removed_semantic,
6456 removed_pending_create,
6457 ),
6458 )
6459 .await;
6460
6461 {
6463 let affected_paths: Vec<std::path::PathBuf> = deleted_keys
6464 .iter()
6465 .filter_map(|k| Url::parse(k).ok())
6466 .filter_map(|u| u.to_file_path().ok())
6467 .collect();
6468 self.invalidate_lib_sub_caches_if_affected(&affected_paths)
6469 .await;
6470 }
6471
6472 let root_key = self.project_cache_key().await;
6476
6477 let foundry_config = self.foundry_config.read().await.clone();
6478 let ast_cache = self.ast_cache.clone();
6479 let client = self.client.clone();
6480 let path_interner = self.path_interner.clone();
6481 let text_cache_snapshot = self.text_cache.read().await.clone();
6482
6483 tokio::spawn(async move {
6484 let Some(cache_key) = root_key else {
6485 return;
6486 };
6487 match crate::solc::solc_project_index(
6488 &foundry_config,
6489 Some(&client),
6490 Some(&text_cache_snapshot),
6491 )
6492 .await
6493 {
6494 Ok(ast_data) => {
6495 let cached_build = Arc::new(crate::goto::CachedBuild::new(
6496 ast_data,
6497 0,
6498 Some(&mut *path_interner.write().await),
6499 ));
6500 let source_count = cached_build.nodes.len();
6501 ast_cache
6502 .write()
6503 .await
6504 .insert(cache_key.into(), cached_build);
6505 client
6506 .log_message(
6507 MessageType::INFO,
6508 format!("didDeleteFiles: re-indexed {} source files", source_count),
6509 )
6510 .await;
6511 }
6512 Err(e) => {
6513 client
6514 .log_message(
6515 MessageType::WARNING,
6516 format!("didDeleteFiles: re-index failed: {e}"),
6517 )
6518 .await;
6519 }
6520 }
6521 });
6522 }
6523
6524 async fn will_create_files(
6525 &self,
6526 params: CreateFilesParams,
6527 ) -> tower_lsp::jsonrpc::Result<Option<WorkspaceEdit>> {
6528 self.client
6529 .log_message(
6530 MessageType::INFO,
6531 format!("workspace/willCreateFiles: {} file(s)", params.files.len()),
6532 )
6533 .await;
6534 if !self
6535 .settings
6536 .read()
6537 .await
6538 .file_operations
6539 .template_on_create
6540 {
6541 self.client
6542 .log_message(
6543 MessageType::INFO,
6544 "willCreateFiles: templateOnCreate disabled",
6545 )
6546 .await;
6547 return Ok(None);
6548 }
6549 self.client
6550 .log_message(
6551 MessageType::INFO,
6552 "willCreateFiles: skipping pre-create edits; scaffolding via didCreateFiles",
6553 )
6554 .await;
6555 Ok(None)
6556 }
6557
6558 async fn did_create_files(&self, params: CreateFilesParams) {
6559 self.client
6560 .log_message(
6561 MessageType::INFO,
6562 format!("workspace/didCreateFiles: {} file(s)", params.files.len()),
6563 )
6564 .await;
6565 self.project_cache_dirty.store(true, Ordering::Release);
6566 {
6567 let mut changed = self.project_cache_changed_files.write().await;
6568 for file in ¶ms.files {
6569 if let Ok(uri) = Url::parse(&file.uri)
6570 && let Ok(path) = uri.to_file_path()
6571 {
6572 changed.insert(path.to_string_lossy().to_string());
6573 }
6574 }
6575 }
6576 if !self
6577 .settings
6578 .read()
6579 .await
6580 .file_operations
6581 .template_on_create
6582 {
6583 self.client
6584 .log_message(
6585 MessageType::INFO,
6586 "didCreateFiles: templateOnCreate disabled",
6587 )
6588 .await;
6589 return;
6590 }
6591
6592 let config = self.foundry_config.read().await;
6593 let solc_version = config.solc_version.clone();
6594 drop(config);
6595
6596 let mut apply_edits: HashMap<Url, Vec<TextEdit>> = HashMap::new();
6601 let mut staged_content: HashMap<String, String> = HashMap::new();
6602 let mut created_uris: Vec<String> = Vec::new();
6603 {
6604 let tc = self.text_cache.read().await;
6605 for file_create in ¶ms.files {
6606 let uri = match Url::parse(&file_create.uri) {
6607 Ok(u) => u,
6608 Err(_) => continue,
6609 };
6610 let uri_str = uri.to_string();
6611
6612 let open_has_content = tc
6613 .get(&uri_str)
6614 .is_some_and(|(_, c)| c.chars().any(|ch| !ch.is_whitespace()));
6615 let path = match uri.to_file_path() {
6616 Ok(p) => p,
6617 Err(_) => continue,
6618 };
6619 let disk_has_content = std::fs::read_to_string(&path)
6620 .is_ok_and(|c| c.chars().any(|ch| !ch.is_whitespace()));
6621
6622 if open_has_content {
6625 self.client
6626 .log_message(
6627 MessageType::INFO,
6628 format!(
6629 "didCreateFiles: skip {} (open buffer already has content)",
6630 uri_str
6631 ),
6632 )
6633 .await;
6634 continue;
6635 }
6636
6637 if disk_has_content {
6639 self.client
6640 .log_message(
6641 MessageType::INFO,
6642 format!(
6643 "didCreateFiles: skip {} (disk file already has content)",
6644 uri_str
6645 ),
6646 )
6647 .await;
6648 continue;
6649 }
6650
6651 let content =
6652 match file_operations::generate_scaffold(&uri, solc_version.as_deref()) {
6653 Some(s) => s,
6654 None => continue,
6655 };
6656
6657 staged_content.insert(uri_str, content.clone());
6658 created_uris.push(uri.to_string());
6659
6660 apply_edits.entry(uri).or_default().push(TextEdit {
6661 range: Range {
6662 start: Position {
6663 line: 0,
6664 character: 0,
6665 },
6666 end: Position {
6667 line: 0,
6668 character: 0,
6669 },
6670 },
6671 new_text: content,
6672 });
6673 }
6674 }
6675
6676 if !apply_edits.is_empty() {
6677 {
6678 let mut pending = self.pending_create_scaffold.write().await;
6679 for uri in &created_uris {
6680 pending.insert(uri.clone().into());
6681 }
6682 }
6683
6684 let edit = WorkspaceEdit {
6685 changes: Some(apply_edits.clone()),
6686 document_changes: None,
6687 change_annotations: None,
6688 };
6689 self.client
6690 .log_message(
6691 MessageType::INFO,
6692 format!(
6693 "didCreateFiles: scaffolding {} empty file(s) via workspace/applyEdit",
6694 apply_edits.len()
6695 ),
6696 )
6697 .await;
6698 let apply_result = self.client.apply_edit(edit).await;
6699 let applied = apply_result.as_ref().is_ok_and(|r| r.applied);
6700
6701 if applied {
6702 let mut tc = self.text_cache.write().await;
6703 for (uri_str, content) in staged_content {
6704 tc.insert(uri_str.into(), (0, content));
6705 }
6706 } else if let Ok(resp) = &apply_result {
6707 self.client
6708 .log_message(
6709 MessageType::WARNING,
6710 format!(
6711 "didCreateFiles: applyEdit rejected (no disk fallback): {:?}",
6712 resp.failure_reason
6713 ),
6714 )
6715 .await;
6716 } else if let Err(e) = &apply_result {
6717 self.client
6718 .log_message(
6719 MessageType::WARNING,
6720 format!("didCreateFiles: applyEdit failed (no disk fallback): {e}"),
6721 )
6722 .await;
6723 }
6724 }
6725
6726 for file_create in ¶ms.files {
6730 let Ok(uri) = Url::parse(&file_create.uri) else {
6731 continue;
6732 };
6733 let (version, content) = {
6734 let tc = self.text_cache.read().await;
6735 match tc.get(&uri.to_string()) {
6736 Some((v, c)) => (*v, c.clone()),
6737 None => continue,
6738 }
6739 };
6740 if !content.chars().any(|ch| !ch.is_whitespace()) {
6741 continue;
6742 }
6743 self.on_change(TextDocumentItem {
6744 uri,
6745 version,
6746 text: content,
6747 language_id: "solidity".to_string(),
6748 })
6749 .await;
6750 }
6751
6752 {
6754 let affected_paths: Vec<std::path::PathBuf> = params
6755 .files
6756 .iter()
6757 .filter_map(|f| Url::parse(&f.uri).ok())
6758 .filter_map(|u| u.to_file_path().ok())
6759 .collect();
6760 self.invalidate_lib_sub_caches_if_affected(&affected_paths)
6761 .await;
6762 }
6763
6764 let root_key = self.project_cache_key().await;
6768
6769 let foundry_config = self.foundry_config.read().await.clone();
6770 let ast_cache = self.ast_cache.clone();
6771 let client = self.client.clone();
6772 let path_interner = self.path_interner.clone();
6773 let text_cache_snapshot = self.text_cache.read().await.clone();
6774
6775 tokio::spawn(async move {
6776 let Some(cache_key) = root_key else {
6777 return;
6778 };
6779 match crate::solc::solc_project_index(
6780 &foundry_config,
6781 Some(&client),
6782 Some(&text_cache_snapshot),
6783 )
6784 .await
6785 {
6786 Ok(ast_data) => {
6787 let cached_build = Arc::new(crate::goto::CachedBuild::new(
6788 ast_data,
6789 0,
6790 Some(&mut *path_interner.write().await),
6791 ));
6792 let source_count = cached_build.nodes.len();
6793 ast_cache
6794 .write()
6795 .await
6796 .insert(cache_key.into(), cached_build);
6797 client
6798 .log_message(
6799 MessageType::INFO,
6800 format!("didCreateFiles: re-indexed {} source files", source_count),
6801 )
6802 .await;
6803 }
6804 Err(e) => {
6805 client
6806 .log_message(
6807 MessageType::WARNING,
6808 format!("didCreateFiles: re-index failed: {e}"),
6809 )
6810 .await;
6811 }
6812 }
6813 });
6814 }
6815
6816 async fn prepare_call_hierarchy(
6819 &self,
6820 params: CallHierarchyPrepareParams,
6821 ) -> tower_lsp::jsonrpc::Result<Option<Vec<CallHierarchyItem>>> {
6822 self.client
6823 .log_message(
6824 MessageType::INFO,
6825 "got textDocument/prepareCallHierarchy request",
6826 )
6827 .await;
6828
6829 let uri = params.text_document_position_params.text_document.uri;
6830 let position = params.text_document_position_params.position;
6831
6832 let file_path = match uri.to_file_path() {
6833 Ok(path) => path,
6834 Err(_) => {
6835 self.client
6836 .log_message(MessageType::ERROR, "invalid file uri")
6837 .await;
6838 return Ok(None);
6839 }
6840 };
6841
6842 let source_bytes = match self.get_source_bytes(&uri, &file_path).await {
6843 Some(bytes) => bytes,
6844 None => return Ok(None),
6845 };
6846
6847 let cached_build = match self.get_or_fetch_build(&uri, &file_path, true).await {
6848 Some(cb) => cb,
6849 None => return Ok(None),
6850 };
6851
6852 let path_str = match file_path.to_str() {
6853 Some(s) => s,
6854 None => return Ok(None),
6855 };
6856 let abs_path = match cached_build.path_to_abs.get(path_str) {
6857 Some(ap) => ap.clone(),
6858 None => {
6859 crate::types::AbsPath::new(path_str)
6861 }
6862 };
6863
6864 let byte_position = goto::pos_to_bytes(&source_bytes, position);
6865
6866 let callable_id = match crate::call_hierarchy::resolve_callable_at_position(
6868 &cached_build,
6869 abs_path.as_str(),
6870 byte_position,
6871 ) {
6872 Some(id) => id,
6873 None => {
6874 self.client
6875 .log_message(MessageType::INFO, "no callable found at cursor position")
6876 .await;
6877 return Ok(None);
6878 }
6879 };
6880
6881 let item = if let Some(decl) = cached_build.decl_index.get(&callable_id) {
6884 crate::call_hierarchy::decl_to_hierarchy_item(
6885 decl,
6886 callable_id,
6887 &cached_build.node_id_to_source_path,
6888 &cached_build.id_to_path_map,
6889 &cached_build.nodes,
6890 )
6891 } else if let Some(info) =
6892 crate::call_hierarchy::find_node_info(&cached_build.nodes, callable_id)
6893 {
6894 crate::call_hierarchy::node_info_to_hierarchy_item(
6895 callable_id,
6896 info,
6897 &cached_build.id_to_path_map,
6898 )
6899 } else {
6900 None
6901 };
6902
6903 match item {
6904 Some(it) => {
6905 self.client
6906 .log_message(
6907 MessageType::INFO,
6908 format!("prepared call hierarchy for: {}", it.name),
6909 )
6910 .await;
6911 Ok(Some(vec![it]))
6912 }
6913 None => {
6914 self.client
6915 .log_message(
6916 MessageType::INFO,
6917 "could not build CallHierarchyItem for callable",
6918 )
6919 .await;
6920 Ok(None)
6921 }
6922 }
6923 }
6924
6925 async fn incoming_calls(
6926 &self,
6927 params: CallHierarchyIncomingCallsParams,
6928 ) -> tower_lsp::jsonrpc::Result<Option<Vec<CallHierarchyIncomingCall>>> {
6929 self.client
6930 .log_message(MessageType::INFO, "got callHierarchy/incomingCalls request")
6931 .await;
6932
6933 let item = ¶ms.item;
6934
6935 let node_id = match item
6937 .data
6938 .as_ref()
6939 .and_then(|d| d.get("nodeId"))
6940 .and_then(|v| v.as_i64())
6941 {
6942 Some(id) => crate::types::NodeId(id),
6943 None => {
6944 self.client
6945 .log_message(
6946 MessageType::ERROR,
6947 "missing nodeId in CallHierarchyItem data",
6948 )
6949 .await;
6950 return Ok(None);
6951 }
6952 };
6953
6954 let file_path = match item.uri.to_file_path() {
6956 Ok(p) => p,
6957 Err(_) => return Ok(None),
6958 };
6959 let file_build = match self.get_or_fetch_build(&item.uri, &file_path, true).await {
6960 Some(cb) => cb,
6961 None => return Ok(None),
6962 };
6963
6964 let project_build = self.ensure_project_cached_build().await;
6966
6967 let mut builds: Vec<&goto::CachedBuild> = vec![&file_build];
6969 if let Some(ref pb) = project_build {
6970 builds.push(pb);
6971 }
6972 let sub_caches = self.sub_caches.read().await;
6973 for sc in sub_caches.iter() {
6974 builds.push(sc);
6975 }
6976
6977 let target_name = &item.name;
6984 let target_sel = &item.selection_range;
6985 let target_abs = item
6986 .uri
6987 .as_ref()
6988 .strip_prefix("file://")
6989 .unwrap_or(item.uri.as_ref());
6990
6991 let target_name_offset = {
6995 let source_bytes = std::fs::read(target_abs).unwrap_or_default();
6996 goto::pos_to_bytes(&source_bytes, target_sel.start)
6997 };
6998
6999 let mut resolved_incoming: Vec<(CallHierarchyItem, (u32, u32), Range)> = Vec::new();
7004
7005 for build in &builds {
7006 let mut build_target_ids = crate::call_hierarchy::resolve_target_in_build(
7010 build,
7011 node_id,
7012 target_abs,
7013 target_name,
7014 target_name_offset,
7015 );
7016
7017 let snapshot: Vec<crate::types::NodeId> = build_target_ids.clone();
7019 for id in &snapshot {
7020 if let Some(related) = build.base_function_implementation.get(id) {
7021 for &related_id in related {
7022 if !build_target_ids.contains(&related_id) {
7023 build_target_ids.push(related_id);
7024 }
7025 }
7026 }
7027 }
7028
7029 if build_target_ids.is_empty() {
7030 continue;
7031 }
7032
7033 let calls = crate::call_hierarchy::incoming_calls(&build.nodes, &build_target_ids);
7034 for (caller_id, call_src) in calls {
7035 let call_range = match crate::call_hierarchy::call_src_to_range(
7036 &call_src,
7037 &build.id_to_path_map,
7038 ) {
7039 Some(r) => r,
7040 None => continue,
7041 };
7042 let caller_item = if let Some(decl) = build.decl_index.get(&caller_id) {
7045 crate::call_hierarchy::decl_to_hierarchy_item(
7046 decl,
7047 caller_id,
7048 &build.node_id_to_source_path,
7049 &build.id_to_path_map,
7050 &build.nodes,
7051 )
7052 } else if let Some(info) =
7053 crate::call_hierarchy::find_node_info(&build.nodes, caller_id)
7054 {
7055 crate::call_hierarchy::node_info_to_hierarchy_item(
7056 caller_id,
7057 info,
7058 &build.id_to_path_map,
7059 )
7060 } else {
7061 None
7062 };
7063 let Some(caller_item) = caller_item else {
7064 continue;
7065 };
7066 let pos = (
7067 caller_item.selection_range.start.line,
7068 caller_item.selection_range.start.character,
7069 );
7070 resolved_incoming.push((caller_item, pos, call_range));
7071 }
7072 }
7073
7074 if resolved_incoming.is_empty() {
7075 self.client
7076 .log_message(MessageType::INFO, "no incoming calls found")
7077 .await;
7078 return Ok(Some(vec![]));
7079 }
7080
7081 let mut grouped: HashMap<(u32, u32), (CallHierarchyItem, Vec<Range>)> = HashMap::new();
7084 for (caller_item, pos, call_range) in resolved_incoming {
7085 let entry = grouped
7086 .entry(pos)
7087 .or_insert_with(|| (caller_item, Vec::new()));
7088 if !entry.1.contains(&call_range) {
7089 entry.1.push(call_range);
7090 }
7091 }
7092
7093 let results: Vec<CallHierarchyIncomingCall> = grouped
7094 .into_values()
7095 .map(|(from, from_ranges)| CallHierarchyIncomingCall { from, from_ranges })
7096 .collect();
7097
7098 self.client
7099 .log_message(
7100 MessageType::INFO,
7101 format!("found {} incoming callers", results.len()),
7102 )
7103 .await;
7104 Ok(Some(results))
7105 }
7106
7107 async fn outgoing_calls(
7108 &self,
7109 params: CallHierarchyOutgoingCallsParams,
7110 ) -> tower_lsp::jsonrpc::Result<Option<Vec<CallHierarchyOutgoingCall>>> {
7111 self.client
7112 .log_message(MessageType::INFO, "got callHierarchy/outgoingCalls request")
7113 .await;
7114
7115 let item = ¶ms.item;
7116
7117 let node_id = match item
7119 .data
7120 .as_ref()
7121 .and_then(|d| d.get("nodeId"))
7122 .and_then(|v| v.as_i64())
7123 {
7124 Some(id) => crate::types::NodeId(id),
7125 None => {
7126 self.client
7127 .log_message(
7128 MessageType::ERROR,
7129 "missing nodeId in CallHierarchyItem data",
7130 )
7131 .await;
7132 return Ok(None);
7133 }
7134 };
7135
7136 let file_path = match item.uri.to_file_path() {
7138 Ok(p) => p,
7139 Err(_) => return Ok(None),
7140 };
7141 let file_build = match self.get_or_fetch_build(&item.uri, &file_path, true).await {
7142 Some(cb) => cb,
7143 None => return Ok(None),
7144 };
7145
7146 let project_build = self.ensure_project_cached_build().await;
7148
7149 let mut builds: Vec<&goto::CachedBuild> = vec![&file_build];
7151 if let Some(ref pb) = project_build {
7152 builds.push(pb);
7153 }
7154 let sub_caches = self.sub_caches.read().await;
7155 for sc in sub_caches.iter() {
7156 builds.push(sc);
7157 }
7158
7159 let target_name = &item.name;
7163 let target_sel = &item.selection_range;
7164 let target_abs = item
7165 .uri
7166 .as_ref()
7167 .strip_prefix("file://")
7168 .unwrap_or(item.uri.as_ref());
7169
7170 let target_name_offset = {
7172 let source_bytes = std::fs::read(target_abs).unwrap_or_default();
7173 goto::pos_to_bytes(&source_bytes, target_sel.start)
7174 };
7175
7176 let mut resolved_outgoing: Vec<(CallHierarchyItem, (u32, u32), Range)> = Vec::new();
7184
7185 for build in &builds {
7186 let build_caller_ids = crate::call_hierarchy::resolve_target_in_build(
7190 build,
7191 node_id,
7192 target_abs,
7193 target_name,
7194 target_name_offset,
7195 );
7196
7197 for &cid in &build_caller_ids {
7198 let calls = crate::call_hierarchy::outgoing_calls(&build.nodes, cid);
7199 for (callee_id, call_src) in calls {
7200 let call_range = match crate::call_hierarchy::call_src_to_range(
7201 &call_src,
7202 &build.id_to_path_map,
7203 ) {
7204 Some(r) => r,
7205 None => continue,
7206 };
7207 let callee_item = if let Some(decl) = build.decl_index.get(&callee_id) {
7210 crate::call_hierarchy::decl_to_hierarchy_item(
7211 decl,
7212 callee_id,
7213 &build.node_id_to_source_path,
7214 &build.id_to_path_map,
7215 &build.nodes,
7216 )
7217 } else if let Some(info) =
7218 crate::call_hierarchy::find_node_info(&build.nodes, callee_id)
7219 {
7220 crate::call_hierarchy::node_info_to_hierarchy_item(
7221 callee_id,
7222 info,
7223 &build.id_to_path_map,
7224 )
7225 } else {
7226 None
7227 };
7228 let Some(callee_item) = callee_item else {
7229 continue;
7230 };
7231 let pos = (
7232 callee_item.selection_range.start.line,
7233 callee_item.selection_range.start.character,
7234 );
7235 resolved_outgoing.push((callee_item, pos, call_range));
7236 }
7237
7238 let ll_calls = crate::call_hierarchy::outgoing_low_level_calls(build, cid);
7240 for (ll_item, call_range) in ll_calls {
7241 let pos = (
7242 ll_item.selection_range.start.line,
7243 ll_item.selection_range.start.character,
7244 );
7245 resolved_outgoing.push((ll_item, pos, call_range));
7246 }
7247 }
7248 }
7249
7250 if resolved_outgoing.is_empty() {
7251 return Ok(Some(vec![]));
7252 }
7253
7254 let mut grouped: HashMap<(u32, u32), (CallHierarchyItem, Vec<Range>)> = HashMap::new();
7257 for (callee_item, pos, call_range) in resolved_outgoing {
7258 let entry = grouped
7259 .entry(pos)
7260 .or_insert_with(|| (callee_item, Vec::new()));
7261 if !entry.1.contains(&call_range) {
7262 entry.1.push(call_range);
7263 }
7264 }
7265
7266 let mut results: Vec<CallHierarchyOutgoingCall> = grouped
7267 .into_values()
7268 .map(|(to, from_ranges)| CallHierarchyOutgoingCall { to, from_ranges })
7269 .collect();
7270
7271 results.sort_by(|a, b| {
7274 let a_first = a.from_ranges.first();
7275 let b_first = b.from_ranges.first();
7276 match (a_first, b_first) {
7277 (Some(a_r), Some(b_r)) => a_r
7278 .start
7279 .line
7280 .cmp(&b_r.start.line)
7281 .then_with(|| a_r.start.character.cmp(&b_r.start.character)),
7282 (Some(_), None) => std::cmp::Ordering::Less,
7283 (None, Some(_)) => std::cmp::Ordering::Greater,
7284 (None, None) => std::cmp::Ordering::Equal,
7285 }
7286 });
7287
7288 Ok(Some(results))
7289 }
7290}
7291
7292#[cfg(test)]
7293mod tests {
7294 use super::{
7295 start_or_mark_project_cache_sync_pending, stop_project_cache_sync_worker_or_reclaim,
7296 take_project_cache_sync_pending, try_claim_project_cache_dirty,
7297 update_imports_on_delete_enabled,
7298 };
7299 use std::sync::atomic::{AtomicBool, Ordering};
7300
7301 #[test]
7302 fn update_imports_on_delete_enabled_defaults_true() {
7303 let s = crate::config::Settings::default();
7304 assert!(update_imports_on_delete_enabled(&s));
7305 }
7306
7307 #[test]
7308 fn update_imports_on_delete_enabled_respects_false() {
7309 let mut s = crate::config::Settings::default();
7310 s.file_operations.update_imports_on_delete = false;
7311 assert!(!update_imports_on_delete_enabled(&s));
7312 }
7313
7314 #[test]
7315 fn project_cache_sync_burst_only_first_starts_worker() {
7316 let pending = AtomicBool::new(false);
7317 let running = AtomicBool::new(false);
7318
7319 assert!(start_or_mark_project_cache_sync_pending(&pending, &running));
7320 assert!(pending.load(Ordering::Acquire));
7321 assert!(running.load(Ordering::Acquire));
7322
7323 assert!(!start_or_mark_project_cache_sync_pending(
7325 &pending, &running
7326 ));
7327 assert!(pending.load(Ordering::Acquire));
7328 assert!(running.load(Ordering::Acquire));
7329 }
7330
7331 #[test]
7332 fn project_cache_sync_take_pending_is_one_shot() {
7333 let pending = AtomicBool::new(true);
7334 assert!(take_project_cache_sync_pending(&pending));
7335 assert!(!pending.load(Ordering::Acquire));
7336 assert!(!take_project_cache_sync_pending(&pending));
7337 }
7338
7339 #[test]
7340 fn project_cache_sync_worker_stop_or_reclaim_handles_race() {
7341 let pending = AtomicBool::new(false);
7342 let running = AtomicBool::new(true);
7343
7344 assert!(!stop_project_cache_sync_worker_or_reclaim(
7346 &pending, &running
7347 ));
7348 assert!(!running.load(Ordering::Acquire));
7349
7350 pending.store(true, Ordering::Release);
7352 running.store(true, Ordering::Release);
7353 assert!(stop_project_cache_sync_worker_or_reclaim(
7354 &pending, &running
7355 ));
7356 assert!(running.load(Ordering::Acquire));
7357 }
7358
7359 #[test]
7360 fn project_cache_dirty_claim_and_retry_cycle() {
7361 let dirty = AtomicBool::new(true);
7362
7363 assert!(try_claim_project_cache_dirty(&dirty));
7364 assert!(!dirty.load(Ordering::Acquire));
7365
7366 assert!(!try_claim_project_cache_dirty(&dirty));
7368
7369 dirty.store(true, Ordering::Release);
7371 assert!(try_claim_project_cache_dirty(&dirty));
7372 assert!(!dirty.load(Ordering::Acquire));
7373 }
7374}