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//! # 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}