Skip to content

Commit cfd2a99

Browse files
committed
repl: show lexically scoped vars in tab completion
Use the V8 inspector protocol, if available, to query the list of lexically scoped variables (defined with `let`, `const` or `class`). PR-URL: nodejs#16591 Fixes: nodejs#983 Reviewed-By: Timothy Gu <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]>
1 parent 993253d commit cfd2a99

File tree

5 files changed

+108
-1
lines changed

5 files changed

+108
-1
lines changed

lib/internal/util/inspector.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict';
2+
3+
const hasInspector = process.config.variables.v8_enable_inspector === 1;
4+
const inspector = hasInspector ? require('inspector') : undefined;
5+
6+
let session;
7+
8+
function sendInspectorCommand(cb, onError) {
9+
if (!hasInspector) return onError();
10+
if (session === undefined) session = new inspector.Session();
11+
try {
12+
session.connect();
13+
try {
14+
return cb(session);
15+
} finally {
16+
session.disconnect();
17+
}
18+
} catch (e) {
19+
return onError();
20+
}
21+
}
22+
23+
module.exports = {
24+
sendInspectorCommand
25+
};

lib/repl.js

+27-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const Module = require('module');
5959
const domain = require('domain');
6060
const debug = util.debuglog('repl');
6161
const errors = require('internal/errors');
62+
const { sendInspectorCommand } = require('internal/util/inspector');
6263

6364
const parentModule = module;
6465
const replMap = new WeakMap();
@@ -76,6 +77,7 @@ for (var n = 0; n < GLOBAL_OBJECT_PROPERTIES.length; n++) {
7677
GLOBAL_OBJECT_PROPERTIES[n];
7778
}
7879
const kBufferedCommandSymbol = Symbol('bufferedCommand');
80+
const kContextId = Symbol('contextId');
7981

8082
try {
8183
// hack for require.resolve("./relative") to work properly.
@@ -158,6 +160,8 @@ function REPLServer(prompt,
158160
self.last = undefined;
159161
self.breakEvalOnSigint = !!breakEvalOnSigint;
160162
self.editorMode = false;
163+
// Context id for use with the inspector protocol.
164+
self[kContextId] = undefined;
161165

162166
// just for backwards compat, see github.com/joyent/node/pull/7127
163167
self.rli = this;
@@ -755,7 +759,16 @@ REPLServer.prototype.createContext = function() {
755759
if (this.useGlobal) {
756760
context = global;
757761
} else {
758-
context = vm.createContext();
762+
sendInspectorCommand((session) => {
763+
session.post('Runtime.enable');
764+
session.on('Runtime.executionContextCreated', ({ params }) => {
765+
this[kContextId] = params.context.id;
766+
});
767+
context = vm.createContext();
768+
session.post('Runtime.disable');
769+
}, () => {
770+
context = vm.createContext();
771+
});
759772
context.global = context;
760773
const _console = new Console(this.outputStream);
761774
Object.defineProperty(context, 'console', {
@@ -890,6 +903,18 @@ function filteredOwnPropertyNames(obj) {
890903
return Object.getOwnPropertyNames(obj).filter(intFilter);
891904
}
892905

906+
function getGlobalLexicalScopeNames(contextId) {
907+
return sendInspectorCommand((session) => {
908+
let names = [];
909+
session.post('Runtime.globalLexicalScopeNames', {
910+
executionContextId: contextId
911+
}, (error, result) => {
912+
if (!error) names = result.names;
913+
});
914+
return names;
915+
}, () => []);
916+
}
917+
893918
REPLServer.prototype.complete = function() {
894919
this.completer.apply(this, arguments);
895920
};
@@ -1053,6 +1078,7 @@ function complete(line, callback) {
10531078
// If context is instance of vm.ScriptContext
10541079
// Get global vars synchronously
10551080
if (this.useGlobal || vm.isContext(this.context)) {
1081+
completionGroups.push(getGlobalLexicalScopeNames(this[kContextId]));
10561082
var contextProto = this.context;
10571083
while (contextProto = Object.getPrototypeOf(contextProto)) {
10581084
completionGroups.push(

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
'lib/internal/url.js',
130130
'lib/internal/util.js',
131131
'lib/internal/util/comparisons.js',
132+
'lib/internal/util/inspector.js',
132133
'lib/internal/util/types.js',
133134
'lib/internal/http2/core.js',
134135
'lib/internal/http2/compat.js',

test/parallel/test-repl-inspector.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const repl = require('repl');
6+
7+
common.skipIfInspectorDisabled();
8+
9+
// This test verifies that the V8 inspector API is usable in the REPL.
10+
11+
const putIn = new common.ArrayStream();
12+
let output = '';
13+
putIn.write = function(data) {
14+
output += data;
15+
};
16+
17+
const testMe = repl.start('', putIn);
18+
19+
putIn.run(['const myVariable = 42']);
20+
21+
testMe.complete('myVar', common.mustCall((error, data) => {
22+
assert.deepStrictEqual(data, [['myVariable'], 'myVar']);
23+
}));
24+
25+
putIn.run([
26+
'const inspector = require("inspector")',
27+
'const session = new inspector.Session()',
28+
'session.connect()',
29+
'session.post("Runtime.evaluate", { expression: "1 + 1" }, console.log)',
30+
'session.disconnect()'
31+
]);
32+
33+
assert(output.includes(
34+
"null { result: { type: 'number', value: 2, description: '2' } }"));

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

+21
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
const common = require('../common');
2525
const assert = require('assert');
2626
const fixtures = require('../common/fixtures');
27+
const hasInspector = process.config.variables.v8_enable_inspector === 1;
2728

2829
// We have to change the directory to ../fixtures before requiring repl
2930
// in order to make the tests for completion of node_modules work properly
@@ -529,3 +530,23 @@ editorStream.run(['.editor']);
529530
editor.completer('var log = console.l', common.mustCall((error, data) => {
530531
assert.deepStrictEqual(data, [['console.log'], 'console.l']);
531532
}));
533+
534+
{
535+
// tab completion of lexically scoped variables
536+
const stream = new common.ArrayStream();
537+
const testRepl = repl.start({ stream });
538+
539+
stream.run([`
540+
let lexicalLet = true;
541+
const lexicalConst = true;
542+
class lexicalKlass {}
543+
`]);
544+
545+
['Let', 'Const', 'Klass'].forEach((type) => {
546+
const query = `lexical${type[0]}`;
547+
const expected = hasInspector ? [[`lexical${type}`], query] : [];
548+
testRepl.complete(query, common.mustCall((error, data) => {
549+
assert.deepStrictEqual(data, expected);
550+
}));
551+
});
552+
}

0 commit comments

Comments
 (0)