Skip to main content

ochrepad_core/
buffer.rs

1//! `Buffer`: one document's text, undo history, revisions and file metadata.
2
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7use crate::history::{History, Revision};
8use crate::text::Document;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
11pub struct BufferId(pub u64);
12
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
14pub enum LineEnding {
15    #[default]
16    Lf,
17    CrLf,
18    Cr,
19}
20
21/// How the file was stored on disk. Filled in and used only by `fileio`; core
22/// never touches the filesystem.
23#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
24pub struct FileMeta {
25    pub path: Option<PathBuf>,
26    /// WHATWG encoding label, e.g. "UTF-8", "UTF-16LE", "windows-1252".
27    pub encoding: String,
28    pub bom: bool,
29    pub line_ending: LineEnding,
30    pub mixed_line_endings: bool,
31}
32
33impl Default for FileMeta {
34    fn default() -> Self {
35        Self {
36            path: None,
37            encoding: "UTF-8".to_owned(),
38            bom: false,
39            line_ending: LineEnding::default(),
40            mixed_line_endings: false,
41        }
42    }
43}
44
45/// How one edit changed the lines: lines `start..=old_end` were replaced by
46/// lines `start..=new_end`, and every later line moved by `new_end - old_end`.
47/// `old_end == new_end == usize::MAX` means "everything from `start` on may
48/// have changed" (the log overflowed).
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct LineEdit {
51    pub start: usize,
52    pub old_end: usize,
53    pub new_end: usize,
54}
55
56impl LineEdit {
57    pub fn everything_from(start: usize) -> Self {
58        Self {
59            start,
60            old_end: usize::MAX,
61            new_end: usize::MAX,
62        }
63    }
64
65    pub fn is_everything(&self) -> bool {
66        self.old_end == usize::MAX
67    }
68}
69
70const MAX_PENDING_LINE_EDITS: usize = 1024;
71
72#[derive(Debug)]
73pub struct Buffer {
74    id: BufferId,
75    pub(crate) doc: Document,
76    pub(crate) history: History,
77    pub(crate) revision: Revision,
78    pub(crate) next_revision: Revision,
79    saved_revision: Revision,
80    pub language: Option<String>,
81    pub meta: FileMeta,
82    /// Line edits since the last `take_line_edits`, for the highlighter.
83    line_edits: Vec<LineEdit>,
84}
85
86impl Buffer {
87    pub(crate) fn new(id: BufferId, text: &str, meta: FileMeta) -> Self {
88        Self {
89            id,
90            doc: Document::new(text),
91            history: History::default(),
92            revision: 0,
93            next_revision: 1,
94            saved_revision: 0,
95            language: None,
96            meta,
97            line_edits: Vec::new(),
98        }
99    }
100
101    pub fn id(&self) -> BufferId {
102        self.id
103    }
104
105    pub fn doc(&self) -> &Document {
106        &self.doc
107    }
108
109    pub fn history(&self) -> &History {
110        &self.history
111    }
112
113    /// Identifies the current text state; returns to an earlier value on undo.
114    pub fn revision(&self) -> Revision {
115        self.revision
116    }
117
118    pub fn is_dirty(&self) -> bool {
119        self.revision != self.saved_revision
120    }
121
122    /// Records that the current state is what is on disk.
123    pub fn mark_saved(&mut self) {
124        self.mark_saved_at(self.revision);
125    }
126
127    /// Records that `revision` is what is on disk (a background save of an
128    /// earlier state finished; the buffer stays dirty if it changed since).
129    pub fn mark_saved_at(&mut self, revision: Revision) {
130        self.saved_revision = revision;
131        self.history.break_coalescing();
132    }
133
134    /// Marks the buffer as differing from disk, e.g. when its text was
135    /// restored from a backup snapshot.
136    pub fn mark_modified(&mut self) {
137        // No reachable revision has this value.
138        self.saved_revision = Revision::MAX;
139    }
140
141    /// How the lines changed since the last call, in the order the edits
142    /// were applied (for incremental highlighting).
143    pub fn take_line_edits(&mut self) -> Vec<LineEdit> {
144        std::mem::take(&mut self.line_edits)
145    }
146
147    pub(crate) fn record_line_edit(&mut self, edit: LineEdit) {
148        if let Some(last) = self.line_edits.last_mut()
149            && last.old_end == usize::MAX
150        {
151            last.start = last.start.min(edit.start);
152            return;
153        }
154        if self.line_edits.len() >= MAX_PENDING_LINE_EDITS {
155            // Nobody is draining the log; keep one conservative entry. The
156            // smallest start is never after the earliest changed line.
157            let start = self
158                .line_edits
159                .iter()
160                .map(|e| e.start)
161                .fold(edit.start, usize::min);
162            self.line_edits = vec![LineEdit::everything_from(start)];
163            return;
164        }
165        self.line_edits.push(edit);
166    }
167}