Files
ARCADIA-Studio/crates/oa-desktop/src/app/source_symbols/tests.rs
T

1614 lines
61 KiB
Rust

use super::*;
use crate::source_workspace::Workspace;
use oa_core::source_project::SourceBundle;
use std::collections::BTreeMap;
use std::time::Instant;
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.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 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 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));
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<query::Results> {
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();
symbol(
&mut app,
nav::SymbolEvent::Choose {
stamp: uses.stamp,
reference: true,
index: 1,
},
);
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
);
symbol(
&mut app,
nav::SymbolEvent::Choose {
stamp: results.stamp,
reference: false,
index,
},
);
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();
symbol(
&mut app,
nav::SymbolEvent::Choose {
stamp: results.stamp,
reference: false,
index,
},
);
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_rejects_file_edit_session_cursor_interaction_and_raw_input_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 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();
send(&mut app, Message::SourceSymbolsReady(ready));
assert!(
app.source_symbols.in_flight.is_none(),
"case {change}: release worker slot"
);
assert!(
app.source_symbols.results.is_none(),
"case {change}: stale result surfaced"
);
assert!(app.source_symbols.back.is_empty());
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()));
}
}
#[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_hides_panel_and_completion_does_not_reopen_it() {
let mut app = fixture();
let request = begin(&mut app, query::Action::Definition);
let ready = completion(&app, request.clone());
assert!(app.source_symbols.open);
symbol(&mut app, nav::SymbolEvent::Close);
assert!(!app.source_symbols.open);
assert_eq!(app.source_symbols.in_flight, Some(request.job));
assert!(app.source_symbols.request.is_none());
send(&mut app, Message::SourceSymbolsReady(ready));
assert!(!app.source_symbols.open);
assert!(app.source_symbols.in_flight.is_none());
assert!(app.source_symbols.results.is_none());
assert_eq!(app.source_workspace.active, USES);
let next = begin(&mut app, query::Action::References);
assert_ne!(next.job, request.job);
assert!(app.source_symbols.open);
}
#[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_dismisses_results_and_cancels_busy_navigation_without_changing_sources() {
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());
if !pending {
send(&mut app, Message::SourceSymbolsReady(ready.clone()));
assert!(app.source_symbols.results.is_some());
}
app.source_navigation.open = true;
send(&mut app, Message::Escape);
assert!(!app.source_symbols.open && !app.source_navigation.open);
assert!(app.source_symbols.results.is_none());
assert!(app.source_symbols.notice.is_none());
assert!(app.source_symbols.request.is_none());
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.source_symbols.open);
assert!(app.source_symbols.results.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!(app.source_symbols.open);
assert!(Arc::ptr_eq(
app.source_symbols.results.as_ref().unwrap(),
&results
));
send(&mut app, Message::Escape);
assert!(!app.source_symbols.open);
assert!(app.source_symbols.results.is_none());
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);
symbol(&mut app, nav::SymbolEvent::Close);
assert!(!app.source_symbols.open && app.source_symbols.results.is_none());
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!(app.source_symbols.open, 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!(app.source_symbols.open && 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!(!app.source_symbols.open);
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 && !app.source_symbols.open);
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<widget::Id>, 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<dyn widget::Operation>,
) {
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.
fn captured_input_subscription_messages(app: &App) -> Vec<Message> {
use iced::advanced::{Layout, Shell, layout, subscription, widget};
use iced::futures::{StreamExt, stream, task::noop_waker_ref};
use iced::keyboard::{self, key::Named};
use std::task::{Context, Poll};
let mut input: iced::Element<'_, (), iced::Theme, ()> =
iced::widget::text_input("Find / modal input", "Retain exact text 雨")
.on_input(|_| ())
.into();
let mut tree = widget::Tree::new(input.as_widget());
let size = iced::Size::new(300., 40.);
let node =
input
.as_widget_mut()
.layout(&mut tree, &(), &layout::Limits::new(iced::Size::ZERO, size));
tree.state
.downcast_mut::<iced::widget::text_input::State<()>>()
.focus();
let event = iced::Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Named(Named::Escape),
modified_key: keyboard::Key::Named(Named::Escape),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Escape),
location: keyboard::Location::Standard,
modifiers: keyboard::Modifiers::empty(),
text: None,
repeat: false,
});
let mut input_messages = Vec::new();
let mut shell = Shell::new(&mut input_messages);
input.as_widget_mut().update(
&mut tree,
&event,
Layout::new(&node),
iced::mouse::Cursor::Unavailable,
&(),
&mut iced::advanced::clipboard::Null,
&mut shell,
&iced::Rectangle::new(iced::Point::ORIGIN, size),
);
assert!(shell.is_event_captured());
assert!(input_messages.is_empty());
assert!(
!tree
.state
.downcast_ref::<iced::widget::text_input::State<()>>()
.is_focused()
);
let event = subscription::Event::Interaction {
window: iced::window::Id::unique(),
event,
status: iced::event::Status::Captured,
};
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
.unwrap();
let _entered = runtime.enter();
let mut messages = Vec::new();
for recipe in subscription::into_recipes(app.subscription()) {
let mut output = recipe.stream(Box::pin(stream::iter([event.clone()])));
let mut context = Context::from_waker(noop_waker_ref());
while let Poll::Ready(Some(message)) = output.poll_next_unpin(&mut context) {
assert!(
!matches!(message, Message::CheckpointTick),
"timer must remain pending"
);
messages.push(message);
}
}
messages
}
#[test]
fn captured_find_and_modal_escape_use_one_actual_subscription_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();
app.source_navigation.open = true;
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!(app.source_symbols.open && app.source_navigation.open);
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!(!app.source_symbols.open && !app.source_navigation.open);
assert!(app.source_symbols.results.is_none() && app.source_symbols.request.is_none());
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.source_symbols.open && app.source_symbols.results.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_subscription_does_not_duplicate_existing_behavior_or_architecture_routes() {
let mut app = fixture();
app.source_symbols.open = true;
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.source_symbols.open = true;
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.source_symbols.open, app.source_symbols.notice.clone());
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::<Vec<_>>(),
back
);
assert_eq!(
(app.source_symbols.open, app.source_symbols.notice.clone()),
panel
);
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),
app.source_symbol_message(nav::SymbolEvent::Close),
app.source_symbol_message(nav::SymbolEvent::Choose {
stamp: old_results.stamp,
reference: false,
index: 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;
symbol(&mut app, nav::SymbolEvent::Close);
assert_ne!(app.source_symbols.interaction, interaction);
assert!(!app.source_symbols.open && app.source_symbols.results.is_none());
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::<usize>(),
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::<Counters>()).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 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 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());
assert!(app.source_symbols.results.is_none());
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);
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());
assert!(app.source_symbols.results.is_none());
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()));
}