1use crate::history::{Edit, EditKind, History};
5use crate::range::Range;
6use crate::{id, layout};
7use ropey::Rope;
8
9pub struct Buffer {
11 pub rope: Rope,
12 pub path: Option<std::path::PathBuf>,
16 pub dirty: bool,
17 pub epoch: u64,
19 pub readonly: bool,
21 pub name: Option<String>,
24 pub history: History,
27 pub replaying: bool,
29 disk_stamp: Option<std::time::SystemTime>,
31}
32
33impl Buffer {
34 pub fn from_text(text: &str) -> Self {
35 Self {
36 rope: Rope::from_str(text),
37 path: None,
38 dirty: false,
39 epoch: 0,
40 readonly: false,
41 name: None,
42 history: History::default(),
43 replaying: false,
44 disk_stamp: None,
45 }
46 }
47
48 pub fn open(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
51 let path = path.as_ref();
52 let text = match std::fs::read_to_string(path) {
53 Ok(t) => t,
54 Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
55 Err(e) => return Err(e),
56 };
57 let disk_stamp = std::fs::metadata(path).and_then(|m| m.modified()).ok();
58 Ok(Self {
59 rope: Rope::from_str(&text),
60 path: Some(path.to_path_buf()),
61 dirty: false,
62 epoch: 0,
63 readonly: false,
64 name: None,
65 history: History::default(),
66 replaying: false,
67 disk_stamp,
68 })
69 }
70
71 pub fn save(&mut self, force: bool) -> std::io::Result<()> {
75 let Some(path) = self.path.clone() else {
76 return Err(std::io::Error::new(
79 std::io::ErrorKind::NotFound,
80 "no file name — :w {path} to name it",
81 ));
82 };
83 let path = std::fs::canonicalize(&path).unwrap_or(path);
86 let current = std::fs::metadata(&path).and_then(|m| m.modified()).ok();
87 if !force && current.is_some() && current != self.disk_stamp {
88 return Err(std::io::Error::new(
89 std::io::ErrorKind::PermissionDenied,
90 "file changed on disk — :w! to force",
91 ));
92 }
93 write_atomic(std::path::Path::new(&path), &self.rope.to_string())?;
94 self.disk_stamp = std::fs::metadata(&path).and_then(|m| m.modified()).ok();
95 self.dirty = false;
96 Ok(())
97 }
98
99 pub fn save_as(
104 &mut self,
105 path: impl AsRef<std::path::Path>,
106 force: bool,
107 ) -> std::io::Result<()> {
108 let target = path.as_ref();
109 if !force && target.exists() {
110 return Err(std::io::Error::new(
111 std::io::ErrorKind::PermissionDenied,
112 "file exists — :w! to overwrite",
113 ));
114 }
115 write_atomic(target, &self.rope.to_string())?;
116 self.path = Some(target.to_path_buf());
118 self.disk_stamp = std::fs::metadata(target).and_then(|m| m.modified()).ok();
119 self.dirty = false;
120 Ok(())
121 }
122 pub fn cell_col_of(&self, offset: impl Into<id::ByteOffset>) -> u16 {
127 self.cell_col_with_tab(offset, 8)
128 }
129
130 pub fn cell_col_with_tab(&self, offset: impl Into<id::ByteOffset>, tab: u16) -> u16 {
133 let offset = offset.into().get();
134 if self.len_bytes() == 0 {
135 return 0;
136 }
137 let line = self.line_of(offset);
138 let (s, e) = (self.line_start(line), self.line_end(line));
139 let text = self.rope.byte_slice(s..e).to_string();
140 let col = offset.saturating_sub(s);
141 let layout = layout::LineLayout::build(text.trim_end_matches('\n'), tab.max(1));
142 layout.cell_at_byte(col.min(layout.len_bytes))
143 }
144
145 pub fn len_bytes(&self) -> usize {
146 self.rope.len_bytes()
147 }
148 pub fn len_lines(&self) -> usize {
149 self.rope.len_lines()
150 }
151
152 pub fn last_content_line(&self) -> usize {
155 let mut l = self.len_lines().saturating_sub(1);
156 if self.len_bytes() > 0 && self.byte(self.len_bytes() - 1) == b'\n' && l > 0 {
157 l -= 1;
158 }
159 l
160 }
161
162 pub fn line_start(&self, line: impl Into<id::LineIndex>) -> usize {
164 self.rope
165 .line_to_byte(line.into().get().min(self.len_lines().saturating_sub(1)))
166 }
167
168 pub fn line_end(&self, line: impl Into<id::LineIndex>) -> usize {
170 let line = line.into().get();
171 let start = self.line_start(line);
172 let mut end = self.line_start((line + 1).min(self.len_lines().saturating_sub(1)));
173 if line + 1 >= self.len_lines() {
174 end = self.len_bytes();
175 }
176 if end > start && self.byte(end - 1) == b'\n' {
178 end -= 1;
179 }
180 end
181 }
182
183 pub fn line_of(&self, offset: impl Into<id::ByteOffset>) -> usize {
184 self.rope
185 .byte_to_line(offset.into().get().min(self.len_bytes()))
186 }
187
188 pub fn col_of(&self, offset: impl Into<id::ByteOffset>) -> usize {
190 let offset = offset.into();
191 offset.get() - self.line_start(self.line_of(offset))
192 }
193 pub fn byte(&self, offset: impl Into<id::ByteOffset>) -> u8 {
198 if self.len_bytes() == 0 {
199 return 0;
200 }
201 self.rope
202 .byte(offset.into().get().min(self.len_bytes().saturating_sub(1)))
203 }
204
205 pub fn byte_at(&self, offset: impl Into<id::ByteOffset>) -> Option<u8> {
206 let off = offset.into().get();
207 if off < self.len_bytes() {
208 Some(self.rope.byte(off))
209 } else {
210 None
211 }
212 }
213
214 pub fn is_boundary(&self, offset: impl Into<id::ByteOffset>) -> bool {
219 let off = offset.into().get();
220 if off == 0 || off == self.len_bytes() {
221 return true;
222 }
223 if off > self.len_bytes() {
224 return false;
225 }
226 match self.rope.try_byte_to_char(off) {
227 Ok(c) => self.rope.try_char_to_byte(c).is_ok_and(|b| b == off),
228 Err(_) => false,
229 }
230 }
231
232 pub fn clamp_boundary(&self, offset: impl Into<id::ByteOffset>) -> usize {
235 let mut offset = offset.into().get().min(self.len_bytes());
236 while offset > 0 && !self.is_boundary(offset) {
237 offset -= 1;
238 }
239 offset
240 }
241
242 pub fn ceil_boundary(&self, offset: impl Into<id::ByteOffset>) -> usize {
247 let mut offset = offset.into().get().min(self.len_bytes());
248 while offset < self.len_bytes() && !self.is_boundary(offset) {
249 offset += 1;
250 }
251 offset
252 }
253
254 pub fn slice_string(&self, range: Range) -> String {
257 let start = range.start.min(self.len_bytes());
258 let end = range.end.min(self.len_bytes());
259 self.rope.byte_slice(start..end.max(start)).to_string()
260 }
261
262 pub fn apply_history(&mut self, ops: Vec<Edit>) {
264 self.replaying = true;
265 for op in ops {
266 match op.kind {
267 EditKind::Insert => {
268 let at = self.clamp_boundary(op.at.min(self.len_bytes()));
269 self.rope.insert(self.rope.byte_to_char(at), &op.text);
270 }
271 EditKind::Delete => {
272 let end = self.clamp_boundary((op.at + op.text.len()).min(self.len_bytes()));
275 let start = self.clamp_boundary(op.at.min(end));
276 if start < end {
277 self.rope
278 .remove(self.rope.byte_to_char(start)..self.rope.byte_to_char(end));
279 }
280 }
281 }
282 }
283 self.replaying = false;
284 self.dirty = true;
285 self.epoch += 1;
286 }
287
288 pub fn replace_all(&mut self, text: &str) {
292 if self.readonly {
293 return;
294 }
295 self.replace_all_system(text);
296 }
297
298 pub fn replace_all_system(&mut self, text: &str) {
302 self.rope = Rope::from_str(text);
303 self.epoch += 1;
304 }
305
306 pub fn delete(&mut self, range: Range) -> String {
310 if self.readonly && !self.replaying {
311 return String::new();
312 }
313 let start = self.clamp_boundary(range.start.min(self.len_bytes()));
315 let end = self.clamp_boundary(range.end.min(self.len_bytes()));
316 if start >= end {
317 return String::new();
318 }
319 let text = self.rope.byte_slice(start..end).to_string();
320 let cstart = self.rope.byte_to_char(start);
322 let cend = self.rope.byte_to_char(end);
323 self.rope.remove(cstart..cend);
324 self.dirty = true;
325 self.epoch += 1;
326 if !self.replaying && !self.readonly {
327 self.history.record(
328 Edit {
329 at: range.start,
330 text: text.clone(),
331 kind: EditKind::Insert,
332 },
333 Edit {
334 at: range.start,
335 text: text.clone(),
336 kind: EditKind::Delete,
337 },
338 );
339 }
340 text
341 }
342
343 pub fn insert(&mut self, at: impl Into<id::ByteOffset>, text: &str) {
344 if self.readonly && !self.replaying {
345 return;
346 }
347 let at = self.clamp_boundary(at);
348 self.rope.insert(self.rope.byte_to_char(at), text);
349 self.dirty = true;
350 self.epoch += 1;
351 if !self.replaying && !self.readonly {
352 self.history.record(
353 Edit {
354 at,
355 text: text.into(),
356 kind: EditKind::Delete,
357 },
358 Edit {
359 at,
360 text: text.into(),
361 kind: EditKind::Insert,
362 },
363 );
364 }
365 }
366
367 pub fn line_text(&self, line: impl Into<id::LineIndex>) -> String {
368 let line = line.into().get();
369 let start = self.line_start(line);
370 let end = self.line_end(line);
371 self.rope.byte_slice(start..end).to_string()
372 }
373}
374
375fn write_atomic(target: &std::path::Path, contents: &str) -> std::io::Result<()> {
378 let tmp = target.with_file_name(format!(
379 ".strop-tmp-{}-{}",
380 std::process::id(),
381 target.file_name().and_then(|n| n.to_str()).unwrap_or("x")
382 ));
383 std::fs::write(&tmp, contents)?;
384 if let Ok(meta) = std::fs::metadata(target) {
385 let _ = std::fs::set_permissions(&tmp, meta.permissions());
387 }
388 std::fs::rename(&tmp, target)
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
395pub struct InputEdit {
396 pub start_byte: usize,
397 pub old_end_byte: usize,
398 pub new_end_byte: usize,
399 pub start_point: (usize, usize),
400 pub old_end_point: (usize, usize),
401 pub new_end_point: (usize, usize),
402}
403
404impl Buffer {
405 pub fn point_of(&self, offset: usize) -> (usize, usize) {
407 let offset = offset.min(self.len_bytes());
408 (self.line_of(offset), self.col_of(offset))
409 }
410
411 fn point_extent(text: &str) -> (usize, usize) {
413 let lines = text.bytes().filter(|b| *b == b'\n').count();
414 let col = if lines == 0 {
415 text.len()
416 } else {
417 text.rsplit('\n').next().map(str::len).unwrap_or(0)
418 };
419 (lines, col)
420 }
421
422 pub fn input_edit_of(&self, op: &crate::history::Edit) -> InputEdit {
425 let start_point = self.point_of(op.at);
426 let extent = Self::point_extent(&op.text);
427 match op.kind {
428 EditKind::Insert => InputEdit {
429 start_byte: op.at,
430 old_end_byte: op.at,
431 new_end_byte: op.at + op.text.len(),
432 start_point,
433 old_end_point: start_point,
434 new_end_point: if extent.0 == 0 {
437 (start_point.0, start_point.1 + extent.1)
438 } else {
439 (start_point.0 + extent.0, extent.1)
440 },
441 },
442 EditKind::Delete => InputEdit {
443 start_byte: op.at,
444 old_end_byte: op.at + op.text.len(),
445 new_end_byte: op.at,
446 start_point,
447 old_end_point: if extent.0 == 0 {
448 (start_point.0, start_point.1 + extent.1)
449 } else {
450 (start_point.0 + extent.0, extent.1)
451 },
452 new_end_point: start_point,
453 },
454 }
455 }
456}
457
458#[cfg(test)]
459mod safety_tests {
460 use super::*;
461
462 #[test]
463 fn save_refuses_external_change_unless_forced() {
464 let dir = tempfile::tempdir().unwrap();
465 let f = dir.path().join("f.txt");
466 std::fs::write(&f, "original\n").unwrap();
467 let mut b = Buffer::open(f.to_str().unwrap()).unwrap();
468 b.insert(id::ByteOffset::new(0), "mine ");
469 std::thread::sleep(std::time::Duration::from_millis(5));
471 std::fs::write(&f, "theirs\n").unwrap();
472 let err = b.save(false).unwrap_err();
473 assert!(err.to_string().contains("changed on disk"));
474 assert_eq!(std::fs::read_to_string(&f).unwrap(), "theirs\n");
475 b.save(true).unwrap(); assert_eq!(std::fs::read_to_string(&f).unwrap(), "mine original\n");
477 assert!(!b.dirty);
478 }
479
480 #[test]
481 fn save_is_atomic_and_keeps_permissions() {
482 use std::os::unix::fs::PermissionsExt;
483 let dir = tempfile::tempdir().unwrap();
484 let f = dir.path().join("x.sh");
485 std::fs::write(&f, "#!/bin/sh\n").unwrap();
486 std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o750)).unwrap();
487 let mut b = Buffer::open(f.to_str().unwrap()).unwrap();
488 b.insert(id::ByteOffset::new(b.len_bytes()), "echo hi\n");
489 b.save(false).unwrap();
490 assert_eq!(std::fs::read_to_string(&f).unwrap(), "#!/bin/sh\necho hi\n");
491 let mode = std::fs::metadata(&f).unwrap().permissions().mode() & 0o777;
492 assert_eq!(mode, 0o750, "permissions survive the swap");
493 assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1);
495 }
496
497 #[test]
498 fn readonly_refuses_mutation_at_the_boundary() {
499 let mut b = Buffer::from_text("abc\n");
501 b.readonly = true;
502 b.insert(id::ByteOffset::new(0), "nope");
503 let gone = b.delete(Range::charwise(0, 2));
504 assert_eq!(gone, "");
505 assert_eq!(b.rope.to_string(), "abc\n", "untouched");
506 b.replace_all_system("gen\n");
508 assert_eq!(b.rope.to_string(), "gen\n");
509 }
510 #[test]
511 fn non_utf8_filename_opens_and_roundtrips() {
512 use std::os::unix::ffi::OsStrExt;
515 let dir = tempfile::tempdir().unwrap();
516 let weird = dir
517 .path()
518 .join(std::ffi::OsStr::from_bytes(b"weird-\xff.rs"));
519 std::fs::write(&weird, "fn main() {}\n").unwrap();
520 let mut b = Buffer::open(&weird).unwrap();
521 assert_eq!(b.path.as_deref(), Some(weird.as_path()));
522 b.insert(0, "// x\n");
523 b.save(false).unwrap();
524 assert!(std::fs::read_to_string(&weird).unwrap().starts_with("// x"));
525 }
526}