use rowan::{GreenNode, GreenToken, NodeOrToken};
use crate::lexer;
use crate::parser::CstDocument;
use crate::syntax::kind::RonLang;
use crate::syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditTarget {
Node(SyntaxNode),
TokenSpan {
first: SyntaxToken,
last: SyntaxToken,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EditKind {
Insert,
Replace,
Remove,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TriviaPolicy {
pub keep_leading: bool,
pub keep_trailing: bool,
}
impl TriviaPolicy {
pub const KEEP_ALL: Self = Self {
keep_leading: true,
keep_trailing: true,
};
pub const DISCARD_ALL: Self = Self {
keep_leading: false,
keep_trailing: false,
};
}
impl Default for TriviaPolicy {
#[inline]
fn default() -> Self {
Self::KEEP_ALL
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EditOperation {
pub target: EditTarget,
pub kind: EditKind,
pub payload: Option<String>,
pub trivia_policy: TriviaPolicy,
}
impl EditOperation {
#[must_use]
pub fn insert(
target: EditTarget,
text: impl Into<String>,
trivia_policy: TriviaPolicy,
) -> Self {
Self {
target,
kind: EditKind::Insert,
payload: Some(text.into()),
trivia_policy,
}
}
#[must_use]
pub fn replace(
target: EditTarget,
text: impl Into<String>,
trivia_policy: TriviaPolicy,
) -> Self {
Self {
target,
kind: EditKind::Replace,
payload: Some(text.into()),
trivia_policy,
}
}
#[must_use]
pub fn remove(target: EditTarget, trivia_policy: TriviaPolicy) -> Self {
Self {
target,
kind: EditKind::Remove,
payload: None,
trivia_policy,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditError {
RootNotEditable,
SpanParentMismatch,
SpanOutOfOrder,
TargetNotFound,
}
impl std::fmt::Display for EditError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::RootNotEditable => "the document root cannot be edited",
Self::SpanParentMismatch => "token-span endpoints have different parents",
Self::SpanOutOfOrder => "token-span `last` precedes `first`",
Self::TargetNotFound => "edit target not found in its parent",
};
f.write_str(s)
}
}
impl std::error::Error for EditError {}
pub fn apply_edit(doc: &CstDocument, edit: EditOperation) -> Result<CstDocument, EditError> {
let (parent, start, end) = resolve_target(&edit.target)?;
if !belongs_to(&parent, doc) {
return Err(EditError::TargetNotFound);
}
let parent_green = parent.raw().green().into_owned();
let child_count = parent_green.children().count();
let (mut splice_start, mut splice_end) = (start, end);
if !edit.trivia_policy.keep_leading {
splice_start = absorb_leading_trivia(&parent_green, splice_start);
}
if !edit.trivia_policy.keep_trailing {
splice_end = absorb_trailing_trivia(&parent_green, splice_end, child_count);
}
let payload_elems: Vec<GreenElem> = edit
.payload
.as_deref()
.map(payload_to_green)
.unwrap_or_default();
let mut children: Vec<GreenElem> = parent_green
.children()
.map(|c| match c {
NodeOrToken::Node(n) => NodeOrToken::Node(n.to_owned()),
NodeOrToken::Token(t) => NodeOrToken::Token(t.to_owned()),
})
.collect();
match edit.kind {
EditKind::Insert => {
let at = if edit.trivia_policy.keep_leading {
start
} else {
splice_start
};
splice(&mut children, at..at, payload_elems);
}
EditKind::Replace => {
splice(&mut children, splice_start..splice_end + 1, payload_elems);
}
EditKind::Remove => {
splice(&mut children, splice_start..splice_end + 1, Vec::new());
}
}
let new_parent = GreenNode::new(rowan_kind(parent.kind()), children);
let new_root_green = parent.raw().replace_with(new_parent);
Ok(CstDocument::from_green_for_edit(new_root_green))
}
type GreenElem = NodeOrToken<GreenNode, GreenToken>;
fn belongs_to(node: &SyntaxNode, doc: &CstDocument) -> bool {
let mut top = node.clone();
while let Some(p) = top.parent() {
top = p;
}
top == doc.root()
}
fn resolve_target(target: &EditTarget) -> Result<(SyntaxNode, usize, usize), EditError> {
match target {
EditTarget::Node(node) => {
let parent = node.parent().ok_or(EditError::RootNotEditable)?;
let idx = child_index_of_node(&parent, node).ok_or(EditError::TargetNotFound)?;
Ok((parent, idx, idx))
}
EditTarget::TokenSpan { first, last } => {
let parent = first.parent().ok_or(EditError::RootNotEditable)?;
let last_parent = last.parent().ok_or(EditError::RootNotEditable)?;
if parent != last_parent {
return Err(EditError::SpanParentMismatch);
}
let start = child_index_of_token(&parent, first).ok_or(EditError::TargetNotFound)?;
let end = child_index_of_token(&parent, last).ok_or(EditError::TargetNotFound)?;
if end < start {
return Err(EditError::SpanOutOfOrder);
}
Ok((parent, start, end))
}
}
}
fn child_index_of_node(parent: &SyntaxNode, node: &SyntaxNode) -> Option<usize> {
parent
.children_with_tokens()
.position(|el| el.as_node() == Some(node))
}
fn child_index_of_token(parent: &SyntaxNode, token: &SyntaxToken) -> Option<usize> {
parent
.children_with_tokens()
.position(|el| el.as_token() == Some(token))
}
fn absorb_leading_trivia(parent: &rowan::GreenNodeData, start: usize) -> usize {
let kinds = child_kinds(parent);
let mut i = start;
while i > 0 && kinds[i - 1].is_trivia() {
i -= 1;
}
i
}
fn absorb_trailing_trivia(parent: &rowan::GreenNodeData, end: usize, count: usize) -> usize {
let kinds = child_kinds(parent);
let mut i = end;
while i + 1 < count && kinds[i + 1].is_trivia() {
i += 1;
}
i
}
fn child_kinds(parent: &rowan::GreenNodeData) -> Vec<SyntaxKind> {
parent
.children()
.map(|c| {
let raw = match c {
NodeOrToken::Node(n) => n.kind(),
NodeOrToken::Token(t) => t.kind(),
};
SyntaxKind::from_raw(raw.0).unwrap_or(SyntaxKind::Error)
})
.collect()
}
fn payload_to_green(text: &str) -> Vec<GreenElem> {
lexer::tokenize(text)
.into_iter()
.map(|t| NodeOrToken::Token(GreenToken::new(rowan_kind(t.kind), t.text)))
.collect()
}
fn splice(
children: &mut Vec<GreenElem>,
range: std::ops::Range<usize>,
replacement: Vec<GreenElem>,
) {
children.splice(range, replacement);
}
#[inline]
fn rowan_kind(kind: SyntaxKind) -> rowan::SyntaxKind {
<RonLang as rowan::Language>::kind_to_raw(kind)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
use crate::printer::print;
fn first_node(doc: &CstDocument, kind: SyntaxKind) -> SyntaxNode {
fn walk(n: SyntaxNode, kind: SyntaxKind, out: &mut Option<SyntaxNode>) {
if out.is_some() {
return;
}
if n.kind() == kind {
*out = Some(n.clone());
return;
}
for c in n.children() {
walk(c, kind, out);
}
}
let mut out = None;
walk(doc.root(), kind, &mut out);
out.unwrap_or_else(|| panic!("no {kind:?} node found"))
}
#[test]
fn replace_node_keeps_unaffected_regions() {
let src = "Foo(x: 1, y: 2)";
let doc = parse(src);
let field = first_node(&doc, SyntaxKind::StructField);
let lit = field
.children()
.find(|c| c.kind() == SyntaxKind::Literal)
.unwrap();
let edited = apply_edit(
&doc,
EditOperation::replace(EditTarget::Node(lit), "99", TriviaPolicy::KEEP_ALL),
)
.unwrap();
assert_eq!(print(&edited), "Foo(x: 99, y: 2)");
assert_eq!(print(&doc), src);
}
#[test]
fn remove_node_keep_trivia() {
let src = "[1, 2, 3]";
let doc = parse(src);
let list = first_node(&doc, SyntaxKind::List);
let first_lit = list
.children()
.find(|c| c.kind() == SyntaxKind::Literal)
.unwrap();
let edited = apply_edit(
&doc,
EditOperation::remove(EditTarget::Node(first_lit), TriviaPolicy::KEEP_ALL),
)
.unwrap();
assert_eq!(print(&edited), "[, 2, 3]");
}
#[test]
fn insert_before_node() {
let src = "[1]";
let doc = parse(src);
let list = first_node(&doc, SyntaxKind::List);
let lit = list
.children()
.find(|c| c.kind() == SyntaxKind::Literal)
.unwrap();
let edited = apply_edit(
&doc,
EditOperation::insert(EditTarget::Node(lit), "0, ", TriviaPolicy::KEEP_ALL),
)
.unwrap();
assert_eq!(print(&edited), "[0, 1]");
}
#[test]
fn token_span_replace() {
let src = "Foo(x: 1)";
let doc = parse(src);
let strukt = first_node(&doc, SyntaxKind::Struct);
let name = strukt.first_token_of(SyntaxKind::Ident).unwrap();
let edited = apply_edit(
&doc,
EditOperation::replace(
EditTarget::TokenSpan {
first: name.clone(),
last: name,
},
"Bar",
TriviaPolicy::KEEP_ALL,
),
)
.unwrap();
assert_eq!(print(&edited), "Bar(x: 1)");
}
#[test]
fn root_is_not_editable() {
let doc = parse("1");
let err = apply_edit(
&doc,
EditOperation::remove(EditTarget::Node(doc.root()), TriviaPolicy::KEEP_ALL),
)
.unwrap_err();
assert_eq!(err, EditError::RootNotEditable);
}
}