Skip to main content

qem/editor/
types.rs

1use super::*;
2use std::sync::Arc;
3
4/// Cursor position in document coordinates, using 1-based indexing.
5///
6/// The column uses the same document text-unit semantics as [`TextPosition`]:
7/// for UTF-8 text this means Unicode scalar values, not grapheme clusters and
8/// not display cells.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct CursorPosition {
11    line: usize,
12    column: usize,
13}
14
15impl Default for CursorPosition {
16    fn default() -> Self {
17        Self::new(1, 1)
18    }
19}
20
21impl CursorPosition {
22    /// Creates a cursor position.
23    ///
24    /// Values smaller than `1` are clamped to `1`.
25    pub fn new(line: usize, column: usize) -> Self {
26        Self {
27            line: line.max(1),
28            column: column.max(1),
29        }
30    }
31
32    /// Returns the 1-based cursor line.
33    pub fn line(&self) -> usize {
34        self.line
35    }
36
37    /// Returns the 1-based cursor column in document text units.
38    pub fn column(&self) -> usize {
39        self.column
40    }
41
42    /// Converts the 1-based cursor into a zero-based document position.
43    pub fn to_text_position(self) -> TextPosition {
44        TextPosition::new(self.line.saturating_sub(1), self.column.saturating_sub(1))
45    }
46
47    /// Converts a zero-based document position into a 1-based cursor.
48    pub fn from_text_position(position: TextPosition) -> Self {
49        Self::new(
50            position.line0().saturating_add(1),
51            position.col0().saturating_add(1),
52        )
53    }
54}
55
56/// Current phase of a background document open.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum LoadPhase {
59    Opening,
60    InspectingSource,
61    PreparingIndex,
62    RecoveringSession,
63    Ready,
64}
65
66impl LoadPhase {
67    pub(super) fn as_raw(self) -> u8 {
68        match self {
69            Self::Opening => 0,
70            Self::InspectingSource => 1,
71            Self::PreparingIndex => 2,
72            Self::RecoveringSession => 3,
73            Self::Ready => 4,
74        }
75    }
76
77    pub(super) fn from_raw(raw: u8) -> Self {
78        match raw {
79            0 => Self::Opening,
80            1 => Self::InspectingSource,
81            2 => Self::PreparingIndex,
82            3 => Self::RecoveringSession,
83            4 => Self::Ready,
84            _ => Self::Opening,
85        }
86    }
87}
88
89/// Typed file-backed progress for background open/save work.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct FileProgress {
92    path: Arc<PathBuf>,
93    completed_bytes: u64,
94    total_bytes: u64,
95    load_phase: Option<LoadPhase>,
96}
97
98impl FileProgress {
99    pub(super) fn new(path: Arc<PathBuf>, completed_bytes: u64, total_bytes: u64) -> Self {
100        Self {
101            path,
102            completed_bytes,
103            total_bytes,
104            load_phase: None,
105        }
106    }
107
108    pub(super) fn loading(
109        path: Arc<PathBuf>,
110        completed_bytes: u64,
111        total_bytes: u64,
112        load_phase: LoadPhase,
113    ) -> Self {
114        Self {
115            path,
116            completed_bytes,
117            total_bytes,
118            load_phase: Some(load_phase),
119        }
120    }
121
122    /// Returns the associated file path.
123    pub fn path(&self) -> &Path {
124        self.path.as_path()
125    }
126
127    /// Returns the completed byte count.
128    ///
129    /// For background loads this tracks source bytes inspected by the open path
130    /// before the document becomes ready. Continued line indexing after the open
131    /// completes is exposed separately through document-local
132    /// `indexing_state()`.
133    pub fn completed_bytes(&self) -> u64 {
134        self.completed_bytes
135    }
136
137    /// Returns the total byte count.
138    ///
139    /// For background loads this is the source file length. For background saves
140    /// this is the destination byte length being written.
141    pub fn total_bytes(&self) -> u64 {
142        self.total_bytes
143    }
144
145    /// Returns the current load phase when this progress value comes from a
146    /// background open.
147    pub fn load_phase(&self) -> Option<LoadPhase> {
148        self.load_phase
149    }
150
151    /// Returns completion as a `0.0..=1.0` fraction.
152    pub fn fraction(&self) -> f32 {
153        if self.total_bytes == 0 {
154            0.0
155        } else {
156            self.completed_bytes as f32 / self.total_bytes as f32
157        }
158    }
159}
160
161/// Current background activity of a [`DocumentSession`] or [`EditorTab`].
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub enum BackgroundActivity {
164    Idle,
165    Loading(FileProgress),
166    Saving(FileProgress),
167}
168
169impl BackgroundActivity {
170    /// Returns `true` when no background work is active.
171    pub fn is_idle(&self) -> bool {
172        matches!(self, Self::Idle)
173    }
174
175    /// Returns the current loading progress when a background open is active.
176    pub fn loading_state(&self) -> Option<&FileProgress> {
177        match self {
178            Self::Loading(progress) => Some(progress),
179            Self::Idle | Self::Saving(_) => None,
180        }
181    }
182
183    /// Returns the current loading phase when a background open is active.
184    pub fn loading_phase(&self) -> Option<LoadPhase> {
185        self.loading_state().and_then(FileProgress::load_phase)
186    }
187
188    /// Returns the current save progress when a background save is active.
189    pub fn save_state(&self) -> Option<&FileProgress> {
190        match self {
191            Self::Saving(progress) => Some(progress),
192            Self::Idle | Self::Loading(_) => None,
193        }
194    }
195
196    /// Returns whichever file-backed progress is currently active.
197    pub fn progress(&self) -> Option<&FileProgress> {
198        match self {
199            Self::Idle => None,
200            Self::Loading(progress) | Self::Saving(progress) => Some(progress),
201        }
202    }
203}
204
205/// Typed summary of the most recent background open/save problem observed by a
206/// session wrapper.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
208pub enum BackgroundIssueKind {
209    LoadFailed,
210    SaveFailed,
211    LoadDiscarded,
212    SaveDiscarded,
213}
214
215impl BackgroundIssueKind {
216    /// Returns `true` when the issue came from a background open.
217    pub const fn is_load(self) -> bool {
218        matches!(self, Self::LoadFailed | Self::LoadDiscarded)
219    }
220
221    /// Returns `true` when the issue came from a background save.
222    pub const fn is_save(self) -> bool {
223        matches!(self, Self::SaveFailed | Self::SaveDiscarded)
224    }
225
226    /// Returns `true` when the worker result was intentionally discarded.
227    pub const fn is_discarded(self) -> bool {
228        matches!(self, Self::LoadDiscarded | Self::SaveDiscarded)
229    }
230}
231
232/// Snapshot of the most recent background open/save problem.
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct BackgroundIssue {
235    kind: BackgroundIssueKind,
236    path: Arc<PathBuf>,
237    message: Arc<str>,
238}
239
240impl BackgroundIssue {
241    pub(super) fn new(kind: BackgroundIssueKind, path: PathBuf, message: String) -> Self {
242        Self {
243            kind,
244            path: Arc::new(path),
245            message: Arc::from(message),
246        }
247    }
248
249    /// Returns the issue kind.
250    pub fn kind(&self) -> BackgroundIssueKind {
251        self.kind
252    }
253
254    /// Returns the affected file path.
255    pub fn path(&self) -> &Path {
256        self.path.as_path()
257    }
258
259    /// Returns the short issue message.
260    pub fn message(&self) -> &str {
261        self.message.as_ref()
262    }
263
264    /// Returns `true` when the issue came from a background open.
265    pub fn is_load(&self) -> bool {
266        self.kind.is_load()
267    }
268
269    /// Returns `true` when the issue came from a background save.
270    pub fn is_save(&self) -> bool {
271        self.kind.is_save()
272    }
273
274    /// Returns `true` when the worker result was discarded instead of applied.
275    pub fn is_discarded(&self) -> bool {
276        self.kind.is_discarded()
277    }
278}
279
280/// Snapshot of a [`DocumentSession`] state for frontend polling.
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct DocumentSessionStatus {
283    generation: u64,
284    document: DocumentStatus,
285    background_activity: BackgroundActivity,
286    background_issue: Option<BackgroundIssue>,
287    close_pending: bool,
288}
289
290impl DocumentSessionStatus {
291    /// Creates a session status snapshot.
292    pub(super) fn new(
293        generation: u64,
294        document: DocumentStatus,
295        background_activity: BackgroundActivity,
296        background_issue: Option<BackgroundIssue>,
297        close_pending: bool,
298    ) -> Self {
299        Self {
300            generation,
301            document,
302            background_activity,
303            background_issue,
304            close_pending,
305        }
306    }
307
308    /// Returns the session generation counter.
309    pub fn generation(&self) -> u64 {
310        self.generation
311    }
312
313    /// Returns the document status snapshot.
314    pub fn document(&self) -> &DocumentStatus {
315        &self.document
316    }
317
318    /// Returns the current document path, if one is set.
319    pub fn path(&self) -> Option<&Path> {
320        self.document.path()
321    }
322
323    /// Returns `true` when the document has unsaved changes.
324    pub fn is_dirty(&self) -> bool {
325        self.document.is_dirty()
326    }
327
328    /// Returns the current document line count.
329    pub fn line_count(&self) -> LineCount {
330        self.document.line_count()
331    }
332
333    /// Returns the best-effort line count for viewport sizing and scrolling.
334    pub fn display_line_count(&self) -> usize {
335        self.document.display_line_count()
336    }
337
338    /// Returns the exact document line count when it is known.
339    pub fn exact_line_count(&self) -> Option<usize> {
340        self.document.exact_line_count()
341    }
342
343    /// Returns `true` when the current line count is exact.
344    pub fn is_line_count_exact(&self) -> bool {
345        self.document.is_line_count_exact()
346    }
347
348    /// Returns the current document length in bytes.
349    pub fn file_len(&self) -> usize {
350        self.document.file_len()
351    }
352
353    /// Returns the currently detected line ending style.
354    pub fn line_ending(&self) -> LineEnding {
355        self.document.line_ending()
356    }
357
358    /// Returns the current document backing mode.
359    pub fn backing(&self) -> DocumentBacking {
360        self.document.backing()
361    }
362
363    /// Returns `true` when the document currently has a mutable edit buffer.
364    pub fn has_edit_buffer(&self) -> bool {
365        self.document.has_edit_buffer()
366    }
367
368    /// Returns `true` when the document is currently rope-backed.
369    pub fn has_rope(&self) -> bool {
370        self.document.has_rope()
371    }
372
373    /// Returns `true` when the document is currently piece-table-backed.
374    pub fn has_piece_table(&self) -> bool {
375        self.document.has_piece_table()
376    }
377
378    /// Returns typed indexing progress while document-local indexing is active.
379    pub fn indexing_state(&self) -> Option<ByteProgress> {
380        self.document.indexing_state()
381    }
382
383    /// Returns `true` while document-local indexing is still running.
384    pub fn is_indexing(&self) -> bool {
385        self.document.is_indexing()
386    }
387
388    /// Returns the current background activity.
389    pub fn background_activity(&self) -> &BackgroundActivity {
390        &self.background_activity
391    }
392
393    /// Returns the most recent background open/save problem, if one is being retained.
394    pub fn background_issue(&self) -> Option<&BackgroundIssue> {
395        self.background_issue.as_ref()
396    }
397
398    /// Returns `true` when `close_file()` was requested while a background job
399    /// was active and the actual close is deferred until that job finishes.
400    pub fn close_pending(&self) -> bool {
401        self.close_pending
402    }
403
404    /// Returns typed loading progress when a background open is active.
405    pub fn loading_state(&self) -> Option<&FileProgress> {
406        self.background_activity.loading_state()
407    }
408
409    /// Returns the current loading phase when a background open is active.
410    pub fn loading_phase(&self) -> Option<LoadPhase> {
411        self.background_activity.loading_phase()
412    }
413
414    /// Returns typed save progress when a background save is active.
415    pub fn save_state(&self) -> Option<&FileProgress> {
416        self.background_activity.save_state()
417    }
418
419    /// Returns `true` while any background open/save job is active.
420    pub fn is_busy(&self) -> bool {
421        !matches!(self.background_activity, BackgroundActivity::Idle)
422    }
423
424    /// Returns `true` while a background load is in progress.
425    pub fn is_loading(&self) -> bool {
426        matches!(self.background_activity, BackgroundActivity::Loading(_))
427    }
428
429    /// Returns `true` while a background save is in progress.
430    pub fn is_saving(&self) -> bool {
431        matches!(self.background_activity, BackgroundActivity::Saving(_))
432    }
433}
434
435/// Snapshot of an [`EditorTab`] state for frontend polling.
436#[derive(Debug, Clone, PartialEq, Eq)]
437pub struct EditorTabStatus {
438    id: u64,
439    session: DocumentSessionStatus,
440    cursor: CursorPosition,
441    pinned: bool,
442}
443
444impl EditorTabStatus {
445    /// Creates an editor-tab status snapshot.
446    pub(super) fn new(
447        id: u64,
448        session: DocumentSessionStatus,
449        cursor: CursorPosition,
450        pinned: bool,
451    ) -> Self {
452        Self {
453            id,
454            session,
455            cursor,
456            pinned,
457        }
458    }
459
460    /// Returns the tab identifier.
461    pub fn id(&self) -> u64 {
462        self.id
463    }
464
465    /// Returns the underlying session status snapshot.
466    pub fn session(&self) -> &DocumentSessionStatus {
467        &self.session
468    }
469
470    /// Returns the current tab generation counter.
471    pub fn generation(&self) -> u64 {
472        self.session.generation()
473    }
474
475    /// Returns the current document status snapshot.
476    pub fn document(&self) -> &DocumentStatus {
477        self.session.document()
478    }
479
480    /// Returns the current document path, if one is set.
481    pub fn path(&self) -> Option<&Path> {
482        self.session.path()
483    }
484
485    /// Returns `true` when the document has unsaved changes.
486    pub fn is_dirty(&self) -> bool {
487        self.session.is_dirty()
488    }
489
490    /// Returns the current document line count.
491    pub fn line_count(&self) -> LineCount {
492        self.session.line_count()
493    }
494
495    /// Returns the best-effort line count for viewport sizing and scrolling.
496    pub fn display_line_count(&self) -> usize {
497        self.session.display_line_count()
498    }
499
500    /// Returns the exact document line count when it is known.
501    pub fn exact_line_count(&self) -> Option<usize> {
502        self.session.exact_line_count()
503    }
504
505    /// Returns `true` when the current line count is exact.
506    pub fn is_line_count_exact(&self) -> bool {
507        self.session.is_line_count_exact()
508    }
509
510    /// Returns the current document length in bytes.
511    pub fn file_len(&self) -> usize {
512        self.session.file_len()
513    }
514
515    /// Returns the currently detected line ending style.
516    pub fn line_ending(&self) -> LineEnding {
517        self.session.line_ending()
518    }
519
520    /// Returns the current document backing mode.
521    pub fn backing(&self) -> DocumentBacking {
522        self.session.backing()
523    }
524
525    /// Returns `true` when the document currently has a mutable edit buffer.
526    pub fn has_edit_buffer(&self) -> bool {
527        self.session.has_edit_buffer()
528    }
529
530    /// Returns `true` when the document is currently rope-backed.
531    pub fn has_rope(&self) -> bool {
532        self.session.has_rope()
533    }
534
535    /// Returns `true` when the document is currently piece-table-backed.
536    pub fn has_piece_table(&self) -> bool {
537        self.session.has_piece_table()
538    }
539
540    /// Returns the current background activity.
541    pub fn background_activity(&self) -> &BackgroundActivity {
542        self.session.background_activity()
543    }
544
545    /// Returns the most recent background open/save problem, if one is being retained.
546    pub fn background_issue(&self) -> Option<&BackgroundIssue> {
547        self.session.background_issue()
548    }
549
550    /// Returns `true` when `close_file()` was requested while a background job
551    /// was active and the actual close is deferred until that job finishes.
552    pub fn close_pending(&self) -> bool {
553        self.session.close_pending()
554    }
555
556    /// Returns typed loading progress when a background open is active.
557    pub fn loading_state(&self) -> Option<&FileProgress> {
558        self.session.loading_state()
559    }
560
561    /// Returns the current loading phase when a background open is active.
562    pub fn loading_phase(&self) -> Option<LoadPhase> {
563        self.session.loading_phase()
564    }
565
566    /// Returns typed save progress when a background save is active.
567    pub fn save_state(&self) -> Option<&FileProgress> {
568        self.session.save_state()
569    }
570
571    /// Returns `true` while any background open/save job is active.
572    pub fn is_busy(&self) -> bool {
573        self.session.is_busy()
574    }
575
576    /// Returns `true` while a background load is in progress.
577    pub fn is_loading(&self) -> bool {
578        self.session.is_loading()
579    }
580
581    /// Returns `true` while a background save is in progress.
582    pub fn is_saving(&self) -> bool {
583        self.session.is_saving()
584    }
585
586    /// Returns the current cursor position.
587    pub fn cursor(&self) -> CursorPosition {
588        self.cursor
589    }
590
591    /// Returns `true` when the tab is pinned.
592    pub fn is_pinned(&self) -> bool {
593        self.pinned
594    }
595}
596
597/// High-level save errors produced by the session wrappers.
598#[derive(Debug)]
599pub enum SaveError {
600    /// No path is associated with the current document.
601    NoPath,
602    /// A background save is already running.
603    InProgress,
604    /// The underlying document save operation failed.
605    Io(DocumentError),
606}
607
608impl std::fmt::Display for SaveError {
609    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
610        match self {
611            Self::NoPath => write!(f, "no path set for current document"),
612            Self::InProgress => write!(f, "save already in progress"),
613            Self::Io(err) => write!(f, "{err}"),
614        }
615    }
616}
617
618impl std::error::Error for SaveError {
619    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
620        match self {
621            Self::Io(err) => Some(err),
622            Self::NoPath | Self::InProgress => None,
623        }
624    }
625}