mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 13:03:31 +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"
|
||||
version = "0.1.0"
|
||||
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)",
|
||||
"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_fmt 0.1.0",
|
||||
"ra_hir 0.1.0",
|
||||
|
|
|
@ -5,8 +5,10 @@ version = "0.1.0"
|
|||
authors = ["rust-analyzer developers"]
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.3.0"
|
||||
join_to_string = "0.1.3"
|
||||
itertools = "0.8.0"
|
||||
arrayvec = "0.4.10"
|
||||
|
||||
ra_syntax = { path = "../ra_syntax" }
|
||||
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 marks;
|
||||
pub mod ast_editor;
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ pub use crate::{
|
|||
ast::AstNode,
|
||||
syntax_error::{SyntaxError, SyntaxErrorKind, Location},
|
||||
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},
|
||||
parsing::{tokenize, classify_literal, Token},
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
/// specific node across reparses of the same file.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct SyntaxNodePtr {
|
||||
range: TextRange,
|
||||
pub(crate) range: TextRange,
|
||||
kind: SyntaxKind,
|
||||
}
|
||||
|
||||
|
|
|
@ -17,13 +17,20 @@ use ra_parser::ParseError;
|
|||
use rowan::{TransparentNewType, GreenNodeBuilder};
|
||||
|
||||
use crate::{
|
||||
SmolStr, SyntaxKind, TextUnit, TextRange, SyntaxText, SourceFile, AstNode,
|
||||
SmolStr, SyntaxKind, TextUnit, TextRange, SyntaxText, SourceFile, AstNode, SyntaxNodePtr,
|
||||
syntax_error::{SyntaxError, SyntaxErrorKind},
|
||||
};
|
||||
|
||||
pub use rowan::WalkEvent;
|
||||
pub(crate) use rowan::{GreenNode, GreenToken};
|
||||
|
||||
pub enum InsertPosition<T> {
|
||||
First,
|
||||
Last,
|
||||
Before(T),
|
||||
After(T),
|
||||
}
|
||||
|
||||
/// Marker trait for CST and AST nodes
|
||||
pub trait SyntaxNodeWrapper: TransparentNewType<Repr = rowan::SyntaxNode> {}
|
||||
impl<T: TransparentNewType<Repr = rowan::SyntaxNode>> SyntaxNodeWrapper for T {}
|
||||
|
@ -309,6 +316,71 @@ impl SyntaxNode {
|
|||
pub(crate) fn replace_with(&self, replacement: GreenNode) -> GreenNode {
|
||||
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)]
|
||||
|
@ -451,6 +523,13 @@ impl<'a> SyntaxElement<'a> {
|
|||
}
|
||||
.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> {
|
||||
|
|
Loading…
Reference in a new issue