Skip to content

Commit

Permalink
repl: simplify repl autocompletion
Browse files Browse the repository at this point in the history
This refactors the repl autocompletion code for simplicity and
readability.

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

PR-URL: #33450
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
  • Loading branch information
BridgeAR committed May 20, 2020
1 parent 76c5dc9 commit 19d9e20
Showing 1 changed file with 58 additions and 79 deletions.
137 changes: 58 additions & 79 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand All @@ -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}`);
Expand All @@ -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
Expand All @@ -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]));
Expand All @@ -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) {
Expand All @@ -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;
Expand Down

0 comments on commit 19d9e20

Please sign in to comment.