Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 3bb02f2

Browse files
nemethfunexge
andcommitted
feat: Add on-typing handler for left angle
Only advertise this feature in the server capabilities when the client supports SnippetTextEdit. Close rust-lang#11398. Co-authored-by: unexge <[email protected]>
1 parent 636d488 commit 3bb02f2

File tree

3 files changed

+220
-6
lines changed

3 files changed

+220
-6
lines changed

crates/ide/src/typing.rs

Lines changed: 207 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ use ide_db::{
2020
RootDatabase,
2121
};
2222
use syntax::{
23-
algo::find_node_at_offset,
23+
algo::{ancestors_at_offset, find_node_at_offset},
2424
ast::{self, edit::IndentLevel, AstToken},
25-
AstNode, Parse, SourceFile, SyntaxKind, TextRange, TextSize,
25+
AstNode, Parse, SourceFile, SyntaxKind, TextRange, TextSize, T,
2626
};
2727

2828
use text_edit::{Indel, TextEdit};
@@ -32,7 +32,7 @@ use crate::SourceChange;
3232
pub(crate) use on_enter::on_enter;
3333

3434
// Don't forget to add new trigger characters to `server_capabilities` in `caps.rs`.
35-
pub(crate) const TRIGGER_CHARS: &str = ".=>{";
35+
pub(crate) const TRIGGER_CHARS: &str = ".=<>{";
3636

3737
struct ExtendedTextEdit {
3838
edit: TextEdit,
@@ -92,7 +92,8 @@ fn on_char_typed_inner(
9292
match char_typed {
9393
'.' => conv(on_dot_typed(&file.tree(), offset)),
9494
'=' => conv(on_eq_typed(&file.tree(), offset)),
95-
'>' => conv(on_arrow_typed(&file.tree(), offset)),
95+
'<' => on_left_angle_typed(&file.tree(), offset),
96+
'>' => conv(on_right_angle_typed(&file.tree(), offset)),
9697
'{' => conv(on_opening_brace_typed(file, offset)),
9798
_ => unreachable!(),
9899
}
@@ -312,8 +313,40 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
312313
Some(TextEdit::replace(TextRange::new(offset - current_indent_len, offset), target_indent))
313314
}
314315

316+
/// Add closing `>` for generic arguments/parameters.
317+
fn on_left_angle_typed(file: &SourceFile, offset: TextSize) -> Option<ExtendedTextEdit> {
318+
let file_text = file.syntax().text();
319+
if !stdx::always!(file_text.char_at(offset) == Some('<')) {
320+
return None;
321+
}
322+
let range = TextRange::at(offset, TextSize::of('<'));
323+
324+
if let Some(t) = file.syntax().token_at_offset(offset).left_biased() {
325+
if T![impl] == t.kind() {
326+
return Some(ExtendedTextEdit {
327+
edit: TextEdit::replace(range, "<$0>".to_string()),
328+
is_snippet: true,
329+
});
330+
}
331+
}
332+
333+
if ancestors_at_offset(file.syntax(), offset)
334+
.find(|n| {
335+
ast::GenericParamList::can_cast(n.kind()) || ast::GenericArgList::can_cast(n.kind())
336+
})
337+
.is_some()
338+
{
339+
return Some(ExtendedTextEdit {
340+
edit: TextEdit::replace(range, "<$0>".to_string()),
341+
is_snippet: true,
342+
});
343+
}
344+
345+
None
346+
}
347+
315348
/// Adds a space after an arrow when `fn foo() { ... }` is turned into `fn foo() -> { ... }`
316-
fn on_arrow_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
349+
fn on_right_angle_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
317350
let file_text = file.syntax().text();
318351
if !stdx::always!(file_text.char_at(offset) == Some('>')) {
319352
return None;
@@ -335,6 +368,12 @@ mod tests {
335368

336369
use super::*;
337370

371+
impl ExtendedTextEdit {
372+
fn apply(&self, text: &mut String) {
373+
self.edit.apply(text);
374+
}
375+
}
376+
338377
fn do_type_char(char_typed: char, before: &str) -> Option<String> {
339378
let (offset, mut before) = extract_offset(before);
340379
let edit = TextEdit::insert(offset, char_typed.to_string());
@@ -879,6 +918,169 @@ use some::pa$0th::to::Item;
879918
);
880919
}
881920

921+
#[test]
922+
fn adds_closing_angle_bracket_for_generic_args() {
923+
type_char(
924+
'<',
925+
r#"
926+
fn foo() {
927+
bar::$0
928+
}
929+
"#,
930+
r#"
931+
fn foo() {
932+
bar::<$0>
933+
}
934+
"#,
935+
);
936+
937+
type_char(
938+
'<',
939+
r#"
940+
fn foo(bar: &[u64]) {
941+
bar.iter().collect::$0();
942+
}
943+
"#,
944+
r#"
945+
fn foo(bar: &[u64]) {
946+
bar.iter().collect::<$0>();
947+
}
948+
"#,
949+
);
950+
}
951+
952+
#[test]
953+
fn adds_closing_angle_bracket_for_generic_params() {
954+
type_char(
955+
'<',
956+
r#"
957+
fn foo$0() {}
958+
"#,
959+
r#"
960+
fn foo<$0>() {}
961+
"#,
962+
);
963+
type_char(
964+
'<',
965+
r#"
966+
fn foo$0
967+
"#,
968+
r#"
969+
fn foo<$0>
970+
"#,
971+
);
972+
type_char(
973+
'<',
974+
r#"
975+
struct Foo$0 {}
976+
"#,
977+
r#"
978+
struct Foo<$0> {}
979+
"#,
980+
);
981+
type_char(
982+
'<',
983+
r#"
984+
struct Foo$0();
985+
"#,
986+
r#"
987+
struct Foo<$0>();
988+
"#,
989+
);
990+
type_char(
991+
'<',
992+
r#"
993+
struct Foo$0
994+
"#,
995+
r#"
996+
struct Foo<$0>
997+
"#,
998+
);
999+
type_char(
1000+
'<',
1001+
r#"
1002+
enum Foo$0
1003+
"#,
1004+
r#"
1005+
enum Foo<$0>
1006+
"#,
1007+
);
1008+
type_char(
1009+
'<',
1010+
r#"
1011+
trait Foo$0
1012+
"#,
1013+
r#"
1014+
trait Foo<$0>
1015+
"#,
1016+
);
1017+
type_char(
1018+
'<',
1019+
r#"
1020+
type Foo$0 = Bar;
1021+
"#,
1022+
r#"
1023+
type Foo<$0> = Bar;
1024+
"#,
1025+
);
1026+
type_char(
1027+
'<',
1028+
r#"
1029+
impl$0 Foo {}
1030+
"#,
1031+
r#"
1032+
impl<$0> Foo {}
1033+
"#,
1034+
);
1035+
type_char(
1036+
'<',
1037+
r#"
1038+
impl<T> Foo$0 {}
1039+
"#,
1040+
r#"
1041+
impl<T> Foo<$0> {}
1042+
"#,
1043+
);
1044+
type_char(
1045+
'<',
1046+
r#"
1047+
impl Foo$0 {}
1048+
"#,
1049+
r#"
1050+
impl Foo<$0> {}
1051+
"#,
1052+
);
1053+
}
1054+
1055+
#[test]
1056+
fn dont_add_closing_angle_bracket_for_comparison() {
1057+
type_char_noop(
1058+
'<',
1059+
r#"
1060+
fn main() {
1061+
42$0
1062+
}
1063+
"#,
1064+
);
1065+
type_char_noop(
1066+
'<',
1067+
r#"
1068+
fn main() {
1069+
42 $0
1070+
}
1071+
"#,
1072+
);
1073+
type_char_noop(
1074+
'<',
1075+
r#"
1076+
fn main() {
1077+
let foo = 42;
1078+
foo $0
1079+
}
1080+
"#,
1081+
);
1082+
}
1083+
8821084
#[test]
8831085
fn regression_629() {
8841086
type_char_noop(

crates/rust-analyzer/src/caps.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub fn server_capabilities(config: &Config) -> ServerCapabilities {
5656
},
5757
document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
5858
first_trigger_character: "=".to_string(),
59-
more_trigger_character: Some(vec![".".to_string(), ">".to_string(), "{".to_string()]),
59+
more_trigger_character: Some(more_trigger_character(&config)),
6060
}),
6161
selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
6262
folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
@@ -189,3 +189,11 @@ fn code_action_capabilities(client_caps: &ClientCapabilities) -> CodeActionProvi
189189
})
190190
})
191191
}
192+
193+
fn more_trigger_character(config: &Config) -> Vec<String> {
194+
let mut res = vec![".".to_string(), ">".to_string(), "{".to_string()];
195+
if config.snippet_cap() {
196+
res.push("<".to_string());
197+
}
198+
res
199+
}

crates/rust-analyzer/src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,10 @@ impl Config {
10701070
}
10711071
}
10721072

1073+
pub fn snippet_cap(&self) -> bool {
1074+
self.experimental("snippetTextEdit")
1075+
}
1076+
10731077
pub fn assist(&self) -> AssistConfig {
10741078
AssistConfig {
10751079
snippet_cap: SnippetCap::new(self.experimental("snippetTextEdit")),

0 commit comments

Comments
 (0)