Skip to main content

code_moniker_workspace/registry/
runtime.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use crate::live::{WorkspaceLiveRefreshPlan, WorkspaceWatchRoot};
5use crate::snapshot::{
6	ResourceGeneration, WorkspaceFailure, WorkspaceRequest, WorkspaceResource, WorkspaceResult,
7	WorkspaceSnapshot, WorkspaceTransition, WorkspaceView,
8};
9
10use super::build::{
11	LivePlanBuild, RefreshPorts, build_catalog_snapshot, build_change_overlay_snapshot,
12	build_complete_snapshot, build_incremental_paths_snapshot, build_index_only_snapshot,
13	build_linkage_snapshot,
14};
15use super::command::{
16	WorkspaceCommand, WorkspaceCommandId, WorkspaceCommandKind, WorkspaceCommandSpec,
17	WorkspaceScopeUri, WorkspaceSnapshotPublication,
18};
19use super::event::{
20	WorkspaceEvent, WorkspaceEventContext, WorkspaceEventCursor, WorkspaceEventKind,
21	WorkspaceEventLog,
22};
23use super::ports::{WorkspaceCommandPort, WorkspaceEventPort, WorkspacePorts, WorkspaceQueryPort};
24use super::staleness::WorkspaceStaleness;
25use super::state::WorkspaceState;
26
27pub struct WorkspaceLivePlanTransition {
28	transition: WorkspaceTransition,
29	replace_watcher: bool,
30}
31
32impl WorkspaceLivePlanTransition {
33	pub fn transition(self) -> WorkspaceTransition {
34		self.transition
35	}
36
37	pub fn replace_watcher(&self) -> bool {
38		self.replace_watcher
39	}
40}
41
42pub struct WorkspaceRegistry {
43	runtime: WorkspaceRuntime,
44	events: WorkspaceEventLog,
45	next_command_id: u64,
46}
47
48pub struct WorkspaceCommands<'a> {
49	runtime: &'a mut WorkspaceRuntime,
50	events: &'a mut WorkspaceEventLog,
51	next_command_id: &'a mut u64,
52}
53
54pub struct WorkspaceLiveCommands<'a> {
55	runtime: &'a mut WorkspaceRuntime,
56	events: &'a mut WorkspaceEventLog,
57	next_command_id: &'a mut u64,
58}
59
60pub struct WorkspaceQueries<'a> {
61	runtime: &'a WorkspaceRuntime,
62}
63
64pub struct WorkspaceEvents<'a> {
65	events: &'a WorkspaceEventLog,
66}
67
68pub struct WorkspaceRuntime {
69	ports: WorkspacePorts,
70	state: WorkspaceState,
71}
72
73impl WorkspaceRegistry {
74	pub fn new(ports: WorkspacePorts) -> Self {
75		Self {
76			runtime: WorkspaceRuntime::new(ports),
77			events: WorkspaceEventLog::default(),
78			next_command_id: 1,
79		}
80	}
81
82	pub fn commands(&mut self) -> WorkspaceCommands<'_> {
83		WorkspaceCommands {
84			runtime: &mut self.runtime,
85			events: &mut self.events,
86			next_command_id: &mut self.next_command_id,
87		}
88	}
89
90	pub fn live_commands(&mut self) -> WorkspaceLiveCommands<'_> {
91		WorkspaceLiveCommands {
92			runtime: &mut self.runtime,
93			events: &mut self.events,
94			next_command_id: &mut self.next_command_id,
95		}
96	}
97
98	pub fn queries(&self) -> WorkspaceQueries<'_> {
99		WorkspaceQueries {
100			runtime: &self.runtime,
101		}
102	}
103
104	pub fn events(&self) -> WorkspaceEvents<'_> {
105		WorkspaceEvents {
106			events: &self.events,
107		}
108	}
109
110	pub fn watch_roots(&self) -> Vec<WorkspaceWatchRoot> {
111		self.runtime
112			.ports
113			.live_watch_roots(self.runtime.state.snapshot())
114	}
115}
116
117impl WorkspaceRuntime {
118	fn new(ports: WorkspacePorts) -> Self {
119		Self {
120			ports,
121			state: WorkspaceState::new(),
122		}
123	}
124}
125
126impl<'a> WorkspaceQueries<'a> {
127	pub fn snapshot(&self) -> Option<&'a WorkspaceSnapshot> {
128		self.runtime.state.snapshot()
129	}
130
131	pub fn snapshot_arc(&self) -> Option<Arc<WorkspaceSnapshot>> {
132		self.runtime.state.snapshot_arc()
133	}
134
135	pub fn view(&self) -> Option<WorkspaceView<'a>> {
136		self.snapshot().map(WorkspaceView::new)
137	}
138
139	pub fn last_failure(&self) -> Option<&'a WorkspaceFailure> {
140		self.runtime.state.last_failure()
141	}
142
143	pub fn staleness(&self) -> WorkspaceStaleness {
144		WorkspaceStaleness::from_plan(&self.runtime.state.pending)
145	}
146}
147
148impl WorkspaceEvents<'_> {
149	pub fn event_cursor(&self) -> WorkspaceEventCursor {
150		self.events.cursor()
151	}
152
153	pub fn events_since(&self, cursor: WorkspaceEventCursor) -> &[WorkspaceEvent] {
154		self.events.since(cursor)
155	}
156}
157
158impl WorkspaceCommands<'_> {
159	pub fn execute(&mut self, spec: WorkspaceCommandSpec) -> WorkspaceTransition {
160		let command = WorkspaceCommand::new(
161			self.allocate_command_id(),
162			spec.scope_uri,
163			spec.kind,
164			spec.request,
165		);
166		self.run_command(command)
167	}
168
169	fn allocate_command_id(&mut self) -> WorkspaceCommandId {
170		let id = WorkspaceCommandId::new(*self.next_command_id);
171		*self.next_command_id += 1;
172		id
173	}
174
175	fn run_command(&mut self, command: WorkspaceCommand) -> WorkspaceTransition {
176		let generation = self.runtime.state.allocate_generation();
177		let context = WorkspaceEventContext::new(command.scope_uri, generation, command.id);
178		publish_command_started(self.events, &context);
179		let result = run_workspace_command(self.runtime, command.kind, command.request, generation);
180		publish_command_finished(self.runtime, self.events, &context, result)
181	}
182
183	pub fn refresh(&mut self, request: WorkspaceRequest) -> WorkspaceTransition {
184		self.execute(WorkspaceCommandSpec::new(
185			WorkspaceCommandKind::Refresh,
186			WorkspaceScopeUri::workspace(),
187			request,
188		))
189	}
190
191	pub fn load_catalog(&mut self, request: WorkspaceRequest) -> WorkspaceTransition {
192		self.execute(WorkspaceCommandSpec::new(
193			WorkspaceCommandKind::LoadSources,
194			WorkspaceScopeUri::workspace(),
195			request,
196		))
197	}
198
199	pub fn load_index(&mut self, request: WorkspaceRequest) -> WorkspaceTransition {
200		self.execute(WorkspaceCommandSpec::new(
201			WorkspaceCommandKind::BuildIndex,
202			WorkspaceScopeUri::workspace(),
203			request,
204		))
205	}
206
207	pub fn resolve_linkage(&mut self, request: WorkspaceRequest) -> WorkspaceTransition {
208		self.execute(WorkspaceCommandSpec::new(
209			WorkspaceCommandKind::ResolveLinkage,
210			WorkspaceScopeUri::workspace(),
211			request,
212		))
213	}
214
215	pub fn refresh_paths(
216		&mut self,
217		request: WorkspaceRequest,
218		paths: Vec<PathBuf>,
219	) -> WorkspaceTransition {
220		let command = WorkspaceCommand::new(
221			self.allocate_command_id(),
222			WorkspaceScopeUri::workspace(),
223			WorkspaceCommandKind::RefreshPaths,
224			request,
225		);
226		let generation = self.runtime.state.allocate_generation();
227		let context = WorkspaceEventContext::new(command.scope_uri, generation, command.id);
228		publish_command_started(self.events, &context);
229		let result = build_incremental_paths_snapshot(
230			self.runtime.state.snapshot(),
231			RefreshPorts {
232				source_catalog: &mut *self.runtime.ports.source_catalog,
233				code_index: &mut *self.runtime.ports.code_index,
234				linkage: &mut *self.runtime.ports.linkage,
235			},
236			command.request,
237			&paths,
238			generation,
239		);
240		publish_command_finished(self.runtime, self.events, &context, result)
241	}
242
243	pub fn refresh_changes(&mut self, request: WorkspaceRequest) -> WorkspaceTransition {
244		let command = WorkspaceCommand::new(
245			self.allocate_command_id(),
246			WorkspaceScopeUri::workspace(),
247			WorkspaceCommandKind::RefreshChanges,
248			request,
249		);
250		let generation = self.runtime.state.allocate_generation();
251		let context = WorkspaceEventContext::new(command.scope_uri, generation, command.id);
252		publish_command_started(self.events, &context);
253		let result = build_change_overlay_snapshot(
254			self.runtime.state.snapshot(),
255			&mut *self.runtime.ports.change_overlay,
256			command.request,
257			generation,
258		);
259		publish_command_finished(self.runtime, self.events, &context, result)
260	}
261
262	pub fn publish_snapshot(
263		&mut self,
264		publication: WorkspaceSnapshotPublication,
265	) -> WorkspaceTransition {
266		let command = WorkspaceCommand::new(
267			self.allocate_command_id(),
268			publication.scope_uri,
269			WorkspaceCommandKind::PublishSnapshot,
270			publication.request,
271		);
272		let context = WorkspaceEventContext::new(
273			command.scope_uri,
274			publication.snapshot.generation,
275			command.id,
276		);
277		publish_command_started(self.events, &context);
278		let transition = self.runtime.state.adopt_snapshot_arc(publication.snapshot);
279		events_for_ready_transition(self.events, &context, &transition);
280		transition
281	}
282}
283
284impl WorkspaceLiveCommands<'_> {
285	pub fn apply_plan(
286		&mut self,
287		request: WorkspaceRequest,
288		plan: WorkspaceLiveRefreshPlan,
289	) -> WorkspaceLivePlanTransition {
290		let command = WorkspaceCommand::new(
291			self.allocate_command_id(),
292			WorkspaceScopeUri::workspace(),
293			WorkspaceCommandKind::RefreshLivePlan,
294			request,
295		);
296		run_live_plan(self.runtime, self.events, command, &plan)
297	}
298
299	pub fn refresh_stale(&mut self, request: WorkspaceRequest) -> WorkspaceLivePlanTransition {
300		let plan = std::mem::take(&mut self.runtime.state.pending);
301		if plan.is_empty()
302			&& let Some(snapshot) = self.runtime.state.snapshot()
303		{
304			return WorkspaceLivePlanTransition {
305				transition: WorkspaceTransition::Ready {
306					generation: snapshot.generation,
307				},
308				replace_watcher: false,
309			};
310		}
311		let command = WorkspaceCommand::new(
312			self.allocate_command_id(),
313			WorkspaceScopeUri::workspace(),
314			WorkspaceCommandKind::RefreshStale,
315			request,
316		);
317		let live = run_live_plan(self.runtime, self.events, command, &plan);
318		if !plan.is_empty() && matches!(live.transition, WorkspaceTransition::Failed { .. }) {
319			coalesce_pending(self.runtime, plan);
320		}
321		live
322	}
323
324	pub fn mark_stale(&mut self, plan: WorkspaceLiveRefreshPlan) -> WorkspaceStaleness {
325		let plan = plan.without_notes();
326		if plan.is_empty() {
327			return WorkspaceStaleness::from_plan(&self.runtime.state.pending);
328		}
329		let command_id = self.allocate_command_id();
330		let staleness = coalesce_pending(self.runtime, plan);
331		let generation = self
332			.runtime
333			.state
334			.snapshot()
335			.map(|snapshot| snapshot.generation)
336			.unwrap_or_else(|| ResourceGeneration::new(0));
337		let context =
338			WorkspaceEventContext::new(WorkspaceScopeUri::workspace(), generation, command_id);
339		self.events
340			.publish(context.event(WorkspaceEventKind::StaleMarked));
341		staleness
342	}
343
344	fn allocate_command_id(&mut self) -> WorkspaceCommandId {
345		let id = WorkspaceCommandId::new(*self.next_command_id);
346		*self.next_command_id += 1;
347		id
348	}
349}
350
351fn run_live_plan(
352	runtime: &mut WorkspaceRuntime,
353	events: &mut WorkspaceEventLog,
354	command: WorkspaceCommand,
355	plan: &WorkspaceLiveRefreshPlan,
356) -> WorkspaceLivePlanTransition {
357	let generation = runtime.state.allocate_generation();
358	let context = WorkspaceEventContext::new(command.scope_uri, generation, command.id);
359	publish_command_started(events, &context);
360	let result = LivePlanBuild {
361		current: runtime.state.snapshot(),
362		source_catalog: &mut *runtime.ports.source_catalog,
363		code_index: &mut *runtime.ports.code_index,
364		linkage: &mut *runtime.ports.linkage,
365		change_overlay: &mut *runtime.ports.change_overlay,
366	}
367	.build(command.request, plan, generation);
368	let replace_watcher = result
369		.as_ref()
370		.map(|result| result.replace_watcher)
371		.unwrap_or_else(|_| plan.requires_rescan());
372	let transition = publish_command_finished(
373		runtime,
374		events,
375		&context,
376		result.map(|result| result.snapshot),
377	);
378	WorkspaceLivePlanTransition {
379		transition,
380		replace_watcher,
381	}
382}
383
384fn coalesce_pending(
385	runtime: &mut WorkspaceRuntime,
386	plan: WorkspaceLiveRefreshPlan,
387) -> WorkspaceStaleness {
388	let pending = std::mem::take(&mut runtime.state.pending);
389	runtime.state.pending = pending.coalesce(plan);
390	WorkspaceStaleness::from_plan(&runtime.state.pending)
391}
392
393fn publish_command_started(events: &mut WorkspaceEventLog, context: &WorkspaceEventContext) {
394	events.publish(context.event(WorkspaceEventKind::CommandAccepted));
395	events.publish(context.event(WorkspaceEventKind::WorkStarted));
396}
397
398fn publish_command_finished(
399	runtime: &mut WorkspaceRuntime,
400	events: &mut WorkspaceEventLog,
401	context: &WorkspaceEventContext,
402	result: WorkspaceResult<WorkspaceSnapshot>,
403) -> WorkspaceTransition {
404	match runtime.state.publish(result) {
405		WorkspaceTransition::Ready { generation } => {
406			events.publish(context.event(WorkspaceEventKind::SnapshotPublished));
407			events.publish(context.event(WorkspaceEventKind::WorkCompleted));
408			WorkspaceTransition::Ready { generation }
409		}
410		WorkspaceTransition::Failed {
411			failure,
412			preserved_generation,
413		} => {
414			events.publish(context.event(WorkspaceEventKind::WorkFailed));
415			WorkspaceTransition::Failed {
416				failure,
417				preserved_generation,
418			}
419		}
420	}
421}
422
423fn events_for_ready_transition(
424	events: &mut WorkspaceEventLog,
425	context: &WorkspaceEventContext,
426	transition: &WorkspaceTransition,
427) {
428	match transition {
429		WorkspaceTransition::Ready { .. } => {
430			events.publish(context.event(WorkspaceEventKind::SnapshotPublished));
431			events.publish(context.event(WorkspaceEventKind::WorkCompleted));
432		}
433		WorkspaceTransition::Failed { .. } => {
434			events.publish(context.event(WorkspaceEventKind::WorkFailed));
435		}
436	}
437}
438
439impl WorkspaceQueryPort for WorkspaceQueries<'_> {
440	fn snapshot(&self) -> Option<&WorkspaceSnapshot> {
441		self.runtime.state.snapshot()
442	}
443
444	fn snapshot_arc(&self) -> Option<Arc<WorkspaceSnapshot>> {
445		self.runtime.state.snapshot_arc()
446	}
447
448	fn view(&self) -> Option<WorkspaceView<'_>> {
449		self.snapshot().map(WorkspaceView::new)
450	}
451
452	fn last_failure(&self) -> Option<&WorkspaceFailure> {
453		self.runtime.state.last_failure()
454	}
455}
456
457impl WorkspaceEventPort for WorkspaceEvents<'_> {
458	fn event_cursor(&self) -> WorkspaceEventCursor {
459		self.events.cursor()
460	}
461
462	fn events_since(&self, cursor: WorkspaceEventCursor) -> &[WorkspaceEvent] {
463		self.events.since(cursor)
464	}
465}
466
467impl WorkspaceCommandPort for WorkspaceCommands<'_> {
468	fn execute_command(
469		&mut self,
470		kind: WorkspaceCommandKind,
471		scope_uri: WorkspaceScopeUri,
472		request: WorkspaceRequest,
473	) -> WorkspaceTransition {
474		self.execute(WorkspaceCommandSpec::new(kind, scope_uri, request))
475	}
476
477	fn publish_snapshot(
478		&mut self,
479		publication: WorkspaceSnapshotPublication,
480	) -> WorkspaceTransition {
481		self.publish_snapshot(publication)
482	}
483}
484
485fn run_workspace_command(
486	runtime: &mut WorkspaceRuntime,
487	kind: WorkspaceCommandKind,
488	request: WorkspaceRequest,
489	generation: ResourceGeneration,
490) -> WorkspaceResult<WorkspaceSnapshot> {
491	match kind {
492		WorkspaceCommandKind::Refresh => build_complete_snapshot(
493			&mut *runtime.ports.source_catalog,
494			&mut *runtime.ports.code_index,
495			&mut *runtime.ports.linkage,
496			&mut *runtime.ports.change_overlay,
497			request,
498			generation,
499		),
500		WorkspaceCommandKind::LoadSources => {
501			build_catalog_snapshot(&mut *runtime.ports.source_catalog, request, generation)
502		}
503		WorkspaceCommandKind::BuildIndex => run_build_index_command(runtime, request, generation),
504		WorkspaceCommandKind::ResolveLinkage => build_linkage_snapshot(
505			runtime.state.snapshot(),
506			&mut *runtime.ports.linkage,
507			&mut *runtime.ports.change_overlay,
508			request,
509			generation,
510		),
511		WorkspaceCommandKind::RefreshPaths => Err(WorkspaceFailure::new(
512			WorkspaceResource::CodeIndex,
513			"RefreshPaths commands require changed paths",
514		)),
515		WorkspaceCommandKind::RefreshChanges => build_change_overlay_snapshot(
516			runtime.state.snapshot(),
517			&mut *runtime.ports.change_overlay,
518			request,
519			generation,
520		),
521		WorkspaceCommandKind::RefreshLivePlan => Err(WorkspaceFailure::new(
522			WorkspaceResource::CodeIndex,
523			"RefreshLivePlan commands require a live refresh plan",
524		)),
525		WorkspaceCommandKind::PublishSnapshot => Err(WorkspaceFailure::new(
526			WorkspaceResource::CodeIndex,
527			"PublishSnapshot commands require a snapshot payload",
528		)),
529		WorkspaceCommandKind::RefreshStale => Err(WorkspaceFailure::new(
530			WorkspaceResource::CodeIndex,
531			"RefreshStale commands run through live refresh_stale",
532		)),
533	}
534}
535
536fn run_build_index_command(
537	runtime: &mut WorkspaceRuntime,
538	request: WorkspaceRequest,
539	generation: ResourceGeneration,
540) -> WorkspaceResult<WorkspaceSnapshot> {
541	let catalog_source = request
542		.should_reuse_current_catalog()
543		.then_some(runtime.state.snapshot())
544		.flatten();
545	build_index_only_snapshot(
546		catalog_source,
547		&mut *runtime.ports.source_catalog,
548		&mut *runtime.ports.code_index,
549		request,
550		generation,
551	)
552}
553
554#[cfg(test)]
555mod tests {
556	use std::fs;
557
558	use crate::LocalWorkspaceOptions;
559
560	use super::*;
561
562	#[test]
563	fn refresh_paths_publishes_symbols_from_modified_source() {
564		let temp = tempfile::tempdir().expect("tempdir");
565		let cache_dir = temp.path().join(".cache");
566		let source = temp.path().join("lib.rs");
567		fs::write(&source, "pub fn before_live_refresh() {}\n").expect("write source");
568		let mut registry = crate::LocalWorkspaceRegistry::local(
569			LocalWorkspaceOptions::new(vec![temp.path().to_path_buf()], None)
570				.with_cache_dir(Some(cache_dir)),
571		);
572
573		assert!(matches!(
574			registry
575				.commands()
576				.load_index(WorkspaceRequest::new("acceptance-index")),
577			WorkspaceTransition::Ready { .. }
578		));
579		assert!(snapshot_has_symbol(&registry, "before_live_refresh"));
580
581		fs::write(&source, "pub fn after_live_refresh() {}\n").expect("rewrite source");
582
583		assert!(matches!(
584			registry.commands().refresh_paths(
585				WorkspaceRequest::new("acceptance-live-refresh"),
586				vec![source]
587			),
588			WorkspaceTransition::Ready { .. }
589		));
590
591		assert!(snapshot_has_symbol(&registry, "after_live_refresh"));
592		assert!(!snapshot_has_symbol(&registry, "before_live_refresh"));
593	}
594
595	#[test]
596	fn mark_stale_records_staleness_without_touching_snapshot() {
597		let (temp, source, mut registry) = indexed_registry("pub fn before_stale() {}\n");
598		let _ = &temp;
599		let cursor = registry.events().event_cursor();
600
601		fs::write(&source, "pub fn after_stale() {}\n").expect("rewrite source");
602		let staleness = registry
603			.live_commands()
604			.mark_stale(WorkspaceLiveRefreshPlan::from_event(
605				crate::live::WorkspaceLiveEvent::SourcesChanged(vec![source.clone()]),
606			));
607
608		assert!(staleness.is_stale());
609		assert_eq!(staleness.stale_paths, vec![source]);
610		assert!(snapshot_has_symbol(&registry, "before_stale"));
611		assert_eq!(
612			registry
613				.events()
614				.events_since(cursor)
615				.iter()
616				.map(|event| event.kind)
617				.collect::<Vec<_>>(),
618			vec![WorkspaceEventKind::StaleMarked]
619		);
620	}
621
622	#[test]
623	fn refresh_stale_applies_coalesced_pending_plan() {
624		let (temp, source, mut registry) = indexed_registry("pub fn before_stale() {}\n");
625		let _ = &temp;
626
627		fs::write(&source, "pub fn after_stale() {}\n").expect("rewrite source");
628		registry
629			.live_commands()
630			.mark_stale(WorkspaceLiveRefreshPlan::from_event(
631				crate::live::WorkspaceLiveEvent::SourcesChanged(vec![source.clone()]),
632			));
633		registry
634			.live_commands()
635			.mark_stale(WorkspaceLiveRefreshPlan::from_event(
636				crate::live::WorkspaceLiveEvent::SourcesChanged(vec![source]),
637			));
638
639		let live = registry
640			.live_commands()
641			.refresh_stale(WorkspaceRequest::new("acceptance-refresh-stale"));
642		assert!(!live.replace_watcher());
643		assert!(matches!(
644			live.transition(),
645			WorkspaceTransition::Ready { .. }
646		));
647		assert!(snapshot_has_symbol(&registry, "after_stale"));
648		assert!(!snapshot_has_symbol(&registry, "before_stale"));
649		assert!(!registry.queries().staleness().is_stale());
650	}
651
652	#[test]
653	fn refresh_stale_without_pending_is_a_noop() {
654		let (temp, _source, mut registry) = indexed_registry("pub fn untouched() {}\n");
655		let _ = &temp;
656		let generation = registry.queries().snapshot().expect("snapshot").generation;
657		let cursor = registry.events().event_cursor();
658
659		let live = registry
660			.live_commands()
661			.refresh_stale(WorkspaceRequest::new("acceptance-noop"));
662
663		assert!(matches!(
664			live.transition(),
665			WorkspaceTransition::Ready { generation: ready } if ready == generation
666		));
667		assert!(registry.events().events_since(cursor).is_empty());
668	}
669
670	#[test]
671	fn refresh_stale_with_rescan_runs_complete_build() {
672		let (temp, source, mut registry) = indexed_registry("pub fn before_stale() {}\n");
673		let _ = &temp;
674
675		fs::write(&source, "pub fn after_stale() {}\n").expect("rewrite source");
676		registry
677			.live_commands()
678			.mark_stale(WorkspaceLiveRefreshPlan::from_event(
679				crate::live::WorkspaceLiveEvent::RescanRequired,
680			));
681
682		let live = registry
683			.live_commands()
684			.refresh_stale(WorkspaceRequest::new("acceptance-rescan"));
685
686		assert!(live.replace_watcher());
687		assert!(matches!(
688			live.transition(),
689			WorkspaceTransition::Ready { .. }
690		));
691		assert!(snapshot_has_symbol(&registry, "after_stale"));
692		assert!(!registry.queries().staleness().is_stale());
693	}
694
695	fn indexed_registry(
696		body: &str,
697	) -> (
698		tempfile::TempDir,
699		std::path::PathBuf,
700		crate::LocalWorkspaceRegistry,
701	) {
702		let temp = tempfile::tempdir().expect("tempdir");
703		let cache_dir = temp.path().join(".cache");
704		let source = temp.path().join("lib.rs");
705		fs::write(&source, body).expect("write source");
706		let mut registry = crate::LocalWorkspaceRegistry::local(
707			LocalWorkspaceOptions::new(vec![temp.path().to_path_buf()], None)
708				.with_cache_dir(Some(cache_dir)),
709		);
710		assert!(matches!(
711			registry
712				.commands()
713				.load_index(WorkspaceRequest::new("acceptance-index")),
714			WorkspaceTransition::Ready { .. }
715		));
716		(temp, source, registry)
717	}
718
719	fn snapshot_has_symbol(registry: &crate::LocalWorkspaceRegistry, name: &str) -> bool {
720		registry.queries().snapshot().is_some_and(|snapshot| {
721			snapshot
722				.index
723				.symbols
724				.iter()
725				.any(|symbol| symbol.name.contains(name))
726		})
727	}
728}