1use std::collections::{BTreeMap, BTreeSet};
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use code_moniker_core::core::moniker::Moniker;
9use rayon::prelude::*;
10use rustc_hash::FxHashMap;
11
12use crate::code::{def_kind, is_navigable_def, last_name, ref_kind};
13use crate::environment::SourceFileSet;
14use crate::lines::LineIndex;
15use crate::snapshot::{
16 CodeIndex, CodeIndexTimings, ExtractionMeasurement, RecordTable, ReferenceId, ReferenceRecord,
17 SourceCatalog, SourceFileRecord, SourceId, SourceUnit, SymbolId, SymbolInventoryIndex,
18 SymbolRecord, WorkspaceCancellation, WorkspaceFailure, WorkspaceResource, WorkspaceResult,
19};
20use crate::source::{
21 CodeIndexMaterial, IndexedSourceFile, LocalResourceCache, SourceCatalogMaterial,
22};
23
24use crate::source::LocalIdentityResolver;
25
26pub trait CodeIndexPort {
27 fn build_index(&mut self, catalog: &SourceCatalog) -> WorkspaceResult<CodeIndex>;
28 fn build_index_cancellable(
29 &mut self,
30 catalog: &SourceCatalog,
31 cancellation: &WorkspaceCancellation,
32 ) -> WorkspaceResult<CodeIndex> {
33 cancellation.check(WorkspaceResource::CodeIndex)?;
34 let index = self.build_index(catalog)?;
35 cancellation.check(WorkspaceResource::CodeIndex)?;
36 Ok(index)
37 }
38 fn refresh_paths(
39 &mut self,
40 current: &CodeIndex,
41 paths: &[PathBuf],
42 ) -> WorkspaceResult<CodeIndexRefresh>;
43 fn refresh_catalog_paths(
44 &mut self,
45 current: &CodeIndex,
46 catalog: &SourceCatalog,
47 paths: &[PathBuf],
48 ) -> WorkspaceResult<CodeIndexRefresh>;
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct CodeIndexRefresh {
53 pub index: CodeIndex,
54 pub changed_sources: Vec<SourceId>,
55 pub graph_diff: CodeIndexGraphDiff,
56}
57
58#[derive(Clone, Debug, Default, Eq, PartialEq)]
59pub struct CodeIndexGraphDiff {
60 pub added_symbols: Vec<SymbolId>,
62 pub modified_symbols: Vec<SymbolId>,
64 pub changed_symbols: Vec<SymbolId>,
66 pub removed_symbols: Vec<SymbolId>,
68 pub modified_symbol_identities: Vec<String>,
69 pub modified_inventory_symbols: Vec<SymbolId>,
74 pub modified_inventory_symbol_identities: Vec<String>,
75 pub removed_symbol_identities: Vec<String>,
76 pub changed_references: Vec<ReferenceId>,
77 pub removed_references: Vec<ReferenceId>,
78 pub removed_reference_kinds: Vec<String>,
79 pub symbol_id_remaps: Vec<(SymbolId, SymbolId)>,
80 pub reference_id_remaps: Vec<(ReferenceId, ReferenceId)>,
81 pub unchanged_symbols: usize,
82 pub unchanged_references: usize,
83}
84
85impl CodeIndexGraphDiff {
86 pub fn changed_symbol_count(&self) -> usize {
87 self.changed_linkage_symbol_count() + self.changed_inventory_symbol_count()
88 }
89
90 pub fn changed_linkage_symbol_count(&self) -> usize {
91 self.changed_symbols.len() + self.removed_symbols.len()
92 }
93
94 pub fn changed_inventory_symbol_count(&self) -> usize {
95 self.modified_inventory_symbols.len()
96 }
97
98 pub fn changed_reference_count(&self) -> usize {
99 self.changed_references.len() + self.removed_references.len()
100 }
101}
102
103#[derive(Clone, Debug, Default, Eq, PartialEq)]
104pub struct LocalCodeIndexOptions {
105 pub cache_dir: Option<PathBuf>,
106 pub detailed_telemetry: bool,
107}
108
109impl LocalCodeIndexOptions {
110 pub fn new(cache_dir: Option<PathBuf>) -> Self {
111 Self {
112 cache_dir,
113 detailed_telemetry: false,
114 }
115 }
116
117 pub fn with_detailed_telemetry(mut self, enabled: bool) -> Self {
118 self.detailed_telemetry = enabled;
119 self
120 }
121}
122
123pub struct LocalCodeIndex {
124 options: LocalCodeIndexOptions,
125 cache: LocalResourceCache,
126}
127
128impl LocalCodeIndex {
129 pub fn new(options: LocalCodeIndexOptions, cache: LocalResourceCache) -> Self {
130 Self { options, cache }
131 }
132
133 pub fn build_index_from_extracted(
134 &mut self,
135 sources: SourceFileSet,
136 identity: LocalIdentityResolver,
137 files: Vec<IndexedSourceFile>,
138 ) -> WorkspaceResult<(SourceCatalog, CodeIndex)> {
139 build_local_code_index_from_extracted(&self.cache, sources, identity, files)
140 }
141}
142
143impl CodeIndexPort for LocalCodeIndex {
144 fn build_index(&mut self, catalog: &SourceCatalog) -> WorkspaceResult<CodeIndex> {
145 build_local_code_index(
146 &self.cache,
147 &self.options,
148 catalog,
149 &WorkspaceCancellation::default(),
150 )
151 }
152
153 fn build_index_cancellable(
154 &mut self,
155 catalog: &SourceCatalog,
156 cancellation: &WorkspaceCancellation,
157 ) -> WorkspaceResult<CodeIndex> {
158 build_local_code_index(&self.cache, &self.options, catalog, cancellation)
159 }
160
161 fn refresh_paths(
162 &mut self,
163 current: &CodeIndex,
164 paths: &[PathBuf],
165 ) -> WorkspaceResult<CodeIndexRefresh> {
166 refresh_local_code_index(&self.cache, &self.options, current, None, paths)
167 }
168
169 fn refresh_catalog_paths(
170 &mut self,
171 current: &CodeIndex,
172 catalog: &SourceCatalog,
173 paths: &[PathBuf],
174 ) -> WorkspaceResult<CodeIndexRefresh> {
175 refresh_local_code_index(&self.cache, &self.options, current, Some(catalog), paths)
176 }
177}
178
179fn build_local_code_index(
180 cache: &LocalResourceCache,
181 options: &LocalCodeIndexOptions,
182 catalog: &SourceCatalog,
183 cancellation: &WorkspaceCancellation,
184) -> WorkspaceResult<CodeIndex> {
185 cancellation.check(WorkspaceResource::CodeIndex)?;
186 let total_timer = Instant::now();
187 let source_material = source_material(cache, catalog)?;
188 let generation = cache.next_generation();
189 let extract_timer = Instant::now();
190 let files = extract_source_files(
191 &source_material,
192 options.cache_dir.as_deref(),
193 cancellation,
194 options.detailed_telemetry,
195 )?;
196 let extract_sources = extract_timer.elapsed();
197 let extraction = if options.detailed_telemetry {
198 extraction_measurements(&files, None)
199 } else {
200 Default::default()
201 };
202 cancellation.check(WorkspaceResource::CodeIndex)?;
203 let semantic_timer = Instant::now();
204 let (symbols, references, material) =
205 build_semantic_index(source_material, files, cancellation)?;
206 let semantic_index = semantic_timer.elapsed();
207 let mut sources = source_records(&material);
208 let identity_scheme = material.identity.scheme().to_string();
209 cache.insert_index(generation, material);
210 sources.shrink_to_fit();
211 let inventory = Arc::new(SymbolInventoryIndex::build(generation, &sources, &symbols));
212 Ok(CodeIndex {
213 generation,
214 catalog_generation: catalog.generation,
215 identity_scheme,
216 sources,
217 symbols,
218 references,
219 inventory,
220 timings: CodeIndexTimings {
221 extract_sources,
222 semantic_index,
223 total: total_timer.elapsed(),
224 extraction,
225 },
226 })
227}
228
229fn build_code_index_from_extracted(
230 cache: &LocalResourceCache,
231 catalog: &SourceCatalog,
232 source_material: SourceCatalogMaterial,
233 files: Vec<Arc<IndexedSourceFile>>,
234) -> WorkspaceResult<CodeIndex> {
235 let generation = cache.next_generation();
236 let cancellation = WorkspaceCancellation::default();
237 let (symbols, references, material) =
238 build_semantic_index(source_material, files, &cancellation)?;
239 let mut sources = source_records(&material);
240 let identity_scheme = material.identity.scheme().to_string();
241 cache.insert_index(generation, material);
242 sources.shrink_to_fit();
243 let inventory = Arc::new(SymbolInventoryIndex::build(generation, &sources, &symbols));
244 Ok(CodeIndex {
245 generation,
246 catalog_generation: catalog.generation,
247 identity_scheme,
248 sources,
249 symbols,
250 references,
251 inventory,
252 timings: CodeIndexTimings {
253 extract_sources: Duration::ZERO,
254 semantic_index: Duration::ZERO,
255 total: Duration::ZERO,
256 extraction: Vec::new(),
257 },
258 })
259}
260
261fn build_local_code_index_from_extracted(
262 cache: &LocalResourceCache,
263 sources: SourceFileSet,
264 identity: LocalIdentityResolver,
265 files: Vec<IndexedSourceFile>,
266) -> WorkspaceResult<(SourceCatalog, CodeIndex)> {
267 validate_extracted_files(&sources, &identity, &files)?;
268 let catalog_generation = cache.next_generation();
269 let units = sources
270 .files
271 .iter()
272 .enumerate()
273 .map(|(file_idx, file)| {
274 SourceUnit::with_language(
275 identity.source_id(file_idx, &file.rel_path),
276 crate::path_util::portable_path(&file.rel_path),
277 file.lang.tag(),
278 )
279 })
280 .collect();
281 let catalog = SourceCatalog::new(catalog_generation, units);
282 let source_material = SourceCatalogMaterial {
283 sources,
284 identity,
285 memory_sources: BTreeMap::new(),
286 memory_slots: BTreeSet::new(),
287 memory_revisions: BTreeMap::new(),
288 };
289 cache.insert_sources(catalog_generation, source_material.clone());
290 let index = build_code_index_from_extracted(
291 cache,
292 &catalog,
293 source_material,
294 files.into_iter().map(Arc::new).collect(),
295 )?;
296 Ok((catalog, index))
297}
298
299fn validate_extracted_files(
300 sources: &SourceFileSet,
301 identity: &LocalIdentityResolver,
302 files: &[IndexedSourceFile],
303) -> WorkspaceResult<()> {
304 if sources.files.len() != files.len() {
305 return Err(WorkspaceFailure::new(
306 WorkspaceResource::CodeIndex,
307 format!(
308 "extracted file count {} does not match source catalog count {}",
309 files.len(),
310 sources.files.len()
311 ),
312 ));
313 }
314 for (file_idx, (source, extracted)) in sources.files.iter().zip(files).enumerate() {
315 let expected_id = identity.source_id(file_idx, &source.rel_path);
316 let matches = source.source == extracted.source_root
317 && expected_id == extracted.source_id
318 && source.path == extracted.path
319 && source.rel_path == extracted.rel_path
320 && source.anchor == extracted.anchor
321 && source.lang == extracted.lang
322 && extracted.identity == *identity;
323 if !matches {
324 return Err(WorkspaceFailure::new(
325 WorkspaceResource::CodeIndex,
326 format!(
327 "extracted file {} does not match source catalog slot {file_idx}",
328 extracted.rel_path.display()
329 ),
330 ));
331 }
332 }
333 Ok(())
334}
335
336fn refresh_local_code_index(
337 cache: &LocalResourceCache,
338 options: &LocalCodeIndexOptions,
339 current: &CodeIndex,
340 extended_catalog: Option<&SourceCatalog>,
341 paths: &[PathBuf],
342) -> WorkspaceResult<CodeIndexRefresh> {
343 let total_timer = Instant::now();
344 let current_material = cache.index_material(current.generation).ok_or_else(|| {
345 WorkspaceFailure::new(
346 WorkspaceResource::CodeIndex,
347 "code index material is unavailable",
348 )
349 })?;
350 let source_catalog = match extended_catalog {
351 Some(catalog) => cache.source_material(catalog.generation).ok_or_else(|| {
352 WorkspaceFailure::new(
353 WorkspaceResource::CodeIndex,
354 "extended source catalog material is unavailable",
355 )
356 })?,
357 None => current_material.source_catalog.clone(),
358 };
359 let mut files = current_material.files.clone();
360 let mut changed_sources = Vec::new();
361 let mut changed_file_indexes = BTreeSet::new();
362 let extract_timer = Instant::now();
363 let extraction_parent = options
364 .detailed_telemetry
365 .then(tracing::Span::current)
366 .unwrap_or_else(tracing::Span::none);
367 refresh_retired_slots(RetiredSlotRefresh {
368 previous_catalog: ¤t_material.source_catalog,
369 source_catalog: &source_catalog,
370 cache_dir: options.cache_dir.as_deref(),
371 files: &mut files,
372 changed_sources: &mut changed_sources,
373 changed_file_indexes: &mut changed_file_indexes,
374 extraction_parent: &extraction_parent,
375 detailed_telemetry: options.detailed_telemetry,
376 })?;
377 for file_idx in files.len()..source_catalog.sources.files.len() {
378 let file = &source_catalog.sources.files[file_idx];
379 let indexed = extract_source_file(
380 &source_catalog,
381 file_idx,
382 &file.path.clone(),
383 options.cache_dir.as_deref(),
384 &extraction_parent,
385 options.detailed_telemetry,
386 )?;
387 push_unique_source(&mut changed_sources, indexed.source_id);
388 changed_file_indexes.insert(file_idx);
389 files.push(Arc::new(indexed));
390 }
391 for path in paths {
392 let Some(source) = source_catalog.resolve_source(path) else {
393 continue;
394 };
395 let Some(file_idx) = source.eager_index else {
396 continue;
397 };
398 if changed_file_indexes.contains(&file_idx) {
399 continue;
400 }
401 let indexed = extract_source_file(
402 &source_catalog,
403 file_idx,
404 &source.path,
405 options.cache_dir.as_deref(),
406 &extraction_parent,
407 options.detailed_telemetry,
408 )?;
409 if let Some(slot) = files.get_mut(file_idx) {
410 push_unique_source(&mut changed_sources, indexed.source_id);
411 changed_file_indexes.insert(file_idx);
412 *slot = Arc::new(indexed);
413 }
414 }
415 let extract_sources = extract_timer.elapsed();
416 if changed_sources.is_empty() {
417 return Ok(CodeIndexRefresh {
418 index: current.clone(),
419 changed_sources,
420 graph_diff: CodeIndexGraphDiff::default(),
421 });
422 }
423 let semantic_timer = Instant::now();
424 let extraction = if options.detailed_telemetry {
425 extraction_measurements(&files, Some(&changed_file_indexes))
426 } else {
427 Default::default()
428 };
429 let material = material_from_files(source_catalog, files, &WorkspaceCancellation::default())?;
430 let sources = source_records(&material);
431 let graph_diff = graph_diff(current_material.as_ref(), &material, &changed_file_indexes);
432 let mut symbols = current.symbols.clone();
433 let mut references = current.references.clone();
434 for file_idx in &changed_file_indexes {
435 let (file_symbols, file_references) =
436 records_for_file(*file_idx, &material.files[*file_idx]);
437 symbols.replace(*file_idx, Arc::from(file_symbols));
438 references.replace(*file_idx, Arc::from(file_references));
439 }
440 let semantic_index = semantic_timer.elapsed();
441 let generation = cache.next_generation();
442 let identity_scheme = material.identity.scheme().to_string();
443 cache_refreshed_index(cache, current, generation, material, &graph_diff);
444 let inventory = Arc::new(current.inventory.refresh(
445 generation,
446 &sources,
447 &symbols,
448 &changed_file_indexes,
449 ));
450 Ok(CodeIndexRefresh {
451 index: CodeIndex {
452 generation,
453 catalog_generation: extended_catalog
454 .map(|catalog| catalog.generation)
455 .unwrap_or(current.catalog_generation),
456 identity_scheme,
457 sources,
458 symbols,
459 references,
460 inventory,
461 timings: CodeIndexTimings {
462 extract_sources,
463 semantic_index,
464 total: total_timer.elapsed(),
465 extraction,
466 },
467 },
468 changed_sources,
469 graph_diff,
470 })
471}
472
473fn cache_refreshed_index(
474 cache: &LocalResourceCache,
475 current: &CodeIndex,
476 generation: crate::snapshot::ResourceGeneration,
477 material: CodeIndexMaterial,
478 graph_diff: &CodeIndexGraphDiff,
479) {
480 cache.insert_index(generation, material);
481 cache.insert_index_diff(generation, current.generation, graph_diff.clone());
482}
483
484struct RetiredSlotRefresh<'a> {
485 previous_catalog: &'a SourceCatalogMaterial,
486 source_catalog: &'a SourceCatalogMaterial,
487 cache_dir: Option<&'a Path>,
488 files: &'a mut Vec<Arc<IndexedSourceFile>>,
489 changed_sources: &'a mut Vec<SourceId>,
490 changed_file_indexes: &'a mut BTreeSet<usize>,
491 extraction_parent: &'a tracing::Span,
492 detailed_telemetry: bool,
493}
494
495fn refresh_retired_slots(refresh: RetiredSlotRefresh<'_>) -> WorkspaceResult<()> {
496 let slots = refresh
497 .files
498 .len()
499 .min(refresh.source_catalog.sources.files.len());
500 for file_idx in 0..slots {
501 let was_retired = refresh.previous_catalog.sources.files[file_idx].retired;
502 let is_retired = refresh.source_catalog.sources.files[file_idx].retired;
503 if was_retired == is_retired {
504 continue;
505 }
506 let indexed = if is_retired {
507 tombstone_file(&refresh.files[file_idx])
508 } else {
509 extract_source_file(
510 refresh.source_catalog,
511 file_idx,
512 &refresh.source_catalog.sources.files[file_idx].path.clone(),
513 refresh.cache_dir,
514 refresh.extraction_parent,
515 refresh.detailed_telemetry,
516 )?
517 };
518 push_unique_source(refresh.changed_sources, indexed.source_id);
519 refresh.changed_file_indexes.insert(file_idx);
520 refresh.files[file_idx] = Arc::new(indexed);
521 }
522 Ok(())
523}
524
525fn tombstone_file(previous: &IndexedSourceFile) -> IndexedSourceFile {
526 IndexedSourceFile {
527 source_root: previous.source_root,
528 source_id: previous.source_id,
529 source_uri: previous.source_uri.clone(),
530 identity: previous.identity.clone(),
531 path: previous.path.clone(),
532 rel_path: previous.rel_path.clone(),
533 anchor: previous.anchor.clone(),
534 lang: previous.lang,
535 graph: code_moniker_core::core::code_graph::CodeGraph::from_records(Vec::new(), Vec::new()),
536 source: String::new(),
537 extraction_cache: "retired",
538 extraction_duration: Duration::ZERO,
539 }
540}
541
542fn source_material(
543 cache: &LocalResourceCache,
544 catalog: &SourceCatalog,
545) -> WorkspaceResult<SourceCatalogMaterial> {
546 cache.source_material(catalog.generation).ok_or_else(|| {
547 WorkspaceFailure::new(
548 WorkspaceResource::CodeIndex,
549 "source catalog material is unavailable",
550 )
551 })
552}
553
554fn extract_source_files(
555 source_material: &SourceCatalogMaterial,
556 cache_dir: Option<&std::path::Path>,
557 cancellation: &WorkspaceCancellation,
558 detailed_telemetry: bool,
559) -> WorkspaceResult<Vec<Arc<IndexedSourceFile>>> {
560 let parent = detailed_telemetry
561 .then(tracing::Span::current)
562 .unwrap_or_else(tracing::Span::none);
563 source_material
564 .sources
565 .files
566 .par_iter()
567 .enumerate()
568 .map(|(file_idx, file)| {
569 cancellation.check(WorkspaceResource::CodeIndex)?;
570 extract_source_file(
571 source_material,
572 file_idx,
573 &file.path,
574 cache_dir,
575 &parent,
576 detailed_telemetry,
577 )
578 .map(Arc::new)
579 })
580 .collect()
581}
582
583fn extract_source_file(
584 source_material: &SourceCatalogMaterial,
585 file_idx: usize,
586 path: &Path,
587 cache_dir: Option<&Path>,
588 parent: &tracing::Span,
589 detailed_telemetry: bool,
590) -> WorkspaceResult<IndexedSourceFile> {
591 let file = source_material.sources.files.get(file_idx).ok_or_else(|| {
592 WorkspaceFailure::new(
593 WorkspaceResource::CodeIndex,
594 format!("source file index {file_idx} is unavailable"),
595 )
596 })?;
597 let root = source_material
598 .sources
599 .roots
600 .get(file.source)
601 .ok_or_else(|| {
602 WorkspaceFailure::new(
603 WorkspaceResource::CodeIndex,
604 format!("source root {} is unavailable", file.source),
605 )
606 })?;
607 let ctx = file.extraction_context(root);
608 let started = detailed_telemetry.then(Instant::now);
609 let span = if detailed_telemetry {
610 tracing::info_span!(
611 parent: parent,
612 "workspace.extract_file",
613 file.path = %file.rel_path.display(),
614 file.language = file.lang.tag(),
615 file.source_bytes = tracing::field::Empty,
616 cache.result = tracing::field::Empty,
617 graph.definitions = tracing::field::Empty,
618 graph.references = tracing::field::Empty,
619 )
620 } else {
621 tracing::Span::none()
622 };
623 let _entered = span.enter();
624 let (graph, source, cache_status) = match source_material.memory_source(path) {
625 Some(source) => (
626 crate::environment::extract_source_with(file.lang, source, &file.anchor, &ctx),
627 source.to_owned(),
628 "memory",
629 ),
630 None => {
631 let (graph, extracted_source, cache_outcome) =
632 crate::cache::load_or_extract_workspace_result(
633 path,
634 &file.anchor,
635 file.lang,
636 cache_dir,
637 &ctx,
638 )
639 .map_err(|err| {
640 WorkspaceFailure::new(
641 WorkspaceResource::CodeIndex,
642 format!("cannot extract {}: {err}", path.display()),
643 )
644 })?;
645 let source = match extracted_source {
646 Some(source) => source,
647 None => crate::cache::read_source_lossy(path).map_err(|err| {
648 WorkspaceFailure::new(
649 WorkspaceResource::CodeIndex,
650 format!("cannot read {}: {err}", path.display()),
651 )
652 })?,
653 };
654 (graph, source, cache_outcome.as_str())
655 }
656 };
657 let elapsed = started.map_or(Duration::ZERO, |started| started.elapsed());
658 if detailed_telemetry {
659 span.record("file.source_bytes", source.len());
660 span.record("cache.result", cache_status);
661 span.record("graph.definitions", graph.def_count());
662 span.record("graph.references", graph.ref_count());
663 }
664 Ok(IndexedSourceFile {
665 source_root: file.source,
666 source_id: source_material
667 .source_id_for_file(file_idx)
668 .ok_or_else(|| {
669 WorkspaceFailure::new(
670 WorkspaceResource::CodeIndex,
671 format!("source id is unavailable for {}", file.rel_path.display()),
672 )
673 })?,
674 source_uri: source_material
675 .source_uri_for_path(&file.path)
676 .ok_or_else(|| {
677 WorkspaceFailure::new(
678 WorkspaceResource::CodeIndex,
679 format!("source uri is unavailable for {}", file.path.display()),
680 )
681 })?,
682 identity: source_material.identity.clone(),
683 path: file.path.clone(),
684 rel_path: file.rel_path.clone(),
685 anchor: file.anchor.clone(),
686 lang: file.lang,
687 graph,
688 source,
689 extraction_cache: cache_status,
690 extraction_duration: elapsed,
691 })
692}
693
694fn extraction_measurements(
695 files: &[Arc<IndexedSourceFile>],
696 selected: Option<&BTreeSet<usize>>,
697) -> Vec<ExtractionMeasurement> {
698 let mut groups = BTreeMap::<(&'static str, &'static str), ExtractionMeasurement>::new();
699 for (file_idx, file) in files.iter().enumerate() {
700 if selected.is_some_and(|selected| !selected.contains(&file_idx)) {
701 continue;
702 }
703 let language = file.lang.tag();
704 let cache = file.extraction_cache;
705 let entry = groups
706 .entry((language, cache))
707 .or_insert_with(|| ExtractionMeasurement {
708 language,
709 cache,
710 ..ExtractionMeasurement::default()
711 });
712 entry.files += 1;
713 entry.source_bytes += file.source.len();
714 entry.duration += file.extraction_duration;
715 }
716 groups.into_values().collect()
717}
718
719fn build_semantic_index(
720 source_material: SourceCatalogMaterial,
721 files: Vec<Arc<IndexedSourceFile>>,
722 cancellation: &WorkspaceCancellation,
723) -> WorkspaceResult<(
724 RecordTable<SymbolRecord>,
725 RecordTable<ReferenceRecord>,
726 CodeIndexMaterial,
727)> {
728 let mut symbol_shards = Vec::with_capacity(files.len());
729 let mut reference_shards = Vec::with_capacity(files.len());
730 for (file_idx, file) in files.iter().enumerate() {
731 cancellation.check(WorkspaceResource::CodeIndex)?;
732 let (symbols, references) = records_for_file(file_idx, file);
733 symbol_shards.push(Arc::from(symbols));
734 reference_shards.push(Arc::from(references));
735 }
736 let material = material_from_files(source_material, files, cancellation)?;
737 Ok((
738 RecordTable::from_shards(symbol_shards),
739 RecordTable::from_shards(reference_shards),
740 material,
741 ))
742}
743
744fn material_from_files(
745 source_material: SourceCatalogMaterial,
746 mut files: Vec<Arc<IndexedSourceFile>>,
747 cancellation: &WorkspaceCancellation,
748) -> WorkspaceResult<CodeIndexMaterial> {
749 let symbol_count = files.iter().map(|file| file.graph.def_count()).sum();
750 let mut symbols_by_moniker = rustc_hash::FxHashMap::default();
751 symbols_by_moniker.reserve(symbol_count);
752 for (file_idx, file) in files.iter().enumerate() {
753 cancellation.check(WorkspaceResource::CodeIndex)?;
754 for (def_idx, def) in file.graph.defs().enumerate() {
755 symbols_by_moniker.insert(
756 def.moniker.clone(),
757 file.identity.symbol_id(file_idx, def_idx),
758 );
759 }
760 }
761 symbols_by_moniker.shrink_to_fit();
762 files.shrink_to_fit();
763 let identity = source_material.identity.clone();
764 Ok(CodeIndexMaterial {
765 source_catalog: source_material,
766 files,
767 identity,
768 symbols_by_moniker,
769 })
770}
771
772fn graph_diff(
773 previous: &CodeIndexMaterial,
774 next: &CodeIndexMaterial,
775 changed_files: &BTreeSet<usize>,
776) -> CodeIndexGraphDiff {
777 let mut diff = CodeIndexGraphDiff::default();
778 for file_idx in changed_files {
779 let Some(next_file) = next.files.get(*file_idx) else {
780 continue;
781 };
782 let (previous_symbols, previous_references) = match previous.files.get(*file_idx) {
783 Some(previous_file) => records_for_file(*file_idx, previous_file),
784 None => (Vec::new(), Vec::new()),
785 };
786 let (next_symbols, next_references) = records_for_file(*file_idx, next_file);
787 diff_symbols(&previous_symbols, &next_symbols, &mut diff);
788 diff_references(
789 &previous_references,
790 previous,
791 &next_references,
792 next,
793 &mut diff,
794 );
795 }
796 diff
797}
798
799fn records_for_file(
800 file_idx: usize,
801 file: &IndexedSourceFile,
802) -> (Vec<SymbolRecord>, Vec<ReferenceRecord>) {
803 let line_index = LineIndex::new(&file.source);
804 let mut symbols = Vec::with_capacity(file.graph.def_count());
805 collect_symbols(file_idx, file, &line_index, &mut symbols);
806 let mut reference_identity_pool = TargetIdentityPool::default();
807 let mut references = Vec::with_capacity(file.graph.ref_count());
808 collect_references(
809 file_idx,
810 file,
811 &line_index,
812 &mut references,
813 &mut reference_identity_pool,
814 );
815 (symbols, references)
816}
817
818fn diff_symbols(previous: &[SymbolRecord], next: &[SymbolRecord], diff: &mut CodeIndexGraphDiff) {
819 let mut next_by_key = symbol_record_indexes(next);
820 for previous_symbol in previous {
821 let key = symbol_key(previous_symbol);
822 let Some(next_idx) = pop_index(&mut next_by_key, &key) else {
823 diff.removed_symbols.push(previous_symbol.id);
824 diff.removed_symbol_identities
825 .push(previous_symbol.identity.to_string());
826 continue;
827 };
828 let next_symbol = &next[next_idx];
829 if symbol_linkage_fields_changed(previous_symbol, next_symbol) {
830 diff.modified_symbols.push(next_symbol.id);
831 diff.modified_symbol_identities
832 .push(next_symbol.identity.to_string());
833 diff.changed_symbols.push(next_symbol.id);
834 continue;
835 }
836 let inventory_changed = symbol_inventory_fields_changed(previous_symbol, next_symbol);
837 if inventory_changed {
838 diff.modified_inventory_symbols.push(next_symbol.id);
839 diff.modified_inventory_symbol_identities
840 .push(next_symbol.identity.to_string());
841 }
842 if previous_symbol.id != next_symbol.id {
843 diff.symbol_id_remaps
844 .push((previous_symbol.id, next_symbol.id));
845 }
846 if inventory_changed {
847 continue;
848 }
849 diff.unchanged_symbols += 1;
850 }
851 for indexes in next_by_key.into_values() {
852 for idx in indexes {
853 diff.added_symbols.push(next[idx].id);
854 diff.changed_symbols.push(next[idx].id);
855 }
856 }
857}
858
859fn diff_references(
860 previous: &[ReferenceRecord],
861 previous_material: &CodeIndexMaterial,
862 next: &[ReferenceRecord],
863 next_material: &CodeIndexMaterial,
864 diff: &mut CodeIndexGraphDiff,
865) {
866 let mut next_by_key = reference_record_indexes(next, next_material);
867 for previous_reference in previous {
868 let Some(key) = reference_key(previous_reference, previous_material) else {
869 diff.removed_references.push(previous_reference.id);
870 diff.removed_reference_kinds
871 .push(previous_reference.kind.clone());
872 continue;
873 };
874 let Some(next_idx) = pop_index(&mut next_by_key, &key) else {
875 diff.removed_references.push(previous_reference.id);
876 diff.removed_reference_kinds
877 .push(previous_reference.kind.clone());
878 continue;
879 };
880 let next_reference = &next[next_idx];
881 if previous_reference.id != next_reference.id {
882 diff.reference_id_remaps
883 .push((previous_reference.id, next_reference.id));
884 }
885 diff.unchanged_references += 1;
886 }
887 for indexes in next_by_key.into_values() {
888 for idx in indexes {
889 diff.changed_references.push(next[idx].id);
890 }
891 }
892}
893
894fn symbol_record_indexes(records: &[SymbolRecord]) -> FxHashMap<Arc<str>, Vec<usize>> {
895 let mut by_key = FxHashMap::<Arc<str>, Vec<usize>>::default();
896 for (idx, record) in records.iter().enumerate() {
897 by_key.entry(symbol_key(record)).or_default().push(idx);
898 }
899 by_key
900}
901
902fn reference_record_indexes(
903 records: &[ReferenceRecord],
904 material: &CodeIndexMaterial,
905) -> FxHashMap<ReferenceKey, Vec<usize>> {
906 let mut by_key = FxHashMap::<ReferenceKey, Vec<usize>>::default();
907 for (idx, record) in records.iter().enumerate() {
908 if let Some(key) = reference_key(record, material) {
909 by_key.entry(key).or_default().push(idx);
910 }
911 }
912 by_key
913}
914
915fn pop_index<K: Eq + std::hash::Hash>(
916 by_key: &mut FxHashMap<K, Vec<usize>>,
917 key: &K,
918) -> Option<usize> {
919 let indexes = by_key.get_mut(key)?;
920 let idx = indexes.remove(0);
921 if indexes.is_empty() {
922 by_key.remove(key);
923 }
924 Some(idx)
925}
926
927fn symbol_key(symbol: &SymbolRecord) -> Arc<str> {
928 Arc::clone(&symbol.identity)
929}
930
931fn symbol_linkage_fields_changed(previous: &SymbolRecord, next: &SymbolRecord) -> bool {
932 previous.identity != next.identity
933 || previous.name != next.name
934 || previous.kind != next.kind
935 || previous.visibility != next.visibility
936 || previous.signature != next.signature
937 || previous.call_name != next.call_name
938 || previous.call_arity != next.call_arity
939 || previous.navigable != next.navigable
940}
941
942fn symbol_inventory_fields_changed(previous: &SymbolRecord, next: &SymbolRecord) -> bool {
943 previous.line_range != next.line_range || previous.parent != next.parent
944}
945
946#[derive(Clone, Debug, Eq, Hash, PartialEq)]
947struct ReferenceKey {
948 source_symbol_identity: String,
949 target_identity: String,
950 kind: String,
951 call_name: Option<String>,
952 call_arity: Option<usize>,
953 confidence: Option<String>,
954 receiver: Option<String>,
955 alias: Option<String>,
956}
957
958fn reference_key(
959 reference: &ReferenceRecord,
960 material: &CodeIndexMaterial,
961) -> Option<ReferenceKey> {
962 let source_symbol_identity = material
963 .symbol_moniker(&reference.source_symbol)
964 .map(|moniker| material.identity.moniker_uri(moniker))?;
965 Some(ReferenceKey {
966 source_symbol_identity,
967 target_identity: reference.target_identity.to_string(),
968 kind: reference.kind.clone(),
969 call_name: reference.call_name.clone(),
970 call_arity: reference.call_arity,
971 confidence: reference.confidence.clone(),
972 receiver: reference.receiver.clone(),
973 alias: reference.alias.clone(),
974 })
975}
976
977fn push_unique_source(sources: &mut Vec<SourceId>, source: SourceId) {
978 if !sources.iter().any(|existing| existing == &source) {
979 sources.push(source);
980 }
981}
982
983fn collect_symbols(
984 file_idx: usize,
985 file: &IndexedSourceFile,
986 line_index: &LineIndex,
987 symbols: &mut Vec<SymbolRecord>,
988) {
989 for (def_idx, def) in file.graph.defs().enumerate() {
990 let id = file.identity.symbol_id(file_idx, def_idx);
991 let parent = def
992 .parent
993 .map(|parent_idx| file.identity.symbol_id(file_idx, parent_idx));
994 symbols.push(SymbolRecord {
995 id,
996 source: file.source_id,
997 identity: Arc::from(file.identity.moniker_uri(&def.moniker)),
998 name: last_name(&def.moniker),
999 kind: def_kind(def),
1000 visibility: def_visibility(def),
1001 signature: String::from_utf8_lossy(&def.signature).to_string(),
1002 call_name: (!def.call_name.is_empty())
1003 .then(|| String::from_utf8_lossy(&def.call_name).to_string()),
1004 call_arity: def.call_arity,
1005 navigable: is_navigable_def(file.lang, def),
1006 line_range: def
1007 .position
1008 .map(|(start, end)| line_index.line_range(start, end)),
1009 parent,
1010 });
1011 }
1012}
1013
1014fn def_visibility(def: &code_moniker_core::core::code_graph::DefRecord) -> String {
1015 std::str::from_utf8(&def.visibility)
1016 .unwrap_or("")
1017 .to_string()
1018}
1019
1020fn collect_references(
1021 file_idx: usize,
1022 file: &IndexedSourceFile,
1023 line_index: &LineIndex,
1024 references: &mut Vec<ReferenceRecord>,
1025 reference_identity_pool: &mut TargetIdentityPool,
1026) {
1027 for (ref_idx, reference) in file.graph.refs().enumerate() {
1028 let id = file.identity.reference_id(file_idx, ref_idx);
1029 let source_symbol = file.identity.symbol_id(file_idx, reference.source);
1030 let target_identity = reference_identity_pool.intern(&file.identity, &reference.target);
1031 references.push(
1032 ReferenceRecord::new(
1033 id,
1034 file.source_id,
1035 source_symbol,
1036 target_identity,
1037 ref_kind(reference),
1038 reference
1039 .position
1040 .map(|(start, end)| line_index.line_range(start, end)),
1041 )
1042 .with_call_metadata(ref_attr(&reference.call_name), reference.call_arity)
1043 .with_metadata(
1044 ref_attr(&reference.confidence),
1045 ref_attr(&reference.receiver_hint),
1046 ref_attr(&reference.alias),
1047 ),
1048 );
1049 }
1050}
1051
1052#[derive(Default)]
1053struct TargetIdentityPool {
1054 values: rustc_hash::FxHashMap<Moniker, Arc<str>>,
1055}
1056
1057impl TargetIdentityPool {
1058 fn intern(&mut self, identity: &LocalIdentityResolver, target: &Moniker) -> Arc<str> {
1059 if let Some(existing) = self.values.get(target) {
1060 return Arc::clone(existing);
1061 }
1062 let shared = Arc::<str>::from(identity.moniker_uri(target));
1063 self.values.insert(target.clone(), Arc::clone(&shared));
1064 shared
1065 }
1066}
1067
1068fn source_records(material: &CodeIndexMaterial) -> Vec<SourceFileRecord> {
1069 material
1070 .files
1071 .iter()
1072 .map(|file| SourceFileRecord {
1073 id: file.source_id,
1074 uri: file.source_uri.clone(),
1075 source_root: file.source_root,
1076 path: file.path.display().to_string(),
1077 rel_path: crate::path_util::portable_path(&file.rel_path),
1078 anchor: crate::path_util::portable_path(&file.anchor),
1079 language: file.lang.tag().to_string(),
1080 text: if material.source_catalog.is_memory_slot(&file.path) {
1081 file.source.to_owned()
1082 } else {
1083 String::new()
1084 },
1085 })
1086 .collect()
1087}
1088
1089fn ref_attr(bytes: &[u8]) -> Option<String> {
1090 if bytes.is_empty() {
1091 return None;
1092 }
1093 std::str::from_utf8(bytes)
1094 .ok()
1095 .filter(|value| !value.is_empty())
1096 .map(ToOwned::to_owned)
1097}
1098
1099#[cfg(test)]
1100mod tests {
1101 use super::*;
1102
1103 #[test]
1104 fn line_range_changes_are_inventory_deltas_not_linkage_deltas() {
1105 let mut previous =
1106 SymbolRecord::new(SymbolId::at(0, 0), SourceId::at(0), "Invoice", "class");
1107 previous.identity = Arc::from("code+moniker://./lang:java/class:Invoice");
1108 previous.line_range = Some((4, 4));
1109 let mut next = previous.clone();
1110 next.line_range = Some((4, 13));
1111 let mut diff = CodeIndexGraphDiff::default();
1112
1113 diff_symbols(&[previous], &[next.clone()], &mut diff);
1114
1115 assert_eq!(diff.modified_inventory_symbols, vec![next.id]);
1116 assert_eq!(
1117 diff.modified_inventory_symbol_identities,
1118 vec![next.identity.to_string()]
1119 );
1120 assert!(diff.modified_symbols.is_empty());
1121 assert!(diff.modified_symbol_identities.is_empty());
1122 assert_eq!(diff.unchanged_symbols, 0);
1123 assert_eq!(diff.changed_symbol_count(), 1);
1124 assert_eq!(diff.changed_linkage_symbol_count(), 0);
1125 }
1126}