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