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