diff --git a/lib/internal/util/inspector.js b/lib/internal/util/inspector.js new file mode 100644 index 00000000000000..634d3302333584 --- /dev/null +++ b/lib/internal/util/inspector.js @@ -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 +}; diff --git a/lib/repl.js b/lib/repl.js index da3ed78e9ebab6..0378a6664c43e4 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -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( diff --git a/node.gyp b/node.gyp index c1c83f6213f262..04d6eff57395b9 100644 --- a/node.gyp +++ b/node.gyp @@ -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', diff --git a/test/parallel/test-repl-inspector.js b/test/parallel/test-repl-inspector.js new file mode 100644 index 00000000000000..b02f6139e72d60 --- /dev/null +++ b/test/parallel/test-repl-inspector.js @@ -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' } }")); diff --git a/test/parallel/test-repl-tab-complete.js b/test/parallel/test-repl-tab-complete.js index 4bf6b7209d0bb4..c9048d887d5cab 100644 --- a/test/parallel/test-repl-tab-complete.js +++ b/test/parallel/test-repl-tab-complete.js @@ -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); + })); + }); +}