Skip to content

Commit 3831d87

Browse files
targosMylesBorins
authored andcommitted
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: #16591 Fixes: #983 Reviewed-By: Timothy Gu <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]>
1 parent 43fbc39 commit 3831d87

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
@@ -58,6 +58,7 @@ const Module = require('module');
5858
const domain = require('domain');
5959
const debug = util.debuglog('repl');
6060
const errors = require('internal/errors');
61+
const { sendInspectorCommand } = require('internal/util/inspector');
6162

6263
const parentModule = module;
6364
const replMap = new WeakMap();
@@ -75,6 +76,7 @@ for (var n = 0; n < GLOBAL_OBJECT_PROPERTIES.length; n++) {
7576
GLOBAL_OBJECT_PROPERTIES[n];
7677
}
7778
const kBufferedCommandSymbol = Symbol('bufferedCommand');
79+
const kContextId = Symbol('contextId');
7880

7981
try {
8082
// hack for require.resolve("./relative") to work properly.
@@ -155,6 +157,8 @@ function REPLServer(prompt,
155157
self.last = undefined;
156158
self.breakEvalOnSigint = !!breakEvalOnSigint;
157159
self.editorMode = false;
160+
// Context id for use with the inspector protocol.
161+
self[kContextId] = undefined;
158162

159163
// just for backwards compat, see github.com/joyent/node/pull/7127
160164
self.rli = this;
@@ -644,7 +648,16 @@ REPLServer.prototype.createContext = function() {
644648
if (this.useGlobal) {
645649
context = global;
646650
} else {
647-
context = vm.createContext();
651+
sendInspectorCommand((session) => {
652+
session.post('Runtime.enable');
653+
session.on('Runtime.executionContextCreated', ({ params }) => {
654+
this[kContextId] = params.context.id;
655+
});
656+
context = vm.createContext();
657+
session.post('Runtime.disable');
658+
}, () => {
659+
context = vm.createContext();
660+
});
648661
context.global = context;
649662
const _console = new Console(this.outputStream);
650663
Object.defineProperty(context, 'console', {
@@ -779,6 +792,18 @@ function filteredOwnPropertyNames(obj) {
779792
return Object.getOwnPropertyNames(obj).filter(intFilter);
780793
}
781794

795+
function getGlobalLexicalScopeNames(contextId) {
796+
return sendInspectorCommand((session) => {
797+
let names = [];
798+
session.post('Runtime.globalLexicalScopeNames', {
799+
executionContextId: contextId
800+
}, (error, result) => {
801+
if (!error) names = result.names;
802+
});
803+
return names;
804+
}, () => []);
805+
}
806+
782807
REPLServer.prototype.complete = function() {
783808
this.completer.apply(this, arguments);
784809
};
@@ -942,6 +967,7 @@ function complete(line, callback) {
942967
// If context is instance of vm.ScriptContext
943968
// Get global vars synchronously
944969
if (this.useGlobal || vm.isContext(this.context)) {
970+
completionGroups.push(getGlobalLexicalScopeNames(this[kContextId]));
945971
var contextProto = this.context;
946972
while (contextProto = Object.getPrototypeOf(contextProto)) {
947973
completionGroups.push(

node.gyp

+1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
'lib/internal/url.js',
128128
'lib/internal/util.js',
129129
'lib/internal/util/comparisons.js',
130+
'lib/internal/util/inspector.js',
130131
'lib/internal/util/types.js',
131132
'lib/internal/http2/core.js',
132133
'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)