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