Merge #750
750: Move assists to a separate crate r=matklad a=matklad I am slowly coming to conclusion that ide_api_light does not make a lot of sense after all :D This PR moves assists to a separate crate, so that assists can use database (so, inspect types, do name-resolution, etc) Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
This commit is contained in:
commit
f6cf9393a8
14
Cargo.lock
generated
14
Cargo.lock
generated
@ -912,6 +912,19 @@ dependencies = [
|
||||
name = "ra_arena"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "ra_assists"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"join_to_string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ra_db 0.1.0",
|
||||
"ra_hir 0.1.0",
|
||||
"ra_ide_api_light 0.1.0",
|
||||
"ra_syntax 0.1.0",
|
||||
"ra_text_edit 0.1.0",
|
||||
"test_utils 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ra_cli"
|
||||
version = "0.1.0"
|
||||
@ -970,6 +983,7 @@ dependencies = [
|
||||
"join_to_string 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ra_assists 0.1.0",
|
||||
"ra_db 0.1.0",
|
||||
"ra_hir 0.1.0",
|
||||
"ra_ide_api_light 0.1.0",
|
||||
|
17
crates/ra_assists/Cargo.toml
Normal file
17
crates/ra_assists/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
edition = "2018"
|
||||
name = "ra_assists"
|
||||
version = "0.1.0"
|
||||
authors = ["Aleksey Kladov <aleksey.kladov@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
join_to_string = "0.1.3"
|
||||
|
||||
ra_ide_api_light = { path = "../ra_ide_api_light" }
|
||||
ra_syntax = { path = "../ra_syntax" }
|
||||
ra_text_edit = { path = "../ra_text_edit" }
|
||||
ra_db = { path = "../ra_db" }
|
||||
hir = { path = "../ra_hir", package = "ra_hir" }
|
||||
|
||||
[dev-dependencies]
|
||||
test_utils = { path = "../test_utils" }
|
@ -1,12 +1,13 @@
|
||||
use hir::db::HirDatabase;
|
||||
use ra_syntax::{
|
||||
ast::{self, AstNode, AttrsOwner},
|
||||
SyntaxKind::{WHITESPACE, COMMENT},
|
||||
TextUnit,
|
||||
};
|
||||
|
||||
use crate::assists::{AssistCtx, Assist};
|
||||
use crate::{AssistCtx, Assist};
|
||||
|
||||
pub fn add_derive(ctx: AssistCtx) -> Option<Assist> {
|
||||
pub(crate) fn add_derive(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
|
||||
let nominal = ctx.node_at_offset::<ast::NominalDef>()?;
|
||||
let node_start = derive_insertion_offset(nominal)?;
|
||||
ctx.build("add `#[derive]`", |edit| {
|
||||
@ -39,7 +40,7 @@ fn derive_insertion_offset(nominal: &ast::NominalDef) -> Option<TextUnit> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assists::check_assist;
|
||||
use crate::helpers::check_assist;
|
||||
|
||||
#[test]
|
||||
fn add_derive_new() {
|
@ -1,12 +1,13 @@
|
||||
use join_to_string::join;
|
||||
use hir::db::HirDatabase;
|
||||
use ra_syntax::{
|
||||
ast::{self, AstNode, AstToken, NameOwner, TypeParamsOwner},
|
||||
TextUnit,
|
||||
};
|
||||
|
||||
use crate::assists::{AssistCtx, Assist};
|
||||
use crate::{AssistCtx, Assist};
|
||||
|
||||
pub fn add_impl(ctx: AssistCtx) -> Option<Assist> {
|
||||
pub(crate) fn add_impl(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
|
||||
let nominal = ctx.node_at_offset::<ast::NominalDef>()?;
|
||||
let name = nominal.name()?;
|
||||
ctx.build("add impl", |edit| {
|
||||
@ -42,7 +43,7 @@ pub fn add_impl(ctx: AssistCtx) -> Option<Assist> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assists::check_assist;
|
||||
use crate::helpers::check_assist;
|
||||
|
||||
#[test]
|
||||
fn test_add_impl() {
|
154
crates/ra_assists/src/assist_ctx.rs
Normal file
154
crates/ra_assists/src/assist_ctx.rs
Normal file
@ -0,0 +1,154 @@
|
||||
use hir::db::HirDatabase;
|
||||
use ra_text_edit::TextEditBuilder;
|
||||
use ra_db::FileRange;
|
||||
use ra_syntax::{
|
||||
SourceFile, TextRange, AstNode, TextUnit, SyntaxNode,
|
||||
algo::{find_leaf_at_offset, find_node_at_offset, find_covering_node, LeafAtOffset},
|
||||
};
|
||||
use ra_ide_api_light::formatting::{leading_indent, reindent};
|
||||
|
||||
use crate::{AssistLabel, AssistAction};
|
||||
|
||||
pub(crate) enum Assist {
|
||||
Unresolved(AssistLabel),
|
||||
Resolved(AssistLabel, AssistAction),
|
||||
}
|
||||
|
||||
/// `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)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AssistCtx<'a, DB> {
|
||||
pub(crate) db: &'a DB,
|
||||
pub(crate) frange: FileRange,
|
||||
source_file: &'a SourceFile,
|
||||
should_compute_edit: bool,
|
||||
}
|
||||
|
||||
impl<'a, DB> Clone for AssistCtx<'a, DB> {
|
||||
fn clone(&self) -> Self {
|
||||
AssistCtx {
|
||||
db: self.db,
|
||||
frange: self.frange,
|
||||
source_file: self.source_file,
|
||||
should_compute_edit: self.should_compute_edit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, DB: HirDatabase> AssistCtx<'a, DB> {
|
||||
pub(crate) fn with_ctx<F, T>(db: &DB, frange: FileRange, should_compute_edit: bool, f: F) -> T
|
||||
where
|
||||
F: FnOnce(AssistCtx<DB>) -> T,
|
||||
{
|
||||
let source_file = &db.parse(frange.file_id);
|
||||
let ctx = AssistCtx {
|
||||
db,
|
||||
frange,
|
||||
source_file,
|
||||
should_compute_edit,
|
||||
};
|
||||
f(ctx)
|
||||
}
|
||||
|
||||
pub(crate) fn build(
|
||||
self,
|
||||
label: impl Into<String>,
|
||||
f: impl FnOnce(&mut AssistBuilder),
|
||||
) -> Option<Assist> {
|
||||
let label = AssistLabel {
|
||||
label: label.into(),
|
||||
};
|
||||
if !self.should_compute_edit {
|
||||
return Some(Assist::Unresolved(label));
|
||||
}
|
||||
let action = {
|
||||
let mut edit = AssistBuilder::default();
|
||||
f(&mut edit);
|
||||
edit.build()
|
||||
};
|
||||
Some(Assist::Resolved(label, action))
|
||||
}
|
||||
|
||||
pub(crate) fn leaf_at_offset(&self) -> LeafAtOffset<&'a SyntaxNode> {
|
||||
find_leaf_at_offset(self.source_file.syntax(), self.frange.range.start())
|
||||
}
|
||||
|
||||
pub(crate) fn node_at_offset<N: AstNode>(&self) -> Option<&'a N> {
|
||||
find_node_at_offset(self.source_file.syntax(), self.frange.range.start())
|
||||
}
|
||||
pub(crate) fn covering_node(&self) -> &'a SyntaxNode {
|
||||
find_covering_node(self.source_file.syntax(), self.frange.range)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AssistBuilder {
|
||||
edit: TextEditBuilder,
|
||||
cursor_position: Option<TextUnit>,
|
||||
}
|
||||
|
||||
impl AssistBuilder {
|
||||
pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
|
||||
self.edit.replace(range, replace_with.into())
|
||||
}
|
||||
|
||||
pub(crate) 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)]
|
||||
pub(crate) fn delete(&mut self, range: TextRange) {
|
||||
self.edit.delete(range)
|
||||
}
|
||||
|
||||
pub(crate) fn insert(&mut self, offset: TextUnit, text: impl Into<String>) {
|
||||
self.edit.insert(offset, text.into())
|
||||
}
|
||||
|
||||
pub(crate) fn set_cursor(&mut self, offset: TextUnit) {
|
||||
self.cursor_position = Some(offset)
|
||||
}
|
||||
|
||||
fn build(self) -> AssistAction {
|
||||
AssistAction {
|
||||
edit: self.edit.finish(),
|
||||
cursor_position: self.cursor_position,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,20 @@
|
||||
use hir::db::HirDatabase;
|
||||
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};
|
||||
use crate::{AssistCtx, Assist};
|
||||
|
||||
pub fn change_visibility(ctx: AssistCtx) -> Option<Assist> {
|
||||
pub(crate) fn change_visibility(ctx: AssistCtx<impl HirDatabase>) -> 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> {
|
||||
fn add_vis(ctx: AssistCtx<impl HirDatabase>) -> 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,
|
||||
@ -57,7 +58,7 @@ fn vis_offset(node: &SyntaxNode) -> TextUnit {
|
||||
.unwrap_or(node.range().start())
|
||||
}
|
||||
|
||||
fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option<Assist> {
|
||||
fn change_vis(ctx: AssistCtx<impl HirDatabase>, 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)");
|
||||
@ -76,7 +77,7 @@ fn change_vis(ctx: AssistCtx, vis: &ast::Visibility) -> Option<Assist> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assists::check_assist;
|
||||
use crate::helpers::check_assist;
|
||||
|
||||
#[test]
|
||||
fn change_visibility_adds_pub_crate_to_items() {
|
145
crates/ra_assists/src/fill_match_arms.rs
Normal file
145
crates/ra_assists/src/fill_match_arms.rs
Normal file
@ -0,0 +1,145 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use hir::{
|
||||
AdtDef, Ty, FieldSource, source_binder,
|
||||
db::HirDatabase,
|
||||
};
|
||||
use ra_syntax::ast::{self, AstNode};
|
||||
|
||||
use crate::{AssistCtx, Assist};
|
||||
|
||||
pub(crate) fn fill_match_arms(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
|
||||
let match_expr = ctx.node_at_offset::<ast::MatchExpr>()?;
|
||||
|
||||
// We already have some match arms, so we don't provide any assists.
|
||||
match match_expr.match_arm_list() {
|
||||
Some(arm_list) if arm_list.arms().count() > 0 => {
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let expr = match_expr.expr()?;
|
||||
let function =
|
||||
source_binder::function_from_child_node(ctx.db, ctx.frange.file_id, expr.syntax())?;
|
||||
let infer_result = function.infer(ctx.db);
|
||||
let syntax_mapping = function.body_syntax_mapping(ctx.db);
|
||||
let node_expr = syntax_mapping.node_expr(expr)?;
|
||||
let match_expr_ty = infer_result[node_expr].clone();
|
||||
let enum_def = match match_expr_ty {
|
||||
Ty::Adt {
|
||||
def_id: AdtDef::Enum(e),
|
||||
..
|
||||
} => e,
|
||||
_ => return None,
|
||||
};
|
||||
let enum_name = enum_def.name(ctx.db)?;
|
||||
let db = ctx.db;
|
||||
|
||||
ctx.build("fill match arms", |edit| {
|
||||
let mut buf = format!("match {} {{\n", expr.syntax().text().to_string());
|
||||
let variants = enum_def.variants(db);
|
||||
for variant in variants {
|
||||
let name = match variant.name(db) {
|
||||
Some(it) => it,
|
||||
None => continue,
|
||||
};
|
||||
write!(&mut buf, " {}::{}", enum_name, name.to_string()).unwrap();
|
||||
|
||||
let pat = variant
|
||||
.fields(db)
|
||||
.into_iter()
|
||||
.map(|field| {
|
||||
let name = field.name(db).to_string();
|
||||
let (_, source) = field.source(db);
|
||||
match source {
|
||||
FieldSource::Named(_) => name,
|
||||
FieldSource::Pos(_) => "_".to_string(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match pat.first().map(|s| s.as_str()) {
|
||||
Some("_") => write!(&mut buf, "({})", pat.join(", ")).unwrap(),
|
||||
Some(_) => write!(&mut buf, "{{{}}}", pat.join(", ")).unwrap(),
|
||||
None => (),
|
||||
};
|
||||
|
||||
buf.push_str(" => (),\n");
|
||||
}
|
||||
buf.push_str("}");
|
||||
edit.set_cursor(expr.syntax().range().start());
|
||||
edit.replace_node_and_indent(match_expr.syntax(), buf);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::helpers::check_assist;
|
||||
|
||||
use super::fill_match_arms;
|
||||
|
||||
#[test]
|
||||
fn fill_match_arms_empty_body() {
|
||||
check_assist(
|
||||
fill_match_arms,
|
||||
r#"
|
||||
enum A {
|
||||
As,
|
||||
Bs,
|
||||
Cs(String),
|
||||
Ds(String, String),
|
||||
Es{x: usize, y: usize}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let a = A::As;
|
||||
match a<|> {}
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
enum A {
|
||||
As,
|
||||
Bs,
|
||||
Cs(String),
|
||||
Ds(String, String),
|
||||
Es{x: usize, y: usize}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let a = A::As;
|
||||
match <|>a {
|
||||
A::As => (),
|
||||
A::Bs => (),
|
||||
A::Cs(_) => (),
|
||||
A::Ds(_, _) => (),
|
||||
A::Es{x, y} => (),
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn fill_match_arms_no_body() {
|
||||
check_assist(
|
||||
fill_match_arms,
|
||||
r#"
|
||||
enum E { X, Y}
|
||||
|
||||
fn main() {
|
||||
match E::X<|>
|
||||
}
|
||||
"#,
|
||||
r#"
|
||||
enum E { X, Y}
|
||||
|
||||
fn main() {
|
||||
match <|>E::X {
|
||||
E::X => (),
|
||||
E::Y => (),
|
||||
}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
use hir::db::HirDatabase;
|
||||
use ra_syntax::{
|
||||
Direction,
|
||||
SyntaxKind::COMMA,
|
||||
};
|
||||
|
||||
use crate::assists::{non_trivia_sibling, AssistCtx, Assist};
|
||||
use crate::{AssistCtx, Assist, non_trivia_sibling};
|
||||
|
||||
pub fn flip_comma(ctx: AssistCtx) -> Option<Assist> {
|
||||
pub(crate) fn flip_comma(ctx: AssistCtx<impl HirDatabase>) -> 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)?;
|
||||
@ -18,7 +19,8 @@ pub fn flip_comma(ctx: AssistCtx) -> Option<Assist> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assists::check_assist;
|
||||
|
||||
use crate::helpers::check_assist;
|
||||
|
||||
#[test]
|
||||
fn flip_comma_works_for_function_parameters() {
|
@ -1,3 +1,4 @@
|
||||
use hir::db::HirDatabase;
|
||||
use ra_syntax::{
|
||||
ast::{self, AstNode},
|
||||
SyntaxKind::{
|
||||
@ -5,9 +6,9 @@
|
||||
}, SyntaxNode, TextUnit,
|
||||
};
|
||||
|
||||
use crate::assists::{AssistCtx, Assist};
|
||||
use crate::{AssistCtx, Assist};
|
||||
|
||||
pub fn introduce_variable<'a>(ctx: AssistCtx) -> Option<Assist> {
|
||||
pub(crate) fn introduce_variable<'a>(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
|
||||
let node = ctx.covering_node();
|
||||
if !valid_covering_node(node) {
|
||||
return None;
|
||||
@ -103,7 +104,7 @@ fn anchor_stmt(expr: &ast::Expr) -> Option<(&SyntaxNode, bool)> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assists::{ check_assist, check_assist_not_applicable, check_assist_range };
|
||||
use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_range};
|
||||
|
||||
#[test]
|
||||
fn test_introduce_var_simple() {
|
170
crates/ra_assists/src/lib.rs
Normal file
170
crates/ra_assists/src/lib.rs
Normal file
@ -0,0 +1,170 @@
|
||||
//! `ra_assits` crate provides a bunch of code assists, aslo known as code
|
||||
//! actions (in LSP) or intentions (in IntelliJ).
|
||||
//!
|
||||
//! An assist is a micro-refactoring, which is automatically activated in
|
||||
//! certain context. For example, if the cursor is over `,`, a "swap `,`" assist
|
||||
//! becomes available.
|
||||
|
||||
mod assist_ctx;
|
||||
|
||||
use ra_text_edit::TextEdit;
|
||||
use ra_syntax::{TextUnit, SyntaxNode, Direction};
|
||||
use ra_db::FileRange;
|
||||
use hir::db::HirDatabase;
|
||||
|
||||
pub(crate) use crate::assist_ctx::{AssistCtx, Assist};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AssistLabel {
|
||||
/// Short description of the assist, as shown in the UI.
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
pub struct AssistAction {
|
||||
pub edit: TextEdit,
|
||||
pub cursor_position: Option<TextUnit>,
|
||||
}
|
||||
|
||||
/// Return all the assists applicable at the given position.
|
||||
///
|
||||
/// Assists are returned in the "unresolved" state, that is only labels are
|
||||
/// returned, without actual edits.
|
||||
pub fn applicable_assists<H>(db: &H, range: FileRange) -> Vec<AssistLabel>
|
||||
where
|
||||
H: HirDatabase + 'static,
|
||||
{
|
||||
AssistCtx::with_ctx(db, range, false, |ctx| {
|
||||
all_assists()
|
||||
.iter()
|
||||
.filter_map(|f| f(ctx.clone()))
|
||||
.map(|a| match a {
|
||||
Assist::Unresolved(label) => label,
|
||||
Assist::Resolved(..) => unreachable!(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Return all the assists applicable at the given position.
|
||||
///
|
||||
/// Assists are returned in the "resolved" state, that is with edit fully
|
||||
/// computed.
|
||||
pub fn assists<H>(db: &H, range: FileRange) -> Vec<(AssistLabel, AssistAction)>
|
||||
where
|
||||
H: HirDatabase + 'static,
|
||||
{
|
||||
AssistCtx::with_ctx(db, range, false, |ctx| {
|
||||
all_assists()
|
||||
.iter()
|
||||
.filter_map(|f| f(ctx.clone()))
|
||||
.map(|a| match a {
|
||||
Assist::Resolved(label, action) => (label, action),
|
||||
Assist::Unresolved(..) => unreachable!(),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
mod add_derive;
|
||||
mod add_impl;
|
||||
mod flip_comma;
|
||||
mod change_visibility;
|
||||
mod fill_match_arms;
|
||||
mod introduce_variable;
|
||||
mod replace_if_let_with_match;
|
||||
mod split_import;
|
||||
fn all_assists<DB: HirDatabase>() -> &'static [fn(AssistCtx<DB>) -> Option<Assist>] {
|
||||
&[
|
||||
add_derive::add_derive,
|
||||
add_impl::add_impl,
|
||||
change_visibility::change_visibility,
|
||||
fill_match_arms::fill_match_arms,
|
||||
flip_comma::flip_comma,
|
||||
introduce_variable::introduce_variable,
|
||||
replace_if_let_with_match::replace_if_let_with_match,
|
||||
split_import::split_import,
|
||||
]
|
||||
}
|
||||
|
||||
fn non_trivia_sibling(node: &SyntaxNode, direction: Direction) -> Option<&SyntaxNode> {
|
||||
node.siblings(direction)
|
||||
.skip(1)
|
||||
.find(|node| !node.kind().is_trivia())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod helpers {
|
||||
use hir::mock::MockDatabase;
|
||||
use ra_syntax::TextRange;
|
||||
use ra_db::FileRange;
|
||||
use test_utils::{extract_offset, assert_eq_text, add_cursor, extract_range};
|
||||
|
||||
use crate::{AssistCtx, Assist};
|
||||
|
||||
pub(crate) fn check_assist(
|
||||
assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
|
||||
before: &str,
|
||||
after: &str,
|
||||
) {
|
||||
let (before_cursor_pos, before) = extract_offset(before);
|
||||
let (db, _source_root, file_id) = MockDatabase::with_single_file(&before);
|
||||
let frange = FileRange {
|
||||
file_id,
|
||||
range: TextRange::offset_len(before_cursor_pos, 0.into()),
|
||||
};
|
||||
let assist =
|
||||
AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
|
||||
let action = match assist {
|
||||
Assist::Unresolved(_) => unreachable!(),
|
||||
Assist::Resolved(_, it) => it,
|
||||
};
|
||||
|
||||
let actual = action.edit.apply(&before);
|
||||
let actual_cursor_pos = match action.cursor_position {
|
||||
None => action
|
||||
.edit
|
||||
.apply_to_offset(before_cursor_pos)
|
||||
.expect("cursor position is affected by the edit"),
|
||||
Some(off) => off,
|
||||
};
|
||||
let actual = add_cursor(&actual, actual_cursor_pos);
|
||||
assert_eq_text!(after, &actual);
|
||||
}
|
||||
|
||||
pub(crate) fn check_assist_range(
|
||||
assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
|
||||
before: &str,
|
||||
after: &str,
|
||||
) {
|
||||
let (range, before) = extract_range(before);
|
||||
let (db, _source_root, file_id) = MockDatabase::with_single_file(&before);
|
||||
let frange = FileRange { file_id, range };
|
||||
let assist =
|
||||
AssistCtx::with_ctx(&db, frange, true, assist).expect("code action is not applicable");
|
||||
let action = match assist {
|
||||
Assist::Unresolved(_) => unreachable!(),
|
||||
Assist::Resolved(_, it) => it,
|
||||
};
|
||||
|
||||
let mut actual = action.edit.apply(&before);
|
||||
if let Some(pos) = action.cursor_position {
|
||||
actual = add_cursor(&actual, pos);
|
||||
}
|
||||
assert_eq_text!(after, &actual);
|
||||
}
|
||||
|
||||
pub(crate) fn check_assist_not_applicable(
|
||||
assist: fn(AssistCtx<MockDatabase>) -> Option<Assist>,
|
||||
before: &str,
|
||||
) {
|
||||
let (before_cursor_pos, before) = extract_offset(before);
|
||||
let (db, _source_root, file_id) = MockDatabase::with_single_file(&before);
|
||||
let frange = FileRange {
|
||||
file_id,
|
||||
range: TextRange::offset_len(before_cursor_pos, 0.into()),
|
||||
};
|
||||
let assist = AssistCtx::with_ctx(&db, frange, true, assist);
|
||||
assert!(assist.is_none());
|
||||
}
|
||||
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
use ra_syntax::{AstNode, ast};
|
||||
use ra_ide_api_light::formatting::extract_trivial_expression;
|
||||
use hir::db::HirDatabase;
|
||||
|
||||
use crate::{
|
||||
assists::{AssistCtx, Assist},
|
||||
formatting::extract_trivial_expression,
|
||||
};
|
||||
use crate::{AssistCtx, Assist};
|
||||
|
||||
pub fn replace_if_let_with_match(ctx: AssistCtx) -> Option<Assist> {
|
||||
pub(crate) fn replace_if_let_with_match(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
|
||||
let if_expr: &ast::IfExpr = ctx.node_at_offset()?;
|
||||
let cond = if_expr.condition()?;
|
||||
let pat = cond.pat()?;
|
||||
@ -51,7 +50,7 @@ fn format_arm(block: &ast::Block) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assists::check_assist;
|
||||
use crate::helpers::check_assist;
|
||||
|
||||
#[test]
|
||||
fn test_replace_if_let_with_match_unwraps_simple_expressions() {
|
@ -1,12 +1,13 @@
|
||||
use hir::db::HirDatabase;
|
||||
use ra_syntax::{
|
||||
TextUnit, AstNode, SyntaxKind::COLONCOLON,
|
||||
ast,
|
||||
algo::generate,
|
||||
};
|
||||
|
||||
use crate::assists::{AssistCtx, Assist};
|
||||
use crate::{AssistCtx, Assist};
|
||||
|
||||
pub fn split_import(ctx: AssistCtx) -> Option<Assist> {
|
||||
pub(crate) fn split_import(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> {
|
||||
let colon_colon = ctx
|
||||
.leaf_at_offset()
|
||||
.find(|leaf| leaf.kind() == COLONCOLON)?;
|
||||
@ -34,7 +35,7 @@ pub fn split_import(ctx: AssistCtx) -> Option<Assist> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::assists::check_assist;
|
||||
use crate::helpers::check_assist;
|
||||
|
||||
#[test]
|
||||
fn test_split_import() {
|
@ -70,7 +70,7 @@ pub struct FileRange {
|
||||
/// Database which stores all significant input facts: source code and project
|
||||
/// model. Everything else in rust-analyzer is derived from these queries.
|
||||
#[salsa::query_group(SourceDatabaseStorage)]
|
||||
pub trait SourceDatabase: CheckCanceled {
|
||||
pub trait SourceDatabase: CheckCanceled + std::fmt::Debug {
|
||||
/// Text of the file.
|
||||
#[salsa::input]
|
||||
fn file_text(&self, file_id: FileId) -> Arc<String>;
|
||||
|
@ -18,8 +18,7 @@ fn from(it: $v) -> $e {
|
||||
}
|
||||
|
||||
pub mod db;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
pub mod mock;
|
||||
mod query_definitions;
|
||||
mod path;
|
||||
pub mod source_binder;
|
||||
|
@ -17,7 +17,7 @@
|
||||
db::PersistentHirDatabaseStorage
|
||||
)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MockDatabase {
|
||||
pub struct MockDatabase {
|
||||
events: Mutex<Option<Vec<salsa::Event<MockDatabase>>>>,
|
||||
runtime: salsa::Runtime<MockDatabase>,
|
||||
interner: Arc<HirInterner>,
|
||||
@ -27,13 +27,13 @@ pub(crate) struct MockDatabase {
|
||||
impl panic::RefUnwindSafe for MockDatabase {}
|
||||
|
||||
impl MockDatabase {
|
||||
pub(crate) fn with_files(fixture: &str) -> (MockDatabase, SourceRoot) {
|
||||
pub fn with_files(fixture: &str) -> (MockDatabase, SourceRoot) {
|
||||
let (db, source_root, position) = MockDatabase::from_fixture(fixture);
|
||||
assert!(position.is_none());
|
||||
(db, source_root)
|
||||
}
|
||||
|
||||
pub(crate) fn with_single_file(text: &str) -> (MockDatabase, SourceRoot, FileId) {
|
||||
pub fn with_single_file(text: &str) -> (MockDatabase, SourceRoot, FileId) {
|
||||
let mut db = MockDatabase::default();
|
||||
let mut source_root = SourceRoot::default();
|
||||
let file_id = db.add_file(WORKSPACE, &mut source_root, "/main.rs", text);
|
||||
@ -41,7 +41,7 @@ pub(crate) fn with_single_file(text: &str) -> (MockDatabase, SourceRoot, FileId)
|
||||
(db, source_root, file_id)
|
||||
}
|
||||
|
||||
pub(crate) fn with_position(fixture: &str) -> (MockDatabase, FilePosition) {
|
||||
pub fn with_position(fixture: &str) -> (MockDatabase, FilePosition) {
|
||||
let (db, _, position) = MockDatabase::from_fixture(fixture);
|
||||
let position = position.expect("expected a marker ( <|> )");
|
||||
(db, position)
|
||||
@ -166,13 +166,13 @@ fn as_ref(&self) -> &HirInterner {
|
||||
}
|
||||
|
||||
impl MockDatabase {
|
||||
pub(crate) fn log(&self, f: impl FnOnce()) -> Vec<salsa::Event<MockDatabase>> {
|
||||
pub fn log(&self, f: impl FnOnce()) -> Vec<salsa::Event<MockDatabase>> {
|
||||
*self.events.lock() = Some(Vec::new());
|
||||
f();
|
||||
self.events.lock().take().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn log_executed(&self, f: impl FnOnce()) -> Vec<String> {
|
||||
pub fn log_executed(&self, f: impl FnOnce()) -> Vec<String> {
|
||||
let events = self.log(f);
|
||||
events
|
||||
.into_iter()
|
||||
|
@ -24,6 +24,7 @@ ra_text_edit = { path = "../ra_text_edit" }
|
||||
ra_db = { path = "../ra_db" }
|
||||
hir = { path = "../ra_hir", package = "ra_hir" }
|
||||
test_utils = { path = "../test_utils" }
|
||||
ra_assists = { path = "../ra_assists" }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "0.6.1"
|
||||
|
@ -1,89 +1,24 @@
|
||||
mod fill_match_arm;
|
||||
use ra_db::{FileRange, FilePosition};
|
||||
|
||||
use ra_syntax::{
|
||||
TextRange, SourceFile, AstNode,
|
||||
algo::find_node_at_offset,
|
||||
};
|
||||
use ra_ide_api_light::{
|
||||
LocalEdit,
|
||||
assists::{
|
||||
Assist,
|
||||
AssistBuilder
|
||||
}
|
||||
};
|
||||
use crate::{
|
||||
db::RootDatabase,
|
||||
FileId
|
||||
};
|
||||
use crate::{SourceFileEdit, SourceChange, db::RootDatabase};
|
||||
|
||||
/// Return all the assists applicable at the given position.
|
||||
pub(crate) fn assists(
|
||||
db: &RootDatabase,
|
||||
file_id: FileId,
|
||||
file: &SourceFile,
|
||||
range: TextRange,
|
||||
) -> Vec<LocalEdit> {
|
||||
let ctx = AssistCtx::new(db, file_id, file, range);
|
||||
[fill_match_arm::fill_match_arm]
|
||||
.iter()
|
||||
.filter_map(|&assist| ctx.clone().apply(assist))
|
||||
pub(crate) fn assists(db: &RootDatabase, frange: FileRange) -> Vec<SourceChange> {
|
||||
ra_assists::assists(db, frange)
|
||||
.into_iter()
|
||||
.map(|(label, action)| {
|
||||
let file_id = frange.file_id;
|
||||
let file_edit = SourceFileEdit {
|
||||
file_id,
|
||||
edit: action.edit,
|
||||
};
|
||||
SourceChange {
|
||||
label: label.label,
|
||||
source_file_edits: vec![file_edit],
|
||||
file_system_edits: vec![],
|
||||
cursor_position: action
|
||||
.cursor_position
|
||||
.map(|offset| FilePosition { offset, file_id }),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AssistCtx<'a> {
|
||||
file_id: FileId,
|
||||
source_file: &'a SourceFile,
|
||||
db: &'a RootDatabase,
|
||||
range: TextRange,
|
||||
should_compute_edit: bool,
|
||||
}
|
||||
|
||||
impl<'a> AssistCtx<'a> {
|
||||
pub(crate) fn new(
|
||||
db: &'a RootDatabase,
|
||||
file_id: FileId,
|
||||
source_file: &'a SourceFile,
|
||||
range: TextRange,
|
||||
) -> AssistCtx<'a> {
|
||||
AssistCtx {
|
||||
source_file,
|
||||
file_id,
|
||||
db,
|
||||
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!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
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 node_at_offset<N: AstNode>(&self) -> Option<&'a N> {
|
||||
find_node_at_offset(self.source_file.syntax(), self.range.start())
|
||||
}
|
||||
}
|
||||
|
@ -1,157 +0,0 @@
|
||||
use std::fmt::Write;
|
||||
use hir::{
|
||||
AdtDef,
|
||||
source_binder,
|
||||
Ty,
|
||||
FieldSource,
|
||||
};
|
||||
use ra_ide_api_light::{
|
||||
assists::{
|
||||
Assist,
|
||||
AssistBuilder
|
||||
}
|
||||
};
|
||||
use ra_syntax::{
|
||||
ast::{
|
||||
self,
|
||||
AstNode,
|
||||
}
|
||||
};
|
||||
|
||||
use crate::assists::AssistCtx;
|
||||
|
||||
pub fn fill_match_arm(ctx: AssistCtx) -> Option<Assist> {
|
||||
let match_expr = ctx.node_at_offset::<ast::MatchExpr>()?;
|
||||
|
||||
// We already have some match arms, so we don't provide any assists.
|
||||
match match_expr.match_arm_list() {
|
||||
Some(arm_list) if arm_list.arms().count() > 0 => {
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let expr = match_expr.expr()?;
|
||||
let function = source_binder::function_from_child_node(ctx.db, ctx.file_id, expr.syntax())?;
|
||||
let infer_result = function.infer(ctx.db);
|
||||
let syntax_mapping = function.body_syntax_mapping(ctx.db);
|
||||
let node_expr = syntax_mapping.node_expr(expr)?;
|
||||
let match_expr_ty = infer_result[node_expr].clone();
|
||||
match match_expr_ty {
|
||||
Ty::Adt { def_id, .. } => match def_id {
|
||||
AdtDef::Enum(e) => {
|
||||
let mut buf = format!("match {} {{\n", expr.syntax().text().to_string());
|
||||
let variants = e.variants(ctx.db);
|
||||
for variant in variants {
|
||||
let name = variant.name(ctx.db)?;
|
||||
write!(
|
||||
&mut buf,
|
||||
" {}::{}",
|
||||
e.name(ctx.db)?.to_string(),
|
||||
name.to_string()
|
||||
)
|
||||
.expect("write fmt");
|
||||
|
||||
let pat = variant
|
||||
.fields(ctx.db)
|
||||
.into_iter()
|
||||
.map(|field| {
|
||||
let name = field.name(ctx.db).to_string();
|
||||
let (_, source) = field.source(ctx.db);
|
||||
match source {
|
||||
FieldSource::Named(_) => name,
|
||||
FieldSource::Pos(_) => "_".to_string(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
match pat.first().map(|s| s.as_str()) {
|
||||
Some("_") => write!(&mut buf, "({})", pat.join(", ")).expect("write fmt"),
|
||||
Some(_) => write!(&mut buf, "{{{}}}", pat.join(", ")).expect("write fmt"),
|
||||
None => (),
|
||||
};
|
||||
|
||||
buf.push_str(" => (),\n");
|
||||
}
|
||||
buf.push_str("}");
|
||||
ctx.build("fill match arms", |edit: &mut AssistBuilder| {
|
||||
edit.replace_node_and_indent(match_expr.syntax(), buf);
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_debug_snapshot_matches;
|
||||
|
||||
use ra_syntax::{TextRange, TextUnit};
|
||||
|
||||
use crate::{
|
||||
FileRange,
|
||||
mock_analysis::{analysis_and_position, single_file_with_position}
|
||||
};
|
||||
use ra_db::SourceDatabase;
|
||||
|
||||
fn test_assit(name: &str, code: &str) {
|
||||
let (analysis, position) = if code.contains("//-") {
|
||||
analysis_and_position(code)
|
||||
} else {
|
||||
single_file_with_position(code)
|
||||
};
|
||||
let frange = FileRange {
|
||||
file_id: position.file_id,
|
||||
range: TextRange::offset_len(position.offset, TextUnit::from(1)),
|
||||
};
|
||||
let source_file = analysis
|
||||
.with_db(|db| db.parse(frange.file_id))
|
||||
.expect("source file");
|
||||
let ret = analysis
|
||||
.with_db(|db| crate::assists::assists(db, frange.file_id, &source_file, frange.range))
|
||||
.expect("assists");
|
||||
|
||||
assert_debug_snapshot_matches!(name, ret);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fill_match_arm() {
|
||||
test_assit(
|
||||
"fill_match_arm1",
|
||||
r#"
|
||||
enum A {
|
||||
As,
|
||||
Bs,
|
||||
Cs(String),
|
||||
Ds(String, String),
|
||||
Es{x: usize, y: usize}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let a = A::As;
|
||||
match a<|>
|
||||
}
|
||||
"#,
|
||||
);
|
||||
|
||||
test_assit(
|
||||
"fill_match_arm2",
|
||||
r#"
|
||||
enum A {
|
||||
As,
|
||||
Bs,
|
||||
Cs(String),
|
||||
Ds(String, String),
|
||||
Es{x: usize, y: usize}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let a = A::As;
|
||||
match a<|> {}
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
---
|
||||
created: "2019-02-03T15:38:46.094184+00:00"
|
||||
creator: insta@0.5.2
|
||||
expression: ret
|
||||
source: crates/ra_ide_api/src/assits/fill_match_arm.rs
|
||||
---
|
||||
[
|
||||
LocalEdit {
|
||||
label: "fill match arms",
|
||||
edit: TextEdit {
|
||||
atoms: [
|
||||
AtomTextEdit {
|
||||
delete: [211; 218),
|
||||
insert: "match a {\n A::As => (),\n A::Bs => (),\n A::Cs(_) => (),\n A::Ds(_, _) => (),\n A::Es{x, y} => (),\n }"
|
||||
}
|
||||
]
|
||||
},
|
||||
cursor_position: None
|
||||
}
|
||||
]
|
@ -1,20 +0,0 @@
|
||||
---
|
||||
created: "2019-02-03T15:41:34.640074+00:00"
|
||||
creator: insta@0.5.2
|
||||
expression: ret
|
||||
source: crates/ra_ide_api/src/assits/fill_match_arm.rs
|
||||
---
|
||||
[
|
||||
LocalEdit {
|
||||
label: "fill match arms",
|
||||
edit: TextEdit {
|
||||
atoms: [
|
||||
AtomTextEdit {
|
||||
delete: [211; 221),
|
||||
insert: "match a {\n A::As => (),\n A::Bs => (),\n A::Cs(_) => (),\n A::Ds(_, _) => (),\n A::Es{x, y} => (),\n }"
|
||||
}
|
||||
]
|
||||
},
|
||||
cursor_position: None
|
||||
}
|
||||
]
|
@ -19,7 +19,7 @@
|
||||
|
||||
use crate::{
|
||||
AnalysisChange,
|
||||
CrateId, db, Diagnostic, FileId, FilePosition, FileRange, FileSystemEdit,
|
||||
CrateId, db, Diagnostic, FileId, FilePosition, FileSystemEdit,
|
||||
Query, RootChange, SourceChange, SourceFileEdit,
|
||||
symbol_index::{FileSymbol, SymbolsDatabase},
|
||||
status::syntax_tree_stats
|
||||
@ -236,15 +236,6 @@ pub(crate) fn diagnostics(&self, file_id: FileId) -> Vec<Diagnostic> {
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn assists(&self, frange: FileRange) -> Vec<SourceChange> {
|
||||
let file = self.parse(frange.file_id);
|
||||
ra_ide_api_light::assists::assists(&file, frange.range)
|
||||
.into_iter()
|
||||
.chain(crate::assists::assists(self, frange.file_id, &file, frange.range).into_iter())
|
||||
.map(|local_edit| SourceChange::from_local_edit(frange.file_id, local_edit))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn index_resolve(&self, name_ref: &ast::NameRef) -> Vec<FileSymbol> {
|
||||
let name = name_ref.text();
|
||||
let mut query = Query::new(name.to_string());
|
||||
|
@ -477,7 +477,7 @@ pub fn completions(&self, position: FilePosition) -> Cancelable<Option<Vec<Compl
|
||||
/// Computes assists (aks code actons aka intentions) for the given
|
||||
/// position.
|
||||
pub fn assists(&self, frange: FileRange) -> Cancelable<Vec<SourceChange>> {
|
||||
self.with_db(|db| db.assists(frange))
|
||||
self.with_db(|db| assists::assists(db, frange))
|
||||
}
|
||||
|
||||
/// Computes the set of diagnostics for the given 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)
|
||||
})
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use itertools::Itertools;
|
||||
use ra_syntax::{
|
||||
AstNode,
|
||||
SyntaxNode, SyntaxKind::*,
|
||||
@ -5,8 +6,13 @@
|
||||
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 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…
Reference in New Issue
Block a user