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.ts | 144 |
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)]; } |