527 lines
21 KiB
Rust
527 lines
21 KiB
Rust
//! 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<T>(&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<Id> {
|
|
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<Id>) -> Task<Message> {
|
|
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<Message> {
|
|
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<Message> {
|
|
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<Message> {
|
|
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;
|