use rowan::NodeOrToken;
use text_size::{TextRange, TextSize};
use crate::{Parse, SyntaxKind, SyntaxNode, parse};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextEdit {
pub range: TextRange,
pub replacement: String,
}
impl TextEdit {
#[must_use]
pub fn replace(range: TextRange, replacement: impl Into<String>) -> Self {
Self {
range,
replacement: replacement.into(),
}
}
#[must_use]
pub fn insert(offset: TextSize, text: impl Into<String>) -> Self {
Self {
range: TextRange::empty(offset),
replacement: text.into(),
}
}
#[must_use]
pub fn apply(&self, src: &str) -> String {
let len = src.len();
let start = usize::from(self.range.start()).min(len);
let end = usize::from(self.range.end()).min(len).max(start);
let mut s = start;
while s > 0 && !src.is_char_boundary(s) {
s -= 1;
}
let mut e = end;
while e < len && !src.is_char_boundary(e) {
e += 1;
}
let mut out = String::with_capacity(len - (e - s) + self.replacement.len());
out.push_str(&src[..s]);
out.push_str(&self.replacement);
out.push_str(&src[e..]);
out
}
}
#[must_use]
pub fn incremental_reparse(old_tree: &SyntaxNode, edit: &TextEdit) -> Parse {
let old_src = old_tree.to_string();
let new_src = edit.apply(&old_src);
#[cfg(feature = "incremental")]
{
if let Some(parsed) = try_incremental_splice(old_tree, &old_src, edit, &new_src) {
return parsed;
}
}
parse(&new_src)
}
#[cfg(feature = "incremental")]
fn try_incremental_splice(
old_tree: &SyntaxNode,
old_src: &str,
edit: &TextEdit,
new_src: &str,
) -> Option<Parse> {
use crate::lexer::lex;
let edit_range = edit.range;
let stmt = covering_statement(old_tree, edit_range)?;
let stmt_range = stmt.text_range();
if !stmt_range.contains_range(edit_range) {
return None;
}
let stmt_start = usize::from(stmt_range.start());
let stmt_end = usize::from(stmt_range.end());
let edit_start = usize::from(edit_range.start()).clamp(stmt_start, stmt_end);
let edit_end = usize::from(edit_range.end()).clamp(edit_start, stmt_end);
if !old_src.is_char_boundary(stmt_start)
|| !old_src.is_char_boundary(stmt_end)
|| !old_src.is_char_boundary(edit_start)
|| !old_src.is_char_boundary(edit_end)
{
return None;
}
let mut new_stmt_text = String::with_capacity(
(edit_start - stmt_start) + edit.replacement.len() + (stmt_end - edit_end),
);
new_stmt_text.push_str(&old_src[stmt_start..edit_start]);
new_stmt_text.push_str(&edit.replacement);
new_stmt_text.push_str(&old_src[edit_end..stmt_end]);
let toks = lex(&new_stmt_text);
for t in &toks {
match t.kind {
SyntaxKind::SEMI | SyntaxKind::UNION_KW => return None,
_ => {}
}
}
let cand = parse(&new_stmt_text);
let cand_root = cand.syntax();
let mut stmt_children = cand_root
.children()
.filter(|n| n.kind() == SyntaxKind::STATEMENT);
let new_stmt = stmt_children.next()?;
if stmt_children.next().is_some() {
return None;
}
if new_stmt.text_range() != cand_root.text_range() {
return None;
}
let new_green_root = stmt.replace_with(new_stmt.green().into_owned());
let full = parse(new_src);
let spliced_root = SyntaxNode::new_root(new_green_root.clone());
if spliced_root.text() != new_src {
return None;
}
Some(make_parse(new_green_root, full.errors().to_vec()))
}
#[cfg(feature = "incremental")]
fn covering_statement(root: &SyntaxNode, range: TextRange) -> Option<SyntaxNode> {
let root_range = root.text_range();
if !root_range.contains_range(range) {
return None;
}
let elem = root.covering_element(range);
let start_node = match elem {
NodeOrToken::Node(n) => n,
NodeOrToken::Token(t) => t.parent()?,
};
let mut cur = Some(start_node);
while let Some(n) = cur {
if n.kind() == SyntaxKind::STATEMENT {
return Some(n);
}
cur = n.parent();
}
None
}
#[cfg(feature = "incremental")]
fn make_parse(green: rowan::GreenNode, errors: Vec<crate::SyntaxError>) -> Parse {
Parse::from_parts(green, errors)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insert_at_middle_preserves_prefix_and_suffix() {
let src = "RETURN 1";
let edit = TextEdit::insert(TextSize::from(6), "0");
let out = edit.apply(src);
assert_eq!(out, "RETURN0 1");
}
#[test]
fn replace_range() {
let src = "RETURN 1";
let range = TextRange::new(TextSize::from(7), TextSize::from(8));
let edit = TextEdit::replace(range, "42");
let out = edit.apply(src);
assert_eq!(out, "RETURN 42");
}
#[test]
fn delete_range() {
let src = "MATCH (n) RETURN n";
let range = TextRange::new(TextSize::from(0), TextSize::from(10));
let edit = TextEdit::replace(range, "");
let out = edit.apply(src);
assert_eq!(out, "RETURN n");
}
#[test]
fn out_of_range_saturates_to_end() {
let src = "RETURN 1";
let edit = TextEdit::insert(TextSize::from(999), ";");
let out = edit.apply(src);
assert_eq!(out, "RETURN 1;");
}
#[test]
fn incremental_reparse_roundtrips() {
let p = parse("RETURN 1");
let root = p.syntax();
let edit = TextEdit::replace(TextRange::new(TextSize::from(7), TextSize::from(8)), "42");
let np = incremental_reparse(&root, &edit);
assert_eq!(np.syntax().to_string(), "RETURN 42");
assert!(np.errors().is_empty(), "edit keeps the file parseable");
}
#[cfg(feature = "incremental")]
fn assert_equivalent_to_full(old: &SyntaxNode, edit: &TextEdit) -> Parse {
let new_src = edit.apply(&old.to_string());
let smart = incremental_reparse(old, edit);
let full = parse(&new_src);
assert_eq!(
smart.syntax().to_string(),
full.syntax().to_string(),
"smart-path text must equal whole-file parse text"
);
assert_eq!(
smart.errors().len(),
full.errors().len(),
"smart-path error count must equal whole-file ({}); errors = {:?}",
full.errors().len(),
smart
.errors()
.iter()
.map(|e| &e.message)
.collect::<Vec<_>>()
);
smart
}
#[test]
#[cfg(feature = "incremental")]
fn smart_path_inside_single_statement() {
let src = "MATCH (n) RETURN n;\nMATCH (m) RETURN m;\n";
let p = parse(src);
assert!(p.errors().is_empty(), "fixture parses clean");
let edit = TextEdit::replace(TextRange::new(TextSize::new(17), TextSize::new(18)), "x");
let np = assert_equivalent_to_full(&p.syntax(), &edit);
assert_eq!(
np.syntax().to_string(),
"MATCH (n) RETURN x;\nMATCH (m) RETURN m;\n"
);
}
#[test]
#[cfg(feature = "incremental")]
fn smart_path_clause_boundary_inside_statement() {
let src = "MATCH (n) RETURN n;\n";
let p = parse(src);
let edit = TextEdit::insert(TextSize::new(10), "WHERE n.x = 1 ");
let np = assert_equivalent_to_full(&p.syntax(), &edit);
assert_eq!(
np.syntax().to_string(),
"MATCH (n) WHERE n.x = 1 RETURN n;\n"
);
}
#[test]
#[cfg(feature = "incremental")]
fn smart_path_bails_when_introducing_semicolon() {
let src = "MATCH (n) RETURN n";
let p = parse(src);
let edit = TextEdit::insert(TextSize::new(18), "; MATCH (m) RETURN m");
let np = assert_equivalent_to_full(&p.syntax(), &edit);
assert_eq!(
np.syntax().to_string(),
"MATCH (n) RETURN n; MATCH (m) RETURN m"
);
}
#[test]
#[cfg(feature = "incremental")]
fn smart_path_introduces_syntax_error() {
let src = "MATCH (n) RETURN n;\n";
let p = parse(src);
let edit = TextEdit::replace(TextRange::new(TextSize::new(6), TextSize::new(9)), "(n");
let np = assert_equivalent_to_full(&p.syntax(), &edit);
assert!(!np.errors().is_empty(), "edit must produce errors");
assert_eq!(np.syntax().to_string(), "MATCH (n RETURN n;\n");
}
#[test]
#[cfg(feature = "incremental")]
fn smart_path_heals_syntax_error() {
let src = "MATCH (n RETURN n;\n";
let p = parse(src);
assert!(
!p.errors().is_empty(),
"fixture has the unclosed paren error"
);
let edit = TextEdit::insert(TextSize::new(8), ")");
let np = assert_equivalent_to_full(&p.syntax(), &edit);
assert_eq!(np.syntax().to_string(), "MATCH (n) RETURN n;\n");
assert!(np.errors().is_empty(), "heal must produce a clean tree");
}
}