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:
bors[bot] 2019-02-06 14:01:07 +00:00
commit f6cf9393a8
26 changed files with 580 additions and 578 deletions

14
Cargo.lock generated
View File

@ -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",

View 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" }

View File

@ -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() {

View File

@ -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() {

View 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,
}
}
}

View File

@ -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() {

View 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 => (),
}
}
"#,
);
}
}

View File

@ -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() {

View File

@ -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() {

View 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());
}
}

View File

@ -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() {

View File

@ -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() {

View File

@ -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>;

View File

@ -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;

View File

@ -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()

View File

@ -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"

View File

@ -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())
}
}

View File

@ -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<|> {}
}
"#,
);
}
}

View File

@ -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
}
]

View File

@ -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
}
]

View File

@ -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());

View File

@ -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.

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,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;

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 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);
}