Skip to main content

code_moniker_workspace/source/
content.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::path::{Path, PathBuf};
3use std::sync::{Arc, Mutex};
4use std::time::Duration;
5
6use code_moniker_core::core::code_graph::CodeGraph;
7use code_moniker_core::core::moniker::{Moniker, MonikerBuilder};
8use code_moniker_core::lang::Lang;
9use rustc_hash::FxHashMap;
10
11use crate::environment::{self, SourceFileSet, SourceRoot};
12use crate::path_util::lexical_path;
13use crate::snapshot::{ReferenceId, SourceId, SymbolId};
14
15use super::identity::LocalIdentityResolver;
16
17pub const MEMORY_SOURCE_ROOT: &str = "memory";
18pub const MEMORY_SOURCE_ROOT_LABEL: &str = "memory";
19const MEMORY_SOURCE_PATH_ROOT: &str = ".code-moniker-memory";
20
21pub fn is_memory_source_path(path: &Path) -> bool {
22	path.starts_with(MEMORY_SOURCE_PATH_ROOT)
23}
24
25#[derive(Clone, Default)]
26pub struct LocalResourceCache {
27	inner: Arc<Mutex<LocalResourceMaterial>>,
28}
29
30impl LocalResourceCache {
31	pub fn checkpoint_memory_source_update(
32		&self,
33		update: &mut MemorySourceSetUpdate,
34	) -> LocalResourceCheckpoint {
35		let inner = self.lock_material();
36		LocalResourceCheckpoint {
37			next_generation: inner.next_generation,
38			sources: inner.sources.clone(),
39			indexes: inner.indexes.clone(),
40			index_diffs: inner.index_diffs.clone(),
41			srcset: update.srcset.clone(),
42			previous: update.previous.take(),
43		}
44	}
45
46	pub fn restore_checkpoint(&self, checkpoint: LocalResourceCheckpoint) {
47		let mut inner = self.lock_material();
48		if let Some(current) = inner.memory_source_sets.remove(&checkpoint.srcset) {
49			for document in current.documents {
50				inner
51					.memory_documents
52					.remove(&memory_source_path(&checkpoint.srcset, &document.uri));
53			}
54		}
55		if let Some(previous) = checkpoint.previous {
56			for document in &previous.documents {
57				inner.memory_documents.insert(
58					memory_source_path(&checkpoint.srcset, &document.uri),
59					CachedMemorySource {
60						srcset: checkpoint.srcset.clone(),
61						document: document.clone(),
62					},
63				);
64			}
65			inner.memory_source_sets.insert(checkpoint.srcset, previous);
66		}
67		inner.next_generation = checkpoint.next_generation;
68		inner.sources = checkpoint.sources;
69		inner.indexes = checkpoint.indexes;
70		inner.index_diffs = checkpoint.index_diffs;
71	}
72
73	pub fn next_generation(&self) -> crate::snapshot::ResourceGeneration {
74		let mut inner = self.lock_material();
75		let generation = crate::snapshot::ResourceGeneration::new(inner.next_generation);
76		inner.next_generation += 1;
77		generation
78	}
79
80	fn lock_material(&self) -> std::sync::MutexGuard<'_, LocalResourceMaterial> {
81		self.inner
82			.lock()
83			.unwrap_or_else(|poisoned| poisoned.into_inner())
84	}
85
86	pub fn insert_sources(
87		&self,
88		generation: crate::snapshot::ResourceGeneration,
89		material: SourceCatalogMaterial,
90	) {
91		let mut inner = self.lock_material();
92		inner.sources.clear();
93		inner.sources.insert(generation.value(), Arc::new(material));
94	}
95
96	pub fn source_material(
97		&self,
98		generation: crate::snapshot::ResourceGeneration,
99	) -> Option<SourceCatalogMaterial> {
100		self.lock_material()
101			.sources
102			.get(&generation.value())
103			.map(|material| material.as_ref().clone())
104	}
105
106	pub fn insert_index(
107		&self,
108		generation: crate::snapshot::ResourceGeneration,
109		material: CodeIndexMaterial,
110	) {
111		let mut inner = self.lock_material();
112		inner.indexes.clear();
113		inner.index_diffs.clear();
114		inner.indexes.insert(generation.value(), Arc::new(material));
115	}
116
117	pub fn insert_index_diff(
118		&self,
119		generation: crate::snapshot::ResourceGeneration,
120		previous_generation: crate::snapshot::ResourceGeneration,
121		diff: crate::code::CodeIndexGraphDiff,
122	) {
123		self.lock_material()
124			.index_diffs
125			.insert(generation.value(), (previous_generation, Arc::new(diff)));
126	}
127
128	pub fn index_diff(
129		&self,
130		generation: crate::snapshot::ResourceGeneration,
131	) -> Option<(
132		crate::snapshot::ResourceGeneration,
133		Arc<crate::code::CodeIndexGraphDiff>,
134	)> {
135		self.lock_material()
136			.index_diffs
137			.get(&generation.value())
138			.cloned()
139	}
140
141	pub fn index_material(
142		&self,
143		generation: crate::snapshot::ResourceGeneration,
144	) -> Option<Arc<CodeIndexMaterial>> {
145		self.lock_material()
146			.indexes
147			.get(&generation.value())
148			.cloned()
149	}
150
151	pub fn replace_memory_source_set(
152		&self,
153		mut source_set: MemorySourceSet,
154	) -> MemorySourceSetUpdate {
155		let mut inner = self.lock_material();
156		source_set
157			.documents
158			.sort_by(|left, right| left.uri.cmp(&right.uri));
159		if inner.memory_source_sets.get(&source_set.srcset) == Some(&source_set) {
160			let document_total = source_set.documents.len();
161			return MemorySourceSetUpdate {
162				srcset: source_set.srcset,
163				delta: MemorySourceSetDelta {
164					unchanged: document_total,
165					..Default::default()
166				},
167				document_total,
168				..Default::default()
169			};
170		}
171		let srcset = source_set.srcset.clone();
172		let delta = memory_source_set_delta(inner.memory_source_sets.get(&srcset), &source_set);
173		let document_total = source_set.documents.len();
174		for uri in &delta.removed {
175			inner
176				.memory_documents
177				.remove(&memory_source_path(&srcset, uri));
178		}
179		for uri in delta.added.iter().chain(&delta.modified) {
180			let document = source_set
181				.documents
182				.binary_search_by(|document| document.uri.as_str().cmp(uri))
183				.ok()
184				.and_then(|index| source_set.documents.get(index))
185				.expect("memory source delta URI belongs to the replacement")
186				.clone();
187			inner.memory_documents.insert(
188				memory_source_path(&srcset, uri),
189				CachedMemorySource {
190					srcset: srcset.clone(),
191					document,
192				},
193			);
194		}
195		let previous = inner.memory_source_sets.insert(srcset.clone(), source_set);
196		MemorySourceSetUpdate {
197			changed: true,
198			paths: delta.paths(&srcset),
199			srcset,
200			previous,
201			delta,
202			document_total,
203		}
204	}
205
206	pub fn remove_memory_source_set(&self, srcset: &str) -> MemorySourceSetUpdate {
207		let mut inner = self.lock_material();
208		let Some(previous) = inner.memory_source_sets.remove(srcset) else {
209			return MemorySourceSetUpdate {
210				srcset: srcset.to_string(),
211				..Default::default()
212			};
213		};
214		for document in &previous.documents {
215			inner
216				.memory_documents
217				.remove(&memory_source_path(srcset, &document.uri));
218		}
219		MemorySourceSetUpdate {
220			changed: true,
221			paths: memory_source_paths(&previous),
222			srcset: srcset.to_string(),
223			delta: MemorySourceSetDelta {
224				removed: previous
225					.documents
226					.iter()
227					.map(|document| document.uri.clone())
228					.collect(),
229				..Default::default()
230			},
231			previous: Some(previous),
232			document_total: 0,
233		}
234	}
235
236	pub fn memory_source_usage_after_replacing(
237		&self,
238		source_set: &MemorySourceSet,
239	) -> (usize, usize, usize) {
240		let inner = self.lock_material();
241		let mut source_sets = 0usize;
242		let mut documents = 0usize;
243		let mut bytes = 0usize;
244		for (srcset, active) in &inner.memory_source_sets {
245			if srcset == &source_set.srcset {
246				continue;
247			}
248			source_sets = source_sets.saturating_add(1);
249			documents = documents.saturating_add(active.documents.len());
250			bytes = bytes.saturating_add(active.size_bytes());
251		}
252		(
253			source_sets.saturating_add(1),
254			documents.saturating_add(source_set.documents.len()),
255			bytes.saturating_add(source_set.size_bytes()),
256		)
257	}
258
259	pub(crate) fn memory_source_sets(&self) -> BTreeMap<String, MemorySourceSet> {
260		self.lock_material().memory_source_sets.clone()
261	}
262
263	pub(crate) fn memory_source_entries(
264		&self,
265		paths: &[PathBuf],
266	) -> BTreeMap<PathBuf, CachedMemorySource> {
267		let inner = self.lock_material();
268		paths
269			.iter()
270			.filter_map(|path| {
271				inner
272					.memory_documents
273					.get(path)
274					.cloned()
275					.map(|source| (path.clone(), source))
276			})
277			.collect()
278	}
279
280	pub(crate) fn memory_source_revisions(&self) -> BTreeMap<String, Option<String>> {
281		self.lock_material()
282			.memory_source_sets
283			.iter()
284			.map(|(srcset, source_set)| (srcset.clone(), source_set.revision.clone()))
285			.collect()
286	}
287}
288
289struct LocalResourceMaterial {
290	next_generation: u64,
291	sources: BTreeMap<u64, Arc<SourceCatalogMaterial>>,
292	indexes: BTreeMap<u64, Arc<CodeIndexMaterial>>,
293	memory_source_sets: BTreeMap<String, MemorySourceSet>,
294	memory_documents: BTreeMap<PathBuf, CachedMemorySource>,
295	index_diffs: BTreeMap<
296		u64,
297		(
298			crate::snapshot::ResourceGeneration,
299			Arc<crate::code::CodeIndexGraphDiff>,
300		),
301	>,
302}
303
304pub struct LocalResourceCheckpoint {
305	next_generation: u64,
306	sources: BTreeMap<u64, Arc<SourceCatalogMaterial>>,
307	indexes: BTreeMap<u64, Arc<CodeIndexMaterial>>,
308	index_diffs: BTreeMap<
309		u64,
310		(
311			crate::snapshot::ResourceGeneration,
312			Arc<crate::code::CodeIndexGraphDiff>,
313		),
314	>,
315	srcset: String,
316	previous: Option<MemorySourceSet>,
317}
318
319#[derive(Clone, Debug, Eq, PartialEq)]
320pub(crate) struct CachedMemorySource {
321	pub(crate) srcset: String,
322	pub(crate) document: MemorySourceDocument,
323}
324
325impl Default for LocalResourceMaterial {
326	fn default() -> Self {
327		Self {
328			next_generation: 1,
329			sources: BTreeMap::new(),
330			indexes: BTreeMap::new(),
331			memory_source_sets: BTreeMap::new(),
332			memory_documents: BTreeMap::new(),
333			index_diffs: BTreeMap::new(),
334		}
335	}
336}
337
338#[derive(Clone, Debug, Eq, PartialEq)]
339pub struct MemorySourceSet {
340	pub srcset: String,
341	pub revision: Option<String>,
342	pub documents: Vec<MemorySourceDocument>,
343}
344
345#[derive(Clone, Debug, Eq, PartialEq)]
346pub struct MemorySourceDocument {
347	pub uri: String,
348	pub lang: Lang,
349	pub content: Arc<str>,
350}
351
352impl MemorySourceSet {
353	pub fn size_bytes(&self) -> usize {
354		self.srcset
355			.len()
356			.saturating_add(self.revision.as_ref().map_or(0, String::len))
357			.saturating_add(self.documents.iter().fold(0usize, |total, document| {
358				total
359					.saturating_add(document.uri.len())
360					.saturating_add(document.content.len())
361					.saturating_add(document.lang.tag().len())
362			}))
363	}
364}
365
366#[derive(Clone, Debug, Default, Eq, PartialEq)]
367pub struct MemorySourceSetUpdate {
368	pub changed: bool,
369	pub paths: Vec<PathBuf>,
370	pub srcset: String,
371	pub previous: Option<MemorySourceSet>,
372	pub delta: MemorySourceSetDelta,
373	pub document_total: usize,
374}
375
376#[derive(Clone, Debug, Default, Eq, PartialEq)]
377pub struct MemorySourceSetDelta {
378	pub added: Vec<String>,
379	pub modified: Vec<String>,
380	pub removed: Vec<String>,
381	pub unchanged: usize,
382}
383
384impl MemorySourceSetDelta {
385	fn paths(&self, srcset: &str) -> Vec<PathBuf> {
386		self.added
387			.iter()
388			.chain(&self.modified)
389			.chain(&self.removed)
390			.map(|uri| memory_source_path(srcset, uri))
391			.collect::<BTreeSet<_>>()
392			.into_iter()
393			.collect()
394	}
395}
396
397fn memory_source_set_delta(
398	previous: Option<&MemorySourceSet>,
399	next: &MemorySourceSet,
400) -> MemorySourceSetDelta {
401	let previous = previous.map_or(&[][..], |source_set| source_set.documents.as_slice());
402	let mut delta = MemorySourceSetDelta::default();
403	let mut previous_idx = 0;
404	let mut next_idx = 0;
405	while previous_idx < previous.len() || next_idx < next.documents.len() {
406		match (previous.get(previous_idx), next.documents.get(next_idx)) {
407			(Some(left), Some(right)) => match left.uri.cmp(&right.uri) {
408				std::cmp::Ordering::Less => {
409					delta.removed.push(left.uri.clone());
410					previous_idx += 1;
411				}
412				std::cmp::Ordering::Greater => {
413					delta.added.push(right.uri.clone());
414					next_idx += 1;
415				}
416				std::cmp::Ordering::Equal => {
417					if left == right {
418						delta.unchanged += 1;
419					} else {
420						delta.modified.push(right.uri.clone());
421					}
422					previous_idx += 1;
423					next_idx += 1;
424				}
425			},
426			(Some(left), None) => {
427				delta.removed.push(left.uri.clone());
428				previous_idx += 1;
429			}
430			(None, Some(right)) => {
431				delta.added.push(right.uri.clone());
432				next_idx += 1;
433			}
434			(None, None) => break,
435		}
436	}
437	delta
438}
439
440pub(crate) fn memory_source_path(srcset: &str, uri: &str) -> PathBuf {
441	PathBuf::from(MEMORY_SOURCE_PATH_ROOT)
442		.join(srcset)
443		.join(hex_path_component(uri.as_bytes()))
444}
445
446fn hex_path_component(value: &[u8]) -> String {
447	const HEX: &[u8; 16] = b"0123456789abcdef";
448	let mut encoded = String::with_capacity(value.len() * 2);
449	for byte in value {
450		encoded.push(HEX[(byte >> 4) as usize] as char);
451		encoded.push(HEX[(byte & 0x0f) as usize] as char);
452	}
453	encoded
454}
455
456fn memory_source_paths(source_set: &MemorySourceSet) -> Vec<PathBuf> {
457	source_set
458		.documents
459		.iter()
460		.map(|document| memory_source_path(&source_set.srcset, &document.uri))
461		.collect()
462}
463
464#[derive(Clone)]
465pub struct SourceCatalogMaterial {
466	pub(crate) sources: SourceFileSet,
467	pub(crate) identity: LocalIdentityResolver,
468	pub(crate) memory_sources: BTreeMap<PathBuf, Arc<str>>,
469	pub(crate) memory_slots: BTreeSet<PathBuf>,
470	pub(crate) memory_revisions: BTreeMap<String, Option<String>>,
471}
472
473impl SourceCatalogMaterial {
474	pub(crate) fn source_id_for_file(&self, file_idx: usize) -> Option<SourceId> {
475		let file = self.sources.files.get(file_idx)?;
476		Some(self.identity.source_id(file_idx, &file.rel_path))
477	}
478
479	pub fn source_uri_for_path(&self, path: &Path) -> Option<String> {
480		let file_idx = self.normalized_file_index(path)?;
481		let file = self.sources.files.get(file_idx)?;
482		let rel_path = file.rel_path.as_path();
483		Some(
484			match self
485				.is_memory_slot(&file.path)
486				.then_some(file.srcset.as_deref())
487				.flatten()
488			{
489				Some(srcset) => {
490					let rel_path = crate::path_util::portable_path(rel_path);
491					let moniker = MonikerBuilder::new()
492						.project(b".")
493						.segment(b"srcset", srcset.as_bytes())
494						.segment(b"file", rel_path.as_bytes())
495						.build();
496					self.identity.moniker_uri(&moniker)
497				}
498				None => self.identity.source_uri(rel_path),
499			},
500		)
501	}
502
503	#[allow(dead_code)]
504	pub(crate) fn resolve_source(&self, path: &Path) -> Option<ResolvedSourceResource> {
505		SourceResourceLookup::new(self).resolve(path)
506	}
507
508	pub(crate) fn normalized_file_index(&self, path: &Path) -> Option<usize> {
509		let normalized = normalize_path(path);
510		self.sources.files.iter().position(|file| {
511			normalize_path(&file.path) == normalized
512				|| normalize_path(&file.rel_path) == normalized
513				|| normalize_path(&file.anchor) == normalized
514		})
515	}
516
517	pub(crate) fn memory_source(&self, path: &Path) -> Option<&str> {
518		self.memory_sources.get(path).map(AsRef::as_ref)
519	}
520
521	pub(crate) fn is_memory_slot(&self, path: &Path) -> bool {
522		self.memory_slots.contains(path)
523	}
524
525	#[allow(dead_code)]
526	fn root_for_path(&self, path: &Path) -> Option<(usize, &SourceRoot)> {
527		self.sources
528			.roots
529			.iter()
530			.enumerate()
531			.filter_map(|(root_idx, root)| {
532				let absolute = absolute_path_against_root(&root.path, path);
533				let root_path = normalize_path(&root.path);
534				normalize_path(&absolute)
535					.starts_with(&root_path)
536					.then_some((root_idx, root, root_path.components().count()))
537			})
538			.max_by_key(|(_, _, depth)| *depth)
539			.map(|(root_idx, root, _)| (root_idx, root))
540	}
541}
542
543#[allow(dead_code)]
544struct SourceResourceLookup<'a> {
545	material: &'a SourceCatalogMaterial,
546}
547
548impl<'a> SourceResourceLookup<'a> {
549	fn new(material: &'a SourceCatalogMaterial) -> Self {
550		Self { material }
551	}
552
553	fn resolve(&self, path: &Path) -> Option<ResolvedSourceResource> {
554		self.indexed(path).or_else(|| self.lazy(path))
555	}
556
557	fn indexed(&self, path: &Path) -> Option<ResolvedSourceResource> {
558		let file_idx = self.match_indexed_file(path)?;
559		let file = self.material.sources.files.get(file_idx)?;
560		Some(ResolvedSourceResource {
561			source_root: file.source,
562			source_id: self.material.identity.source_id(file_idx, &file.rel_path),
563			source_uri: self.material.identity.source_uri(&file.rel_path),
564			path: file.path.clone(),
565			rel_path: file.rel_path.clone(),
566			anchor: file.anchor.clone(),
567			lang: file.lang,
568			eager_index: Some(file_idx),
569		})
570	}
571
572	fn match_indexed_file(&self, path: &Path) -> Option<usize> {
573		self.material
574			.sources
575			.files
576			.iter()
577			.enumerate()
578			.filter(|(_, file)| path.ends_with(&file.rel_path))
579			.max_by_key(|(_, file)| file.rel_path.components().count())
580			.map(|(file_idx, _)| file_idx)
581			.or_else(|| self.material.normalized_file_index(path))
582	}
583
584	fn lazy(&self, path: &Path) -> Option<ResolvedSourceResource> {
585		let (source_root, root) = self.material.root_for_path(path)?;
586		let abs_path = absolute_path_against_root(&root.path, path);
587		if !abs_path.is_file() {
588			return None;
589		}
590		let lang = environment::language_for_path(&abs_path).ok()?;
591		let rel = abs_path.strip_prefix(&root.path).ok()?.to_path_buf();
592		let rel_path = self.rel_path(root, &rel);
593		Some(ResolvedSourceResource {
594			source_root,
595			source_id: SourceId::at(u32::MAX as usize),
596			source_uri: self.material.identity.source_uri(&rel_path),
597			path: abs_path,
598			rel_path,
599			anchor: rel,
600			lang,
601			eager_index: None,
602		})
603	}
604
605	fn rel_path(&self, root: &SourceRoot, rel: &Path) -> PathBuf {
606		if self.material.sources.multi {
607			PathBuf::from(&root.label).join(rel)
608		} else {
609			rel.to_path_buf()
610		}
611	}
612}
613
614#[derive(Clone)]
615#[allow(dead_code)]
616pub struct ResolvedSourceResource {
617	pub(crate) source_root: usize,
618	pub(crate) source_id: SourceId,
619	pub(crate) source_uri: String,
620	pub(crate) path: PathBuf,
621	pub(crate) rel_path: PathBuf,
622	pub(crate) anchor: PathBuf,
623	pub(crate) lang: Lang,
624	pub(crate) eager_index: Option<usize>,
625}
626
627#[derive(Clone)]
628pub struct CodeIndexMaterial {
629	pub source_catalog: SourceCatalogMaterial,
630	pub files: Vec<Arc<IndexedSourceFile>>,
631	pub identity: LocalIdentityResolver,
632	pub symbols_by_moniker: FxHashMap<Moniker, SymbolId>,
633}
634
635impl CodeIndexMaterial {
636	pub fn source_set(&self) -> &SourceFileSet {
637		&self.source_catalog.sources
638	}
639
640	pub fn symbol_moniker(&self, symbol: &SymbolId) -> Option<&Moniker> {
641		let (file_idx, def_idx) = self.identity.symbol_location(symbol)?;
642		let graph = &self.files.get(file_idx)?.graph;
643		(def_idx < graph.def_count()).then(|| &graph.def_at(def_idx).moniker)
644	}
645
646	pub fn symbol_source(&self, symbol: &SymbolId) -> Option<SourceId> {
647		let (file_idx, def_idx) = self.identity.symbol_location(symbol)?;
648		let file = self.files.get(file_idx)?;
649		(def_idx < file.graph.def_count()).then(|| file.source_id)
650	}
651
652	pub fn symbol_exists(&self, symbol: &SymbolId) -> bool {
653		self.symbol_moniker(symbol).is_some()
654	}
655
656	pub fn reference_target(&self, reference: &ReferenceId) -> Option<&Moniker> {
657		let (file_idx, ref_idx) = self.identity.reference_location(reference)?;
658		let graph = &self.files.get(file_idx)?.graph;
659		(ref_idx < graph.ref_count()).then(|| &graph.ref_at(ref_idx).target)
660	}
661
662	pub fn symbols(&self) -> impl Iterator<Item = (SymbolId, &Moniker)> + '_ {
663		self.files.iter().enumerate().flat_map(|(file_idx, file)| {
664			file.graph.defs().enumerate().map(move |(def_idx, def)| {
665				(file.identity.symbol_id(file_idx, def_idx), &def.moniker)
666			})
667		})
668	}
669}
670
671#[derive(Clone)]
672pub struct IndexedSourceFile {
673	pub source_root: usize,
674	pub source_id: SourceId,
675	pub source_uri: String,
676	pub identity: LocalIdentityResolver,
677	pub path: PathBuf,
678	pub rel_path: PathBuf,
679	pub anchor: PathBuf,
680	pub lang: Lang,
681	pub graph: CodeGraph,
682	pub source: String,
683	pub extraction_cache: &'static str,
684	pub extraction_duration: Duration,
685}
686
687fn normalize_path(path: &Path) -> PathBuf {
688	lexical_path(path)
689}
690
691#[allow(dead_code)]
692fn absolute_path_against_root(root: &Path, path: &Path) -> PathBuf {
693	if path.is_absolute() {
694		normalize_path(path)
695	} else {
696		normalize_path(&root.join(path))
697	}
698}
699
700#[cfg(test)]
701mod tests {
702	use super::*;
703	use code_moniker_core::core::moniker::MonikerBuilder;
704	use code_moniker_core::lang::Lang;
705
706	#[test]
707	fn symbol_moniker_returns_none_for_out_of_range_symbol_id() {
708		let (material, root, _) = material_with_one_reference();
709
710		assert_eq!(material.symbol_moniker(&SymbolId::at(0, 0)), Some(&root));
711		assert!(material.symbol_moniker(&SymbolId::at(0, 999999)).is_none());
712	}
713
714	#[test]
715	fn reference_target_returns_none_for_out_of_range_reference_id() {
716		let (material, _, target) = material_with_one_reference();
717
718		assert_eq!(
719			material.reference_target(&ReferenceId::at(0, 0)),
720			Some(&target)
721		);
722		assert!(
723			material
724				.reference_target(&ReferenceId::at(0, 999999))
725				.is_none()
726		);
727	}
728
729	#[test]
730	fn memory_source_set_replacement_reports_only_the_document_delta() {
731		let cache = LocalResourceCache::default();
732		let first = memory_set(
733			"r1",
734			vec![
735				memory_document("keep.sql", Lang::Sql, "select 1;"),
736				memory_document("modify.sql", Lang::Sql, "select 2;"),
737				memory_document("remove.sql", Lang::Sql, "select 3;"),
738			],
739		);
740		let initial = cache.replace_memory_source_set(first);
741		assert_eq!(initial.delta.added.len(), 3);
742		assert_eq!(initial.paths.len(), 3);
743
744		let update = cache.replace_memory_source_set(memory_set(
745			"r2",
746			vec![
747				memory_document("add.sql", Lang::Sql, "select 4;"),
748				memory_document("modify.sql", Lang::Sql, "select 20;"),
749				memory_document("keep.sql", Lang::Sql, "select 1;"),
750			],
751		));
752
753		assert!(update.changed);
754		assert_eq!(
755			update
756				.delta
757				.added
758				.iter()
759				.map(String::as_str)
760				.collect::<Vec<_>>(),
761			vec!["add.sql"]
762		);
763		assert_eq!(
764			update
765				.delta
766				.modified
767				.iter()
768				.map(String::as_str)
769				.collect::<Vec<_>>(),
770			vec!["modify.sql"]
771		);
772		assert_eq!(
773			update
774				.delta
775				.removed
776				.iter()
777				.map(String::as_str)
778				.collect::<Vec<_>>(),
779			vec!["remove.sql"]
780		);
781		assert_eq!(update.delta.unchanged, 1);
782		assert_eq!(update.paths.len(), 3);
783		assert_eq!(update.document_total, 3);
784	}
785
786	#[test]
787	fn memory_source_set_delta_distinguishes_language_uri_and_revision_changes() {
788		let cache = LocalResourceCache::default();
789		cache.replace_memory_source_set(memory_set(
790			"r1",
791			vec![
792				memory_document("language.sql", Lang::Sql, "same"),
793				memory_document("old.sql", Lang::Sql, "same"),
794			],
795		));
796
797		let update = cache.replace_memory_source_set(memory_set(
798			"r2",
799			vec![
800				memory_document("new.sql", Lang::Sql, "same"),
801				memory_document("language.sql", Lang::Ts, "same"),
802			],
803		));
804		assert_eq!(update.delta.added.len(), 1);
805		assert_eq!(update.delta.modified.len(), 1);
806		assert_eq!(update.delta.removed.len(), 1);
807		assert_eq!(update.delta.unchanged, 0);
808
809		let revision_only = cache.replace_memory_source_set(memory_set(
810			"r3",
811			vec![
812				memory_document("language.sql", Lang::Ts, "same"),
813				memory_document("new.sql", Lang::Sql, "same"),
814			],
815		));
816		assert!(revision_only.changed);
817		assert!(revision_only.paths.is_empty());
818		assert_eq!(revision_only.delta.unchanged, 2);
819	}
820
821	#[test]
822	fn memory_source_set_document_order_is_not_a_change() {
823		let cache = LocalResourceCache::default();
824		cache.replace_memory_source_set(memory_set(
825			"r1",
826			vec![
827				memory_document("b.sql", Lang::Sql, "select 2;"),
828				memory_document("a.sql", Lang::Sql, "select 1;"),
829			],
830		));
831
832		let update = cache.replace_memory_source_set(memory_set(
833			"r1",
834			vec![
835				memory_document("a.sql", Lang::Sql, "select 1;"),
836				memory_document("b.sql", Lang::Sql, "select 2;"),
837			],
838		));
839		assert!(!update.changed);
840		assert!(update.paths.is_empty());
841	}
842
843	#[test]
844	fn memory_source_set_checkpoint_restores_the_previous_publication_state() {
845		let cache = LocalResourceCache::default();
846		let first = memory_set(
847			"r1",
848			vec![memory_document("table.sql", Lang::Sql, "select 1;")],
849		);
850		cache.replace_memory_source_set(first.clone());
851		let mut update = cache.replace_memory_source_set(memory_set(
852			"r2",
853			vec![memory_document("table.sql", Lang::Sql, "select 2;")],
854		));
855		let checkpoint = cache.checkpoint_memory_source_update(&mut update);
856
857		cache.restore_checkpoint(checkpoint);
858
859		let restored = cache.replace_memory_source_set(first);
860		assert!(!restored.changed);
861		assert_eq!(restored.delta.unchanged, 1);
862		let path = memory_source_path("catalog", "table.sql");
863		let entries = cache.memory_source_entries(std::slice::from_ref(&path));
864		assert_eq!(
865			entries
866				.get(&path)
867				.expect("restored memory source")
868				.document
869				.content
870				.as_ref(),
871			"select 1;"
872		);
873	}
874
875	fn memory_set(revision: &str, documents: Vec<MemorySourceDocument>) -> MemorySourceSet {
876		MemorySourceSet {
877			srcset: "catalog".to_string(),
878			revision: Some(revision.to_string()),
879			documents,
880		}
881	}
882
883	fn memory_document(uri: &str, lang: Lang, content: &str) -> MemorySourceDocument {
884		MemorySourceDocument {
885			uri: uri.to_string(),
886			lang,
887			content: Arc::from(content),
888		}
889	}
890
891	fn material_with_one_reference() -> (CodeIndexMaterial, Moniker, Moniker) {
892		let identity = LocalIdentityResolver::default();
893		let root = MonikerBuilder::new()
894			.project(b"app")
895			.segment(b"module", b"main")
896			.build();
897		let target = MonikerBuilder::new()
898			.project(b"app")
899			.segment(b"module", b"other")
900			.build();
901		let mut graph = CodeGraph::new(root.clone(), b"module");
902		graph
903			.add_ref(&root, target.clone(), b"calls", None)
904			.expect("test graph ref must be valid");
905		let rel_path = PathBuf::from("main.rs");
906		let file = IndexedSourceFile {
907			source_root: 0,
908			source_id: identity.source_id(0, &rel_path),
909			source_uri: identity.source_uri(&rel_path),
910			identity: identity.clone(),
911			path: rel_path.clone(),
912			rel_path: rel_path.clone(),
913			anchor: rel_path,
914			lang: Lang::Rs,
915			graph,
916			source: String::new(),
917			extraction_cache: "provided",
918			extraction_duration: Duration::ZERO,
919		};
920		let material = CodeIndexMaterial {
921			source_catalog: SourceCatalogMaterial {
922				sources: SourceFileSet {
923					roots: Vec::new(),
924					files: Vec::new(),
925					multi: false,
926				},
927				identity: identity.clone(),
928				memory_sources: BTreeMap::new(),
929				memory_slots: BTreeSet::new(),
930				memory_revisions: BTreeMap::new(),
931			},
932			files: vec![Arc::new(file)],
933			identity,
934			symbols_by_moniker: FxHashMap::default(),
935		};
936		(material, root, target)
937	}
938}