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..0aee6f65 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,67 @@ export function toggleInlayHints(ctx: Ctx) { } }; } + +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; + } + + 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'); + + 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]; + } + + 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 3a7fcfbb..a3855572 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.applySnippetWorkspaceEditCommand); ctx.registerCommand('selectAndApplySourceChange', cmds.selectAndApplySourceChange); ctx.registerCommand('collectGarbage', cmds.collectGarbage); ctx.registerCommand('expandMacro', cmds.expandMacro); 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); } }