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