/*
* BSD 3-Clause License
*
* Copyright (c) 2015, Nicolas Riesco and others as credited in the AUTHORS file
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/
/** @module nel
*
* @description Module `nel` provides a Javascript REPL session. A Javascript
* session can be used to run Javascript code within `Node.js`, pass the result
* to a callback function and even capture its `stdout` and `stderr` streams.
*
*/
module.exports = {
Session: Session,
};
var spawn = require("child_process").spawn;
var util = require("util");
var doc = require("./mdn.js"); // Documentation for Javascript builtins
var log = require("./log.js")("NEL:");
var server = require("./server/index.js"); // Server source code
function isPromise(x) {
if (!global.Promise || typeof global.Promise !== "function") {
return false;
}
return x instanceof global.Promise;
}
// File paths
var paths = {
node: process.argv[0],
};
/**
* Javascript session configuration.
*
* @typedef Config
*
* @property {boolean} [awaitExecution] Enable automatic await if the execution
* result is a promise. Default: false.
* @property {string} [cwd] Session current working directory
* @property {module:nel~Transpiler}
* [transpile] Function that transpiles the request code
* into Javascript that can be run by the
* Node.js session.
*
* @see {@link module:nel~Session}
*/
/**
* Function that transpiles the request code into Javascript that can be run by
* the Node.js session.
*
* @typedef Transpiler
*
* @type {function}
* @param {string} code Request code
* @returns {(string|Promise<string>)} Transpiled code
*
* @see {@link module:nel~Config}
*/
/**
* @class
* @classdesc Implements a Node.js session
* @param {module:nel~Config} [nelConfig] Session configuration.
*/
function Session(nelConfig) {
nelConfig = nelConfig || {};
/**
* Session configuration
* @member {module:nel~Config}
* @private
*/
this._nelConfig = {
awaitExecution: !!nelConfig.awaitExecution,
cwd: nelConfig.cwd,
transpile: nelConfig.transpile,
};
/**
* Function that transpiles the request code into Javascript that can be run
* by the Node.js session (null/undefined if no transpilation is needed).
* @member {?module:nel~Transpiler}
*/
this.transpile = this._nelConfig.transpile;
/**
* Queue of tasks to be run
* @member {module:nel~Task[]}
* @private
*/
this._tasks = [];
/**
* Task currently being run (null if the last running task has finished)
* @member {module:nel~Task}
* @private
*/
this._currentTask = null;
/**
* Table of execution contexts
* (execution contexts are created to allow asynchronous execution of cells)
* (indexed by execution context ID)
* (contexts that complete their execution are removed from this table)
* @member {Object.<number, module:nel~Task>}
* @private
*/
this._contextTable = {};
/**
* Table of execution contexts that create a display ID
* (displays are created to allow multiple outputs per cell)
* (indexed by execution context ID)
* (cannot use this._contextTable)
* @member {Object.<number, module:nel~Task>}
* @private
*/
this._displayTable = {};
/**
* Last execution context id (0 if none have been created)
* @member {number}
* @private
*/
this._lastContextId = 0;
/**
* Last run task (null if none have been run)
* @member {module:nel~Task}
* @private
*/
this._lastTask = null;
/**
* Server that runs the code requests for this session
* @member {module:child_process~ChildProcess}
* @private
*/
this._server = spawn(Session._command, Session._args, {
cwd: this._nelConfig.cwd,
stdio: global.DEBUG ?
[process.stdin, process.stdout, process.stderr, "ipc"] :
["ignore", "ignore", "ignore", "ipc"],
});
this._server.on("message", Session.prototype._onMessage.bind(this));
this._server.on("exit", (function(code, signal) {
log("SESSION: EXIT:", code, signal);
this._status = "dead";
}).bind(this));
/**
* Server status as a string that can take the following values:
* "starting" (status from the time the server is spawned until the time
* the server is ready to receive requests);
* "online" (status when the server is ready to receive requests);
* "dead" (status after receiving a request to kill the server
* {@link module:nel~Session.kill}).
* @member {string}
* @private
*/
this._status = "starting";
// Note that Session#execute will queue the execution request
// until Session#_status becomes 'online'.
this.execute(
"$$.config.awaitExecution = " + this._nelConfig.awaitExecution + ";"
);
}
/**
* Path to node executable
* @member {String}
* @private
*/
Session._command = paths.node;
/**
* Arguments passed onto the node executable
* @member {String[]}
* @private
*/
Session._args = ["--eval", server]; // --eval workaround
/**
* Combination of a piece of code to be run within a session and all the
* associated callbacks.
* @see {@link module:nel~Session#_run}
*
* @typedef Task
*
* @property {string} action Type of task:
* "run" to evaluate a piece of
* code and return the result;
* "getAllPropertyNames" to
* evaluate a piece of code and
* return all the property names
* of the result;
* "inspect" to inspect an object
* and return information such as
* the list of constructors,
* string representation,
* length...
* @property {string} code Code to evaluate
* @property {module:nel~OnSuccessCB} [onSuccess] Called if no errors occurred
* @property {module:nel~OnErrorCB} [onError] Called if an error occurred
* @property {module:nel~BeforeRunCB} [beforeRun] Called before running the code
* @property {module:nel~AfterRunCB} [afterRun] Called after running the code
* @property {module:nel~OnStdioCB} [onStdout] Called if process.stdout data
* @property {module:nel~OnStdioCB} [onStderr] Called if process.stderr data
* @property {module:nel~OnDisplayCB} [onDisplay] Called on display updates
* @property {module:nel~OnRequestCB} [onRequest] Called on requests
*
* @private
*/
/**
* Callback run with a request
* @see {@link module:nel~Session#execute}
*
* @callback OnRequestCB
* @param {module:nel~RequestMessage} request Request
* @param {module:nel~onReplyCB} onReply Callback invoked with the reply
*/
/**
* Callback run with a reply
* @see {@link module:nel~Session#execute}
*
* @callback OnReplyCB
* @param {module:nel~ReplyMessage} reply Reply
*/
/**
* Callback run with a display update
* @see {@link module:nel~Session#execute}
*
* @callback OnDisplayCB
* @param {module:nel~DisplayMessage} update Display ID and MIME bundle
*/
/**
* Callback invoked with the data written on `process.stdout` or
* `process.stderr` after a request to the server.
* @see {@link module:nel~Task}
*
* @callback OnStdioCB
* @param {string} data
*/
/**
* Callback invoked before running a task
* @see {@link module:nel~Task}
*
* @callback BeforeRunCB
*/
/**
* Callback invoked after running a task (regardless of success or failure)
* @see {@link module:nel~Task}
*
* @callback AfterRunCB
*/
/**
* Callback invoked with the error obtained while running a task
* @see {@link module:nel~Task}
*
* @callback OnErrorCB
* @param {module:nel~ErrorResult} error
*/
/**
* Callback invoked with the result of a task
* @see {@link module:nel~Task}
*
* @typedef OnSuccessCB {
* module:nel~OnExecutionSuccessCB |
* module:nel~OnCompletionSuccessCB |
* module:nel~OnInspectionSuccessCB |
* module:nel~OnNameListSuccessCB
* }
*/
/**
* Callback run with the result of an execution request
* @see {@link module:nel~Session#execute}
*
* @callback OnExecutionSuccessCB
* @param {module:nel~ExecutionMessage} result MIME representations
*/
/**
* Callback run with the result of an completion request
* @see {@link module:nel~Session#complete}
*
* @callback OnCompletionSuccessCB
* @param {module:nel~CompletionMessage} result Completion request results
*/
/**
* Callback run with the result of an inspection request
* @see {@link module:nel~Session#inspect}
*
* @callback OnInspectionSuccessCB
* @param {module:nel~InspectionMessage} result Inspection request result
*/
/**
* Callback run with the list of all the property names
*
* @callback OnNameListSuccessCB
* @param {module:nel~NameListMessage} result List of all the property names
*
* @private
*/
/**
* Callback run after the session server has been killed
* @see {@link module:nel~Session#kill}
*
* @callback KillCB
* @param {Number} [code] Exit code from session server if exited normally
* @param {String} [signal] Signal passed to kill the session server
*/
/**
* Callback run after the session server has been restarted
* @see {@link module:nel~Session#restart}
*
* @callback RestartCB
* @param {Number} [code] Exit code from old session if exited normally
* @param {String} [signal] Signal passed to kill the old session
*/
/**
* Message received from the session server
*
* @typedef Message {
* module:nel~LogMessage |
* module:nel~RequestMessage |
* module:nel~DisplayMessage |
* module:nel~StatusMessage |
* module:nel~StdoutMessage |
* module:nel~StderrMessage |
* module:nel~ErrorMessage |
* module:nel~SuccessMessage
* }
*/
/**
* Log message received from the session server
*
* @typedef LogMessage
*
* @property {string} log Message for logging purposes
*
* @private
*/
/**
* Request message
*
* @typedef RequestMessage
*
* @property {number} [id] Execution context id
* (deleted before the message reaches the API user)
*
* @property {object} request
* @property {number} [request.id] Request id
* (deleted before reaching the API user)
* @property {object} [request.input] Input request
* @property {string} [request.input.prompt] Prompt message
* @property {boolean} [request.input.password] Treat input as a password
*
* @property {object} [request.clear] Request to clear the output
* @property {boolean} [request.clear.wait] Whether to wait for an update
* before clearing the output
*/
/**
* Reply message
*
* @typedef ReplyMessage
*
* @property {string} [input] Input reply
*/
/**
* Display message received from the session server
*
* @typedef DisplayMessage
*
* @property {number} [id] Execution context id
* (deleted before the message reaches the API user)
* @property {string} [open] Display id
* (only present in the opening message)
* @property {string} [display_id] Display id
* (only present in update messages)
* @property {string} [close] Display id
* (only present in the closing message)
* @property [mime] (not present in the opening and closing messages)
* @property {string} [mime."text/plain"] Plain text
* @property {string} [mime."text/html"] HTML format
* @property {string} [mime."image/svg+xml"] SVG format
* @property {string} [mime."image/png"] PNG as a base64 string
* @property {string} [mime."image/jpeg"] JPEG as a base64 string
*
* @private
*/
/**
* Status message received from the session server
*
* @typedef StatusMessage
*
* @property {string} status Message for reporting a server status change:
* "online" (when the server is ready to process
* requests).
*
* @private
*/
/**
* Stdout message received from the session server
*
* @typedef StdoutMessage
*
* @property {number} id Execution context id
* @property {string} stdout Data written on the session stdout
*
* @private
*/
/**
* Stderr message received from the session server
*
* @typedef StderrMessage
*
* @property {number} id Execution context id
* @property {string} stderr Data written on the session stderr
*
* @private
*/
/**
* Error thrown when running a task within a session
* @see {@link module:nel~Session#execute}, {@link module:nel~Session#complete},
* and {@link module:nel~Session#inspect}
*
* @typedef ErrorMessage
*
* @property {number} [id] Execution context id
* (deleted before passing the message
* onto the API user)
* @property {boolean} [end] Flag to terminate the execution context
* @property error
* @property {String} error.ename Error name
* @property {String} error.evalue Error value
* @property {String[]} error.traceback Error traceback
*/
/**
* Request result
* @see {@link module:nel~Session#execute}, {@link module:nel~Session#complete},
* and {@link module:nel~Session#inspect}
*
* @typedef SuccessMessage {
* module:nel~ExecutionMessage |
* module:nel~CompletionMessage |
* module:nel~InspectionMessage |
* module:nel~NameListMessage
* }
*/
/**
* MIME representations of the result of an execution request
* @see {@link module:nel~Session#execute}
*
* @typedef ExecutionMessage
*
* @property {number} [id] Execution context id
* (deleted before the message reaches the API user)
* @property {boolean} [end] Flag to terminate the execution context
* @property mime
* @property {string} [mime."text/plain"] Result in plain text
* @property {string} [mime."text/html"] Result in HTML format
* @property {string} [mime."image/svg+xml"] Result in SVG format
* @property {string} [mime."image/png"] Result as PNG in a base64 string
* @property {string} [mime."image/jpeg"] Result as JPEG in a base64 string
*/
/**
* Results of a completion request
* @see {@link module:nel~Session#complete}
*
* @typedef CompletionMessage
*
* @property {number} [id] Execution context id
* (deleted before passing the
* message onto the API user)
* @property completion
* @property {String[]} completion.list Array of completion matches
* @property {String} completion.code Javascript code to be completed
* @property {Integer} completion.cursorPos Cursor position within
* `completion.code`
* @property {String} completion.matchedText Text within `completion.code`
* that has been matched
* @property {Integer} completion.cursorStart Position of the start of
* `completion.matchedText` within
* `completion.code`
* @property {Integer} completion.cursorEnd Position of the end of
* `completion.matchedText` within
* `completion.code`
*/
/**
* Results of an inspection request
* @see {@link module:nel~Session#inspect}
*
* @typedef InspectionMessage
*
* @property {number} [id] Execution context id
* (deleted before passing the
* message onto the API user)
* @property inspection
* @property {String} inspection.code Javascript code to be inspected
* @property {Integer} inspection.cursorPos Cursor position within
* `inspection.code`.
* @property {String} inspection.matchedText Text within `inspection.code`
* that has been matched as an
* expression.
* @property {String} inspection.string String representation
* @property {String} inspection.type Javascript type
* @property {String[]} [inspection.constructorList]
* List of constructors (not
* defined for `null` or
* `undefined`).
* @property {Integer} [inspection.length] Length property (if present)
*
* @property [doc] Defined only for calls to {@link
* module:nel~inspect} that succeed
* to find documentation for a
* Javascript expression
* @property {String} doc.description Description
* @property {String} [doc.usage] Usage
* @property {String} doc.url Link to the documentation source
*/
/**
* Results of an "getAllPropertyNames" action
* @see {@link module:nel~Task}
*
* @typedef NameListMessage
*
* @property {number} [id] Execution context id
* (deleted before the message reaches the API user)
* @property {String[]} names List of all property names
*
* @private
*/
/* eslint-disable complexity */
/**
* Callback to handle messages from the session server
*
* @param {module:nel~Message} message
* @private
*/
Session.prototype._onMessage = function(message) {
// Handle message.log
if (message.hasOwnProperty("log")) {
log(message.log);
return;
}
log("SESSION: RECEIVED:", message);
// Handle message.status
if (message.status === "online") {
log("SESSION: ONLINE");
this._status = message.status;
this._runNext();
return;
}
var contextId = message.id;
delete message.id;
var endMessage = message.end;
delete message.end;
// Handle message.display
if (message.hasOwnProperty("display")) {
var displayId;
var displayTask;
// Handle message.display.open
// (keep track of the task that opened the display id,
// so that it can be updated by other execution requests)
if (message.display.hasOwnProperty("open")) {
displayId = message.display.open;
displayTask = this._contextTable[contextId] || this._lastTask;
if (displayTask && displayTask.onDisplay) {
this._displayTable[displayId] = displayTask;
} else {
log("SESSION: RECEIVED: DISPLAY: OPEN: Missing onDisplay");
}
return;
}
// Handle message.display.mime
if (message.display.hasOwnProperty("mime")) {
if (message.display.hasOwnProperty("display_id")) {
displayId = message.display.display_id;
displayTask = this._displayTable[displayId] || this._lastTask;
} else {
displayTask = this._contextTable[contextId] || this._lastTask;
}
if (displayTask && displayTask.onDisplay) {
displayTask.onDisplay(message.display);
} else {
log("SESSION: RECEIVED: DISPLAY: UPDATE: Missing onDisplay");
}
return;
}
// Handle message.display.close
// (unsupported by Jupyter Messaging Protocol)
if (message.display.hasOwnProperty("close")) {
displayId = message.display.close;
delete this._displayTable[displayId];
return;
}
}
// Get execution context
// (if context is missing, default to using the last context)
var task = this._contextTable[contextId];
if (!task) {
log(
"SESSION: CONTEXT: Missing context, using last context, id =",
contextId
);
task = this._lastTask;
if (!task) {
log("SESSION: RECEIVED: DROPPED: There is no last context");
return;
}
}
// Handle message.request
if (message.hasOwnProperty("request")) {
if (task.onRequest) {
var request = message.request;
if (request.hasOwnProperty("clear")) {
// clear_output requests don't get a reply
task.onRequest(request);
} else {
var requestId = message.request.id;
delete request.id;
task.onRequest(request, function onReply(reply) {
// TODO: handle the case when reply is an instance of Error
this._server.send(["reply", reply, contextId, requestId]);
}.bind(this));
}
} else {
log("SESSION: RECEIVED: REQUEST: Missing request callback");
}
return;
}
// Handle message.stdout
if (message.hasOwnProperty("stdout")) {
if (task.onStdout) {
task.onStdout(message.stdout);
} else {
log("SESSION: RECEIVED: STDOUT: Missing stdout callback");
}
return;
}
// Handle message.stderr
if (message.hasOwnProperty("stderr")) {
if (task.onStderr) {
task.onStderr(message.stderr);
} else {
log("SESSION: RECEIVED: STDERR: Missing stderr callback");
}
return;
}
// Handle error and success messages
if (message.hasOwnProperty("error")) {
if (task.onError) {
task.onError(message);
} else {
log("SESSION: RECEIVED: ERROR: Missing onError callback");
}
} else {
if (task.onSuccess) {
task.onSuccess(message);
} else {
log("SESSION: RECEIVED: SUCCESS: Missing onSuccess callback");
}
}
// Handle message.end
if (endMessage) {
if (task) {
log("SESSION: RECEIVED: END: id =", contextId);
delete this._contextTable[contextId];
if (task.afterRun) {
task.afterRun();
}
} else {
log("SESSION: RECEIVED: END: DROPPED: id =", contextId);
}
}
// If the task for this message is the last running task,
// proceed to run the next task on the queue.
if (task && task === this._currentTask) {
this._currentTask = null;
this._runNext();
}
};
/* eslint-enable complexity */
/**
* Run a task
*
* @param {module:nel~Task} task
* @private
*/
Session.prototype._run = function(task) {
if (this._status === "online" && this._currentTask === null) {
this._runNow(task);
} else if (this._status !== "dead") {
this._runLater(task);
}
};
/**
* Run a task now
*
* @param {module:nel~Task} task
* @private
*/
Session.prototype._runNow = function(task) {
var id = this._lastContextId + 1;
log("SESSION: RUN: TASK:", id, task);
this._currentTask = task;
this._lastTask = task;
this._lastContextId = id;
this._contextTable[id] = task;
if (task.beforeRun) {
task.beforeRun();
}
var sendTask = (function sendTask() {
this._server.send([task.action, task.code, id]);
}).bind(this);
var sendError = (function sendError(error) {
this._onMessage({
id: id,
end: true,
error: {
ename: (error && error.name) ?
error.name : typeof error,
evalue: (error && error.message) ?
error.message : util.inspect(error),
traceback: (error && error.stack) ?
error.stack.split("\n") : "",
},
});
}).bind(this);
if (this.transpile && task.action === "run") {
try {
// Adapted from https://github.com/n-riesco/nel/issues/1 by kebot
var transpiledCode = this.transpile(task.code);
log("SESSION: RUN: TRANSPILE:\n" + transpiledCode + "\n");
if (isPromise(transpiledCode)) {
transpiledCode.then(function(value) {
task.code = value;
sendTask();
}).catch(sendError);
return;
} else {
task.code = transpiledCode;
}
} catch (error) {
sendError(error);
return;
}
}
sendTask();
return;
};
/**
* Run a task later
*
* @param {module:nel~Task} task
* @private
*/
Session.prototype._runLater = function(task) {
log("SESSION: QUEUE: TASK:", task);
this._tasks.push(task);
};
/**
* Run next task (if any)
*
* @private
*/
Session.prototype._runNext = function() {
var task = this._tasks.shift();
if (task) {
this._runNow(task);
}
};
/**
* Make an execution request
*
* @param {String} code Code to execute in session
* @param [callbacks]
* @param {OnExecutionSuccessCB} [callbacks.onSuccess]
* @param {OnErrorCB} [callbacks.onError]
* @param {BeforeRunCB} [callbacks.beforeRun]
* @param {AfterRunCB} [callbacks.afterRun]
* @param {OnStdioCB} [callbacks.onStdout]
* @param {OnStdioCB} [callbacks.onStderr]
* @param {OnDisplayCB} [callbacks.onDisplay]
* @param {OnRequestCB} [callbacks.onRequest]
*/
Session.prototype.execute = function(code, callbacks) {
log("SESSION: EXECUTE:", code);
var task = {
action: "run",
code: code,
};
if (callbacks) {
if (callbacks.onSuccess) {
task.onSuccess = callbacks.onSuccess;
}
if (callbacks.onError) {
task.onError = callbacks.onError;
}
if (callbacks.beforeRun) {
task.beforeRun = callbacks.beforeRun;
}
if (callbacks.afterRun) {
task.afterRun = callbacks.afterRun;
}
if (callbacks.onStdout) {
task.onStdout = callbacks.onStdout;
}
if (callbacks.onStderr) {
task.onStderr = callbacks.onStderr;
}
if (callbacks.onDisplay) {
task.onDisplay = callbacks.onDisplay;
}
if (callbacks.onRequest) {
task.onRequest = callbacks.onRequest;
}
}
this._run(task);
};
/* eslint-disable complexity */
/**
* Complete a Javascript expression
*
* @param {String} code Javascript code
* @param {Number} cursorPos Cursor position in code
* @param [callbacks]
* @param {OnCompletionSuccessCB} [callbacks.onSuccess]
* @param {OnErrorCB} [callbacks.onError]
* @param {BeforeRunCB} [callbacks.beforeRun]
* @param {AfterRunCB} [callbacks.afterRun]
* @param {OnStdioCB} [callbacks.onStdout]
* @param {OnStdioCB} [callbacks.onStderr]
*/
Session.prototype.complete = function(code, cursorPos, callbacks) {
var matchList = [];
var matchedText;
var cursorStart;
var cursorEnd;
var expression = parseExpression(code, cursorPos);
log("SESSION: COMPLETE: expression", expression);
if (expression === null) {
if (callbacks) {
if (callbacks.beforeRun) {
callbacks.beforeRun();
}
if (callbacks.onSuccess) {
callbacks.onSuccess({
completion: {
list: matchList,
code: code,
cursorPos: cursorPos,
matchedText: "",
cursorStart: cursorPos,
cursorEnd: cursorPos,
},
});
}
if (callbacks.afterRun) {
callbacks.afterRun();
}
}
return;
}
var task = {
action: "getAllPropertyNames",
code: (expression.scope === "") ? "global" : expression.scope,
};
if (callbacks) {
if (callbacks.onError) {
task.onError = callbacks.onError;
}
if (callbacks.beforeRun) {
task.beforeRun = callbacks.beforeRun;
}
if (callbacks.afterRun) {
task.afterRun = callbacks.afterRun;
}
if (callbacks.onStdout) {
task.onStdout = callbacks.onStdout;
}
if (callbacks.onStderr) {
task.onStderr = callbacks.onStderr;
}
}
task.onSuccess = function(result) {
// append list of all property names
matchList = matchList.concat(result.names);
// append list of reserved words
if (expression.scope === "") {
matchList = matchList.concat(javascriptKeywords);
}
// filter matches
if (expression.selector) {
matchList = matchList.filter(function(e) {
return e.lastIndexOf(expression.selector, 0) === 0;
});
}
// append expression.rightOp to each match
var left = expression.scope + expression.leftOp;
var right = expression.rightOp;
if (left || right) {
matchList = matchList.map(function(e) {
return left + e + right;
});
}
// find range of text that should be replaced
if (matchList.length > 0) {
var shortestMatch = matchList.reduce(function(p, c) {
return p.length <= c.length ? p : c;
});
cursorStart = code.indexOf(expression.matchedText);
cursorEnd = cursorStart;
var cl = code.length;
var ml = shortestMatch.length;
for (var i = 0; i < ml && cursorEnd < cl; i++, cursorEnd++) {
if (shortestMatch.charAt(i) !== code.charAt(cursorEnd)) {
break;
}
}
} else {
cursorStart = cursorPos;
cursorEnd = cursorPos;
}
// return completion results to the callback
matchedText = expression.matchedText;
if (callbacks && callbacks.onSuccess) {
callbacks.onSuccess({
completion: {
list: matchList,
code: code,
cursorPos: cursorPos,
matchedText: matchedText,
cursorStart: cursorStart,
cursorEnd: cursorEnd,
},
});
}
};
this._run(task);
};
/* eslint-enable complexity */
/**
* Inspect a Javascript expression
*
* @param {String} code Javascript code
* @param {Number} cursorPos Cursor position in code
* @param [callbacks]
* @param {OnInspectionSuccessCB} [callbacks.onSuccess]
* @param {OnErrorCB} [callbacks.onError]
* @param {BeforeRunCB} [callbacks.beforeRun]
* @param {AfterRunCB} [callbacks.afterRun]
* @param {OnStdioCB} [callbacks.onStdout]
* @param {OnStdioCB} [callbacks.onStderr]
*/
Session.prototype.inspect = function(code, cursorPos, callbacks) {
var expression = parseExpression(code, cursorPos);
log("SESSION: INSPECT: expression:", expression);
if (expression === null) {
if (callbacks) {
if (callbacks.beforeRun) {
callbacks.beforeRun();
}
if (callbacks.onSuccess) {
callbacks.onSuccess({
inspection: {
code: code,
cursorPos: cursorPos,
matchedText: "",
string: "",
type: ""
},
});
}
if (callbacks.afterRun) {
callbacks.afterRun();
}
}
return;
}
var inspectionResult;
var task = {
action: "inspect",
code: expression.matchedText,
};
if (callbacks) {
if (callbacks.onError) {
task.onError = callbacks.onError;
}
if (callbacks.beforeRun) {
task.beforeRun = callbacks.beforeRun;
}
if (callbacks.onStdout) {
task.onStdout = callbacks.onStdout;
}
if (callbacks.onStderr) {
task.onStderr = callbacks.onStderr;
}
}
task.onSuccess = (function(result) {
inspectionResult = result;
inspectionResult.inspection.code = code;
inspectionResult.inspection.cursorPos = cursorPos;
inspectionResult.inspection.matchedText = expression.matchedText;
getDocumentationAndInvokeCallbacks.call(this);
}).bind(this);
this._run(task);
return;
function getDocumentationAndInvokeCallbacks() {
var doc;
// Find documentation associated with the matched text
if (!expression.scope) {
doc = getDocumentation(expression.matchedText);
if (doc) {
inspectionResult.doc = doc;
}
if (callbacks) {
if (callbacks.onSuccess) {
callbacks.onSuccess(inspectionResult);
}
if (callbacks.afterRun) {
callbacks.afterRun();
}
}
return;
}
// Find documentation by searching the chain of constructors
var task = {
action: "inspect",
code: expression.scope,
};
if (callbacks) {
if (callbacks.onError) {
task.onError = callbacks.onError;
}
if (callbacks.afterRun) {
task.afterRun = callbacks.afterRun;
}
if (callbacks.onStdout) {
task.onStdout = callbacks.onStdout;
}
if (callbacks.onStderr) {
task.onStderr = callbacks.onStderr;
}
}
task.onSuccess = function(result) {
var constructorList = result.inspection.constructorList;
if (constructorList) {
for (var i in constructorList) {
var constructorName = constructorList[i];
doc = getDocumentation(
constructorName +
".prototype." +
expression.selector
);
if (doc) {
inspectionResult.doc = doc;
break;
}
}
}
if (callbacks && callbacks.onSuccess) {
callbacks.onSuccess(inspectionResult);
}
};
this._run(task);
}
};
/**
* Kill session
*
* @param {String} [signal="SIGTERM"] Signal passed to kill the
* session server
* @param {module:nel~KillCB} [killCB] Callback run after the
* session server has been
* killed
*/
Session.prototype.kill = function(signal, killCB) {
this._status = "dead";
this._server.removeAllListeners();
this._server.on("exit", (function(code, signal) {
if (killCB) {
killCB(code, signal);
}
}).bind(this));
this._server.kill(signal || "SIGTERM");
};
/**
* Restart session
*
* @param {String} [signal="SIGTERM"] Signal passed to kill the
* old session
* @param {module:nel~RestartCB} [restartCB] Callback run after restart
*/
Session.prototype.restart = function(signal, restartCB) {
this._nelConfig.transpile = this.transpile;
this.kill(signal || "SIGTERM", (function(code, signal) {
Session.call(this, this._nelConfig);
if (restartCB) {
restartCB(code, signal);
}
}).bind(this));
};
/**
* List of Javascript reserved words (ecma-262)
* @member {RegExp}
* @private
*/
var javascriptKeywords = [
// keywords
"break", "case", "catch", "continue", "debugger", "default",
"delete", "do", "else", "finally", "for", "function", "if",
"in", "instanceof", "new", "return", "switch", "this",
"throw", "try", "typeof", "var", "void", "while", "with",
// future reserved words
"class", "const", "enum", "export", "extends", "import",
"super",
// future reserved words in strict mode
"implements", "interface", "let", "package", "private",
"protected", "public", "static", "yield",
// null literal
"null",
// boolean literals
"true", "false"
];
/**
* RegExp for whitespace
* @member {RegExp}
* @private
*/
var whitespaceRE = /\s/;
/**
* RegExp for a simple identifier in Javascript
* @member {RegExp}
* @private
*/
var simpleIdentifierRE = /[_$a-zA-Z][_$a-zA-Z0-9]*$/;
/**
* RegExp for a complex identifier in Javascript
* @member {RegExp}
* @private
*/
var complexIdentifierRE = /[_$a-zA-Z][_$a-zA-Z0-9]*(?:[_$a-zA-Z][_$a-zA-Z0-9]*|\.[_$a-zA-Z][_$a-zA-Z0-9]*|\[".*"\]|\['.*'\])*$/; // eslint-disable-line max-len
/**
* Javascript expression
*
* @typedef Expression
*
* @property {String} matchedText Matched expression, e.g. `foo["bar`
* @property {String} scope Scope of the matched property, e.g. `foo`
* @property {String} leftOp Left-hand-side selector operator, e.g. `["`
* @property {String} selector Stem of the property being matched, e.g. `bar`
* @property {String} rightOp Right-hand-side selector operator, e.g. `"]`
*
* @see {@link module:nel~parseExpression}
* @private
*/
/**
* Parse a Javascript expression
*
* @param {String} code Javascript code
* @param {Number} cursorPos Cursor position within `code`
*
* @returns {module:nel~Expression}
*
* @todo Parse expressions with parenthesis
* @private
*/
function parseExpression(code, cursorPos) {
var expression = code.slice(0, cursorPos);
if (!expression ||
whitespaceRE.test(expression[expression.length - 1])) {
return {
matchedText: "",
scope: "",
leftOp: "",
selector: "",
rightOp: "",
};
}
var selector;
var re = simpleIdentifierRE.exec(expression);
if (re === null) {
selector = "";
} else {
selector = re[0];
expression = expression.slice(0, re.index);
}
var leftOp;
var rightOp;
if (expression[expression.length - 1] === ".") {
leftOp = ".";
rightOp = "";
expression = expression.slice(0, expression.length - 1);
} else if (
(expression[expression.length - 2] === "[") &&
(expression[expression.length - 1] === "\"")
) {
leftOp = "[\"";
rightOp = "\"]";
expression = expression.slice(0, expression.length - 2);
} else if (
(expression[expression.length - 2] === "[") &&
(expression[expression.length - 1] === "'")
) {
leftOp = "['";
rightOp = "']";
expression = expression.slice(0, expression.length - 2);
} else {
return {
matchedText: code.slice(expression.length, cursorPos),
scope: "",
leftOp: "",
selector: selector,
rightOp: "",
};
}
var scope;
re = complexIdentifierRE.exec(expression);
if (re) {
scope = re[0];
return {
matchedText: code.slice(re.index, cursorPos),
scope: scope,
leftOp: leftOp,
selector: selector,
rightOp: rightOp,
};
} else if (!leftOp) {
scope = "";
return {
matchedText: code.slice(expression.length, cursorPos),
scope: scope,
leftOp: leftOp,
selector: selector,
rightOp: rightOp,
};
}
// Not implemented
return null;
}
/**
* Javascript documentation
*
* @typedef Documentation
*
* @property {String} description Description
* @property {String} [usage] Usage
* @property {String} url Link to documentation source
* @private
*/
/**
* Get Javascript documentation
*
* @param {String} name Javascript name
*
* @returns {?module:parser~Documentation}
* @private
*/
function getDocumentation(name) {
var builtinName = name;
if (builtinName in doc) {
return doc[builtinName];
}
builtinName = name.replace(/^[a-zA-Z]+Error./, "Error.");
if (builtinName in doc) {
return doc[builtinName];
}
builtinName = name.replace(/^[a-zA-Z]+Array./, "TypedArray.");
if (builtinName in doc) {
return doc[builtinName];
}
return null;
}