From 4b495da368162a5b373d078be4ff51e55bffdf69 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Thu, 21 May 2020 14:26:44 +0200 Subject: [PATCH] Transition OnEnter to WorkspaceSnippetEdit This also changes our handiling of snippet edits on the client side. `editor.insertSnippet` unfortunately forces indentation, which we really don't want to have to deal with. So, let's just implement our manual hacky way of dealing with a simple subset of snippets we actually use in rust-analyzer --- crates/ra_ide/src/typing/on_enter.rs | 29 +++---- crates/rust-analyzer/src/lsp_ext.rs | 2 +- .../rust-analyzer/src/main_loop/handlers.rs | 4 +- .../rust-analyzer/tests/heavy_tests/main.rs | 68 +++++++--------- editors/code/src/commands/index.ts | 81 ++++++++++++------- editors/code/src/commands/on_enter.ts | 5 +- editors/code/src/main.ts | 2 +- editors/code/src/rust-analyzer-api.ts | 2 +- 8 files changed, 100 insertions(+), 93 deletions(-) diff --git a/crates/ra_ide/src/typing/on_enter.rs b/crates/ra_ide/src/typing/on_enter.rs index 78a40cc94c..85be14ad33 100644 --- a/crates/ra_ide/src/typing/on_enter.rs +++ b/crates/ra_ide/src/typing/on_enter.rs @@ -38,17 +38,15 @@ pub(crate) fn on_enter(db: &RootDatabase, position: FilePosition) -> Option bool { @@ -84,7 +82,7 @@ fn node_indent(file: &SourceFile, token: &SyntaxToken) -> Option { #[cfg(test)] mod tests { - use test_utils::{add_cursor, assert_eq_text, extract_offset}; + use test_utils::{assert_eq_text, extract_offset}; use crate::mock_analysis::single_file; @@ -98,7 +96,6 @@ mod tests { assert_eq!(result.source_file_edits.len(), 1); let mut actual = before.to_string(); result.source_file_edits[0].edit.apply(&mut actual); - let actual = add_cursor(&actual, result.cursor_position.unwrap().offset); Some(actual) } @@ -121,7 +118,7 @@ fn foo() { ", r" /// Some docs -/// <|> +/// $0 fn foo() { } ", @@ -137,7 +134,7 @@ impl S { r" impl S { /// Some - /// <|> docs. + /// $0 docs. fn foo() {} } ", @@ -151,7 +148,7 @@ fn foo() { ", r" /// -/// <|> Some docs +/// $0 Some docs fn foo() { } ", @@ -175,7 +172,7 @@ fn main() { r" fn main() { // Fix - // <|> me + // $0 me let x = 1 + 1; } ", @@ -195,7 +192,7 @@ fn main() { r" fn main() { // Fix - // <|> + // $0 // me let x = 1 + 1; } diff --git a/crates/rust-analyzer/src/lsp_ext.rs b/crates/rust-analyzer/src/lsp_ext.rs index f75a26eb79..3c7bd609d2 100644 --- a/crates/rust-analyzer/src/lsp_ext.rs +++ b/crates/rust-analyzer/src/lsp_ext.rs @@ -102,7 +102,7 @@ pub enum OnEnter {} impl Request for OnEnter { type Params = lsp_types::TextDocumentPositionParams; - type Result = Option; + type Result = Option; const METHOD: &'static str = "rust-analyzer/onEnter"; } diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs index be6a0aece9..fcf08cd798 100644 --- a/crates/rust-analyzer/src/main_loop/handlers.rs +++ b/crates/rust-analyzer/src/main_loop/handlers.rs @@ -159,12 +159,12 @@ 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::source_change(&world, source_change).map(Some), + Some(source_change) => to_proto::snippet_workspace_edit(&world, source_change).map(Some), } } diff --git a/crates/rust-analyzer/tests/heavy_tests/main.rs b/crates/rust-analyzer/tests/heavy_tests/main.rs index 74676b3eed..4e94c37e1c 100644 --- a/crates/rust-analyzer/tests/heavy_tests/main.rs +++ b/crates/rust-analyzer/tests/heavy_tests/main.rs @@ -474,27 +474,21 @@ fn main() {{}} position: Position { line: 0, character: 5 }, }, json!({ - "cursorPosition": { - "position": { "character": 4, "line": 1 }, - "textDocument": { "uri": "file:///[..]src/m0.rs" } - }, - "label": "On enter", - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "\n/// ", - "range": { - "end": { "character": 5, "line": 0 }, - "start": { "character": 5, "line": 0 } - } + "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 } - } - ] - } + } + ], + "textDocument": { "uri": "file:///[..]src/m0.rs", "version": null } + } + ] }), ); let elapsed = start.elapsed(); @@ -526,27 +520,21 @@ version = \"0.0.0\" position: Position { line: 0, character: 8 }, }, json!({ - "cursorPosition": { - "position": { "line": 1, "character": 4 }, - "textDocument": { "uri": "file:///[..]src/main.rs" } - }, - "label": "On enter", - "workspaceEdit": { - "documentChanges": [ - { - "edits": [ - { - "newText": "\r\n/// ", - "range": { - "end": { "line": 0, "character": 8 }, - "start": { "line": 0, "character": 8 } - } + "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 } - } - ] - } + } + ], + "textDocument": { "uri": "file:///[..]src/main.rs", "version": null } + } + ] }), ); } diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts index 0937b495c2..e5ed77e322 100644 --- a/editors/code/src/commands/index.ts +++ b/editors/code/src/commands/index.ts @@ -53,36 +53,57 @@ export function selectAndApplySourceChange(ctx: Ctx): Cmd { }; } -export function applySnippetWorkspaceEdit(_ctx: Ctx): Cmd { +export function applySnippetWorkspaceEditCommand(_ctx: Ctx): Cmd { return async (edit: vscode.WorkspaceEdit) => { - assert(edit.entries().length === 1, `bad ws edit: ${JSON.stringify(edit)}`); - const [uri, edits] = edit.entries()[0]; - - const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString()); - if (!editor) return; - - let editWithSnippet: vscode.TextEdit | undefined = undefined; - let lineDelta = 0; - await editor.edit((builder) => { - for (const indel of edits) { - const isSnippet = indel.newText.indexOf('$0') !== -1 || indel.newText.indexOf('${') !== -1; - if (isSnippet) { - editWithSnippet = indel; - } else { - if (!editWithSnippet) { - lineDelta = (indel.newText.match(/\n/g) || []).length - (indel.range.end.line - indel.range.start.line); - } - builder.replace(indel.range, indel.newText); - } - } - }); - if (editWithSnippet) { - const snip = editWithSnippet as vscode.TextEdit; - const range = snip.range.with( - snip.range.start.with(snip.range.start.line + lineDelta), - snip.range.end.with(snip.range.end.line + lineDelta), - ); - await editor.insertSnippet(new vscode.SnippetString(snip.newText), range); - } + await applySnippetWorkspaceEdit(edit); }; } + +export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) { + assert(edit.entries().length === 1, `bad ws edit: ${JSON.stringify(edit)}`); + const [uri, edits] = edit.entries()[0]; + + const editor = vscode.window.visibleTextEditors.find((it) => it.document.uri.toString() === uri.toString()); + if (!editor) return; + + let selection: vscode.Selection | undefined = undefined; + let lineDelta = 0; + await editor.edit((builder) => { + for (const indel of edits) { + const parsed = parseSnippet(indel.newText); + if (parsed) { + const [newText, [placeholderStart, placeholderLength]] = parsed; + const prefix = newText.substr(0, placeholderStart); + const lastNewline = prefix.lastIndexOf('\n'); + + const startLine = indel.range.start.line + lineDelta + countLines(prefix); + const startColumn = lastNewline === -1 ? + indel.range.start.character + placeholderStart + : prefix.length - lastNewline - 1; + const endColumn = startColumn + placeholderLength; + selection = new vscode.Selection( + new vscode.Position(startLine, startColumn), + new vscode.Position(startLine, endColumn), + ); + builder.replace(indel.range, newText); + } else { + lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line); + builder.replace(indel.range, indel.newText); + } + } + }); + if (selection) editor.selection = selection; +} + +function parseSnippet(snip: string): [string, [number, number]] | undefined { + const m = snip.match(/\$(0|\{0:([^}]*)\})/); + if (!m) return undefined; + const placeholder = m[2] ?? ""; + const range: [number, number] = [m.index!!, placeholder.length]; + const insert = snip.replace(m[0], placeholder); + return [insert, range]; +} + +function countLines(text: string): number { + return (text.match(/\n/g) || []).length; +} diff --git a/editors/code/src/commands/on_enter.ts b/editors/code/src/commands/on_enter.ts index 285849db70..a7871c31ee 100644 --- a/editors/code/src/commands/on_enter.ts +++ b/editors/code/src/commands/on_enter.ts @@ -1,8 +1,8 @@ import * as vscode from 'vscode'; import * as ra from '../rust-analyzer-api'; -import { applySourceChange } from '../source_change'; import { Cmd, Ctx } from '../ctx'; +import { applySnippetWorkspaceEdit } from '.'; async function handleKeypress(ctx: Ctx) { const editor = ctx.activeRustEditor; @@ -21,7 +21,8 @@ async function handleKeypress(ctx: Ctx) { }); if (!change) return false; - await applySourceChange(ctx, change); + const workspaceEdit = client.protocol2CodeConverter.asWorkspaceEdit(change); + await applySnippetWorkspaceEdit(workspaceEdit); return true; } diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index ac3bb365e2..8b0a9d8706 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -91,7 +91,7 @@ export async function activate(context: vscode.ExtensionContext) { ctx.registerCommand('debugSingle', commands.debugSingle); ctx.registerCommand('showReferences', commands.showReferences); ctx.registerCommand('applySourceChange', commands.applySourceChange); - ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEdit); + ctx.registerCommand('applySnippetWorkspaceEdit', commands.applySnippetWorkspaceEditCommand); ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange); ctx.pushCleanup(activateTaskProvider(workspaceFolder)); diff --git a/editors/code/src/rust-analyzer-api.ts b/editors/code/src/rust-analyzer-api.ts index 400ac37145..3b83b10e38 100644 --- a/editors/code/src/rust-analyzer-api.ts +++ b/editors/code/src/rust-analyzer-api.ts @@ -69,7 +69,7 @@ export interface JoinLinesParams { export const joinLines = request("joinLines"); -export const onEnter = request>("onEnter"); +export const onEnter = request>("onEnter"); export interface RunnablesParams { textDocument: lc.TextDocumentIdentifier;