Skip to main content

perl_incremental_parsing/incremental/
incremental_integration.rs

1//! Integration module for incremental parsing with the main LSP server
2//!
3//! This module provides the bridge between the existing LspServer and
4//! incremental parsing capabilities, controlled by feature flags.
5
6use crate::{
7    ast::Node,
8    error::ParseResult,
9    incremental_document::{IncrementalDocument, ParseMetrics},
10    incremental_edit::{IncrementalEdit, IncrementalEditSet},
11    parser::Parser,
12};
13use ropey::Rope;
14use serde_json::Value;
15use std::sync::Arc;
16
17/// Configuration for incremental parsing
18pub struct IncrementalConfig {
19    /// Enable incremental parsing
20    pub enabled: bool,
21    /// Target parse time in milliseconds
22    pub target_parse_time_ms: f64,
23    /// Maximum cache size for subtrees
24    pub max_cache_size: usize,
25}
26
27impl Default for IncrementalConfig {
28    fn default() -> Self {
29        Self {
30            // Check environment variable to enable incremental parsing
31            enabled: std::env::var("PERL_LSP_INCREMENTAL").is_ok(),
32            target_parse_time_ms: 1.0,
33            max_cache_size: 10000,
34        }
35    }
36}
37
38/// Helper to convert LSP ContentChange to IncrementalEdit
39pub fn lsp_change_to_edit(change: &Value, rope: &Rope) -> Option<IncrementalEdit> {
40    // Check if this is a full document change or incremental
41    if let Some(range) = change.get("range") {
42        // Incremental change with range
43        let start_line = range["start"]["line"].as_u64()? as usize;
44        let start_char = range["start"]["character"].as_u64()? as usize;
45        let end_line = range["end"]["line"].as_u64()? as usize;
46        let end_char = range["end"]["character"].as_u64()? as usize;
47
48        // Convert LSP positions to byte offsets using rope
49        let start_byte = lsp_pos_to_byte(rope, start_line, start_char);
50        let end_byte = lsp_pos_to_byte(rope, end_line, end_char);
51
52        let new_text = change["text"].as_str()?.to_string();
53
54        // Create position objects
55        let start_position =
56            crate::position::Position::new(start_byte, start_line as u32, start_char as u32);
57        let old_end_position =
58            crate::position::Position::new(end_byte, end_line as u32, end_char as u32);
59
60        Some(IncrementalEdit::with_positions(
61            start_byte,
62            end_byte,
63            new_text,
64            start_position,
65            old_end_position,
66        ))
67    } else {
68        // Full document change - return None to trigger full reparse
69        None
70    }
71}
72
73/// Convert LSP position to byte offset using rope
74pub fn lsp_pos_to_byte(rope: &Rope, line: usize, character: usize) -> usize {
75    if line >= rope.len_lines() {
76        return rope.len_bytes();
77    }
78
79    let line_start = rope.line_to_byte(line);
80    let line = rope.line(line);
81
82    // Handle UTF-16 code units (LSP uses UTF-16)
83    let mut utf16_pos = 0;
84    let mut byte_pos = 0;
85
86    for ch in line.chars() {
87        if utf16_pos >= character {
88            break;
89        }
90        utf16_pos += ch.len_utf16();
91        byte_pos += ch.len_utf8();
92    }
93
94    line_start + byte_pos
95}
96
97/// Convert byte offset to LSP position using rope
98pub fn byte_to_lsp_pos(rope: &Rope, byte_offset: usize) -> (usize, usize) {
99    let byte_offset = byte_offset.min(rope.len_bytes());
100    let line = rope.byte_to_line(byte_offset);
101    let line_start = rope.line_to_byte(line);
102    let column_bytes = byte_offset - line_start;
103
104    // Convert byte offset to UTF-16 code units
105    let line_str = rope.line(line);
106    let mut utf16_pos = 0;
107    let mut current_bytes = 0;
108
109    for ch in line_str.chars() {
110        if current_bytes >= column_bytes {
111            break;
112        }
113        current_bytes += ch.len_utf8();
114        utf16_pos += ch.len_utf16();
115    }
116
117    (line, utf16_pos)
118}
119
120/// Wrapper for document state with incremental parsing support
121pub enum DocumentParser {
122    /// Full parsing mode (current implementation)
123    Full { content: String, ast: Option<Arc<Node>> },
124    /// Incremental parsing mode
125    Incremental { document: Box<IncrementalDocument>, rope: Rope },
126}
127
128impl DocumentParser {
129    /// Create a new document parser based on configuration
130    pub fn new(content: String, config: &IncrementalConfig) -> ParseResult<Self> {
131        if config.enabled {
132            // Use incremental parsing
133            let document = IncrementalDocument::new(content.clone())?;
134            let rope = Rope::from_str(&content);
135            Ok(DocumentParser::Incremental { document: Box::new(document), rope })
136        } else {
137            // Use full parsing
138            let mut parser = Parser::new(&content);
139            let ast = parser.parse().ok().map(Arc::new);
140            Ok(DocumentParser::Full { content, ast })
141        }
142    }
143
144    /// Apply changes to the document
145    pub fn apply_changes(
146        &mut self,
147        changes: &[Value],
148        _config: &IncrementalConfig,
149    ) -> ParseResult<()> {
150        match self {
151            DocumentParser::Full { content, ast } => {
152                // Full document replacement
153                if let Some(change) = changes.first() {
154                    if let Some(text) = change["text"].as_str() {
155                        *content = text.to_string();
156                        let mut parser = Parser::new(content);
157                        *ast = parser.parse().ok().map(Arc::new);
158                    }
159                }
160                Ok(())
161            }
162            DocumentParser::Incremental { document: boxed_document, rope } => {
163                let document = boxed_document.as_mut();
164                // Incremental updates
165                let mut edits = Vec::new();
166
167                for change in changes {
168                    if let Some(edit) = lsp_change_to_edit(change, rope) {
169                        edits.push(edit);
170                    } else {
171                        // Fall back to full document replacement
172                        if let Some(text) = change["text"].as_str() {
173                            *document = IncrementalDocument::new(text.to_string())?;
174                            *rope = Rope::from_str(text);
175                            return Ok(());
176                        }
177                    }
178                }
179
180                if !edits.is_empty() {
181                    // Apply incremental edits
182                    let edit_set = IncrementalEditSet { edits };
183                    document.apply_edits(&edit_set)?;
184
185                    // Update rope to match new content
186                    *rope = Rope::from_str(&document.source);
187                }
188
189                Ok(())
190            }
191        }
192    }
193
194    /// Get the current AST
195    pub fn ast(&self) -> Option<Arc<Node>> {
196        match self {
197            DocumentParser::Full { ast, .. } => ast.clone(),
198            DocumentParser::Incremental { document, .. } => Some(document.root.clone()),
199        }
200    }
201
202    /// Get the current content
203    pub fn content(&self) -> &str {
204        match self {
205            DocumentParser::Full { content, .. } => content,
206            DocumentParser::Incremental { document, .. } => &document.source,
207        }
208    }
209
210    /// Get parsing metrics (if available)
211    pub fn metrics(&self) -> Option<&ParseMetrics> {
212        match self {
213            DocumentParser::Full { .. } => None,
214            DocumentParser::Incremental { document, .. } => Some(document.metrics()),
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_lsp_pos_to_byte() {
225        let text = "Hello\nWorld\n";
226        let rope = Rope::from_str(text);
227
228        // Start of document
229        assert_eq!(lsp_pos_to_byte(&rope, 0, 0), 0);
230
231        // Start of second line
232        assert_eq!(lsp_pos_to_byte(&rope, 1, 0), 6);
233
234        // Middle of second line
235        assert_eq!(lsp_pos_to_byte(&rope, 1, 3), 9);
236    }
237
238    #[test]
239    fn test_byte_to_lsp_pos() {
240        let text = "Hello\nWorld\n";
241        let rope = Rope::from_str(text);
242
243        // Start of document
244        assert_eq!(byte_to_lsp_pos(&rope, 0), (0, 0));
245
246        // Start of second line
247        assert_eq!(byte_to_lsp_pos(&rope, 6), (1, 0));
248
249        // Middle of second line
250        assert_eq!(byte_to_lsp_pos(&rope, 9), (1, 3));
251    }
252
253    #[test]
254    fn test_crlf_handling() {
255        let text = "Hello\r\nWorld\r\n";
256        let rope = Rope::from_str(text);
257
258        // Start of second line (after CRLF)
259        assert_eq!(lsp_pos_to_byte(&rope, 1, 0), 7);
260        assert_eq!(byte_to_lsp_pos(&rope, 7), (1, 0));
261    }
262
263    #[test]
264    fn test_utf16_handling() {
265        let text = "Hello 😀 World"; // Emoji is 2 UTF-16 code units
266        let rope = Rope::from_str(text);
267
268        // Position after emoji
269        let byte_after_emoji = "Hello 😀".len();
270        let (line, char) = byte_to_lsp_pos(&rope, byte_after_emoji);
271        assert_eq!(line, 0);
272        assert_eq!(char, 8); // "Hello " = 6 + emoji = 2 = 8 UTF-16 units
273    }
274}