mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-09-28 21:05:02 +00:00
move assists to a separate crate
This commit is contained in:
parent
736a55c97e
commit
0c5fd8f7cb
26 changed files with 580 additions and 578 deletions
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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, }
|
||||
",
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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}",
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
",
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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) {}",
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
",
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
} ",
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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}}",
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue