make on dot typed actually work

This commit is contained in:
Aleksey Kladov 2019-01-11 14:57:19 +03:00
parent dd122145b5
commit 4aa632761f
3 changed files with 120 additions and 124 deletions

View file

@ -15,10 +15,11 @@ use ra_text_edit::{TextEdit, TextEditBuilder};
use ra_syntax::{ use ra_syntax::{
Direction, SyntaxNode, TextUnit, TextRange, SourceFile, AstNode, Direction, SyntaxNode, TextUnit, TextRange, SourceFile, AstNode,
algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset}, algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset},
ast::{self, AstToken},
}; };
use itertools::Itertools; use itertools::Itertools;
use crate::formatting::leading_indent;
pub use self::{ pub use self::{
flip_comma::flip_comma, flip_comma::flip_comma,
add_derive::add_derive, add_derive::add_derive,
@ -165,7 +166,7 @@ impl AssistBuilder {
} }
fn replace_node_and_indent(&mut self, node: &SyntaxNode, replace_with: impl Into<String>) { fn replace_node_and_indent(&mut self, node: &SyntaxNode, replace_with: impl Into<String>) {
let mut replace_with = replace_with.into(); let mut replace_with = replace_with.into();
if let Some(indent) = calc_indent(node) { if let Some(indent) = leading_indent(node) {
replace_with = reindent(&replace_with, indent) replace_with = reindent(&replace_with, indent)
} }
self.replace(node.range(), replace_with) self.replace(node.range(), replace_with)
@ -182,12 +183,6 @@ impl AssistBuilder {
} }
} }
fn calc_indent(node: &SyntaxNode) -> Option<&str> {
let prev = node.prev_sibling()?;
let ws_text = ast::Whitespace::cast(prev)?.text();
ws_text.rfind('\n').map(|pos| &ws_text[pos + 1..])
}
fn reindent(text: &str, indent: &str) -> String { fn reindent(text: &str, indent: &str) -> String {
let indent = format!("\n{}", indent); let indent = format!("\n{}", indent);
text.lines().intersperse(&indent).collect() text.lines().intersperse(&indent).collect()

View file

@ -1,8 +1,16 @@
use ra_syntax::{ use ra_syntax::{
ast, AstNode, AstNode,
SyntaxNode, SyntaxKind::*, SyntaxNode, SyntaxKind::*,
ast::{self, AstToken},
}; };
/// If the node is on the begining of the line, calculate indent.
pub(crate) fn leading_indent(node: &SyntaxNode) -> Option<&str> {
let prev = node.prev_sibling()?;
let ws_text = ast::Whitespace::cast(prev)?.text();
ws_text.rfind('\n').map(|pos| &ws_text[pos + 1..])
}
pub(crate) fn extract_trivial_expression(block: &ast::Block) -> Option<&ast::Expr> { pub(crate) fn extract_trivial_expression(block: &ast::Block) -> Option<&ast::Expr> {
let expr = block.expr()?; let expr = block.expr()?;
if expr.syntax().text().contains('\n') { if expr.syntax().text().contains('\n') {

View file

@ -1,11 +1,11 @@
use ra_syntax::{ use ra_syntax::{
AstNode, SourceFile, SyntaxKind::*,
SyntaxNode, TextUnit, TextRange,
algo::{find_node_at_offset, find_leaf_at_offset, LeafAtOffset}, algo::{find_node_at_offset, find_leaf_at_offset, LeafAtOffset},
ast, ast::{self, AstToken},
AstNode, Direction, SourceFile, SyntaxKind::*,
SyntaxNode, TextUnit,
}; };
use crate::{LocalEdit, TextEditBuilder}; use crate::{LocalEdit, TextEditBuilder, formatting::leading_indent};
pub fn on_enter(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> { pub fn on_enter(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> {
let comment = find_leaf_at_offset(file.syntax(), offset) let comment = find_leaf_at_offset(file.syntax(), offset)
@ -53,20 +53,21 @@ fn node_indent<'a>(file: &'a SourceFile, node: &SyntaxNode) -> Option<&'a str> {
Some(&text[pos..]) Some(&text[pos..])
} }
pub fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> { pub fn on_eq_typed(file: &SourceFile, eq_offset: TextUnit) -> Option<LocalEdit> {
let let_stmt: &ast::LetStmt = find_node_at_offset(file.syntax(), offset)?; assert_eq!(file.syntax().text().char_at(eq_offset), Some('='));
let let_stmt: &ast::LetStmt = find_node_at_offset(file.syntax(), eq_offset)?;
if let_stmt.has_semi() { if let_stmt.has_semi() {
return None; return None;
} }
if let Some(expr) = let_stmt.initializer() { if let Some(expr) = let_stmt.initializer() {
let expr_range = expr.syntax().range(); let expr_range = expr.syntax().range();
if expr_range.contains(offset) && offset != expr_range.start() { if expr_range.contains(eq_offset) && eq_offset != expr_range.start() {
return None; return None;
} }
if file if file
.syntax() .syntax()
.text() .text()
.slice(offset..expr_range.start()) .slice(eq_offset..expr_range.start())
.contains('\n') .contains('\n')
{ {
return None; return None;
@ -84,54 +85,44 @@ pub fn on_eq_typed(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> {
}) })
} }
pub fn on_dot_typed(file: &SourceFile, offset: TextUnit) -> Option<LocalEdit> { pub fn on_dot_typed(file: &SourceFile, dot_offset: TextUnit) -> Option<LocalEdit> {
let before_dot_offset = offset - TextUnit::of_char('.'); assert_eq!(file.syntax().text().char_at(dot_offset), Some('.'));
let whitespace = find_leaf_at_offset(file.syntax(), before_dot_offset).left_biased()?; let whitespace = find_leaf_at_offset(file.syntax(), dot_offset)
.left_biased()
.and_then(ast::Whitespace::cast)?;
// find whitespace just left of the dot let current_indent = {
ast::Whitespace::cast(whitespace)?; let text = whitespace.text();
let newline = text.rfind('\n')?;
&text[newline + 1..]
};
let current_indent_len = TextUnit::of_str(current_indent);
// make sure there is a method call // Make sure dot is a part of call chain
let method_call = whitespace let field_expr = whitespace
.siblings(Direction::Prev) .syntax()
// first is whitespace .parent()
.skip(1) .and_then(ast::FieldExpr::cast)?;
.next()?; let prev_indent = leading_indent(field_expr.syntax())?;
let target_indent = format!(" {}", prev_indent);
ast::MethodCallExpr::cast(method_call)?; let target_indent_len = TextUnit::of_str(&target_indent);
if current_indent_len == target_indent_len {
// find how much the _method call is indented return None;
let method_chain_indent = method_call }
.parent()?
.siblings(Direction::Prev)
.skip(1)
.next()?
.leaf_text()
.map(|x| last_line_indent_in_whitespace(x))?;
let current_indent = TextUnit::of_str(last_line_indent_in_whitespace(whitespace.leaf_text()?));
// TODO: indent is always 4 spaces now. A better heuristic could look on the previous line(s)
let target_indent = TextUnit::of_str(method_chain_indent) + TextUnit::from_usize(4);
let diff = target_indent - current_indent;
let indent = "".repeat(diff.to_usize());
let cursor_position = offset + diff;
let mut edit = TextEditBuilder::default(); let mut edit = TextEditBuilder::default();
edit.insert(before_dot_offset, indent); edit.replace(
Some(LocalEdit { TextRange::from_to(dot_offset - current_indent_len, dot_offset),
label: "indent dot".to_string(), target_indent.into(),
);
let res = LocalEdit {
label: "reindent dot".to_string(),
edit: edit.finish(), edit: edit.finish(),
cursor_position: Some(cursor_position), cursor_position: Some(
}) dot_offset + target_indent_len - current_indent_len + TextUnit::of_char('.'),
} ),
};
/// Finds the last line in the whitespace Some(res)
fn last_line_indent_in_whitespace(ws: &str) -> &str {
ws.split('\n').last().unwrap_or("")
} }
#[cfg(test)] #[cfg(test)]
@ -162,7 +153,7 @@ mod tests {
do_check( do_check(
r" r"
fn foo() { fn foo() {
let foo =<|> 1 + 1 let foo <|>= 1 + 1
} }
", ",
r" r"
@ -189,9 +180,11 @@ fn foo() {
fn do_check(before: &str, after: &str) { fn do_check(before: &str, after: &str) {
let (offset, before) = extract_offset(before); let (offset, before) = extract_offset(before);
let file = SourceFile::parse(&before); let file = SourceFile::parse(&before);
if let Some(result) = on_eq_typed(&file, offset) { if let Some(result) = on_dot_typed(&file, offset) {
let actual = result.edit.apply(&before); let actual = result.edit.apply(&before);
assert_eq_text!(after, &actual); assert_eq_text!(after, &actual);
} else {
assert_eq_text!(&before, after)
}; };
} }
// indent if continuing chain call // indent if continuing chain call
@ -199,15 +192,15 @@ fn foo() {
r" r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name) self.child_impl(db, name)
.<|> <|>.
} }
", ",
r" r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name) self.child_impl(db, name)
. .
} }
", ",
); );
// do not indent if already indented // do not indent if already indented
@ -215,15 +208,15 @@ fn foo() {
r" r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name) self.child_impl(db, name)
.<|> <|>.
} }
", ",
r" r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name) self.child_impl(db, name)
. .
} }
", ",
); );
// indent if the previous line is already indented // indent if the previous line is already indented
@ -232,16 +225,16 @@ fn foo() {
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name) self.child_impl(db, name)
.first() .first()
.<|> <|>.
} }
", ",
r" r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name) self.child_impl(db, name)
.first() .first()
. .
} }
", ",
); );
// don't indent if indent matches previous line // don't indent if indent matches previous line
@ -250,30 +243,30 @@ fn foo() {
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name) self.child_impl(db, name)
.first() .first()
.<|> <|>.
} }
", ",
r" r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name) self.child_impl(db, name)
.first() .first()
. .
} }
", ",
); );
// don't indent if there is no method call on previous line // don't indent if there is no method call on previous line
do_check( do_check(
r" r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
.<|> <|>.
} }
", ",
r" r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
. .
} }
", ",
); );
// indent to match previous expr // indent to match previous expr
@ -281,15 +274,15 @@ fn foo() {
r" r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name) self.child_impl(db, name)
.<|> <|>.
} }
", ",
r" r"
pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> { pub fn child(&self, db: &impl HirDatabase, name: &Name) -> Cancelable<Option<Module>> {
self.child_impl(db, name) self.child_impl(db, name)
. .
} }
", ",
); );
} }