|
| 1 | +//! Syntax Tree editor |
| 2 | +//! |
| 3 | +//! Inspired by Roslyn's [`SyntaxEditor`], but is temporarily built upon mutable syntax tree editing. |
| 4 | +//! |
| 5 | +//! [`SyntaxEditor`]: https://github.com/dotnet/roslyn/blob/43b0b05cc4f492fd5de00f6f6717409091df8daa/src/Workspaces/Core/Portable/Editing/SyntaxEditor.cs |
| 6 | +
|
| 7 | +use std::{ |
| 8 | + num::NonZeroU32, |
| 9 | + sync::atomic::{AtomicU32, Ordering}, |
| 10 | +}; |
| 11 | + |
| 12 | +use rowan::TextRange; |
| 13 | +use rustc_hash::FxHashMap; |
| 14 | + |
| 15 | +use crate::{SyntaxElement, SyntaxNode, SyntaxToken}; |
| 16 | + |
| 17 | +mod edit_algo; |
| 18 | +mod mapping; |
| 19 | + |
| 20 | +pub use mapping::{SyntaxMapping, SyntaxMappingBuilder}; |
| 21 | + |
| 22 | +#[derive(Debug)] |
| 23 | +pub struct SyntaxEditor { |
| 24 | + root: SyntaxNode, |
| 25 | + changes: Vec<Change>, |
| 26 | + mappings: SyntaxMapping, |
| 27 | + annotations: Vec<(SyntaxElement, SyntaxAnnotation)>, |
| 28 | +} |
| 29 | + |
| 30 | +impl SyntaxEditor { |
| 31 | + /// Creates a syntax editor to start editing from `root` |
| 32 | + pub fn new(root: SyntaxNode) -> Self { |
| 33 | + Self { root, changes: vec![], mappings: SyntaxMapping::new(), annotations: vec![] } |
| 34 | + } |
| 35 | + |
| 36 | + pub fn add_annotation(&mut self, element: impl Element, annotation: SyntaxAnnotation) { |
| 37 | + self.annotations.push((element.syntax_element(), annotation)) |
| 38 | + } |
| 39 | + |
| 40 | + pub fn combine(&mut self, other: SyntaxEditor) { |
| 41 | + todo!() |
| 42 | + } |
| 43 | + |
| 44 | + pub fn delete(&mut self, element: impl Element) { |
| 45 | + self.changes.push(Change::Replace(element.syntax_element(), None)); |
| 46 | + } |
| 47 | + |
| 48 | + pub fn replace(&mut self, old: impl Element, new: impl Element) { |
| 49 | + self.changes.push(Change::Replace(old.syntax_element(), Some(new.syntax_element()))); |
| 50 | + } |
| 51 | + |
| 52 | + pub fn finish(self) -> SyntaxEdit { |
| 53 | + edit_algo::apply_edits(self) |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +pub struct SyntaxEdit { |
| 58 | + root: SyntaxNode, |
| 59 | + changed_elements: Vec<SyntaxElement>, |
| 60 | + annotations: FxHashMap<SyntaxAnnotation, Vec<SyntaxElement>>, |
| 61 | +} |
| 62 | + |
| 63 | +impl SyntaxEdit { |
| 64 | + pub fn root(&self) -> &SyntaxNode { |
| 65 | + &self.root |
| 66 | + } |
| 67 | + |
| 68 | + pub fn changed_elements(&self) -> &[SyntaxElement] { |
| 69 | + self.changed_elements.as_slice() |
| 70 | + } |
| 71 | + |
| 72 | + pub fn find_annotation(&self, annotation: SyntaxAnnotation) -> Option<&[SyntaxElement]> { |
| 73 | + self.annotations.get(&annotation).as_ref().map(|it| it.as_slice()) |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
| 78 | +#[repr(transparent)] |
| 79 | +pub struct SyntaxAnnotation(NonZeroU32); |
| 80 | + |
| 81 | +impl SyntaxAnnotation { |
| 82 | + /// Creates a unique syntax annotation to attach data to. |
| 83 | + pub fn new() -> Self { |
| 84 | + static COUNTER: AtomicU32 = AtomicU32::new(1); |
| 85 | + |
| 86 | + // We want the id to be unique across threads, but we don't want to |
| 87 | + // tie it to other `SeqCst` operations. |
| 88 | + let id = COUNTER.fetch_add(1, Ordering::AcqRel); |
| 89 | + |
| 90 | + Self(NonZeroU32::new(id).expect("syntax annotation id overflow")) |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +/// Position describing where to insert elements |
| 95 | +#[derive(Debug)] |
| 96 | +pub struct Position { |
| 97 | + repr: PositionRepr, |
| 98 | +} |
| 99 | + |
| 100 | +#[derive(Debug)] |
| 101 | +enum PositionRepr { |
| 102 | + FirstChild(SyntaxNode), |
| 103 | + After(SyntaxElement), |
| 104 | +} |
| 105 | + |
| 106 | +impl Position { |
| 107 | + pub fn after(elem: impl Element) -> Position { |
| 108 | + let repr = PositionRepr::After(elem.syntax_element()); |
| 109 | + Position { repr } |
| 110 | + } |
| 111 | + |
| 112 | + pub fn before(elem: impl Element) -> Position { |
| 113 | + let elem = elem.syntax_element(); |
| 114 | + let repr = match elem.prev_sibling_or_token() { |
| 115 | + Some(it) => PositionRepr::After(it), |
| 116 | + None => PositionRepr::FirstChild(elem.parent().unwrap()), |
| 117 | + }; |
| 118 | + Position { repr } |
| 119 | + } |
| 120 | + |
| 121 | + pub fn first_child_of(node: &(impl Into<SyntaxNode> + Clone)) -> Position { |
| 122 | + let repr = PositionRepr::FirstChild(node.clone().into()); |
| 123 | + Position { repr } |
| 124 | + } |
| 125 | + |
| 126 | + pub fn last_child_of(node: &(impl Into<SyntaxNode> + Clone)) -> Position { |
| 127 | + let node = node.clone().into(); |
| 128 | + let repr = match node.last_child_or_token() { |
| 129 | + Some(it) => PositionRepr::After(it), |
| 130 | + None => PositionRepr::FirstChild(node), |
| 131 | + }; |
| 132 | + Position { repr } |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +#[derive(Debug)] |
| 137 | +enum Change { |
| 138 | + /// Represents both a replace single element and a delete element operation. |
| 139 | + Replace(SyntaxElement, Option<SyntaxElement>), |
| 140 | +} |
| 141 | + |
| 142 | +impl Change { |
| 143 | + fn target_range(&self) -> TextRange { |
| 144 | + match self { |
| 145 | + Change::Replace(target, _) => target.text_range(), |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + fn target_parent(&self) -> SyntaxNode { |
| 150 | + match self { |
| 151 | + Change::Replace(target, _) => target.parent().unwrap(), |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + fn change_kind(&self) -> ChangeKind { |
| 156 | + match self { |
| 157 | + Change::Replace(_, _) => ChangeKind::Replace, |
| 158 | + } |
| 159 | + } |
| 160 | +} |
| 161 | + |
| 162 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
| 163 | +enum ChangeKind { |
| 164 | + Insert, |
| 165 | + // TODO: deal with replace spans |
| 166 | + Replace, |
| 167 | +} |
| 168 | + |
| 169 | +/// Utility trait to allow calling syntax editor functions with references or owned |
| 170 | +/// nodes. Do not use outside of this module. |
| 171 | +pub trait Element { |
| 172 | + fn syntax_element(self) -> SyntaxElement; |
| 173 | +} |
| 174 | + |
| 175 | +impl<E: Element + Clone> Element for &'_ E { |
| 176 | + fn syntax_element(self) -> SyntaxElement { |
| 177 | + self.clone().syntax_element() |
| 178 | + } |
| 179 | +} |
| 180 | + |
| 181 | +impl Element for SyntaxElement { |
| 182 | + fn syntax_element(self) -> SyntaxElement { |
| 183 | + self |
| 184 | + } |
| 185 | +} |
| 186 | + |
| 187 | +impl Element for SyntaxNode { |
| 188 | + fn syntax_element(self) -> SyntaxElement { |
| 189 | + self.into() |
| 190 | + } |
| 191 | +} |
| 192 | + |
| 193 | +impl Element for SyntaxToken { |
| 194 | + fn syntax_element(self) -> SyntaxElement { |
| 195 | + self.into() |
| 196 | + } |
| 197 | +} |
| 198 | + |
| 199 | +#[cfg(test)] |
| 200 | +mod tests { |
| 201 | + use expect_test::expect; |
| 202 | + use itertools::Itertools; |
| 203 | + |
| 204 | + use crate::{ |
| 205 | + ast::{self, make, HasName}, |
| 206 | + AstNode, |
| 207 | + }; |
| 208 | + |
| 209 | + use super::*; |
| 210 | + |
| 211 | + fn make_ident_pat( |
| 212 | + editor: Option<&mut SyntaxEditor>, |
| 213 | + ref_: bool, |
| 214 | + mut_: bool, |
| 215 | + name: ast::Name, |
| 216 | + ) -> ast::IdentPat { |
| 217 | + let ast = make::ident_pat(ref_, mut_, name.clone()).clone_for_update(); |
| 218 | + |
| 219 | + if let Some(editor) = editor { |
| 220 | + let mut mapping = SyntaxMappingBuilder::new(ast.syntax().clone()); |
| 221 | + mapping.map_node(name.syntax().clone(), ast.name().unwrap().syntax().clone()); |
| 222 | + mapping.finish(editor); |
| 223 | + } |
| 224 | + |
| 225 | + ast |
| 226 | + } |
| 227 | + |
| 228 | + fn make_let_stmt( |
| 229 | + editor: Option<&mut SyntaxEditor>, |
| 230 | + pattern: ast::Pat, |
| 231 | + ty: Option<ast::Type>, |
| 232 | + initializer: Option<ast::Expr>, |
| 233 | + ) -> ast::LetStmt { |
| 234 | + let ast = |
| 235 | + make::let_stmt(pattern.clone(), ty.clone(), initializer.clone()).clone_for_update(); |
| 236 | + |
| 237 | + if let Some(editor) = editor { |
| 238 | + let mut mapping = SyntaxMappingBuilder::new(ast.syntax().clone()); |
| 239 | + mapping.map_node(pattern.syntax().clone(), ast.pat().unwrap().syntax().clone()); |
| 240 | + if let Some(input) = ty { |
| 241 | + mapping.map_node(input.syntax().clone(), ast.ty().unwrap().syntax().clone()); |
| 242 | + } |
| 243 | + if let Some(input) = initializer { |
| 244 | + mapping |
| 245 | + .map_node(input.syntax().clone(), ast.initializer().unwrap().syntax().clone()); |
| 246 | + } |
| 247 | + mapping.finish(editor); |
| 248 | + } |
| 249 | + |
| 250 | + ast |
| 251 | + } |
| 252 | + |
| 253 | + fn make_block_expr( |
| 254 | + editor: Option<&mut SyntaxEditor>, |
| 255 | + stmts: impl IntoIterator<Item = ast::Stmt>, |
| 256 | + tail_expr: Option<ast::Expr>, |
| 257 | + ) -> ast::BlockExpr { |
| 258 | + let stmts = stmts.into_iter().collect_vec(); |
| 259 | + let input = stmts.iter().map(|it| it.syntax().clone()).collect_vec(); |
| 260 | + |
| 261 | + let ast = make::block_expr(stmts, tail_expr.clone()).clone_for_update(); |
| 262 | + |
| 263 | + if let Some((editor, stmt_list)) = editor.zip(ast.stmt_list()) { |
| 264 | + let mut mapping = SyntaxMappingBuilder::new(stmt_list.syntax().clone()); |
| 265 | + |
| 266 | + mapping.map_children( |
| 267 | + input.into_iter(), |
| 268 | + stmt_list.statements().map(|it| it.syntax().clone()), |
| 269 | + ); |
| 270 | + |
| 271 | + if let Some((input, output)) = tail_expr.zip(stmt_list.tail_expr()) { |
| 272 | + mapping.map_node(input.syntax().clone(), output.syntax().clone()); |
| 273 | + } |
| 274 | + |
| 275 | + mapping.finish(editor); |
| 276 | + } |
| 277 | + |
| 278 | + ast |
| 279 | + } |
| 280 | + |
| 281 | + #[test] |
| 282 | + fn it() { |
| 283 | + let root = make::match_arm( |
| 284 | + [make::wildcard_pat().into()], |
| 285 | + None, |
| 286 | + make::expr_tuple([ |
| 287 | + make::expr_bin_op( |
| 288 | + make::expr_literal("2").into(), |
| 289 | + ast::BinaryOp::ArithOp(ast::ArithOp::Add), |
| 290 | + make::expr_literal("2").into(), |
| 291 | + ), |
| 292 | + make::expr_literal("true").into(), |
| 293 | + ]), |
| 294 | + ); |
| 295 | + |
| 296 | + let to_wrap = root.syntax().descendants().find_map(ast::TupleExpr::cast).unwrap(); |
| 297 | + let to_replace = root.syntax().descendants().find_map(ast::BinExpr::cast).unwrap(); |
| 298 | + |
| 299 | + let mut editor = SyntaxEditor::new(root.syntax().clone()); |
| 300 | + |
| 301 | + let name = make::name("var_name"); |
| 302 | + let name_ref = make::name_ref("var_name").clone_for_update(); |
| 303 | + |
| 304 | + let placeholder_snippet = SyntaxAnnotation::new(); |
| 305 | + editor.add_annotation(name.syntax(), placeholder_snippet); |
| 306 | + editor.add_annotation(name_ref.syntax(), placeholder_snippet); |
| 307 | + |
| 308 | + let make_ident_pat = make_ident_pat(Some(&mut editor), false, false, name); |
| 309 | + let make_let_stmt = make_let_stmt( |
| 310 | + Some(&mut editor), |
| 311 | + make_ident_pat.into(), |
| 312 | + None, |
| 313 | + Some(to_replace.clone().into()), |
| 314 | + ); |
| 315 | + let new_block = make_block_expr( |
| 316 | + Some(&mut editor), |
| 317 | + [make_let_stmt.into()], |
| 318 | + Some(to_wrap.clone().into()), |
| 319 | + ); |
| 320 | + |
| 321 | + // should die: |
| 322 | + editor.replace(to_replace.syntax(), name_ref.syntax()); |
| 323 | + editor.replace(to_wrap.syntax(), new_block.syntax()); |
| 324 | + // editor.replace(to_replace.syntax(), name_ref.syntax()); |
| 325 | + |
| 326 | + // dbg!(&editor.mappings); |
| 327 | + let edit = editor.finish(); |
| 328 | + |
| 329 | + let expect = expect![]; |
| 330 | + expect.assert_eq(&edit.root.to_string()); |
| 331 | + assert_eq!(edit.find_annotation(placeholder_snippet).map(|it| it.len()), Some(2)); |
| 332 | + } |
| 333 | +} |
0 commit comments