130 lines
3.6 KiB
Rust
130 lines
3.6 KiB
Rust
//! 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 {
|
|
DiagramUsages,
|
|
Query(query::Action),
|
|
Back,
|
|
Choose {
|
|
stamp: query::QueryStamp,
|
|
visit: Id,
|
|
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, PartialEq)]
|
|
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 interaction: Id,
|
|
pub raw_interaction: Arc<AtomicU64>,
|
|
pub serial: u64,
|
|
pub in_flight: Option<Id>,
|
|
pub request: Option<Request>,
|
|
/// Latest admitted query owns displayed data independently of whether its
|
|
/// original navigation intent is still current. Hiding a drawer cancels
|
|
/// navigation, but a matching completion can still populate retained data.
|
|
pub display_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 {
|
|
interaction: Id::new_v4(),
|
|
raw_interaction: Arc::new(AtomicU64::new(0)),
|
|
serial: 0,
|
|
in_flight: None,
|
|
request: None,
|
|
display_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())
|
|
}
|