oxilean_parse/incremental/
api.rs1use super::types::TextChange;
19use crate::lexer::Lexer;
20use crate::tokens::{Span, Token, TokenKind};
21
22#[derive(Debug, Clone)]
26pub struct IncrementalChangeResult {
27 pub changed_token_range: std::ops::Range<usize>,
32 pub tokens: Vec<Token>,
34 pub is_stable: bool,
38}
39
40pub fn parse_incremental_change(
64 old_tokens: &[Token],
65 new_source: &str,
66 change: &TextChange,
67) -> IncrementalChangeResult {
68 let edit_start_char = change.range.start;
70 let old_edit_end_char = change.range.end;
71 let new_edit_end_char = edit_start_char + char_count(&change.new_text);
72 let char_delta: i64 = new_edit_end_char as i64 - old_edit_end_char as i64;
74
75 let first_affected = old_tokens
78 .iter()
79 .position(|t| token_end_char(t) > edit_start_char)
80 .unwrap_or(old_tokens.len());
81
82 let first_unaffected = old_tokens[first_affected..]
85 .iter()
86 .position(|t| token_start_char(t) >= old_edit_end_char)
87 .map(|p| p + first_affected)
88 .unwrap_or(old_tokens.len());
89
90 let relex_start_char = old_tokens
94 .get(first_affected)
95 .map(token_start_char)
96 .unwrap_or(edit_start_char);
97
98 let new_tokens_in_range = relex_dirty_range(new_source, relex_start_char, new_edit_end_char);
107
108 let shifted_tail: Vec<Token> = old_tokens[first_unaffected..]
110 .iter()
111 .map(|t| shift_token(t, char_delta))
112 .collect();
113
114 let is_stable = check_boundary_stable(&new_tokens_in_range, &shifted_tail);
118
119 let prefix = &old_tokens[..first_affected];
121 let new_len = prefix.len() + new_tokens_in_range.len() + shifted_tail.len();
122 let mut result_tokens: Vec<Token> = Vec::with_capacity(new_len);
123 result_tokens.extend_from_slice(prefix);
124 result_tokens.extend(new_tokens_in_range.iter().cloned());
125 result_tokens.extend(shifted_tail);
126
127 let changed_range_start = first_affected;
128 let changed_range_end = first_affected + new_tokens_in_range.len();
129
130 IncrementalChangeResult {
131 changed_token_range: changed_range_start..changed_range_end,
132 tokens: result_tokens,
133 is_stable,
134 }
135}
136
137#[inline]
141fn char_count(s: &str) -> usize {
142 s.chars().count()
143}
144
145#[inline]
147fn token_start_char(token: &Token) -> usize {
148 token.span.start
149}
150
151#[inline]
153fn token_end_char(token: &Token) -> usize {
154 token.span.end
155}
156
157fn char_to_byte_offset(source: &str, char_index: usize) -> usize {
159 source
160 .char_indices()
161 .nth(char_index)
162 .map(|(byte_pos, _)| byte_pos)
163 .unwrap_or(source.len())
164}
165
166fn relex_dirty_range(source: &str, relex_start_char: usize, dirty_end_char: usize) -> Vec<Token> {
172 let mut lexer = Lexer::new(source);
173 let all_tokens = lexer.tokenize();
174
175 all_tokens
176 .into_iter()
177 .filter(|t| {
178 if token_end_char(t) <= relex_start_char {
180 return false;
181 }
182 token_start_char(t) < dirty_end_char
186 })
187 .collect()
188}
189
190fn shift_token(token: &Token, char_delta: i64) -> Token {
192 let new_start = (token.span.start as i64 + char_delta).max(0) as usize;
193 let new_end = (token.span.end as i64 + char_delta).max(0) as usize;
194 Token::new(
195 token.kind.clone(),
196 Span::new(new_start, new_end, token.span.line, token.span.column),
197 )
198}
199
200fn check_boundary_stable(new_tokens: &[Token], tail: &[Token]) -> bool {
203 match (new_tokens.last(), tail.first()) {
204 (Some(last), Some(first)) => token_end_char(last) == token_start_char(first),
205 (_, None) => true,
207 (None, Some(_)) => true,
209 }
210}
211
212#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::incremental::types::TextChange;
218
219 fn lex_full(source: &str) -> Vec<Token> {
220 Lexer::new(source).tokenize()
221 }
222
223 fn kind_tag(t: &Token) -> String {
225 match &t.kind {
226 TokenKind::Ident(_) => "Ident".into(),
227 TokenKind::Nat(_) => "Nat".into(),
228 TokenKind::Float(_) => "Float".into(),
229 TokenKind::String(_) => "String".into(),
230 TokenKind::Char(_) => "Char".into(),
231 TokenKind::DocComment(_) => "DocComment".into(),
232 TokenKind::InterpolatedString(_) => "InterpolatedString".into(),
233 TokenKind::Error(_) => "Error".into(),
234 TokenKind::Eof => "Eof".into(),
235 other => format!("{:?}", other),
236 }
237 }
238
239 #[test]
240 fn test_empty_change_is_noop() {
241 let source = "theorem foo : True := trivial";
242 let old_tokens = lex_full(source);
243 let change = TextChange::new(0, 0, "");
244 let result = parse_incremental_change(&old_tokens, source, &change);
245 assert_eq!(
246 result.tokens.len(),
247 old_tokens.len(),
248 "empty edit must produce same token count"
249 );
250 }
251
252 #[test]
253 fn test_same_length_replacement_token_count() {
254 let source = "theorem foo : True := trivial";
256 let old_tokens = lex_full(source);
257 let new_source = "theorem bar : True := trivial";
258 let change = TextChange::new(8, 11, "bar");
260 let result = parse_incremental_change(&old_tokens, new_source, &change);
261 let full_tokens = lex_full(new_source);
262 assert_eq!(
263 result.tokens.len(),
264 full_tokens.len(),
265 "incremental and full lex should produce same token count"
266 );
267 }
268
269 #[test]
270 fn test_same_length_replacement_kinds_match() {
271 let source = "theorem foo : True := trivial";
272 let old_tokens = lex_full(source);
273 let new_source = "theorem bar : True := trivial";
274 let change = TextChange::new(8, 11, "bar");
275 let result = parse_incremental_change(&old_tokens, new_source, &change);
276 let full_tokens = lex_full(new_source);
277 let inc_kinds: Vec<String> = result.tokens.iter().map(kind_tag).collect();
278 let full_kinds: Vec<String> = full_tokens.iter().map(kind_tag).collect();
279 assert_eq!(
280 inc_kinds, full_kinds,
281 "token kind sequences must match between incremental and full lex"
282 );
283 }
284
285 #[test]
286 fn test_insertion_changes_token_count() {
287 let source = "def x := 1";
288 let old_tokens = lex_full(source);
289 let new_source = "def x := 1 + 2";
291 let change = TextChange::new(10, 10, " + 2");
292 let result = parse_incremental_change(&old_tokens, new_source, &change);
293 let full_tokens = lex_full(new_source);
294 assert_eq!(
295 result.tokens.len(),
296 full_tokens.len(),
297 "incremental token count after insertion must match full lex"
298 );
299 }
300
301 #[test]
302 fn test_deletion_changes_token_count() {
303 let source = "def x := 1 + 2";
304 let old_tokens = lex_full(source);
305 let new_source = "def x := 1";
307 let change = TextChange::new(10, 14, "");
308 let result = parse_incremental_change(&old_tokens, new_source, &change);
309 let full_tokens = lex_full(new_source);
310 assert_eq!(
311 result.tokens.len(),
312 full_tokens.len(),
313 "incremental token count after deletion must match full lex"
314 );
315 }
316
317 #[test]
318 fn test_relex_matches_full_lex_various_edits() {
319 let cases: Vec<(&str, &str, usize, usize, &str)> = vec![
321 ("def x := 1", "def y := 1", 4, 5, "y"),
323 ("def x := foo", "def x := bar", 9, 12, "bar"),
325 ("def x := 1", "def x := 42", 9, 10, "42"),
327 ];
328 for (old_src, new_src, cs, ce, new_text) in cases {
329 let old_tokens = lex_full(old_src);
330 let change = TextChange::new(cs, ce, new_text);
331 let result = parse_incremental_change(&old_tokens, new_src, &change);
332 let full_tokens = lex_full(new_src);
333 assert_eq!(
334 result.tokens.len(),
335 full_tokens.len(),
336 "count mismatch for edit '{old_src}' -> '{new_src}'"
337 );
338 let inc_kinds: Vec<String> = result.tokens.iter().map(kind_tag).collect();
339 let full_kinds: Vec<String> = full_tokens.iter().map(kind_tag).collect();
340 assert_eq!(
341 inc_kinds, full_kinds,
342 "kind mismatch for edit '{old_src}' -> '{new_src}'"
343 );
344 }
345 }
346
347 #[test]
348 fn test_changed_token_range_is_non_empty_on_real_edit() {
349 let source = "def x := 1";
350 let old_tokens = lex_full(source);
351 let new_source = "def x := 42";
352 let change = TextChange::new(9, 10, "42");
353 let result = parse_incremental_change(&old_tokens, new_source, &change);
354 assert!(
355 !result.changed_token_range.is_empty(),
356 "changed_token_range must be non-empty for a real edit"
357 );
358 }
359
360 #[test]
361 fn test_eof_token_is_present_after_incremental() {
362 let source = "def x := 1";
363 let old_tokens = lex_full(source);
364 let new_source = "def x := 42";
365 let change = TextChange::new(9, 10, "42");
366 let result = parse_incremental_change(&old_tokens, new_source, &change);
367 assert!(
368 result
369 .tokens
370 .last()
371 .map(|t| matches!(t.kind, TokenKind::Eof))
372 .unwrap_or(false),
373 "last token must be Eof"
374 );
375 }
376
377 #[test]
378 fn test_prefix_tokens_unmodified() {
379 let source = "def x := 1 + 2";
381 let old_tokens = lex_full(source);
382 let new_source = "def x := 1 + 99";
384 let change = TextChange::new(13, 14, "99");
385 let result = parse_incremental_change(&old_tokens, new_source, &change);
386 let first_affected = old_tokens
388 .iter()
389 .position(|t| t.span.end > 13)
390 .unwrap_or(old_tokens.len());
391 for (i, old_tok) in old_tokens.iter().enumerate().take(first_affected) {
392 assert_eq!(
393 result.tokens[i].span.start, old_tok.span.start,
394 "prefix token {i} span.start must be unmodified"
395 );
396 }
397 }
398}