Skip to main content

code_moniker_workspace/snapshot/
model.rs

1use std::sync::Arc;
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::time::Duration;
4
5use super::records::RecordTable;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
8pub struct ResourceGeneration(u64);
9
10impl ResourceGeneration {
11	pub fn new(value: u64) -> Self {
12		Self(value)
13	}
14
15	pub fn value(self) -> u64 {
16		self.0
17	}
18}
19
20#[derive(Clone, Debug)]
21pub struct WorkspaceRequest {
22	pub label: String,
23	pub catalog: CatalogRequest,
24	cancellation: WorkspaceCancellation,
25}
26
27impl PartialEq for WorkspaceRequest {
28	fn eq(&self, other: &Self) -> bool {
29		self.label == other.label && self.catalog == other.catalog
30	}
31}
32
33impl Eq for WorkspaceRequest {}
34
35#[derive(Clone, Debug, Default)]
36pub struct WorkspaceCancellation(Arc<AtomicBool>);
37
38impl WorkspaceCancellation {
39	pub fn cancel(&self) {
40		self.0.store(true, Ordering::Release);
41	}
42
43	pub fn is_cancelled(&self) -> bool {
44		self.0.load(Ordering::Acquire)
45	}
46
47	pub fn check(&self, resource: WorkspaceResource) -> WorkspaceResult<()> {
48		if self.is_cancelled() {
49			return Err(WorkspaceFailure::new(resource, "workspace build cancelled"));
50		}
51		Ok(())
52	}
53}
54
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum CatalogRequest {
57	Refresh,
58	ReuseCurrent,
59}
60
61impl WorkspaceRequest {
62	pub fn new(label: impl Into<String>) -> Self {
63		Self {
64			label: label.into(),
65			catalog: CatalogRequest::Refresh,
66			cancellation: WorkspaceCancellation::default(),
67		}
68	}
69
70	pub fn with_cancellation(mut self, cancellation: WorkspaceCancellation) -> Self {
71		self.cancellation = cancellation;
72		self
73	}
74
75	pub fn cancellation(&self) -> &WorkspaceCancellation {
76		&self.cancellation
77	}
78
79	pub fn reuse_current_catalog(mut self) -> Self {
80		self.catalog = CatalogRequest::ReuseCurrent;
81		self
82	}
83
84	pub fn should_reuse_current_catalog(&self) -> bool {
85		self.catalog == CatalogRequest::ReuseCurrent
86	}
87}
88
89#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
90pub struct SourceId {
91	file: u32,
92}
93
94impl SourceId {
95	pub fn at(file: usize) -> Self {
96		Self { file: file as u32 }
97	}
98
99	pub fn parse(value: &str) -> Option<Self> {
100		let rest = value.strip_prefix("source:")?;
101		let file = rest.split(':').next()?;
102		Some(Self {
103			file: file.parse().ok()?,
104		})
105	}
106
107	pub fn file(self) -> usize {
108		self.file as usize
109	}
110}
111
112impl std::fmt::Display for SourceId {
113	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114		write!(formatter, "source:{}", self.file)
115	}
116}
117
118#[derive(Clone, Debug, Eq, PartialEq)]
119pub struct SourceUnit {
120	pub id: SourceId,
121	pub display_name: String,
122	pub language: Option<String>,
123}
124
125impl SourceUnit {
126	pub fn new(id: SourceId, display_name: impl Into<String>) -> Self {
127		Self {
128			id,
129			display_name: display_name.into(),
130			language: None,
131		}
132	}
133
134	pub fn with_language(
135		id: SourceId,
136		display_name: impl Into<String>,
137		language: impl Into<String>,
138	) -> Self {
139		Self {
140			id,
141			display_name: display_name.into(),
142			language: Some(language.into()),
143		}
144	}
145}
146
147#[derive(Clone, Debug, Eq, PartialEq)]
148pub struct SourceCatalog {
149	pub generation: ResourceGeneration,
150	pub sources: Vec<SourceUnit>,
151}
152
153impl SourceCatalog {
154	pub fn new(generation: ResourceGeneration, mut sources: Vec<SourceUnit>) -> Self {
155		sources.shrink_to_fit();
156		Self {
157			generation,
158			sources,
159		}
160	}
161}
162
163#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
164pub struct SymbolId {
165	file: u32,
166	def: u32,
167}
168
169impl SymbolId {
170	pub fn at(file: usize, def: usize) -> Self {
171		Self {
172			file: file as u32,
173			def: def as u32,
174		}
175	}
176
177	pub fn parse(value: &str) -> Option<Self> {
178		let rest = value.strip_prefix("symbol:")?;
179		let (file, def) = rest.split_once(':')?;
180		Some(Self {
181			file: file.parse().ok()?,
182			def: def.parse().ok()?,
183		})
184	}
185
186	pub fn file(self) -> usize {
187		self.file as usize
188	}
189
190	pub fn def(self) -> usize {
191		self.def as usize
192	}
193}
194
195impl std::fmt::Display for SymbolId {
196	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197		write!(formatter, "symbol:{}:{}", self.file, self.def)
198	}
199}
200
201#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
202pub struct SymbolLocation {
203	pub file: usize,
204	pub symbol: usize,
205}
206
207#[derive(Clone, Debug, Eq, PartialEq)]
208pub struct SymbolRecord {
209	pub id: SymbolId,
210	pub source: SourceId,
211	pub identity: Arc<str>,
212	pub name: String,
213	pub kind: String,
214	pub visibility: String,
215	pub signature: String,
216	pub call_name: Option<String>,
217	pub call_arity: Option<usize>,
218	pub navigable: bool,
219	pub line_range: Option<(u32, u32)>,
220	pub parent: Option<SymbolId>,
221}
222
223impl SymbolRecord {
224	pub fn new(
225		id: SymbolId,
226		source: SourceId,
227		name: impl Into<String>,
228		kind: impl Into<String>,
229	) -> Self {
230		Self {
231			identity: Arc::from(id.to_string()),
232			id,
233			source,
234			name: name.into(),
235			kind: kind.into(),
236			visibility: String::new(),
237			signature: String::new(),
238			call_name: None,
239			call_arity: None,
240			navigable: true,
241			line_range: None,
242			parent: None,
243		}
244	}
245}
246
247#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
248pub struct ReferenceId {
249	file: u32,
250	reference: u32,
251}
252
253impl ReferenceId {
254	pub fn at(file: usize, reference: usize) -> Self {
255		Self {
256			file: file as u32,
257			reference: reference as u32,
258		}
259	}
260
261	pub fn parse(value: &str) -> Option<Self> {
262		let rest = value.strip_prefix("reference:")?;
263		let (file, reference) = rest.split_once(':')?;
264		Some(Self {
265			file: file.parse().ok()?,
266			reference: reference.parse().ok()?,
267		})
268	}
269
270	pub fn file(self) -> usize {
271		self.file as usize
272	}
273
274	pub fn reference(self) -> usize {
275		self.reference as usize
276	}
277}
278
279impl std::fmt::Display for ReferenceId {
280	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281		write!(formatter, "reference:{}:{}", self.file, self.reference)
282	}
283}
284
285#[derive(Clone, Debug, Eq, PartialEq)]
286pub struct ReferenceRecord {
287	pub id: ReferenceId,
288	pub source: SourceId,
289	pub source_symbol: SymbolId,
290	pub target_identity: Arc<str>,
291	pub kind: String,
292	pub call_name: Option<String>,
293	pub call_arity: Option<usize>,
294	pub confidence: Option<String>,
295	pub receiver: Option<String>,
296	pub alias: Option<String>,
297	pub line_range: Option<(u32, u32)>,
298}
299
300impl ReferenceRecord {
301	pub fn new(
302		id: ReferenceId,
303		source: SourceId,
304		source_symbol: SymbolId,
305		target_identity: impl Into<Arc<str>>,
306		kind: impl Into<String>,
307		line_range: Option<(u32, u32)>,
308	) -> Self {
309		Self {
310			id,
311			source,
312			source_symbol,
313			target_identity: target_identity.into(),
314			kind: kind.into(),
315			call_name: None,
316			call_arity: None,
317			confidence: None,
318			receiver: None,
319			alias: None,
320			line_range,
321		}
322	}
323
324	pub fn with_call_metadata(
325		mut self,
326		call_name: Option<String>,
327		call_arity: Option<usize>,
328	) -> Self {
329		self.call_name = call_name;
330		self.call_arity = call_arity;
331		self
332	}
333
334	pub fn with_metadata(
335		mut self,
336		confidence: Option<String>,
337		receiver: Option<String>,
338		alias: Option<String>,
339	) -> Self {
340		self.confidence = confidence;
341		self.receiver = receiver;
342		self.alias = alias;
343		self
344	}
345}
346
347#[derive(Clone, Debug, Eq, PartialEq)]
348pub struct SourceFileRecord {
349	pub id: SourceId,
350	pub uri: String,
351	pub source_root: usize,
352	pub path: String,
353	pub rel_path: String,
354	pub anchor: String,
355	pub language: String,
356	pub text: String,
357}
358
359#[derive(Clone, Debug, Eq, PartialEq)]
360pub struct CodeIndex {
361	pub generation: ResourceGeneration,
362	pub catalog_generation: ResourceGeneration,
363	pub identity_scheme: String,
364	pub sources: Vec<SourceFileRecord>,
365	pub symbols: RecordTable<SymbolRecord>,
366	pub references: RecordTable<ReferenceRecord>,
367	pub timings: CodeIndexTimings,
368}
369
370#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
371pub struct CodeIndexTimings {
372	pub extract_sources: Duration,
373	pub semantic_index: Duration,
374	pub total: Duration,
375}
376
377impl CodeIndex {
378	pub fn new(
379		generation: ResourceGeneration,
380		catalog_generation: ResourceGeneration,
381		symbols: Vec<SymbolRecord>,
382	) -> Self {
383		Self::with_references(generation, catalog_generation, symbols, Vec::new())
384	}
385
386	pub fn with_references(
387		generation: ResourceGeneration,
388		catalog_generation: ResourceGeneration,
389		symbols: Vec<SymbolRecord>,
390		references: Vec<ReferenceRecord>,
391	) -> Self {
392		Self {
393			generation,
394			catalog_generation,
395			identity_scheme: crate::DEFAULT_IDENTITY_SCHEME.to_string(),
396			sources: Vec::new(),
397			symbols: RecordTable::from_records(symbols),
398			references: RecordTable::from_records(references),
399			timings: CodeIndexTimings::default(),
400		}
401	}
402}
403
404#[derive(Clone, Debug, Eq, PartialEq)]
405pub struct LinkageEdge {
406	pub reference: ReferenceId,
407	pub target: SymbolId,
408	pub evidence: ResolutionEvidence,
409}
410
411impl LinkageEdge {
412	pub fn new(reference: ReferenceId, target: SymbolId) -> Self {
413		Self::with_evidence(reference, target, ResolutionEvidence::ExactBinding)
414	}
415
416	pub fn with_evidence(
417		reference: ReferenceId,
418		target: SymbolId,
419		evidence: ResolutionEvidence,
420	) -> Self {
421		Self {
422			reference,
423			target,
424			evidence,
425		}
426	}
427}
428
429#[derive(Clone, Copy, Debug, Eq, PartialEq)]
430pub enum ResolutionEvidence {
431	ExactBinding,
432	LocalBinding,
433	GlobalBinding,
434	TypeConstraint,
435	Mro,
436	Injected,
437	NameMatch,
438}
439
440impl ResolutionEvidence {
441	pub fn as_str(self) -> &'static str {
442		match self {
443			Self::ExactBinding => "exact_binding",
444			Self::LocalBinding => "local_binding",
445			Self::GlobalBinding => "global_binding",
446			Self::TypeConstraint => "type_constraint",
447			Self::Mro => "mro",
448			Self::Injected => "injected",
449			Self::NameMatch => "name_match",
450		}
451	}
452
453	pub fn rank(self) -> u8 {
454		match self {
455			Self::ExactBinding => 100,
456			Self::LocalBinding | Self::TypeConstraint => 90,
457			Self::Mro => 85,
458			Self::GlobalBinding | Self::Injected => 80,
459			Self::NameMatch => 10,
460		}
461	}
462}
463
464#[derive(Clone, Copy, Debug, Eq, PartialEq)]
465pub enum CandidateReason {
466	WeakNameMatch,
467	MultipleTargets,
468	AmbiguousLookup,
469}
470
471impl CandidateReason {
472	pub fn as_str(self) -> &'static str {
473		match self {
474			Self::WeakNameMatch => "weak_name_match",
475			Self::MultipleTargets => "multiple_targets",
476			Self::AmbiguousLookup => "ambiguous_lookup",
477		}
478	}
479}
480
481#[derive(Clone, Copy, Debug, Eq, PartialEq)]
482pub enum CandidateScope {
483	Local,
484	Global,
485	Builtin,
486	Injected,
487	Unknown,
488}
489
490impl CandidateScope {
491	pub fn as_str(self) -> &'static str {
492		match self {
493			Self::Local => "local",
494			Self::Global => "global",
495			Self::Builtin => "builtin",
496			Self::Injected => "injected",
497			Self::Unknown => "unknown",
498		}
499	}
500}
501
502#[derive(Clone, Debug, Eq, PartialEq)]
503pub struct CandidateReference {
504	pub reference: ReferenceId,
505	pub targets: Vec<SymbolId>,
506	pub reason: CandidateReason,
507	pub scope: CandidateScope,
508	pub evidence: ResolutionEvidence,
509}
510
511impl CandidateReference {
512	pub fn new(
513		reference: ReferenceId,
514		targets: Vec<SymbolId>,
515		reason: CandidateReason,
516		scope: CandidateScope,
517		evidence: ResolutionEvidence,
518	) -> Self {
519		Self {
520			reference,
521			targets,
522			reason,
523			scope,
524			evidence,
525		}
526	}
527}
528
529#[derive(Clone, Copy, Debug, Eq, PartialEq)]
530pub enum DynamicReason {
531	DynamicAttribute,
532	DescriptorOrFrameworkInjected,
533	DuckTypedCandidateSet,
534	MixinContract,
535	ExternalDependencyUnindexed,
536	RuntimeImport,
537	RuntimeMutation,
538	PreprocessorExpansion,
539	InsufficientLocalFacts,
540}
541
542impl DynamicReason {
543	pub fn as_str(self) -> &'static str {
544		match self {
545			Self::DynamicAttribute => "dynamic_attribute",
546			Self::DescriptorOrFrameworkInjected => "descriptor_or_framework_injected",
547			Self::DuckTypedCandidateSet => "duck_typed_candidate_set",
548			Self::MixinContract => "mixin_contract",
549			Self::ExternalDependencyUnindexed => "external_dependency_unindexed",
550			Self::RuntimeImport => "runtime_import",
551			Self::RuntimeMutation => "runtime_mutation",
552			Self::PreprocessorExpansion => "preprocessor_expansion",
553			Self::InsufficientLocalFacts => "insufficient_local_facts",
554		}
555	}
556}
557
558#[derive(Clone, Debug, Eq, PartialEq)]
559pub struct DynamicReference {
560	pub reference: ReferenceId,
561	pub target_identity: Arc<str>,
562	pub reason: DynamicReason,
563	pub candidates: Vec<SymbolId>,
564}
565
566impl DynamicReference {
567	pub fn new(
568		reference: ReferenceId,
569		target_identity: impl Into<Arc<str>>,
570		reason: DynamicReason,
571		candidates: Vec<SymbolId>,
572	) -> Self {
573		Self {
574			reference,
575			target_identity: target_identity.into(),
576			reason,
577			candidates,
578		}
579	}
580}
581
582#[derive(Clone, Copy, Debug, Eq, PartialEq)]
583pub enum ExternalReferenceOrigin {
584	Sdk,
585	Dependency,
586	Injected,
587	UnknownExternal,
588}
589
590impl ExternalReferenceOrigin {
591	pub fn label(self) -> &'static str {
592		match self {
593			Self::Sdk => "sdk",
594			Self::Dependency => "dependency",
595			Self::Injected => "injected",
596			Self::UnknownExternal => "unknown_external",
597		}
598	}
599}
600
601#[derive(Clone, Debug, Eq, PartialEq)]
602pub struct ExternalReference {
603	pub reference: ReferenceId,
604	pub target_identity: Arc<str>,
605	pub origin: ExternalReferenceOrigin,
606}
607
608impl ExternalReference {
609	pub fn new(
610		reference: ReferenceId,
611		target_identity: impl Into<Arc<str>>,
612		origin: ExternalReferenceOrigin,
613	) -> Self {
614		Self {
615			reference,
616			target_identity: target_identity.into(),
617			origin,
618		}
619	}
620}
621
622/// Why linkage could not bind a reference. The taxonomy is part of the
623/// resolution contract: consumers can split external-by-design from real
624/// resolution gaps instead of reading one opaque count.
625#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
626pub enum UnresolvedReason {
627	ManifestBlocked,
628	Visibility,
629	LanguageBoundary,
630	MissingQuery,
631	NoCandidate,
632	Ambiguous,
633	UnsupportedLanguageRule,
634	IncompleteExtractorMetadata,
635}
636
637impl UnresolvedReason {
638	pub fn as_str(&self) -> &'static str {
639		match self {
640			Self::ManifestBlocked => "manifest_blocked",
641			Self::Visibility => "visibility",
642			Self::LanguageBoundary => "language_boundary",
643			Self::MissingQuery => "missing_query",
644			Self::NoCandidate => "no_candidate",
645			Self::Ambiguous => "ambiguous",
646			Self::UnsupportedLanguageRule => "unsupported_language_rule",
647			Self::IncompleteExtractorMetadata => "incomplete_extractor_metadata",
648		}
649	}
650}
651
652#[derive(Clone, Debug, Eq, PartialEq)]
653pub struct UnresolvedReference {
654	pub reference: ReferenceId,
655	pub target_identity: Arc<str>,
656	pub reason: UnresolvedReason,
657}
658
659impl UnresolvedReference {
660	pub fn new(
661		reference: ReferenceId,
662		target_identity: impl Into<Arc<str>>,
663		reason: UnresolvedReason,
664	) -> Self {
665		Self {
666			reference,
667			target_identity: target_identity.into(),
668			reason,
669		}
670	}
671}
672
673#[derive(Clone, Debug, Eq, PartialEq)]
674pub struct LinkageSnapshot {
675	pub generation: ResourceGeneration,
676	pub index_generation: ResourceGeneration,
677	pub resolved_refs: usize,
678	pub candidate_refs: usize,
679	pub external_refs: usize,
680	pub dynamic_refs: usize,
681	pub blocked_refs: usize,
682	/// Compatibility counter for manifest-policy blocks.
683	pub manifest_blocked_refs: usize,
684	pub unresolved_refs: usize,
685	/// Compatibility counter for existing consumers. Candidate references are
686	/// now stored separately and never projected as graph edges.
687	pub ambiguous_refs: usize,
688	pub resolved: Vec<LinkageEdge>,
689	pub candidates: Vec<CandidateReference>,
690	pub external: Vec<ExternalReference>,
691	pub dynamic: Vec<DynamicReference>,
692	pub blocked: Vec<UnresolvedReference>,
693	/// Compatibility view containing only `ManifestBlocked` entries.
694	pub manifest_blocked: Vec<UnresolvedReference>,
695	pub unresolved: Vec<UnresolvedReference>,
696	pub read_index: LinkageReadIndexHandle,
697}
698
699#[derive(Debug)]
700pub struct LinkageReadIndex {
701	pub(crate) incoming: rustc_hash::FxHashMap<SymbolId, Vec<ReferenceId>>,
702	pub(crate) targets: rustc_hash::FxHashMap<ReferenceId, SymbolId>,
703}
704
705impl LinkageReadIndex {
706	pub fn from_edges(edges: &[LinkageEdge]) -> Self {
707		let mut incoming = rustc_hash::FxHashMap::<SymbolId, Vec<ReferenceId>>::default();
708		let mut targets = rustc_hash::FxHashMap::<ReferenceId, SymbolId>::default();
709		for edge in edges {
710			let LinkageEdge {
711				reference, target, ..
712			} = edge.clone();
713			targets.entry(reference).or_insert(target);
714			incoming.entry(target).or_default().push(reference);
715		}
716		Self { incoming, targets }
717	}
718
719	pub fn incoming(&self, symbol: &SymbolId) -> &[ReferenceId] {
720		self.incoming.get(symbol).map(Vec::as_slice).unwrap_or(&[])
721	}
722
723	pub fn resolved_target(&self, reference: &ReferenceId) -> Option<&SymbolId> {
724		self.targets.get(reference)
725	}
726}
727
728#[derive(Clone, Debug, Default)]
729pub struct LinkageReadIndexHandle(Option<Arc<LinkageReadIndex>>);
730
731impl LinkageReadIndexHandle {
732	pub fn from_edges(edges: &[LinkageEdge]) -> Self {
733		Self(Some(Arc::new(LinkageReadIndex::from_edges(edges))))
734	}
735
736	pub fn get(&self) -> Option<&LinkageReadIndex> {
737		self.0.as_deref()
738	}
739}
740
741impl PartialEq for LinkageReadIndexHandle {
742	fn eq(&self, _other: &Self) -> bool {
743		true
744	}
745}
746
747impl Eq for LinkageReadIndexHandle {}
748
749impl LinkageSnapshot {
750	pub fn new(
751		generation: ResourceGeneration,
752		index_generation: ResourceGeneration,
753		resolved_refs: usize,
754		unresolved_refs: usize,
755	) -> Self {
756		Self {
757			generation,
758			index_generation,
759			resolved_refs,
760			candidate_refs: 0,
761			external_refs: 0,
762			dynamic_refs: 0,
763			blocked_refs: 0,
764			manifest_blocked_refs: 0,
765			unresolved_refs,
766			ambiguous_refs: 0,
767			resolved: Vec::new(),
768			candidates: Vec::new(),
769			external: Vec::new(),
770			dynamic: Vec::new(),
771			blocked: Vec::new(),
772			manifest_blocked: Vec::new(),
773			unresolved: Vec::new(),
774			read_index: LinkageReadIndexHandle::default(),
775		}
776	}
777
778	pub fn with_refs(
779		generation: ResourceGeneration,
780		index_generation: ResourceGeneration,
781		mut resolved: Vec<LinkageEdge>,
782		mut unresolved: Vec<UnresolvedReference>,
783	) -> Self {
784		resolved.shrink_to_fit();
785		unresolved.shrink_to_fit();
786		let read_index = LinkageReadIndexHandle::from_edges(&resolved);
787		Self {
788			generation,
789			index_generation,
790			resolved_refs: resolved.len(),
791			candidate_refs: 0,
792			external_refs: 0,
793			dynamic_refs: 0,
794			blocked_refs: 0,
795			manifest_blocked_refs: 0,
796			unresolved_refs: unresolved.len(),
797			ambiguous_refs: 0,
798			resolved,
799			candidates: Vec::new(),
800			external: Vec::new(),
801			dynamic: Vec::new(),
802			blocked: Vec::new(),
803			manifest_blocked: Vec::new(),
804			unresolved,
805			read_index,
806		}
807	}
808}
809
810#[derive(Clone, Debug, Eq, PartialEq)]
811pub struct ChangeOverlay {
812	pub generation: ResourceGeneration,
813	pub catalog_generation: ResourceGeneration,
814	pub index_generation: ResourceGeneration,
815	pub scope: String,
816	pub resources: Vec<ChangeResource>,
817	pub diagnostics: Vec<String>,
818	pub changed_symbols: Vec<SymbolId>,
819	pub changes: Vec<ChangeRecord>,
820	pub semantic: Option<std::sync::Arc<crate::changes::semantic::review::SemanticReview>>,
821}
822
823pub struct ChangeOverlayReport {
824	pub generation: ResourceGeneration,
825	pub catalog_generation: ResourceGeneration,
826	pub index_generation: ResourceGeneration,
827	pub scope: String,
828	pub resources: Vec<ChangeResource>,
829	pub diagnostics: Vec<String>,
830	pub changes: Vec<ChangeRecord>,
831}
832
833impl ChangeOverlay {
834	pub fn new(
835		generation: ResourceGeneration,
836		catalog_generation: ResourceGeneration,
837		index_generation: ResourceGeneration,
838		mut changed_symbols: Vec<SymbolId>,
839	) -> Self {
840		changed_symbols.shrink_to_fit();
841		Self {
842			generation,
843			catalog_generation,
844			index_generation,
845			scope: "HEAD..worktree".to_string(),
846			resources: Vec::new(),
847			diagnostics: Vec::new(),
848			changed_symbols,
849			changes: Vec::new(),
850			semantic: None,
851		}
852	}
853
854	pub fn with_records(
855		generation: ResourceGeneration,
856		catalog_generation: ResourceGeneration,
857		index_generation: ResourceGeneration,
858		mut changes: Vec<ChangeRecord>,
859	) -> Self {
860		changes.shrink_to_fit();
861		let changed_symbols = changes.iter().filter_map(|change| change.symbol).fold(
862			Vec::new(),
863			|mut out, symbol| {
864				if !out.contains(&symbol) {
865					out.push(symbol);
866				}
867				out
868			},
869		);
870		let mut changed_symbols = changed_symbols;
871		changed_symbols.shrink_to_fit();
872		Self {
873			generation,
874			catalog_generation,
875			index_generation,
876			scope: "HEAD..worktree".to_string(),
877			resources: Vec::new(),
878			diagnostics: Vec::new(),
879			changed_symbols,
880			changes,
881			semantic: None,
882		}
883	}
884
885	pub fn from_report(report: ChangeOverlayReport) -> Self {
886		let mut resources = report.resources;
887		let mut diagnostics = report.diagnostics;
888		resources.shrink_to_fit();
889		diagnostics.shrink_to_fit();
890		let mut overlay = Self::with_records(
891			report.generation,
892			report.catalog_generation,
893			report.index_generation,
894			report.changes,
895		);
896		overlay.scope = report.scope;
897		overlay.resources = resources;
898		overlay.diagnostics = diagnostics;
899		overlay
900	}
901}
902
903#[derive(Clone, Debug, Eq, PartialEq)]
904pub struct ChangeResource {
905	pub available: bool,
906	pub label: String,
907	pub message: String,
908}
909
910#[derive(Clone, Copy, Debug, Eq, PartialEq)]
911pub enum ChangeStatus {
912	Added,
913	Modified,
914	Removed,
915}
916
917impl ChangeStatus {
918	pub fn label(self) -> &'static str {
919		match self {
920			Self::Added => "added",
921			Self::Modified => "modified",
922			Self::Removed => "removed",
923		}
924	}
925}
926
927#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
928pub struct ChangeId(String);
929
930impl ChangeId {
931	pub fn new(value: impl Into<String>) -> Self {
932		Self(value.into())
933	}
934
935	pub fn as_str(&self) -> &str {
936		&self.0
937	}
938}
939
940#[derive(Clone, Debug, Eq, PartialEq)]
941pub struct ChangeRecord {
942	pub id: ChangeId,
943	pub status: ChangeStatus,
944	pub source: Option<SourceId>,
945	pub source_uri: Option<String>,
946	pub symbol: Option<SymbolId>,
947	pub identity: String,
948	pub language: String,
949	pub file_path: String,
950	pub name: String,
951	pub kind: String,
952	pub line_range: Option<(u32, u32)>,
953	pub hunk_count: usize,
954}
955
956#[derive(Clone, Debug, Eq, PartialEq)]
957pub struct ChangeRecordCoreFields {
958	pub id: ChangeId,
959	pub status: ChangeStatus,
960	pub identity: String,
961	pub language: String,
962	pub file_path: String,
963	pub name: String,
964	pub kind: String,
965	pub line_range: Option<(u32, u32)>,
966	pub hunk_count: usize,
967}
968
969impl ChangeRecord {
970	pub fn new(fields: ChangeRecordCoreFields) -> Self {
971		Self {
972			id: fields.id,
973			status: fields.status,
974			source: None,
975			source_uri: None,
976			symbol: None,
977			identity: fields.identity,
978			language: fields.language,
979			file_path: fields.file_path,
980			name: fields.name,
981			kind: fields.kind,
982			line_range: fields.line_range,
983			hunk_count: fields.hunk_count,
984		}
985	}
986
987	pub fn with_source(mut self, source: SourceId, source_uri: impl Into<String>) -> Self {
988		self.source = Some(source);
989		self.source_uri = Some(source_uri.into());
990		self
991	}
992
993	pub fn with_symbol(mut self, symbol: SymbolId) -> Self {
994		self.symbol = Some(symbol);
995		self
996	}
997}
998
999#[derive(Clone, Debug, Eq, PartialEq)]
1000pub struct WorkspaceSnapshot {
1001	pub generation: ResourceGeneration,
1002	pub catalog: SourceCatalog,
1003	pub index: CodeIndex,
1004	pub linkage: LinkageSnapshot,
1005	pub changes: ChangeOverlay,
1006	pub timings: WorkspaceTimings,
1007}
1008
1009#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1010pub struct WorkspaceTimings {
1011	pub source_catalog: Duration,
1012	pub extract_sources: Duration,
1013	pub semantic_index: Duration,
1014	pub code_index: Duration,
1015	pub linkage: Duration,
1016	pub change_overlay: Duration,
1017	pub total: Duration,
1018}
1019
1020#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1021pub enum WorkspaceResource {
1022	SourceCatalog,
1023	CodeIndex,
1024	LinkageSnapshot,
1025	ChangeOverlay,
1026}
1027
1028#[derive(Clone, Debug, Eq, PartialEq)]
1029pub struct WorkspaceFailure {
1030	pub resource: WorkspaceResource,
1031	pub message: String,
1032}
1033
1034impl WorkspaceFailure {
1035	pub fn new(resource: WorkspaceResource, message: impl Into<String>) -> Self {
1036		Self {
1037			resource,
1038			message: message.into(),
1039		}
1040	}
1041}
1042
1043pub type WorkspaceResult<T> = Result<T, WorkspaceFailure>;
1044
1045#[derive(Clone, Debug, Eq, PartialEq)]
1046pub enum WorkspaceTransition {
1047	Ready {
1048		generation: ResourceGeneration,
1049	},
1050	Failed {
1051		failure: WorkspaceFailure,
1052		preserved_generation: Option<ResourceGeneration>,
1053	},
1054}