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 next_generation(&self) -> crate::snapshot::ResourceGeneration {
32 let mut inner = self.lock_material();
33 let generation = crate::snapshot::ResourceGeneration::new(inner.next_generation);
34 inner.next_generation += 1;
35 generation
36 }
37
38 fn lock_material(&self) -> std::sync::MutexGuard<'_, LocalResourceMaterial> {
39 self.inner
40 .lock()
41 .unwrap_or_else(|poisoned| poisoned.into_inner())
42 }
43
44 pub fn insert_sources(
45 &self,
46 generation: crate::snapshot::ResourceGeneration,
47 material: SourceCatalogMaterial,
48 ) {
49 let mut inner = self.lock_material();
50 inner.sources.clear();
51 inner.sources.insert(generation.value(), material);
52 }
53
54 pub fn source_material(
55 &self,
56 generation: crate::snapshot::ResourceGeneration,
57 ) -> Option<SourceCatalogMaterial> {
58 self.lock_material()
59 .sources
60 .get(&generation.value())
61 .cloned()
62 }
63
64 pub fn insert_index(
65 &self,
66 generation: crate::snapshot::ResourceGeneration,
67 material: CodeIndexMaterial,
68 ) {
69 let mut inner = self.lock_material();
70 inner.indexes.clear();
71 inner.index_diffs.clear();
72 inner.indexes.insert(generation.value(), Arc::new(material));
73 }
74
75 pub fn insert_index_diff(
76 &self,
77 generation: crate::snapshot::ResourceGeneration,
78 previous_generation: crate::snapshot::ResourceGeneration,
79 diff: crate::code::CodeIndexGraphDiff,
80 ) {
81 self.lock_material()
82 .index_diffs
83 .insert(generation.value(), (previous_generation, Arc::new(diff)));
84 }
85
86 pub fn index_diff(
87 &self,
88 generation: crate::snapshot::ResourceGeneration,
89 ) -> Option<(
90 crate::snapshot::ResourceGeneration,
91 Arc<crate::code::CodeIndexGraphDiff>,
92 )> {
93 self.lock_material()
94 .index_diffs
95 .get(&generation.value())
96 .cloned()
97 }
98
99 pub fn index_material(
100 &self,
101 generation: crate::snapshot::ResourceGeneration,
102 ) -> Option<Arc<CodeIndexMaterial>> {
103 self.lock_material()
104 .indexes
105 .get(&generation.value())
106 .cloned()
107 }
108
109 pub fn replace_memory_source_set(&self, source_set: MemorySourceSet) -> MemorySourceSetUpdate {
110 let mut inner = self.lock_material();
111 if inner.memory_source_sets.get(&source_set.srcset) == Some(&source_set) {
112 return MemorySourceSetUpdate {
113 srcset: source_set.srcset,
114 ..Default::default()
115 };
116 }
117 let srcset = source_set.srcset.clone();
118 let next_paths = memory_source_paths(&source_set);
119 let previous = inner.memory_source_sets.insert(srcset.clone(), source_set);
120 let mut paths = previous
121 .as_ref()
122 .into_iter()
123 .flat_map(memory_source_paths)
124 .collect::<BTreeSet<_>>();
125 paths.extend(next_paths);
126 MemorySourceSetUpdate {
127 changed: true,
128 paths: paths.into_iter().collect(),
129 srcset,
130 previous,
131 }
132 }
133
134 pub fn remove_memory_source_set(&self, srcset: &str) -> MemorySourceSetUpdate {
135 let mut inner = self.lock_material();
136 let Some(previous) = inner.memory_source_sets.remove(srcset) else {
137 return MemorySourceSetUpdate {
138 srcset: srcset.to_string(),
139 ..Default::default()
140 };
141 };
142 MemorySourceSetUpdate {
143 changed: true,
144 paths: memory_source_paths(&previous),
145 srcset: srcset.to_string(),
146 previous: Some(previous),
147 }
148 }
149
150 pub fn restore_memory_source_set(&self, srcset: String, previous: Option<MemorySourceSet>) {
151 let mut inner = self.lock_material();
152 match previous {
153 Some(previous) => {
154 inner.memory_source_sets.insert(srcset, previous);
155 }
156 None => {
157 inner.memory_source_sets.remove(&srcset);
158 }
159 }
160 }
161
162 pub fn memory_source_usage_after_replacing(
163 &self,
164 source_set: &MemorySourceSet,
165 ) -> (usize, usize, usize) {
166 let inner = self.lock_material();
167 let mut source_sets = 0usize;
168 let mut documents = 0usize;
169 let mut bytes = 0usize;
170 for (srcset, active) in &inner.memory_source_sets {
171 if srcset == &source_set.srcset {
172 continue;
173 }
174 source_sets = source_sets.saturating_add(1);
175 documents = documents.saturating_add(active.documents.len());
176 bytes = bytes.saturating_add(active.size_bytes());
177 }
178 (
179 source_sets.saturating_add(1),
180 documents.saturating_add(source_set.documents.len()),
181 bytes.saturating_add(source_set.size_bytes()),
182 )
183 }
184
185 pub(crate) fn memory_source_sets(&self) -> BTreeMap<String, MemorySourceSet> {
186 self.lock_material().memory_source_sets.clone()
187 }
188}
189
190struct LocalResourceMaterial {
191 next_generation: u64,
192 sources: BTreeMap<u64, SourceCatalogMaterial>,
193 indexes: BTreeMap<u64, Arc<CodeIndexMaterial>>,
194 memory_source_sets: BTreeMap<String, MemorySourceSet>,
195 index_diffs: BTreeMap<
196 u64,
197 (
198 crate::snapshot::ResourceGeneration,
199 Arc<crate::code::CodeIndexGraphDiff>,
200 ),
201 >,
202}
203
204impl Default for LocalResourceMaterial {
205 fn default() -> Self {
206 Self {
207 next_generation: 1,
208 sources: BTreeMap::new(),
209 indexes: BTreeMap::new(),
210 memory_source_sets: BTreeMap::new(),
211 index_diffs: BTreeMap::new(),
212 }
213 }
214}
215
216#[derive(Clone, Debug, Eq, PartialEq)]
217pub struct MemorySourceSet {
218 pub srcset: String,
219 pub revision: Option<String>,
220 pub documents: Vec<MemorySourceDocument>,
221}
222
223#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct MemorySourceDocument {
225 pub uri: String,
226 pub lang: Lang,
227 pub content: String,
228}
229
230impl MemorySourceSet {
231 pub fn size_bytes(&self) -> usize {
232 self.srcset
233 .len()
234 .saturating_add(self.revision.as_ref().map_or(0, String::len))
235 .saturating_add(self.documents.iter().fold(0usize, |total, document| {
236 total
237 .saturating_add(document.uri.len())
238 .saturating_add(document.content.len())
239 .saturating_add(document.lang.tag().len())
240 }))
241 }
242}
243
244#[derive(Clone, Debug, Default, Eq, PartialEq)]
245pub struct MemorySourceSetUpdate {
246 pub changed: bool,
247 pub paths: Vec<PathBuf>,
248 pub srcset: String,
249 pub previous: Option<MemorySourceSet>,
250}
251
252pub(crate) fn memory_source_path(srcset: &str, uri: &str) -> PathBuf {
253 PathBuf::from(MEMORY_SOURCE_PATH_ROOT)
254 .join(srcset)
255 .join(hex_path_component(uri.as_bytes()))
256}
257
258fn hex_path_component(value: &[u8]) -> String {
259 const HEX: &[u8; 16] = b"0123456789abcdef";
260 let mut encoded = String::with_capacity(value.len() * 2);
261 for byte in value {
262 encoded.push(HEX[(byte >> 4) as usize] as char);
263 encoded.push(HEX[(byte & 0x0f) as usize] as char);
264 }
265 encoded
266}
267
268fn memory_source_paths(source_set: &MemorySourceSet) -> Vec<PathBuf> {
269 source_set
270 .documents
271 .iter()
272 .map(|document| memory_source_path(&source_set.srcset, &document.uri))
273 .collect()
274}
275
276#[derive(Clone)]
277pub struct SourceCatalogMaterial {
278 pub(crate) sources: SourceFileSet,
279 pub(crate) identity: LocalIdentityResolver,
280 pub(crate) memory_sources: BTreeMap<PathBuf, String>,
281 pub(crate) memory_slots: BTreeSet<PathBuf>,
282 pub(crate) memory_revisions: BTreeMap<String, Option<String>>,
283}
284
285impl SourceCatalogMaterial {
286 pub(crate) fn source_id_for_file(&self, file_idx: usize) -> Option<SourceId> {
287 let file = self.sources.files.get(file_idx)?;
288 Some(self.identity.source_id(file_idx, &file.rel_path))
289 }
290
291 pub fn source_uri_for_path(&self, path: &Path) -> Option<String> {
292 let file_idx = self.normalized_file_index(path)?;
293 let file = self.sources.files.get(file_idx)?;
294 let rel_path = file.rel_path.as_path();
295 Some(
296 match self
297 .is_memory_slot(&file.path)
298 .then_some(file.srcset.as_deref())
299 .flatten()
300 {
301 Some(srcset) => {
302 let rel_path = crate::path_util::portable_path(rel_path);
303 let moniker = MonikerBuilder::new()
304 .project(b".")
305 .segment(b"srcset", srcset.as_bytes())
306 .segment(b"file", rel_path.as_bytes())
307 .build();
308 self.identity.moniker_uri(&moniker)
309 }
310 None => self.identity.source_uri(rel_path),
311 },
312 )
313 }
314
315 #[allow(dead_code)]
316 pub(crate) fn resolve_source(&self, path: &Path) -> Option<ResolvedSourceResource> {
317 SourceResourceLookup::new(self).resolve(path)
318 }
319
320 pub(crate) fn normalized_file_index(&self, path: &Path) -> Option<usize> {
321 let normalized = normalize_path(path);
322 self.sources.files.iter().position(|file| {
323 normalize_path(&file.path) == normalized
324 || normalize_path(&file.rel_path) == normalized
325 || normalize_path(&file.anchor) == normalized
326 })
327 }
328
329 pub(crate) fn memory_source(&self, path: &Path) -> Option<&str> {
330 self.memory_sources.get(path).map(String::as_str)
331 }
332
333 pub(crate) fn is_memory_slot(&self, path: &Path) -> bool {
334 self.memory_slots.contains(path)
335 }
336
337 #[allow(dead_code)]
338 fn root_for_path(&self, path: &Path) -> Option<(usize, &SourceRoot)> {
339 self.sources
340 .roots
341 .iter()
342 .enumerate()
343 .filter_map(|(root_idx, root)| {
344 let absolute = absolute_path_against_root(&root.path, path);
345 let root_path = normalize_path(&root.path);
346 normalize_path(&absolute)
347 .starts_with(&root_path)
348 .then_some((root_idx, root, root_path.components().count()))
349 })
350 .max_by_key(|(_, _, depth)| *depth)
351 .map(|(root_idx, root, _)| (root_idx, root))
352 }
353}
354
355#[allow(dead_code)]
356struct SourceResourceLookup<'a> {
357 material: &'a SourceCatalogMaterial,
358}
359
360impl<'a> SourceResourceLookup<'a> {
361 fn new(material: &'a SourceCatalogMaterial) -> Self {
362 Self { material }
363 }
364
365 fn resolve(&self, path: &Path) -> Option<ResolvedSourceResource> {
366 self.indexed(path).or_else(|| self.lazy(path))
367 }
368
369 fn indexed(&self, path: &Path) -> Option<ResolvedSourceResource> {
370 let file_idx = self.match_indexed_file(path)?;
371 let file = self.material.sources.files.get(file_idx)?;
372 Some(ResolvedSourceResource {
373 source_root: file.source,
374 source_id: self.material.identity.source_id(file_idx, &file.rel_path),
375 source_uri: self.material.identity.source_uri(&file.rel_path),
376 path: file.path.clone(),
377 rel_path: file.rel_path.clone(),
378 anchor: file.anchor.clone(),
379 lang: file.lang,
380 eager_index: Some(file_idx),
381 })
382 }
383
384 fn match_indexed_file(&self, path: &Path) -> Option<usize> {
385 self.material
386 .sources
387 .files
388 .iter()
389 .enumerate()
390 .filter(|(_, file)| path.ends_with(&file.rel_path))
391 .max_by_key(|(_, file)| file.rel_path.components().count())
392 .map(|(file_idx, _)| file_idx)
393 .or_else(|| self.material.normalized_file_index(path))
394 }
395
396 fn lazy(&self, path: &Path) -> Option<ResolvedSourceResource> {
397 let (source_root, root) = self.material.root_for_path(path)?;
398 let abs_path = absolute_path_against_root(&root.path, path);
399 if !abs_path.is_file() {
400 return None;
401 }
402 let lang = environment::language_for_path(&abs_path).ok()?;
403 let rel = abs_path.strip_prefix(&root.path).ok()?.to_path_buf();
404 let rel_path = self.rel_path(root, &rel);
405 Some(ResolvedSourceResource {
406 source_root,
407 source_id: SourceId::at(u32::MAX as usize),
408 source_uri: self.material.identity.source_uri(&rel_path),
409 path: abs_path,
410 rel_path,
411 anchor: rel,
412 lang,
413 eager_index: None,
414 })
415 }
416
417 fn rel_path(&self, root: &SourceRoot, rel: &Path) -> PathBuf {
418 if self.material.sources.multi {
419 PathBuf::from(&root.label).join(rel)
420 } else {
421 rel.to_path_buf()
422 }
423 }
424}
425
426#[derive(Clone)]
427#[allow(dead_code)]
428pub struct ResolvedSourceResource {
429 pub(crate) source_root: usize,
430 pub(crate) source_id: SourceId,
431 pub(crate) source_uri: String,
432 pub(crate) path: PathBuf,
433 pub(crate) rel_path: PathBuf,
434 pub(crate) anchor: PathBuf,
435 pub(crate) lang: Lang,
436 pub(crate) eager_index: Option<usize>,
437}
438
439#[derive(Clone)]
440pub struct CodeIndexMaterial {
441 pub source_catalog: SourceCatalogMaterial,
442 pub files: Vec<Arc<IndexedSourceFile>>,
443 pub identity: LocalIdentityResolver,
444 pub symbols_by_moniker: FxHashMap<Moniker, SymbolId>,
445}
446
447impl CodeIndexMaterial {
448 pub fn source_set(&self) -> &SourceFileSet {
449 &self.source_catalog.sources
450 }
451
452 pub fn symbol_moniker(&self, symbol: &SymbolId) -> Option<&Moniker> {
453 let (file_idx, def_idx) = self.identity.symbol_location(symbol)?;
454 let graph = &self.files.get(file_idx)?.graph;
455 (def_idx < graph.def_count()).then(|| &graph.def_at(def_idx).moniker)
456 }
457
458 pub fn symbol_source(&self, symbol: &SymbolId) -> Option<SourceId> {
459 let (file_idx, def_idx) = self.identity.symbol_location(symbol)?;
460 let file = self.files.get(file_idx)?;
461 (def_idx < file.graph.def_count()).then(|| file.source_id)
462 }
463
464 pub fn symbol_exists(&self, symbol: &SymbolId) -> bool {
465 self.symbol_moniker(symbol).is_some()
466 }
467
468 pub fn reference_target(&self, reference: &ReferenceId) -> Option<&Moniker> {
469 let (file_idx, ref_idx) = self.identity.reference_location(reference)?;
470 let graph = &self.files.get(file_idx)?.graph;
471 (ref_idx < graph.ref_count()).then(|| &graph.ref_at(ref_idx).target)
472 }
473
474 pub fn symbols(&self) -> impl Iterator<Item = (SymbolId, &Moniker)> + '_ {
475 self.files.iter().enumerate().flat_map(|(file_idx, file)| {
476 file.graph.defs().enumerate().map(move |(def_idx, def)| {
477 (file.identity.symbol_id(file_idx, def_idx), &def.moniker)
478 })
479 })
480 }
481}
482
483#[derive(Clone)]
484pub struct IndexedSourceFile {
485 pub source_root: usize,
486 pub source_id: SourceId,
487 pub source_uri: String,
488 pub identity: LocalIdentityResolver,
489 pub path: PathBuf,
490 pub rel_path: PathBuf,
491 pub anchor: PathBuf,
492 pub lang: Lang,
493 pub graph: CodeGraph,
494 pub source: String,
495 pub extraction_cache: &'static str,
496 pub extraction_duration: Duration,
497}
498
499fn normalize_path(path: &Path) -> PathBuf {
500 lexical_path(path)
501}
502
503#[allow(dead_code)]
504fn absolute_path_against_root(root: &Path, path: &Path) -> PathBuf {
505 if path.is_absolute() {
506 normalize_path(path)
507 } else {
508 normalize_path(&root.join(path))
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use code_moniker_core::core::moniker::MonikerBuilder;
516 use code_moniker_core::lang::Lang;
517
518 #[test]
519 fn symbol_moniker_returns_none_for_out_of_range_symbol_id() {
520 let (material, root, _) = material_with_one_reference();
521
522 assert_eq!(material.symbol_moniker(&SymbolId::at(0, 0)), Some(&root));
523 assert!(material.symbol_moniker(&SymbolId::at(0, 999999)).is_none());
524 }
525
526 #[test]
527 fn reference_target_returns_none_for_out_of_range_reference_id() {
528 let (material, _, target) = material_with_one_reference();
529
530 assert_eq!(
531 material.reference_target(&ReferenceId::at(0, 0)),
532 Some(&target)
533 );
534 assert!(
535 material
536 .reference_target(&ReferenceId::at(0, 999999))
537 .is_none()
538 );
539 }
540
541 fn material_with_one_reference() -> (CodeIndexMaterial, Moniker, Moniker) {
542 let identity = LocalIdentityResolver::default();
543 let root = MonikerBuilder::new()
544 .project(b"app")
545 .segment(b"module", b"main")
546 .build();
547 let target = MonikerBuilder::new()
548 .project(b"app")
549 .segment(b"module", b"other")
550 .build();
551 let mut graph = CodeGraph::new(root.clone(), b"module");
552 graph
553 .add_ref(&root, target.clone(), b"calls", None)
554 .expect("test graph ref must be valid");
555 let rel_path = PathBuf::from("main.rs");
556 let file = IndexedSourceFile {
557 source_root: 0,
558 source_id: identity.source_id(0, &rel_path),
559 source_uri: identity.source_uri(&rel_path),
560 identity: identity.clone(),
561 path: rel_path.clone(),
562 rel_path: rel_path.clone(),
563 anchor: rel_path,
564 lang: Lang::Rs,
565 graph,
566 source: String::new(),
567 extraction_cache: "provided",
568 extraction_duration: Duration::ZERO,
569 };
570 let material = CodeIndexMaterial {
571 source_catalog: SourceCatalogMaterial {
572 sources: SourceFileSet {
573 roots: Vec::new(),
574 files: Vec::new(),
575 multi: false,
576 },
577 identity: identity.clone(),
578 memory_sources: BTreeMap::new(),
579 memory_slots: BTreeSet::new(),
580 memory_revisions: BTreeMap::new(),
581 },
582 files: vec![Arc::new(file)],
583 identity,
584 symbols_by_moniker: FxHashMap::default(),
585 };
586 (material, root, target)
587 }
588}