Skip to main content

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}