1use crate::diagnostics::{BufferTraceId, MutationSource};
5use crate::history::{Edit, EditKind, History};
6use crate::range::Range;
7use crate::{id, layout};
8use ropey::Rope;
9
10pub struct Buffer {
12 pub(crate) trace_identity: BufferTraceId,
13 pub rope: Rope,
14 pub path: Option<std::path::PathBuf>,
18 pub dirty: bool,
19 pub epoch: u64,
21 pub readonly: bool,
23 pub name: Option<String>,
26 pub history: History,
29 pub replaying: bool,
31 disk_stamp: Option<std::time::SystemTime>,
33}
34
35impl Buffer {
36 pub fn from_text(text: &str) -> Self {
37 Self {
38 trace_identity: BufferTraceId::next(),
39 rope: Rope::from_str(text),
40 path: None,
41 dirty: false,
42 epoch: 0,
43 readonly: false,
44 name: None,
45 history: History::default(),
46 replaying: false,
47 disk_stamp: None,
48 }
49 }
50
51 pub fn open(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
54 let path = path.as_ref();
55 let text = match std::fs::read_to_string(path) {
56 Ok(t) => t,
57 Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
58 Err(e) => return Err(e),
59 };
60 let disk_stamp = std::fs::metadata(path).and_then(|m| m.modified()).ok();
61 Ok(Self {
62 trace_identity: BufferTraceId::next(),
63 rope: Rope::from_str(&text),
64 path: Some(path.to_path_buf()),
65 dirty: false,
66 epoch: 0,
67 readonly: false,
68 name: None,
69 history: History::default(),
70 replaying: false,
71 disk_stamp,
72 })
73 }
74
75 pub fn save(&mut self, force: bool) -> std::io::Result<()> {
79 let Some(path) = self.path.clone() else {
80 return Err(std::io::Error::new(
83 std::io::ErrorKind::NotFound,
84 "no file name — :w {path} to name it",
85 ));
86 };
87 let path = std::fs::canonicalize(&path).unwrap_or(path);
90 let current = std::fs::metadata(&path).and_then(|m| m.modified()).ok();
91 if !force && current.is_some() && current != self.disk_stamp {
92 return Err(std::io::Error::new(
93 std::io::ErrorKind::PermissionDenied,
94 "file changed on disk — :w! to force",
95 ));
96 }
97 write_atomic(std::path::Path::new(&path), &self.rope.to_string(), true)?;
98 self.disk_stamp = std::fs::metadata(&path).and_then(|m| m.modified()).ok();
99 self.dirty = false;
100 Ok(())
101 }
102
103 pub fn save_as(
108 &mut self,
109 path: impl AsRef<std::path::Path>,
110 force: bool,
111 ) -> std::io::Result<()> {
112 let target = path.as_ref();
113 if !force && target.exists() {
114 return Err(std::io::Error::new(
115 std::io::ErrorKind::PermissionDenied,
116 "file exists — :w! to overwrite",
117 ));
118 }
119 write_atomic(target, &self.rope.to_string(), force)?;
120 self.path = Some(target.to_path_buf());
122 self.disk_stamp = std::fs::metadata(target).and_then(|m| m.modified()).ok();
123 self.dirty = false;
124 Ok(())
125 }
126 pub fn cell_col_of(&self, offset: impl Into<id::ByteOffset>) -> u16 {
131 self.cell_col_with_tab(offset, 8)
132 }
133
134 pub fn cell_col_with_tab(&self, offset: impl Into<id::ByteOffset>, tab: u16) -> u16 {
137 let offset = offset.into().get();
138 if self.len_bytes() == 0 {
139 return 0;
140 }
141 let line = self.line_of(offset);
142 let (s, e) = (self.line_start(line), self.line_end(line));
143 let text = self.rope.byte_slice(s..e).to_string();
144 let col = offset.saturating_sub(s);
145 let layout = layout::LineLayout::build(text.trim_end_matches('\n'), tab.max(1));
146 layout.cell_at_byte(col.min(layout.len_bytes))
147 }
148
149 pub fn len_bytes(&self) -> usize {
150 self.rope.len_bytes()
151 }
152 pub fn len_lines(&self) -> usize {
153 self.rope.len_lines()
154 }
155
156 pub fn last_content_line(&self) -> usize {
159 let mut l = self.len_lines().saturating_sub(1);
160 if self.len_bytes() > 0 && self.byte(self.len_bytes() - 1) == b'\n' && l > 0 {
161 l -= 1;
162 }
163 l
164 }
165
166 pub fn line_start(&self, line: impl Into<id::LineIndex>) -> usize {
168 self.rope
169 .line_to_byte(line.into().get().min(self.len_lines().saturating_sub(1)))
170 }
171
172 pub fn line_end(&self, line: impl Into<id::LineIndex>) -> usize {
174 let line = line.into().get();
175 let start = self.line_start(line);
176 let mut end = self.line_start((line + 1).min(self.len_lines().saturating_sub(1)));
177 if line + 1 >= self.len_lines() {
178 end = self.len_bytes();
179 }
180 if end > start && self.byte(end - 1) == b'\n' {
182 end -= 1;
183 if end > start && self.byte(end - 1) == b'\r' {
184 end -= 1;
185 }
186 }
187 end
188 }
189
190 pub fn line_of(&self, offset: impl Into<id::ByteOffset>) -> usize {
191 self.rope
192 .byte_to_line(offset.into().get().min(self.len_bytes()))
193 }
194
195 pub fn col_of(&self, offset: impl Into<id::ByteOffset>) -> usize {
197 let offset = offset.into();
198 offset.get() - self.line_start(self.line_of(offset))
199 }
200 pub fn byte(&self, offset: impl Into<id::ByteOffset>) -> u8 {
205 if self.len_bytes() == 0 {
206 return 0;
207 }
208 self.rope
209 .byte(offset.into().get().min(self.len_bytes().saturating_sub(1)))
210 }
211
212 pub fn byte_at(&self, offset: impl Into<id::ByteOffset>) -> Option<u8> {
213 let off = offset.into().get();
214 if off < self.len_bytes() {
215 Some(self.rope.byte(off))
216 } else {
217 None
218 }
219 }
220
221 pub fn is_boundary(&self, offset: impl Into<id::ByteOffset>) -> bool {
226 let off = offset.into().get();
227 if off == 0 || off == self.len_bytes() {
228 return true;
229 }
230 if off > self.len_bytes() {
231 return false;
232 }
233 match self.rope.try_byte_to_char(off) {
234 Ok(c) => self.rope.try_char_to_byte(c).is_ok_and(|b| b == off),
235 Err(_) => false,
236 }
237 }
238
239 pub fn clamp_boundary(&self, offset: impl Into<id::ByteOffset>) -> usize {
242 let mut offset = offset.into().get().min(self.len_bytes());
243 while offset > 0 && !self.is_boundary(offset) {
244 offset -= 1;
245 }
246 offset
247 }
248
249 pub fn ceil_boundary(&self, offset: impl Into<id::ByteOffset>) -> usize {
254 let mut offset = offset.into().get().min(self.len_bytes());
255 while offset < self.len_bytes() && !self.is_boundary(offset) {
256 offset += 1;
257 }
258 offset
259 }
260
261 pub fn slice_string(&self, range: Range) -> String {
264 let start = range.start.min(self.len_bytes());
265 let end = range.end.min(self.len_bytes());
266 self.rope.byte_slice(start..end.max(start)).to_string()
267 }
268
269 pub fn apply_history(&mut self, ops: Vec<Edit>) {
271 self.replaying = true;
272 for op in &ops {
273 match op.kind {
274 EditKind::Insert => {
275 let at = self.clamp_boundary(op.at.min(self.len_bytes()));
276 self.rope.insert(self.rope.byte_to_char(at), &op.text);
277 }
278 EditKind::Delete => {
279 let end = self.clamp_boundary((op.at + op.text.len()).min(self.len_bytes()));
282 let start = self.clamp_boundary(op.at.min(end));
283 if start < end {
284 self.rope
285 .remove(self.rope.byte_to_char(start)..self.rope.byte_to_char(end));
286 }
287 }
288 }
289 }
290 self.replaying = false;
291 self.dirty = true;
292 self.epoch += 1;
293 self.trace_history(&ops);
294 }
295
296 pub fn replace_all(&mut self, text: &str) {
300 if self.readonly {
301 return;
302 }
303 self.replace_all_system(text);
304 }
305
306 pub fn replace_all_system(&mut self, text: &str) {
310 let removed_bytes = self.len_bytes();
311 self.rope = Rope::from_str(text);
312 self.epoch += 1;
313 self.trace_edit(MutationSource::System, 0, removed_bytes, text);
314 }
315
316 pub fn delete(&mut self, range: Range) -> String {
320 if self.readonly && !self.replaying {
321 return String::new();
322 }
323 let start = self.clamp_boundary(range.start.min(self.len_bytes()));
325 let end = self.clamp_boundary(range.end.min(self.len_bytes()));
326 if start >= end {
327 return String::new();
328 }
329 let text = self.rope.byte_slice(start..end).to_string();
330 let cstart = self.rope.byte_to_char(start);
332 let cend = self.rope.byte_to_char(end);
333 self.rope.remove(cstart..cend);
334 self.dirty = true;
335 self.epoch += 1;
336 self.trace_edit(MutationSource::User, start, end - start, "");
337 if !self.replaying && !self.readonly {
338 self.history.record(
339 Edit {
340 at: start,
341 text: text.clone(),
342 kind: EditKind::Insert,
343 },
344 Edit {
345 at: start,
346 text: text.clone(),
347 kind: EditKind::Delete,
348 },
349 );
350 }
351 text
352 }
353
354 pub fn insert(&mut self, at: impl Into<id::ByteOffset>, text: &str) {
355 if self.readonly && !self.replaying {
356 return;
357 }
358 let at = self.clamp_boundary(at);
359 self.rope.insert(self.rope.byte_to_char(at), text);
360 self.dirty = true;
361 self.epoch += 1;
362 self.trace_edit(MutationSource::User, at, 0, text);
363 if !self.replaying && !self.readonly {
364 self.history.record(
365 Edit {
366 at,
367 text: text.into(),
368 kind: EditKind::Delete,
369 },
370 Edit {
371 at,
372 text: text.into(),
373 kind: EditKind::Insert,
374 },
375 );
376 }
377 }
378
379 pub fn line_text(&self, line: impl Into<id::LineIndex>) -> String {
380 let line = line.into().get();
381 let start = self.line_start(line);
382 let end = self.line_end(line);
383 self.rope.byte_slice(start..end).to_string()
384 }
385}
386
387fn write_atomic(target: &std::path::Path, contents: &str, overwrite: bool) -> std::io::Result<()> {
390 use std::io::Write;
391 let parent = target
392 .parent()
393 .filter(|path| !path.as_os_str().is_empty())
394 .unwrap_or_else(|| std::path::Path::new("."));
395 let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
396 match std::fs::metadata(target) {
397 Ok(metadata) => temporary
398 .as_file()
399 .set_permissions(metadata.permissions())?,
400 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
401 Err(error) => return Err(error),
402 }
403 temporary.write_all(contents.as_bytes())?;
404 temporary.as_file().sync_all()?;
405 let result = if overwrite {
406 temporary.persist(target)
407 } else {
408 temporary.persist_noclobber(target)
409 };
410 result.map(|_| ()).map_err(|error| error.error)
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq)]
417pub struct InputEdit {
418 pub start_byte: usize,
419 pub old_end_byte: usize,
420 pub new_end_byte: usize,
421 pub start_point: (usize, usize),
422 pub old_end_point: (usize, usize),
423 pub new_end_point: (usize, usize),
424}
425
426impl Buffer {
427 pub fn point_of(&self, offset: usize) -> (usize, usize) {
429 let offset = offset.min(self.len_bytes());
430 (self.line_of(offset), self.col_of(offset))
431 }
432
433 fn point_extent(text: &str) -> (usize, usize) {
435 let lines = text.bytes().filter(|b| *b == b'\n').count();
436 let col = if lines == 0 {
437 text.len()
438 } else {
439 text.rsplit('\n').next().map(str::len).unwrap_or(0)
440 };
441 (lines, col)
442 }
443
444 pub fn input_edit_of(&self, op: &crate::history::Edit) -> InputEdit {
447 let start_point = self.point_of(op.at);
448 let extent = Self::point_extent(&op.text);
449 match op.kind {
450 EditKind::Insert => InputEdit {
451 start_byte: op.at,
452 old_end_byte: op.at,
453 new_end_byte: op.at + op.text.len(),
454 start_point,
455 old_end_point: start_point,
456 new_end_point: if extent.0 == 0 {
459 (start_point.0, start_point.1 + extent.1)
460 } else {
461 (start_point.0 + extent.0, extent.1)
462 },
463 },
464 EditKind::Delete => InputEdit {
465 start_byte: op.at,
466 old_end_byte: op.at + op.text.len(),
467 new_end_byte: op.at,
468 start_point,
469 old_end_point: if extent.0 == 0 {
470 (start_point.0, start_point.1 + extent.1)
471 } else {
472 (start_point.0 + extent.0, extent.1)
473 },
474 new_end_point: start_point,
475 },
476 }
477 }
478}
479
480#[cfg(test)]
481mod safety_tests {
482 use super::*;
483
484 #[test]
485 fn save_refuses_external_change_unless_forced() {
486 let dir = tempfile::tempdir().unwrap();
487 let f = dir.path().join("f.txt");
488 std::fs::write(&f, "original\n").unwrap();
489 let mut b = Buffer::open(f.to_str().unwrap()).unwrap();
490 b.insert(id::ByteOffset::new(0), "mine ");
491 std::thread::sleep(std::time::Duration::from_millis(5));
493 std::fs::write(&f, "theirs\n").unwrap();
494 let err = b.save(false).unwrap_err();
495 assert!(err.to_string().contains("changed on disk"));
496 assert_eq!(std::fs::read_to_string(&f).unwrap(), "theirs\n");
497 b.save(true).unwrap(); assert_eq!(std::fs::read_to_string(&f).unwrap(), "mine original\n");
499 assert!(!b.dirty);
500 }
501
502 #[test]
503 fn save_is_atomic_and_keeps_permissions() {
504 use std::os::unix::fs::PermissionsExt;
505 let dir = tempfile::tempdir().unwrap();
506 let f = dir.path().join("x.sh");
507 std::fs::write(&f, "#!/bin/sh\n").unwrap();
508 std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o750)).unwrap();
509 let mut b = Buffer::open(f.to_str().unwrap()).unwrap();
510 b.insert(id::ByteOffset::new(b.len_bytes()), "echo hi\n");
511 b.save(false).unwrap();
512 assert_eq!(std::fs::read_to_string(&f).unwrap(), "#!/bin/sh\necho hi\n");
513 let mode = std::fs::metadata(&f).unwrap().permissions().mode() & 0o777;
514 assert_eq!(mode, 0o750, "permissions survive the swap");
515 assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1);
517 }
518
519 #[test]
520 fn readonly_refuses_mutation_at_the_boundary() {
521 let mut b = Buffer::from_text("abc\n");
523 b.readonly = true;
524 b.insert(id::ByteOffset::new(0), "nope");
525 let gone = b.delete(Range::charwise(0, 2));
526 assert_eq!(gone, "");
527 assert_eq!(b.rope.to_string(), "abc\n", "untouched");
528 b.replace_all_system("gen\n");
530 assert_eq!(b.rope.to_string(), "gen\n");
531 }
532 #[test]
533 fn non_utf8_filename_opens_and_roundtrips() {
534 use std::os::unix::ffi::OsStrExt;
537 let dir = tempfile::tempdir().unwrap();
538 let weird = dir
539 .path()
540 .join(std::ffi::OsStr::from_bytes(b"weird-\xff.rs"));
541 std::fs::write(&weird, "fn main() {}\n").unwrap();
542 let mut b = Buffer::open(&weird).unwrap();
543 assert_eq!(b.path.as_deref(), Some(weird.as_path()));
544 b.insert(0, "// x\n");
545 b.save(false).unwrap();
546 assert!(std::fs::read_to_string(&weird).unwrap().starts_with("// x"));
547 }
548}