Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/syntax/src/syntax_editor.rs')
-rw-r--r--crates/syntax/src/syntax_editor.rs333
1 files changed, 333 insertions, 0 deletions
diff --git a/crates/syntax/src/syntax_editor.rs b/crates/syntax/src/syntax_editor.rs
new file mode 100644
index 0000000000..3ec5abcaea
--- /dev/null
+++ b/crates/syntax/src/syntax_editor.rs
@@ -0,0 +1,333 @@
+//! Syntax Tree editor
+//!
+//! Inspired by Roslyn's [`SyntaxEditor`], but is temporarily built upon mutable syntax tree editing.
+//!
+//! [`SyntaxEditor`]: https://github.com/dotnet/roslyn/blob/43b0b05cc4f492fd5de00f6f6717409091df8daa/src/Workspaces/Core/Portable/Editing/SyntaxEditor.cs
+
+use std::{
+ num::NonZeroU32,
+ sync::atomic::{AtomicU32, Ordering},
+};
+
+use rowan::TextRange;
+use rustc_hash::FxHashMap;
+
+use crate::{SyntaxElement, SyntaxNode, SyntaxToken};
+
+mod edit_algo;
+mod mapping;
+
+pub use mapping::{SyntaxMapping, SyntaxMappingBuilder};
+
+#[derive(Debug)]
+pub struct SyntaxEditor {
+ root: SyntaxNode,
+ changes: Vec<Change>,
+ mappings: SyntaxMapping,
+ annotations: Vec<(SyntaxElement, SyntaxAnnotation)>,
+}
+
+impl SyntaxEditor {
+ /// Creates a syntax editor to start editing from `root`
+ pub fn new(root: SyntaxNode) -> Self {
+ Self { root, changes: vec![], mappings: SyntaxMapping::new(), annotations: vec![] }
+ }
+
+ pub fn add_annotation(&mut self, element: impl Element, annotation: SyntaxAnnotation) {
+ self.annotations.push((element.syntax_element(), annotation))
+ }
+
+ pub fn combine(&mut self, other: SyntaxEditor) {
+ todo!()
+ }
+
+ pub fn delete(&mut self, element: impl Element) {
+ self.changes.push(Change::Replace(element.syntax_element(), None));
+ }
+
+ pub fn replace(&mut self, old: impl Element, new: impl Element) {
+ self.changes.push(Change::Replace(old.syntax_element(), Some(new.syntax_element())));
+ }
+
+ pub fn finish(self) -> SyntaxEdit {
+ edit_algo::apply_edits(self)
+ }
+}
+
+pub struct SyntaxEdit {
+ root: SyntaxNode,
+ changed_elements: Vec<SyntaxElement>,
+ annotations: FxHashMap<SyntaxAnnotation, Vec<SyntaxElement>>,
+}
+
+impl SyntaxEdit {
+ pub fn root(&self) -> &SyntaxNode {
+ &self.root
+ }
+
+ pub fn changed_elements(&self) -> &[SyntaxElement] {
+ self.changed_elements.as_slice()
+ }
+
+ pub fn find_annotation(&self, annotation: SyntaxAnnotation) -> Option<&[SyntaxElement]> {
+ self.annotations.get(&annotation).as_ref().map(|it| it.as_slice())
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[repr(transparent)]
+pub struct SyntaxAnnotation(NonZeroU32);
+
+impl SyntaxAnnotation {
+ /// Creates a unique syntax annotation to attach data to.
+ pub fn new() -> Self {
+ static COUNTER: AtomicU32 = AtomicU32::new(1);
+
+ // We want the id to be unique across threads, but we don't want to
+ // tie it to other `SeqCst` operations.
+ let id = COUNTER.fetch_add(1, Ordering::AcqRel);
+
+ Self(NonZeroU32::new(id).expect("syntax annotation id overflow"))
+ }
+}
+
+/// Position describing where to insert elements
+#[derive(Debug)]
+pub struct Position {
+ repr: PositionRepr,
+}
+
+#[derive(Debug)]
+enum PositionRepr {
+ FirstChild(SyntaxNode),
+ After(SyntaxElement),
+}
+
+impl Position {
+ pub fn after(elem: impl Element) -> Position {
+ let repr = PositionRepr::After(elem.syntax_element());
+ Position { repr }
+ }
+
+ pub fn before(elem: impl Element) -> Position {
+ let elem = elem.syntax_element();
+ let repr = match elem.prev_sibling_or_token() {
+ Some(it) => PositionRepr::After(it),
+ None => PositionRepr::FirstChild(elem.parent().unwrap()),
+ };
+ Position { repr }
+ }
+
+ pub fn first_child_of(node: &(impl Into<SyntaxNode> + Clone)) -> Position {
+ let repr = PositionRepr::FirstChild(node.clone().into());
+ Position { repr }
+ }
+
+ pub fn last_child_of(node: &(impl Into<SyntaxNode> + Clone)) -> Position {
+ let node = node.clone().into();
+ let repr = match node.last_child_or_token() {
+ Some(it) => PositionRepr::After(it),
+ None => PositionRepr::FirstChild(node),
+ };
+ Position { repr }
+ }
+}
+
+#[derive(Debug)]
+enum Change {
+ /// Represents both a replace single element and a delete element operation.
+ Replace(SyntaxElement, Option<SyntaxElement>),
+}
+
+impl Change {
+ fn target_range(&self) -> TextRange {
+ match self {
+ Change::Replace(target, _) => target.text_range(),
+ }
+ }
+
+ fn target_parent(&self) -> SyntaxNode {
+ match self {
+ Change::Replace(target, _) => target.parent().unwrap(),
+ }
+ }
+
+ fn change_kind(&self) -> ChangeKind {
+ match self {
+ Change::Replace(_, _) => ChangeKind::Replace,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+enum ChangeKind {
+ Insert,
+ // TODO: deal with replace spans
+ Replace,
+}
+
+/// Utility trait to allow calling syntax editor functions with references or owned
+/// nodes. Do not use outside of this module.
+pub trait Element {
+ fn syntax_element(self) -> SyntaxElement;
+}
+
+impl<E: Element + Clone> Element for &'_ E {
+ fn syntax_element(self) -> SyntaxElement {
+ self.clone().syntax_element()
+ }
+}
+
+impl Element for SyntaxElement {
+ fn syntax_element(self) -> SyntaxElement {
+ self
+ }
+}
+
+impl Element for SyntaxNode {
+ fn syntax_element(self) -> SyntaxElement {
+ self.into()
+ }
+}
+
+impl Element for SyntaxToken {
+ fn syntax_element(self) -> SyntaxElement {
+ self.into()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use expect_test::expect;
+ use itertools::Itertools;
+
+ use crate::{
+ ast::{self, make, HasName},
+ AstNode,
+ };
+
+ use super::*;
+
+ fn make_ident_pat(
+ editor: Option<&mut SyntaxEditor>,
+ ref_: bool,
+ mut_: bool,
+ name: ast::Name,
+ ) -> ast::IdentPat {
+ let ast = make::ident_pat(ref_, mut_, name.clone()).clone_for_update();
+
+ if let Some(editor) = editor {
+ let mut mapping = SyntaxMappingBuilder::new(ast.syntax().clone());
+ mapping.map_node(name.syntax().clone(), ast.name().unwrap().syntax().clone());
+ mapping.finish(editor);
+ }
+
+ ast
+ }
+
+ fn make_let_stmt(
+ editor: Option<&mut SyntaxEditor>,
+ pattern: ast::Pat,
+ ty: Option<ast::Type>,
+ initializer: Option<ast::Expr>,
+ ) -> ast::LetStmt {
+ let ast =
+ make::let_stmt(pattern.clone(), ty.clone(), initializer.clone()).clone_for_update();
+
+ if let Some(editor) = editor {
+ let mut mapping = SyntaxMappingBuilder::new(ast.syntax().clone());
+ mapping.map_node(pattern.syntax().clone(), ast.pat().unwrap().syntax().clone());
+ if let Some(input) = ty {
+ mapping.map_node(input.syntax().clone(), ast.ty().unwrap().syntax().clone());
+ }
+ if let Some(input) = initializer {
+ mapping
+ .map_node(input.syntax().clone(), ast.initializer().unwrap().syntax().clone());
+ }
+ mapping.finish(editor);
+ }
+
+ ast
+ }
+
+ fn make_block_expr(
+ editor: Option<&mut SyntaxEditor>,
+ stmts: impl IntoIterator<Item = ast::Stmt>,
+ tail_expr: Option<ast::Expr>,
+ ) -> ast::BlockExpr {
+ let stmts = stmts.into_iter().collect_vec();
+ let input = stmts.iter().map(|it| it.syntax().clone()).collect_vec();
+
+ let ast = make::block_expr(stmts, tail_expr.clone()).clone_for_update();
+
+ if let Some((editor, stmt_list)) = editor.zip(ast.stmt_list()) {
+ let mut mapping = SyntaxMappingBuilder::new(stmt_list.syntax().clone());
+
+ mapping.map_children(
+ input.into_iter(),
+ stmt_list.statements().map(|it| it.syntax().clone()),
+ );
+
+ if let Some((input, output)) = tail_expr.zip(stmt_list.tail_expr()) {
+ mapping.map_node(input.syntax().clone(), output.syntax().clone());
+ }
+
+ mapping.finish(editor);
+ }
+
+ ast
+ }
+
+ #[test]
+ fn it() {
+ let root = make::match_arm(
+ [make::wildcard_pat().into()],
+ None,
+ make::expr_tuple([
+ make::expr_bin_op(
+ make::expr_literal("2").into(),
+ ast::BinaryOp::ArithOp(ast::ArithOp::Add),
+ make::expr_literal("2").into(),
+ ),
+ make::expr_literal("true").into(),
+ ]),
+ );
+
+ let to_wrap = root.syntax().descendants().find_map(ast::TupleExpr::cast).unwrap();
+ let to_replace = root.syntax().descendants().find_map(ast::BinExpr::cast).unwrap();
+
+ let mut editor = SyntaxEditor::new(root.syntax().clone());
+
+ let name = make::name("var_name");
+ let name_ref = make::name_ref("var_name").clone_for_update();
+
+ let placeholder_snippet = SyntaxAnnotation::new();
+ editor.add_annotation(name.syntax(), placeholder_snippet);
+ editor.add_annotation(name_ref.syntax(), placeholder_snippet);
+
+ let make_ident_pat = make_ident_pat(Some(&mut editor), false, false, name);
+ let make_let_stmt = make_let_stmt(
+ Some(&mut editor),
+ make_ident_pat.into(),
+ None,
+ Some(to_replace.clone().into()),
+ );
+ let new_block = make_block_expr(
+ Some(&mut editor),
+ [make_let_stmt.into()],
+ Some(to_wrap.clone().into()),
+ );
+
+ // should die:
+ editor.replace(to_replace.syntax(), name_ref.syntax());
+ editor.replace(to_wrap.syntax(), new_block.syntax());
+ // editor.replace(to_replace.syntax(), name_ref.syntax());
+
+ // dbg!(&editor.mappings);
+ let edit = editor.finish();
+
+ let expect = expect![];
+ expect.assert_eq(&edit.root.to_string());
+ assert_eq!(edit.find_annotation(placeholder_snippet).map(|it| it.len()), Some(2));
+ }
+}