mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 21:13:37 +00:00
start structured editing API
This commit is contained in:
parent
ee94edc722
commit
7cc845e88d
7 changed files with 240 additions and 3 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
153
crates/ra_assists/src/ast_editor.rs
Normal file
153
crates/ra_assists/src/ast_editor.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
mod assist_ctx;
|
mod assist_ctx;
|
||||||
mod marks;
|
mod marks;
|
||||||
|
pub mod ast_editor;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
|
|
@ -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},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
Loading…
Reference in a new issue