From b831b17b3dbc475420924834b250f07bccd673d0 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 7 Feb 2020 14:53:50 +0100 Subject: Simplify --- crates/ra_assists/src/assist_ctx.rs | 16 ++--------- crates/ra_assists/src/lib.rs | 55 ++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 42 deletions(-) (limited to 'crates') diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs index f32072dbd..b2381bd97 100644 --- a/crates/ra_assists/src/assist_ctx.rs +++ b/crates/ra_assists/src/assist_ctx.rs @@ -69,23 +69,11 @@ impl<'a> Clone for AssistCtx<'a> { } impl<'a> AssistCtx<'a> { - pub(crate) fn with_ctx( - db: &RootDatabase, - frange: FileRange, - should_compute_edit: bool, - f: F, - ) -> T - where - F: FnOnce(AssistCtx) -> T, - { + pub fn new(db: &RootDatabase, frange: FileRange, should_compute_edit: bool) -> AssistCtx { let parse = db.parse(frange.file_id); - - let ctx = AssistCtx { db, frange, source_file: parse.tree(), should_compute_edit }; - f(ctx) + AssistCtx { db, frange, source_file: parse.tree(), should_compute_edit } } -} -impl<'a> AssistCtx<'a> { pub(crate) fn add_assist( self, id: AssistId, diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 3f3df3f96..fcdfe6c14 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -37,6 +37,7 @@ pub struct AssistAction { pub label: Option, pub edit: TextEdit, pub cursor_position: Option, + // FIXME: This belongs to `AssistLabel` pub target: Option, } @@ -60,16 +61,15 @@ impl ResolvedAssist { /// Assists are returned in the "unresolved" state, that is only labels are /// returned, without actual edits. pub fn unresolved_assists(db: &RootDatabase, range: FileRange) -> Vec { - AssistCtx::with_ctx(db, range, false, |ctx| { - assists::all() - .iter() - .filter_map(|f| f(ctx.clone())) - .map(|a| match a { - Assist::Unresolved { label } => label, - Assist::Resolved { .. } => unreachable!(), - }) - .collect() - }) + let ctx = AssistCtx::new(db, range, false); + assists::all() + .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. @@ -77,18 +77,17 @@ pub fn unresolved_assists(db: &RootDatabase, range: FileRange) -> Vec Vec { - AssistCtx::with_ctx(db, range, true, |ctx| { - let mut a = assists::all() - .iter() - .filter_map(|f| f(ctx.clone())) - .map(|a| match a { - Assist::Resolved { assist } => assist, - Assist::Unresolved { .. } => unreachable!(), - }) - .collect(); - sort_assists(&mut a); - a - }) + let ctx = AssistCtx::new(db, range, true); + let mut a = assists::all() + .iter() + .filter_map(|f| f(ctx.clone())) + .map(|a| match a { + Assist::Resolved { assist } => assist, + Assist::Unresolved { .. } => unreachable!(), + }) + .collect(); + sort_assists(&mut a); + a } fn sort_assists(assists: &mut Vec) { @@ -192,7 +191,7 @@ mod helpers { 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"); + assist(AssistCtx::new(&db, frange, true)).expect("code action is not applicable"); let action = match assist { Assist::Unresolved { .. } => unreachable!(), Assist::Resolved { assist } => assist.get_first_action(), @@ -219,7 +218,7 @@ mod helpers { let (db, file_id) = 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"); + assist(AssistCtx::new(&db, frange, true)).expect("code action is not applicable"); let action = match assist { Assist::Unresolved { .. } => unreachable!(), Assist::Resolved { assist } => assist.get_first_action(), @@ -242,7 +241,7 @@ mod helpers { 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"); + assist(AssistCtx::new(&db, frange, true)).expect("code action is not applicable"); let action = match assist { Assist::Unresolved { .. } => unreachable!(), Assist::Resolved { assist } => assist.get_first_action(), @@ -261,7 +260,7 @@ mod helpers { let (db, file_id) = 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"); + assist(AssistCtx::new(&db, frange, true)).expect("code action is not applicable"); let action = match assist { Assist::Unresolved { .. } => unreachable!(), Assist::Resolved { assist } => assist.get_first_action(), @@ -279,7 +278,7 @@ mod helpers { let (db, file_id) = 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); + let assist = assist(AssistCtx::new(&db, frange, true)); assert!(assist.is_none()); } @@ -290,7 +289,7 @@ mod helpers { let (range, before) = extract_range(before); let (db, file_id) = with_single_file(&before); let frange = FileRange { file_id, range }; - let assist = AssistCtx::with_ctx(&db, frange, true, assist); + let assist = assist(AssistCtx::new(&db, frange, true)); assert!(assist.is_none()); } } -- cgit v1.2.3 From 2d95047f7c273f9e97c33b93487c9091ec6abcb7 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 7 Feb 2020 14:55:47 +0100 Subject: Cleanup --- crates/ra_assists/src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'crates') diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index fcdfe6c14..b71df7e5d 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -11,6 +11,8 @@ mod marks; mod doc_tests; pub mod ast_transform; +use std::cmp::Ordering; + use either::Either; use ra_db::FileRange; use ra_ide_db::RootDatabase; @@ -85,13 +87,12 @@ pub fn resolved_assists(db: &RootDatabase, range: FileRange) -> Vec assist, Assist::Unresolved { .. } => unreachable!(), }) - .collect(); + .collect::>(); sort_assists(&mut a); a } -fn sort_assists(assists: &mut Vec) { - use std::cmp::Ordering; +fn sort_assists(assists: &mut [ResolvedAssist]) { assists.sort_by(|a, b| match (a.get_first_action().target, b.get_first_action().target) { (Some(a), Some(b)) => a.len().cmp(&b.len()), (Some(_), None) => Ordering::Less, -- cgit v1.2.3 From 6ac9c4ad6ae2ce246a8c70d468ae2dabb484a03d Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 7 Feb 2020 15:04:50 +0100 Subject: Cleanup --- crates/ra_assists/src/assist_ctx.rs | 7 +++---- crates/ra_assists/src/lib.rs | 8 ++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) (limited to 'crates') diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs index b2381bd97..44d6f6808 100644 --- a/crates/ra_assists/src/assist_ctx.rs +++ b/crates/ra_assists/src/assist_ctx.rs @@ -57,7 +57,7 @@ pub(crate) struct AssistCtx<'a> { should_compute_edit: bool, } -impl<'a> Clone for AssistCtx<'a> { +impl Clone for AssistCtx<'_> { fn clone(&self) -> Self { AssistCtx { db: self.db, @@ -80,8 +80,7 @@ impl<'a> AssistCtx<'a> { label: impl Into, f: impl FnOnce(&mut ActionBuilder), ) -> Option { - let label = AssistLabel { label: label.into(), id }; - assert!(label.label.chars().nth(0).unwrap().is_uppercase()); + let label = AssistLabel::new(label.into(), id); let assist = if self.should_compute_edit { let action = { @@ -103,7 +102,7 @@ impl<'a> AssistCtx<'a> { label: impl Into, f: impl FnOnce() -> Vec, ) -> Option { - let label = AssistLabel { label: label.into(), id }; + let label = AssistLabel::new(label.into(), id); let assist = if self.should_compute_edit { let actions = f(); assert!(!actions.is_empty(), "Assist cannot have no"); diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index b71df7e5d..d476088a2 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -34,6 +34,14 @@ pub struct AssistLabel { pub id: AssistId, } +impl AssistLabel { + pub(crate) fn new(label: String, id: AssistId) -> AssistLabel { + // FIXME: make fields private, so that this invariant can't be broken + assert!(label.chars().nth(0).unwrap().is_uppercase()); + AssistLabel { label: label.into(), id } + } +} + #[derive(Debug, Clone)] pub struct AssistAction { pub label: Option, -- cgit v1.2.3 From ce44547cfb4f16761bf5cbef87088f117fc07bdf Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 7 Feb 2020 15:10:19 +0100 Subject: Cleanup imports --- crates/ra_assists/src/assists/auto_import.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'crates') diff --git a/crates/ra_assists/src/assists/auto_import.rs b/crates/ra_assists/src/assists/auto_import.rs index 10c4b7d7c..18ea98105 100644 --- a/crates/ra_assists/src/assists/auto_import.rs +++ b/crates/ra_assists/src/assists/auto_import.rs @@ -1,4 +1,5 @@ use hir::ModPath; +use ra_ide_db::imports_locator::ImportsLocator; use ra_syntax::{ ast::{self, AstNode}, SyntaxNode, @@ -8,7 +9,6 @@ use crate::{ assist_ctx::{ActionBuilder, Assist, AssistCtx}, auto_import_text_edit, AssistId, }; -use ra_ide_db::imports_locator::ImportsLocator; // Assist: auto_import // -- cgit v1.2.3 From aa64a84b493aa9c0b22f36b472a445d622cd2172 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 7 Feb 2020 15:12:51 +0100 Subject: Cleanups --- crates/ra_assists/src/assists/auto_import.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'crates') diff --git a/crates/ra_assists/src/assists/auto_import.rs b/crates/ra_assists/src/assists/auto_import.rs index 18ea98105..84b5474f9 100644 --- a/crates/ra_assists/src/assists/auto_import.rs +++ b/crates/ra_assists/src/assists/auto_import.rs @@ -9,6 +9,7 @@ use crate::{ assist_ctx::{ActionBuilder, Assist, AssistCtx}, auto_import_text_edit, AssistId, }; +use std::collections::BTreeSet; // Assist: auto_import // @@ -60,7 +61,8 @@ pub(crate) fn auto_import(ctx: AssistCtx) -> Option { .filter_map(|module_def| module_with_name_to_import.find_use_path(ctx.db, module_def)) .filter(|use_path| !use_path.segments.is_empty()) .take(20) - .collect::>(); + .collect::>(); + if proposed_imports.is_empty() { return None; } @@ -82,9 +84,10 @@ fn import_to_action(import: ModPath, position: &SyntaxNode, anchor: &SyntaxNode) #[cfg(test)] mod tests { - use super::*; use crate::helpers::{check_assist, check_assist_not_applicable}; + use super::*; + #[test] fn applicable_when_found_an_import() { check_assist( -- cgit v1.2.3 From 561b4b11ff1d87ea1ff2477dcba6ae1f396573a3 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 7 Feb 2020 15:53:31 +0100 Subject: Name assist handlers --- crates/ra_assists/src/assist_ctx.rs | 2 + crates/ra_assists/src/assists/add_custom_impl.rs | 209 ----- crates/ra_assists/src/assists/add_derive.rs | 120 --- crates/ra_assists/src/assists/add_explicit_type.rs | 178 ---- crates/ra_assists/src/assists/add_impl.rs | 94 -- crates/ra_assists/src/assists/add_import.rs | 967 --------------------- .../src/assists/add_missing_impl_members.rs | 608 ------------- crates/ra_assists/src/assists/add_new.rs | 430 --------- crates/ra_assists/src/assists/apply_demorgan.rs | 90 -- crates/ra_assists/src/assists/auto_import.rs | 258 ------ crates/ra_assists/src/assists/change_visibility.rs | 167 ---- crates/ra_assists/src/assists/early_return.rs | 505 ----------- crates/ra_assists/src/assists/fill_match_arms.rs | 290 ------ crates/ra_assists/src/assists/flip_binexpr.rs | 142 --- crates/ra_assists/src/assists/flip_comma.rs | 80 -- crates/ra_assists/src/assists/flip_trait_bound.rs | 116 --- .../src/assists/inline_local_variable.rs | 662 -------------- .../ra_assists/src/assists/introduce_variable.rs | 529 ----------- crates/ra_assists/src/assists/invert_if.rs | 112 --- crates/ra_assists/src/assists/merge_match_arms.rs | 264 ------ crates/ra_assists/src/assists/move_bounds.rs | 137 --- crates/ra_assists/src/assists/move_guard.rs | 308 ------- crates/ra_assists/src/assists/raw_string.rs | 499 ----------- crates/ra_assists/src/assists/remove_dbg.rs | 150 ---- .../src/assists/replace_if_let_with_match.rs | 148 ---- crates/ra_assists/src/assists/split_import.rs | 69 -- crates/ra_assists/src/handlers/add_custom_impl.rs | 209 +++++ crates/ra_assists/src/handlers/add_derive.rs | 120 +++ .../ra_assists/src/handlers/add_explicit_type.rs | 178 ++++ crates/ra_assists/src/handlers/add_impl.rs | 94 ++ crates/ra_assists/src/handlers/add_import.rs | 967 +++++++++++++++++++++ .../src/handlers/add_missing_impl_members.rs | 608 +++++++++++++ crates/ra_assists/src/handlers/add_new.rs | 430 +++++++++ crates/ra_assists/src/handlers/apply_demorgan.rs | 90 ++ crates/ra_assists/src/handlers/auto_import.rs | 258 ++++++ .../ra_assists/src/handlers/change_visibility.rs | 167 ++++ crates/ra_assists/src/handlers/early_return.rs | 505 +++++++++++ crates/ra_assists/src/handlers/fill_match_arms.rs | 290 ++++++ crates/ra_assists/src/handlers/flip_binexpr.rs | 142 +++ crates/ra_assists/src/handlers/flip_comma.rs | 80 ++ crates/ra_assists/src/handlers/flip_trait_bound.rs | 116 +++ .../src/handlers/inline_local_variable.rs | 662 ++++++++++++++ .../ra_assists/src/handlers/introduce_variable.rs | 529 +++++++++++ crates/ra_assists/src/handlers/invert_if.rs | 112 +++ crates/ra_assists/src/handlers/merge_match_arms.rs | 264 ++++++ crates/ra_assists/src/handlers/move_bounds.rs | 137 +++ crates/ra_assists/src/handlers/move_guard.rs | 308 +++++++ crates/ra_assists/src/handlers/raw_string.rs | 499 +++++++++++ crates/ra_assists/src/handlers/remove_dbg.rs | 150 ++++ .../src/handlers/replace_if_let_with_match.rs | 148 ++++ crates/ra_assists/src/handlers/split_import.rs | 69 ++ crates/ra_assists/src/lib.rs | 46 +- 52 files changed, 7148 insertions(+), 7164 deletions(-) delete mode 100644 crates/ra_assists/src/assists/add_custom_impl.rs delete mode 100644 crates/ra_assists/src/assists/add_derive.rs delete mode 100644 crates/ra_assists/src/assists/add_explicit_type.rs delete mode 100644 crates/ra_assists/src/assists/add_impl.rs delete mode 100644 crates/ra_assists/src/assists/add_import.rs delete mode 100644 crates/ra_assists/src/assists/add_missing_impl_members.rs delete mode 100644 crates/ra_assists/src/assists/add_new.rs delete mode 100644 crates/ra_assists/src/assists/apply_demorgan.rs delete mode 100644 crates/ra_assists/src/assists/auto_import.rs delete mode 100644 crates/ra_assists/src/assists/change_visibility.rs delete mode 100644 crates/ra_assists/src/assists/early_return.rs delete mode 100644 crates/ra_assists/src/assists/fill_match_arms.rs delete mode 100644 crates/ra_assists/src/assists/flip_binexpr.rs delete mode 100644 crates/ra_assists/src/assists/flip_comma.rs delete mode 100644 crates/ra_assists/src/assists/flip_trait_bound.rs delete mode 100644 crates/ra_assists/src/assists/inline_local_variable.rs delete mode 100644 crates/ra_assists/src/assists/introduce_variable.rs delete mode 100644 crates/ra_assists/src/assists/invert_if.rs delete mode 100644 crates/ra_assists/src/assists/merge_match_arms.rs delete mode 100644 crates/ra_assists/src/assists/move_bounds.rs delete mode 100644 crates/ra_assists/src/assists/move_guard.rs delete mode 100644 crates/ra_assists/src/assists/raw_string.rs delete mode 100644 crates/ra_assists/src/assists/remove_dbg.rs delete mode 100644 crates/ra_assists/src/assists/replace_if_let_with_match.rs delete mode 100644 crates/ra_assists/src/assists/split_import.rs create mode 100644 crates/ra_assists/src/handlers/add_custom_impl.rs create mode 100644 crates/ra_assists/src/handlers/add_derive.rs create mode 100644 crates/ra_assists/src/handlers/add_explicit_type.rs create mode 100644 crates/ra_assists/src/handlers/add_impl.rs create mode 100644 crates/ra_assists/src/handlers/add_import.rs create mode 100644 crates/ra_assists/src/handlers/add_missing_impl_members.rs create mode 100644 crates/ra_assists/src/handlers/add_new.rs create mode 100644 crates/ra_assists/src/handlers/apply_demorgan.rs create mode 100644 crates/ra_assists/src/handlers/auto_import.rs create mode 100644 crates/ra_assists/src/handlers/change_visibility.rs create mode 100644 crates/ra_assists/src/handlers/early_return.rs create mode 100644 crates/ra_assists/src/handlers/fill_match_arms.rs create mode 100644 crates/ra_assists/src/handlers/flip_binexpr.rs create mode 100644 crates/ra_assists/src/handlers/flip_comma.rs create mode 100644 crates/ra_assists/src/handlers/flip_trait_bound.rs create mode 100644 crates/ra_assists/src/handlers/inline_local_variable.rs create mode 100644 crates/ra_assists/src/handlers/introduce_variable.rs create mode 100644 crates/ra_assists/src/handlers/invert_if.rs create mode 100644 crates/ra_assists/src/handlers/merge_match_arms.rs create mode 100644 crates/ra_assists/src/handlers/move_bounds.rs create mode 100644 crates/ra_assists/src/handlers/move_guard.rs create mode 100644 crates/ra_assists/src/handlers/raw_string.rs create mode 100644 crates/ra_assists/src/handlers/remove_dbg.rs create mode 100644 crates/ra_assists/src/handlers/replace_if_let_with_match.rs create mode 100644 crates/ra_assists/src/handlers/split_import.rs (limited to 'crates') diff --git a/crates/ra_assists/src/assist_ctx.rs b/crates/ra_assists/src/assist_ctx.rs index 44d6f6808..81f999090 100644 --- a/crates/ra_assists/src/assist_ctx.rs +++ b/crates/ra_assists/src/assist_ctx.rs @@ -19,6 +19,8 @@ pub(crate) enum Assist { Resolved { assist: ResolvedAssist }, } +pub(crate) type AssistHandler = fn(AssistCtx) -> Option; + /// `AssistCtx` allows to apply an assist or check if it could be applied. /// /// Assists use a somewhat over-engineered approach, given the current needs. The diff --git a/crates/ra_assists/src/assists/add_custom_impl.rs b/crates/ra_assists/src/assists/add_custom_impl.rs deleted file mode 100644 index 7fdd816bf..000000000 --- a/crates/ra_assists/src/assists/add_custom_impl.rs +++ /dev/null @@ -1,209 +0,0 @@ -//! FIXME: write short doc here - -use crate::{Assist, AssistCtx, AssistId}; - -use join_to_string::join; -use ra_syntax::{ - ast::{self, AstNode}, - Direction, SmolStr, - SyntaxKind::{IDENT, WHITESPACE}, - TextRange, TextUnit, -}; - -const DERIVE_TRAIT: &str = "derive"; - -// Assist: add_custom_impl -// -// Adds impl block for derived trait. -// -// ``` -// #[derive(Deb<|>ug, Display)] -// struct S; -// ``` -// -> -// ``` -// #[derive(Display)] -// struct S; -// -// impl Debug for S { -// -// } -// ``` -pub(crate) fn add_custom_impl(ctx: AssistCtx) -> Option { - let input = ctx.find_node_at_offset::()?; - let attr = input.syntax().parent().and_then(ast::Attr::cast)?; - - let attr_name = attr - .syntax() - .descendants_with_tokens() - .filter(|t| t.kind() == IDENT) - .find_map(|i| i.into_token()) - .filter(|t| *t.text() == DERIVE_TRAIT)? - .text() - .clone(); - - let trait_token = - ctx.token_at_offset().filter(|t| t.kind() == IDENT && *t.text() != attr_name).next()?; - - let annotated = attr.syntax().siblings(Direction::Next).find_map(|s| ast::Name::cast(s))?; - let annotated_name = annotated.syntax().text().to_string(); - let start_offset = annotated.syntax().parent()?.text_range().end(); - - let label = - format!("Add custom impl '{}' for '{}'", trait_token.text().as_str(), annotated_name); - - ctx.add_assist(AssistId("add_custom_impl"), label, |edit| { - edit.target(attr.syntax().text_range()); - - let new_attr_input = input - .syntax() - .descendants_with_tokens() - .filter(|t| t.kind() == IDENT) - .filter_map(|t| t.into_token().map(|t| t.text().clone())) - .filter(|t| t != trait_token.text()) - .collect::>(); - let has_more_derives = new_attr_input.len() > 0; - let new_attr_input = - join(new_attr_input.iter()).separator(", ").surround_with("(", ")").to_string(); - let new_attr_input_len = new_attr_input.len(); - - let mut buf = String::new(); - buf.push_str("\n\nimpl "); - buf.push_str(trait_token.text().as_str()); - buf.push_str(" for "); - buf.push_str(annotated_name.as_str()); - buf.push_str(" {\n"); - - let cursor_delta = if has_more_derives { - edit.replace(input.syntax().text_range(), new_attr_input); - input.syntax().text_range().len() - TextUnit::from_usize(new_attr_input_len) - } else { - let attr_range = attr.syntax().text_range(); - edit.delete(attr_range); - - let line_break_range = attr - .syntax() - .next_sibling_or_token() - .filter(|t| t.kind() == WHITESPACE) - .map(|t| t.text_range()) - .unwrap_or(TextRange::from_to(TextUnit::from(0), TextUnit::from(0))); - edit.delete(line_break_range); - - attr_range.len() + line_break_range.len() - }; - - edit.set_cursor(start_offset + TextUnit::of_str(&buf) - cursor_delta); - buf.push_str("\n}"); - edit.insert(start_offset, buf); - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::helpers::{check_assist, check_assist_not_applicable}; - - #[test] - fn add_custom_impl_for_unique_input() { - check_assist( - add_custom_impl, - " -#[derive(Debu<|>g)] -struct Foo { - bar: String, -} - ", - " -struct Foo { - bar: String, -} - -impl Debug for Foo { -<|> -} - ", - ) - } - - #[test] - fn add_custom_impl_for_with_visibility_modifier() { - check_assist( - add_custom_impl, - " -#[derive(Debug<|>)] -pub struct Foo { - bar: String, -} - ", - " -pub struct Foo { - bar: String, -} - -impl Debug for Foo { -<|> -} - ", - ) - } - - #[test] - fn add_custom_impl_when_multiple_inputs() { - check_assist( - add_custom_impl, - " -#[derive(Display, Debug<|>, Serialize)] -struct Foo {} - ", - " -#[derive(Display, Serialize)] -struct Foo {} - -impl Debug for Foo { -<|> -} - ", - ) - } - - #[test] - fn test_ignore_derive_macro_without_input() { - check_assist_not_applicable( - add_custom_impl, - " -#[derive(<|>)] -struct Foo {} - ", - ) - } - - #[test] - fn test_ignore_if_cursor_on_param() { - check_assist_not_applicable( - add_custom_impl, - " -#[derive<|>(Debug)] -struct Foo {} - ", - ); - - check_assist_not_applicable( - add_custom_impl, - " -#[derive(Debug)<|>] -struct Foo {} - ", - ) - } - - #[test] - fn test_ignore_if_not_derive() { - check_assist_not_applicable( - add_custom_impl, - " -#[allow(non_camel_<|>case_types)] -struct Foo {} - ", - ) - } -} diff --git a/crates/ra_assists/src/assists/add_derive.rs b/crates/ra_assists/src/assists/add_derive.rs deleted file mode 100644 index b0d1a0a80..000000000 --- a/crates/ra_assists/src/assists/add_derive.rs +++ /dev/null @@ -1,120 +0,0 @@ -use ra_syntax::{ - ast::{self, AstNode, AttrsOwner}, - SyntaxKind::{COMMENT, WHITESPACE}, - TextUnit, -}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: add_derive -// -// Adds a new `#[derive()]` clause to a struct or enum. -// -// ``` -// struct Point { -// x: u32, -// y: u32,<|> -// } -// ``` -// -> -// ``` -// #[derive()] -// struct Point { -// x: u32, -// y: u32, -// } -// ``` -pub(crate) fn add_derive(ctx: AssistCtx) -> Option { - let nominal = ctx.find_node_at_offset::()?; - let node_start = derive_insertion_offset(&nominal)?; - ctx.add_assist(AssistId("add_derive"), "Add `#[derive]`", |edit| { - let derive_attr = nominal - .attrs() - .filter_map(|x| x.as_simple_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().text_range().end() - TextUnit::of_char(')'), - }; - edit.target(nominal.syntax().text_range()); - edit.set_cursor(offset) - }) -} - -// Insert `derive` after doc comments. -fn derive_insertion_offset(nominal: &ast::NominalDef) -> Option { - let non_ws_child = nominal - .syntax() - .children_with_tokens() - .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?; - Some(non_ws_child.text_range().start()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::helpers::{check_assist, check_assist_target}; - - #[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, } - ", - ); - } - - #[test] - fn add_derive_target() { - check_assist_target( - add_derive, - " -struct SomeThingIrrelevant; -/// `Foo` is a pretty important struct. -/// It does stuff. -struct Foo { a: i32<|>, } -struct EvenMoreIrrelevant; - ", - "/// `Foo` is a pretty important struct. -/// It does stuff. -struct Foo { a: i32, }", - ); - } -} diff --git a/crates/ra_assists/src/assists/add_explicit_type.rs b/crates/ra_assists/src/assists/add_explicit_type.rs deleted file mode 100644 index 2cb9d2f48..000000000 --- a/crates/ra_assists/src/assists/add_explicit_type.rs +++ /dev/null @@ -1,178 +0,0 @@ -use hir::HirDisplay; -use ra_syntax::{ - ast::{self, AstNode, LetStmt, NameOwner, TypeAscriptionOwner}, - TextRange, -}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: add_explicit_type -// -// Specify type for a let binding. -// -// ``` -// fn main() { -// let x<|> = 92; -// } -// ``` -// -> -// ``` -// fn main() { -// let x: i32 = 92; -// } -// ``` -pub(crate) fn add_explicit_type(ctx: AssistCtx) -> Option { - let stmt = ctx.find_node_at_offset::()?; - let expr = stmt.initializer()?; - let pat = stmt.pat()?; - // Must be a binding - let pat = match pat { - ast::Pat::BindPat(bind_pat) => bind_pat, - _ => return None, - }; - let pat_range = pat.syntax().text_range(); - // The binding must have a name - let name = pat.name()?; - let name_range = name.syntax().text_range(); - let stmt_range = stmt.syntax().text_range(); - let eq_range = stmt.eq_token()?.text_range(); - // Assist should only be applicable if cursor is between 'let' and '=' - let let_range = TextRange::from_to(stmt_range.start(), eq_range.start()); - let cursor_in_range = ctx.frange.range.is_subrange(&let_range); - if !cursor_in_range { - return None; - } - // Assist not applicable if the type has already been specified - // and it has no placeholders - let ascribed_ty = stmt.ascribed_type(); - if let Some(ref ty) = ascribed_ty { - if ty.syntax().descendants().find_map(ast::PlaceholderType::cast).is_none() { - return None; - } - } - // Infer type - let db = ctx.db; - let analyzer = ctx.source_analyzer(stmt.syntax(), None); - let ty = analyzer.type_of(db, &expr)?; - // Assist not applicable if the type is unknown - if ty.contains_unknown() { - return None; - } - - ctx.add_assist( - AssistId("add_explicit_type"), - format!("Insert explicit type '{}'", ty.display(db)), - |edit| { - edit.target(pat_range); - if let Some(ascribed_ty) = ascribed_ty { - edit.replace(ascribed_ty.syntax().text_range(), format!("{}", ty.display(db))); - } else { - edit.insert(name_range.end(), format!(": {}", ty.display(db))); - } - }, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; - - #[test] - fn add_explicit_type_target() { - check_assist_target(add_explicit_type, "fn f() { let a<|> = 1; }", "a"); - } - - #[test] - fn add_explicit_type_works_for_simple_expr() { - check_assist( - add_explicit_type, - "fn f() { let a<|> = 1; }", - "fn f() { let a<|>: i32 = 1; }", - ); - } - - #[test] - fn add_explicit_type_works_for_underscore() { - check_assist( - add_explicit_type, - "fn f() { let a<|>: _ = 1; }", - "fn f() { let a<|>: i32 = 1; }", - ); - } - - #[test] - fn add_explicit_type_works_for_nested_underscore() { - check_assist( - add_explicit_type, - r#" - enum Option { - Some(T), - None - } - - fn f() { - let a<|>: Option<_> = Option::Some(1); - }"#, - r#" - enum Option { - Some(T), - None - } - - fn f() { - let a<|>: Option = Option::Some(1); - }"#, - ); - } - - #[test] - fn add_explicit_type_works_for_macro_call() { - check_assist( - add_explicit_type, - "macro_rules! v { () => {0u64} } fn f() { let a<|> = v!(); }", - "macro_rules! v { () => {0u64} } fn f() { let a<|>: u64 = v!(); }", - ); - } - - #[test] - fn add_explicit_type_works_for_macro_call_recursive() { - check_assist( - add_explicit_type, - "macro_rules! u { () => {0u64} } macro_rules! v { () => {u!()} } fn f() { let a<|> = v!(); }", - "macro_rules! u { () => {0u64} } macro_rules! v { () => {u!()} } fn f() { let a<|>: u64 = v!(); }", - ); - } - - #[test] - fn add_explicit_type_not_applicable_if_ty_not_inferred() { - check_assist_not_applicable(add_explicit_type, "fn f() { let a<|> = None; }"); - } - - #[test] - fn add_explicit_type_not_applicable_if_ty_already_specified() { - check_assist_not_applicable(add_explicit_type, "fn f() { let a<|>: i32 = 1; }"); - } - - #[test] - fn add_explicit_type_not_applicable_if_specified_ty_is_tuple() { - check_assist_not_applicable(add_explicit_type, "fn f() { let a<|>: (i32, i32) = (3, 4); }"); - } - - #[test] - fn add_explicit_type_not_applicable_if_cursor_after_equals() { - check_assist_not_applicable( - add_explicit_type, - "fn f() {let a =<|> match 1 {2 => 3, 3 => 5};}", - ) - } - - #[test] - fn add_explicit_type_not_applicable_if_cursor_before_let() { - check_assist_not_applicable( - add_explicit_type, - "fn f() <|>{let a = match 1 {2 => 3, 3 => 5};}", - ) - } -} diff --git a/crates/ra_assists/src/assists/add_impl.rs b/crates/ra_assists/src/assists/add_impl.rs deleted file mode 100644 index 241b085fd..000000000 --- a/crates/ra_assists/src/assists/add_impl.rs +++ /dev/null @@ -1,94 +0,0 @@ -use format_buf::format; - -use join_to_string::join; -use ra_syntax::{ - ast::{self, AstNode, NameOwner, TypeParamsOwner}, - TextUnit, -}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: add_impl -// -// Adds a new inherent impl for a type. -// -// ``` -// struct Ctx { -// data: T,<|> -// } -// ``` -// -> -// ``` -// struct Ctx { -// data: T, -// } -// -// impl Ctx { -// -// } -// ``` -pub(crate) fn add_impl(ctx: AssistCtx) -> Option { - let nominal = ctx.find_node_at_offset::()?; - let name = nominal.name()?; - ctx.add_assist(AssistId("add_impl"), format!("Implement {}", name.text().as_str()), |edit| { - edit.target(nominal.syntax().text_range()); - let type_params = nominal.type_param_list(); - let start_offset = nominal.syntax().text_range().end(); - let mut buf = String::new(); - buf.push_str("\n\nimpl"); - if let Some(type_params) = &type_params { - format!(buf, "{}", type_params.syntax()); - } - 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_token()) - .map(|it| it.text().clone()); - let type_params = - type_params.type_params().filter_map(|it| it.name()).map(|it| it.text().clone()); - 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::helpers::{check_assist, check_assist_target}; - - #[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 {<|>}", - "struct Foo {}\n\nimpl Foo {\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}", - ); - } - - #[test] - fn add_impl_target() { - check_assist_target( - add_impl, - " -struct SomeThingIrrelevant; -/// Has a lifetime parameter -struct Foo<'a, T: Foo<'a>> {<|>} -struct EvenMoreIrrelevant; -", - "/// Has a lifetime parameter -struct Foo<'a, T: Foo<'a>> {}", - ); - } -} diff --git a/crates/ra_assists/src/assists/add_import.rs b/crates/ra_assists/src/assists/add_import.rs deleted file mode 100644 index f03dddac8..000000000 --- a/crates/ra_assists/src/assists/add_import.rs +++ /dev/null @@ -1,967 +0,0 @@ -use hir::{self, ModPath}; -use ra_syntax::{ - ast::{self, NameOwner}, - AstNode, Direction, SmolStr, - SyntaxKind::{PATH, PATH_SEGMENT}, - SyntaxNode, TextRange, T, -}; -use ra_text_edit::TextEditBuilder; - -use crate::{ - assist_ctx::{Assist, AssistCtx}, - AssistId, -}; - -/// This function produces sequence of text edits into edit -/// to import the target path in the most appropriate scope given -/// the cursor position -pub fn auto_import_text_edit( - // Ideally the position of the cursor, used to - position: &SyntaxNode, - // The statement to use as anchor (last resort) - anchor: &SyntaxNode, - path_to_import: &ModPath, - edit: &mut TextEditBuilder, -) { - let target = path_to_import.to_string().split("::").map(SmolStr::new).collect::>(); - let container = position.ancestors().find_map(|n| { - if let Some(module) = ast::Module::cast(n.clone()) { - return module.item_list().map(|it| it.syntax().clone()); - } - ast::SourceFile::cast(n).map(|it| it.syntax().clone()) - }); - - if let Some(container) = container { - let action = best_action_for_target(container, anchor.clone(), &target); - make_assist(&action, &target, edit); - } -} - -// Assist: add_import -// -// Adds a use statement for a given fully-qualified path. -// -// ``` -// fn process(map: std::collections::<|>HashMap) {} -// ``` -// -> -// ``` -// use std::collections::HashMap; -// -// fn process(map: HashMap) {} -// ``` -pub(crate) fn add_import(ctx: AssistCtx) -> Option { - let path: ast::Path = ctx.find_node_at_offset()?; - // We don't want to mess with use statements - if path.syntax().ancestors().find_map(ast::UseItem::cast).is_some() { - return None; - } - - let hir_path = hir::Path::from_ast(path.clone())?; - let segments = collect_hir_path_segments(&hir_path)?; - if segments.len() < 2 { - return None; - } - - let module = path.syntax().ancestors().find_map(ast::Module::cast); - let position = match module.and_then(|it| it.item_list()) { - Some(item_list) => item_list.syntax().clone(), - None => { - let current_file = path.syntax().ancestors().find_map(ast::SourceFile::cast)?; - current_file.syntax().clone() - } - }; - - ctx.add_assist(AssistId("add_import"), format!("Import {}", fmt_segments(&segments)), |edit| { - apply_auto_import(&position, &path, &segments, edit.text_edit_builder()); - }) -} - -fn collect_path_segments_raw( - segments: &mut Vec, - mut path: ast::Path, -) -> Option { - let oldlen = segments.len(); - loop { - let mut children = path.syntax().children_with_tokens(); - let (first, second, third) = ( - children.next().map(|n| (n.clone(), n.kind())), - children.next().map(|n| (n.clone(), n.kind())), - children.next().map(|n| (n.clone(), n.kind())), - ); - match (first, second, third) { - (Some((subpath, PATH)), Some((_, T![::])), Some((segment, PATH_SEGMENT))) => { - path = ast::Path::cast(subpath.as_node()?.clone())?; - segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?); - } - (Some((segment, PATH_SEGMENT)), _, _) => { - segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?); - break; - } - (_, _, _) => return None, - } - } - // We need to reverse only the new added segments - let only_new_segments = segments.split_at_mut(oldlen).1; - only_new_segments.reverse(); - Some(segments.len() - oldlen) -} - -fn fmt_segments(segments: &[SmolStr]) -> String { - let mut buf = String::new(); - fmt_segments_raw(segments, &mut buf); - buf -} - -fn fmt_segments_raw(segments: &[SmolStr], buf: &mut String) { - let mut iter = segments.iter(); - if let Some(s) = iter.next() { - buf.push_str(s); - } - for s in iter { - buf.push_str("::"); - buf.push_str(s); - } -} - -/// Returns the number of common segments. -fn compare_path_segments(left: &[SmolStr], right: &[ast::PathSegment]) -> usize { - left.iter().zip(right).take_while(|(l, r)| compare_path_segment(l, r)).count() -} - -fn compare_path_segment(a: &SmolStr, b: &ast::PathSegment) -> bool { - if let Some(kb) = b.kind() { - match kb { - ast::PathSegmentKind::Name(nameref_b) => a == nameref_b.text(), - ast::PathSegmentKind::SelfKw => a == "self", - ast::PathSegmentKind::SuperKw => a == "super", - ast::PathSegmentKind::CrateKw => a == "crate", - ast::PathSegmentKind::Type { .. } => false, // not allowed in imports - } - } else { - false - } -} - -fn compare_path_segment_with_name(a: &SmolStr, b: &ast::Name) -> bool { - a == b.text() -} - -#[derive(Clone, Debug)] -enum ImportAction { - Nothing, - // Add a brand new use statement. - AddNewUse { - anchor: Option, // anchor node - add_after_anchor: bool, - }, - - // To split an existing use statement creating a nested import. - AddNestedImport { - // how may segments matched with the target path - common_segments: usize, - path_to_split: ast::Path, - // the first segment of path_to_split we want to add into the new nested list - first_segment_to_split: Option, - // Wether to add 'self' in addition to the target path - add_self: bool, - }, - // To add the target path to an existing nested import tree list. - AddInTreeList { - common_segments: usize, - // The UseTreeList where to add the target path - tree_list: ast::UseTreeList, - add_self: bool, - }, -} - -impl ImportAction { - fn add_new_use(anchor: Option, add_after_anchor: bool) -> Self { - ImportAction::AddNewUse { anchor, add_after_anchor } - } - - fn add_nested_import( - common_segments: usize, - path_to_split: ast::Path, - first_segment_to_split: Option, - add_self: bool, - ) -> Self { - ImportAction::AddNestedImport { - common_segments, - path_to_split, - first_segment_to_split, - add_self, - } - } - - fn add_in_tree_list( - common_segments: usize, - tree_list: ast::UseTreeList, - add_self: bool, - ) -> Self { - ImportAction::AddInTreeList { common_segments, tree_list, add_self } - } - - fn better(left: ImportAction, right: ImportAction) -> ImportAction { - if left.is_better(&right) { - left - } else { - right - } - } - - fn is_better(&self, other: &ImportAction) -> bool { - match (self, other) { - (ImportAction::Nothing, _) => true, - (ImportAction::AddInTreeList { .. }, ImportAction::Nothing) => false, - ( - ImportAction::AddNestedImport { common_segments: n, .. }, - ImportAction::AddInTreeList { common_segments: m, .. }, - ) - | ( - ImportAction::AddInTreeList { common_segments: n, .. }, - ImportAction::AddNestedImport { common_segments: m, .. }, - ) - | ( - ImportAction::AddInTreeList { common_segments: n, .. }, - ImportAction::AddInTreeList { common_segments: m, .. }, - ) - | ( - ImportAction::AddNestedImport { common_segments: n, .. }, - ImportAction::AddNestedImport { common_segments: m, .. }, - ) => n > m, - (ImportAction::AddInTreeList { .. }, _) => true, - (ImportAction::AddNestedImport { .. }, ImportAction::Nothing) => false, - (ImportAction::AddNestedImport { .. }, _) => true, - (ImportAction::AddNewUse { .. }, _) => false, - } - } -} - -// Find out the best ImportAction to import target path against current_use_tree. -// If current_use_tree has a nested import the function gets called recursively on every UseTree inside a UseTreeList. -fn walk_use_tree_for_best_action( - current_path_segments: &mut Vec, // buffer containing path segments - current_parent_use_tree_list: Option, // will be Some value if we are in a nested import - current_use_tree: ast::UseTree, // the use tree we are currently examinating - target: &[SmolStr], // the path we want to import -) -> ImportAction { - // We save the number of segments in the buffer so we can restore the correct segments - // before returning. Recursive call will add segments so we need to delete them. - let prev_len = current_path_segments.len(); - - let tree_list = current_use_tree.use_tree_list(); - let alias = current_use_tree.alias(); - - let path = match current_use_tree.path() { - Some(path) => path, - None => { - // If the use item don't have a path, it means it's broken (syntax error) - return ImportAction::add_new_use( - current_use_tree - .syntax() - .ancestors() - .find_map(ast::UseItem::cast) - .map(|it| it.syntax().clone()), - true, - ); - } - }; - - // This can happen only if current_use_tree is a direct child of a UseItem - if let Some(name) = alias.and_then(|it| it.name()) { - if compare_path_segment_with_name(&target[0], &name) { - return ImportAction::Nothing; - } - } - - collect_path_segments_raw(current_path_segments, path.clone()); - - // We compare only the new segments added in the line just above. - // The first prev_len segments were already compared in 'parent' recursive calls. - let left = target.split_at(prev_len).1; - let right = current_path_segments.split_at(prev_len).1; - let common = compare_path_segments(left, &right); - let mut action = match common { - 0 => ImportAction::add_new_use( - // e.g: target is std::fmt and we can have - // use foo::bar - // We add a brand new use statement - current_use_tree - .syntax() - .ancestors() - .find_map(ast::UseItem::cast) - .map(|it| it.syntax().clone()), - true, - ), - common if common == left.len() && left.len() == right.len() => { - // e.g: target is std::fmt and we can have - // 1- use std::fmt; - // 2- use std::fmt::{ ... } - if let Some(list) = tree_list { - // In case 2 we need to add self to the nested list - // unless it's already there - let has_self = list.use_trees().map(|it| it.path()).any(|p| { - p.and_then(|it| it.segment()) - .and_then(|it| it.kind()) - .filter(|k| *k == ast::PathSegmentKind::SelfKw) - .is_some() - }); - - if has_self { - ImportAction::Nothing - } else { - ImportAction::add_in_tree_list(current_path_segments.len(), list, true) - } - } else { - // Case 1 - ImportAction::Nothing - } - } - common if common != left.len() && left.len() == right.len() => { - // e.g: target is std::fmt and we have - // use std::io; - // We need to split. - let segments_to_split = current_path_segments.split_at(prev_len + common).1; - ImportAction::add_nested_import( - prev_len + common, - path, - Some(segments_to_split[0].clone()), - false, - ) - } - common if common == right.len() && left.len() > right.len() => { - // e.g: target is std::fmt and we can have - // 1- use std; - // 2- use std::{ ... }; - - // fallback action - let mut better_action = ImportAction::add_new_use( - current_use_tree - .syntax() - .ancestors() - .find_map(ast::UseItem::cast) - .map(|it| it.syntax().clone()), - true, - ); - if let Some(list) = tree_list { - // Case 2, check recursively if the path is already imported in the nested list - for u in list.use_trees() { - let child_action = walk_use_tree_for_best_action( - current_path_segments, - Some(list.clone()), - u, - target, - ); - if child_action.is_better(&better_action) { - better_action = child_action; - if let ImportAction::Nothing = better_action { - return better_action; - } - } - } - } else { - // Case 1, split adding self - better_action = ImportAction::add_nested_import(prev_len + common, path, None, true) - } - better_action - } - common if common == left.len() && left.len() < right.len() => { - // e.g: target is std::fmt and we can have - // use std::fmt::Debug; - let segments_to_split = current_path_segments.split_at(prev_len + common).1; - ImportAction::add_nested_import( - prev_len + common, - path, - Some(segments_to_split[0].clone()), - true, - ) - } - common if common < left.len() && common < right.len() => { - // e.g: target is std::fmt::nested::Debug - // use std::fmt::Display - let segments_to_split = current_path_segments.split_at(prev_len + common).1; - ImportAction::add_nested_import( - prev_len + common, - path, - Some(segments_to_split[0].clone()), - false, - ) - } - _ => unreachable!(), - }; - - // If we are inside a UseTreeList adding a use statement become adding to the existing - // tree list. - action = match (current_parent_use_tree_list, action.clone()) { - (Some(use_tree_list), ImportAction::AddNewUse { .. }) => { - ImportAction::add_in_tree_list(prev_len, use_tree_list, false) - } - (_, _) => action, - }; - - // We remove the segments added - current_path_segments.truncate(prev_len); - action -} - -fn best_action_for_target( - container: SyntaxNode, - anchor: SyntaxNode, - target: &[SmolStr], -) -> ImportAction { - let mut storage = Vec::with_capacity(16); // this should be the only allocation - let best_action = container - .children() - .filter_map(ast::UseItem::cast) - .filter_map(|it| it.use_tree()) - .map(|u| walk_use_tree_for_best_action(&mut storage, None, u, target)) - .fold(None, |best, a| match best { - Some(best) => Some(ImportAction::better(best, a)), - None => Some(a), - }); - - match best_action { - Some(action) => action, - None => { - // We have no action and no UseItem was found in container so we find - // another item and we use it as anchor. - // If there are no items above, we choose the target path itself as anchor. - // todo: we should include even whitespace blocks as anchor candidates - let anchor = container - .children() - .find(|n| n.text_range().start() < anchor.text_range().start()) - .or_else(|| Some(anchor)); - - ImportAction::add_new_use(anchor, false) - } - } -} - -fn make_assist(action: &ImportAction, target: &[SmolStr], edit: &mut TextEditBuilder) { - match action { - ImportAction::AddNewUse { anchor, add_after_anchor } => { - make_assist_add_new_use(anchor, *add_after_anchor, target, edit) - } - ImportAction::AddInTreeList { common_segments, tree_list, add_self } => { - // We know that the fist n segments already exists in the use statement we want - // to modify, so we want to add only the last target.len() - n segments. - let segments_to_add = target.split_at(*common_segments).1; - make_assist_add_in_tree_list(tree_list, segments_to_add, *add_self, edit) - } - ImportAction::AddNestedImport { - common_segments, - path_to_split, - first_segment_to_split, - add_self, - } => { - let segments_to_add = target.split_at(*common_segments).1; - make_assist_add_nested_import( - path_to_split, - first_segment_to_split, - segments_to_add, - *add_self, - edit, - ) - } - _ => {} - } -} - -fn make_assist_add_new_use( - anchor: &Option, - after: bool, - target: &[SmolStr], - edit: &mut TextEditBuilder, -) { - if let Some(anchor) = anchor { - let indent = ra_fmt::leading_indent(anchor); - let mut buf = String::new(); - if after { - buf.push_str("\n"); - if let Some(spaces) = &indent { - buf.push_str(spaces); - } - } - buf.push_str("use "); - fmt_segments_raw(target, &mut buf); - buf.push_str(";"); - if !after { - buf.push_str("\n\n"); - if let Some(spaces) = &indent { - buf.push_str(&spaces); - } - } - let position = if after { anchor.text_range().end() } else { anchor.text_range().start() }; - edit.insert(position, buf); - } -} - -fn make_assist_add_in_tree_list( - tree_list: &ast::UseTreeList, - target: &[SmolStr], - add_self: bool, - edit: &mut TextEditBuilder, -) { - let last = tree_list.use_trees().last(); - if let Some(last) = last { - let mut buf = String::new(); - let comma = last.syntax().siblings(Direction::Next).find(|n| n.kind() == T![,]); - let offset = if let Some(comma) = comma { - comma.text_range().end() - } else { - buf.push_str(","); - last.syntax().text_range().end() - }; - if add_self { - buf.push_str(" self") - } else { - buf.push_str(" "); - } - fmt_segments_raw(target, &mut buf); - edit.insert(offset, buf); - } else { - } -} - -fn make_assist_add_nested_import( - path: &ast::Path, - first_segment_to_split: &Option, - target: &[SmolStr], - add_self: bool, - edit: &mut TextEditBuilder, -) { - let use_tree = path.syntax().ancestors().find_map(ast::UseTree::cast); - if let Some(use_tree) = use_tree { - let (start, add_colon_colon) = if let Some(first_segment_to_split) = first_segment_to_split - { - (first_segment_to_split.syntax().text_range().start(), false) - } else { - (use_tree.syntax().text_range().end(), true) - }; - let end = use_tree.syntax().text_range().end(); - - let mut buf = String::new(); - if add_colon_colon { - buf.push_str("::"); - } - buf.push_str("{"); - if add_self { - buf.push_str("self, "); - } - fmt_segments_raw(target, &mut buf); - if !target.is_empty() { - buf.push_str(", "); - } - edit.insert(start, buf); - edit.insert(end, "}".to_string()); - } -} - -fn apply_auto_import( - container: &SyntaxNode, - path: &ast::Path, - target: &[SmolStr], - edit: &mut TextEditBuilder, -) { - let action = best_action_for_target(container.clone(), path.syntax().clone(), target); - make_assist(&action, target, edit); - if let Some(last) = path.segment() { - // Here we are assuming the assist will provide a correct use statement - // so we can delete the path qualifier - edit.delete(TextRange::from_to( - path.syntax().text_range().start(), - last.syntax().text_range().start(), - )); - } -} - -fn collect_hir_path_segments(path: &hir::Path) -> Option> { - let mut ps = Vec::::with_capacity(10); - match path.kind() { - hir::PathKind::Abs => ps.push("".into()), - hir::PathKind::Crate => ps.push("crate".into()), - hir::PathKind::Plain => {} - hir::PathKind::Super(0) => ps.push("self".into()), - hir::PathKind::Super(lvl) => { - let mut chain = "super".to_string(); - for _ in 0..*lvl { - chain += "::super"; - } - ps.push(chain.into()); - } - hir::PathKind::DollarCrate(_) => return None, - } - ps.extend(path.segments().iter().map(|it| it.name.to_string().into())); - Some(ps) -} - -#[cfg(test)] -mod tests { - use crate::helpers::{check_assist, check_assist_not_applicable}; - - use super::*; - - #[test] - fn test_auto_import_add_use_no_anchor() { - check_assist( - add_import, - " -std::fmt::Debug<|> - ", - " -use std::fmt::Debug; - -Debug<|> - ", - ); - } - #[test] - fn test_auto_import_add_use_no_anchor_with_item_below() { - check_assist( - add_import, - " -std::fmt::Debug<|> - -fn main() { -} - ", - " -use std::fmt::Debug; - -Debug<|> - -fn main() { -} - ", - ); - } - - #[test] - fn test_auto_import_add_use_no_anchor_with_item_above() { - check_assist( - add_import, - " -fn main() { -} - -std::fmt::Debug<|> - ", - " -use std::fmt::Debug; - -fn main() { -} - -Debug<|> - ", - ); - } - - #[test] - fn test_auto_import_add_use_no_anchor_2seg() { - check_assist( - add_import, - " -std::fmt<|>::Debug - ", - " -use std::fmt; - -fmt<|>::Debug - ", - ); - } - - #[test] - fn test_auto_import_add_use() { - check_assist( - add_import, - " -use stdx; - -impl std::fmt::Debug<|> for Foo { -} - ", - " -use stdx; -use std::fmt::Debug; - -impl Debug<|> for Foo { -} - ", - ); - } - - #[test] - fn test_auto_import_file_use_other_anchor() { - check_assist( - add_import, - " -impl std::fmt::Debug<|> for Foo { -} - ", - " -use std::fmt::Debug; - -impl Debug<|> for Foo { -} - ", - ); - } - - #[test] - fn test_auto_import_add_use_other_anchor_indent() { - check_assist( - add_import, - " - impl std::fmt::Debug<|> for Foo { - } - ", - " - use std::fmt::Debug; - - impl Debug<|> for Foo { - } - ", - ); - } - - #[test] - fn test_auto_import_split_different() { - check_assist( - add_import, - " -use std::fmt; - -impl std::io<|> for Foo { -} - ", - " -use std::{io, fmt}; - -impl io<|> for Foo { -} - ", - ); - } - - #[test] - fn test_auto_import_split_self_for_use() { - check_assist( - add_import, - " -use std::fmt; - -impl std::fmt::Debug<|> for Foo { -} - ", - " -use std::fmt::{self, Debug, }; - -impl Debug<|> for Foo { -} - ", - ); - } - - #[test] - fn test_auto_import_split_self_for_target() { - check_assist( - add_import, - " -use std::fmt::Debug; - -impl std::fmt<|> for Foo { -} - ", - " -use std::fmt::{self, Debug}; - -impl fmt<|> for Foo { -} - ", - ); - } - - #[test] - fn test_auto_import_add_to_nested_self_nested() { - check_assist( - add_import, - " -use std::fmt::{Debug, nested::{Display}}; - -impl std::fmt::nested<|> for Foo { -} -", - " -use std::fmt::{Debug, nested::{Display, self}}; - -impl nested<|> for Foo { -} -", - ); - } - - #[test] - fn test_auto_import_add_to_nested_self_already_included() { - check_assist( - add_import, - " -use std::fmt::{Debug, nested::{self, Display}}; - -impl std::fmt::nested<|> for Foo { -} -", - " -use std::fmt::{Debug, nested::{self, Display}}; - -impl nested<|> for Foo { -} -", - ); - } - - #[test] - fn test_auto_import_add_to_nested_nested() { - check_assist( - add_import, - " -use std::fmt::{Debug, nested::{Display}}; - -impl std::fmt::nested::Debug<|> for Foo { -} -", - " -use std::fmt::{Debug, nested::{Display, Debug}}; - -impl Debug<|> for Foo { -} -", - ); - } - - #[test] - fn test_auto_import_split_common_target_longer() { - check_assist( - add_import, - " -use std::fmt::Debug; - -impl std::fmt::nested::Display<|> for Foo { -} -", - " -use std::fmt::{nested::Display, Debug}; - -impl Display<|> for Foo { -} -", - ); - } - - #[test] - fn test_auto_import_split_common_use_longer() { - check_assist( - add_import, - " -use std::fmt::nested::Debug; - -impl std::fmt::Display<|> for Foo { -} -", - " -use std::fmt::{Display, nested::Debug}; - -impl Display<|> for Foo { -} -", - ); - } - - #[test] - fn test_auto_import_use_nested_import() { - check_assist( - add_import, - " -use crate::{ - ty::{Substs, Ty}, - AssocItem, -}; - -fn foo() { crate::ty::lower<|>::trait_env() } -", - " -use crate::{ - ty::{Substs, Ty, lower}, - AssocItem, -}; - -fn foo() { lower<|>::trait_env() } -", - ); - } - - #[test] - fn test_auto_import_alias() { - check_assist( - add_import, - " -use std::fmt as foo; - -impl foo::Debug<|> for Foo { -} -", - " -use std::fmt as foo; - -impl Debug<|> for Foo { -} -", - ); - } - - #[test] - fn test_auto_import_not_applicable_one_segment() { - check_assist_not_applicable( - add_import, - " -impl foo<|> for Foo { -} -", - ); - } - - #[test] - fn test_auto_import_not_applicable_in_use() { - check_assist_not_applicable( - add_import, - " -use std::fmt<|>; -", - ); - } - - #[test] - fn test_auto_import_add_use_no_anchor_in_mod_mod() { - check_assist( - add_import, - " -mod foo { - mod bar { - std::fmt::Debug<|> - } -} - ", - " -mod foo { - mod bar { - use std::fmt::Debug; - - Debug<|> - } -} - ", - ); - } -} diff --git a/crates/ra_assists/src/assists/add_missing_impl_members.rs b/crates/ra_assists/src/assists/add_missing_impl_members.rs deleted file mode 100644 index 448697d31..000000000 --- a/crates/ra_assists/src/assists/add_missing_impl_members.rs +++ /dev/null @@ -1,608 +0,0 @@ -use hir::{db::HirDatabase, HasSource, InFile}; -use ra_syntax::{ - ast::{self, edit, make, AstNode, NameOwner}, - SmolStr, -}; - -use crate::{ - ast_transform::{self, AstTransform, QualifyPaths, SubstituteTypeParams}, - Assist, AssistCtx, AssistId, -}; - -#[derive(PartialEq)] -enum AddMissingImplMembersMode { - DefaultMethodsOnly, - NoDefaultMethods, -} - -// Assist: add_impl_missing_members -// -// Adds scaffold for required impl members. -// -// ``` -// trait Trait { -// Type X; -// fn foo(&self) -> T; -// fn bar(&self) {} -// } -// -// impl Trait for () {<|> -// -// } -// ``` -// -> -// ``` -// trait Trait { -// Type X; -// fn foo(&self) -> T; -// fn bar(&self) {} -// } -// -// impl Trait for () { -// fn foo(&self) -> u32 { unimplemented!() } -// -// } -// ``` -pub(crate) fn add_missing_impl_members(ctx: AssistCtx) -> Option { - add_missing_impl_members_inner( - ctx, - AddMissingImplMembersMode::NoDefaultMethods, - "add_impl_missing_members", - "Implement missing members", - ) -} - -// Assist: add_impl_default_members -// -// Adds scaffold for overriding default impl members. -// -// ``` -// trait Trait { -// Type X; -// fn foo(&self); -// fn bar(&self) {} -// } -// -// impl Trait for () { -// Type X = (); -// fn foo(&self) {}<|> -// -// } -// ``` -// -> -// ``` -// trait Trait { -// Type X; -// fn foo(&self); -// fn bar(&self) {} -// } -// -// impl Trait for () { -// Type X = (); -// fn foo(&self) {} -// fn bar(&self) {} -// -// } -// ``` -pub(crate) fn add_missing_default_members(ctx: AssistCtx) -> Option { - add_missing_impl_members_inner( - ctx, - AddMissingImplMembersMode::DefaultMethodsOnly, - "add_impl_default_members", - "Implement default members", - ) -} - -fn add_missing_impl_members_inner( - ctx: AssistCtx, - mode: AddMissingImplMembersMode, - assist_id: &'static str, - label: &'static str, -) -> Option { - let _p = ra_prof::profile("add_missing_impl_members_inner"); - let impl_node = ctx.find_node_at_offset::()?; - let impl_item_list = impl_node.item_list()?; - - let (trait_, trait_def) = { - let analyzer = ctx.source_analyzer(impl_node.syntax(), None); - - resolve_target_trait_def(ctx.db, &analyzer, &impl_node)? - }; - - let def_name = |item: &ast::ImplItem| -> Option { - match item { - ast::ImplItem::FnDef(def) => def.name(), - ast::ImplItem::TypeAliasDef(def) => def.name(), - ast::ImplItem::ConstDef(def) => def.name(), - } - .map(|it| it.text().clone()) - }; - - let trait_items = trait_def.item_list()?.impl_items(); - let impl_items = impl_item_list.impl_items().collect::>(); - - let missing_items: Vec<_> = trait_items - .filter(|t| def_name(t).is_some()) - .filter(|t| match t { - ast::ImplItem::FnDef(def) => match mode { - AddMissingImplMembersMode::DefaultMethodsOnly => def.body().is_some(), - AddMissingImplMembersMode::NoDefaultMethods => def.body().is_none(), - }, - _ => mode == AddMissingImplMembersMode::NoDefaultMethods, - }) - .filter(|t| impl_items.iter().all(|i| def_name(i) != def_name(t))) - .collect(); - if missing_items.is_empty() { - return None; - } - - let db = ctx.db; - let file_id = ctx.frange.file_id; - let trait_file_id = trait_.source(db).file_id; - - ctx.add_assist(AssistId(assist_id), label, |edit| { - let n_existing_items = impl_item_list.impl_items().count(); - let module = hir::SourceAnalyzer::new( - db, - hir::InFile::new(file_id.into(), impl_node.syntax()), - None, - ) - .module(); - let ast_transform = QualifyPaths::new(db, module) - .or(SubstituteTypeParams::for_trait_impl(db, trait_, impl_node)); - let items = missing_items - .into_iter() - .map(|it| ast_transform::apply(&*ast_transform, InFile::new(trait_file_id, it))) - .map(|it| match it { - ast::ImplItem::FnDef(def) => ast::ImplItem::FnDef(add_body(def)), - _ => it, - }) - .map(|it| edit::strip_attrs_and_docs(&it)); - let new_impl_item_list = impl_item_list.append_items(items); - let cursor_position = { - let first_new_item = new_impl_item_list.impl_items().nth(n_existing_items).unwrap(); - first_new_item.syntax().text_range().start() - }; - - edit.replace_ast(impl_item_list, new_impl_item_list); - edit.set_cursor(cursor_position); - }) -} - -fn add_body(fn_def: ast::FnDef) -> ast::FnDef { - if fn_def.body().is_none() { - fn_def.with_body(make::block_from_expr(make::expr_unimplemented())) - } else { - fn_def - } -} - -/// Given an `ast::ImplBlock`, resolves the target trait (the one being -/// implemented) to a `ast::TraitDef`. -fn resolve_target_trait_def( - db: &impl HirDatabase, - analyzer: &hir::SourceAnalyzer, - impl_block: &ast::ImplBlock, -) -> Option<(hir::Trait, ast::TraitDef)> { - let ast_path = impl_block - .target_trait() - .map(|it| it.syntax().clone()) - .and_then(ast::PathType::cast)? - .path()?; - - match analyzer.resolve_path(db, &ast_path) { - Some(hir::PathResolution::Def(hir::ModuleDef::Trait(def))) => { - Some((def, def.source(db).value)) - } - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::helpers::{check_assist, check_assist_not_applicable}; - - #[test] - fn test_add_missing_impl_members() { - check_assist( - add_missing_impl_members, - " -trait Foo { - type Output; - - const CONST: usize = 42; - - fn foo(&self); - fn bar(&self); - fn baz(&self); -} - -struct S; - -impl Foo for S { - fn bar(&self) {} -<|> -}", - " -trait Foo { - type Output; - - const CONST: usize = 42; - - fn foo(&self); - fn bar(&self); - fn baz(&self); -} - -struct S; - -impl Foo for S { - fn bar(&self) {} - <|>type Output; - const CONST: usize = 42; - fn foo(&self) { unimplemented!() } - fn baz(&self) { unimplemented!() } - -}", - ); - } - - #[test] - fn test_copied_overriden_members() { - check_assist( - add_missing_impl_members, - " -trait Foo { - fn foo(&self); - fn bar(&self) -> bool { true } - fn baz(&self) -> u32 { 42 } -} - -struct S; - -impl Foo for S { - fn bar(&self) {} -<|> -}", - " -trait Foo { - fn foo(&self); - fn bar(&self) -> bool { true } - fn baz(&self) -> u32 { 42 } -} - -struct S; - -impl Foo for S { - fn bar(&self) {} - <|>fn foo(&self) { unimplemented!() } - -}", - ); - } - - #[test] - fn test_empty_impl_block() { - check_assist( - add_missing_impl_members, - " -trait Foo { fn foo(&self); } -struct S; -impl Foo for S { <|> }", - " -trait Foo { fn foo(&self); } -struct S; -impl Foo for S { - <|>fn foo(&self) { unimplemented!() } -}", - ); - } - - #[test] - fn fill_in_type_params_1() { - check_assist( - add_missing_impl_members, - " -trait Foo { fn foo(&self, t: T) -> &T; } -struct S; -impl Foo for S { <|> }", - " -trait Foo { fn foo(&self, t: T) -> &T; } -struct S; -impl Foo for S { - <|>fn foo(&self, t: u32) -> &u32 { unimplemented!() } -}", - ); - } - - #[test] - fn fill_in_type_params_2() { - check_assist( - add_missing_impl_members, - " -trait Foo { fn foo(&self, t: T) -> &T; } -struct S; -impl Foo for S { <|> }", - " -trait Foo { fn foo(&self, t: T) -> &T; } -struct S; -impl Foo for S { - <|>fn foo(&self, t: U) -> &U { unimplemented!() } -}", - ); - } - - #[test] - fn test_cursor_after_empty_impl_block() { - check_assist( - add_missing_impl_members, - " -trait Foo { fn foo(&self); } -struct S; -impl Foo for S {}<|>", - " -trait Foo { fn foo(&self); } -struct S; -impl Foo for S { - <|>fn foo(&self) { unimplemented!() } -}", - ) - } - - #[test] - fn test_qualify_path_1() { - check_assist( - add_missing_impl_members, - " -mod foo { - pub struct Bar; - trait Foo { fn foo(&self, bar: Bar); } -} -struct S; -impl foo::Foo for S { <|> }", - " -mod foo { - pub struct Bar; - trait Foo { fn foo(&self, bar: Bar); } -} -struct S; -impl foo::Foo for S { - <|>fn foo(&self, bar: foo::Bar) { unimplemented!() } -}", - ); - } - - #[test] - fn test_qualify_path_generic() { - check_assist( - add_missing_impl_members, - " -mod foo { - pub struct Bar; - trait Foo { fn foo(&self, bar: Bar); } -} -struct S; -impl foo::Foo for S { <|> }", - " -mod foo { - pub struct Bar; - trait Foo { fn foo(&self, bar: Bar); } -} -struct S; -impl foo::Foo for S { - <|>fn foo(&self, bar: foo::Bar) { unimplemented!() } -}", - ); - } - - #[test] - fn test_qualify_path_and_substitute_param() { - check_assist( - add_missing_impl_members, - " -mod foo { - pub struct Bar; - trait Foo { fn foo(&self, bar: Bar); } -} -struct S; -impl foo::Foo for S { <|> }", - " -mod foo { - pub struct Bar; - trait Foo { fn foo(&self, bar: Bar); } -} -struct S; -impl foo::Foo for S { - <|>fn foo(&self, bar: foo::Bar) { unimplemented!() } -}", - ); - } - - #[test] - fn test_substitute_param_no_qualify() { - // when substituting params, the substituted param should not be qualified! - check_assist( - add_missing_impl_members, - " -mod foo { - trait Foo { fn foo(&self, bar: T); } - pub struct Param; -} -struct Param; -struct S; -impl foo::Foo for S { <|> }", - " -mod foo { - trait Foo { fn foo(&self, bar: T); } - pub struct Param; -} -struct Param; -struct S; -impl foo::Foo for S { - <|>fn foo(&self, bar: Param) { unimplemented!() } -}", - ); - } - - #[test] - fn test_qualify_path_associated_item() { - check_assist( - add_missing_impl_members, - " -mod foo { - pub struct Bar; - impl Bar { type Assoc = u32; } - trait Foo { fn foo(&self, bar: Bar::Assoc); } -} -struct S; -impl foo::Foo for S { <|> }", - " -mod foo { - pub struct Bar; - impl Bar { type Assoc = u32; } - trait Foo { fn foo(&self, bar: Bar::Assoc); } -} -struct S; -impl foo::Foo for S { - <|>fn foo(&self, bar: foo::Bar::Assoc) { unimplemented!() } -}", - ); - } - - #[test] - fn test_qualify_path_nested() { - check_assist( - add_missing_impl_members, - " -mod foo { - pub struct Bar; - pub struct Baz; - trait Foo { fn foo(&self, bar: Bar); } -} -struct S; -impl foo::Foo for S { <|> }", - " -mod foo { - pub struct Bar; - pub struct Baz; - trait Foo { fn foo(&self, bar: Bar); } -} -struct S; -impl foo::Foo for S { - <|>fn foo(&self, bar: foo::Bar) { unimplemented!() } -}", - ); - } - - #[test] - fn test_qualify_path_fn_trait_notation() { - check_assist( - add_missing_impl_members, - " -mod foo { - pub trait Fn { type Output; } - trait Foo { fn foo(&self, bar: dyn Fn(u32) -> i32); } -} -struct S; -impl foo::Foo for S { <|> }", - " -mod foo { - pub trait Fn { type Output; } - trait Foo { fn foo(&self, bar: dyn Fn(u32) -> i32); } -} -struct S; -impl foo::Foo for S { - <|>fn foo(&self, bar: dyn Fn(u32) -> i32) { unimplemented!() } -}", - ); - } - - #[test] - fn test_empty_trait() { - check_assist_not_applicable( - add_missing_impl_members, - " -trait Foo; -struct S; -impl Foo for S { <|> }", - ) - } - - #[test] - fn test_ignore_unnamed_trait_members_and_default_methods() { - check_assist_not_applicable( - add_missing_impl_members, - " -trait Foo { - fn (arg: u32); - fn valid(some: u32) -> bool { false } -} -struct S; -impl Foo for S { <|> }", - ) - } - - #[test] - fn test_with_docstring_and_attrs() { - check_assist( - add_missing_impl_members, - r#" -#[doc(alias = "test alias")] -trait Foo { - /// doc string - type Output; - - #[must_use] - fn foo(&self); -} -struct S; -impl Foo for S {}<|>"#, - r#" -#[doc(alias = "test alias")] -trait Foo { - /// doc string - type Output; - - #[must_use] - fn foo(&self); -} -struct S; -impl Foo for S { - <|>type Output; - fn foo(&self) { unimplemented!() } -}"#, - ) - } - - #[test] - fn test_default_methods() { - check_assist( - add_missing_default_members, - " -trait Foo { - type Output; - - const CONST: usize = 42; - - fn valid(some: u32) -> bool { false } - fn foo(some: u32) -> bool; -} -struct S; -impl Foo for S { <|> }", - " -trait Foo { - type Output; - - const CONST: usize = 42; - - fn valid(some: u32) -> bool { false } - fn foo(some: u32) -> bool; -} -struct S; -impl Foo for S { - <|>fn valid(some: u32) -> bool { false } -}", - ) - } -} diff --git a/crates/ra_assists/src/assists/add_new.rs b/crates/ra_assists/src/assists/add_new.rs deleted file mode 100644 index a08639311..000000000 --- a/crates/ra_assists/src/assists/add_new.rs +++ /dev/null @@ -1,430 +0,0 @@ -use format_buf::format; -use hir::InFile; -use join_to_string::join; -use ra_syntax::{ - ast::{ - self, AstNode, NameOwner, StructKind, TypeAscriptionOwner, TypeParamsOwner, VisibilityOwner, - }, - TextUnit, T, -}; -use std::fmt::Write; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: add_new -// -// Adds a new inherent impl for a type. -// -// ``` -// struct Ctx { -// data: T,<|> -// } -// ``` -// -> -// ``` -// struct Ctx { -// data: T, -// } -// -// impl Ctx { -// fn new(data: T) -> Self { Self { data } } -// } -// -// ``` -pub(crate) fn add_new(ctx: AssistCtx) -> Option { - let strukt = ctx.find_node_at_offset::()?; - - // We want to only apply this to non-union structs with named fields - let field_list = match strukt.kind() { - StructKind::Record(named) => named, - _ => return None, - }; - - // Return early if we've found an existing new fn - let impl_block = find_struct_impl(&ctx, &strukt)?; - - ctx.add_assist(AssistId("add_new"), "Add default constructor", |edit| { - edit.target(strukt.syntax().text_range()); - - let mut buf = String::with_capacity(512); - - if impl_block.is_some() { - buf.push('\n'); - } - - let vis = strukt.visibility().map(|v| format!("{} ", v.syntax())); - let vis = vis.as_ref().map(String::as_str).unwrap_or(""); - write!(&mut buf, " {}fn new(", vis).unwrap(); - - join(field_list.fields().filter_map(|f| { - Some(format!("{}: {}", f.name()?.syntax().text(), f.ascribed_type()?.syntax().text())) - })) - .separator(", ") - .to_buf(&mut buf); - - buf.push_str(") -> Self { Self {"); - - join(field_list.fields().filter_map(|f| Some(f.name()?.syntax().text()))) - .separator(", ") - .surround_with(" ", " ") - .to_buf(&mut buf); - - buf.push_str("} }"); - - let (start_offset, end_offset) = impl_block - .and_then(|impl_block| { - buf.push('\n'); - let start = impl_block - .syntax() - .descendants_with_tokens() - .find(|t| t.kind() == T!['{'])? - .text_range() - .end(); - - Some((start, TextUnit::from_usize(1))) - }) - .unwrap_or_else(|| { - buf = generate_impl_text(&strukt, &buf); - let start = strukt.syntax().text_range().end(); - - (start, TextUnit::from_usize(3)) - }); - - edit.set_cursor(start_offset + TextUnit::of_str(&buf) - end_offset); - edit.insert(start_offset, buf); - }) -} - -// Generates the surrounding `impl Type { }` including type and lifetime -// parameters -fn generate_impl_text(strukt: &ast::StructDef, code: &str) -> String { - let type_params = strukt.type_param_list(); - let mut buf = String::with_capacity(code.len()); - buf.push_str("\n\nimpl"); - if let Some(type_params) = &type_params { - format!(buf, "{}", type_params.syntax()); - } - buf.push_str(" "); - buf.push_str(strukt.name().unwrap().text().as_str()); - if let Some(type_params) = type_params { - let lifetime_params = type_params - .lifetime_params() - .filter_map(|it| it.lifetime_token()) - .map(|it| it.text().clone()); - let type_params = - type_params.type_params().filter_map(|it| it.name()).map(|it| it.text().clone()); - join(lifetime_params.chain(type_params)).surround_with("<", ">").to_buf(&mut buf); - } - - format!(&mut buf, " {{\n{}\n}}\n", code); - - buf -} - -// Uses a syntax-driven approach to find any impl blocks for the struct that -// exist within the module/file -// -// Returns `None` if we've found an existing `new` fn -// -// FIXME: change the new fn checking to a more semantic approach when that's more -// viable (e.g. we process proc macros, etc) -fn find_struct_impl(ctx: &AssistCtx, strukt: &ast::StructDef) -> Option> { - let db = ctx.db; - let module = strukt.syntax().ancestors().find(|node| { - ast::Module::can_cast(node.kind()) || ast::SourceFile::can_cast(node.kind()) - })?; - let mut sb = ctx.source_binder(); - - let struct_ty = { - let src = InFile { file_id: ctx.frange.file_id.into(), value: strukt.clone() }; - sb.to_def(src)?.ty(db) - }; - - let block = module.descendants().filter_map(ast::ImplBlock::cast).find_map(|impl_blk| { - let src = InFile { file_id: ctx.frange.file_id.into(), value: impl_blk.clone() }; - let blk = sb.to_def(src)?; - - let same_ty = blk.target_ty(db) == struct_ty; - let not_trait_impl = blk.target_trait(db).is_none(); - - if !(same_ty && not_trait_impl) { - None - } else { - Some(impl_blk) - } - }); - - if let Some(ref impl_blk) = block { - if has_new_fn(impl_blk) { - return None; - } - } - - Some(block) -} - -fn has_new_fn(imp: &ast::ImplBlock) -> bool { - if let Some(il) = imp.item_list() { - for item in il.impl_items() { - if let ast::ImplItem::FnDef(f) = item { - if let Some(name) = f.name() { - if name.text().eq_ignore_ascii_case("new") { - return true; - } - } - } - } - } - - false -} - -#[cfg(test)] -mod tests { - use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; - - use super::*; - - #[test] - #[rustfmt::skip] - fn test_add_new() { - // Check output of generation - check_assist( - add_new, -"struct Foo {<|>}", -"struct Foo {} - -impl Foo { - fn new() -> Self { Self { } }<|> -} -", - ); - check_assist( - add_new, -"struct Foo {<|>}", -"struct Foo {} - -impl Foo { - fn new() -> Self { Self { } }<|> -} -", - ); - check_assist( - add_new, -"struct Foo<'a, T: Foo<'a>> {<|>}", -"struct Foo<'a, T: Foo<'a>> {} - -impl<'a, T: Foo<'a>> Foo<'a, T> { - fn new() -> Self { Self { } }<|> -} -", - ); - check_assist( - add_new, -"struct Foo { baz: String <|>}", -"struct Foo { baz: String } - -impl Foo { - fn new(baz: String) -> Self { Self { baz } }<|> -} -", - ); - check_assist( - add_new, -"struct Foo { baz: String, qux: Vec <|>}", -"struct Foo { baz: String, qux: Vec } - -impl Foo { - fn new(baz: String, qux: Vec) -> Self { Self { baz, qux } }<|> -} -", - ); - - // Check that visibility modifiers don't get brought in for fields - check_assist( - add_new, -"struct Foo { pub baz: String, pub qux: Vec <|>}", -"struct Foo { pub baz: String, pub qux: Vec } - -impl Foo { - fn new(baz: String, qux: Vec) -> Self { Self { baz, qux } }<|> -} -", - ); - - // Check that it reuses existing impls - check_assist( - add_new, -"struct Foo {<|>} - -impl Foo {} -", -"struct Foo {} - -impl Foo { - fn new() -> Self { Self { } }<|> -} -", - ); - check_assist( - add_new, -"struct Foo {<|>} - -impl Foo { - fn qux(&self) {} -} -", -"struct Foo {} - -impl Foo { - fn new() -> Self { Self { } }<|> - - fn qux(&self) {} -} -", - ); - - check_assist( - add_new, -"struct Foo {<|>} - -impl Foo { - fn qux(&self) {} - fn baz() -> i32 { - 5 - } -} -", -"struct Foo {} - -impl Foo { - fn new() -> Self { Self { } }<|> - - fn qux(&self) {} - fn baz() -> i32 { - 5 - } -} -", - ); - - // Check visibility of new fn based on struct - check_assist( - add_new, -"pub struct Foo {<|>}", -"pub struct Foo {} - -impl Foo { - pub fn new() -> Self { Self { } }<|> -} -", - ); - check_assist( - add_new, -"pub(crate) struct Foo {<|>}", -"pub(crate) struct Foo {} - -impl Foo { - pub(crate) fn new() -> Self { Self { } }<|> -} -", - ); - } - - #[test] - fn add_new_not_applicable_if_fn_exists() { - check_assist_not_applicable( - add_new, - " -struct Foo {<|>} - -impl Foo { - fn new() -> Self { - Self - } -}", - ); - - check_assist_not_applicable( - add_new, - " -struct Foo {<|>} - -impl Foo { - fn New() -> Self { - Self - } -}", - ); - } - - #[test] - fn add_new_target() { - check_assist_target( - add_new, - " -struct SomeThingIrrelevant; -/// Has a lifetime parameter -struct Foo<'a, T: Foo<'a>> {<|>} -struct EvenMoreIrrelevant; -", - "/// Has a lifetime parameter -struct Foo<'a, T: Foo<'a>> {}", - ); - } - - #[test] - fn test_unrelated_new() { - check_assist( - add_new, - r##" -pub struct AstId { - file_id: HirFileId, - file_ast_id: FileAstId, -} - -impl AstId { - pub fn new(file_id: HirFileId, file_ast_id: FileAstId) -> AstId { - AstId { file_id, file_ast_id } - } -} - -pub struct Source { - pub file_id: HirFileId,<|> - pub ast: T, -} - -impl Source { - pub fn map U, U>(self, f: F) -> Source { - Source { file_id: self.file_id, ast: f(self.ast) } - } -} -"##, - r##" -pub struct AstId { - file_id: HirFileId, - file_ast_id: FileAstId, -} - -impl AstId { - pub fn new(file_id: HirFileId, file_ast_id: FileAstId) -> AstId { - AstId { file_id, file_ast_id } - } -} - -pub struct Source { - pub file_id: HirFileId, - pub ast: T, -} - -impl Source { - pub fn new(file_id: HirFileId, ast: T) -> Self { Self { file_id, ast } }<|> - - pub fn map U, U>(self, f: F) -> Source { - Source { file_id: self.file_id, ast: f(self.ast) } - } -} -"##, - ); - } -} diff --git a/crates/ra_assists/src/assists/apply_demorgan.rs b/crates/ra_assists/src/assists/apply_demorgan.rs deleted file mode 100644 index ba08a8223..000000000 --- a/crates/ra_assists/src/assists/apply_demorgan.rs +++ /dev/null @@ -1,90 +0,0 @@ -use super::invert_if::invert_boolean_expression; -use ra_syntax::ast::{self, AstNode}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: apply_demorgan -// -// Apply [De Morgan's law](https://en.wikipedia.org/wiki/De_Morgan%27s_laws). -// This transforms expressions of the form `!l || !r` into `!(l && r)`. -// This also works with `&&`. This assist can only be applied with the cursor -// on either `||` or `&&`, with both operands being a negation of some kind. -// This means something of the form `!x` or `x != y`. -// -// ``` -// fn main() { -// if x != 4 ||<|> !y {} -// } -// ``` -// -> -// ``` -// fn main() { -// if !(x == 4 && y) {} -// } -// ``` -pub(crate) fn apply_demorgan(ctx: AssistCtx) -> Option { - let expr = ctx.find_node_at_offset::()?; - let op = expr.op_kind()?; - let op_range = expr.op_token()?.text_range(); - let opposite_op = opposite_logic_op(op)?; - let cursor_in_range = ctx.frange.range.is_subrange(&op_range); - if !cursor_in_range { - return None; - } - - let lhs = expr.lhs()?; - let lhs_range = lhs.syntax().text_range(); - let not_lhs = invert_boolean_expression(lhs); - - let rhs = expr.rhs()?; - let rhs_range = rhs.syntax().text_range(); - let not_rhs = invert_boolean_expression(rhs); - - ctx.add_assist(AssistId("apply_demorgan"), "Apply De Morgan's law", |edit| { - edit.target(op_range); - edit.replace(op_range, opposite_op); - edit.replace(lhs_range, format!("!({}", not_lhs.syntax().text())); - edit.replace(rhs_range, format!("{})", not_rhs.syntax().text())); - }) -} - -// Return the opposite text for a given logical operator, if it makes sense -fn opposite_logic_op(kind: ast::BinOp) -> Option<&'static str> { - match kind { - ast::BinOp::BooleanOr => Some("&&"), - ast::BinOp::BooleanAnd => Some("||"), - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::helpers::{check_assist, check_assist_not_applicable}; - - #[test] - fn demorgan_turns_and_into_or() { - check_assist(apply_demorgan, "fn f() { !x &&<|> !x }", "fn f() { !(x ||<|> x) }") - } - - #[test] - fn demorgan_turns_or_into_and() { - check_assist(apply_demorgan, "fn f() { !x ||<|> !x }", "fn f() { !(x &&<|> x) }") - } - - #[test] - fn demorgan_removes_inequality() { - check_assist(apply_demorgan, "fn f() { x != x ||<|> !x }", "fn f() { !(x == x &&<|> x) }") - } - - #[test] - fn demorgan_general_case() { - check_assist(apply_demorgan, "fn f() { x ||<|> x }", "fn f() { !(!x &&<|> !x) }") - } - - #[test] - fn demorgan_doesnt_apply_with_cursor_not_on_op() { - check_assist_not_applicable(apply_demorgan, "fn f() { <|> !x || !x }") - } -} diff --git a/crates/ra_assists/src/assists/auto_import.rs b/crates/ra_assists/src/assists/auto_import.rs deleted file mode 100644 index 84b5474f9..000000000 --- a/crates/ra_assists/src/assists/auto_import.rs +++ /dev/null @@ -1,258 +0,0 @@ -use hir::ModPath; -use ra_ide_db::imports_locator::ImportsLocator; -use ra_syntax::{ - ast::{self, AstNode}, - SyntaxNode, -}; - -use crate::{ - assist_ctx::{ActionBuilder, Assist, AssistCtx}, - auto_import_text_edit, AssistId, -}; -use std::collections::BTreeSet; - -// Assist: auto_import -// -// If the name is unresolved, provides all possible imports for it. -// -// ``` -// fn main() { -// let map = HashMap<|>::new(); -// } -// # pub mod std { pub mod collections { pub struct HashMap { } } } -// ``` -// -> -// ``` -// use std::collections::HashMap; -// -// fn main() { -// let map = HashMap::new(); -// } -// # pub mod std { pub mod collections { pub struct HashMap { } } } -// ``` -pub(crate) fn auto_import(ctx: AssistCtx) -> Option { - let path_to_import: ast::Path = ctx.find_node_at_offset()?; - let path_to_import_syntax = path_to_import.syntax(); - if path_to_import_syntax.ancestors().find_map(ast::UseItem::cast).is_some() { - return None; - } - let name_to_import = - path_to_import_syntax.descendants().find_map(ast::NameRef::cast)?.syntax().to_string(); - - let module = path_to_import_syntax.ancestors().find_map(ast::Module::cast); - let position = match module.and_then(|it| it.item_list()) { - Some(item_list) => item_list.syntax().clone(), - None => { - let current_file = path_to_import_syntax.ancestors().find_map(ast::SourceFile::cast)?; - current_file.syntax().clone() - } - }; - let source_analyzer = ctx.source_analyzer(&position, None); - let module_with_name_to_import = source_analyzer.module()?; - if source_analyzer.resolve_path(ctx.db, &path_to_import).is_some() { - return None; - } - - let mut imports_locator = ImportsLocator::new(ctx.db); - - let proposed_imports = imports_locator - .find_imports(&name_to_import) - .into_iter() - .filter_map(|module_def| module_with_name_to_import.find_use_path(ctx.db, module_def)) - .filter(|use_path| !use_path.segments.is_empty()) - .take(20) - .collect::>(); - - if proposed_imports.is_empty() { - return None; - } - - ctx.add_assist_group(AssistId("auto_import"), format!("Import {}", name_to_import), || { - proposed_imports - .into_iter() - .map(|import| import_to_action(import, &position, &path_to_import_syntax)) - .collect() - }) -} - -fn import_to_action(import: ModPath, position: &SyntaxNode, anchor: &SyntaxNode) -> ActionBuilder { - let mut action_builder = ActionBuilder::default(); - action_builder.label(format!("Import `{}`", &import)); - auto_import_text_edit(position, anchor, &import, action_builder.text_edit_builder()); - action_builder -} - -#[cfg(test)] -mod tests { - use crate::helpers::{check_assist, check_assist_not_applicable}; - - use super::*; - - #[test] - fn applicable_when_found_an_import() { - check_assist( - auto_import, - r" - <|>PubStruct - - pub mod PubMod { - pub struct PubStruct; - } - ", - r" - <|>use PubMod::PubStruct; - - PubStruct - - pub mod PubMod { - pub struct PubStruct; - } - ", - ); - } - - #[test] - fn auto_imports_are_merged() { - check_assist( - auto_import, - r" - use PubMod::PubStruct1; - - struct Test { - test: Pub<|>Struct2, - } - - pub mod PubMod { - pub struct PubStruct1; - pub struct PubStruct2 { - _t: T, - } - } - ", - r" - use PubMod::{PubStruct2, PubStruct1}; - - struct Test { - test: Pub<|>Struct2, - } - - pub mod PubMod { - pub struct PubStruct1; - pub struct PubStruct2 { - _t: T, - } - } - ", - ); - } - - #[test] - fn applicable_when_found_multiple_imports() { - check_assist( - auto_import, - r" - PubSt<|>ruct - - pub mod PubMod1 { - pub struct PubStruct; - } - pub mod PubMod2 { - pub struct PubStruct; - } - pub mod PubMod3 { - pub struct PubStruct; - } - ", - r" - use PubMod1::PubStruct; - - PubSt<|>ruct - - pub mod PubMod1 { - pub struct PubStruct; - } - pub mod PubMod2 { - pub struct PubStruct; - } - pub mod PubMod3 { - pub struct PubStruct; - } - ", - ); - } - - #[test] - fn not_applicable_for_already_imported_types() { - check_assist_not_applicable( - auto_import, - r" - use PubMod::PubStruct; - - PubStruct<|> - - pub mod PubMod { - pub struct PubStruct; - } - ", - ); - } - - #[test] - fn not_applicable_for_types_with_private_paths() { - check_assist_not_applicable( - auto_import, - r" - PrivateStruct<|> - - pub mod PubMod { - struct PrivateStruct; - } - ", - ); - } - - #[test] - fn not_applicable_when_no_imports_found() { - check_assist_not_applicable( - auto_import, - " - PubStruct<|>", - ); - } - - #[test] - fn not_applicable_in_import_statements() { - check_assist_not_applicable( - auto_import, - r" - use PubStruct<|>; - - pub mod PubMod { - pub struct PubStruct; - }", - ); - } - - #[test] - fn function_import() { - check_assist( - auto_import, - r" - test_function<|> - - pub mod PubMod { - pub fn test_function() {}; - } - ", - r" - use PubMod::test_function; - - test_function<|> - - pub mod PubMod { - pub fn test_function() {}; - } - ", - ); - } -} diff --git a/crates/ra_assists/src/assists/change_visibility.rs b/crates/ra_assists/src/assists/change_visibility.rs deleted file mode 100644 index f325b6f92..000000000 --- a/crates/ra_assists/src/assists/change_visibility.rs +++ /dev/null @@ -1,167 +0,0 @@ -use ra_syntax::{ - ast::{self, NameOwner, VisibilityOwner}, - AstNode, - SyntaxKind::{ - ATTR, COMMENT, ENUM_DEF, FN_DEF, IDENT, MODULE, STRUCT_DEF, TRAIT_DEF, VISIBILITY, - WHITESPACE, - }, - SyntaxNode, TextUnit, T, -}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: change_visibility -// -// Adds or changes existing visibility specifier. -// -// ``` -// <|>fn frobnicate() {} -// ``` -// -> -// ``` -// pub(crate) fn frobnicate() {} -// ``` -pub(crate) fn change_visibility(ctx: AssistCtx) -> Option { - if let Some(vis) = ctx.find_node_at_offset::() { - return change_vis(ctx, vis); - } - add_vis(ctx) -} - -fn add_vis(ctx: AssistCtx) -> Option { - let item_keyword = ctx.token_at_offset().find(|leaf| match leaf.kind() { - T![fn] | T![mod] | T![struct] | T![enum] | T![trait] => true, - _ => false, - }); - - let (offset, target) = 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), keyword.text_range()) - } else { - let ident = ctx.token_at_offset().find(|leaf| leaf.kind() == IDENT)?; - let field = ident.parent().ancestors().find_map(ast::RecordFieldDef::cast)?; - if field.name()?.syntax().text_range() != ident.text_range() && field.visibility().is_some() - { - return None; - } - (vis_offset(field.syntax()), ident.text_range()) - }; - - ctx.add_assist(AssistId("change_visibility"), "Change visibility to pub(crate)", |edit| { - edit.target(target); - edit.insert(offset, "pub(crate) "); - edit.set_cursor(offset); - }) -} - -fn vis_offset(node: &SyntaxNode) -> TextUnit { - node.children_with_tokens() - .skip_while(|it| match it.kind() { - WHITESPACE | COMMENT | ATTR => true, - _ => false, - }) - .next() - .map(|it| it.text_range().start()) - .unwrap_or_else(|| node.text_range().start()) -} - -fn change_vis(ctx: AssistCtx, vis: ast::Visibility) -> Option { - if vis.syntax().text() == "pub" { - return ctx.add_assist( - AssistId("change_visibility"), - "Change Visibility to pub(crate)", - |edit| { - edit.target(vis.syntax().text_range()); - edit.replace(vis.syntax().text_range(), "pub(crate)"); - edit.set_cursor(vis.syntax().text_range().start()) - }, - ); - } - if vis.syntax().text() == "pub(crate)" { - return ctx.add_assist(AssistId("change_visibility"), "Change visibility to pub", |edit| { - edit.target(vis.syntax().text_range()); - edit.replace(vis.syntax().text_range(), "pub"); - edit.set_cursor(vis.syntax().text_range().start()); - }); - } - None -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::helpers::{check_assist, check_assist_target}; - - #[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; - ", - ) - } - - #[test] - fn change_visibility_target() { - check_assist_target(change_visibility, "<|>fn foo() {}", "fn"); - check_assist_target(change_visibility, "pub(crate)<|> fn foo() {}", "pub(crate)"); - check_assist_target(change_visibility, "struct S { <|>field: u32 }", "field"); - } -} diff --git a/crates/ra_assists/src/assists/early_return.rs b/crates/ra_assists/src/assists/early_return.rs deleted file mode 100644 index 8f30dc586..000000000 --- a/crates/ra_assists/src/assists/early_return.rs +++ /dev/null @@ -1,505 +0,0 @@ -use std::{iter::once, ops::RangeInclusive}; - -use ra_syntax::{ - algo::replace_children, - ast::{self, edit::IndentLevel, make, Block, Pat::TupleStructPat}, - AstNode, - SyntaxKind::{FN_DEF, LOOP_EXPR, L_CURLY, R_CURLY, WHILE_EXPR, WHITESPACE}, - SyntaxNode, -}; - -use crate::{ - assist_ctx::{Assist, AssistCtx}, - assists::invert_if::invert_boolean_expression, - AssistId, -}; - -// Assist: convert_to_guarded_return -// -// Replace a large conditional with a guarded return. -// -// ``` -// fn main() { -// <|>if cond { -// foo(); -// bar(); -// } -// } -// ``` -// -> -// ``` -// fn main() { -// if !cond { -// return; -// } -// foo(); -// bar(); -// } -// ``` -pub(crate) fn convert_to_guarded_return(ctx: AssistCtx) -> Option { - let if_expr: ast::IfExpr = ctx.find_node_at_offset()?; - if if_expr.else_branch().is_some() { - return None; - } - - let cond = if_expr.condition()?; - - // Check if there is an IfLet that we can handle. - let if_let_pat = match cond.pat() { - None => None, // No IfLet, supported. - Some(TupleStructPat(pat)) if pat.args().count() == 1 => { - let path = pat.path()?; - match path.qualifier() { - None => { - let bound_ident = pat.args().next().unwrap(); - Some((path, bound_ident)) - } - Some(_) => return None, - } - } - Some(_) => return None, // Unsupported IfLet. - }; - - let cond_expr = cond.expr()?; - let then_block = if_expr.then_branch()?.block()?; - - let parent_block = if_expr.syntax().parent()?.ancestors().find_map(ast::Block::cast)?; - - if parent_block.expr()? != if_expr.clone().into() { - return None; - } - - // check for early return and continue - let first_in_then_block = then_block.syntax().first_child()?; - if ast::ReturnExpr::can_cast(first_in_then_block.kind()) - || ast::ContinueExpr::can_cast(first_in_then_block.kind()) - || first_in_then_block - .children() - .any(|x| ast::ReturnExpr::can_cast(x.kind()) || ast::ContinueExpr::can_cast(x.kind())) - { - return None; - } - - let parent_container = parent_block.syntax().parent()?.parent()?; - - let early_expression: ast::Expr = match parent_container.kind() { - WHILE_EXPR | LOOP_EXPR => make::expr_continue(), - FN_DEF => make::expr_return(), - _ => return None, - }; - - if then_block.syntax().first_child_or_token().map(|t| t.kind() == L_CURLY).is_none() { - return None; - } - - then_block.syntax().last_child_or_token().filter(|t| t.kind() == R_CURLY)?; - let cursor_position = ctx.frange.range.start(); - - ctx.add_assist(AssistId("convert_to_guarded_return"), "Convert to guarded return", |edit| { - let if_indent_level = IndentLevel::from_node(&if_expr.syntax()); - let new_block = match if_let_pat { - None => { - // If. - let new_expr = { - let then_branch = - make::block_expr(once(make::expr_stmt(early_expression).into()), None); - let cond = invert_boolean_expression(cond_expr); - let e = make::expr_if(cond, then_branch); - if_indent_level.increase_indent(e) - }; - replace(new_expr.syntax(), &then_block, &parent_block, &if_expr) - } - Some((path, bound_ident)) => { - // If-let. - let match_expr = { - let happy_arm = make::match_arm( - once( - make::tuple_struct_pat( - path, - once(make::bind_pat(make::name("it")).into()), - ) - .into(), - ), - make::expr_path(make::path_from_name_ref(make::name_ref("it"))), - ); - - let sad_arm = make::match_arm( - // FIXME: would be cool to use `None` or `Err(_)` if appropriate - once(make::placeholder_pat().into()), - early_expression, - ); - - make::expr_match(cond_expr, make::match_arm_list(vec![happy_arm, sad_arm])) - }; - - let let_stmt = make::let_stmt( - make::bind_pat(make::name(&bound_ident.syntax().to_string())).into(), - Some(match_expr), - ); - let let_stmt = if_indent_level.increase_indent(let_stmt); - replace(let_stmt.syntax(), &then_block, &parent_block, &if_expr) - } - }; - edit.target(if_expr.syntax().text_range()); - edit.replace_ast(parent_block, ast::Block::cast(new_block).unwrap()); - edit.set_cursor(cursor_position); - - fn replace( - new_expr: &SyntaxNode, - then_block: &Block, - parent_block: &Block, - if_expr: &ast::IfExpr, - ) -> SyntaxNode { - let then_block_items = IndentLevel::from(1).decrease_indent(then_block.clone()); - let end_of_then = then_block_items.syntax().last_child_or_token().unwrap(); - let end_of_then = - if end_of_then.prev_sibling_or_token().map(|n| n.kind()) == Some(WHITESPACE) { - end_of_then.prev_sibling_or_token().unwrap() - } else { - end_of_then - }; - let mut then_statements = new_expr.children_with_tokens().chain( - then_block_items - .syntax() - .children_with_tokens() - .skip(1) - .take_while(|i| *i != end_of_then), - ); - replace_children( - &parent_block.syntax(), - RangeInclusive::new( - if_expr.clone().syntax().clone().into(), - if_expr.syntax().clone().into(), - ), - &mut then_statements, - ) - } - }) -} - -#[cfg(test)] -mod tests { - use crate::helpers::{check_assist, check_assist_not_applicable}; - - use super::*; - - #[test] - fn convert_inside_fn() { - check_assist( - convert_to_guarded_return, - r#" - fn main() { - bar(); - if<|> true { - foo(); - - //comment - bar(); - } - } - "#, - r#" - fn main() { - bar(); - if<|> !true { - return; - } - foo(); - - //comment - bar(); - } - "#, - ); - } - - #[test] - fn convert_let_inside_fn() { - check_assist( - convert_to_guarded_return, - r#" - fn main(n: Option) { - bar(); - if<|> let Some(n) = n { - foo(n); - - //comment - bar(); - } - } - "#, - r#" - fn main(n: Option) { - bar(); - le<|>t n = match n { - Some(it) => it, - _ => return, - }; - foo(n); - - //comment - bar(); - } - "#, - ); - } - - #[test] - fn convert_if_let_result() { - check_assist( - convert_to_guarded_return, - r#" - fn main() { - if<|> let Ok(x) = Err(92) { - foo(x); - } - } - "#, - r#" - fn main() { - le<|>t x = match Err(92) { - Ok(it) => it, - _ => return, - }; - foo(x); - } - "#, - ); - } - - #[test] - fn convert_let_ok_inside_fn() { - check_assist( - convert_to_guarded_return, - r#" - fn main(n: Option) { - bar(); - if<|> let Ok(n) = n { - foo(n); - - //comment - bar(); - } - } - "#, - r#" - fn main(n: Option) { - bar(); - le<|>t n = match n { - Ok(it) => it, - _ => return, - }; - foo(n); - - //comment - bar(); - } - "#, - ); - } - - #[test] - fn convert_inside_while() { - check_assist( - convert_to_guarded_return, - r#" - fn main() { - while true { - if<|> true { - foo(); - bar(); - } - } - } - "#, - r#" - fn main() { - while true { - if<|> !true { - continue; - } - foo(); - bar(); - } - } - "#, - ); - } - - #[test] - fn convert_let_inside_while() { - check_assist( - convert_to_guarded_return, - r#" - fn main() { - while true { - if<|> let Some(n) = n { - foo(n); - bar(); - } - } - } - "#, - r#" - fn main() { - while true { - le<|>t n = match n { - Some(it) => it, - _ => continue, - }; - foo(n); - bar(); - } - } - "#, - ); - } - - #[test] - fn convert_inside_loop() { - check_assist( - convert_to_guarded_return, - r#" - fn main() { - loop { - if<|> true { - foo(); - bar(); - } - } - } - "#, - r#" - fn main() { - loop { - if<|> !true { - continue; - } - foo(); - bar(); - } - } - "#, - ); - } - - #[test] - fn convert_let_inside_loop() { - check_assist( - convert_to_guarded_return, - r#" - fn main() { - loop { - if<|> let Some(n) = n { - foo(n); - bar(); - } - } - } - "#, - r#" - fn main() { - loop { - le<|>t n = match n { - Some(it) => it, - _ => continue, - }; - foo(n); - bar(); - } - } - "#, - ); - } - - #[test] - fn ignore_already_converted_if() { - check_assist_not_applicable( - convert_to_guarded_return, - r#" - fn main() { - if<|> true { - return; - } - } - "#, - ); - } - - #[test] - fn ignore_already_converted_loop() { - check_assist_not_applicable( - convert_to_guarded_return, - r#" - fn main() { - loop { - if<|> true { - continue; - } - } - } - "#, - ); - } - - #[test] - fn ignore_return() { - check_assist_not_applicable( - convert_to_guarded_return, - r#" - fn main() { - if<|> true { - return - } - } - "#, - ); - } - - #[test] - fn ignore_else_branch() { - check_assist_not_applicable( - convert_to_guarded_return, - r#" - fn main() { - if<|> true { - foo(); - } else { - bar() - } - } - "#, - ); - } - - #[test] - fn ignore_statements_aftert_if() { - check_assist_not_applicable( - convert_to_guarded_return, - r#" - fn main() { - if<|> true { - foo(); - } - bar(); - } - "#, - ); - } - - #[test] - fn ignore_statements_inside_if() { - check_assist_not_applicable( - convert_to_guarded_return, - r#" - fn main() { - if false { - if<|> true { - foo(); - } - } - } - "#, - ); - } -} diff --git a/crates/ra_assists/src/assists/fill_match_arms.rs b/crates/ra_assists/src/assists/fill_match_arms.rs deleted file mode 100644 index 0908fc246..000000000 --- a/crates/ra_assists/src/assists/fill_match_arms.rs +++ /dev/null @@ -1,290 +0,0 @@ -//! FIXME: write short doc here - -use std::iter; - -use hir::{db::HirDatabase, Adt, HasSource}; -use ra_syntax::ast::{self, edit::IndentLevel, make, AstNode, NameOwner}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: fill_match_arms -// -// Adds missing clauses to a `match` expression. -// -// ``` -// enum Action { Move { distance: u32 }, Stop } -// -// fn handle(action: Action) { -// match action { -// <|> -// } -// } -// ``` -// -> -// ``` -// enum Action { Move { distance: u32 }, Stop } -// -// fn handle(action: Action) { -// match action { -// Action::Move { distance } => (), -// Action::Stop => (), -// } -// } -// ``` -pub(crate) fn fill_match_arms(ctx: AssistCtx) -> Option { - let match_expr = ctx.find_node_at_offset::()?; - let match_arm_list = match_expr.match_arm_list()?; - - // We already have some match arms, so we don't provide any assists. - // Unless if there is only one trivial match arm possibly created - // by match postfix complete. Trivial match arm is the catch all arm. - let mut existing_arms = match_arm_list.arms(); - if let Some(arm) = existing_arms.next() { - if !is_trivial(&arm) || existing_arms.next().is_some() { - return None; - } - }; - - let expr = match_expr.expr()?; - let (enum_def, module) = { - let analyzer = ctx.source_analyzer(expr.syntax(), None); - (resolve_enum_def(ctx.db, &analyzer, &expr)?, analyzer.module()?) - }; - let variants = enum_def.variants(ctx.db); - if variants.is_empty() { - return None; - } - - let db = ctx.db; - - ctx.add_assist(AssistId("fill_match_arms"), "Fill match arms", |edit| { - let indent_level = IndentLevel::from_node(match_arm_list.syntax()); - - let new_arm_list = { - let arms = variants - .into_iter() - .filter_map(|variant| build_pat(db, module, variant)) - .map(|pat| make::match_arm(iter::once(pat), make::expr_unit())); - indent_level.increase_indent(make::match_arm_list(arms)) - }; - - edit.target(match_expr.syntax().text_range()); - edit.set_cursor(expr.syntax().text_range().start()); - edit.replace_ast(match_arm_list, new_arm_list); - }) -} - -fn is_trivial(arm: &ast::MatchArm) -> bool { - arm.pats().any(|pat| match pat { - ast::Pat::PlaceholderPat(..) => true, - _ => false, - }) -} - -fn resolve_enum_def( - db: &impl HirDatabase, - analyzer: &hir::SourceAnalyzer, - expr: &ast::Expr, -) -> Option { - let expr_ty = analyzer.type_of(db, &expr)?; - - let result = expr_ty.autoderef(db).find_map(|ty| match ty.as_adt() { - Some(Adt::Enum(e)) => Some(e), - _ => None, - }); - result -} - -fn build_pat( - db: &impl HirDatabase, - module: hir::Module, - var: hir::EnumVariant, -) -> Option { - let path = crate::ast_transform::path_to_ast(module.find_use_path(db, var.into())?); - - // FIXME: use HIR for this; it doesn't currently expose struct vs. tuple vs. unit variants though - let pat: ast::Pat = match var.source(db).value.kind() { - ast::StructKind::Tuple(field_list) => { - let pats = - iter::repeat(make::placeholder_pat().into()).take(field_list.fields().count()); - make::tuple_struct_pat(path, pats).into() - } - ast::StructKind::Record(field_list) => { - let pats = field_list.fields().map(|f| make::bind_pat(f.name().unwrap()).into()); - make::record_pat(path, pats).into() - } - ast::StructKind::Unit => make::path_pat(path), - }; - - Some(pat) -} - -#[cfg(test)] -mod tests { - use crate::helpers::{check_assist, check_assist_target}; - - 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 test_fill_match_arm_refs() { - check_assist( - fill_match_arms, - r#" - enum A { - As, - } - - fn foo(a: &A) { - match a<|> { - } - } - "#, - r#" - enum A { - As, - } - - fn foo(a: &A) { - match <|>a { - A::As => (), - } - } - "#, - ); - - check_assist( - fill_match_arms, - r#" - enum A { - Es{ x: usize, y: usize } - } - - fn foo(a: &mut A) { - match a<|> { - } - } - "#, - r#" - enum A { - Es{ x: usize, y: usize } - } - - fn foo(a: &mut A) { - match <|>a { - A::Es { x, y } => (), - } - } - "#, - ); - } - - #[test] - fn fill_match_arms_target() { - check_assist_target( - fill_match_arms, - r#" - enum E { X, Y } - - fn main() { - match E::X<|> {} - } - "#, - "match E::X {}", - ); - } - - #[test] - fn fill_match_arms_trivial_arm() { - 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 => (), - } - } - "#, - ); - } - - #[test] - fn fill_match_arms_qualifies_path() { - check_assist( - fill_match_arms, - r#" - mod foo { pub enum E { X, Y } } - use foo::E::X; - - fn main() { - match X { - <|> - } - } - "#, - r#" - mod foo { pub enum E { X, Y } } - use foo::E::X; - - fn main() { - match <|>X { - X => (), - foo::E::Y => (), - } - } - "#, - ); - } -} diff --git a/crates/ra_assists/src/assists/flip_binexpr.rs b/crates/ra_assists/src/assists/flip_binexpr.rs deleted file mode 100644 index bfcc09e90..000000000 --- a/crates/ra_assists/src/assists/flip_binexpr.rs +++ /dev/null @@ -1,142 +0,0 @@ -use ra_syntax::ast::{AstNode, BinExpr, BinOp}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: flip_binexpr -// -// Flips operands of a binary expression. -// -// ``` -// fn main() { -// let _ = 90 +<|> 2; -// } -// ``` -// -> -// ``` -// fn main() { -// let _ = 2 + 90; -// } -// ``` -pub(crate) fn flip_binexpr(ctx: AssistCtx) -> Option { - let expr = ctx.find_node_at_offset::()?; - let lhs = expr.lhs()?.syntax().clone(); - let rhs = expr.rhs()?.syntax().clone(); - let op_range = expr.op_token()?.text_range(); - // The assist should be applied only if the cursor is on the operator - let cursor_in_range = ctx.frange.range.is_subrange(&op_range); - if !cursor_in_range { - return None; - } - let action: FlipAction = expr.op_kind()?.into(); - // The assist should not be applied for certain operators - if let FlipAction::DontFlip = action { - return None; - } - - ctx.add_assist(AssistId("flip_binexpr"), "Flip binary expression", |edit| { - edit.target(op_range); - if let FlipAction::FlipAndReplaceOp(new_op) = action { - edit.replace(op_range, new_op); - } - edit.replace(lhs.text_range(), rhs.text()); - edit.replace(rhs.text_range(), lhs.text()); - }) -} - -enum FlipAction { - // Flip the expression - Flip, - // Flip the expression and replace the operator with this string - FlipAndReplaceOp(&'static str), - // Do not flip the expression - DontFlip, -} - -impl From for FlipAction { - fn from(op_kind: BinOp) -> Self { - match op_kind { - kind if kind.is_assignment() => FlipAction::DontFlip, - BinOp::GreaterTest => FlipAction::FlipAndReplaceOp("<"), - BinOp::GreaterEqualTest => FlipAction::FlipAndReplaceOp("<="), - BinOp::LesserTest => FlipAction::FlipAndReplaceOp(">"), - BinOp::LesserEqualTest => FlipAction::FlipAndReplaceOp(">="), - _ => FlipAction::Flip, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; - - #[test] - fn flip_binexpr_target_is_the_op() { - check_assist_target(flip_binexpr, "fn f() { let res = 1 ==<|> 2; }", "==") - } - - #[test] - fn flip_binexpr_not_applicable_for_assignment() { - check_assist_not_applicable(flip_binexpr, "fn f() { let mut _x = 1; _x +=<|> 2 }") - } - - #[test] - fn flip_binexpr_works_for_eq() { - check_assist( - flip_binexpr, - "fn f() { let res = 1 ==<|> 2; }", - "fn f() { let res = 2 ==<|> 1; }", - ) - } - - #[test] - fn flip_binexpr_works_for_gt() { - check_assist( - flip_binexpr, - "fn f() { let res = 1 ><|> 2; }", - "fn f() { let res = 2 <<|> 1; }", - ) - } - - #[test] - fn flip_binexpr_works_for_lteq() { - check_assist( - flip_binexpr, - "fn f() { let res = 1 <=<|> 2; }", - "fn f() { let res = 2 >=<|> 1; }", - ) - } - - #[test] - fn flip_binexpr_works_for_complex_expr() { - check_assist( - flip_binexpr, - "fn f() { let res = (1 + 1) ==<|> (2 + 2); }", - "fn f() { let res = (2 + 2) ==<|> (1 + 1); }", - ) - } - - #[test] - fn flip_binexpr_works_inside_match() { - check_assist( - flip_binexpr, - r#" - fn dyn_eq(&self, other: &dyn Diagnostic) -> bool { - match other.downcast_ref::() { - None => false, - Some(it) => it ==<|> self, - } - } - "#, - r#" - fn dyn_eq(&self, other: &dyn Diagnostic) -> bool { - match other.downcast_ref::() { - None => false, - Some(it) => self ==<|> it, - } - } - "#, - ) - } -} diff --git a/crates/ra_assists/src/assists/flip_comma.rs b/crates/ra_assists/src/assists/flip_comma.rs deleted file mode 100644 index 1dacf29f8..000000000 --- a/crates/ra_assists/src/assists/flip_comma.rs +++ /dev/null @@ -1,80 +0,0 @@ -use ra_syntax::{algo::non_trivia_sibling, Direction, T}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: flip_comma -// -// Flips two comma-separated items. -// -// ``` -// fn main() { -// ((1, 2),<|> (3, 4)); -// } -// ``` -// -> -// ``` -// fn main() { -// ((3, 4), (1, 2)); -// } -// ``` -pub(crate) fn flip_comma(ctx: AssistCtx) -> Option { - let comma = ctx.find_token_at_offset(T![,])?; - let prev = non_trivia_sibling(comma.clone().into(), Direction::Prev)?; - let next = non_trivia_sibling(comma.clone().into(), Direction::Next)?; - - // Don't apply a "flip" in case of a last comma - // that typically comes before punctuation - if next.kind().is_punct() { - return None; - } - - ctx.add_assist(AssistId("flip_comma"), "Flip comma", |edit| { - edit.target(comma.text_range()); - edit.replace(prev.text_range(), next.to_string()); - edit.replace(next.text_range(), prev.to_string()); - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::helpers::{check_assist, check_assist_target}; - - #[test] - fn flip_comma_works_for_function_parameters() { - check_assist( - flip_comma, - "fn foo(x: i32,<|> y: Result<(), ()>) {}", - "fn foo(y: Result<(), ()>,<|> x: i32) {}", - ) - } - - #[test] - fn flip_comma_target() { - check_assist_target(flip_comma, "fn foo(x: i32,<|> y: Result<(), ()>) {}", ",") - } - - #[test] - #[should_panic] - fn flip_comma_before_punct() { - // See https://github.com/rust-analyzer/rust-analyzer/issues/1619 - // "Flip comma" assist shouldn't be applicable to the last comma in enum or struct - // declaration body. - check_assist_target( - flip_comma, - "pub enum Test { \ - A,<|> \ - }", - ",", - ); - - check_assist_target( - flip_comma, - "pub struct Test { \ - foo: usize,<|> \ - }", - ",", - ); - } -} diff --git a/crates/ra_assists/src/assists/flip_trait_bound.rs b/crates/ra_assists/src/assists/flip_trait_bound.rs deleted file mode 100644 index f56769624..000000000 --- a/crates/ra_assists/src/assists/flip_trait_bound.rs +++ /dev/null @@ -1,116 +0,0 @@ -use ra_syntax::{ - algo::non_trivia_sibling, - ast::{self, AstNode}, - Direction, T, -}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: flip_trait_bound -// -// Flips two trait bounds. -// -// ``` -// fn foo Copy>() { } -// ``` -// -> -// ``` -// fn foo() { } -// ``` -pub(crate) fn flip_trait_bound(ctx: AssistCtx) -> Option { - // We want to replicate the behavior of `flip_binexpr` by only suggesting - // the assist when the cursor is on a `+` - let plus = ctx.find_token_at_offset(T![+])?; - - // Make sure we're in a `TypeBoundList` - if ast::TypeBoundList::cast(plus.parent()).is_none() { - return None; - } - - let (before, after) = ( - non_trivia_sibling(plus.clone().into(), Direction::Prev)?, - non_trivia_sibling(plus.clone().into(), Direction::Next)?, - ); - - ctx.add_assist(AssistId("flip_trait_bound"), "Flip trait bounds", |edit| { - edit.target(plus.text_range()); - edit.replace(before.text_range(), after.to_string()); - edit.replace(after.text_range(), before.to_string()); - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; - - #[test] - fn flip_trait_bound_assist_available() { - check_assist_target(flip_trait_bound, "struct S where T: A <|>+ B + C { }", "+") - } - - #[test] - fn flip_trait_bound_not_applicable_for_single_trait_bound() { - check_assist_not_applicable(flip_trait_bound, "struct S where T: <|>A { }") - } - - #[test] - fn flip_trait_bound_works_for_struct() { - check_assist( - flip_trait_bound, - "struct S where T: A <|>+ B { }", - "struct S where T: B <|>+ A { }", - ) - } - - #[test] - fn flip_trait_bound_works_for_trait_impl() { - check_assist( - flip_trait_bound, - "impl X for S where T: A +<|> B { }", - "impl X for S where T: B +<|> A { }", - ) - } - - #[test] - fn flip_trait_bound_works_for_fn() { - check_assist(flip_trait_bound, "fn f+ B>(t: T) { }", "fn f+ A>(t: T) { }") - } - - #[test] - fn flip_trait_bound_works_for_fn_where_clause() { - check_assist( - flip_trait_bound, - "fn f(t: T) where T: A +<|> B { }", - "fn f(t: T) where T: B +<|> A { }", - ) - } - - #[test] - fn flip_trait_bound_works_for_lifetime() { - check_assist( - flip_trait_bound, - "fn f(t: T) where T: A <|>+ 'static { }", - "fn f(t: T) where T: 'static <|>+ A { }", - ) - } - - #[test] - fn flip_trait_bound_works_for_complex_bounds() { - check_assist( - flip_trait_bound, - "struct S where T: A <|>+ b_mod::B + C { }", - "struct S where T: b_mod::B <|>+ A + C { }", - ) - } - - #[test] - fn flip_trait_bound_works_for_long_bounds() { - check_assist( - flip_trait_bound, - "struct S where T: A + B + C + D + E + F +<|> G + H + I + J { }", - "struct S where T: A + B + C + D + E + G +<|> F + H + I + J { }", - ) - } -} diff --git a/crates/ra_assists/src/assists/inline_local_variable.rs b/crates/ra_assists/src/assists/inline_local_variable.rs deleted file mode 100644 index 91b588243..000000000 --- a/crates/ra_assists/src/assists/inline_local_variable.rs +++ /dev/null @@ -1,662 +0,0 @@ -use ra_syntax::{ - ast::{self, AstNode, AstToken}, - TextRange, -}; - -use crate::assist_ctx::ActionBuilder; -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: inline_local_variable -// -// Inlines local variable. -// -// ``` -// fn main() { -// let x<|> = 1 + 2; -// x * 4; -// } -// ``` -// -> -// ``` -// fn main() { -// (1 + 2) * 4; -// } -// ``` -pub(crate) fn inline_local_variable(ctx: AssistCtx) -> Option { - let let_stmt = ctx.find_node_at_offset::()?; - let bind_pat = match let_stmt.pat()? { - ast::Pat::BindPat(pat) => pat, - _ => return None, - }; - if bind_pat.is_mutable() { - return None; - } - let initializer_expr = let_stmt.initializer()?; - let delete_range = if let Some(whitespace) = let_stmt - .syntax() - .next_sibling_or_token() - .and_then(|it| ast::Whitespace::cast(it.as_token()?.clone())) - { - TextRange::from_to( - let_stmt.syntax().text_range().start(), - whitespace.syntax().text_range().end(), - ) - } else { - let_stmt.syntax().text_range() - }; - let analyzer = ctx.source_analyzer(bind_pat.syntax(), None); - let refs = analyzer.find_all_refs(&bind_pat); - if refs.is_empty() { - return None; - }; - - let mut wrap_in_parens = vec![true; refs.len()]; - - for (i, desc) in refs.iter().enumerate() { - let usage_node = - ctx.covering_node_for_range(desc.range).ancestors().find_map(ast::PathExpr::cast)?; - let usage_parent_option = usage_node.syntax().parent().and_then(ast::Expr::cast); - let usage_parent = match usage_parent_option { - Some(u) => u, - None => { - wrap_in_parens[i] = false; - continue; - } - }; - - wrap_in_parens[i] = match (&initializer_expr, usage_parent) { - (ast::Expr::CallExpr(_), _) - | (ast::Expr::IndexExpr(_), _) - | (ast::Expr::MethodCallExpr(_), _) - | (ast::Expr::FieldExpr(_), _) - | (ast::Expr::TryExpr(_), _) - | (ast::Expr::RefExpr(_), _) - | (ast::Expr::Literal(_), _) - | (ast::Expr::TupleExpr(_), _) - | (ast::Expr::ArrayExpr(_), _) - | (ast::Expr::ParenExpr(_), _) - | (ast::Expr::PathExpr(_), _) - | (ast::Expr::BlockExpr(_), _) - | (_, ast::Expr::CallExpr(_)) - | (_, ast::Expr::TupleExpr(_)) - | (_, ast::Expr::ArrayExpr(_)) - | (_, ast::Expr::ParenExpr(_)) - | (_, ast::Expr::ForExpr(_)) - | (_, ast::Expr::WhileExpr(_)) - | (_, ast::Expr::BreakExpr(_)) - | (_, ast::Expr::ReturnExpr(_)) - | (_, ast::Expr::MatchExpr(_)) => false, - _ => true, - }; - } - - let init_str = initializer_expr.syntax().text().to_string(); - let init_in_paren = format!("({})", &init_str); - - ctx.add_assist( - AssistId("inline_local_variable"), - "Inline variable", - move |edit: &mut ActionBuilder| { - edit.delete(delete_range); - for (desc, should_wrap) in refs.iter().zip(wrap_in_parens) { - if should_wrap { - edit.replace(desc.range, init_in_paren.clone()) - } else { - edit.replace(desc.range, init_str.clone()) - } - } - edit.set_cursor(delete_range.start()) - }, - ) -} - -#[cfg(test)] -mod tests { - use crate::helpers::{check_assist, check_assist_not_applicable}; - - use super::*; - - #[test] - fn test_inline_let_bind_literal_expr() { - check_assist( - inline_local_variable, - " -fn bar(a: usize) {} -fn foo() { - let a<|> = 1; - a + 1; - if a > 10 { - } - - while a > 10 { - - } - let b = a * 10; - bar(a); -}", - " -fn bar(a: usize) {} -fn foo() { - <|>1 + 1; - if 1 > 10 { - } - - while 1 > 10 { - - } - let b = 1 * 10; - bar(1); -}", - ); - } - - #[test] - fn test_inline_let_bind_bin_expr() { - check_assist( - inline_local_variable, - " -fn bar(a: usize) {} -fn foo() { - let a<|> = 1 + 1; - a + 1; - if a > 10 { - } - - while a > 10 { - - } - let b = a * 10; - bar(a); -}", - " -fn bar(a: usize) {} -fn foo() { - <|>(1 + 1) + 1; - if (1 + 1) > 10 { - } - - while (1 + 1) > 10 { - - } - let b = (1 + 1) * 10; - bar(1 + 1); -}", - ); - } - - #[test] - fn test_inline_let_bind_function_call_expr() { - check_assist( - inline_local_variable, - " -fn bar(a: usize) {} -fn foo() { - let a<|> = bar(1); - a + 1; - if a > 10 { - } - - while a > 10 { - - } - let b = a * 10; - bar(a); -}", - " -fn bar(a: usize) {} -fn foo() { - <|>bar(1) + 1; - if bar(1) > 10 { - } - - while bar(1) > 10 { - - } - let b = bar(1) * 10; - bar(bar(1)); -}", - ); - } - - #[test] - fn test_inline_let_bind_cast_expr() { - check_assist( - inline_local_variable, - " -fn bar(a: usize): usize { a } -fn foo() { - let a<|> = bar(1) as u64; - a + 1; - if a > 10 { - } - - while a > 10 { - - } - let b = a * 10; - bar(a); -}", - " -fn bar(a: usize): usize { a } -fn foo() { - <|>(bar(1) as u64) + 1; - if (bar(1) as u64) > 10 { - } - - while (bar(1) as u64) > 10 { - - } - let b = (bar(1) as u64) * 10; - bar(bar(1) as u64); -}", - ); - } - - #[test] - fn test_inline_let_bind_block_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = { 10 + 1 }; - a + 1; - if a > 10 { - } - - while a > 10 { - - } - let b = a * 10; - bar(a); -}", - " -fn foo() { - <|>{ 10 + 1 } + 1; - if { 10 + 1 } > 10 { - } - - while { 10 + 1 } > 10 { - - } - let b = { 10 + 1 } * 10; - bar({ 10 + 1 }); -}", - ); - } - - #[test] - fn test_inline_let_bind_paren_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = ( 10 + 1 ); - a + 1; - if a > 10 { - } - - while a > 10 { - - } - let b = a * 10; - bar(a); -}", - " -fn foo() { - <|>( 10 + 1 ) + 1; - if ( 10 + 1 ) > 10 { - } - - while ( 10 + 1 ) > 10 { - - } - let b = ( 10 + 1 ) * 10; - bar(( 10 + 1 )); -}", - ); - } - - #[test] - fn test_not_inline_mut_variable() { - check_assist_not_applicable( - inline_local_variable, - " -fn foo() { - let mut a<|> = 1 + 1; - a + 1; -}", - ); - } - - #[test] - fn test_call_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = bar(10 + 1); - let b = a * 10; - let c = a as usize; -}", - " -fn foo() { - <|>let b = bar(10 + 1) * 10; - let c = bar(10 + 1) as usize; -}", - ); - } - - #[test] - fn test_index_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let x = vec![1, 2, 3]; - let a<|> = x[0]; - let b = a * 10; - let c = a as usize; -}", - " -fn foo() { - let x = vec![1, 2, 3]; - <|>let b = x[0] * 10; - let c = x[0] as usize; -}", - ); - } - - #[test] - fn test_method_call_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let bar = vec![1]; - let a<|> = bar.len(); - let b = a * 10; - let c = a as usize; -}", - " -fn foo() { - let bar = vec![1]; - <|>let b = bar.len() * 10; - let c = bar.len() as usize; -}", - ); - } - - #[test] - fn test_field_expr() { - check_assist( - inline_local_variable, - " -struct Bar { - foo: usize -} - -fn foo() { - let bar = Bar { foo: 1 }; - let a<|> = bar.foo; - let b = a * 10; - let c = a as usize; -}", - " -struct Bar { - foo: usize -} - -fn foo() { - let bar = Bar { foo: 1 }; - <|>let b = bar.foo * 10; - let c = bar.foo as usize; -}", - ); - } - - #[test] - fn test_try_expr() { - check_assist( - inline_local_variable, - " -fn foo() -> Option { - let bar = Some(1); - let a<|> = bar?; - let b = a * 10; - let c = a as usize; - None -}", - " -fn foo() -> Option { - let bar = Some(1); - <|>let b = bar? * 10; - let c = bar? as usize; - None -}", - ); - } - - #[test] - fn test_ref_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let bar = 10; - let a<|> = &bar; - let b = a * 10; -}", - " -fn foo() { - let bar = 10; - <|>let b = &bar * 10; -}", - ); - } - - #[test] - fn test_tuple_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = (10, 20); - let b = a[0]; -}", - " -fn foo() { - <|>let b = (10, 20)[0]; -}", - ); - } - - #[test] - fn test_array_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = [1, 2, 3]; - let b = a.len(); -}", - " -fn foo() { - <|>let b = [1, 2, 3].len(); -}", - ); - } - - #[test] - fn test_paren() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = (10 + 20); - let b = a * 10; - let c = a as usize; -}", - " -fn foo() { - <|>let b = (10 + 20) * 10; - let c = (10 + 20) as usize; -}", - ); - } - - #[test] - fn test_path_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let d = 10; - let a<|> = d; - let b = a * 10; - let c = a as usize; -}", - " -fn foo() { - let d = 10; - <|>let b = d * 10; - let c = d as usize; -}", - ); - } - - #[test] - fn test_block_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = { 10 }; - let b = a * 10; - let c = a as usize; -}", - " -fn foo() { - <|>let b = { 10 } * 10; - let c = { 10 } as usize; -}", - ); - } - - #[test] - fn test_used_in_different_expr1() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = 10 + 20; - let b = a * 10; - let c = (a, 20); - let d = [a, 10]; - let e = (a); -}", - " -fn foo() { - <|>let b = (10 + 20) * 10; - let c = (10 + 20, 20); - let d = [10 + 20, 10]; - let e = (10 + 20); -}", - ); - } - - #[test] - fn test_used_in_for_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = vec![10, 20]; - for i in a {} -}", - " -fn foo() { - <|>for i in vec![10, 20] {} -}", - ); - } - - #[test] - fn test_used_in_while_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = 1 > 0; - while a {} -}", - " -fn foo() { - <|>while 1 > 0 {} -}", - ); - } - - #[test] - fn test_used_in_break_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = 1 + 1; - loop { - break a; - } -}", - " -fn foo() { - <|>loop { - break 1 + 1; - } -}", - ); - } - - #[test] - fn test_used_in_return_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = 1 > 0; - return a; -}", - " -fn foo() { - <|>return 1 > 0; -}", - ); - } - - #[test] - fn test_used_in_match_expr() { - check_assist( - inline_local_variable, - " -fn foo() { - let a<|> = 1 > 0; - match a {} -}", - " -fn foo() { - <|>match 1 > 0 {} -}", - ); - } - - #[test] - fn test_not_applicable_if_variable_unused() { - check_assist_not_applicable( - inline_local_variable, - " -fn foo() { - let <|>a = 0; -} - ", - ) - } -} diff --git a/crates/ra_assists/src/assists/introduce_variable.rs b/crates/ra_assists/src/assists/introduce_variable.rs deleted file mode 100644 index 7312ce687..000000000 --- a/crates/ra_assists/src/assists/introduce_variable.rs +++ /dev/null @@ -1,529 +0,0 @@ -use format_buf::format; -use ra_syntax::{ - ast::{self, AstNode}, - SyntaxKind::{ - BLOCK_EXPR, BREAK_EXPR, COMMENT, LAMBDA_EXPR, LOOP_EXPR, MATCH_ARM, PATH_EXPR, RETURN_EXPR, - WHITESPACE, - }, - SyntaxNode, TextUnit, -}; -use test_utils::tested_by; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: introduce_variable -// -// Extracts subexpression into a variable. -// -// ``` -// fn main() { -// <|>(1 + 2)<|> * 4; -// } -// ``` -// -> -// ``` -// fn main() { -// let var_name = (1 + 2); -// var_name * 4; -// } -// ``` -pub(crate) fn introduce_variable(ctx: AssistCtx) -> Option { - if ctx.frange.range.is_empty() { - return None; - } - let node = ctx.covering_element(); - if node.kind() == COMMENT { - tested_by!(introduce_var_in_comment_is_not_applicable); - return None; - } - let expr = node.ancestors().find_map(valid_target_expr)?; - let (anchor_stmt, wrap_in_block) = anchor_stmt(expr.clone())?; - let indent = anchor_stmt.prev_sibling_or_token()?.as_token()?.clone(); - if indent.kind() != WHITESPACE { - return None; - } - ctx.add_assist(AssistId("introduce_variable"), "Extract into 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 ") - }; - format!(buf, "{}", expr.syntax()); - let full_stmt = ast::ExprStmt::cast(anchor_stmt.clone()); - let is_full_stmt = if let Some(expr_stmt) = &full_stmt { - Some(expr.syntax().clone()) == expr_stmt.expr().map(|e| e.syntax().clone()) - } else { - false - }; - if is_full_stmt { - tested_by!(test_introduce_var_expr_stmt); - if !full_stmt.unwrap().has_semi() { - buf.push_str(";"); - } - edit.replace(expr.syntax().text_range(), buf); - } else { - buf.push_str(";"); - - // We want to maintain the indent level, - // but we do not want to duplicate possible - // extra newlines in the indent block - let text = indent.text(); - if text.starts_with('\n') { - buf.push_str("\n"); - buf.push_str(text.trim_start_matches('\n')); - } else { - buf.push_str(text); - } - - edit.target(expr.syntax().text_range()); - edit.replace(expr.syntax().text_range(), "var_name".to_string()); - edit.insert(anchor_stmt.text_range().start(), buf); - if wrap_in_block { - edit.insert(anchor_stmt.text_range().end(), " }"); - } - } - edit.set_cursor(anchor_stmt.text_range().start() + cursor_offset); - }) -} - -/// Check whether 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 { - match node.kind() { - PATH_EXPR | LOOP_EXPR => None, - BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()), - RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()), - BLOCK_EXPR => { - ast::BlockExpr::cast(node).filter(|it| it.is_standalone()).map(ast::Expr::from) - } - _ => 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 lambda or match arm. -fn anchor_stmt(expr: ast::Expr) -> Option<(SyntaxNode, bool)> { - expr.syntax().ancestors().find_map(|node| { - if let Some(expr) = node.parent().and_then(ast::Block::cast).and_then(|it| it.expr()) { - if expr.syntax() == &node { - tested_by!(test_introduce_var_last_expr); - return Some((node, false)); - } - } - - if let Some(parent) = node.parent() { - if parent.kind() == MATCH_ARM || parent.kind() == LAMBDA_EXPR { - return Some((node, true)); - } - } - - if ast::Stmt::cast(node.clone()).is_some() { - return Some((node, false)); - } - - None - }) -} - -#[cfg(test)] -mod tests { - use test_utils::covers; - - use crate::helpers::{ - check_assist_range, check_assist_range_not_applicable, check_assist_range_target, - }; - - use super::*; - - #[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 introduce_var_in_comment_is_not_applicable() { - covers!(introduce_var_in_comment_is_not_applicable); - check_assist_range_not_applicable( - introduce_variable, - "fn main() { 1 + /* <|>comment<|> */ 1; }", - ); - } - - #[test] - fn test_introduce_var_expr_stmt() { - covers!(test_introduce_var_expr_stmt); - check_assist_range( - introduce_variable, - " -fn foo() { - <|>1 + 1<|>; -}", - " -fn foo() { - let <|>var_name = 1 + 1; -}", - ); - 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_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() { - covers!(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) -}", - ); - 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_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_range( - introduce_variable, - " -fn main() { - let o = <|>Some(true)<|>; -} -", - " -fn main() { - let <|>var_name = Some(true); - let o = var_name; -} -", - ); - } - - #[test] - fn test_introduce_var_path_method() { - check_assist_range( - introduce_variable, - " -fn main() { - let v = <|>bar.foo()<|>; -} -", - " -fn main() { - let <|>var_name = bar.foo(); - let v = var_name; -} -", - ); - } - - #[test] - fn test_introduce_var_return() { - check_assist_range( - introduce_variable, - " -fn foo() -> u32 { - <|>return 2 + 2<|>; -} -", - " -fn foo() -> u32 { - let <|>var_name = 2 + 2; - return var_name; -} -", - ); - } - - #[test] - fn test_introduce_var_does_not_add_extra_whitespace() { - check_assist_range( - introduce_variable, - " -fn foo() -> u32 { - - - <|>return 2 + 2<|>; -} -", - " -fn foo() -> u32 { - - - let <|>var_name = 2 + 2; - return var_name; -} -", - ); - - check_assist_range( - introduce_variable, - " -fn foo() -> u32 { - - <|>return 2 + 2<|>; -} -", - " -fn foo() -> u32 { - - let <|>var_name = 2 + 2; - return var_name; -} -", - ); - - check_assist_range( - introduce_variable, - " -fn foo() -> u32 { - let foo = 1; - - // bar - - - <|>return 2 + 2<|>; -} -", - " -fn foo() -> u32 { - let foo = 1; - - // bar - - - let <|>var_name = 2 + 2; - return var_name; -} -", - ); - } - - #[test] - fn test_introduce_var_break() { - check_assist_range( - introduce_variable, - " -fn main() { - let result = loop { - <|>break 2 + 2<|>; - }; -} -", - " -fn main() { - let result = loop { - let <|>var_name = 2 + 2; - break var_name; - }; -} -", - ); - } - - #[test] - fn test_introduce_var_for_cast() { - check_assist_range( - introduce_variable, - " -fn main() { - let v = <|>0f32 as u32<|>; -} -", - " -fn main() { - let <|>var_name = 0f32 as u32; - let v = var_name; -} -", - ); - } - - #[test] - fn test_introduce_var_for_return_not_applicable() { - check_assist_range_not_applicable(introduce_variable, "fn foo() { <|>return<|>; } "); - } - - #[test] - fn test_introduce_var_for_break_not_applicable() { - check_assist_range_not_applicable( - introduce_variable, - "fn main() { loop { <|>break<|>; }; }", - ); - } - - // FIXME: This is not quite correct, but good enough(tm) for the sorting heuristic - #[test] - fn introduce_var_target() { - check_assist_range_target( - introduce_variable, - "fn foo() -> u32 { <|>return 2 + 2<|>; }", - "2 + 2", - ); - - check_assist_range_target( - introduce_variable, - " -fn main() { - let x = true; - let tuple = match x { - true => (<|>2 + 2<|>, true) - _ => (0, false) - }; -} -", - "2 + 2", - ); - } -} diff --git a/crates/ra_assists/src/assists/invert_if.rs b/crates/ra_assists/src/assists/invert_if.rs deleted file mode 100644 index 983392f21..000000000 --- a/crates/ra_assists/src/assists/invert_if.rs +++ /dev/null @@ -1,112 +0,0 @@ -use ra_syntax::ast::{self, make, AstNode}; -use ra_syntax::T; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: invert_if -// -// Apply invert_if -// This transforms if expressions of the form `if !x {A} else {B}` into `if x {B} else {A}` -// This also works with `!=`. This assist can only be applied with the cursor -// on `if`. -// -// ``` -// fn main() { -// if<|> !y { A } else { B } -// } -// ``` -// -> -// ``` -// fn main() { -// if y { B } else { A } -// } -// ``` - -pub(crate) fn invert_if(ctx: AssistCtx) -> Option { - let if_keyword = ctx.find_token_at_offset(T![if])?; - let expr = ast::IfExpr::cast(if_keyword.parent())?; - let if_range = if_keyword.text_range(); - let cursor_in_range = ctx.frange.range.is_subrange(&if_range); - if !cursor_in_range { - return None; - } - - let cond = expr.condition()?.expr()?; - let then_node = expr.then_branch()?.syntax().clone(); - - if let ast::ElseBranch::Block(else_block) = expr.else_branch()? { - let cond_range = cond.syntax().text_range(); - let flip_cond = invert_boolean_expression(cond); - let else_node = else_block.syntax(); - let else_range = else_node.text_range(); - let then_range = then_node.text_range(); - return ctx.add_assist(AssistId("invert_if"), "Invert if", |edit| { - edit.target(if_range); - edit.replace(cond_range, flip_cond.syntax().text()); - edit.replace(else_range, then_node.text()); - edit.replace(then_range, else_node.text()); - }); - } - - None -} - -pub(crate) fn invert_boolean_expression(expr: ast::Expr) -> ast::Expr { - if let Some(expr) = invert_special_case(&expr) { - return expr; - } - make::expr_prefix(T![!], expr) -} - -pub(crate) fn invert_special_case(expr: &ast::Expr) -> Option { - match expr { - ast::Expr::BinExpr(bin) => match bin.op_kind()? { - ast::BinOp::NegatedEqualityTest => bin.replace_op(T![==]).map(|it| it.into()), - ast::BinOp::EqualityTest => bin.replace_op(T![!=]).map(|it| it.into()), - _ => None, - }, - ast::Expr::PrefixExpr(pe) if pe.op_kind()? == ast::PrefixOp::Not => pe.expr(), - // FIXME: - // ast::Expr::Literal(true | false ) - _ => None, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::helpers::{check_assist, check_assist_not_applicable}; - - #[test] - fn invert_if_remove_inequality() { - check_assist( - invert_if, - "fn f() { i<|>f x != 3 { 1 } else { 3 + 2 } }", - "fn f() { i<|>f x == 3 { 3 + 2 } else { 1 } }", - ) - } - - #[test] - fn invert_if_remove_not() { - check_assist( - invert_if, - "fn f() { <|>if !cond { 3 * 2 } else { 1 } }", - "fn f() { <|>if cond { 1 } else { 3 * 2 } }", - ) - } - - #[test] - fn invert_if_general_case() { - check_assist( - invert_if, - "fn f() { i<|>f cond { 3 * 2 } else { 1 } }", - "fn f() { i<|>f !cond { 1 } else { 3 * 2 } }", - ) - } - - #[test] - fn invert_if_doesnt_apply_with_cursor_not_on_if() { - check_assist_not_applicable(invert_if, "fn f() { if !<|>cond { 3 * 2 } else { 1 } }") - } -} diff --git a/crates/ra_assists/src/assists/merge_match_arms.rs b/crates/ra_assists/src/assists/merge_match_arms.rs deleted file mode 100644 index 670614dd8..000000000 --- a/crates/ra_assists/src/assists/merge_match_arms.rs +++ /dev/null @@ -1,264 +0,0 @@ -use std::iter::successors; - -use ra_syntax::{ - ast::{self, AstNode}, - Direction, TextUnit, -}; - -use crate::{Assist, AssistCtx, AssistId, TextRange}; - -// Assist: merge_match_arms -// -// Merges identical match arms. -// -// ``` -// enum Action { Move { distance: u32 }, Stop } -// -// fn handle(action: Action) { -// match action { -// <|>Action::Move(..) => foo(), -// Action::Stop => foo(), -// } -// } -// ``` -// -> -// ``` -// enum Action { Move { distance: u32 }, Stop } -// -// fn handle(action: Action) { -// match action { -// Action::Move(..) | Action::Stop => foo(), -// } -// } -// ``` -pub(crate) fn merge_match_arms(ctx: AssistCtx) -> Option { - let current_arm = ctx.find_node_at_offset::()?; - // Don't try to handle arms with guards for now - can add support for this later - if current_arm.guard().is_some() { - return None; - } - let current_expr = current_arm.expr()?; - let current_text_range = current_arm.syntax().text_range(); - - enum CursorPos { - InExpr(TextUnit), - InPat(TextUnit), - } - let cursor_pos = ctx.frange.range.start(); - let cursor_pos = if current_expr.syntax().text_range().contains(cursor_pos) { - CursorPos::InExpr(current_text_range.end() - cursor_pos) - } else { - CursorPos::InPat(cursor_pos) - }; - - // We check if the following match arms match this one. We could, but don't, - // compare to the previous match arm as well. - let arms_to_merge = successors(Some(current_arm), next_arm) - .take_while(|arm| { - if arm.guard().is_some() { - return false; - } - match arm.expr() { - Some(expr) => expr.syntax().text() == current_expr.syntax().text(), - None => false, - } - }) - .collect::>(); - - if arms_to_merge.len() <= 1 { - return None; - } - - ctx.add_assist(AssistId("merge_match_arms"), "Merge match arms", |edit| { - let pats = if arms_to_merge.iter().any(contains_placeholder) { - "_".into() - } else { - arms_to_merge - .iter() - .flat_map(ast::MatchArm::pats) - .map(|x| x.syntax().to_string()) - .collect::>() - .join(" | ") - }; - - let arm = format!("{} => {}", pats, current_expr.syntax().text()); - - let start = arms_to_merge.first().unwrap().syntax().text_range().start(); - let end = arms_to_merge.last().unwrap().syntax().text_range().end(); - - edit.target(current_text_range); - edit.set_cursor(match cursor_pos { - CursorPos::InExpr(back_offset) => start + TextUnit::from_usize(arm.len()) - back_offset, - CursorPos::InPat(offset) => offset, - }); - edit.replace(TextRange::from_to(start, end), arm); - }) -} - -fn contains_placeholder(a: &ast::MatchArm) -> bool { - a.pats().any(|x| match x { - ra_syntax::ast::Pat::PlaceholderPat(..) => true, - _ => false, - }) -} - -fn next_arm(arm: &ast::MatchArm) -> Option { - arm.syntax().siblings(Direction::Next).skip(1).find_map(ast::MatchArm::cast) -} - -#[cfg(test)] -mod tests { - use super::merge_match_arms; - use crate::helpers::{check_assist, check_assist_not_applicable}; - - #[test] - fn merge_match_arms_single_patterns() { - check_assist( - merge_match_arms, - r#" - #[derive(Debug)] - enum X { A, B, C } - - fn main() { - let x = X::A; - let y = match x { - X::A => { 1i32<|> } - X::B => { 1i32 } - X::C => { 2i32 } - } - } - "#, - r#" - #[derive(Debug)] - enum X { A, B, C } - - fn main() { - let x = X::A; - let y = match x { - X::A | X::B => { 1i32<|> } - X::C => { 2i32 } - } - } - "#, - ); - } - - #[test] - fn merge_match_arms_multiple_patterns() { - check_assist( - merge_match_arms, - r#" - #[derive(Debug)] - enum X { A, B, C, D, E } - - fn main() { - let x = X::A; - let y = match x { - X::A | X::B => {<|> 1i32 }, - X::C | X::D => { 1i32 }, - X::E => { 2i32 }, - } - } - "#, - r#" - #[derive(Debug)] - enum X { A, B, C, D, E } - - fn main() { - let x = X::A; - let y = match x { - X::A | X::B | X::C | X::D => {<|> 1i32 }, - X::E => { 2i32 }, - } - } - "#, - ); - } - - #[test] - fn merge_match_arms_placeholder_pattern() { - check_assist( - merge_match_arms, - r#" - #[derive(Debug)] - enum X { A, B, C, D, E } - - fn main() { - let x = X::A; - let y = match x { - X::A => { 1i32 }, - X::B => { 2i<|>32 }, - _ => { 2i32 } - } - } - "#, - r#" - #[derive(Debug)] - enum X { A, B, C, D, E } - - fn main() { - let x = X::A; - let y = match x { - X::A => { 1i32 }, - _ => { 2i<|>32 } - } - } - "#, - ); - } - - #[test] - fn merges_all_subsequent_arms() { - check_assist( - merge_match_arms, - r#" - enum X { A, B, C, D, E } - - fn main() { - match X::A { - X::A<|> => 92, - X::B => 92, - X::C => 92, - X::D => 62, - _ => panic!(), - } - } - "#, - r#" - enum X { A, B, C, D, E } - - fn main() { - match X::A { - X::A<|> | X::B | X::C => 92, - X::D => 62, - _ => panic!(), - } - } - "#, - ) - } - - #[test] - fn merge_match_arms_rejects_guards() { - check_assist_not_applicable( - merge_match_arms, - r#" - #[derive(Debug)] - enum X { - A(i32), - B, - C - } - - fn main() { - let x = X::A; - let y = match x { - X::A(a) if a > 5 => { <|>1i32 }, - X::B => { 1i32 }, - X::C => { 2i32 } - } - } - "#, - ); - } -} diff --git a/crates/ra_assists/src/assists/move_bounds.rs b/crates/ra_assists/src/assists/move_bounds.rs deleted file mode 100644 index 90793b5fc..000000000 --- a/crates/ra_assists/src/assists/move_bounds.rs +++ /dev/null @@ -1,137 +0,0 @@ -use ra_syntax::{ - ast::{self, edit, make, AstNode, NameOwner, TypeBoundsOwner}, - SyntaxElement, - SyntaxKind::*, -}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: move_bounds_to_where_clause -// -// Moves inline type bounds to a where clause. -// -// ``` -// fn applyF: FnOnce(T) -> U>(f: F, x: T) -> U { -// f(x) -// } -// ``` -// -> -// ``` -// fn apply(f: F, x: T) -> U where F: FnOnce(T) -> U { -// f(x) -// } -// ``` -pub(crate) fn move_bounds_to_where_clause(ctx: AssistCtx) -> Option { - let type_param_list = ctx.find_node_at_offset::()?; - - let mut type_params = type_param_list.type_params(); - if type_params.all(|p| p.type_bound_list().is_none()) { - return None; - } - - let parent = type_param_list.syntax().parent()?; - if parent.children_with_tokens().any(|it| it.kind() == WHERE_CLAUSE) { - return None; - } - - let anchor: SyntaxElement = match parent.kind() { - FN_DEF => ast::FnDef::cast(parent)?.body()?.syntax().clone().into(), - TRAIT_DEF => ast::TraitDef::cast(parent)?.item_list()?.syntax().clone().into(), - IMPL_BLOCK => ast::ImplBlock::cast(parent)?.item_list()?.syntax().clone().into(), - ENUM_DEF => ast::EnumDef::cast(parent)?.variant_list()?.syntax().clone().into(), - STRUCT_DEF => parent - .children_with_tokens() - .find(|it| it.kind() == RECORD_FIELD_DEF_LIST || it.kind() == SEMI)?, - _ => return None, - }; - - ctx.add_assist(AssistId("move_bounds_to_where_clause"), "Move to where clause", |edit| { - let new_params = type_param_list - .type_params() - .filter(|it| it.type_bound_list().is_some()) - .map(|type_param| { - let without_bounds = type_param.remove_bounds(); - (type_param, without_bounds) - }); - - let new_type_param_list = edit::replace_descendants(&type_param_list, new_params); - edit.replace_ast(type_param_list.clone(), new_type_param_list); - - let where_clause = { - let predicates = type_param_list.type_params().filter_map(build_predicate); - make::where_clause(predicates) - }; - - let to_insert = match anchor.prev_sibling_or_token() { - Some(ref elem) if elem.kind() == WHITESPACE => format!("{} ", where_clause.syntax()), - _ => format!(" {}", where_clause.syntax()), - }; - edit.insert(anchor.text_range().start(), to_insert); - edit.target(type_param_list.syntax().text_range()); - }) -} - -fn build_predicate(param: ast::TypeParam) -> Option { - let path = make::path_from_name_ref(make::name_ref(¶m.name()?.syntax().to_string())); - let predicate = make::where_pred(path, param.type_bound_list()?.bounds()); - Some(predicate) -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::helpers::check_assist; - - #[test] - fn move_bounds_to_where_clause_fn() { - check_assist( - move_bounds_to_where_clause, - r#" - fn fooF: FnOnce(T) -> T>() {} - "#, - r#" - fn fooF>() where T: u32, F: FnOnce(T) -> T {} - "#, - ); - } - - #[test] - fn move_bounds_to_where_clause_impl() { - check_assist( - move_bounds_to_where_clause, - r#" - implT> A {} - "#, - r#" - implT> A where U: u32 {} - "#, - ); - } - - #[test] - fn move_bounds_to_where_clause_struct() { - check_assist( - move_bounds_to_where_clause, - r#" - struct A<<|>T: Iterator> {} - "#, - r#" - struct A<<|>T> where T: Iterator {} - "#, - ); - } - - #[test] - fn move_bounds_to_where_clause_tuple_struct() { - check_assist( - move_bounds_to_where_clause, - r#" - struct Pair<<|>T: u32>(T, T); - "#, - r#" - struct Pair<<|>T>(T, T) where T: u32; - "#, - ); - } -} diff --git a/crates/ra_assists/src/assists/move_guard.rs b/crates/ra_assists/src/assists/move_guard.rs deleted file mode 100644 index 2b91ce7c4..000000000 --- a/crates/ra_assists/src/assists/move_guard.rs +++ /dev/null @@ -1,308 +0,0 @@ -use ra_syntax::{ - ast, - ast::{AstNode, AstToken, IfExpr, MatchArm}, - TextUnit, -}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: move_guard_to_arm_body -// -// Moves match guard into match arm body. -// -// ``` -// enum Action { Move { distance: u32 }, Stop } -// -// fn handle(action: Action) { -// match action { -// Action::Move { distance } <|>if distance > 10 => foo(), -// _ => (), -// } -// } -// ``` -// -> -// ``` -// enum Action { Move { distance: u32 }, Stop } -// -// fn handle(action: Action) { -// match action { -// Action::Move { distance } => if distance > 10 { foo() }, -// _ => (), -// } -// } -// ``` -pub(crate) fn move_guard_to_arm_body(ctx: AssistCtx) -> Option { - let match_arm = ctx.find_node_at_offset::()?; - let guard = match_arm.guard()?; - let space_before_guard = guard.syntax().prev_sibling_or_token(); - - let guard_conditions = guard.expr()?; - let arm_expr = match_arm.expr()?; - let buf = format!("if {} {{ {} }}", guard_conditions.syntax().text(), arm_expr.syntax().text()); - - ctx.add_assist(AssistId("move_guard_to_arm_body"), "Move guard to arm body", |edit| { - edit.target(guard.syntax().text_range()); - let offseting_amount = match space_before_guard.and_then(|it| it.into_token()) { - Some(tok) => { - if let Some(_) = ast::Whitespace::cast(tok.clone()) { - let ele = tok.text_range(); - edit.delete(ele); - ele.len() - } else { - TextUnit::from(0) - } - } - _ => TextUnit::from(0), - }; - - edit.delete(guard.syntax().text_range()); - edit.replace_node_and_indent(arm_expr.syntax(), buf); - edit.set_cursor( - arm_expr.syntax().text_range().start() + TextUnit::from(3) - offseting_amount, - ); - }) -} - -// Assist: move_arm_cond_to_match_guard -// -// Moves if expression from match arm body into a guard. -// -// ``` -// enum Action { Move { distance: u32 }, Stop } -// -// fn handle(action: Action) { -// match action { -// Action::Move { distance } => <|>if distance > 10 { foo() }, -// _ => (), -// } -// } -// ``` -// -> -// ``` -// enum Action { Move { distance: u32 }, Stop } -// -// fn handle(action: Action) { -// match action { -// Action::Move { distance } if distance > 10 => foo(), -// _ => (), -// } -// } -// ``` -pub(crate) fn move_arm_cond_to_match_guard(ctx: AssistCtx) -> Option { - let match_arm: MatchArm = ctx.find_node_at_offset::()?; - let last_match_pat = match_arm.pats().last()?; - - let arm_body = match_arm.expr()?; - let if_expr: IfExpr = IfExpr::cast(arm_body.syntax().clone())?; - let cond = if_expr.condition()?; - let then_block = if_expr.then_branch()?; - - // Not support if with else branch - if let Some(_) = if_expr.else_branch() { - return None; - } - // Not support moving if let to arm guard - if let Some(_) = cond.pat() { - return None; - } - - let buf = format!(" if {}", cond.syntax().text()); - - ctx.add_assist( - AssistId("move_arm_cond_to_match_guard"), - "Move condition to match guard", - |edit| { - edit.target(if_expr.syntax().text_range()); - let then_only_expr = then_block.block().and_then(|it| it.statements().next()).is_none(); - - match &then_block.block().and_then(|it| it.expr()) { - Some(then_expr) if then_only_expr => { - edit.replace(if_expr.syntax().text_range(), then_expr.syntax().text()) - } - _ => edit.replace(if_expr.syntax().text_range(), then_block.syntax().text()), - } - - edit.insert(last_match_pat.syntax().text_range().end(), buf); - edit.set_cursor(last_match_pat.syntax().text_range().end() + TextUnit::from(1)); - }, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; - - #[test] - fn move_guard_to_arm_body_target() { - check_assist_target( - move_guard_to_arm_body, - r#" - fn f() { - let t = 'a'; - let chars = "abcd"; - match t { - '\r' <|>if chars.clone().next() == Some('\n') => false, - _ => true - } - } - "#, - r#"if chars.clone().next() == Some('\n')"#, - ); - } - - #[test] - fn move_guard_to_arm_body_works() { - check_assist( - move_guard_to_arm_body, - r#" - fn f() { - let t = 'a'; - let chars = "abcd"; - match t { - '\r' <|>if chars.clone().next() == Some('\n') => false, - _ => true - } - } - "#, - r#" - fn f() { - let t = 'a'; - let chars = "abcd"; - match t { - '\r' => if chars.clone().next() == Some('\n') { <|>false }, - _ => true - } - } - "#, - ); - } - - #[test] - fn move_guard_to_arm_body_works_complex_match() { - check_assist( - move_guard_to_arm_body, - r#" - fn f() { - match x { - <|>y @ 4 | y @ 5 if y > 5 => true, - _ => false - } - } - "#, - r#" - fn f() { - match x { - y @ 4 | y @ 5 => if y > 5 { <|>true }, - _ => false - } - } - "#, - ); - } - - #[test] - fn move_arm_cond_to_match_guard_works() { - check_assist( - move_arm_cond_to_match_guard, - r#" - fn f() { - let t = 'a'; - let chars = "abcd"; - match t { - '\r' => if chars.clone().next() == Some('\n') { <|>false }, - _ => true - } - } - "#, - r#" - fn f() { - let t = 'a'; - let chars = "abcd"; - match t { - '\r' <|>if chars.clone().next() == Some('\n') => false, - _ => true - } - } - "#, - ); - } - - #[test] - fn move_arm_cond_to_match_guard_if_let_not_works() { - check_assist_not_applicable( - move_arm_cond_to_match_guard, - r#" - fn f() { - let t = 'a'; - let chars = "abcd"; - match t { - '\r' => if let Some(_) = chars.clone().next() { <|>false }, - _ => true - } - } - "#, - ); - } - - #[test] - fn move_arm_cond_to_match_guard_if_empty_body_works() { - check_assist( - move_arm_cond_to_match_guard, - r#" - fn f() { - let t = 'a'; - let chars = "abcd"; - match t { - '\r' => if chars.clone().next().is_some() { <|> }, - _ => true - } - } - "#, - r#" - fn f() { - let t = 'a'; - let chars = "abcd"; - match t { - '\r' <|>if chars.clone().next().is_some() => { }, - _ => true - } - } - "#, - ); - } - - #[test] - fn move_arm_cond_to_match_guard_if_multiline_body_works() { - check_assist( - move_arm_cond_to_match_guard, - r#" - fn f() { - let mut t = 'a'; - let chars = "abcd"; - match t { - '\r' => if chars.clone().next().is_some() { - t = 'e';<|> - false - }, - _ => true - } - } - "#, - r#" - fn f() { - let mut t = 'a'; - let chars = "abcd"; - match t { - '\r' <|>if chars.clone().next().is_some() => { - t = 'e'; - false - }, - _ => true - } - } - "#, - ); - } -} diff --git a/crates/ra_assists/src/assists/raw_string.rs b/crates/ra_assists/src/assists/raw_string.rs deleted file mode 100644 index 2c0a1e126..000000000 --- a/crates/ra_assists/src/assists/raw_string.rs +++ /dev/null @@ -1,499 +0,0 @@ -use ra_syntax::{ - ast, AstToken, - SyntaxKind::{RAW_STRING, STRING}, - TextUnit, -}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: make_raw_string -// -// Adds `r#` to a plain string literal. -// -// ``` -// fn main() { -// "Hello,<|> World!"; -// } -// ``` -// -> -// ``` -// fn main() { -// r#"Hello, World!"#; -// } -// ``` -pub(crate) fn make_raw_string(ctx: AssistCtx) -> Option { - let token = ctx.find_token_at_offset(STRING).and_then(ast::String::cast)?; - let value = token.value()?; - ctx.add_assist(AssistId("make_raw_string"), "Rewrite as raw string", |edit| { - edit.target(token.syntax().text_range()); - let max_hash_streak = count_hashes(&value); - let mut hashes = String::with_capacity(max_hash_streak + 1); - for _ in 0..hashes.capacity() { - hashes.push('#'); - } - edit.replace(token.syntax().text_range(), format!("r{}\"{}\"{}", hashes, value, hashes)); - }) -} - -// Assist: make_usual_string -// -// Turns a raw string into a plain string. -// -// ``` -// fn main() { -// r#"Hello,<|> "World!""#; -// } -// ``` -// -> -// ``` -// fn main() { -// "Hello, \"World!\""; -// } -// ``` -pub(crate) fn make_usual_string(ctx: AssistCtx) -> Option { - let token = ctx.find_token_at_offset(RAW_STRING).and_then(ast::RawString::cast)?; - let value = token.value()?; - ctx.add_assist(AssistId("make_usual_string"), "Rewrite as regular string", |edit| { - edit.target(token.syntax().text_range()); - // parse inside string to escape `"` - let escaped = value.escape_default().to_string(); - edit.replace(token.syntax().text_range(), format!("\"{}\"", escaped)); - }) -} - -// Assist: add_hash -// -// Adds a hash to a raw string literal. -// -// ``` -// fn main() { -// r#"Hello,<|> World!"#; -// } -// ``` -// -> -// ``` -// fn main() { -// r##"Hello, World!"##; -// } -// ``` -pub(crate) fn add_hash(ctx: AssistCtx) -> Option { - let token = ctx.find_token_at_offset(RAW_STRING)?; - ctx.add_assist(AssistId("add_hash"), "Add # to raw string", |edit| { - edit.target(token.text_range()); - edit.insert(token.text_range().start() + TextUnit::of_char('r'), "#"); - edit.insert(token.text_range().end(), "#"); - }) -} - -// Assist: remove_hash -// -// Removes a hash from a raw string literal. -// -// ``` -// fn main() { -// r#"Hello,<|> World!"#; -// } -// ``` -// -> -// ``` -// fn main() { -// r"Hello, World!"; -// } -// ``` -pub(crate) fn remove_hash(ctx: AssistCtx) -> Option { - let token = ctx.find_token_at_offset(RAW_STRING)?; - let text = token.text().as_str(); - if text.starts_with("r\"") { - // no hash to remove - return None; - } - ctx.add_assist(AssistId("remove_hash"), "Remove hash from raw string", |edit| { - edit.target(token.text_range()); - let result = &text[2..text.len() - 1]; - let result = if result.starts_with('\"') { - // FIXME: this logic is wrong, not only the last has has to handled specially - // no more hash, escape - let internal_str = &result[1..result.len() - 1]; - format!("\"{}\"", internal_str.escape_default().to_string()) - } else { - result.to_owned() - }; - edit.replace(token.text_range(), format!("r{}", result)); - }) -} - -fn count_hashes(s: &str) -> usize { - let mut max_hash_streak = 0usize; - for idx in s.match_indices("\"#").map(|(i, _)| i) { - let (_, sub) = s.split_at(idx + 1); - let nb_hash = sub.chars().take_while(|c| *c == '#').count(); - if nb_hash > max_hash_streak { - max_hash_streak = nb_hash; - } - } - max_hash_streak -} - -#[cfg(test)] -mod test { - use super::*; - use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; - - #[test] - fn make_raw_string_target() { - check_assist_target( - make_raw_string, - r#" - fn f() { - let s = <|>"random\nstring"; - } - "#, - r#""random\nstring""#, - ); - } - - #[test] - fn make_raw_string_works() { - check_assist( - make_raw_string, - r#" - fn f() { - let s = <|>"random\nstring"; - } - "#, - r##" - fn f() { - let s = <|>r#"random -string"#; - } - "##, - ) - } - - #[test] - fn make_raw_string_works_inside_macros() { - check_assist( - make_raw_string, - r#" - fn f() { - format!(<|>"x = {}", 92) - } - "#, - r##" - fn f() { - format!(<|>r#"x = {}"#, 92) - } - "##, - ) - } - - #[test] - fn make_raw_string_hashes_inside_works() { - check_assist( - make_raw_string, - r###" - fn f() { - let s = <|>"#random##\nstring"; - } - "###, - r####" - fn f() { - let s = <|>r#"#random## -string"#; - } - "####, - ) - } - - #[test] - fn make_raw_string_closing_hashes_inside_works() { - check_assist( - make_raw_string, - r###" - fn f() { - let s = <|>"#random\"##\nstring"; - } - "###, - r####" - fn f() { - let s = <|>r###"#random"## -string"###; - } - "####, - ) - } - - #[test] - fn make_raw_string_nothing_to_unescape_works() { - check_assist( - make_raw_string, - r#" - fn f() { - let s = <|>"random string"; - } - "#, - r##" - fn f() { - let s = <|>r#"random string"#; - } - "##, - ) - } - - #[test] - fn make_raw_string_not_works_on_partial_string() { - check_assist_not_applicable( - make_raw_string, - r#" - fn f() { - let s = "foo<|> - } - "#, - ) - } - - #[test] - fn make_usual_string_not_works_on_partial_string() { - check_assist_not_applicable( - make_usual_string, - r#" - fn main() { - let s = r#"bar<|> - } - "#, - ) - } - - #[test] - fn add_hash_target() { - check_assist_target( - add_hash, - r#" - fn f() { - let s = <|>r"random string"; - } - "#, - r#"r"random string""#, - ); - } - - #[test] - fn add_hash_works() { - check_assist( - add_hash, - r#" - fn f() { - let s = <|>r"random string"; - } - "#, - r##" - fn f() { - let s = <|>r#"random string"#; - } - "##, - ) - } - - #[test] - fn add_more_hash_works() { - check_assist( - add_hash, - r##" - fn f() { - let s = <|>r#"random"string"#; - } - "##, - r###" - fn f() { - let s = <|>r##"random"string"##; - } - "###, - ) - } - - #[test] - fn add_hash_not_works() { - check_assist_not_applicable( - add_hash, - r#" - fn f() { - let s = <|>"random string"; - } - "#, - ); - } - - #[test] - fn remove_hash_target() { - check_assist_target( - remove_hash, - r##" - fn f() { - let s = <|>r#"random string"#; - } - "##, - r##"r#"random string"#"##, - ); - } - - #[test] - fn remove_hash_works() { - check_assist( - remove_hash, - r##" - fn f() { - let s = <|>r#"random string"#; - } - "##, - r#" - fn f() { - let s = <|>r"random string"; - } - "#, - ) - } - - #[test] - fn remove_hash_with_quote_works() { - check_assist( - remove_hash, - r##" - fn f() { - let s = <|>r#"random"str"ing"#; - } - "##, - r#" - fn f() { - let s = <|>r"random\"str\"ing"; - } - "#, - ) - } - - #[test] - fn remove_more_hash_works() { - check_assist( - remove_hash, - r###" - fn f() { - let s = <|>r##"random string"##; - } - "###, - r##" - fn f() { - let s = <|>r#"random string"#; - } - "##, - ) - } - - #[test] - fn remove_hash_not_works() { - check_assist_not_applicable( - remove_hash, - r#" - fn f() { - let s = <|>"random string"; - } - "#, - ); - } - - #[test] - fn remove_hash_no_hash_not_works() { - check_assist_not_applicable( - remove_hash, - r#" - fn f() { - let s = <|>r"random string"; - } - "#, - ); - } - - #[test] - fn make_usual_string_target() { - check_assist_target( - make_usual_string, - r##" - fn f() { - let s = <|>r#"random string"#; - } - "##, - r##"r#"random string"#"##, - ); - } - - #[test] - fn make_usual_string_works() { - check_assist( - make_usual_string, - r##" - fn f() { - let s = <|>r#"random string"#; - } - "##, - r#" - fn f() { - let s = <|>"random string"; - } - "#, - ) - } - - #[test] - fn make_usual_string_with_quote_works() { - check_assist( - make_usual_string, - r##" - fn f() { - let s = <|>r#"random"str"ing"#; - } - "##, - r#" - fn f() { - let s = <|>"random\"str\"ing"; - } - "#, - ) - } - - #[test] - fn make_usual_string_more_hash_works() { - check_assist( - make_usual_string, - r###" - fn f() { - let s = <|>r##"random string"##; - } - "###, - r##" - fn f() { - let s = <|>"random string"; - } - "##, - ) - } - - #[test] - fn make_usual_string_not_works() { - check_assist_not_applicable( - make_usual_string, - r#" - fn f() { - let s = <|>"random string"; - } - "#, - ); - } - - #[test] - fn count_hashes_test() { - assert_eq!(0, count_hashes("abc")); - assert_eq!(0, count_hashes("###")); - assert_eq!(1, count_hashes("\"#abc")); - assert_eq!(0, count_hashes("#abc")); - assert_eq!(2, count_hashes("#ab\"##c")); - assert_eq!(4, count_hashes("#ab\"##\"####c")); - } -} diff --git a/crates/ra_assists/src/assists/remove_dbg.rs b/crates/ra_assists/src/assists/remove_dbg.rs deleted file mode 100644 index 5085649b4..000000000 --- a/crates/ra_assists/src/assists/remove_dbg.rs +++ /dev/null @@ -1,150 +0,0 @@ -use ra_syntax::{ - ast::{self, AstNode}, - TextUnit, T, -}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: remove_dbg -// -// Removes `dbg!()` macro call. -// -// ``` -// fn main() { -// <|>dbg!(92); -// } -// ``` -// -> -// ``` -// fn main() { -// 92; -// } -// ``` -pub(crate) fn remove_dbg(ctx: AssistCtx) -> Option { - let macro_call = ctx.find_node_at_offset::()?; - - if !is_valid_macrocall(¯o_call, "dbg")? { - return None; - } - - let macro_range = macro_call.syntax().text_range(); - - // If the cursor is inside the macro call, we'll try to maintain the cursor - // position by subtracting the length of dbg!( from the start of the file - // range, otherwise we'll default to using the start of the macro call - let cursor_pos = { - let file_range = ctx.frange.range; - - let offset_start = file_range - .start() - .checked_sub(macro_range.start()) - .unwrap_or_else(|| TextUnit::from(0)); - - let dbg_size = TextUnit::of_str("dbg!("); - - if offset_start > dbg_size { - file_range.start() - dbg_size - } else { - macro_range.start() - } - }; - - let macro_content = { - let macro_args = macro_call.token_tree()?.syntax().clone(); - - let text = macro_args.text(); - let without_parens = TextUnit::of_char('(')..text.len() - TextUnit::of_char(')'); - text.slice(without_parens).to_string() - }; - - ctx.add_assist(AssistId("remove_dbg"), "Remove dbg!()", |edit| { - edit.target(macro_call.syntax().text_range()); - edit.replace(macro_range, macro_content); - edit.set_cursor(cursor_pos); - }) -} - -/// Verifies that the given macro_call actually matches the given name -/// and contains proper ending tokens -fn is_valid_macrocall(macro_call: &ast::MacroCall, macro_name: &str) -> Option { - let path = macro_call.path()?; - let name_ref = path.segment()?.name_ref()?; - - // Make sure it is actually a dbg-macro call, dbg followed by ! - let excl = path.syntax().next_sibling_or_token()?; - - if name_ref.text() != macro_name || excl.kind() != T![!] { - return None; - } - - let node = macro_call.token_tree()?.syntax().clone(); - let first_child = node.first_child_or_token()?; - let last_child = node.last_child_or_token()?; - - match (first_child.kind(), last_child.kind()) { - (T!['('], T![')']) | (T!['['], T![']']) | (T!['{'], T!['}']) => Some(true), - _ => Some(false), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; - - #[test] - fn test_remove_dbg() { - check_assist(remove_dbg, "<|>dbg!(1 + 1)", "<|>1 + 1"); - - check_assist(remove_dbg, "dbg!<|>((1 + 1))", "<|>(1 + 1)"); - - check_assist(remove_dbg, "dbg!(1 <|>+ 1)", "1 <|>+ 1"); - - check_assist(remove_dbg, "let _ = <|>dbg!(1 + 1)", "let _ = <|>1 + 1"); - - check_assist( - remove_dbg, - " -fn foo(n: usize) { - if let Some(_) = dbg!(n.<|>checked_sub(4)) { - // ... - } -} -", - " -fn foo(n: usize) { - if let Some(_) = n.<|>checked_sub(4) { - // ... - } -} -", - ); - } - #[test] - fn test_remove_dbg_with_brackets_and_braces() { - check_assist(remove_dbg, "dbg![<|>1 + 1]", "<|>1 + 1"); - check_assist(remove_dbg, "dbg!{<|>1 + 1}", "<|>1 + 1"); - } - - #[test] - fn test_remove_dbg_not_applicable() { - check_assist_not_applicable(remove_dbg, "<|>vec![1, 2, 3]"); - check_assist_not_applicable(remove_dbg, "<|>dbg(5, 6, 7)"); - check_assist_not_applicable(remove_dbg, "<|>dbg!(5, 6, 7"); - } - - #[test] - fn remove_dbg_target() { - check_assist_target( - remove_dbg, - " -fn foo(n: usize) { - if let Some(_) = dbg!(n.<|>checked_sub(4)) { - // ... - } -} -", - "dbg!(n.checked_sub(4))", - ); - } -} diff --git a/crates/ra_assists/src/assists/replace_if_let_with_match.rs b/crates/ra_assists/src/assists/replace_if_let_with_match.rs deleted file mode 100644 index e6cd50bc1..000000000 --- a/crates/ra_assists/src/assists/replace_if_let_with_match.rs +++ /dev/null @@ -1,148 +0,0 @@ -use ra_fmt::unwrap_trivial_block; -use ra_syntax::{ - ast::{self, make}, - AstNode, -}; - -use crate::{Assist, AssistCtx, AssistId}; -use ast::edit::IndentLevel; - -// Assist: replace_if_let_with_match -// -// Replaces `if let` with an else branch with a `match` expression. -// -// ``` -// enum Action { Move { distance: u32 }, Stop } -// -// fn handle(action: Action) { -// <|>if let Action::Move { distance } = action { -// foo(distance) -// } else { -// bar() -// } -// } -// ``` -// -> -// ``` -// enum Action { Move { distance: u32 }, Stop } -// -// fn handle(action: Action) { -// match action { -// Action::Move { distance } => foo(distance), -// _ => bar(), -// } -// } -// ``` -pub(crate) fn replace_if_let_with_match(ctx: AssistCtx) -> Option { - let if_expr: ast::IfExpr = ctx.find_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::ElseBranch::Block(it) => it, - ast::ElseBranch::IfExpr(_) => return None, - }; - - ctx.add_assist(AssistId("replace_if_let_with_match"), "Replace with match", |edit| { - let match_expr = { - let then_arm = { - let then_expr = unwrap_trivial_block(then_block); - make::match_arm(vec![pat], then_expr) - }; - let else_arm = { - let else_expr = unwrap_trivial_block(else_block); - make::match_arm(vec![make::placeholder_pat().into()], else_expr) - }; - make::expr_match(expr, make::match_arm_list(vec![then_arm, else_arm])) - }; - - let match_expr = IndentLevel::from_node(if_expr.syntax()).increase_indent(match_expr); - - edit.target(if_expr.syntax().text_range()); - edit.set_cursor(if_expr.syntax().text_range().start()); - edit.replace_ast::(if_expr.into(), match_expr.into()); - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::helpers::{check_assist, check_assist_target}; - - #[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, - } - } -} ", - ) - } - - #[test] - fn test_replace_if_let_with_match_doesnt_unwrap_multiline_expressions() { - check_assist( - replace_if_let_with_match, - " -fn foo() { - if <|>let VariantData::Struct(..) = a { - bar( - 123 - ) - } else { - false - } -} ", - " -fn foo() { - <|>match a { - VariantData::Struct(..) => { - bar( - 123 - ) - } - _ => false, - } -} ", - ) - } - - #[test] - fn replace_if_let_with_match_target() { - check_assist_target( - replace_if_let_with_match, - " -impl VariantData { - pub fn is_struct(&self) -> bool { - if <|>let VariantData::Struct(..) = *self { - true - } else { - false - } - } -} ", - "if let VariantData::Struct(..) = *self { - true - } else { - false - }", - ); - } -} diff --git a/crates/ra_assists/src/assists/split_import.rs b/crates/ra_assists/src/assists/split_import.rs deleted file mode 100644 index 2c3f07a79..000000000 --- a/crates/ra_assists/src/assists/split_import.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::iter::successors; - -use ra_syntax::{ast, AstNode, TextUnit, T}; - -use crate::{Assist, AssistCtx, AssistId}; - -// Assist: split_import -// -// Wraps the tail of import into braces. -// -// ``` -// use std::<|>collections::HashMap; -// ``` -// -> -// ``` -// use std::{collections::HashMap}; -// ``` -pub(crate) fn split_import(ctx: AssistCtx) -> Option { - let colon_colon = ctx.find_token_at_offset(T![::])?; - let path = ast::Path::cast(colon_colon.parent())?; - let top_path = successors(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.text_range().end(); - let r_curly = match top_path.syntax().parent().and_then(ast::UseTree::cast) { - Some(tree) => tree.syntax().text_range().end(), - None => top_path.syntax().text_range().end(), - }; - - ctx.add_assist(AssistId("split_import"), "Split import", |edit| { - edit.target(colon_colon.text_range()); - edit.insert(l_curly, "{"); - edit.insert(r_curly, "}"); - edit.set_cursor(l_curly + TextUnit::of_str("{")); - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::helpers::{check_assist, check_assist_target}; - - #[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 crate:<|>:db::{RootDatabase, FileSymbol}", - "use crate::{<|>db::{RootDatabase, FileSymbol}}", - ) - } - - #[test] - fn split_import_target() { - check_assist_target(split_import, "use crate::<|>db::{RootDatabase, FileSymbol}", "::"); - } -} diff --git a/crates/ra_assists/src/handlers/add_custom_impl.rs b/crates/ra_assists/src/handlers/add_custom_impl.rs new file mode 100644 index 000000000..7fdd816bf --- /dev/null +++ b/crates/ra_assists/src/handlers/add_custom_impl.rs @@ -0,0 +1,209 @@ +//! FIXME: write short doc here + +use crate::{Assist, AssistCtx, AssistId}; + +use join_to_string::join; +use ra_syntax::{ + ast::{self, AstNode}, + Direction, SmolStr, + SyntaxKind::{IDENT, WHITESPACE}, + TextRange, TextUnit, +}; + +const DERIVE_TRAIT: &str = "derive"; + +// Assist: add_custom_impl +// +// Adds impl block for derived trait. +// +// ``` +// #[derive(Deb<|>ug, Display)] +// struct S; +// ``` +// -> +// ``` +// #[derive(Display)] +// struct S; +// +// impl Debug for S { +// +// } +// ``` +pub(crate) fn add_custom_impl(ctx: AssistCtx) -> Option { + let input = ctx.find_node_at_offset::()?; + let attr = input.syntax().parent().and_then(ast::Attr::cast)?; + + let attr_name = attr + .syntax() + .descendants_with_tokens() + .filter(|t| t.kind() == IDENT) + .find_map(|i| i.into_token()) + .filter(|t| *t.text() == DERIVE_TRAIT)? + .text() + .clone(); + + let trait_token = + ctx.token_at_offset().filter(|t| t.kind() == IDENT && *t.text() != attr_name).next()?; + + let annotated = attr.syntax().siblings(Direction::Next).find_map(|s| ast::Name::cast(s))?; + let annotated_name = annotated.syntax().text().to_string(); + let start_offset = annotated.syntax().parent()?.text_range().end(); + + let label = + format!("Add custom impl '{}' for '{}'", trait_token.text().as_str(), annotated_name); + + ctx.add_assist(AssistId("add_custom_impl"), label, |edit| { + edit.target(attr.syntax().text_range()); + + let new_attr_input = input + .syntax() + .descendants_with_tokens() + .filter(|t| t.kind() == IDENT) + .filter_map(|t| t.into_token().map(|t| t.text().clone())) + .filter(|t| t != trait_token.text()) + .collect::>(); + let has_more_derives = new_attr_input.len() > 0; + let new_attr_input = + join(new_attr_input.iter()).separator(", ").surround_with("(", ")").to_string(); + let new_attr_input_len = new_attr_input.len(); + + let mut buf = String::new(); + buf.push_str("\n\nimpl "); + buf.push_str(trait_token.text().as_str()); + buf.push_str(" for "); + buf.push_str(annotated_name.as_str()); + buf.push_str(" {\n"); + + let cursor_delta = if has_more_derives { + edit.replace(input.syntax().text_range(), new_attr_input); + input.syntax().text_range().len() - TextUnit::from_usize(new_attr_input_len) + } else { + let attr_range = attr.syntax().text_range(); + edit.delete(attr_range); + + let line_break_range = attr + .syntax() + .next_sibling_or_token() + .filter(|t| t.kind() == WHITESPACE) + .map(|t| t.text_range()) + .unwrap_or(TextRange::from_to(TextUnit::from(0), TextUnit::from(0))); + edit.delete(line_break_range); + + attr_range.len() + line_break_range.len() + }; + + edit.set_cursor(start_offset + TextUnit::of_str(&buf) - cursor_delta); + buf.push_str("\n}"); + edit.insert(start_offset, buf); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::{check_assist, check_assist_not_applicable}; + + #[test] + fn add_custom_impl_for_unique_input() { + check_assist( + add_custom_impl, + " +#[derive(Debu<|>g)] +struct Foo { + bar: String, +} + ", + " +struct Foo { + bar: String, +} + +impl Debug for Foo { +<|> +} + ", + ) + } + + #[test] + fn add_custom_impl_for_with_visibility_modifier() { + check_assist( + add_custom_impl, + " +#[derive(Debug<|>)] +pub struct Foo { + bar: String, +} + ", + " +pub struct Foo { + bar: String, +} + +impl Debug for Foo { +<|> +} + ", + ) + } + + #[test] + fn add_custom_impl_when_multiple_inputs() { + check_assist( + add_custom_impl, + " +#[derive(Display, Debug<|>, Serialize)] +struct Foo {} + ", + " +#[derive(Display, Serialize)] +struct Foo {} + +impl Debug for Foo { +<|> +} + ", + ) + } + + #[test] + fn test_ignore_derive_macro_without_input() { + check_assist_not_applicable( + add_custom_impl, + " +#[derive(<|>)] +struct Foo {} + ", + ) + } + + #[test] + fn test_ignore_if_cursor_on_param() { + check_assist_not_applicable( + add_custom_impl, + " +#[derive<|>(Debug)] +struct Foo {} + ", + ); + + check_assist_not_applicable( + add_custom_impl, + " +#[derive(Debug)<|>] +struct Foo {} + ", + ) + } + + #[test] + fn test_ignore_if_not_derive() { + check_assist_not_applicable( + add_custom_impl, + " +#[allow(non_camel_<|>case_types)] +struct Foo {} + ", + ) + } +} diff --git a/crates/ra_assists/src/handlers/add_derive.rs b/crates/ra_assists/src/handlers/add_derive.rs new file mode 100644 index 000000000..b0d1a0a80 --- /dev/null +++ b/crates/ra_assists/src/handlers/add_derive.rs @@ -0,0 +1,120 @@ +use ra_syntax::{ + ast::{self, AstNode, AttrsOwner}, + SyntaxKind::{COMMENT, WHITESPACE}, + TextUnit, +}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: add_derive +// +// Adds a new `#[derive()]` clause to a struct or enum. +// +// ``` +// struct Point { +// x: u32, +// y: u32,<|> +// } +// ``` +// -> +// ``` +// #[derive()] +// struct Point { +// x: u32, +// y: u32, +// } +// ``` +pub(crate) fn add_derive(ctx: AssistCtx) -> Option { + let nominal = ctx.find_node_at_offset::()?; + let node_start = derive_insertion_offset(&nominal)?; + ctx.add_assist(AssistId("add_derive"), "Add `#[derive]`", |edit| { + let derive_attr = nominal + .attrs() + .filter_map(|x| x.as_simple_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().text_range().end() - TextUnit::of_char(')'), + }; + edit.target(nominal.syntax().text_range()); + edit.set_cursor(offset) + }) +} + +// Insert `derive` after doc comments. +fn derive_insertion_offset(nominal: &ast::NominalDef) -> Option { + let non_ws_child = nominal + .syntax() + .children_with_tokens() + .find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?; + Some(non_ws_child.text_range().start()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::{check_assist, check_assist_target}; + + #[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, } + ", + ); + } + + #[test] + fn add_derive_target() { + check_assist_target( + add_derive, + " +struct SomeThingIrrelevant; +/// `Foo` is a pretty important struct. +/// It does stuff. +struct Foo { a: i32<|>, } +struct EvenMoreIrrelevant; + ", + "/// `Foo` is a pretty important struct. +/// It does stuff. +struct Foo { a: i32, }", + ); + } +} diff --git a/crates/ra_assists/src/handlers/add_explicit_type.rs b/crates/ra_assists/src/handlers/add_explicit_type.rs new file mode 100644 index 000000000..2cb9d2f48 --- /dev/null +++ b/crates/ra_assists/src/handlers/add_explicit_type.rs @@ -0,0 +1,178 @@ +use hir::HirDisplay; +use ra_syntax::{ + ast::{self, AstNode, LetStmt, NameOwner, TypeAscriptionOwner}, + TextRange, +}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: add_explicit_type +// +// Specify type for a let binding. +// +// ``` +// fn main() { +// let x<|> = 92; +// } +// ``` +// -> +// ``` +// fn main() { +// let x: i32 = 92; +// } +// ``` +pub(crate) fn add_explicit_type(ctx: AssistCtx) -> Option { + let stmt = ctx.find_node_at_offset::()?; + let expr = stmt.initializer()?; + let pat = stmt.pat()?; + // Must be a binding + let pat = match pat { + ast::Pat::BindPat(bind_pat) => bind_pat, + _ => return None, + }; + let pat_range = pat.syntax().text_range(); + // The binding must have a name + let name = pat.name()?; + let name_range = name.syntax().text_range(); + let stmt_range = stmt.syntax().text_range(); + let eq_range = stmt.eq_token()?.text_range(); + // Assist should only be applicable if cursor is between 'let' and '=' + let let_range = TextRange::from_to(stmt_range.start(), eq_range.start()); + let cursor_in_range = ctx.frange.range.is_subrange(&let_range); + if !cursor_in_range { + return None; + } + // Assist not applicable if the type has already been specified + // and it has no placeholders + let ascribed_ty = stmt.ascribed_type(); + if let Some(ref ty) = ascribed_ty { + if ty.syntax().descendants().find_map(ast::PlaceholderType::cast).is_none() { + return None; + } + } + // Infer type + let db = ctx.db; + let analyzer = ctx.source_analyzer(stmt.syntax(), None); + let ty = analyzer.type_of(db, &expr)?; + // Assist not applicable if the type is unknown + if ty.contains_unknown() { + return None; + } + + ctx.add_assist( + AssistId("add_explicit_type"), + format!("Insert explicit type '{}'", ty.display(db)), + |edit| { + edit.target(pat_range); + if let Some(ascribed_ty) = ascribed_ty { + edit.replace(ascribed_ty.syntax().text_range(), format!("{}", ty.display(db))); + } else { + edit.insert(name_range.end(), format!(": {}", ty.display(db))); + } + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; + + #[test] + fn add_explicit_type_target() { + check_assist_target(add_explicit_type, "fn f() { let a<|> = 1; }", "a"); + } + + #[test] + fn add_explicit_type_works_for_simple_expr() { + check_assist( + add_explicit_type, + "fn f() { let a<|> = 1; }", + "fn f() { let a<|>: i32 = 1; }", + ); + } + + #[test] + fn add_explicit_type_works_for_underscore() { + check_assist( + add_explicit_type, + "fn f() { let a<|>: _ = 1; }", + "fn f() { let a<|>: i32 = 1; }", + ); + } + + #[test] + fn add_explicit_type_works_for_nested_underscore() { + check_assist( + add_explicit_type, + r#" + enum Option { + Some(T), + None + } + + fn f() { + let a<|>: Option<_> = Option::Some(1); + }"#, + r#" + enum Option { + Some(T), + None + } + + fn f() { + let a<|>: Option = Option::Some(1); + }"#, + ); + } + + #[test] + fn add_explicit_type_works_for_macro_call() { + check_assist( + add_explicit_type, + "macro_rules! v { () => {0u64} } fn f() { let a<|> = v!(); }", + "macro_rules! v { () => {0u64} } fn f() { let a<|>: u64 = v!(); }", + ); + } + + #[test] + fn add_explicit_type_works_for_macro_call_recursive() { + check_assist( + add_explicit_type, + "macro_rules! u { () => {0u64} } macro_rules! v { () => {u!()} } fn f() { let a<|> = v!(); }", + "macro_rules! u { () => {0u64} } macro_rules! v { () => {u!()} } fn f() { let a<|>: u64 = v!(); }", + ); + } + + #[test] + fn add_explicit_type_not_applicable_if_ty_not_inferred() { + check_assist_not_applicable(add_explicit_type, "fn f() { let a<|> = None; }"); + } + + #[test] + fn add_explicit_type_not_applicable_if_ty_already_specified() { + check_assist_not_applicable(add_explicit_type, "fn f() { let a<|>: i32 = 1; }"); + } + + #[test] + fn add_explicit_type_not_applicable_if_specified_ty_is_tuple() { + check_assist_not_applicable(add_explicit_type, "fn f() { let a<|>: (i32, i32) = (3, 4); }"); + } + + #[test] + fn add_explicit_type_not_applicable_if_cursor_after_equals() { + check_assist_not_applicable( + add_explicit_type, + "fn f() {let a =<|> match 1 {2 => 3, 3 => 5};}", + ) + } + + #[test] + fn add_explicit_type_not_applicable_if_cursor_before_let() { + check_assist_not_applicable( + add_explicit_type, + "fn f() <|>{let a = match 1 {2 => 3, 3 => 5};}", + ) + } +} diff --git a/crates/ra_assists/src/handlers/add_impl.rs b/crates/ra_assists/src/handlers/add_impl.rs new file mode 100644 index 000000000..241b085fd --- /dev/null +++ b/crates/ra_assists/src/handlers/add_impl.rs @@ -0,0 +1,94 @@ +use format_buf::format; + +use join_to_string::join; +use ra_syntax::{ + ast::{self, AstNode, NameOwner, TypeParamsOwner}, + TextUnit, +}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: add_impl +// +// Adds a new inherent impl for a type. +// +// ``` +// struct Ctx { +// data: T,<|> +// } +// ``` +// -> +// ``` +// struct Ctx { +// data: T, +// } +// +// impl Ctx { +// +// } +// ``` +pub(crate) fn add_impl(ctx: AssistCtx) -> Option { + let nominal = ctx.find_node_at_offset::()?; + let name = nominal.name()?; + ctx.add_assist(AssistId("add_impl"), format!("Implement {}", name.text().as_str()), |edit| { + edit.target(nominal.syntax().text_range()); + let type_params = nominal.type_param_list(); + let start_offset = nominal.syntax().text_range().end(); + let mut buf = String::new(); + buf.push_str("\n\nimpl"); + if let Some(type_params) = &type_params { + format!(buf, "{}", type_params.syntax()); + } + 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_token()) + .map(|it| it.text().clone()); + let type_params = + type_params.type_params().filter_map(|it| it.name()).map(|it| it.text().clone()); + 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::helpers::{check_assist, check_assist_target}; + + #[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 {<|>}", + "struct Foo {}\n\nimpl Foo {\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}", + ); + } + + #[test] + fn add_impl_target() { + check_assist_target( + add_impl, + " +struct SomeThingIrrelevant; +/// Has a lifetime parameter +struct Foo<'a, T: Foo<'a>> {<|>} +struct EvenMoreIrrelevant; +", + "/// Has a lifetime parameter +struct Foo<'a, T: Foo<'a>> {}", + ); + } +} diff --git a/crates/ra_assists/src/handlers/add_import.rs b/crates/ra_assists/src/handlers/add_import.rs new file mode 100644 index 000000000..f03dddac8 --- /dev/null +++ b/crates/ra_assists/src/handlers/add_import.rs @@ -0,0 +1,967 @@ +use hir::{self, ModPath}; +use ra_syntax::{ + ast::{self, NameOwner}, + AstNode, Direction, SmolStr, + SyntaxKind::{PATH, PATH_SEGMENT}, + SyntaxNode, TextRange, T, +}; +use ra_text_edit::TextEditBuilder; + +use crate::{ + assist_ctx::{Assist, AssistCtx}, + AssistId, +}; + +/// This function produces sequence of text edits into edit +/// to import the target path in the most appropriate scope given +/// the cursor position +pub fn auto_import_text_edit( + // Ideally the position of the cursor, used to + position: &SyntaxNode, + // The statement to use as anchor (last resort) + anchor: &SyntaxNode, + path_to_import: &ModPath, + edit: &mut TextEditBuilder, +) { + let target = path_to_import.to_string().split("::").map(SmolStr::new).collect::>(); + let container = position.ancestors().find_map(|n| { + if let Some(module) = ast::Module::cast(n.clone()) { + return module.item_list().map(|it| it.syntax().clone()); + } + ast::SourceFile::cast(n).map(|it| it.syntax().clone()) + }); + + if let Some(container) = container { + let action = best_action_for_target(container, anchor.clone(), &target); + make_assist(&action, &target, edit); + } +} + +// Assist: add_import +// +// Adds a use statement for a given fully-qualified path. +// +// ``` +// fn process(map: std::collections::<|>HashMap) {} +// ``` +// -> +// ``` +// use std::collections::HashMap; +// +// fn process(map: HashMap) {} +// ``` +pub(crate) fn add_import(ctx: AssistCtx) -> Option { + let path: ast::Path = ctx.find_node_at_offset()?; + // We don't want to mess with use statements + if path.syntax().ancestors().find_map(ast::UseItem::cast).is_some() { + return None; + } + + let hir_path = hir::Path::from_ast(path.clone())?; + let segments = collect_hir_path_segments(&hir_path)?; + if segments.len() < 2 { + return None; + } + + let module = path.syntax().ancestors().find_map(ast::Module::cast); + let position = match module.and_then(|it| it.item_list()) { + Some(item_list) => item_list.syntax().clone(), + None => { + let current_file = path.syntax().ancestors().find_map(ast::SourceFile::cast)?; + current_file.syntax().clone() + } + }; + + ctx.add_assist(AssistId("add_import"), format!("Import {}", fmt_segments(&segments)), |edit| { + apply_auto_import(&position, &path, &segments, edit.text_edit_builder()); + }) +} + +fn collect_path_segments_raw( + segments: &mut Vec, + mut path: ast::Path, +) -> Option { + let oldlen = segments.len(); + loop { + let mut children = path.syntax().children_with_tokens(); + let (first, second, third) = ( + children.next().map(|n| (n.clone(), n.kind())), + children.next().map(|n| (n.clone(), n.kind())), + children.next().map(|n| (n.clone(), n.kind())), + ); + match (first, second, third) { + (Some((subpath, PATH)), Some((_, T![::])), Some((segment, PATH_SEGMENT))) => { + path = ast::Path::cast(subpath.as_node()?.clone())?; + segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?); + } + (Some((segment, PATH_SEGMENT)), _, _) => { + segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?); + break; + } + (_, _, _) => return None, + } + } + // We need to reverse only the new added segments + let only_new_segments = segments.split_at_mut(oldlen).1; + only_new_segments.reverse(); + Some(segments.len() - oldlen) +} + +fn fmt_segments(segments: &[SmolStr]) -> String { + let mut buf = String::new(); + fmt_segments_raw(segments, &mut buf); + buf +} + +fn fmt_segments_raw(segments: &[SmolStr], buf: &mut String) { + let mut iter = segments.iter(); + if let Some(s) = iter.next() { + buf.push_str(s); + } + for s in iter { + buf.push_str("::"); + buf.push_str(s); + } +} + +/// Returns the number of common segments. +fn compare_path_segments(left: &[SmolStr], right: &[ast::PathSegment]) -> usize { + left.iter().zip(right).take_while(|(l, r)| compare_path_segment(l, r)).count() +} + +fn compare_path_segment(a: &SmolStr, b: &ast::PathSegment) -> bool { + if let Some(kb) = b.kind() { + match kb { + ast::PathSegmentKind::Name(nameref_b) => a == nameref_b.text(), + ast::PathSegmentKind::SelfKw => a == "self", + ast::PathSegmentKind::SuperKw => a == "super", + ast::PathSegmentKind::CrateKw => a == "crate", + ast::PathSegmentKind::Type { .. } => false, // not allowed in imports + } + } else { + false + } +} + +fn compare_path_segment_with_name(a: &SmolStr, b: &ast::Name) -> bool { + a == b.text() +} + +#[derive(Clone, Debug)] +enum ImportAction { + Nothing, + // Add a brand new use statement. + AddNewUse { + anchor: Option, // anchor node + add_after_anchor: bool, + }, + + // To split an existing use statement creating a nested import. + AddNestedImport { + // how may segments matched with the target path + common_segments: usize, + path_to_split: ast::Path, + // the first segment of path_to_split we want to add into the new nested list + first_segment_to_split: Option, + // Wether to add 'self' in addition to the target path + add_self: bool, + }, + // To add the target path to an existing nested import tree list. + AddInTreeList { + common_segments: usize, + // The UseTreeList where to add the target path + tree_list: ast::UseTreeList, + add_self: bool, + }, +} + +impl ImportAction { + fn add_new_use(anchor: Option, add_after_anchor: bool) -> Self { + ImportAction::AddNewUse { anchor, add_after_anchor } + } + + fn add_nested_import( + common_segments: usize, + path_to_split: ast::Path, + first_segment_to_split: Option, + add_self: bool, + ) -> Self { + ImportAction::AddNestedImport { + common_segments, + path_to_split, + first_segment_to_split, + add_self, + } + } + + fn add_in_tree_list( + common_segments: usize, + tree_list: ast::UseTreeList, + add_self: bool, + ) -> Self { + ImportAction::AddInTreeList { common_segments, tree_list, add_self } + } + + fn better(left: ImportAction, right: ImportAction) -> ImportAction { + if left.is_better(&right) { + left + } else { + right + } + } + + fn is_better(&self, other: &ImportAction) -> bool { + match (self, other) { + (ImportAction::Nothing, _) => true, + (ImportAction::AddInTreeList { .. }, ImportAction::Nothing) => false, + ( + ImportAction::AddNestedImport { common_segments: n, .. }, + ImportAction::AddInTreeList { common_segments: m, .. }, + ) + | ( + ImportAction::AddInTreeList { common_segments: n, .. }, + ImportAction::AddNestedImport { common_segments: m, .. }, + ) + | ( + ImportAction::AddInTreeList { common_segments: n, .. }, + ImportAction::AddInTreeList { common_segments: m, .. }, + ) + | ( + ImportAction::AddNestedImport { common_segments: n, .. }, + ImportAction::AddNestedImport { common_segments: m, .. }, + ) => n > m, + (ImportAction::AddInTreeList { .. }, _) => true, + (ImportAction::AddNestedImport { .. }, ImportAction::Nothing) => false, + (ImportAction::AddNestedImport { .. }, _) => true, + (ImportAction::AddNewUse { .. }, _) => false, + } + } +} + +// Find out the best ImportAction to import target path against current_use_tree. +// If current_use_tree has a nested import the function gets called recursively on every UseTree inside a UseTreeList. +fn walk_use_tree_for_best_action( + current_path_segments: &mut Vec, // buffer containing path segments + current_parent_use_tree_list: Option, // will be Some value if we are in a nested import + current_use_tree: ast::UseTree, // the use tree we are currently examinating + target: &[SmolStr], // the path we want to import +) -> ImportAction { + // We save the number of segments in the buffer so we can restore the correct segments + // before returning. Recursive call will add segments so we need to delete them. + let prev_len = current_path_segments.len(); + + let tree_list = current_use_tree.use_tree_list(); + let alias = current_use_tree.alias(); + + let path = match current_use_tree.path() { + Some(path) => path, + None => { + // If the use item don't have a path, it means it's broken (syntax error) + return ImportAction::add_new_use( + current_use_tree + .syntax() + .ancestors() + .find_map(ast::UseItem::cast) + .map(|it| it.syntax().clone()), + true, + ); + } + }; + + // This can happen only if current_use_tree is a direct child of a UseItem + if let Some(name) = alias.and_then(|it| it.name()) { + if compare_path_segment_with_name(&target[0], &name) { + return ImportAction::Nothing; + } + } + + collect_path_segments_raw(current_path_segments, path.clone()); + + // We compare only the new segments added in the line just above. + // The first prev_len segments were already compared in 'parent' recursive calls. + let left = target.split_at(prev_len).1; + let right = current_path_segments.split_at(prev_len).1; + let common = compare_path_segments(left, &right); + let mut action = match common { + 0 => ImportAction::add_new_use( + // e.g: target is std::fmt and we can have + // use foo::bar + // We add a brand new use statement + current_use_tree + .syntax() + .ancestors() + .find_map(ast::UseItem::cast) + .map(|it| it.syntax().clone()), + true, + ), + common if common == left.len() && left.len() == right.len() => { + // e.g: target is std::fmt and we can have + // 1- use std::fmt; + // 2- use std::fmt::{ ... } + if let Some(list) = tree_list { + // In case 2 we need to add self to the nested list + // unless it's already there + let has_self = list.use_trees().map(|it| it.path()).any(|p| { + p.and_then(|it| it.segment()) + .and_then(|it| it.kind()) + .filter(|k| *k == ast::PathSegmentKind::SelfKw) + .is_some() + }); + + if has_self { + ImportAction::Nothing + } else { + ImportAction::add_in_tree_list(current_path_segments.len(), list, true) + } + } else { + // Case 1 + ImportAction::Nothing + } + } + common if common != left.len() && left.len() == right.len() => { + // e.g: target is std::fmt and we have + // use std::io; + // We need to split. + let segments_to_split = current_path_segments.split_at(prev_len + common).1; + ImportAction::add_nested_import( + prev_len + common, + path, + Some(segments_to_split[0].clone()), + false, + ) + } + common if common == right.len() && left.len() > right.len() => { + // e.g: target is std::fmt and we can have + // 1- use std; + // 2- use std::{ ... }; + + // fallback action + let mut better_action = ImportAction::add_new_use( + current_use_tree + .syntax() + .ancestors() + .find_map(ast::UseItem::cast) + .map(|it| it.syntax().clone()), + true, + ); + if let Some(list) = tree_list { + // Case 2, check recursively if the path is already imported in the nested list + for u in list.use_trees() { + let child_action = walk_use_tree_for_best_action( + current_path_segments, + Some(list.clone()), + u, + target, + ); + if child_action.is_better(&better_action) { + better_action = child_action; + if let ImportAction::Nothing = better_action { + return better_action; + } + } + } + } else { + // Case 1, split adding self + better_action = ImportAction::add_nested_import(prev_len + common, path, None, true) + } + better_action + } + common if common == left.len() && left.len() < right.len() => { + // e.g: target is std::fmt and we can have + // use std::fmt::Debug; + let segments_to_split = current_path_segments.split_at(prev_len + common).1; + ImportAction::add_nested_import( + prev_len + common, + path, + Some(segments_to_split[0].clone()), + true, + ) + } + common if common < left.len() && common < right.len() => { + // e.g: target is std::fmt::nested::Debug + // use std::fmt::Display + let segments_to_split = current_path_segments.split_at(prev_len + common).1; + ImportAction::add_nested_import( + prev_len + common, + path, + Some(segments_to_split[0].clone()), + false, + ) + } + _ => unreachable!(), + }; + + // If we are inside a UseTreeList adding a use statement become adding to the existing + // tree list. + action = match (current_parent_use_tree_list, action.clone()) { + (Some(use_tree_list), ImportAction::AddNewUse { .. }) => { + ImportAction::add_in_tree_list(prev_len, use_tree_list, false) + } + (_, _) => action, + }; + + // We remove the segments added + current_path_segments.truncate(prev_len); + action +} + +fn best_action_for_target( + container: SyntaxNode, + anchor: SyntaxNode, + target: &[SmolStr], +) -> ImportAction { + let mut storage = Vec::with_capacity(16); // this should be the only allocation + let best_action = container + .children() + .filter_map(ast::UseItem::cast) + .filter_map(|it| it.use_tree()) + .map(|u| walk_use_tree_for_best_action(&mut storage, None, u, target)) + .fold(None, |best, a| match best { + Some(best) => Some(ImportAction::better(best, a)), + None => Some(a), + }); + + match best_action { + Some(action) => action, + None => { + // We have no action and no UseItem was found in container so we find + // another item and we use it as anchor. + // If there are no items above, we choose the target path itself as anchor. + // todo: we should include even whitespace blocks as anchor candidates + let anchor = container + .children() + .find(|n| n.text_range().start() < anchor.text_range().start()) + .or_else(|| Some(anchor)); + + ImportAction::add_new_use(anchor, false) + } + } +} + +fn make_assist(action: &ImportAction, target: &[SmolStr], edit: &mut TextEditBuilder) { + match action { + ImportAction::AddNewUse { anchor, add_after_anchor } => { + make_assist_add_new_use(anchor, *add_after_anchor, target, edit) + } + ImportAction::AddInTreeList { common_segments, tree_list, add_self } => { + // We know that the fist n segments already exists in the use statement we want + // to modify, so we want to add only the last target.len() - n segments. + let segments_to_add = target.split_at(*common_segments).1; + make_assist_add_in_tree_list(tree_list, segments_to_add, *add_self, edit) + } + ImportAction::AddNestedImport { + common_segments, + path_to_split, + first_segment_to_split, + add_self, + } => { + let segments_to_add = target.split_at(*common_segments).1; + make_assist_add_nested_import( + path_to_split, + first_segment_to_split, + segments_to_add, + *add_self, + edit, + ) + } + _ => {} + } +} + +fn make_assist_add_new_use( + anchor: &Option, + after: bool, + target: &[SmolStr], + edit: &mut TextEditBuilder, +) { + if let Some(anchor) = anchor { + let indent = ra_fmt::leading_indent(anchor); + let mut buf = String::new(); + if after { + buf.push_str("\n"); + if let Some(spaces) = &indent { + buf.push_str(spaces); + } + } + buf.push_str("use "); + fmt_segments_raw(target, &mut buf); + buf.push_str(";"); + if !after { + buf.push_str("\n\n"); + if let Some(spaces) = &indent { + buf.push_str(&spaces); + } + } + let position = if after { anchor.text_range().end() } else { anchor.text_range().start() }; + edit.insert(position, buf); + } +} + +fn make_assist_add_in_tree_list( + tree_list: &ast::UseTreeList, + target: &[SmolStr], + add_self: bool, + edit: &mut TextEditBuilder, +) { + let last = tree_list.use_trees().last(); + if let Some(last) = last { + let mut buf = String::new(); + let comma = last.syntax().siblings(Direction::Next).find(|n| n.kind() == T![,]); + let offset = if let Some(comma) = comma { + comma.text_range().end() + } else { + buf.push_str(","); + last.syntax().text_range().end() + }; + if add_self { + buf.push_str(" self") + } else { + buf.push_str(" "); + } + fmt_segments_raw(target, &mut buf); + edit.insert(offset, buf); + } else { + } +} + +fn make_assist_add_nested_import( + path: &ast::Path, + first_segment_to_split: &Option, + target: &[SmolStr], + add_self: bool, + edit: &mut TextEditBuilder, +) { + let use_tree = path.syntax().ancestors().find_map(ast::UseTree::cast); + if let Some(use_tree) = use_tree { + let (start, add_colon_colon) = if let Some(first_segment_to_split) = first_segment_to_split + { + (first_segment_to_split.syntax().text_range().start(), false) + } else { + (use_tree.syntax().text_range().end(), true) + }; + let end = use_tree.syntax().text_range().end(); + + let mut buf = String::new(); + if add_colon_colon { + buf.push_str("::"); + } + buf.push_str("{"); + if add_self { + buf.push_str("self, "); + } + fmt_segments_raw(target, &mut buf); + if !target.is_empty() { + buf.push_str(", "); + } + edit.insert(start, buf); + edit.insert(end, "}".to_string()); + } +} + +fn apply_auto_import( + container: &SyntaxNode, + path: &ast::Path, + target: &[SmolStr], + edit: &mut TextEditBuilder, +) { + let action = best_action_for_target(container.clone(), path.syntax().clone(), target); + make_assist(&action, target, edit); + if let Some(last) = path.segment() { + // Here we are assuming the assist will provide a correct use statement + // so we can delete the path qualifier + edit.delete(TextRange::from_to( + path.syntax().text_range().start(), + last.syntax().text_range().start(), + )); + } +} + +fn collect_hir_path_segments(path: &hir::Path) -> Option> { + let mut ps = Vec::::with_capacity(10); + match path.kind() { + hir::PathKind::Abs => ps.push("".into()), + hir::PathKind::Crate => ps.push("crate".into()), + hir::PathKind::Plain => {} + hir::PathKind::Super(0) => ps.push("self".into()), + hir::PathKind::Super(lvl) => { + let mut chain = "super".to_string(); + for _ in 0..*lvl { + chain += "::super"; + } + ps.push(chain.into()); + } + hir::PathKind::DollarCrate(_) => return None, + } + ps.extend(path.segments().iter().map(|it| it.name.to_string().into())); + Some(ps) +} + +#[cfg(test)] +mod tests { + use crate::helpers::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn test_auto_import_add_use_no_anchor() { + check_assist( + add_import, + " +std::fmt::Debug<|> + ", + " +use std::fmt::Debug; + +Debug<|> + ", + ); + } + #[test] + fn test_auto_import_add_use_no_anchor_with_item_below() { + check_assist( + add_import, + " +std::fmt::Debug<|> + +fn main() { +} + ", + " +use std::fmt::Debug; + +Debug<|> + +fn main() { +} + ", + ); + } + + #[test] + fn test_auto_import_add_use_no_anchor_with_item_above() { + check_assist( + add_import, + " +fn main() { +} + +std::fmt::Debug<|> + ", + " +use std::fmt::Debug; + +fn main() { +} + +Debug<|> + ", + ); + } + + #[test] + fn test_auto_import_add_use_no_anchor_2seg() { + check_assist( + add_import, + " +std::fmt<|>::Debug + ", + " +use std::fmt; + +fmt<|>::Debug + ", + ); + } + + #[test] + fn test_auto_import_add_use() { + check_assist( + add_import, + " +use stdx; + +impl std::fmt::Debug<|> for Foo { +} + ", + " +use stdx; +use std::fmt::Debug; + +impl Debug<|> for Foo { +} + ", + ); + } + + #[test] + fn test_auto_import_file_use_other_anchor() { + check_assist( + add_import, + " +impl std::fmt::Debug<|> for Foo { +} + ", + " +use std::fmt::Debug; + +impl Debug<|> for Foo { +} + ", + ); + } + + #[test] + fn test_auto_import_add_use_other_anchor_indent() { + check_assist( + add_import, + " + impl std::fmt::Debug<|> for Foo { + } + ", + " + use std::fmt::Debug; + + impl Debug<|> for Foo { + } + ", + ); + } + + #[test] + fn test_auto_import_split_different() { + check_assist( + add_import, + " +use std::fmt; + +impl std::io<|> for Foo { +} + ", + " +use std::{io, fmt}; + +impl io<|> for Foo { +} + ", + ); + } + + #[test] + fn test_auto_import_split_self_for_use() { + check_assist( + add_import, + " +use std::fmt; + +impl std::fmt::Debug<|> for Foo { +} + ", + " +use std::fmt::{self, Debug, }; + +impl Debug<|> for Foo { +} + ", + ); + } + + #[test] + fn test_auto_import_split_self_for_target() { + check_assist( + add_import, + " +use std::fmt::Debug; + +impl std::fmt<|> for Foo { +} + ", + " +use std::fmt::{self, Debug}; + +impl fmt<|> for Foo { +} + ", + ); + } + + #[test] + fn test_auto_import_add_to_nested_self_nested() { + check_assist( + add_import, + " +use std::fmt::{Debug, nested::{Display}}; + +impl std::fmt::nested<|> for Foo { +} +", + " +use std::fmt::{Debug, nested::{Display, self}}; + +impl nested<|> for Foo { +} +", + ); + } + + #[test] + fn test_auto_import_add_to_nested_self_already_included() { + check_assist( + add_import, + " +use std::fmt::{Debug, nested::{self, Display}}; + +impl std::fmt::nested<|> for Foo { +} +", + " +use std::fmt::{Debug, nested::{self, Display}}; + +impl nested<|> for Foo { +} +", + ); + } + + #[test] + fn test_auto_import_add_to_nested_nested() { + check_assist( + add_import, + " +use std::fmt::{Debug, nested::{Display}}; + +impl std::fmt::nested::Debug<|> for Foo { +} +", + " +use std::fmt::{Debug, nested::{Display, Debug}}; + +impl Debug<|> for Foo { +} +", + ); + } + + #[test] + fn test_auto_import_split_common_target_longer() { + check_assist( + add_import, + " +use std::fmt::Debug; + +impl std::fmt::nested::Display<|> for Foo { +} +", + " +use std::fmt::{nested::Display, Debug}; + +impl Display<|> for Foo { +} +", + ); + } + + #[test] + fn test_auto_import_split_common_use_longer() { + check_assist( + add_import, + " +use std::fmt::nested::Debug; + +impl std::fmt::Display<|> for Foo { +} +", + " +use std::fmt::{Display, nested::Debug}; + +impl Display<|> for Foo { +} +", + ); + } + + #[test] + fn test_auto_import_use_nested_import() { + check_assist( + add_import, + " +use crate::{ + ty::{Substs, Ty}, + AssocItem, +}; + +fn foo() { crate::ty::lower<|>::trait_env() } +", + " +use crate::{ + ty::{Substs, Ty, lower}, + AssocItem, +}; + +fn foo() { lower<|>::trait_env() } +", + ); + } + + #[test] + fn test_auto_import_alias() { + check_assist( + add_import, + " +use std::fmt as foo; + +impl foo::Debug<|> for Foo { +} +", + " +use std::fmt as foo; + +impl Debug<|> for Foo { +} +", + ); + } + + #[test] + fn test_auto_import_not_applicable_one_segment() { + check_assist_not_applicable( + add_import, + " +impl foo<|> for Foo { +} +", + ); + } + + #[test] + fn test_auto_import_not_applicable_in_use() { + check_assist_not_applicable( + add_import, + " +use std::fmt<|>; +", + ); + } + + #[test] + fn test_auto_import_add_use_no_anchor_in_mod_mod() { + check_assist( + add_import, + " +mod foo { + mod bar { + std::fmt::Debug<|> + } +} + ", + " +mod foo { + mod bar { + use std::fmt::Debug; + + Debug<|> + } +} + ", + ); + } +} diff --git a/crates/ra_assists/src/handlers/add_missing_impl_members.rs b/crates/ra_assists/src/handlers/add_missing_impl_members.rs new file mode 100644 index 000000000..448697d31 --- /dev/null +++ b/crates/ra_assists/src/handlers/add_missing_impl_members.rs @@ -0,0 +1,608 @@ +use hir::{db::HirDatabase, HasSource, InFile}; +use ra_syntax::{ + ast::{self, edit, make, AstNode, NameOwner}, + SmolStr, +}; + +use crate::{ + ast_transform::{self, AstTransform, QualifyPaths, SubstituteTypeParams}, + Assist, AssistCtx, AssistId, +}; + +#[derive(PartialEq)] +enum AddMissingImplMembersMode { + DefaultMethodsOnly, + NoDefaultMethods, +} + +// Assist: add_impl_missing_members +// +// Adds scaffold for required impl members. +// +// ``` +// trait Trait { +// Type X; +// fn foo(&self) -> T; +// fn bar(&self) {} +// } +// +// impl Trait for () {<|> +// +// } +// ``` +// -> +// ``` +// trait Trait { +// Type X; +// fn foo(&self) -> T; +// fn bar(&self) {} +// } +// +// impl Trait for () { +// fn foo(&self) -> u32 { unimplemented!() } +// +// } +// ``` +pub(crate) fn add_missing_impl_members(ctx: AssistCtx) -> Option { + add_missing_impl_members_inner( + ctx, + AddMissingImplMembersMode::NoDefaultMethods, + "add_impl_missing_members", + "Implement missing members", + ) +} + +// Assist: add_impl_default_members +// +// Adds scaffold for overriding default impl members. +// +// ``` +// trait Trait { +// Type X; +// fn foo(&self); +// fn bar(&self) {} +// } +// +// impl Trait for () { +// Type X = (); +// fn foo(&self) {}<|> +// +// } +// ``` +// -> +// ``` +// trait Trait { +// Type X; +// fn foo(&self); +// fn bar(&self) {} +// } +// +// impl Trait for () { +// Type X = (); +// fn foo(&self) {} +// fn bar(&self) {} +// +// } +// ``` +pub(crate) fn add_missing_default_members(ctx: AssistCtx) -> Option { + add_missing_impl_members_inner( + ctx, + AddMissingImplMembersMode::DefaultMethodsOnly, + "add_impl_default_members", + "Implement default members", + ) +} + +fn add_missing_impl_members_inner( + ctx: AssistCtx, + mode: AddMissingImplMembersMode, + assist_id: &'static str, + label: &'static str, +) -> Option { + let _p = ra_prof::profile("add_missing_impl_members_inner"); + let impl_node = ctx.find_node_at_offset::()?; + let impl_item_list = impl_node.item_list()?; + + let (trait_, trait_def) = { + let analyzer = ctx.source_analyzer(impl_node.syntax(), None); + + resolve_target_trait_def(ctx.db, &analyzer, &impl_node)? + }; + + let def_name = |item: &ast::ImplItem| -> Option { + match item { + ast::ImplItem::FnDef(def) => def.name(), + ast::ImplItem::TypeAliasDef(def) => def.name(), + ast::ImplItem::ConstDef(def) => def.name(), + } + .map(|it| it.text().clone()) + }; + + let trait_items = trait_def.item_list()?.impl_items(); + let impl_items = impl_item_list.impl_items().collect::>(); + + let missing_items: Vec<_> = trait_items + .filter(|t| def_name(t).is_some()) + .filter(|t| match t { + ast::ImplItem::FnDef(def) => match mode { + AddMissingImplMembersMode::DefaultMethodsOnly => def.body().is_some(), + AddMissingImplMembersMode::NoDefaultMethods => def.body().is_none(), + }, + _ => mode == AddMissingImplMembersMode::NoDefaultMethods, + }) + .filter(|t| impl_items.iter().all(|i| def_name(i) != def_name(t))) + .collect(); + if missing_items.is_empty() { + return None; + } + + let db = ctx.db; + let file_id = ctx.frange.file_id; + let trait_file_id = trait_.source(db).file_id; + + ctx.add_assist(AssistId(assist_id), label, |edit| { + let n_existing_items = impl_item_list.impl_items().count(); + let module = hir::SourceAnalyzer::new( + db, + hir::InFile::new(file_id.into(), impl_node.syntax()), + None, + ) + .module(); + let ast_transform = QualifyPaths::new(db, module) + .or(SubstituteTypeParams::for_trait_impl(db, trait_, impl_node)); + let items = missing_items + .into_iter() + .map(|it| ast_transform::apply(&*ast_transform, InFile::new(trait_file_id, it))) + .map(|it| match it { + ast::ImplItem::FnDef(def) => ast::ImplItem::FnDef(add_body(def)), + _ => it, + }) + .map(|it| edit::strip_attrs_and_docs(&it)); + let new_impl_item_list = impl_item_list.append_items(items); + let cursor_position = { + let first_new_item = new_impl_item_list.impl_items().nth(n_existing_items).unwrap(); + first_new_item.syntax().text_range().start() + }; + + edit.replace_ast(impl_item_list, new_impl_item_list); + edit.set_cursor(cursor_position); + }) +} + +fn add_body(fn_def: ast::FnDef) -> ast::FnDef { + if fn_def.body().is_none() { + fn_def.with_body(make::block_from_expr(make::expr_unimplemented())) + } else { + fn_def + } +} + +/// Given an `ast::ImplBlock`, resolves the target trait (the one being +/// implemented) to a `ast::TraitDef`. +fn resolve_target_trait_def( + db: &impl HirDatabase, + analyzer: &hir::SourceAnalyzer, + impl_block: &ast::ImplBlock, +) -> Option<(hir::Trait, ast::TraitDef)> { + let ast_path = impl_block + .target_trait() + .map(|it| it.syntax().clone()) + .and_then(ast::PathType::cast)? + .path()?; + + match analyzer.resolve_path(db, &ast_path) { + Some(hir::PathResolution::Def(hir::ModuleDef::Trait(def))) => { + Some((def, def.source(db).value)) + } + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::{check_assist, check_assist_not_applicable}; + + #[test] + fn test_add_missing_impl_members() { + check_assist( + add_missing_impl_members, + " +trait Foo { + type Output; + + const CONST: usize = 42; + + fn foo(&self); + fn bar(&self); + fn baz(&self); +} + +struct S; + +impl Foo for S { + fn bar(&self) {} +<|> +}", + " +trait Foo { + type Output; + + const CONST: usize = 42; + + fn foo(&self); + fn bar(&self); + fn baz(&self); +} + +struct S; + +impl Foo for S { + fn bar(&self) {} + <|>type Output; + const CONST: usize = 42; + fn foo(&self) { unimplemented!() } + fn baz(&self) { unimplemented!() } + +}", + ); + } + + #[test] + fn test_copied_overriden_members() { + check_assist( + add_missing_impl_members, + " +trait Foo { + fn foo(&self); + fn bar(&self) -> bool { true } + fn baz(&self) -> u32 { 42 } +} + +struct S; + +impl Foo for S { + fn bar(&self) {} +<|> +}", + " +trait Foo { + fn foo(&self); + fn bar(&self) -> bool { true } + fn baz(&self) -> u32 { 42 } +} + +struct S; + +impl Foo for S { + fn bar(&self) {} + <|>fn foo(&self) { unimplemented!() } + +}", + ); + } + + #[test] + fn test_empty_impl_block() { + check_assist( + add_missing_impl_members, + " +trait Foo { fn foo(&self); } +struct S; +impl Foo for S { <|> }", + " +trait Foo { fn foo(&self); } +struct S; +impl Foo for S { + <|>fn foo(&self) { unimplemented!() } +}", + ); + } + + #[test] + fn fill_in_type_params_1() { + check_assist( + add_missing_impl_members, + " +trait Foo { fn foo(&self, t: T) -> &T; } +struct S; +impl Foo for S { <|> }", + " +trait Foo { fn foo(&self, t: T) -> &T; } +struct S; +impl Foo for S { + <|>fn foo(&self, t: u32) -> &u32 { unimplemented!() } +}", + ); + } + + #[test] + fn fill_in_type_params_2() { + check_assist( + add_missing_impl_members, + " +trait Foo { fn foo(&self, t: T) -> &T; } +struct S; +impl Foo for S { <|> }", + " +trait Foo { fn foo(&self, t: T) -> &T; } +struct S; +impl Foo for S { + <|>fn foo(&self, t: U) -> &U { unimplemented!() } +}", + ); + } + + #[test] + fn test_cursor_after_empty_impl_block() { + check_assist( + add_missing_impl_members, + " +trait Foo { fn foo(&self); } +struct S; +impl Foo for S {}<|>", + " +trait Foo { fn foo(&self); } +struct S; +impl Foo for S { + <|>fn foo(&self) { unimplemented!() } +}", + ) + } + + #[test] + fn test_qualify_path_1() { + check_assist( + add_missing_impl_members, + " +mod foo { + pub struct Bar; + trait Foo { fn foo(&self, bar: Bar); } +} +struct S; +impl foo::Foo for S { <|> }", + " +mod foo { + pub struct Bar; + trait Foo { fn foo(&self, bar: Bar); } +} +struct S; +impl foo::Foo for S { + <|>fn foo(&self, bar: foo::Bar) { unimplemented!() } +}", + ); + } + + #[test] + fn test_qualify_path_generic() { + check_assist( + add_missing_impl_members, + " +mod foo { + pub struct Bar; + trait Foo { fn foo(&self, bar: Bar); } +} +struct S; +impl foo::Foo for S { <|> }", + " +mod foo { + pub struct Bar; + trait Foo { fn foo(&self, bar: Bar); } +} +struct S; +impl foo::Foo for S { + <|>fn foo(&self, bar: foo::Bar) { unimplemented!() } +}", + ); + } + + #[test] + fn test_qualify_path_and_substitute_param() { + check_assist( + add_missing_impl_members, + " +mod foo { + pub struct Bar; + trait Foo { fn foo(&self, bar: Bar); } +} +struct S; +impl foo::Foo for S { <|> }", + " +mod foo { + pub struct Bar; + trait Foo { fn foo(&self, bar: Bar); } +} +struct S; +impl foo::Foo for S { + <|>fn foo(&self, bar: foo::Bar) { unimplemented!() } +}", + ); + } + + #[test] + fn test_substitute_param_no_qualify() { + // when substituting params, the substituted param should not be qualified! + check_assist( + add_missing_impl_members, + " +mod foo { + trait Foo { fn foo(&self, bar: T); } + pub struct Param; +} +struct Param; +struct S; +impl foo::Foo for S { <|> }", + " +mod foo { + trait Foo { fn foo(&self, bar: T); } + pub struct Param; +} +struct Param; +struct S; +impl foo::Foo for S { + <|>fn foo(&self, bar: Param) { unimplemented!() } +}", + ); + } + + #[test] + fn test_qualify_path_associated_item() { + check_assist( + add_missing_impl_members, + " +mod foo { + pub struct Bar; + impl Bar { type Assoc = u32; } + trait Foo { fn foo(&self, bar: Bar::Assoc); } +} +struct S; +impl foo::Foo for S { <|> }", + " +mod foo { + pub struct Bar; + impl Bar { type Assoc = u32; } + trait Foo { fn foo(&self, bar: Bar::Assoc); } +} +struct S; +impl foo::Foo for S { + <|>fn foo(&self, bar: foo::Bar::Assoc) { unimplemented!() } +}", + ); + } + + #[test] + fn test_qualify_path_nested() { + check_assist( + add_missing_impl_members, + " +mod foo { + pub struct Bar; + pub struct Baz; + trait Foo { fn foo(&self, bar: Bar); } +} +struct S; +impl foo::Foo for S { <|> }", + " +mod foo { + pub struct Bar; + pub struct Baz; + trait Foo { fn foo(&self, bar: Bar); } +} +struct S; +impl foo::Foo for S { + <|>fn foo(&self, bar: foo::Bar) { unimplemented!() } +}", + ); + } + + #[test] + fn test_qualify_path_fn_trait_notation() { + check_assist( + add_missing_impl_members, + " +mod foo { + pub trait Fn { type Output; } + trait Foo { fn foo(&self, bar: dyn Fn(u32) -> i32); } +} +struct S; +impl foo::Foo for S { <|> }", + " +mod foo { + pub trait Fn { type Output; } + trait Foo { fn foo(&self, bar: dyn Fn(u32) -> i32); } +} +struct S; +impl foo::Foo for S { + <|>fn foo(&self, bar: dyn Fn(u32) -> i32) { unimplemented!() } +}", + ); + } + + #[test] + fn test_empty_trait() { + check_assist_not_applicable( + add_missing_impl_members, + " +trait Foo; +struct S; +impl Foo for S { <|> }", + ) + } + + #[test] + fn test_ignore_unnamed_trait_members_and_default_methods() { + check_assist_not_applicable( + add_missing_impl_members, + " +trait Foo { + fn (arg: u32); + fn valid(some: u32) -> bool { false } +} +struct S; +impl Foo for S { <|> }", + ) + } + + #[test] + fn test_with_docstring_and_attrs() { + check_assist( + add_missing_impl_members, + r#" +#[doc(alias = "test alias")] +trait Foo { + /// doc string + type Output; + + #[must_use] + fn foo(&self); +} +struct S; +impl Foo for S {}<|>"#, + r#" +#[doc(alias = "test alias")] +trait Foo { + /// doc string + type Output; + + #[must_use] + fn foo(&self); +} +struct S; +impl Foo for S { + <|>type Output; + fn foo(&self) { unimplemented!() } +}"#, + ) + } + + #[test] + fn test_default_methods() { + check_assist( + add_missing_default_members, + " +trait Foo { + type Output; + + const CONST: usize = 42; + + fn valid(some: u32) -> bool { false } + fn foo(some: u32) -> bool; +} +struct S; +impl Foo for S { <|> }", + " +trait Foo { + type Output; + + const CONST: usize = 42; + + fn valid(some: u32) -> bool { false } + fn foo(some: u32) -> bool; +} +struct S; +impl Foo for S { + <|>fn valid(some: u32) -> bool { false } +}", + ) + } +} diff --git a/crates/ra_assists/src/handlers/add_new.rs b/crates/ra_assists/src/handlers/add_new.rs new file mode 100644 index 000000000..a08639311 --- /dev/null +++ b/crates/ra_assists/src/handlers/add_new.rs @@ -0,0 +1,430 @@ +use format_buf::format; +use hir::InFile; +use join_to_string::join; +use ra_syntax::{ + ast::{ + self, AstNode, NameOwner, StructKind, TypeAscriptionOwner, TypeParamsOwner, VisibilityOwner, + }, + TextUnit, T, +}; +use std::fmt::Write; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: add_new +// +// Adds a new inherent impl for a type. +// +// ``` +// struct Ctx { +// data: T,<|> +// } +// ``` +// -> +// ``` +// struct Ctx { +// data: T, +// } +// +// impl Ctx { +// fn new(data: T) -> Self { Self { data } } +// } +// +// ``` +pub(crate) fn add_new(ctx: AssistCtx) -> Option { + let strukt = ctx.find_node_at_offset::()?; + + // We want to only apply this to non-union structs with named fields + let field_list = match strukt.kind() { + StructKind::Record(named) => named, + _ => return None, + }; + + // Return early if we've found an existing new fn + let impl_block = find_struct_impl(&ctx, &strukt)?; + + ctx.add_assist(AssistId("add_new"), "Add default constructor", |edit| { + edit.target(strukt.syntax().text_range()); + + let mut buf = String::with_capacity(512); + + if impl_block.is_some() { + buf.push('\n'); + } + + let vis = strukt.visibility().map(|v| format!("{} ", v.syntax())); + let vis = vis.as_ref().map(String::as_str).unwrap_or(""); + write!(&mut buf, " {}fn new(", vis).unwrap(); + + join(field_list.fields().filter_map(|f| { + Some(format!("{}: {}", f.name()?.syntax().text(), f.ascribed_type()?.syntax().text())) + })) + .separator(", ") + .to_buf(&mut buf); + + buf.push_str(") -> Self { Self {"); + + join(field_list.fields().filter_map(|f| Some(f.name()?.syntax().text()))) + .separator(", ") + .surround_with(" ", " ") + .to_buf(&mut buf); + + buf.push_str("} }"); + + let (start_offset, end_offset) = impl_block + .and_then(|impl_block| { + buf.push('\n'); + let start = impl_block + .syntax() + .descendants_with_tokens() + .find(|t| t.kind() == T!['{'])? + .text_range() + .end(); + + Some((start, TextUnit::from_usize(1))) + }) + .unwrap_or_else(|| { + buf = generate_impl_text(&strukt, &buf); + let start = strukt.syntax().text_range().end(); + + (start, TextUnit::from_usize(3)) + }); + + edit.set_cursor(start_offset + TextUnit::of_str(&buf) - end_offset); + edit.insert(start_offset, buf); + }) +} + +// Generates the surrounding `impl Type { }` including type and lifetime +// parameters +fn generate_impl_text(strukt: &ast::StructDef, code: &str) -> String { + let type_params = strukt.type_param_list(); + let mut buf = String::with_capacity(code.len()); + buf.push_str("\n\nimpl"); + if let Some(type_params) = &type_params { + format!(buf, "{}", type_params.syntax()); + } + buf.push_str(" "); + buf.push_str(strukt.name().unwrap().text().as_str()); + if let Some(type_params) = type_params { + let lifetime_params = type_params + .lifetime_params() + .filter_map(|it| it.lifetime_token()) + .map(|it| it.text().clone()); + let type_params = + type_params.type_params().filter_map(|it| it.name()).map(|it| it.text().clone()); + join(lifetime_params.chain(type_params)).surround_with("<", ">").to_buf(&mut buf); + } + + format!(&mut buf, " {{\n{}\n}}\n", code); + + buf +} + +// Uses a syntax-driven approach to find any impl blocks for the struct that +// exist within the module/file +// +// Returns `None` if we've found an existing `new` fn +// +// FIXME: change the new fn checking to a more semantic approach when that's more +// viable (e.g. we process proc macros, etc) +fn find_struct_impl(ctx: &AssistCtx, strukt: &ast::StructDef) -> Option> { + let db = ctx.db; + let module = strukt.syntax().ancestors().find(|node| { + ast::Module::can_cast(node.kind()) || ast::SourceFile::can_cast(node.kind()) + })?; + let mut sb = ctx.source_binder(); + + let struct_ty = { + let src = InFile { file_id: ctx.frange.file_id.into(), value: strukt.clone() }; + sb.to_def(src)?.ty(db) + }; + + let block = module.descendants().filter_map(ast::ImplBlock::cast).find_map(|impl_blk| { + let src = InFile { file_id: ctx.frange.file_id.into(), value: impl_blk.clone() }; + let blk = sb.to_def(src)?; + + let same_ty = blk.target_ty(db) == struct_ty; + let not_trait_impl = blk.target_trait(db).is_none(); + + if !(same_ty && not_trait_impl) { + None + } else { + Some(impl_blk) + } + }); + + if let Some(ref impl_blk) = block { + if has_new_fn(impl_blk) { + return None; + } + } + + Some(block) +} + +fn has_new_fn(imp: &ast::ImplBlock) -> bool { + if let Some(il) = imp.item_list() { + for item in il.impl_items() { + if let ast::ImplItem::FnDef(f) = item { + if let Some(name) = f.name() { + if name.text().eq_ignore_ascii_case("new") { + return true; + } + } + } + } + } + + false +} + +#[cfg(test)] +mod tests { + use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; + + use super::*; + + #[test] + #[rustfmt::skip] + fn test_add_new() { + // Check output of generation + check_assist( + add_new, +"struct Foo {<|>}", +"struct Foo {} + +impl Foo { + fn new() -> Self { Self { } }<|> +} +", + ); + check_assist( + add_new, +"struct Foo {<|>}", +"struct Foo {} + +impl Foo { + fn new() -> Self { Self { } }<|> +} +", + ); + check_assist( + add_new, +"struct Foo<'a, T: Foo<'a>> {<|>}", +"struct Foo<'a, T: Foo<'a>> {} + +impl<'a, T: Foo<'a>> Foo<'a, T> { + fn new() -> Self { Self { } }<|> +} +", + ); + check_assist( + add_new, +"struct Foo { baz: String <|>}", +"struct Foo { baz: String } + +impl Foo { + fn new(baz: String) -> Self { Self { baz } }<|> +} +", + ); + check_assist( + add_new, +"struct Foo { baz: String, qux: Vec <|>}", +"struct Foo { baz: String, qux: Vec } + +impl Foo { + fn new(baz: String, qux: Vec) -> Self { Self { baz, qux } }<|> +} +", + ); + + // Check that visibility modifiers don't get brought in for fields + check_assist( + add_new, +"struct Foo { pub baz: String, pub qux: Vec <|>}", +"struct Foo { pub baz: String, pub qux: Vec } + +impl Foo { + fn new(baz: String, qux: Vec) -> Self { Self { baz, qux } }<|> +} +", + ); + + // Check that it reuses existing impls + check_assist( + add_new, +"struct Foo {<|>} + +impl Foo {} +", +"struct Foo {} + +impl Foo { + fn new() -> Self { Self { } }<|> +} +", + ); + check_assist( + add_new, +"struct Foo {<|>} + +impl Foo { + fn qux(&self) {} +} +", +"struct Foo {} + +impl Foo { + fn new() -> Self { Self { } }<|> + + fn qux(&self) {} +} +", + ); + + check_assist( + add_new, +"struct Foo {<|>} + +impl Foo { + fn qux(&self) {} + fn baz() -> i32 { + 5 + } +} +", +"struct Foo {} + +impl Foo { + fn new() -> Self { Self { } }<|> + + fn qux(&self) {} + fn baz() -> i32 { + 5 + } +} +", + ); + + // Check visibility of new fn based on struct + check_assist( + add_new, +"pub struct Foo {<|>}", +"pub struct Foo {} + +impl Foo { + pub fn new() -> Self { Self { } }<|> +} +", + ); + check_assist( + add_new, +"pub(crate) struct Foo {<|>}", +"pub(crate) struct Foo {} + +impl Foo { + pub(crate) fn new() -> Self { Self { } }<|> +} +", + ); + } + + #[test] + fn add_new_not_applicable_if_fn_exists() { + check_assist_not_applicable( + add_new, + " +struct Foo {<|>} + +impl Foo { + fn new() -> Self { + Self + } +}", + ); + + check_assist_not_applicable( + add_new, + " +struct Foo {<|>} + +impl Foo { + fn New() -> Self { + Self + } +}", + ); + } + + #[test] + fn add_new_target() { + check_assist_target( + add_new, + " +struct SomeThingIrrelevant; +/// Has a lifetime parameter +struct Foo<'a, T: Foo<'a>> {<|>} +struct EvenMoreIrrelevant; +", + "/// Has a lifetime parameter +struct Foo<'a, T: Foo<'a>> {}", + ); + } + + #[test] + fn test_unrelated_new() { + check_assist( + add_new, + r##" +pub struct AstId { + file_id: HirFileId, + file_ast_id: FileAstId, +} + +impl AstId { + pub fn new(file_id: HirFileId, file_ast_id: FileAstId) -> AstId { + AstId { file_id, file_ast_id } + } +} + +pub struct Source { + pub file_id: HirFileId,<|> + pub ast: T, +} + +impl Source { + pub fn map U, U>(self, f: F) -> Source { + Source { file_id: self.file_id, ast: f(self.ast) } + } +} +"##, + r##" +pub struct AstId { + file_id: HirFileId, + file_ast_id: FileAstId, +} + +impl AstId { + pub fn new(file_id: HirFileId, file_ast_id: FileAstId) -> AstId { + AstId { file_id, file_ast_id } + } +} + +pub struct Source { + pub file_id: HirFileId, + pub ast: T, +} + +impl Source { + pub fn new(file_id: HirFileId, ast: T) -> Self { Self { file_id, ast } }<|> + + pub fn map U, U>(self, f: F) -> Source { + Source { file_id: self.file_id, ast: f(self.ast) } + } +} +"##, + ); + } +} diff --git a/crates/ra_assists/src/handlers/apply_demorgan.rs b/crates/ra_assists/src/handlers/apply_demorgan.rs new file mode 100644 index 000000000..ba08a8223 --- /dev/null +++ b/crates/ra_assists/src/handlers/apply_demorgan.rs @@ -0,0 +1,90 @@ +use super::invert_if::invert_boolean_expression; +use ra_syntax::ast::{self, AstNode}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: apply_demorgan +// +// Apply [De Morgan's law](https://en.wikipedia.org/wiki/De_Morgan%27s_laws). +// This transforms expressions of the form `!l || !r` into `!(l && r)`. +// This also works with `&&`. This assist can only be applied with the cursor +// on either `||` or `&&`, with both operands being a negation of some kind. +// This means something of the form `!x` or `x != y`. +// +// ``` +// fn main() { +// if x != 4 ||<|> !y {} +// } +// ``` +// -> +// ``` +// fn main() { +// if !(x == 4 && y) {} +// } +// ``` +pub(crate) fn apply_demorgan(ctx: AssistCtx) -> Option { + let expr = ctx.find_node_at_offset::()?; + let op = expr.op_kind()?; + let op_range = expr.op_token()?.text_range(); + let opposite_op = opposite_logic_op(op)?; + let cursor_in_range = ctx.frange.range.is_subrange(&op_range); + if !cursor_in_range { + return None; + } + + let lhs = expr.lhs()?; + let lhs_range = lhs.syntax().text_range(); + let not_lhs = invert_boolean_expression(lhs); + + let rhs = expr.rhs()?; + let rhs_range = rhs.syntax().text_range(); + let not_rhs = invert_boolean_expression(rhs); + + ctx.add_assist(AssistId("apply_demorgan"), "Apply De Morgan's law", |edit| { + edit.target(op_range); + edit.replace(op_range, opposite_op); + edit.replace(lhs_range, format!("!({}", not_lhs.syntax().text())); + edit.replace(rhs_range, format!("{})", not_rhs.syntax().text())); + }) +} + +// Return the opposite text for a given logical operator, if it makes sense +fn opposite_logic_op(kind: ast::BinOp) -> Option<&'static str> { + match kind { + ast::BinOp::BooleanOr => Some("&&"), + ast::BinOp::BooleanAnd => Some("||"), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::helpers::{check_assist, check_assist_not_applicable}; + + #[test] + fn demorgan_turns_and_into_or() { + check_assist(apply_demorgan, "fn f() { !x &&<|> !x }", "fn f() { !(x ||<|> x) }") + } + + #[test] + fn demorgan_turns_or_into_and() { + check_assist(apply_demorgan, "fn f() { !x ||<|> !x }", "fn f() { !(x &&<|> x) }") + } + + #[test] + fn demorgan_removes_inequality() { + check_assist(apply_demorgan, "fn f() { x != x ||<|> !x }", "fn f() { !(x == x &&<|> x) }") + } + + #[test] + fn demorgan_general_case() { + check_assist(apply_demorgan, "fn f() { x ||<|> x }", "fn f() { !(!x &&<|> !x) }") + } + + #[test] + fn demorgan_doesnt_apply_with_cursor_not_on_op() { + check_assist_not_applicable(apply_demorgan, "fn f() { <|> !x || !x }") + } +} diff --git a/crates/ra_assists/src/handlers/auto_import.rs b/crates/ra_assists/src/handlers/auto_import.rs new file mode 100644 index 000000000..84b5474f9 --- /dev/null +++ b/crates/ra_assists/src/handlers/auto_import.rs @@ -0,0 +1,258 @@ +use hir::ModPath; +use ra_ide_db::imports_locator::ImportsLocator; +use ra_syntax::{ + ast::{self, AstNode}, + SyntaxNode, +}; + +use crate::{ + assist_ctx::{ActionBuilder, Assist, AssistCtx}, + auto_import_text_edit, AssistId, +}; +use std::collections::BTreeSet; + +// Assist: auto_import +// +// If the name is unresolved, provides all possible imports for it. +// +// ``` +// fn main() { +// let map = HashMap<|>::new(); +// } +// # pub mod std { pub mod collections { pub struct HashMap { } } } +// ``` +// -> +// ``` +// use std::collections::HashMap; +// +// fn main() { +// let map = HashMap::new(); +// } +// # pub mod std { pub mod collections { pub struct HashMap { } } } +// ``` +pub(crate) fn auto_import(ctx: AssistCtx) -> Option { + let path_to_import: ast::Path = ctx.find_node_at_offset()?; + let path_to_import_syntax = path_to_import.syntax(); + if path_to_import_syntax.ancestors().find_map(ast::UseItem::cast).is_some() { + return None; + } + let name_to_import = + path_to_import_syntax.descendants().find_map(ast::NameRef::cast)?.syntax().to_string(); + + let module = path_to_import_syntax.ancestors().find_map(ast::Module::cast); + let position = match module.and_then(|it| it.item_list()) { + Some(item_list) => item_list.syntax().clone(), + None => { + let current_file = path_to_import_syntax.ancestors().find_map(ast::SourceFile::cast)?; + current_file.syntax().clone() + } + }; + let source_analyzer = ctx.source_analyzer(&position, None); + let module_with_name_to_import = source_analyzer.module()?; + if source_analyzer.resolve_path(ctx.db, &path_to_import).is_some() { + return None; + } + + let mut imports_locator = ImportsLocator::new(ctx.db); + + let proposed_imports = imports_locator + .find_imports(&name_to_import) + .into_iter() + .filter_map(|module_def| module_with_name_to_import.find_use_path(ctx.db, module_def)) + .filter(|use_path| !use_path.segments.is_empty()) + .take(20) + .collect::>(); + + if proposed_imports.is_empty() { + return None; + } + + ctx.add_assist_group(AssistId("auto_import"), format!("Import {}", name_to_import), || { + proposed_imports + .into_iter() + .map(|import| import_to_action(import, &position, &path_to_import_syntax)) + .collect() + }) +} + +fn import_to_action(import: ModPath, position: &SyntaxNode, anchor: &SyntaxNode) -> ActionBuilder { + let mut action_builder = ActionBuilder::default(); + action_builder.label(format!("Import `{}`", &import)); + auto_import_text_edit(position, anchor, &import, action_builder.text_edit_builder()); + action_builder +} + +#[cfg(test)] +mod tests { + use crate::helpers::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn applicable_when_found_an_import() { + check_assist( + auto_import, + r" + <|>PubStruct + + pub mod PubMod { + pub struct PubStruct; + } + ", + r" + <|>use PubMod::PubStruct; + + PubStruct + + pub mod PubMod { + pub struct PubStruct; + } + ", + ); + } + + #[test] + fn auto_imports_are_merged() { + check_assist( + auto_import, + r" + use PubMod::PubStruct1; + + struct Test { + test: Pub<|>Struct2, + } + + pub mod PubMod { + pub struct PubStruct1; + pub struct PubStruct2 { + _t: T, + } + } + ", + r" + use PubMod::{PubStruct2, PubStruct1}; + + struct Test { + test: Pub<|>Struct2, + } + + pub mod PubMod { + pub struct PubStruct1; + pub struct PubStruct2 { + _t: T, + } + } + ", + ); + } + + #[test] + fn applicable_when_found_multiple_imports() { + check_assist( + auto_import, + r" + PubSt<|>ruct + + pub mod PubMod1 { + pub struct PubStruct; + } + pub mod PubMod2 { + pub struct PubStruct; + } + pub mod PubMod3 { + pub struct PubStruct; + } + ", + r" + use PubMod1::PubStruct; + + PubSt<|>ruct + + pub mod PubMod1 { + pub struct PubStruct; + } + pub mod PubMod2 { + pub struct PubStruct; + } + pub mod PubMod3 { + pub struct PubStruct; + } + ", + ); + } + + #[test] + fn not_applicable_for_already_imported_types() { + check_assist_not_applicable( + auto_import, + r" + use PubMod::PubStruct; + + PubStruct<|> + + pub mod PubMod { + pub struct PubStruct; + } + ", + ); + } + + #[test] + fn not_applicable_for_types_with_private_paths() { + check_assist_not_applicable( + auto_import, + r" + PrivateStruct<|> + + pub mod PubMod { + struct PrivateStruct; + } + ", + ); + } + + #[test] + fn not_applicable_when_no_imports_found() { + check_assist_not_applicable( + auto_import, + " + PubStruct<|>", + ); + } + + #[test] + fn not_applicable_in_import_statements() { + check_assist_not_applicable( + auto_import, + r" + use PubStruct<|>; + + pub mod PubMod { + pub struct PubStruct; + }", + ); + } + + #[test] + fn function_import() { + check_assist( + auto_import, + r" + test_function<|> + + pub mod PubMod { + pub fn test_function() {}; + } + ", + r" + use PubMod::test_function; + + test_function<|> + + pub mod PubMod { + pub fn test_function() {}; + } + ", + ); + } +} diff --git a/crates/ra_assists/src/handlers/change_visibility.rs b/crates/ra_assists/src/handlers/change_visibility.rs new file mode 100644 index 000000000..f325b6f92 --- /dev/null +++ b/crates/ra_assists/src/handlers/change_visibility.rs @@ -0,0 +1,167 @@ +use ra_syntax::{ + ast::{self, NameOwner, VisibilityOwner}, + AstNode, + SyntaxKind::{ + ATTR, COMMENT, ENUM_DEF, FN_DEF, IDENT, MODULE, STRUCT_DEF, TRAIT_DEF, VISIBILITY, + WHITESPACE, + }, + SyntaxNode, TextUnit, T, +}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: change_visibility +// +// Adds or changes existing visibility specifier. +// +// ``` +// <|>fn frobnicate() {} +// ``` +// -> +// ``` +// pub(crate) fn frobnicate() {} +// ``` +pub(crate) fn change_visibility(ctx: AssistCtx) -> Option { + if let Some(vis) = ctx.find_node_at_offset::() { + return change_vis(ctx, vis); + } + add_vis(ctx) +} + +fn add_vis(ctx: AssistCtx) -> Option { + let item_keyword = ctx.token_at_offset().find(|leaf| match leaf.kind() { + T![fn] | T![mod] | T![struct] | T![enum] | T![trait] => true, + _ => false, + }); + + let (offset, target) = 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), keyword.text_range()) + } else { + let ident = ctx.token_at_offset().find(|leaf| leaf.kind() == IDENT)?; + let field = ident.parent().ancestors().find_map(ast::RecordFieldDef::cast)?; + if field.name()?.syntax().text_range() != ident.text_range() && field.visibility().is_some() + { + return None; + } + (vis_offset(field.syntax()), ident.text_range()) + }; + + ctx.add_assist(AssistId("change_visibility"), "Change visibility to pub(crate)", |edit| { + edit.target(target); + edit.insert(offset, "pub(crate) "); + edit.set_cursor(offset); + }) +} + +fn vis_offset(node: &SyntaxNode) -> TextUnit { + node.children_with_tokens() + .skip_while(|it| match it.kind() { + WHITESPACE | COMMENT | ATTR => true, + _ => false, + }) + .next() + .map(|it| it.text_range().start()) + .unwrap_or_else(|| node.text_range().start()) +} + +fn change_vis(ctx: AssistCtx, vis: ast::Visibility) -> Option { + if vis.syntax().text() == "pub" { + return ctx.add_assist( + AssistId("change_visibility"), + "Change Visibility to pub(crate)", + |edit| { + edit.target(vis.syntax().text_range()); + edit.replace(vis.syntax().text_range(), "pub(crate)"); + edit.set_cursor(vis.syntax().text_range().start()) + }, + ); + } + if vis.syntax().text() == "pub(crate)" { + return ctx.add_assist(AssistId("change_visibility"), "Change visibility to pub", |edit| { + edit.target(vis.syntax().text_range()); + edit.replace(vis.syntax().text_range(), "pub"); + edit.set_cursor(vis.syntax().text_range().start()); + }); + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::{check_assist, check_assist_target}; + + #[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; + ", + ) + } + + #[test] + fn change_visibility_target() { + check_assist_target(change_visibility, "<|>fn foo() {}", "fn"); + check_assist_target(change_visibility, "pub(crate)<|> fn foo() {}", "pub(crate)"); + check_assist_target(change_visibility, "struct S { <|>field: u32 }", "field"); + } +} diff --git a/crates/ra_assists/src/handlers/early_return.rs b/crates/ra_assists/src/handlers/early_return.rs new file mode 100644 index 000000000..8eb3bd473 --- /dev/null +++ b/crates/ra_assists/src/handlers/early_return.rs @@ -0,0 +1,505 @@ +use std::{iter::once, ops::RangeInclusive}; + +use ra_syntax::{ + algo::replace_children, + ast::{self, edit::IndentLevel, make, Block, Pat::TupleStructPat}, + AstNode, + SyntaxKind::{FN_DEF, LOOP_EXPR, L_CURLY, R_CURLY, WHILE_EXPR, WHITESPACE}, + SyntaxNode, +}; + +use crate::{ + assist_ctx::{Assist, AssistCtx}, + handlers::invert_if::invert_boolean_expression, + AssistId, +}; + +// Assist: convert_to_guarded_return +// +// Replace a large conditional with a guarded return. +// +// ``` +// fn main() { +// <|>if cond { +// foo(); +// bar(); +// } +// } +// ``` +// -> +// ``` +// fn main() { +// if !cond { +// return; +// } +// foo(); +// bar(); +// } +// ``` +pub(crate) fn convert_to_guarded_return(ctx: AssistCtx) -> Option { + let if_expr: ast::IfExpr = ctx.find_node_at_offset()?; + if if_expr.else_branch().is_some() { + return None; + } + + let cond = if_expr.condition()?; + + // Check if there is an IfLet that we can handle. + let if_let_pat = match cond.pat() { + None => None, // No IfLet, supported. + Some(TupleStructPat(pat)) if pat.args().count() == 1 => { + let path = pat.path()?; + match path.qualifier() { + None => { + let bound_ident = pat.args().next().unwrap(); + Some((path, bound_ident)) + } + Some(_) => return None, + } + } + Some(_) => return None, // Unsupported IfLet. + }; + + let cond_expr = cond.expr()?; + let then_block = if_expr.then_branch()?.block()?; + + let parent_block = if_expr.syntax().parent()?.ancestors().find_map(ast::Block::cast)?; + + if parent_block.expr()? != if_expr.clone().into() { + return None; + } + + // check for early return and continue + let first_in_then_block = then_block.syntax().first_child()?; + if ast::ReturnExpr::can_cast(first_in_then_block.kind()) + || ast::ContinueExpr::can_cast(first_in_then_block.kind()) + || first_in_then_block + .children() + .any(|x| ast::ReturnExpr::can_cast(x.kind()) || ast::ContinueExpr::can_cast(x.kind())) + { + return None; + } + + let parent_container = parent_block.syntax().parent()?.parent()?; + + let early_expression: ast::Expr = match parent_container.kind() { + WHILE_EXPR | LOOP_EXPR => make::expr_continue(), + FN_DEF => make::expr_return(), + _ => return None, + }; + + if then_block.syntax().first_child_or_token().map(|t| t.kind() == L_CURLY).is_none() { + return None; + } + + then_block.syntax().last_child_or_token().filter(|t| t.kind() == R_CURLY)?; + let cursor_position = ctx.frange.range.start(); + + ctx.add_assist(AssistId("convert_to_guarded_return"), "Convert to guarded return", |edit| { + let if_indent_level = IndentLevel::from_node(&if_expr.syntax()); + let new_block = match if_let_pat { + None => { + // If. + let new_expr = { + let then_branch = + make::block_expr(once(make::expr_stmt(early_expression).into()), None); + let cond = invert_boolean_expression(cond_expr); + let e = make::expr_if(cond, then_branch); + if_indent_level.increase_indent(e) + }; + replace(new_expr.syntax(), &then_block, &parent_block, &if_expr) + } + Some((path, bound_ident)) => { + // If-let. + let match_expr = { + let happy_arm = make::match_arm( + once( + make::tuple_struct_pat( + path, + once(make::bind_pat(make::name("it")).into()), + ) + .into(), + ), + make::expr_path(make::path_from_name_ref(make::name_ref("it"))), + ); + + let sad_arm = make::match_arm( + // FIXME: would be cool to use `None` or `Err(_)` if appropriate + once(make::placeholder_pat().into()), + early_expression, + ); + + make::expr_match(cond_expr, make::match_arm_list(vec![happy_arm, sad_arm])) + }; + + let let_stmt = make::let_stmt( + make::bind_pat(make::name(&bound_ident.syntax().to_string())).into(), + Some(match_expr), + ); + let let_stmt = if_indent_level.increase_indent(let_stmt); + replace(let_stmt.syntax(), &then_block, &parent_block, &if_expr) + } + }; + edit.target(if_expr.syntax().text_range()); + edit.replace_ast(parent_block, ast::Block::cast(new_block).unwrap()); + edit.set_cursor(cursor_position); + + fn replace( + new_expr: &SyntaxNode, + then_block: &Block, + parent_block: &Block, + if_expr: &ast::IfExpr, + ) -> SyntaxNode { + let then_block_items = IndentLevel::from(1).decrease_indent(then_block.clone()); + let end_of_then = then_block_items.syntax().last_child_or_token().unwrap(); + let end_of_then = + if end_of_then.prev_sibling_or_token().map(|n| n.kind()) == Some(WHITESPACE) { + end_of_then.prev_sibling_or_token().unwrap() + } else { + end_of_then + }; + let mut then_statements = new_expr.children_with_tokens().chain( + then_block_items + .syntax() + .children_with_tokens() + .skip(1) + .take_while(|i| *i != end_of_then), + ); + replace_children( + &parent_block.syntax(), + RangeInclusive::new( + if_expr.clone().syntax().clone().into(), + if_expr.syntax().clone().into(), + ), + &mut then_statements, + ) + } + }) +} + +#[cfg(test)] +mod tests { + use crate::helpers::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn convert_inside_fn() { + check_assist( + convert_to_guarded_return, + r#" + fn main() { + bar(); + if<|> true { + foo(); + + //comment + bar(); + } + } + "#, + r#" + fn main() { + bar(); + if<|> !true { + return; + } + foo(); + + //comment + bar(); + } + "#, + ); + } + + #[test] + fn convert_let_inside_fn() { + check_assist( + convert_to_guarded_return, + r#" + fn main(n: Option) { + bar(); + if<|> let Some(n) = n { + foo(n); + + //comment + bar(); + } + } + "#, + r#" + fn main(n: Option) { + bar(); + le<|>t n = match n { + Some(it) => it, + _ => return, + }; + foo(n); + + //comment + bar(); + } + "#, + ); + } + + #[test] + fn convert_if_let_result() { + check_assist( + convert_to_guarded_return, + r#" + fn main() { + if<|> let Ok(x) = Err(92) { + foo(x); + } + } + "#, + r#" + fn main() { + le<|>t x = match Err(92) { + Ok(it) => it, + _ => return, + }; + foo(x); + } + "#, + ); + } + + #[test] + fn convert_let_ok_inside_fn() { + check_assist( + convert_to_guarded_return, + r#" + fn main(n: Option) { + bar(); + if<|> let Ok(n) = n { + foo(n); + + //comment + bar(); + } + } + "#, + r#" + fn main(n: Option) { + bar(); + le<|>t n = match n { + Ok(it) => it, + _ => return, + }; + foo(n); + + //comment + bar(); + } + "#, + ); + } + + #[test] + fn convert_inside_while() { + check_assist( + convert_to_guarded_return, + r#" + fn main() { + while true { + if<|> true { + foo(); + bar(); + } + } + } + "#, + r#" + fn main() { + while true { + if<|> !true { + continue; + } + foo(); + bar(); + } + } + "#, + ); + } + + #[test] + fn convert_let_inside_while() { + check_assist( + convert_to_guarded_return, + r#" + fn main() { + while true { + if<|> let Some(n) = n { + foo(n); + bar(); + } + } + } + "#, + r#" + fn main() { + while true { + le<|>t n = match n { + Some(it) => it, + _ => continue, + }; + foo(n); + bar(); + } + } + "#, + ); + } + + #[test] + fn convert_inside_loop() { + check_assist( + convert_to_guarded_return, + r#" + fn main() { + loop { + if<|> true { + foo(); + bar(); + } + } + } + "#, + r#" + fn main() { + loop { + if<|> !true { + continue; + } + foo(); + bar(); + } + } + "#, + ); + } + + #[test] + fn convert_let_inside_loop() { + check_assist( + convert_to_guarded_return, + r#" + fn main() { + loop { + if<|> let Some(n) = n { + foo(n); + bar(); + } + } + } + "#, + r#" + fn main() { + loop { + le<|>t n = match n { + Some(it) => it, + _ => continue, + }; + foo(n); + bar(); + } + } + "#, + ); + } + + #[test] + fn ignore_already_converted_if() { + check_assist_not_applicable( + convert_to_guarded_return, + r#" + fn main() { + if<|> true { + return; + } + } + "#, + ); + } + + #[test] + fn ignore_already_converted_loop() { + check_assist_not_applicable( + convert_to_guarded_return, + r#" + fn main() { + loop { + if<|> true { + continue; + } + } + } + "#, + ); + } + + #[test] + fn ignore_return() { + check_assist_not_applicable( + convert_to_guarded_return, + r#" + fn main() { + if<|> true { + return + } + } + "#, + ); + } + + #[test] + fn ignore_else_branch() { + check_assist_not_applicable( + convert_to_guarded_return, + r#" + fn main() { + if<|> true { + foo(); + } else { + bar() + } + } + "#, + ); + } + + #[test] + fn ignore_statements_aftert_if() { + check_assist_not_applicable( + convert_to_guarded_return, + r#" + fn main() { + if<|> true { + foo(); + } + bar(); + } + "#, + ); + } + + #[test] + fn ignore_statements_inside_if() { + check_assist_not_applicable( + convert_to_guarded_return, + r#" + fn main() { + if false { + if<|> true { + foo(); + } + } + } + "#, + ); + } +} diff --git a/crates/ra_assists/src/handlers/fill_match_arms.rs b/crates/ra_assists/src/handlers/fill_match_arms.rs new file mode 100644 index 000000000..0908fc246 --- /dev/null +++ b/crates/ra_assists/src/handlers/fill_match_arms.rs @@ -0,0 +1,290 @@ +//! FIXME: write short doc here + +use std::iter; + +use hir::{db::HirDatabase, Adt, HasSource}; +use ra_syntax::ast::{self, edit::IndentLevel, make, AstNode, NameOwner}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: fill_match_arms +// +// Adds missing clauses to a `match` expression. +// +// ``` +// enum Action { Move { distance: u32 }, Stop } +// +// fn handle(action: Action) { +// match action { +// <|> +// } +// } +// ``` +// -> +// ``` +// enum Action { Move { distance: u32 }, Stop } +// +// fn handle(action: Action) { +// match action { +// Action::Move { distance } => (), +// Action::Stop => (), +// } +// } +// ``` +pub(crate) fn fill_match_arms(ctx: AssistCtx) -> Option { + let match_expr = ctx.find_node_at_offset::()?; + let match_arm_list = match_expr.match_arm_list()?; + + // We already have some match arms, so we don't provide any assists. + // Unless if there is only one trivial match arm possibly created + // by match postfix complete. Trivial match arm is the catch all arm. + let mut existing_arms = match_arm_list.arms(); + if let Some(arm) = existing_arms.next() { + if !is_trivial(&arm) || existing_arms.next().is_some() { + return None; + } + }; + + let expr = match_expr.expr()?; + let (enum_def, module) = { + let analyzer = ctx.source_analyzer(expr.syntax(), None); + (resolve_enum_def(ctx.db, &analyzer, &expr)?, analyzer.module()?) + }; + let variants = enum_def.variants(ctx.db); + if variants.is_empty() { + return None; + } + + let db = ctx.db; + + ctx.add_assist(AssistId("fill_match_arms"), "Fill match arms", |edit| { + let indent_level = IndentLevel::from_node(match_arm_list.syntax()); + + let new_arm_list = { + let arms = variants + .into_iter() + .filter_map(|variant| build_pat(db, module, variant)) + .map(|pat| make::match_arm(iter::once(pat), make::expr_unit())); + indent_level.increase_indent(make::match_arm_list(arms)) + }; + + edit.target(match_expr.syntax().text_range()); + edit.set_cursor(expr.syntax().text_range().start()); + edit.replace_ast(match_arm_list, new_arm_list); + }) +} + +fn is_trivial(arm: &ast::MatchArm) -> bool { + arm.pats().any(|pat| match pat { + ast::Pat::PlaceholderPat(..) => true, + _ => false, + }) +} + +fn resolve_enum_def( + db: &impl HirDatabase, + analyzer: &hir::SourceAnalyzer, + expr: &ast::Expr, +) -> Option { + let expr_ty = analyzer.type_of(db, &expr)?; + + let result = expr_ty.autoderef(db).find_map(|ty| match ty.as_adt() { + Some(Adt::Enum(e)) => Some(e), + _ => None, + }); + result +} + +fn build_pat( + db: &impl HirDatabase, + module: hir::Module, + var: hir::EnumVariant, +) -> Option { + let path = crate::ast_transform::path_to_ast(module.find_use_path(db, var.into())?); + + // FIXME: use HIR for this; it doesn't currently expose struct vs. tuple vs. unit variants though + let pat: ast::Pat = match var.source(db).value.kind() { + ast::StructKind::Tuple(field_list) => { + let pats = + iter::repeat(make::placeholder_pat().into()).take(field_list.fields().count()); + make::tuple_struct_pat(path, pats).into() + } + ast::StructKind::Record(field_list) => { + let pats = field_list.fields().map(|f| make::bind_pat(f.name().unwrap()).into()); + make::record_pat(path, pats).into() + } + ast::StructKind::Unit => make::path_pat(path), + }; + + Some(pat) +} + +#[cfg(test)] +mod tests { + use crate::helpers::{check_assist, check_assist_target}; + + 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 test_fill_match_arm_refs() { + check_assist( + fill_match_arms, + r#" + enum A { + As, + } + + fn foo(a: &A) { + match a<|> { + } + } + "#, + r#" + enum A { + As, + } + + fn foo(a: &A) { + match <|>a { + A::As => (), + } + } + "#, + ); + + check_assist( + fill_match_arms, + r#" + enum A { + Es{ x: usize, y: usize } + } + + fn foo(a: &mut A) { + match a<|> { + } + } + "#, + r#" + enum A { + Es{ x: usize, y: usize } + } + + fn foo(a: &mut A) { + match <|>a { + A::Es { x, y } => (), + } + } + "#, + ); + } + + #[test] + fn fill_match_arms_target() { + check_assist_target( + fill_match_arms, + r#" + enum E { X, Y } + + fn main() { + match E::X<|> {} + } + "#, + "match E::X {}", + ); + } + + #[test] + fn fill_match_arms_trivial_arm() { + 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 => (), + } + } + "#, + ); + } + + #[test] + fn fill_match_arms_qualifies_path() { + check_assist( + fill_match_arms, + r#" + mod foo { pub enum E { X, Y } } + use foo::E::X; + + fn main() { + match X { + <|> + } + } + "#, + r#" + mod foo { pub enum E { X, Y } } + use foo::E::X; + + fn main() { + match <|>X { + X => (), + foo::E::Y => (), + } + } + "#, + ); + } +} diff --git a/crates/ra_assists/src/handlers/flip_binexpr.rs b/crates/ra_assists/src/handlers/flip_binexpr.rs new file mode 100644 index 000000000..bfcc09e90 --- /dev/null +++ b/crates/ra_assists/src/handlers/flip_binexpr.rs @@ -0,0 +1,142 @@ +use ra_syntax::ast::{AstNode, BinExpr, BinOp}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: flip_binexpr +// +// Flips operands of a binary expression. +// +// ``` +// fn main() { +// let _ = 90 +<|> 2; +// } +// ``` +// -> +// ``` +// fn main() { +// let _ = 2 + 90; +// } +// ``` +pub(crate) fn flip_binexpr(ctx: AssistCtx) -> Option { + let expr = ctx.find_node_at_offset::()?; + let lhs = expr.lhs()?.syntax().clone(); + let rhs = expr.rhs()?.syntax().clone(); + let op_range = expr.op_token()?.text_range(); + // The assist should be applied only if the cursor is on the operator + let cursor_in_range = ctx.frange.range.is_subrange(&op_range); + if !cursor_in_range { + return None; + } + let action: FlipAction = expr.op_kind()?.into(); + // The assist should not be applied for certain operators + if let FlipAction::DontFlip = action { + return None; + } + + ctx.add_assist(AssistId("flip_binexpr"), "Flip binary expression", |edit| { + edit.target(op_range); + if let FlipAction::FlipAndReplaceOp(new_op) = action { + edit.replace(op_range, new_op); + } + edit.replace(lhs.text_range(), rhs.text()); + edit.replace(rhs.text_range(), lhs.text()); + }) +} + +enum FlipAction { + // Flip the expression + Flip, + // Flip the expression and replace the operator with this string + FlipAndReplaceOp(&'static str), + // Do not flip the expression + DontFlip, +} + +impl From for FlipAction { + fn from(op_kind: BinOp) -> Self { + match op_kind { + kind if kind.is_assignment() => FlipAction::DontFlip, + BinOp::GreaterTest => FlipAction::FlipAndReplaceOp("<"), + BinOp::GreaterEqualTest => FlipAction::FlipAndReplaceOp("<="), + BinOp::LesserTest => FlipAction::FlipAndReplaceOp(">"), + BinOp::LesserEqualTest => FlipAction::FlipAndReplaceOp(">="), + _ => FlipAction::Flip, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; + + #[test] + fn flip_binexpr_target_is_the_op() { + check_assist_target(flip_binexpr, "fn f() { let res = 1 ==<|> 2; }", "==") + } + + #[test] + fn flip_binexpr_not_applicable_for_assignment() { + check_assist_not_applicable(flip_binexpr, "fn f() { let mut _x = 1; _x +=<|> 2 }") + } + + #[test] + fn flip_binexpr_works_for_eq() { + check_assist( + flip_binexpr, + "fn f() { let res = 1 ==<|> 2; }", + "fn f() { let res = 2 ==<|> 1; }", + ) + } + + #[test] + fn flip_binexpr_works_for_gt() { + check_assist( + flip_binexpr, + "fn f() { let res = 1 ><|> 2; }", + "fn f() { let res = 2 <<|> 1; }", + ) + } + + #[test] + fn flip_binexpr_works_for_lteq() { + check_assist( + flip_binexpr, + "fn f() { let res = 1 <=<|> 2; }", + "fn f() { let res = 2 >=<|> 1; }", + ) + } + + #[test] + fn flip_binexpr_works_for_complex_expr() { + check_assist( + flip_binexpr, + "fn f() { let res = (1 + 1) ==<|> (2 + 2); }", + "fn f() { let res = (2 + 2) ==<|> (1 + 1); }", + ) + } + + #[test] + fn flip_binexpr_works_inside_match() { + check_assist( + flip_binexpr, + r#" + fn dyn_eq(&self, other: &dyn Diagnostic) -> bool { + match other.downcast_ref::() { + None => false, + Some(it) => it ==<|> self, + } + } + "#, + r#" + fn dyn_eq(&self, other: &dyn Diagnostic) -> bool { + match other.downcast_ref::() { + None => false, + Some(it) => self ==<|> it, + } + } + "#, + ) + } +} diff --git a/crates/ra_assists/src/handlers/flip_comma.rs b/crates/ra_assists/src/handlers/flip_comma.rs new file mode 100644 index 000000000..1dacf29f8 --- /dev/null +++ b/crates/ra_assists/src/handlers/flip_comma.rs @@ -0,0 +1,80 @@ +use ra_syntax::{algo::non_trivia_sibling, Direction, T}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: flip_comma +// +// Flips two comma-separated items. +// +// ``` +// fn main() { +// ((1, 2),<|> (3, 4)); +// } +// ``` +// -> +// ``` +// fn main() { +// ((3, 4), (1, 2)); +// } +// ``` +pub(crate) fn flip_comma(ctx: AssistCtx) -> Option { + let comma = ctx.find_token_at_offset(T![,])?; + let prev = non_trivia_sibling(comma.clone().into(), Direction::Prev)?; + let next = non_trivia_sibling(comma.clone().into(), Direction::Next)?; + + // Don't apply a "flip" in case of a last comma + // that typically comes before punctuation + if next.kind().is_punct() { + return None; + } + + ctx.add_assist(AssistId("flip_comma"), "Flip comma", |edit| { + edit.target(comma.text_range()); + edit.replace(prev.text_range(), next.to_string()); + edit.replace(next.text_range(), prev.to_string()); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::helpers::{check_assist, check_assist_target}; + + #[test] + fn flip_comma_works_for_function_parameters() { + check_assist( + flip_comma, + "fn foo(x: i32,<|> y: Result<(), ()>) {}", + "fn foo(y: Result<(), ()>,<|> x: i32) {}", + ) + } + + #[test] + fn flip_comma_target() { + check_assist_target(flip_comma, "fn foo(x: i32,<|> y: Result<(), ()>) {}", ",") + } + + #[test] + #[should_panic] + fn flip_comma_before_punct() { + // See https://github.com/rust-analyzer/rust-analyzer/issues/1619 + // "Flip comma" assist shouldn't be applicable to the last comma in enum or struct + // declaration body. + check_assist_target( + flip_comma, + "pub enum Test { \ + A,<|> \ + }", + ",", + ); + + check_assist_target( + flip_comma, + "pub struct Test { \ + foo: usize,<|> \ + }", + ",", + ); + } +} diff --git a/crates/ra_assists/src/handlers/flip_trait_bound.rs b/crates/ra_assists/src/handlers/flip_trait_bound.rs new file mode 100644 index 000000000..f56769624 --- /dev/null +++ b/crates/ra_assists/src/handlers/flip_trait_bound.rs @@ -0,0 +1,116 @@ +use ra_syntax::{ + algo::non_trivia_sibling, + ast::{self, AstNode}, + Direction, T, +}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: flip_trait_bound +// +// Flips two trait bounds. +// +// ``` +// fn foo Copy>() { } +// ``` +// -> +// ``` +// fn foo() { } +// ``` +pub(crate) fn flip_trait_bound(ctx: AssistCtx) -> Option { + // We want to replicate the behavior of `flip_binexpr` by only suggesting + // the assist when the cursor is on a `+` + let plus = ctx.find_token_at_offset(T![+])?; + + // Make sure we're in a `TypeBoundList` + if ast::TypeBoundList::cast(plus.parent()).is_none() { + return None; + } + + let (before, after) = ( + non_trivia_sibling(plus.clone().into(), Direction::Prev)?, + non_trivia_sibling(plus.clone().into(), Direction::Next)?, + ); + + ctx.add_assist(AssistId("flip_trait_bound"), "Flip trait bounds", |edit| { + edit.target(plus.text_range()); + edit.replace(before.text_range(), after.to_string()); + edit.replace(after.text_range(), before.to_string()); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; + + #[test] + fn flip_trait_bound_assist_available() { + check_assist_target(flip_trait_bound, "struct S where T: A <|>+ B + C { }", "+") + } + + #[test] + fn flip_trait_bound_not_applicable_for_single_trait_bound() { + check_assist_not_applicable(flip_trait_bound, "struct S where T: <|>A { }") + } + + #[test] + fn flip_trait_bound_works_for_struct() { + check_assist( + flip_trait_bound, + "struct S where T: A <|>+ B { }", + "struct S where T: B <|>+ A { }", + ) + } + + #[test] + fn flip_trait_bound_works_for_trait_impl() { + check_assist( + flip_trait_bound, + "impl X for S where T: A +<|> B { }", + "impl X for S where T: B +<|> A { }", + ) + } + + #[test] + fn flip_trait_bound_works_for_fn() { + check_assist(flip_trait_bound, "fn f+ B>(t: T) { }", "fn f+ A>(t: T) { }") + } + + #[test] + fn flip_trait_bound_works_for_fn_where_clause() { + check_assist( + flip_trait_bound, + "fn f(t: T) where T: A +<|> B { }", + "fn f(t: T) where T: B +<|> A { }", + ) + } + + #[test] + fn flip_trait_bound_works_for_lifetime() { + check_assist( + flip_trait_bound, + "fn f(t: T) where T: A <|>+ 'static { }", + "fn f(t: T) where T: 'static <|>+ A { }", + ) + } + + #[test] + fn flip_trait_bound_works_for_complex_bounds() { + check_assist( + flip_trait_bound, + "struct S where T: A <|>+ b_mod::B + C { }", + "struct S where T: b_mod::B <|>+ A + C { }", + ) + } + + #[test] + fn flip_trait_bound_works_for_long_bounds() { + check_assist( + flip_trait_bound, + "struct S where T: A + B + C + D + E + F +<|> G + H + I + J { }", + "struct S where T: A + B + C + D + E + G +<|> F + H + I + J { }", + ) + } +} diff --git a/crates/ra_assists/src/handlers/inline_local_variable.rs b/crates/ra_assists/src/handlers/inline_local_variable.rs new file mode 100644 index 000000000..91b588243 --- /dev/null +++ b/crates/ra_assists/src/handlers/inline_local_variable.rs @@ -0,0 +1,662 @@ +use ra_syntax::{ + ast::{self, AstNode, AstToken}, + TextRange, +}; + +use crate::assist_ctx::ActionBuilder; +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: inline_local_variable +// +// Inlines local variable. +// +// ``` +// fn main() { +// let x<|> = 1 + 2; +// x * 4; +// } +// ``` +// -> +// ``` +// fn main() { +// (1 + 2) * 4; +// } +// ``` +pub(crate) fn inline_local_variable(ctx: AssistCtx) -> Option { + let let_stmt = ctx.find_node_at_offset::()?; + let bind_pat = match let_stmt.pat()? { + ast::Pat::BindPat(pat) => pat, + _ => return None, + }; + if bind_pat.is_mutable() { + return None; + } + let initializer_expr = let_stmt.initializer()?; + let delete_range = if let Some(whitespace) = let_stmt + .syntax() + .next_sibling_or_token() + .and_then(|it| ast::Whitespace::cast(it.as_token()?.clone())) + { + TextRange::from_to( + let_stmt.syntax().text_range().start(), + whitespace.syntax().text_range().end(), + ) + } else { + let_stmt.syntax().text_range() + }; + let analyzer = ctx.source_analyzer(bind_pat.syntax(), None); + let refs = analyzer.find_all_refs(&bind_pat); + if refs.is_empty() { + return None; + }; + + let mut wrap_in_parens = vec![true; refs.len()]; + + for (i, desc) in refs.iter().enumerate() { + let usage_node = + ctx.covering_node_for_range(desc.range).ancestors().find_map(ast::PathExpr::cast)?; + let usage_parent_option = usage_node.syntax().parent().and_then(ast::Expr::cast); + let usage_parent = match usage_parent_option { + Some(u) => u, + None => { + wrap_in_parens[i] = false; + continue; + } + }; + + wrap_in_parens[i] = match (&initializer_expr, usage_parent) { + (ast::Expr::CallExpr(_), _) + | (ast::Expr::IndexExpr(_), _) + | (ast::Expr::MethodCallExpr(_), _) + | (ast::Expr::FieldExpr(_), _) + | (ast::Expr::TryExpr(_), _) + | (ast::Expr::RefExpr(_), _) + | (ast::Expr::Literal(_), _) + | (ast::Expr::TupleExpr(_), _) + | (ast::Expr::ArrayExpr(_), _) + | (ast::Expr::ParenExpr(_), _) + | (ast::Expr::PathExpr(_), _) + | (ast::Expr::BlockExpr(_), _) + | (_, ast::Expr::CallExpr(_)) + | (_, ast::Expr::TupleExpr(_)) + | (_, ast::Expr::ArrayExpr(_)) + | (_, ast::Expr::ParenExpr(_)) + | (_, ast::Expr::ForExpr(_)) + | (_, ast::Expr::WhileExpr(_)) + | (_, ast::Expr::BreakExpr(_)) + | (_, ast::Expr::ReturnExpr(_)) + | (_, ast::Expr::MatchExpr(_)) => false, + _ => true, + }; + } + + let init_str = initializer_expr.syntax().text().to_string(); + let init_in_paren = format!("({})", &init_str); + + ctx.add_assist( + AssistId("inline_local_variable"), + "Inline variable", + move |edit: &mut ActionBuilder| { + edit.delete(delete_range); + for (desc, should_wrap) in refs.iter().zip(wrap_in_parens) { + if should_wrap { + edit.replace(desc.range, init_in_paren.clone()) + } else { + edit.replace(desc.range, init_str.clone()) + } + } + edit.set_cursor(delete_range.start()) + }, + ) +} + +#[cfg(test)] +mod tests { + use crate::helpers::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn test_inline_let_bind_literal_expr() { + check_assist( + inline_local_variable, + " +fn bar(a: usize) {} +fn foo() { + let a<|> = 1; + a + 1; + if a > 10 { + } + + while a > 10 { + + } + let b = a * 10; + bar(a); +}", + " +fn bar(a: usize) {} +fn foo() { + <|>1 + 1; + if 1 > 10 { + } + + while 1 > 10 { + + } + let b = 1 * 10; + bar(1); +}", + ); + } + + #[test] + fn test_inline_let_bind_bin_expr() { + check_assist( + inline_local_variable, + " +fn bar(a: usize) {} +fn foo() { + let a<|> = 1 + 1; + a + 1; + if a > 10 { + } + + while a > 10 { + + } + let b = a * 10; + bar(a); +}", + " +fn bar(a: usize) {} +fn foo() { + <|>(1 + 1) + 1; + if (1 + 1) > 10 { + } + + while (1 + 1) > 10 { + + } + let b = (1 + 1) * 10; + bar(1 + 1); +}", + ); + } + + #[test] + fn test_inline_let_bind_function_call_expr() { + check_assist( + inline_local_variable, + " +fn bar(a: usize) {} +fn foo() { + let a<|> = bar(1); + a + 1; + if a > 10 { + } + + while a > 10 { + + } + let b = a * 10; + bar(a); +}", + " +fn bar(a: usize) {} +fn foo() { + <|>bar(1) + 1; + if bar(1) > 10 { + } + + while bar(1) > 10 { + + } + let b = bar(1) * 10; + bar(bar(1)); +}", + ); + } + + #[test] + fn test_inline_let_bind_cast_expr() { + check_assist( + inline_local_variable, + " +fn bar(a: usize): usize { a } +fn foo() { + let a<|> = bar(1) as u64; + a + 1; + if a > 10 { + } + + while a > 10 { + + } + let b = a * 10; + bar(a); +}", + " +fn bar(a: usize): usize { a } +fn foo() { + <|>(bar(1) as u64) + 1; + if (bar(1) as u64) > 10 { + } + + while (bar(1) as u64) > 10 { + + } + let b = (bar(1) as u64) * 10; + bar(bar(1) as u64); +}", + ); + } + + #[test] + fn test_inline_let_bind_block_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = { 10 + 1 }; + a + 1; + if a > 10 { + } + + while a > 10 { + + } + let b = a * 10; + bar(a); +}", + " +fn foo() { + <|>{ 10 + 1 } + 1; + if { 10 + 1 } > 10 { + } + + while { 10 + 1 } > 10 { + + } + let b = { 10 + 1 } * 10; + bar({ 10 + 1 }); +}", + ); + } + + #[test] + fn test_inline_let_bind_paren_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = ( 10 + 1 ); + a + 1; + if a > 10 { + } + + while a > 10 { + + } + let b = a * 10; + bar(a); +}", + " +fn foo() { + <|>( 10 + 1 ) + 1; + if ( 10 + 1 ) > 10 { + } + + while ( 10 + 1 ) > 10 { + + } + let b = ( 10 + 1 ) * 10; + bar(( 10 + 1 )); +}", + ); + } + + #[test] + fn test_not_inline_mut_variable() { + check_assist_not_applicable( + inline_local_variable, + " +fn foo() { + let mut a<|> = 1 + 1; + a + 1; +}", + ); + } + + #[test] + fn test_call_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = bar(10 + 1); + let b = a * 10; + let c = a as usize; +}", + " +fn foo() { + <|>let b = bar(10 + 1) * 10; + let c = bar(10 + 1) as usize; +}", + ); + } + + #[test] + fn test_index_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let x = vec![1, 2, 3]; + let a<|> = x[0]; + let b = a * 10; + let c = a as usize; +}", + " +fn foo() { + let x = vec![1, 2, 3]; + <|>let b = x[0] * 10; + let c = x[0] as usize; +}", + ); + } + + #[test] + fn test_method_call_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let bar = vec![1]; + let a<|> = bar.len(); + let b = a * 10; + let c = a as usize; +}", + " +fn foo() { + let bar = vec![1]; + <|>let b = bar.len() * 10; + let c = bar.len() as usize; +}", + ); + } + + #[test] + fn test_field_expr() { + check_assist( + inline_local_variable, + " +struct Bar { + foo: usize +} + +fn foo() { + let bar = Bar { foo: 1 }; + let a<|> = bar.foo; + let b = a * 10; + let c = a as usize; +}", + " +struct Bar { + foo: usize +} + +fn foo() { + let bar = Bar { foo: 1 }; + <|>let b = bar.foo * 10; + let c = bar.foo as usize; +}", + ); + } + + #[test] + fn test_try_expr() { + check_assist( + inline_local_variable, + " +fn foo() -> Option { + let bar = Some(1); + let a<|> = bar?; + let b = a * 10; + let c = a as usize; + None +}", + " +fn foo() -> Option { + let bar = Some(1); + <|>let b = bar? * 10; + let c = bar? as usize; + None +}", + ); + } + + #[test] + fn test_ref_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let bar = 10; + let a<|> = &bar; + let b = a * 10; +}", + " +fn foo() { + let bar = 10; + <|>let b = &bar * 10; +}", + ); + } + + #[test] + fn test_tuple_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = (10, 20); + let b = a[0]; +}", + " +fn foo() { + <|>let b = (10, 20)[0]; +}", + ); + } + + #[test] + fn test_array_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = [1, 2, 3]; + let b = a.len(); +}", + " +fn foo() { + <|>let b = [1, 2, 3].len(); +}", + ); + } + + #[test] + fn test_paren() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = (10 + 20); + let b = a * 10; + let c = a as usize; +}", + " +fn foo() { + <|>let b = (10 + 20) * 10; + let c = (10 + 20) as usize; +}", + ); + } + + #[test] + fn test_path_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let d = 10; + let a<|> = d; + let b = a * 10; + let c = a as usize; +}", + " +fn foo() { + let d = 10; + <|>let b = d * 10; + let c = d as usize; +}", + ); + } + + #[test] + fn test_block_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = { 10 }; + let b = a * 10; + let c = a as usize; +}", + " +fn foo() { + <|>let b = { 10 } * 10; + let c = { 10 } as usize; +}", + ); + } + + #[test] + fn test_used_in_different_expr1() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = 10 + 20; + let b = a * 10; + let c = (a, 20); + let d = [a, 10]; + let e = (a); +}", + " +fn foo() { + <|>let b = (10 + 20) * 10; + let c = (10 + 20, 20); + let d = [10 + 20, 10]; + let e = (10 + 20); +}", + ); + } + + #[test] + fn test_used_in_for_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = vec![10, 20]; + for i in a {} +}", + " +fn foo() { + <|>for i in vec![10, 20] {} +}", + ); + } + + #[test] + fn test_used_in_while_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = 1 > 0; + while a {} +}", + " +fn foo() { + <|>while 1 > 0 {} +}", + ); + } + + #[test] + fn test_used_in_break_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = 1 + 1; + loop { + break a; + } +}", + " +fn foo() { + <|>loop { + break 1 + 1; + } +}", + ); + } + + #[test] + fn test_used_in_return_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = 1 > 0; + return a; +}", + " +fn foo() { + <|>return 1 > 0; +}", + ); + } + + #[test] + fn test_used_in_match_expr() { + check_assist( + inline_local_variable, + " +fn foo() { + let a<|> = 1 > 0; + match a {} +}", + " +fn foo() { + <|>match 1 > 0 {} +}", + ); + } + + #[test] + fn test_not_applicable_if_variable_unused() { + check_assist_not_applicable( + inline_local_variable, + " +fn foo() { + let <|>a = 0; +} + ", + ) + } +} diff --git a/crates/ra_assists/src/handlers/introduce_variable.rs b/crates/ra_assists/src/handlers/introduce_variable.rs new file mode 100644 index 000000000..7312ce687 --- /dev/null +++ b/crates/ra_assists/src/handlers/introduce_variable.rs @@ -0,0 +1,529 @@ +use format_buf::format; +use ra_syntax::{ + ast::{self, AstNode}, + SyntaxKind::{ + BLOCK_EXPR, BREAK_EXPR, COMMENT, LAMBDA_EXPR, LOOP_EXPR, MATCH_ARM, PATH_EXPR, RETURN_EXPR, + WHITESPACE, + }, + SyntaxNode, TextUnit, +}; +use test_utils::tested_by; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: introduce_variable +// +// Extracts subexpression into a variable. +// +// ``` +// fn main() { +// <|>(1 + 2)<|> * 4; +// } +// ``` +// -> +// ``` +// fn main() { +// let var_name = (1 + 2); +// var_name * 4; +// } +// ``` +pub(crate) fn introduce_variable(ctx: AssistCtx) -> Option { + if ctx.frange.range.is_empty() { + return None; + } + let node = ctx.covering_element(); + if node.kind() == COMMENT { + tested_by!(introduce_var_in_comment_is_not_applicable); + return None; + } + let expr = node.ancestors().find_map(valid_target_expr)?; + let (anchor_stmt, wrap_in_block) = anchor_stmt(expr.clone())?; + let indent = anchor_stmt.prev_sibling_or_token()?.as_token()?.clone(); + if indent.kind() != WHITESPACE { + return None; + } + ctx.add_assist(AssistId("introduce_variable"), "Extract into 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 ") + }; + format!(buf, "{}", expr.syntax()); + let full_stmt = ast::ExprStmt::cast(anchor_stmt.clone()); + let is_full_stmt = if let Some(expr_stmt) = &full_stmt { + Some(expr.syntax().clone()) == expr_stmt.expr().map(|e| e.syntax().clone()) + } else { + false + }; + if is_full_stmt { + tested_by!(test_introduce_var_expr_stmt); + if !full_stmt.unwrap().has_semi() { + buf.push_str(";"); + } + edit.replace(expr.syntax().text_range(), buf); + } else { + buf.push_str(";"); + + // We want to maintain the indent level, + // but we do not want to duplicate possible + // extra newlines in the indent block + let text = indent.text(); + if text.starts_with('\n') { + buf.push_str("\n"); + buf.push_str(text.trim_start_matches('\n')); + } else { + buf.push_str(text); + } + + edit.target(expr.syntax().text_range()); + edit.replace(expr.syntax().text_range(), "var_name".to_string()); + edit.insert(anchor_stmt.text_range().start(), buf); + if wrap_in_block { + edit.insert(anchor_stmt.text_range().end(), " }"); + } + } + edit.set_cursor(anchor_stmt.text_range().start() + cursor_offset); + }) +} + +/// Check whether 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 { + match node.kind() { + PATH_EXPR | LOOP_EXPR => None, + BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()), + RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()), + BLOCK_EXPR => { + ast::BlockExpr::cast(node).filter(|it| it.is_standalone()).map(ast::Expr::from) + } + _ => 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 lambda or match arm. +fn anchor_stmt(expr: ast::Expr) -> Option<(SyntaxNode, bool)> { + expr.syntax().ancestors().find_map(|node| { + if let Some(expr) = node.parent().and_then(ast::Block::cast).and_then(|it| it.expr()) { + if expr.syntax() == &node { + tested_by!(test_introduce_var_last_expr); + return Some((node, false)); + } + } + + if let Some(parent) = node.parent() { + if parent.kind() == MATCH_ARM || parent.kind() == LAMBDA_EXPR { + return Some((node, true)); + } + } + + if ast::Stmt::cast(node.clone()).is_some() { + return Some((node, false)); + } + + None + }) +} + +#[cfg(test)] +mod tests { + use test_utils::covers; + + use crate::helpers::{ + check_assist_range, check_assist_range_not_applicable, check_assist_range_target, + }; + + use super::*; + + #[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 introduce_var_in_comment_is_not_applicable() { + covers!(introduce_var_in_comment_is_not_applicable); + check_assist_range_not_applicable( + introduce_variable, + "fn main() { 1 + /* <|>comment<|> */ 1; }", + ); + } + + #[test] + fn test_introduce_var_expr_stmt() { + covers!(test_introduce_var_expr_stmt); + check_assist_range( + introduce_variable, + " +fn foo() { + <|>1 + 1<|>; +}", + " +fn foo() { + let <|>var_name = 1 + 1; +}", + ); + 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_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() { + covers!(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) +}", + ); + 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_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_range( + introduce_variable, + " +fn main() { + let o = <|>Some(true)<|>; +} +", + " +fn main() { + let <|>var_name = Some(true); + let o = var_name; +} +", + ); + } + + #[test] + fn test_introduce_var_path_method() { + check_assist_range( + introduce_variable, + " +fn main() { + let v = <|>bar.foo()<|>; +} +", + " +fn main() { + let <|>var_name = bar.foo(); + let v = var_name; +} +", + ); + } + + #[test] + fn test_introduce_var_return() { + check_assist_range( + introduce_variable, + " +fn foo() -> u32 { + <|>return 2 + 2<|>; +} +", + " +fn foo() -> u32 { + let <|>var_name = 2 + 2; + return var_name; +} +", + ); + } + + #[test] + fn test_introduce_var_does_not_add_extra_whitespace() { + check_assist_range( + introduce_variable, + " +fn foo() -> u32 { + + + <|>return 2 + 2<|>; +} +", + " +fn foo() -> u32 { + + + let <|>var_name = 2 + 2; + return var_name; +} +", + ); + + check_assist_range( + introduce_variable, + " +fn foo() -> u32 { + + <|>return 2 + 2<|>; +} +", + " +fn foo() -> u32 { + + let <|>var_name = 2 + 2; + return var_name; +} +", + ); + + check_assist_range( + introduce_variable, + " +fn foo() -> u32 { + let foo = 1; + + // bar + + + <|>return 2 + 2<|>; +} +", + " +fn foo() -> u32 { + let foo = 1; + + // bar + + + let <|>var_name = 2 + 2; + return var_name; +} +", + ); + } + + #[test] + fn test_introduce_var_break() { + check_assist_range( + introduce_variable, + " +fn main() { + let result = loop { + <|>break 2 + 2<|>; + }; +} +", + " +fn main() { + let result = loop { + let <|>var_name = 2 + 2; + break var_name; + }; +} +", + ); + } + + #[test] + fn test_introduce_var_for_cast() { + check_assist_range( + introduce_variable, + " +fn main() { + let v = <|>0f32 as u32<|>; +} +", + " +fn main() { + let <|>var_name = 0f32 as u32; + let v = var_name; +} +", + ); + } + + #[test] + fn test_introduce_var_for_return_not_applicable() { + check_assist_range_not_applicable(introduce_variable, "fn foo() { <|>return<|>; } "); + } + + #[test] + fn test_introduce_var_for_break_not_applicable() { + check_assist_range_not_applicable( + introduce_variable, + "fn main() { loop { <|>break<|>; }; }", + ); + } + + // FIXME: This is not quite correct, but good enough(tm) for the sorting heuristic + #[test] + fn introduce_var_target() { + check_assist_range_target( + introduce_variable, + "fn foo() -> u32 { <|>return 2 + 2<|>; }", + "2 + 2", + ); + + check_assist_range_target( + introduce_variable, + " +fn main() { + let x = true; + let tuple = match x { + true => (<|>2 + 2<|>, true) + _ => (0, false) + }; +} +", + "2 + 2", + ); + } +} diff --git a/crates/ra_assists/src/handlers/invert_if.rs b/crates/ra_assists/src/handlers/invert_if.rs new file mode 100644 index 000000000..983392f21 --- /dev/null +++ b/crates/ra_assists/src/handlers/invert_if.rs @@ -0,0 +1,112 @@ +use ra_syntax::ast::{self, make, AstNode}; +use ra_syntax::T; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: invert_if +// +// Apply invert_if +// This transforms if expressions of the form `if !x {A} else {B}` into `if x {B} else {A}` +// This also works with `!=`. This assist can only be applied with the cursor +// on `if`. +// +// ``` +// fn main() { +// if<|> !y { A } else { B } +// } +// ``` +// -> +// ``` +// fn main() { +// if y { B } else { A } +// } +// ``` + +pub(crate) fn invert_if(ctx: AssistCtx) -> Option { + let if_keyword = ctx.find_token_at_offset(T![if])?; + let expr = ast::IfExpr::cast(if_keyword.parent())?; + let if_range = if_keyword.text_range(); + let cursor_in_range = ctx.frange.range.is_subrange(&if_range); + if !cursor_in_range { + return None; + } + + let cond = expr.condition()?.expr()?; + let then_node = expr.then_branch()?.syntax().clone(); + + if let ast::ElseBranch::Block(else_block) = expr.else_branch()? { + let cond_range = cond.syntax().text_range(); + let flip_cond = invert_boolean_expression(cond); + let else_node = else_block.syntax(); + let else_range = else_node.text_range(); + let then_range = then_node.text_range(); + return ctx.add_assist(AssistId("invert_if"), "Invert if", |edit| { + edit.target(if_range); + edit.replace(cond_range, flip_cond.syntax().text()); + edit.replace(else_range, then_node.text()); + edit.replace(then_range, else_node.text()); + }); + } + + None +} + +pub(crate) fn invert_boolean_expression(expr: ast::Expr) -> ast::Expr { + if let Some(expr) = invert_special_case(&expr) { + return expr; + } + make::expr_prefix(T![!], expr) +} + +pub(crate) fn invert_special_case(expr: &ast::Expr) -> Option { + match expr { + ast::Expr::BinExpr(bin) => match bin.op_kind()? { + ast::BinOp::NegatedEqualityTest => bin.replace_op(T![==]).map(|it| it.into()), + ast::BinOp::EqualityTest => bin.replace_op(T![!=]).map(|it| it.into()), + _ => None, + }, + ast::Expr::PrefixExpr(pe) if pe.op_kind()? == ast::PrefixOp::Not => pe.expr(), + // FIXME: + // ast::Expr::Literal(true | false ) + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::helpers::{check_assist, check_assist_not_applicable}; + + #[test] + fn invert_if_remove_inequality() { + check_assist( + invert_if, + "fn f() { i<|>f x != 3 { 1 } else { 3 + 2 } }", + "fn f() { i<|>f x == 3 { 3 + 2 } else { 1 } }", + ) + } + + #[test] + fn invert_if_remove_not() { + check_assist( + invert_if, + "fn f() { <|>if !cond { 3 * 2 } else { 1 } }", + "fn f() { <|>if cond { 1 } else { 3 * 2 } }", + ) + } + + #[test] + fn invert_if_general_case() { + check_assist( + invert_if, + "fn f() { i<|>f cond { 3 * 2 } else { 1 } }", + "fn f() { i<|>f !cond { 1 } else { 3 * 2 } }", + ) + } + + #[test] + fn invert_if_doesnt_apply_with_cursor_not_on_if() { + check_assist_not_applicable(invert_if, "fn f() { if !<|>cond { 3 * 2 } else { 1 } }") + } +} diff --git a/crates/ra_assists/src/handlers/merge_match_arms.rs b/crates/ra_assists/src/handlers/merge_match_arms.rs new file mode 100644 index 000000000..670614dd8 --- /dev/null +++ b/crates/ra_assists/src/handlers/merge_match_arms.rs @@ -0,0 +1,264 @@ +use std::iter::successors; + +use ra_syntax::{ + ast::{self, AstNode}, + Direction, TextUnit, +}; + +use crate::{Assist, AssistCtx, AssistId, TextRange}; + +// Assist: merge_match_arms +// +// Merges identical match arms. +// +// ``` +// enum Action { Move { distance: u32 }, Stop } +// +// fn handle(action: Action) { +// match action { +// <|>Action::Move(..) => foo(), +// Action::Stop => foo(), +// } +// } +// ``` +// -> +// ``` +// enum Action { Move { distance: u32 }, Stop } +// +// fn handle(action: Action) { +// match action { +// Action::Move(..) | Action::Stop => foo(), +// } +// } +// ``` +pub(crate) fn merge_match_arms(ctx: AssistCtx) -> Option { + let current_arm = ctx.find_node_at_offset::()?; + // Don't try to handle arms with guards for now - can add support for this later + if current_arm.guard().is_some() { + return None; + } + let current_expr = current_arm.expr()?; + let current_text_range = current_arm.syntax().text_range(); + + enum CursorPos { + InExpr(TextUnit), + InPat(TextUnit), + } + let cursor_pos = ctx.frange.range.start(); + let cursor_pos = if current_expr.syntax().text_range().contains(cursor_pos) { + CursorPos::InExpr(current_text_range.end() - cursor_pos) + } else { + CursorPos::InPat(cursor_pos) + }; + + // We check if the following match arms match this one. We could, but don't, + // compare to the previous match arm as well. + let arms_to_merge = successors(Some(current_arm), next_arm) + .take_while(|arm| { + if arm.guard().is_some() { + return false; + } + match arm.expr() { + Some(expr) => expr.syntax().text() == current_expr.syntax().text(), + None => false, + } + }) + .collect::>(); + + if arms_to_merge.len() <= 1 { + return None; + } + + ctx.add_assist(AssistId("merge_match_arms"), "Merge match arms", |edit| { + let pats = if arms_to_merge.iter().any(contains_placeholder) { + "_".into() + } else { + arms_to_merge + .iter() + .flat_map(ast::MatchArm::pats) + .map(|x| x.syntax().to_string()) + .collect::>() + .join(" | ") + }; + + let arm = format!("{} => {}", pats, current_expr.syntax().text()); + + let start = arms_to_merge.first().unwrap().syntax().text_range().start(); + let end = arms_to_merge.last().unwrap().syntax().text_range().end(); + + edit.target(current_text_range); + edit.set_cursor(match cursor_pos { + CursorPos::InExpr(back_offset) => start + TextUnit::from_usize(arm.len()) - back_offset, + CursorPos::InPat(offset) => offset, + }); + edit.replace(TextRange::from_to(start, end), arm); + }) +} + +fn contains_placeholder(a: &ast::MatchArm) -> bool { + a.pats().any(|x| match x { + ra_syntax::ast::Pat::PlaceholderPat(..) => true, + _ => false, + }) +} + +fn next_arm(arm: &ast::MatchArm) -> Option { + arm.syntax().siblings(Direction::Next).skip(1).find_map(ast::MatchArm::cast) +} + +#[cfg(test)] +mod tests { + use super::merge_match_arms; + use crate::helpers::{check_assist, check_assist_not_applicable}; + + #[test] + fn merge_match_arms_single_patterns() { + check_assist( + merge_match_arms, + r#" + #[derive(Debug)] + enum X { A, B, C } + + fn main() { + let x = X::A; + let y = match x { + X::A => { 1i32<|> } + X::B => { 1i32 } + X::C => { 2i32 } + } + } + "#, + r#" + #[derive(Debug)] + enum X { A, B, C } + + fn main() { + let x = X::A; + let y = match x { + X::A | X::B => { 1i32<|> } + X::C => { 2i32 } + } + } + "#, + ); + } + + #[test] + fn merge_match_arms_multiple_patterns() { + check_assist( + merge_match_arms, + r#" + #[derive(Debug)] + enum X { A, B, C, D, E } + + fn main() { + let x = X::A; + let y = match x { + X::A | X::B => {<|> 1i32 }, + X::C | X::D => { 1i32 }, + X::E => { 2i32 }, + } + } + "#, + r#" + #[derive(Debug)] + enum X { A, B, C, D, E } + + fn main() { + let x = X::A; + let y = match x { + X::A | X::B | X::C | X::D => {<|> 1i32 }, + X::E => { 2i32 }, + } + } + "#, + ); + } + + #[test] + fn merge_match_arms_placeholder_pattern() { + check_assist( + merge_match_arms, + r#" + #[derive(Debug)] + enum X { A, B, C, D, E } + + fn main() { + let x = X::A; + let y = match x { + X::A => { 1i32 }, + X::B => { 2i<|>32 }, + _ => { 2i32 } + } + } + "#, + r#" + #[derive(Debug)] + enum X { A, B, C, D, E } + + fn main() { + let x = X::A; + let y = match x { + X::A => { 1i32 }, + _ => { 2i<|>32 } + } + } + "#, + ); + } + + #[test] + fn merges_all_subsequent_arms() { + check_assist( + merge_match_arms, + r#" + enum X { A, B, C, D, E } + + fn main() { + match X::A { + X::A<|> => 92, + X::B => 92, + X::C => 92, + X::D => 62, + _ => panic!(), + } + } + "#, + r#" + enum X { A, B, C, D, E } + + fn main() { + match X::A { + X::A<|> | X::B | X::C => 92, + X::D => 62, + _ => panic!(), + } + } + "#, + ) + } + + #[test] + fn merge_match_arms_rejects_guards() { + check_assist_not_applicable( + merge_match_arms, + r#" + #[derive(Debug)] + enum X { + A(i32), + B, + C + } + + fn main() { + let x = X::A; + let y = match x { + X::A(a) if a > 5 => { <|>1i32 }, + X::B => { 1i32 }, + X::C => { 2i32 } + } + } + "#, + ); + } +} diff --git a/crates/ra_assists/src/handlers/move_bounds.rs b/crates/ra_assists/src/handlers/move_bounds.rs new file mode 100644 index 000000000..90793b5fc --- /dev/null +++ b/crates/ra_assists/src/handlers/move_bounds.rs @@ -0,0 +1,137 @@ +use ra_syntax::{ + ast::{self, edit, make, AstNode, NameOwner, TypeBoundsOwner}, + SyntaxElement, + SyntaxKind::*, +}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: move_bounds_to_where_clause +// +// Moves inline type bounds to a where clause. +// +// ``` +// fn applyF: FnOnce(T) -> U>(f: F, x: T) -> U { +// f(x) +// } +// ``` +// -> +// ``` +// fn apply(f: F, x: T) -> U where F: FnOnce(T) -> U { +// f(x) +// } +// ``` +pub(crate) fn move_bounds_to_where_clause(ctx: AssistCtx) -> Option { + let type_param_list = ctx.find_node_at_offset::()?; + + let mut type_params = type_param_list.type_params(); + if type_params.all(|p| p.type_bound_list().is_none()) { + return None; + } + + let parent = type_param_list.syntax().parent()?; + if parent.children_with_tokens().any(|it| it.kind() == WHERE_CLAUSE) { + return None; + } + + let anchor: SyntaxElement = match parent.kind() { + FN_DEF => ast::FnDef::cast(parent)?.body()?.syntax().clone().into(), + TRAIT_DEF => ast::TraitDef::cast(parent)?.item_list()?.syntax().clone().into(), + IMPL_BLOCK => ast::ImplBlock::cast(parent)?.item_list()?.syntax().clone().into(), + ENUM_DEF => ast::EnumDef::cast(parent)?.variant_list()?.syntax().clone().into(), + STRUCT_DEF => parent + .children_with_tokens() + .find(|it| it.kind() == RECORD_FIELD_DEF_LIST || it.kind() == SEMI)?, + _ => return None, + }; + + ctx.add_assist(AssistId("move_bounds_to_where_clause"), "Move to where clause", |edit| { + let new_params = type_param_list + .type_params() + .filter(|it| it.type_bound_list().is_some()) + .map(|type_param| { + let without_bounds = type_param.remove_bounds(); + (type_param, without_bounds) + }); + + let new_type_param_list = edit::replace_descendants(&type_param_list, new_params); + edit.replace_ast(type_param_list.clone(), new_type_param_list); + + let where_clause = { + let predicates = type_param_list.type_params().filter_map(build_predicate); + make::where_clause(predicates) + }; + + let to_insert = match anchor.prev_sibling_or_token() { + Some(ref elem) if elem.kind() == WHITESPACE => format!("{} ", where_clause.syntax()), + _ => format!(" {}", where_clause.syntax()), + }; + edit.insert(anchor.text_range().start(), to_insert); + edit.target(type_param_list.syntax().text_range()); + }) +} + +fn build_predicate(param: ast::TypeParam) -> Option { + let path = make::path_from_name_ref(make::name_ref(¶m.name()?.syntax().to_string())); + let predicate = make::where_pred(path, param.type_bound_list()?.bounds()); + Some(predicate) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::helpers::check_assist; + + #[test] + fn move_bounds_to_where_clause_fn() { + check_assist( + move_bounds_to_where_clause, + r#" + fn fooF: FnOnce(T) -> T>() {} + "#, + r#" + fn fooF>() where T: u32, F: FnOnce(T) -> T {} + "#, + ); + } + + #[test] + fn move_bounds_to_where_clause_impl() { + check_assist( + move_bounds_to_where_clause, + r#" + implT> A {} + "#, + r#" + implT> A where U: u32 {} + "#, + ); + } + + #[test] + fn move_bounds_to_where_clause_struct() { + check_assist( + move_bounds_to_where_clause, + r#" + struct A<<|>T: Iterator> {} + "#, + r#" + struct A<<|>T> where T: Iterator {} + "#, + ); + } + + #[test] + fn move_bounds_to_where_clause_tuple_struct() { + check_assist( + move_bounds_to_where_clause, + r#" + struct Pair<<|>T: u32>(T, T); + "#, + r#" + struct Pair<<|>T>(T, T) where T: u32; + "#, + ); + } +} diff --git a/crates/ra_assists/src/handlers/move_guard.rs b/crates/ra_assists/src/handlers/move_guard.rs new file mode 100644 index 000000000..2b91ce7c4 --- /dev/null +++ b/crates/ra_assists/src/handlers/move_guard.rs @@ -0,0 +1,308 @@ +use ra_syntax::{ + ast, + ast::{AstNode, AstToken, IfExpr, MatchArm}, + TextUnit, +}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: move_guard_to_arm_body +// +// Moves match guard into match arm body. +// +// ``` +// enum Action { Move { distance: u32 }, Stop } +// +// fn handle(action: Action) { +// match action { +// Action::Move { distance } <|>if distance > 10 => foo(), +// _ => (), +// } +// } +// ``` +// -> +// ``` +// enum Action { Move { distance: u32 }, Stop } +// +// fn handle(action: Action) { +// match action { +// Action::Move { distance } => if distance > 10 { foo() }, +// _ => (), +// } +// } +// ``` +pub(crate) fn move_guard_to_arm_body(ctx: AssistCtx) -> Option { + let match_arm = ctx.find_node_at_offset::()?; + let guard = match_arm.guard()?; + let space_before_guard = guard.syntax().prev_sibling_or_token(); + + let guard_conditions = guard.expr()?; + let arm_expr = match_arm.expr()?; + let buf = format!("if {} {{ {} }}", guard_conditions.syntax().text(), arm_expr.syntax().text()); + + ctx.add_assist(AssistId("move_guard_to_arm_body"), "Move guard to arm body", |edit| { + edit.target(guard.syntax().text_range()); + let offseting_amount = match space_before_guard.and_then(|it| it.into_token()) { + Some(tok) => { + if let Some(_) = ast::Whitespace::cast(tok.clone()) { + let ele = tok.text_range(); + edit.delete(ele); + ele.len() + } else { + TextUnit::from(0) + } + } + _ => TextUnit::from(0), + }; + + edit.delete(guard.syntax().text_range()); + edit.replace_node_and_indent(arm_expr.syntax(), buf); + edit.set_cursor( + arm_expr.syntax().text_range().start() + TextUnit::from(3) - offseting_amount, + ); + }) +} + +// Assist: move_arm_cond_to_match_guard +// +// Moves if expression from match arm body into a guard. +// +// ``` +// enum Action { Move { distance: u32 }, Stop } +// +// fn handle(action: Action) { +// match action { +// Action::Move { distance } => <|>if distance > 10 { foo() }, +// _ => (), +// } +// } +// ``` +// -> +// ``` +// enum Action { Move { distance: u32 }, Stop } +// +// fn handle(action: Action) { +// match action { +// Action::Move { distance } if distance > 10 => foo(), +// _ => (), +// } +// } +// ``` +pub(crate) fn move_arm_cond_to_match_guard(ctx: AssistCtx) -> Option { + let match_arm: MatchArm = ctx.find_node_at_offset::()?; + let last_match_pat = match_arm.pats().last()?; + + let arm_body = match_arm.expr()?; + let if_expr: IfExpr = IfExpr::cast(arm_body.syntax().clone())?; + let cond = if_expr.condition()?; + let then_block = if_expr.then_branch()?; + + // Not support if with else branch + if let Some(_) = if_expr.else_branch() { + return None; + } + // Not support moving if let to arm guard + if let Some(_) = cond.pat() { + return None; + } + + let buf = format!(" if {}", cond.syntax().text()); + + ctx.add_assist( + AssistId("move_arm_cond_to_match_guard"), + "Move condition to match guard", + |edit| { + edit.target(if_expr.syntax().text_range()); + let then_only_expr = then_block.block().and_then(|it| it.statements().next()).is_none(); + + match &then_block.block().and_then(|it| it.expr()) { + Some(then_expr) if then_only_expr => { + edit.replace(if_expr.syntax().text_range(), then_expr.syntax().text()) + } + _ => edit.replace(if_expr.syntax().text_range(), then_block.syntax().text()), + } + + edit.insert(last_match_pat.syntax().text_range().end(), buf); + edit.set_cursor(last_match_pat.syntax().text_range().end() + TextUnit::from(1)); + }, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; + + #[test] + fn move_guard_to_arm_body_target() { + check_assist_target( + move_guard_to_arm_body, + r#" + fn f() { + let t = 'a'; + let chars = "abcd"; + match t { + '\r' <|>if chars.clone().next() == Some('\n') => false, + _ => true + } + } + "#, + r#"if chars.clone().next() == Some('\n')"#, + ); + } + + #[test] + fn move_guard_to_arm_body_works() { + check_assist( + move_guard_to_arm_body, + r#" + fn f() { + let t = 'a'; + let chars = "abcd"; + match t { + '\r' <|>if chars.clone().next() == Some('\n') => false, + _ => true + } + } + "#, + r#" + fn f() { + let t = 'a'; + let chars = "abcd"; + match t { + '\r' => if chars.clone().next() == Some('\n') { <|>false }, + _ => true + } + } + "#, + ); + } + + #[test] + fn move_guard_to_arm_body_works_complex_match() { + check_assist( + move_guard_to_arm_body, + r#" + fn f() { + match x { + <|>y @ 4 | y @ 5 if y > 5 => true, + _ => false + } + } + "#, + r#" + fn f() { + match x { + y @ 4 | y @ 5 => if y > 5 { <|>true }, + _ => false + } + } + "#, + ); + } + + #[test] + fn move_arm_cond_to_match_guard_works() { + check_assist( + move_arm_cond_to_match_guard, + r#" + fn f() { + let t = 'a'; + let chars = "abcd"; + match t { + '\r' => if chars.clone().next() == Some('\n') { <|>false }, + _ => true + } + } + "#, + r#" + fn f() { + let t = 'a'; + let chars = "abcd"; + match t { + '\r' <|>if chars.clone().next() == Some('\n') => false, + _ => true + } + } + "#, + ); + } + + #[test] + fn move_arm_cond_to_match_guard_if_let_not_works() { + check_assist_not_applicable( + move_arm_cond_to_match_guard, + r#" + fn f() { + let t = 'a'; + let chars = "abcd"; + match t { + '\r' => if let Some(_) = chars.clone().next() { <|>false }, + _ => true + } + } + "#, + ); + } + + #[test] + fn move_arm_cond_to_match_guard_if_empty_body_works() { + check_assist( + move_arm_cond_to_match_guard, + r#" + fn f() { + let t = 'a'; + let chars = "abcd"; + match t { + '\r' => if chars.clone().next().is_some() { <|> }, + _ => true + } + } + "#, + r#" + fn f() { + let t = 'a'; + let chars = "abcd"; + match t { + '\r' <|>if chars.clone().next().is_some() => { }, + _ => true + } + } + "#, + ); + } + + #[test] + fn move_arm_cond_to_match_guard_if_multiline_body_works() { + check_assist( + move_arm_cond_to_match_guard, + r#" + fn f() { + let mut t = 'a'; + let chars = "abcd"; + match t { + '\r' => if chars.clone().next().is_some() { + t = 'e';<|> + false + }, + _ => true + } + } + "#, + r#" + fn f() { + let mut t = 'a'; + let chars = "abcd"; + match t { + '\r' <|>if chars.clone().next().is_some() => { + t = 'e'; + false + }, + _ => true + } + } + "#, + ); + } +} diff --git a/crates/ra_assists/src/handlers/raw_string.rs b/crates/ra_assists/src/handlers/raw_string.rs new file mode 100644 index 000000000..2c0a1e126 --- /dev/null +++ b/crates/ra_assists/src/handlers/raw_string.rs @@ -0,0 +1,499 @@ +use ra_syntax::{ + ast, AstToken, + SyntaxKind::{RAW_STRING, STRING}, + TextUnit, +}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: make_raw_string +// +// Adds `r#` to a plain string literal. +// +// ``` +// fn main() { +// "Hello,<|> World!"; +// } +// ``` +// -> +// ``` +// fn main() { +// r#"Hello, World!"#; +// } +// ``` +pub(crate) fn make_raw_string(ctx: AssistCtx) -> Option { + let token = ctx.find_token_at_offset(STRING).and_then(ast::String::cast)?; + let value = token.value()?; + ctx.add_assist(AssistId("make_raw_string"), "Rewrite as raw string", |edit| { + edit.target(token.syntax().text_range()); + let max_hash_streak = count_hashes(&value); + let mut hashes = String::with_capacity(max_hash_streak + 1); + for _ in 0..hashes.capacity() { + hashes.push('#'); + } + edit.replace(token.syntax().text_range(), format!("r{}\"{}\"{}", hashes, value, hashes)); + }) +} + +// Assist: make_usual_string +// +// Turns a raw string into a plain string. +// +// ``` +// fn main() { +// r#"Hello,<|> "World!""#; +// } +// ``` +// -> +// ``` +// fn main() { +// "Hello, \"World!\""; +// } +// ``` +pub(crate) fn make_usual_string(ctx: AssistCtx) -> Option { + let token = ctx.find_token_at_offset(RAW_STRING).and_then(ast::RawString::cast)?; + let value = token.value()?; + ctx.add_assist(AssistId("make_usual_string"), "Rewrite as regular string", |edit| { + edit.target(token.syntax().text_range()); + // parse inside string to escape `"` + let escaped = value.escape_default().to_string(); + edit.replace(token.syntax().text_range(), format!("\"{}\"", escaped)); + }) +} + +// Assist: add_hash +// +// Adds a hash to a raw string literal. +// +// ``` +// fn main() { +// r#"Hello,<|> World!"#; +// } +// ``` +// -> +// ``` +// fn main() { +// r##"Hello, World!"##; +// } +// ``` +pub(crate) fn add_hash(ctx: AssistCtx) -> Option { + let token = ctx.find_token_at_offset(RAW_STRING)?; + ctx.add_assist(AssistId("add_hash"), "Add # to raw string", |edit| { + edit.target(token.text_range()); + edit.insert(token.text_range().start() + TextUnit::of_char('r'), "#"); + edit.insert(token.text_range().end(), "#"); + }) +} + +// Assist: remove_hash +// +// Removes a hash from a raw string literal. +// +// ``` +// fn main() { +// r#"Hello,<|> World!"#; +// } +// ``` +// -> +// ``` +// fn main() { +// r"Hello, World!"; +// } +// ``` +pub(crate) fn remove_hash(ctx: AssistCtx) -> Option { + let token = ctx.find_token_at_offset(RAW_STRING)?; + let text = token.text().as_str(); + if text.starts_with("r\"") { + // no hash to remove + return None; + } + ctx.add_assist(AssistId("remove_hash"), "Remove hash from raw string", |edit| { + edit.target(token.text_range()); + let result = &text[2..text.len() - 1]; + let result = if result.starts_with('\"') { + // FIXME: this logic is wrong, not only the last has has to handled specially + // no more hash, escape + let internal_str = &result[1..result.len() - 1]; + format!("\"{}\"", internal_str.escape_default().to_string()) + } else { + result.to_owned() + }; + edit.replace(token.text_range(), format!("r{}", result)); + }) +} + +fn count_hashes(s: &str) -> usize { + let mut max_hash_streak = 0usize; + for idx in s.match_indices("\"#").map(|(i, _)| i) { + let (_, sub) = s.split_at(idx + 1); + let nb_hash = sub.chars().take_while(|c| *c == '#').count(); + if nb_hash > max_hash_streak { + max_hash_streak = nb_hash; + } + } + max_hash_streak +} + +#[cfg(test)] +mod test { + use super::*; + use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; + + #[test] + fn make_raw_string_target() { + check_assist_target( + make_raw_string, + r#" + fn f() { + let s = <|>"random\nstring"; + } + "#, + r#""random\nstring""#, + ); + } + + #[test] + fn make_raw_string_works() { + check_assist( + make_raw_string, + r#" + fn f() { + let s = <|>"random\nstring"; + } + "#, + r##" + fn f() { + let s = <|>r#"random +string"#; + } + "##, + ) + } + + #[test] + fn make_raw_string_works_inside_macros() { + check_assist( + make_raw_string, + r#" + fn f() { + format!(<|>"x = {}", 92) + } + "#, + r##" + fn f() { + format!(<|>r#"x = {}"#, 92) + } + "##, + ) + } + + #[test] + fn make_raw_string_hashes_inside_works() { + check_assist( + make_raw_string, + r###" + fn f() { + let s = <|>"#random##\nstring"; + } + "###, + r####" + fn f() { + let s = <|>r#"#random## +string"#; + } + "####, + ) + } + + #[test] + fn make_raw_string_closing_hashes_inside_works() { + check_assist( + make_raw_string, + r###" + fn f() { + let s = <|>"#random\"##\nstring"; + } + "###, + r####" + fn f() { + let s = <|>r###"#random"## +string"###; + } + "####, + ) + } + + #[test] + fn make_raw_string_nothing_to_unescape_works() { + check_assist( + make_raw_string, + r#" + fn f() { + let s = <|>"random string"; + } + "#, + r##" + fn f() { + let s = <|>r#"random string"#; + } + "##, + ) + } + + #[test] + fn make_raw_string_not_works_on_partial_string() { + check_assist_not_applicable( + make_raw_string, + r#" + fn f() { + let s = "foo<|> + } + "#, + ) + } + + #[test] + fn make_usual_string_not_works_on_partial_string() { + check_assist_not_applicable( + make_usual_string, + r#" + fn main() { + let s = r#"bar<|> + } + "#, + ) + } + + #[test] + fn add_hash_target() { + check_assist_target( + add_hash, + r#" + fn f() { + let s = <|>r"random string"; + } + "#, + r#"r"random string""#, + ); + } + + #[test] + fn add_hash_works() { + check_assist( + add_hash, + r#" + fn f() { + let s = <|>r"random string"; + } + "#, + r##" + fn f() { + let s = <|>r#"random string"#; + } + "##, + ) + } + + #[test] + fn add_more_hash_works() { + check_assist( + add_hash, + r##" + fn f() { + let s = <|>r#"random"string"#; + } + "##, + r###" + fn f() { + let s = <|>r##"random"string"##; + } + "###, + ) + } + + #[test] + fn add_hash_not_works() { + check_assist_not_applicable( + add_hash, + r#" + fn f() { + let s = <|>"random string"; + } + "#, + ); + } + + #[test] + fn remove_hash_target() { + check_assist_target( + remove_hash, + r##" + fn f() { + let s = <|>r#"random string"#; + } + "##, + r##"r#"random string"#"##, + ); + } + + #[test] + fn remove_hash_works() { + check_assist( + remove_hash, + r##" + fn f() { + let s = <|>r#"random string"#; + } + "##, + r#" + fn f() { + let s = <|>r"random string"; + } + "#, + ) + } + + #[test] + fn remove_hash_with_quote_works() { + check_assist( + remove_hash, + r##" + fn f() { + let s = <|>r#"random"str"ing"#; + } + "##, + r#" + fn f() { + let s = <|>r"random\"str\"ing"; + } + "#, + ) + } + + #[test] + fn remove_more_hash_works() { + check_assist( + remove_hash, + r###" + fn f() { + let s = <|>r##"random string"##; + } + "###, + r##" + fn f() { + let s = <|>r#"random string"#; + } + "##, + ) + } + + #[test] + fn remove_hash_not_works() { + check_assist_not_applicable( + remove_hash, + r#" + fn f() { + let s = <|>"random string"; + } + "#, + ); + } + + #[test] + fn remove_hash_no_hash_not_works() { + check_assist_not_applicable( + remove_hash, + r#" + fn f() { + let s = <|>r"random string"; + } + "#, + ); + } + + #[test] + fn make_usual_string_target() { + check_assist_target( + make_usual_string, + r##" + fn f() { + let s = <|>r#"random string"#; + } + "##, + r##"r#"random string"#"##, + ); + } + + #[test] + fn make_usual_string_works() { + check_assist( + make_usual_string, + r##" + fn f() { + let s = <|>r#"random string"#; + } + "##, + r#" + fn f() { + let s = <|>"random string"; + } + "#, + ) + } + + #[test] + fn make_usual_string_with_quote_works() { + check_assist( + make_usual_string, + r##" + fn f() { + let s = <|>r#"random"str"ing"#; + } + "##, + r#" + fn f() { + let s = <|>"random\"str\"ing"; + } + "#, + ) + } + + #[test] + fn make_usual_string_more_hash_works() { + check_assist( + make_usual_string, + r###" + fn f() { + let s = <|>r##"random string"##; + } + "###, + r##" + fn f() { + let s = <|>"random string"; + } + "##, + ) + } + + #[test] + fn make_usual_string_not_works() { + check_assist_not_applicable( + make_usual_string, + r#" + fn f() { + let s = <|>"random string"; + } + "#, + ); + } + + #[test] + fn count_hashes_test() { + assert_eq!(0, count_hashes("abc")); + assert_eq!(0, count_hashes("###")); + assert_eq!(1, count_hashes("\"#abc")); + assert_eq!(0, count_hashes("#abc")); + assert_eq!(2, count_hashes("#ab\"##c")); + assert_eq!(4, count_hashes("#ab\"##\"####c")); + } +} diff --git a/crates/ra_assists/src/handlers/remove_dbg.rs b/crates/ra_assists/src/handlers/remove_dbg.rs new file mode 100644 index 000000000..5085649b4 --- /dev/null +++ b/crates/ra_assists/src/handlers/remove_dbg.rs @@ -0,0 +1,150 @@ +use ra_syntax::{ + ast::{self, AstNode}, + TextUnit, T, +}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: remove_dbg +// +// Removes `dbg!()` macro call. +// +// ``` +// fn main() { +// <|>dbg!(92); +// } +// ``` +// -> +// ``` +// fn main() { +// 92; +// } +// ``` +pub(crate) fn remove_dbg(ctx: AssistCtx) -> Option { + let macro_call = ctx.find_node_at_offset::()?; + + if !is_valid_macrocall(¯o_call, "dbg")? { + return None; + } + + let macro_range = macro_call.syntax().text_range(); + + // If the cursor is inside the macro call, we'll try to maintain the cursor + // position by subtracting the length of dbg!( from the start of the file + // range, otherwise we'll default to using the start of the macro call + let cursor_pos = { + let file_range = ctx.frange.range; + + let offset_start = file_range + .start() + .checked_sub(macro_range.start()) + .unwrap_or_else(|| TextUnit::from(0)); + + let dbg_size = TextUnit::of_str("dbg!("); + + if offset_start > dbg_size { + file_range.start() - dbg_size + } else { + macro_range.start() + } + }; + + let macro_content = { + let macro_args = macro_call.token_tree()?.syntax().clone(); + + let text = macro_args.text(); + let without_parens = TextUnit::of_char('(')..text.len() - TextUnit::of_char(')'); + text.slice(without_parens).to_string() + }; + + ctx.add_assist(AssistId("remove_dbg"), "Remove dbg!()", |edit| { + edit.target(macro_call.syntax().text_range()); + edit.replace(macro_range, macro_content); + edit.set_cursor(cursor_pos); + }) +} + +/// Verifies that the given macro_call actually matches the given name +/// and contains proper ending tokens +fn is_valid_macrocall(macro_call: &ast::MacroCall, macro_name: &str) -> Option { + let path = macro_call.path()?; + let name_ref = path.segment()?.name_ref()?; + + // Make sure it is actually a dbg-macro call, dbg followed by ! + let excl = path.syntax().next_sibling_or_token()?; + + if name_ref.text() != macro_name || excl.kind() != T![!] { + return None; + } + + let node = macro_call.token_tree()?.syntax().clone(); + let first_child = node.first_child_or_token()?; + let last_child = node.last_child_or_token()?; + + match (first_child.kind(), last_child.kind()) { + (T!['('], T![')']) | (T!['['], T![']']) | (T!['{'], T!['}']) => Some(true), + _ => Some(false), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::{check_assist, check_assist_not_applicable, check_assist_target}; + + #[test] + fn test_remove_dbg() { + check_assist(remove_dbg, "<|>dbg!(1 + 1)", "<|>1 + 1"); + + check_assist(remove_dbg, "dbg!<|>((1 + 1))", "<|>(1 + 1)"); + + check_assist(remove_dbg, "dbg!(1 <|>+ 1)", "1 <|>+ 1"); + + check_assist(remove_dbg, "let _ = <|>dbg!(1 + 1)", "let _ = <|>1 + 1"); + + check_assist( + remove_dbg, + " +fn foo(n: usize) { + if let Some(_) = dbg!(n.<|>checked_sub(4)) { + // ... + } +} +", + " +fn foo(n: usize) { + if let Some(_) = n.<|>checked_sub(4) { + // ... + } +} +", + ); + } + #[test] + fn test_remove_dbg_with_brackets_and_braces() { + check_assist(remove_dbg, "dbg![<|>1 + 1]", "<|>1 + 1"); + check_assist(remove_dbg, "dbg!{<|>1 + 1}", "<|>1 + 1"); + } + + #[test] + fn test_remove_dbg_not_applicable() { + check_assist_not_applicable(remove_dbg, "<|>vec![1, 2, 3]"); + check_assist_not_applicable(remove_dbg, "<|>dbg(5, 6, 7)"); + check_assist_not_applicable(remove_dbg, "<|>dbg!(5, 6, 7"); + } + + #[test] + fn remove_dbg_target() { + check_assist_target( + remove_dbg, + " +fn foo(n: usize) { + if let Some(_) = dbg!(n.<|>checked_sub(4)) { + // ... + } +} +", + "dbg!(n.checked_sub(4))", + ); + } +} diff --git a/crates/ra_assists/src/handlers/replace_if_let_with_match.rs b/crates/ra_assists/src/handlers/replace_if_let_with_match.rs new file mode 100644 index 000000000..e6cd50bc1 --- /dev/null +++ b/crates/ra_assists/src/handlers/replace_if_let_with_match.rs @@ -0,0 +1,148 @@ +use ra_fmt::unwrap_trivial_block; +use ra_syntax::{ + ast::{self, make}, + AstNode, +}; + +use crate::{Assist, AssistCtx, AssistId}; +use ast::edit::IndentLevel; + +// Assist: replace_if_let_with_match +// +// Replaces `if let` with an else branch with a `match` expression. +// +// ``` +// enum Action { Move { distance: u32 }, Stop } +// +// fn handle(action: Action) { +// <|>if let Action::Move { distance } = action { +// foo(distance) +// } else { +// bar() +// } +// } +// ``` +// -> +// ``` +// enum Action { Move { distance: u32 }, Stop } +// +// fn handle(action: Action) { +// match action { +// Action::Move { distance } => foo(distance), +// _ => bar(), +// } +// } +// ``` +pub(crate) fn replace_if_let_with_match(ctx: AssistCtx) -> Option { + let if_expr: ast::IfExpr = ctx.find_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::ElseBranch::Block(it) => it, + ast::ElseBranch::IfExpr(_) => return None, + }; + + ctx.add_assist(AssistId("replace_if_let_with_match"), "Replace with match", |edit| { + let match_expr = { + let then_arm = { + let then_expr = unwrap_trivial_block(then_block); + make::match_arm(vec![pat], then_expr) + }; + let else_arm = { + let else_expr = unwrap_trivial_block(else_block); + make::match_arm(vec![make::placeholder_pat().into()], else_expr) + }; + make::expr_match(expr, make::match_arm_list(vec![then_arm, else_arm])) + }; + + let match_expr = IndentLevel::from_node(if_expr.syntax()).increase_indent(match_expr); + + edit.target(if_expr.syntax().text_range()); + edit.set_cursor(if_expr.syntax().text_range().start()); + edit.replace_ast::(if_expr.into(), match_expr.into()); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::{check_assist, check_assist_target}; + + #[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, + } + } +} ", + ) + } + + #[test] + fn test_replace_if_let_with_match_doesnt_unwrap_multiline_expressions() { + check_assist( + replace_if_let_with_match, + " +fn foo() { + if <|>let VariantData::Struct(..) = a { + bar( + 123 + ) + } else { + false + } +} ", + " +fn foo() { + <|>match a { + VariantData::Struct(..) => { + bar( + 123 + ) + } + _ => false, + } +} ", + ) + } + + #[test] + fn replace_if_let_with_match_target() { + check_assist_target( + replace_if_let_with_match, + " +impl VariantData { + pub fn is_struct(&self) -> bool { + if <|>let VariantData::Struct(..) = *self { + true + } else { + false + } + } +} ", + "if let VariantData::Struct(..) = *self { + true + } else { + false + }", + ); + } +} diff --git a/crates/ra_assists/src/handlers/split_import.rs b/crates/ra_assists/src/handlers/split_import.rs new file mode 100644 index 000000000..2c3f07a79 --- /dev/null +++ b/crates/ra_assists/src/handlers/split_import.rs @@ -0,0 +1,69 @@ +use std::iter::successors; + +use ra_syntax::{ast, AstNode, TextUnit, T}; + +use crate::{Assist, AssistCtx, AssistId}; + +// Assist: split_import +// +// Wraps the tail of import into braces. +// +// ``` +// use std::<|>collections::HashMap; +// ``` +// -> +// ``` +// use std::{collections::HashMap}; +// ``` +pub(crate) fn split_import(ctx: AssistCtx) -> Option { + let colon_colon = ctx.find_token_at_offset(T![::])?; + let path = ast::Path::cast(colon_colon.parent())?; + let top_path = successors(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.text_range().end(); + let r_curly = match top_path.syntax().parent().and_then(ast::UseTree::cast) { + Some(tree) => tree.syntax().text_range().end(), + None => top_path.syntax().text_range().end(), + }; + + ctx.add_assist(AssistId("split_import"), "Split import", |edit| { + edit.target(colon_colon.text_range()); + edit.insert(l_curly, "{"); + edit.insert(r_curly, "}"); + edit.set_cursor(l_curly + TextUnit::of_str("{")); + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::{check_assist, check_assist_target}; + + #[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 crate:<|>:db::{RootDatabase, FileSymbol}", + "use crate::{<|>db::{RootDatabase, FileSymbol}}", + ) + } + + #[test] + fn split_import_target() { + check_assist_target(split_import, "use crate::<|>db::{RootDatabase, FileSymbol}", "::"); + } +} diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index d476088a2..7b08e8fd7 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -19,8 +19,8 @@ use ra_ide_db::RootDatabase; use ra_syntax::{TextRange, TextUnit}; use ra_text_edit::TextEdit; -pub(crate) use crate::assist_ctx::{Assist, AssistCtx}; -pub use crate::assists::add_import::auto_import_text_edit; +pub(crate) use crate::assist_ctx::{Assist, AssistCtx, AssistHandler}; +pub use crate::handlers::add_import::auto_import_text_edit; /// Unique identifier of the assist, should not be shown to the user /// directly. @@ -72,7 +72,7 @@ impl ResolvedAssist { /// returned, without actual edits. pub fn unresolved_assists(db: &RootDatabase, range: FileRange) -> Vec { let ctx = AssistCtx::new(db, range, false); - assists::all() + handlers::all() .iter() .filter_map(|f| f(ctx.clone())) .map(|a| match a { @@ -88,7 +88,7 @@ pub fn unresolved_assists(db: &RootDatabase, range: FileRange) -> Vec Vec { let ctx = AssistCtx::new(db, range, true); - let mut a = assists::all() + let mut a = handlers::all() .iter() .filter_map(|f| f(ctx.clone())) .map(|a| match a { @@ -109,8 +109,8 @@ fn sort_assists(assists: &mut [ResolvedAssist]) { }); } -mod assists { - use crate::{Assist, AssistCtx}; +mod handlers { + use crate::AssistHandler; mod add_derive; mod add_explicit_type; @@ -138,7 +138,7 @@ mod assists { mod move_bounds; mod early_return; - pub(crate) fn all() -> &'static [fn(AssistCtx) -> Option] { + pub(crate) fn all() -> &'static [AssistHandler] { &[ add_derive::add_derive, add_explicit_type::add_explicit_type, @@ -183,7 +183,7 @@ mod helpers { use ra_syntax::TextRange; use test_utils::{add_cursor, assert_eq_text, extract_offset, extract_range}; - use crate::{Assist, AssistCtx}; + use crate::{Assist, AssistCtx, AssistHandler}; pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) { let (mut db, file_id) = RootDatabase::with_single_file(text); @@ -194,7 +194,7 @@ mod helpers { (db, file_id) } - pub(crate) fn check_assist(assist: fn(AssistCtx) -> Option, before: &str, after: &str) { + pub(crate) fn check_assist(assist: AssistHandler, before: &str, after: &str) { let (before_cursor_pos, before) = extract_offset(before); let (db, file_id) = with_single_file(&before); let frange = @@ -218,11 +218,7 @@ mod helpers { assert_eq_text!(after, &actual); } - pub(crate) fn check_assist_range( - assist: fn(AssistCtx) -> Option, - before: &str, - after: &str, - ) { + pub(crate) fn check_assist_range(assist: AssistHandler, before: &str, after: &str) { let (range, before) = extract_range(before); let (db, file_id) = with_single_file(&before); let frange = FileRange { file_id, range }; @@ -240,11 +236,7 @@ mod helpers { assert_eq_text!(after, &actual); } - pub(crate) fn check_assist_target( - assist: fn(AssistCtx) -> Option, - before: &str, - target: &str, - ) { + pub(crate) fn check_assist_target(assist: AssistHandler, before: &str, target: &str) { let (before_cursor_pos, before) = extract_offset(before); let (db, file_id) = with_single_file(&before); let frange = @@ -260,11 +252,7 @@ mod helpers { assert_eq_text!(&before[range.start().to_usize()..range.end().to_usize()], target); } - pub(crate) fn check_assist_range_target( - assist: fn(AssistCtx) -> Option, - before: &str, - target: &str, - ) { + pub(crate) fn check_assist_range_target(assist: AssistHandler, before: &str, target: &str) { let (range, before) = extract_range(before); let (db, file_id) = with_single_file(&before); let frange = FileRange { file_id, range }; @@ -279,10 +267,7 @@ mod helpers { assert_eq_text!(&before[range.start().to_usize()..range.end().to_usize()], target); } - pub(crate) fn check_assist_not_applicable( - assist: fn(AssistCtx) -> Option, - before: &str, - ) { + pub(crate) fn check_assist_not_applicable(assist: AssistHandler, before: &str) { let (before_cursor_pos, before) = extract_offset(before); let (db, file_id) = with_single_file(&before); let frange = @@ -291,10 +276,7 @@ mod helpers { assert!(assist.is_none()); } - pub(crate) fn check_assist_range_not_applicable( - assist: fn(AssistCtx) -> Option, - before: &str, - ) { + pub(crate) fn check_assist_range_not_applicable(assist: AssistHandler, before: &str) { let (range, before) = extract_range(before); let (db, file_id) = with_single_file(&before); let frange = FileRange { file_id, range }; -- cgit v1.2.3 From d00add1f1fec59494c3c1a99c27937ae3891458d Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 7 Feb 2020 15:57:38 +0100 Subject: Introduce assists utils --- crates/ra_assists/src/handlers/apply_demorgan.rs | 3 +-- crates/ra_assists/src/handlers/early_return.rs | 2 +- crates/ra_assists/src/handlers/invert_if.rs | 25 ++-------------------- crates/ra_assists/src/lib.rs | 1 + crates/ra_assists/src/utils.rs | 27 ++++++++++++++++++++++++ 5 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 crates/ra_assists/src/utils.rs (limited to 'crates') diff --git a/crates/ra_assists/src/handlers/apply_demorgan.rs b/crates/ra_assists/src/handlers/apply_demorgan.rs index ba08a8223..239807e24 100644 --- a/crates/ra_assists/src/handlers/apply_demorgan.rs +++ b/crates/ra_assists/src/handlers/apply_demorgan.rs @@ -1,7 +1,6 @@ -use super::invert_if::invert_boolean_expression; use ra_syntax::ast::{self, AstNode}; -use crate::{Assist, AssistCtx, AssistId}; +use crate::{utils::invert_boolean_expression, Assist, AssistCtx, AssistId}; // Assist: apply_demorgan // diff --git a/crates/ra_assists/src/handlers/early_return.rs b/crates/ra_assists/src/handlers/early_return.rs index 8eb3bd473..22f88884f 100644 --- a/crates/ra_assists/src/handlers/early_return.rs +++ b/crates/ra_assists/src/handlers/early_return.rs @@ -10,7 +10,7 @@ use ra_syntax::{ use crate::{ assist_ctx::{Assist, AssistCtx}, - handlers::invert_if::invert_boolean_expression, + utils::invert_boolean_expression, AssistId, }; diff --git a/crates/ra_assists/src/handlers/invert_if.rs b/crates/ra_assists/src/handlers/invert_if.rs index 983392f21..a594e7e0c 100644 --- a/crates/ra_assists/src/handlers/invert_if.rs +++ b/crates/ra_assists/src/handlers/invert_if.rs @@ -1,7 +1,7 @@ -use ra_syntax::ast::{self, make, AstNode}; +use ra_syntax::ast::{self, AstNode}; use ra_syntax::T; -use crate::{Assist, AssistCtx, AssistId}; +use crate::{utils::invert_boolean_expression, Assist, AssistCtx, AssistId}; // Assist: invert_if // @@ -51,27 +51,6 @@ pub(crate) fn invert_if(ctx: AssistCtx) -> Option { None } -pub(crate) fn invert_boolean_expression(expr: ast::Expr) -> ast::Expr { - if let Some(expr) = invert_special_case(&expr) { - return expr; - } - make::expr_prefix(T![!], expr) -} - -pub(crate) fn invert_special_case(expr: &ast::Expr) -> Option { - match expr { - ast::Expr::BinExpr(bin) => match bin.op_kind()? { - ast::BinOp::NegatedEqualityTest => bin.replace_op(T![==]).map(|it| it.into()), - ast::BinOp::EqualityTest => bin.replace_op(T![!=]).map(|it| it.into()), - _ => None, - }, - ast::Expr::PrefixExpr(pe) if pe.op_kind()? == ast::PrefixOp::Not => pe.expr(), - // FIXME: - // ast::Expr::Literal(true | false ) - _ => None, - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 7b08e8fd7..eca6dec4b 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs @@ -9,6 +9,7 @@ mod assist_ctx; mod marks; #[cfg(test)] mod doc_tests; +mod utils; pub mod ast_transform; use std::cmp::Ordering; diff --git a/crates/ra_assists/src/utils.rs b/crates/ra_assists/src/utils.rs new file mode 100644 index 000000000..0d5722295 --- /dev/null +++ b/crates/ra_assists/src/utils.rs @@ -0,0 +1,27 @@ +//! Assorted functions shared by several assists. + +use ra_syntax::{ + ast::{self, make}, + T, +}; + +pub(crate) fn invert_boolean_expression(expr: ast::Expr) -> ast::Expr { + if let Some(expr) = invert_special_case(&expr) { + return expr; + } + make::expr_prefix(T![!], expr) +} + +fn invert_special_case(expr: &ast::Expr) -> Option { + match expr { + ast::Expr::BinExpr(bin) => match bin.op_kind()? { + ast::BinOp::NegatedEqualityTest => bin.replace_op(T![==]).map(|it| it.into()), + ast::BinOp::EqualityTest => bin.replace_op(T![!=]).map(|it| it.into()), + _ => None, + }, + ast::Expr::PrefixExpr(pe) if pe.op_kind()? == ast::PrefixOp::Not => pe.expr(), + // FIXME: + // ast::Expr::Literal(true | false ) + _ => None, + } +} -- cgit v1.2.3