Skip to content

Commit

Permalink
repl: make autocomplete case-insensitive if user input is lowercase
Browse files Browse the repository at this point in the history
This changes autocomplete suggestion filter to ignore input case
when first character of the last member of the user input is lower-case
allowing for more autosuggest results shown on the screen

Fixes: #41631
  • Loading branch information
gribnoysup committed Jun 5, 2022
1 parent b6a2072 commit bb6f57b
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 63 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
58 changes: 54 additions & 4 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,11 +620,29 @@ const testNonGlobal = repl.start({
useGlobal: false
});

const builtins = [['Infinity', 'Int16Array', 'Int32Array',
'Int8Array'], 'I'];
const builtins = [
[
'if',
'import',
'in',
'instanceof',
'',
'Infinity',
'Int16Array',
'Int32Array',
'Int8Array',
'Intl',
'inspector',
'isFinite',
'isNaN',
'',
'isPrototypeOf',
],
'I',
];

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

0 comments on commit bb6f57b

Please sign in to comment.