Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

repl: support optional chaining during autocompletion #33450

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 69 additions & 86 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,7 @@ REPLServer.prototype.turnOffEditorMode = deprecate(
const requireRE = /\brequire\s*\(\s*['"`](([\w@./-]+\/)?(?:[\w@./-]*))(?![^'"`])$/;
const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
const simpleExpressionRE =
/(?:[a-zA-Z_$](?:\w|\$)*\.)*[a-zA-Z_$](?:\w|\$)*\.?$/;
/(?:[a-zA-Z_$](?:\w|\$)*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;

function isIdentifier(str) {
if (str === '') {
Expand Down 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 All @@ -1138,15 +1154,13 @@ function complete(line, callback) {
line = line.trimLeft();

// REPL commands (e.g. ".break").
let filter;
let filter = '';
if (/^\s*\.(\w*)$/.test(line)) {
completionGroups.push(ObjectKeys(this.commands));
completeOn = line.match(/^\s*\.(\w*)$/)[1];
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,27 +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) {
filter = '';
expr = '';
} else if (line[line.length - 1] === '.') {
filter = '';
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 @@ -1285,63 +1267,64 @@ function complete(line, callback) {
return;
}

let chaining = '.';
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) {
for (let i = 0; i < memberGroups.length; i++) {
completionGroups.push(
memberGroups[i].map((member) => `${expr}.${member}`));
expr += chaining;
for (const group of memberGroups) {
completionGroups.push(group.map((member) => `${expr}${member}`));
}
if (filter) {
filter = `${expr}.${filter}`;
filter = `${expr}${filter}`;
}
}

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
13 changes: 13 additions & 0 deletions test/parallel/test-repl-tab-complete.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ testMe.complete('console.lo', common.mustCall(function(error, data) {
assert.deepStrictEqual(data, [['console.log'], 'console.lo']);
}));

testMe.complete('console?.lo', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [['console?.log'], 'console?.lo']);
}));

testMe.complete('console?.zzz', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [[], 'console?.zzz']);
}));

testMe.complete('console?.', common.mustCall((error, data) => {
assert(data[0].includes('console?.log'));
assert.strictEqual(data[1], 'console?.');
}));

// Tab Complete will return globally scoped variables
putIn.run(['};']);
testMe.complete('inner.o', common.mustCall(function(error, data) {
Expand Down