use super::*; use crate::app::text_workspace::{DrawerTab, Event as TextEvent}; use crate::source_workspace::Workspace; use oa_core::source_project::SourceBundle; use std::collections::BTreeMap; use std::time::Instant; mod departure_selection; const DEFINITIONS: &str = "defs/活動.arcadia"; const USES: &str = "uses.arcadia"; fn fixture_bundle() -> SourceBundle { SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([ ("project.arcadia".into(), concat!( "arcadia 7;\r\nproject model \"Navigation\" id \"00000000-0000-0000-0000-000000000001\" {\r\n", " include \"defs/活動.arcadia\";\r\n include \"uses.arcadia\";\r\n}\r\n" ).into()), (DEFINITIONS.into(), concat!( "// 船 and exact CRLF\r\n", "oa_activity pump \"送水 🚀\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n", "oa_activity backup \"送水 🚀\" id \"00000000-0000-0000-0000-000000000004\" {}\r\n" ).into()), (USES.into(), concat!( "// Reference names remain lexical\r\n", "interaction first \"先\" id \"00000000-0000-0000-0000-000000000003\" {\r\n", " description \"船 @pump\"; source @pump; target @backup;\r\n}\r\n", "interaction second \"後\" id \"00000000-0000-0000-0000-000000000005\" {\r\n", " source @backup; target @ // @pump is a comment\r\n pump;\r\n}\r\n" ).into()), ]), } } fn fixture() -> App { let mut app = App::with_project(Project::empty()); app.document = Document::from_bundle(fixture_bundle(), LIMITS).unwrap(); app.source_workspace = Workspace::new(app.document.bundle(), true); app.publish_source_diagnostics(app.document.compiled().diagnostics.clone()); app.selected = app.project().id; app.refresh_draft(); app.page = Page::Text; app.show_source_file(false); show(&mut app, USES); let text = app.source_workspace.text(); let offset = text.find("source @pump").unwrap() + "source @".len(); app.source_editor .move_to(selection(text, offset, offset + 4, true)); assert!(app.source_navigation_available()); app } fn select_effective_pump(app: &mut App, line: bool) -> String { let source = app.source_workspace.text(); let offset = source.find("source @pump").unwrap() + "source @pu".len(); app.source_editor.move_to(text_editor::Cursor { position: position(source, offset), selection: None, }); send( app, Message::SourceEdit(if line { text_editor::Action::SelectLine } else { text_editor::Action::SelectWord }), ); let selected = app.source_editor.selection().unwrap(); assert!(selected.contains("pump")); let raw = app.source_editor.cursor(); assert_eq!(raw.selection, Some(raw.position)); selected } fn send(app: &mut App, message: Message) { // Dropping the returned Task does not poll its worker/lint future. Completion // below is deterministic and still traverses the real App message handler. let _ = app.update(message); } fn symbol(app: &mut App, event: nav::SymbolEvent) { let message = app.source_symbol_message(event); send(app, message); } fn close_drawer(app: &mut App) { let message = Message::TextWorkspace(TextEvent::HideDrawer(app.text_drawer_context())); send(app, message); } fn choice(app: &App, stamp: query::QueryStamp, reference: bool, index: usize) -> Message { app.source_symbol_message(nav::SymbolEvent::Choose { stamp, visit: app.text_workspace.drawer_visit(), reference, index, }) } fn references_visible(app: &App) -> bool { app.text_workspace.drawer() == Some(DrawerTab::References) } fn show(app: &mut App, path: &str) { send(app, Message::SourceFile(path.into())); reveal(app); } fn reveal(app: &mut App) { if let Some(ticket) = app.source_reveal_ticket() { send(app, Message::SourceViewport(Some(ticket))); } } fn position(text: &str, offset: usize) -> text_editor::Position { assert!(text.is_char_boundary(offset)); let prefix = &text[..offset]; text_editor::Position { line: prefix.bytes().filter(|byte| *byte == b'\n').count(), column: prefix .rsplit_once('\n') .map_or(prefix.len(), |(_, tail)| tail.len()), } } fn selection(text: &str, start: usize, end: usize, reversed: bool) -> text_editor::Cursor { let (start, end) = (position(text, start), position(text, end)); text_editor::Cursor { position: if reversed { end } else { start }, selection: Some(if reversed { start } else { end }), } } fn begin(app: &mut App, action: query::Action) -> nav::Request { symbol(app, nav::SymbolEvent::Query(action)); let request = app .source_symbols .request .clone() .expect("request admitted"); assert_eq!(app.source_symbols.in_flight, Some(request.job)); assert_eq!(app.source_symbols.display_request.as_ref(), Some(&request)); assert_eq!(app.text_workspace.references().stamp(), Some(request.stamp)); assert!(references_visible(app)); request } fn completion(app: &App, request: nav::Request) -> nav::Completion { let snapshot = Arc::new(app.with_source_input(|input| { query::admit_owned(input, query::Limits::default()) .unwrap() .capture() .unwrap() })); let result = Arc::new( snapshot .query(query::Query { stamp: request.stamp, path: &request.path, byte_offset: request.offset, action: request.action, }) .unwrap(), ); nav::Completion { request, result: Ok((snapshot, result)), } } fn finish(app: &mut App, action: query::Action) -> Arc { let request = begin(app, action); let ready = completion(app, request); let results = ready.result.as_ref().unwrap().1.clone(); send(app, Message::SourceSymbolsReady(ready)); assert!(app.source_symbols.in_flight.is_none()); results } #[test] fn valid_definition_and_reference_choice_keep_whole_model_and_source_unchanged() { let mut app = fixture(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let results = finish(&mut app, query::Action::Definition); assert_eq!(results.disposition, query::Disposition::Direct); assert_eq!(results.total_definitions, 1); assert_eq!(app.source_workspace.active, DEFINITIONS); assert_eq!(app.source_symbols.back.len(), 1); let row = &results.definitions[0]; assert_eq!(row.declaration.alias, "pump"); assert_eq!(row.declaration.name.as_deref(), Some("送水 🚀")); let expected = selection(&raw.files[DEFINITIONS], row.span.start, row.span.end, false); assert_eq!( app.source_jump, Some(expected), "jump waits for the matching viewport" ); reveal(&mut app); assert_eq!(app.source_editor.cursor(), expected); let uses = finish(&mut app, query::Action::References); assert_eq!( uses.total_references, 2, "comment/string references must not leak into App results" ); assert_eq!( app.source_workspace.active, DEFINITIONS, "references require a choice" ); let second = uses.references[1].clone(); let message = choice(&app, uses.stamp, true, 1); send(&mut app, message); reveal(&mut app); assert_eq!(app.source_workspace.active, USES); assert_eq!( app.source_editor.cursor(), selection(&raw.files[USES], second.span.start, second.span.end, false) ); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert_eq!(app.source_workspace.bundle(), raw); assert_eq!(app.document.revision(), 0); assert!(!app.document.can_undo() && !app.document.can_redo()); } #[test] fn detached_duplicate_requires_an_explicit_choice_even_when_included_source_is_validated() { let mut app = fixture(); let mut loaded = app.source_workspace.bundle(); loaded.files.insert( "detached.arcadia".into(), "oa_activity pump \"Different source\" id \"00000000-0000-0000-0000-000000000099\" {}\r\n" .into(), ); let cursor = app.source_editor.cursor(); app.source_workspace = Workspace::new(&loaded, true); app.source_workspace.active = USES.into(); app.show_source_file(false); app.source_editor.move_to(cursor); let results = finish(&mut app, query::Action::Definition); assert_eq!(results.total_definitions, 2); assert_eq!(results.disposition, query::Disposition::Choices); assert_eq!(app.source_workspace.active, USES); assert!(app.source_symbols.back.is_empty()); let index = results .definitions .iter() .position(|row| row.path == "detached.arcadia") .unwrap(); assert_eq!( results.definitions[index].membership, query::Membership::Detached ); let message = choice(&app, results.stamp, false, index); send(&mut app, message); reveal(&mut app); assert_eq!(app.source_workspace.active, "detached.arcadia"); assert_eq!(app.source_workspace.bundle(), loaded); assert_eq!( app.document.bundle().files.len(), 3, "choice must not adopt the detached source" ); } #[test] fn invalid_unapplied_drafts_navigate_without_apply_or_losing_either_history() { let mut app = fixture(); let base = app.document.snapshot(); let id = app.project().id; app.document .execute(Command::Rename(id, "Accepted one".into())) .unwrap(); let one = app.document.snapshot(); app.document .execute(Command::Rename(id, "Accepted two".into())) .unwrap(); let two = app.document.snapshot(); assert!(app.document.undo()); app.sync_source(); show(&mut app, USES); let prior_raw = app.source_workspace.text().to_owned(); let invalid = format!("{prior_raw}\r\n? unfinished raw edit 雨\r\n"); let _ = app.update(Message::SourceEdit(text_editor::Action::SelectAll)); let _ = app.update(Message::SourceEdit(text_editor::Action::Edit( text_editor::Edit::Paste(Arc::new(invalid)), ))); app.source_workspace.applied = false; let text = app.source_workspace.text(); let alias = text.find("source @pump").unwrap() + "source @".len(); app.source_editor .move_to(selection(text, alias, alias + 4, true)); let raw = app.source_workspace.bundle(); let revision = app.document.revision(); let results = finish(&mut app, query::Action::Definition); assert_eq!(results.disposition, query::Disposition::Choices); assert!(results.scope.partial() && !results.scope.validated); let index = results .definitions .iter() .position(|row| row.declaration.alias == "pump") .unwrap(); let message = choice(&app, results.stamp, false, index); send(&mut app, message); reveal(&mut app); symbol(&mut app, nav::SymbolEvent::Back); reveal(&mut app); assert!(Arc::ptr_eq(&one, &app.document.snapshot())); assert_eq!(app.project(), &one.decoded.project); assert_eq!(app.document.bundle(), &one.bundle); assert_eq!(app.document.revision(), revision); assert_eq!(app.source_workspace.bundle(), raw); assert!(!app.source_workspace.applied); assert!(app.document.can_undo() && app.document.can_redo()); // Actual round trips, not only Boolean history flags. assert!(app.document.undo()); assert_eq!(app.document.bundle(), &base.bundle); assert!(app.document.redo()); assert_eq!(app.document.bundle(), &one.bundle); assert!(app.document.redo()); assert_eq!(app.document.bundle(), &two.bundle); assert!(app.source_workspace.undo(false)); assert_eq!(app.source_workspace.text(), prior_raw); assert!(app.source_workspace.undo(true)); assert_eq!(app.source_workspace.bundle(), raw); } #[test] fn completion_retains_current_display_but_rejects_navigation_after_context_changes() { for change in 0..7 { let mut app = fixture(); let request = begin(&mut app, query::Action::Definition); let ready = completion(&app, request.clone()); let expected = ready.result.as_ref().unwrap().1.clone(); let accepted = app.document.snapshot(); match change { 0 => show(&mut app, "project.arcadia"), 1 => send( &mut app, Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Insert('x'))), ), 2 => app.source_workspace.reset_navigation_session(), 3 => app.source_editor.move_to(text_editor::Cursor { position: text_editor::Position { line: 0, column: 0 }, selection: None, }), 4 => app.source_interaction(), 5 => { app.source_symbols .raw_interaction .fetch_add(1, Ordering::SeqCst); } _ => app.page = Page::Diagram, } let after = app.source_workspace.bundle(); let active = app.source_workspace.active.clone(); let cursor = app.source_editor.cursor(); let drawer = app.text_workspace.drawer(); send(&mut app, Message::SourceSymbolsReady(ready)); assert!( app.source_symbols.in_flight.is_none(), "case {change}: release worker slot" ); if matches!(change, 1 | 2) { assert!( app.source_symbols.results.is_none(), "case {change}: changed source/session cannot own display" ); } else { assert!( Arc::ptr_eq(app.source_symbols.results.as_ref().unwrap(), &expected), "case {change}: canceled navigation still owns current-source display" ); } assert_eq!( app.text_workspace.drawer(), drawer, "completion cannot select a tab" ); assert!(app.source_symbols.back.is_empty()); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert_eq!(app.source_workspace.active, active); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), after); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert_eq!(app.document.revision(), 0); assert!(!app.document.can_undo() && !app.document.can_redo()); } } #[test] fn unknown_job_cannot_release_current_worker_and_old_duplicate_cannot_replace_new_results() { let mut app = fixture(); let request = begin(&mut app, query::Action::References); let ready = completion(&app, request.clone()); let mut unrelated = ready.clone(); unrelated.request.job = Id::new_v4(); send(&mut app, Message::SourceSymbolsReady(unrelated)); assert_eq!(app.source_symbols.in_flight, Some(request.job)); assert!(app.source_symbols.results.is_none()); send(&mut app, Message::SourceSymbolsReady(ready.clone())); let newer = begin(&mut app, query::Action::References); let newer_ready = completion(&app, newer.clone()); send(&mut app, Message::SourceSymbolsReady(ready)); assert_eq!(app.source_symbols.in_flight, Some(newer.job)); send(&mut app, Message::SourceSymbolsReady(newer_ready)); assert_eq!( app.source_symbols.results.as_ref().unwrap().stamp, newer.stamp ); } #[test] fn close_during_busy_retains_hidden_completion_without_reopening_or_navigation() { let mut app = fixture(); let request = begin(&mut app, query::Action::Definition); let ready = completion(&app, request.clone()); let expected = ready.result.as_ref().unwrap().1.clone(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); assert!(references_visible(&app)); close_drawer(&mut app); assert!(app.text_workspace.drawer().is_none()); assert_eq!(app.source_symbols.in_flight, Some(request.job)); assert!(app.source_symbols.request.is_none()); assert_eq!(app.source_symbols.display_request.as_ref(), Some(&request)); send(&mut app, Message::SourceSymbolsReady(ready)); assert!(app.text_workspace.drawer().is_none()); assert!(app.source_symbols.in_flight.is_none()); assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &expected )); assert!(app.source_symbols.back.is_empty()); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert_eq!(app.source_workspace.active, USES); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); let next = begin(&mut app, query::Action::References); assert_ne!(next.job, request.job); assert!(references_visible(&app)); assert!( app.source_symbols.results.is_none(), "explicit new query replaces prior display ownership" ); } #[test] fn matching_reveal_ticket_is_required_and_raw_input_invalidates_pending_reveal() { let mut app = fixture(); finish(&mut app, query::Action::Definition); let ticket = app.source_reveal_ticket().unwrap(); let destination = app.source_jump.unwrap(); let before = app.source_editor.cursor(); assert_ne!(before, destination); for wrong in [None, Some(Id::new_v4())] { send(&mut app, Message::SourceViewport(wrong)); assert_eq!(app.source_editor.cursor(), before); assert_eq!(app.source_reveal_ticket(), Some(ticket)); } app.source_symbols .raw_interaction .fetch_add(1, Ordering::SeqCst); send(&mut app, Message::SourceViewport(Some(ticket))); assert_eq!( app.source_editor.cursor(), before, "raw input arrived before App event dispatch" ); app.queue_source_reveal(destination, false); let fresh = app.source_reveal_ticket().unwrap(); assert_ne!(ticket, fresh); send(&mut app, Message::SourceViewport(Some(ticket))); assert_eq!(app.source_editor.cursor(), before); send(&mut app, Message::SourceViewport(Some(fresh))); assert_eq!(app.source_editor.cursor(), destination); assert!(app.source_reveal_ticket().is_none() && app.source_jump.is_none()); } #[test] fn escape_hides_results_and_cancels_navigation_but_retains_latest_display() { for pending in [false, true] { let mut app = fixture(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); let request = begin(&mut app, query::Action::References); let ready = completion(&app, request.clone()); let expected = ready.result.as_ref().unwrap().1.clone(); if !pending { send(&mut app, Message::SourceSymbolsReady(ready.clone())); } let results = app.source_symbols.results.clone(); app.source_navigation.open = true; let visit = app.text_workspace.drawer_visit(); send(&mut app, Message::Escape); assert!(references_visible(&app) && !app.source_navigation.open); assert_eq!(app.text_workspace.drawer_visit(), visit); match (&results, &app.source_symbols.results) { (Some(before), Some(after)) => assert!(Arc::ptr_eq(before, after)), (None, None) => {} _ => panic!("Find Escape must retain underlying results"), } assert!(app.source_symbols.request.is_none()); reveal(&mut app); send(&mut app, Message::Escape); assert!(app.text_workspace.drawer().is_none() && !app.source_navigation.open); assert_eq!(app.source_symbols.results.is_some(), !pending); assert!(app.source_symbols.request.is_none()); // Explicit Escape may restore editor focus; consume that ticket before // delivering completion so any later focus request is discriminated. reveal(&mut app); if pending { assert_eq!(app.source_symbols.in_flight, Some(request.job)); send(&mut app, Message::SourceSymbolsReady(ready)); } assert!(app.source_symbols.in_flight.is_none()); assert!(app.text_workspace.drawer().is_none()); assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &expected )); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert_eq!(app.source_workspace.active, USES); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert!(app.source_symbols.back.is_empty()); assert!(!app.document.can_undo() && !app.document.can_redo()); } } #[test] fn escape_dismisses_modal_before_underlying_source_results() { let mut app = fixture(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let results = finish(&mut app, query::Action::References); app.hosted.visible = true; send(&mut app, Message::Escape); assert!(!app.hosted.visible); assert!( app.source_reveal_ticket().is_none(), "modal dismissal must not focus the editor" ); assert!(references_visible(&app)); assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &results )); send(&mut app, Message::Escape); assert!(app.text_workspace.drawer().is_none()); assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &results )); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); } #[test] fn closed_results_stay_closed_on_exact_back_and_reopen_only_for_a_return_notice() { for changed in [false, true] { let mut app = fixture(); let accepted = app.document.snapshot(); let origin = app.source_editor.cursor(); finish(&mut app, query::Action::Definition); reveal(&mut app); close_drawer(&mut app); assert!(app.text_workspace.drawer().is_none() && app.source_symbols.results.is_some()); if changed { // Use the workspace edit API to give the origin a new generation. show(&mut app, USES); let text = app.source_workspace.text().to_owned(); send( &mut app, Message::SourceEdit(text_editor::Action::SelectAll), ); send( &mut app, Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste( Arc::new(format!("{text}// current cursor\r\n")), ))), ); show(&mut app, DEFINITIONS); } let raw = app.source_workspace.bundle(); symbol(&mut app, nav::SymbolEvent::Back); assert_eq!(app.source_workspace.active, USES); assert_eq!(references_visible(&app), changed); assert_eq!(app.source_symbols.notice.is_some(), changed); reveal(&mut app); if !changed { assert_eq!(app.source_editor.cursor(), origin); } assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert!(!app.document.can_undo() && !app.document.can_redo()); } let mut app = fixture(); symbol(&mut app, nav::SymbolEvent::Back); assert!(references_visible(&app) && app.source_symbols.notice.is_some()); } #[test] fn find_only_captured_escape_closes_once_and_preserves_raw_query_and_selection() { let mut app = fixture(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); app.source_navigation.open = true; app.source_navigation.query = "送水 🚀".into(); assert!(!references_visible(&app)); let mut messages = captured_input_subscription_messages(&app); assert_eq!(messages.len(), 1); assert!(matches!(messages[0], Message::Escape)); send(&mut app, messages.remove(0)); assert!(!app.source_navigation.open && !references_visible(&app)); assert_eq!(app.source_navigation.query, "送水 🚀"); assert!(app.source_reveal_ticket().is_some()); reveal(&mut app); assert_eq!(app.source_editor.cursor(), cursor); assert!(captured_input_subscription_messages(&app).is_empty()); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert!(!app.document.can_undo() && !app.document.can_redo()); } #[test] fn old_find_close_cannot_cancel_a_new_file_session_or_reopened_find_intent() { for replacement in 0..3 { let mut app = fixture(); if replacement == 0 { show(&mut app, "project.arcadia"); } send(&mut app, Message::SourceNavigation(nav::Event::Open(false))); let old_context = app.source_symbol_context(); let old_instance = app.source_find_instance; let old_close = app.source_find_close_message(); match replacement { 0 => { let project = app.project().clone(); app.reset(project, None); app.page = Page::Text; assert_eq!(app.source_workspace.active, old_context.path); assert_ne!(app.source_workspace.session(), old_context.session); } 1 => { show(&mut app, DEFINITIONS); assert_eq!(app.source_workspace.session(), old_context.session); assert_ne!(app.source_workspace.active, old_context.path); } 2 => { send(&mut app, old_close.clone()); assert!(!app.source_navigation.open); assert!(app.source_reveal_ticket().is_some()); assert_eq!(app.source_symbol_context(), old_context); } _ => unreachable!(), } send(&mut app, Message::SourceNavigation(nav::Event::Open(false))); assert_ne!(app.source_find_instance, old_instance); send( &mut app, Message::SourceNavigation(nav::Event::Query("Current Find 雨".into())), ); let text = app.source_workspace.text(); let (offset, len) = match replacement { 0 => (text.find("project model").unwrap() + "project ".len(), 5), 1 => ( text.find("oa_activity pump").unwrap() + "oa_activity ".len(), 4, ), _ => (text.find("source @pump").unwrap() + "source @".len(), 4), }; let cursor = selection(text, offset, offset + len, true); app.source_editor.move_to(cursor); let request = begin(&mut app, query::Action::References); let ready = completion(&app, request); app.queue_source_reveal(cursor, false); let find_instance = app.source_find_instance; let mut stale = vec![old_close, Message::SourceNavigation(nav::Event::Close)]; if replacement != 2 { // Independently require the session/path guard, even with a current // invocation token. A token check alone is not the location contract. stale.push(Message::SourceFindClose(old_context, find_instance)); } assert_stale_envelopes_preserve_current(&mut app, &stale); assert!(app.source_navigation.open); assert_eq!(app.source_navigation.query, "Current Find 雨"); assert_eq!(app.source_find_instance, find_instance); send(&mut app, Message::SourceSymbolsReady(ready)); assert_stale_envelopes_preserve_current(&mut app, &stale); assert!(app.source_navigation.open); assert_eq!(app.source_navigation.query, "Current Find 雨"); let current_close = app.source_find_close_message(); send(&mut app, current_close); assert!(!app.source_navigation.open); assert_eq!(app.source_navigation.query, "Current Find 雨"); assert!(app.source_reveal_ticket().is_some()); reveal(&mut app); assert_eq!(app.source_editor.cursor(), cursor); } } #[tokio::test] async fn find_close_releases_removed_input_and_guarded_focus_respects_later_input() { use iced::advanced::{Layout, layout, widget}; use iced::futures::StreamExt; use iced::widget::{column, text_input}; #[derive(Default)] struct Focus(Vec<(Option, bool)>); impl widget::Operation for Focus { fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) { visit(self); } fn focusable( &mut self, id: Option<&widget::Id>, _: iced::Rectangle, state: &mut dyn widget::operation::Focusable, ) { self.0.push((id.cloned(), state.is_focused())); } } fn view(app: &App, later: bool) -> iced::Element<'static, Message, iced::Theme, ()> { let mut content = column![ text_input("Editor", "unchanged source") .id(app.source_editor_id()) .on_input(|s| Message::SourceNavigation(nav::Event::Query(s))) ]; if app.source_navigation.open { content = content.push( text_input("Find", "送水") .id("source-find") .on_input(|s| Message::SourceNavigation(nav::Event::Query(s))), ); } if later { content = content.push( text_input("Other", "preserve focus") .id("later-input") .on_input(|s| Message::SourceNavigation(nav::Event::Query(s))), ); } content.into() } fn run( ui: &mut iced::Element<'_, Message, iced::Theme, ()>, tree: &mut widget::Tree, node: &layout::Node, mut op: Box, ) { loop { ui.as_widget_mut() .operate(tree, Layout::new(node), &(), op.as_mut()); match op.finish() { widget::operation::Outcome::Chain(next) => op = next, widget::operation::Outcome::None => break, widget::operation::Outcome::Some(()) => panic!("focus must not produce a message"), } } } for escape in [false, true] { for later in 0..3 { let mut app = fixture(); app.source_navigation.open = true; let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); let mut ui = view(&app, false); let mut tree = widget::Tree::new(ui.as_widget()); let limits = layout::Limits::new(iced::Size::ZERO, iced::Size::new(500., 250.)); let node = ui.as_widget_mut().layout(&mut tree, &(), &limits); run( &mut ui, &mut tree, &node, Box::new(widget::operation::focusable::focus(widget::Id::new( "source-find", ))), ); let mut focus = Focus::default(); ui.as_widget_mut() .operate(&mut tree, Layout::new(&node), &(), &mut focus); assert!( focus .0 .contains(&(Some(widget::Id::new("source-find")), true)) ); let close = if escape { Message::Escape } else { app.source_find_close_message() }; send(&mut app, close); assert!(!app.source_navigation.open); let ticket = app .source_reveal_ticket() .expect("closing an open Find queues focus"); ui = view(&app, later != 0); tree.diff(ui.as_widget()); let node = ui.as_widget_mut().layout(&mut tree, &(), &limits); let task = app.update(Message::SourceViewport(Some(ticket))); if later != 0 { run( &mut ui, &mut tree, &node, Box::new(widget::operation::focusable::focus(widget::Id::new( "later-input", ))), ); } if later == 2 { app.source_symbols .raw_interaction .fetch_add(1, Ordering::SeqCst); } let mut actions = iced_runtime::task::into_stream(task).expect("guarded widget operation"); while let Some(action) = actions.next().await { let iced_runtime::Action::Widget(op) = action else { panic!("focus-only task"); }; run(&mut ui, &mut tree, &node, op); } let mut focus = Focus::default(); ui.as_widget_mut() .operate(&mut tree, Layout::new(&node), &(), &mut focus); assert!( !focus .0 .iter() .any(|(id, _)| id.as_ref() == Some(&widget::Id::new("source-find"))) ); assert!( focus .0 .contains(&(Some(app.source_editor_id()), later == 0)) ); if later != 0 { assert!( focus .0 .contains(&(Some(widget::Id::new("later-input")), true)) ); } assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); } } } /// Feed the real App subscription recipes the status produced by a mounted /// TextInput. Poll only immediately available output: no timer wait, worker, /// file dialog or App task is executed by this helper. Require the canvas keyboard /// notification separately, then return every command routed to an Escape owner. fn captured_input_subscription_messages(app: &App) -> Vec { input_subscription_messages(app, true) } fn input_subscription_messages(app: &App, focused: bool) -> Vec { crate::app::escape::tests::input_messages(app, iced::keyboard::key::Named::Escape, focused) } #[test] fn captured_find_and_modal_escape_use_one_actual_delivery_and_preserve_pending_work() { for pending in [false, true] { for modal in 0..4 { let mut app = fixture(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); let request = begin(&mut app, query::Action::References); let ready = completion(&app, request.clone()); if !pending { send(&mut app, Message::SourceSymbolsReady(ready.clone())); } let results = app.source_symbols.results.clone(); let expected = ready.result.as_ref().unwrap().1.clone(); app.source_navigation.open = true; let visit = app.text_workspace.drawer_visit(); match modal { 0 => {} 1 => app.hosted.visible = true, 2 => { app.file_prompt = Some(FilePrompt { operation: FileOperation::Save, path: "unchanged-destination.arcadia".into(), overwrite: false, }) } 3 => app.pending = Some(Pending::Walkthrough), _ => unreachable!(), } let mut messages = captured_input_subscription_messages(&app); assert_eq!(messages.len(), 1, "pending={pending}, modal={modal}"); assert!(matches!(messages[0], Message::Escape)); send(&mut app, messages.remove(0)); if modal != 0 { assert!(!app.hosted.visible && app.file_prompt.is_none() && app.pending.is_none()); assert!(references_visible(&app) && app.source_navigation.open); assert_eq!(app.text_workspace.drawer_visit(), visit); assert!( app.source_reveal_ticket().is_none(), "modal Escape cannot steal editor focus" ); match (&results, &app.source_symbols.results) { (Some(before), Some(after)) => assert!(Arc::ptr_eq(before, after)), (None, None) => {} _ => panic!("modal Escape must retain the underlying results"), } let mut messages = captured_input_subscription_messages(&app); assert_eq!(messages.len(), 1); assert!(matches!(messages[0], Message::Escape)); send(&mut app, messages.remove(0)); } assert!(references_visible(&app) && !app.source_navigation.open); assert_eq!(app.text_workspace.drawer_visit(), visit); match (&results, &app.source_symbols.results) { (Some(before), Some(after)) => assert!(Arc::ptr_eq(before, after)), (None, None) => {} _ => panic!("Find Escape must retain underlying results"), } assert_eq!(app.source_symbols.results.is_some(), !pending); assert!(app.source_reveal_ticket().is_some()); reveal(&mut app); assert_eq!(app.source_editor.cursor(), cursor); // One more actual Escape hides the remaining drawer. Both status // routes emit exactly one App message; no second bridge is needed. let mut messages = input_subscription_messages(&app, false); assert_eq!(messages.len(), 1); assert!(matches!(messages[0], Message::Escape)); send(&mut app, messages.remove(0)); assert!(!references_visible(&app) && !app.source_navigation.open); assert!(app.source_symbols.request.is_none()); reveal(&mut app); if pending { assert_eq!(app.source_symbols.in_flight, Some(request.job)); send(&mut app, Message::SourceSymbolsReady(ready)); } assert!(app.source_symbols.in_flight.is_none()); assert!(app.text_workspace.drawer().is_none()); assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &expected )); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert_eq!(app.source_workspace.active, USES); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert!(!app.document.can_undo() && !app.document.can_redo()); assert!( captured_input_subscription_messages(&app).is_empty(), "closed panel opts out" ); } } } #[test] fn captured_delivery_does_not_duplicate_existing_behavior_or_architecture_routes() { let mut app = fixture(); app.text_workspace.show_drawer(Some(DrawerTab::References)); let project = app.project().clone(); assert!( app.behavior .creation .update(&project, crate::behavior::creation::Event::Open(Layer::Oa)) .unwrap() .is_none() ); assert!(app.behavior.creation.has_pending()); let messages = captured_input_subscription_messages(&app); assert_eq!( messages.len(), 1, "behavior and symbols share the same bridge" ); assert!(matches!(messages[0], Message::Escape)); let mut app = App::with_project(Project::empty()); send(&mut app, Message::AddDiagram); send( &mut app, Message::Architecture(crate::app::architecture_authoring::Event::Start( Kind::Actor, Some(oa_core::Position { x: 120., y: 160. }), )), ); assert!(app.architecture.as_ref().unwrap().inline()); // A retained Text panel must not add a second route to inline architecture. app.page = Page::Text; app.text_workspace.show_drawer(Some(DrawerTab::References)); let messages = captured_input_subscription_messages(&app); assert_eq!(messages.len(), 1); assert!(matches!( messages[0], Message::Architecture(crate::app::architecture_authoring::Event::CapturedEscape) )); app.architecture = None; app.page = Page::Behavior; assert!( captured_input_subscription_messages(&app).is_empty(), "off-page symbols opt out" ); } #[test] fn saturated_raw_clock_never_authorizes_completion_or_reveal() { let mut app = fixture(); app.source_symbols .raw_interaction .store(u64::MAX, Ordering::SeqCst); symbol(&mut app, nav::SymbolEvent::Query(query::Action::Definition)); if let Some(request) = app.source_symbols.request.clone() { let ready = completion(&app, request); send(&mut app, Message::SourceSymbolsReady(ready)); } assert!(app.source_symbols.results.is_none()); assert_eq!(app.source_workspace.active, USES); let before = app.source_editor.cursor(); let cursor = text_editor::Cursor { position: text_editor::Position { line: 0, column: 0 }, selection: None, }; app.queue_source_reveal(cursor, false); reveal(&mut app); assert_eq!(app.source_editor.cursor(), before); } #[test] fn back_restores_exact_reversed_utf8_selection_and_crlf_source() { let mut app = fixture(); let origin = app.source_editor.cursor(); assert!( origin.position.column > origin.selection.unwrap().column, "reverse selection fixture" ); let raw = app.source_workspace.bundle(); assert!(raw.files[USES].contains("\r\n")); finish(&mut app, query::Action::Definition); reveal(&mut app); symbol(&mut app, nav::SymbolEvent::Back); assert_eq!(app.source_workspace.active, USES); assert_eq!(app.source_jump, Some(origin)); reveal(&mut app); assert_eq!(app.source_editor.cursor(), origin); assert_eq!(app.source_workspace.bundle(), raw); assert!(app.source_symbols.back.is_empty()); } #[test] fn back_after_origin_edit_uses_its_current_cursor_instead_of_the_old_range() { let mut app = fixture(); let old_origin = app.source_editor.cursor(); finish(&mut app, query::Action::Definition); reveal(&mut app); show(&mut app, USES); send( &mut app, Message::SourceEdit(text_editor::Action::SelectAll), ); send( &mut app, Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste( Arc::new("// changed\n@pump\n".into()), ))), ); let current = text_editor::Cursor { position: text_editor::Position { line: 1, column: 5 }, selection: Some(text_editor::Position { line: 1, column: 1 }), }; app.source_editor.move_to(current); show(&mut app, DEFINITIONS); assert_ne!(old_origin, current); let changed = app.source_workspace.bundle(); symbol(&mut app, nav::SymbolEvent::Back); assert_eq!(app.source_jump, Some(current)); reveal(&mut app); assert_eq!(app.source_editor.cursor(), current); assert_eq!(app.source_workspace.bundle(), changed); assert!( app.source_symbols .notice .as_deref() .is_some_and(|notice| notice.contains("changed")) ); } #[test] fn reset_retains_worker_slot_until_old_completion_then_admits_new_session_query() { let mut app = fixture(); let request = begin(&mut app, query::Action::Definition); let ready = completion(&app, request.clone()); let old_session = app.source_workspace.session(); let project = app.project().clone(); app.reset(project, None); assert_ne!(app.source_workspace.session(), old_session); assert_eq!(app.source_symbols.in_flight, Some(request.job)); assert!(app.source_symbols.request.is_none()); app.page = Page::Text; let text = app.source_workspace.text(); let alias = text.find("project model").unwrap() + "project ".len(); app.source_editor .move_to(selection(text, alias, alias + 5, false)); symbol(&mut app, nav::SymbolEvent::Query(query::Action::Definition)); assert_eq!(app.source_symbols.in_flight, Some(request.job)); assert!( app.source_symbols.request.is_none(), "reset must not create a second active worker" ); send(&mut app, Message::SourceSymbolsReady(ready)); assert!(app.source_symbols.in_flight.is_none()); assert!(app.source_symbols.results.is_none()); let fresh = begin(&mut app, query::Action::Definition); assert_eq!( fresh.stamp.snapshot.workspace, app.source_workspace.session() ); assert_ne!(fresh.stamp.snapshot.workspace, old_session); let ready = completion(&app, fresh); send(&mut app, Message::SourceSymbolsReady(ready)); assert!(app.source_symbols.results.is_some()); } #[test] fn back_keeps_the_latest_64_actual_locations_without_reusing_dropped_history() { let mut app = fixture(); let text: String = (0..80).map(|line| format!("// 行 {line}\r\n")).collect(); let loaded = SourceBundle { entry: "trail.arcadia".into(), files: BTreeMap::from([("trail.arcadia".into(), text)]), }; app.source_workspace = Workspace::new(&loaded, false); app.show_source_file(false); let cursor = |line| text_editor::Cursor { position: text_editor::Position { line, column: 0 }, selection: Some(text_editor::Position { line, column: 1 }), }; app.source_editor.move_to(cursor(0)); for line in 1..=66 { app.source_symbol_jump( "trail.arcadia", query::Position { line, byte_column: 0, }, query::Position { line, byte_column: 1, }, ); reveal(&mut app); assert_eq!(app.source_editor.cursor(), cursor(line)); } assert_eq!(app.source_symbols.back.len(), 64); assert_eq!(app.source_symbols.back.first().unwrap().cursor, cursor(2)); for line in (2..=65).rev() { symbol(&mut app, nav::SymbolEvent::Back); reveal(&mut app); assert_eq!(app.source_editor.cursor(), cursor(line)); } assert!(app.source_symbols.back.is_empty()); symbol(&mut app, nav::SymbolEvent::Back); assert_eq!(app.source_editor.cursor(), cursor(2)); assert_eq!(app.source_workspace.bundle(), loaded); } /// Replay frozen widget messages without rebuilding their context. In a real /// Iced batch these can follow a reset or file switch before the next view diff. fn assert_stale_envelopes_preserve_current(app: &mut App, messages: &[Message]) { let interaction = app.source_symbols.interaction; let raw_interaction = app.source_symbols.raw_interaction.load(Ordering::SeqCst); let request = app.source_symbols.request.as_ref().map(|request| { ( request.job, request.interaction, request.raw_interaction, request.stamp, request.path.clone(), request.offset, request.cursor, request.action, ) }); let in_flight = app.source_symbols.in_flight; let serial = app.source_symbols.serial; let reveal = app.source_symbols.reveal.as_ref().map(|reveal| { ( reveal.ticket, reveal.interaction, reveal.raw_interaction, reveal.snapshot, reveal.path.clone(), ) }); let jump = (app.source_jump, app.source_select_line); let back: Vec<_> = app .source_symbols .back .iter() .map(|entry| { ( entry.session, entry.path.clone(), entry.generation, entry.cursor, ) }) .collect(); let results = app.source_symbols.results.clone(); let cache = app.source_symbols.cache.clone(); let panel = ( app.text_workspace.drawer(), app.source_symbols.notice.clone(), ); let display_request = app.source_symbols.display_request.clone(); let reference_presentation = ( app.text_workspace.references().stamp(), app.text_workspace.drawer_visit(), app.text_workspace.references().scroll_y(), ); let key = app.source_key(); let active = app.source_workspace.active.clone(); let cursor = app.source_editor.cursor(); let editor = app.source_editor.text(); let raw = app.source_workspace.bundle(); let accepted = app.document.snapshot(); let history = ( app.document.revision(), app.document.can_undo(), app.document.can_redo(), app.document.dirty(), ); for message in messages.iter().cloned() { send(app, message); assert_eq!( app.source_symbols.interaction, interaction, "reject stale context before invalidating current intent" ); assert_eq!( app.source_symbols.raw_interaction.load(Ordering::SeqCst), raw_interaction ); assert_eq!( app.source_symbols.request.as_ref().map(|request| ( request.job, request.interaction, request.raw_interaction, request.stamp, request.path.clone(), request.offset, request.cursor, request.action, )), request ); assert_eq!( (app.source_symbols.in_flight, app.source_symbols.serial), (in_flight, serial) ); assert_eq!( app.source_symbols.reveal.as_ref().map(|reveal| ( reveal.ticket, reveal.interaction, reveal.raw_interaction, reveal.snapshot, reveal.path.clone(), )), reveal ); assert_eq!((app.source_jump, app.source_select_line), jump); assert_eq!( app.source_symbols .back .iter() .map(|entry| ( entry.session, entry.path.clone(), entry.generation, entry.cursor, )) .collect::>(), back ); assert_eq!( ( app.text_workspace.drawer(), app.source_symbols.notice.clone() ), panel ); assert_eq!(app.source_symbols.display_request, display_request); assert_eq!( ( app.text_workspace.references().stamp(), app.text_workspace.drawer_visit(), app.text_workspace.references().scroll_y() ), reference_presentation ); match (&results, &app.source_symbols.results) { (Some(before), Some(after)) => assert!(Arc::ptr_eq(before, after)), (None, None) => {} _ => panic!("stale envelope replaced or cleared current results"), } match (&cache, &app.source_symbols.cache) { (Some(before), Some(after)) => assert!(Arc::ptr_eq(before, after)), (None, None) => {} _ => panic!("stale envelope replaced or cleared current snapshot"), } assert_eq!(app.source_key(), key); assert_eq!(app.source_workspace.active, active); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_editor.text(), editor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert_eq!(app.project(), &accepted.decoded.project); assert_eq!(app.document.bundle(), &accepted.bundle); assert_eq!( ( app.document.revision(), app.document.can_undo(), app.document.can_redo(), app.document.dirty() ), history ); } } fn stale_envelope_trial(reset: bool) { let mut app = fixture(); if reset { show(&mut app, "project.arcadia"); let text = app.source_workspace.text(); let alias = text.find("project model").unwrap() + "project ".len(); app.source_editor .move_to(selection(text, alias, alias + 5, false)); } let old_results = finish(&mut app, query::Action::References); let old_context = app.source_symbol_context(); let old_messages = [ app.source_symbol_message(nav::SymbolEvent::Query(query::Action::Definition)), app.source_symbol_message(nav::SymbolEvent::Back), Message::TextWorkspace(TextEvent::HideDrawer(app.text_drawer_context())), choice(&app, old_results.stamp, false, 0), ]; if reset { let project = app.project().clone(); app.reset(project, None); app.page = Page::Text; let new_context = app.source_symbol_context(); assert_eq!( new_context.path, old_context.path, "same path isolates the session guard" ); assert_ne!(new_context.session, old_context.session); } else { show(&mut app, DEFINITIONS); let new_context = app.source_symbol_context(); assert_ne!(new_context.path, old_context.path); assert_eq!( new_context.session, old_context.session, "file switch isolates the path guard" ); assert_eq!(app.source_key(), old_results.stamp.snapshot); } let text = app.source_workspace.text(); let (alias, length) = if reset { (text.find("project model").unwrap() + "project ".len(), 5) } else { ( text.find("oa_activity pump").unwrap() + "oa_activity ".len(), 4, ) }; let cursor = selection(text, alias, alias + length, false); app.source_editor.move_to(cursor); // A real jump creates useful current Back history before the next request. let path = app.source_workspace.active.clone(); app.source_symbol_jump( &path, query::Position { line: cursor.position.line, byte_column: cursor.position.column, }, query::Position { line: cursor.selection.unwrap().line, byte_column: cursor.selection.unwrap().column, }, ); reveal(&mut app); let current = begin(&mut app, query::Action::References); let ready = completion(&app, current.clone()); app.queue_source_reveal(cursor, false); assert_stale_envelopes_preserve_current(&mut app, &old_messages); send(&mut app, Message::SourceSymbolsReady(ready)); assert_eq!( app.source_symbols.results.as_ref().unwrap().stamp, current.stamp ); assert!(app.source_symbols.in_flight.is_none()); // Replay once more with no busy-slot fallback and visible current results. assert_stale_envelopes_preserve_current(&mut app, &old_messages); let interaction = app.source_symbols.interaction; close_drawer(&mut app); assert_ne!(app.source_symbols.interaction, interaction); assert!(app.text_workspace.drawer().is_none() && app.source_symbols.results.is_some()); let next = begin(&mut app, query::Action::References); assert_ne!( next.job, current.job, "a freshly wrapped current-context Query remains usable" ); let ready = completion(&app, next); send(&mut app, Message::SourceSymbolsReady(ready)); assert!(app.source_symbols.results.is_some()); } #[test] fn old_symbol_widget_envelopes_after_reset_preserve_new_same_path_intent() { stale_envelope_trial(true); } #[test] fn old_symbol_widget_envelopes_after_file_switch_preserve_same_session_intent() { stale_envelope_trial(false); } #[test] #[ignore = "manual 100-module latency/process-memory trial; run alone with --ignored --nocapture --test-threads=1"] fn source_symbols_100_modules_1000_definitions_10000_uses() { let mut files = BTreeMap::new(); let mut entry = "arcadia 7; project performance \"Current text trial\" {\n".to_owned(); for module in 0..100 { let path = format!("modules/m{module:03}.arcadia"); entry.push_str(&format!(" include \"{path}\";\n")); let mut text = String::new(); for declaration in 0..10 { text.push_str(&format!("oa_process p_{module}_{declaration} \"Process {module}/{declaration} 雨\" {{ steps [")); for reference in 0..10 { if reference != 0 { text.push_str(", "); } text.push_str("@p_0_0"); } text.push_str("]; description \"@p_0_0 is literal documentation\"; }\n"); } files.insert(path, text); } entry.push_str("}\n"); files.insert("project.arcadia".into(), entry); let original = files.clone(); let input_bytes: usize = files.values().map(String::len).sum(); let drafts: Vec<_> = files .iter() .map(|(path, text)| query::DraftFile { path, text }) .collect(); let key = query::SnapshotKey { workspace: Id::new_v4(), revision: 1, }; let memory_before = process_memory(); let started = Instant::now(); let owned = query::admit_owned( query::Input { key, entry: "project.arcadia", files: &drafts, applied: false, validation: None, }, query::Limits::default(), ) .unwrap(); let admitted = started.elapsed(); let started = Instant::now(); let snapshot = owned.capture().unwrap(); let captured = started.elapsed(); let path = "modules/m000.arcadia"; let offset = files[path].find("p_0_0").unwrap(); let started = Instant::now(); let result = snapshot .query(query::Query { stamp: query::QueryStamp { snapshot: key, serial: 1, }, path, byte_offset: offset, action: query::Action::References, }) .unwrap(); let queried = started.elapsed(); let memory_after = process_memory(); assert_eq!(files.len(), 101, "100 modules plus entry"); assert_eq!( files .values() .map(|text| text.matches("oa_process ").count()) .sum::(), 1000 ); assert_eq!(result.total_definitions, 1); assert_eq!(result.total_references, 10_000); assert_eq!(result.references.len() + result.omitted_references, 10_000); assert!( result.definitions.len() + result.references.len() <= query::Limits::default().result_rows ); assert!(result.scope.closure_complete && !result.scope.validated); assert_eq!(files, original); eprintln!( "100-module current-draft trial: bytes={input_bytes}, admission={admitted:?}, capture={captured:?}, references={queried:?}, rows={}, omitted={}, process_memory_before={memory_before:?}, process_memory_after={memory_after:?}", result.references.len(), result.omitted_references ); // This fixture intentionally has process-to-process placeholder steps. It // measures lexical draft navigation, not engineering validity or GUI latency. } #[cfg(not(windows))] fn process_memory() -> Option<(usize, usize)> { None } #[cfg(windows)] fn process_memory() -> Option<(usize, usize)> { #[repr(C)] #[derive(Default)] struct Counters { cb: u32, page_fault_count: u32, peak_working_set: usize, working_set: usize, quota_peak_paged_pool: usize, quota_paged_pool: usize, quota_peak_non_paged_pool: usize, quota_non_paged_pool: usize, pagefile_usage: usize, peak_pagefile_usage: usize, } #[link(name = "kernel32")] unsafe extern "system" { fn GetCurrentProcess() -> *mut std::ffi::c_void; fn K32GetProcessMemoryInfo( process: *mut std::ffi::c_void, counters: *mut Counters, size: u32, ) -> i32; } let size = u32::try_from(std::mem::size_of::()).unwrap(); let mut counters = Counters { cb: size, ..Counters::default() }; // Windows writes one correctly sized PROCESS_MEMORY_COUNTERS for this // process. These are process-wide resident/peak bytes, not exact allocations. let ok = unsafe { K32GetProcessMemoryInfo(GetCurrentProcess(), &mut counters, size) }; (ok != 0).then_some((counters.working_set, counters.peak_working_set)) } #[test] fn delayed_definition_after_hide_or_tab_change_retains_data_without_reopening_or_focus() { for destination in [None, Some(DrawerTab::Problems), Some(DrawerTab::Status)] { let mut app = fixture(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); let request = begin(&mut app, query::Action::Definition); let ready = completion(&app, request.clone()); let expected = ready.result.as_ref().unwrap().1.clone(); assert_eq!(expected.disposition, query::Disposition::Direct); let event = destination.map_or(TextEvent::HideDrawer(app.text_drawer_context()), |tab| { TextEvent::ShowDrawer(app.text_drawer_context(), tab) }); send(&mut app, Message::TextWorkspace(event)); assert_eq!(app.text_workspace.drawer(), destination); assert!(app.source_symbols.request.is_none()); assert_eq!(app.source_symbols.display_request.as_ref(), Some(&request)); assert_eq!(app.source_symbols.in_flight, Some(request.job)); let visit = app.text_workspace.drawer_visit(); send(&mut app, Message::SourceSymbolsReady(ready)); assert!(app.source_symbols.in_flight.is_none()); assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &expected )); assert_eq!(app.text_workspace.drawer(), destination); assert_eq!(app.text_workspace.drawer_visit(), visit); assert_eq!(app.text_workspace.references().stamp(), Some(request.stamp)); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert!(app.source_symbols.back.is_empty()); assert_eq!(app.source_workspace.active, USES); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert!(!app.document.can_undo() && !app.document.can_redo()); { let captured = Message::TextWorkspace(TextEvent::ShowDrawer( app.text_drawer_context(), DrawerTab::References, )); send(&mut app, captured); }; assert!(references_visible(&app)); assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &expected )); assert!(app.source_symbols.in_flight.is_none() && app.source_symbols.request.is_none()); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert_eq!(app.source_editor.cursor(), cursor); } } #[test] fn same_worker_with_changed_request_envelope_cannot_publish_or_navigate() { for changed in 0..7 { let mut app = fixture(); let request = begin(&mut app, query::Action::Definition); let mut ready = completion(&app, request.clone()); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); match changed { 0 => ready.request.stamp.serial += 1, 1 => ready.request.path = DEFINITIONS.into(), 2 => ready.request.offset += 1, 3 => ready.request.cursor.selection = None, 4 => ready.request.interaction = Id::new_v4(), 5 => ready.request.raw_interaction += 1, _ => ready.request.action = query::Action::References, } assert_eq!(ready.request.job, request.job); send(&mut app, Message::SourceSymbolsReady(ready)); assert!( app.source_symbols.in_flight.is_none(), "worker completed in case {changed}" ); assert_eq!(app.source_symbols.display_request.as_ref(), Some(&request)); assert!(app.source_symbols.results.is_none()); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert!(app.source_symbols.back.is_empty()); assert_eq!(app.source_workspace.active, USES); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); } } #[test] fn matching_old_worker_cannot_replace_a_newer_display_owner() { let mut app = fixture(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); let old = begin(&mut app, query::Action::Definition); let ready = completion(&app, old.clone()); let mut latest = old.clone(); latest.stamp.serial += 1; latest.job = Id::new_v4(); // Isolate display ownership from both worker ownership and navigation intent. app.source_interaction(); app.source_symbols.display_request = Some(latest.clone()); send(&mut app, Message::SourceSymbolsReady(ready)); assert!(app.source_symbols.in_flight.is_none()); assert_eq!(app.source_symbols.display_request.as_ref(), Some(&latest)); assert!(app.source_symbols.results.is_none()); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert!(app.source_symbols.back.is_empty()); assert_eq!(app.source_workspace.active, USES); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert_eq!(app.document.revision(), 0); assert!(!app.document.can_undo() && !app.document.can_redo()); } #[test] fn hidden_completion_checks_exact_bytes_even_when_snapshot_key_was_not_advanced() { let mut app = fixture(); let request = begin(&mut app, query::Action::Definition); let ready = completion(&app, request.clone()); let accepted = app.document.snapshot(); { let captured = Message::TextWorkspace(TextEvent::HideDrawer(app.text_drawer_context())); send(&mut app, captured); }; // Deliberately violate the normal edit API's key advancement: the exact // source witness must still reject stale bytes instead of trusting the key. app.source_workspace .files .get_mut(USES) .unwrap() .text .push_str("\r\n// changed bytes"); assert_eq!(app.source_key(), request.stamp.snapshot); let changed = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); send(&mut app, Message::SourceSymbolsReady(ready)); assert!(app.source_symbols.in_flight.is_none()); assert!(app.source_symbols.results.is_none()); assert!(app.text_workspace.drawer().is_none()); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert!(app.source_symbols.back.is_empty()); assert_eq!(app.source_workspace.bundle(), changed); assert_eq!(app.source_editor.cursor(), cursor); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); } #[test] fn popup_escape_has_no_duplicate_bridge_and_dismissal_preserves_underlying_find_and_drawer() { for line in [false, true] { for captured_find_escape in [false, true] { let mut app = fixture(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let results = finish(&mut app, query::Action::References); let selected = select_effective_pump(&mut app, line); let cursor = app.source_editor.cursor(); let group = TextEvent::ToggleFile { stamp: results.stamp, visit: app.text_workspace.drawer_visit(), path: USES.into(), }; send(&mut app, Message::TextWorkspace(group)); let visit = app.text_workspace.drawer_visit(); send( &mut app, Message::TextWorkspace(TextEvent::ReferenceScrolled { stamp: results.stamp, visit, y: 96., }), ); assert!(app.text_workspace.references().collapsed(USES)); assert_eq!(app.text_workspace.references().scroll_y(), 96.); send(&mut app, Message::SourceNavigation(nav::Event::Open(false))); app.source_navigation.query = "Retain Find 雨".into(); let open = app.text_menu_message(iced::Point::new(30., 30.)); send(&mut app, open); let menu = app.text_workspace_menu().expect("Text popup opened"); assert!( captured_input_subscription_messages(&app).is_empty(), "Popup owns captured Escape; a bridge Escape would also close its underlying UI" ); // The popup itself publishes this tokened dismissal on Escape. send(&mut app, menu.on_close); assert!(app.text_workspace.menu().is_none()); assert!(references_visible(&app) && app.source_navigation.open); assert_eq!(app.text_workspace.drawer_visit(), visit); assert!(app.source_symbols.reveal.is_none() && app.source_jump.is_none()); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!( app.source_editor.selection().as_deref(), Some(selected.as_str()) ); let mut next = input_subscription_messages(&app, captured_find_escape); assert_eq!( next.len(), 1, "exactly one message for either Escape status" ); assert!(matches!(next[0], Message::Escape)); send(&mut app, next.remove(0)); assert!(references_visible(&app) && !app.source_navigation.open); assert_eq!( app.text_workspace.drawer_visit(), visit, "closing Find does not replace the drawer visit" ); assert_eq!(app.source_navigation.query, "Retain Find 雨"); assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &results )); assert!(app.text_workspace.references().collapsed(USES)); assert_eq!(app.text_workspace.references().scroll_y(), 96.); assert!(app.source_reveal_ticket().is_some() && app.source_jump.is_none()); reveal(&mut app); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!( app.source_editor.selection().as_deref(), Some(selected.as_str()) ); let mut last = input_subscription_messages(&app, false); assert_eq!(last.len(), 1); assert!(matches!(last[0], Message::Escape)); send(&mut app, last.remove(0)); assert!(app.text_workspace.drawer().is_none() && !app.source_navigation.open); assert_ne!(app.text_workspace.drawer_visit(), visit); assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &results )); assert!(app.text_workspace.references().collapsed(USES)); assert_eq!(app.text_workspace.references().scroll_y(), 96.); assert_eq!(app.source_navigation.query, "Retain Find 雨"); reveal(&mut app); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!( app.source_editor.selection().as_deref(), Some(selected.as_str()) ); assert!(captured_input_subscription_messages(&app).is_empty()); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert!(app.source_symbols.back.is_empty()); assert!(!app.document.can_undo() && !app.document.can_redo()); } } } #[test] fn retained_choices_cannot_replay_after_hide_or_references_problems_references() { for reference in [false, true] { for hide in [false, true] { let mut app = fixture(); let results = finish(&mut app, query::Action::References); let index = usize::from(reference); let old = choice(&app, results.stamp, reference, index); let old_visit = app.text_workspace.drawer_visit(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let event = if hide { TextEvent::HideDrawer(app.text_drawer_context()) } else { TextEvent::ShowDrawer(app.text_drawer_context(), DrawerTab::Problems) }; send(&mut app, Message::TextWorkspace(event)); assert_ne!(app.text_workspace.drawer_visit(), old_visit); assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &results )); assert_stale_envelopes_preserve_current(&mut app, std::slice::from_ref(&old)); { let captured = Message::TextWorkspace(TextEvent::ShowDrawer( app.text_drawer_context(), DrawerTab::References, )); send(&mut app, captured); }; assert_ne!(app.text_workspace.drawer_visit(), old_visit); assert_eq!( app.source_key(), results.stamp.snapshot, "same source isolates the visit guard" ); assert_stale_envelopes_preserve_current(&mut app, &[old]); let fresh = choice(&app, results.stamp, reference, index); send(&mut app, fresh); assert!( app.source_symbols.reveal.is_some(), "freshly captured reopened choice remains usable" ); assert_eq!(app.source_symbols.back.len(), 1); reveal(&mut app); let (path, span) = if reference { let row = &results.references[index]; (&row.path, row.span) } else { let row = &results.definitions[index]; (&row.path, row.span) }; assert_eq!(&app.source_workspace.active, path); assert_eq!( app.source_editor.cursor(), selection(&raw.files[path], span.start, span.end, false) ); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); assert_eq!(app.document.revision(), 0); assert!(!app.document.can_undo() && !app.document.can_redo()); } } } #[test] fn combined_modal_completion_releases_worker_without_navigation_or_model_changes() { for modal in 0..5 { let mut app = fixture(); let request = begin(&mut app, query::Action::Definition); let ready = completion(&app, request); let expected = ready.result.as_ref().unwrap().1.clone(); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); match modal { 0 => app.busy = true, 1 => app.hosted.visible = true, 2 => app.pending = Some(Pending::Walkthrough), 3 => { app.file_prompt = Some(FilePrompt { operation: FileOperation::Save, path: "unchanged.arcadia".into(), overwrite: false, }); } 4 => { let project = app.project().clone(); assert!( app.behavior .creation .update(&project, crate::behavior::creation::Event::Open(Layer::Oa)) .unwrap() .is_none() ); assert!(app.behavior.has_pending()); } _ => unreachable!(), } assert!(!app.source_navigation_available()); send(&mut app, Message::SourceSymbolsReady(ready)); assert!(app.source_symbols.in_flight.is_none(), "modal {modal}"); assert!(app.source_symbols.request.is_none()); // Current-source display data may finish behind a modal; it grants no navigation. assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &expected )); assert!(app.source_symbols.back.is_empty()); assert!(app.source_reveal_ticket().is_none()); assert_eq!(app.source_workspace.active, USES); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); } } #[test] fn combined_reference_chooser_blocks_navigation_and_completion_precedes_reconciliation() { let mut project = Project::empty(); let record = project .add_element(Kind::DataType, Layer::Oa, "Record", None) .unwrap(); let Some(SharedBody::DataType { definition, .. }) = project.elements.get_mut(&record).unwrap().shared_mut() else { panic!("record datatype fixture"); }; *definition = DataTypeDefinition::Record { supertype: None }; let mut app = App::with_project(project); send(&mut app, Message::Select(record)); let path = "/body/Oa/Shared/DataType/definition/Record/supertype"; let open = app.draft.as_ref().unwrap().reference_open( app.project(), path, iced::Rectangle { x: 100., y: 100., width: 240., height: 32., }, ); let alias = app.project().elements[&record].alias.clone(); let source_path = app .source_workspace .files .iter() .find(|(_, buffer)| buffer.text.contains(&format!(" {alias} "))) .map(|(path, _)| path.clone()) .unwrap(); app.page = Page::Text; show(&mut app, &source_path); let text = app.source_workspace.text(); let offset = text.find(&format!(" {alias} ")).unwrap() + 1; app.source_editor .move_to(selection(text, offset, offset + alias.len(), false)); let request = begin(&mut app, query::Action::Definition); let ready = completion(&app, request); let expected = ready.result.as_ref().unwrap().1.clone(); app.page = Page::Diagram; send(&mut app, Message::FieldReferenceOpen(open)); assert!(app.reference_input_active(), "{}", app.status); // Reproduce a queued completion observing a retained chooser before normal // reconciliation catches the changed page. Neither may authorize navigation. app.page = Page::Text; assert!(!app.source_navigation_available()); let accepted = app.document.snapshot(); let raw = app.source_workspace.bundle(); let cursor = app.source_editor.cursor(); send(&mut app, Message::SourceSymbolsReady(ready)); assert!(app.source_symbols.in_flight.is_none()); // Current-source display data may finish behind a modal; it grants no navigation. assert!(Arc::ptr_eq( app.source_symbols.results.as_ref().unwrap(), &expected )); assert!(app.source_reveal_ticket().is_none()); assert!( app.reference_input_active(), "internal completion must precede chooser reconciliation and modal dispatch" ); assert_eq!(app.source_editor.cursor(), cursor); assert_eq!(app.source_workspace.bundle(), raw); assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); } #[test] fn same_key_changed_bytes_mark_retained_rows_stale_before_choice_boundaries() { for path in [USES, DEFINITIONS] { let mut app = fixture(); let results = finish(&mut app, query::Action::References); assert!(app.source_results_current()); #[cfg(debug_assertions)] assert!(!crate::views::captured_source_reference_stale_summary(&app)); let captured = choice(&app, results.stamp, true, 0); let key = app.source_key(); // Isolate byte authority from revision authority in both the displayed // and an inactive buffer. Views share this exact currentness decision. app.source_workspace .files .get_mut(path) .unwrap() .text .push_str("\r\n// exact bytes changed"); assert_eq!(app.source_key(), key); assert!(!app.source_results_current()); #[cfg(debug_assertions)] assert!(crate::views::captured_source_reference_stale_summary(&app)); app.queue_source_reveal(app.source_editor.cursor(), false); assert!(app.source_reveal_ticket().is_some()); assert_stale_envelopes_preserve_current(&mut app, &[captured]); } }