Skip to content
This repository was archived by the owner on Dec 17, 2024. It is now read-only.

Commit b41359e

Browse files
committed
update tab completion to prefill the longest common prefix of matches
Fixes #814
1 parent 4266ea2 commit b41359e

File tree

2 files changed

+99
-39
lines changed

2 files changed

+99
-39
lines changed

app/plugins/ui/commands/tab-completion.js

Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017 IBM Corporation
2+
* Copyright 2017-18 IBM Corporation
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,49 +20,54 @@ const fs = require('fs'),
2020
path = require('path'),
2121
expandHomeDir = require('expand-home-dir')
2222

23+
/**
24+
* Return partial updated with the given match; there may be some
25+
* overlap at the beginning.
26+
*
27+
*/
28+
const completeWith = (partial, match, addSpace=false) => {
29+
const partialIdx = match.indexOf(partial)
30+
return (partialIdx >= 0 ? match.substring(partialIdx + partial.length) : match) + (addSpace ? ' ' : '')
31+
}
32+
2333
/**
2434
* We've found a match. Add this match to the given partial match,
2535
* located in the given dirname'd directory, and update the given
2636
* prompt, which is an <input>.
27-
*
28-
*/
37+
*
38+
*/
2939
const complete = (match, prompt, { temporaryContainer, partial=temporaryContainer.partial, dirname=temporaryContainer.dirname, addSpace=false }) => {
3040
debug('completion', match, partial, dirname)
3141

3242
// in case match includes partial as a prefix
33-
const partialIdx = match.indexOf(partial),
34-
completion = (partialIdx >= 0 ? match.substring(partialIdx + partial.length) : match) + (addSpace ? ' ' : '')
43+
const completion = completeWith(partial, match, addSpace)
3544

3645
if (temporaryContainer) {
3746
temporaryContainer.cleanup()
3847
}
3948

40-
if (completion) {
41-
if (dirname) {
42-
// see if we need to add a trailing slash
43-
fs.lstat(expandHomeDir(path.join(dirname, match)), (err, stats) => {
44-
if (!err) {
45-
if (stats.isDirectory()) {
46-
// add a trailing slash if the dirname/match is a directory
47-
debug('complete as directory')
48-
prompt.value = prompt.value + completion + '/'
49-
} else {
50-
// otherwise, dirname/match is not a directory
51-
debug('complete as scalar')
52-
prompt.value = prompt.value + completion
53-
}
49+
if (dirname) {
50+
// see if we need to add a trailing slash
51+
fs.lstat(expandHomeDir(path.join(dirname, match)), (err, stats) => {
52+
if (!err) {
53+
if (stats.isDirectory()) {
54+
// add a trailing slash if the dirname/match is a directory
55+
debug('complete as directory')
56+
prompt.value = prompt.value + completion + '/'
5457
} else {
55-
console.error(err)
58+
// otherwise, dirname/match is not a directory
59+
debug('complete as scalar')
60+
prompt.value = prompt.value + completion
5661
}
57-
})
62+
} else {
63+
console.error(err)
64+
}
65+
})
5866

59-
} else {
60-
// otherwise, just add the completion to the prompt
61-
debug('complete as scalar (alt)')
62-
prompt.value = prompt.value + completion
63-
}
6467
} else {
65-
debug('no completion string')
68+
// otherwise, just add the completion to the prompt
69+
debug('complete as scalar (alt)')
70+
prompt.value = prompt.value + completion
6671
}
6772
}
6873

@@ -80,6 +85,44 @@ const installKeyHandlers = prompt => {
8085
}
8186
}
8287

88+
/**
89+
* Given a list of matches to the partial that is in the
90+
* prompt.value, update prompt.value so that it contains the longest
91+
* common prefix of the matches
92+
*
93+
*/
94+
const updateReplToReflectLongestPrefix = (prompt, matches, temporaryContainer, partial=temporaryContainer.partial) => {
95+
if (matches.length > 0) {
96+
const shortest = matches.reduce((minLength, match) => !minLength ? match.length : Math.min(minLength, match.length), false)
97+
let idx = 0
98+
99+
const partialComplete = idx => {
100+
const completion = completeWith(partial, matches[0].substring(0, idx))
101+
temporaryContainer.partial = temporaryContainer.partial + completion
102+
prompt.value = prompt.value + completion
103+
}
104+
105+
for (idx = 0; idx < shortest; idx++) {
106+
const char = matches[0].charAt(idx)
107+
108+
for (let jdx = 1; jdx < matches.length; jdx++) {
109+
const other = matches[jdx].charAt(idx)
110+
if (char != other) {
111+
if (idx > 0) {
112+
// then we found some common prefix
113+
return partialComplete(idx)
114+
}
115+
}
116+
}
117+
}
118+
119+
if (idx > 0) {
120+
partialComplete(idx)
121+
}
122+
}
123+
}
124+
125+
83126
/**
84127
* Install keyboard up-arrow and down-arrow handlers in the given REPL
85128
* prompt. This needs to be installed in the prompt, as ui.js installs
@@ -247,7 +290,7 @@ const makeCompletionContainer = (block, prompt, partial, dirname, lastIdx) => {
247290
* Add a suggestion to the suggestion container
248291
*
249292
*/
250-
const addSuggestion = (temporaryContainer, partial, dirname, prompt) => (match, idx) => {
293+
const addSuggestion = (temporaryContainer, dirname, prompt) => (match, idx) => {
251294
const matchLabel = match.label || match,
252295
matchCompletion = match.completion || matchLabel
253296

@@ -281,7 +324,7 @@ const addSuggestion = (temporaryContainer, partial, dirname, prompt) => (match,
281324

282325
// onclick, use this match as the completion
283326
option.addEventListener('click', () => {
284-
complete(matchCompletion, prompt, { temporaryContainer, partial, dirname, addSpace: match.addSpace })
327+
complete(matchCompletion, prompt, { temporaryContainer, dirname, addSpace: match.addSpace })
285328
})
286329

287330
option.setAttribute('data-match', matchLabel)
@@ -341,9 +384,11 @@ const suggestLocalFile = (last, block, prompt, temporaryContainer, lastIdx) => {
341384
temporaryContainer = makeCompletionContainer(block, prompt, partial, dirname, lastIdx)
342385
}
343386

387+
updateReplToReflectLongestPrefix(prompt, matches, temporaryContainer)
388+
344389
// add each match to that temporary div
345390
matches.forEach((match, idx) => {
346-
const { option, optionInner } = addSuggestion(temporaryContainer, partial, dirname, prompt)(match, idx)
391+
const { option, optionInner } = addSuggestion(temporaryContainer, dirname, prompt)(match, idx)
347392

348393
// see if the match is a directory, so that we add a trailing slash
349394
fs.lstat(expandHomeDir(path.join(dirname, match)), (err, stats) => {
@@ -398,7 +443,9 @@ const filterAndPresentEntitySuggestions = (last, block, prompt, temporaryContain
398443
temporaryContainer = makeCompletionContainer(block, prompt, partial, dirname, lastIdx)
399444
}
400445

401-
filteredList.forEach(addSuggestion(temporaryContainer, partial, dirname, prompt))
446+
updateReplToReflectLongestPrefix(prompt, filteredList, temporaryContainer)
447+
448+
filteredList.forEach(addSuggestion(temporaryContainer, dirname, prompt))
402449
}
403450
}
404451

@@ -429,7 +476,7 @@ const suggestCommandCompletions = (matches, partial, block, prompt, temporaryCon
429476
}
430477

431478
// add suggestions to the container
432-
matches.forEach(addSuggestion(temporaryContainer, partial, undefined, prompt))
479+
matches.forEach(addSuggestion(temporaryContainer, undefined, prompt))
433480
}
434481
}
435482

tests/tests/passes/02/tab-completion.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,18 @@ describe('Tab completion', function() {
5252
})
5353
.catch(common.oops(this));
5454

55-
const tabbyWithOptions = (app, partial, expected, full, { click, nTabs, expectOK=true, iter=0 }={}) => {
55+
const tabbyWithOptions = (app, partial, expected, full, { click, nTabs, expectOK=true, iter=0, expectedPromptAfterTab }={}) => {
5656
return app.client.waitForExist(ui.selectors.CURRENT_PROMPT_BLOCK)
5757
.then(() => app.client.getAttribute(ui.selectors.CURRENT_PROMPT_BLOCK, 'data-input-count'))
5858
.then(count => parseInt(count))
5959
.then(count => app.client.setValue(ui.selectors.CURRENT_PROMPT, partial)
6060
.then(() => app.client.waitForValue(ui.selectors.PROMPT_N(count), partial))
6161
.then(() => app.client.setValue(ui.selectors.CURRENT_PROMPT, `${partial}${keys.TAB}`))
62+
.then(() => {
63+
if (expectedPromptAfterTab) {
64+
return app.client.waitForValue(ui.selectors.PROMPT_N(count), expectedPromptAfterTab)
65+
}
66+
})
6267
.then(() => {
6368
if (!expected) {
6469
// then we expect non-visibility of the tab-completion popup
@@ -123,22 +128,29 @@ describe('Tab completion', function() {
123128

124129
it('should have an active repl', () => cli.waitForRepl(this.app))
125130

131+
const options = ['commandFile.wsk',
132+
'composer-source/',
133+
'composer-source-expect-errors/',
134+
'composer-wookiechat/']
135+
126136
// tab completion with options, then click on the second (idx=1) entry of the expected cmpletion list
127137
it('should tab complete local file path with options', () => tabbyWithOptions(this.app,
128138
'lls data/com',
129139
options,
130140
'lls data/composer-source/',
131141
{ click: 1 }))
132142

143+
it('should tab complete local file path with options, expect prompt update', () => tabbyWithOptions(this.app,
144+
'lls data/comp',
145+
options.slice(1), // except the first
146+
'lls data/composer-source/',
147+
{ click: 1,
148+
expectedPromptAfterTab: 'lls data/composer-'}))
149+
133150
it('should tab complete the data directory', () => tabby(this.app, 'lls da', 'lls data/'))
134151
it('should tab complete the data/fsm.js file', () => tabby(this.app, 'lls data/fsm.js', 'lls data/fsm.json'))
135152
it('should tab complete the ../app directory', () => tabby(this.app, 'lls ../ap', 'lls ../app/'))
136153

137-
const options = ['commandFile.wsk',
138-
'composer-source/',
139-
'composer-source-expect-errors/',
140-
'composer-wookiechat/']
141-
142154
// same, but this time tab to cycle through the options
143155
it('should tab complete local file path', () => tabbyWithOptions(this.app,
144156
'lls data/com',
@@ -174,7 +186,8 @@ describe('Tab completion', function() {
174186
it('should tab complete action foo2 with options', () => tabbyWithOptions(this.app, 'action get f',
175187
['foofoo/yum', 'foo2', 'foo'],
176188
'action get foo2',
177-
{ click: 1 })
189+
{ click: 1,
190+
expectedPromptAfterTab: 'action get foo' })
178191
.then(sidecar.expectOpen)
179192
.then(sidecar.expectShowing('foo2'))
180193
.catch(common.oops(this)))

0 commit comments

Comments
 (0)