Make bracket typing handler work on more things

This commit is contained in:
Lukas Wirth 2024-11-04 11:03:36 +01:00
parent cddaf7485f
commit bac0ed579f
5 changed files with 200 additions and 131 deletions

View file

@ -16,11 +16,11 @@
mod on_enter;
use ide_db::{base_db::SourceDatabase, FilePosition, RootDatabase};
use span::EditionedFileId;
use span::{Edition, EditionedFileId};
use syntax::{
algo::{ancestors_at_offset, find_node_at_offset},
ast::{self, edit::IndentLevel, AstToken},
AstNode, Parse, SourceFile, SyntaxKind, TextRange, TextSize, T,
AstNode, Parse, SourceFile, SyntaxKind, TextRange, TextSize,
};
use ide_db::text_edit::TextEdit;
@ -47,6 +47,7 @@ struct ExtendedTextEdit {
// - typing `.` in a chain method call auto-indents
// - typing `{` or `(` in front of an expression inserts a closing `}` or `)` after the expression
// - typing `{` in a use item adds a closing `}` in the right place
// - typing `>` to complete a return type `->` will insert a whitespace after it
//
// VS Code::
//
@ -66,55 +67,65 @@ pub(crate) fn on_char_typed(
if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) {
return None;
}
let file = &db.parse(EditionedFileId::current_edition(position.file_id));
if !stdx::always!(file.tree().syntax().text().char_at(position.offset) == Some(char_typed)) {
// FIXME: We need to figure out the edition of the file here, but that means hitting the
// database for more than just parsing the file which is bad.
// FIXME: We are hitting the database here, if we are unlucky this call might block momentarily
// causing the editor to feel sluggish!
let edition = Edition::CURRENT_FIXME;
let file = &db.parse(EditionedFileId::new(position.file_id, edition));
let char_matches_position =
file.tree().syntax().text().char_at(position.offset) == Some(char_typed);
if !stdx::always!(char_matches_position) {
return None;
}
let edit = on_char_typed_inner(file, position.offset, char_typed)?;
let edit = on_char_typed_(file, position.offset, char_typed, edition)?;
let mut sc = SourceChange::from_text_edit(position.file_id, edit.edit);
sc.is_snippet = edit.is_snippet;
Some(sc)
}
fn on_char_typed_inner(
fn on_char_typed_(
file: &Parse<SourceFile>,
offset: TextSize,
char_typed: char,
edition: Edition,
) -> Option<ExtendedTextEdit> {
if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) {
return None;
}
let conv = |text_edit: Option<TextEdit>| {
Some(ExtendedTextEdit { edit: text_edit?, is_snippet: false })
};
match char_typed {
'.' => conv(on_dot_typed(&file.tree(), offset)),
'=' => conv(on_eq_typed(&file.tree(), offset)),
'<' => on_left_angle_typed(&file.tree(), offset),
'>' => conv(on_right_angle_typed(&file.tree(), offset)),
'{' => conv(on_opening_bracket_typed(file, offset, '{')),
'(' => conv(on_opening_bracket_typed(file, offset, '(')),
'.' => on_dot_typed(&file.tree(), offset),
'=' => on_eq_typed(&file.tree(), offset),
'>' => on_right_angle_typed(&file.tree(), offset),
'{' | '(' | '<' => on_opening_delimiter_typed(file, offset, char_typed, edition),
_ => None,
}
.map(conv)
}
/// Inserts a closing bracket when the user types an opening bracket, wrapping an existing expression in a
fn conv(edit: TextEdit) -> ExtendedTextEdit {
ExtendedTextEdit { edit, is_snippet: false }
}
/// Inserts a closing delimiter when the user types an opening bracket, wrapping an existing expression in a
/// block, or a part of a `use` item (for `{`).
fn on_opening_bracket_typed(
fn on_opening_delimiter_typed(
file: &Parse<SourceFile>,
offset: TextSize,
opening_bracket: char,
edition: Edition,
) -> Option<TextEdit> {
let (closing_bracket, expected_ast_bracket) = match opening_bracket {
'{' => ('}', SyntaxKind::L_CURLY),
'(' => (')', SyntaxKind::L_PAREN),
type FilterFn = fn(SyntaxKind) -> bool;
let (closing_bracket, expected_ast_bracket, allowed_kinds) = match opening_bracket {
'{' => ('}', SyntaxKind::L_CURLY, &[ast::Expr::can_cast as FilterFn] as &[FilterFn]),
'(' => (
')',
SyntaxKind::L_PAREN,
&[ast::Expr::can_cast, ast::Pat::can_cast, ast::Type::can_cast] as &[FilterFn],
),
'<' => ('>', SyntaxKind::L_ANGLE, &[ast::Type::can_cast as FilterFn] as &[FilterFn]),
_ => return None,
};
if !stdx::always!(file.tree().syntax().text().char_at(offset) == Some(opening_bracket)) {
return None;
}
let brace_token = file.tree().syntax().token_at_offset(offset).right_biased()?;
if brace_token.kind() != expected_ast_bracket {
return None;
@ -125,58 +136,53 @@ fn on_opening_bracket_typed(
if !stdx::always!(range.len() == TextSize::of(opening_bracket)) {
return None;
}
// FIXME: Edition
let file = file.reparse(range, "", span::Edition::CURRENT_FIXME);
let reparsed = file.reparse(range, "", edition).tree();
if let Some(edit) = bracket_expr(&file.tree(), offset, opening_bracket, closing_bracket) {
if let Some(edit) =
on_delimited_node_typed(&reparsed, offset, opening_bracket, closing_bracket, allowed_kinds)
{
return Some(edit);
}
if closing_bracket == '}' {
if let Some(edit) = brace_use_path(&file.tree(), offset) {
return Some(edit);
}
match opening_bracket {
'{' => on_left_brace_typed(&reparsed, offset),
'<' => on_left_angle_typed(&file.tree(), &reparsed, offset),
_ => None,
}
}
fn on_left_brace_typed(reparsed: &SourceFile, offset: TextSize) -> Option<TextEdit> {
let segment: ast::PathSegment = find_node_at_offset(reparsed.syntax(), offset)?;
if segment.syntax().text_range().start() != offset {
return None;
}
return None;
let tree: ast::UseTree = find_node_at_offset(reparsed.syntax(), offset)?;
fn brace_use_path(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
let segment: ast::PathSegment = find_node_at_offset(file.syntax(), offset)?;
if segment.syntax().text_range().start() != offset {
return None;
}
Some(TextEdit::insert(tree.syntax().text_range().end() + TextSize::of("{"), "}".to_owned()))
}
let tree: ast::UseTree = find_node_at_offset(file.syntax(), offset)?;
fn on_delimited_node_typed(
reparsed: &SourceFile,
offset: TextSize,
opening_bracket: char,
closing_bracket: char,
kinds: &[fn(SyntaxKind) -> bool],
) -> Option<TextEdit> {
let t = reparsed.syntax().token_at_offset(offset).right_biased()?;
let (filter, node) = t
.parent_ancestors()
.take_while(|n| n.text_range().start() == offset)
.find_map(|n| kinds.iter().find(|&kind_filter| kind_filter(n.kind())).zip(Some(n)))?;
let mut node = node
.ancestors()
.take_while(|n| n.text_range().start() == offset && filter(n.kind()))
.last()?;
Some(TextEdit::insert(tree.syntax().text_range().end() + TextSize::of("{"), "}".to_owned()))
}
fn bracket_expr(
file: &SourceFile,
offset: TextSize,
opening_bracket: char,
closing_bracket: char,
) -> Option<TextEdit> {
let mut expr: ast::Expr = find_node_at_offset(file.syntax(), offset)?;
if expr.syntax().text_range().start() != offset {
return None;
}
// Enclose the outermost expression starting at `offset`
while let Some(parent) = expr.syntax().parent() {
if parent.text_range().start() != expr.syntax().text_range().start() {
break;
}
match ast::Expr::cast(parent) {
Some(parent) => expr = parent,
None => break,
}
}
if let Some(parent) = expr.syntax().parent().and_then(ast::Expr::cast) {
let mut node = expr.syntax().clone();
let all_prev_sib_attr = loop {
if let Some(parent) = node.parent().filter(|it| filter(it.kind())) {
let all_prev_sib_attr = {
let mut node = node.clone();
loop {
match node.prev_sibling() {
Some(sib) if sib.kind().is_trivia() || sib.kind() == SyntaxKind::ATTR => {
node = sib
@ -184,21 +190,20 @@ fn on_opening_bracket_typed(
Some(_) => break false,
None => break true,
};
};
if all_prev_sib_attr {
expr = parent;
}
};
if all_prev_sib_attr {
node = parent;
}
// Insert the closing bracket right after the expression.
Some(TextEdit::insert(
expr.syntax().text_range().end() + TextSize::of(opening_bracket),
closing_bracket.to_string(),
))
}
}
// Insert the closing bracket right after the node.
Some(TextEdit::insert(
node.text_range().end() + TextSize::of(opening_bracket),
closing_bracket.to_string(),
))
}
/// Returns an edit which should be applied after `=` was typed. Primarily,
/// this works when adding `let =`.
// FIXME: use a snippet completion instead of this hack here.
@ -342,14 +347,15 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
}
/// Add closing `>` for generic arguments/parameters.
fn on_left_angle_typed(file: &SourceFile, offset: TextSize) -> Option<ExtendedTextEdit> {
let file_text = file.syntax().text();
if !stdx::always!(file_text.char_at(offset) == Some('<')) {
return None;
}
fn on_left_angle_typed(
file: &SourceFile,
reparsed: &SourceFile,
offset: TextSize,
) -> Option<TextEdit> {
let file_text = reparsed.syntax().text();
// Find the next non-whitespace char in the line.
let mut next_offset = offset + TextSize::of('<');
// Find the next non-whitespace char in the line, check if its a `>`
let mut next_offset = offset;
while file_text.char_at(next_offset) == Some(' ') {
next_offset += TextSize::of(' ')
}
@ -357,23 +363,14 @@ fn on_left_angle_typed(file: &SourceFile, offset: TextSize) -> Option<ExtendedTe
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_owned()),
is_snippet: true,
});
}
}
if ancestors_at_offset(file.syntax(), offset).any(|n| {
ast::GenericParamList::can_cast(n.kind()) || ast::GenericArgList::can_cast(n.kind())
}) {
Some(ExtendedTextEdit {
edit: TextEdit::replace(range, "<$0>".to_owned()),
is_snippet: true,
if ancestors_at_offset(file.syntax(), offset)
.take_while(|n| !ast::Item::can_cast(n.kind()))
.any(|n| {
ast::GenericParamList::can_cast(n.kind()) || ast::GenericArgList::can_cast(n.kind())
})
{
// Insert the closing bracket right after
Some(TextEdit::insert(offset + TextSize::of('<'), '>'.to_string()))
} else {
None
}
@ -411,7 +408,7 @@ mod tests {
let edit = TextEdit::insert(offset, char_typed.to_string());
edit.apply(&mut before);
let parse = SourceFile::parse(&before, span::Edition::CURRENT_FIXME);
on_char_typed_inner(&parse, offset, char_typed).map(|it| {
on_char_typed_(&parse, offset, char_typed, span::Edition::CURRENT_FIXME).map(|it| {
it.apply(&mut before);
before.to_string()
})
@ -426,7 +423,7 @@ mod tests {
fn type_char_noop(char_typed: char, ra_fixture_before: &str) {
let file_change = do_type_char(char_typed, ra_fixture_before);
assert!(file_change.is_none())
assert_eq!(file_change, None)
}
#[test]
@ -1066,6 +1063,81 @@ fn f() {
);
}
#[test]
fn adds_closing_parenthesis_for_pat() {
type_char(
'(',
r#"
fn f() { match () { $0() => () } }
"#,
r#"
fn f() { match () { (()) => () } }
"#,
);
type_char(
'(',
r#"
fn f($0n: ()) {}
"#,
r#"
fn f((n): ()) {}
"#,
);
}
#[test]
fn adds_closing_parenthesis_for_ty() {
type_char(
'(',
r#"
fn f(n: $0()) {}
"#,
r#"
fn f(n: (())) {}
"#,
);
type_char(
'(',
r#"
fn f(n: $0a::b::<d>::c) {}
"#,
r#"
fn f(n: (a::b::<d>::c)) {}
"#,
);
}
#[test]
fn adds_closing_angles_for_ty() {
type_char(
'<',
r#"
fn f(n: $0()) {}
"#,
r#"
fn f(n: <()>) {}
"#,
);
type_char(
'<',
r#"
fn f(n: $0a::b::<d>::c) {}
"#,
r#"
fn f(n: <a::b::<d>::c>) {}
"#,
);
type_char(
'<',
r#"
fn f(n: a$0b::<d>::c) {}
"#,
r#"
fn f(n: a<>b::<d>::c) {}
"#,
);
}
#[test]
fn parenthesis_noop_in_string_literal() {
// Regression test for #9351
@ -1154,6 +1226,12 @@ use $0Thing as _;
type_char_noop(
'(',
r#"
use some::pa$0th::to::Item;
"#,
);
type_char_noop(
'<',
r#"
use some::pa$0th::to::Item;
"#,
);
@ -1170,7 +1248,7 @@ fn foo() {
"#,
r#"
fn foo() {
bar::<$0>
bar::<>
}
"#,
);
@ -1184,7 +1262,7 @@ fn foo(bar: &[u64]) {
"#,
r#"
fn foo(bar: &[u64]) {
bar.iter().collect::<$0>();
bar.iter().collect::<>();
}
"#,
);
@ -1198,7 +1276,7 @@ fn foo(bar: &[u64]) {
fn foo$0() {}
"#,
r#"
fn foo<$0>() {}
fn foo<>() {}
"#,
);
type_char(
@ -1207,7 +1285,7 @@ fn foo<$0>() {}
fn foo$0
"#,
r#"
fn foo<$0>
fn foo<>
"#,
);
type_char(
@ -1216,7 +1294,7 @@ fn foo<$0>
struct Foo$0 {}
"#,
r#"
struct Foo<$0> {}
struct Foo<> {}
"#,
);
type_char(
@ -1225,7 +1303,7 @@ struct Foo<$0> {}
struct Foo$0();
"#,
r#"
struct Foo<$0>();
struct Foo<>();
"#,
);
type_char(
@ -1234,7 +1312,7 @@ struct Foo<$0>();
struct Foo$0
"#,
r#"
struct Foo<$0>
struct Foo<>
"#,
);
type_char(
@ -1243,7 +1321,7 @@ struct Foo<$0>
enum Foo$0
"#,
r#"
enum Foo<$0>
enum Foo<>
"#,
);
type_char(
@ -1252,7 +1330,7 @@ enum Foo<$0>
trait Foo$0
"#,
r#"
trait Foo<$0>
trait Foo<>
"#,
);
type_char(
@ -1261,16 +1339,7 @@ trait Foo<$0>
type Foo$0 = Bar;
"#,
r#"
type Foo<$0> = Bar;
"#,
);
type_char(
'<',
r#"
impl$0 Foo {}
"#,
r#"
impl<$0> Foo {}
type Foo<> = Bar;
"#,
);
type_char(
@ -1279,7 +1348,7 @@ impl<$0> Foo {}
impl<T> Foo$0 {}
"#,
r#"
impl<T> Foo<$0> {}
impl<T> Foo<> {}
"#,
);
type_char(
@ -1288,7 +1357,7 @@ impl<T> Foo<$0> {}
impl Foo$0 {}
"#,
r#"
impl Foo<$0> {}
impl Foo<> {}
"#,
);
}

View file

@ -309,7 +309,7 @@ config_data! {
signatureInfo_documentation_enable: bool = true,
/// Specify the characters to exclude from triggering typing assists. The default trigger characters are `.`, `=`, `<`, `>`, `{`, and `(`. Setting this to a string will disable typing assists for the specified characters.
typing_excludeChars: Option<String> = Some("<".to_owned()),
typing_excludeChars: Option<String> = None,
/// Enables automatic discovery of projects using [`DiscoverWorkspaceConfig::command`].

View file

@ -992,7 +992,7 @@ Show full signature of the callable. Only shows parameters if disabled.
--
Show documentation.
--
[[rust-analyzer.typing.excludeChars]]rust-analyzer.typing.excludeChars (default: `"<"`)::
[[rust-analyzer.typing.excludeChars]]rust-analyzer.typing.excludeChars (default: `null`)::
+
--
Specify the characters to exclude from triggering typing assists. The default trigger characters are `.`, `=`, `<`, `>`, `{`, and `(`. Setting this to a string will disable typing assists for the specified characters.

View file

@ -2607,7 +2607,7 @@
"properties": {
"rust-analyzer.typing.excludeChars": {
"markdownDescription": "Specify the characters to exclude from triggering typing assists. The default trigger characters are `.`, `=`, `<`, `>`, `{`, and `(`. Setting this to a string will disable typing assists for the specified characters.",
"default": "<",
"default": null,
"type": [
"null",
"string"

View file

@ -324,7 +324,7 @@ class ExperimentalFeatures implements lc.StaticFeature {
}
fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
capabilities.experimental = {
snippetTextEdit: true,
snippetTextEdit: false,
codeActionGroup: true,
hoverActions: true,
serverStatusNotification: true,