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(®istry, "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(®istry, "after_live_refresh"));
592 assert!(!snapshot_has_symbol(®istry, "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(®istry, "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(®istry, "after_stale"));
648 assert!(!snapshot_has_symbol(®istry, "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(®istry, "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}