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