redox_core/buffer/text_buffer/core.rs
1//! Core `TextBuffer` definition and constructors.
2//!
3//! Note to self: this file intentionally only does a couple of things:
4//! - It defines the `TextBuffer` type and its invariants.
5//! - It provides basic constructors and low-level rope access.
6//!
7//! Everything else (line indexing, movement, slicing, editing) should live in
8//! sibling modules as additional `impl TextBuffer` blocks.
9
10use anyhow::{Context as _, Result};
11use ropey::Rope;
12
13/// A Ropey-backed text buffer.
14///
15/// Invariants and conventions:
16/// - The backing store is a `ropey::Rope`.
17/// - Public APIs should generally speak in char indices (Unicode scalar
18/// value offsets) and logical positions (line/col in chars) because Ropey’s
19/// safe indexing APIs are char-based.
20/// - Byte indexing can be supported where needed, but should not be the primary
21/// index type for the editor core.
22///
23/// Higher-level editor state (modes, undo, viewports, etc.) should be built on
24/// top of this type rather than embedded inside it.
25#[derive(Debug, Clone)]
26pub struct TextBuffer {
27 pub(super) rope: Rope,
28}
29
30impl Default for TextBuffer {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl TextBuffer {
37 /// Create an empty buffer
38 #[inline]
39 pub fn new() -> Self {
40 Self { rope: Rope::new() }
41 }
42
43 /// Create a buffer from UTF-8 text
44 #[inline]
45 pub fn from_str(s: &str) -> Self {
46 Self {
47 rope: Rope::from_str(s),
48 }
49 }
50
51 /// Load a file as UTF-8 and create a buffer.
52 ///
53 /// This is intentionally simple for now. It just:
54 /// - reads the whole file into memory
55 /// - requires valid UTF-8
56 ///
57 /// NOTE: If/when I add encoding detection or incremental IO, those should likely
58 /// live in a separate IO-focused module.
59 pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self> {
60 let path = path.as_ref();
61
62 let bytes = std::fs::read(path)
63 .with_context(|| format!("failed to read file: {}", path.to_string_lossy()))?;
64
65 let s = String::from_utf8(bytes)
66 .with_context(|| format!("file is not valid UTF-8: {}", path.to_string_lossy()))?;
67
68 Ok(Self::from_str(&s))
69 }
70
71 /// Access the underlying rope.
72 ///
73 /// Prefer higher-level APIs in other modules for most editor operations.
74 #[inline]
75 pub fn rope(&self) -> &Rope {
76 &self.rope
77 }
78
79 /// Mutable access to the underlying rope (use this sparingly!)
80 ///
81 /// Prefer dedicated editing APIs so invariants and future bookkeeping (eg.
82 /// undo/redo, marks, spans) remain easy to maintain.
83 #[inline]
84 pub fn rope_mut(&mut self) -> &mut Rope {
85 &mut self.rope
86 }
87
88 /// Total number of chars in the buffer.
89 ///
90 /// Kept here because it is a fundamental primitive used by most other modules.
91 #[inline]
92 pub fn len_chars(&self) -> usize {
93 self.rope.len_chars()
94 }
95
96 /// Whether or not the buffer contains zero characters (is empty).
97 #[inline]
98 pub fn is_empty(&self) -> bool {
99 self.rope.len_chars() == 0
100 }
101}