From e43fffca1ba0a731d9cc0bf930d07774153acd77 Mon Sep 17 00:00:00 2001 From: Heyward Fann Date: Wed, 20 May 2020 18:56:21 +0800 Subject: [PATCH 1/3] feat(cmds): support snippet text edit https://github.com/rust-analyzer/rust-analyzer/pull/4494 --- src/client.ts | 46 +++++++++++++++++++++++++++++++++++++++++++++- src/cmds/index.ts | 31 ++++++++++++++++++++++++++++++- src/index.ts | 1 + 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index 66afdafc..614c6b67 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,4 +1,26 @@ -import { Executable, LanguageClient, LanguageClientOptions, ServerOptions, Uri, workspace } from 'coc.nvim'; +import { Executable, LanguageClient, LanguageClientOptions, ServerOptions, StaticFeature, Uri, workspace } from 'coc.nvim'; +import { ClientCapabilities, CodeAction, CodeActionParams, CodeActionRequest, Command, InsertTextFormat, TextDocumentEdit } from 'vscode-languageserver-protocol'; + +class SnippetTextEditFeature implements StaticFeature { + fillClientCapabilities(capabilities: ClientCapabilities): void { + const caps: any = capabilities.experimental ?? {}; + caps.snippetTextEdit = true; + capabilities.experimental = caps; + } + initialize(): void {} +} + +function isSnippetEdit(action: CodeAction): boolean { + const documentChanges = action.edit?.documentChanges ?? []; + for (const edit of documentChanges) { + if (TextDocumentEdit.is(edit)) { + if (edit.edits.some((indel) => (indel as any).insertTextFormat === InsertTextFormat.Snippet)) { + return true; + } + } + } + return false; +} export function createClient(bin: string): LanguageClient { let folder = '.'; @@ -24,6 +46,26 @@ export function createClient(bin: string): LanguageClient { position.character = character - 1; return help; }, + provideCodeActions(document, range, context, token) { + const params: CodeActionParams = { + textDocument: { uri: document.uri }, + range, + context, + }; + // eslint-disable-next-line @typescript-eslint/no-use-before-define + return client.sendRequest(CodeActionRequest.type, params, token).then((values) => { + if (values === null) return undefined; + const result: (CodeAction | Command)[] = []; + for (const item of values) { + if (CodeAction.is(item) && isSnippetEdit(item)) { + item.command = Command.create('', 'rust-analyzer.applySnippetWorkspaceEdit', item.edit); + item.edit = undefined; + } + result.push(item); + } + return result; + }); + }, }, outputChannel, }; @@ -51,5 +93,7 @@ export function createClient(bin: string): LanguageClient { }, }; client.registerProposedFeatures(); + client.registerFeature(new SnippetTextEditFeature()); + return client; } diff --git a/src/cmds/index.ts b/src/cmds/index.ts index 9b82efb3..333be599 100644 --- a/src/cmds/index.ts +++ b/src/cmds/index.ts @@ -1,5 +1,5 @@ import { commands, Uri, workspace } from 'coc.nvim'; -import { Location, Position } from 'vscode-languageserver-protocol'; +import { Location, Position, Range, TextDocumentEdit, TextEdit, WorkspaceEdit } from 'vscode-languageserver-protocol'; import { Cmd, Ctx } from '../ctx'; import * as ra from '../rust-analyzer-api'; import * as sourceChange from '../source_change'; @@ -67,3 +67,32 @@ export function toggleInlayHints(ctx: Ctx) { } }; } + +export function applySnippetWorkspaceEdit(): Cmd { + return async (edit: WorkspaceEdit) => { + if (edit.documentChanges && edit.documentChanges.length) { + let editWithSnippet: TextEdit | undefined = undefined; + let lineDelta = 0; + + const edits = (edit.documentChanges as TextDocumentEdit[])[0].edits; + 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); + } + TextEdit.replace(indel.range, indel.newText); + } + } + + if (editWithSnippet) { + const snip = editWithSnippet as TextEdit; + const range = Range.create(snip.range.start.line + lineDelta, snip.range.start.character, snip.range.end.line + lineDelta, snip.range.end.character); + snip.range = range; + await commands.executeCommand('editor.action.insertSnippet', snip); + } + } + }; +} diff --git a/src/index.ts b/src/index.ts index 3a7fcfbb..abc8288d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ export async function activate(context: ExtensionContext): Promise { ctx.registerCommand('analyzerStatus', cmds.analyzerStatus); ctx.registerCommand('applySourceChange', cmds.applySourceChange); + ctx.registerCommand('applySnippetWorkspaceEdit', cmds.applySnippetWorkspaceEdit); ctx.registerCommand('selectAndApplySourceChange', cmds.selectAndApplySourceChange); ctx.registerCommand('collectGarbage', cmds.collectGarbage); ctx.registerCommand('expandMacro', cmds.expandMacro); From 1185aaaad76e0a9519a06048ae6537caf068ce41 Mon Sep 17 00:00:00 2001 From: Heyward Fann Date: Fri, 22 May 2020 18:36:18 +0800 Subject: [PATCH 2/3] feat(cmds): improve snippet text edit --- src/cmds/index.ts | 30 +++++++++++++++++------------- src/source_change.ts | 2 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/cmds/index.ts b/src/cmds/index.ts index 333be599..8eb4be3f 100644 --- a/src/cmds/index.ts +++ b/src/cmds/index.ts @@ -70,28 +70,32 @@ export function toggleInlayHints(ctx: Ctx) { export function applySnippetWorkspaceEdit(): Cmd { return async (edit: WorkspaceEdit) => { - if (edit.documentChanges && edit.documentChanges.length) { + if (!edit.documentChanges?.length) { + return; + } + + const change = edit.documentChanges[0]; + if (TextDocumentEdit.is(change)) { let editWithSnippet: TextEdit | undefined = undefined; - let lineDelta = 0; - const edits = (edit.documentChanges as TextDocumentEdit[])[0].edits; - for (const indel of edits) { + for (const indel of change.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); - } - TextEdit.replace(indel.range, indel.newText); } } if (editWithSnippet) { - const snip = editWithSnippet as TextEdit; - const range = Range.create(snip.range.start.line + lineDelta, snip.range.start.character, snip.range.end.line + lineDelta, snip.range.end.character); - snip.range = range; - await commands.executeCommand('editor.action.insertSnippet', snip); + const current = await workspace.document; + if (current.uri !== change.textDocument.uri) { + const start = Position.create(editWithSnippet.range.start.line - 1, editWithSnippet.range.start.character); + const end = Position.create(editWithSnippet.range.end.line - 1, editWithSnippet.range.end.character); + editWithSnippet = TextEdit.replace(Range.create(start, end), editWithSnippet.newText); + + await workspace.loadFile(change.textDocument.uri); + await workspace.jumpTo(change.textDocument.uri); + } + await commands.executeCommand('editor.action.insertSnippet', editWithSnippet); } } }; diff --git a/src/source_change.ts b/src/source_change.ts index 85a81153..1280a1a9 100644 --- a/src/source_change.ts +++ b/src/source_change.ts @@ -29,6 +29,6 @@ export async function applySourceChange(change: SourceChange) { } else if (toReveal) { const uri = toReveal.textDocument.uri; const position = toReveal.position; - workspace.jumpTo(uri, position); + await workspace.jumpTo(uri, position); } } From 0f7e6bc75f7df982341b0aed57fc6680b7329f23 Mon Sep 17 00:00:00 2001 From: Heyward Fann Date: Mon, 25 May 2020 17:23:35 +0800 Subject: [PATCH 3/3] feat(cmds): snippet textedit --- src/cmds/index.ts | 79 +++++++++++++++++++++++++++++++++-------------- src/index.ts | 2 +- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/src/cmds/index.ts b/src/cmds/index.ts index 8eb4be3f..0aee6f65 100644 --- a/src/cmds/index.ts +++ b/src/cmds/index.ts @@ -68,35 +68,66 @@ export function toggleInlayHints(ctx: Ctx) { }; } -export function applySnippetWorkspaceEdit(): Cmd { - return async (edit: WorkspaceEdit) => { - if (!edit.documentChanges?.length) { - return; - } +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; +} + +export async function applySnippetWorkspaceEdit(edit: WorkspaceEdit) { + if (!edit.documentChanges?.length) { + return; + } - const change = edit.documentChanges[0]; - if (TextDocumentEdit.is(change)) { - let editWithSnippet: TextEdit | undefined = undefined; + let selection: Range | undefined = undefined; + let lineDelta = 0; + const change = edit.documentChanges[0]; + if (TextDocumentEdit.is(change)) { + for (const indel of change.edits) { + const wsEdit: WorkspaceEdit = {}; + const parsed = parseSnippet(indel.newText); + if (parsed) { + const [newText, [placeholderStart, placeholderLength]] = parsed; + const prefix = newText.substr(0, placeholderStart); + const lastNewline = prefix.lastIndexOf('\n'); - for (const indel of change.edits) { - const isSnippet = indel.newText.indexOf('$0') !== -1 || indel.newText.indexOf('${') !== -1; - if (isSnippet) { - editWithSnippet = indel; - } + 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 = Range.create(startLine, startColumn, startLine, endColumn); + + const newChange = TextDocumentEdit.create(change.textDocument, [TextEdit.replace(indel.range, newText)]); + wsEdit.documentChanges = [newChange]; + } else { + lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line); + wsEdit.documentChanges = [change]; } - if (editWithSnippet) { - const current = await workspace.document; - if (current.uri !== change.textDocument.uri) { - const start = Position.create(editWithSnippet.range.start.line - 1, editWithSnippet.range.start.character); - const end = Position.create(editWithSnippet.range.end.line - 1, editWithSnippet.range.end.character); - editWithSnippet = TextEdit.replace(Range.create(start, end), editWithSnippet.newText); - - await workspace.loadFile(change.textDocument.uri); - await workspace.jumpTo(change.textDocument.uri); - } - await commands.executeCommand('editor.action.insertSnippet', editWithSnippet); + await workspace.applyEdit(wsEdit); + } + + if (selection) { + const current = await workspace.document; + if (current.uri !== change.textDocument.uri) { + await workspace.loadFile(change.textDocument.uri); + await workspace.jumpTo(change.textDocument.uri); + // FIXME + return; } + await workspace.selectRange(selection); } + } +} + +export function applySnippetWorkspaceEditCommand(): Cmd { + return async (edit: WorkspaceEdit) => { + await applySnippetWorkspaceEdit(edit); }; } diff --git a/src/index.ts b/src/index.ts index abc8288d..a3855572 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,7 +38,7 @@ export async function activate(context: ExtensionContext): Promise { ctx.registerCommand('analyzerStatus', cmds.analyzerStatus); ctx.registerCommand('applySourceChange', cmds.applySourceChange); - ctx.registerCommand('applySnippetWorkspaceEdit', cmds.applySnippetWorkspaceEdit); + ctx.registerCommand('applySnippetWorkspaceEdit', cmds.applySnippetWorkspaceEditCommand); ctx.registerCommand('selectAndApplySourceChange', cmds.selectAndApplySourceChange); ctx.registerCommand('collectGarbage', cmds.collectGarbage); ctx.registerCommand('expandMacro', cmds.expandMacro);