Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit fc3a311

Browse files
committed
feat(plugins/plugin-bash-like): tab completion for s3 buckets
Fixes #5326
1 parent 7cce673 commit fc3a311

File tree

2 files changed

+51
-86
lines changed

2 files changed

+51
-86
lines changed

plugins/plugin-bash-like/fs/src/lib/glob.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ export interface GlobStats extends BaseStats {
6262
}
6363

6464
export interface KuiGlobOptions extends ParsedOptions {
65-
a: boolean
66-
all: boolean
67-
d: boolean
68-
l: boolean
69-
C: boolean
65+
a?: boolean
66+
all?: boolean
67+
d?: boolean
68+
l?: boolean
69+
C?: boolean
7070
}
7171

7272
function formatPermissions(stats: PartialStats, isFile: boolean, isDirectory: boolean, isSymbolicLink: boolean) {

plugins/plugin-bash-like/fs/src/lib/tab-completion.ts

Lines changed: 46 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { lstat, realpath, readdir } from 'fs'
18-
import { basename, join, dirname as pathDirname } from 'path'
17+
import { basename, dirname as pathDirname } from 'path'
1918

2019
import {
2120
Tab,
@@ -28,40 +27,45 @@ import {
2827
CompletionResponse
2928
} from '@kui-shell/core'
3029

31-
/**
32-
* Is the given filepath a directory?
33-
*
34-
*/
35-
const isDirectory = (filepath: string): Promise<boolean> =>
36-
new Promise<boolean>((resolve, reject) => {
37-
lstat(filepath, (err, stats) => {
38-
if (err) {
39-
reject(err)
40-
} else {
41-
if (stats.isSymbolicLink()) {
42-
// debug('following symlink')
43-
// TODO: consider turning these into the better async calls?
44-
return realpath(filepath, (err, realpath) => {
45-
if (err) {
46-
reject(err)
47-
} else {
48-
isDirectory(realpath)
49-
.then(resolve)
50-
.catch(reject)
51-
}
52-
})
53-
}
30+
import { ls } from '../vfs/delegates'
31+
import { GlobStats } from '../lib/glob'
5432

55-
resolve(stats.isDirectory())
33+
function findMatchingFilesFrom(files: GlobStats[], dirToScan: string, last: string, lastIsDir: boolean) {
34+
const partial = basename(last) + (lastIsDir ? '/' : '')
35+
const partialHasADot = partial.startsWith('.')
36+
37+
const matches = files.filter(_f => {
38+
const f = _f.name
39+
40+
// exclude dot files from tab completion, also emacs ~ temp files just for fun
41+
return (
42+
(lastIsDir || f.indexOf(partial) === 0) &&
43+
!f.endsWith('~') &&
44+
f !== '.' &&
45+
f !== '..' &&
46+
(partialHasADot || !f.startsWith('.'))
47+
)
48+
})
49+
50+
// add a trailing slash to any matched directory names
51+
const lastHasPath = /\//.test(last)
52+
return {
53+
mode: 'raw',
54+
content: matches.map(matchStats => {
55+
const match = matchStats.nameForDisplay
56+
const completion = lastIsDir ? match : match.substring(partial.length)
57+
58+
// show a special label only if we have a dirname prefix
59+
const label = lastHasPath ? basename(match) : undefined
60+
61+
if (matchStats.dirent.isDirectory) {
62+
return { completion: `${completion}/`, label: label ? `${label}/` : undefined }
63+
} else {
64+
return { completion, addSpace: true, label }
5665
}
5766
})
58-
}).catch(err => {
59-
if (err.code === 'ENOENT') {
60-
return false
61-
} else {
62-
throw err
63-
}
64-
})
67+
}
68+
}
6569

6670
/**
6771
* Tab completion handler for local files
@@ -75,7 +79,7 @@ async function completeLocalFiles(
7579
return (await tab.REPL.rexec<CompletionResponse[]>(`fscomplete -- "${toBeCompleted}"`)).content
7680
}
7781

78-
function doComplete(args: Arguments) {
82+
async function doComplete(args: Arguments) {
7983
const last = args.command.substring(args.command.indexOf('-- ') + '-- '.length).replace(/^"(.*)"$/, '$1')
8084

8185
// dirname will "foo" in the above example; it
@@ -84,55 +88,16 @@ function doComplete(args: Arguments) {
8488
const lastIsDir = last.charAt(last.length - 1) === '/'
8589
const dirname = lastIsDir ? last : pathDirname(last)
8690

87-
// debug('suggest local file', dirname, last)
88-
8991
if (dirname) {
90-
// then dirname exists! now scan the directory so we can find matches
91-
return new Promise((resolve, reject) => {
92+
try {
93+
// Note: by passing a: true, we effect an `ls -a`, which will give us dot files
9294
const dirToScan = expandHomeDir(dirname)
93-
readdir(dirToScan, async (err, files) => {
94-
if (err) {
95-
console.error('fs.readdir error', err)
96-
reject(err)
97-
} else {
98-
const partial = basename(last) + (lastIsDir ? '/' : '')
99-
const partialHasADot = partial.startsWith('.')
100-
101-
const matches: string[] = files.filter(_f => {
102-
const f = _f
103-
104-
// exclude dot files from tab completion, also emacs ~ temp files just for fun
105-
return (
106-
(lastIsDir || f.indexOf(partial) === 0) &&
107-
!f.endsWith('~') &&
108-
f !== '.' &&
109-
f !== '..' &&
110-
(partialHasADot || !f.startsWith('.'))
111-
)
112-
})
113-
114-
// add a trailing slash to any matched directory names
115-
const lastHasPath = /\//.test(last)
116-
resolve({
117-
mode: 'raw',
118-
content: await Promise.all(
119-
matches.map(async match => {
120-
const completion = lastIsDir ? match : match.substring(partial.length)
121-
122-
// show a special label only if we have a dirname prefix
123-
const label = lastHasPath ? basename(match) : undefined
124-
125-
if (await isDirectory(join(dirToScan, match))) {
126-
return { completion: `${completion}/`, label: label ? `${label}/` : undefined }
127-
} else {
128-
return { completion, addSpace: true, label }
129-
}
130-
})
131-
)
132-
})
133-
}
134-
})
135-
})
95+
const fileList = await ls({ tab: args.tab, REPL: args.REPL, parsedOptions: { a: true } }, [dirToScan])
96+
return findMatchingFilesFrom(fileList, dirToScan, last, lastIsDir)
97+
} catch (err) {
98+
console.error('tab completion vfs.ls error', err)
99+
throw err
100+
}
136101
}
137102
}
138103

0 commit comments

Comments
 (0)