Skip to main content

code_moniker_workspace/snapshot/
model.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use super::records::RecordTable;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
7pub struct ResourceGeneration(u64);
8
9impl ResourceGeneration {
10	pub fn new(value: u64) -> Self {
11		Self(value)
12	}
13
14	pub fn value(self) -> u64 {
15		self.0
16	}
17}
18
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct WorkspaceRequest {
21	pub label: String,
22	pub catalog: CatalogRequest,
23}
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum CatalogRequest {
27	Refresh,
28	ReuseCurrent,
29}
30
31impl WorkspaceRequest {
32	pub fn new(label: impl Into<String>) -> Self {
33		Self {
34			label: label.into(),
35			catalog: CatalogRequest::Refresh,
36		}
37	}
38
39	pub fn reuse_current_catalog(mut self) -> Self {
40		self.catalog = CatalogRequest::ReuseCurrent;
41		self
42	}
43
44	pub fn should_reuse_current_catalog(&self) -> bool {
45		self.catalog == CatalogRequest::ReuseCurrent
46	}
47}
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
50pub struct SourceId {
51	file: u32,
52}
53
54impl SourceId {
55	pub fn at(file: usize) -> Self {
56		Self { file: file as u32 }
57	}
58
59	pub fn parse(value: &str) -> Option<Self> {
60		let rest = value.strip_prefix("source:")?;
61		let file = rest.split(':').next()?;
62		Some(Self {
63			file: file.parse().ok()?,
64		})
65	}
66
67	pub fn file(self) -> usize {
68		self.file as usize
69	}
70}
71
72impl std::fmt::Display for SourceId {
73	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74		write!(formatter, "source:{}", self.file)
75	}
76}
77
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct SourceUnit {
80	pub id: SourceId,
81	pub display_name: String,
82	pub language: Option<String>,
83}
84
85impl SourceUnit {
86	pub fn new(id: SourceId, display_name: impl Into<String>) -> Self {
87		Self {
88			id,
89			display_name: display_name.into(),
90			language: None,
91		}
92	}
93
94	pub fn with_language(
95		id: SourceId,
96		display_name: impl Into<String>,
97		language: impl Into<String>,
98	) -> Self {
99		Self {
100			id,
101			display_name: display_name.into(),
102			language: Some(language.into()),
103		}
104	}
105}
106
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct SourceCatalog {
109	pub generation: ResourceGeneration,
110	pub sources: Vec<SourceUnit>,
111}
112
113impl SourceCatalog {
114	pub fn new(generation: ResourceGeneration, mut sources: Vec<SourceUnit>) -> Self {
115		sources.shrink_to_fit();
116		Self {
117			generation,
118			sources,
119		}
120	}
121}
122
123#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
124pub struct SymbolId {
125	file: u32,
126	def: u32,
127}
128
129impl SymbolId {
130	pub fn at(file: usize, def: usize) -> Self {
131		Self {
132			file: file as u32,
133			def: def as u32,
134		}
135	}
136
137	pub fn parse(value: &str) -> Option<Self> {
138		let rest = value.strip_prefix("symbol:")?;
139		let (file, def) = rest.split_once(':')?;
140		Some(Self {
141			file: file.parse().ok()?,
142			def: def.parse().ok()?,
143		})
144	}
145
146	pub fn file(self) -> usize {
147		self.file as usize
148	}
149
150	pub fn def(self) -> usize {
151		self.def as usize
152	}
153}
154
155impl std::fmt::Display for SymbolId {
156	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157		write!(formatter, "symbol:{}:{}", self.file, self.def)
158	}
159}
160
161#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
162pub struct SymbolLocation {
163	pub file: usize,
164	pub symbol: usize,
165}
166
167#[derive(Clone, Debug, Eq, PartialEq)]
168pub struct SymbolRecord {
169	pub id: SymbolId,
170	pub source: SourceId,
171	pub identity: Arc<str>,
172	pub name: String,
173	pub kind: String,
174	pub visibility: String,
175	pub signature: String,
176	pub navigable: bool,
177	pub line_range: Option<(u32, u32)>,
178	pub parent: Option<SymbolId>,
179}
180
181impl SymbolRecord {
182	pub fn new(
183		id: SymbolId,
184		source: SourceId,
185		name: impl Into<String>,
186		kind: impl Into<String>,
187	) -> Self {
188		Self {
189			identity: Arc::from(id.to_string()),
190			id,
191			source,
192			name: name.into(),
193			kind: kind.into(),
194			visibility: String::new(),
195			signature: String::new(),
196			navigable: true,
197			line_range: None,
198			parent: None,
199		}
200	}
201}
202
203#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
204pub struct ReferenceId {
205	file: u32,
206	reference: u32,
207}
208
209impl ReferenceId {
210	pub fn at(file: usize, reference: usize) -> Self {
211		Self {
212			file: file as u32,
213			reference: reference as u32,
214		}
215	}
216
217	pub fn parse(value: &str) -> Option<Self> {
218		let rest = value.strip_prefix("reference:")?;
219		let (file, reference) = rest.split_once(':')?;
220		Some(Self {
221			file: file.parse().ok()?,
222			reference: reference.parse().ok()?,
223		})
224	}
225
226	pub fn file(self) -> usize {
227		self.file as usize
228	}
229
230	pub fn reference(self) -> usize {
231		self.reference as usize
232	}
233}
234
235impl std::fmt::Display for ReferenceId {
236	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237		write!(formatter, "reference:{}:{}", self.file, self.reference)
238	}
239}
240
241#[derive(Clone, Debug, Eq, PartialEq)]
242pub struct ReferenceRecord {
243	pub id: ReferenceId,
244	pub source: SourceId,
245	pub source_symbol: SymbolId,
246	pub target_identity: Arc<str>,
247	pub kind: String,
248	pub call_name: Option<String>,
249	pub call_arity: Option<usize>,
250	pub confidence: Option<String>,
251	pub receiver: Option<String>,
252	pub alias: Option<String>,
253	pub line_range: Option<(u32, u32)>,
254}
255
256impl ReferenceRecord {
257	pub fn new(
258		id: ReferenceId,
259		source: SourceId,
260		source_symbol: SymbolId,
261		target_identity: impl Into<Arc<str>>,
262		kind: impl Into<String>,
263		line_range: Option<(u32, u32)>,
264	) -> Self {
265		Self {
266			id,
267			source,
268			source_symbol,
269			target_identity: target_identity.into(),
270			kind: kind.into(),
271			call_name: None,
272			call_arity: None,
273			confidence: None,
274			receiver: None,
275			alias: None,
276			line_range,
277		}
278	}
279
280	pub fn with_call_metadata(
281		mut self,
282		call_name: Option<String>,
283		call_arity: Option<usize>,
284	) -> Self {
285		self.call_name = call_name;
286		self.call_arity = call_arity;
287		self
288	}
289
290	pub fn with_metadata(
291		mut self,
292		confidence: Option<String>,
293		receiver: Option<String>,
294		alias: Option<String>,
295	) -> Self {
296		self.confidence = confidence;
297		self.receiver = receiver;
298		self.alias = alias;
299		self
300	}
301}
302
303#[derive(Clone, Debug, Eq, PartialEq)]
304pub struct SourceFileRecord {
305	pub id: SourceId,
306	pub uri: String,
307	pub source_root: usize,
308	pub path: String,
309	pub rel_path: String,
310	pub anchor: String,
311	pub language: String,
312	pub text: String,
313}
314
315#[derive(Clone, Debug, Eq, PartialEq)]
316pub struct CodeIndex {
317	pub generation: ResourceGeneration,
318	pub catalog_generation: ResourceGeneration,
319	pub identity_scheme: String,
320	pub sources: Vec<SourceFileRecord>,
321	pub symbols: RecordTable<SymbolRecord>,
322	pub references: RecordTable<ReferenceRecord>,
323	pub timings: CodeIndexTimings,
324}
325
326#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
327pub struct CodeIndexTimings {
328	pub extract_sources: Duration,
329	pub semantic_index: Duration,
330	pub total: Duration,
331}
332
333impl CodeIndex {
334	pub fn new(
335		generation: ResourceGeneration,
336		catalog_generation: ResourceGeneration,
337		symbols: Vec<SymbolRecord>,
338	) -> Self {
339		Self::with_references(generation, catalog_generation, symbols, Vec::new())
340	}
341
342	pub fn with_references(
343		generation: ResourceGeneration,
344		catalog_generation: ResourceGeneration,
345		symbols: Vec<SymbolRecord>,
346		references: Vec<ReferenceRecord>,
347	) -> Self {
348		Self {
349			generation,
350			catalog_generation,
351			identity_scheme: crate::DEFAULT_IDENTITY_SCHEME.to_string(),
352			sources: Vec::new(),
353			symbols: RecordTable::from_records(symbols),
354			references: RecordTable::from_records(references),
355			timings: CodeIndexTimings::default(),
356		}
357	}
358}
359
360#[derive(Clone, Debug, Eq, PartialEq)]
361pub struct LinkageEdge {
362	pub reference: ReferenceId,
363	pub target: SymbolId,
364}
365
366impl LinkageEdge {
367	pub fn new(reference: ReferenceId, target: SymbolId) -> Self {
368		Self { reference, target }
369	}
370}
371
372#[derive(Clone, Copy, Debug, Eq, PartialEq)]
373pub enum ExternalReferenceOrigin {
374	Dependency,
375	Injected,
376	UnknownExternal,
377}
378
379impl ExternalReferenceOrigin {
380	pub fn label(self) -> &'static str {
381		match self {
382			Self::Dependency => "dependency",
383			Self::Injected => "injected",
384			Self::UnknownExternal => "unknown_external",
385		}
386	}
387}
388
389#[derive(Clone, Debug, Eq, PartialEq)]
390pub struct ExternalReference {
391	pub reference: ReferenceId,
392	pub target_identity: Arc<str>,
393	pub origin: ExternalReferenceOrigin,
394}
395
396impl ExternalReference {
397	pub fn new(
398		reference: ReferenceId,
399		target_identity: impl Into<Arc<str>>,
400		origin: ExternalReferenceOrigin,
401	) -> Self {
402		Self {
403			reference,
404			target_identity: target_identity.into(),
405			origin,
406		}
407	}
408}
409
410/// Why linkage could not bind a reference. The taxonomy is part of the
411/// resolution contract: consumers can split external-by-design from real
412/// resolution gaps instead of reading one opaque count.
413#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
414pub enum UnresolvedReason {
415	ManifestBlocked,
416	Visibility,
417	LanguageBoundary,
418	MissingQuery,
419	NoCandidate,
420	Ambiguous,
421	UnsupportedLanguageRule,
422	IncompleteExtractorMetadata,
423}
424
425impl UnresolvedReason {
426	pub fn as_str(&self) -> &'static str {
427		match self {
428			Self::ManifestBlocked => "manifest_blocked",
429			Self::Visibility => "visibility",
430			Self::LanguageBoundary => "language_boundary",
431			Self::MissingQuery => "missing_query",
432			Self::NoCandidate => "no_candidate",
433			Self::Ambiguous => "ambiguous",
434			Self::UnsupportedLanguageRule => "unsupported_language_rule",
435			Self::IncompleteExtractorMetadata => "incomplete_extractor_metadata",
436		}
437	}
438}
439
440#[derive(Clone, Debug, Eq, PartialEq)]
441pub struct UnresolvedReference {
442	pub reference: ReferenceId,
443	pub target_identity: Arc<str>,
444	pub reason: UnresolvedReason,
445}
446
447impl UnresolvedReference {
448	pub fn new(
449		reference: ReferenceId,
450		target_identity: impl Into<Arc<str>>,
451		reason: UnresolvedReason,
452	) -> Self {
453		Self {
454			reference,
455			target_identity: target_identity.into(),
456			reason,
457		}
458	}
459}
460
461#[derive(Clone, Debug, Eq, PartialEq)]
462pub struct LinkageSnapshot {
463	pub generation: ResourceGeneration,
464	pub index_generation: ResourceGeneration,
465	pub resolved_refs: usize,
466	pub external_refs: usize,
467	pub manifest_blocked_refs: usize,
468	pub unresolved_refs: usize,
469	pub ambiguous_refs: usize,
470	pub resolved: Vec<LinkageEdge>,
471	pub external: Vec<ExternalReference>,
472	pub manifest_blocked: Vec<UnresolvedReference>,
473	pub unresolved: Vec<UnresolvedReference>,
474	pub read_index: LinkageReadIndexHandle,
475}
476
477#[derive(Debug)]
478pub struct LinkageReadIndex {
479	pub(crate) incoming: rustc_hash::FxHashMap<SymbolId, Vec<ReferenceId>>,
480	pub(crate) targets: rustc_hash::FxHashMap<ReferenceId, SymbolId>,
481}
482
483impl LinkageReadIndex {
484	pub fn from_edges(edges: &[LinkageEdge]) -> Self {
485		let mut incoming = rustc_hash::FxHashMap::<SymbolId, Vec<ReferenceId>>::default();
486		let mut targets = rustc_hash::FxHashMap::<ReferenceId, SymbolId>::default();
487		for edge in edges {
488			let LinkageEdge { reference, target } = edge.clone();
489			targets.entry(reference).or_insert(target);
490			incoming.entry(target).or_default().push(reference);
491		}
492		Self { incoming, targets }
493	}
494
495	pub fn incoming(&self, symbol: &SymbolId) -> &[ReferenceId] {
496		self.incoming.get(symbol).map(Vec::as_slice).unwrap_or(&[])
497	}
498
499	pub fn resolved_target(&self, reference: &ReferenceId) -> Option<&SymbolId> {
500		self.targets.get(reference)
501	}
502}
503
504#[derive(Clone, Debug, Default)]
505pub struct LinkageReadIndexHandle(Option<Arc<LinkageReadIndex>>);
506
507impl LinkageReadIndexHandle {
508	pub fn from_edges(edges: &[LinkageEdge]) -> Self {
509		Self(Some(Arc::new(LinkageReadIndex::from_edges(edges))))
510	}
511
512	pub fn get(&self) -> Option<&LinkageReadIndex> {
513		self.0.as_deref()
514	}
515}
516
517impl PartialEq for LinkageReadIndexHandle {
518	fn eq(&self, _other: &Self) -> bool {
519		true
520	}
521}
522
523impl Eq for LinkageReadIndexHandle {}
524
525impl LinkageSnapshot {
526	pub fn new(
527		generation: ResourceGeneration,
528		index_generation: ResourceGeneration,
529		resolved_refs: usize,
530		unresolved_refs: usize,
531	) -> Self {
532		Self {
533			generation,
534			index_generation,
535			resolved_refs,
536			external_refs: 0,
537			manifest_blocked_refs: 0,
538			unresolved_refs,
539			ambiguous_refs: 0,
540			resolved: Vec::new(),
541			external: Vec::new(),
542			manifest_blocked: Vec::new(),
543			unresolved: Vec::new(),
544			read_index: LinkageReadIndexHandle::default(),
545		}
546	}
547
548	pub fn with_refs(
549		generation: ResourceGeneration,
550		index_generation: ResourceGeneration,
551		mut resolved: Vec<LinkageEdge>,
552		mut unresolved: Vec<UnresolvedReference>,
553	) -> Self {
554		resolved.shrink_to_fit();
555		unresolved.shrink_to_fit();
556		let read_index = LinkageReadIndexHandle::from_edges(&resolved);
557		Self {
558			generation,
559			index_generation,
560			resolved_refs: resolved.len(),
561			external_refs: 0,
562			manifest_blocked_refs: 0,
563			unresolved_refs: unresolved.len(),
564			ambiguous_refs: 0,
565			resolved,
566			external: Vec::new(),
567			manifest_blocked: Vec::new(),
568			unresolved,
569			read_index,
570		}
571	}
572}
573
574#[derive(Clone, Debug, Eq, PartialEq)]
575pub struct ChangeOverlay {
576	pub generation: ResourceGeneration,
577	pub catalog_generation: ResourceGeneration,
578	pub index_generation: ResourceGeneration,
579	pub scope: String,
580	pub resources: Vec<ChangeResource>,
581	pub diagnostics: Vec<String>,
582	pub changed_symbols: Vec<SymbolId>,
583	pub changes: Vec<ChangeRecord>,
584	pub semantic: Option<std::sync::Arc<crate::changes::semantic::review::SemanticReview>>,
585}
586
587pub struct ChangeOverlayReport {
588	pub generation: ResourceGeneration,
589	pub catalog_generation: ResourceGeneration,
590	pub index_generation: ResourceGeneration,
591	pub scope: String,
592	pub resources: Vec<ChangeResource>,
593	pub diagnostics: Vec<String>,
594	pub changes: Vec<ChangeRecord>,
595}
596
597impl ChangeOverlay {
598	pub fn new(
599		generation: ResourceGeneration,
600		catalog_generation: ResourceGeneration,
601		index_generation: ResourceGeneration,
602		mut changed_symbols: Vec<SymbolId>,
603	) -> Self {
604		changed_symbols.shrink_to_fit();
605		Self {
606			generation,
607			catalog_generation,
608			index_generation,
609			scope: "HEAD..worktree".to_string(),
610			resources: Vec::new(),
611			diagnostics: Vec::new(),
612			changed_symbols,
613			changes: Vec::new(),
614			semantic: None,
615		}
616	}
617
618	pub fn with_records(
619		generation: ResourceGeneration,
620		catalog_generation: ResourceGeneration,
621		index_generation: ResourceGeneration,
622		mut changes: Vec<ChangeRecord>,
623	) -> Self {
624		changes.shrink_to_fit();
625		let changed_symbols = changes.iter().filter_map(|change| change.symbol).fold(
626			Vec::new(),
627			|mut out, symbol| {
628				if !out.contains(&symbol) {
629					out.push(symbol);
630				}
631				out
632			},
633		);
634		let mut changed_symbols = changed_symbols;
635		changed_symbols.shrink_to_fit();
636		Self {
637			generation,
638			catalog_generation,
639			index_generation,
640			scope: "HEAD..worktree".to_string(),
641			resources: Vec::new(),
642			diagnostics: Vec::new(),
643			changed_symbols,
644			changes,
645			semantic: None,
646		}
647	}
648
649	pub fn from_report(report: ChangeOverlayReport) -> Self {
650		let mut resources = report.resources;
651		let mut diagnostics = report.diagnostics;
652		resources.shrink_to_fit();
653		diagnostics.shrink_to_fit();
654		let mut overlay = Self::with_records(
655			report.generation,
656			report.catalog_generation,
657			report.index_generation,
658			report.changes,
659		);
660		overlay.scope = report.scope;
661		overlay.resources = resources;
662		overlay.diagnostics = diagnostics;
663		overlay
664	}
665}
666
667#[derive(Clone, Debug, Eq, PartialEq)]
668pub struct ChangeResource {
669	pub available: bool,
670	pub label: String,
671	pub message: String,
672}
673
674#[derive(Clone, Copy, Debug, Eq, PartialEq)]
675pub enum ChangeStatus {
676	Added,
677	Modified,
678	Removed,
679}
680
681impl ChangeStatus {
682	pub fn label(self) -> &'static str {
683		match self {
684			Self::Added => "added",
685			Self::Modified => "modified",
686			Self::Removed => "removed",
687		}
688	}
689}
690
691#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
692pub struct ChangeId(String);
693
694impl ChangeId {
695	pub fn new(value: impl Into<String>) -> Self {
696		Self(value.into())
697	}
698
699	pub fn as_str(&self) -> &str {
700		&self.0
701	}
702}
703
704#[derive(Clone, Debug, Eq, PartialEq)]
705pub struct ChangeRecord {
706	pub id: ChangeId,
707	pub status: ChangeStatus,
708	pub source: Option<SourceId>,
709	pub source_uri: Option<String>,
710	pub symbol: Option<SymbolId>,
711	pub identity: String,
712	pub language: String,
713	pub file_path: String,
714	pub name: String,
715	pub kind: String,
716	pub line_range: Option<(u32, u32)>,
717	pub hunk_count: usize,
718}
719
720#[derive(Clone, Debug, Eq, PartialEq)]
721pub struct ChangeRecordCoreFields {
722	pub id: ChangeId,
723	pub status: ChangeStatus,
724	pub identity: String,
725	pub language: String,
726	pub file_path: String,
727	pub name: String,
728	pub kind: String,
729	pub line_range: Option<(u32, u32)>,
730	pub hunk_count: usize,
731}
732
733impl ChangeRecord {
734	pub fn new(fields: ChangeRecordCoreFields) -> Self {
735		Self {
736			id: fields.id,
737			status: fields.status,
738			source: None,
739			source_uri: None,
740			symbol: None,
741			identity: fields.identity,
742			language: fields.language,
743			file_path: fields.file_path,
744			name: fields.name,
745			kind: fields.kind,
746			line_range: fields.line_range,
747			hunk_count: fields.hunk_count,
748		}
749	}
750
751	pub fn with_source(mut self, source: SourceId, source_uri: impl Into<String>) -> Self {
752		self.source = Some(source);
753		self.source_uri = Some(source_uri.into());
754		self
755	}
756
757	pub fn with_symbol(mut self, symbol: SymbolId) -> Self {
758		self.symbol = Some(symbol);
759		self
760	}
761}
762
763#[derive(Clone, Debug, Eq, PartialEq)]
764pub struct WorkspaceSnapshot {
765	pub generation: ResourceGeneration,
766	pub catalog: SourceCatalog,
767	pub index: CodeIndex,
768	pub linkage: LinkageSnapshot,
769	pub changes: ChangeOverlay,
770	pub timings: WorkspaceTimings,
771}
772
773#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
774pub struct WorkspaceTimings {
775	pub source_catalog: Duration,
776	pub extract_sources: Duration,
777	pub semantic_index: Duration,
778	pub code_index: Duration,
779	pub linkage: Duration,
780	pub change_overlay: Duration,
781	pub total: Duration,
782}
783
784#[derive(Clone, Copy, Debug, Eq, PartialEq)]
785pub enum WorkspaceResource {
786	SourceCatalog,
787	CodeIndex,
788	LinkageSnapshot,
789	ChangeOverlay,
790}
791
792#[derive(Clone, Debug, Eq, PartialEq)]
793pub struct WorkspaceFailure {
794	pub resource: WorkspaceResource,
795	pub message: String,
796}
797
798impl WorkspaceFailure {
799	pub fn new(resource: WorkspaceResource, message: impl Into<String>) -> Self {
800		Self {
801			resource,
802			message: message.into(),
803		}
804	}
805}
806
807pub type WorkspaceResult<T> = Result<T, WorkspaceFailure>;
808
809#[derive(Clone, Debug, Eq, PartialEq)]
810pub enum WorkspaceTransition {
811	Ready {
812		generation: ResourceGeneration,
813	},
814	Failed {
815		failure: WorkspaceFailure,
816		preserved_generation: Option<ResourceGeneration>,
817	},
818}