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

Commit df4ee2f

Browse files
committed
fix: port filesystem tab completion to tab completion API
Fixes #3446 in addition to cleaning up some old and bad code, this fixes a few issues Fixes #2403 Fixes #3447
1 parent 7eec254 commit df4ee2f

File tree

7 files changed

+204
-141
lines changed

7 files changed

+204
-141
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2017-20 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { readdir } from 'fs'
18+
import { basename, join, dirname as pathDirname } from 'path'
19+
20+
import { Tab, Arguments, CommandLine, Registrar, expandHomeDir } from '@kui-shell/core'
21+
22+
import {
23+
shellescape,
24+
isDirectory,
25+
registerTabCompletionEnumerator,
26+
TabCompletionSpec,
27+
CompletionResponse
28+
} from '@kui-shell/plugin-core-support/tab-completion'
29+
30+
/**
31+
* Tab completion handler for local files
32+
*
33+
*/
34+
async function completeLocalFiles(
35+
tab: Tab,
36+
commandLine: CommandLine,
37+
{ toBeCompleted }: TabCompletionSpec
38+
): Promise<CompletionResponse[]> {
39+
return (await tab.REPL.rexec<CompletionResponse[]>(`fscomplete ${toBeCompleted}`)).content
40+
}
41+
42+
function doComplete(args: Arguments) {
43+
const last = args.command.substring(args.command.indexOf('fscomplete ') + 'fscomplete '.length)
44+
45+
// dirname will "foo" in the above example; it
46+
// could also be that last is itself the name
47+
// of a directory
48+
const lastIsDir = last.charAt(last.length - 1) === '/'
49+
const dirname = lastIsDir ? last : pathDirname(last)
50+
51+
// debug('suggest local file', dirname, last)
52+
53+
if (dirname) {
54+
// then dirname exists! now scan the directory so we can find matches
55+
return new Promise((resolve, reject) => {
56+
const dirToScan = expandHomeDir(dirname)
57+
readdir(dirToScan, async (err, files) => {
58+
if (err) {
59+
console.error('fs.readdir error', err)
60+
reject(err)
61+
} else {
62+
const partial = shellescape(basename(last) + (lastIsDir ? '/' : ''))
63+
const partialHasADot = partial.startsWith('.')
64+
65+
const matches: string[] = files.filter(_f => {
66+
const f = shellescape(_f)
67+
68+
// exclude dot files from tab completion, also emacs ~ temp files just for fun
69+
return (
70+
(lastIsDir || f.indexOf(partial) === 0) &&
71+
!f.endsWith('~') &&
72+
f !== '.' &&
73+
f !== '..' &&
74+
(partialHasADot || !f.startsWith('.'))
75+
)
76+
})
77+
78+
// add a trailing slash to any matched directory names
79+
resolve({
80+
mode: 'raw',
81+
content: await Promise.all(
82+
matches.map(async match => {
83+
const completion = lastIsDir ? match : match.substring(partial.length)
84+
85+
if (await isDirectory(join(dirToScan, match))) {
86+
return `${completion}/`
87+
} else {
88+
return { completion, addSpace: true }
89+
}
90+
})
91+
)
92+
})
93+
}
94+
})
95+
})
96+
}
97+
}
98+
99+
/**
100+
* Entry point to register tab completion handlers
101+
*
102+
*/
103+
export function preload() {
104+
registerTabCompletionEnumerator(completeLocalFiles)
105+
}
106+
107+
export function plugin(registrar: Registrar) {
108+
registrar.listen('/fscomplete', doComplete, { hidden: true, requiresLocal: true })
109+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,18 @@ import ls from './lib/ls'
1818
import glob from './lib/glob'
1919
import open from './lib/open'
2020
import fstat from './lib/fstat'
21+
import { plugin as tabCompletion } from './lib/tab-completion'
2122

2223
import { Registrar } from '@kui-shell/core'
2324

2425
/**
2526
* This is the module
2627
*
2728
*/
28-
export default (registrar: Registrar) => {
29+
export default async (registrar: Registrar) => {
2930
ls(registrar)
30-
glob(registrar)
3131
open(registrar)
3232
fstat(registrar)
33+
glob(registrar)
34+
tabCompletion(registrar)
3335
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright 2020 IBM Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { isHeadless } from '@kui-shell/core'
18+
19+
export default async () => {
20+
if (!isHeadless()) {
21+
import('./lib/tab-completion').then(_ => _.preload())
22+
}
23+
}

plugins/plugin-core-support/src/test/core-support2/tab-completion.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ describe('Tab completion core', function(this: Common.ISuite) {
3030
before(Common.before(this))
3131
after(Common.after(this))
3232

33-
const options = ['core_empty.js', 'core_single_entry_directory/', 'core_test_directory_1/']
33+
const options = ['core_empty.js', 'core_single_entry_directory/', 'core_test_directory_1/'].map(_ =>
34+
join(ROOT, 'data/core', _)
35+
)
3436

35-
const fileOptions = ['empty1.js', 'empty2.js']
37+
const fileOptions = ['empty1.js', 'empty2.js'].map(_ => join(ROOT, 'data/core/core_test_directory_1', _))
3638

3739
const tmp1 = tmpDirSync()
3840
touch(join(tmp1.name, 'foo bar'))
@@ -53,12 +55,12 @@ describe('Tab completion core', function(this: Common.ISuite) {
5355
return tabby(this.app, `ls ${join(tmp1.name, 'foo\\ ')}`, `ls ${join(tmp1.name, 'foo bar')}`)
5456
})
5557

56-
Common.localIt('should tab complete file with spaces non-unique', () => {
58+
Common.localIt('should tab complete file with spaces non-unique yoyoyoyo', () => {
5759
return tabbyWithOptions(
5860
this.app,
5961
`ls ${join(tmp2.name, 'foo')}`,
60-
['foo bar1', 'foo bar2'],
61-
`ls ${join(tmp2.name, 'foo bar1')}`,
62+
[join(tmp2.name, 'foo bar1'), join(tmp2.name, 'foo bar2')],
63+
`ls ${join(tmp2.name, 'foo\\ bar1')}`,
6264
{ click: 0 }
6365
)
6466
})
@@ -67,8 +69,8 @@ describe('Tab completion core', function(this: Common.ISuite) {
6769
return tabbyWithOptions(
6870
this.app,
6971
`ls -l ${join(tmp2.name, 'foo')}`,
70-
['foo bar1', 'foo bar2'],
71-
`ls -l ${join(tmp2.name, 'foo bar1')}`,
72+
[join(tmp2.name, 'foo bar1'), join(tmp2.name, 'foo bar2')],
73+
`ls -l ${join(tmp2.name, 'foo\\ bar1')}`,
7274
{ click: 0 }
7375
)
7476
})
@@ -77,8 +79,8 @@ describe('Tab completion core', function(this: Common.ISuite) {
7779
return tabbyWithOptions(
7880
this.app,
7981
`ls ${join(tmp2.name, 'foo\\ ')}`,
80-
['foo bar1', 'foo bar2'],
81-
`ls ${join(tmp2.name, 'foo bar2')}`,
82+
[join(tmp2.name, 'foo\\ bar1'), join(tmp2.name, 'foo\\ bar2')],
83+
`ls ${join(tmp2.name, 'foo\\ bar2')}`,
8284
{ click: 1 }
8385
)
8486
})
@@ -107,7 +109,7 @@ describe('Tab completion core', function(this: Common.ISuite) {
107109
)
108110

109111
// tab completion with options, then click on the second (idx=1) entry of the expected cmpletion list
110-
Common.localIt('should tab complete local file path with options', () =>
112+
Common.localIt('should tab complete local file path with options then click on second', () =>
111113
tabbyWithOptions(this.app, `lls ${ROOT}/data/core/core_`, options, `lls ${ROOT}/data/core/core_single_entry_dir/`, {
112114
click: 1
113115
})
@@ -121,7 +123,7 @@ describe('Tab completion core', function(this: Common.ISuite) {
121123
)
122124

123125
// tab completion with file options, then click on the first (idx=0) entry of the expected cmpletion list
124-
Common.localIt('should tab complete local file path with options', () =>
126+
Common.localIt('should tab complete local file path with options then click on first', () =>
125127
tabbyWithOptions(
126128
this.app,
127129
`lls ${ROOT}/data/core/core_test_directory_1/em`,

0 commit comments

Comments
 (0)