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