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:
parent
d1d6b54b69
commit
416c0ec952
25
lib/internal/util/inspector.js
Normal file
25
lib/internal/util/inspector.js
Normal 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
|
||||||
|
};
|
28
lib/repl.js
28
lib/repl.js
@ -59,6 +59,7 @@ const Module = require('module');
|
|||||||
const domain = require('domain');
|
const domain = require('domain');
|
||||||
const debug = util.debuglog('repl');
|
const debug = util.debuglog('repl');
|
||||||
const errors = require('internal/errors');
|
const errors = require('internal/errors');
|
||||||
|
const { sendInspectorCommand } = require('internal/util/inspector');
|
||||||
|
|
||||||
const parentModule = module;
|
const parentModule = module;
|
||||||
const replMap = new WeakMap();
|
const replMap = new WeakMap();
|
||||||
@ -76,6 +77,7 @@ for (var n = 0; n < GLOBAL_OBJECT_PROPERTIES.length; n++) {
|
|||||||
GLOBAL_OBJECT_PROPERTIES[n];
|
GLOBAL_OBJECT_PROPERTIES[n];
|
||||||
}
|
}
|
||||||
const kBufferedCommandSymbol = Symbol('bufferedCommand');
|
const kBufferedCommandSymbol = Symbol('bufferedCommand');
|
||||||
|
const kContextId = Symbol('contextId');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// hack for require.resolve("./relative") to work properly.
|
// hack for require.resolve("./relative") to work properly.
|
||||||
@ -158,6 +160,8 @@ function REPLServer(prompt,
|
|||||||
self.last = undefined;
|
self.last = undefined;
|
||||||
self.breakEvalOnSigint = !!breakEvalOnSigint;
|
self.breakEvalOnSigint = !!breakEvalOnSigint;
|
||||||
self.editorMode = false;
|
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
|
// just for backwards compat, see github.com/joyent/node/pull/7127
|
||||||
self.rli = this;
|
self.rli = this;
|
||||||
@ -755,7 +759,16 @@ REPLServer.prototype.createContext = function() {
|
|||||||
if (this.useGlobal) {
|
if (this.useGlobal) {
|
||||||
context = global;
|
context = global;
|
||||||
} else {
|
} 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;
|
context.global = context;
|
||||||
const _console = new Console(this.outputStream);
|
const _console = new Console(this.outputStream);
|
||||||
Object.defineProperty(context, 'console', {
|
Object.defineProperty(context, 'console', {
|
||||||
@ -890,6 +903,18 @@ function filteredOwnPropertyNames(obj) {
|
|||||||
return Object.getOwnPropertyNames(obj).filter(intFilter);
|
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() {
|
REPLServer.prototype.complete = function() {
|
||||||
this.completer.apply(this, arguments);
|
this.completer.apply(this, arguments);
|
||||||
};
|
};
|
||||||
@ -1053,6 +1078,7 @@ function complete(line, callback) {
|
|||||||
// If context is instance of vm.ScriptContext
|
// If context is instance of vm.ScriptContext
|
||||||
// Get global vars synchronously
|
// Get global vars synchronously
|
||||||
if (this.useGlobal || vm.isContext(this.context)) {
|
if (this.useGlobal || vm.isContext(this.context)) {
|
||||||
|
completionGroups.push(getGlobalLexicalScopeNames(this[kContextId]));
|
||||||
var contextProto = this.context;
|
var contextProto = this.context;
|
||||||
while (contextProto = Object.getPrototypeOf(contextProto)) {
|
while (contextProto = Object.getPrototypeOf(contextProto)) {
|
||||||
completionGroups.push(
|
completionGroups.push(
|
||||||
|
1
node.gyp
1
node.gyp
@ -129,6 +129,7 @@
|
|||||||
'lib/internal/url.js',
|
'lib/internal/url.js',
|
||||||
'lib/internal/util.js',
|
'lib/internal/util.js',
|
||||||
'lib/internal/util/comparisons.js',
|
'lib/internal/util/comparisons.js',
|
||||||
|
'lib/internal/util/inspector.js',
|
||||||
'lib/internal/util/types.js',
|
'lib/internal/util/types.js',
|
||||||
'lib/internal/http2/core.js',
|
'lib/internal/http2/core.js',
|
||||||
'lib/internal/http2/compat.js',
|
'lib/internal/http2/compat.js',
|
||||||
|
34
test/parallel/test-repl-inspector.js
Normal file
34
test/parallel/test-repl-inspector.js
Normal 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' } }"));
|
@ -24,6 +24,7 @@
|
|||||||
const common = require('../common');
|
const common = require('../common');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const fixtures = require('../common/fixtures');
|
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
|
// We have to change the directory to ../fixtures before requiring repl
|
||||||
// in order to make the tests for completion of node_modules work properly
|
// 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) => {
|
editor.completer('var log = console.l', common.mustCall((error, data) => {
|
||||||
assert.deepStrictEqual(data, [['console.log'], 'console.l']);
|
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);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user