Add bounded definition and reference navigation for current source drafts
Preserve loaded draft bytes and model history, reject stale workspace and input events, and keep captured Escape under modal priority. Verified 181 focused Windows tests and strict desktop Clippy on exact inputs4; earlier full737 scope and component/native preparation evidence are archived separately. Native navigation acceptance remains pending.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
//! Ephemeral editor navigation; no model commands or filesystem operations.
|
||||
use super::query;
|
||||
use iced::widget::text_editor::{Cursor, Position};
|
||||
use oa_core::Id;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SymbolEvent {
|
||||
Query(query::Action),
|
||||
Back,
|
||||
Close,
|
||||
Choose {
|
||||
stamp: query::QueryStamp,
|
||||
reference: bool,
|
||||
index: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// Identity of the mounted source view that emitted an action. Message queues
|
||||
/// may outlive that view; request-time stamping alone cannot detect replay.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SymbolContext {
|
||||
pub session: Id,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HistoryEntry {
|
||||
pub session: Id,
|
||||
pub path: String,
|
||||
pub generation: u64,
|
||||
pub cursor: Cursor,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Request {
|
||||
pub job: Id,
|
||||
pub interaction: Id,
|
||||
pub raw_interaction: u64,
|
||||
pub stamp: query::QueryStamp,
|
||||
pub path: String,
|
||||
pub offset: usize,
|
||||
pub cursor: Cursor,
|
||||
pub action: query::Action,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Completion {
|
||||
pub request: Request,
|
||||
pub result: Result<(Arc<query::Snapshot>, Arc<query::Results>), String>,
|
||||
}
|
||||
|
||||
pub struct Reveal {
|
||||
pub ticket: Id,
|
||||
pub interaction: Id,
|
||||
pub raw_interaction: u64,
|
||||
pub snapshot: query::SnapshotKey,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
pub struct SymbolState {
|
||||
pub open: bool,
|
||||
pub interaction: Id,
|
||||
pub raw_interaction: Arc<AtomicU64>,
|
||||
pub serial: u64,
|
||||
pub in_flight: Option<Id>,
|
||||
pub request: Option<Request>,
|
||||
pub cache: Option<Arc<query::Snapshot>>,
|
||||
pub results: Option<Arc<query::Results>>,
|
||||
pub notice: Option<String>,
|
||||
pub back: Vec<HistoryEntry>,
|
||||
pub reveal: Option<Reveal>,
|
||||
}
|
||||
|
||||
impl Default for SymbolState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
open: false,
|
||||
interaction: Id::new_v4(),
|
||||
raw_interaction: Arc::new(AtomicU64::new(0)),
|
||||
serial: 0,
|
||||
in_flight: None,
|
||||
request: None,
|
||||
cache: None,
|
||||
results: None,
|
||||
notice: None,
|
||||
back: Vec::new(),
|
||||
reveal: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iced uses zero-based UTF-8 byte columns. Reject stale or split-codepoint
|
||||
/// coordinates; do not normalize CRLF or silently clamp query destinations.
|
||||
pub fn byte_offset(text: &str, position: Position) -> Option<usize> {
|
||||
let start = if position.line == 0 {
|
||||
0
|
||||
} else {
|
||||
text.match_indices('\n').nth(position.line - 1)?.0 + 1
|
||||
};
|
||||
let end = text[start..]
|
||||
.find('\n')
|
||||
.map_or(text.len(), |end| start + end);
|
||||
let offset = start.checked_add(position.column)?;
|
||||
(offset <= end && text.is_char_boundary(offset)).then_some(offset)
|
||||
}
|
||||
|
||||
pub fn cursor_offset(text: &str, cursor: Cursor) -> Option<usize> {
|
||||
let position = cursor.selection.map_or(cursor.position, |selection| {
|
||||
if (selection.line, selection.column) < (cursor.position.line, cursor.position.column) {
|
||||
selection
|
||||
} else {
|
||||
cursor.position
|
||||
}
|
||||
});
|
||||
byte_offset(text, position)
|
||||
}
|
||||
|
||||
pub fn exact_cursor(text: &str, cursor: Cursor) -> bool {
|
||||
byte_offset(text, cursor.position).is_some()
|
||||
&& cursor
|
||||
.selection
|
||||
.is_none_or(|position| byte_offset(text, position).is_some())
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
//! Raw input invalidation and focus checks at widget-operation execution time.
|
||||
use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer, widget};
|
||||
use iced::{Element, Event, Length, Rectangle, Size, Task, Vector, keyboard, mouse, touch};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
/// Exhaustion fails closed: a saturated clock can never authorize navigation.
|
||||
/// Replace the clock with a new workspace/controller, rather than wrapping it.
|
||||
pub fn is_current(clock: &AtomicU64, expected: u64) -> bool {
|
||||
expected != u64::MAX && clock.load(Ordering::Acquire) == expected
|
||||
}
|
||||
|
||||
fn invalidate(clock: &AtomicU64, event: &Event) {
|
||||
if matches!(
|
||||
event,
|
||||
Event::Keyboard(keyboard::Event::KeyPressed { .. })
|
||||
| Event::Mouse(mouse::Event::ButtonPressed(_) | mouse::Event::WheelScrolled { .. })
|
||||
| Event::Touch(touch::Event::FingerPressed { .. })
|
||||
| Event::Window(iced::window::Event::Unfocused)
|
||||
) {
|
||||
let _ = clock.fetch_update(Ordering::AcqRel, Ordering::Acquire, |value| {
|
||||
Some(value.saturating_add(1))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Transparent, including overlays: invalidate before the child can capture input.
|
||||
pub fn observe<'a, Message: 'a, Theme: 'a, Renderer: renderer::Renderer + 'a>(
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
clock: Arc<AtomicU64>,
|
||||
) -> Element<'a, Message, Theme, Renderer> {
|
||||
Element::new(Observed { content, clock })
|
||||
}
|
||||
|
||||
struct Observed<'a, Message, Theme, Renderer> {
|
||||
content: Element<'a, Message, Theme, Renderer>,
|
||||
clock: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl<Message, Theme, Renderer: renderer::Renderer> Widget<Message, Theme, Renderer>
|
||||
for Observed<'_, Message, Theme, Renderer>
|
||||
{
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.content.as_widget().size()
|
||||
}
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
self.content.as_widget().size_hint()
|
||||
}
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
self.content.as_widget().tag()
|
||||
}
|
||||
fn state(&self) -> widget::tree::State {
|
||||
self.content.as_widget().state()
|
||||
}
|
||||
fn children(&self) -> Vec<widget::Tree> {
|
||||
self.content.as_widget().children()
|
||||
}
|
||||
fn diff(&self, tree: &mut widget::Tree) {
|
||||
self.content.as_widget().diff(tree);
|
||||
}
|
||||
fn layout(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
self.content.as_widget_mut().layout(tree, renderer, limits)
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.content
|
||||
.as_widget()
|
||||
.draw(tree, renderer, theme, style, layout, cursor, viewport);
|
||||
}
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(tree, layout, renderer, operation);
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut widget::Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
invalidate(&self.clock, event);
|
||||
self.content.as_widget_mut().update(
|
||||
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||
);
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content
|
||||
.as_widget()
|
||||
.mouse_interaction(tree, layout, cursor, viewport, renderer)
|
||||
}
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
tree: &'a mut widget::Tree,
|
||||
layout: Layout<'a>,
|
||||
renderer: &Renderer,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
||||
let clock = self.clock.clone();
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.overlay(tree, layout, renderer, viewport, translation)
|
||||
.map(|content| overlay::Element::new(Box::new(ObservedOverlay { content, clock })))
|
||||
}
|
||||
}
|
||||
|
||||
struct ObservedOverlay<'a, Message, Theme, Renderer> {
|
||||
content: overlay::Element<'a, Message, Theme, Renderer>,
|
||||
clock: Arc<AtomicU64>,
|
||||
}
|
||||
impl<Message, Theme, Renderer: renderer::Renderer> overlay::Overlay<Message, Theme, Renderer>
|
||||
for ObservedOverlay<'_, Message, Theme, Renderer>
|
||||
{
|
||||
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||
self.content.as_overlay_mut().layout(renderer, bounds)
|
||||
}
|
||||
fn draw(
|
||||
&self,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
) {
|
||||
self.content
|
||||
.as_overlay()
|
||||
.draw(renderer, theme, style, layout, cursor);
|
||||
}
|
||||
fn operate(
|
||||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_overlay_mut()
|
||||
.operate(layout, renderer, operation);
|
||||
}
|
||||
fn update(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) {
|
||||
invalidate(&self.clock, event);
|
||||
self.content
|
||||
.as_overlay_mut()
|
||||
.update(event, layout, cursor, renderer, clipboard, shell);
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content
|
||||
.as_overlay()
|
||||
.mouse_interaction(layout, cursor, renderer)
|
||||
}
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
layout: Layout<'a>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
||||
let clock = self.clock.clone();
|
||||
self.content
|
||||
.as_overlay_mut()
|
||||
.overlay(layout, renderer)
|
||||
.map(|content| overlay::Element::new(Box::new(ObservedOverlay { content, clock })))
|
||||
}
|
||||
fn index(&self) -> f32 {
|
||||
self.content.as_overlay().index()
|
||||
}
|
||||
}
|
||||
|
||||
/// Inspect all focus owners, then focus only the mounted target. Iced executes
|
||||
/// Operation::Chain synchronously, without an intervening input event. Both
|
||||
/// traversals recheck the raw clock; no result message authorizes later focus.
|
||||
pub fn guarded_focus<Message: Send + 'static>(
|
||||
target: widget::Id,
|
||||
clock: Arc<AtomicU64>,
|
||||
expected: u64,
|
||||
) -> Task<Message> {
|
||||
widget::operate(GuardedFocus::new(target, clock, expected))
|
||||
}
|
||||
|
||||
struct GuardedFocus {
|
||||
target: widget::Id,
|
||||
clock: Arc<AtomicU64>,
|
||||
expected: u64,
|
||||
target_count: usize,
|
||||
foreign_focus: bool,
|
||||
apply: bool,
|
||||
}
|
||||
impl GuardedFocus {
|
||||
fn new(target: widget::Id, clock: Arc<AtomicU64>, expected: u64) -> Self {
|
||||
Self {
|
||||
target,
|
||||
clock,
|
||||
expected,
|
||||
target_count: 0,
|
||||
foreign_focus: false,
|
||||
apply: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<Message: 'static> widget::Operation<Message> for GuardedFocus {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation<Message>)) {
|
||||
if is_current(&self.clock, self.expected) {
|
||||
visit(self);
|
||||
}
|
||||
}
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
_: Rectangle,
|
||||
state: &mut dyn widget::operation::Focusable,
|
||||
) {
|
||||
if !is_current(&self.clock, self.expected) {
|
||||
return;
|
||||
}
|
||||
if id == Some(&self.target) {
|
||||
self.target_count += 1;
|
||||
if self.apply {
|
||||
state.focus();
|
||||
}
|
||||
} else if state.is_focused() {
|
||||
self.foreign_focus = true;
|
||||
}
|
||||
}
|
||||
fn finish(&self) -> widget::operation::Outcome<Message> {
|
||||
if !self.apply
|
||||
&& self.target_count == 1
|
||||
&& !self.foreign_focus
|
||||
&& is_current(&self.clock, self.expected)
|
||||
{
|
||||
widget::operation::Outcome::Chain(Box::new(Self {
|
||||
apply: true,
|
||||
..Self::new(self.target.clone(), self.clock.clone(), self.expected)
|
||||
}))
|
||||
} else {
|
||||
widget::operation::Outcome::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
mod tests;
|
||||
@@ -0,0 +1,190 @@
|
||||
use super::*;
|
||||
use iced::advanced::widget::operation::{Focusable, Outcome};
|
||||
use iced::widget::{column, text_input};
|
||||
use iced::{Point, Theme};
|
||||
|
||||
#[derive(Default)]
|
||||
struct FocusMap(Vec<(Option<widget::Id>, Rectangle, bool)>);
|
||||
impl widget::Operation for FocusMap {
|
||||
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||
visit(self);
|
||||
}
|
||||
fn focusable(&mut self, id: Option<&widget::Id>, bounds: Rectangle, state: &mut dyn Focusable) {
|
||||
self.0.push((id.cloned(), bounds, state.is_focused()));
|
||||
}
|
||||
}
|
||||
|
||||
struct Mounted {
|
||||
element: Element<'static, (), Theme, ()>,
|
||||
tree: widget::Tree,
|
||||
node: layout::Node,
|
||||
}
|
||||
impl Mounted {
|
||||
fn new(clock: Arc<AtomicU64>) -> Self {
|
||||
let content = column![
|
||||
text_input("Editor", "source")
|
||||
.id("editor")
|
||||
.on_input(|_| ())
|
||||
.padding(12),
|
||||
text_input("Find", "")
|
||||
.id("find")
|
||||
.on_input(|_| ())
|
||||
.padding(12),
|
||||
]
|
||||
.spacing(10)
|
||||
.into();
|
||||
let mut element = observe(content, clock);
|
||||
let mut tree = widget::Tree::new(element.as_widget());
|
||||
let node = element.as_widget_mut().layout(
|
||||
&mut tree,
|
||||
&(),
|
||||
&layout::Limits::new(Size::ZERO, Size::new(400., 200.)),
|
||||
);
|
||||
Self {
|
||||
element,
|
||||
tree,
|
||||
node,
|
||||
}
|
||||
}
|
||||
fn pass(&mut self, operation: &mut dyn widget::Operation) {
|
||||
self.element.as_widget_mut().operate(
|
||||
&mut self.tree,
|
||||
Layout::new(&self.node),
|
||||
&(),
|
||||
operation,
|
||||
);
|
||||
}
|
||||
fn run(&mut self, operation: impl widget::Operation + 'static) {
|
||||
let mut next: Box<dyn widget::Operation> = Box::new(operation);
|
||||
loop {
|
||||
self.pass(next.as_mut());
|
||||
match next.finish() {
|
||||
Outcome::Chain(chained) => next = chained,
|
||||
Outcome::None => break,
|
||||
Outcome::Some(()) => panic!("focus must not publish an application message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn focus(&mut self) -> FocusMap {
|
||||
let mut focus = FocusMap::default();
|
||||
self.pass(&mut focus);
|
||||
focus
|
||||
}
|
||||
fn click(&mut self, id: &str) {
|
||||
let bounds = self
|
||||
.focus()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|(found, _, _)| found.as_ref() == Some(&widget::Id::from(id.to_owned())))
|
||||
.unwrap()
|
||||
.1;
|
||||
let mut messages = Vec::new();
|
||||
let mut shell = Shell::new(&mut messages);
|
||||
self.element.as_widget_mut().update(
|
||||
&mut self.tree,
|
||||
&Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
Layout::new(&self.node),
|
||||
mouse::Cursor::Available(bounds.center()),
|
||||
&(),
|
||||
&mut iced::advanced::clipboard::Null,
|
||||
&mut shell,
|
||||
&Rectangle::with_size(Size::new(400., 200.)),
|
||||
);
|
||||
assert!(shell.is_event_captured());
|
||||
assert!(
|
||||
messages.is_empty(),
|
||||
"focus-only click must not rely on App messages"
|
||||
);
|
||||
}
|
||||
fn focused(&mut self, id: &str) -> bool {
|
||||
self.focus()
|
||||
.0
|
||||
.into_iter()
|
||||
.find(|(found, _, _)| found.as_ref() == Some(&widget::Id::from(id.to_owned())))
|
||||
.unwrap()
|
||||
.2
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn captured_find_click_invalidates_without_app_message_and_preserves_focus() {
|
||||
let clock = Arc::new(AtomicU64::new(0));
|
||||
let mut ui = Mounted::new(clock.clone());
|
||||
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 0));
|
||||
assert!(ui.focused("editor"));
|
||||
ui.click("find");
|
||||
assert!(!is_current(&clock, 0));
|
||||
assert!(ui.focused("find"));
|
||||
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 0));
|
||||
assert!(ui.focused("find"));
|
||||
// Even a matching clock cannot authorize stealing another widget's focus.
|
||||
ui.run(GuardedFocus::new(
|
||||
"editor".into(),
|
||||
clock.clone(),
|
||||
clock.load(Ordering::Acquire),
|
||||
));
|
||||
assert!(ui.focused("find"));
|
||||
assert!(!ui.focused("editor"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_rechecks_clock_between_inspection_and_application_and_missing_target_is_inert() {
|
||||
let clock = Arc::new(AtomicU64::new(0));
|
||||
let mut ui = Mounted::new(clock.clone());
|
||||
let mut operation = GuardedFocus::new("editor".into(), clock.clone(), 0);
|
||||
ui.pass(&mut operation);
|
||||
let Outcome::Chain(mut apply) = <GuardedFocus as widget::Operation<()>>::finish(&operation)
|
||||
else {
|
||||
panic!("mounted, unopposed target must be eligible");
|
||||
};
|
||||
clock.store(1, Ordering::Release);
|
||||
ui.pass(apply.as_mut());
|
||||
assert!(!ui.focused("editor"));
|
||||
ui.run(GuardedFocus::new("editor".into(), clock.clone(), 1));
|
||||
assert!(ui.focused("editor"));
|
||||
ui.run(GuardedFocus::new("removed-editor".into(), clock, 1));
|
||||
assert!(ui.focused("editor"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_navigation_inputs_advance_but_passive_geometry_does_not_and_exhaustion_refuses() {
|
||||
let clock = AtomicU64::new(0);
|
||||
let events = [
|
||||
Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Named(keyboard::key::Named::ArrowLeft),
|
||||
modified_key: keyboard::Key::Named(keyboard::key::Named::ArrowLeft),
|
||||
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::ArrowLeft),
|
||||
location: keyboard::Location::Standard,
|
||||
modifiers: keyboard::Modifiers::empty(),
|
||||
text: None,
|
||||
repeat: false,
|
||||
}),
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { x: 0., y: 1. },
|
||||
}),
|
||||
Event::Touch(touch::Event::FingerPressed {
|
||||
id: touch::Finger(1),
|
||||
position: Point::ORIGIN,
|
||||
}),
|
||||
Event::Window(iced::window::Event::Unfocused),
|
||||
];
|
||||
for (index, event) in events.iter().enumerate() {
|
||||
invalidate(&clock, event);
|
||||
assert_eq!(clock.load(Ordering::Acquire), index as u64 + 1);
|
||||
}
|
||||
invalidate(
|
||||
&clock,
|
||||
&Event::Mouse(mouse::Event::CursorMoved {
|
||||
position: Point::ORIGIN,
|
||||
}),
|
||||
);
|
||||
invalidate(&clock, &Event::Window(iced::window::Event::Focused));
|
||||
assert!(is_current(&clock, 5));
|
||||
clock.store(u64::MAX - 1, Ordering::Release);
|
||||
invalidate(&clock, &events[0]);
|
||||
assert!(!is_current(&clock, u64::MAX));
|
||||
invalidate(&clock, &events[0]);
|
||||
assert_eq!(clock.load(Ordering::Acquire), u64::MAX);
|
||||
assert!(!is_current(&clock, 0));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,989 @@
|
||||
use super::*;
|
||||
|
||||
const PROJECT: &str = "00000000-0000-0000-0000-000000000001";
|
||||
const PUMP: &str = "00000000-0000-0000-0000-000000000002";
|
||||
const LINK: &str = "00000000-0000-0000-0000-000000000003";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Fixture {
|
||||
entry: String,
|
||||
files: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
fn standard() -> Self {
|
||||
Self {
|
||||
entry: "project.arcadia".into(),
|
||||
files: vec![
|
||||
(
|
||||
"project.arcadia".into(),
|
||||
format!(
|
||||
"arcadia 7;\r\nproject model \"Harbour\" id \"{PROJECT}\" {{\r\n include \"模块/活動.arcadia\";\r\n include \"links.arcadia\";\r\n}}\r\n"
|
||||
),
|
||||
),
|
||||
(
|
||||
"模块/活動.arcadia".into(),
|
||||
format!(
|
||||
"// 原文 remains exact\r\noa_activity pump \"泵 🚀\" id \"{PUMP}\" {{}}\r\n"
|
||||
),
|
||||
),
|
||||
(
|
||||
"links.arcadia".into(),
|
||||
format!(
|
||||
"// 航海\r\ninteraction connection \"流れ\" id \"{LINK}\" {{\r\n source @ // separator @pump\r\n pump;\r\n description \"船 @pump\"; target @pump;\r\n}}\r\n"
|
||||
),
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn borrowed(&self) -> Vec<DraftFile<'_>> {
|
||||
self.files
|
||||
.iter()
|
||||
.map(|(path, text)| DraftFile { path, text })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn text(&self, path: &str) -> &str {
|
||||
&self
|
||||
.files
|
||||
.iter()
|
||||
.find(|(candidate, _)| candidate == path)
|
||||
.unwrap()
|
||||
.1
|
||||
}
|
||||
|
||||
fn text_mut(&mut self, path: &str) -> &mut String {
|
||||
&mut self
|
||||
.files
|
||||
.iter_mut()
|
||||
.find(|(candidate, _)| candidate == path)
|
||||
.unwrap()
|
||||
.1
|
||||
}
|
||||
|
||||
fn snapshot(&self, validated: Option<&Self>, applied: bool, limits: Limits) -> Snapshot {
|
||||
let current = self.borrowed();
|
||||
let old = validated.map(Self::borrowed);
|
||||
capture(
|
||||
Input {
|
||||
key: key(1),
|
||||
entry: &self.entry,
|
||||
files: ¤t,
|
||||
applied,
|
||||
validation: old.as_ref().map(|files| ValidatedSources {
|
||||
entry: &validated.unwrap().entry,
|
||||
files,
|
||||
}),
|
||||
},
|
||||
limits,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn key(revision: u64) -> SnapshotKey {
|
||||
SnapshotKey {
|
||||
workspace: Id::from_u128(100),
|
||||
revision,
|
||||
}
|
||||
}
|
||||
|
||||
fn query(snapshot: &Snapshot, path: &str, byte_offset: usize, action: Action) -> Results {
|
||||
snapshot
|
||||
.query(Query {
|
||||
stamp: QueryStamp {
|
||||
snapshot: snapshot.key(),
|
||||
serial: 37,
|
||||
},
|
||||
path,
|
||||
byte_offset,
|
||||
action,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn reference_offset(fixture: &Fixture) -> usize {
|
||||
fixture.text("links.arcadia").find("source @").unwrap() + "source ".len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_multifile_unicode_crlf_definition_and_repeated_lexical_references() {
|
||||
let fixture = Fixture::standard();
|
||||
let original = fixture.files.clone();
|
||||
let snapshot = fixture.snapshot(Some(&fixture), true, Limits::default());
|
||||
assert!(snapshot.scope().closure_complete && snapshot.scope().validated);
|
||||
assert_eq!(
|
||||
(
|
||||
snapshot.scope().included_files,
|
||||
snapshot.scope().detached_files
|
||||
),
|
||||
(3, 0)
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Direct);
|
||||
assert_eq!(
|
||||
(result.total_definitions, result.omitted_definitions),
|
||||
(1, 0)
|
||||
);
|
||||
let definition = &result.definitions[0];
|
||||
assert_eq!(definition.path, "模块/活動.arcadia");
|
||||
assert_eq!(definition.declaration.kind, "oa_activity");
|
||||
assert_eq!(definition.declaration.name.as_deref(), Some("泵 🚀"));
|
||||
assert_eq!(
|
||||
definition.declaration.authored_id,
|
||||
Some(Id::parse_str(PUMP).unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
definition.position,
|
||||
Position {
|
||||
line: 1,
|
||||
byte_column: "oa_activity ".len()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
&fixture.text(&definition.path)[definition.span.start..definition.span.end],
|
||||
"pump"
|
||||
);
|
||||
let uses = query(
|
||||
&snapshot,
|
||||
&definition.path,
|
||||
definition.span.start,
|
||||
Action::References,
|
||||
);
|
||||
assert_eq!(uses.disposition, Disposition::LexicalReferences);
|
||||
assert_eq!((uses.total_references, uses.omitted_references), (2, 0));
|
||||
assert_eq!(
|
||||
uses.references[0].position,
|
||||
Position {
|
||||
line: 3,
|
||||
byte_column: 3
|
||||
}
|
||||
);
|
||||
let target_prefix = " description \"船 @pump\"; target @";
|
||||
assert_ne!(target_prefix.len(), target_prefix.chars().count());
|
||||
assert_eq!(
|
||||
uses.references[1].position,
|
||||
Position {
|
||||
line: 4,
|
||||
byte_column: target_prefix.len()
|
||||
}
|
||||
);
|
||||
for row in &uses.references {
|
||||
let text = fixture.text(&row.path);
|
||||
assert_eq!(&text[row.span.start..row.span.end], "pump");
|
||||
assert_eq!(&text[row.marker.start..row.marker.end], "@");
|
||||
let context = row.context.as_ref().unwrap();
|
||||
assert_eq!(context.alias, "connection");
|
||||
assert_eq!(context.kind, "interaction");
|
||||
assert_eq!(context.name.as_deref(), Some("流れ"));
|
||||
assert_eq!(row.evidence, Evidence::Validated);
|
||||
}
|
||||
assert_eq!(
|
||||
uses.stamp,
|
||||
QueryStamp {
|
||||
snapshot: key(1),
|
||||
serial: 37
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
fixture.files, original,
|
||||
"no stabilization, newline rewrite or accepted adoption"
|
||||
);
|
||||
for (index, (path, text)) in original.iter().enumerate() {
|
||||
let owned = snapshot.file(index).unwrap();
|
||||
assert_eq!((owned.path, owned.text), (path.as_str(), text.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comments_strings_and_comment_gap_are_not_navigation_hits() {
|
||||
let fixture = Fixture::standard();
|
||||
let snapshot = fixture.snapshot(None, true, Limits::default());
|
||||
let text = fixture.text("links.arcadia");
|
||||
for offset in [
|
||||
text.find("separator @pump").unwrap() + "separator ".len(),
|
||||
text.find("船 @pump").unwrap() + "船 ".len(),
|
||||
text.find("@ //").unwrap() + 1,
|
||||
] {
|
||||
let result = query(&snapshot, "links.arcadia", offset, Action::Definition);
|
||||
assert_eq!(result.disposition, Disposition::NoSymbol);
|
||||
assert_eq!(result.total_definitions, 0);
|
||||
}
|
||||
assert_eq!(
|
||||
query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
text.find(" pump;").unwrap() + 3,
|
||||
Action::Definition
|
||||
)
|
||||
.total_definitions,
|
||||
1
|
||||
);
|
||||
let inside_utf8 = text.find('船').unwrap() + 1;
|
||||
assert_eq!(
|
||||
snapshot.query(Query {
|
||||
stamp: QueryStamp {
|
||||
snapshot: key(1),
|
||||
serial: 1
|
||||
},
|
||||
path: "links.arcadia",
|
||||
byte_offset: inside_utf8,
|
||||
action: Action::Definition
|
||||
}),
|
||||
Err(QueryError::Query(symbols::QueryError::InvalidBytePosition))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dirty_but_parseable_text_has_current_names_and_no_validated_fallback() {
|
||||
let original = Fixture::standard();
|
||||
let mut dirty = original.clone();
|
||||
*dirty.text_mut("模块/活動.arcadia") = dirty
|
||||
.text("模块/活動.arcadia")
|
||||
.replace("泵 🚀", "Edited 泵\\nwith detail");
|
||||
let snapshot = dirty.snapshot(Some(&original), true, Limits::default());
|
||||
assert!(snapshot.scope().closure_complete);
|
||||
assert!(!snapshot.scope().validated);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&dirty),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
assert_eq!(
|
||||
result.definitions[0].declaration.name.as_deref(),
|
||||
Some("Edited 泵\nwith detail")
|
||||
);
|
||||
assert_eq!(result.definitions[0].evidence, Evidence::Draft);
|
||||
assert_eq!(
|
||||
snapshot.file(1).unwrap().text,
|
||||
dirty.text("模块/活動.arcadia")
|
||||
);
|
||||
assert_ne!(
|
||||
snapshot.file(1).unwrap().text,
|
||||
original.text("模块/活動.arcadia")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_include_syntax_never_borrows_the_old_reachable_file_set() {
|
||||
let original = Fixture::standard();
|
||||
let mut invalid = original.clone();
|
||||
*invalid.text_mut("project.arcadia") = invalid
|
||||
.text("project.arcadia")
|
||||
.replace("include \"links.arcadia\";", "include \"links.arcadia\"");
|
||||
let snapshot = invalid.snapshot(Some(&original), true, Limits::default());
|
||||
assert!(snapshot.scope().partial());
|
||||
assert!(!snapshot.scope().validated);
|
||||
assert!(
|
||||
snapshot
|
||||
.scope()
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| issue.kind == ScopeIssueKind::InvalidIncludes)
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&invalid),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.total_definitions, 1);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
assert_eq!(result.definitions[0].membership, Membership::Unverified);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_include_partial_scope_does_not_resurrect_witness_text() {
|
||||
let original = Fixture::standard();
|
||||
let mut missing = original.clone();
|
||||
missing
|
||||
.files
|
||||
.retain(|(path, _)| path != "模块/活動.arcadia");
|
||||
let snapshot = missing.snapshot(Some(&original), true, Limits::default());
|
||||
assert!(snapshot.scope().partial());
|
||||
assert!(
|
||||
snapshot
|
||||
.scope()
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| issue.kind == ScopeIssueKind::MissingInclude)
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&missing),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::NoDefinition);
|
||||
assert!(result.definitions.is_empty());
|
||||
assert!(
|
||||
result.scope.partial(),
|
||||
"zero means no loaded definition, not no project definition"
|
||||
);
|
||||
assert!(snapshot.file(2).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detached_alias_and_identity_collisions_never_auto_select() {
|
||||
let accepted = Fixture::standard();
|
||||
for (alias, id) in [
|
||||
("pump", "00000000-0000-0000-0000-000000000099"),
|
||||
("other", PUMP),
|
||||
] {
|
||||
let mut loaded = accepted.clone();
|
||||
loaded.files.push((
|
||||
"detached.arcadia".into(),
|
||||
format!("oa_activity {alias} \"Detached\" id \"{id}\" {{}}"),
|
||||
));
|
||||
let snapshot = loaded.snapshot(Some(&accepted), true, Limits::default());
|
||||
assert!(snapshot.scope().validated);
|
||||
assert_eq!(snapshot.scope().detached_files, 1);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&loaded),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
if alias == "pump" {
|
||||
assert_eq!(result.total_definitions, 2);
|
||||
assert!(result.definitions.iter().any(
|
||||
|row| row.membership == Membership::Detached && row.evidence == Evidence::Draft
|
||||
));
|
||||
} else {
|
||||
assert_eq!(result.total_definitions, 1);
|
||||
assert_eq!(
|
||||
result.definitions[0].declaration.identity,
|
||||
Identity::Collision { count: 2 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sole_detached_candidate_and_unapplied_equal_bytes_require_choice() {
|
||||
let accepted = Fixture::standard();
|
||||
let mut detached = accepted.clone();
|
||||
*detached.text_mut("project.arcadia") = detached
|
||||
.text("project.arcadia")
|
||||
.replace(" include \"模块/活動.arcadia\";\r\n", "");
|
||||
let snapshot = detached.snapshot(None, true, Limits::default());
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&detached),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
assert_eq!(result.definitions[0].membership, Membership::Detached);
|
||||
let unapplied = accepted.snapshot(Some(&accepted), false, Limits::default());
|
||||
assert!(unapplied.scope().closure_complete && unapplied.scope().partial());
|
||||
assert!(!unapplied.scope().validated);
|
||||
assert_eq!(
|
||||
query(
|
||||
&unapplied,
|
||||
"links.arcadia",
|
||||
reference_offset(&accepted),
|
||||
Action::Definition
|
||||
)
|
||||
.disposition,
|
||||
Disposition::Choices
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn include_cycle_depth_and_aggregate_edges_are_explicit_partial_outcomes() {
|
||||
let mut fixture = Fixture::standard();
|
||||
fixture
|
||||
.text_mut("模块/活動.arcadia")
|
||||
.push_str("include \"../project.arcadia\";\r\n");
|
||||
let cycle = fixture.snapshot(None, true, Limits::default());
|
||||
assert!(
|
||||
cycle
|
||||
.scope()
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| issue.kind == ScopeIssueKind::IncludeCycle)
|
||||
);
|
||||
let fixture = Fixture::standard();
|
||||
for (depth, edges, expected) in [
|
||||
(0, 1024, ScopeIssueKind::IncludeDepth),
|
||||
(32, 1, ScopeIssueKind::IncludeBudget),
|
||||
] {
|
||||
let snapshot = fixture.snapshot(
|
||||
None,
|
||||
true,
|
||||
Limits {
|
||||
include_depth: depth,
|
||||
include_edges: edges,
|
||||
..Limits::default()
|
||||
},
|
||||
);
|
||||
assert!(snapshot.scope().partial());
|
||||
assert!(
|
||||
snapshot
|
||||
.scope()
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| issue.kind == expected)
|
||||
);
|
||||
assert_eq!(
|
||||
query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition
|
||||
)
|
||||
.disposition,
|
||||
Disposition::Choices
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_snapshot_readback_rejects_same_key_byte_membership_and_evidence_changes() {
|
||||
let fixture = Fixture::standard();
|
||||
let files = fixture.borrowed();
|
||||
let validation = Some(ValidatedSources {
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
});
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation,
|
||||
};
|
||||
let snapshot = capture(input, Limits::default()).unwrap();
|
||||
assert!(snapshot.matches(input));
|
||||
assert!(!snapshot.matches(Input {
|
||||
key: key(2),
|
||||
..input
|
||||
}));
|
||||
assert!(!snapshot.matches(Input {
|
||||
validation: None,
|
||||
..input
|
||||
}));
|
||||
assert!(!snapshot.matches(Input {
|
||||
applied: false,
|
||||
..input
|
||||
}));
|
||||
let mut reversed = files.clone();
|
||||
reversed.reverse();
|
||||
assert!(snapshot.matches(Input {
|
||||
files: &reversed,
|
||||
..input
|
||||
}));
|
||||
let mut changed = fixture.clone();
|
||||
changed.text_mut("links.arcadia").push(' ');
|
||||
let altered = changed.borrowed();
|
||||
assert!(!snapshot.matches(Input {
|
||||
files: &altered,
|
||||
..input
|
||||
}));
|
||||
let missing = &files[..files.len() - 1];
|
||||
assert!(!snapshot.matches(Input {
|
||||
files: missing,
|
||||
..input
|
||||
}));
|
||||
let duplicated = [files[0], files[0], files[2]];
|
||||
assert!(!snapshot.matches(Input {
|
||||
files: &duplicated,
|
||||
..input
|
||||
}));
|
||||
assert_eq!(
|
||||
snapshot.query(Query {
|
||||
stamp: QueryStamp {
|
||||
snapshot: key(2),
|
||||
serial: 37
|
||||
},
|
||||
path: "links.arcadia",
|
||||
byte_offset: 0,
|
||||
action: Action::Definition
|
||||
}),
|
||||
Err(QueryError::Query(symbols::QueryError::StaleSnapshot))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omitted_rows_are_counted_before_result_allocation_and_never_confer_uniqueness() {
|
||||
let fixture = Fixture::standard();
|
||||
let snapshot = fixture.snapshot(
|
||||
Some(&fixture),
|
||||
true,
|
||||
Limits {
|
||||
result_rows: 2,
|
||||
..Limits::default()
|
||||
},
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::References,
|
||||
);
|
||||
assert_eq!((result.total_definitions, result.total_references), (1, 2));
|
||||
assert_eq!(
|
||||
(
|
||||
result.definitions.len(),
|
||||
result.references.len(),
|
||||
result.omitted_references
|
||||
),
|
||||
(1, 1, 1)
|
||||
);
|
||||
let zero = fixture.snapshot(
|
||||
Some(&fixture),
|
||||
true,
|
||||
Limits {
|
||||
result_rows: 0,
|
||||
..Limits::default()
|
||||
},
|
||||
);
|
||||
let result = query(
|
||||
&zero,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
result.definitions.len(),
|
||||
result.total_definitions,
|
||||
result.omitted_definitions
|
||||
),
|
||||
(0, 1, 1)
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
let envelope = fixture.snapshot(
|
||||
Some(&fixture),
|
||||
true,
|
||||
Limits {
|
||||
result_bytes: 0,
|
||||
..Limits::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
envelope.query(Query {
|
||||
stamp: QueryStamp {
|
||||
snapshot: key(1),
|
||||
serial: 1
|
||||
},
|
||||
path: "links.arcadia",
|
||||
byte_offset: reference_offset(&fixture),
|
||||
action: Action::Definition
|
||||
}),
|
||||
Err(QueryError::ResultEnvelopeLimit)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_display_name_cannot_escape_the_owned_row_string_budget() {
|
||||
let mut fixture = Fixture::standard();
|
||||
*fixture.text_mut("模块/活動.arcadia") = fixture
|
||||
.text("模块/活動.arcadia")
|
||||
.replace("泵 🚀", &"長".repeat(8192));
|
||||
let original = fixture.files.clone();
|
||||
let snapshot = fixture.snapshot(
|
||||
None,
|
||||
true,
|
||||
Limits {
|
||||
result_bytes: 64,
|
||||
..Limits::default()
|
||||
},
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(
|
||||
(result.total_definitions, result.omitted_definitions),
|
||||
(1, 1)
|
||||
);
|
||||
assert!(result.definitions.is_empty());
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
assert_eq!(fixture.files, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validation_witness_must_match_entry_and_complete_included_file_set() {
|
||||
let fixture = Fixture::standard();
|
||||
for mutation in 0..3 {
|
||||
let mut witness = fixture.clone();
|
||||
match mutation {
|
||||
0 => witness.entry = "other.arcadia".into(),
|
||||
1 => {
|
||||
witness.files.pop();
|
||||
}
|
||||
_ => witness.files.push(("unused.arcadia".into(), String::new())),
|
||||
}
|
||||
let snapshot = fixture.snapshot(Some(&witness), true, Limits::default());
|
||||
assert!(snapshot.scope().closure_complete);
|
||||
assert!(!snapshot.scope().validated);
|
||||
assert_eq!(
|
||||
query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition
|
||||
)
|
||||
.disposition,
|
||||
Disposition::Choices
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_aggregate_limits_refuse_before_outline_or_copied_snapshot() {
|
||||
// Token budget zero and malformed paths/text would fail later. Raw byte/path
|
||||
// limits must win, including a large second file; no partial Snapshot exists.
|
||||
let large = "not a model \0".repeat(16_384);
|
||||
let files = [
|
||||
DraftFile {
|
||||
path: "../bad.arcadia",
|
||||
text: "?",
|
||||
},
|
||||
DraftFile {
|
||||
path: "large.arcadia",
|
||||
text: &large,
|
||||
},
|
||||
];
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: "project.arcadia",
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation: None,
|
||||
};
|
||||
for (resource, files_limit, bytes_limit, paths_limit) in [
|
||||
(symbols::Resource::Files, 1, usize::MAX, usize::MAX),
|
||||
(symbols::Resource::SourceBytes, 8, 1, usize::MAX),
|
||||
(symbols::Resource::PathBytes, 8, usize::MAX, 1),
|
||||
] {
|
||||
let limits = Limits {
|
||||
index: symbols::Limits {
|
||||
files: files_limit,
|
||||
source_bytes: bytes_limit,
|
||||
path_bytes: paths_limit,
|
||||
tokens: 0,
|
||||
records: 0,
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
assert!(
|
||||
matches!(capture(input, limits), Err(AdmissionError::Index(symbols::BuildError::Limit { resource: actual, .. })) if actual == resource)
|
||||
);
|
||||
}
|
||||
assert_eq!(files[1].text, large);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_declaration_and_live_reference_limits_are_aggregate_not_per_file() {
|
||||
let files = [
|
||||
DraftFile {
|
||||
path: "a.arcadia",
|
||||
text: "oa_activity a \"A\" {}",
|
||||
},
|
||||
DraftFile {
|
||||
path: "b.arcadia",
|
||||
text: "oa_activity b \"B\" {}",
|
||||
},
|
||||
];
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: "a.arcadia",
|
||||
files: &files,
|
||||
applied: false,
|
||||
validation: None,
|
||||
};
|
||||
let limits = Limits {
|
||||
index: symbols::Limits {
|
||||
tokens: 10,
|
||||
..Limits::default().index
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
capture(input, limits),
|
||||
Err(AdmissionError::Outline {
|
||||
file: 1,
|
||||
error: language::OutlineLimit {
|
||||
resource: language::OutlineResource::Tokens,
|
||||
..
|
||||
}
|
||||
})
|
||||
));
|
||||
let limits = Limits {
|
||||
index: symbols::Limits {
|
||||
records: 1,
|
||||
..Limits::default().index
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
capture(input, limits),
|
||||
Err(AdmissionError::Outline {
|
||||
file: 1,
|
||||
error: language::OutlineLimit {
|
||||
resource: language::OutlineResource::Declarations,
|
||||
..
|
||||
}
|
||||
})
|
||||
));
|
||||
let use_files = [
|
||||
files[0],
|
||||
DraftFile {
|
||||
path: "uses.arcadia",
|
||||
text: "source @a; target @a; // @a ignored\n",
|
||||
},
|
||||
];
|
||||
let limits = Limits {
|
||||
index: symbols::Limits {
|
||||
records: 2,
|
||||
..Limits::default().index
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
capture(
|
||||
Input {
|
||||
files: &use_files,
|
||||
..input
|
||||
},
|
||||
limits
|
||||
),
|
||||
Err(AdmissionError::Index(symbols::BuildError::Limit {
|
||||
resource: symbols::Resource::Records,
|
||||
observed_at_least: 3,
|
||||
..
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_and_noncanonical_authored_ids_remain_draft_spans_without_generated_anchors() {
|
||||
let fixture = Fixture::standard();
|
||||
for replacement in [
|
||||
String::new(),
|
||||
" id \"00000000000000000000000000000002\"".into(),
|
||||
] {
|
||||
let mut draft = fixture.clone();
|
||||
let original_id = format!(" id \"{PUMP}\"");
|
||||
*draft.text_mut("模块/活動.arcadia") = draft
|
||||
.text("模块/活動.arcadia")
|
||||
.replace(&original_id, &replacement);
|
||||
draft
|
||||
.text_mut("模块/活動.arcadia")
|
||||
.push_str(&format!("// Historical UUID {PUMP} and @pump\r\n"));
|
||||
let before = draft.files.clone();
|
||||
let snapshot = draft.snapshot(None, false, Limits::default());
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&draft),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.definitions[0].declaration.authored_id, None);
|
||||
assert_eq!(
|
||||
result.definitions[0].declaration.identity,
|
||||
Identity::Missing
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::Choices);
|
||||
assert_eq!(draft.files, before);
|
||||
assert_eq!(
|
||||
snapshot.file(1).unwrap().text,
|
||||
draft.text("模块/活動.arcadia")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyword_alias_and_loaded_path_case_collisions_use_reviewed_index_rules() {
|
||||
let mut fixture = Fixture::standard();
|
||||
for (_, text) in &mut fixture.files {
|
||||
*text = text.replace("pump", "source");
|
||||
}
|
||||
let snapshot = fixture.snapshot(None, true, Limits::default());
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.alias.as_deref(), Some("source"));
|
||||
assert_eq!(result.total_definitions, 1);
|
||||
fixture.files.push(("LINKS.arcadia".into(), String::new()));
|
||||
let files = fixture.borrowed();
|
||||
assert!(matches!(
|
||||
capture(
|
||||
Input {
|
||||
key: key(1),
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation: None
|
||||
},
|
||||
Limits::default()
|
||||
),
|
||||
Err(AdmissionError::Index(symbols::BuildError::Input {
|
||||
problem: symbols::InputProblem::DuplicateOrCaseCollidingPath,
|
||||
..
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_request_moves_exact_text_into_worker_snapshot_and_matches_sync_results() {
|
||||
let fixture = Fixture::standard();
|
||||
let files = fixture.borrowed();
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation: Some(ValidatedSources {
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
}),
|
||||
};
|
||||
let owned = admit_owned(input, Limits::default()).unwrap();
|
||||
let allocations: Vec<_> = owned.files.iter().map(|file| file.text.as_ptr()).collect();
|
||||
let snapshot = owned.capture().unwrap();
|
||||
for (file, allocation) in allocations.iter().enumerate() {
|
||||
assert_eq!(
|
||||
snapshot.file(file).unwrap().text.as_ptr(),
|
||||
*allocation,
|
||||
"worker moves the one current-text copy"
|
||||
);
|
||||
}
|
||||
assert!(snapshot.matches(input));
|
||||
let synchronous = capture(input, Limits::default()).unwrap();
|
||||
assert_eq!(
|
||||
query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::References
|
||||
),
|
||||
query(
|
||||
&synchronous,
|
||||
"links.arcadia",
|
||||
reference_offset(&fixture),
|
||||
Action::References
|
||||
)
|
||||
);
|
||||
let mut edited = fixture.clone();
|
||||
edited
|
||||
.text_mut("links.arcadia")
|
||||
.push_str("// edited after request\r\n");
|
||||
let changed = edited.borrowed();
|
||||
assert!(!snapshot.matches(Input {
|
||||
files: &changed,
|
||||
..input
|
||||
}));
|
||||
assert!(!snapshot.matches(Input {
|
||||
validation: None,
|
||||
..input
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_request_checks_raw_limits_immediately_and_extracts_only_on_worker() {
|
||||
let fixture = Fixture::standard();
|
||||
let files = fixture.borrowed();
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: &fixture.entry,
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation: None,
|
||||
};
|
||||
let no_tokens = Limits {
|
||||
index: symbols::Limits {
|
||||
tokens: 0,
|
||||
..Limits::default().index
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
let owned = admit_owned(input, no_tokens).expect("request admission does not lex or parse");
|
||||
assert!(matches!(
|
||||
owned.capture(),
|
||||
Err(AdmissionError::Outline {
|
||||
file: 0,
|
||||
error: language::OutlineLimit {
|
||||
resource: language::OutlineResource::Tokens,
|
||||
..
|
||||
}
|
||||
})
|
||||
));
|
||||
let no_bytes = Limits {
|
||||
index: symbols::Limits {
|
||||
source_bytes: 1,
|
||||
..Limits::default().index
|
||||
},
|
||||
..Limits::default()
|
||||
};
|
||||
assert!(matches!(
|
||||
admit_owned(input, no_bytes),
|
||||
Err(AdmissionError::Index(symbols::BuildError::Limit {
|
||||
resource: symbols::Resource::SourceBytes,
|
||||
..
|
||||
}))
|
||||
));
|
||||
assert_eq!(files[1].text, fixture.text("模块/活動.arcadia"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn owned_validation_proof_preserves_full_witness_count_and_partial_scope() {
|
||||
let accepted = Fixture::standard();
|
||||
for missing_include in [false, true] {
|
||||
let mut current = accepted.clone();
|
||||
current
|
||||
.files
|
||||
.retain(|(path, _)| path != "模块/活動.arcadia");
|
||||
if !missing_include {
|
||||
*current.text_mut("project.arcadia") = current
|
||||
.text("project.arcadia")
|
||||
.replace(" include \"模块/活動.arcadia\";\r\n", "");
|
||||
}
|
||||
let files = current.borrowed();
|
||||
let old = accepted.borrowed();
|
||||
let input = Input {
|
||||
key: key(1),
|
||||
entry: ¤t.entry,
|
||||
files: &files,
|
||||
applied: true,
|
||||
validation: Some(ValidatedSources {
|
||||
entry: &accepted.entry,
|
||||
files: &old,
|
||||
}),
|
||||
};
|
||||
let snapshot = admit_owned(input, Limits::default())
|
||||
.unwrap()
|
||||
.capture()
|
||||
.unwrap();
|
||||
assert_eq!(snapshot.scope().closure_complete, !missing_include);
|
||||
assert!(
|
||||
!snapshot.scope().validated,
|
||||
"the old witness is not silently reduced to matching loaded files"
|
||||
);
|
||||
let result = query(
|
||||
&snapshot,
|
||||
"links.arcadia",
|
||||
reference_offset(¤t),
|
||||
Action::Definition,
|
||||
);
|
||||
assert_eq!(result.disposition, Disposition::NoDefinition);
|
||||
assert!(snapshot.matches(input));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user