-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathsnippets.ts
140 lines (126 loc) · 5.18 KB
/
snippets.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import * as vscode from "vscode";
import { assert, unwrapUndefinable } from "./util";
export type SnippetTextDocumentEdit = [vscode.Uri, (vscode.TextEdit | vscode.SnippetTextEdit)[]];
export async function applySnippetWorkspaceEdit(
edit: vscode.WorkspaceEdit,
editEntries: SnippetTextDocumentEdit[],
) {
if (editEntries.length === 1) {
const [uri, edits] = unwrapUndefinable(editEntries[0]);
const editor = await editorFromUri(uri);
if (editor) {
edit.set(uri, removeLeadingWhitespace(editor, edits));
await vscode.workspace.applyEdit(edit);
}
return;
}
for (const [uri, edits] of editEntries) {
const editor = await editorFromUri(uri);
if (editor) {
await editor.edit((builder) => {
for (const indel of edits) {
assert(
!(indel instanceof vscode.SnippetTextEdit),
`bad ws edit: snippet received with multiple edits: ${JSON.stringify(edit)}`,
);
builder.replace(indel.range, indel.newText);
}
});
}
}
}
async function editorFromUri(uri: vscode.Uri): Promise<vscode.TextEditor | undefined> {
if (vscode.window.activeTextEditor?.document.uri !== uri) {
// `vscode.window.visibleTextEditors` only contains editors whose contents are being displayed
await vscode.window.showTextDocument(uri, {});
}
return vscode.window.visibleTextEditors.find(
(it) => it.document.uri.toString() === uri.toString(),
);
}
export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vscode.TextEdit[]) {
const edit = new vscode.WorkspaceEdit();
const snippetEdits = toSnippetTextEdits(edits);
edit.set(editor.document.uri, removeLeadingWhitespace(editor, snippetEdits));
await vscode.workspace.applyEdit(edit);
}
function hasSnippet(snip: string): boolean {
const m = snip.match(/\$\d+|\{\d+:[^}]*\}/);
return m != null;
}
function toSnippetTextEdits(
edits: vscode.TextEdit[],
): (vscode.TextEdit | vscode.SnippetTextEdit)[] {
return edits.map((textEdit) => {
// Note: text edits without any snippets are returned as-is instead of
// being wrapped in a SnippetTextEdit, as otherwise it would be
// treated as if it had a tab stop at the end.
if (hasSnippet(textEdit.newText)) {
return new vscode.SnippetTextEdit(
textEdit.range,
new vscode.SnippetString(textEdit.newText),
);
} else {
return textEdit;
}
});
}
/**
* Removes the leading whitespace from snippet edits, so as to not double up
* on indentation.
*
* Snippet edits by default adjust any multi-line snippets to match the
* indentation of the line to insert at. Unfortunately, we (the server) also
* include the required indentation to match what we line insert at, so we end
* up doubling up the indentation. Since there isn't any way to tell vscode to
* not fixup indentation for us, we instead opt to remove the indentation and
* then let vscode add it back in.
*
* This assumes that the source snippet text edits have the required
* indentation, but that's okay as even without this workaround and the problem
* to workaround, those snippet edits would already be inserting at the wrong
* indentation.
*/
function removeLeadingWhitespace(
editor: vscode.TextEditor,
edits: (vscode.TextEdit | vscode.SnippetTextEdit)[],
) {
return edits.map((edit) => {
if (edit instanceof vscode.SnippetTextEdit) {
const snippetEdit: vscode.SnippetTextEdit = edit;
const firstLineEnd = snippetEdit.snippet.value.indexOf("\n");
if (firstLineEnd !== -1) {
// Is a multi-line snippet, remove the indentation which
// would be added back in by vscode.
const startLine = editor.document.lineAt(snippetEdit.range.start.line);
const leadingWhitespace = getLeadingWhitespace(
startLine.text,
0,
startLine.firstNonWhitespaceCharacterIndex,
);
const [firstLine, rest] = splitAt(snippetEdit.snippet.value, firstLineEnd + 1);
const unindentedLines = rest
.split("\n")
.map((line) => line.replace(leadingWhitespace, ""))
.join("\n");
snippetEdit.snippet.value = firstLine + unindentedLines;
}
return snippetEdit;
} else {
return edit;
}
});
}
// based on https://github.com/microsoft/vscode/blob/main/src/vs/base/common/strings.ts#L284
function getLeadingWhitespace(str: string, start: number = 0, end: number = str.length): string {
for (let i = start; i < end; i++) {
const chCode = str.charCodeAt(i);
if (chCode !== " ".charCodeAt(0) && chCode !== " ".charCodeAt(0)) {
return str.substring(start, i);
}
}
return str.substring(start, end);
}
function splitAt(str: string, index: number): [string, string] {
return [str.substring(0, index), str.substring(index)];
}