1mod io;
5pub use io::{SaveReceipt, SaveRequest};
6mod seed;
7pub use seed::BufferSeed;
8mod mutation;
9use crate::diagnostics::BufferTraceId;
10use crate::history::History;
11use crate::range::Range;
12use crate::{id, layout};
13pub use mutation::{
14 Change, ChangeOrigin, EditError, HistoryMove, PreparedReplacements, Replacement, SystemEdit,
15 UserEdit,
16};
17use ropey::Rope;
18
19pub struct Buffer {
21 pub(crate) trace_identity: BufferTraceId,
22 rope: Rope,
23 pub path: Option<std::path::PathBuf>,
27 pub dirty: bool,
28 epoch: u64,
30 pub readonly: bool,
32 pub name: Option<String>,
35 history: History,
38 changes: Vec<Change>,
39 disk_stamp: Option<std::time::SystemTime>,
41 file_identity: Option<std::path::PathBuf>,
42}
43
44impl Buffer {
45 pub fn text(&self) -> &Rope {
46 &self.rope
47 }
48 pub fn snapshot(&self) -> Rope {
49 self.rope.clone()
50 }
51 pub fn history(&self) -> &History {
52 &self.history
53 }
54 pub fn revision(&self) -> id::BufferRevision {
55 id::BufferRevision::new(self.epoch)
56 }
57 pub fn file_identity(&self) -> Option<&std::path::Path> {
58 self.file_identity.as_deref()
59 }
60
61 pub fn restore_history(
62 &mut self,
63 history: History,
64 ) -> Result<(), crate::history::HistoryError> {
65 history.validate_for(&self.rope)?;
66 self.history = history;
67 Ok(())
68 }
69
70 pub fn from_text(text: &str) -> Self {
71 Self {
72 trace_identity: BufferTraceId::next(),
73 rope: Rope::from_str(text),
74 path: None,
75 dirty: false,
76 epoch: 0,
77 readonly: false,
78 name: None,
79 history: History::default(),
80 changes: Vec::new(),
81 disk_stamp: None,
82 file_identity: None,
83 }
84 }
85
86 pub fn open(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
89 let path = path.as_ref();
90 let (rope, disk_stamp) = match std::fs::File::open(path) {
91 Ok(file) => {
92 let stamp = file.metadata()?.modified()?;
93 (Rope::from_reader(file)?, Some(stamp))
94 }
95 Err(e) if e.kind() == std::io::ErrorKind::NotFound => (Rope::new(), None),
96 Err(e) => return Err(e),
97 };
98 Ok(Self {
99 trace_identity: BufferTraceId::next(),
100 rope,
101 path: Some(path.to_path_buf()),
102 dirty: false,
103 epoch: 0,
104 readonly: false,
105 name: None,
106 history: History::default(),
107 changes: Vec::new(),
108 disk_stamp,
109 file_identity: Some(std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())),
110 })
111 }
112
113 pub fn cell_col_with_tab(
118 &self,
119 offset: impl Into<id::ByteOffset>,
120 tab: usize,
121 ) -> id::DisplayColumn {
122 let offset = offset.into().get().min(self.len_bytes());
123 let line = self.line_of(offset);
124 let start = self.line_start(line);
125 let text = self.text().byte_slice(start..self.line_end(line));
126 let byte = offset.saturating_sub(start).min(text.len_bytes());
127 let mut end = id::DisplayColumn::new(0);
128 for (span, cluster) in layout::RopeGraphemes::new(text, tab) {
129 if byte < span.byte + cluster.len() {
130 return span.cell;
131 }
132 end = span.cell + span.width;
133 }
134 end
135 }
136
137 pub fn len_bytes(&self) -> usize {
138 self.rope.len_bytes()
139 }
140 pub fn len_lines(&self) -> usize {
141 self.rope.len_lines()
142 }
143
144 pub fn last_content_line(&self) -> usize {
147 let mut l = self.len_lines().saturating_sub(1);
148 if self.len_bytes() > 0 && self.byte(self.len_bytes() - 1) == b'\n' && l > 0 {
149 l -= 1;
150 }
151 l
152 }
153
154 pub fn line_start(&self, line: impl Into<id::LineIndex>) -> usize {
156 self.rope
157 .line_to_byte(line.into().get().min(self.len_lines().saturating_sub(1)))
158 }
159
160 pub fn line_end(&self, line: impl Into<id::LineIndex>) -> usize {
162 let line = line.into().get();
163 let start = self.line_start(line);
164 let mut end = self.line_start((line + 1).min(self.len_lines().saturating_sub(1)));
165 if line + 1 >= self.len_lines() {
166 end = self.len_bytes();
167 }
168 if end > start && self.byte(end - 1) == b'\n' {
170 end -= 1;
171 if end > start && self.byte(end - 1) == b'\r' {
172 end -= 1;
173 }
174 }
175 end
176 }
177
178 pub fn line_of(&self, offset: impl Into<id::ByteOffset>) -> usize {
179 self.rope
180 .byte_to_line(offset.into().get().min(self.len_bytes()))
181 }
182
183 pub fn col_of(&self, offset: impl Into<id::ByteOffset>) -> usize {
185 let offset = offset.into();
186 offset.get() - self.line_start(self.line_of(offset))
187 }
188 pub fn byte(&self, offset: impl Into<id::ByteOffset>) -> u8 {
193 if self.len_bytes() == 0 {
194 return 0;
195 }
196 self.rope
197 .byte(offset.into().get().min(self.len_bytes().saturating_sub(1)))
198 }
199
200 pub fn byte_at(&self, offset: impl Into<id::ByteOffset>) -> Option<u8> {
201 let off = offset.into().get();
202 if off < self.len_bytes() {
203 Some(self.rope.byte(off))
204 } else {
205 None
206 }
207 }
208
209 pub fn is_boundary(&self, offset: impl Into<id::ByteOffset>) -> bool {
214 let off = offset.into().get();
215 if off == 0 || off == self.len_bytes() {
216 return true;
217 }
218 if off > self.len_bytes() {
219 return false;
220 }
221 match self.rope.try_byte_to_char(off) {
222 Ok(c) => self.rope.try_char_to_byte(c).is_ok_and(|b| b == off),
223 Err(_) => false,
224 }
225 }
226
227 pub fn clamp_boundary(&self, offset: impl Into<id::ByteOffset>) -> usize {
230 let mut offset = offset.into().get().min(self.len_bytes());
231 while offset > 0 && !self.is_boundary(offset) {
232 offset -= 1;
233 }
234 offset
235 }
236
237 pub fn ceil_boundary(&self, offset: impl Into<id::ByteOffset>) -> usize {
242 let mut offset = offset.into().get().min(self.len_bytes());
243 while offset < self.len_bytes() && !self.is_boundary(offset) {
244 offset += 1;
245 }
246 offset
247 }
248
249 pub fn slice_string(&self, range: Range) -> String {
252 let start = self.clamp_boundary(range.start);
253 let end = self.clamp_boundary(range.end);
254 self.rope.byte_slice(start..end.max(start)).to_string()
255 }
256
257 pub fn line_text(&self, line: impl Into<id::LineIndex>) -> String {
258 let line = line.into().get();
259 let start = self.line_start(line);
260 let end = self.line_end(line);
261 self.rope.byte_slice(start..end).to_string()
262 }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub struct InputEdit {
268 pub start_byte: usize,
269 pub old_end_byte: usize,
270 pub new_end_byte: usize,
271 pub start_point: (usize, usize),
272 pub old_end_point: (usize, usize),
273 pub new_end_point: (usize, usize),
274}
275
276impl Buffer {
277 pub fn point_of(&self, offset: usize) -> (usize, usize) {
279 let offset = offset.min(self.len_bytes());
280 (self.line_of(offset), self.col_of(offset))
281 }
282
283 fn point_extent(text: &str) -> (usize, usize) {
285 let lines = text.bytes().filter(|b| *b == b'\n').count();
286 let col = if lines == 0 {
287 text.len()
288 } else {
289 text.rsplit('\n').next().map(str::len).unwrap_or(0)
290 };
291 (lines, col)
292 }
293}
294
295#[cfg(test)]
296mod safety_tests {
297 use super::*;
298
299 #[test]
300 fn save_refuses_external_change_unless_forced() {
301 let dir = tempfile::tempdir().unwrap();
302 let f = dir.path().join("f.txt");
303 std::fs::write(&f, "original\n").unwrap();
304 let mut b = Buffer::open(f.to_str().unwrap()).unwrap();
305 b.edit().insert(id::ByteOffset::new(0), "mine ").unwrap();
306 std::fs::write(&f, "theirs\n").unwrap();
308 std::fs::File::options()
309 .write(true)
310 .open(&f)
311 .unwrap()
312 .set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(123))
313 .unwrap();
314 let err = b.prepare_save(None, false).unwrap().execute().unwrap_err();
315 assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
316 assert_eq!(std::fs::read_to_string(&f).unwrap(), "theirs\n");
317 let receipt = b.prepare_save(None, true).unwrap().execute().unwrap();
318 assert!(b.accept_save(receipt));
319 assert_eq!(std::fs::read_to_string(&f).unwrap(), "mine original\n");
320 assert!(!b.dirty);
321 }
322
323 #[test]
324 fn save_is_atomic_and_keeps_permissions() {
325 use std::os::unix::fs::PermissionsExt;
326 let dir = tempfile::tempdir().unwrap();
327 let f = dir.path().join("x.sh");
328 std::fs::write(&f, "#!/bin/sh\n").unwrap();
329 std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o750)).unwrap();
330 let mut b = Buffer::open(f.to_str().unwrap()).unwrap();
331 let end = b.len_bytes();
332 b.edit()
333 .insert(id::ByteOffset::new(end), "echo hi\n")
334 .unwrap();
335 let receipt = b.prepare_save(None, false).unwrap().execute().unwrap();
336 assert!(b.accept_save(receipt));
337 assert_eq!(std::fs::read_to_string(&f).unwrap(), "#!/bin/sh\necho hi\n");
338 let mode = std::fs::metadata(&f).unwrap().permissions().mode() & 0o777;
339 assert_eq!(mode, 0o750, "permissions survive the swap");
340 assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1);
342 }
343
344 #[test]
345 fn readonly_refuses_mutation_at_the_boundary() {
346 let mut b = Buffer::from_text("abc\n");
348 b.readonly = true;
349 assert_eq!(b.edit().insert(0, "nope"), Err(EditError::ReadOnly));
350 assert_eq!(
351 b.edit().delete(Range::charwise(0, 2)),
352 Err(EditError::ReadOnly)
353 );
354 assert_eq!(b.rope.to_string(), "abc\n", "untouched");
355 b.system_edit().replace_all("gen\n").unwrap();
357 assert_eq!(b.rope.to_string(), "gen\n");
358 }
359 #[test]
360 fn non_utf8_filename_opens_and_roundtrips() {
361 use std::os::unix::ffi::OsStrExt;
364 let dir = tempfile::tempdir().unwrap();
365 let weird = dir
366 .path()
367 .join(std::ffi::OsStr::from_bytes(b"weird-\xff.rs"));
368 std::fs::write(&weird, "fn main() {}\n").unwrap();
369 let mut b = Buffer::open(&weird).unwrap();
370 assert_eq!(b.path.as_deref(), Some(weird.as_path()));
371 b.edit().insert(0, "// x\n").unwrap();
372 let receipt = b.prepare_save(None, false).unwrap().execute().unwrap();
373 assert!(b.accept_save(receipt));
374 assert_eq!(
375 std::fs::read_to_string(&weird).unwrap(),
376 "// x\nfn main() {}\n"
377 );
378 }
379}