Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'editors/code/src/snippets.ts')
-rw-r--r--editors/code/src/snippets.ts144
1 files changed, 98 insertions, 46 deletions
diff --git a/editors/code/src/snippets.ts b/editors/code/src/snippets.ts
index d81765649f..b3982bdf2b 100644
--- a/editors/code/src/snippets.ts
+++ b/editors/code/src/snippets.ts
@@ -3,20 +3,28 @@ import * as vscode from "vscode";
import { assert } from "./util";
import { unwrapUndefinable } from "./undefinable";
-export async function applySnippetWorkspaceEdit(edit: vscode.WorkspaceEdit) {
- if (edit.entries().length === 1) {
- const [uri, edits] = unwrapUndefinable(edit.entries()[0]);
+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) await applySnippetTextEdits(editor, edits);
+ if (editor) {
+ edit.set(uri, removeLeadingWhitespace(editor, edits));
+ await vscode.workspace.applyEdit(edit);
+ }
return;
}
- for (const [uri, edits] of edit.entries()) {
+ for (const [uri, edits] of editEntries) {
const editor = await editorFromUri(uri);
if (editor) {
await editor.edit((builder) => {
for (const indel of edits) {
assert(
- !parseSnippet(indel.newText),
+ !(indel instanceof vscode.SnippetTextEdit),
`bad ws edit: snippet received with multiple edits: ${JSON.stringify(
edit,
)}`,
@@ -39,53 +47,97 @@ async function editorFromUri(uri: vscode.Uri): Promise<vscode.TextEditor | undef
}
export async function applySnippetTextEdits(editor: vscode.TextEditor, edits: vscode.TextEdit[]) {
- const selections: vscode.Selection[] = [];
- let lineDelta = 0;
- await editor.edit((builder) => {
- for (const indel of edits) {
- const parsed = parseSnippet(indel.newText);
- if (parsed) {
- const [newText, [placeholderStart, placeholderLength]] = parsed;
- const prefix = newText.substr(0, placeholderStart);
- const lastNewline = prefix.lastIndexOf("\n");
+ const edit = new vscode.WorkspaceEdit();
+ const snippetEdits = toSnippetTextEdits(edits);
+ edit.set(editor.document.uri, removeLeadingWhitespace(editor, snippetEdits));
+ await vscode.workspace.applyEdit(edit);
+}
- 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;
- selections.push(
- new vscode.Selection(
- new vscode.Position(startLine, startColumn),
- new vscode.Position(startLine, endColumn),
- ),
+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,
);
- builder.replace(indel.range, newText);
- } else {
- builder.replace(indel.range, indel.newText);
+
+ 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;
}
- lineDelta +=
- countLines(indel.newText) - (indel.range.end.line - indel.range.start.line);
+
+ return snippetEdit;
+ } else {
+ return edit;
}
});
- if (selections.length > 0) editor.selections = selections;
- if (selections.length === 1) {
- const selection = unwrapUndefinable(selections[0]);
- editor.revealRange(selection, vscode.TextEditorRevealType.InCenterIfOutsideViewport);
- }
}
-function parseSnippet(snip: string): [string, [number, number]] | undefined {
- const m = snip.match(/\$(0|\{0:([^}]*)\})/);
- if (!m) return undefined;
- const placeholder = m[2] ?? "";
- if (m.index == null) return undefined;
- const range: [number, number] = [m.index, placeholder.length];
- const insert = snip.replace(m[0], placeholder);
- return [insert, range];
+// 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 countLines(text: string): number {
- return (text.match(/\n/g) || []).length;
+function splitAt(str: string, index: number): [string, string] {
+ return [str.substring(0, index), str.substring(index)];
}