//! 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, Arc), 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, pub serial: u64, pub in_flight: Option, pub request: Option, /// 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, pub cache: Option>, pub results: Option>, pub notice: Option, pub back: Vec, pub reveal: Option, } 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 { 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 { 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()) }