]> git.lizzy.rs Git - rust.git/commitdiff
Auto merge of #11830 - nemethf:on-type-formatting, r=nemethf
authorbors <bors@rust-lang.org>
Sun, 22 May 2022 08:59:04 +0000 (08:59 +0000)
committerbors <bors@rust-lang.org>
Sun, 22 May 2022 08:59:04 +0000 (08:59 +0000)
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.

crates/ide/src/typing.rs
crates/rust-analyzer/src/caps.rs
crates/rust-analyzer/src/config.rs
crates/rust-analyzer/src/handlers.rs
crates/rust-analyzer/src/lsp_ext.rs
crates/rust-analyzer/src/main_loop.rs
docs/dev/lsp-extensions.md

index a75e6be8b8abf117fcc3a9da95a548177a847efd..6af62d0ab23339b9d77f7c7d653e7249da92c8f9 100644 (file)
@@ -20,9 +20,9 @@
     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};
 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 @@ fn adds_closing_brace_for_use_tree() {
         );
     }
 
+    #[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(
index a653ec289b387ca6abadd77218f2b50a88c0c908..58b1f29df544ca321b1fa1ecaa99415a0ce934d1 100644 (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
+}
index d7ae4c72f5c56e424b6debdcae88cb12249ad221..c53f7e8c5921b64c586ac6965c17ea040e6dd483 100644 (file)
@@ -1070,6 +1070,10 @@ pub fn completion(&self) -> CompletionConfig {
         }
     }
 
+    pub fn snippet_cap(&self) -> bool {
+        self.experimental("snippetTextEdit")
+    }
+
     pub fn assist(&self) -> AssistConfig {
         AssistConfig {
             snippet_cap: SnippetCap::new(self.experimental("snippetTextEdit")),
index 261c02816d59f72bc8bf21ba63494d05984a3c3e..a16f0d904ce960d3d07873d9c1a13a312fcec69b 100644 (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))
 }
 
index c1b230bd9dff2dbd9e8eb8d0cd55888568255449..5f0e108624b2ef4991cbd065c4611d49b8c2613e 100644 (file)
@@ -4,8 +4,8 @@
 
 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,
index b5ac55e60d6bea15466dbba84ee3d019587649df..3c879687433d3574ec3a323277e0450a019d2350 100644 (file)
@@ -605,7 +605,7 @@ fn on_request(&mut self, request_received: Instant, req: Request) -> Result<()>
             .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)
index 875561608dc009a523cfacabc4c6fadeb58a7e11..983d8e1ac095dfb2ca0f4ea54cd5ed20944d7b74 100644 (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