perl_parser/incremental/
incremental_integration.rs1use 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
15pub struct IncrementalConfig {
17 pub enabled: bool,
19 pub target_parse_time_ms: f64,
21 pub max_cache_size: usize,
23}
24
25impl Default for IncrementalConfig {
26 fn default() -> Self {
27 Self {
28 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
36pub fn lsp_change_to_edit(change: &Value, rope: &Rope) -> Option<IncrementalEdit> {
38 if let Some(range) = change.get("range") {
40 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 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 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 None
71 }
72}
73
74pub 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 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
98pub 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 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
121pub enum DocumentParser {
123 Full { content: String, ast: Option<Arc<Node>> },
125 Incremental { document: Box<IncrementalDocument>, rope: Rope },
127}
128
129impl DocumentParser {
130 pub fn new(content: String, config: &IncrementalConfig) -> ParseResult<Self> {
132 if config.enabled {
133 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 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 pub fn apply_changes(
147 &mut self,
148 changes: &[Value],
149 _config: &IncrementalConfig,
150 ) -> ParseResult<()> {
151 match self {
152 DocumentParser::Full { content, ast } => {
153 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 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 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 let edit_set = IncrementalEditSet { edits };
184 document.apply_edits(&edit_set)?;
185
186 *rope = Rope::from_str(&document.source);
188 }
189
190 Ok(())
191 }
192 }
193 }
194
195 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 pub fn content(&self) -> &str {
205 match self {
206 DocumentParser::Full { content, .. } => content,
207 DocumentParser::Incremental { document, .. } => &document.source,
208 }
209 }
210
211 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 assert_eq!(lsp_pos_to_byte(&rope, 0, 0), 0);
231
232 assert_eq!(lsp_pos_to_byte(&rope, 1, 0), 6);
234
235 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 assert_eq!(byte_to_lsp_pos(&rope, 0), (0, 0));
246
247 assert_eq!(byte_to_lsp_pos(&rope, 6), (1, 0));
249
250 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 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"; let rope = Rope::from_str(text);
268
269 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); }
275}