1use crate::Parser;
65use crate::ast::{Node, NodeKind};
66use crate::document_store::{Document, DocumentStore};
67use crate::position::{Position, Range};
68use crate::workspace::monitoring::IndexInstrumentation;
69use parking_lot::RwLock;
70use perl_position_tracking::{WireLocation, WirePosition, WireRange};
71use serde::{Deserialize, Serialize};
72use std::collections::hash_map::DefaultHasher;
73use std::collections::{HashMap, HashSet};
74use std::hash::{Hash, Hasher};
75use std::path::Path;
76use std::sync::Arc;
77use std::time::Instant;
78use url::Url;
79
80pub use crate::workspace::monitoring::{
81 DegradationReason, EarlyExitReason, EarlyExitRecord, IndexInstrumentationSnapshot,
82 IndexMetrics, IndexPerformanceCaps, IndexPhase, IndexPhaseTransition, IndexResourceLimits,
83 IndexStateKind, IndexStateTransition, ResourceKind,
84};
85
86#[cfg(not(target_arch = "wasm32"))]
88pub use perl_uri::{fs_path_to_uri, uri_to_fs_path};
90pub use perl_uri::{is_file_uri, is_special_scheme, uri_extension, uri_key};
92
93#[derive(Clone, Debug)]
134pub enum IndexState {
135 Building {
137 phase: IndexPhase,
139 indexed_count: usize,
141 total_count: usize,
143 started_at: Instant,
145 },
146
147 Ready {
149 symbol_count: usize,
151 file_count: usize,
153 completed_at: Instant,
155 },
156
157 Degraded {
159 reason: DegradationReason,
161 available_symbols: usize,
163 since: Instant,
165 },
166}
167
168impl IndexState {
169 pub fn kind(&self) -> IndexStateKind {
171 match self {
172 IndexState::Building { .. } => IndexStateKind::Building,
173 IndexState::Ready { .. } => IndexStateKind::Ready,
174 IndexState::Degraded { .. } => IndexStateKind::Degraded,
175 }
176 }
177
178 pub fn phase(&self) -> Option<IndexPhase> {
180 match self {
181 IndexState::Building { phase, .. } => Some(*phase),
182 _ => None,
183 }
184 }
185
186 pub fn state_started_at(&self) -> Instant {
188 match self {
189 IndexState::Building { started_at, .. } => *started_at,
190 IndexState::Ready { completed_at, .. } => *completed_at,
191 IndexState::Degraded { since, .. } => *since,
192 }
193 }
194}
195
196pub struct IndexCoordinator {
248 state: Arc<RwLock<IndexState>>,
250
251 index: Arc<WorkspaceIndex>,
253
254 limits: IndexResourceLimits,
261
262 caps: IndexPerformanceCaps,
264
265 metrics: IndexMetrics,
267
268 instrumentation: IndexInstrumentation,
270}
271
272impl std::fmt::Debug for IndexCoordinator {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 f.debug_struct("IndexCoordinator")
275 .field("state", &*self.state.read())
276 .field("limits", &self.limits)
277 .field("caps", &self.caps)
278 .finish_non_exhaustive()
279 }
280}
281
282impl IndexCoordinator {
283 pub fn new() -> Self {
300 Self {
301 state: Arc::new(RwLock::new(IndexState::Building {
302 phase: IndexPhase::Idle,
303 indexed_count: 0,
304 total_count: 0,
305 started_at: Instant::now(),
306 })),
307 index: Arc::new(WorkspaceIndex::new()),
308 limits: IndexResourceLimits::default(),
309 caps: IndexPerformanceCaps::default(),
310 metrics: IndexMetrics::new(),
311 instrumentation: IndexInstrumentation::new(),
312 }
313 }
314
315 pub fn with_limits(limits: IndexResourceLimits) -> Self {
334 Self {
335 state: Arc::new(RwLock::new(IndexState::Building {
336 phase: IndexPhase::Idle,
337 indexed_count: 0,
338 total_count: 0,
339 started_at: Instant::now(),
340 })),
341 index: Arc::new(WorkspaceIndex::new()),
342 limits,
343 caps: IndexPerformanceCaps::default(),
344 metrics: IndexMetrics::new(),
345 instrumentation: IndexInstrumentation::new(),
346 }
347 }
348
349 pub fn with_limits_and_caps(limits: IndexResourceLimits, caps: IndexPerformanceCaps) -> Self {
356 Self {
357 state: Arc::new(RwLock::new(IndexState::Building {
358 phase: IndexPhase::Idle,
359 indexed_count: 0,
360 total_count: 0,
361 started_at: Instant::now(),
362 })),
363 index: Arc::new(WorkspaceIndex::new()),
364 limits,
365 caps,
366 metrics: IndexMetrics::new(),
367 instrumentation: IndexInstrumentation::new(),
368 }
369 }
370
371 pub fn state(&self) -> IndexState {
396 self.state.read().clone()
397 }
398
399 pub fn index(&self) -> &Arc<WorkspaceIndex> {
417 &self.index
418 }
419
420 pub fn limits(&self) -> &IndexResourceLimits {
422 &self.limits
423 }
424
425 pub fn performance_caps(&self) -> &IndexPerformanceCaps {
427 &self.caps
428 }
429
430 pub fn instrumentation_snapshot(&self) -> IndexInstrumentationSnapshot {
432 self.instrumentation.snapshot()
433 }
434
435 pub fn notify_change(&self, _uri: &str) {
457 let pending = self.metrics.increment_pending_parses();
458
459 if self.metrics.is_parse_storm() {
461 self.transition_to_degraded(DegradationReason::ParseStorm { pending_parses: pending });
462 }
463 }
464
465 pub fn notify_parse_complete(&self, _uri: &str) {
487 let pending = self.metrics.decrement_pending_parses();
488
489 if pending == 0 {
491 if let IndexState::Degraded { reason: DegradationReason::ParseStorm { .. }, .. } =
492 self.state()
493 {
494 let mut state = self.state.write();
496 let from_kind = state.kind();
497 self.instrumentation.record_state_transition(from_kind, IndexStateKind::Building);
498 *state = IndexState::Building {
499 phase: IndexPhase::Idle,
500 indexed_count: 0,
501 total_count: 0,
502 started_at: Instant::now(),
503 };
504 }
505 }
506
507 self.enforce_limits();
509 }
510
511 pub fn transition_to_ready(&self, file_count: usize, symbol_count: usize) {
541 let mut state = self.state.write();
542 let from_kind = state.kind();
543
544 match &*state {
546 IndexState::Building { .. } | IndexState::Degraded { .. } => {
547 *state =
549 IndexState::Ready { symbol_count, file_count, completed_at: Instant::now() };
550 }
551 IndexState::Ready { .. } => {
552 *state =
554 IndexState::Ready { symbol_count, file_count, completed_at: Instant::now() };
555 }
556 }
557 self.instrumentation.record_state_transition(from_kind, IndexStateKind::Ready);
558 drop(state); self.enforce_limits();
562 }
563
564 pub fn transition_to_scanning(&self) {
568 let mut state = self.state.write();
569 let from_kind = state.kind();
570
571 match &*state {
572 IndexState::Building { phase, indexed_count, total_count, started_at } => {
573 if *phase != IndexPhase::Scanning {
574 self.instrumentation.record_phase_transition(*phase, IndexPhase::Scanning);
575 }
576 *state = IndexState::Building {
577 phase: IndexPhase::Scanning,
578 indexed_count: *indexed_count,
579 total_count: *total_count,
580 started_at: *started_at,
581 };
582 }
583 IndexState::Ready { .. } | IndexState::Degraded { .. } => {
584 self.instrumentation.record_state_transition(from_kind, IndexStateKind::Building);
585 self.instrumentation
586 .record_phase_transition(IndexPhase::Idle, IndexPhase::Scanning);
587 *state = IndexState::Building {
588 phase: IndexPhase::Scanning,
589 indexed_count: 0,
590 total_count: 0,
591 started_at: Instant::now(),
592 };
593 }
594 }
595 }
596
597 pub fn update_scan_progress(&self, total_count: usize) {
599 let mut state = self.state.write();
600 if let IndexState::Building { phase, indexed_count, started_at, .. } = &*state {
601 if *phase != IndexPhase::Scanning {
602 self.instrumentation.record_phase_transition(*phase, IndexPhase::Scanning);
603 }
604 *state = IndexState::Building {
605 phase: IndexPhase::Scanning,
606 indexed_count: *indexed_count,
607 total_count,
608 started_at: *started_at,
609 };
610 }
611 }
612
613 pub fn transition_to_indexing(&self, total_count: usize) {
617 let mut state = self.state.write();
618 let from_kind = state.kind();
619
620 match &*state {
621 IndexState::Building { phase, indexed_count, started_at, .. } => {
622 if *phase != IndexPhase::Indexing {
623 self.instrumentation.record_phase_transition(*phase, IndexPhase::Indexing);
624 }
625 *state = IndexState::Building {
626 phase: IndexPhase::Indexing,
627 indexed_count: *indexed_count,
628 total_count,
629 started_at: *started_at,
630 };
631 }
632 IndexState::Ready { .. } | IndexState::Degraded { .. } => {
633 self.instrumentation.record_state_transition(from_kind, IndexStateKind::Building);
634 self.instrumentation
635 .record_phase_transition(IndexPhase::Idle, IndexPhase::Indexing);
636 *state = IndexState::Building {
637 phase: IndexPhase::Indexing,
638 indexed_count: 0,
639 total_count,
640 started_at: Instant::now(),
641 };
642 }
643 }
644 }
645
646 pub fn transition_to_building(&self, total_count: usize) {
650 let mut state = self.state.write();
651 let from_kind = state.kind();
652
653 match &*state {
655 IndexState::Degraded { .. } | IndexState::Ready { .. } => {
656 self.instrumentation.record_state_transition(from_kind, IndexStateKind::Building);
657 self.instrumentation
658 .record_phase_transition(IndexPhase::Idle, IndexPhase::Indexing);
659 *state = IndexState::Building {
660 phase: IndexPhase::Indexing,
661 indexed_count: 0,
662 total_count,
663 started_at: Instant::now(),
664 };
665 }
666 IndexState::Building { phase, indexed_count, started_at, .. } => {
667 let mut next_phase = *phase;
668 if *phase == IndexPhase::Idle {
669 self.instrumentation
670 .record_phase_transition(IndexPhase::Idle, IndexPhase::Indexing);
671 next_phase = IndexPhase::Indexing;
672 }
673 *state = IndexState::Building {
674 phase: next_phase,
675 indexed_count: *indexed_count,
676 total_count,
677 started_at: *started_at,
678 };
679 }
680 }
681 }
682
683 pub fn update_building_progress(&self, indexed_count: usize) {
705 let mut state = self.state.write();
706
707 if let IndexState::Building { phase, started_at, total_count, .. } = &*state {
708 let elapsed = started_at.elapsed().as_millis() as u64;
709
710 if elapsed > self.limits.max_scan_duration_ms {
712 drop(state);
714 self.transition_to_degraded(DegradationReason::ScanTimeout { elapsed_ms: elapsed });
715 return;
716 }
717
718 *state = IndexState::Building {
720 phase: *phase,
721 indexed_count,
722 total_count: *total_count,
723 started_at: *started_at,
724 };
725 }
726 }
727
728 pub fn transition_to_degraded(&self, reason: DegradationReason) {
753 let mut state = self.state.write();
754 let from_kind = state.kind();
755
756 let available_symbols = match &*state {
758 IndexState::Ready { symbol_count, .. } => *symbol_count,
759 IndexState::Degraded { available_symbols, .. } => *available_symbols,
760 IndexState::Building { .. } => 0,
761 };
762
763 self.instrumentation.record_state_transition(from_kind, IndexStateKind::Degraded);
764 *state = IndexState::Degraded { reason, available_symbols, since: Instant::now() };
765 }
766
767 pub fn check_limits(&self) -> Option<DegradationReason> {
798 let files = self.index.files.read();
799
800 let file_count = files.len();
802 if file_count > self.limits.max_files {
803 return Some(DegradationReason::ResourceLimit { kind: ResourceKind::MaxFiles });
804 }
805
806 let total_symbols: usize = files.values().map(|fi| fi.symbols.len()).sum();
808 if total_symbols > self.limits.max_total_symbols {
809 return Some(DegradationReason::ResourceLimit { kind: ResourceKind::MaxSymbols });
810 }
811
812 None
813 }
814
815 pub fn enforce_limits(&self) {
841 if let Some(reason) = self.check_limits() {
842 self.transition_to_degraded(reason);
843 }
844 }
845
846 pub fn record_early_exit(
848 &self,
849 reason: EarlyExitReason,
850 elapsed_ms: u64,
851 indexed_files: usize,
852 total_files: usize,
853 ) {
854 self.instrumentation.record_early_exit(EarlyExitRecord {
855 reason,
856 elapsed_ms,
857 indexed_files,
858 total_files,
859 });
860 }
861
862 pub fn query<T, F1, F2>(&self, full_query: F1, partial_query: F2) -> T
895 where
896 F1: FnOnce(&WorkspaceIndex) -> T,
897 F2: FnOnce(&WorkspaceIndex) -> T,
898 {
899 match self.state() {
900 IndexState::Ready { .. } => full_query(&self.index),
901 _ => partial_query(&self.index),
902 }
903 }
904}
905
906impl Default for IndexCoordinator {
907 fn default() -> Self {
908 Self::new()
909 }
910}
911
912#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
917pub enum SymKind {
919 Var,
921 Sub,
923 Pack,
925}
926
927#[derive(Clone, Debug, Eq, PartialEq, Hash)]
928pub struct SymbolKey {
930 pub pkg: Arc<str>,
932 pub name: Arc<str>,
934 pub sigil: Option<char>,
936 pub kind: SymKind,
938}
939
940pub fn normalize_var(name: &str) -> (Option<char>, &str) {
961 if name.is_empty() {
962 return (None, "");
963 }
964
965 let Some(first_char) = name.chars().next() else {
967 return (None, name); };
969 match first_char {
970 '$' | '@' | '%' => {
971 if name.len() > 1 {
972 (Some(first_char), &name[1..])
973 } else {
974 (Some(first_char), "")
975 }
976 }
977 _ => (None, name),
978 }
979}
980
981#[derive(Debug, Clone)]
984pub struct Location {
986 pub uri: String,
988 pub range: Range,
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize)]
993pub struct WorkspaceSymbol {
995 pub name: String,
997 pub kind: SymbolKind,
999 pub uri: String,
1001 pub range: Range,
1003 pub qualified_name: Option<String>,
1005 pub documentation: Option<String>,
1007 pub container_name: Option<String>,
1009 #[serde(default = "default_has_body")]
1011 pub has_body: bool,
1012}
1013
1014fn default_has_body() -> bool {
1015 true
1016}
1017
1018pub use perl_symbol_types::{SymbolKind, VarKind};
1021
1022fn sigil_to_var_kind(sigil: &str) -> VarKind {
1024 match sigil {
1025 "@" => VarKind::Array,
1026 "%" => VarKind::Hash,
1027 _ => VarKind::Scalar, }
1029}
1030
1031#[derive(Debug, Clone)]
1032pub struct SymbolReference {
1034 pub uri: String,
1036 pub range: Range,
1038 pub kind: ReferenceKind,
1040}
1041
1042#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1043pub enum ReferenceKind {
1045 Definition,
1047 Usage,
1049 Import,
1051 Read,
1053 Write,
1055}
1056
1057#[derive(Debug, Serialize)]
1058#[serde(rename_all = "camelCase")]
1059pub struct LspWorkspaceSymbol {
1061 pub name: String,
1063 pub kind: u32,
1065 pub location: WireLocation,
1067 #[serde(skip_serializing_if = "Option::is_none")]
1069 pub container_name: Option<String>,
1070}
1071
1072impl From<&WorkspaceSymbol> for LspWorkspaceSymbol {
1073 fn from(sym: &WorkspaceSymbol) -> Self {
1074 let range = WireRange {
1075 start: WirePosition { line: sym.range.start.line, character: sym.range.start.column },
1076 end: WirePosition { line: sym.range.end.line, character: sym.range.end.column },
1077 };
1078
1079 Self {
1080 name: sym.name.clone(),
1081 kind: sym.kind.to_lsp_kind(),
1082 location: WireLocation { uri: sym.uri.clone(), range },
1083 container_name: sym.container_name.clone(),
1084 }
1085 }
1086}
1087
1088#[derive(Default)]
1090struct FileIndex {
1091 symbols: Vec<WorkspaceSymbol>,
1093 references: HashMap<String, Vec<SymbolReference>>,
1095 dependencies: HashSet<String>,
1097 content_hash: u64,
1099}
1100
1101pub struct WorkspaceIndex {
1103 files: Arc<RwLock<HashMap<String, FileIndex>>>,
1105 symbols: Arc<RwLock<HashMap<String, String>>>,
1107 global_references: Arc<RwLock<HashMap<String, Vec<Location>>>>,
1112 document_store: DocumentStore,
1114}
1115
1116impl WorkspaceIndex {
1117 fn rebuild_symbol_cache(
1118 files: &HashMap<String, FileIndex>,
1119 symbols: &mut HashMap<String, String>,
1120 ) {
1121 symbols.clear();
1122
1123 for file_index in files.values() {
1124 for symbol in &file_index.symbols {
1125 if let Some(ref qname) = symbol.qualified_name {
1126 symbols.insert(qname.clone(), symbol.uri.clone());
1127 }
1128 symbols.insert(symbol.name.clone(), symbol.uri.clone());
1129 }
1130 }
1131 }
1132
1133 fn incremental_remove_symbols(
1136 files: &HashMap<String, FileIndex>,
1137 symbols: &mut HashMap<String, String>,
1138 old_file_index: &FileIndex,
1139 ) {
1140 let mut affected_names: Vec<String> = Vec::new();
1141 for sym in &old_file_index.symbols {
1142 if let Some(ref qname) = sym.qualified_name {
1143 if symbols.get(qname) == Some(&sym.uri) {
1144 symbols.remove(qname);
1145 affected_names.push(qname.clone());
1146 }
1147 }
1148 if symbols.get(&sym.name) == Some(&sym.uri) {
1149 symbols.remove(&sym.name);
1150 affected_names.push(sym.name.clone());
1151 }
1152 }
1153 if !affected_names.is_empty() {
1154 for file_index in files.values() {
1155 for sym in &file_index.symbols {
1156 if let Some(ref qname) = sym.qualified_name {
1157 if !symbols.contains_key(qname) && affected_names.contains(qname) {
1158 symbols.insert(qname.clone(), sym.uri.clone());
1159 }
1160 }
1161 if !symbols.contains_key(&sym.name) && affected_names.contains(&sym.name) {
1162 symbols.insert(sym.name.clone(), sym.uri.clone());
1163 }
1164 }
1165 }
1166 }
1167 }
1168
1169 fn incremental_add_symbols(symbols: &mut HashMap<String, String>, file_index: &FileIndex) {
1171 for sym in &file_index.symbols {
1172 if let Some(ref qname) = sym.qualified_name {
1173 symbols.insert(qname.clone(), sym.uri.clone());
1174 }
1175 symbols.insert(sym.name.clone(), sym.uri.clone());
1176 }
1177 }
1178
1179 fn find_definition_in_files(
1180 files: &HashMap<String, FileIndex>,
1181 symbol_name: &str,
1182 uri_filter: Option<&str>,
1183 ) -> Option<(Location, String)> {
1184 for file_index in files.values() {
1185 if let Some(filter) = uri_filter
1186 && file_index.symbols.first().is_some_and(|symbol| symbol.uri != filter)
1187 {
1188 continue;
1189 }
1190
1191 for symbol in &file_index.symbols {
1192 if symbol.name == symbol_name
1193 || symbol.qualified_name.as_deref() == Some(symbol_name)
1194 {
1195 return Some((
1196 Location { uri: symbol.uri.clone(), range: symbol.range },
1197 symbol.uri.clone(),
1198 ));
1199 }
1200 }
1201 }
1202
1203 None
1204 }
1205
1206 pub fn new() -> Self {
1221 Self {
1222 files: Arc::new(RwLock::new(HashMap::new())),
1223 symbols: Arc::new(RwLock::new(HashMap::new())),
1224 global_references: Arc::new(RwLock::new(HashMap::new())),
1225 document_store: DocumentStore::new(),
1226 }
1227 }
1228
1229 pub fn with_capacity(estimated_files: usize, avg_symbols_per_file: usize) -> Self {
1254 let sym_cap =
1256 estimated_files.saturating_mul(avg_symbols_per_file).saturating_mul(2).min(1_000_000);
1257 let ref_cap = (sym_cap / 4).min(1_000_000);
1258 Self {
1259 files: Arc::new(RwLock::new(HashMap::with_capacity(estimated_files))),
1260 symbols: Arc::new(RwLock::new(HashMap::with_capacity(sym_cap))),
1261 global_references: Arc::new(RwLock::new(HashMap::with_capacity(ref_cap))),
1262 document_store: DocumentStore::new(),
1263 }
1264 }
1265
1266 fn normalize_uri(uri: &str) -> String {
1268 perl_uri::normalize_uri(uri)
1269 }
1270
1271 fn remove_file_global_refs(
1276 global_refs: &mut HashMap<String, Vec<Location>>,
1277 file_index: &FileIndex,
1278 file_uri: &str,
1279 ) {
1280 for name in file_index.references.keys() {
1281 if let Some(locs) = global_refs.get_mut(name) {
1282 locs.retain(|loc| loc.uri != file_uri);
1283 if locs.is_empty() {
1284 global_refs.remove(name);
1285 }
1286 }
1287 }
1288 }
1289
1290 pub fn index_file(&self, uri: Url, text: String) -> Result<(), String> {
1321 let uri_str = uri.to_string();
1322
1323 let mut hasher = DefaultHasher::new();
1325 text.hash(&mut hasher);
1326 let content_hash = hasher.finish();
1327
1328 let key = DocumentStore::uri_key(&uri_str);
1330 {
1331 let files = self.files.read();
1332 if let Some(existing_index) = files.get(&key) {
1333 if existing_index.content_hash == content_hash {
1334 return Ok(());
1336 }
1337 }
1338 }
1339
1340 if self.document_store.is_open(&uri_str) {
1342 self.document_store.update(&uri_str, 1, text.clone());
1343 } else {
1344 self.document_store.open(uri_str.clone(), 1, text.clone());
1345 }
1346
1347 let mut parser = Parser::new(&text);
1349 let ast = match parser.parse() {
1350 Ok(ast) => ast,
1351 Err(e) => return Err(format!("Parse error: {}", e)),
1352 };
1353
1354 let mut doc = self.document_store.get(&uri_str).ok_or("Document not found")?;
1356
1357 let mut file_index = FileIndex { content_hash, ..Default::default() };
1359 let mut visitor = IndexVisitor::new(&mut doc, uri_str.clone());
1360 visitor.visit(&ast, &mut file_index);
1361
1362 {
1365 let mut files = self.files.write();
1366
1367 if let Some(old_index) = files.get(&key) {
1369 let mut global_refs = self.global_references.write();
1370 Self::remove_file_global_refs(&mut global_refs, old_index, &uri_str);
1371 }
1372
1373 if let Some(old_index) = files.get(&key) {
1375 let mut symbols = self.symbols.write();
1376 Self::incremental_remove_symbols(&files, &mut symbols, old_index);
1377 drop(symbols);
1378 }
1379 files.insert(key.clone(), file_index);
1380 let mut symbols = self.symbols.write();
1381 if let Some(new_index) = files.get(&key) {
1382 Self::incremental_add_symbols(&mut symbols, new_index);
1383 }
1384
1385 if let Some(file_index) = files.get(&key) {
1386 let mut global_refs = self.global_references.write();
1387 for (name, refs) in &file_index.references {
1388 let entry = global_refs.entry(name.clone()).or_default();
1389 for reference in refs {
1390 entry.push(Location { uri: reference.uri.clone(), range: reference.range });
1391 }
1392 }
1393 }
1394 }
1395
1396 Ok(())
1397 }
1398
1399 pub fn remove_file(&self, uri: &str) {
1418 let uri_str = Self::normalize_uri(uri);
1419 let key = DocumentStore::uri_key(&uri_str);
1420
1421 self.document_store.close(&uri_str);
1423
1424 let mut files = self.files.write();
1426 if let Some(file_index) = files.remove(&key) {
1427 let mut symbols = self.symbols.write();
1429 Self::incremental_remove_symbols(&files, &mut symbols, &file_index);
1430
1431 let mut global_refs = self.global_references.write();
1433 Self::remove_file_global_refs(&mut global_refs, &file_index, &uri_str);
1434 }
1435 }
1436
1437 pub fn remove_file_url(&self, uri: &Url) {
1461 self.remove_file(uri.as_str())
1462 }
1463
1464 pub fn clear_file(&self, uri: &str) {
1483 self.remove_file(uri);
1484 }
1485
1486 pub fn clear_file_url(&self, uri: &Url) {
1510 self.clear_file(uri.as_str())
1511 }
1512
1513 #[cfg(not(target_arch = "wasm32"))]
1514 pub fn index_file_str(&self, uri: &str, text: &str) -> Result<(), String> {
1544 let path = Path::new(uri);
1545 let url = if path.is_absolute() {
1546 url::Url::from_file_path(path)
1547 .map_err(|_| format!("Invalid URI or file path: {}", uri))?
1548 } else {
1549 url::Url::parse(uri).or_else(|_| {
1552 url::Url::from_file_path(path)
1553 .map_err(|_| format!("Invalid URI or file path: {}", uri))
1554 })?
1555 };
1556 self.index_file(url, text.to_string())
1557 }
1558
1559 pub fn index_files_batch(&self, files_to_index: Vec<(Url, String)>) -> Vec<String> {
1568 let mut errors = Vec::new();
1569
1570 let mut parsed: Vec<(String, String, FileIndex)> = Vec::with_capacity(files_to_index.len());
1572 for (uri, text) in &files_to_index {
1573 let uri_str = uri.to_string();
1574
1575 let mut hasher = DefaultHasher::new();
1577 text.hash(&mut hasher);
1578 let content_hash = hasher.finish();
1579
1580 let key = DocumentStore::uri_key(&uri_str);
1581
1582 {
1584 let files = self.files.read();
1585 if let Some(existing) = files.get(&key) {
1586 if existing.content_hash == content_hash {
1587 continue;
1588 }
1589 }
1590 }
1591
1592 if self.document_store.is_open(&uri_str) {
1594 self.document_store.update(&uri_str, 1, text.clone());
1595 } else {
1596 self.document_store.open(uri_str.clone(), 1, text.clone());
1597 }
1598
1599 let mut parser = Parser::new(text);
1601 let ast = match parser.parse() {
1602 Ok(ast) => ast,
1603 Err(e) => {
1604 errors.push(format!("Parse error in {}: {}", uri_str, e));
1605 continue;
1606 }
1607 };
1608
1609 let mut doc = match self.document_store.get(&uri_str) {
1610 Some(d) => d,
1611 None => {
1612 errors.push(format!("Document not found: {}", uri_str));
1613 continue;
1614 }
1615 };
1616
1617 let mut file_index = FileIndex { content_hash, ..Default::default() };
1618 let mut visitor = IndexVisitor::new(&mut doc, uri_str.clone());
1619 visitor.visit(&ast, &mut file_index);
1620
1621 parsed.push((key, uri_str, file_index));
1622 }
1623
1624 {
1626 let mut files = self.files.write();
1627 let mut symbols = self.symbols.write();
1628 let mut global_refs = self.global_references.write();
1629
1630 files.reserve(parsed.len());
1633 symbols.reserve(parsed.len().saturating_mul(20).saturating_mul(2));
1634
1635 for (key, uri_str, file_index) in parsed {
1636 if let Some(old_index) = files.get(&key) {
1638 Self::remove_file_global_refs(&mut global_refs, old_index, &uri_str);
1639 }
1640
1641 files.insert(key.clone(), file_index);
1642
1643 if let Some(fi) = files.get(&key) {
1645 for (name, refs) in &fi.references {
1646 let entry = global_refs.entry(name.clone()).or_default();
1647 for reference in refs {
1648 entry.push(Location {
1649 uri: reference.uri.clone(),
1650 range: reference.range,
1651 });
1652 }
1653 }
1654 }
1655 }
1656
1657 Self::rebuild_symbol_cache(&files, &mut symbols);
1659 }
1660
1661 errors
1662 }
1663
1664 pub fn find_references(&self, symbol_name: &str) -> Vec<Location> {
1692 let global_refs = self.global_references.read();
1693 let mut seen: HashSet<(String, u32, u32, u32, u32)> = HashSet::new();
1694 let mut locations = Vec::new();
1695
1696 if let Some(refs) = global_refs.get(symbol_name) {
1698 for loc in refs {
1699 let key = (
1700 loc.uri.clone(),
1701 loc.range.start.line,
1702 loc.range.start.column,
1703 loc.range.end.line,
1704 loc.range.end.column,
1705 );
1706 if seen.insert(key) {
1707 locations.push(Location { uri: loc.uri.clone(), range: loc.range });
1708 }
1709 }
1710 }
1711
1712 if let Some(idx) = symbol_name.rfind("::") {
1714 let bare_name = &symbol_name[idx + 2..];
1715 if let Some(refs) = global_refs.get(bare_name) {
1716 for loc in refs {
1717 let key = (
1718 loc.uri.clone(),
1719 loc.range.start.line,
1720 loc.range.start.column,
1721 loc.range.end.line,
1722 loc.range.end.column,
1723 );
1724 if seen.insert(key) {
1725 locations.push(Location { uri: loc.uri.clone(), range: loc.range });
1726 }
1727 }
1728 }
1729 }
1730
1731 locations
1732 }
1733
1734 pub fn count_usages(&self, symbol_name: &str) -> usize {
1740 let files = self.files.read();
1741 let mut seen: HashSet<(String, u32, u32, u32, u32)> = HashSet::new();
1742
1743 for (_uri_key, file_index) in files.iter() {
1744 if let Some(refs) = file_index.references.get(symbol_name) {
1745 for r in refs.iter().filter(|r| r.kind != ReferenceKind::Definition) {
1746 seen.insert((
1747 r.uri.clone(),
1748 r.range.start.line,
1749 r.range.start.column,
1750 r.range.end.line,
1751 r.range.end.column,
1752 ));
1753 }
1754 }
1755
1756 if let Some(idx) = symbol_name.rfind("::") {
1757 let bare_name = &symbol_name[idx + 2..];
1758 if let Some(refs) = file_index.references.get(bare_name) {
1759 for r in refs.iter().filter(|r| r.kind != ReferenceKind::Definition) {
1760 seen.insert((
1761 r.uri.clone(),
1762 r.range.start.line,
1763 r.range.start.column,
1764 r.range.end.line,
1765 r.range.end.column,
1766 ));
1767 }
1768 }
1769 }
1770 }
1771
1772 seen.len()
1773 }
1774
1775 pub fn find_definition(&self, symbol_name: &str) -> Option<Location> {
1794 let cached_uri = {
1795 let symbols = self.symbols.read();
1796 symbols.get(symbol_name).cloned()
1797 };
1798
1799 let files = self.files.read();
1800 if let Some(ref uri_str) = cached_uri
1801 && let Some((location, _uri)) =
1802 Self::find_definition_in_files(&files, symbol_name, Some(uri_str))
1803 {
1804 return Some(location);
1805 }
1806
1807 let resolved = Self::find_definition_in_files(&files, symbol_name, None);
1808 drop(files);
1809
1810 if let Some((location, uri)) = resolved {
1811 let mut symbols = self.symbols.write();
1812 symbols.insert(symbol_name.to_string(), uri);
1813 return Some(location);
1814 }
1815
1816 None
1817 }
1818
1819 pub fn all_symbols(&self) -> Vec<WorkspaceSymbol> {
1834 let files = self.files.read();
1835 let mut symbols = Vec::new();
1836
1837 for (_uri_key, file_index) in files.iter() {
1838 symbols.extend(file_index.symbols.clone());
1839 }
1840
1841 symbols
1842 }
1843
1844 pub fn clear(&self) {
1846 self.files.write().clear();
1847 self.symbols.write().clear();
1848 self.global_references.write().clear();
1849 }
1850
1851 pub fn file_count(&self) -> usize {
1853 let files = self.files.read();
1854 files.len()
1855 }
1856
1857 pub fn symbol_count(&self) -> usize {
1859 let files = self.files.read();
1860 files.values().map(|file_index| file_index.symbols.len()).sum()
1861 }
1862
1863 #[cfg(feature = "memory-profiling")]
1871 pub fn memory_snapshot(&self) -> crate::workspace::memory::MemorySnapshot {
1872 use std::mem::size_of;
1873
1874 let files_guard = self.files.read();
1875 let symbols_guard = self.symbols.read();
1876 let global_refs_guard = self.global_references.read();
1877
1878 let mut files_bytes: usize = 0;
1880 let mut total_symbol_count: usize = 0;
1881 for (uri_key, fi) in files_guard.iter() {
1882 files_bytes += uri_key.len();
1884 for sym in &fi.symbols {
1886 files_bytes += sym.name.len()
1887 + sym.uri.len()
1888 + sym.qualified_name.as_deref().map_or(0, str::len)
1889 + sym.documentation.as_deref().map_or(0, str::len)
1890 + sym.container_name.as_deref().map_or(0, str::len)
1891 + size_of::<WorkspaceSymbol>();
1893 }
1894 total_symbol_count += fi.symbols.len();
1895 for (ref_name, refs) in &fi.references {
1897 files_bytes += ref_name.len();
1898 for r in refs {
1899 files_bytes += r.uri.len() + size_of::<SymbolReference>();
1900 }
1901 }
1902 for dep in &fi.dependencies {
1904 files_bytes += dep.len();
1905 }
1906 files_bytes += size_of::<u64>();
1908 }
1909
1910 let mut symbols_bytes: usize = 0;
1912 for (qname, uri) in symbols_guard.iter() {
1913 symbols_bytes += qname.len() + uri.len();
1914 }
1915
1916 let mut global_refs_bytes: usize = 0;
1918 for (sym_name, locs) in global_refs_guard.iter() {
1919 global_refs_bytes += sym_name.len();
1920 for loc in locs {
1921 global_refs_bytes += loc.uri.len() + size_of::<Location>();
1922 }
1923 }
1924
1925 let document_store_bytes = self.document_store.total_text_bytes();
1927
1928 crate::workspace::memory::MemorySnapshot {
1929 file_count: files_guard.len(),
1930 symbol_count: total_symbol_count,
1931 files_bytes,
1932 symbols_bytes,
1933 global_refs_bytes,
1934 document_store_bytes,
1935 }
1936 }
1937
1938 pub fn has_symbols(&self) -> bool {
1957 let files = self.files.read();
1958 files.values().any(|file_index| !file_index.symbols.is_empty())
1959 }
1960
1961 pub fn search_symbols(&self, query: &str) -> Vec<WorkspaceSymbol> {
1980 let query_lower = query.to_lowercase();
1981 let files = self.files.read();
1982 let mut results = Vec::new();
1983 for file_index in files.values() {
1984 for symbol in &file_index.symbols {
1985 if symbol.name.to_lowercase().contains(&query_lower)
1986 || symbol
1987 .qualified_name
1988 .as_ref()
1989 .map(|qn| qn.to_lowercase().contains(&query_lower))
1990 .unwrap_or(false)
1991 {
1992 results.push(symbol.clone());
1993 }
1994 }
1995 }
1996 results
1997 }
1998
1999 pub fn find_symbols(&self, query: &str) -> Vec<WorkspaceSymbol> {
2018 self.search_symbols(query)
2019 }
2020
2021 pub fn file_symbols(&self, uri: &str) -> Vec<WorkspaceSymbol> {
2040 let normalized_uri = Self::normalize_uri(uri);
2041 let key = DocumentStore::uri_key(&normalized_uri);
2042 let files = self.files.read();
2043
2044 files.get(&key).map(|fi| fi.symbols.clone()).unwrap_or_default()
2045 }
2046
2047 pub fn file_dependencies(&self, uri: &str) -> HashSet<String> {
2066 let normalized_uri = Self::normalize_uri(uri);
2067 let key = DocumentStore::uri_key(&normalized_uri);
2068 let files = self.files.read();
2069
2070 files.get(&key).map(|fi| fi.dependencies.clone()).unwrap_or_default()
2071 }
2072
2073 pub fn find_dependents(&self, module_name: &str) -> Vec<String> {
2092 let files = self.files.read();
2093 let mut dependents = Vec::new();
2094
2095 for (uri_key, file_index) in files.iter() {
2096 if file_index.dependencies.contains(module_name) {
2097 dependents.push(uri_key.clone());
2098 }
2099 }
2100
2101 dependents
2102 }
2103
2104 pub fn document_store(&self) -> &DocumentStore {
2119 &self.document_store
2120 }
2121
2122 pub fn find_unused_symbols(&self) -> Vec<WorkspaceSymbol> {
2137 let files = self.files.read();
2138 let mut unused = Vec::new();
2139
2140 for (_uri_key, file_index) in files.iter() {
2142 for symbol in &file_index.symbols {
2143 let has_usage = files.values().any(|fi| {
2145 if let Some(refs) = fi.references.get(&symbol.name) {
2146 refs.iter().any(|r| r.kind != ReferenceKind::Definition)
2147 } else {
2148 false
2149 }
2150 });
2151
2152 if !has_usage {
2153 unused.push(symbol.clone());
2154 }
2155 }
2156 }
2157
2158 unused
2159 }
2160
2161 pub fn get_package_members(&self, package_name: &str) -> Vec<WorkspaceSymbol> {
2180 let files = self.files.read();
2181 let mut members = Vec::new();
2182
2183 for (_uri_key, file_index) in files.iter() {
2184 for symbol in &file_index.symbols {
2185 if let Some(ref container) = symbol.container_name {
2187 if container == package_name {
2188 members.push(symbol.clone());
2189 }
2190 }
2191 if let Some(ref qname) = symbol.qualified_name {
2193 if qname.starts_with(&format!("{}::", package_name)) {
2194 if symbol.container_name.as_deref() != Some(package_name) {
2196 members.push(symbol.clone());
2197 }
2198 }
2199 }
2200 }
2201 }
2202
2203 members
2204 }
2205
2206 pub fn find_def(&self, key: &SymbolKey) -> Option<Location> {
2227 if let Some(sigil) = key.sigil {
2228 let var_name = format!("{}{}", sigil, key.name);
2230 self.find_definition(&var_name)
2231 } else if key.kind == SymKind::Pack {
2232 self.find_definition(key.pkg.as_ref())
2235 .or_else(|| self.find_definition(key.name.as_ref()))
2236 } else {
2237 let qualified_name = format!("{}::{}", key.pkg, key.name);
2239 self.find_definition(&qualified_name)
2240 }
2241 }
2242
2243 pub fn find_refs(&self, key: &SymbolKey) -> Vec<Location> {
2266 let files_locked = self.files.read();
2267 let mut all_refs = if let Some(sigil) = key.sigil {
2268 let var_name = format!("{}{}", sigil, key.name);
2270 let mut refs = Vec::new();
2271 for (_uri_key, file_index) in files_locked.iter() {
2272 if let Some(var_refs) = file_index.references.get(&var_name) {
2273 for reference in var_refs {
2274 refs.push(Location { uri: reference.uri.clone(), range: reference.range });
2275 }
2276 }
2277 }
2278 refs
2279 } else {
2280 if key.pkg.as_ref() == "main" {
2282 let mut refs = self.find_references(&format!("main::{}", key.name));
2284 for (_uri_key, file_index) in files_locked.iter() {
2286 if let Some(bare_refs) = file_index.references.get(key.name.as_ref()) {
2287 for reference in bare_refs {
2288 refs.push(Location {
2289 uri: reference.uri.clone(),
2290 range: reference.range,
2291 });
2292 }
2293 }
2294 }
2295 refs
2296 } else {
2297 let qualified_name = format!("{}::{}", key.pkg, key.name);
2298 self.find_references(&qualified_name)
2299 }
2300 };
2301 drop(files_locked);
2302
2303 if let Some(def) = self.find_def(key) {
2305 all_refs.retain(|loc| !(loc.uri == def.uri && loc.range == def.range));
2306 }
2307
2308 let mut seen = HashSet::new();
2310 all_refs.retain(|loc| {
2311 seen.insert((
2312 loc.uri.clone(),
2313 loc.range.start.line,
2314 loc.range.start.column,
2315 loc.range.end.line,
2316 loc.range.end.column,
2317 ))
2318 });
2319
2320 all_refs
2321 }
2322}
2323
2324struct IndexVisitor {
2326 document: Document,
2327 uri: String,
2328 current_package: Option<String>,
2329}
2330
2331fn is_interpolated_var_start(byte: u8) -> bool {
2332 byte.is_ascii_alphabetic() || byte == b'_'
2333}
2334
2335fn is_interpolated_var_continue(byte: u8) -> bool {
2336 byte.is_ascii_alphanumeric() || byte == b'_' || byte == b':'
2337}
2338
2339fn has_escaped_interpolation_marker(bytes: &[u8], index: usize) -> bool {
2340 if index == 0 {
2341 return false;
2342 }
2343
2344 let mut backslashes = 0usize;
2345 let mut cursor = index;
2346 while cursor > 0 && bytes[cursor - 1] == b'\\' {
2347 backslashes += 1;
2348 cursor -= 1;
2349 }
2350
2351 backslashes % 2 == 1
2352}
2353
2354fn strip_matching_quote_delimiters(raw_content: &str) -> &str {
2355 if raw_content.len() < 2 {
2356 return raw_content;
2357 }
2358
2359 let bytes = raw_content.as_bytes();
2360 match (bytes.first(), bytes.last()) {
2361 (Some(b'"'), Some(b'"')) | (Some(b'\''), Some(b'\'')) => {
2362 &raw_content[1..raw_content.len() - 1]
2363 }
2364 _ => raw_content,
2365 }
2366}
2367
2368impl IndexVisitor {
2369 fn new(document: &mut Document, uri: String) -> Self {
2370 Self { document: document.clone(), uri, current_package: Some("main".to_string()) }
2371 }
2372
2373 fn visit(&mut self, node: &Node, file_index: &mut FileIndex) {
2374 self.visit_node(node, file_index);
2375 }
2376
2377 fn record_interpolated_variable_references(
2378 &self,
2379 raw_content: &str,
2380 range: Range,
2381 file_index: &mut FileIndex,
2382 ) {
2383 let content = strip_matching_quote_delimiters(raw_content);
2384 let bytes = content.as_bytes();
2385 let mut index = 0;
2386
2387 while index < bytes.len() {
2388 if has_escaped_interpolation_marker(bytes, index) {
2389 index += 1;
2390 continue;
2391 }
2392
2393 let sigil = match bytes[index] {
2394 b'$' => "$",
2395 b'@' => "@",
2396 _ => {
2397 index += 1;
2398 continue;
2399 }
2400 };
2401
2402 if index + 1 >= bytes.len() {
2403 break;
2404 }
2405
2406 let (start, needs_closing_brace) =
2407 if bytes[index + 1] == b'{' { (index + 2, true) } else { (index + 1, false) };
2408
2409 if start >= bytes.len() || !is_interpolated_var_start(bytes[start]) {
2410 index += 1;
2411 continue;
2412 }
2413
2414 let mut end = start + 1;
2415 while end < bytes.len() && is_interpolated_var_continue(bytes[end]) {
2416 end += 1;
2417 }
2418
2419 if needs_closing_brace && (end >= bytes.len() || bytes[end] != b'}') {
2420 index += 1;
2421 continue;
2422 }
2423
2424 if let Some(name) = content.get(start..end) {
2425 let var_name = format!("{sigil}{name}");
2426 file_index.references.entry(var_name).or_default().push(SymbolReference {
2427 uri: self.uri.clone(),
2428 range,
2429 kind: ReferenceKind::Read,
2430 });
2431 }
2432
2433 index = if needs_closing_brace { end + 1 } else { end };
2434 }
2435 }
2436
2437 fn visit_node(&mut self, node: &Node, file_index: &mut FileIndex) {
2438 match &node.kind {
2439 NodeKind::Package { name, .. } => {
2440 let package_name = name.clone();
2441
2442 self.current_package = Some(package_name.clone());
2444
2445 file_index.symbols.push(WorkspaceSymbol {
2446 name: package_name.clone(),
2447 kind: SymbolKind::Package,
2448 uri: self.uri.clone(),
2449 range: self.node_to_range(node),
2450 qualified_name: Some(package_name),
2451 documentation: None,
2452 container_name: None,
2453 has_body: true,
2454 });
2455 }
2456
2457 NodeKind::Subroutine { name, body, .. } => {
2458 if let Some(name_str) = name.clone() {
2459 let qualified_name = if let Some(ref pkg) = self.current_package {
2460 format!("{}::{}", pkg, name_str)
2461 } else {
2462 name_str.clone()
2463 };
2464
2465 let existing_symbol_idx = file_index.symbols.iter().position(|s| {
2467 s.name == name_str && s.container_name == self.current_package
2468 });
2469
2470 if let Some(idx) = existing_symbol_idx {
2471 file_index.symbols[idx].range = self.node_to_range(node);
2473 } else {
2474 file_index.symbols.push(WorkspaceSymbol {
2476 name: name_str.clone(),
2477 kind: SymbolKind::Subroutine,
2478 uri: self.uri.clone(),
2479 range: self.node_to_range(node),
2480 qualified_name: Some(qualified_name),
2481 documentation: None,
2482 container_name: self.current_package.clone(),
2483 has_body: true, });
2485 }
2486
2487 file_index.references.entry(name_str.clone()).or_default().push(
2489 SymbolReference {
2490 uri: self.uri.clone(),
2491 range: self.node_to_range(node),
2492 kind: ReferenceKind::Definition,
2493 },
2494 );
2495 }
2496
2497 self.visit_node(body, file_index);
2499 }
2500
2501 NodeKind::VariableDeclaration { variable, initializer, .. } => {
2502 if let NodeKind::Variable { sigil, name } = &variable.kind {
2503 let var_name = format!("{}{}", sigil, name);
2504
2505 file_index.symbols.push(WorkspaceSymbol {
2506 name: var_name.clone(),
2507 kind: SymbolKind::Variable(sigil_to_var_kind(sigil)),
2508 uri: self.uri.clone(),
2509 range: self.node_to_range(variable),
2510 qualified_name: None,
2511 documentation: None,
2512 container_name: self.current_package.clone(),
2513 has_body: true, });
2515
2516 file_index.references.entry(var_name.clone()).or_default().push(
2518 SymbolReference {
2519 uri: self.uri.clone(),
2520 range: self.node_to_range(variable),
2521 kind: ReferenceKind::Definition,
2522 },
2523 );
2524 }
2525
2526 if let Some(init) = initializer {
2528 self.visit_node(init, file_index);
2529 }
2530 }
2531
2532 NodeKind::VariableListDeclaration { variables, initializer, .. } => {
2533 for var in variables {
2535 if let NodeKind::Variable { sigil, name } = &var.kind {
2536 let var_name = format!("{}{}", sigil, name);
2537
2538 file_index.symbols.push(WorkspaceSymbol {
2539 name: var_name.clone(),
2540 kind: SymbolKind::Variable(sigil_to_var_kind(sigil)),
2541 uri: self.uri.clone(),
2542 range: self.node_to_range(var),
2543 qualified_name: None,
2544 documentation: None,
2545 container_name: self.current_package.clone(),
2546 has_body: true,
2547 });
2548
2549 file_index.references.entry(var_name).or_default().push(SymbolReference {
2551 uri: self.uri.clone(),
2552 range: self.node_to_range(var),
2553 kind: ReferenceKind::Definition,
2554 });
2555 }
2556 }
2557
2558 if let Some(init) = initializer {
2560 self.visit_node(init, file_index);
2561 }
2562 }
2563
2564 NodeKind::Variable { sigil, name } => {
2565 let var_name = format!("{}{}", sigil, name);
2566
2567 file_index.references.entry(var_name).or_default().push(SymbolReference {
2569 uri: self.uri.clone(),
2570 range: self.node_to_range(node),
2571 kind: ReferenceKind::Read, });
2573 }
2574
2575 NodeKind::FunctionCall { name, args, .. } => {
2576 let func_name = name.clone();
2577 let location = self.node_to_range(node);
2578
2579 let (pkg, bare_name) = if let Some(idx) = func_name.rfind("::") {
2581 (&func_name[..idx], &func_name[idx + 2..])
2582 } else {
2583 (self.current_package.as_deref().unwrap_or("main"), func_name.as_str())
2584 };
2585
2586 let qualified = format!("{}::{}", pkg, bare_name);
2587
2588 file_index.references.entry(bare_name.to_string()).or_default().push(
2592 SymbolReference {
2593 uri: self.uri.clone(),
2594 range: location,
2595 kind: ReferenceKind::Usage,
2596 },
2597 );
2598 file_index.references.entry(qualified).or_default().push(SymbolReference {
2599 uri: self.uri.clone(),
2600 range: location,
2601 kind: ReferenceKind::Usage,
2602 });
2603
2604 for arg in args {
2606 self.visit_node(arg, file_index);
2607 }
2608 }
2609
2610 NodeKind::Use { module, args, .. } => {
2611 let module_name = module.clone();
2612 file_index.dependencies.insert(module_name.clone());
2613
2614 if module == "parent" || module == "base" {
2618 for name in extract_module_names_from_use_args(args) {
2619 file_index.dependencies.insert(name);
2620 }
2621 }
2622
2623 file_index.references.entry(module_name).or_default().push(SymbolReference {
2625 uri: self.uri.clone(),
2626 range: self.node_to_range(node),
2627 kind: ReferenceKind::Import,
2628 });
2629 }
2630
2631 NodeKind::Assignment { lhs, rhs, op } => {
2633 let is_compound = op != "=";
2635
2636 if let NodeKind::Variable { sigil, name } = &lhs.kind {
2637 let var_name = format!("{}{}", sigil, name);
2638
2639 if is_compound {
2641 file_index.references.entry(var_name.clone()).or_default().push(
2642 SymbolReference {
2643 uri: self.uri.clone(),
2644 range: self.node_to_range(lhs),
2645 kind: ReferenceKind::Read,
2646 },
2647 );
2648 }
2649
2650 file_index.references.entry(var_name).or_default().push(SymbolReference {
2652 uri: self.uri.clone(),
2653 range: self.node_to_range(lhs),
2654 kind: ReferenceKind::Write,
2655 });
2656 }
2657
2658 self.visit_node(rhs, file_index);
2660 }
2661
2662 NodeKind::Block { statements } => {
2664 for stmt in statements {
2665 self.visit_node(stmt, file_index);
2666 }
2667 }
2668
2669 NodeKind::If { condition, then_branch, elsif_branches, else_branch } => {
2670 self.visit_node(condition, file_index);
2671 self.visit_node(then_branch, file_index);
2672 for (cond, branch) in elsif_branches {
2673 self.visit_node(cond, file_index);
2674 self.visit_node(branch, file_index);
2675 }
2676 if let Some(else_br) = else_branch {
2677 self.visit_node(else_br, file_index);
2678 }
2679 }
2680
2681 NodeKind::While { condition, body, continue_block } => {
2682 self.visit_node(condition, file_index);
2683 self.visit_node(body, file_index);
2684 if let Some(cont) = continue_block {
2685 self.visit_node(cont, file_index);
2686 }
2687 }
2688
2689 NodeKind::For { init, condition, update, body, continue_block } => {
2690 if let Some(i) = init {
2691 self.visit_node(i, file_index);
2692 }
2693 if let Some(c) = condition {
2694 self.visit_node(c, file_index);
2695 }
2696 if let Some(u) = update {
2697 self.visit_node(u, file_index);
2698 }
2699 self.visit_node(body, file_index);
2700 if let Some(cont) = continue_block {
2701 self.visit_node(cont, file_index);
2702 }
2703 }
2704
2705 NodeKind::Foreach { variable, list, body, continue_block } => {
2706 if let Some(cb) = continue_block {
2708 self.visit_node(cb, file_index);
2709 }
2710 if let NodeKind::Variable { sigil, name } = &variable.kind {
2711 let var_name = format!("{}{}", sigil, name);
2712 file_index.references.entry(var_name).or_default().push(SymbolReference {
2713 uri: self.uri.clone(),
2714 range: self.node_to_range(variable),
2715 kind: ReferenceKind::Write,
2716 });
2717 }
2718 self.visit_node(variable, file_index);
2719 self.visit_node(list, file_index);
2720 self.visit_node(body, file_index);
2721 }
2722
2723 NodeKind::MethodCall { object, method, args } => {
2724 let qualified_method = if let NodeKind::Identifier { name } = &object.kind {
2726 Some(format!("{}::{}", name, method))
2728 } else {
2729 None
2731 };
2732
2733 self.visit_node(object, file_index);
2735
2736 let method_key = qualified_method.as_ref().unwrap_or(method);
2738 file_index.references.entry(method_key.clone()).or_default().push(
2739 SymbolReference {
2740 uri: self.uri.clone(),
2741 range: self.node_to_range(node),
2742 kind: ReferenceKind::Usage,
2743 },
2744 );
2745
2746 for arg in args {
2748 self.visit_node(arg, file_index);
2749 }
2750 }
2751
2752 NodeKind::No { module, .. } => {
2753 let module_name = module.clone();
2754 file_index.dependencies.insert(module_name.clone());
2755 }
2756
2757 NodeKind::Class { name, .. } => {
2758 let class_name = name.clone();
2759 self.current_package = Some(class_name.clone());
2760
2761 file_index.symbols.push(WorkspaceSymbol {
2762 name: class_name.clone(),
2763 kind: SymbolKind::Class,
2764 uri: self.uri.clone(),
2765 range: self.node_to_range(node),
2766 qualified_name: Some(class_name),
2767 documentation: None,
2768 container_name: None,
2769 has_body: true,
2770 });
2771 }
2772
2773 NodeKind::Method { name, body, signature, .. } => {
2774 let method_name = name.clone();
2775 let qualified_name = if let Some(ref pkg) = self.current_package {
2776 format!("{}::{}", pkg, method_name)
2777 } else {
2778 method_name.clone()
2779 };
2780
2781 file_index.symbols.push(WorkspaceSymbol {
2782 name: method_name.clone(),
2783 kind: SymbolKind::Method,
2784 uri: self.uri.clone(),
2785 range: self.node_to_range(node),
2786 qualified_name: Some(qualified_name),
2787 documentation: None,
2788 container_name: self.current_package.clone(),
2789 has_body: true,
2790 });
2791
2792 if let Some(sig) = signature {
2794 if let NodeKind::Signature { parameters } = &sig.kind {
2795 for param in parameters {
2796 self.visit_node(param, file_index);
2797 }
2798 }
2799 }
2800
2801 self.visit_node(body, file_index);
2803 }
2804
2805 NodeKind::String { value, interpolated } => {
2806 if *interpolated {
2807 let range = self.node_to_range(node);
2808 self.record_interpolated_variable_references(value, range, file_index);
2809 }
2810 }
2811
2812 NodeKind::Heredoc { content, interpolated, .. } => {
2813 if *interpolated {
2814 let range = self.node_to_range(node);
2815 self.record_interpolated_variable_references(content, range, file_index);
2816 }
2817 }
2818
2819 NodeKind::Unary { op, operand } if op == "++" || op == "--" => {
2821 if let NodeKind::Variable { sigil, name } = &operand.kind {
2823 let var_name = format!("{}{}", sigil, name);
2824
2825 file_index.references.entry(var_name.clone()).or_default().push(
2827 SymbolReference {
2828 uri: self.uri.clone(),
2829 range: self.node_to_range(operand),
2830 kind: ReferenceKind::Read,
2831 },
2832 );
2833
2834 file_index.references.entry(var_name).or_default().push(SymbolReference {
2835 uri: self.uri.clone(),
2836 range: self.node_to_range(operand),
2837 kind: ReferenceKind::Write,
2838 });
2839 }
2840 }
2841
2842 _ => {
2843 self.visit_children(node, file_index);
2845 }
2846 }
2847 }
2848
2849 fn visit_children(&mut self, node: &Node, file_index: &mut FileIndex) {
2850 match &node.kind {
2852 NodeKind::Program { statements } => {
2853 for stmt in statements {
2854 self.visit_node(stmt, file_index);
2855 }
2856 }
2857 NodeKind::ExpressionStatement { expression } => {
2858 self.visit_node(expression, file_index);
2859 }
2860 NodeKind::Unary { operand, .. } => {
2862 self.visit_node(operand, file_index);
2863 }
2864 NodeKind::Binary { left, right, .. } => {
2865 self.visit_node(left, file_index);
2866 self.visit_node(right, file_index);
2867 }
2868 NodeKind::Ternary { condition, then_expr, else_expr } => {
2869 self.visit_node(condition, file_index);
2870 self.visit_node(then_expr, file_index);
2871 self.visit_node(else_expr, file_index);
2872 }
2873 NodeKind::ArrayLiteral { elements } => {
2874 for elem in elements {
2875 self.visit_node(elem, file_index);
2876 }
2877 }
2878 NodeKind::HashLiteral { pairs } => {
2879 for (key, value) in pairs {
2880 self.visit_node(key, file_index);
2881 self.visit_node(value, file_index);
2882 }
2883 }
2884 NodeKind::Return { value } => {
2885 if let Some(val) = value {
2886 self.visit_node(val, file_index);
2887 }
2888 }
2889 NodeKind::Eval { block } | NodeKind::Do { block } => {
2890 self.visit_node(block, file_index);
2891 }
2892 NodeKind::Try { body, catch_blocks, finally_block } => {
2893 self.visit_node(body, file_index);
2894 for (_, block) in catch_blocks {
2895 self.visit_node(block, file_index);
2896 }
2897 if let Some(finally) = finally_block {
2898 self.visit_node(finally, file_index);
2899 }
2900 }
2901 NodeKind::Given { expr, body } => {
2902 self.visit_node(expr, file_index);
2903 self.visit_node(body, file_index);
2904 }
2905 NodeKind::When { condition, body } => {
2906 self.visit_node(condition, file_index);
2907 self.visit_node(body, file_index);
2908 }
2909 NodeKind::Default { body } => {
2910 self.visit_node(body, file_index);
2911 }
2912 NodeKind::StatementModifier { statement, condition, .. } => {
2913 self.visit_node(statement, file_index);
2914 self.visit_node(condition, file_index);
2915 }
2916 NodeKind::VariableWithAttributes { variable, .. } => {
2917 self.visit_node(variable, file_index);
2918 }
2919 NodeKind::LabeledStatement { statement, .. } => {
2920 self.visit_node(statement, file_index);
2921 }
2922 _ => {
2923 }
2925 }
2926 }
2927
2928 fn node_to_range(&mut self, node: &Node) -> Range {
2929 let ((start_line, start_col), (end_line, end_col)) =
2931 self.document.line_index.range(node.location.start, node.location.end);
2932 Range {
2934 start: Position { byte: node.location.start, line: start_line, column: start_col },
2935 end: Position { byte: node.location.end, line: end_line, column: end_col },
2936 }
2937 }
2938}
2939
2940fn extract_module_names_from_use_args(args: &[String]) -> Vec<String> {
2950 let joined = args.join(" ");
2951
2952 let inner = if let Some(start) = joined.find("qw(") {
2954 if let Some(end) = joined[start..].find(')') {
2955 joined[start + 3..start + end].to_string()
2956 } else {
2957 joined.clone()
2958 }
2959 } else {
2960 joined.clone()
2961 };
2962
2963 inner
2964 .split_whitespace()
2965 .filter_map(|token| {
2966 if token.starts_with('-') {
2968 return None;
2969 }
2970 let stripped = token.trim_matches('\'').trim_matches('"');
2972 let stripped = stripped.trim_matches('(').trim_matches(')');
2974 let stripped = stripped.trim_matches('\'').trim_matches('"');
2975 if stripped.is_empty() {
2977 return None;
2978 }
2979 if stripped.chars().all(|c| c.is_alphanumeric() || c == '_' || c == ':' || c == '\'') {
2980 Some(stripped.to_string())
2981 } else {
2982 None
2983 }
2984 })
2985 .collect()
2986}
2987
2988impl Default for WorkspaceIndex {
2989 fn default() -> Self {
2990 Self::new()
2991 }
2992}
2993
2994#[cfg(all(feature = "workspace", feature = "lsp-compat"))]
2996pub mod lsp_adapter {
2998 use super::Location as IxLocation;
2999 use lsp_types::Location as LspLocation;
3000 type LspUrl = lsp_types::Uri;
3002
3003 pub fn to_lsp_location(ix: &IxLocation) -> Option<LspLocation> {
3023 parse_url(&ix.uri).map(|uri| {
3024 let start =
3025 lsp_types::Position { line: ix.range.start.line, character: ix.range.start.column };
3026 let end =
3027 lsp_types::Position { line: ix.range.end.line, character: ix.range.end.column };
3028 let range = lsp_types::Range { start, end };
3029 LspLocation { uri, range }
3030 })
3031 }
3032
3033 pub fn to_lsp_locations(all: impl IntoIterator<Item = IxLocation>) -> Vec<LspLocation> {
3054 all.into_iter().filter_map(|ix| to_lsp_location(&ix)).collect()
3055 }
3056
3057 #[cfg(not(target_arch = "wasm32"))]
3058 fn parse_url(s: &str) -> Option<LspUrl> {
3059 use std::str::FromStr;
3061
3062 LspUrl::from_str(s).ok().or_else(|| {
3064 std::path::Path::new(s).canonicalize().ok().and_then(|p| {
3066 crate::workspace_index::fs_path_to_uri(&p)
3068 .ok()
3069 .and_then(|uri_string| LspUrl::from_str(&uri_string).ok())
3070 })
3071 })
3072 }
3073
3074 #[cfg(target_arch = "wasm32")]
3076 fn parse_url(s: &str) -> Option<LspUrl> {
3077 use std::str::FromStr;
3078 LspUrl::from_str(s).ok()
3079 }
3080}
3081
3082#[cfg(test)]
3083mod tests {
3084 use super::*;
3085 use perl_tdd_support::{must, must_some};
3086
3087 #[test]
3088 fn test_basic_indexing() {
3089 let index = WorkspaceIndex::new();
3090 let uri = "file:///test.pl";
3091
3092 let code = r#"
3093package MyPackage;
3094
3095sub hello {
3096 print "Hello";
3097}
3098
3099my $var = 42;
3100"#;
3101
3102 must(index.index_file(must(url::Url::parse(uri)), code.to_string()));
3103
3104 let symbols = index.file_symbols(uri);
3106 assert!(symbols.iter().any(|s| s.name == "MyPackage" && s.kind == SymbolKind::Package));
3107 assert!(symbols.iter().any(|s| s.name == "hello" && s.kind == SymbolKind::Subroutine));
3108 assert!(symbols.iter().any(|s| s.name == "$var" && s.kind.is_variable()));
3109 }
3110
3111 #[test]
3112 fn test_find_references() {
3113 let index = WorkspaceIndex::new();
3114 let uri = "file:///test.pl";
3115
3116 let code = r#"
3117sub test {
3118 my $x = 1;
3119 $x = 2;
3120 print $x;
3121}
3122"#;
3123
3124 must(index.index_file(must(url::Url::parse(uri)), code.to_string()));
3125
3126 let refs = index.find_references("$x");
3127 assert!(refs.len() >= 2); }
3129
3130 #[test]
3131 fn test_dependencies() {
3132 let index = WorkspaceIndex::new();
3133 let uri = "file:///test.pl";
3134
3135 let code = r#"
3136use strict;
3137use warnings;
3138use Data::Dumper;
3139"#;
3140
3141 must(index.index_file(must(url::Url::parse(uri)), code.to_string()));
3142
3143 let deps = index.file_dependencies(uri);
3144 assert!(deps.contains("strict"));
3145 assert!(deps.contains("warnings"));
3146 assert!(deps.contains("Data::Dumper"));
3147 }
3148
3149 #[test]
3150 fn test_uri_to_fs_path_basic() {
3151 if let Some(path) = uri_to_fs_path("file:///tmp/test.pl") {
3153 assert_eq!(path, std::path::PathBuf::from("/tmp/test.pl"));
3154 }
3155
3156 assert!(uri_to_fs_path("not-a-uri").is_none());
3158
3159 assert!(uri_to_fs_path("http://example.com").is_none());
3161 }
3162
3163 #[test]
3164 fn test_uri_to_fs_path_with_spaces() {
3165 if let Some(path) = uri_to_fs_path("file:///tmp/path%20with%20spaces/test.pl") {
3167 assert_eq!(path, std::path::PathBuf::from("/tmp/path with spaces/test.pl"));
3168 }
3169
3170 if let Some(path) = uri_to_fs_path("file:///tmp/My%20Documents/test%20file.pl") {
3172 assert_eq!(path, std::path::PathBuf::from("/tmp/My Documents/test file.pl"));
3173 }
3174 }
3175
3176 #[test]
3177 fn test_uri_to_fs_path_with_unicode() {
3178 if let Some(path) = uri_to_fs_path("file:///tmp/caf%C3%A9/test.pl") {
3180 assert_eq!(path, std::path::PathBuf::from("/tmp/café/test.pl"));
3181 }
3182
3183 if let Some(path) = uri_to_fs_path("file:///tmp/emoji%F0%9F%98%80/test.pl") {
3185 assert_eq!(path, std::path::PathBuf::from("/tmp/emoji😀/test.pl"));
3186 }
3187 }
3188
3189 #[test]
3190 fn test_fs_path_to_uri_basic() {
3191 let result = fs_path_to_uri("/tmp/test.pl");
3193 assert!(result.is_ok());
3194 let uri = must(result);
3195 assert!(uri.starts_with("file://"));
3196 assert!(uri.contains("/tmp/test.pl"));
3197 }
3198
3199 #[test]
3200 fn test_fs_path_to_uri_with_spaces() {
3201 let result = fs_path_to_uri("/tmp/path with spaces/test.pl");
3203 assert!(result.is_ok());
3204 let uri = must(result);
3205 assert!(uri.starts_with("file://"));
3206 assert!(uri.contains("path%20with%20spaces"));
3208 }
3209
3210 #[test]
3211 fn test_fs_path_to_uri_with_unicode() {
3212 let result = fs_path_to_uri("/tmp/café/test.pl");
3214 assert!(result.is_ok());
3215 let uri = must(result);
3216 assert!(uri.starts_with("file://"));
3217 assert!(uri.contains("caf%C3%A9"));
3219 }
3220
3221 #[test]
3222 fn test_normalize_uri_file_schemes() {
3223 let uri = WorkspaceIndex::normalize_uri("file:///tmp/test.pl");
3225 assert_eq!(uri, "file:///tmp/test.pl");
3226
3227 let uri = WorkspaceIndex::normalize_uri("file:///tmp/path%20with%20spaces/test.pl");
3229 assert_eq!(uri, "file:///tmp/path%20with%20spaces/test.pl");
3230 }
3231
3232 #[test]
3233 fn test_normalize_uri_absolute_paths() {
3234 let uri = WorkspaceIndex::normalize_uri("/tmp/test.pl");
3236 assert!(uri.starts_with("file://"));
3237 assert!(uri.contains("/tmp/test.pl"));
3238 }
3239
3240 #[test]
3241 fn test_normalize_uri_special_schemes() {
3242 let uri = WorkspaceIndex::normalize_uri("untitled:Untitled-1");
3244 assert_eq!(uri, "untitled:Untitled-1");
3245 }
3246
3247 #[test]
3248 fn test_roundtrip_conversion() {
3249 let original_uri = "file:///tmp/path%20with%20spaces/caf%C3%A9.pl";
3251
3252 if let Some(path) = uri_to_fs_path(original_uri) {
3253 if let Ok(converted_uri) = fs_path_to_uri(&path) {
3254 assert!(converted_uri.starts_with("file://"));
3256
3257 if let Some(roundtrip_path) = uri_to_fs_path(&converted_uri) {
3259 #[cfg(windows)]
3260 if let Ok(rootless) = path.strip_prefix(std::path::Path::new(r"\")) {
3261 assert!(roundtrip_path.ends_with(rootless));
3262 } else {
3263 assert_eq!(path, roundtrip_path);
3264 }
3265
3266 #[cfg(not(windows))]
3267 assert_eq!(path, roundtrip_path);
3268 }
3269 }
3270 }
3271 }
3272
3273 #[cfg(target_os = "windows")]
3274 #[test]
3275 fn test_windows_paths() {
3276 let result = fs_path_to_uri(r"C:\Users\test\Documents\script.pl");
3278 assert!(result.is_ok());
3279 let uri = must(result);
3280 assert!(uri.starts_with("file://"));
3281
3282 let result = fs_path_to_uri(r"C:\Program Files\My App\script.pl");
3284 assert!(result.is_ok());
3285 let uri = must(result);
3286 assert!(uri.starts_with("file://"));
3287 assert!(uri.contains("Program%20Files"));
3288 }
3289
3290 #[test]
3295 fn test_coordinator_initial_state() {
3296 let coordinator = IndexCoordinator::new();
3297 assert!(matches!(
3298 coordinator.state(),
3299 IndexState::Building { phase: IndexPhase::Idle, .. }
3300 ));
3301 }
3302
3303 #[test]
3304 fn test_transition_to_scanning_phase() {
3305 let coordinator = IndexCoordinator::new();
3306 coordinator.transition_to_scanning();
3307
3308 let state = coordinator.state();
3309 assert!(
3310 matches!(state, IndexState::Building { phase: IndexPhase::Scanning, .. }),
3311 "Expected Building state after scanning, got: {:?}",
3312 state
3313 );
3314 }
3315
3316 #[test]
3317 fn test_transition_to_indexing_phase() {
3318 let coordinator = IndexCoordinator::new();
3319 coordinator.transition_to_scanning();
3320 coordinator.update_scan_progress(3);
3321 coordinator.transition_to_indexing(3);
3322
3323 let state = coordinator.state();
3324 assert!(
3325 matches!(
3326 state,
3327 IndexState::Building { phase: IndexPhase::Indexing, total_count: 3, .. }
3328 ),
3329 "Expected Building state after indexing with total_count 3, got: {:?}",
3330 state
3331 );
3332 }
3333
3334 #[test]
3335 fn test_transition_to_ready() {
3336 let coordinator = IndexCoordinator::new();
3337 coordinator.transition_to_ready(100, 5000);
3338
3339 let state = coordinator.state();
3340 if let IndexState::Ready { file_count, symbol_count, .. } = state {
3341 assert_eq!(file_count, 100);
3342 assert_eq!(symbol_count, 5000);
3343 } else {
3344 unreachable!("Expected Ready state, got: {:?}", state);
3345 }
3346 }
3347
3348 #[test]
3349 fn test_parse_storm_degradation() {
3350 let coordinator = IndexCoordinator::new();
3351 coordinator.transition_to_ready(100, 5000);
3352
3353 for _ in 0..15 {
3355 coordinator.notify_change("file.pm");
3356 }
3357
3358 let state = coordinator.state();
3359 assert!(
3360 matches!(state, IndexState::Degraded { .. }),
3361 "Expected Degraded state, got: {:?}",
3362 state
3363 );
3364 if let IndexState::Degraded { reason, .. } = state {
3365 assert!(matches!(reason, DegradationReason::ParseStorm { .. }));
3366 }
3367 }
3368
3369 #[test]
3370 fn test_recovery_from_parse_storm() {
3371 let coordinator = IndexCoordinator::new();
3372 coordinator.transition_to_ready(100, 5000);
3373
3374 for _ in 0..15 {
3376 coordinator.notify_change("file.pm");
3377 }
3378
3379 for _ in 0..15 {
3381 coordinator.notify_parse_complete("file.pm");
3382 }
3383
3384 assert!(matches!(coordinator.state(), IndexState::Building { .. }));
3386 }
3387
3388 #[test]
3389 fn test_query_dispatch_ready() {
3390 let coordinator = IndexCoordinator::new();
3391 coordinator.transition_to_ready(100, 5000);
3392
3393 let result = coordinator.query(|_index| "full_query", |_index| "partial_query");
3394
3395 assert_eq!(result, "full_query");
3396 }
3397
3398 #[test]
3399 fn test_query_dispatch_degraded() {
3400 let coordinator = IndexCoordinator::new();
3401 let result = coordinator.query(|_index| "full_query", |_index| "partial_query");
3404
3405 assert_eq!(result, "partial_query");
3406 }
3407
3408 #[test]
3409 fn test_metrics_pending_count() {
3410 let coordinator = IndexCoordinator::new();
3411
3412 coordinator.notify_change("file1.pm");
3413 coordinator.notify_change("file2.pm");
3414
3415 assert_eq!(coordinator.metrics.pending_count(), 2);
3416
3417 coordinator.notify_parse_complete("file1.pm");
3418 assert_eq!(coordinator.metrics.pending_count(), 1);
3419 }
3420
3421 #[test]
3422 fn test_instrumentation_records_transitions() {
3423 let coordinator = IndexCoordinator::new();
3424 coordinator.transition_to_ready(10, 100);
3425
3426 let snapshot = coordinator.instrumentation_snapshot();
3427 let transition =
3428 IndexStateTransition { from: IndexStateKind::Building, to: IndexStateKind::Ready };
3429 let count = snapshot.state_transition_counts.get(&transition).copied().unwrap_or(0);
3430 assert_eq!(count, 1);
3431 }
3432
3433 #[test]
3434 fn test_instrumentation_records_early_exit() {
3435 let coordinator = IndexCoordinator::new();
3436 coordinator.record_early_exit(EarlyExitReason::InitialTimeBudget, 25, 1, 10);
3437
3438 let snapshot = coordinator.instrumentation_snapshot();
3439 let count = snapshot
3440 .early_exit_counts
3441 .get(&EarlyExitReason::InitialTimeBudget)
3442 .copied()
3443 .unwrap_or(0);
3444 assert_eq!(count, 1);
3445 assert!(snapshot.last_early_exit.is_some());
3446 }
3447
3448 #[test]
3449 fn test_custom_limits() {
3450 let limits = IndexResourceLimits {
3451 max_files: 5000,
3452 max_symbols_per_file: 1000,
3453 max_total_symbols: 100_000,
3454 max_ast_cache_bytes: 128 * 1024 * 1024,
3455 max_ast_cache_items: 50,
3456 max_scan_duration_ms: 30_000,
3457 };
3458
3459 let coordinator = IndexCoordinator::with_limits(limits.clone());
3460 assert_eq!(coordinator.limits.max_files, 5000);
3461 assert_eq!(coordinator.limits.max_total_symbols, 100_000);
3462 }
3463
3464 #[test]
3465 fn test_degradation_preserves_symbol_count() {
3466 let coordinator = IndexCoordinator::new();
3467 coordinator.transition_to_ready(100, 5000);
3468
3469 coordinator.transition_to_degraded(DegradationReason::IoError {
3470 message: "Test error".to_string(),
3471 });
3472
3473 let state = coordinator.state();
3474 assert!(
3475 matches!(state, IndexState::Degraded { .. }),
3476 "Expected Degraded state, got: {:?}",
3477 state
3478 );
3479 if let IndexState::Degraded { available_symbols, .. } = state {
3480 assert_eq!(available_symbols, 5000);
3481 }
3482 }
3483
3484 #[test]
3485 fn test_index_access() {
3486 let coordinator = IndexCoordinator::new();
3487 let index = coordinator.index();
3488
3489 assert!(index.all_symbols().is_empty());
3491 }
3492
3493 #[test]
3494 fn test_resource_limit_enforcement_max_files() {
3495 let limits = IndexResourceLimits {
3496 max_files: 5,
3497 max_symbols_per_file: 1000,
3498 max_total_symbols: 50_000,
3499 max_ast_cache_bytes: 128 * 1024 * 1024,
3500 max_ast_cache_items: 50,
3501 max_scan_duration_ms: 30_000,
3502 };
3503
3504 let coordinator = IndexCoordinator::with_limits(limits);
3505 coordinator.transition_to_ready(10, 100);
3506
3507 for i in 0..10 {
3509 let uri_str = format!("file:///test{}.pl", i);
3510 let uri = must(url::Url::parse(&uri_str));
3511 let code = "sub test { }";
3512 must(coordinator.index().index_file(uri, code.to_string()));
3513 }
3514
3515 coordinator.enforce_limits();
3517
3518 let state = coordinator.state();
3519 assert!(
3520 matches!(
3521 state,
3522 IndexState::Degraded {
3523 reason: DegradationReason::ResourceLimit { kind: ResourceKind::MaxFiles },
3524 ..
3525 }
3526 ),
3527 "Expected Degraded state with ResourceLimit(MaxFiles), got: {:?}",
3528 state
3529 );
3530 }
3531
3532 #[test]
3533 fn test_resource_limit_enforcement_max_symbols() {
3534 let limits = IndexResourceLimits {
3535 max_files: 100,
3536 max_symbols_per_file: 10,
3537 max_total_symbols: 50, max_ast_cache_bytes: 128 * 1024 * 1024,
3539 max_ast_cache_items: 50,
3540 max_scan_duration_ms: 30_000,
3541 };
3542
3543 let coordinator = IndexCoordinator::with_limits(limits);
3544 coordinator.transition_to_ready(0, 0);
3545
3546 for i in 0..10 {
3548 let uri_str = format!("file:///test{}.pl", i);
3549 let uri = must(url::Url::parse(&uri_str));
3550 let code = r#"
3552package Test;
3553sub sub1 { }
3554sub sub2 { }
3555sub sub3 { }
3556sub sub4 { }
3557sub sub5 { }
3558sub sub6 { }
3559sub sub7 { }
3560sub sub8 { }
3561sub sub9 { }
3562sub sub10 { }
3563"#;
3564 must(coordinator.index().index_file(uri, code.to_string()));
3565 }
3566
3567 coordinator.enforce_limits();
3569
3570 let state = coordinator.state();
3571 assert!(
3572 matches!(
3573 state,
3574 IndexState::Degraded {
3575 reason: DegradationReason::ResourceLimit { kind: ResourceKind::MaxSymbols },
3576 ..
3577 }
3578 ),
3579 "Expected Degraded state with ResourceLimit(MaxSymbols), got: {:?}",
3580 state
3581 );
3582 }
3583
3584 #[test]
3585 fn test_check_limits_returns_none_within_bounds() {
3586 let coordinator = IndexCoordinator::new();
3587 coordinator.transition_to_ready(0, 0);
3588
3589 for i in 0..5 {
3591 let uri_str = format!("file:///test{}.pl", i);
3592 let uri = must(url::Url::parse(&uri_str));
3593 let code = "sub test { }";
3594 must(coordinator.index().index_file(uri, code.to_string()));
3595 }
3596
3597 let limit_check = coordinator.check_limits();
3599 assert!(limit_check.is_none(), "check_limits should return None when within bounds");
3600
3601 assert!(
3603 matches!(coordinator.state(), IndexState::Ready { .. }),
3604 "State should remain Ready when within limits"
3605 );
3606 }
3607
3608 #[test]
3609 fn test_enforce_limits_called_on_transition_to_ready() {
3610 let limits = IndexResourceLimits {
3611 max_files: 3,
3612 max_symbols_per_file: 1000,
3613 max_total_symbols: 50_000,
3614 max_ast_cache_bytes: 128 * 1024 * 1024,
3615 max_ast_cache_items: 50,
3616 max_scan_duration_ms: 30_000,
3617 };
3618
3619 let coordinator = IndexCoordinator::with_limits(limits);
3620
3621 for i in 0..5 {
3623 let uri_str = format!("file:///test{}.pl", i);
3624 let uri = must(url::Url::parse(&uri_str));
3625 let code = "sub test { }";
3626 must(coordinator.index().index_file(uri, code.to_string()));
3627 }
3628
3629 coordinator.transition_to_ready(5, 100);
3631
3632 let state = coordinator.state();
3633 assert!(
3634 matches!(
3635 state,
3636 IndexState::Degraded {
3637 reason: DegradationReason::ResourceLimit { kind: ResourceKind::MaxFiles },
3638 ..
3639 }
3640 ),
3641 "Expected Degraded state after transition_to_ready with exceeded limits, got: {:?}",
3642 state
3643 );
3644 }
3645
3646 #[test]
3647 fn test_state_transition_guard_ready_to_ready() {
3648 let coordinator = IndexCoordinator::new();
3650 coordinator.transition_to_ready(100, 5000);
3651
3652 coordinator.transition_to_ready(150, 7500);
3654
3655 let state = coordinator.state();
3656 assert!(
3657 matches!(state, IndexState::Ready { file_count: 150, symbol_count: 7500, .. }),
3658 "Expected Ready state with updated metrics, got: {:?}",
3659 state
3660 );
3661 }
3662
3663 #[test]
3664 fn test_state_transition_guard_building_to_building() {
3665 let coordinator = IndexCoordinator::new();
3667
3668 coordinator.transition_to_building(100);
3670
3671 let state = coordinator.state();
3672 assert!(
3673 matches!(state, IndexState::Building { indexed_count: 0, total_count: 100, .. }),
3674 "Expected Building state, got: {:?}",
3675 state
3676 );
3677
3678 coordinator.transition_to_building(200);
3680
3681 let state = coordinator.state();
3682 assert!(
3683 matches!(state, IndexState::Building { indexed_count: 0, total_count: 200, .. }),
3684 "Expected Building state, got: {:?}",
3685 state
3686 );
3687 }
3688
3689 #[test]
3690 fn test_state_transition_ready_to_building() {
3691 let coordinator = IndexCoordinator::new();
3693 coordinator.transition_to_ready(100, 5000);
3694
3695 coordinator.transition_to_building(150);
3697
3698 let state = coordinator.state();
3699 assert!(
3700 matches!(state, IndexState::Building { indexed_count: 0, total_count: 150, .. }),
3701 "Expected Building state after re-scan, got: {:?}",
3702 state
3703 );
3704 }
3705
3706 #[test]
3707 fn test_state_transition_degraded_to_building() {
3708 let coordinator = IndexCoordinator::new();
3710 coordinator.transition_to_degraded(DegradationReason::IoError {
3711 message: "Test error".to_string(),
3712 });
3713
3714 coordinator.transition_to_building(100);
3716
3717 let state = coordinator.state();
3718 assert!(
3719 matches!(state, IndexState::Building { indexed_count: 0, total_count: 100, .. }),
3720 "Expected Building state after recovery, got: {:?}",
3721 state
3722 );
3723 }
3724
3725 #[test]
3726 fn test_update_building_progress() {
3727 let coordinator = IndexCoordinator::new();
3728 coordinator.transition_to_building(100);
3729
3730 coordinator.update_building_progress(50);
3732
3733 let state = coordinator.state();
3734 assert!(
3735 matches!(state, IndexState::Building { indexed_count: 50, total_count: 100, .. }),
3736 "Expected Building state with updated progress, got: {:?}",
3737 state
3738 );
3739
3740 coordinator.update_building_progress(100);
3742
3743 let state = coordinator.state();
3744 assert!(
3745 matches!(state, IndexState::Building { indexed_count: 100, total_count: 100, .. }),
3746 "Expected Building state with completed progress, got: {:?}",
3747 state
3748 );
3749 }
3750
3751 #[test]
3752 fn test_scan_timeout_detection() {
3753 let limits = IndexResourceLimits {
3755 max_scan_duration_ms: 0, ..Default::default()
3757 };
3758
3759 let coordinator = IndexCoordinator::with_limits(limits);
3760 coordinator.transition_to_building(100);
3761
3762 std::thread::sleep(std::time::Duration::from_millis(1));
3764
3765 coordinator.update_building_progress(10);
3767
3768 let state = coordinator.state();
3769 assert!(
3770 matches!(
3771 state,
3772 IndexState::Degraded { reason: DegradationReason::ScanTimeout { .. }, .. }
3773 ),
3774 "Expected Degraded state with ScanTimeout, got: {:?}",
3775 state
3776 );
3777 }
3778
3779 #[test]
3780 fn test_scan_timeout_does_not_trigger_within_limit() {
3781 let limits = IndexResourceLimits {
3783 max_scan_duration_ms: 10_000, ..Default::default()
3785 };
3786
3787 let coordinator = IndexCoordinator::with_limits(limits);
3788 coordinator.transition_to_building(100);
3789
3790 coordinator.update_building_progress(50);
3792
3793 let state = coordinator.state();
3794 assert!(
3795 matches!(state, IndexState::Building { indexed_count: 50, .. }),
3796 "Expected Building state (no timeout), got: {:?}",
3797 state
3798 );
3799 }
3800
3801 #[test]
3802 fn test_early_exit_optimization_unchanged_content() {
3803 let index = WorkspaceIndex::new();
3804 let uri = must(url::Url::parse("file:///test.pl"));
3805 let code = r#"
3806package MyPackage;
3807
3808sub hello {
3809 print "Hello";
3810}
3811"#;
3812
3813 must(index.index_file(uri.clone(), code.to_string()));
3815 let symbols1 = index.file_symbols(uri.as_str());
3816 assert!(symbols1.iter().any(|s| s.name == "MyPackage" && s.kind == SymbolKind::Package));
3817 assert!(symbols1.iter().any(|s| s.name == "hello" && s.kind == SymbolKind::Subroutine));
3818
3819 must(index.index_file(uri.clone(), code.to_string()));
3822 let symbols2 = index.file_symbols(uri.as_str());
3823 assert_eq!(symbols1.len(), symbols2.len());
3824 assert!(symbols2.iter().any(|s| s.name == "MyPackage" && s.kind == SymbolKind::Package));
3825 assert!(symbols2.iter().any(|s| s.name == "hello" && s.kind == SymbolKind::Subroutine));
3826 }
3827
3828 #[test]
3829 fn test_early_exit_optimization_changed_content() {
3830 let index = WorkspaceIndex::new();
3831 let uri = must(url::Url::parse("file:///test.pl"));
3832 let code1 = r#"
3833package MyPackage;
3834
3835sub hello {
3836 print "Hello";
3837}
3838"#;
3839
3840 let code2 = r#"
3841package MyPackage;
3842
3843sub goodbye {
3844 print "Goodbye";
3845}
3846"#;
3847
3848 must(index.index_file(uri.clone(), code1.to_string()));
3850 let symbols1 = index.file_symbols(uri.as_str());
3851 assert!(symbols1.iter().any(|s| s.name == "hello" && s.kind == SymbolKind::Subroutine));
3852 assert!(!symbols1.iter().any(|s| s.name == "goodbye"));
3853
3854 must(index.index_file(uri.clone(), code2.to_string()));
3856 let symbols2 = index.file_symbols(uri.as_str());
3857 assert!(!symbols2.iter().any(|s| s.name == "hello"));
3858 assert!(symbols2.iter().any(|s| s.name == "goodbye" && s.kind == SymbolKind::Subroutine));
3859 }
3860
3861 #[test]
3862 fn test_early_exit_optimization_whitespace_only_change() {
3863 let index = WorkspaceIndex::new();
3864 let uri = must(url::Url::parse("file:///test.pl"));
3865 let code1 = r#"
3866package MyPackage;
3867
3868sub hello {
3869 print "Hello";
3870}
3871"#;
3872
3873 let code2 = r#"
3874package MyPackage;
3875
3876
3877sub hello {
3878 print "Hello";
3879}
3880"#;
3881
3882 must(index.index_file(uri.clone(), code1.to_string()));
3884 let symbols1 = index.file_symbols(uri.as_str());
3885 assert!(symbols1.iter().any(|s| s.name == "hello" && s.kind == SymbolKind::Subroutine));
3886
3887 must(index.index_file(uri.clone(), code2.to_string()));
3889 let symbols2 = index.file_symbols(uri.as_str());
3890 assert!(symbols2.iter().any(|s| s.name == "hello" && s.kind == SymbolKind::Subroutine));
3892 }
3893
3894 #[test]
3895 fn test_reindex_file_refreshes_symbol_cache_for_removed_names() {
3896 let index = WorkspaceIndex::new();
3897 let uri1 = must(url::Url::parse("file:///lib/A.pm"));
3898 let uri2 = must(url::Url::parse("file:///lib/B.pm"));
3899 let code1 = "package A;\nsub foo { return 1; }\n1;\n";
3900 let code2 = "package B;\nsub foo { return 2; }\n1;\n";
3901 let code2_reindexed = "package B;\nsub bar { return 3; }\n1;\n";
3902
3903 must(index.index_file(uri1.clone(), code1.to_string()));
3904 must(index.index_file(uri2.clone(), code2.to_string()));
3905 must(index.index_file(uri2.clone(), code2_reindexed.to_string()));
3906
3907 let foo_location = must_some(index.find_definition("foo"));
3908 assert_eq!(foo_location.uri, uri1.to_string());
3909
3910 let bar_location = must_some(index.find_definition("bar"));
3911 assert_eq!(bar_location.uri, uri2.to_string());
3912 }
3913
3914 #[test]
3915 fn test_remove_file_preserves_other_colliding_symbol_entries() {
3916 let index = WorkspaceIndex::new();
3917 let uri1 = must(url::Url::parse("file:///lib/A.pm"));
3918 let uri2 = must(url::Url::parse("file:///lib/B.pm"));
3919 let code1 = "package A;\nsub foo { return 1; }\n1;\n";
3920 let code2 = "package B;\nsub foo { return 2; }\n1;\n";
3921
3922 must(index.index_file(uri1.clone(), code1.to_string()));
3923 must(index.index_file(uri2.clone(), code2.to_string()));
3924
3925 index.remove_file(uri2.as_str());
3926
3927 let foo_location = must_some(index.find_definition("foo"));
3928 assert_eq!(foo_location.uri, uri1.to_string());
3929 }
3930
3931 #[test]
3932 fn test_count_usages_no_double_counting_for_qualified_calls() {
3933 let index = WorkspaceIndex::new();
3934
3935 let uri1 = "file:///lib/Utils.pm";
3937 let code1 = r#"
3938package Utils;
3939
3940sub process_data {
3941 return 1;
3942}
3943"#;
3944 must(index.index_file(must(url::Url::parse(uri1)), code1.to_string()));
3945
3946 let uri2 = "file:///app.pl";
3948 let code2 = r#"
3949use Utils;
3950Utils::process_data();
3951Utils::process_data();
3952"#;
3953 must(index.index_file(must(url::Url::parse(uri2)), code2.to_string()));
3954
3955 let count = index.count_usages("Utils::process_data");
3959
3960 assert_eq!(
3963 count, 2,
3964 "count_usages should not double-count qualified calls, got {} (expected 2)",
3965 count
3966 );
3967
3968 let refs = index.find_references("Utils::process_data");
3970 let non_def_refs: Vec<_> =
3971 refs.iter().filter(|loc| loc.uri != "file:///lib/Utils.pm").collect();
3972 assert_eq!(
3973 non_def_refs.len(),
3974 2,
3975 "find_references should not return duplicates for qualified calls, got {} non-def refs",
3976 non_def_refs.len()
3977 );
3978 }
3979
3980 #[test]
3981 fn test_batch_indexing() {
3982 let index = WorkspaceIndex::new();
3983 let files: Vec<(Url, String)> = (0..5)
3984 .map(|i| {
3985 let uri = must(Url::parse(&format!("file:///batch/module{}.pm", i)));
3986 let code =
3987 format!("package Batch::Mod{};\nsub func_{} {{ return {}; }}\n1;", i, i, i);
3988 (uri, code)
3989 })
3990 .collect();
3991
3992 let errors = index.index_files_batch(files);
3993 assert!(errors.is_empty(), "batch indexing errors: {:?}", errors);
3994 assert_eq!(index.file_count(), 5);
3995 assert!(index.find_definition("Batch::Mod0::func_0").is_some());
3996 assert!(index.find_definition("Batch::Mod4::func_4").is_some());
3997 }
3998
3999 #[test]
4000 fn test_batch_indexing_skips_unchanged() {
4001 let index = WorkspaceIndex::new();
4002 let uri = must(Url::parse("file:///batch/skip.pm"));
4003 let code = "package Skip;\nsub skip_fn { 1 }\n1;".to_string();
4004
4005 index.index_file(uri.clone(), code.clone()).ok();
4006 assert_eq!(index.file_count(), 1);
4007
4008 let errors = index.index_files_batch(vec![(uri, code)]);
4009 assert!(errors.is_empty());
4010 assert_eq!(index.file_count(), 1);
4011 }
4012
4013 #[test]
4014 fn test_incremental_update_preserves_other_symbols() {
4015 let index = WorkspaceIndex::new();
4016
4017 let uri_a = must(Url::parse("file:///incr/a.pm"));
4018 let uri_b = must(Url::parse("file:///incr/b.pm"));
4019 index.index_file(uri_a.clone(), "package A;\nsub a_func { 1 }\n1;".into()).ok();
4020 index.index_file(uri_b.clone(), "package B;\nsub b_func { 2 }\n1;".into()).ok();
4021
4022 assert!(index.find_definition("A::a_func").is_some());
4023 assert!(index.find_definition("B::b_func").is_some());
4024
4025 index.index_file(uri_a, "package A;\nsub a_func_v2 { 11 }\n1;".into()).ok();
4026
4027 assert!(index.find_definition("A::a_func_v2").is_some());
4028 assert!(index.find_definition("B::b_func").is_some());
4029 }
4030
4031 #[test]
4032 fn test_remove_file_preserves_shadowed_symbols() {
4033 let index = WorkspaceIndex::new();
4034
4035 let uri_a = must(Url::parse("file:///shadow/a.pm"));
4036 let uri_b = must(Url::parse("file:///shadow/b.pm"));
4037 index.index_file(uri_a.clone(), "package ShadowA;\nsub helper { 1 }\n1;".into()).ok();
4038 index.index_file(uri_b.clone(), "package ShadowB;\nsub helper { 2 }\n1;".into()).ok();
4039
4040 assert!(index.find_definition("helper").is_some());
4041
4042 index.remove_file_url(&uri_a);
4043 assert!(index.find_definition("helper").is_some());
4044 assert!(index.find_definition("ShadowB::helper").is_some());
4045 }
4046
4047 #[test]
4052 fn test_index_dependency_via_use_parent_end_to_end() {
4053 let index = WorkspaceIndex::new();
4059
4060 let base_url = url::Url::parse("file:///test/workspace/lib/MyBase.pm").unwrap();
4061 index
4062 .index_file(base_url, "package MyBase;\nsub new { bless {}, shift }\n1;\n".to_string())
4063 .expect("indexing MyBase.pm");
4064
4065 let child_url = url::Url::parse("file:///test/workspace/child.pl").unwrap();
4066 index
4067 .index_file(child_url, "package Child;\nuse parent 'MyBase';\n1;\n".to_string())
4068 .expect("indexing child.pl");
4069
4070 let dependents = index.find_dependents("MyBase");
4071 assert!(
4072 !dependents.is_empty(),
4073 "find_dependents('MyBase') returned empty — \
4074 use parent 'MyBase' should register MyBase as a dependency. \
4075 Dependencies in index: {:?}",
4076 {
4077 let files = index.files.read();
4078 files
4079 .iter()
4080 .map(|(k, v)| (k.clone(), v.dependencies.iter().cloned().collect::<Vec<_>>()))
4081 .collect::<Vec<_>>()
4082 }
4083 );
4084 assert!(
4085 dependents.contains(&"file:///test/workspace/child.pl".to_string()),
4086 "child.pl should be in dependents, got: {:?}",
4087 dependents
4088 );
4089 }
4090
4091 #[test]
4092 fn test_parser_produces_correct_args_for_use_parent() {
4093 use crate::Parser;
4097 let mut p = Parser::new("package Child;\nuse parent 'MyBase';\n1;\n");
4098 let ast = p.parse().expect("parse succeeded");
4099 if let NodeKind::Program { statements } = &ast.kind {
4100 for stmt in statements {
4101 if let NodeKind::Use { module, args, .. } = &stmt.kind {
4102 if module == "parent" {
4103 assert_eq!(
4104 args,
4105 &["'MyBase'".to_string()],
4106 "Expected args=[\"'MyBase'\"] for `use parent 'MyBase'`, got: {:?}",
4107 args
4108 );
4109 let extracted = extract_module_names_from_use_args(args);
4110 assert_eq!(
4111 extracted,
4112 vec!["MyBase".to_string()],
4113 "extract_module_names_from_use_args should return [\"MyBase\"], got {:?}",
4114 extracted
4115 );
4116 return; }
4118 }
4119 }
4120 panic!("No Use node with module='parent' found in AST");
4121 } else {
4122 panic!("Expected Program root");
4123 }
4124 }
4125
4126 #[test]
4131 fn test_extract_module_names_single_quoted() {
4132 let names = extract_module_names_from_use_args(&["'Foo::Bar'".to_string()]);
4133 assert_eq!(names, vec!["Foo::Bar"]);
4134 }
4135
4136 #[test]
4137 fn test_extract_module_names_double_quoted() {
4138 let names = extract_module_names_from_use_args(&["\"Foo::Bar\"".to_string()]);
4139 assert_eq!(names, vec!["Foo::Bar"]);
4140 }
4141
4142 #[test]
4143 fn test_extract_module_names_qw_list() {
4144 let names = extract_module_names_from_use_args(&["qw(Foo::Bar Other::Base)".to_string()]);
4145 assert_eq!(names, vec!["Foo::Bar", "Other::Base"]);
4146 }
4147
4148 #[test]
4149 fn test_extract_module_names_norequire_flag() {
4150 let names = extract_module_names_from_use_args(&[
4151 "-norequire".to_string(),
4152 "'Foo::Bar'".to_string(),
4153 ]);
4154 assert_eq!(names, vec!["Foo::Bar"]);
4155 }
4156
4157 #[test]
4158 fn test_extract_module_names_empty_args() {
4159 let names = extract_module_names_from_use_args(&[]);
4160 assert!(names.is_empty());
4161 }
4162
4163 #[test]
4164 fn test_extract_module_names_legacy_separator() {
4165 let names = extract_module_names_from_use_args(&["'Foo'Bar'".to_string()]);
4167 assert_eq!(names, vec!["Foo'Bar"]);
4169 }
4170
4171 #[test]
4172 fn test_with_capacity_accepts_large_batch_without_panic() {
4173 let index = WorkspaceIndex::with_capacity(100, 20);
4174 for i in 0..100 {
4175 let uri = must(url::Url::parse(&format!("file:///lib/Mod{}.pm", i)));
4176 let src = format!("package Mod{};\nsub foo_{} {{ 1 }}\n1;\n", i, i);
4177 index.index_file(uri, src).ok();
4178 }
4179 assert!(index.has_symbols());
4180 }
4181
4182 #[test]
4183 fn test_with_capacity_zero_does_not_panic() {
4184 let index = WorkspaceIndex::with_capacity(0, 0);
4185 assert!(!index.has_symbols());
4186 }
4187}