diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index 5ac002d82f0c..d983cd910023 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 e7d64b4f68c2..4a239bc67deb 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 { diff --git a/crates/rust-analyzer/src/caps.rs b/crates/rust-analyzer/src/caps.rs index 780fc93174fa..d55cbb15fe88 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 52e4fcbecae5..1cce1baa4552 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 d73107968123..a13a0e1f523b 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 81a347247cb6..39d58f1e01bd 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/docs/dev/lsp-extensions.md b/docs/dev/lsp-extensions.md index 55035cfae183..0cf009175cfd 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:** TBA + +**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 573af5aa580d..e080301405dd 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 900c5cd5bce5..c10c0fa78942 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 794530162dc0..bcb3f2cc7613 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) => {