From 30273d400c2ef1c67276dfbff9e3b4a103413224 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= Date: Sun, 29 Oct 2017 18:19:24 +0100 Subject: [PATCH] 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 Reviewed-By: Ruben Bridgewater --- lib/internal/util/inspector.js | 25 ++++++++++++++++++ lib/repl.js | 28 +++++++++++++++++++- node.gyp | 1 + test/parallel/test-repl-inspector.js | 34 +++++++++++++++++++++++++ test/parallel/test-repl-tab-complete.js | 21 +++++++++++++++ 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 lib/internal/util/inspector.js create mode 100644 test/parallel/test-repl-inspector.js 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 0d1ee052bc08e6..f9d6b284b1b3d9 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -58,6 +58,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(); @@ -75,6 +76,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. @@ -155,6 +157,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; @@ -644,7 +648,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', { @@ -779,6 +792,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); }; @@ -942,6 +967,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 9578e01a906f41..46796d01fd800f 100644 --- a/node.gyp +++ b/node.gyp @@ -127,6 +127,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); + })); + }); +}