diff --git a/crates/ide_assists/src/assist_context.rs b/crates/ide_assists/src/assist_context.rs index 1482d37f842c..8714e4978c54 100644 --- a/crates/ide_assists/src/assist_context.rs +++ b/crates/ide_assists/src/assist_context.rs @@ -13,7 +13,7 @@ use ide_db::{ RootDatabase, }; use syntax::{ - algo::{self, find_node_at_offset, SyntaxRewriter}, + algo::{self, find_node_at_offset, find_node_at_range, SyntaxRewriter}, AstNode, AstToken, SourceFile, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxNodePtr, SyntaxToken, TextRange, TextSize, TokenAtOffset, }; @@ -89,6 +89,9 @@ impl<'a> AssistContext<'a> { pub(crate) fn find_node_at_offset(&self) -> Option { find_node_at_offset(self.source_file.syntax(), self.offset()) } + pub(crate) fn find_node_at_range(&self) -> Option { + find_node_at_range(self.source_file.syntax(), self.frange.range) + } pub(crate) fn find_node_at_offset_with_descend(&self) -> Option { self.sema.find_node_at_offset_with_descend(self.source_file.syntax(), self.offset()) } diff --git a/crates/ide_assists/src/handlers/extract_type_alias.rs b/crates/ide_assists/src/handlers/extract_type_alias.rs new file mode 100644 index 000000000000..442a209b976e --- /dev/null +++ b/crates/ide_assists/src/handlers/extract_type_alias.rs @@ -0,0 +1,149 @@ +use syntax::ast::{self, AstNode}; + +use crate::{AssistContext, AssistId, AssistKind, Assists}; + +// Assist: extract_type_alias +// +// Extracts the selected type as a type alias. +// +// ``` +// struct S { +// field: $0(u8, u8, u8)$0, +// } +// ``` +// -> +// ``` +// type $0Type = (u8, u8, u8); +// +// struct S { +// field: Type, +// } +// ``` +pub(crate) fn extract_type_alias(acc: &mut Assists, ctx: &AssistContext) -> Option<()> { + if ctx.frange.range.is_empty() { + return None; + } + + let node = ctx.find_node_at_range::()?; + let insert = ctx.find_node_at_offset::()?.syntax().text_range().start(); + let target = node.syntax().text_range(); + + acc.add( + AssistId("extract_type_alias", AssistKind::RefactorExtract), + "Extract type as type alias", + target, + |builder| { + builder.edit_file(ctx.frange.file_id); + builder.replace(target, "Type"); + match ctx.config.snippet_cap { + Some(cap) => { + builder.insert_snippet(cap, insert, format!("type $0Type = {};\n\n", node)); + } + None => { + builder.insert(insert, format!("type Type = {};\n\n", node)); + } + } + }, + ) +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_assist, check_assist_not_applicable}; + + use super::*; + + #[test] + fn test_not_applicable_without_selection() { + check_assist_not_applicable( + extract_type_alias, + r" +struct S { + field: $0(u8, u8, u8), +} + ", + ); + } + + #[test] + fn test_simple_types() { + check_assist( + extract_type_alias, + r" +struct S { + field: $0u8$0, +} + ", + r#" +type $0Type = u8; + +struct S { + field: Type, +} + "#, + ); + } + + #[test] + fn test_generic_type_arg() { + check_assist( + extract_type_alias, + r" +fn generic() {} + +fn f() { + generic::<$0()$0>(); +} + ", + r#" +fn generic() {} + +type $0Type = (); + +fn f() { + generic::(); +} + "#, + ); + } + + #[test] + fn test_inner_type_arg() { + check_assist( + extract_type_alias, + r" +struct Vec {} +struct S { + v: Vec$0>>, +} + ", + r#" +struct Vec {} +type $0Type = Vec; + +struct S { + v: Vec>, +} + "#, + ); + } + + #[test] + fn test_extract_inner_type() { + check_assist( + extract_type_alias, + r" +struct S { + field: ($0u8$0,), +} + ", + r#" +type $0Type = u8; + +struct S { + field: (Type,), +} + "#, + ); + } +} diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs index 8c068a6c0efd..3d1dcef4cead 100644 --- a/crates/ide_assists/src/lib.rs +++ b/crates/ide_assists/src/lib.rs @@ -121,6 +121,7 @@ mod handlers { mod expand_glob_import; mod extract_function; mod extract_struct_from_enum_variant; + mod extract_type_alias; mod extract_variable; mod fill_match_arms; mod fix_visibility; @@ -187,6 +188,7 @@ mod handlers { early_return::convert_to_guarded_return, expand_glob_import::expand_glob_import, extract_struct_from_enum_variant::extract_struct_from_enum_variant, + extract_type_alias::extract_type_alias, fill_match_arms::fill_match_arms, fix_visibility::fix_visibility, flip_binexpr::flip_binexpr, diff --git a/crates/ide_assists/src/tests/generated.rs b/crates/ide_assists/src/tests/generated.rs index 736027ff00b6..03b7fb366231 100644 --- a/crates/ide_assists/src/tests/generated.rs +++ b/crates/ide_assists/src/tests/generated.rs @@ -328,6 +328,25 @@ enum A { One(One) } ) } +#[test] +fn doctest_extract_type_alias() { + check_doc_test( + "extract_type_alias", + r#####" +struct S { + field: $0(u8, u8, u8)$0, +} +"#####, + r#####" +type $0Type = (u8, u8, u8); + +struct S { + field: Type, +} +"#####, + ) +} + #[test] fn doctest_extract_variable() { check_doc_test( diff --git a/editors/code/src/snippets.ts b/editors/code/src/snippets.ts index dc53ebe2eeb6..9561aa345b6f 100644 --- a/editors/code/src/snippets.ts +++ b/editors/code/src/snippets.ts @@ -29,7 +29,7 @@ async function editorFromUri(uri: vscode.Uri): Promise { for (const indel of edits) { @@ -44,18 +44,18 @@ export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vs indel.range.start.character + placeholderStart : prefix.length - lastNewline - 1; const endColumn = startColumn + placeholderLength; - selection = new vscode.Selection( + selections.push(new vscode.Selection( new vscode.Position(startLine, startColumn), new vscode.Position(startLine, endColumn), - ); + )); builder.replace(indel.range, newText); } else { - lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line); builder.replace(indel.range, indel.newText); } + lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line); } }); - if (selection) editor.selection = selection; + if (selections.length > 0) editor.selections = selections; } function parseSnippet(snip: string): [string, [number, number]] | undefined {