@@ -20,11 +20,13 @@ function getBuiltinCommands(shell: string): string[] | undefined {
20
20
if ( cachedCommands ) {
21
21
return cachedCommands ;
22
22
}
23
+ // fixes a bug with file/folder completions brought about by the '.' command
24
+ const filter = ( cmd : string ) => cmd && cmd !== '.' ;
23
25
const options : ExecOptionsWithStringEncoding = { encoding : 'utf-8' , shell } ;
24
26
switch ( shellType ) {
25
27
case 'bash' : {
26
28
const bashOutput = execSync ( 'compgen -b' , options ) ;
27
- const bashResult = bashOutput . split ( '\n' ) . filter ( cmd => cmd ) ;
29
+ const bashResult = bashOutput . split ( '\n' ) . filter ( filter ) ;
28
30
if ( bashResult . length ) {
29
31
cachedBuiltinCommands ?. set ( shellType , bashResult ) ;
30
32
return bashResult ;
@@ -33,7 +35,7 @@ function getBuiltinCommands(shell: string): string[] | undefined {
33
35
}
34
36
case 'zsh' : {
35
37
const zshOutput = execSync ( 'printf "%s\\n" ${(k)builtins}' , options ) ;
36
- const zshResult = zshOutput . split ( '\n' ) . filter ( cmd => cmd ) ;
38
+ const zshResult = zshOutput . split ( '\n' ) . filter ( filter ) ;
37
39
if ( zshResult . length ) {
38
40
cachedBuiltinCommands ?. set ( shellType , zshResult ) ;
39
41
return zshResult ;
@@ -43,7 +45,7 @@ function getBuiltinCommands(shell: string): string[] | undefined {
43
45
// TODO: ghost text in the command line prevents
44
46
// completions from working ATM for fish
45
47
const fishOutput = execSync ( 'functions -n' , options ) ;
46
- const fishResult = fishOutput . split ( ', ' ) . filter ( cmd => cmd ) ;
48
+ const fishResult = fishOutput . split ( ', ' ) . filter ( filter ) ;
47
49
if ( fishResult . length ) {
48
50
cachedBuiltinCommands ?. set ( shellType , fishResult ) ;
49
51
return fishResult ;
@@ -64,122 +66,81 @@ function getBuiltinCommands(shell: string): string[] | undefined {
64
66
export async function activate ( context : vscode . ExtensionContext ) {
65
67
context . subscriptions . push ( vscode . window . registerTerminalCompletionProvider ( {
66
68
id : 'terminal-suggest' ,
67
- async provideTerminalCompletions ( terminal : vscode . Terminal , terminalContext : { commandLine : string ; cursorPosition : number } , token : vscode . CancellationToken ) : Promise < vscode . TerminalCompletionItem [ ] | undefined > {
69
+ async provideTerminalCompletions ( terminal : vscode . Terminal , terminalContext : { commandLine : string ; cursorPosition : number } , token : vscode . CancellationToken ) : Promise < vscode . TerminalCompletionItem [ ] | vscode . TerminalCompletionList | undefined > {
68
70
if ( token . isCancellationRequested ) {
69
71
return ;
70
72
}
71
73
72
- const availableCommands = await getCommandsInPath ( ) ;
73
- if ( ! availableCommands ) {
74
- return ;
75
- }
76
-
77
74
// TODO: Leverage shellType when available https://github.com/microsoft/vscode/issues/230165
78
75
const shellPath = 'shellPath' in terminal . creationOptions ? terminal . creationOptions . shellPath : vscode . env . shell ;
79
76
if ( ! shellPath ) {
80
77
return ;
81
78
}
82
79
80
+ const commandsInPath = await getCommandsInPath ( ) ;
83
81
const builtinCommands = getBuiltinCommands ( shellPath ) ;
84
- builtinCommands ?. forEach ( command => availableCommands . add ( command ) ) ;
82
+ if ( ! commandsInPath || ! builtinCommands ) {
83
+ return ;
84
+ }
85
+ const commands = [ ...commandsInPath , ...builtinCommands ] ;
85
86
87
+ const items : vscode . TerminalCompletionItem [ ] = [ ] ;
86
88
const prefix = getPrefix ( terminalContext . commandLine , terminalContext . cursorPosition ) ;
87
- let result : vscode . TerminalCompletionItem [ ] = [ ] ;
88
- const specs = [ codeCompletionSpec , codeInsidersCompletionSpec ] ;
89
- for ( const spec of specs ) {
90
- const specName = getLabel ( spec ) ;
91
- if ( ! specName || ! availableCommands . has ( specName ) ) {
92
- continue ;
93
- }
94
- if ( terminalContext . commandLine . startsWith ( specName ) ) {
95
- if ( 'options' in codeInsidersCompletionSpec && codeInsidersCompletionSpec . options ) {
96
- for ( const option of codeInsidersCompletionSpec . options ) {
97
- const optionLabel = getLabel ( option ) ;
98
- if ( ! optionLabel ) {
99
- continue ;
100
- }
101
89
102
- if ( optionLabel . startsWith ( prefix ) || ( prefix . length > specName . length && prefix . trim ( ) === specName ) ) {
103
- result . push ( createCompletionItem ( terminalContext . cursorPosition , prefix , optionLabel , option . description , false , vscode . TerminalCompletionItemKind . Flag ) ) ;
104
- }
105
- if ( option . args !== undefined ) {
106
- const args = Array . isArray ( option . args ) ? option . args : [ option . args ] ;
107
- for ( const arg of args ) {
108
- if ( ! arg ) {
109
- continue ;
110
- }
90
+ const specs = [ codeCompletionSpec , codeInsidersCompletionSpec ] ;
91
+ const specCompletions = await getCompletionItemsFromSpecs ( specs , terminalContext , new Set ( commands ) , prefix , token ) ;
111
92
112
- if ( arg . template ) {
113
- // TODO: return file/folder completion items
114
- if ( arg . template === 'filepaths' ) {
115
- // if (label.startsWith(prefix+\s*)) {
116
- // result.push(FilePathCompletionItem)
117
- // }
118
- } else if ( arg . template === 'folders' ) {
119
- // if (label.startsWith(prefix+\s*)) {
120
- // result.push(FolderPathCompletionItem)
121
- // }
122
- }
123
- continue ;
124
- }
93
+ let filesRequested = specCompletions . filesRequested ;
94
+ let foldersRequested = specCompletions . foldersRequested ;
95
+ items . push ( ...specCompletions . items ) ;
125
96
126
- const precedingText = terminalContext . commandLine . slice ( 0 , terminalContext . cursorPosition ) ;
127
- const expectedText = `${ optionLabel } ` ;
128
- if ( arg . suggestions ?. length && precedingText . includes ( expectedText ) ) {
129
- // there are specific suggestions to show
130
- result = [ ] ;
131
- const indexOfPrecedingText = terminalContext . commandLine . lastIndexOf ( expectedText ) ;
132
- const currentPrefix = precedingText . slice ( indexOfPrecedingText + expectedText . length ) ;
133
- for ( const suggestion of arg . suggestions ) {
134
- const suggestionLabel = getLabel ( suggestion ) ;
135
- if ( suggestionLabel && suggestionLabel . startsWith ( currentPrefix ) ) {
136
- const hasSpaceBeforeCursor = terminalContext . commandLine [ terminalContext . cursorPosition - 1 ] === ' ' ;
137
- // prefix will be '' if there is a space before the cursor
138
- result . push ( createCompletionItem ( terminalContext . cursorPosition , precedingText , suggestionLabel , arg . name , hasSpaceBeforeCursor , vscode . TerminalCompletionItemKind . Argument ) ) ;
139
- }
140
- }
141
- if ( result . length ) {
142
- return result ;
143
- }
144
- }
145
- }
146
- }
147
- }
97
+ if ( ! specCompletions . specificSuggestionsProvided ) {
98
+ for ( const command of commands ) {
99
+ if ( command . startsWith ( prefix ) ) {
100
+ items . push ( createCompletionItem ( terminalContext . cursorPosition , prefix , command ) ) ;
148
101
}
149
102
}
150
103
}
151
104
152
- for ( const command of availableCommands ) {
153
- if ( command . startsWith ( prefix ) ) {
154
- result . push ( createCompletionItem ( terminalContext . cursorPosition , prefix , command ) ) ;
155
- }
156
- }
157
-
158
105
if ( token . isCancellationRequested ) {
159
106
return undefined ;
160
107
}
108
+
161
109
const uniqueResults = new Map < string , vscode . TerminalCompletionItem > ( ) ;
162
- for ( const item of result ) {
110
+ for ( const item of items ) {
163
111
if ( ! uniqueResults . has ( item . label ) ) {
164
112
uniqueResults . set ( item . label , item ) ;
165
113
}
166
114
}
167
- return uniqueResults . size ? Array . from ( uniqueResults . values ( ) ) : undefined ;
115
+ const resultItems = uniqueResults . size ? Array . from ( uniqueResults . values ( ) ) : undefined ;
116
+
117
+ // If no completions are found, the prefix is a path, and neither files nor folders
118
+ // are going to be requested (for a specific spec's argument), show file/folder completions
119
+ const shouldShowResourceCompletions = ! resultItems ?. length && prefix . match ( / ^ [ . / \\ ] / ) && ! filesRequested && ! foldersRequested ;
120
+ if ( shouldShowResourceCompletions ) {
121
+ filesRequested = true ;
122
+ foldersRequested = true ;
123
+ }
124
+
125
+ if ( filesRequested || foldersRequested ) {
126
+ return new vscode . TerminalCompletionList ( resultItems , { filesRequested, foldersRequested, cwd : terminal . shellIntegration ?. cwd , pathSeparator : shellPath . includes ( '/' ) ? '/' : '\\' } ) ;
127
+ }
128
+ return resultItems ;
168
129
}
169
130
} ) ) ;
170
131
}
171
132
172
- function getLabel ( spec : Fig . Spec | Fig . Arg | Fig . Suggestion | string ) : string | undefined {
133
+ function getLabel ( spec : Fig . Spec | Fig . Arg | Fig . Suggestion | string ) : string [ ] | undefined {
173
134
if ( typeof spec === 'string' ) {
174
- return spec ;
135
+ return [ spec ] ;
175
136
}
176
137
if ( typeof spec . name === 'string' ) {
177
- return spec . name ;
138
+ return [ spec . name ] ;
178
139
}
179
140
if ( ! Array . isArray ( spec . name ) || spec . name . length === 0 ) {
180
141
return ;
181
142
}
182
- return spec . name [ 0 ] ;
143
+ return spec . name ;
183
144
}
184
145
185
146
function createCompletionItem ( cursorPosition : number , prefix : string , label : string , description ?: string , hasSpaceBeforeCursor ?: boolean , kind ?: vscode . TerminalCompletionItemKind ) : vscode . TerminalCompletionItem {
@@ -245,3 +206,89 @@ function getPrefix(commandLine: string, cursorPosition: number): string {
245
206
return match ? match [ 0 ] : '' ;
246
207
}
247
208
209
+ export function asArray < T > ( x : T | T [ ] ) : T [ ] ;
210
+ export function asArray < T > ( x : T | readonly T [ ] ) : readonly T [ ] ;
211
+ export function asArray < T > ( x : T | T [ ] ) : T [ ] {
212
+ return Array . isArray ( x ) ? x : [ x ] ;
213
+ }
214
+
215
+ function getCompletionItemsFromSpecs ( specs : Fig . Spec [ ] , terminalContext : { commandLine : string ; cursorPosition : number } , availableCommands : Set < string > , prefix : string , token : vscode . CancellationToken ) : { items : vscode . TerminalCompletionItem [ ] ; filesRequested : boolean ; foldersRequested : boolean ; specificSuggestionsProvided : boolean } {
216
+ let items : vscode . TerminalCompletionItem [ ] = [ ] ;
217
+ let filesRequested = false ;
218
+ let foldersRequested = false ;
219
+ for ( const spec of specs ) {
220
+ const specLabels = getLabel ( spec ) ;
221
+ if ( ! specLabels ) {
222
+ continue ;
223
+ }
224
+ for ( const specLabel of specLabels ) {
225
+ if ( ! availableCommands . has ( specLabel ) || token . isCancellationRequested ) {
226
+ continue ;
227
+ }
228
+ if ( terminalContext . commandLine . startsWith ( specLabel ) ) {
229
+ if ( 'options' in spec && spec . options ) {
230
+ for ( const option of spec . options ) {
231
+ const optionLabels = getLabel ( option ) ;
232
+ if ( ! optionLabels ) {
233
+ continue ;
234
+ }
235
+ for ( const optionLabel of optionLabels ) {
236
+ if ( optionLabel . startsWith ( prefix ) || ( prefix . length > specLabel . length && prefix . trim ( ) === specLabel ) ) {
237
+ items . push ( createCompletionItem ( terminalContext . cursorPosition , prefix , optionLabel , option . description , false , vscode . TerminalCompletionItemKind . Flag ) ) ;
238
+ }
239
+ if ( ! option . args ) {
240
+ continue ;
241
+ }
242
+ const args = asArray ( option . args ) ;
243
+ for ( const arg of args ) {
244
+ if ( ! arg ) {
245
+ continue ;
246
+ }
247
+ const precedingText = terminalContext . commandLine . slice ( 0 , terminalContext . cursorPosition + 1 ) ;
248
+ const expectedText = `${ specLabel } ${ optionLabel } ` ;
249
+ if ( ! precedingText . includes ( expectedText ) ) {
250
+ continue ;
251
+ }
252
+ if ( arg . template ) {
253
+ if ( arg . template === 'filepaths' ) {
254
+ if ( precedingText . includes ( expectedText ) ) {
255
+ filesRequested = true ;
256
+ }
257
+ } else if ( arg . template === 'folders' ) {
258
+ if ( precedingText . includes ( expectedText ) ) {
259
+ foldersRequested = true ;
260
+ }
261
+ }
262
+ }
263
+ if ( arg . suggestions ?. length ) {
264
+ // there are specific suggestions to show
265
+ items = [ ] ;
266
+ const indexOfPrecedingText = terminalContext . commandLine . lastIndexOf ( expectedText ) ;
267
+ const currentPrefix = precedingText . slice ( indexOfPrecedingText + expectedText . length ) ;
268
+ for ( const suggestion of arg . suggestions ) {
269
+ const suggestionLabels = getLabel ( suggestion ) ;
270
+ if ( ! suggestionLabels ) {
271
+ continue ;
272
+ }
273
+ for ( const suggestionLabel of suggestionLabels ) {
274
+ if ( suggestionLabel && suggestionLabel . startsWith ( currentPrefix . trim ( ) ) ) {
275
+ const hasSpaceBeforeCursor = terminalContext . commandLine [ terminalContext . cursorPosition - 1 ] === ' ' ;
276
+ // prefix will be '' if there is a space before the cursor
277
+ items . push ( createCompletionItem ( terminalContext . cursorPosition , precedingText , suggestionLabel , arg . name , hasSpaceBeforeCursor , vscode . TerminalCompletionItemKind . Argument ) ) ;
278
+ }
279
+ }
280
+ }
281
+ if ( items . length ) {
282
+ return { items, filesRequested, foldersRequested, specificSuggestionsProvided : true } ;
283
+ }
284
+ }
285
+ }
286
+ }
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+ return { items, filesRequested, foldersRequested, specificSuggestionsProvided : false } ;
293
+ }
294
+
0 commit comments