Skip to content

Commit

Permalink
repl: make autocomplete case-insensitive
Browse files Browse the repository at this point in the history
This changes autocomplete suggestion filter to ignore input case
allowing for more autosuggest results shown on the screen

Fixes: #41631

PR-URL: #41632
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
  • Loading branch information
gribnoysup authored and danielleadams committed Jun 12, 2022
1 parent 357bdca commit 0c9d3be
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 64 deletions.
16 changes: 11 additions & 5 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const {
StringPrototypeStartsWith,
StringPrototypeTrim,
StringPrototypeTrimLeft,
StringPrototypeToLocaleLowerCase,
Symbol,
SyntaxError,
SyntaxErrorPrototype,
Expand Down Expand Up @@ -1301,8 +1302,8 @@ function complete(line, callback) {
// Ignore right whitespace. It could change the outcome.
line = StringPrototypeTrimLeft(line);

// REPL commands (e.g. ".break").
let filter = '';
// REPL commands (e.g. ".break").
if (RegExpPrototypeTest(/^\s*\.(\w*)$/, line)) {
ArrayPrototypePush(completionGroups, ObjectKeys(this.commands));
completeOn = StringPrototypeMatch(line, /^\s*\.(\w*)$/)[1];
Expand Down Expand Up @@ -1545,11 +1546,16 @@ function complete(line, callback) {
// Filter, sort (within each group), uniq and merge the completion groups.
if (completionGroups.length && filter) {
const newCompletionGroups = [];
const lowerCaseFilter = StringPrototypeToLocaleLowerCase(filter);
ArrayPrototypeForEach(completionGroups, (group) => {
const filteredGroup = ArrayPrototypeFilter(
group,
(str) => StringPrototypeStartsWith(str, filter)
);
const filteredGroup = ArrayPrototypeFilter(group, (str) => {
// Filter is always case-insensitive following chromium autocomplete
// behavior.
return StringPrototypeStartsWith(
StringPrototypeToLocaleLowerCase(str),
lowerCaseFilter
);
});
if (filteredGroup.length) {
ArrayPrototypePush(newCompletionGroups, filteredGroup);
}
Expand Down
125 changes: 74 additions & 51 deletions test/parallel/test-repl-history-navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ const tests = [
env: { NODE_REPL_HISTORY: defaultHistoryPath },
test: [ 'let ab = 45', ENTER,
'555 + 909', ENTER,
'let autocompleteMe = 123', ENTER,
'{key : {key2 :[] }}', ENTER,
'Array(100).fill(1).map((e, i) => i ** i)', LEFT, LEFT, DELETE,
'2', ENTER],
Expand All @@ -82,7 +83,7 @@ const tests = [
},
{
env: { NODE_REPL_HISTORY: defaultHistoryPath },
test: [UP, UP, UP, UP, UP, DOWN, DOWN, DOWN, DOWN, DOWN],
test: [UP, UP, UP, UP, UP, UP, DOWN, DOWN, DOWN, DOWN, DOWN, DOWN],
expected: [prompt,
`${prompt}Array(100).fill(1).map((e, i) => i ** 2)`,
prev && '\n// [ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, ' +
Expand All @@ -92,13 +93,15 @@ const tests = [
' 2025, 2116, 2209,...',
`${prompt}{key : {key2 :[] }}`,
prev && '\n// { key: { key2: [] } }',
`${prompt}let autocompleteMe = 123`,
`${prompt}555 + 909`,
prev && '\n// 1464',
`${prompt}let ab = 45`,
prompt,
`${prompt}let ab = 45`,
`${prompt}555 + 909`,
prev && '\n// 1464',
`${prompt}let autocompleteMe = 123`,
`${prompt}{key : {key2 :[] }}`,
prev && '\n// { key: { key2: [] } }',
`${prompt}Array(100).fill(1).map((e, i) => i ** 2)`,
Expand Down Expand Up @@ -128,7 +131,7 @@ const tests = [
preview: false,
showEscapeCodes: true,
test: [
'55', UP, UP, UP, UP, UP, UP, ENTER,
'55', UP, UP, UP, UP, UP, UP, UP, ENTER,
],
expected: [
'\x1B[1G', '\x1B[0J', prompt, '\x1B[3G',
Expand Down Expand Up @@ -185,10 +188,10 @@ const tests = [
ENTER,
'veryLongName'.repeat(30),
ENTER,
`${'\x1B[90m \x1B[39m'.repeat(235)} fun`,
`${'\x1B[90m \x1B[39m'.repeat(229)} aut`,
ESCAPE,
ENTER,
`${' '.repeat(236)} fun`,
`${' '.repeat(230)} aut`,
ESCAPE,
ENTER,
],
Expand Down Expand Up @@ -236,19 +239,20 @@ const tests = [
prompt, '\x1B[3G',
// 1. UP
// This exceeds the maximum columns (250):
// Whitespace + prompt + ' // '.length + 'function'.length
// 236 + 2 + 4 + 8
// Whitespace + prompt + ' // '.length + 'autocompleteMe'.length
// 230 + 2 + 4 + 14
'\x1B[1G', '\x1B[0J',
`${prompt}${' '.repeat(236)} fun`, '\x1B[243G',
' // ction', '\x1B[243G',
' // ction', '\x1B[243G',
`${prompt}${' '.repeat(230)} aut`, '\x1B[237G',
' // ocompleteMe', '\x1B[237G',
'\n// 123', '\x1B[237G',
'\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A',
'\x1B[0K',
// 2. UP
'\x1B[1G', '\x1B[0J',
`${prompt}${' '.repeat(235)} fun`, '\x1B[242G',
// TODO(BridgeAR): Investigate why the preview is generated twice.
' // ction', '\x1B[242G',
' // ction', '\x1B[242G',
`${prompt}${' '.repeat(229)} aut`, '\x1B[236G',
' // ocompleteMe', '\x1B[236G',
'\n// 123', '\x1B[236G',
'\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A',
// Preview cleanup
'\x1B[0K',
// 3. UP
Expand Down Expand Up @@ -326,8 +330,8 @@ const tests = [
skip: !process.features.inspector,
checkTotal: true,
test: [
'fu',
'n',
'au',
't',
RIGHT,
BACKSPACE,
LEFT,
Expand All @@ -353,74 +357,93 @@ const tests = [
// K = Erase in line; 0 = right; 1 = left; 2 = total
expected: [
// 0.
// 'f'
'\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', 'f',
// 'a'
'\x1B[1G', '\x1B[0J', prompt, '\x1B[3G', 'a',
// 'u'
'u', ' // nction', '\x1B[5G',
// 'n' - Cleanup
'u', ' // tocompleteMe', '\x1B[5G',
'\n// 123', '\x1B[5G',
'\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A',
// 't' - Cleanup
'\x1B[0K',
'n', ' // ction', '\x1B[6G',
't', ' // ocompleteMe', '\x1B[6G',
'\n// 123', '\x1B[6G',
'\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A',
// 1. Right. Cleanup
'\x1B[0K',
'ction',
'ocompleteMe',
'\n// 123', '\x1B[17G',
'\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A',
// 2. Backspace. Refresh
'\x1B[1G', '\x1B[0J', `${prompt}functio`, '\x1B[10G',
'\x1B[1G', '\x1B[0J', `${prompt}autocompleteM`, '\x1B[16G',
// Autocomplete and refresh?
' // n', '\x1B[10G', ' // n', '\x1B[10G',
' // e', '\x1B[16G',
'\n// 123', '\x1B[16G',
'\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A',
// 3. Left. Cleanup
'\x1B[0K',
'\x1B[1D', '\x1B[10G', ' // n', '\x1B[9G',
'\x1B[1D', '\x1B[16G', ' // e', '\x1B[15G',
// 4. Left. Cleanup
'\x1B[10G', '\x1B[0K', '\x1B[9G',
'\x1B[1D', '\x1B[10G', ' // n', '\x1B[8G',
'\x1B[16G', '\x1B[0K', '\x1B[15G',
'\x1B[1D', '\x1B[16G', ' // e', '\x1B[14G',
// 5. 'A' - Cleanup
'\x1B[10G', '\x1B[0K', '\x1B[8G',
'\x1B[16G', '\x1B[0K', '\x1B[14G',
// Refresh
'\x1B[1G', '\x1B[0J', `${prompt}functAio`, '\x1B[9G',
'\x1B[1G', '\x1B[0J', `${prompt}autocompletAeM`, '\x1B[15G',
// 6. Backspace. Refresh
'\x1B[1G', '\x1B[0J', `${prompt}functio`, '\x1B[8G', '\x1B[10G', ' // n',
'\x1B[8G', '\x1B[10G', ' // n',
'\x1B[8G', '\x1B[10G',
'\x1B[1G', '\x1B[0J', `${prompt}autocompleteM`,
'\x1B[14G', '\x1B[16G', ' // e',
'\x1B[14G', '\x1B[16G', ' // e',
'\x1B[14G', '\x1B[16G',
// 7. Go to end. Cleanup
'\x1B[0K', '\x1B[8G', '\x1B[2C',
'n',
'\x1B[0K', '\x1B[14G', '\x1B[2C',
'e',
'\n// 123', '\x1B[17G',
'\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A',
// 8. Backspace. Refresh
'\x1B[1G', '\x1B[0J', `${prompt}functio`, '\x1B[10G',
'\x1B[1G', '\x1B[0J', `${prompt}autocompleteM`, '\x1B[16G',
// Autocomplete
' // n', '\x1B[10G', ' // n', '\x1B[10G',
' // e', '\x1B[16G',
'\n// 123', '\x1B[16G',
'\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A',
// 9. Word left. Cleanup
'\x1B[0K', '\x1B[7D', '\x1B[10G', ' // n', '\x1B[3G', '\x1B[10G',
'\x1B[0K', '\x1B[13D', '\x1B[16G', ' // e', '\x1B[3G', '\x1B[16G',
// 10. Word right. Cleanup
'\x1B[0K', '\x1B[3G', '\x1B[7C', ' // n', '\x1B[10G',
'\x1B[0K', '\x1B[3G', '\x1B[13C', ' // e', '\x1B[16G',
'\n// 123', '\x1B[16G',
'\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A',
// 11. ESCAPE
'\x1B[0K',
// 12. ENTER
'\r\n',
'Uncaught ReferenceError: functio is not defined\n',
'Uncaught ReferenceError: autocompleteM is not defined\n',
'\x1B[1G', '\x1B[0J',
// 13. UP
prompt, '\x1B[3G', '\x1B[1G', '\x1B[0J',
`${prompt}functio`, '\x1B[10G',
' // n', '\x1B[10G',
' // n', '\x1B[10G',
`${prompt}autocompleteM`, '\x1B[16G',
' // e', '\x1B[16G',
'\n// 123', '\x1B[16G',
'\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A',
// 14. LEFT
'\x1B[0K', '\x1B[1D',
'\x1B[10G', ' // n', '\x1B[9G', '\x1B[10G',
'\x1B[0K', '\x1B[1D', '\x1B[16G',
' // e', '\x1B[15G', '\x1B[16G',
// 15. ENTER
'\x1B[0K', '\x1B[9G', '\x1B[1C',
'\x1B[0K', '\x1B[15G', '\x1B[1C',
'\r\n',
'Uncaught ReferenceError: functio is not defined\n',
'Uncaught ReferenceError: autocompleteM is not defined\n',
'\x1B[1G', '\x1B[0J',
'> ', '\x1B[3G',
prompt, '\x1B[3G',
// 16. UP
'\x1B[1G', '\x1B[0J',
'> functio', '\x1B[10G',
' // n', '\x1B[10G',
' // n', '\x1B[10G', '\x1B[0K',
`${prompt}autocompleteM`, '\x1B[16G',
' // e', '\x1B[16G',
'\n// 123', '\x1B[16G',
'\x1B[1A', '\x1B[1B', '\x1B[2K', '\x1B[1A',
'\x1B[0K',
// 17. ENTER
'n', '\r\n',
'e', '\r\n',
'123\n',
'\x1B[1G', '\x1B[0J',
'... ', '\x1B[5G',
prompt, '\x1B[3G',
'\r\n',
],
clean: true
Expand Down
4 changes: 1 addition & 3 deletions test/parallel/test-repl-reverse-search.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,7 @@ const tests = [
expected: [
'\x1B[1G', '\x1B[0J',
prompt, '\x1B[3G',
'f', 'u', ' // nction',
'\x1B[5G', '\x1B[0K',
'\nbck-i-search: _', '\x1B[1A', '\x1B[5G',
'f', 'u', '\nbck-i-search: _', '\x1B[1A', '\x1B[5G',
'\x1B[3G', '\x1B[0J',
'{key : {key2 :[] }}\nbck-i-search: }_', '\x1B[1A', '\x1B[21G',
'\x1B[3G', '\x1B[0J',
Expand Down
57 changes: 52 additions & 5 deletions test/parallel/test-repl-tab-complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,38 @@ testMe.complete('str.len', common.mustCall(function(error, data) {

putIn.run(['.clear']);

// Tab completion should be case-insensitive if member part is lower-case
putIn.run([
'var foo = { barBar: 1, BARbuz: 2, barBLA: 3 };',
]);
testMe.complete(
'foo.b',
common.mustCall(function(error, data) {
assert.deepStrictEqual(data, [
['foo.BARbuz', 'foo.barBLA', 'foo.barBar'],
'foo.b',
]);
})
);

putIn.run(['.clear']);

// Tab completion should be case-insensitive if member part is upper-case
putIn.run([
'var foo = { barBar: 1, BARbuz: 2, barBLA: 3 };',
]);
testMe.complete(
'foo.B',
common.mustCall(function(error, data) {
assert.deepStrictEqual(data, [
['foo.BARbuz', 'foo.barBLA', 'foo.barBar'],
'foo.B',
]);
})
);

putIn.run(['.clear']);

// Tab completion should not break on spaces
const spaceTimeout = setTimeout(function() {
throw new Error('timeout');
Expand Down Expand Up @@ -588,12 +620,27 @@ const testNonGlobal = repl.start({
useGlobal: false
});

const builtins = [['Infinity', 'Int16Array', 'Int32Array',
'Int8Array'], 'I'];
const builtins = [
[
'if',
'import',
'in',
'instanceof',
'',
'Infinity',
'Int16Array',
'Int32Array',
'Int8Array',
...(common.hasIntl ? ['Intl'] : []),
'inspector',
'isFinite',
'isNaN',
'',
'isPrototypeOf',
],
'I',
];

if (common.hasIntl) {
builtins[0].push('Intl');
}
testNonGlobal.complete('I', common.mustCall((error, data) => {
assert.deepStrictEqual(data, builtins);
}));
Expand Down

0 comments on commit 0c9d3be

Please sign in to comment.