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);