Skip to content

Commit 6257cf2

Browse files
BridgeARcodebytere
authored andcommitted
repl: improve repl autocompletion for require calls
This improves the autocompletion for require calls. It had multiple small issues so far. Most important: it won't suggest completions for require statements that are fully written out. Second, it'll detect require calls that have whitespace behind the opening bracket. Third, it makes sure node modules are detected as such instead of only suggesting them as folders. Last, it adds suggestions for input that starts with backticks. Signed-off-by: Ruben Bridgewater <[email protected]> PR-URL: #33282 Fixes: #33238 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
1 parent 95842db commit 6257cf2

File tree

4 files changed

+77
-59
lines changed

4 files changed

+77
-59
lines changed

lib/repl.js

+44-52
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,7 @@ REPLServer.prototype.turnOffEditorMode = deprecate(
10461046
'REPLServer.turnOffEditorMode() is deprecated',
10471047
'DEP0078');
10481048

1049-
const requireRE = /\brequire\s*\(['"](([\w@./-]+\/)?(?:[\w@./-]*))/;
1049+
const requireRE = /\brequire\s*\(\s*['"`](([\w@./-]+\/)?(?:[\w@./-]*))(?![^'"`])$/;
10501050
const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
10511051
const simpleExpressionRE =
10521052
/(?:[a-zA-Z_$](?:\w|\$)*\.)*[a-zA-Z_$](?:\w|\$)*\.?$/;
@@ -1094,8 +1094,13 @@ REPLServer.prototype.complete = function() {
10941094
this.completer.apply(this, arguments);
10951095
};
10961096

1097-
// TODO: Native module names should be auto-resolved.
1098-
// That improves the auto completion.
1097+
function gracefulOperation(fn, args, alternative) {
1098+
try {
1099+
return fn(...args);
1100+
} catch {
1101+
return alternative;
1102+
}
1103+
}
10991104

11001105
// Provide a list of completions for the given leading text. This is
11011106
// given to the readline interface for handling tab completion.
@@ -1117,26 +1122,25 @@ function complete(line, callback) {
11171122

11181123
// REPL commands (e.g. ".break").
11191124
let filter;
1120-
let match = line.match(/^\s*\.(\w*)$/);
1121-
if (match) {
1125+
if (/^\s*\.(\w*)$/.test(line)) {
11221126
completionGroups.push(ObjectKeys(this.commands));
1123-
completeOn = match[1];
1124-
if (match[1].length) {
1125-
filter = match[1];
1127+
completeOn = line.match(/^\s*\.(\w*)$/)[1];
1128+
if (completeOn.length) {
1129+
filter = completeOn;
11261130
}
11271131

11281132
completionGroupsLoaded();
1129-
} else if (match = line.match(requireRE)) {
1133+
} else if (requireRE.test(line)) {
11301134
// require('...<Tab>')
1131-
const exts = ObjectKeys(this.context.require.extensions);
1132-
const indexRe = new RegExp('^index(?:' + exts.map(regexpEscape).join('|') +
1133-
')$');
1135+
const extensions = ObjectKeys(this.context.require.extensions);
1136+
const indexes = extensions.map((extension) => `index${extension}`);
1137+
indexes.push('package.json', 'index');
11341138
const versionedFileNamesRe = /-\d+\.\d+/;
11351139

1140+
const match = line.match(requireRE);
11361141
completeOn = match[1];
11371142
const subdir = match[2] || '';
1138-
filter = match[1];
1139-
let dir, files, subfiles, isDirectory;
1143+
filter = completeOn;
11401144
group = [];
11411145
let paths = [];
11421146

@@ -1150,41 +1154,34 @@ function complete(line, callback) {
11501154
paths = module.paths.concat(CJSModule.globalPaths);
11511155
}
11521156

1153-
for (let i = 0; i < paths.length; i++) {
1154-
dir = path.resolve(paths[i], subdir);
1155-
try {
1156-
files = fs.readdirSync(dir);
1157-
} catch {
1158-
continue;
1159-
}
1160-
for (let f = 0; f < files.length; f++) {
1161-
const name = files[f];
1162-
const ext = path.extname(name);
1163-
const base = name.slice(0, -ext.length);
1164-
if (versionedFileNamesRe.test(base) || name === '.npm') {
1157+
for (let dir of paths) {
1158+
dir = path.resolve(dir, subdir);
1159+
const dirents = gracefulOperation(
1160+
fs.readdirSync,
1161+
[dir, { withFileTypes: true }],
1162+
[]
1163+
);
1164+
for (const dirent of dirents) {
1165+
if (versionedFileNamesRe.test(dirent.name) || dirent.name === '.npm') {
11651166
// Exclude versioned names that 'npm' installs.
11661167
continue;
11671168
}
1168-
const abs = path.resolve(dir, name);
1169-
try {
1170-
isDirectory = fs.statSync(abs).isDirectory();
1171-
} catch {
1169+
const extension = path.extname(dirent.name);
1170+
const base = dirent.name.slice(0, -extension.length);
1171+
if (!dirent.isDirectory()) {
1172+
if (extensions.includes(extension) && (!subdir || base !== 'index')) {
1173+
group.push(`${subdir}${base}`);
1174+
}
11721175
continue;
11731176
}
1174-
if (isDirectory) {
1175-
group.push(subdir + name + '/');
1176-
try {
1177-
subfiles = fs.readdirSync(abs);
1178-
} catch {
1179-
continue;
1177+
group.push(`${subdir}${dirent.name}/`);
1178+
const absolute = path.resolve(dir, dirent.name);
1179+
const subfiles = gracefulOperation(fs.readdirSync, [absolute], []);
1180+
for (const subfile of subfiles) {
1181+
if (indexes.includes(subfile)) {
1182+
group.push(`${subdir}${dirent.name}`);
1183+
break;
11801184
}
1181-
for (let s = 0; s < subfiles.length; s++) {
1182-
if (indexRe.test(subfiles[s])) {
1183-
group.push(subdir + name);
1184-
}
1185-
}
1186-
} else if (exts.includes(ext) && (!subdir || base !== 'index')) {
1187-
group.push(subdir + base);
11881185
}
11891186
}
11901187
}
@@ -1197,11 +1194,10 @@ function complete(line, callback) {
11971194
}
11981195

11991196
completionGroupsLoaded();
1200-
} else if (match = line.match(fsAutoCompleteRE)) {
1201-
1202-
let filePath = match[1];
1203-
let fileList;
1197+
} else if (fsAutoCompleteRE.test(line)) {
12041198
filter = '';
1199+
let filePath = line.match(fsAutoCompleteRE)[1];
1200+
let fileList;
12051201

12061202
try {
12071203
fileList = fs.readdirSync(filePath, { withFileTypes: true });
@@ -1232,7 +1228,7 @@ function complete(line, callback) {
12321228
// foo<|> # all scope vars with filter 'foo'
12331229
// foo.<|> # completions for 'foo' with filter ''
12341230
} else if (line.length === 0 || /\w|\.|\$/.test(line[line.length - 1])) {
1235-
match = simpleExpressionRE.exec(line);
1231+
const match = simpleExpressionRE.exec(line);
12361232
if (line.length !== 0 && !match) {
12371233
completionGroupsLoaded();
12381234
return;
@@ -1582,10 +1578,6 @@ function defineDefaultCommands(repl) {
15821578
}
15831579
}
15841580

1585-
function regexpEscape(s) {
1586-
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
1587-
}
1588-
15891581
function Recoverable(err) {
15901582
this.err = err;
15911583
}

test/fixtures/node_modules/no_index/lib/index.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/node_modules/no_index/package.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/parallel/test-repl-tab-complete.js

+29-7
Original file line numberDiff line numberDiff line change
@@ -229,24 +229,46 @@ testMe.complete('require(\'', common.mustCall(function(error, data) {
229229
});
230230
}));
231231

232-
testMe.complete('require(\'n', common.mustCall(function(error, data) {
232+
testMe.complete("require\t( 'n", common.mustCall(function(error, data) {
233233
assert.strictEqual(error, null);
234234
assert.strictEqual(data.length, 2);
235235
assert.strictEqual(data[1], 'n');
236-
assert(data[0].includes('net'));
236+
// There is only one Node.js module that starts with n:
237+
assert.strictEqual(data[0][0], 'net');
238+
assert.strictEqual(data[0][1], '');
237239
// It's possible to pick up non-core modules too
238-
data[0].forEach(function(completion) {
239-
if (completion)
240-
assert(/^n/.test(completion));
240+
data[0].slice(2).forEach((completion) => {
241+
assert.match(completion, /^n/);
241242
});
242243
}));
243244

244245
{
245246
const expected = ['@nodejsscope', '@nodejsscope/'];
247+
// Require calls should handle all types of quotation marks.
248+
for (const quotationMark of ["'", '"', '`']) {
249+
putIn.run(['.clear']);
250+
testMe.complete('require(`@nodejs', common.mustCall((err, data) => {
251+
assert.strictEqual(err, null);
252+
assert.deepStrictEqual(data, [expected, '@nodejs']);
253+
}));
254+
255+
putIn.run(['.clear']);
256+
// Completions should not be greedy in case the quotation ends.
257+
const input = `require(${quotationMark}@nodejsscope${quotationMark}`;
258+
testMe.complete(input, common.mustCall((err, data) => {
259+
assert.strictEqual(err, null);
260+
assert.deepStrictEqual(data, [[], undefined]);
261+
}));
262+
}
263+
}
264+
265+
{
246266
putIn.run(['.clear']);
247-
testMe.complete('require(\'@nodejs', common.mustCall((err, data) => {
267+
// Completions should find modules and handle whitespace after the opening
268+
// bracket.
269+
testMe.complete('require \t("no_ind', common.mustCall((err, data) => {
248270
assert.strictEqual(err, null);
249-
assert.deepStrictEqual(data, [expected, '@nodejs']);
271+
assert.deepStrictEqual(data, [['no_index', 'no_index/'], 'no_ind']);
250272
}));
251273
}
252274

0 commit comments

Comments
 (0)