mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-12-26 21:13:37 +00:00
Auto merge of #15874 - DropDemBits:structured-snippet-migrate-4, r=Veykril
internal: Migrate assists to the structured snippet API, part 4 Continuing from #15260 Migrates the following assists: - `add_turbo_fish` - `add_type_ascription` - `destructure_tuple_binding` - `destructure_tuple_binding_in_subpattern` I did this a while ago, but forgot to make a PR for the changes until now. 😅
This commit is contained in:
commit
535eb0da9d
5 changed files with 433 additions and 141 deletions
|
@ -1,6 +1,9 @@
|
|||
use either::Either;
|
||||
use ide_db::defs::{Definition, NameRefClass};
|
||||
use itertools::Itertools;
|
||||
use syntax::{ast, AstNode, SyntaxKind, T};
|
||||
use syntax::{
|
||||
ast::{self, make, HasArgList},
|
||||
ted, AstNode,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
assist_context::{AssistContext, Assists},
|
||||
|
@ -25,21 +28,45 @@ use crate::{
|
|||
// }
|
||||
// ```
|
||||
pub(crate) fn add_turbo_fish(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
|
||||
let ident = ctx.find_token_syntax_at_offset(SyntaxKind::IDENT).or_else(|| {
|
||||
let arg_list = ctx.find_node_at_offset::<ast::ArgList>()?;
|
||||
if arg_list.args().next().is_some() {
|
||||
return None;
|
||||
}
|
||||
cov_mark::hit!(add_turbo_fish_after_call);
|
||||
cov_mark::hit!(add_type_ascription_after_call);
|
||||
arg_list.l_paren_token()?.prev_token().filter(|it| it.kind() == SyntaxKind::IDENT)
|
||||
})?;
|
||||
let next_token = ident.next_token()?;
|
||||
if next_token.kind() == T![::] {
|
||||
let turbofish_target =
|
||||
ctx.find_node_at_offset::<ast::PathSegment>().map(Either::Left).or_else(|| {
|
||||
let callable_expr = ctx.find_node_at_offset::<ast::CallableExpr>()?;
|
||||
|
||||
if callable_expr.arg_list()?.args().next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
cov_mark::hit!(add_turbo_fish_after_call);
|
||||
cov_mark::hit!(add_type_ascription_after_call);
|
||||
|
||||
match callable_expr {
|
||||
ast::CallableExpr::Call(it) => {
|
||||
let ast::Expr::PathExpr(path) = it.expr()? else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(Either::Left(path.path()?.segment()?))
|
||||
}
|
||||
ast::CallableExpr::MethodCall(it) => Some(Either::Right(it)),
|
||||
}
|
||||
})?;
|
||||
|
||||
let already_has_turbofish = match &turbofish_target {
|
||||
Either::Left(path_segment) => path_segment.generic_arg_list().is_some(),
|
||||
Either::Right(method_call) => method_call.generic_arg_list().is_some(),
|
||||
};
|
||||
|
||||
if already_has_turbofish {
|
||||
cov_mark::hit!(add_turbo_fish_one_fish_is_enough);
|
||||
return None;
|
||||
}
|
||||
let name_ref = ast::NameRef::cast(ident.parent()?)?;
|
||||
|
||||
let name_ref = match &turbofish_target {
|
||||
Either::Left(path_segment) => path_segment.name_ref()?,
|
||||
Either::Right(method_call) => method_call.name_ref()?,
|
||||
};
|
||||
let ident = name_ref.ident_token()?;
|
||||
|
||||
let def = match NameRefClass::classify(&ctx.sema, &name_ref)? {
|
||||
NameRefClass::Definition(def) => def,
|
||||
NameRefClass::FieldShorthand { .. } | NameRefClass::ExternCrateShorthand { .. } => {
|
||||
|
@ -58,20 +85,27 @@ pub(crate) fn add_turbo_fish(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opti
|
|||
|
||||
if let Some(let_stmt) = ctx.find_node_at_offset::<ast::LetStmt>() {
|
||||
if let_stmt.colon_token().is_none() {
|
||||
let type_pos = let_stmt.pat()?.syntax().last_token()?.text_range().end();
|
||||
let semi_pos = let_stmt.syntax().last_token()?.text_range().end();
|
||||
if let_stmt.pat().is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
acc.add(
|
||||
AssistId("add_type_ascription", AssistKind::RefactorRewrite),
|
||||
"Add `: _` before assignment operator",
|
||||
ident.text_range(),
|
||||
|builder| {
|
||||
|edit| {
|
||||
let let_stmt = edit.make_mut(let_stmt);
|
||||
|
||||
if let_stmt.semicolon_token().is_none() {
|
||||
builder.insert(semi_pos, ";");
|
||||
ted::append_child(let_stmt.syntax(), make::tokens::semicolon());
|
||||
}
|
||||
match ctx.config.snippet_cap {
|
||||
Some(cap) => builder.insert_snippet(cap, type_pos, ": ${0:_}"),
|
||||
None => builder.insert(type_pos, ": _"),
|
||||
|
||||
let placeholder_ty = make::ty_placeholder().clone_for_update();
|
||||
|
||||
let_stmt.set_ty(Some(placeholder_ty.clone()));
|
||||
|
||||
if let Some(cap) = ctx.config.snippet_cap {
|
||||
edit.add_placeholder_snippet(cap, placeholder_ty);
|
||||
}
|
||||
},
|
||||
)?
|
||||
|
@ -91,38 +125,46 @@ pub(crate) fn add_turbo_fish(acc: &mut Assists, ctx: &AssistContext<'_>) -> Opti
|
|||
AssistId("add_turbo_fish", AssistKind::RefactorRewrite),
|
||||
"Add `::<>`",
|
||||
ident.text_range(),
|
||||
|builder| {
|
||||
builder.trigger_signature_help();
|
||||
match ctx.config.snippet_cap {
|
||||
Some(cap) => {
|
||||
let fish_head = get_snippet_fish_head(number_of_arguments);
|
||||
let snip = format!("::<{fish_head}>");
|
||||
builder.insert_snippet(cap, ident.text_range().end(), snip)
|
||||
|edit| {
|
||||
edit.trigger_signature_help();
|
||||
|
||||
let new_arg_list = match turbofish_target {
|
||||
Either::Left(path_segment) => {
|
||||
edit.make_mut(path_segment).get_or_create_generic_arg_list()
|
||||
}
|
||||
None => {
|
||||
let fish_head = std::iter::repeat("_").take(number_of_arguments).format(", ");
|
||||
let snip = format!("::<{fish_head}>");
|
||||
builder.insert(ident.text_range().end(), snip);
|
||||
Either::Right(method_call) => {
|
||||
edit.make_mut(method_call).get_or_create_generic_arg_list()
|
||||
}
|
||||
};
|
||||
|
||||
let fish_head = get_fish_head(number_of_arguments).clone_for_update();
|
||||
|
||||
// Note: we need to replace the `new_arg_list` instead of being able to use something like
|
||||
// `GenericArgList::add_generic_arg` as `PathSegment::get_or_create_generic_arg_list`
|
||||
// always creates a non-turbofish form generic arg list.
|
||||
ted::replace(new_arg_list.syntax(), fish_head.syntax());
|
||||
|
||||
if let Some(cap) = ctx.config.snippet_cap {
|
||||
for arg in fish_head.generic_args() {
|
||||
edit.add_placeholder_snippet(cap, arg)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// This will create a snippet string with tabstops marked
|
||||
fn get_snippet_fish_head(number_of_arguments: usize) -> String {
|
||||
let mut fish_head = (1..number_of_arguments)
|
||||
.format_with("", |i, f| f(&format_args!("${{{i}:_}}, ")))
|
||||
.to_string();
|
||||
|
||||
// tabstop 0 is a special case and always the last one
|
||||
fish_head.push_str("${0:_}");
|
||||
fish_head
|
||||
/// This will create a turbofish generic arg list corresponding to the number of arguments
|
||||
fn get_fish_head(number_of_arguments: usize) -> ast::GenericArgList {
|
||||
let args = (0..number_of_arguments).map(|_| make::type_arg(make::ty_placeholder()).into());
|
||||
make::turbofish_generic_arg_list(args)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::{check_assist, check_assist_by_label, check_assist_not_applicable};
|
||||
use crate::tests::{
|
||||
check_assist, check_assist_by_label, check_assist_not_applicable,
|
||||
check_assist_not_applicable_by_label,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -363,6 +405,20 @@ fn main() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_type_ascription_missing_pattern() {
|
||||
check_assist_not_applicable_by_label(
|
||||
add_turbo_fish,
|
||||
r#"
|
||||
fn make<T>() -> T {}
|
||||
fn main() {
|
||||
let = make$0()
|
||||
}
|
||||
"#,
|
||||
"Add `: _` before assignment operator",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_turbo_fish_function_lifetime_parameter() {
|
||||
check_assist(
|
||||
|
|
|
@ -3,10 +3,12 @@ use ide_db::{
|
|||
defs::Definition,
|
||||
search::{FileReference, SearchScope, UsageSearchResult},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use syntax::{
|
||||
ast::{self, AstNode, FieldExpr, HasName, IdentPat, MethodCallExpr},
|
||||
TextRange,
|
||||
ast::{self, make, AstNode, FieldExpr, HasName, IdentPat, MethodCallExpr},
|
||||
ted, T,
|
||||
};
|
||||
use text_edit::TextRange;
|
||||
|
||||
use crate::assist_context::{AssistContext, Assists, SourceChangeBuilder};
|
||||
|
||||
|
@ -61,27 +63,36 @@ pub(crate) fn destructure_tuple_binding_impl(
|
|||
acc.add(
|
||||
AssistId("destructure_tuple_binding_in_sub_pattern", AssistKind::RefactorRewrite),
|
||||
"Destructure tuple in sub-pattern",
|
||||
data.range,
|
||||
|builder| {
|
||||
edit_tuple_assignment(ctx, builder, &data, true);
|
||||
edit_tuple_usages(&data, builder, ctx, true);
|
||||
},
|
||||
data.ident_pat.syntax().text_range(),
|
||||
|edit| destructure_tuple_edit_impl(ctx, edit, &data, true),
|
||||
);
|
||||
}
|
||||
|
||||
acc.add(
|
||||
AssistId("destructure_tuple_binding", AssistKind::RefactorRewrite),
|
||||
if with_sub_pattern { "Destructure tuple in place" } else { "Destructure tuple" },
|
||||
data.range,
|
||||
|builder| {
|
||||
edit_tuple_assignment(ctx, builder, &data, false);
|
||||
edit_tuple_usages(&data, builder, ctx, false);
|
||||
},
|
||||
data.ident_pat.syntax().text_range(),
|
||||
|edit| destructure_tuple_edit_impl(ctx, edit, &data, false),
|
||||
);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn destructure_tuple_edit_impl(
|
||||
ctx: &AssistContext<'_>,
|
||||
edit: &mut SourceChangeBuilder,
|
||||
data: &TupleData,
|
||||
in_sub_pattern: bool,
|
||||
) {
|
||||
let assignment_edit = edit_tuple_assignment(ctx, edit, &data, in_sub_pattern);
|
||||
let current_file_usages_edit = edit_tuple_usages(&data, edit, ctx, in_sub_pattern);
|
||||
|
||||
assignment_edit.apply();
|
||||
if let Some(usages_edit) = current_file_usages_edit {
|
||||
usages_edit.into_iter().for_each(|usage_edit| usage_edit.apply(edit))
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleData> {
|
||||
if ident_pat.at_token().is_some() {
|
||||
// Cannot destructure pattern with sub-pattern:
|
||||
|
@ -109,7 +120,6 @@ fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleDat
|
|||
}
|
||||
|
||||
let name = ident_pat.name()?.to_string();
|
||||
let range = ident_pat.syntax().text_range();
|
||||
|
||||
let usages = ctx.sema.to_def(&ident_pat).map(|def| {
|
||||
Definition::Local(def)
|
||||
|
@ -122,7 +132,7 @@ fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleDat
|
|||
.map(|i| generate_name(ctx, i, &name, &ident_pat, &usages))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Some(TupleData { ident_pat, range, ref_type, field_names, usages })
|
||||
Some(TupleData { ident_pat, ref_type, field_names, usages })
|
||||
}
|
||||
|
||||
fn generate_name(
|
||||
|
@ -142,72 +152,100 @@ enum RefType {
|
|||
}
|
||||
struct TupleData {
|
||||
ident_pat: IdentPat,
|
||||
// name: String,
|
||||
range: TextRange,
|
||||
ref_type: Option<RefType>,
|
||||
field_names: Vec<String>,
|
||||
// field_types: Vec<Type>,
|
||||
usages: Option<UsageSearchResult>,
|
||||
}
|
||||
fn edit_tuple_assignment(
|
||||
ctx: &AssistContext<'_>,
|
||||
builder: &mut SourceChangeBuilder,
|
||||
edit: &mut SourceChangeBuilder,
|
||||
data: &TupleData,
|
||||
in_sub_pattern: bool,
|
||||
) {
|
||||
) -> AssignmentEdit {
|
||||
let ident_pat = edit.make_mut(data.ident_pat.clone());
|
||||
|
||||
let tuple_pat = {
|
||||
let original = &data.ident_pat;
|
||||
let is_ref = original.ref_token().is_some();
|
||||
let is_mut = original.mut_token().is_some();
|
||||
let fields = data.field_names.iter().map(|name| {
|
||||
ast::Pat::from(ast::make::ident_pat(is_ref, is_mut, ast::make::name(name)))
|
||||
});
|
||||
ast::make::tuple_pat(fields)
|
||||
let fields = data
|
||||
.field_names
|
||||
.iter()
|
||||
.map(|name| ast::Pat::from(make::ident_pat(is_ref, is_mut, make::name(name))));
|
||||
make::tuple_pat(fields).clone_for_update()
|
||||
};
|
||||
|
||||
let add_cursor = |text: &str| {
|
||||
// place cursor on first tuple item
|
||||
let first_tuple = &data.field_names[0];
|
||||
text.replacen(first_tuple, &format!("$0{first_tuple}"), 1)
|
||||
};
|
||||
if let Some(cap) = ctx.config.snippet_cap {
|
||||
// place cursor on first tuple name
|
||||
if let Some(ast::Pat::IdentPat(first_pat)) = tuple_pat.fields().next() {
|
||||
edit.add_tabstop_before(
|
||||
cap,
|
||||
first_pat.name().expect("first ident pattern should have a name"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// with sub_pattern: keep original tuple and add subpattern: `tup @ (_0, _1)`
|
||||
if in_sub_pattern {
|
||||
let text = format!(" @ {tuple_pat}");
|
||||
match ctx.config.snippet_cap {
|
||||
Some(cap) => {
|
||||
let snip = add_cursor(&text);
|
||||
builder.insert_snippet(cap, data.range.end(), snip);
|
||||
}
|
||||
None => builder.insert(data.range.end(), text),
|
||||
};
|
||||
} else {
|
||||
let text = tuple_pat.to_string();
|
||||
match ctx.config.snippet_cap {
|
||||
Some(cap) => {
|
||||
let snip = add_cursor(&text);
|
||||
builder.replace_snippet(cap, data.range, snip);
|
||||
}
|
||||
None => builder.replace(data.range, text),
|
||||
};
|
||||
AssignmentEdit { ident_pat, tuple_pat, in_sub_pattern }
|
||||
}
|
||||
struct AssignmentEdit {
|
||||
ident_pat: ast::IdentPat,
|
||||
tuple_pat: ast::TuplePat,
|
||||
in_sub_pattern: bool,
|
||||
}
|
||||
|
||||
impl AssignmentEdit {
|
||||
fn apply(self) {
|
||||
// with sub_pattern: keep original tuple and add subpattern: `tup @ (_0, _1)`
|
||||
if self.in_sub_pattern {
|
||||
self.ident_pat.set_pat(Some(self.tuple_pat.into()))
|
||||
} else {
|
||||
ted::replace(self.ident_pat.syntax(), self.tuple_pat.syntax())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_tuple_usages(
|
||||
data: &TupleData,
|
||||
builder: &mut SourceChangeBuilder,
|
||||
edit: &mut SourceChangeBuilder,
|
||||
ctx: &AssistContext<'_>,
|
||||
in_sub_pattern: bool,
|
||||
) {
|
||||
if let Some(usages) = data.usages.as_ref() {
|
||||
for (file_id, refs) in usages.iter() {
|
||||
builder.edit_file(*file_id);
|
||||
) -> Option<Vec<EditTupleUsage>> {
|
||||
let mut current_file_usages = None;
|
||||
|
||||
for r in refs {
|
||||
edit_tuple_usage(ctx, builder, r, data, in_sub_pattern);
|
||||
if let Some(usages) = data.usages.as_ref() {
|
||||
// We need to collect edits first before actually applying them
|
||||
// as mapping nodes to their mutable node versions requires an
|
||||
// unmodified syntax tree.
|
||||
//
|
||||
// We also defer editing usages in the current file first since
|
||||
// tree mutation in the same file breaks when `builder.edit_file`
|
||||
// is called
|
||||
|
||||
if let Some((_, refs)) = usages.iter().find(|(file_id, _)| **file_id == ctx.file_id()) {
|
||||
current_file_usages = Some(
|
||||
refs.iter()
|
||||
.filter_map(|r| edit_tuple_usage(ctx, edit, r, data, in_sub_pattern))
|
||||
.collect_vec(),
|
||||
);
|
||||
}
|
||||
|
||||
for (file_id, refs) in usages.iter() {
|
||||
if *file_id == ctx.file_id() {
|
||||
continue;
|
||||
}
|
||||
|
||||
edit.edit_file(*file_id);
|
||||
|
||||
let tuple_edits = refs
|
||||
.iter()
|
||||
.filter_map(|r| edit_tuple_usage(ctx, edit, r, data, in_sub_pattern))
|
||||
.collect_vec();
|
||||
|
||||
tuple_edits.into_iter().for_each(|tuple_edit| tuple_edit.apply(edit))
|
||||
}
|
||||
}
|
||||
|
||||
current_file_usages
|
||||
}
|
||||
fn edit_tuple_usage(
|
||||
ctx: &AssistContext<'_>,
|
||||
|
@ -215,25 +253,14 @@ fn edit_tuple_usage(
|
|||
usage: &FileReference,
|
||||
data: &TupleData,
|
||||
in_sub_pattern: bool,
|
||||
) {
|
||||
) -> Option<EditTupleUsage> {
|
||||
match detect_tuple_index(usage, data) {
|
||||
Some(index) => edit_tuple_field_usage(ctx, builder, data, index),
|
||||
None => {
|
||||
if in_sub_pattern {
|
||||
cov_mark::hit!(destructure_tuple_call_with_subpattern);
|
||||
return;
|
||||
}
|
||||
|
||||
// no index access -> make invalid -> requires handling by user
|
||||
// -> put usage in block comment
|
||||
//
|
||||
// Note: For macro invocations this might result in still valid code:
|
||||
// When a macro accepts the tuple as argument, as well as no arguments at all,
|
||||
// uncommenting the tuple still leaves the macro call working (see `tests::in_macro_call::empty_macro`).
|
||||
// But this is an unlikely case. Usually the resulting macro call will become erroneous.
|
||||
builder.insert(usage.range.start(), "/*");
|
||||
builder.insert(usage.range.end(), "*/");
|
||||
Some(index) => Some(edit_tuple_field_usage(ctx, builder, data, index)),
|
||||
None if in_sub_pattern => {
|
||||
cov_mark::hit!(destructure_tuple_call_with_subpattern);
|
||||
return None;
|
||||
}
|
||||
None => Some(EditTupleUsage::NoIndex(usage.range)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,19 +269,47 @@ fn edit_tuple_field_usage(
|
|||
builder: &mut SourceChangeBuilder,
|
||||
data: &TupleData,
|
||||
index: TupleIndex,
|
||||
) {
|
||||
) -> EditTupleUsage {
|
||||
let field_name = &data.field_names[index.index];
|
||||
let field_name = make::expr_path(make::ext::ident_path(field_name));
|
||||
|
||||
if data.ref_type.is_some() {
|
||||
let ref_data = handle_ref_field_usage(ctx, &index.field_expr);
|
||||
builder.replace(ref_data.range, ref_data.format(field_name));
|
||||
let (replace_expr, ref_data) = handle_ref_field_usage(ctx, &index.field_expr);
|
||||
let replace_expr = builder.make_mut(replace_expr);
|
||||
EditTupleUsage::ReplaceExpr(replace_expr, ref_data.wrap_expr(field_name))
|
||||
} else {
|
||||
builder.replace(index.range, field_name);
|
||||
let field_expr = builder.make_mut(index.field_expr);
|
||||
EditTupleUsage::ReplaceExpr(field_expr.into(), field_name)
|
||||
}
|
||||
}
|
||||
enum EditTupleUsage {
|
||||
/// no index access -> make invalid -> requires handling by user
|
||||
/// -> put usage in block comment
|
||||
///
|
||||
/// Note: For macro invocations this might result in still valid code:
|
||||
/// When a macro accepts the tuple as argument, as well as no arguments at all,
|
||||
/// uncommenting the tuple still leaves the macro call working (see `tests::in_macro_call::empty_macro`).
|
||||
/// But this is an unlikely case. Usually the resulting macro call will become erroneous.
|
||||
NoIndex(TextRange),
|
||||
ReplaceExpr(ast::Expr, ast::Expr),
|
||||
}
|
||||
|
||||
impl EditTupleUsage {
|
||||
fn apply(self, edit: &mut SourceChangeBuilder) {
|
||||
match self {
|
||||
EditTupleUsage::NoIndex(range) => {
|
||||
edit.insert(range.start(), "/*");
|
||||
edit.insert(range.end(), "*/");
|
||||
}
|
||||
EditTupleUsage::ReplaceExpr(target_expr, replace_with) => {
|
||||
ted::replace(target_expr.syntax(), replace_with.clone_for_update().syntax())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TupleIndex {
|
||||
index: usize,
|
||||
range: TextRange,
|
||||
field_expr: FieldExpr,
|
||||
}
|
||||
fn detect_tuple_index(usage: &FileReference, data: &TupleData) -> Option<TupleIndex> {
|
||||
|
@ -296,7 +351,7 @@ fn detect_tuple_index(usage: &FileReference, data: &TupleData) -> Option<TupleIn
|
|||
return None;
|
||||
}
|
||||
|
||||
Some(TupleIndex { index: idx, range: field_expr.syntax().text_range(), field_expr })
|
||||
Some(TupleIndex { index: idx, field_expr })
|
||||
} else {
|
||||
// tuple index out of range
|
||||
None
|
||||
|
@ -307,32 +362,34 @@ fn detect_tuple_index(usage: &FileReference, data: &TupleData) -> Option<TupleIn
|
|||
}
|
||||
|
||||
struct RefData {
|
||||
range: TextRange,
|
||||
needs_deref: bool,
|
||||
needs_parentheses: bool,
|
||||
}
|
||||
impl RefData {
|
||||
fn format(&self, field_name: &str) -> String {
|
||||
match (self.needs_deref, self.needs_parentheses) {
|
||||
(true, true) => format!("(*{field_name})"),
|
||||
(true, false) => format!("*{field_name}"),
|
||||
(false, true) => format!("({field_name})"),
|
||||
(false, false) => field_name.to_string(),
|
||||
fn wrap_expr(&self, mut expr: ast::Expr) -> ast::Expr {
|
||||
if self.needs_deref {
|
||||
expr = make::expr_prefix(T![*], expr);
|
||||
}
|
||||
|
||||
if self.needs_parentheses {
|
||||
expr = make::expr_paren(expr);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> RefData {
|
||||
fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> (ast::Expr, RefData) {
|
||||
let s = field_expr.syntax();
|
||||
let mut ref_data =
|
||||
RefData { range: s.text_range(), needs_deref: true, needs_parentheses: true };
|
||||
let mut ref_data = RefData { needs_deref: true, needs_parentheses: true };
|
||||
let mut target_node = field_expr.clone().into();
|
||||
|
||||
let parent = match s.parent().map(ast::Expr::cast) {
|
||||
Some(Some(parent)) => parent,
|
||||
Some(None) => {
|
||||
ref_data.needs_parentheses = false;
|
||||
return ref_data;
|
||||
return (target_node, ref_data);
|
||||
}
|
||||
None => return ref_data,
|
||||
None => return (target_node, ref_data),
|
||||
};
|
||||
|
||||
match parent {
|
||||
|
@ -342,7 +399,7 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
|
|||
// there might be a ref outside: `&(t.0)` -> can be removed
|
||||
if let Some(it) = it.syntax().parent().and_then(ast::RefExpr::cast) {
|
||||
ref_data.needs_deref = false;
|
||||
ref_data.range = it.syntax().text_range();
|
||||
target_node = it.into();
|
||||
}
|
||||
}
|
||||
ast::Expr::RefExpr(it) => {
|
||||
|
@ -351,8 +408,8 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
|
|||
ref_data.needs_parentheses = false;
|
||||
// might be surrounded by parens -> can be removed too
|
||||
match it.syntax().parent().and_then(ast::ParenExpr::cast) {
|
||||
Some(parent) => ref_data.range = parent.syntax().text_range(),
|
||||
None => ref_data.range = it.syntax().text_range(),
|
||||
Some(parent) => target_node = parent.into(),
|
||||
None => target_node = it.into(),
|
||||
};
|
||||
}
|
||||
// higher precedence than deref `*`
|
||||
|
@ -414,7 +471,7 @@ fn handle_ref_field_usage(ctx: &AssistContext<'_>, field_expr: &FieldExpr) -> Re
|
|||
}
|
||||
};
|
||||
|
||||
ref_data
|
||||
(target_node, ref_data)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -100,6 +100,11 @@ pub(crate) fn check_assist_not_applicable(assist: Handler, ra_fixture: &str) {
|
|||
check(assist, ra_fixture, ExpectedResult::NotApplicable, None);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn check_assist_not_applicable_by_label(assist: Handler, ra_fixture: &str, label: &str) {
|
||||
check(assist, ra_fixture, ExpectedResult::NotApplicable, Some(label));
|
||||
}
|
||||
|
||||
/// Check assist in unresolved state. Useful to check assists for lazy computation.
|
||||
#[track_caller]
|
||||
pub(crate) fn check_assist_unresolved(assist: Handler, ra_fixture: &str) {
|
||||
|
|
|
@ -3,18 +3,17 @@
|
|||
use std::iter::{empty, successors};
|
||||
|
||||
use parser::{SyntaxKind, T};
|
||||
use rowan::SyntaxElement;
|
||||
|
||||
use crate::{
|
||||
algo::{self, neighbor},
|
||||
ast::{self, edit::IndentLevel, make, HasGenericParams},
|
||||
ted::{self, Position},
|
||||
AstNode, AstToken, Direction,
|
||||
AstNode, AstToken, Direction, SyntaxElement,
|
||||
SyntaxKind::{ATTR, COMMENT, WHITESPACE},
|
||||
SyntaxNode, SyntaxToken,
|
||||
};
|
||||
|
||||
use super::HasName;
|
||||
use super::{HasArgList, HasName};
|
||||
|
||||
pub trait GenericParamsOwnerEdit: ast::HasGenericParams {
|
||||
fn get_or_create_generic_param_list(&self) -> ast::GenericParamList;
|
||||
|
@ -362,6 +361,24 @@ impl ast::PathSegment {
|
|||
}
|
||||
}
|
||||
|
||||
impl ast::MethodCallExpr {
|
||||
pub fn get_or_create_generic_arg_list(&self) -> ast::GenericArgList {
|
||||
if self.generic_arg_list().is_none() {
|
||||
let generic_arg_list = make::turbofish_generic_arg_list(empty()).clone_for_update();
|
||||
|
||||
if let Some(arg_list) = self.arg_list() {
|
||||
ted::insert_raw(
|
||||
ted::Position::before(arg_list.syntax()),
|
||||
generic_arg_list.syntax(),
|
||||
);
|
||||
} else {
|
||||
ted::append_child(self.syntax(), generic_arg_list.syntax());
|
||||
}
|
||||
}
|
||||
self.generic_arg_list().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Removable for ast::UseTree {
|
||||
fn remove(&self) {
|
||||
for dir in [Direction::Next, Direction::Prev] {
|
||||
|
@ -559,7 +576,7 @@ impl ast::AssocItemList {
|
|||
None => (IndentLevel::single(), Position::last_child_of(self.syntax()), "\n"),
|
||||
},
|
||||
};
|
||||
let elements: Vec<SyntaxElement<_>> = vec![
|
||||
let elements: Vec<SyntaxElement> = vec![
|
||||
make::tokens::whitespace(&format!("{whitespace}{indent}")).into(),
|
||||
item.syntax().clone().into(),
|
||||
];
|
||||
|
@ -629,6 +646,50 @@ impl ast::MatchArmList {
|
|||
}
|
||||
}
|
||||
|
||||
impl ast::LetStmt {
|
||||
pub fn set_ty(&self, ty: Option<ast::Type>) {
|
||||
match ty {
|
||||
None => {
|
||||
if let Some(colon_token) = self.colon_token() {
|
||||
ted::remove(colon_token);
|
||||
}
|
||||
|
||||
if let Some(existing_ty) = self.ty() {
|
||||
if let Some(sibling) = existing_ty.syntax().prev_sibling_or_token() {
|
||||
if sibling.kind() == SyntaxKind::WHITESPACE {
|
||||
ted::remove(sibling);
|
||||
}
|
||||
}
|
||||
|
||||
ted::remove(existing_ty.syntax());
|
||||
}
|
||||
|
||||
// Remove any trailing ws
|
||||
if let Some(last) = self.syntax().last_token().filter(|it| it.kind() == WHITESPACE)
|
||||
{
|
||||
last.detach();
|
||||
}
|
||||
}
|
||||
Some(new_ty) => {
|
||||
if self.colon_token().is_none() {
|
||||
ted::insert_raw(
|
||||
Position::after(
|
||||
self.pat().expect("let stmt should have a pattern").syntax(),
|
||||
),
|
||||
make::token(T![:]),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(old_ty) = self.ty() {
|
||||
ted::replace(old_ty.syntax(), new_ty.syntax());
|
||||
} else {
|
||||
ted::insert(Position::after(self.colon_token().unwrap()), new_ty.syntax());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ast::RecordExprFieldList {
|
||||
pub fn add_field(&self, field: ast::RecordExprField) {
|
||||
let is_multiline = self.syntax().text().contains_char('\n');
|
||||
|
@ -753,7 +814,7 @@ impl ast::VariantList {
|
|||
None => (IndentLevel::single(), Position::last_child_of(self.syntax())),
|
||||
},
|
||||
};
|
||||
let elements: Vec<SyntaxElement<_>> = vec![
|
||||
let elements: Vec<SyntaxElement> = vec![
|
||||
make::tokens::whitespace(&format!("{}{indent}", "\n")).into(),
|
||||
variant.syntax().clone().into(),
|
||||
ast::make::token(T![,]).into(),
|
||||
|
@ -788,6 +849,53 @@ fn normalize_ws_between_braces(node: &SyntaxNode) -> Option<()> {
|
|||
Some(())
|
||||
}
|
||||
|
||||
impl ast::IdentPat {
|
||||
pub fn set_pat(&self, pat: Option<ast::Pat>) {
|
||||
match pat {
|
||||
None => {
|
||||
if let Some(at_token) = self.at_token() {
|
||||
// Remove `@ Pat`
|
||||
let start = at_token.clone().into();
|
||||
let end = self
|
||||
.pat()
|
||||
.map(|it| it.syntax().clone().into())
|
||||
.unwrap_or_else(|| at_token.into());
|
||||
|
||||
ted::remove_all(start..=end);
|
||||
|
||||
// Remove any trailing ws
|
||||
if let Some(last) =
|
||||
self.syntax().last_token().filter(|it| it.kind() == WHITESPACE)
|
||||
{
|
||||
last.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(pat) => {
|
||||
if let Some(old_pat) = self.pat() {
|
||||
// Replace existing pattern
|
||||
ted::replace(old_pat.syntax(), pat.syntax())
|
||||
} else if let Some(at_token) = self.at_token() {
|
||||
// Have an `@` token but not a pattern yet
|
||||
ted::insert(ted::Position::after(at_token), pat.syntax());
|
||||
} else {
|
||||
// Don't have an `@`, should have a name
|
||||
let name = self.name().unwrap();
|
||||
|
||||
ted::insert_all(
|
||||
ted::Position::after(name.syntax()),
|
||||
vec![
|
||||
make::token(T![@]).into(),
|
||||
make::tokens::single_space().into(),
|
||||
pat.syntax().clone().into(),
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasVisibilityEdit: ast::HasVisibility {
|
||||
fn set_visibility(&self, visbility: ast::Visibility) {
|
||||
match self.visibility() {
|
||||
|
@ -889,6 +997,65 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ident_pat_set_pat() {
|
||||
#[track_caller]
|
||||
fn check(before: &str, expected: &str, pat: Option<ast::Pat>) {
|
||||
let pat = pat.map(|it| it.clone_for_update());
|
||||
|
||||
let ident_pat = ast_mut_from_text::<ast::IdentPat>(&format!("fn f() {{ {before} }}"));
|
||||
ident_pat.set_pat(pat);
|
||||
|
||||
let after = ast_mut_from_text::<ast::IdentPat>(&format!("fn f() {{ {expected} }}"));
|
||||
assert_eq!(ident_pat.to_string(), after.to_string());
|
||||
}
|
||||
|
||||
// replacing
|
||||
check("let a @ _;", "let a @ ();", Some(make::tuple_pat([]).into()));
|
||||
|
||||
// note: no trailing semicolon is added for the below tests since it
|
||||
// seems to be picked up by the ident pat during error recovery?
|
||||
|
||||
// adding
|
||||
check("let a ", "let a @ ()", Some(make::tuple_pat([]).into()));
|
||||
check("let a @ ", "let a @ ()", Some(make::tuple_pat([]).into()));
|
||||
|
||||
// removing
|
||||
check("let a @ ()", "let a", None);
|
||||
check("let a @ ", "let a", None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_let_stmt_set_ty() {
|
||||
#[track_caller]
|
||||
fn check(before: &str, expected: &str, ty: Option<ast::Type>) {
|
||||
let ty = ty.map(|it| it.clone_for_update());
|
||||
|
||||
let let_stmt = ast_mut_from_text::<ast::LetStmt>(&format!("fn f() {{ {before} }}"));
|
||||
let_stmt.set_ty(ty);
|
||||
|
||||
let after = ast_mut_from_text::<ast::LetStmt>(&format!("fn f() {{ {expected} }}"));
|
||||
assert_eq!(let_stmt.to_string(), after.to_string(), "{let_stmt:#?}\n!=\n{after:#?}");
|
||||
}
|
||||
|
||||
// adding
|
||||
check("let a;", "let a: ();", Some(make::ty_tuple([])));
|
||||
// no semicolon due to it being eaten during error recovery
|
||||
check("let a:", "let a: ()", Some(make::ty_tuple([])));
|
||||
|
||||
// replacing
|
||||
check("let a: u8;", "let a: ();", Some(make::ty_tuple([])));
|
||||
check("let a: u8 = 3;", "let a: () = 3;", Some(make::ty_tuple([])));
|
||||
check("let a: = 3;", "let a: () = 3;", Some(make::ty_tuple([])));
|
||||
|
||||
// removing
|
||||
check("let a: u8;", "let a;", None);
|
||||
check("let a:;", "let a;", None);
|
||||
|
||||
check("let a: u8 = 3;", "let a = 3;", None);
|
||||
check("let a: = 3;", "let a = 3;", None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_variant_to_empty_enum() {
|
||||
let variant = make::variant(make::name("Bar"), None).clone_for_update();
|
||||
|
|
|
@ -941,6 +941,13 @@ pub fn lifetime_arg(lifetime: ast::Lifetime) -> ast::LifetimeArg {
|
|||
ast_from_text(&format!("const S: T<{lifetime}> = ();"))
|
||||
}
|
||||
|
||||
pub fn turbofish_generic_arg_list(
|
||||
args: impl IntoIterator<Item = ast::GenericArg>,
|
||||
) -> ast::GenericArgList {
|
||||
let args = args.into_iter().join(", ");
|
||||
ast_from_text(&format!("const S: T::<{args}> = ();"))
|
||||
}
|
||||
|
||||
pub(crate) fn generic_arg_list(
|
||||
args: impl IntoIterator<Item = ast::GenericArg>,
|
||||
) -> ast::GenericArgList {
|
||||
|
@ -1126,7 +1133,7 @@ pub mod tokens {
|
|||
|
||||
pub(super) static SOURCE_FILE: Lazy<Parse<SourceFile>> = Lazy::new(|| {
|
||||
SourceFile::parse(
|
||||
"const C: <()>::Item = ( true && true , true || true , 1 != 1, 2 == 2, 3 < 3, 4 <= 4, 5 > 5, 6 >= 6, !true, *p, &p , &mut p)\n;\n\n",
|
||||
"const C: <()>::Item = ( true && true , true || true , 1 != 1, 2 == 2, 3 < 3, 4 <= 4, 5 > 5, 6 >= 6, !true, *p, &p , &mut p, { let a @ [] })\n;\n\n",
|
||||
)
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue