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