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}