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: https://github.com/nodejs/node/pull/16591
Fixes: https://github.com/nodejs/node/issues/983
Reviewed-By: Timothy Gu <timothygu99@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
This commit is contained in:
Michaël Zasso 2017-10-29 18:19:24 +01:00
parent d1d6b54b69
commit 416c0ec952
5 changed files with 108 additions and 1 deletions

View File

@ -0,0 +1,25 @@
'use strict';
const hasInspector = process.config.variables.v8_enable_inspector === 1;
const inspector = hasInspector ? require('inspector') : undefined;
let session;
function sendInspectorCommand(cb, onError) {
if (!hasInspector) return onError();
if (session === undefined) session = new inspector.Session();
try {
session.connect();
try {
return cb(session);
} finally {
session.disconnect();
}
} catch (e) {
return onError();
}
}
module.exports = {
sendInspectorCommand
};

View File

@ -59,6 +59,7 @@ const Module = require('module');
const domain = require('domain');
const debug = util.debuglog('repl');
const errors = require('internal/errors');
const { sendInspectorCommand } = require('internal/util/inspector');
const parentModule = module;
const replMap = new WeakMap();
@ -76,6 +77,7 @@ for (var n = 0; n < GLOBAL_OBJECT_PROPERTIES.length; n++) {
GLOBAL_OBJECT_PROPERTIES[n];
}
const kBufferedCommandSymbol = Symbol('bufferedCommand');
const kContextId = Symbol('contextId');
try {
// hack for require.resolve("./relative") to work properly.
@ -158,6 +160,8 @@ function REPLServer(prompt,
self.last = undefined;
self.breakEvalOnSigint = !!breakEvalOnSigint;
self.editorMode = false;
// Context id for use with the inspector protocol.
self[kContextId] = undefined;
// just for backwards compat, see github.com/joyent/node/pull/7127
self.rli = this;
@ -755,7 +759,16 @@ REPLServer.prototype.createContext = function() {
if (this.useGlobal) {
context = global;
} else {
context = vm.createContext();
sendInspectorCommand((session) => {
session.post('Runtime.enable');
session.on('Runtime.executionContextCreated', ({ params }) => {
this[kContextId] = params.context.id;
});
context = vm.createContext();
session.post('Runtime.disable');
}, () => {
context = vm.createContext();
});
context.global = context;
const _console = new Console(this.outputStream);
Object.defineProperty(context, 'console', {
@ -890,6 +903,18 @@ function filteredOwnPropertyNames(obj) {
return Object.getOwnPropertyNames(obj).filter(intFilter);
}
function getGlobalLexicalScopeNames(contextId) {
return sendInspectorCommand((session) => {
let names = [];
session.post('Runtime.globalLexicalScopeNames', {
executionContextId: contextId
}, (error, result) => {
if (!error) names = result.names;
});
return names;
}, () => []);
}
REPLServer.prototype.complete = function() {
this.completer.apply(this, arguments);
};
@ -1053,6 +1078,7 @@ function complete(line, callback) {
// If context is instance of vm.ScriptContext
// Get global vars synchronously
if (this.useGlobal || vm.isContext(this.context)) {
completionGroups.push(getGlobalLexicalScopeNames(this[kContextId]));
var contextProto = this.context;
while (contextProto = Object.getPrototypeOf(contextProto)) {
completionGroups.push(

View File

@ -129,6 +129,7 @@
'lib/internal/url.js',
'lib/internal/util.js',
'lib/internal/util/comparisons.js',
'lib/internal/util/inspector.js',
'lib/internal/util/types.js',
'lib/internal/http2/core.js',
'lib/internal/http2/compat.js',

View File

@ -0,0 +1,34 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const repl = require('repl');
common.skipIfInspectorDisabled();
// This test verifies that the V8 inspector API is usable in the REPL.
const putIn = new common.ArrayStream();
let output = '';
putIn.write = function(data) {
output += data;
};
const testMe = repl.start('', putIn);
putIn.run(['const myVariable = 42']);
testMe.complete('myVar', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [['myVariable'], 'myVar']);
}));
putIn.run([
'const inspector = require("inspector")',
'const session = new inspector.Session()',
'session.connect()',
'session.post("Runtime.evaluate", { expression: "1 + 1" }, console.log)',
'session.disconnect()'
]);
assert(output.includes(
"null { result: { type: 'number', value: 2, description: '2' } }"));

View File

@ -24,6 +24,7 @@
const common = require('../common');
const assert = require('assert');
const fixtures = require('../common/fixtures');
const hasInspector = process.config.variables.v8_enable_inspector === 1;
// We have to change the directory to ../fixtures before requiring repl
// in order to make the tests for completion of node_modules work properly
@ -529,3 +530,23 @@ editorStream.run(['.editor']);
editor.completer('var log = console.l', common.mustCall((error, data) => {
assert.deepStrictEqual(data, [['console.log'], 'console.l']);
}));
{
// tab completion of lexically scoped variables
const stream = new common.ArrayStream();
const testRepl = repl.start({ stream });
stream.run([`
let lexicalLet = true;
const lexicalConst = true;
class lexicalKlass {}
`]);
['Let', 'Const', 'Klass'].forEach((type) => {
const query = `lexical${type[0]}`;
const expected = hasInspector ? [[`lexical${type}`], query] : [];
testRepl.complete(query, common.mustCall((error, data) => {
assert.deepStrictEqual(data, expected);
}));
});
}