Skip to main content

code_moniker_workspace/code/
index.rs

1// code-moniker: ignore-file[smell-clone-reflex]
2// Code index refresh and graph diffing clone stable IDs into owned snapshots/diffs.
3use std::collections::BTreeSet;
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::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::lines::LineIndex;
14use crate::snapshot::{
15	CodeIndex, CodeIndexTimings, RecordTable, ReferenceId, ReferenceRecord, SourceCatalog,
16	SourceFileRecord, SourceId, SymbolId, SymbolRecord, WorkspaceCancellation, WorkspaceFailure,
17	WorkspaceResource, WorkspaceResult,
18};
19use crate::source::{
20	CodeIndexMaterial, IndexedSourceFile, LocalResourceCache, SourceCatalogMaterial,
21};
22
23use crate::source::LocalIdentityResolver;
24
25pub trait CodeIndexPort {
26	fn build_index(&mut self, catalog: &SourceCatalog) -> WorkspaceResult<CodeIndex>;
27	fn build_index_cancellable(
28		&mut self,
29		catalog: &SourceCatalog,
30		cancellation: &WorkspaceCancellation,
31	) -> WorkspaceResult<CodeIndex> {
32		cancellation.check(WorkspaceResource::CodeIndex)?;
33		let index = self.build_index(catalog)?;
34		cancellation.check(WorkspaceResource::CodeIndex)?;
35		Ok(index)
36	}
37	fn refresh_paths(
38		&mut self,
39		current: &CodeIndex,
40		paths: &[PathBuf],
41	) -> WorkspaceResult<CodeIndexRefresh>;
42	fn refresh_catalog_paths(
43		&mut self,
44		current: &CodeIndex,
45		catalog: &SourceCatalog,
46		paths: &[PathBuf],
47	) -> WorkspaceResult<CodeIndexRefresh>;
48}
49
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct CodeIndexRefresh {
52	pub index: CodeIndex,
53	pub changed_sources: Vec<SourceId>,
54	pub graph_diff: CodeIndexGraphDiff,
55}
56
57#[derive(Clone, Debug, Default, Eq, PartialEq)]
58pub struct CodeIndexGraphDiff {
59	pub added_symbols: Vec<SymbolId>,
60	pub modified_symbols: Vec<SymbolId>,
61	pub changed_symbols: Vec<SymbolId>,
62	pub removed_symbols: Vec<SymbolId>,
63	pub modified_symbol_identities: Vec<String>,
64	pub removed_symbol_identities: Vec<String>,
65	pub changed_references: Vec<ReferenceId>,
66	pub removed_references: Vec<ReferenceId>,
67	pub removed_reference_kinds: Vec<String>,
68	pub symbol_id_remaps: Vec<(SymbolId, SymbolId)>,
69	pub reference_id_remaps: Vec<(ReferenceId, ReferenceId)>,
70	pub unchanged_symbols: usize,
71	pub unchanged_references: usize,
72}
73
74impl CodeIndexGraphDiff {
75	pub fn changed_symbol_count(&self) -> usize {
76		self.changed_symbols.len() + self.removed_symbols.len()
77	}
78
79	pub fn changed_reference_count(&self) -> usize {
80		self.changed_references.len() + self.removed_references.len()
81	}
82}
83
84#[derive(Clone, Debug, Default, Eq, PartialEq)]
85pub struct LocalCodeIndexOptions {
86	pub cache_dir: Option<PathBuf>,
87}
88
89impl LocalCodeIndexOptions {
90	pub fn new(cache_dir: Option<PathBuf>) -> Self {
91		Self { cache_dir }
92	}
93}
94
95pub struct LocalCodeIndex {
96	options: LocalCodeIndexOptions,
97	cache: LocalResourceCache,
98}
99
100impl LocalCodeIndex {
101	pub fn new(options: LocalCodeIndexOptions, cache: LocalResourceCache) -> Self {
102		Self { options, cache }
103	}
104}
105
106impl CodeIndexPort for LocalCodeIndex {
107	fn build_index(&mut self, catalog: &SourceCatalog) -> WorkspaceResult<CodeIndex> {
108		build_local_code_index(
109			&self.cache,
110			&self.options,
111			catalog,
112			&WorkspaceCancellation::default(),
113		)
114	}
115
116	fn build_index_cancellable(
117		&mut self,
118		catalog: &SourceCatalog,
119		cancellation: &WorkspaceCancellation,
120	) -> WorkspaceResult<CodeIndex> {
121		build_local_code_index(&self.cache, &self.options, catalog, cancellation)
122	}
123
124	fn refresh_paths(
125		&mut self,
126		current: &CodeIndex,
127		paths: &[PathBuf],
128	) -> WorkspaceResult<CodeIndexRefresh> {
129		refresh_local_code_index(&self.cache, &self.options, current, None, paths)
130	}
131
132	fn refresh_catalog_paths(
133		&mut self,
134		current: &CodeIndex,
135		catalog: &SourceCatalog,
136		paths: &[PathBuf],
137	) -> WorkspaceResult<CodeIndexRefresh> {
138		refresh_local_code_index(&self.cache, &self.options, current, Some(catalog), paths)
139	}
140}
141
142fn build_local_code_index(
143	cache: &LocalResourceCache,
144	options: &LocalCodeIndexOptions,
145	catalog: &SourceCatalog,
146	cancellation: &WorkspaceCancellation,
147) -> WorkspaceResult<CodeIndex> {
148	cancellation.check(WorkspaceResource::CodeIndex)?;
149	let total_timer = Instant::now();
150	let source_material = source_material(cache, catalog)?;
151	let generation = cache.next_generation();
152	let extract_timer = Instant::now();
153	let files = extract_source_files(&source_material, options.cache_dir.as_deref(), cancellation)?;
154	let extract_sources = extract_timer.elapsed();
155	cancellation.check(WorkspaceResource::CodeIndex)?;
156	let semantic_timer = Instant::now();
157	let (symbols, references, material) =
158		build_semantic_index(source_material, files, cancellation)?;
159	let semantic_index = semantic_timer.elapsed();
160	let mut sources = source_records(&material.files);
161	let identity_scheme = material.identity.scheme().to_string();
162	cache.insert_index(generation, material);
163	sources.shrink_to_fit();
164	Ok(CodeIndex {
165		generation,
166		catalog_generation: catalog.generation,
167		identity_scheme,
168		sources,
169		symbols,
170		references,
171		timings: CodeIndexTimings {
172			extract_sources,
173			semantic_index,
174			total: total_timer.elapsed(),
175		},
176	})
177}
178
179fn refresh_local_code_index(
180	cache: &LocalResourceCache,
181	options: &LocalCodeIndexOptions,
182	current: &CodeIndex,
183	extended_catalog: Option<&SourceCatalog>,
184	paths: &[PathBuf],
185) -> WorkspaceResult<CodeIndexRefresh> {
186	let total_timer = Instant::now();
187	let current_material = cache.index_material(current.generation).ok_or_else(|| {
188		WorkspaceFailure::new(
189			WorkspaceResource::CodeIndex,
190			"code index material is unavailable",
191		)
192	})?;
193	let source_catalog = match extended_catalog {
194		Some(catalog) => cache.source_material(catalog.generation).ok_or_else(|| {
195			WorkspaceFailure::new(
196				WorkspaceResource::CodeIndex,
197				"extended source catalog material is unavailable",
198			)
199		})?,
200		None => current_material.source_catalog.clone(),
201	};
202	let mut files = current_material.files.clone();
203	let mut changed_sources = Vec::new();
204	let mut changed_file_indexes = BTreeSet::new();
205	let extract_timer = Instant::now();
206	refresh_retired_slots(RetiredSlotRefresh {
207		previous_catalog: &current_material.source_catalog,
208		source_catalog: &source_catalog,
209		cache_dir: options.cache_dir.as_deref(),
210		files: &mut files,
211		changed_sources: &mut changed_sources,
212		changed_file_indexes: &mut changed_file_indexes,
213	})?;
214	for file_idx in files.len()..source_catalog.sources.files.len() {
215		let file = &source_catalog.sources.files[file_idx];
216		let indexed = extract_source_file(
217			&source_catalog,
218			file_idx,
219			&file.path.clone(),
220			options.cache_dir.as_deref(),
221		)?;
222		push_unique_source(&mut changed_sources, indexed.source_id);
223		changed_file_indexes.insert(file_idx);
224		files.push(Arc::new(indexed));
225	}
226	for path in paths {
227		let Some(source) = source_catalog.resolve_source(path) else {
228			continue;
229		};
230		let Some(file_idx) = source.eager_index else {
231			continue;
232		};
233		if changed_file_indexes.contains(&file_idx) {
234			continue;
235		}
236		let indexed = extract_source_file(
237			&source_catalog,
238			file_idx,
239			&source.path,
240			options.cache_dir.as_deref(),
241		)?;
242		if let Some(slot) = files.get_mut(file_idx) {
243			push_unique_source(&mut changed_sources, indexed.source_id);
244			changed_file_indexes.insert(file_idx);
245			*slot = Arc::new(indexed);
246		}
247	}
248	let extract_sources = extract_timer.elapsed();
249	if changed_sources.is_empty() {
250		return Ok(CodeIndexRefresh {
251			index: current.clone(),
252			changed_sources,
253			graph_diff: CodeIndexGraphDiff::default(),
254		});
255	}
256	let semantic_timer = Instant::now();
257	let material = material_from_files(source_catalog, files, &WorkspaceCancellation::default())?;
258	let sources = source_records(&material.files);
259	let graph_diff = graph_diff(current_material.as_ref(), &material, &changed_file_indexes);
260	let mut symbols = current.symbols.clone();
261	let mut references = current.references.clone();
262	for file_idx in &changed_file_indexes {
263		let (file_symbols, file_references) =
264			records_for_file(*file_idx, &material.files[*file_idx]);
265		symbols.replace(*file_idx, Arc::from(file_symbols));
266		references.replace(*file_idx, Arc::from(file_references));
267	}
268	let semantic_index = semantic_timer.elapsed();
269	let generation = cache.next_generation();
270	let identity_scheme = material.identity.scheme().to_string();
271	cache.insert_index(generation, material);
272	Ok(CodeIndexRefresh {
273		index: CodeIndex {
274			generation,
275			catalog_generation: extended_catalog
276				.map(|catalog| catalog.generation)
277				.unwrap_or(current.catalog_generation),
278			identity_scheme,
279			sources,
280			symbols,
281			references,
282			timings: CodeIndexTimings {
283				extract_sources,
284				semantic_index,
285				total: total_timer.elapsed(),
286			},
287		},
288		changed_sources,
289		graph_diff,
290	})
291}
292
293struct RetiredSlotRefresh<'a> {
294	previous_catalog: &'a SourceCatalogMaterial,
295	source_catalog: &'a SourceCatalogMaterial,
296	cache_dir: Option<&'a Path>,
297	files: &'a mut Vec<Arc<IndexedSourceFile>>,
298	changed_sources: &'a mut Vec<SourceId>,
299	changed_file_indexes: &'a mut BTreeSet<usize>,
300}
301
302fn refresh_retired_slots(refresh: RetiredSlotRefresh<'_>) -> WorkspaceResult<()> {
303	let slots = refresh
304		.files
305		.len()
306		.min(refresh.source_catalog.sources.files.len());
307	for file_idx in 0..slots {
308		let was_retired = refresh.previous_catalog.sources.files[file_idx].retired;
309		let is_retired = refresh.source_catalog.sources.files[file_idx].retired;
310		if was_retired == is_retired {
311			continue;
312		}
313		let indexed = if is_retired {
314			tombstone_file(&refresh.files[file_idx])
315		} else {
316			extract_source_file(
317				refresh.source_catalog,
318				file_idx,
319				&refresh.source_catalog.sources.files[file_idx].path.clone(),
320				refresh.cache_dir,
321			)?
322		};
323		push_unique_source(refresh.changed_sources, indexed.source_id);
324		refresh.changed_file_indexes.insert(file_idx);
325		refresh.files[file_idx] = Arc::new(indexed);
326	}
327	Ok(())
328}
329
330fn tombstone_file(previous: &IndexedSourceFile) -> IndexedSourceFile {
331	IndexedSourceFile {
332		source_root: previous.source_root,
333		source_id: previous.source_id,
334		source_uri: previous.source_uri.clone(),
335		identity: previous.identity.clone(),
336		path: previous.path.clone(),
337		rel_path: previous.rel_path.clone(),
338		anchor: previous.anchor.clone(),
339		lang: previous.lang,
340		graph: code_moniker_core::core::code_graph::CodeGraph::from_records(Vec::new(), Vec::new()),
341		source: String::new(),
342	}
343}
344
345fn source_material(
346	cache: &LocalResourceCache,
347	catalog: &SourceCatalog,
348) -> WorkspaceResult<SourceCatalogMaterial> {
349	cache.source_material(catalog.generation).ok_or_else(|| {
350		WorkspaceFailure::new(
351			WorkspaceResource::CodeIndex,
352			"source catalog material is unavailable",
353		)
354	})
355}
356
357fn extract_source_files(
358	source_material: &SourceCatalogMaterial,
359	cache_dir: Option<&std::path::Path>,
360	cancellation: &WorkspaceCancellation,
361) -> WorkspaceResult<Vec<Arc<IndexedSourceFile>>> {
362	source_material
363		.sources
364		.files
365		.par_iter()
366		.enumerate()
367		.map(|(file_idx, file)| {
368			cancellation.check(WorkspaceResource::CodeIndex)?;
369			extract_source_file(source_material, file_idx, &file.path, cache_dir).map(Arc::new)
370		})
371		.collect()
372}
373
374fn extract_source_file(
375	source_material: &SourceCatalogMaterial,
376	file_idx: usize,
377	path: &Path,
378	cache_dir: Option<&Path>,
379) -> WorkspaceResult<IndexedSourceFile> {
380	let file = source_material.sources.files.get(file_idx).ok_or_else(|| {
381		WorkspaceFailure::new(
382			WorkspaceResource::CodeIndex,
383			format!("source file index {file_idx} is unavailable"),
384		)
385	})?;
386	let ctx = &source_material.sources.roots[file.source].ctx;
387	let (graph, extracted_source) = crate::cache::load_or_extract_workspace_result(
388		path,
389		&file.anchor,
390		file.lang,
391		cache_dir,
392		ctx,
393	)
394	.map_err(|err| {
395		WorkspaceFailure::new(
396			WorkspaceResource::CodeIndex,
397			format!("cannot extract {}: {err}", path.display()),
398		)
399	})?;
400	let source = match extracted_source {
401		Some(source) => source,
402		None => crate::cache::read_source_lossy(path).map_err(|err| {
403			WorkspaceFailure::new(
404				WorkspaceResource::CodeIndex,
405				format!("cannot read {}: {err}", path.display()),
406			)
407		})?,
408	};
409	Ok(IndexedSourceFile {
410		source_root: file.source,
411		source_id: source_material
412			.source_id_for_file(file_idx)
413			.ok_or_else(|| {
414				WorkspaceFailure::new(
415					WorkspaceResource::CodeIndex,
416					format!("source id is unavailable for {}", file.rel_path.display()),
417				)
418			})?,
419		source_uri: source_material
420			.source_uri_for_path(&file.path)
421			.ok_or_else(|| {
422				WorkspaceFailure::new(
423					WorkspaceResource::CodeIndex,
424					format!("source uri is unavailable for {}", file.path.display()),
425				)
426			})?,
427		identity: source_material.identity.clone(),
428		path: file.path.clone(),
429		rel_path: file.rel_path.clone(),
430		anchor: file.anchor.clone(),
431		lang: file.lang,
432		graph,
433		source,
434	})
435}
436
437fn build_semantic_index(
438	source_material: SourceCatalogMaterial,
439	files: Vec<Arc<IndexedSourceFile>>,
440	cancellation: &WorkspaceCancellation,
441) -> WorkspaceResult<(
442	RecordTable<SymbolRecord>,
443	RecordTable<ReferenceRecord>,
444	CodeIndexMaterial,
445)> {
446	let mut symbol_shards = Vec::with_capacity(files.len());
447	let mut reference_shards = Vec::with_capacity(files.len());
448	for (file_idx, file) in files.iter().enumerate() {
449		cancellation.check(WorkspaceResource::CodeIndex)?;
450		let (symbols, references) = records_for_file(file_idx, file);
451		symbol_shards.push(Arc::from(symbols));
452		reference_shards.push(Arc::from(references));
453	}
454	let material = material_from_files(source_material, files, cancellation)?;
455	Ok((
456		RecordTable::from_shards(symbol_shards),
457		RecordTable::from_shards(reference_shards),
458		material,
459	))
460}
461
462fn material_from_files(
463	source_material: SourceCatalogMaterial,
464	mut files: Vec<Arc<IndexedSourceFile>>,
465	cancellation: &WorkspaceCancellation,
466) -> WorkspaceResult<CodeIndexMaterial> {
467	let symbol_count = files.iter().map(|file| file.graph.def_count()).sum();
468	let mut symbols_by_moniker = rustc_hash::FxHashMap::default();
469	symbols_by_moniker.reserve(symbol_count);
470	for (file_idx, file) in files.iter().enumerate() {
471		cancellation.check(WorkspaceResource::CodeIndex)?;
472		for (def_idx, def) in file.graph.defs().enumerate() {
473			symbols_by_moniker.insert(
474				def.moniker.clone(),
475				file.graph_identity().symbol_id(file_idx, def_idx),
476			);
477		}
478	}
479	symbols_by_moniker.shrink_to_fit();
480	files.shrink_to_fit();
481	let identity = source_material.identity.clone();
482	Ok(CodeIndexMaterial {
483		source_catalog: source_material,
484		files,
485		identity,
486		symbols_by_moniker,
487	})
488}
489
490fn graph_diff(
491	previous: &CodeIndexMaterial,
492	next: &CodeIndexMaterial,
493	changed_files: &BTreeSet<usize>,
494) -> CodeIndexGraphDiff {
495	let mut diff = CodeIndexGraphDiff::default();
496	for file_idx in changed_files {
497		let Some(next_file) = next.files.get(*file_idx) else {
498			continue;
499		};
500		let (previous_symbols, previous_references) = match previous.files.get(*file_idx) {
501			Some(previous_file) => records_for_file(*file_idx, previous_file),
502			None => (Vec::new(), Vec::new()),
503		};
504		let (next_symbols, next_references) = records_for_file(*file_idx, next_file);
505		diff_symbols(&previous_symbols, &next_symbols, &mut diff);
506		diff_references(
507			&previous_references,
508			previous,
509			&next_references,
510			next,
511			&mut diff,
512		);
513	}
514	diff
515}
516
517fn records_for_file(
518	file_idx: usize,
519	file: &IndexedSourceFile,
520) -> (Vec<SymbolRecord>, Vec<ReferenceRecord>) {
521	let line_index = LineIndex::new(&file.source);
522	let mut symbols = Vec::with_capacity(file.graph.def_count());
523	collect_symbols(file_idx, file, &line_index, &mut symbols);
524	let mut reference_identity_pool = TargetIdentityPool::default();
525	let mut references = Vec::with_capacity(file.graph.ref_count());
526	collect_references(
527		file_idx,
528		file,
529		&line_index,
530		&mut references,
531		&mut reference_identity_pool,
532	);
533	(symbols, references)
534}
535
536fn diff_symbols(previous: &[SymbolRecord], next: &[SymbolRecord], diff: &mut CodeIndexGraphDiff) {
537	let mut next_by_key = symbol_record_indexes(next);
538	for previous_symbol in previous {
539		let key = symbol_key(previous_symbol);
540		let Some(next_idx) = pop_index(&mut next_by_key, &key) else {
541			diff.removed_symbols.push(previous_symbol.id);
542			diff.removed_symbol_identities
543				.push(previous_symbol.identity.to_string());
544			continue;
545		};
546		let next_symbol = &next[next_idx];
547		if symbol_linkage_fields_changed(previous_symbol, next_symbol) {
548			diff.modified_symbols.push(next_symbol.id);
549			diff.modified_symbol_identities
550				.push(next_symbol.identity.to_string());
551			diff.changed_symbols.push(next_symbol.id);
552			continue;
553		}
554		if previous_symbol.id != next_symbol.id {
555			diff.symbol_id_remaps
556				.push((previous_symbol.id, next_symbol.id));
557		}
558		diff.unchanged_symbols += 1;
559	}
560	for indexes in next_by_key.into_values() {
561		for idx in indexes {
562			diff.added_symbols.push(next[idx].id);
563			diff.changed_symbols.push(next[idx].id);
564		}
565	}
566}
567
568fn diff_references(
569	previous: &[ReferenceRecord],
570	previous_material: &CodeIndexMaterial,
571	next: &[ReferenceRecord],
572	next_material: &CodeIndexMaterial,
573	diff: &mut CodeIndexGraphDiff,
574) {
575	let mut next_by_key = reference_record_indexes(next, next_material);
576	for previous_reference in previous {
577		let Some(key) = reference_key(previous_reference, previous_material) else {
578			diff.removed_references.push(previous_reference.id);
579			diff.removed_reference_kinds
580				.push(previous_reference.kind.clone());
581			continue;
582		};
583		let Some(next_idx) = pop_index(&mut next_by_key, &key) else {
584			diff.removed_references.push(previous_reference.id);
585			diff.removed_reference_kinds
586				.push(previous_reference.kind.clone());
587			continue;
588		};
589		let next_reference = &next[next_idx];
590		if previous_reference.id != next_reference.id {
591			diff.reference_id_remaps
592				.push((previous_reference.id, next_reference.id));
593		}
594		diff.unchanged_references += 1;
595	}
596	for indexes in next_by_key.into_values() {
597		for idx in indexes {
598			diff.changed_references.push(next[idx].id);
599		}
600	}
601}
602
603fn symbol_record_indexes(records: &[SymbolRecord]) -> FxHashMap<Arc<str>, Vec<usize>> {
604	let mut by_key = FxHashMap::<Arc<str>, Vec<usize>>::default();
605	for (idx, record) in records.iter().enumerate() {
606		by_key.entry(symbol_key(record)).or_default().push(idx);
607	}
608	by_key
609}
610
611fn reference_record_indexes(
612	records: &[ReferenceRecord],
613	material: &CodeIndexMaterial,
614) -> FxHashMap<ReferenceKey, Vec<usize>> {
615	let mut by_key = FxHashMap::<ReferenceKey, Vec<usize>>::default();
616	for (idx, record) in records.iter().enumerate() {
617		if let Some(key) = reference_key(record, material) {
618			by_key.entry(key).or_default().push(idx);
619		}
620	}
621	by_key
622}
623
624fn pop_index<K: Eq + std::hash::Hash>(
625	by_key: &mut FxHashMap<K, Vec<usize>>,
626	key: &K,
627) -> Option<usize> {
628	let indexes = by_key.get_mut(key)?;
629	let idx = indexes.remove(0);
630	if indexes.is_empty() {
631		by_key.remove(key);
632	}
633	Some(idx)
634}
635
636fn symbol_key(symbol: &SymbolRecord) -> Arc<str> {
637	Arc::clone(&symbol.identity)
638}
639
640fn symbol_linkage_fields_changed(previous: &SymbolRecord, next: &SymbolRecord) -> bool {
641	previous.identity != next.identity
642		|| previous.name != next.name
643		|| previous.kind != next.kind
644		|| previous.visibility != next.visibility
645		|| previous.signature != next.signature
646		|| previous.call_name != next.call_name
647		|| previous.call_arity != next.call_arity
648		|| previous.navigable != next.navigable
649}
650
651#[derive(Clone, Debug, Eq, Hash, PartialEq)]
652struct ReferenceKey {
653	source_symbol_identity: String,
654	target_identity: String,
655	kind: String,
656	call_name: Option<String>,
657	call_arity: Option<usize>,
658	confidence: Option<String>,
659	receiver: Option<String>,
660	alias: Option<String>,
661}
662
663fn reference_key(
664	reference: &ReferenceRecord,
665	material: &CodeIndexMaterial,
666) -> Option<ReferenceKey> {
667	let source_symbol_identity = material
668		.symbol_moniker(&reference.source_symbol)
669		.map(|moniker| material.identity.moniker_uri(moniker))?;
670	Some(ReferenceKey {
671		source_symbol_identity,
672		target_identity: reference.target_identity.to_string(),
673		kind: reference.kind.clone(),
674		call_name: reference.call_name.clone(),
675		call_arity: reference.call_arity,
676		confidence: reference.confidence.clone(),
677		receiver: reference.receiver.clone(),
678		alias: reference.alias.clone(),
679	})
680}
681
682fn push_unique_source(sources: &mut Vec<SourceId>, source: SourceId) {
683	if !sources.iter().any(|existing| existing == &source) {
684		sources.push(source);
685	}
686}
687
688fn collect_symbols(
689	file_idx: usize,
690	file: &IndexedSourceFile,
691	line_index: &LineIndex,
692	symbols: &mut Vec<SymbolRecord>,
693) {
694	for (def_idx, def) in file.graph.defs().enumerate() {
695		let id = file.graph_identity().symbol_id(file_idx, def_idx);
696		let parent = def
697			.parent
698			.map(|parent_idx| file.graph_identity().symbol_id(file_idx, parent_idx));
699		symbols.push(SymbolRecord {
700			id,
701			source: file.source_id,
702			identity: Arc::from(file.graph_identity().moniker_uri(&def.moniker)),
703			name: last_name(&def.moniker),
704			kind: def_kind(def),
705			visibility: def_visibility(def),
706			signature: String::from_utf8_lossy(&def.signature).to_string(),
707			call_name: (!def.call_name.is_empty())
708				.then(|| String::from_utf8_lossy(&def.call_name).to_string()),
709			call_arity: def.call_arity,
710			navigable: is_navigable_def(file.lang, def),
711			line_range: def
712				.position
713				.map(|(start, end)| line_index.line_range(start, end)),
714			parent,
715		});
716	}
717}
718
719fn def_visibility(def: &code_moniker_core::core::code_graph::DefRecord) -> String {
720	std::str::from_utf8(&def.visibility)
721		.unwrap_or("")
722		.to_string()
723}
724
725fn collect_references(
726	file_idx: usize,
727	file: &IndexedSourceFile,
728	line_index: &LineIndex,
729	references: &mut Vec<ReferenceRecord>,
730	reference_identity_pool: &mut TargetIdentityPool,
731) {
732	for (ref_idx, reference) in file.graph.refs().enumerate() {
733		let id = file.graph_identity().reference_id(file_idx, ref_idx);
734		let source_symbol = file.graph_identity().symbol_id(file_idx, reference.source);
735		let target_identity =
736			reference_identity_pool.intern(file.graph_identity(), &reference.target);
737		references.push(
738			ReferenceRecord::new(
739				id,
740				file.source_id,
741				source_symbol,
742				target_identity,
743				ref_kind(reference),
744				reference
745					.position
746					.map(|(start, end)| line_index.line_range(start, end)),
747			)
748			.with_call_metadata(ref_attr(&reference.call_name), reference.call_arity)
749			.with_metadata(
750				ref_attr(&reference.confidence),
751				ref_attr(&reference.receiver_hint),
752				ref_attr(&reference.alias),
753			),
754		);
755	}
756}
757
758#[derive(Default)]
759struct TargetIdentityPool {
760	values: rustc_hash::FxHashMap<Moniker, Arc<str>>,
761}
762
763impl TargetIdentityPool {
764	fn intern(&mut self, identity: &LocalIdentityResolver, target: &Moniker) -> Arc<str> {
765		if let Some(existing) = self.values.get(target) {
766			return Arc::clone(existing);
767		}
768		let shared = Arc::<str>::from(identity.moniker_uri(target));
769		self.values.insert(target.clone(), Arc::clone(&shared));
770		shared
771	}
772}
773
774fn source_records(files: &[Arc<IndexedSourceFile>]) -> Vec<SourceFileRecord> {
775	files
776		.iter()
777		.map(|file| SourceFileRecord {
778			id: file.source_id,
779			uri: file.source_uri.clone(),
780			source_root: file.source_root,
781			path: file.path.display().to_string(),
782			rel_path: file.rel_path.display().to_string(),
783			anchor: file.anchor.display().to_string(),
784			language: file.lang.tag().to_string(),
785			text: String::new(),
786		})
787		.collect()
788}
789
790fn ref_attr(bytes: &[u8]) -> Option<String> {
791	if bytes.is_empty() {
792		return None;
793	}
794	std::str::from_utf8(bytes)
795		.ok()
796		.filter(|value| !value.is_empty())
797		.map(ToOwned::to_owned)
798}
799
800trait IndexedSourceIdentity {
801	fn graph_identity(&self) -> &LocalIdentityResolver;
802}
803
804impl IndexedSourceIdentity for IndexedSourceFile {
805	fn graph_identity(&self) -> &LocalIdentityResolver {
806		&self.identity
807	}
808}