move assists to a separate crate

This commit is contained in:
Aleksey Kladov 2019-02-03 21:26:35 +03:00
parent 736a55c97e
commit 0c5fd8f7cb
26 changed files with 580 additions and 578 deletions

View file

@ -1,215 +0,0 @@
//! This modules contains various "assists": suggestions for source code edits
//! which are likely to occur at a given cursor position. For example, if the
//! cursor is on the `,`, a possible assist is swapping the elements around the
//! comma.
mod flip_comma;
mod add_derive;
mod add_impl;
mod introduce_variable;
mod change_visibility;
mod split_import;
mod replace_if_let_with_match;
use ra_text_edit::{TextEdit, TextEditBuilder};
use ra_syntax::{
Direction, SyntaxNode, TextUnit, TextRange, SourceFile, AstNode,
algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset},
};
use itertools::Itertools;
use crate::formatting::leading_indent;
pub use self::{
flip_comma::flip_comma,
add_derive::add_derive,
add_impl::add_impl,
introduce_variable::introduce_variable,
change_visibility::change_visibility,
split_import::split_import,
replace_if_let_with_match::replace_if_let_with_match,
};
/// Return all the assists applicable at the given position.
pub fn assists(file: &SourceFile, range: TextRange) -> Vec<LocalEdit> {
let ctx = AssistCtx::new(file, range);
[
flip_comma,
add_derive,
add_impl,
introduce_variable,
change_visibility,
split_import,
replace_if_let_with_match,
]
.iter()
.filter_map(|&assist| ctx.clone().apply(assist))
.collect()
}
#[derive(Debug)]
pub struct LocalEdit {
pub label: String,
pub edit: TextEdit,
pub cursor_position: Option<TextUnit>,
}
fn non_trivia_sibling(node: &SyntaxNode, direction: Direction) -> Option<&SyntaxNode> {
node.siblings(direction)
.skip(1)
.find(|node| !node.kind().is_trivia())
}
/// `AssistCtx` allows to apply an assist or check if it could be applied.
///
/// Assists use a somewhat overengineered approach, given the current needs. The
/// assists workflow consists of two phases. In the first phase, a user asks for
/// the list of available assists. In the second phase, the user picks a
/// particular assist and it gets applied.
///
/// There are two peculiarities here:
///
/// * first, we ideally avoid computing more things then necessary to answer
/// "is assist applicable" in the first phase.
/// * second, when we are applying assist, we don't have a guarantee that there
/// weren't any changes between the point when user asked for assists and when
/// they applied a particular assist. So, when applying assist, we need to do
/// all the checks from scratch.
///
/// To avoid repeating the same code twice for both "check" and "apply"
/// functions, we use an approach reminiscent of that of Django's function based
/// views dealing with forms. Each assist receives a runtime parameter,
/// `should_compute_edit`. It first check if an edit is applicable (potentially
/// computing info required to compute the actual edit). If it is applicable,
/// and `should_compute_edit` is `true`, it then computes the actual edit.
///
/// So, to implement the original assists workflow, we can first apply each edit
/// with `should_compute_edit = false`, and then applying the selected edit
/// again, with `should_compute_edit = true` this time.
///
/// Note, however, that we don't actually use such two-phase logic at the
/// moment, because the LSP API is pretty awkward in this place, and it's much
/// easier to just compute the edit eagerly :-)
#[derive(Debug, Clone)]
pub struct AssistCtx<'a> {
source_file: &'a SourceFile,
range: TextRange,
should_compute_edit: bool,
}
#[derive(Debug)]
pub enum Assist {
Applicable,
Edit(LocalEdit),
}
#[derive(Default)]
pub struct AssistBuilder {
edit: TextEditBuilder,
cursor_position: Option<TextUnit>,
}
impl<'a> AssistCtx<'a> {
pub fn new(source_file: &'a SourceFile, range: TextRange) -> AssistCtx {
AssistCtx {
source_file,
range,
should_compute_edit: false,
}
}
pub fn apply(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> Option<LocalEdit> {
self.should_compute_edit = true;
match assist(self) {
None => None,
Some(Assist::Edit(e)) => Some(e),
Some(Assist::Applicable) => unreachable!(),
}
}
pub fn check(mut self, assist: fn(AssistCtx) -> Option<Assist>) -> bool {
self.should_compute_edit = false;
match assist(self) {
None => false,
Some(Assist::Edit(_)) => unreachable!(),
Some(Assist::Applicable) => true,
}
}
fn build(self, label: impl Into<String>, f: impl FnOnce(&mut AssistBuilder)) -> Option<Assist> {
if !self.should_compute_edit {
return Some(Assist::Applicable);
}
let mut edit = AssistBuilder::default();
f(&mut edit);
Some(edit.build(label))
}
pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<&'a SyntaxNode> {
find_leaf_at_offset(self.source_file.syntax(), self.range.start())
}
pub(crate) fn node_at_offset<N: AstNode>(&self) -> Option<&'a N> {
find_node_at_offset(self.source_file.syntax(), self.range.start())
}
pub(crate) fn covering_node(&self) -> &'a SyntaxNode {
find_covering_node(self.source_file.syntax(), self.range)
}
}
impl AssistBuilder {
fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
self.edit.replace(range, replace_with.into())
}
pub fn replace_node_and_indent(&mut self, node: &SyntaxNode, replace_with: impl Into<String>) {
let mut replace_with = replace_with.into();
if let Some(indent) = leading_indent(node) {
replace_with = reindent(&replace_with, indent)
}
self.replace(node.range(), replace_with)
}
#[allow(unused)]
fn delete(&mut self, range: TextRange) {
self.edit.delete(range)
}
fn insert(&mut self, offset: TextUnit, text: impl Into<String>) {
self.edit.insert(offset, text.into())
}
fn set_cursor(&mut self, offset: TextUnit) {
self.cursor_position = Some(offset)
}
pub fn build(self, label: impl Into<String>) -> Assist {
Assist::Edit(LocalEdit {
label: label.into(),
cursor_position: self.cursor_position,
edit: self.edit.finish(),
})
}
}
fn reindent(text: &str, indent: &str) -> String {
let indent = format!("\n{}", indent);
text.lines().intersperse(&indent).collect()
}
#[cfg(test)]
fn check_assist(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) {
crate::test_utils::check_action(before, after, |file, off| {
let range = TextRange::offset_len(off, 0.into());
AssistCtx::new(file, range).apply(assist)
})
}
#[cfg(test)]
fn check_assist_not_applicable(assist: fn(AssistCtx) -> Option<Assist>, text: &str) {
crate::test_utils::check_action_not_applicable(text, |file, off| {
let range = TextRange::offset_len(off, 0.into());
AssistCtx::new(file, range).apply(assist)
})
}
#[cfg(test)]
fn check_assist_range(assist: fn(AssistCtx) -> Option<Assist>, before: &str, after: &str) {
crate::test_utils::check_action_range(before, after, |file, range| {
AssistCtx::new(file, range).apply(assist)
})
}

View file

@ -1,84 +0,0 @@
use ra_syntax::{
ast::{self, AstNode, AttrsOwner},
SyntaxKind::{WHITESPACE, COMMENT},
TextUnit,
};
use crate::assists::{AssistCtx, Assist};
pub fn add_derive(ctx: AssistCtx) -> Option<Assist> {
let nominal = ctx.node_at_offset::<ast::NominalDef>()?;
let node_start = derive_insertion_offset(nominal)?;
ctx.build("add `#[derive]`", |edit| {
let derive_attr = nominal
.attrs()
.filter_map(|x| x.as_call())
.filter(|(name, _arg)| name == "derive")
.map(|(_name, arg)| arg)
.next();
let offset = match derive_attr {
None => {
edit.insert(node_start, "#[derive()]\n");
node_start + TextUnit::of_str("#[derive(")
}
Some(tt) => tt.syntax().range().end() - TextUnit::of_char(')'),
};
edit.set_cursor(offset)
})
}
// Insert `derive` after doc comments.
fn derive_insertion_offset(nominal: &ast::NominalDef) -> Option<TextUnit> {
let non_ws_child = nominal
.syntax()
.children()
.find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?;
Some(non_ws_child.range().start())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assists::check_assist;
#[test]
fn add_derive_new() {
check_assist(
add_derive,
"struct Foo { a: i32, <|>}",
"#[derive(<|>)]\nstruct Foo { a: i32, }",
);
check_assist(
add_derive,
"struct Foo { <|> a: i32, }",
"#[derive(<|>)]\nstruct Foo { a: i32, }",
);
}
#[test]
fn add_derive_existing() {
check_assist(
add_derive,
"#[derive(Clone)]\nstruct Foo { a: i32<|>, }",
"#[derive(Clone<|>)]\nstruct Foo { a: i32, }",
);
}
#[test]
fn add_derive_new_with_doc_comment() {
check_assist(
add_derive,
"
/// `Foo` is a pretty important struct.
/// It does stuff.
struct Foo { a: i32<|>, }
",
"
/// `Foo` is a pretty important struct.
/// It does stuff.
#[derive(<|>)]
struct Foo { a: i32, }
",
);
}
}

View file

@ -1,66 +0,0 @@
use join_to_string::join;
use ra_syntax::{
ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner},
TextUnit,
};
use crate::assists::{AssistCtx, Assist};
pub fn add_impl(ctx: AssistCtx) -> Option<Assist> {
let nominal = ctx.node_at_offset::<ast::NominalDef>()?;
let name = nominal.name()?;
ctx.build("add impl", |edit| {
let type_params = nominal.type_param_list();
let start_offset = nominal.syntax().range().end();
let mut buf = String::new();
buf.push_str("\n\nimpl");
if let Some(type_params) = type_params {
type_params.syntax().text().push_to(&mut buf);
}
buf.push_str(" ");
buf.push_str(name.text().as_str());
if let Some(type_params) = type_params {
let lifetime_params = type_params
.lifetime_params()
.filter_map(|it| it.lifetime())
.map(|it| it.text());
let type_params = type_params
.type_params()
.filter_map(|it| it.name())
.map(|it| it.text());
join(lifetime_params.chain(type_params))
.surround_with("<", ">")
.to_buf(&mut buf);
}
buf.push_str(" {\n");
edit.set_cursor(start_offset + TextUnit::of_str(&buf));
buf.push_str("\n}");
edit.insert(start_offset, buf);
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assists::check_assist;
#[test]
fn test_add_impl() {
check_assist(
add_impl,
"struct Foo {<|>}\n",
"struct Foo {}\n\nimpl Foo {\n<|>\n}\n",
);
check_assist(
add_impl,
"struct Foo<T: Clone> {<|>}",
"struct Foo<T: Clone> {}\n\nimpl<T: Clone> Foo<T> {\n<|>\n}",
);
check_assist(
add_impl,
"struct Foo<'a, T: Foo<'a>> {<|>}",
"struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n<|>\n}",
);
}
}

View file

@ -1,165 +0,0 @@
use ra_syntax::{
AstNode, SyntaxNode, TextUnit,
ast::{self, VisibilityOwner, NameOwner},
SyntaxKind::{VISIBILITY, FN_KW, MOD_KW, STRUCT_KW, ENUM_KW, TRAIT_KW, FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF, IDENT, WHITESPACE, COMMENT, ATTR},
};
use crate::assists::{AssistCtx, Assist};
pub fn change_visibility(ctx: AssistCtx) -> Option<Assist> {
if let Some(vis) = ctx.node_at_offset::<ast::Visibility>() {
return change_vis(ctx, vis);
}
add_vis(ctx)
}
fn add_vis(ctx: AssistCtx) -> Option<Assist> {
let item_keyword = ctx.leaf_at_offset().find(|leaf| match leaf.kind() {
FN_KW | MOD_KW | STRUCT_KW | ENUM_KW | TRAIT_KW => true,
_ => false,
});
let offset = if let Some(keyword) = item_keyword {
let parent = keyword.parent()?;
let def_kws = vec![FN_DEF, MODULE, STRUCT_DEF, ENUM_DEF, TRAIT_DEF];
// Parent is not a definition, can't add visibility
if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) {
return None;
}
// Already have visibility, do nothing
if parent.children().any(|child| child.kind() == VISIBILITY) {
return None;
}
vis_offset(parent)
} else {
let ident = ctx.leaf_at_offset().find(|leaf| leaf.kind() == IDENT)?;
let field = ident.ancestors().find_map(ast::NamedFieldDef::cast)?;
if field.name()?.syntax().range() != ident.range() && field.visibility().is_some() {
return None;
}
vis_offset(field.syntax())
};
ctx.build("make pub(crate)", |edit| {
edit.insert(offset, "pub(crate) ");
edit.set_cursor(offset);
})
}
fn vis_offset(node: &SyntaxNode) -> TextUnit {
node.children()
.skip_while(|it| match it.kind() {
WHITESPACE | COMMENT | ATTR => true,
_ => false,
})
.next()
.map(|it| it.range().start())
.unwrap_or(node.range().start())
}
fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option<Assist> {
if vis.syntax().text() == "pub" {
return ctx.build("chage to pub(crate)", |edit| {
edit.replace(vis.syntax().range(), "pub(crate)");
edit.set_cursor(vis.syntax().range().start());
});
}
if vis.syntax().text() == "pub(crate)" {
return ctx.build("chage to pub", |edit| {
edit.replace(vis.syntax().range(), "pub");
edit.set_cursor(vis.syntax().range().start());
});
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assists::check_assist;
#[test]
fn change_visibility_adds_pub_crate_to_items() {
check_assist(
change_visibility,
"<|>fn foo() {}",
"<|>pub(crate) fn foo() {}",
);
check_assist(
change_visibility,
"f<|>n foo() {}",
"<|>pub(crate) fn foo() {}",
);
check_assist(
change_visibility,
"<|>struct Foo {}",
"<|>pub(crate) struct Foo {}",
);
check_assist(
change_visibility,
"<|>mod foo {}",
"<|>pub(crate) mod foo {}",
);
check_assist(
change_visibility,
"<|>trait Foo {}",
"<|>pub(crate) trait Foo {}",
);
check_assist(change_visibility, "m<|>od {}", "<|>pub(crate) mod {}");
check_assist(
change_visibility,
"unsafe f<|>n foo() {}",
"<|>pub(crate) unsafe fn foo() {}",
);
}
#[test]
fn change_visibility_works_with_struct_fields() {
check_assist(
change_visibility,
"struct S { <|>field: u32 }",
"struct S { <|>pub(crate) field: u32 }",
)
}
#[test]
fn change_visibility_pub_to_pub_crate() {
check_assist(
change_visibility,
"<|>pub fn foo() {}",
"<|>pub(crate) fn foo() {}",
)
}
#[test]
fn change_visibility_pub_crate_to_pub() {
check_assist(
change_visibility,
"<|>pub(crate) fn foo() {}",
"<|>pub fn foo() {}",
)
}
#[test]
fn change_visibility_handles_comment_attrs() {
check_assist(
change_visibility,
"
/// docs
// comments
#[derive(Debug)]
<|>struct Foo;
",
"
/// docs
// comments
#[derive(Debug)]
<|>pub(crate) struct Foo;
",
)
}
}

View file

@ -1,31 +0,0 @@
use ra_syntax::{
Direction,
SyntaxKind::COMMA,
};
use crate::assists::{non_trivia_sibling, AssistCtx, Assist};
pub fn flip_comma(ctx: AssistCtx) -> Option<Assist> {
let comma = ctx.leaf_at_offset().find(|leaf| leaf.kind() == COMMA)?;
let prev = non_trivia_sibling(comma, Direction::Prev)?;
let next = non_trivia_sibling(comma, Direction::Next)?;
ctx.build("flip comma", |edit| {
edit.replace(prev.range(), next.text());
edit.replace(next.range(), prev.text());
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assists::check_assist;
#[test]
fn flip_comma_works_for_function_parameters() {
check_assist(
flip_comma,
"fn foo(x: i32,<|> y: Result<(), ()>) {}",
"fn foo(y: Result<(), ()>,<|> x: i32) {}",
)
}
}

View file

@ -1,431 +0,0 @@
use ra_syntax::{
ast::{self, AstNode},
SyntaxKind::{
WHITESPACE, MATCH_ARM, LAMBDA_EXPR, PATH_EXPR, BREAK_EXPR, LOOP_EXPR, RETURN_EXPR, COMMENT
}, SyntaxNode, TextUnit,
};
use crate::assists::{AssistCtx, Assist};
pub fn introduce_variable<'a>(ctx: AssistCtx) -> Option<Assist> {
let node = ctx.covering_node();
if !valid_covering_node(node) {
return None;
}
let expr = node.ancestors().filter_map(valid_target_expr).next()?;
let (anchor_stmt, wrap_in_block) = anchor_stmt(expr)?;
let indent = anchor_stmt.prev_sibling()?;
if indent.kind() != WHITESPACE {
return None;
}
ctx.build("introduce variable", move |edit| {
let mut buf = String::new();
let cursor_offset = if wrap_in_block {
buf.push_str("{ let var_name = ");
TextUnit::of_str("{ let ")
} else {
buf.push_str("let var_name = ");
TextUnit::of_str("let ")
};
expr.syntax().text().push_to(&mut buf);
let full_stmt = ast::ExprStmt::cast(anchor_stmt);
let is_full_stmt = if let Some(expr_stmt) = full_stmt {
Some(expr.syntax()) == expr_stmt.expr().map(|e| e.syntax())
} else {
false
};
if is_full_stmt {
if !full_stmt.unwrap().has_semi() {
buf.push_str(";");
}
edit.replace(expr.syntax().range(), buf);
} else {
buf.push_str(";");
indent.text().push_to(&mut buf);
edit.replace(expr.syntax().range(), "var_name".to_string());
edit.insert(anchor_stmt.range().start(), buf);
if wrap_in_block {
edit.insert(anchor_stmt.range().end(), " }");
}
}
edit.set_cursor(anchor_stmt.range().start() + cursor_offset);
})
}
fn valid_covering_node(node: &SyntaxNode) -> bool {
node.kind() != COMMENT
}
/// Check wether the node is a valid expression which can be extracted to a variable.
/// In general that's true for any expression, but in some cases that would produce invalid code.
fn valid_target_expr(node: &SyntaxNode) -> Option<&ast::Expr> {
return match node.kind() {
PATH_EXPR => None,
BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()),
RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()),
LOOP_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()),
_ => ast::Expr::cast(node),
};
}
/// Returns the syntax node which will follow the freshly introduced var
/// and a boolean indicating whether we have to wrap it within a { } block
/// to produce correct code.
/// It can be a statement, the last in a block expression or a wanna be block
/// expression like a lamba or match arm.
fn anchor_stmt(expr: &ast::Expr) -> Option<(&SyntaxNode, bool)> {
expr.syntax().ancestors().find_map(|node| {
if ast::Stmt::cast(node).is_some() {
return Some((node, false));
}
if let Some(expr) = node
.parent()
.and_then(ast::Block::cast)
.and_then(|it| it.expr())
{
if expr.syntax() == node {
return Some((node, false));
}
}
if let Some(parent) = node.parent() {
if parent.kind() == MATCH_ARM || parent.kind() == LAMBDA_EXPR {
return Some((node, true));
}
}
None
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assists::{ check_assist, check_assist_not_applicable, check_assist_range };
#[test]
fn test_introduce_var_simple() {
check_assist_range(
introduce_variable,
"
fn foo() {
foo(<|>1 + 1<|>);
}",
"
fn foo() {
let <|>var_name = 1 + 1;
foo(var_name);
}",
);
}
#[test]
fn test_introduce_var_expr_stmt() {
check_assist_range(
introduce_variable,
"
fn foo() {
<|>1 + 1<|>;
}",
"
fn foo() {
let <|>var_name = 1 + 1;
}",
);
}
#[test]
fn test_introduce_var_part_of_expr_stmt() {
check_assist_range(
introduce_variable,
"
fn foo() {
<|>1<|> + 1;
}",
"
fn foo() {
let <|>var_name = 1;
var_name + 1;
}",
);
}
#[test]
fn test_introduce_var_last_expr() {
check_assist_range(
introduce_variable,
"
fn foo() {
bar(<|>1 + 1<|>)
}",
"
fn foo() {
let <|>var_name = 1 + 1;
bar(var_name)
}",
);
}
#[test]
fn test_introduce_var_last_full_expr() {
check_assist_range(
introduce_variable,
"
fn foo() {
<|>bar(1 + 1)<|>
}",
"
fn foo() {
let <|>var_name = bar(1 + 1);
var_name
}",
);
}
#[test]
fn test_introduce_var_block_expr_second_to_last() {
check_assist_range(
introduce_variable,
"
fn foo() {
<|>{ let x = 0; x }<|>
something_else();
}",
"
fn foo() {
let <|>var_name = { let x = 0; x };
something_else();
}",
);
}
#[test]
fn test_introduce_var_in_match_arm_no_block() {
check_assist_range(
introduce_variable,
"
fn main() {
let x = true;
let tuple = match x {
true => (<|>2 + 2<|>, true)
_ => (0, false)
};
}
",
"
fn main() {
let x = true;
let tuple = match x {
true => { let <|>var_name = 2 + 2; (var_name, true) }
_ => (0, false)
};
}
",
);
}
#[test]
fn test_introduce_var_in_match_arm_with_block() {
check_assist_range(
introduce_variable,
"
fn main() {
let x = true;
let tuple = match x {
true => {
let y = 1;
(<|>2 + y<|>, true)
}
_ => (0, false)
};
}
",
"
fn main() {
let x = true;
let tuple = match x {
true => {
let y = 1;
let <|>var_name = 2 + y;
(var_name, true)
}
_ => (0, false)
};
}
",
);
}
#[test]
fn test_introduce_var_in_closure_no_block() {
check_assist_range(
introduce_variable,
"
fn main() {
let lambda = |x: u32| <|>x * 2<|>;
}
",
"
fn main() {
let lambda = |x: u32| { let <|>var_name = x * 2; var_name };
}
",
);
}
#[test]
fn test_introduce_var_in_closure_with_block() {
check_assist_range(
introduce_variable,
"
fn main() {
let lambda = |x: u32| { <|>x * 2<|> };
}
",
"
fn main() {
let lambda = |x: u32| { let <|>var_name = x * 2; var_name };
}
",
);
}
#[test]
fn test_introduce_var_path_simple() {
check_assist(
introduce_variable,
"
fn main() {
let o = S<|>ome(true);
}
",
"
fn main() {
let <|>var_name = Some(true);
let o = var_name;
}
",
);
}
#[test]
fn test_introduce_var_path_method() {
check_assist(
introduce_variable,
"
fn main() {
let v = b<|>ar.foo();
}
",
"
fn main() {
let <|>var_name = bar.foo();
let v = var_name;
}
",
);
}
#[test]
fn test_introduce_var_return() {
check_assist(
introduce_variable,
"
fn foo() -> u32 {
r<|>eturn 2 + 2;
}
",
"
fn foo() -> u32 {
let <|>var_name = 2 + 2;
return var_name;
}
",
);
}
#[test]
fn test_introduce_var_break() {
check_assist(
introduce_variable,
"
fn main() {
let result = loop {
b<|>reak 2 + 2;
};
}
",
"
fn main() {
let result = loop {
let <|>var_name = 2 + 2;
break var_name;
};
}
",
);
}
#[test]
fn test_introduce_var_for_cast() {
check_assist(
introduce_variable,
"
fn main() {
let v = 0f32 a<|>s u32;
}
",
"
fn main() {
let <|>var_name = 0f32 as u32;
let v = var_name;
}
",
);
}
#[test]
fn test_introduce_var_for_return_not_applicable() {
check_assist_not_applicable(
introduce_variable,
"
fn foo() {
r<|>eturn;
}
",
);
}
#[test]
fn test_introduce_var_for_break_not_applicable() {
check_assist_not_applicable(
introduce_variable,
"
fn main() {
loop {
b<|>reak;
};
}
",
);
}
#[test]
fn test_introduce_var_in_comment_not_applicable() {
check_assist_not_applicable(
introduce_variable,
"
fn main() {
let x = true;
let tuple = match x {
// c<|>omment
true => (2 + 2, true)
_ => (0, false)
};
}
",
);
}
}

View file

@ -1,81 +0,0 @@
use ra_syntax::{AstNode, ast};
use crate::{
assists::{AssistCtx, Assist},
formatting::extract_trivial_expression,
};
pub fn replace_if_let_with_match(ctx: AssistCtx) -> Option<Assist> {
let if_expr: &ast::IfExpr = ctx.node_at_offset()?;
let cond = if_expr.condition()?;
let pat = cond.pat()?;
let expr = cond.expr()?;
let then_block = if_expr.then_branch()?;
let else_block = match if_expr.else_branch()? {
ast::ElseBranchFlavor::Block(it) => it,
ast::ElseBranchFlavor::IfExpr(_) => return None,
};
ctx.build("replace with match", |edit| {
let match_expr = build_match_expr(expr, pat, then_block, else_block);
edit.replace_node_and_indent(if_expr.syntax(), match_expr);
edit.set_cursor(if_expr.syntax().range().start())
})
}
fn build_match_expr(
expr: &ast::Expr,
pat1: &ast::Pat,
arm1: &ast::Block,
arm2: &ast::Block,
) -> String {
let mut buf = String::new();
buf.push_str(&format!("match {} {{\n", expr.syntax().text()));
buf.push_str(&format!(
" {} => {}\n",
pat1.syntax().text(),
format_arm(arm1)
));
buf.push_str(&format!(" _ => {}\n", format_arm(arm2)));
buf.push_str("}");
buf
}
fn format_arm(block: &ast::Block) -> String {
match extract_trivial_expression(block) {
None => block.syntax().text().to_string(),
Some(e) => format!("{},", e.syntax().text()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assists::check_assist;
#[test]
fn test_replace_if_let_with_match_unwraps_simple_expressions() {
check_assist(
replace_if_let_with_match,
"
impl VariantData {
pub fn is_struct(&self) -> bool {
if <|>let VariantData::Struct(..) = *self {
true
} else {
false
}
}
} ",
"
impl VariantData {
pub fn is_struct(&self) -> bool {
<|>match *self {
VariantData::Struct(..) => true,
_ => false,
}
}
} ",
)
}
}

View file

@ -1,56 +0,0 @@
use ra_syntax::{
TextUnit, AstNode, SyntaxKind::COLONCOLON,
ast,
algo::generate,
};
use crate::assists::{AssistCtx, Assist};
pub fn split_import(ctx: AssistCtx) -> Option<Assist> {
let colon_colon = ctx
.leaf_at_offset()
.find(|leaf| leaf.kind() == COLONCOLON)?;
let path = colon_colon.parent().and_then(ast::Path::cast)?;
let top_path = generate(Some(path), |it| it.parent_path()).last()?;
let use_tree = top_path.syntax().ancestors().find_map(ast::UseTree::cast);
if use_tree.is_none() {
return None;
}
let l_curly = colon_colon.range().end();
let r_curly = match top_path.syntax().parent().and_then(ast::UseTree::cast) {
Some(tree) => tree.syntax().range().end(),
None => top_path.syntax().range().end(),
};
ctx.build("split import", |edit| {
edit.insert(l_curly, "{");
edit.insert(r_curly, "}");
edit.set_cursor(l_curly + TextUnit::of_str("{"));
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assists::check_assist;
#[test]
fn test_split_import() {
check_assist(
split_import,
"use crate::<|>db::RootDatabase;",
"use crate::{<|>db::RootDatabase};",
)
}
#[test]
fn split_import_works_with_trees() {
check_assist(
split_import,
"use algo:<|>:visitor::{Visitor, visit}",
"use algo::{<|>visitor::{Visitor, visit}}",
)
}
}

View file

@ -1,3 +1,4 @@
use itertools::Itertools;
use ra_syntax::{
AstNode,
SyntaxNode, SyntaxKind::*,
@ -5,8 +6,13 @@ use ra_syntax::{
algo::generate,
};
pub fn reindent(text: &str, indent: &str) -> String {
let indent = format!("\n{}", indent);
text.lines().intersperse(&indent).collect()
}
/// If the node is on the beginning of the line, calculate indent.
pub(crate) fn leading_indent(node: &SyntaxNode) -> Option<&str> {
pub fn leading_indent(node: &SyntaxNode) -> Option<&str> {
for leaf in prev_leaves(node) {
if let Some(ws) = ast::Whitespace::cast(leaf) {
let ws_text = ws.text();
@ -32,7 +38,7 @@ fn prev_leaf(node: &SyntaxNode) -> Option<&SyntaxNode> {
.last()
}
pub(crate) fn extract_trivial_expression(block: &ast::Block) -> Option<&ast::Expr> {
pub fn extract_trivial_expression(block: &ast::Block) -> Option<&ast::Expr> {
let expr = block.expr()?;
if expr.syntax().text().contains('\n') {
return None;

View file

@ -3,7 +3,7 @@
//! This usually means functions which take syntax tree as an input and produce
//! an edit or some auxiliary info.
pub mod assists;
pub mod formatting;
mod extend_selection;
mod folding_ranges;
mod line_index;
@ -14,10 +14,15 @@ mod test_utils;
mod join_lines;
mod typing;
mod diagnostics;
pub(crate) mod formatting;
#[derive(Debug)]
pub struct LocalEdit {
pub label: String,
pub edit: ra_text_edit::TextEdit,
pub cursor_position: Option<TextUnit>,
}
pub use self::{
assists::LocalEdit,
extend_selection::extend_selection,
folding_ranges::{folding_ranges, Fold, FoldKind},
line_index::{LineCol, LineIndex},

View file

@ -1,4 +1,4 @@
use ra_syntax::{SourceFile, TextRange, TextUnit};
use ra_syntax::{SourceFile, TextUnit};
use crate::LocalEdit;
pub use test_utils::*;
@ -22,32 +22,3 @@ pub fn check_action<F: Fn(&SourceFile, TextUnit) -> Option<LocalEdit>>(
let actual = add_cursor(&actual, actual_cursor_pos);
assert_eq_text!(after, &actual);
}
pub fn check_action_not_applicable<F: Fn(&SourceFile, TextUnit) -> Option<LocalEdit>>(
text: &str,
f: F,
) {
let (text_cursor_pos, text) = extract_offset(text);
let file = SourceFile::parse(&text);
assert!(
f(&file, text_cursor_pos).is_none(),
"code action is applicable but it shouldn't"
);
}
pub fn check_action_range<F: Fn(&SourceFile, TextRange) -> Option<LocalEdit>>(
before: &str,
after: &str,
f: F,
) {
let (range, before) = extract_range(before);
let file = SourceFile::parse(&before);
let result = f(&file, range).expect("code action is not applicable");
let actual = result.edit.apply(&before);
let actual_cursor_pos = match result.cursor_position {
None => result.edit.apply_to_offset(range.start()).unwrap(),
Some(off) => off,
};
let actual = add_cursor(&actual, actual_cursor_pos);
assert_eq_text!(after, &actual);
}