//! Current draft navigation, isolated from model application and disk discovery. use super::*; use crate::source_navigation::{self as nav, query}; use std::sync::atomic::Ordering; fn admission_notice(error: query::AdmissionError) -> String { use nav::symbols::{BuildError, InputProblem, Resource}; let (limit, resource) = match error { query::AdmissionError::Index(BuildError::Limit { limit, resource, .. }) => (limit, resource), query::AdmissionError::Outline { error, .. } => (error.limit, match error.resource { language::OutlineResource::SourceBytes => Resource::SourceBytes, language::OutlineResource::Tokens => Resource::Tokens, language::OutlineResource::Declarations => Resource::Records, }), query::AdmissionError::Index(BuildError::Input { problem, .. }) => return match problem { InputProblem::NonCanonicalPath | InputProblem::DuplicateOrCaseCollidingPath => "Navigation needs distinct relative source paths. Check file names and include paths.".into(), _ => "Navigation could not verify the current source locations. Correct source errors and try again.".into(), }, }; let unit = match resource { Resource::Files => "loaded files", Resource::SourceBytes => "source bytes", Resource::PathBytes => "bytes in file paths", Resource::Tokens => "source tokens", Resource::Records => "declarations and references", }; format!( "Navigation reached its limit of {limit} {unit}. Reduce the loaded source size and try again." ) } impl App { pub(super) fn source_input_envelope(&self, message: &Message) -> bool { match message { Message::SourceSymbols(context, _) => { context.session == self.source_workspace.session() && context.path == self.source_workspace.active } Message::SourceFindClose(context, instance) => { context == &self.source_symbol_context() && *instance == self.source_find_instance && self.source_navigation.open && self.source_navigation_available() } // The checked Find-close envelope is mapped inside update_inner. Message::SourceNavigation(nav::Event::Close) => false, _ => true, } } pub fn source_symbol_context(&self) -> nav::SymbolContext { nav::SymbolContext { session: self.source_workspace.session(), path: self.source_workspace.active.clone(), } } pub fn source_symbol_message(&self, event: nav::SymbolEvent) -> Message { Message::SourceSymbols(self.source_symbol_context(), event) } pub fn source_find_close_message(&self) -> Message { Message::SourceFindClose(self.source_symbol_context(), self.source_find_instance) } pub fn source_key(&self) -> query::SnapshotKey { query::SnapshotKey { workspace: self.source_workspace.session(), revision: self.source_workspace.revision(), } } pub(super) fn with_source_input(&self, use_input: impl FnOnce(query::Input<'_>) -> T) -> T { let files: Vec<_> = self .source_workspace .files .iter() .map(|(path, buffer)| query::DraftFile { path, text: &buffer.text, }) .collect(); let accepted = self.document.bundle(); let validated: Vec<_> = accepted .files .iter() .map(|(path, text)| query::DraftFile { path, text }) .collect(); use_input(query::Input { key: self.source_key(), entry: &self.source_workspace.entry, files: &files, applied: self.source_workspace.applied, validation: self .source_workspace .applied .then_some(query::ValidatedSources { entry: &accepted.entry, files: &validated, }), }) } pub(super) fn source_snapshot_matches(&self, snapshot: &query::Snapshot) -> bool { snapshot.key() == self.source_key() && self.with_source_input(|input| snapshot.matches(input)) } pub fn source_navigation_available(&self) -> bool { self.page == Page::Text && !self.source_rename.open() && !self.busy && self.pending.is_none() && self.file_prompt.is_none() && !self.delete_pending && self.workflow.is_none() && !self.hosted.visible && self.model_menu.is_none() && self.diagram_usages.is_none() && !self.reference_input_active() && self.architecture.is_none() && !self.behavior.has_pending() } /// Mounted widget IDs change when another interaction supersedes a reveal. /// A queued focus operation therefore cannot find a subsequently reused editor. pub fn source_editor_id(&self) -> iced::widget::Id { iced::widget::Id::from(format!( "source-editor-{}-{}-{}", self.source_workspace.session(), self.source_workspace.active, self.source_symbols.interaction )) } pub fn source_reveal_ticket(&self) -> Option { self.source_symbols .reveal .as_ref() .map(|reveal| reveal.ticket) } pub(super) fn source_interaction(&mut self) { self.source_symbols.interaction = Id::new_v4(); self.source_symbols.request = None; self.source_symbols.reveal = None; self.source_jump = None; } pub(super) fn queue_source_reveal(&mut self, cursor: text_editor::Cursor, select_line: bool) { self.source_jump = Some(cursor); self.source_select_line = select_line; self.source_symbols.reveal = Some(nav::Reveal { ticket: Id::new_v4(), interaction: self.source_symbols.interaction, raw_interaction: self.source_symbols.raw_interaction.load(Ordering::SeqCst), snapshot: self.source_key(), path: self.source_workspace.active.clone(), }); } pub(super) fn restore_source_editor_focus(&mut self) { let cursor = self.source_editor.cursor(); if self.source_navigation_available() && nav::exact_cursor(self.source_workspace.text(), cursor) { self.queue_source_reveal(cursor, false); } } pub(super) fn source_viewport(&mut self, ticket: Option) -> Task { let Some(reveal) = &self.source_symbols.reveal else { return Task::none(); }; if Some(reveal.ticket) != ticket || reveal.interaction != self.source_symbols.interaction || !nav::interaction::is_current( &self.source_symbols.raw_interaction, reveal.raw_interaction, ) || reveal.snapshot != self.source_key() || reveal.path != self.source_workspace.active || !self.source_navigation_available() { return Task::none(); } let raw_interaction = reveal.raw_interaction; self.source_symbols.reveal = None; if let Some(cursor) = self.source_jump.take() { if !nav::exact_cursor(self.source_workspace.text(), cursor) { return Task::none(); } self.source_editor.move_to(cursor); if self.source_select_line && cursor.selection.is_none() { self.source_editor.perform(text_editor::Action::SelectLine); } return nav::interaction::guarded_focus( self.source_editor_id(), self.source_symbols.raw_interaction.clone(), raw_interaction, ); } Task::none() } pub(super) fn source_symbol_event(&mut self, event: nav::SymbolEvent) -> Task { if !self.source_navigation_available() { return Task::none(); } match event { nav::SymbolEvent::DiagramUsages => { if self.source_dirty() || !self.source_workspace.applied { self.source_symbols.open = true; self.source_symbols.notice = Some("Apply source before finding diagram representations. Your current text is retained.".into()); return Task::none(); } let task = self.start_source_query(query::Action::Definition); self.source_diagram_job = self .source_symbols .request .as_ref() .map(|request| request.job); return task; } nav::SymbolEvent::Close => { self.source_symbols.open = false; self.source_symbols.results = None; self.source_symbols.notice = None; } nav::SymbolEvent::Back => { self.source_symbol_back(); // A successful Back needs no empty results panel. Retain an // already open panel, or reveal an actionable return notice. self.source_symbols.open |= self.source_symbols.notice.is_some(); } nav::SymbolEvent::Choose { stamp, reference, index, } => { let Some(results) = self.source_symbols.results.clone() else { return Task::none(); }; if results.stamp != stamp || stamp.snapshot != self.source_key() || self .source_symbols .cache .as_ref() .is_none_or(|cache| !self.source_snapshot_matches(cache)) { self.source_symbols.notice = Some("Source changed. Run navigation again for current locations.".into()); return Task::none(); } let destination = if reference { results .references .get(index) .map(|row| (&row.path, row.position, row.end_position)) } else { results .definitions .get(index) .map(|row| (&row.path, row.position, row.end_position)) }; if let Some((path, start, end)) = destination { self.source_symbol_jump(path, start, end); } } nav::SymbolEvent::Query(action) => return self.start_source_query(action), } Task::none() } fn start_source_query(&mut self, action: query::Action) -> Task { self.source_symbols.open = true; if !nav::interaction::is_current( &self.source_symbols.raw_interaction, self.source_symbols.raw_interaction.load(Ordering::SeqCst), ) { self.source_symbols.notice = Some("Reopen this workspace to reset navigation.".into()); return Task::none(); } if self.source_symbols.in_flight.is_some() { self.source_symbols.notice = Some("Navigation is finishing. Try again shortly.".into()); return Task::none(); } let cursor = self.source_editor.cursor(); let Some(offset) = nav::cursor_offset(self.source_workspace.text(), cursor) else { self.source_symbols.notice = Some("Place the cursor on an alias or its @ reference.".into()); return Task::none(); }; self.source_symbols.serial = match self.source_symbols.serial.checked_add(1) { Some(serial) => serial, None => { self.source_workspace.reset_navigation_session(); self.source_symbols.back.clear(); self.source_symbols.cache = None; 0 } }; let request = nav::Request { job: Id::new_v4(), interaction: self.source_symbols.interaction, raw_interaction: self.source_symbols.raw_interaction.load(Ordering::SeqCst), stamp: query::QueryStamp { snapshot: self.source_key(), serial: self.source_symbols.serial, }, path: self.source_workspace.active.clone(), offset, cursor, action, }; let cache = self .source_symbols .cache .as_ref() .filter(|cache| self.source_snapshot_matches(cache)) .cloned(); let input = if cache.is_none() { match self .with_source_input(|input| query::admit_owned(input, query::Limits::default())) { Ok(input) => Some(input), Err(error) => { self.source_symbols.notice = Some(admission_notice(error)); return Task::none(); } } } else { None }; self.source_symbols.in_flight = Some(request.job); self.source_symbols.request = Some(request.clone()); self.source_symbols.results = None; self.source_symbols.notice = None; Task::perform( async move { let worker_request = request.clone(); let result = tokio::task::spawn_blocking(move || { let snapshot = match cache { Some(cache) => cache, None => Arc::new( input .expect("admitted input for uncached request") .capture() .map_err(admission_notice)?, ), }; let results = snapshot .query(query::Query { stamp: worker_request.stamp, path: &worker_request.path, byte_offset: worker_request.offset, action: worker_request.action, }) .map_err(|error| match error { query::QueryError::ResultEnvelopeLimit => "These result details exceed the display limit. Try literal Find for this alias.".to_owned(), _ => "Navigation could not verify this location. Place the cursor on an alias and try again.".to_owned(), })?; Ok((snapshot, Arc::new(results))) }) .await .unwrap_or_else(|error| Err(format!("Navigation task failed: {error}"))); nav::Completion { request, result } }, Message::SourceSymbolsReady, ) } pub(super) fn source_symbols_ready(&mut self, completion: nav::Completion) -> Task { let request = completion.request; if self.source_symbols.in_flight != Some(request.job) { return Task::none(); } self.source_symbols.in_flight = None; let diagram_query = self.source_diagram_job == Some(request.job); if diagram_query { self.source_diagram_job = None; } let wanted = self .source_symbols .request .take() .is_some_and(|current| current.job == request.job) && request.interaction == self.source_symbols.interaction && request.stamp.snapshot == self.source_key() && nav::interaction::is_current( &self.source_symbols.raw_interaction, request.raw_interaction, ) && request.path == self.source_workspace.active && request.cursor == self.source_editor.cursor() && self.source_navigation_available(); if !wanted { return Task::none(); } match completion.result { Ok((snapshot, results)) if self.source_snapshot_matches(&snapshot) => { self.source_symbols.cache = Some(snapshot); self.source_symbols.results = Some(results.clone()); if diagram_query { if results.disposition == query::Disposition::Direct && results.scope.validated && results.omitted_definitions == 0 && let [row] = results.definitions.as_slice() && let query::Identity::Unique(identity) = row.declaration.identity && row.membership == query::Membership::Included && row.evidence == query::Evidence::Validated && self.project().contains(identity.id()) && crate::diagram_usage::alias(self.project(), identity.id()) == row.declaration.alias { self.source_symbols.open = false; return self.diagram_usage_event(crate::diagram_usage::Event::Open( crate::diagram_usage::Context { document: self.canvas_document, revision: self.document.revision(), subject: identity.id(), }, )); } self.source_symbols.notice = Some("Place the caret on a uniquely resolved alias in applied source to find diagram representations.".into()); return Task::none(); } if results.disposition == query::Disposition::Direct && let Some(row) = results.definitions.first() { self.source_symbol_jump(&row.path, row.position, row.end_position); } } Ok(_) => { self.source_symbols.notice = Some("Source changed. Run navigation again.".into()) } Err(error) => self.source_symbols.notice = Some(error), } Task::none() } fn source_symbol_jump(&mut self, path: &str, start: query::Position, end: query::Position) { let cursor = text_editor::Cursor { position: text_editor::Position { line: start.line, column: start.byte_column, }, selection: Some(text_editor::Position { line: end.line, column: end.byte_column, }), }; if self .source_workspace .files .get(path) .is_none_or(|buffer| !nav::exact_cursor(&buffer.text, cursor)) { self.source_symbols.notice = Some("That source location is no longer available. Run navigation again.".into()); return; } if let Some(buffer) = self .source_workspace .files .get(&self.source_workspace.active) { let origin = nav::HistoryEntry { session: self.source_workspace.session(), path: self.source_workspace.active.clone(), generation: buffer.generation(), cursor: self.source_editor.cursor(), }; self.source_symbols .back .retain(|entry| entry.session == origin.session); if self.source_symbols.back.len() == 64 { self.source_symbols.back.remove(0); } self.source_symbols.back.push(origin); } self.source_workspace .switch(path, self.source_editor.cursor()); self.show_source_file(false); self.queue_source_reveal(cursor, false); } fn source_symbol_back(&mut self) { while let Some(origin) = self.source_symbols.back.pop() { if origin.session != self.source_workspace.session() { continue; } let Some(buffer) = self.source_workspace.files.get(&origin.path) else { self.source_symbols.notice = Some("The previous file is no longer loaded.".into()); return; }; let unchanged = origin.generation == buffer.generation(); let cursor = if unchanged { origin.cursor } else if origin.path == self.source_workspace.active { self.source_editor.cursor() } else { buffer.cursor }; if !nav::exact_cursor(&buffer.text, cursor) { self.source_symbols.notice = Some( "The previous location changed. Open the file to choose a current position." .into(), ); return; } self.source_workspace .switch(&origin.path, self.source_editor.cursor()); self.show_source_file(false); self.queue_source_reveal(cursor, false); self.source_symbols.notice = (!unchanged).then(|| "This draft changed; its current cursor was restored.".into()); return; } self.source_symbols.notice = Some("No previous source location in this workspace.".into()); } } #[cfg(test)] mod tests;