start structured editing API

This commit is contained in:
Aleksey Kladov 2019-04-21 17:47:55 +03:00
parent ee94edc722
commit 7cc845e88d
7 changed files with 240 additions and 3 deletions

2
Cargo.lock generated
View file

@ -903,8 +903,10 @@ version = "0.1.0"
name = "ra_assists" name = "ra_assists"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"join_to_string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "join_to_string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"ra_db 0.1.0", "ra_db 0.1.0",
"ra_fmt 0.1.0", "ra_fmt 0.1.0",
"ra_hir 0.1.0", "ra_hir 0.1.0",

View file

@ -5,8 +5,10 @@ version = "0.1.0"
authors = ["rust-analyzer developers"] authors = ["rust-analyzer developers"]
[dependencies] [dependencies]
lazy_static = "1.3.0"
join_to_string = "0.1.3" join_to_string = "0.1.3"
itertools = "0.8.0" itertools = "0.8.0"
arrayvec = "0.4.10"
ra_syntax = { path = "../ra_syntax" } ra_syntax = { path = "../ra_syntax" }
ra_text_edit = { path = "../ra_text_edit" } ra_text_edit = { path = "../ra_text_edit" }

View file

@ -0,0 +1,153 @@
use arrayvec::ArrayVec;
use ra_text_edit::{TextEdit, TextEditBuilder};
use ra_syntax::{AstNode, TreeArc, ast, SyntaxKind::*, SyntaxElement, SourceFile, InsertPosition, Direction};
pub struct AstEditor<N: AstNode> {
original_ast: TreeArc<N>,
ast: TreeArc<N>,
}
impl<N: AstNode> AstEditor<N> {
pub fn new(node: &N) -> AstEditor<N> {
AstEditor { original_ast: node.to_owned(), ast: node.to_owned() }
}
pub fn into_text_edit(self) -> TextEdit {
// FIXME: compute a more fine-grained diff here.
// If *you* know a nice algorithm to compute diff between two syntax
// tree, tell me about it!
let mut builder = TextEditBuilder::default();
builder.replace(self.original_ast.syntax().range(), self.ast().syntax().text().to_string());
builder.finish()
}
pub fn ast(&self) -> &N {
&*self.ast
}
}
impl AstEditor<ast::NamedFieldList> {
pub fn append_field(&mut self, field: &ast::NamedField) {
self.insert_field(InsertPosition::Last, field)
}
pub fn insert_field(
&mut self,
position: InsertPosition<&'_ ast::NamedField>,
field: &ast::NamedField,
) {
let mut to_insert: ArrayVec<[SyntaxElement; 2]> =
[field.syntax().into(), tokens::comma().into()].into();
let position = match position {
InsertPosition::First => {
let anchor = match self
.ast()
.syntax()
.children_with_tokens()
.find(|it| it.kind() == L_CURLY)
{
Some(it) => it,
None => return,
};
InsertPosition::After(anchor)
}
InsertPosition::Last => {
let anchor = match self
.ast()
.syntax()
.children_with_tokens()
.find(|it| it.kind() == R_CURLY)
{
Some(it) => it,
None => return,
};
InsertPosition::Before(anchor)
}
InsertPosition::Before(anchor) => InsertPosition::Before(anchor.syntax().into()),
InsertPosition::After(anchor) => {
if let Some(comma) = anchor
.syntax()
.siblings_with_tokens(Direction::Next)
.find(|it| it.kind() == COMMA)
{
InsertPosition::After(comma)
} else {
to_insert.insert(0, tokens::comma().into());
InsertPosition::After(anchor.syntax().into())
}
}
};
self.ast = insert_children_into_ast(self.ast(), position, to_insert.iter().cloned());
}
}
fn insert_children_into_ast<'a, N: AstNode>(
node: &N,
position: InsertPosition<SyntaxElement<'_>>,
to_insert: impl Iterator<Item = SyntaxElement<'a>>,
) -> TreeArc<N> {
let new_syntax = node.syntax().insert_children(position, to_insert);
N::cast(&new_syntax).unwrap().to_owned()
}
pub struct AstBuilder<N: AstNode> {
_phantom: std::marker::PhantomData<N>,
}
impl AstBuilder<ast::NamedField> {
pub fn from_text(text: &str) -> TreeArc<ast::NamedField> {
ast_node_from_file_text(&format!("fn f() {{ S {{ {}, }} }}", text))
}
}
fn ast_node_from_file_text<N: AstNode>(text: &str) -> TreeArc<N> {
let file = SourceFile::parse(text);
let res = file.syntax().descendants().find_map(N::cast).unwrap().to_owned();
res
}
mod tokens {
use lazy_static::lazy_static;
use ra_syntax::{AstNode, SourceFile, TreeArc, SyntaxToken, SyntaxKind::*};
lazy_static! {
static ref SOURCE_FILE: TreeArc<SourceFile> = SourceFile::parse(",");
}
pub(crate) fn comma() -> SyntaxToken<'static> {
SOURCE_FILE
.syntax()
.descendants_with_tokens()
.filter_map(|it| it.as_token())
.find(|it| it.kind() == COMMA)
.unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ra_syntax::SourceFile;
#[test]
fn structure_editing() {
let file = SourceFile::parse(
"\
fn foo() {
let s = S {
original: 92,
}
}
",
);
let field_list = file.syntax().descendants().find_map(ast::NamedFieldList::cast).unwrap();
let mut editor = AstEditor::new(field_list);
let field = AstBuilder::<ast::NamedField>::from_text("first_inserted: 1");
editor.append_field(&field);
let field = AstBuilder::<ast::NamedField>::from_text("second_inserted: 2");
editor.append_field(&field);
eprintln!("{}", editor.ast().syntax());
}
}

View file

@ -7,6 +7,7 @@
mod assist_ctx; mod assist_ctx;
mod marks; mod marks;
pub mod ast_editor;
use itertools::Itertools; use itertools::Itertools;

View file

@ -38,7 +38,7 @@ pub use crate::{
ast::AstNode, ast::AstNode,
syntax_error::{SyntaxError, SyntaxErrorKind, Location}, syntax_error::{SyntaxError, SyntaxErrorKind, Location},
syntax_text::SyntaxText, syntax_text::SyntaxText,
syntax_node::{Direction, SyntaxNode, WalkEvent, TreeArc, SyntaxTreeBuilder, SyntaxElement, SyntaxToken}, syntax_node::{Direction, SyntaxNode, WalkEvent, TreeArc, SyntaxTreeBuilder, SyntaxElement, SyntaxToken, InsertPosition},
ptr::{SyntaxNodePtr, AstPtr}, ptr::{SyntaxNodePtr, AstPtr},
parsing::{tokenize, classify_literal, Token}, parsing::{tokenize, classify_literal, Token},
}; };

View file

@ -10,7 +10,7 @@ use crate::{
/// specific node across reparses of the same file. /// specific node across reparses of the same file.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SyntaxNodePtr { pub struct SyntaxNodePtr {
range: TextRange, pub(crate) range: TextRange,
kind: SyntaxKind, kind: SyntaxKind,
} }

View file

@ -17,13 +17,20 @@ use ra_parser::ParseError;
use rowan::{TransparentNewType, GreenNodeBuilder}; use rowan::{TransparentNewType, GreenNodeBuilder};
use crate::{ use crate::{
SmolStr, SyntaxKind, TextUnit, TextRange, SyntaxText, SourceFile, AstNode, SmolStr, SyntaxKind, TextUnit, TextRange, SyntaxText, SourceFile, AstNode, SyntaxNodePtr,
syntax_error::{SyntaxError, SyntaxErrorKind}, syntax_error::{SyntaxError, SyntaxErrorKind},
}; };
pub use rowan::WalkEvent; pub use rowan::WalkEvent;
pub(crate) use rowan::{GreenNode, GreenToken}; pub(crate) use rowan::{GreenNode, GreenToken};
pub enum InsertPosition<T> {
First,
Last,
Before(T),
After(T),
}
/// Marker trait for CST and AST nodes /// Marker trait for CST and AST nodes
pub trait SyntaxNodeWrapper: TransparentNewType<Repr = rowan::SyntaxNode> {} pub trait SyntaxNodeWrapper: TransparentNewType<Repr = rowan::SyntaxNode> {}
impl<T: TransparentNewType<Repr = rowan::SyntaxNode>> SyntaxNodeWrapper for T {} impl<T: TransparentNewType<Repr = rowan::SyntaxNode>> SyntaxNodeWrapper for T {}
@ -309,6 +316,71 @@ impl SyntaxNode {
pub(crate) fn replace_with(&self, replacement: GreenNode) -> GreenNode { pub(crate) fn replace_with(&self, replacement: GreenNode) -> GreenNode {
self.0.replace_with(replacement) self.0.replace_with(replacement)
} }
/// Adds specified children (tokens or nodes) to the current node at the
/// specific position.
///
/// This is a type-unsafe low-level editing API, if you need to use it,
/// prefer to create a type-safe abstraction on top of it instead.
///
///
pub fn insert_children<'a>(
&self,
position: InsertPosition<SyntaxElement<'_>>,
to_insert: impl Iterator<Item = SyntaxElement<'a>>,
) -> TreeArc<SyntaxNode> {
let mut delta = TextUnit::default();
let to_insert = to_insert.map(|element| {
delta += element.text_len();
to_green_element(element)
});
let old_children = self.0.green().children();
let get_anchor_pos = |anchor: SyntaxElement| -> usize {
self.children_with_tokens()
.position(|it| it == anchor)
.expect("anchor is not a child of current element")
};
let new_children = match position {
InsertPosition::First => {
to_insert.chain(old_children.iter().cloned()).collect::<Box<[_]>>()
}
InsertPosition::Last => {
old_children.iter().cloned().chain(to_insert).collect::<Box<[_]>>()
}
InsertPosition::Before(anchor) | InsertPosition::After(anchor) => {
let take_anchor = if let InsertPosition::After(_) = position { 1 } else { 0 };
let (before, after) = old_children.split_at(get_anchor_pos(anchor) + take_anchor);
before
.iter()
.cloned()
.chain(to_insert)
.chain(after.iter().cloned())
.collect::<Box<[_]>>()
}
};
let new_node = GreenNode::new(rowan::SyntaxKind(self.kind() as u16), new_children);
let new_file_node = self.replace_with(new_node);
let file = SourceFile::new(new_file_node, Vec::new());
// FIXME: use a more elegant way to re-fetch the node (#1185), make
// `range` private afterwards
let mut ptr = SyntaxNodePtr::new(self);
ptr.range = TextRange::from_to(ptr.range().start(), ptr.range().end() + delta);
return ptr.to_node(&file).to_owned();
fn to_green_element(element: SyntaxElement) -> rowan::GreenElement {
match element {
SyntaxElement::Node(node) => node.0.green().clone().into(),
SyntaxElement::Token(tok) => {
GreenToken::new(rowan::SyntaxKind(tok.kind() as u16), tok.text().clone()).into()
}
}
}
}
} }
#[derive(Clone, Copy, PartialEq, Eq, Hash)] #[derive(Clone, Copy, PartialEq, Eq, Hash)]
@ -451,6 +523,13 @@ impl<'a> SyntaxElement<'a> {
} }
.ancestors() .ancestors()
} }
fn text_len(&self) -> TextUnit {
match self {
SyntaxElement::Node(node) => node.0.green().text_len(),
SyntaxElement::Token(token) => TextUnit::of_str(token.0.text()),
}
}
} }
impl<'a> From<rowan::SyntaxElement<'a>> for SyntaxElement<'a> { impl<'a> From<rowan::SyntaxElement<'a>> for SyntaxElement<'a> {