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