From 3bb02f2329623f1bb83512135746ce77ecb72b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felici=C3=A1n=20N=C3=A9meth?= Date: Sun, 27 Mar 2022 10:49:00 +0200 Subject: [PATCH] feat: Add on-typing handler for left angle Only advertise this feature in the server capabilities when the client supports SnippetTextEdit. Close #11398. Co-authored-by: unexge --- crates/ide/src/typing.rs | 212 ++++++++++++++++++++++++++++- crates/rust-analyzer/src/caps.rs | 10 +- crates/rust-analyzer/src/config.rs | 4 + 3 files changed, 220 insertions(+), 6 deletions(-) diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs index d423d8a0546..be1a6da7ea8 100644 --- a/crates/ide/src/typing.rs +++ b/crates/ide/src/typing.rs @@ -20,9 +20,9 @@ use ide_db::{ RootDatabase, }; use syntax::{ - algo::find_node_at_offset, + algo::{ancestors_at_offset, find_node_at_offset}, ast::{self, edit::IndentLevel, AstToken}, - AstNode, Parse, SourceFile, SyntaxKind, TextRange, TextSize, + AstNode, Parse, SourceFile, SyntaxKind, TextRange, TextSize, T, }; use text_edit::{Indel, TextEdit}; @@ -32,7 +32,7 @@ use crate::SourceChange; pub(crate) use on_enter::on_enter; // Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`. -pub(crate) const TRIGGER_CHARS: &str = ".=>{"; +pub(crate) const TRIGGER_CHARS: &str = ".=<>{"; struct ExtendedTextEdit { edit: TextEdit, @@ -92,7 +92,8 @@ fn on_char_typed_inner( match char_typed { '.' => conv(on_dot_typed(&file.tree(), offset)), '=' => conv(on_eq_typed(&file.tree(), offset)), - '>' => conv(on_arrow_typed(&file.tree(), offset)), + '<' => on_left_angle_typed(&file.tree(), offset), + '>' => conv(on_right_angle_typed(&file.tree(), offset)), '{' => conv(on_opening_brace_typed(file, offset)), _ => unreachable!(), } @@ -312,8 +313,40 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option { Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent)) } +/// Add closing `>` for generic arguments/parameters. +fn on_left_angle_typed(file: &SourceFile, offset: TextSize) -> Option { + let file_text = file.syntax().text(); + if !stdx::always!(file_text.char_at(offset) == Some('<')) { + return None; + } + let range = TextRange::at(offset, TextSize::of('<')); + + if let Some(t) = file.syntax().token_at_offset(offset).left_biased() { + if T![impl] == t.kind() { + return Some(ExtendedTextEdit { + edit: TextEdit::replace(range, "<$0>".to_string()), + is_snippet: true, + }); + } + } + + if ancestors_at_offset(file.syntax(), offset) + .find(|n| { + ast::GenericParamList::can_cast(n.kind()) || ast::GenericArgList::can_cast(n.kind()) + }) + .is_some() + { + return Some(ExtendedTextEdit { + edit: TextEdit::replace(range, "<$0>".to_string()), + is_snippet: true, + }); + } + + None +} + /// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }` -fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option { +fn on_right_angle_typed(file: &SourceFile, offset: TextSize) -> Option { let file_text = file.syntax().text(); if !stdx::always!(file_text.char_at(offset) == Some('>')) { return None; @@ -335,6 +368,12 @@ mod tests { use super::*; + impl ExtendedTextEdit { + fn apply(&self, text: &mut String) { + self.edit.apply(text); + } + } + fn do_type_char(char_typed: char, before: &str) -> Option { let (offset, mut before) = extract_offset(before); let edit = TextEdit::insert(offset, char_typed.to_string()); @@ -879,6 +918,169 @@ use some::pa$0th::to::Item; ); } + #[test] + fn adds_closing_angle_bracket_for_generic_args() { + type_char( + '<', + r#" +fn foo() { + bar::$0 +} + "#, + r#" +fn foo() { + bar::<$0> +} + "#, + ); + + type_char( + '<', + r#" +fn foo(bar: &[u64]) { + bar.iter().collect::$0(); +} + "#, + r#" +fn foo(bar: &[u64]) { + bar.iter().collect::<$0>(); +} + "#, + ); + } + + #[test] + fn adds_closing_angle_bracket_for_generic_params() { + type_char( + '<', + r#" +fn foo$0() {} + "#, + r#" +fn foo<$0>() {} + "#, + ); + type_char( + '<', + r#" +fn foo$0 + "#, + r#" +fn foo<$0> + "#, + ); + type_char( + '<', + r#" +struct Foo$0 {} + "#, + r#" +struct Foo<$0> {} + "#, + ); + type_char( + '<', + r#" +struct Foo$0(); + "#, + r#" +struct Foo<$0>(); + "#, + ); + type_char( + '<', + r#" +struct Foo$0 + "#, + r#" +struct Foo<$0> + "#, + ); + type_char( + '<', + r#" +enum Foo$0 + "#, + r#" +enum Foo<$0> + "#, + ); + type_char( + '<', + r#" +trait Foo$0 + "#, + r#" +trait Foo<$0> + "#, + ); + type_char( + '<', + r#" +type Foo$0 = Bar; + "#, + r#" +type Foo<$0> = Bar; + "#, + ); + type_char( + '<', + r#" +impl$0 Foo {} + "#, + r#" +impl<$0> Foo {} + "#, + ); + type_char( + '<', + r#" +impl Foo$0 {} + "#, + r#" +impl Foo<$0> {} + "#, + ); + type_char( + '<', + r#" +impl Foo$0 {} + "#, + r#" +impl Foo<$0> {} + "#, + ); + } + + #[test] + fn dont_add_closing_angle_bracket_for_comparison() { + type_char_noop( + '<', + r#" +fn main() { + 42$0 +} + "#, + ); + type_char_noop( + '<', + r#" +fn main() { + 42 $0 +} + "#, + ); + type_char_noop( + '<', + r#" +fn main() { + let foo = 42; + foo $0 +} + "#, + ); + } + #[test] fn regression_629() { type_char_noop( diff --git a/crates/rust-analyzer/src/caps.rs b/crates/rust-analyzer/src/caps.rs index a653ec289b3..58b1f29df54 100644 --- a/crates/rust-analyzer/src/caps.rs +++ b/crates/rust-analyzer/src/caps.rs @@ -56,7 +56,7 @@ pub fn server_capabilities(config: &Config) -> ServerCapabilities { }, document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions { first_trigger_character: "=".to_string(), - more_trigger_character: Some(vec![".".to_string(), ">".to_string(), "{".to_string()]), + more_trigger_character: Some(more_trigger_character(&config)), }), selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), @@ -189,3 +189,11 @@ fn code_action_capabilities(client_caps: &ClientCapabilities) -> CodeActionProvi }) }) } + +fn more_trigger_character(config: &Config) -> Vec { + let mut res = vec![".".to_string(), ">".to_string(), "{".to_string()]; + if config.snippet_cap() { + res.push("<".to_string()); + } + res +} diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index d7ae4c72f5c..c53f7e8c592 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -1070,6 +1070,10 @@ impl Config { } } + pub fn snippet_cap(&self) -> bool { + self.experimental("snippetTextEdit") + } + pub fn assist(&self) -> AssistConfig { AssistConfig { snippet_cap: SnippetCap::new(self.experimental("snippetTextEdit")),