Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/syntax/src/syntax_editor/edits.rs')
| -rw-r--r-- | crates/syntax/src/syntax_editor/edits.rs | 206 |
1 files changed, 204 insertions, 2 deletions
diff --git a/crates/syntax/src/syntax_editor/edits.rs b/crates/syntax/src/syntax_editor/edits.rs index 73196f5cb1..8069fdd06f 100644 --- a/crates/syntax/src/syntax_editor/edits.rs +++ b/crates/syntax/src/syntax_editor/edits.rs @@ -1,8 +1,12 @@ //! Structural editing for ast using `SyntaxEditor` use crate::{ - ast::make, ast::AstNode, ast::Fn, ast::GenericParam, ast::HasGenericParams, ast::HasName, - syntax_editor::Position, syntax_editor::SyntaxEditor, SyntaxKind, + ast::{ + self, edit::IndentLevel, make, syntax_factory::SyntaxFactory, AstNode, Fn, GenericParam, + HasGenericParams, HasName, + }, + syntax_editor::{Position, SyntaxEditor}, + Direction, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, T, }; impl SyntaxEditor { @@ -70,3 +74,201 @@ impl SyntaxEditor { } } } + +fn get_or_insert_comma_after(editor: &mut SyntaxEditor, syntax: &SyntaxNode) -> SyntaxToken { + let make = SyntaxFactory::without_mappings(); + match syntax + .siblings_with_tokens(Direction::Next) + .filter_map(|it| it.into_token()) + .find(|it| it.kind() == T![,]) + { + Some(it) => it, + None => { + let comma = make.token(T![,]); + editor.insert(Position::after(syntax), &comma); + comma + } + } +} + +impl ast::VariantList { + pub fn add_variant(&self, editor: &mut SyntaxEditor, variant: &ast::Variant) { + let make = SyntaxFactory::without_mappings(); + let (indent, position) = match self.variants().last() { + Some(last_item) => ( + IndentLevel::from_node(last_item.syntax()), + Position::after(get_or_insert_comma_after(editor, last_item.syntax())), + ), + None => match self.l_curly_token() { + Some(l_curly) => { + normalize_ws_between_braces(editor, self.syntax()); + (IndentLevel::from_token(&l_curly) + 1, Position::after(&l_curly)) + } + None => (IndentLevel::single(), Position::last_child_of(self.syntax())), + }, + }; + let elements: Vec<SyntaxElement> = vec![ + make.whitespace(&format!("{}{indent}", "\n")).into(), + variant.syntax().clone().into(), + make.token(T![,]).into(), + ]; + editor.insert_all(position, elements); + } +} + +fn normalize_ws_between_braces(editor: &mut SyntaxEditor, node: &SyntaxNode) -> Option<()> { + let make = SyntaxFactory::without_mappings(); + let l = node + .children_with_tokens() + .filter_map(|it| it.into_token()) + .find(|it| it.kind() == T!['{'])?; + let r = node + .children_with_tokens() + .filter_map(|it| it.into_token()) + .find(|it| it.kind() == T!['}'])?; + + let indent = IndentLevel::from_node(node); + + match l.next_sibling_or_token() { + Some(ws) if ws.kind() == SyntaxKind::WHITESPACE => { + if ws.next_sibling_or_token()?.into_token()? == r { + editor.replace(ws, make.whitespace(&format!("\n{indent}"))); + } + } + Some(ws) if ws.kind() == T!['}'] => { + editor.insert(Position::after(l), make.whitespace(&format!("\n{indent}"))); + } + _ => (), + } + Some(()) +} + +#[cfg(test)] +mod tests { + use parser::Edition; + use stdx::trim_indent; + use test_utils::assert_eq_text; + + use crate::SourceFile; + + use super::*; + + fn ast_from_text<N: AstNode>(text: &str) -> N { + let parse = SourceFile::parse(text, Edition::CURRENT); + let node = match parse.tree().syntax().descendants().find_map(N::cast) { + Some(it) => it, + None => { + let node = std::any::type_name::<N>(); + panic!("Failed to make ast node `{node}` from text {text}") + } + }; + let node = node.clone_subtree(); + assert_eq!(node.syntax().text_range().start(), 0.into()); + node + } + + #[test] + fn add_variant_to_empty_enum() { + let make = SyntaxFactory::without_mappings(); + let variant = make.variant(None, make.name("Bar"), None, None); + + check_add_variant( + r#" +enum Foo {} +"#, + r#" +enum Foo { + Bar, +} +"#, + variant, + ); + } + + #[test] + fn add_variant_to_non_empty_enum() { + let make = SyntaxFactory::without_mappings(); + let variant = make.variant(None, make.name("Baz"), None, None); + + check_add_variant( + r#" +enum Foo { + Bar, +} +"#, + r#" +enum Foo { + Bar, + Baz, +} +"#, + variant, + ); + } + + #[test] + fn add_variant_with_tuple_field_list() { + let make = SyntaxFactory::without_mappings(); + let variant = make.variant( + None, + make.name("Baz"), + Some(make.tuple_field_list([make.tuple_field(None, make.ty("bool"))]).into()), + None, + ); + + check_add_variant( + r#" +enum Foo { + Bar, +} +"#, + r#" +enum Foo { + Bar, + Baz(bool), +} +"#, + variant, + ); + } + + #[test] + fn add_variant_with_record_field_list() { + let make = SyntaxFactory::without_mappings(); + let variant = make.variant( + None, + make.name("Baz"), + Some( + make.record_field_list([make.record_field(None, make.name("x"), make.ty("bool"))]) + .into(), + ), + None, + ); + + check_add_variant( + r#" +enum Foo { + Bar, +} +"#, + r#" +enum Foo { + Bar, + Baz { x: bool }, +} +"#, + variant, + ); + } + + fn check_add_variant(before: &str, expected: &str, variant: ast::Variant) { + let enum_ = ast_from_text::<ast::Enum>(before); + let mut editor = SyntaxEditor::new(enum_.syntax().clone()); + if let Some(it) = enum_.variant_list() { + it.add_variant(&mut editor, &variant) + } + let edit = editor.finish(); + let after = edit.new_root.to_string(); + assert_eq_text!(&trim_indent(expected.trim()), &trim_indent(after.trim())); + } +} |