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_retaining_failure(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 run_live_plan_retaining_failure(self.runtime, self.events, command, plan)
318 }
319
320 pub fn mark_stale(&mut self, plan: WorkspaceLiveRefreshPlan) -> WorkspaceStaleness {
321 let plan = plan.without_notes();
322 if plan.is_empty() {
323 return WorkspaceStaleness::from_plan(&self.runtime.state.pending);
324 }
325 let command_id = self.allocate_command_id();
326 let staleness = coalesce_pending(self.runtime, plan);
327 let generation = self
328 .runtime
329 .state
330 .snapshot()
331 .map(|snapshot| snapshot.generation)
332 .unwrap_or_else(|| ResourceGeneration::new(0));
333 let context =
334 WorkspaceEventContext::new(WorkspaceScopeUri::workspace(), generation, command_id);
335 self.events
336 .publish(context.event(WorkspaceEventKind::StaleMarked));
337 staleness
338 }
339
340 fn allocate_command_id(&mut self) -> WorkspaceCommandId {
341 let id = WorkspaceCommandId::new(*self.next_command_id);
342 *self.next_command_id += 1;
343 id
344 }
345}
346
347fn run_live_plan(
348 runtime: &mut WorkspaceRuntime,
349 events: &mut WorkspaceEventLog,
350 command: WorkspaceCommand,
351 plan: &WorkspaceLiveRefreshPlan,
352) -> WorkspaceLivePlanTransition {
353 let generation = runtime.state.allocate_generation();
354 let context = WorkspaceEventContext::new(command.scope_uri, generation, command.id);
355 publish_command_started(events, &context);
356 let result = LivePlanBuild {
357 current: runtime.state.snapshot(),
358 source_catalog: &mut *runtime.ports.source_catalog,
359 code_index: &mut *runtime.ports.code_index,
360 linkage: &mut *runtime.ports.linkage,
361 change_overlay: &mut *runtime.ports.change_overlay,
362 }
363 .build(command.request, plan, generation);
364 let replace_watcher = result
365 .as_ref()
366 .map(|result| result.replace_watcher)
367 .unwrap_or_else(|_| plan.requires_rescan());
368 let transition = publish_command_finished(
369 runtime,
370 events,
371 &context,
372 result.map(|result| result.snapshot),
373 );
374 WorkspaceLivePlanTransition {
375 transition,
376 replace_watcher,
377 }
378}
379
380fn run_live_plan_retaining_failure(
381 runtime: &mut WorkspaceRuntime,
382 events: &mut WorkspaceEventLog,
383 command: WorkspaceCommand,
384 plan: WorkspaceLiveRefreshPlan,
385) -> WorkspaceLivePlanTransition {
386 let live = run_live_plan(runtime, events, command, &plan);
387 if !plan.is_empty() && matches!(live.transition, WorkspaceTransition::Failed { .. }) {
388 coalesce_pending(runtime, plan);
389 }
390 live
391}
392
393fn coalesce_pending(
394 runtime: &mut WorkspaceRuntime,
395 plan: WorkspaceLiveRefreshPlan,
396) -> WorkspaceStaleness {
397 let pending = std::mem::take(&mut runtime.state.pending);
398 runtime.state.pending = pending.coalesce(plan);
399 WorkspaceStaleness::from_plan(&runtime.state.pending)
400}
401
402fn publish_command_started(events: &mut WorkspaceEventLog, context: &WorkspaceEventContext) {
403 events.publish(context.event(WorkspaceEventKind::CommandAccepted));
404 events.publish(context.event(WorkspaceEventKind::WorkStarted));
405}
406
407fn publish_command_finished(
408 runtime: &mut WorkspaceRuntime,
409 events: &mut WorkspaceEventLog,
410 context: &WorkspaceEventContext,
411 result: WorkspaceResult<WorkspaceSnapshot>,
412) -> WorkspaceTransition {
413 match runtime.state.publish(result) {
414 WorkspaceTransition::Ready { generation } => {
415 events.publish(context.event(WorkspaceEventKind::SnapshotPublished));
416 events.publish(context.event(WorkspaceEventKind::WorkCompleted));
417 WorkspaceTransition::Ready { generation }
418 }
419 WorkspaceTransition::Failed {
420 failure,
421 preserved_generation,
422 } => {
423 events.publish(context.event(WorkspaceEventKind::WorkFailed));
424 WorkspaceTransition::Failed {
425 failure,
426 preserved_generation,
427 }
428 }
429 }
430}
431
432fn events_for_ready_transition(
433 events: &mut WorkspaceEventLog,
434 context: &WorkspaceEventContext,
435 transition: &WorkspaceTransition,
436) {
437 match transition {
438 WorkspaceTransition::Ready { .. } => {
439 events.publish(context.event(WorkspaceEventKind::SnapshotPublished));
440 events.publish(context.event(WorkspaceEventKind::WorkCompleted));
441 }
442 WorkspaceTransition::Failed { .. } => {
443 events.publish(context.event(WorkspaceEventKind::WorkFailed));
444 }
445 }
446}
447
448impl WorkspaceQueryPort for WorkspaceQueries<'_> {
449 fn snapshot(&self) -> Option<&WorkspaceSnapshot> {
450 self.runtime.state.snapshot()
451 }
452
453 fn snapshot_arc(&self) -> Option<Arc<WorkspaceSnapshot>> {
454 self.runtime.state.snapshot_arc()
455 }
456
457 fn view(&self) -> Option<WorkspaceView<'_>> {
458 self.snapshot().map(WorkspaceView::new)
459 }
460
461 fn last_failure(&self) -> Option<&WorkspaceFailure> {
462 self.runtime.state.last_failure()
463 }
464}
465
466impl WorkspaceEventPort for WorkspaceEvents<'_> {
467 fn event_cursor(&self) -> WorkspaceEventCursor {
468 self.events.cursor()
469 }
470
471 fn events_since(&self, cursor: WorkspaceEventCursor) -> &[WorkspaceEvent] {
472 self.events.since(cursor)
473 }
474}
475
476impl WorkspaceCommandPort for WorkspaceCommands<'_> {
477 fn execute_command(
478 &mut self,
479 kind: WorkspaceCommandKind,
480 scope_uri: WorkspaceScopeUri,
481 request: WorkspaceRequest,
482 ) -> WorkspaceTransition {
483 self.execute(WorkspaceCommandSpec::new(kind, scope_uri, request))
484 }
485
486 fn publish_snapshot(
487 &mut self,
488 publication: WorkspaceSnapshotPublication,
489 ) -> WorkspaceTransition {
490 self.publish_snapshot(publication)
491 }
492}
493
494fn run_workspace_command(
495 runtime: &mut WorkspaceRuntime,
496 kind: WorkspaceCommandKind,
497 request: WorkspaceRequest,
498 generation: ResourceGeneration,
499) -> WorkspaceResult<WorkspaceSnapshot> {
500 match kind {
501 WorkspaceCommandKind::Refresh => build_complete_snapshot(
502 &mut *runtime.ports.source_catalog,
503 &mut *runtime.ports.code_index,
504 &mut *runtime.ports.linkage,
505 &mut *runtime.ports.change_overlay,
506 request,
507 generation,
508 ),
509 WorkspaceCommandKind::LoadSources => {
510 build_catalog_snapshot(&mut *runtime.ports.source_catalog, request, generation)
511 }
512 WorkspaceCommandKind::BuildIndex => run_build_index_command(runtime, request, generation),
513 WorkspaceCommandKind::ResolveLinkage => build_linkage_snapshot(
514 runtime.state.snapshot(),
515 &mut *runtime.ports.linkage,
516 &mut *runtime.ports.change_overlay,
517 request,
518 generation,
519 ),
520 WorkspaceCommandKind::RefreshPaths => Err(WorkspaceFailure::new(
521 WorkspaceResource::CodeIndex,
522 "RefreshPaths commands require changed paths",
523 )),
524 WorkspaceCommandKind::RefreshChanges => build_change_overlay_snapshot(
525 runtime.state.snapshot(),
526 &mut *runtime.ports.change_overlay,
527 request,
528 generation,
529 ),
530 WorkspaceCommandKind::RefreshLivePlan => Err(WorkspaceFailure::new(
531 WorkspaceResource::CodeIndex,
532 "RefreshLivePlan commands require a live refresh plan",
533 )),
534 WorkspaceCommandKind::PublishSnapshot => Err(WorkspaceFailure::new(
535 WorkspaceResource::CodeIndex,
536 "PublishSnapshot commands require a snapshot payload",
537 )),
538 WorkspaceCommandKind::RefreshStale => Err(WorkspaceFailure::new(
539 WorkspaceResource::CodeIndex,
540 "RefreshStale commands run through live refresh_stale",
541 )),
542 }
543}
544
545fn run_build_index_command(
546 runtime: &mut WorkspaceRuntime,
547 request: WorkspaceRequest,
548 generation: ResourceGeneration,
549) -> WorkspaceResult<WorkspaceSnapshot> {
550 let catalog_source = request
551 .should_reuse_current_catalog()
552 .then_some(runtime.state.snapshot())
553 .flatten();
554 build_index_only_snapshot(
555 catalog_source,
556 &mut *runtime.ports.source_catalog,
557 &mut *runtime.ports.code_index,
558 request,
559 generation,
560 )
561}
562
563#[cfg(test)]
564mod tests {
565 use std::fs;
566
567 use crate::LocalWorkspaceOptions;
568
569 use super::*;
570
571 #[test]
572 fn refresh_paths_publishes_symbols_from_modified_source() {
573 let temp = tempfile::tempdir().expect("tempdir");
574 let cache_dir = temp.path().join(".cache");
575 let source = temp.path().join("lib.rs");
576 fs::write(&source, "pub fn before_live_refresh() {}\n").expect("write source");
577 let mut registry = crate::LocalWorkspaceRegistry::local(
578 LocalWorkspaceOptions::new(vec![temp.path().to_path_buf()], None)
579 .with_cache_dir(Some(cache_dir)),
580 );
581
582 assert!(matches!(
583 registry
584 .commands()
585 .load_index(WorkspaceRequest::new("acceptance-index")),
586 WorkspaceTransition::Ready { .. }
587 ));
588 assert!(snapshot_has_symbol(®istry, "before_live_refresh"));
589
590 fs::write(&source, "pub fn after_live_refresh() {}\n").expect("rewrite source");
591
592 assert!(matches!(
593 registry.commands().refresh_paths(
594 WorkspaceRequest::new("acceptance-live-refresh"),
595 vec![source]
596 ),
597 WorkspaceTransition::Ready { .. }
598 ));
599
600 assert!(snapshot_has_symbol(®istry, "after_live_refresh"));
601 assert!(!snapshot_has_symbol(®istry, "before_live_refresh"));
602 }
603
604 #[test]
605 fn refresh_paths_classifies_new_files_with_declared_srcset() {
606 let temp = tempfile::tempdir().expect("tempdir");
607 let production = temp.path().join("src/java/com/acme/Production.java");
608 fs::create_dir_all(production.parent().expect("production parent"))
609 .expect("production dirs");
610 fs::write(
611 temp.path().join(".code-moniker.toml"),
612 r#"
613[[workspace.source_group]]
614roots = [
615 { path = "src/java", srcset = "main" },
616 { path = "test", srcset = "test" },
617]
618"#,
619 )
620 .expect("source group config");
621 fs::write(
622 &production,
623 "package com.acme; public class Production {}\n",
624 )
625 .expect("production source");
626 let mut registry = crate::LocalWorkspaceRegistry::local(LocalWorkspaceOptions::new(
627 vec![temp.path().to_path_buf()],
628 None,
629 ));
630 assert!(matches!(
631 registry
632 .commands()
633 .load_index(WorkspaceRequest::new("source-group-initial")),
634 WorkspaceTransition::Ready { .. }
635 ));
636
637 let test = temp.path().join("test/unit/com/acme/AddedTest.java");
638 fs::create_dir_all(test.parent().expect("test parent")).expect("test dirs");
639 fs::write(&test, "package com.acme; public class AddedTest {}\n").expect("test source");
640 assert!(matches!(
641 registry
642 .commands()
643 .refresh_paths(WorkspaceRequest::new("source-group-new-file"), vec![test],),
644 WorkspaceTransition::Ready { .. }
645 ));
646
647 assert!(snapshot_has_identity(
648 ®istry,
649 "srcset:test/lang:java/package:com/package:acme/module:AddedTest/class:AddedTest"
650 ));
651 }
652
653 #[test]
654 fn full_rescan_reclassifies_cached_files_after_source_group_config_change() {
655 let temp = tempfile::tempdir().expect("tempdir");
656 let source = temp.path().join("custom/com/acme/Reclassified.java");
657 fs::create_dir_all(source.parent().expect("source parent")).expect("source dirs");
658 let config = temp.path().join(".code-moniker.toml");
659 fs::write(
660 &config,
661 r#"
662[[workspace.source_group]]
663roots = [{ path = "custom", srcset = "main" }]
664"#,
665 )
666 .expect("initial config");
667 fs::write(&source, "package com.acme; public class Reclassified {}\n").expect("source");
668 let cache_dir = temp.path().join(".cache");
669 let mut registry = crate::LocalWorkspaceRegistry::local(
670 LocalWorkspaceOptions::new(vec![temp.path().to_path_buf()], None)
671 .with_cache_dir(Some(cache_dir)),
672 );
673 assert!(matches!(
674 registry
675 .commands()
676 .load_index(WorkspaceRequest::new("source-group-main")),
677 WorkspaceTransition::Ready { .. }
678 ));
679 assert!(snapshot_has_identity(®istry, "srcset:main/lang:java"));
680
681 fs::write(
682 &config,
683 r#"
684[[workspace.source_group]]
685roots = [{ path = "custom", srcset = "test" }]
686"#,
687 )
688 .expect("updated config");
689 registry
690 .live_commands()
691 .mark_stale(WorkspaceLiveRefreshPlan::from_event(
692 crate::live::WorkspaceLiveEvent::RescanRequired,
693 ));
694 let live = registry
695 .live_commands()
696 .refresh_stale(WorkspaceRequest::new("source-group-test"));
697 assert!(matches!(
698 live.transition(),
699 WorkspaceTransition::Ready { .. }
700 ));
701
702 assert!(snapshot_has_identity(®istry, "srcset:test/lang:java"));
703 assert!(!snapshot_has_identity(®istry, "srcset:main/lang:java"));
704 }
705
706 #[test]
707 fn invalid_source_group_config_fails_with_the_mapping_diagnostic() {
708 let temp = tempfile::tempdir().expect("tempdir");
709 fs::write(temp.path().join("lib.rs"), "pub fn source() {}\n").expect("source");
710 fs::write(
711 temp.path().join(".code-moniker.toml"),
712 r#"
713[[workspace.source_group]]
714roots = ["src"]
715
716[[workspace.source_group]]
717roots = ["src/generated"]
718"#,
719 )
720 .expect("invalid config");
721 let mut registry = crate::LocalWorkspaceRegistry::local(LocalWorkspaceOptions::new(
722 vec![temp.path().to_path_buf()],
723 None,
724 ));
725
726 let transition = registry
727 .commands()
728 .load_index(WorkspaceRequest::new("invalid-source-group"));
729
730 assert!(matches!(
731 transition,
732 WorkspaceTransition::Failed { failure, .. }
733 if failure.message.contains("invalid source groups")
734 && failure.message.contains("overlap")
735 ));
736 }
737
738 #[test]
739 fn mark_stale_records_staleness_without_touching_snapshot() {
740 let (temp, source, mut registry) = indexed_registry("pub fn before_stale() {}\n");
741 let _ = &temp;
742 let cursor = registry.events().event_cursor();
743
744 fs::write(&source, "pub fn after_stale() {}\n").expect("rewrite source");
745 let staleness = registry
746 .live_commands()
747 .mark_stale(WorkspaceLiveRefreshPlan::from_event(
748 crate::live::WorkspaceLiveEvent::SourcesChanged(vec![source.clone()]),
749 ));
750
751 assert!(staleness.is_stale());
752 assert_eq!(staleness.stale_paths, vec![source]);
753 assert!(snapshot_has_symbol(®istry, "before_stale"));
754 assert_eq!(
755 registry
756 .events()
757 .events_since(cursor)
758 .iter()
759 .map(|event| event.kind)
760 .collect::<Vec<_>>(),
761 vec![WorkspaceEventKind::StaleMarked]
762 );
763 }
764
765 #[test]
766 fn refresh_stale_applies_coalesced_pending_plan() {
767 let (temp, source, mut registry) = indexed_registry("pub fn before_stale() {}\n");
768 let _ = &temp;
769
770 fs::write(&source, "pub fn after_stale() {}\n").expect("rewrite source");
771 registry
772 .live_commands()
773 .mark_stale(WorkspaceLiveRefreshPlan::from_event(
774 crate::live::WorkspaceLiveEvent::SourcesChanged(vec![source.clone()]),
775 ));
776 registry
777 .live_commands()
778 .mark_stale(WorkspaceLiveRefreshPlan::from_event(
779 crate::live::WorkspaceLiveEvent::SourcesChanged(vec![source]),
780 ));
781
782 let live = registry
783 .live_commands()
784 .refresh_stale(WorkspaceRequest::new("acceptance-refresh-stale"));
785 assert!(!live.replace_watcher());
786 assert!(matches!(
787 live.transition(),
788 WorkspaceTransition::Ready { .. }
789 ));
790 assert!(snapshot_has_symbol(®istry, "after_stale"));
791 assert!(!snapshot_has_symbol(®istry, "before_stale"));
792 assert!(!registry.queries().staleness().is_stale());
793 }
794
795 #[test]
796 fn failed_live_plan_is_retained_for_a_later_refresh() {
797 let (temp, source, mut registry) = indexed_registry("pub fn before_retry() {}\n");
798 let _ = &temp;
799 fs::write(&source, "pub fn after_retry() {}\n").expect("rewrite source");
800 let plan =
801 WorkspaceLiveRefreshPlan::from_event(crate::live::WorkspaceLiveEvent::RescanRequired);
802 let cancellation = crate::snapshot::WorkspaceCancellation::default();
803 cancellation.cancel();
804
805 let failed = registry.live_commands().apply_plan(
806 WorkspaceRequest::new("cancelled-live-plan").with_cancellation(cancellation),
807 plan,
808 );
809 assert!(matches!(
810 failed.transition(),
811 WorkspaceTransition::Failed { .. }
812 ));
813 assert!(registry.queries().staleness().is_stale());
814 assert!(snapshot_has_symbol(®istry, "before_retry"));
815
816 let retried = registry
817 .live_commands()
818 .refresh_stale(WorkspaceRequest::new("retry-live-plan"));
819 assert!(matches!(
820 retried.transition(),
821 WorkspaceTransition::Ready { .. }
822 ));
823 assert!(snapshot_has_symbol(®istry, "after_retry"));
824 assert!(!registry.queries().staleness().is_stale());
825 }
826
827 #[test]
828 fn refresh_stale_without_pending_is_a_noop() {
829 let (temp, _source, mut registry) = indexed_registry("pub fn untouched() {}\n");
830 let _ = &temp;
831 let generation = registry.queries().snapshot().expect("snapshot").generation;
832 let cursor = registry.events().event_cursor();
833
834 let live = registry
835 .live_commands()
836 .refresh_stale(WorkspaceRequest::new("acceptance-noop"));
837
838 assert!(matches!(
839 live.transition(),
840 WorkspaceTransition::Ready { generation: ready } if ready == generation
841 ));
842 assert!(registry.events().events_since(cursor).is_empty());
843 }
844
845 #[test]
846 fn refresh_stale_with_rescan_runs_complete_build() {
847 let (temp, source, mut registry) = indexed_registry("pub fn before_stale() {}\n");
848 let _ = &temp;
849
850 fs::write(&source, "pub fn after_stale() {}\n").expect("rewrite source");
851 registry
852 .live_commands()
853 .mark_stale(WorkspaceLiveRefreshPlan::from_event(
854 crate::live::WorkspaceLiveEvent::RescanRequired,
855 ));
856
857 let live = registry
858 .live_commands()
859 .refresh_stale(WorkspaceRequest::new("acceptance-rescan"));
860
861 assert!(live.replace_watcher());
862 assert!(matches!(
863 live.transition(),
864 WorkspaceTransition::Ready { .. }
865 ));
866 assert!(snapshot_has_symbol(®istry, "after_stale"));
867 assert!(!registry.queries().staleness().is_stale());
868 }
869
870 #[test]
871 fn cancelled_refresh_stops_before_publishing_a_snapshot() {
872 let temp = tempfile::tempdir().expect("tempdir");
873 fs::write(temp.path().join("lib.rs"), "pub fn never_indexed() {}\n").expect("write source");
874 let mut registry = crate::LocalWorkspaceRegistry::local(LocalWorkspaceOptions::new(
875 vec![temp.path().to_path_buf()],
876 None,
877 ));
878 let cancellation = crate::snapshot::WorkspaceCancellation::default();
879 cancellation.cancel();
880
881 let transition = registry
882 .commands()
883 .refresh(WorkspaceRequest::new("cancelled-refresh").with_cancellation(cancellation));
884
885 assert!(matches!(
886 transition,
887 WorkspaceTransition::Failed { failure, .. }
888 if failure.message == "workspace build cancelled"
889 ));
890 assert!(registry.queries().snapshot().is_none());
891 }
892
893 #[test]
894 fn cancelled_incremental_refresh_keeps_the_previous_snapshot() {
895 let (temp, source, mut registry) = indexed_registry("pub fn before_cancel() {}\n");
896 let _ = &temp;
897 let generation = registry.queries().snapshot().expect("snapshot").generation;
898 fs::write(&source, "pub fn after_cancel() {}\n").expect("rewrite source");
899 let cancellation = crate::snapshot::WorkspaceCancellation::default();
900 cancellation.cancel();
901
902 let transition = registry.commands().refresh_paths(
903 WorkspaceRequest::new("cancelled-incremental").with_cancellation(cancellation),
904 vec![source],
905 );
906
907 assert!(matches!(
908 transition,
909 WorkspaceTransition::Failed { failure, .. }
910 if failure.message == "workspace build cancelled"
911 ));
912 assert_eq!(
913 registry
914 .queries()
915 .snapshot()
916 .expect("previous snapshot")
917 .generation,
918 generation
919 );
920 assert!(snapshot_has_symbol(®istry, "before_cancel"));
921 assert!(!snapshot_has_symbol(®istry, "after_cancel"));
922 }
923
924 fn indexed_registry(
925 body: &str,
926 ) -> (
927 tempfile::TempDir,
928 std::path::PathBuf,
929 crate::LocalWorkspaceRegistry,
930 ) {
931 let temp = tempfile::tempdir().expect("tempdir");
932 let cache_dir = temp.path().join(".cache");
933 let source = temp.path().join("lib.rs");
934 fs::write(&source, body).expect("write source");
935 let mut registry = crate::LocalWorkspaceRegistry::local(
936 LocalWorkspaceOptions::new(vec![temp.path().to_path_buf()], None)
937 .with_cache_dir(Some(cache_dir)),
938 );
939 assert!(matches!(
940 registry
941 .commands()
942 .load_index(WorkspaceRequest::new("acceptance-index")),
943 WorkspaceTransition::Ready { .. }
944 ));
945 (temp, source, registry)
946 }
947
948 fn snapshot_has_symbol(registry: &crate::LocalWorkspaceRegistry, name: &str) -> bool {
949 registry.queries().snapshot().is_some_and(|snapshot| {
950 snapshot
951 .index
952 .symbols
953 .iter()
954 .any(|symbol| symbol.name.contains(name))
955 })
956 }
957
958 fn snapshot_has_identity(registry: &crate::LocalWorkspaceRegistry, identity: &str) -> bool {
959 registry.queries().snapshot().is_some_and(|snapshot| {
960 snapshot
961 .index
962 .symbols
963 .iter()
964 .any(|symbol| symbol.identity.contains(identity))
965 })
966 }
967}