use super::types::TextChange;
use crate::lexer::Lexer;
use crate::tokens::{Span, Token, TokenKind};
#[derive(Debug, Clone)]
pub struct IncrementalChangeResult {
pub changed_token_range: std::ops::Range<usize>,
pub tokens: Vec<Token>,
pub is_stable: bool,
}
pub fn parse_incremental_change(
old_tokens: &[Token],
new_source: &str,
change: &TextChange,
) -> IncrementalChangeResult {
let edit_start_char = change.range.start;
let old_edit_end_char = change.range.end;
let new_edit_end_char = edit_start_char + char_count(&change.new_text);
let char_delta: i64 = new_edit_end_char as i64 - old_edit_end_char as i64;
let first_affected = old_tokens
.iter()
.position(|t| token_end_char(t) > edit_start_char)
.unwrap_or(old_tokens.len());
let first_unaffected = old_tokens[first_affected..]
.iter()
.position(|t| token_start_char(t) >= old_edit_end_char)
.map(|p| p + first_affected)
.unwrap_or(old_tokens.len());
let relex_start_char = old_tokens
.get(first_affected)
.map(token_start_char)
.unwrap_or(edit_start_char);
let new_tokens_in_range = relex_dirty_range(new_source, relex_start_char, new_edit_end_char);
let shifted_tail: Vec<Token> = old_tokens[first_unaffected..]
.iter()
.map(|t| shift_token(t, char_delta))
.collect();
let is_stable = check_boundary_stable(&new_tokens_in_range, &shifted_tail);
let prefix = &old_tokens[..first_affected];
let new_len = prefix.len() + new_tokens_in_range.len() + shifted_tail.len();
let mut result_tokens: Vec<Token> = Vec::with_capacity(new_len);
result_tokens.extend_from_slice(prefix);
result_tokens.extend(new_tokens_in_range.iter().cloned());
result_tokens.extend(shifted_tail);
let changed_range_start = first_affected;
let changed_range_end = first_affected + new_tokens_in_range.len();
IncrementalChangeResult {
changed_token_range: changed_range_start..changed_range_end,
tokens: result_tokens,
is_stable,
}
}
#[inline]
fn char_count(s: &str) -> usize {
s.chars().count()
}
#[inline]
fn token_start_char(token: &Token) -> usize {
token.span.start
}
#[inline]
fn token_end_char(token: &Token) -> usize {
token.span.end
}
fn char_to_byte_offset(source: &str, char_index: usize) -> usize {
source
.char_indices()
.nth(char_index)
.map(|(byte_pos, _)| byte_pos)
.unwrap_or(source.len())
}
fn relex_dirty_range(source: &str, relex_start_char: usize, dirty_end_char: usize) -> Vec<Token> {
let mut lexer = Lexer::new(source);
let all_tokens = lexer.tokenize();
all_tokens
.into_iter()
.filter(|t| {
if token_end_char(t) <= relex_start_char {
return false;
}
token_start_char(t) < dirty_end_char
})
.collect()
}
fn shift_token(token: &Token, char_delta: i64) -> Token {
let new_start = (token.span.start as i64 + char_delta).max(0) as usize;
let new_end = (token.span.end as i64 + char_delta).max(0) as usize;
Token::new(
token.kind.clone(),
Span::new(new_start, new_end, token.span.line, token.span.column),
)
}
fn check_boundary_stable(new_tokens: &[Token], tail: &[Token]) -> bool {
match (new_tokens.last(), tail.first()) {
(Some(last), Some(first)) => token_end_char(last) == token_start_char(first),
(_, None) => true,
(None, Some(_)) => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::incremental::types::TextChange;
fn lex_full(source: &str) -> Vec<Token> {
Lexer::new(source).tokenize()
}
fn kind_tag(t: &Token) -> String {
match &t.kind {
TokenKind::Ident(_) => "Ident".into(),
TokenKind::Nat(_) => "Nat".into(),
TokenKind::Float(_) => "Float".into(),
TokenKind::String(_) => "String".into(),
TokenKind::Char(_) => "Char".into(),
TokenKind::DocComment(_) => "DocComment".into(),
TokenKind::InterpolatedString(_) => "InterpolatedString".into(),
TokenKind::Error(_) => "Error".into(),
TokenKind::Eof => "Eof".into(),
other => format!("{:?}", other),
}
}
#[test]
fn test_empty_change_is_noop() {
let source = "theorem foo : True := trivial";
let old_tokens = lex_full(source);
let change = TextChange::new(0, 0, "");
let result = parse_incremental_change(&old_tokens, source, &change);
assert_eq!(
result.tokens.len(),
old_tokens.len(),
"empty edit must produce same token count"
);
}
#[test]
fn test_same_length_replacement_token_count() {
let source = "theorem foo : True := trivial";
let old_tokens = lex_full(source);
let new_source = "theorem bar : True := trivial";
let change = TextChange::new(8, 11, "bar");
let result = parse_incremental_change(&old_tokens, new_source, &change);
let full_tokens = lex_full(new_source);
assert_eq!(
result.tokens.len(),
full_tokens.len(),
"incremental and full lex should produce same token count"
);
}
#[test]
fn test_same_length_replacement_kinds_match() {
let source = "theorem foo : True := trivial";
let old_tokens = lex_full(source);
let new_source = "theorem bar : True := trivial";
let change = TextChange::new(8, 11, "bar");
let result = parse_incremental_change(&old_tokens, new_source, &change);
let full_tokens = lex_full(new_source);
let inc_kinds: Vec<String> = result.tokens.iter().map(kind_tag).collect();
let full_kinds: Vec<String> = full_tokens.iter().map(kind_tag).collect();
assert_eq!(
inc_kinds, full_kinds,
"token kind sequences must match between incremental and full lex"
);
}
#[test]
fn test_insertion_changes_token_count() {
let source = "def x := 1";
let old_tokens = lex_full(source);
let new_source = "def x := 1 + 2";
let change = TextChange::new(10, 10, " + 2");
let result = parse_incremental_change(&old_tokens, new_source, &change);
let full_tokens = lex_full(new_source);
assert_eq!(
result.tokens.len(),
full_tokens.len(),
"incremental token count after insertion must match full lex"
);
}
#[test]
fn test_deletion_changes_token_count() {
let source = "def x := 1 + 2";
let old_tokens = lex_full(source);
let new_source = "def x := 1";
let change = TextChange::new(10, 14, "");
let result = parse_incremental_change(&old_tokens, new_source, &change);
let full_tokens = lex_full(new_source);
assert_eq!(
result.tokens.len(),
full_tokens.len(),
"incremental token count after deletion must match full lex"
);
}
#[test]
fn test_relex_matches_full_lex_various_edits() {
let cases: Vec<(&str, &str, usize, usize, &str)> = vec![
("def x := 1", "def y := 1", 4, 5, "y"),
("def x := foo", "def x := bar", 9, 12, "bar"),
("def x := 1", "def x := 42", 9, 10, "42"),
];
for (old_src, new_src, cs, ce, new_text) in cases {
let old_tokens = lex_full(old_src);
let change = TextChange::new(cs, ce, new_text);
let result = parse_incremental_change(&old_tokens, new_src, &change);
let full_tokens = lex_full(new_src);
assert_eq!(
result.tokens.len(),
full_tokens.len(),
"count mismatch for edit '{old_src}' -> '{new_src}'"
);
let inc_kinds: Vec<String> = result.tokens.iter().map(kind_tag).collect();
let full_kinds: Vec<String> = full_tokens.iter().map(kind_tag).collect();
assert_eq!(
inc_kinds, full_kinds,
"kind mismatch for edit '{old_src}' -> '{new_src}'"
);
}
}
#[test]
fn test_changed_token_range_is_non_empty_on_real_edit() {
let source = "def x := 1";
let old_tokens = lex_full(source);
let new_source = "def x := 42";
let change = TextChange::new(9, 10, "42");
let result = parse_incremental_change(&old_tokens, new_source, &change);
assert!(
!result.changed_token_range.is_empty(),
"changed_token_range must be non-empty for a real edit"
);
}
#[test]
fn test_eof_token_is_present_after_incremental() {
let source = "def x := 1";
let old_tokens = lex_full(source);
let new_source = "def x := 42";
let change = TextChange::new(9, 10, "42");
let result = parse_incremental_change(&old_tokens, new_source, &change);
assert!(
result
.tokens
.last()
.map(|t| matches!(t.kind, TokenKind::Eof))
.unwrap_or(false),
"last token must be Eof"
);
}
#[test]
fn test_prefix_tokens_unmodified() {
let source = "def x := 1 + 2";
let old_tokens = lex_full(source);
let new_source = "def x := 1 + 99";
let change = TextChange::new(13, 14, "99");
let result = parse_incremental_change(&old_tokens, new_source, &change);
let first_affected = old_tokens
.iter()
.position(|t| t.span.end > 13)
.unwrap_or(old_tokens.len());
for (i, old_tok) in old_tokens.iter().enumerate().take(first_affected) {
assert_eq!(
result.tokens[i].span.start, old_tok.span.start,
"prefix token {i} span.start must be unmodified"
);
}
}
}