Skip to main content

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}