reovim_kernel/mm/snapshot.rs
1//! Read-only buffer snapshot for lock-free access.
2//!
3//! This module provides `BufferSnapshot`, a read-only capture of buffer state
4//! that can be safely shared across threads without locking.
5//!
6//! # Cursor Isolation (#471)
7//!
8//! Cursor must be passed explicitly to `from_buffer()` since cursor is now
9//! per-client state in Window, not Buffer.
10//!
11//! # Design Philosophy
12//!
13//! Following the kernel "mechanism, not policy" principle:
14//! - Snapshot captures state at a point in time
15//! - No modification methods - purely read-only
16//! - Thread-safe by construction (Clone, Send, Sync)
17//!
18//! # Structural Sharing (#711)
19//!
20//! Snapshots store a rope clone, which is O(1) via `Arc` sharing.
21//! The snapshot shares unchanged text nodes with the original buffer.
22//!
23//! # Use Cases
24//!
25//! - RPC handlers need buffer content without blocking edits
26//! - Syntax highlighting processes snapshot while user edits
27//! - Undo/redo can capture state before operations
28//!
29//! # Example
30//!
31//! ```
32//! use reovim_kernel::api::v1::*;
33//!
34//! let mut buffer = Buffer::from_string("Hello\nWorld");
35//! let cursor = Cursor::new(Position::new(0, 5)); // Get from Window
36//!
37//! // Capture state with explicit cursor
38//! let snapshot = BufferSnapshot::from_buffer(&buffer, cursor);
39//!
40//! // Snapshot is independent of buffer changes
41//! buffer.insert_at(Position::new(0, 5), "!");
42//!
43//! assert_eq!(snapshot.line_count(), 2);
44//! assert_eq!(snapshot.content(), "Hello\nWorld"); // Original content
45//! ```
46
47use super::{BufferId, Cursor, Position, rope::Rope};
48
49/// Read-only snapshot of buffer state.
50///
51/// A `BufferSnapshot` captures the complete state of a buffer at a point
52/// in time. Clone is O(1) via `Arc` structural sharing.
53///
54/// # Cursor Isolation (#471)
55///
56/// Cursor is passed explicitly to `from_buffer()` - get it from Window.
57///
58/// # Immutability
59///
60/// All methods are read-only. The snapshot cannot be modified after
61/// creation, ensuring thread safety without locks.
62///
63/// # Fields Captured
64///
65/// - `id`: Buffer identifier
66/// - `text`: Text content (rope clone, O(1))
67/// - `cursor`: Cursor state (passed from Window)
68/// - `file_path`: Associated file path (if any)
69/// - `modified`: Whether buffer had unsaved changes
70///
71/// Note: Selection removed in Phase 8 (#465) - it now lives in Window.
72/// Note: Cursor removed from Buffer in #471 - it now lives in Window.
73#[derive(Debug, Clone)]
74pub struct BufferSnapshot {
75 /// Buffer identifier.
76 pub id: BufferId,
77 /// Text content as a rope (O(1) clone via Arc sharing).
78 text: Rope,
79 /// Cursor state (from Window, not Buffer).
80 pub cursor: Cursor,
81 /// File path (if buffer is associated with a file).
82 pub file_path: Option<String>,
83 /// Whether buffer has unsaved modifications.
84 pub modified: bool,
85}
86
87impl BufferSnapshot {
88 /// Create a snapshot from a buffer.
89 ///
90 /// Cursor must be passed explicitly - get it from Window.
91 /// This is O(1) — the rope is cloned via `Arc` sharing.
92 #[must_use]
93 pub fn from_buffer(buffer: &super::Buffer, cursor: Cursor) -> Self {
94 Self {
95 id: buffer.id(),
96 text: buffer.clone_rope(),
97 cursor,
98 file_path: buffer.file_path().map(String::from),
99 modified: buffer.is_modified(),
100 }
101 }
102
103 /// Create a snapshot from individual components.
104 ///
105 /// This is useful for testing or when constructing a snapshot
106 /// without a buffer.
107 #[must_use]
108 pub fn new(
109 id: BufferId,
110 lines: &[String],
111 cursor: Cursor,
112 file_path: Option<String>,
113 modified: bool,
114 ) -> Self {
115 let content = lines.join("\n");
116 let text = if content.is_empty() {
117 Rope::new()
118 } else {
119 Rope::from_str(&content)
120 };
121 Self {
122 id,
123 text,
124 cursor,
125 file_path,
126 modified,
127 }
128 }
129
130 // === Line Access ===
131
132 /// Get the number of lines.
133 #[must_use]
134 pub fn line_count(&self) -> usize {
135 self.text.line_count()
136 }
137
138 /// Check if the snapshot is empty.
139 #[must_use]
140 pub fn is_empty(&self) -> bool {
141 self.text.is_empty()
142 }
143
144 /// Get a specific line by index.
145 ///
146 /// Returns `None` if index is out of bounds.
147 #[must_use]
148 pub fn line(&self, idx: usize) -> Option<&str> {
149 self.text.line(idx)
150 }
151
152 /// Get the length of a line in characters.
153 #[must_use]
154 pub fn line_len(&self, idx: usize) -> Option<usize> {
155 self.text.line_len(idx)
156 }
157
158 /// Collect all lines as a `Vec<String>`.
159 ///
160 /// This allocates — prefer `line(idx)` for individual access.
161 #[must_use]
162 pub fn lines(&self) -> Vec<String> {
163 (0..self.text.line_count())
164 .filter_map(|i| self.text.line(i).map(String::from))
165 .collect()
166 }
167
168 /// Get the full content as a string (lines joined with newlines).
169 #[must_use]
170 pub fn content(&self) -> String {
171 self.text.content()
172 }
173
174 // === Text Extraction ===
175
176 /// Extract text within a range.
177 ///
178 /// Returns the text between `start` and `end` positions.
179 /// Positions are clamped to valid bounds.
180 #[must_use]
181 pub fn text_in_range(&self, start: Position, end: Position) -> String {
182 if self.text.is_empty() {
183 return String::new();
184 }
185
186 let (start, end) = if start <= end {
187 (start, end)
188 } else {
189 (end, start)
190 };
191
192 let line_count = self.text.line_count();
193
194 // Clamp positions
195 let start_line = start.line.min(line_count - 1);
196 let end_line = end.line.min(line_count - 1);
197
198 if start_line == end_line {
199 // Single line extraction
200 let line = self.text.line(start_line).unwrap_or("");
201 let chars: Vec<char> = line.chars().collect();
202 let start_col = start.column.min(chars.len());
203 let end_col = end.column.min(chars.len());
204 return chars[start_col..end_col].iter().collect();
205 }
206
207 // Multi-line extraction
208 let mut result = String::new();
209
210 for line_idx in start_line..=end_line {
211 let line = self.text.line(line_idx).unwrap_or("");
212 let chars: Vec<char> = line.chars().collect();
213
214 if line_idx == start_line {
215 // First line: from start column to end
216 let start_col = start.column.min(chars.len());
217 result.extend(&chars[start_col..]);
218 result.push('\n');
219 } else if line_idx == end_line {
220 // Last line: from start to end column
221 let end_col = end.column.min(chars.len());
222 result.extend(&chars[..end_col]);
223 } else {
224 // Middle line: entire line
225 result.push_str(line);
226 result.push('\n');
227 }
228 }
229
230 result
231 }
232
233 // === Position Queries ===
234
235 /// Get the cursor position.
236 #[must_use]
237 pub const fn position(&self) -> Position {
238 self.cursor.position
239 }
240
241 /// Check if a position is valid within this snapshot.
242 #[must_use]
243 pub fn is_valid_position(&self, pos: Position) -> bool {
244 if pos.line >= self.text.line_count() {
245 return false;
246 }
247 self.line(pos.line)
248 .is_some_and(|line| pos.column <= line.chars().count())
249 }
250
251 // NOTE: Selection methods removed in Phase 8 (#465).
252 // Selection now lives in Window (per-window state), not Buffer/BufferSnapshot.
253 // Use SessionRuntime::selection(buffer_id) via BufferApi trait.
254}