fresh/model/document_model.rs
1/// Document Model Architecture
2///
3/// This module provides a clean abstraction layer between the editor's rendering/editing
4/// operations and the underlying text buffer implementation. It supports both small files
5/// with precise line indexing and huge files with lazy loading and byte-based positioning.
6///
7/// # Overview
8///
9/// The document model is inspired by VSCode's architecture but enhanced to support huge files
10/// (multi-GB) with lazy loading. It provides a three-layer architecture:
11///
12/// ```text
13/// ┌─────────────────────────────────────┐
14/// │ View/Editor Layer │
15/// │ (rendering, user interaction) │
16/// └────────────┬────────────────────────┘
17/// │ Uses DocumentModel trait
18/// ▼
19/// ┌─────────────────────────────────────┐
20/// │ DocumentModel (this module) │
21/// │ - get_viewport_content() │
22/// │ - get_range(), insert(), delete() │
23/// │ - Dual coordinate systems │
24/// └────────────┬────────────────────────┘
25/// │ Implemented by EditorState
26/// ▼
27/// ┌─────────────────────────────────────┐
28/// │ TextBuffer (implementation) │
29/// │ - Piece tree operations │
30/// │ - Lazy loading for large files │
31/// │ - Line indexing for small files │
32/// └─────────────────────────────────────┘
33/// ```
34///
35/// # Key Concepts
36///
37/// ## Dual Position System
38///
39/// Documents support two coordinate systems:
40/// - **Line/Column**: For small files with precise line indexing (like VSCode)
41/// - **Byte Offset**: For huge files where line indexing may be unavailable or approximate
42///
43/// ## Transparent Lazy Loading
44///
45/// For huge files, the document model uses a two-phase rendering approach:
46/// 1. **Prepare Phase** (`prepare_for_render()`): Pre-loads viewport data with `&mut` access
47/// 2. **Render Phase** (`get_viewport_content()`): Accesses pre-loaded data with `&self`
48///
49/// This avoids RefCell complexity while supporting lazy loading.
50///
51/// ## Explicit Error Handling
52///
53/// Unlike TextBuffer's `slice()` which returns empty strings on error, DocumentModel methods
54/// return `Result<T>` or `Option<T>` to make failures explicit and allow proper error messages.
55///
56/// # Usage Example
57///
58/// ```rust,ignore
59/// use fresh::document_model::{DocumentModel, DocumentPosition};
60///
61/// // Query document capabilities
62/// let caps = state.capabilities();
63/// if caps.has_line_index {
64/// // Use line/column positioning
65/// let pos = DocumentPosition::line_col(10, 5);
66/// } else {
67/// // Use byte offset positioning
68/// let pos = DocumentPosition::byte(1024);
69/// }
70///
71/// // Prepare viewport before rendering
72/// state.prepare_for_render()?;
73///
74/// // Get viewport content for rendering
75/// let viewport = state.get_viewport_content(
76/// DocumentPosition::byte(0),
77/// 24 // lines
78/// )?;
79///
80/// for line in viewport.lines {
81/// println!("{}: {}", line.byte_offset, line.content);
82/// }
83/// ```
84///
85/// # Design Benefits
86///
87/// 1. **Clean Abstraction**: Rendering never touches TextBuffer directly
88/// 2. **Better Than VSCode**: Supports multi-GB files (VSCode has 20MB limit)
89/// 3. **Type Safety**: Explicit Optional/Result types prevent silent failures
90/// 4. **Extensibility**: Easy to add RemoteDocument, VirtualDocument, etc.
91///
92/// See also: `docs/DOCUMENT_MODEL.md` for detailed architecture documentation.
93use anyhow::Result;
94
95/// Position in a document - can be line-based or byte-based
96///
97/// For small files with line indexing enabled, LineColumn provides precise positioning.
98/// For huge files without line indexing, ByteOffset provides always-available positioning.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum DocumentPosition {
101 /// Line and column (1-indexed line, 0-indexed column in bytes)
102 /// Only available when line indexing is enabled
103 LineColumn { line: usize, column: usize },
104
105 /// Byte offset from start of file
106 /// Always available, even for huge files
107 ByteOffset(usize),
108}
109
110impl DocumentPosition {
111 /// Create a line/column position
112 pub fn line_col(line: usize, column: usize) -> Self {
113 DocumentPosition::LineColumn { line, column }
114 }
115
116 /// Create a byte offset position
117 pub fn byte(offset: usize) -> Self {
118 DocumentPosition::ByteOffset(offset)
119 }
120}
121
122/// Information about a document's capabilities
123///
124/// This helps callers understand what operations are available and how to
125/// interact with the document efficiently.
126#[derive(Debug, Clone, Copy)]
127pub struct DocumentCapabilities {
128 /// Whether precise line indexing is available
129 pub has_line_index: bool,
130
131 /// Whether the document is using lazy loading
132 pub uses_lazy_loading: bool,
133
134 /// Total byte size (always known)
135 pub byte_length: usize,
136
137 /// Approximate line count (may be estimated for huge files)
138 pub approximate_line_count: usize,
139}
140
141/// A single line in the viewport
142#[derive(Debug, Clone)]
143pub struct ViewportLine {
144 /// Start byte offset of this line in the document
145 pub byte_offset: usize,
146
147 /// The line content (without trailing newline for display)
148 pub content: String,
149
150 /// Whether this line ends with a newline
151 pub has_newline: bool,
152
153 /// Approximate line number (may be estimated for huge files)
154 /// None if line indexing is not available
155 pub approximate_line_number: Option<usize>,
156}
157
158/// Content for rendering a viewport
159#[derive(Debug)]
160pub struct ViewportContent {
161 /// The actual start position of the returned content
162 /// May differ from requested position if adjusted to line boundary
163 pub start_position: DocumentPosition,
164
165 /// Lines of content
166 pub lines: Vec<ViewportLine>,
167
168 /// Whether there's more content after these lines
169 pub has_more: bool,
170}
171
172/// High-level document interface supporting both line and byte operations
173///
174/// This trait provides a clean abstraction for all editor operations, whether
175/// rendering, editing, or searching. It works transparently with both small
176/// files (with line indexing) and huge files (with lazy loading).
177pub trait DocumentModel {
178 // ===== Capability Queries =====
179
180 /// Get document capabilities
181 fn capabilities(&self) -> DocumentCapabilities;
182
183 /// Check if line indexing is available
184 fn has_line_index(&self) -> bool {
185 self.capabilities().has_line_index
186 }
187
188 // ===== Position Queries =====
189
190 /// Get content at a viewport (the core rendering primitive)
191 ///
192 /// Returns lines starting from position, up to max_lines.
193 /// This works for both line-based and byte-based positions.
194 ///
195 /// For large files, this automatically loads chunks on-demand (never scans entire file).
196 fn get_viewport_content(
197 &mut self,
198 start_pos: DocumentPosition,
199 max_lines: usize,
200 ) -> Result<ViewportContent>;
201
202 /// Convert position to byte offset (always works)
203 fn position_to_offset(&self, pos: DocumentPosition) -> Result<usize>;
204
205 /// Convert byte offset to a position
206 ///
207 /// For huge files without line index, returns ByteOffset.
208 /// For small files, returns LineColumn.
209 fn offset_to_position(&self, offset: usize) -> DocumentPosition;
210
211 // ===== Content Access =====
212
213 /// Get a range of text by positions
214 /// May trigger lazy loading for large files
215 fn get_range(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<String>;
216
217 /// Get a single line if line indexing is available
218 ///
219 /// Returns None if line indexing is not available.
220 /// For large files, this may trigger lazy loading of chunks.
221 fn get_line_content(&mut self, line_number: usize) -> Option<String>;
222
223 /// Get text around a byte offset (for operations that don't need exact lines)
224 ///
225 /// Returns (offset, content) where offset is the start of returned content.
226 /// May trigger lazy loading for large files
227 fn get_chunk_at_offset(&mut self, offset: usize, size: usize) -> Result<(usize, String)>;
228
229 // ===== Editing Operations =====
230
231 /// Insert text at a position
232 ///
233 /// Returns the number of bytes inserted.
234 fn insert(&mut self, pos: DocumentPosition, text: &str) -> Result<usize>;
235
236 /// Delete a range
237 fn delete(&mut self, start: DocumentPosition, end: DocumentPosition) -> Result<()>;
238
239 /// Replace a range
240 fn replace(&mut self, start: DocumentPosition, end: DocumentPosition, text: &str)
241 -> Result<()>;
242
243 // ===== Search Operations =====
244
245 /// Find all matches of a pattern in a range
246 ///
247 /// Returns byte offsets (always precise).
248 /// May trigger lazy loading for large files
249 fn find_matches(
250 &mut self,
251 pattern: &str,
252 search_range: Option<(DocumentPosition, DocumentPosition)>,
253 ) -> Result<Vec<usize>>;
254}