Auto merge of #11830 - nemethf:on-type-formatting, r=nemethf

On typing handler for angle brackets(<) with snippets

I implemented my idea in #11398 in "cargo cult programming"-style without actually know what I'm doing, so feedback is welcome.  The PR is split into two commits to ease the review.  I used `@unexge's` original prototype, which forms the basis of the PR.
This commit is contained in:
bors 2022-05-22 08:59:04 +00:00
commit 90236dd77a
7 changed files with 354 additions and 21 deletions

View File

@ -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,12 @@ 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,
is_snippet: bool,
}
// Feature: On Typing Assists
//
@ -68,23 +73,30 @@ pub(crate) fn on_char_typed(
return None;
}
let edit = on_char_typed_inner(file, position.offset, char_typed)?;
Some(SourceChange::from_text_edit(position.file_id, edit))
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(
file: &Parse<SourceFile>,
offset: TextSize,
char_typed: char,
) -> Option<TextEdit> {
) -> Option<ExtendedTextEdit> {
if !stdx::always!(TRIGGER_CHARS.contains(char_typed)) {
return None;
}
match char_typed {
'.' => on_dot_typed(&file.tree(), offset),
'=' => on_eq_typed(&file.tree(), offset),
'>' => on_arrow_typed(&file.tree(), offset),
'{' => on_opening_brace_typed(file, offset),
return 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_brace_typed(file, offset)),
_ => unreachable!(),
};
fn conv(text_edit: Option<TextEdit>) -> Option<ExtendedTextEdit> {
Some(ExtendedTextEdit { edit: text_edit?, is_snippet: false })
}
}
@ -302,8 +314,49 @@ fn on_dot_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
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<ExtendedTextEdit> {
let file_text = file.syntax().text();
if !stdx::always!(file_text.char_at(offset) == Some('<')) {
return None;
}
// Find the next non-whitespace char in the line.
let mut next_offset = offset + TextSize::of('<');
while file_text.char_at(next_offset) == Some(' ') {
next_offset += TextSize::of(' ')
}
if file_text.char_at(next_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<TextEdit> {
fn on_right_angle_typed(file: &SourceFile, offset: TextSize) -> Option<TextEdit> {
let file_text = file.syntax().text();
if !stdx::always!(file_text.char_at(offset) == Some('>')) {
return None;
@ -325,6 +378,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<String> {
let (offset, mut before) = extract_offset(before);
let edit = TextEdit::insert(offset, char_typed.to_string());
@ -869,6 +928,255 @@ 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<T> Foo$0 {}
"#,
r#"
impl<T> 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 dont_add_closing_angle_bracket_if_it_is_already_there() {
type_char_noop(
'<',
r#"
fn foo() {
bar::$0>
}
"#,
);
type_char_noop(
'<',
r#"
fn foo(bar: &[u64]) {
bar.iter().collect::$0 >();
}
"#,
);
type_char_noop(
'<',
r#"
fn foo$0>() {}
"#,
);
type_char_noop(
'<',
r#"
fn foo$0>
"#,
);
type_char_noop(
'<',
r#"
struct Foo$0> {}
"#,
);
type_char_noop(
'<',
r#"
struct Foo$0>();
"#,
);
type_char_noop(
'<',
r#"
struct Foo$0>
"#,
);
type_char_noop(
'<',
r#"
enum Foo$0>
"#,
);
type_char_noop(
'<',
r#"
trait Foo$0>
"#,
);
type_char_noop(
'<',
r#"
type Foo$0> = Bar;
"#,
);
type_char_noop(
'<',
r#"
impl$0> Foo {}
"#,
);
type_char_noop(
'<',
r#"
impl<T> Foo$0> {}
"#,
);
type_char_noop(
'<',
r#"
impl Foo$0> {}
"#,
);
}
#[test]
fn regression_629() {
type_char_noop(

View File

@ -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<String> {
let mut res = vec![".".to_string(), ">".to_string(), "{".to_string()];
if config.snippet_cap() {
res.push("<".to_string());
}
res
}

View File

@ -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")),

View File

@ -276,7 +276,7 @@ pub(crate) fn handle_on_enter(
pub(crate) fn handle_on_type_formatting(
snap: GlobalStateSnapshot,
params: lsp_types::DocumentOnTypeFormattingParams,
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
) -> Result<Option<Vec<lsp_ext::SnippetTextEdit>>> {
let _p = profile::span("handle_on_type_formatting");
let mut position = from_proto::file_position(&snap, params.text_document_position)?;
let line_index = snap.file_line_index(position.file_id)?;
@ -306,9 +306,9 @@ pub(crate) fn handle_on_type_formatting(
};
// This should be a single-file edit
let (_, edit) = edit.source_file_edits.into_iter().next().unwrap();
let (_, text_edit) = edit.source_file_edits.into_iter().next().unwrap();
let change = to_proto::text_edit_vec(&line_index, edit);
let change = to_proto::snippet_text_edit_vec(&line_index, edit.is_snippet, text_edit);
Ok(Some(change))
}

View File

@ -4,8 +4,8 @@ use std::{collections::HashMap, path::PathBuf};
use lsp_types::request::Request;
use lsp_types::{
notification::Notification, CodeActionKind, PartialResultParams, Position, Range,
TextDocumentIdentifier, WorkDoneProgressParams,
notification::Notification, CodeActionKind, DocumentOnTypeFormattingParams,
PartialResultParams, Position, Range, TextDocumentIdentifier, WorkDoneProgressParams,
};
use serde::{Deserialize, Serialize};
@ -512,6 +512,19 @@ pub enum WorkspaceSymbolSearchKind {
AllSymbols,
}
/// The document on type formatting request is sent from the client to
/// the server to format parts of the document during typing. This is
/// almost same as lsp_types::request::OnTypeFormatting, but the
/// result has SnippetTextEdit in it instead of TextEdit.
#[derive(Debug)]
pub enum OnTypeFormatting {}
impl Request for OnTypeFormatting {
type Params = DocumentOnTypeFormattingParams;
type Result = Option<Vec<SnippetTextEdit>>;
const METHOD: &'static str = "textDocument/onTypeFormatting";
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CompletionResolveData {
pub position: lsp_types::TextDocumentPositionParams,

View File

@ -605,7 +605,7 @@ impl GlobalState {
.on::<lsp_ext::OpenCargoToml>(handlers::handle_open_cargo_toml)
.on::<lsp_ext::MoveItem>(handlers::handle_move_item)
.on::<lsp_ext::WorkspaceSymbol>(handlers::handle_workspace_symbol)
.on::<lsp_types::request::OnTypeFormatting>(handlers::handle_on_type_formatting)
.on::<lsp_ext::OnTypeFormatting>(handlers::handle_on_type_formatting)
.on::<lsp_types::request::DocumentSymbolRequest>(handlers::handle_document_symbol)
.on::<lsp_types::request::GotoDefinition>(handlers::handle_goto_definition)
.on::<lsp_types::request::GotoDeclaration>(handlers::handle_goto_declaration)

View File

@ -1,5 +1,5 @@
<!---
lsp_ext.rs hash: 44e8238e4fbd4128
lsp_ext.rs hash: 2a188defec26cc7c
If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue:
@ -47,7 +47,7 @@ If a language client does not know about `rust-analyzer`'s configuration options
**Experimental Client Capability:** `{ "snippetTextEdit": boolean }`
If this capability is set, `WorkspaceEdit`s returned from `codeAction` requests might contain `SnippetTextEdit`s instead of usual `TextEdit`s:
If this capability is set, `WorkspaceEdit`s returned from `codeAction` requests and `TextEdit`s returned from `textDocument/onTypeFormatting` requests might contain `SnippetTextEdit`s instead of usual `TextEdit`s:
```typescript
interface SnippetTextEdit extends TextEdit {
@ -63,7 +63,7 @@ export interface TextDocumentEdit {
}
```
When applying such code action, the editor should insert snippet, with tab stops and placeholder.
When applying such code action or text edit, the editor should insert snippet, with tab stops and placeholder.
At the moment, rust-analyzer guarantees that only a single edit will have `InsertTextFormat.Snippet`.
### Example