Skip to main content

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