@@ -3,20 +3,28 @@ import * as vscode from "vscode";
3
3
import { assert } from "./util" ;
4
4
import { unwrapUndefinable } from "./undefinable" ;
5
5
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 ] ) ;
9
14
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
+ }
11
19
return ;
12
20
}
13
- for ( const [ uri , edits ] of edit . entries ( ) ) {
21
+ for ( const [ uri , edits ] of editEntries ) {
14
22
const editor = await editorFromUri ( uri ) ;
15
23
if ( editor ) {
16
24
await editor . edit ( ( builder ) => {
17
25
for ( const indel of edits ) {
18
26
assert (
19
- ! parseSnippet ( indel . newText ) ,
27
+ ! ( indel instanceof vscode . SnippetTextEdit ) ,
20
28
`bad ws edit: snippet received with multiple edits: ${ JSON . stringify (
21
29
edit ,
22
30
) } `,
@@ -39,53 +47,97 @@ async function editorFromUri(uri: vscode.Uri): Promise<vscode.TextEditor | undef
39
47
}
40
48
41
49
export 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
+ }
51
55
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 ,
63
112
) ;
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 ;
67
121
}
68
- lineDelta +=
69
- countLines ( indel . newText ) - ( indel . range . end . line - indel . range . start . line ) ;
122
+
123
+ return snippetEdit ;
124
+ } else {
125
+ return edit ;
70
126
}
71
127
} ) ;
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
- }
77
128
}
78
129
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 ) ;
87
139
}
88
140
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 ) ] ;
91
143
}
0 commit comments