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:
@@ -7,6 +7,7 @@ mod diagram_selection_tests;
|
||||
mod hosted;
|
||||
#[cfg(test)]
|
||||
mod review_navigation_tests;
|
||||
mod source_symbols;
|
||||
#[cfg(test)]
|
||||
mod transition_context_tests;
|
||||
#[cfg(test)]
|
||||
@@ -415,7 +416,12 @@ pub enum Message {
|
||||
Linted(u64, Result<Vec<SourceDiagnostic>, String>),
|
||||
SourceDiagnostic(usize),
|
||||
SourceRelated(SourceLocation),
|
||||
SourceViewport,
|
||||
SourceViewport(Option<Id>),
|
||||
SourceSymbols(
|
||||
crate::source_navigation::SymbolContext,
|
||||
crate::source_navigation::SymbolEvent,
|
||||
),
|
||||
SourceSymbolsReady(crate::source_navigation::Completion),
|
||||
SourceForSelection,
|
||||
SourceNavigation(crate::source_navigation::Event),
|
||||
Save(bool),
|
||||
@@ -491,6 +497,7 @@ pub struct App {
|
||||
pub source_jump: Option<text_editor::Cursor>,
|
||||
source_select_line: bool,
|
||||
pub source_navigation: crate::source_navigation::State,
|
||||
pub source_symbols: crate::source_navigation::SymbolState,
|
||||
pub model_issues: Vec<validation::Issue>,
|
||||
pub migration: Option<persistence::MigrationReport>,
|
||||
source_version: u64,
|
||||
@@ -596,6 +603,7 @@ impl App {
|
||||
source_jump: None,
|
||||
source_select_line: false,
|
||||
source_navigation: crate::source_navigation::State::default(),
|
||||
source_symbols: crate::source_navigation::SymbolState::default(),
|
||||
model_issues,
|
||||
migration: None,
|
||||
source_version: 0,
|
||||
@@ -649,6 +657,7 @@ impl App {
|
||||
self.lint_requested = None;
|
||||
}
|
||||
fn show_source_file(&mut self, restore_cursor: bool) {
|
||||
self.source_symbols.reveal = None;
|
||||
self.source_tree.reveal(&self.source_workspace.active);
|
||||
self.source_select_line = false;
|
||||
let source = self.source_workspace.text().to_owned();
|
||||
@@ -663,6 +672,9 @@ impl App {
|
||||
.map(|b| b.cursor)
|
||||
})
|
||||
.flatten();
|
||||
if let Some(cursor) = self.source_jump {
|
||||
self.queue_source_reveal(cursor, false);
|
||||
}
|
||||
}
|
||||
fn selected_architecture_placement(&self) -> Option<Id> {
|
||||
self.active_diagram
|
||||
@@ -916,6 +928,7 @@ impl App {
|
||||
let canvas_size = self.canvas_size;
|
||||
let source_version = self.source_version + 1;
|
||||
let lint_running = self.lint_running;
|
||||
let navigation_in_flight = self.source_symbols.in_flight;
|
||||
let guide = std::mem::take(&mut self.guide);
|
||||
let mut hosted = std::mem::take(&mut self.hosted);
|
||||
hosted.leave_document();
|
||||
@@ -927,6 +940,7 @@ impl App {
|
||||
self.canvas_size = canvas_size;
|
||||
self.source_version = source_version;
|
||||
self.lint_running = lint_running;
|
||||
self.source_symbols.in_flight = navigation_in_flight;
|
||||
self.guide = guide;
|
||||
self.path = path;
|
||||
self.status = "Project ready".into();
|
||||
@@ -1027,7 +1041,10 @@ impl App {
|
||||
crate::model_menu::popup(self),
|
||||
)
|
||||
.into();
|
||||
crate::hosted::view::overlay(self, base)
|
||||
crate::source_navigation::interaction::observe(
|
||||
crate::hosted::view::overlay(self, base),
|
||||
self.source_symbols.raw_interaction.clone(),
|
||||
)
|
||||
}
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch([
|
||||
@@ -1035,8 +1052,14 @@ impl App {
|
||||
window::close_requests().map(|id| Message::Request(Pending::Close(id))),
|
||||
window::resize_events().map(|(_, size)| Message::WindowResized(size)),
|
||||
architecture_authoring::subscription(self),
|
||||
// Use one captured-Escape bridge: a focused Find input consumes
|
||||
// Escape, while modal priority remains in the ordinary handler.
|
||||
behavior::creation::subscription(
|
||||
self.behavior.creation.has_pending() || self.behavior.source_active(),
|
||||
self.behavior.creation.has_pending()
|
||||
|| self.behavior.source_active()
|
||||
|| (self.page == Page::Text
|
||||
&& self.source_symbols.open
|
||||
&& self.architecture.is_none()),
|
||||
)
|
||||
.map(|()| Message::Escape),
|
||||
iced::event::listen_with(|event, status, _| {
|
||||
@@ -1083,6 +1106,30 @@ impl App {
|
||||
])
|
||||
}
|
||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||
if let Message::SourceSymbols(context, _) = &message
|
||||
&& (context.session != self.source_workspace.session()
|
||||
|| context.path != self.source_workspace.active)
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
// Internal results and geometry do not constitute a new user intent.
|
||||
// Process query completion even behind a modal to release its worker slot;
|
||||
// its handler independently refuses navigation outside the live editor.
|
||||
if let Message::SourceSymbolsReady(completion) = message {
|
||||
return self.source_symbols_ready(completion);
|
||||
}
|
||||
if !matches!(
|
||||
&message,
|
||||
Message::CheckpointTick
|
||||
| Message::CheckpointWritten(..)
|
||||
| Message::Lint(_)
|
||||
| Message::Linted(..)
|
||||
| Message::SourceViewport(_)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
) {
|
||||
self.source_interaction();
|
||||
}
|
||||
if matches!(&message, Message::Connection(_))
|
||||
&& self
|
||||
.architecture
|
||||
@@ -1147,7 +1194,7 @@ impl App {
|
||||
| Message::CanvasResized(_)
|
||||
| Message::Lint(_)
|
||||
| Message::Linted(_, _)
|
||||
| Message::SourceViewport
|
||||
| Message::SourceViewport(_)
|
||||
| Message::Request(Pending::Close(_))
|
||||
)
|
||||
{
|
||||
@@ -1193,7 +1240,7 @@ impl App {
|
||||
| Message::Exported(_)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
| Message::SourceViewport
|
||||
| Message::SourceViewport(_)
|
||||
)
|
||||
{
|
||||
return Task::none();
|
||||
@@ -1231,7 +1278,7 @@ impl App {
|
||||
| Message::CheckpointLoaded(..)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
| Message::SourceViewport
|
||||
| Message::SourceViewport(_)
|
||||
| Message::Lint(_)
|
||||
| Message::Linted(_, _)
|
||||
| Message::Saved(_)
|
||||
@@ -1807,6 +1854,9 @@ impl App {
|
||||
self.status = "Drawing cancelled · Select / move is active".into();
|
||||
}
|
||||
self.source_navigation.open = false;
|
||||
self.source_symbols.open = false;
|
||||
self.source_symbols.results = None;
|
||||
self.source_symbols.notice = None;
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
self.selected_bend = None;
|
||||
}
|
||||
@@ -2855,6 +2905,9 @@ impl App {
|
||||
if self.path.is_none()
|
||||
|| self.path.as_ref() == Some(&receipt.target)
|
||||
{
|
||||
if self.path.as_ref() != Some(&receipt.target) {
|
||||
self.source_workspace.reset_navigation_session();
|
||||
}
|
||||
self.path = Some(receipt.target);
|
||||
self.disk_project = Some(disk);
|
||||
}
|
||||
@@ -2917,14 +2970,10 @@ impl App {
|
||||
}
|
||||
}
|
||||
Message::SourceRelated(location) => self.jump_to_source(location),
|
||||
Message::SourceViewport => {
|
||||
if let Some(cursor) = self.source_jump.take() {
|
||||
self.source_editor.move_to(cursor);
|
||||
if self.source_select_line && cursor.selection.is_none() {
|
||||
self.source_editor.perform(text_editor::Action::SelectLine);
|
||||
}
|
||||
return iced::widget::operation::focus("source-editor");
|
||||
}
|
||||
Message::SourceViewport(ticket) => return self.source_viewport(ticket),
|
||||
Message::SourceSymbols(_, event) => return self.source_symbol_event(event),
|
||||
Message::SourceSymbolsReady(completion) => {
|
||||
return self.source_symbols_ready(completion);
|
||||
}
|
||||
Message::ApplySource => {
|
||||
self.apply_source();
|
||||
@@ -2985,6 +3034,9 @@ impl App {
|
||||
}
|
||||
Event::Next(backwards) if self.page == Page::Text => {
|
||||
self.source_jump = self.source_navigation.next(backwards);
|
||||
if let Some(cursor) = self.source_jump {
|
||||
self.queue_source_reveal(cursor, false);
|
||||
}
|
||||
if self.source_jump.is_none() {
|
||||
self.status = "No matching source text.".into();
|
||||
}
|
||||
@@ -2995,8 +3047,7 @@ impl App {
|
||||
.destination(self.source_editor.line_count())
|
||||
{
|
||||
Ok(cursor) => {
|
||||
self.source_jump = Some(cursor);
|
||||
self.source_select_line = true;
|
||||
self.queue_source_reveal(cursor, true);
|
||||
}
|
||||
Err(error) => self.status = error,
|
||||
},
|
||||
@@ -3079,6 +3130,9 @@ impl App {
|
||||
}
|
||||
self.sync_source();
|
||||
}
|
||||
if self.path.as_ref() != Some(&receipt.path) {
|
||||
self.source_workspace.reset_navigation_session();
|
||||
}
|
||||
self.disk_project = Some(disk);
|
||||
self.path = Some(receipt.path);
|
||||
self.hosted.session = None;
|
||||
@@ -3638,7 +3692,7 @@ impl App {
|
||||
self.canvas_epoch = self.canvas_epoch.wrapping_add(1);
|
||||
self.path = Some(path);
|
||||
self.source_workspace = Workspace::new(&disk.bundle, false);
|
||||
self.source_workspace.reachable = disk.reachable.clone();
|
||||
self.source_workspace.set_reachable(disk.reachable.clone());
|
||||
self.show_source_file(false);
|
||||
self.source_diagnostics = error.diagnostics().to_vec();
|
||||
self.source_version += 1;
|
||||
@@ -3690,11 +3744,13 @@ impl App {
|
||||
let column = prefix
|
||||
.rfind('\n')
|
||||
.map_or(offset, |start| offset - start - 1);
|
||||
self.source_jump = Some(text_editor::Cursor {
|
||||
position: text_editor::Position { line, column },
|
||||
selection: None,
|
||||
});
|
||||
self.source_select_line = true;
|
||||
self.queue_source_reveal(
|
||||
text_editor::Cursor {
|
||||
position: text_editor::Position { line, column },
|
||||
selection: None,
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
fn create_source_file(&mut self) -> Result<(), String> {
|
||||
|
||||
Reference in New Issue
Block a user