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::bookmarks::Bookmarks;
8use crate::fold::FoldStrategy;
9use crate::history::{History, Revision};
10use crate::text::Document;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
13pub struct BufferId(pub u64);
14
15/// How a document is indented: the Tab key's character and the tab width
16/// (also how wide a tab character is drawn).
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub struct Indent {
19    /// The Tab key inserts spaces instead of a tab character.
20    pub use_spaces: bool,
21    /// Cells per indent level and per tab stop.
22    pub width: usize,
23}
24
25impl Default for Indent {
26    fn default() -> Self {
27        Self {
28            use_spaces: false,
29            width: 4,
30        }
31    }
32}
33
34/// A language's comment markers.
35#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
36pub struct CommentSyntax {
37    /// Starts a comment that runs to the end of the line (`//`, `#`).
38    pub line: Option<String>,
39    /// Opens and closes a block comment (`/*`, `*/`).
40    pub block: Option<(String, String)>,
41}
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
44pub enum LineEnding {
45    #[default]
46    Lf,
47    CrLf,
48    Cr,
49}
50
51/// How the file was stored on disk. Filled in and used only by `fileio`; core
52/// never touches the filesystem.
53#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
54pub struct FileMeta {
55    pub path: Option<PathBuf>,
56    /// WHATWG encoding label, e.g. "UTF-8", "UTF-16LE", "windows-1252".
57    pub encoding: String,
58    pub bom: bool,
59    pub line_ending: LineEnding,
60    pub mixed_line_endings: bool,
61}
62
63impl Default for FileMeta {
64    fn default() -> Self {
65        Self {
66            path: None,
67            encoding: "UTF-8".to_owned(),
68            bom: false,
69            line_ending: LineEnding::default(),
70            mixed_line_endings: false,
71        }
72    }
73}
74
75/// How one edit changed the lines: lines `start..=old_end` were replaced by
76/// lines `start..=new_end`, and every later line moved by `new_end - old_end`.
77/// `old_end == new_end == usize::MAX` means "everything from `start` on may
78/// have changed" (the log overflowed).
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct LineEdit {
81    pub start: usize,
82    pub old_end: usize,
83    pub new_end: usize,
84}
85
86impl LineEdit {
87    pub fn everything_from(start: usize) -> Self {
88        Self {
89            start,
90            old_end: usize::MAX,
91            new_end: usize::MAX,
92        }
93    }
94
95    pub fn is_everything(&self) -> bool {
96        self.old_end == usize::MAX
97    }
98}
99
100const MAX_PENDING_LINE_EDITS: usize = 1024;
101
102#[derive(Debug)]
103pub struct Buffer {
104    id: BufferId,
105    pub(crate) doc: Document,
106    pub(crate) history: History,
107    pub(crate) revision: Revision,
108    pub(crate) next_revision: Revision,
109    saved_revision: Revision,
110    pub language: Option<String>,
111    /// How fold regions are found; the app sets it from the language.
112    pub fold_strategy: FoldStrategy,
113    /// Edits are refused (a compare view's copy of a file on disk).
114    pub read_only: bool,
115    /// The language's comment markers, for toggling comments; the app sets
116    /// them from the language.
117    pub comments: Option<CommentSyntax>,
118    /// Indentation and tab width (from the preferences, or detected).
119    pub indent: Indent,
120    /// `indent` was detected from the file or chosen for it, so changing
121    /// the preferences leaves it alone.
122    pub indent_set: bool,
123    /// Some lines are indented with tabs and others with spaces.
124    pub indent_mixed: bool,
125    pub meta: FileMeta,
126    /// Line edits since the last `take_line_edits`, for the highlighter.
127    line_edits: Vec<LineEdit>,
128    /// The bookmarked lines; edits move them (`Editor` maps them).
129    pub(crate) bookmarks: Bookmarks,
130}
131
132impl Buffer {
133    pub(crate) fn new(id: BufferId, text: &str, meta: FileMeta) -> Self {
134        Self {
135            id,
136            doc: Document::new(text),
137            history: History::default(),
138            revision: 0,
139            next_revision: 1,
140            saved_revision: 0,
141            language: None,
142            fold_strategy: FoldStrategy::Indent,
143            read_only: false,
144            comments: None,
145            indent: Indent::default(),
146            indent_set: false,
147            indent_mixed: false,
148            meta,
149            line_edits: Vec::new(),
150            bookmarks: Bookmarks::default(),
151        }
152    }
153
154    pub fn id(&self) -> BufferId {
155        self.id
156    }
157
158    pub fn doc(&self) -> &Document {
159        &self.doc
160    }
161
162    pub fn history(&self) -> &History {
163        &self.history
164    }
165
166    pub fn bookmarks(&self) -> &Bookmarks {
167        &self.bookmarks
168    }
169
170    /// Replaces the bookmarks (a session restoring them); lines past the
171    /// end are dropped.
172    pub fn set_bookmarks(&mut self, lines: impl IntoIterator<Item = usize>) {
173        let count = self.doc.line_count();
174        self.bookmarks.set(lines, count);
175    }
176
177    /// Bookmarks `line`, or removes its bookmark (a click in the margin);
178    /// returns whether it has one now.
179    pub fn toggle_bookmark(&mut self, line: usize) -> bool {
180        line < self.doc.line_count() && self.bookmarks.toggle(line)
181    }
182
183    /// Identifies the current text state; returns to an earlier value on undo.
184    pub fn revision(&self) -> Revision {
185        self.revision
186    }
187
188    pub fn is_dirty(&self) -> bool {
189        self.revision != self.saved_revision
190    }
191
192    /// Records that the current state is what is on disk.
193    pub fn mark_saved(&mut self) {
194        self.mark_saved_at(self.revision);
195    }
196
197    /// Records that `revision` is what is on disk (a background save of an
198    /// earlier state finished; the buffer stays dirty if it changed since).
199    pub fn mark_saved_at(&mut self, revision: Revision) {
200        self.saved_revision = revision;
201        self.history.break_coalescing();
202    }
203
204    /// The history to keep between sessions: the newest steps within about
205    /// `max_bytes`, ending at the current text.
206    pub fn saved_history(&self, max_bytes: usize) -> crate::history::SavedHistory {
207        let (undo, redo) = self.history.newest(max_bytes);
208        crate::history::SavedHistory {
209            undo,
210            redo,
211            revision: self.revision,
212            next_revision: self.next_revision,
213        }
214    }
215
216    /// Takes a history kept from an earlier session for this same text
217    /// (the caller compares the text's hash first). It is checked step by
218    /// step against the text ([`SavedHistory::check`]) and refused if
219    /// anything does not fit. `on_disk`: the text is the file's (so undoing
220    /// and redoing back to it leaves the buffer clean); otherwise the buffer
221    /// stays unsaved.
222    pub fn restore_history(
223        &mut self,
224        saved: crate::history::SavedHistory,
225        on_disk: bool,
226    ) -> Result<(), String> {
227        saved.check(&self.doc)?;
228        self.history.replace(saved.undo, saved.redo);
229        self.revision = saved.revision;
230        self.next_revision = self.next_revision.max(saved.next_revision);
231        if on_disk {
232            self.saved_revision = saved.revision;
233        } else {
234            self.mark_modified();
235        }
236        Ok(())
237    }
238
239    /// Forgets the undo and redo history.
240    pub fn clear_history(&mut self) {
241        self.history.clear();
242    }
243
244    /// Marks the buffer as differing from disk, e.g. when its text was
245    /// restored from a backup snapshot.
246    pub fn mark_modified(&mut self) {
247        // No reachable revision has this value.
248        self.saved_revision = Revision::MAX;
249    }
250
251    /// How the lines changed since the last call, in the order the edits
252    /// were applied (for incremental highlighting).
253    pub fn take_line_edits(&mut self) -> Vec<LineEdit> {
254        std::mem::take(&mut self.line_edits)
255    }
256
257    pub(crate) fn record_line_edit(&mut self, edit: LineEdit) {
258        if let Some(last) = self.line_edits.last_mut()
259            && last.old_end == usize::MAX
260        {
261            last.start = last.start.min(edit.start);
262            return;
263        }
264        if self.line_edits.len() >= MAX_PENDING_LINE_EDITS {
265            // Nobody is draining the log; keep one conservative entry. The
266            // smallest start is never after the earliest changed line.
267            let start = self
268                .line_edits
269                .iter()
270                .map(|e| e.start)
271                .fold(edit.start, usize::min);
272            self.line_edits = vec![LineEdit::everything_from(start)];
273            return;
274        }
275        self.line_edits.push(edit);
276    }
277}