1mod io;
5pub use io::{SavePlan, 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
20#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum ReadonlyReason {
27 Filesystem,
29 Command,
31 RemoteAuthority,
33 Container,
35 GitSurface,
37 Output,
39 DirectoryListing,
41 DirectoryOperation,
43 CollectionProjection,
45 RecoveryCheckpoint,
47 OutsideWorkspace,
49}
50
51pub struct Buffer {
53 pub(crate) trace_identity: BufferTraceId,
54 rope: Rope,
55 pub path: Option<std::path::PathBuf>,
59 pub dirty: bool,
60 epoch: u64,
62 pub readonly: bool,
64 pub readonly_reason: Option<ReadonlyReason>,
66 pub name: Option<String>,
69 history: History,
72 changes: Vec<Change>,
73 disk_stamp: Option<std::time::SystemTime>,
75 file_identity: Option<std::path::PathBuf>,
76 line_layouts: layout_cache::LineLayouts,
77}
78
79impl Buffer {
80 pub fn set_readonly(&mut self, reason: ReadonlyReason) {
84 if !self.readonly {
85 self.readonly_reason = Some(reason);
86 }
87 self.readonly = true;
88 }
89
90 pub fn clear_readonly(&mut self) {
93 self.readonly = false;
94 self.readonly_reason = None;
95 }
96
97 pub fn text(&self) -> &Rope {
98 &self.rope
99 }
100 pub fn snapshot(&self) -> Rope {
101 self.rope.clone()
102 }
103
104 pub fn text_excerpt(&self) -> (String, bool) {
108 let rope = &self.rope;
109 if rope.len_bytes() <= strop_trace::MAX_EXCERPT_BYTES {
110 return (rope.to_string(), false);
111 }
112 let mut end = strop_trace::MAX_EXCERPT_BYTES;
113 while end > 0 && (rope.byte(end - 1) & 0xC0) == 0x80 {
114 end -= 1;
115 }
116 (
117 rope.get_byte_slice(..end)
118 .map_or_else(String::new, |head| head.to_string()),
119 true,
120 )
121 }
122 pub fn history(&self) -> &History {
123 &self.history
124 }
125 pub fn revision(&self) -> id::BufferRevision {
126 id::BufferRevision::new(self.epoch)
127 }
128 pub fn file_identity(&self) -> Option<&std::path::Path> {
129 self.file_identity.as_deref()
130 }
131
132 pub fn disk_stamp(&self) -> Option<std::time::SystemTime> {
135 self.disk_stamp
136 }
137 pub fn restore_history(
138 &mut self,
139 history: History,
140 ) -> Result<(), crate::history::HistoryError> {
141 history.validate_for(&self.rope)?;
142 self.adopt_history(history);
143 Ok(())
144 }
145
146 pub fn from_text(text: &str) -> Self {
147 Self::from_snapshot(Rope::from_str(text))
148 }
149
150 pub fn from_snapshot(rope: Rope) -> Self {
153 Self {
154 trace_identity: BufferTraceId::next(),
155 rope,
156 path: None,
157 dirty: false,
158 readonly_reason: None,
159 epoch: 0,
160 readonly: false,
161 name: None,
162 history: History::default(),
163 changes: Vec::new(),
164 disk_stamp: None,
165 file_identity: None,
166 line_layouts: layout_cache::LineLayouts::default(),
167 }
168 }
169
170 pub fn open(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
175 let path = path.as_ref();
176 let (rope, disk_stamp, writable) = match std::fs::File::open(path) {
177 Ok(file) => {
178 let metadata = file.metadata()?;
179 let stamp = metadata.modified()?;
180 (
181 Rope::from_reader(file)?,
182 Some(stamp),
183 !metadata.permissions().readonly(),
184 )
185 }
186 Err(e) if e.kind() == std::io::ErrorKind::NotFound => (Rope::new(), None, true),
187 Err(e) => return Err(e),
188 };
189 let mut buffer = Self {
190 trace_identity: BufferTraceId::next(),
191 rope,
192 path: Some(path.to_path_buf()),
193 dirty: false,
194 epoch: 0,
195 readonly: false,
196 readonly_reason: None,
197 name: None,
198 history: History::default(),
199 changes: Vec::new(),
200 disk_stamp,
201 file_identity: Some(std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())),
202 line_layouts: layout_cache::LineLayouts::default(),
203 };
204 if !writable {
205 buffer.set_readonly(ReadonlyReason::Filesystem);
206 }
207 Ok(buffer)
208 }
209
210 pub fn from_read(
220 path: std::path::PathBuf,
221 rope: Rope,
222 disk_stamp: Option<std::time::SystemTime>,
223 canonical: std::path::PathBuf,
224 writable: bool,
225 ) -> Self {
226 let mut buffer = Self {
227 trace_identity: BufferTraceId::next(),
228 rope,
229 path: Some(path),
230 dirty: false,
231 epoch: 0,
232 readonly: false,
233 readonly_reason: None,
234 name: None,
235 history: History::default(),
236 changes: Vec::new(),
237 disk_stamp,
238 file_identity: Some(canonical),
239 line_layouts: layout_cache::LineLayouts::default(),
240 };
241 if !writable {
242 buffer.set_readonly(ReadonlyReason::Filesystem);
243 }
244 buffer
245 }
246
247 pub fn cell_col_with_tab(
252 &self,
253 offset: impl Into<id::ByteOffset>,
254 tab: usize,
255 ) -> id::DisplayColumn {
256 match self.column_from_layout(offset.into().get(), tab, false) {
257 Some(column) => column,
258 None => unreachable!("an unbounded layout projection always completes"),
259 }
260 }
261
262 pub fn len_bytes(&self) -> usize {
263 self.rope.len_bytes()
264 }
265 pub fn len_lines(&self) -> usize {
266 self.rope.len_lines()
267 }
268
269 pub fn last_content_line(&self) -> usize {
272 let mut l = self.len_lines().saturating_sub(1);
273 if self.len_bytes() > 0 && self.byte(self.len_bytes() - 1) == b'\n' && l > 0 {
274 l -= 1;
275 }
276 l
277 }
278
279 pub fn line_start(&self, line: impl Into<id::LineIndex>) -> usize {
281 self.rope
282 .line_to_byte(line.into().get().min(self.len_lines().saturating_sub(1)))
283 }
284
285 pub fn line_end(&self, line: impl Into<id::LineIndex>) -> usize {
287 let line = line.into().get();
288 let start = self.line_start(line);
289 let mut end = self.line_start((line + 1).min(self.len_lines().saturating_sub(1)));
290 if line + 1 >= self.len_lines() {
291 end = self.len_bytes();
292 }
293 if end > start && self.byte(end - 1) == b'\n' {
295 end -= 1;
296 if end > start && self.byte(end - 1) == b'\r' {
297 end -= 1;
298 }
299 }
300 end
301 }
302
303 pub fn line_of(&self, offset: impl Into<id::ByteOffset>) -> usize {
304 self.rope
305 .byte_to_line(offset.into().get().min(self.len_bytes()))
306 }
307
308 pub fn col_of(&self, offset: impl Into<id::ByteOffset>) -> usize {
310 let offset = offset.into();
311 offset.get() - self.line_start(self.line_of(offset))
312 }
313 pub fn byte(&self, offset: impl Into<id::ByteOffset>) -> u8 {
318 if self.len_bytes() == 0 {
319 return 0;
320 }
321 self.rope
322 .byte(offset.into().get().min(self.len_bytes().saturating_sub(1)))
323 }
324
325 pub fn byte_at(&self, offset: impl Into<id::ByteOffset>) -> Option<u8> {
326 let off = offset.into().get();
327 if off < self.len_bytes() {
328 Some(self.rope.byte(off))
329 } else {
330 None
331 }
332 }
333
334 pub fn is_boundary(&self, offset: impl Into<id::ByteOffset>) -> bool {
339 let off = offset.into().get();
340 if off == 0 || off == self.len_bytes() {
341 return true;
342 }
343 if off > self.len_bytes() {
344 return false;
345 }
346 match self.rope.try_byte_to_char(off) {
347 Ok(c) => self.rope.try_char_to_byte(c).is_ok_and(|b| b == off),
348 Err(_) => false,
349 }
350 }
351
352 pub fn clamp_boundary(&self, offset: impl Into<id::ByteOffset>) -> usize {
355 let mut offset = offset.into().get().min(self.len_bytes());
356 while offset > 0 && !self.is_boundary(offset) {
357 offset -= 1;
358 }
359 offset
360 }
361
362 pub fn ceil_boundary(&self, offset: impl Into<id::ByteOffset>) -> usize {
367 let mut offset = offset.into().get().min(self.len_bytes());
368 while offset < self.len_bytes() && !self.is_boundary(offset) {
369 offset += 1;
370 }
371 offset
372 }
373
374 pub fn slice_string(&self, range: Range) -> String {
377 let start = self.clamp_boundary(range.start);
378 let end = self.clamp_boundary(range.end);
379 self.rope.byte_slice(start..end.max(start)).to_string()
380 }
381
382 pub fn line_text(&self, line: impl Into<id::LineIndex>) -> String {
383 let line = line.into().get();
384 let start = self.line_start(line);
385 let end = self.line_end(line);
386 self.rope.byte_slice(start..end).to_string()
387 }
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
392pub struct InputEdit {
393 pub start_byte: usize,
394 pub old_end_byte: usize,
395 pub new_end_byte: usize,
396 pub start_point: (usize, usize),
397 pub old_end_point: (usize, usize),
398 pub new_end_point: (usize, usize),
399}
400
401impl Buffer {
402 pub fn point_of(&self, offset: usize) -> (usize, usize) {
404 let offset = offset.min(self.len_bytes());
405 (self.line_of(offset), self.col_of(offset))
406 }
407
408 fn point_extent(text: &str) -> (usize, usize) {
410 let lines = text.bytes().filter(|b| *b == b'\n').count();
411 let col = if lines == 0 {
412 text.len()
413 } else {
414 text.rsplit('\n').next().map(str::len).unwrap_or(0)
415 };
416 (lines, col)
417 }
418}
419
420#[cfg(test)]
421mod safety_tests {
422 use super::*;
423
424 #[test]
425 fn save_refuses_external_change_unless_forced() {
426 let dir = tempfile::tempdir().unwrap();
427 let f = dir.path().join("f.txt");
428 std::fs::write(&f, "original\n").unwrap();
429 let mut b = Buffer::open(f.to_str().unwrap()).unwrap();
430 b.edit().insert(id::ByteOffset::new(0), "mine ").unwrap();
431 std::fs::write(&f, "theirs\n").unwrap();
433 std::fs::File::options()
434 .write(true)
435 .open(&f)
436 .unwrap()
437 .set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(123))
438 .unwrap();
439 let err = b.prepare_save(None, false).unwrap().execute().unwrap_err();
440 assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
441 assert_eq!(std::fs::read_to_string(&f).unwrap(), "theirs\n");
442 let receipt = b.prepare_save(None, true).unwrap().execute().unwrap();
443 assert!(b.accept_save(receipt));
444 assert_eq!(std::fs::read_to_string(&f).unwrap(), "mine original\n");
445 assert!(!b.dirty);
446 }
447
448 #[test]
449 fn save_is_atomic_and_keeps_permissions() {
450 use std::os::unix::fs::PermissionsExt;
451 let dir = tempfile::tempdir().unwrap();
452 let f = dir.path().join("x.sh");
453 std::fs::write(&f, "#!/bin/sh\n").unwrap();
454 std::fs::set_permissions(&f, std::fs::Permissions::from_mode(0o750)).unwrap();
455 let mut b = Buffer::open(f.to_str().unwrap()).unwrap();
456 let end = b.len_bytes();
457 b.edit()
458 .insert(id::ByteOffset::new(end), "echo hi\n")
459 .unwrap();
460 let receipt = b.prepare_save(None, false).unwrap().execute().unwrap();
461 assert!(b.accept_save(receipt));
462 assert_eq!(std::fs::read_to_string(&f).unwrap(), "#!/bin/sh\necho hi\n");
463 let mode = std::fs::metadata(&f).unwrap().permissions().mode() & 0o777;
464 assert_eq!(mode, 0o750, "permissions survive the swap");
465 assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 1);
467 }
468
469 #[test]
470 fn readonly_refuses_mutation_at_the_boundary() {
471 let mut b = Buffer::from_text("abc\n");
473 b.readonly = true;
474 assert_eq!(b.edit().insert(0, "nope"), Err(EditError::ReadOnly));
475 assert_eq!(
476 b.edit().delete(Range::charwise(0, 2)),
477 Err(EditError::ReadOnly)
478 );
479 assert_eq!(b.rope.to_string(), "abc\n", "untouched");
480 b.system_edit().replace_all("gen\n").unwrap();
482 assert_eq!(b.rope.to_string(), "gen\n");
483 }
484 #[test]
485 fn non_utf8_filename_opens_and_roundtrips() {
486 use std::os::unix::ffi::OsStrExt;
489 let dir = tempfile::tempdir().unwrap();
490 let weird = dir
491 .path()
492 .join(std::ffi::OsStr::from_bytes(b"weird-\xff.rs"));
493 std::fs::write(&weird, "fn main() {}\n").unwrap();
494 let mut b = Buffer::open(&weird).unwrap();
495 assert_eq!(b.path.as_deref(), Some(weird.as_path()));
496 b.edit().insert(0, "// x\n").unwrap();
497 let receipt = b.prepare_save(None, false).unwrap().execute().unwrap();
498 assert!(b.accept_save(receipt));
499 assert_eq!(
500 std::fs::read_to_string(&weird).unwrap(),
501 "// x\nfn main() {}\n"
502 );
503 }
504}