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