From 76e170c3d0d0784c0e612c5849798c65a2034f29 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov 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 5ac002d82f..d983cd9100 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> { + /// The edit will be a snippet (with `$0`). + pub fn on_enter(&self, position: FilePosition) -> Cancelable> { 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 e7d64b4f68..a40d8af9c4 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 { +pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option { 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 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 780fc93174..d55cbb15fe 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 52e4fcbeca..1cce1baa45 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; - const METHOD: &'static str = "rust-analyzer/onEnter"; + type Result = Option>; + 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 d731079681..a13a0e1f52 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> { +) -> Result>> { 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 81a347247c..39d58f1e01 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 { + 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 738a9a8e37..b1bfc968a8 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 55035cfae1..e4b9fb2c25 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 Enter 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 573af5aa58..e080301405 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 900c5cd5bc..c10c0fa789 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('experimental/joinLines'); - -export const onEnter = request>("onEnter"); +export const onEnter = new lc.RequestType('experimental/onEnter'); export interface RunnablesParams { textDocument: lc.TextDocumentIdentifier; diff --git a/editors/code/src/snippets.ts b/editors/code/src/snippets.ts index 794530162d..bcb3f2cc76 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) => {