@@ -3,20 +3,28 @@ import * as vscode from "vscode";
33import { assert } from "./util" ;
44import { unwrapUndefinable } from "./undefinable" ;
55
6- export async function applySnippetWorkspaceEdit ( edit : vscode . WorkspaceEdit ) {
7- if ( edit . entries ( ) . length === 1 ) {
8- const [ uri , edits ] = unwrapUndefinable ( edit . entries ( ) [ 0 ] ) ;
6+ export type SnippetTextDocumentEdit = [ vscode . Uri , ( vscode . TextEdit | vscode . SnippetTextEdit ) [ ] ] ;
7+
8+ export async function applySnippetWorkspaceEdit (
9+ edit : vscode . WorkspaceEdit ,
10+ editEntries : SnippetTextDocumentEdit [ ] ,
11+ ) {
12+ if ( editEntries . length === 1 ) {
13+ const [ uri , edits ] = unwrapUndefinable ( editEntries [ 0 ] ) ;
914 const editor = await editorFromUri ( uri ) ;
10- if ( editor ) await applySnippetTextEdits ( editor , edits ) ;
15+ if ( editor ) {
16+ edit . set ( uri , removeLeadingWhitespace ( editor , edits ) ) ;
17+ await vscode . workspace . applyEdit ( edit ) ;
18+ }
1119 return ;
1220 }
13- for ( const [ uri , edits ] of edit . entries ( ) ) {
21+ for ( const [ uri , edits ] of editEntries ) {
1422 const editor = await editorFromUri ( uri ) ;
1523 if ( editor ) {
1624 await editor . edit ( ( builder ) => {
1725 for ( const indel of edits ) {
1826 assert (
19- ! parseSnippet ( indel . newText ) ,
27+ ! ( indel instanceof vscode . SnippetTextEdit ) ,
2028 `bad ws edit: snippet received with multiple edits: ${ JSON . stringify (
2129 edit ,
2230 ) } `,
@@ -39,53 +47,97 @@ async function editorFromUri(uri: vscode.Uri): Promise<vscode.TextEditor | undef
3947}
4048
4149export async function applySnippetTextEdits ( editor : vscode . TextEditor , edits : vscode . TextEdit [ ] ) {
42- const selections : vscode . Selection [ ] = [ ] ;
43- let lineDelta = 0 ;
44- await editor . edit ( ( builder ) => {
45- for ( const indel of edits ) {
46- const parsed = parseSnippet ( indel . newText ) ;
47- if ( parsed ) {
48- const [ newText , [ placeholderStart , placeholderLength ] ] = parsed ;
49- const prefix = newText . substr ( 0 , placeholderStart ) ;
50- const lastNewline = prefix . lastIndexOf ( "\n" ) ;
50+ const edit = new vscode . WorkspaceEdit ( ) ;
51+ const snippetEdits = toSnippetTextEdits ( edits ) ;
52+ edit . set ( editor . document . uri , removeLeadingWhitespace ( editor , snippetEdits ) ) ;
53+ await vscode . workspace . applyEdit ( edit ) ;
54+ }
5155
52- const startLine = indel . range . start . line + lineDelta + countLines ( prefix ) ;
53- const startColumn =
54- lastNewline === - 1
55- ? indel . range . start . character + placeholderStart
56- : prefix . length - lastNewline - 1 ;
57- const endColumn = startColumn + placeholderLength ;
58- selections . push (
59- new vscode . Selection (
60- new vscode . Position ( startLine , startColumn ) ,
61- new vscode . Position ( startLine , endColumn ) ,
62- ) ,
56+ function hasSnippet ( snip : string ) : boolean {
57+ const m = snip . match ( / \$ \d + | \{ \d + : [ ^ } ] * \} / ) ;
58+ return m != null ;
59+ }
60+
61+ function toSnippetTextEdits (
62+ edits : vscode . TextEdit [ ] ,
63+ ) : ( vscode . TextEdit | vscode . SnippetTextEdit ) [ ] {
64+ return edits . map ( ( textEdit ) => {
65+ // Note: text edits without any snippets are returned as-is instead of
66+ // being wrapped in a SnippetTextEdit, as otherwise it would be
67+ // treated as if it had a tab stop at the end.
68+ if ( hasSnippet ( textEdit . newText ) ) {
69+ return new vscode . SnippetTextEdit (
70+ textEdit . range ,
71+ new vscode . SnippetString ( textEdit . newText ) ,
72+ ) ;
73+ } else {
74+ return textEdit ;
75+ }
76+ } ) ;
77+ }
78+
79+ /**
80+ * Removes the leading whitespace from snippet edits, so as to not double up
81+ * on indentation.
82+ *
83+ * Snippet edits by default adjust any multi-line snippets to match the
84+ * indentation of the line to insert at. Unfortunately, we (the server) also
85+ * include the required indentation to match what we line insert at, so we end
86+ * up doubling up the indentation. Since there isn't any way to tell vscode to
87+ * not fixup indentation for us, we instead opt to remove the indentation and
88+ * then let vscode add it back in.
89+ *
90+ * This assumes that the source snippet text edits have the required
91+ * indentation, but that's okay as even without this workaround and the problem
92+ * to workaround, those snippet edits would already be inserting at the wrong
93+ * indentation.
94+ */
95+ function removeLeadingWhitespace (
96+ editor : vscode . TextEditor ,
97+ edits : ( vscode . TextEdit | vscode . SnippetTextEdit ) [ ] ,
98+ ) {
99+ return edits . map ( ( edit ) => {
100+ if ( edit instanceof vscode . SnippetTextEdit ) {
101+ const snippetEdit : vscode . SnippetTextEdit = edit ;
102+ const firstLineEnd = snippetEdit . snippet . value . indexOf ( "\n" ) ;
103+
104+ if ( firstLineEnd !== - 1 ) {
105+ // Is a multi-line snippet, remove the indentation which
106+ // would be added back in by vscode.
107+ const startLine = editor . document . lineAt ( snippetEdit . range . start . line ) ;
108+ const leadingWhitespace = getLeadingWhitespace (
109+ startLine . text ,
110+ 0 ,
111+ startLine . firstNonWhitespaceCharacterIndex ,
63112 ) ;
64- builder . replace ( indel . range , newText ) ;
65- } else {
66- builder . replace ( indel . range , indel . newText ) ;
113+
114+ const [ firstLine , rest ] = splitAt ( snippetEdit . snippet . value , firstLineEnd + 1 ) ;
115+ const unindentedLines = rest
116+ . split ( "\n" )
117+ . map ( ( line ) => line . replace ( leadingWhitespace , "" ) )
118+ . join ( "\n" ) ;
119+
120+ snippetEdit . snippet . value = firstLine + unindentedLines ;
67121 }
68- lineDelta +=
69- countLines ( indel . newText ) - ( indel . range . end . line - indel . range . start . line ) ;
122+
123+ return snippetEdit ;
124+ } else {
125+ return edit ;
70126 }
71127 } ) ;
72- if ( selections . length > 0 ) editor . selections = selections ;
73- if ( selections . length === 1 ) {
74- const selection = unwrapUndefinable ( selections [ 0 ] ) ;
75- editor . revealRange ( selection , vscode . TextEditorRevealType . InCenterIfOutsideViewport ) ;
76- }
77128}
78129
79- function parseSnippet ( snip : string ) : [ string , [ number , number ] ] | undefined {
80- const m = snip . match ( / \$ ( 0 | \{ 0 : ( [ ^ } ] * ) \} ) / ) ;
81- if ( ! m ) return undefined ;
82- const placeholder = m [ 2 ] ?? "" ;
83- if ( m . index == null ) return undefined ;
84- const range : [ number , number ] = [ m . index , placeholder . length ] ;
85- const insert = snip . replace ( m [ 0 ] , placeholder ) ;
86- return [ insert , range ] ;
130+ // based on https://github.com/microsoft/vscode/blob/main/src/vs/base/common/strings.ts#L284
131+ function getLeadingWhitespace ( str : string , start : number = 0 , end : number = str . length ) : string {
132+ for ( let i = start ; i < end ; i ++ ) {
133+ const chCode = str . charCodeAt ( i ) ;
134+ if ( chCode !== " " . charCodeAt ( 0 ) && chCode !== " " . charCodeAt ( 0 ) ) {
135+ return str . substring ( start , i ) ;
136+ }
137+ }
138+ return str . substring ( start , end ) ;
87139}
88140
89- function countLines ( text : string ) : number {
90- return ( text . match ( / \n / g ) || [ ] ) . length ;
141+ function splitAt ( str : string , index : number ) : [ string , string ] {
142+ return [ str . substring ( 0 , index ) , str . substring ( index ) ] ;
91143}
0 commit comments