@@ -15,8 +15,8 @@ import { isExecutable } from './helpers/executable';
15
15
16
16
const isWindows = osIsWindows ( ) ;
17
17
let cachedAvailableCommandsPath : string | undefined ;
18
- let cachedAvailableCommands : Set < string > | undefined ;
19
- const cachedBuiltinCommands : Map < string , string [ ] | undefined > = new Map ( ) ;
18
+ let cachedAvailableCommands : Set < ICompletionResource > | undefined ;
19
+ const cachedBuiltinCommands : Map < string , ICompletionResource [ ] | undefined > = new Map ( ) ;
20
20
21
21
export const availableSpecs : Fig . Spec [ ] = [
22
22
cdSpec ,
@@ -27,14 +27,14 @@ for (const spec of upstreamSpecs) {
27
27
availableSpecs . push ( require ( `./completions/upstream/${ spec } ` ) . default ) ;
28
28
}
29
29
30
- function getBuiltinCommands ( shell : string ) : string [ ] | undefined {
30
+ function getBuiltinCommands ( shell : string , existingCommands ?: Set < string > ) : ICompletionResource [ ] | undefined {
31
31
try {
32
32
const shellType = path . basename ( shell , path . extname ( shell ) ) ;
33
33
const cachedCommands = cachedBuiltinCommands . get ( shellType ) ;
34
34
if ( cachedCommands ) {
35
35
return cachedCommands ;
36
36
}
37
- const filter = ( cmd : string ) => cmd ;
37
+ const filter = ( cmd : string ) => cmd && ! existingCommands ?. has ( cmd ) ;
38
38
const options : ExecOptionsWithStringEncoding = { encoding : 'utf-8' , shell } ;
39
39
let commands : string [ ] | undefined ;
40
40
switch ( shellType ) {
@@ -68,8 +68,10 @@ function getBuiltinCommands(shell: string): string[] | undefined {
68
68
break ;
69
69
}
70
70
}
71
- cachedBuiltinCommands . set ( shellType , commands ) ;
72
- return commands ;
71
+
72
+ const commandResources = commands ?. map ( command => ( { label : command } ) ) ;
73
+ cachedBuiltinCommands . set ( shellType , commandResources ) ;
74
+ return commandResources ;
73
75
74
76
} catch ( error ) {
75
77
console . error ( 'Error fetching builtin commands:' , error ) ;
@@ -92,11 +94,11 @@ export async function activate(context: vscode.ExtensionContext) {
92
94
}
93
95
94
96
const commandsInPath = await getCommandsInPath ( terminal . shellIntegration ?. env ) ;
95
- const builtinCommands = getBuiltinCommands ( shellPath ) ;
96
- if ( ! commandsInPath || ! builtinCommands ) {
97
+ const builtinCommands = getBuiltinCommands ( shellPath , commandsInPath ?. labels ) ?? [ ] ;
98
+ if ( ! commandsInPath ?. completionResources ) {
97
99
return ;
98
100
}
99
- const commands = [ ...commandsInPath , ...builtinCommands ] ;
101
+ const commands = [ ...commandsInPath . completionResources , ...builtinCommands ] ;
100
102
101
103
const prefix = getPrefix ( terminalContext . commandLine , terminalContext . cursorPosition ) ;
102
104
@@ -169,20 +171,24 @@ function getLabel(spec: Fig.Spec | Fig.Arg | Fig.Suggestion | string): string[]
169
171
return spec . name ;
170
172
}
171
173
172
- function createCompletionItem ( cursorPosition : number , prefix : string , label : string , description ?: string , kind ?: vscode . TerminalCompletionItemKind ) : vscode . TerminalCompletionItem {
174
+ function createCompletionItem ( cursorPosition : number , prefix : string , commandResource : ICompletionResource , description ?: string , kind ?: vscode . TerminalCompletionItemKind ) : vscode . TerminalCompletionItem {
173
175
const endsWithSpace = prefix . endsWith ( ' ' ) ;
174
176
const lastWord = endsWithSpace ? '' : prefix . split ( ' ' ) . at ( - 1 ) ?? '' ;
175
177
return {
176
- label,
177
- detail : description ?? '' ,
178
+ label : commandResource . label ,
179
+ detail : description ?? commandResource . path ?? '' ,
178
180
replacementIndex : cursorPosition - lastWord . length ,
179
181
replacementLength : lastWord . length ,
180
182
kind : kind ?? vscode . TerminalCompletionItemKind . Method
181
183
} ;
182
184
}
183
185
184
- async function getCommandsInPath ( env : { [ key : string ] : string | undefined } = process . env ) : Promise < Set < string > | undefined > {
185
- // Get PATH value
186
+ interface ICompletionResource {
187
+ label : string ;
188
+ path ?: string ;
189
+ }
190
+ async function getCommandsInPath ( env : { [ key : string ] : string | undefined } = process . env ) : Promise < { completionResources : Set < ICompletionResource > | undefined ; labels : Set < string > | undefined } | undefined > {
191
+ const labels : Set < string > = new Set < string > ( ) ;
186
192
let pathValue : string | undefined ;
187
193
if ( isWindows ) {
188
194
const caseSensitivePathKey = Object . keys ( env ) . find ( key => key . toLowerCase ( ) === 'path' ) ;
@@ -198,24 +204,26 @@ async function getCommandsInPath(env: { [key: string]: string | undefined } = pr
198
204
199
205
// Check cache
200
206
if ( cachedAvailableCommands && cachedAvailableCommandsPath === pathValue ) {
201
- return cachedAvailableCommands ;
207
+ return { completionResources : cachedAvailableCommands , labels } ;
202
208
}
203
209
204
210
// Extract executables from PATH
205
211
const paths = pathValue . split ( isWindows ? ';' : ':' ) ;
206
212
const pathSeparator = isWindows ? '\\' : '/' ;
207
- const executables = new Set < string > ( ) ;
213
+ const executables = new Set < ICompletionResource > ( ) ;
208
214
for ( const path of paths ) {
209
215
try {
210
216
const dirExists = await fs . stat ( path ) . then ( stat => stat . isDirectory ( ) ) . catch ( ( ) => false ) ;
211
217
if ( ! dirExists ) {
212
218
continue ;
213
219
}
214
- const files = await vscode . workspace . fs . readDirectory ( vscode . Uri . file ( path ) ) ;
215
-
220
+ const fileResource = vscode . Uri . file ( path ) ;
221
+ const files = await vscode . workspace . fs . readDirectory ( fileResource ) ;
216
222
for ( const [ file , fileType ] of files ) {
217
- if ( fileType !== vscode . FileType . Unknown && fileType !== vscode . FileType . Directory && await isExecutable ( path + pathSeparator + file ) ) {
218
- executables . add ( file ) ;
223
+ const formattedPath = getFriendlyFilePath ( vscode . Uri . joinPath ( fileResource , file ) , pathSeparator ) ;
224
+ if ( ! labels . has ( file ) && fileType !== vscode . FileType . Unknown && fileType !== vscode . FileType . Directory && await isExecutable ( formattedPath ) ) {
225
+ executables . add ( { label : file , path : formattedPath } ) ;
226
+ labels . add ( file ) ;
219
227
}
220
228
}
221
229
} catch ( e ) {
@@ -224,7 +232,7 @@ async function getCommandsInPath(env: { [key: string]: string | undefined } = pr
224
232
}
225
233
}
226
234
cachedAvailableCommands = executables ;
227
- return executables ;
235
+ return { completionResources : executables , labels } ;
228
236
}
229
237
230
238
function getPrefix ( commandLine : string , cursorPosition : number ) : string {
@@ -257,7 +265,7 @@ export function asArray<T>(x: T | T[]): T[] {
257
265
export async function getCompletionItemsFromSpecs (
258
266
specs : Fig . Spec [ ] ,
259
267
terminalContext : { commandLine : string ; cursorPosition : number } ,
260
- availableCommands : string [ ] ,
268
+ availableCommands : ICompletionResource [ ] ,
261
269
prefix : string ,
262
270
shellIntegrationCwd ?: vscode . Uri ,
263
271
token ?: vscode . CancellationToken
@@ -277,7 +285,7 @@ export async function getCompletionItemsFromSpecs(
277
285
}
278
286
279
287
for ( const specLabel of specLabels ) {
280
- if ( ! availableCommands . includes ( specLabel ) || ( token && token . isCancellationRequested ) ) {
288
+ if ( ! availableCommands . find ( command => command . label === specLabel ) || ( token && token . isCancellationRequested ) ) {
281
289
continue ;
282
290
}
283
291
@@ -288,7 +296,7 @@ export async function getCompletionItemsFromSpecs(
288
296
|| ! ! firstCommand && specLabel . startsWith ( firstCommand )
289
297
) {
290
298
// push it to the completion items
291
- items . push ( createCompletionItem ( terminalContext . cursorPosition , prefix , specLabel ) ) ;
299
+ items . push ( createCompletionItem ( terminalContext . cursorPosition , prefix , { label : specLabel } ) ) ;
292
300
}
293
301
294
302
if ( ! terminalContext . commandLine . startsWith ( specLabel ) ) {
@@ -323,7 +331,7 @@ export async function getCompletionItemsFromSpecs(
323
331
// Include builitin/available commands in the results
324
332
const labels = new Set ( items . map ( ( i ) => i . label ) ) ;
325
333
for ( const command of availableCommands ) {
326
- if ( ! labels . has ( command ) ) {
334
+ if ( ! labels . has ( command . label ) ) {
327
335
items . push ( createCompletionItem ( terminalContext . cursorPosition , prefix , command ) ) ;
328
336
}
329
337
}
@@ -393,7 +401,7 @@ function handleOptions(specLabel: string, spec: Fig.Spec, terminalContext: { com
393
401
createCompletionItem (
394
402
terminalContext . cursorPosition ,
395
403
prefix ,
396
- optionLabel ,
404
+ { label : optionLabel } ,
397
405
option . description ,
398
406
vscode . TerminalCompletionItemKind . Flag
399
407
)
@@ -455,7 +463,7 @@ function getCompletionItemsFromArgs(args: Fig.SingleOrArray<Fig.Arg> | undefined
455
463
}
456
464
if ( suggestionLabel && suggestionLabel . startsWith ( currentPrefix . trim ( ) ) ) {
457
465
const description = typeof suggestion !== 'string' ? suggestion . description : '' ;
458
- items . push ( createCompletionItem ( terminalContext . cursorPosition , wordBefore ?? '' , suggestionLabel , description , vscode . TerminalCompletionItemKind . Argument ) ) ;
466
+ items . push ( createCompletionItem ( terminalContext . cursorPosition , wordBefore ?? '' , { label : suggestionLabel } , description , vscode . TerminalCompletionItemKind . Argument ) ) ;
459
467
}
460
468
}
461
469
}
@@ -479,3 +487,12 @@ function getFirstCommand(commandLine: string): string | undefined {
479
487
}
480
488
return firstCommand ;
481
489
}
490
+
491
+ function getFriendlyFilePath ( uri : vscode . Uri , pathSeparator : string ) : string {
492
+ let path = uri . fsPath ;
493
+ // Ensure drive is capitalized on Windows
494
+ if ( pathSeparator === '\\' && path . match ( / ^ [ a - z A - Z ] : \\ / ) ) {
495
+ path = `${ path [ 0 ] . toUpperCase ( ) } :${ path . slice ( 2 ) } ` ;
496
+ }
497
+ return path ;
498
+ }
0 commit comments