Skip to content

Commit 1d89e2d

Browse files
committed
feat(cmds): support snippet text edit (#252)
* feat(cmds): support snippet text edit rust-lang/rust-analyzer#4494 * feat(cmds): improve snippet text edit * feat(cmds): snippet textedit
1 parent ab9bec9 commit 1d89e2d

File tree

4 files changed

+112
-3
lines changed

4 files changed

+112
-3
lines changed

src/client.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
1-
import { Executable, LanguageClient, LanguageClientOptions, ServerOptions, Uri, workspace } from 'coc.nvim';
1+
import { Executable, LanguageClient, LanguageClientOptions, ServerOptions, StaticFeature, Uri, workspace } from 'coc.nvim';
2+
import { ClientCapabilities, CodeAction, CodeActionParams, CodeActionRequest, Command, InsertTextFormat, TextDocumentEdit } from 'vscode-languageserver-protocol';
3+
4+
class SnippetTextEditFeature implements StaticFeature {
5+
fillClientCapabilities(capabilities: ClientCapabilities): void {
6+
const caps: any = capabilities.experimental ?? {};
7+
caps.snippetTextEdit = true;
8+
capabilities.experimental = caps;
9+
}
10+
initialize(): void {}
11+
}
12+
13+
function isSnippetEdit(action: CodeAction): boolean {
14+
const documentChanges = action.edit?.documentChanges ?? [];
15+
for (const edit of documentChanges) {
16+
if (TextDocumentEdit.is(edit)) {
17+
if (edit.edits.some((indel) => (indel as any).insertTextFormat === InsertTextFormat.Snippet)) {
18+
return true;
19+
}
20+
}
21+
}
22+
return false;
23+
}
224

325
export function createClient(bin: string): LanguageClient {
426
let folder = '.';
@@ -24,6 +46,26 @@ export function createClient(bin: string): LanguageClient {
2446
position.character = character - 1;
2547
return help;
2648
},
49+
provideCodeActions(document, range, context, token) {
50+
const params: CodeActionParams = {
51+
textDocument: { uri: document.uri },
52+
range,
53+
context,
54+
};
55+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
56+
return client.sendRequest(CodeActionRequest.type, params, token).then((values) => {
57+
if (values === null) return undefined;
58+
const result: (CodeAction | Command)[] = [];
59+
for (const item of values) {
60+
if (CodeAction.is(item) && isSnippetEdit(item)) {
61+
item.command = Command.create('', 'rust-analyzer.applySnippetWorkspaceEdit', item.edit);
62+
item.edit = undefined;
63+
}
64+
result.push(item);
65+
}
66+
return result;
67+
});
68+
},
2769
},
2870
outputChannel,
2971
};
@@ -51,5 +93,7 @@ export function createClient(bin: string): LanguageClient {
5193
},
5294
};
5395
client.registerProposedFeatures();
96+
client.registerFeature(new SnippetTextEditFeature());
97+
5498
return client;
5599
}

src/cmds/index.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { commands, Uri, workspace } from 'coc.nvim';
2-
import { Location, Position } from 'vscode-languageserver-protocol';
2+
import { Location, Position, Range, TextDocumentEdit, TextEdit, WorkspaceEdit } from 'vscode-languageserver-protocol';
33
import { Cmd, Ctx } from '../ctx';
44
import * as ra from '../rust-analyzer-api';
55
import * as sourceChange from '../source_change';
@@ -67,3 +67,67 @@ export function toggleInlayHints(ctx: Ctx) {
6767
}
6868
};
6969
}
70+
71+
function parseSnippet(snip: string): [string, [number, number]] | undefined {
72+
const m = snip.match(/\$(0|\{0:([^}]*)\})/);
73+
if (!m) return undefined;
74+
const placeholder = m[2] ?? '';
75+
const range: [number, number] = [m.index!!, placeholder.length];
76+
const insert = snip.replace(m[0], placeholder);
77+
return [insert, range];
78+
}
79+
80+
function countLines(text: string): number {
81+
return (text.match(/\n/g) || []).length;
82+
}
83+
84+
export async function applySnippetWorkspaceEdit(edit: WorkspaceEdit) {
85+
if (!edit.documentChanges?.length) {
86+
return;
87+
}
88+
89+
let selection: Range | undefined = undefined;
90+
let lineDelta = 0;
91+
const change = edit.documentChanges[0];
92+
if (TextDocumentEdit.is(change)) {
93+
for (const indel of change.edits) {
94+
const wsEdit: WorkspaceEdit = {};
95+
const parsed = parseSnippet(indel.newText);
96+
if (parsed) {
97+
const [newText, [placeholderStart, placeholderLength]] = parsed;
98+
const prefix = newText.substr(0, placeholderStart);
99+
const lastNewline = prefix.lastIndexOf('\n');
100+
101+
const startLine = indel.range.start.line + lineDelta + countLines(prefix);
102+
const startColumn = lastNewline === -1 ? indel.range.start.character + placeholderStart : prefix.length - lastNewline - 1;
103+
const endColumn = startColumn + placeholderLength;
104+
selection = Range.create(startLine, startColumn, startLine, endColumn);
105+
106+
const newChange = TextDocumentEdit.create(change.textDocument, [TextEdit.replace(indel.range, newText)]);
107+
wsEdit.documentChanges = [newChange];
108+
} else {
109+
lineDelta = countLines(indel.newText) - (indel.range.end.line - indel.range.start.line);
110+
wsEdit.documentChanges = [change];
111+
}
112+
113+
await workspace.applyEdit(wsEdit);
114+
}
115+
116+
if (selection) {
117+
const current = await workspace.document;
118+
if (current.uri !== change.textDocument.uri) {
119+
await workspace.loadFile(change.textDocument.uri);
120+
await workspace.jumpTo(change.textDocument.uri);
121+
// FIXME
122+
return;
123+
}
124+
await workspace.selectRange(selection);
125+
}
126+
}
127+
}
128+
129+
export function applySnippetWorkspaceEditCommand(): Cmd {
130+
return async (edit: WorkspaceEdit) => {
131+
await applySnippetWorkspaceEdit(edit);
132+
};
133+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export async function activate(context: ExtensionContext): Promise<void> {
3838

3939
ctx.registerCommand('analyzerStatus', cmds.analyzerStatus);
4040
ctx.registerCommand('applySourceChange', cmds.applySourceChange);
41+
ctx.registerCommand('applySnippetWorkspaceEdit', cmds.applySnippetWorkspaceEditCommand);
4142
ctx.registerCommand('selectAndApplySourceChange', cmds.selectAndApplySourceChange);
4243
ctx.registerCommand('collectGarbage', cmds.collectGarbage);
4344
ctx.registerCommand('expandMacro', cmds.expandMacro);

src/source_change.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@ export async function applySourceChange(change: SourceChange) {
2929
} else if (toReveal) {
3030
const uri = toReveal.textDocument.uri;
3131
const position = toReveal.position;
32-
workspace.jumpTo(uri, position);
32+
await workspace.jumpTo(uri, position);
3333
}
3434
}

0 commit comments

Comments
 (0)