repl: simplify repl autocompletion

This refactors the repl autocompletion code for simplicity and
readability.

Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>

PR-URL: https://github.com/nodejs/node/pull/33450
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
Ruben Bridgewater
2020-05-17 05:20:45 +02:00
parent 76c5dc995e
commit 19d9e2003e

View File

@@ -1111,12 +1111,28 @@ REPLServer.prototype.complete = function() {
this.completer.apply(this, arguments);
};
function gracefulOperation(fn, args, alternative) {
function gracefulReaddir(...args) {
try {
return fn(...args);
} catch {
return alternative;
return fs.readdirSync(...args);
} catch {}
}
function completeFSFunctions(line) {
let baseName = '';
let filePath = line.match(fsAutoCompleteRE)[1];
let fileList = gracefulReaddir(filePath, { withFileTypes: true });
if (!fileList) {
baseName = path.basename(filePath);
filePath = path.dirname(filePath);
fileList = gracefulReaddir(filePath, { withFileTypes: true }) || [];
}
const completions = fileList
.filter((dirent) => dirent.name.startsWith(baseName))
.map((d) => d.name);
return [[completions], baseName];
}
// Provide a list of completions for the given leading text. This is
@@ -1145,8 +1161,6 @@ function complete(line, callback) {
if (completeOn.length) {
filter = completeOn;
}
completionGroupsLoaded();
} else if (requireRE.test(line)) {
// require('...<Tab>')
const extensions = ObjectKeys(this.context.require.extensions);
@@ -1173,11 +1187,7 @@ function complete(line, callback) {
for (let dir of paths) {
dir = path.resolve(dir, subdir);
const dirents = gracefulOperation(
fs.readdirSync,
[dir, { withFileTypes: true }],
[]
);
const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
for (const dirent of dirents) {
if (versionedFileNamesRe.test(dirent.name) || dirent.name === '.npm') {
// Exclude versioned names that 'npm' installs.
@@ -1193,7 +1203,7 @@ function complete(line, callback) {
}
group.push(`${subdir}${dirent.name}/`);
const absolute = path.resolve(dir, dirent.name);
const subfiles = gracefulOperation(fs.readdirSync, [absolute], []);
const subfiles = gracefulReaddir(absolute) || [];
for (const subfile of subfiles) {
if (indexes.includes(subfile)) {
group.push(`${subdir}${dirent.name}`);
@@ -1209,31 +1219,8 @@ function complete(line, callback) {
if (!subdir) {
completionGroups.push(_builtinLibs);
}
completionGroupsLoaded();
} else if (fsAutoCompleteRE.test(line)) {
filter = '';
let filePath = line.match(fsAutoCompleteRE)[1];
let fileList;
try {
fileList = fs.readdirSync(filePath, { withFileTypes: true });
completionGroups.push(fileList.map((dirent) => dirent.name));
completeOn = '';
} catch {
try {
const baseName = path.basename(filePath);
filePath = path.dirname(filePath);
fileList = fs.readdirSync(filePath, { withFileTypes: true });
const filteredValue = fileList.filter((d) =>
d.name.startsWith(baseName))
.map((d) => d.name);
completionGroups.push(filteredValue);
completeOn = baseName;
} catch {}
}
completionGroupsLoaded();
[completionGroups, completeOn] = completeFSFunctions(line);
// Handle variable member lookup.
// We support simple chained expressions like the following (no function
// calls, etc.). That is for simplicity and also because we *eval* that
@@ -1245,25 +1232,22 @@ function complete(line, callback) {
// foo<|> # all scope vars with filter 'foo'
// foo.<|> # completions for 'foo' with filter ''
} else if (line.length === 0 || /\w|\.|\$/.test(line[line.length - 1])) {
const match = simpleExpressionRE.exec(line);
const [match] = simpleExpressionRE.exec(line) || [''];
if (line.length !== 0 && !match) {
completionGroupsLoaded();
return;
}
let expr;
completeOn = (match ? match[0] : '');
if (line.length === 0) {
expr = '';
} else if (line[line.length - 1] === '.') {
expr = match[0].slice(0, match[0].length - 1);
} else {
const bits = match[0].split('.');
let expr = '';
completeOn = match;
if (line.endsWith('.')) {
expr = match.slice(0, -1);
} else if (line.length !== 0) {
const bits = match.split('.');
filter = bits.pop();
expr = bits.join('.');
}
// Resolve expr and get its completions.
const memberGroups = [];
if (!expr) {
// Get global vars synchronously
completionGroups.push(getGlobalLexicalScopeNames(this[kContextId]));
@@ -1284,39 +1268,34 @@ function complete(line, callback) {
}
let chaining = '.';
if (expr[expr.length - 1] === '?') {
if (expr.endsWith('?')) {
expr = expr.slice(0, -1);
chaining = '?.';
}
const memberGroups = [];
const evalExpr = `try { ${expr} } catch {}`;
this.eval(evalExpr, this.context, 'repl', (e, obj) => {
if (obj != null) {
if (typeof obj === 'object' || typeof obj === 'function') {
try {
memberGroups.push(filteredOwnPropertyNames(obj));
} catch {
// Probably a Proxy object without `getOwnPropertyNames` trap.
// We simply ignore it here, as we don't want to break the
// autocompletion. Fixes the bug
// https://github.com/nodejs/node/issues/2119
}
try {
let p;
if ((typeof obj === 'object' && obj !== null) ||
typeof obj === 'function') {
memberGroups.push(filteredOwnPropertyNames(obj));
p = ObjectGetPrototypeOf(obj);
} else {
p = obj.constructor ? obj.constructor.prototype : null;
}
// Works for non-objects
try {
let p;
if (typeof obj === 'object' || typeof obj === 'function') {
p = ObjectGetPrototypeOf(obj);
} else {
p = obj.constructor ? obj.constructor.prototype : null;
}
// Circular refs possible? Let's guard against that.
let sentinel = 5;
while (p !== null && sentinel-- !== 0) {
memberGroups.push(filteredOwnPropertyNames(p));
p = ObjectGetPrototypeOf(p);
}
} catch {}
// Circular refs possible? Let's guard against that.
let sentinel = 5;
while (p !== null && sentinel-- !== 0) {
memberGroups.push(filteredOwnPropertyNames(p));
p = ObjectGetPrototypeOf(p);
}
} catch {
// Maybe a Proxy object without `getOwnPropertyNames` trap.
// We simply ignore it here, as we don't want to break the
// autocompletion. Fixes the bug
// https://github.com/nodejs/node/issues/2119
}
if (memberGroups.length) {
@@ -1331,21 +1310,21 @@ function complete(line, callback) {
completionGroupsLoaded();
});
} else {
completionGroupsLoaded();
return;
}
return completionGroupsLoaded();
// Will be called when all completionGroups are in place
// Useful for async autocompletion
function completionGroupsLoaded() {
// Filter, sort (within each group), uniq and merge the completion groups.
if (completionGroups.length && filter) {
const newCompletionGroups = [];
for (let i = 0; i < completionGroups.length; i++) {
group = completionGroups[i]
.filter((elem) => elem.indexOf(filter) === 0);
if (group.length) {
newCompletionGroups.push(group);
for (const group of completionGroups) {
const filteredGroup = group.filter((str) => str.startsWith(filter));
if (filteredGroup.length) {
newCompletionGroups.push(filteredGroup);
}
}
completionGroups = newCompletionGroups;