1pub mod history;
5pub mod id;
6pub mod selection;
7
8use history::{Edit, EditKind, History};
9use ropey::Rope;
10
11pub struct Buffer {
13 pub rope: Rope,
14 pub path: Option<String>,
15 pub dirty: bool,
16 pub epoch: u64,
18 pub readonly: bool,
20 pub name: Option<String>,
23 pub history: History,
26 pub replaying: bool,
28 disk_stamp: Option<std::time::SystemTime>,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum MotionShape {
37 Characterwise { inclusive: bool },
38 Linewise,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Range {
45 pub start: usize,
46 pub end: usize,
47 pub shape: MotionShape,
48}
49
50impl Range {
51 pub fn charwise(start: impl Into<id::ByteOffset>, end: impl Into<id::ByteOffset>) -> Self {
52 let (start, end) = (start.into().get(), end.into().get());
53 debug_assert!(start <= end);
54 Self {
55 start,
56 end,
57 shape: MotionShape::Characterwise { inclusive: false },
58 }
59 }
60 pub fn linewise(start: impl Into<id::ByteOffset>, end: impl Into<id::ByteOffset>) -> Self {
61 let (start, end) = (start.into().get(), end.into().get());
62 debug_assert!(start <= end);
63 Self {
64 start,
65 end,
66 shape: MotionShape::Linewise,
67 }
68 }
69 pub fn is_linewise(&self) -> bool {
70 matches!(self.shape, MotionShape::Linewise)
71 }
72 pub fn with_inclusive(mut self, inclusive: bool) -> Self {
74 if let MotionShape::Characterwise { inclusive: i } = &mut self.shape {
75 *i = inclusive;
76 }
77 self
78 }
79 pub fn inclusive(&self) -> bool {
80 matches!(self.shape, MotionShape::Characterwise { inclusive: true })
81 }
82 pub fn len(&self) -> usize {
84 self.end - self.start
85 }
86 pub fn is_empty(&self) -> bool {
87 self.start == self.end
88 }
89}
90
91impl Buffer {
92 pub fn from_text(text: &str) -> Self {
93 Self {
94 rope: Rope::from_str(text),
95 path: None,
96 dirty: false,
97 epoch: 0,
98 readonly: false,
99 name: None,
100 history: History::default(),
101 replaying: false,
102 disk_stamp: None,
103 }
104 }
105
106 pub fn open(path: &str) -> std::io::Result<Self> {
109 let text = match std::fs::read_to_string(path) {
110 Ok(t) => t,
111 Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
112 Err(e) => return Err(e),
113 };
114 let disk_stamp = std::fs::metadata(path).and_then(|m| m.modified()).ok();
115 Ok(Self {
116 rope: Rope::from_str(&text),
117 path: Some(path.to_string()),
118 dirty: false,
119 epoch: 0,
120 readonly: false,
121 name: None,
122 history: History::default(),
123 replaying: false,
124 disk_stamp,
125 })
126 }
127
128 pub fn save(&mut self, force: bool) -> std::io::Result<()> {
132 let Some(path) = self.path.clone() else {
133 return Err(std::io::Error::new(
136 std::io::ErrorKind::NotFound,
137 "no file name — :w {path} to name it",
138 ));
139 };
140 let current = std::fs::metadata(&path).and_then(|m| m.modified()).ok();
141 if !force && current.is_some() && current != self.disk_stamp {
142 return Err(std::io::Error::new(
143 std::io::ErrorKind::PermissionDenied,
144 "file changed on disk — :w! to force",
145 ));
146 }
147 let target = std::path::Path::new(&path);
148 let tmp = target.with_file_name(format!(
149 ".strop-tmp-{}-{}",
150 std::process::id(),
151 target.file_name().and_then(|n| n.to_str()).unwrap_or("x")
152 ));
153 std::fs::write(&tmp, self.rope.to_string())?;
154 if let Ok(meta) = std::fs::metadata(target) {
155 let _ = std::fs::set_permissions(&tmp, meta.permissions());
157 }
158 std::fs::rename(&tmp, target)?;
159 self.disk_stamp = std::fs::metadata(&path).and_then(|m| m.modified()).ok();
160 self.dirty = false;
161 Ok(())
162 }
163
164 pub fn save_as(&mut self, path: &str) -> std::io::Result<()> {
167 self.path = Some(path.to_string());
168 self.disk_stamp = None; self.save(true)
170 }
171 pub fn len_bytes(&self) -> usize {
172 self.rope.len_bytes()
173 }
174 pub fn len_lines(&self) -> usize {
175 self.rope.len_lines()
176 }
177
178 pub fn last_content_line(&self) -> usize {
181 let mut l = self.len_lines().saturating_sub(1);
182 if self.len_bytes() > 0 && self.byte(self.len_bytes() - 1) == b'\n' && l > 0 {
183 l -= 1;
184 }
185 l
186 }
187
188 pub fn line_start(&self, line: impl Into<id::LineIndex>) -> usize {
190 self.rope
191 .line_to_byte(line.into().get().min(self.len_lines().saturating_sub(1)))
192 }
193
194 pub fn line_end(&self, line: impl Into<id::LineIndex>) -> usize {
196 let line = line.into().get();
197 let start = self.line_start(line);
198 let mut end = self.line_start((line + 1).min(self.len_lines().saturating_sub(1)));
199 if line + 1 >= self.len_lines() {
200 end = self.len_bytes();
201 }
202 if end > start && self.byte(end - 1) == b'\n' {
204 end -= 1;
205 }
206 end
207 }
208
209 pub fn line_of(&self, offset: impl Into<id::ByteOffset>) -> usize {
210 self.rope
211 .byte_to_line(offset.into().get().min(self.len_bytes()))
212 }
213
214 pub fn col_of(&self, offset: impl Into<id::ByteOffset>) -> usize {
216 let offset = offset.into();
217 offset.get() - self.line_start(self.line_of(offset))
218 }
219 pub fn byte(&self, offset: impl Into<id::ByteOffset>) -> u8 {
224 if self.len_bytes() == 0 {
225 return 0;
226 }
227 self.rope
228 .byte(offset.into().get().min(self.len_bytes().saturating_sub(1)))
229 }
230
231 pub fn byte_at(&self, offset: impl Into<id::ByteOffset>) -> Option<u8> {
232 let off = offset.into().get();
233 if off < self.len_bytes() {
234 Some(self.rope.byte(off))
235 } else {
236 None
237 }
238 }
239
240 pub fn is_boundary(&self, offset: impl Into<id::ByteOffset>) -> bool {
245 let off = offset.into().get();
246 if off == 0 || off == self.len_bytes() {
247 return true;
248 }
249 if off > self.len_bytes() {
250 return false;
251 }
252 match self.rope.try_byte_to_char(off) {
253 Ok(c) => self.rope.try_char_to_byte(c).is_ok_and(|b| b == off),
254 Err(_) => false,
255 }
256 }
257
258 pub fn clamp_boundary(&self, offset: impl Into<id::ByteOffset>) -> usize {
261 let mut offset = offset.into().get().min(self.len_bytes());
262 while offset > 0 && !self.is_boundary(offset) {
263 offset -= 1;
264 }
265 offset
266 }
267
268 pub fn ceil_boundary(&self, offset: impl Into<id::ByteOffset>) -> usize {
273 let mut offset = offset.into().get().min(self.len_bytes());
274 while offset < self.len_bytes() && !self.is_boundary(offset) {
275 offset += 1;
276 }
277 offset
278 }
279
280 pub fn slice_string(&self, range: Range) -> String {
283 let start = range.start.min(self.len_bytes());
284 let end = range.end.min(self.len_bytes());
285 self.rope.byte_slice(start..end.max(start)).to_string()
286 }
287
288 pub fn apply_history(&mut self, ops: Vec<Edit>) {
290 self.replaying = true;
291 for op in ops {
292 match op.kind {
293 EditKind::Insert => {
294 let at = self.clamp_boundary(op.at.min(self.len_bytes()));
295 self.rope.insert(self.rope.byte_to_char(at), &op.text);
296 }
297 EditKind::Delete => {
298 let end = self.clamp_boundary((op.at + op.text.len()).min(self.len_bytes()));
301 let start = self.clamp_boundary(op.at.min(end));
302 if start < end {
303 self.rope
304 .remove(self.rope.byte_to_char(start)..self.rope.byte_to_char(end));
305 }
306 }
307 }
308 }
309 self.replaying = false;
310 self.dirty = true;
311 self.epoch += 1;
312 }
313
314 pub fn replace_all(&mut self, text: &str) {
318 if self.readonly {
319 return;
320 }
321 self.replace_all_system(text);
322 }
323
324 pub fn replace_all_system(&mut self, text: &str) {
328 self.rope = Rope::from_str(text);
329 self.epoch += 1;
330 }
331
332 pub fn delete(&mut self, range: Range) -> String {
336 if self.readonly && !self.replaying {
337 return String::new();
338 }
339 let start = self.clamp_boundary(range.start.min(self.len_bytes()));
341 let end = self.clamp_boundary(range.end.min(self.len_bytes()));
342 if start >= end {
343 return String::new();
344 }
345 let text = self.rope.byte_slice(start..end).to_string();
346 let cstart = self.rope.byte_to_char(start);
348 let cend = self.rope.byte_to_char(end);
349 self.rope.remove(cstart..cend);
350 self.dirty = true;
351 self.epoch += 1;
352 if !self.replaying && !self.readonly {
353 self.history.record(
354 Edit {
355 at: range.start,
356 text: text.clone(),
357 kind: EditKind::Insert,
358 },
359 Edit {
360 at: range.start,
361 text: text.clone(),
362 kind: EditKind::Delete,
363 },
364 );
365 }
366 text
367 }
368
369 pub fn insert(&mut self, at: impl Into<id::ByteOffset>, text: &str) {
370 if self.readonly && !self.replaying {
371 return;
372 }
373 let at = self.clamp_boundary(at);
374 self.rope.insert(self.rope.byte_to_char(at), text);
375 self.dirty = true;
376 self.epoch += 1;
377 if !self.replaying && !self.readonly {
378 self.history.record(
379 Edit {
380 at,
381 text: text.into(),
382 kind: EditKind::Delete,
383 },
384 Edit {
385 at,
386 text: text.into(),
387 kind: EditKind::Insert,
388 },
389 );
390 }
391 }
392
393 pub fn line_text(&self, line: impl Into<id::LineIndex>) -> String {
394 let line = line.into().get();
395 let start = self.line_start(line);
396 let end = self.line_end(line);
397 self.rope.byte_slice(start..end).to_string()
398 }
399}
400
401#[cfg(test)]
402mod safety_tests {
403 use super::*;
404
405 #[test]
406 fn save_refuses_external_change_unless_forced() {
407 let dir = tempfile::tempdir().unwrap();
408 let f = dir.path().join("f.txt");
409 std::fs::write(&f, "original\n").unwrap();
410 let mut b = Buffer::open(f.to_str().unwrap()).unwrap();
411 b.insert(id::ByteOffset::new(0), "mine ");
412 std::thread::sleep(std::time::Duration::from_millis(5));
414 std::fs::write(&f, "theirs\n").unwrap();
415 let err = b.save(false).unwrap_err();
416 assert!(err.to_string().contains("changed on disk"));
417 assert_eq!(std::fs::read_to_string(&f).unwrap(), "theirs\n");
418 b.save(true).unwrap(); assert_eq!(std::fs::read_to_string(&f).unwrap(), "mine original\n");
420 assert!(!b.dirty);
421 }
422
423 #[test]
424 fn save_is_atomic_and_keeps_permissions() {
425 use std::os::unix::fs::PermissionsExt;
426 let dir = tempfile::tempdir().unwrap();
427 let f = dir.path().join("x.sh");
428 std::fs::write(&f, "#!/bin/sh\n").unwrap();
429 std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o750)).unwrap();
430 let mut b = Buffer::open(f.to_str().unwrap()).unwrap();
431 b.insert(id::ByteOffset::new(b.len_bytes()), "echo hi\n");
432 b.save(false).unwrap();
433 assert_eq!(std::fs::read_to_string(&f).unwrap(), "#!/bin/sh\necho hi\n");
434 let mode = std::fs::metadata(&f).unwrap().permissions().mode() & 0o777;
435 assert_eq!(mode, 0o750, "permissions survive the swap");
436 assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1);
438 }
439
440 #[test]
441 fn readonly_refuses_mutation_at_the_boundary() {
442 let mut b = Buffer::from_text("abc\n");
444 b.readonly = true;
445 b.insert(id::ByteOffset::new(0), "nope");
446 let gone = b.delete(Range::charwise(0, 2));
447 assert_eq!(gone, "");
448 assert_eq!(b.rope.to_string(), "abc\n", "untouched");
449 b.replace_all_system("gen\n");
451 assert_eq!(b.rope.to_string(), "gen\n");
452 }
453}