From 76e170c3d0d0784c0e612c5849798c65a2034f29 Mon Sep 17 00:00:00 2001
From: Aleksey Kladov <aleksey.kladov@gmail.com>
Date: Mon, 25 May 2020 14:12:53 +0200
Subject: [PATCH] Less rust-analyzer specific onEnter

---
 crates/ra_ide/src/lib.rs                      |  3 +-
 crates/ra_ide/src/typing/on_enter.rs          | 11 ++--
 crates/rust-analyzer/src/caps.rs              |  1 +
 crates/rust-analyzer/src/lsp_ext.rs           |  4 +-
 .../rust-analyzer/src/main_loop/handlers.rs   | 14 +++--
 crates/rust-analyzer/src/to_proto.rs          | 12 +++++
 .../rust-analyzer/tests/heavy_tests/main.rs   | 46 +++++-----------
 docs/dev/lsp-extensions.md                    | 53 +++++++++++++++++++
 editors/code/src/commands.ts                  | 10 ++--
 editors/code/src/rust-analyzer-api.ts         |  3 +-
 editors/code/src/snippets.ts                  |  3 ++
 11 files changed, 105 insertions(+), 55 deletions(-)

diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs
index 5ac002d82f0..d983cd91002 100644
--- a/crates/ra_ide/src/lib.rs
+++ b/crates/ra_ide/src/lib.rs
@@ -309,7 +309,8 @@ impl Analysis {
 
     /// Returns an edit which should be applied when opening a new line, fixing
     /// up minor stuff like continuing the comment.
-    pub fn on_enter(&self, position: FilePosition) -> Cancelable<Option<SourceChange>> {
+    /// The edit will be a snippet (with `$0`).
+    pub fn on_enter(&self, position: FilePosition) -> Cancelable<Option<TextEdit>> {
         self.with_db(|db| typing::on_enter(&db, position))
     }
 
diff --git a/crates/ra_ide/src/typing/on_enter.rs b/crates/ra_ide/src/typing/on_enter.rs
index e7d64b4f68c..a40d8af9c43 100644
--- a/crates/ra_ide/src/typing/on_enter.rs
+++ b/crates/ra_ide/src/typing/on_enter.rs
@@ -11,9 +11,7 @@ use ra_syntax::{
 };
 use ra_text_edit::TextEdit;
 
-use crate::{SourceChange, SourceFileEdit};
-
-pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<SourceChange> {
+pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<TextEdit> {
     let parse = db.parse(position.file_id);
     let file = parse.tree();
     let comment = file
@@ -41,9 +39,7 @@ pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option<Sour
     let inserted = format!("\n{}{} $0", indent, prefix);
     let edit = TextEdit::insert(position.offset, inserted);
 
-    let mut res = SourceChange::from(SourceFileEdit { edit, file_id: position.file_id });
-    res.is_snippet = true;
-    Some(res)
+    Some(edit)
 }
 
 fn followed_by_comment(comment: &ast::Comment) -> bool {
@@ -90,9 +86,8 @@ mod tests {
         let (analysis, file_id) = single_file(&before);
         let result = analysis.on_enter(FilePosition { offset, file_id }).unwrap()?;
 
-        assert_eq!(result.source_file_edits.len(), 1);
         let mut actual = before.to_string();
-        result.source_file_edits[0].edit.apply(&mut actual);
+        result.apply(&mut actual);
         Some(actual)
     }
 
diff --git a/crates/rust-analyzer/src/caps.rs b/crates/rust-analyzer/src/caps.rs
index 780fc93174f..d55cbb15fe8 100644
--- a/crates/rust-analyzer/src/caps.rs
+++ b/crates/rust-analyzer/src/caps.rs
@@ -85,6 +85,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti
         experimental: Some(json!({
             "joinLines": true,
             "ssr": true,
+            "onEnter": true,
         })),
     }
 }
diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs
index 52e4fcbecae..1cce1baa455 100644
--- a/crates/rust-analyzer/src/lsp_ext.rs
+++ b/crates/rust-analyzer/src/lsp_ext.rs
@@ -102,8 +102,8 @@ pub enum OnEnter {}
 
 impl Request for OnEnter {
     type Params = lsp_types::TextDocumentPositionParams;
-    type Result = Option<SnippetWorkspaceEdit>;
-    const METHOD: &'static str = "rust-analyzer/onEnter";
+    type Result = Option<Vec<SnippetTextEdit>>;
+    const METHOD: &'static str = "experimental/onEnter";
 }
 
 pub enum Runnables {}
diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs
index d7310796812..a13a0e1f523 100644
--- a/crates/rust-analyzer/src/main_loop/handlers.rs
+++ b/crates/rust-analyzer/src/main_loop/handlers.rs
@@ -174,13 +174,17 @@ pub fn handle_join_lines(
 pub fn handle_on_enter(
     world: WorldSnapshot,
     params: lsp_types::TextDocumentPositionParams,
-) -> Result<Option<lsp_ext::SnippetWorkspaceEdit>> {
+) -> Result<Option<Vec<lsp_ext::SnippetTextEdit>>> {
     let _p = profile("handle_on_enter");
     let position = from_proto::file_position(&world, params)?;
-    match world.analysis().on_enter(position)? {
-        None => Ok(None),
-        Some(source_change) => to_proto::snippet_workspace_edit(&world, source_change).map(Some),
-    }
+    let edit = match world.analysis().on_enter(position)? {
+        None => return Ok(None),
+        Some(it) => it,
+    };
+    let line_index = world.analysis().file_line_index(position.file_id)?;
+    let line_endings = world.file_line_endings(position.file_id);
+    let edit = to_proto::snippet_text_edit_vec(&line_index, line_endings, true, edit);
+    Ok(Some(edit))
 }
 
 // Don't forget to add new trigger characters to `ServerCapabilities` in `caps.rs`.
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index 81a347247cb..39d58f1e01b 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -135,6 +135,18 @@ pub(crate) fn text_edit_vec(
     text_edit.into_iter().map(|indel| self::text_edit(line_index, line_endings, indel)).collect()
 }
 
+pub(crate) fn snippet_text_edit_vec(
+    line_index: &LineIndex,
+    line_endings: LineEndings,
+    is_snippet: bool,
+    text_edit: TextEdit,
+) -> Vec<lsp_ext::SnippetTextEdit> {
+    text_edit
+        .into_iter()
+        .map(|indel| self::snippet_text_edit(line_index, line_endings, is_snippet, indel))
+        .collect()
+}
+
 pub(crate) fn completion_item(
     line_index: &LineIndex,
     line_endings: LineEndings,
diff --git a/crates/rust-analyzer/tests/heavy_tests/main.rs b/crates/rust-analyzer/tests/heavy_tests/main.rs
index 738a9a8e375..b1bfc968a8d 100644
--- a/crates/rust-analyzer/tests/heavy_tests/main.rs
+++ b/crates/rust-analyzer/tests/heavy_tests/main.rs
@@ -473,23 +473,14 @@ fn main() {{}}
             text_document: server.doc_id("src/m0.rs"),
             position: Position { line: 0, character: 5 },
         },
-        json!({
-          "documentChanges": [
-            {
-              "edits": [
-                {
-                  "insertTextFormat": 2,
-                  "newText": "\n/// $0",
-                  "range": {
-                    "end": { "character": 5, "line": 0 },
-                    "start": { "character": 5, "line": 0 }
-                  }
-                }
-              ],
-              "textDocument": { "uri": "file:///[..]src/m0.rs", "version": null }
+        json!([{
+            "insertTextFormat": 2,
+            "newText": "\n/// $0",
+            "range": {
+            "end": { "character": 5, "line": 0 },
+            "start": { "character": 5, "line": 0 }
             }
-          ]
-        }),
+        }]),
     );
     let elapsed = start.elapsed();
     assert!(elapsed.as_millis() < 2000, "typing enter took {:?}", elapsed);
@@ -519,23 +510,14 @@ version = \"0.0.0\"
             text_document: server.doc_id("src/main.rs"),
             position: Position { line: 0, character: 8 },
         },
-        json!({
-          "documentChanges": [
-            {
-              "edits": [
-                {
-                  "insertTextFormat": 2,
-                  "newText": "\r\n/// $0",
-                  "range": {
-                    "end": { "line": 0, "character": 8 },
-                    "start": { "line": 0, "character": 8 }
-                  }
-                }
-              ],
-              "textDocument": { "uri": "file:///[..]src/main.rs", "version": null }
+        json!([{
+            "insertTextFormat": 2,
+            "newText": "\r\n/// $0",
+            "range": {
+            "end": { "line": 0, "character": 8 },
+            "start": { "line": 0, "character": 8 }
             }
-          ]
-        }),
+        }]),
     );
 }
 
diff --git a/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md
index 55035cfae18..e4b9fb2c25e 100644
--- a/docs/dev/lsp-extensions.md
+++ b/docs/dev/lsp-extensions.md
@@ -138,6 +138,59 @@ fn main() {
   Currently this is left to editor's discretion, but it might be useful to specify on the server via snippets.
   However, it then becomes unclear how it works with multi cursor.
 
+## On Enter
+
+**Issue:** https://github.com/microsoft/language-server-protocol/issues/1001
+
+**Server Capability:** `{ "onEnter": boolean }`
+
+This request is send from client to server to handle <kbd>Enter</kbd> keypress.
+
+**Method:** `experimental/onEnter`
+
+**Request:**: `TextDocumentPositionParams`
+
+**Response:**
+
+```typescript
+SnippetTextEdit[]
+```
+
+### Example
+
+```rust
+fn main() {
+    // Some /*cursor here*/ docs
+    let x = 92;
+}
+```
+
+`experimental/onEnter` returns the following snippet
+
+```rust
+fn main() {
+    // Some
+    // $0 docs
+    let x = 92;
+}
+```
+
+The primary goal of `onEnter` is to handle automatic indentation when opening a new line.
+This is not yet implemented.
+The secondary goal is to handle fixing up syntax, like continuing doc strings and comments, and escaping `\n` in string literals.
+
+As proper cursor positioning is raison-d'etat for `onEnter`, it uses `SnippetTextEdit`.
+
+### Unresolved Question
+
+* How to deal with synchronicity of the request?
+  One option is to require the client to block until the server returns the response.
+  Another option is to do a OT-style merging of edits from client and server.
+  A third option is to do a record-replay: client applies heuristic on enter immediatelly, then applies all user's keypresses.
+  When the server is ready with the response, the client rollbacks all the changes and applies the recorded actions on top of the correct response.
+* How to deal with multiple carets?
+* Should we extend this to arbitrary typed events and not just `onEnter`?
+
 ## Structural Search Replace (SSR)
 
 **Server Capability:** `{ "ssr": boolean }`
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 573af5aa580..e080301405d 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -3,7 +3,7 @@ import * as lc from 'vscode-languageclient';
 import * as ra from './rust-analyzer-api';
 
 import { Ctx, Cmd } from './ctx';
-import { applySnippetWorkspaceEdit } from './snippets';
+import { applySnippetWorkspaceEdit, applySnippetTextEdits } from './snippets';
 import { spawnSync } from 'child_process';
 import { RunnableQuickPick, selectRunnable, createTask } from './run';
 import { AstInspector } from './ast_inspector';
@@ -102,7 +102,7 @@ export function onEnter(ctx: Ctx): Cmd {
 
         if (!editor || !client) return false;
 
-        const change = await client.sendRequest(ra.onEnter, {
+        const lcEdits = await client.sendRequest(ra.onEnter, {
             textDocument: { uri: editor.document.uri.toString() },
             position: client.code2ProtocolConverter.asPosition(
                 editor.selection.active,
@@ -111,10 +111,10 @@ export function onEnter(ctx: Ctx): Cmd {
             // client.logFailedRequest(OnEnterRequest.type, error);
             return null;
         });
-        if (!change) return false;
+        if (!lcEdits) return false;
 
-        const workspaceEdit = client.protocol2CodeConverter.asWorkspaceEdit(change);
-        await applySnippetWorkspaceEdit(workspaceEdit);
+        const edits = client.protocol2CodeConverter.asTextEdits(lcEdits);
+        await applySnippetTextEdits(editor, edits);
         return true;
     }
 
diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts
index 900c5cd5bce..c10c0fa7894 100644
--- a/editors/code/src/rust-analyzer-api.ts
+++ b/editors/code/src/rust-analyzer-api.ts
@@ -67,8 +67,7 @@ export interface JoinLinesParams {
 }
 export const joinLines = new lc.RequestType<JoinLinesParams, lc.TextEdit[], unknown>('experimental/joinLines');
 
-
-export const onEnter = request<lc.TextDocumentPositionParams, Option<lc.WorkspaceEdit>>("onEnter");
+export const onEnter = new lc.RequestType<lc.TextDocumentPositionParams, lc.TextEdit[], unknown>('experimental/onEnter');
 
 export interface RunnablesParams {
     textDocument: lc.TextDocumentIdentifier;
diff --git a/editors/code/src/snippets.ts b/editors/code/src/snippets.ts
index 794530162dc..bcb3f2cc761 100644
--- a/editors/code/src/snippets.ts
+++ b/editors/code/src/snippets.ts
@@ -8,7 +8,10 @@ export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) {
 
     const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString());
     if (!editor) return;
+    await applySnippetTextEdits(editor, edits);
+}
 
+export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vscode.TextEdit[]) {
     let selection: vscode.Selection | undefined = undefined;
     let lineDelta = 0;
     await editor.edit((builder) => {