config/emacs.d/swank-js/swank-handler.js
2011-06-26 15:55:20 -07:00

416 lines
13 KiB
JavaScript

// -*- mode: js2; js-run: "swank-handler-tests.js" -*-
//
// Copyright (c) 2010 Ivan Shvedunov. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// * 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.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
// 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 AUTHOR 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.
var EventEmitter = require("events").EventEmitter;
var Script = process.binding('evals').Script;
var evalcx = Script.runInContext;
var util = require("util");
var url = require("url");
var assert = process.assert;
var lisp = require("./lisp");
var S = lisp.S, list = lisp.list, consp = lisp.consp, car = lisp.car, cdr = lisp.cdr,
repr = lisp.repr, fromLisp = lisp.fromLisp, toLisp = lisp.toLisp;
var DEFAULT_SLIME_VERSION = "2010-11-13";
function Handler (executive) {
this.executive = executive;
var self = this;
this.executive.on("output", function (str) { self.output(str); });
this.executive.on("newPackage", function (name) { self.newPackage(name); });
};
util.inherits(Handler, EventEmitter);
Handler.prototype.receive = function receive (message) {
// FIXME: error handling
console.log("Handler.prototype.receive(): %s", repr(message).replace(/\n/, "\\n"));
if (!consp(message) || car(message) != S(":emacs-rex")) {
console.log("bad message: %s", message);
return;
}
var d, expr;
try {
d = fromLisp(message, ["S:op", ">:form",
["S:name", "R*:args"],
"_:package", "_:threadId", "N:id"]);
} catch (e) {
if (e instanceof TypeError) {
console.log("failed to parse %s: %s", message, e);
return; // FIXME
}
throw e;
}
var r = { status: ":ok", result: null };
var self = this;
var cont = function cont () {
self.sendResponse({ r: r, id: d.id },
[S(":return"), ">:r", ["S:status", "_:result"], "N:id"]);
};
switch (d.form.name) {
case "swank:connection-info":
this.executive.connectionInfo(
function (info) {
console.log("info = %j", info);
r.result = toLisp(
info,
{ "pid": "N:pid",
"encoding": { name: "encoding", spec: { "coding-system": "s:codingSystem",
"external-format": "s:externalFormat" } },
"package": { name: "packageSpec", spec: { name: "s", prompt: "s" } },
"lisp-implementation": {
name: "implementation",
spec: { type: "s", name: "s", version: "s" } },
"version": "s:version" });
cont();
});
return;
case "swank:create-repl":
r.result = toLisp(this.executive.createRepl(), ["s:packageName", "s:prompt"]);
break;
case "swank:autodoc":
r.result = S(":not-available");
break;
case "js:list-remotes":
// FIXME: support 'list of similar elements' type spec
r.result = toLisp(
this.executive.listRemotes().map(
function (item) {
return toLisp(item, ["N:index", "K:kind", "s:id", "B:isActive"]);
}, this));
break;
case "js:select-remote":
if (d.form.args.length != 2) {
console.log("bad args len for SWANK:SELECT-REMOTE -- %s", d.form.args.length);
return; // FIXME
}
// FIXME: get rid of spaghetti
var remoteIndex, sticky;
try {
// FIXME: args should be a cons / NIL
remoteIndex = fromLisp(d.form.args[0], "N");
sticky = fromLisp(d.form.args[1]);
} catch (e) {
if (e instanceof TypeError) {
console.log("can't parse arg -- %s", d.form.args[0]);
return; // FIXME
}
throw e;
}
this.executive.selectRemote(remoteIndex, sticky);
break;
case "js:set-target-url":
case "js:set-slime-version":
if (d.form.args.length != 1) {
console.log("bad args len for JS:SET-TARGET-URL -- %s", d.form.args.length);
return; // FIXME
}
try {
expr = fromLisp(d.form.args[0], "s");
} catch (e) {
if (e instanceof TypeError) {
console.log("can't parse arg -- %s", d.form.args[0]);
return; // FIXME
}
throw e;
}
this.executive[d.form.name == "js:set-target-url" ? "setTargetUrl" : "setSlimeVersion"](expr);
break;
case "swank:interactive-eval":
case "swank:listener-eval":
if (d.form.args.length != 1) {
console.log("bad args len for SWANK:LISTENER-EVAL -- %s", d.form.args.length);
return; // FIXME
}
try {
expr = fromLisp(d.form.args[0], "s");
} catch (e) {
if (e instanceof TypeError) {
console.log("can't parse arg -- %s", d.form.args[0]);
return; // FIXME
}
throw e;
}
this.executive.listenerEval(
expr, function (values) {
if (values.length)
r.result = toLisp({ values: values }, [S(":values"), "R:values"]);
cont();
});
return;
default:
// FIXME: handle unknown commands
}
cont();
};
Handler.prototype.output = function output (str) {
this.sendResponse([S(":write-string"), str]);
};
Handler.prototype.newPackage = function newPackage (name) {
this.sendResponse([S(":new-package"), name, name]);
};
Handler.prototype.sendResponse = function sendResponse(response, spec)
{
this.emit("response", repr(toLisp(response, spec || "@")));
};
function Remote () {};
util.inherits(Remote, EventEmitter);
Remote.prototype.prompt = function prompt () {
return "JS";
};
Remote.prototype.kind = function kind () {
throw new Error("must override Remote.prototype.kind()");
};
Remote.prototype.id = function id () {
throw new Error("must override Remote.prototype.id()");
};
Remote.prototype.evaluate = function evaluate (id, str) {
throw new Error("must override Remote.prototype.evaluate()");
};
Remote.prototype.fullName = function fullName () {
return "(" + this.kind() + ") " + this.id();
};
Remote.prototype.disconnect = function disconnect () {
this.emit("disconnect");
};
Remote.prototype.detachSelf = function detachSelf () {
this.removeAllListeners("output");
this.removeAllListeners("disconnect");
this.removeAllListeners("result");
};
Remote.prototype.output = function output (str) {
this.emit("output", String(str));
};
Remote.prototype.setIndex = function setIndex (n) {
this._index = n;
};
Remote.prototype.index = function index () {
return this._index;
};
Remote.prototype.sendResult = function sendResult (id, values) {
this.emit("result", id, values);
};
function DefaultRemote () {
this.context = Script.createContext();
for (var i in global) this.context[i] = global[i];
this.context.module = module;
this.context.require = require;
var self = this;
this.context._swank = {
output: function output (arg) {
self.output(arg);
}
};
}
util.inherits(DefaultRemote, Remote);
DefaultRemote.prototype.prompt = function prompt () {
return "NODE";
};
DefaultRemote.prototype.kind = function kind () {
return "direct";
};
DefaultRemote.prototype.id = function id () {
return "node.js";
};
DefaultRemote.prototype.evaluate = function evaluate (id, str) {
var r;
try {
r = evalcx(str, this.context, "repl");
} catch (e) {
r = undefined;
this.output(e.stack);
}
this.sendResult(id, r === undefined ? [] : [util.inspect(r)]);
};
// TBD: rename Executive to Dispatcher
function Executive (options) {
options = options || {};
assert(options.hasOwnProperty("config") && options.config);
this.config = options.config;
this.pid = options.hasOwnProperty("pid") ? options.pid : null;
this.remotes = [];
this.attachRemote(new DefaultRemote());
this.activeRemote = this.remotes[0];
this.pendingRequests = {};
};
util.inherits(Executive, EventEmitter);
Executive.nextId = 1; // request id counter is global in order to avoid inter-connection conflicts
Executive.nextRemoteIndex = 1;
Executive.prototype.attachRemote = function attachRemote (remote) {
assert(this.remotes.indexOf(remote) < 0);
remote.setIndex(Executive.nextRemoteIndex++);
var self = this;
remote.on(
"output", function (str) {
if (remote == self.activeRemote)
self.emit("output", str);
});
remote.on(
"disconnect", function (str) {
self.handleDisconnectRemote(remote);
});
remote.on(
"result", function (id, values) {
if (!self.pendingRequests[id]) {
self.emit("output", "WARNING: received late result from " + remote.fullName() + "\n");
return;
}
try {
self.pendingRequests[id](values);
} finally {
delete self.pendingRequests[id];
}
});
this.remotes.push(remote);
this.emit("output", "Remote attached: " + remote.fullName() + "\n");
this.config.get(
"stickyRemote",
function (stickyRemote) {
if (stickyRemote !== null &&
(!self.activeRemote || self.activeRemote.fullName() != stickyRemote) &&
remote.fullName() == stickyRemote)
self.selectRemote(remote.index(), true, true);
});
};
Executive.prototype.handleDisconnectRemote = function handleDisconnectRemote (remote) {
remote.detachSelf();
var index = this.remotes.indexOf(remote);
if (index < 0) {
this.emit("output", "WARNING: disconnectRemote() called for an unknown remote: " + remote.fullName() + "\n");
return;
}
this.remotes.splice(index, 1);
this.emit("output", "Remote detached: " + remote.fullName() + "\n");
if (remote == this.activeRemote)
this.selectRemote(this.remotes[0].index(), false, true);
};
Executive.prototype.connectionInfo = function connectionInfo (cont) {
var self = this;
var prompt = this.activeRemote.prompt();
this.config.get(
"slimeVersion",
function (slimeVersion) {
cont({ pid: self.pid === null ? process.pid : self.pid,
encoding: { codingSystem: "utf-8", externalFormat: "UTF-8" },
packageSpec: { name: prompt, prompt: prompt },
implementation: { type: "JS", name: "JS", version: "1.5" },
version: slimeVersion || DEFAULT_SLIME_VERSION });
});
};
Executive.prototype.createRepl = function createRepl () {
var prompt = this.activeRemote.prompt();
return { packageName: prompt, prompt: prompt };
};
Executive.prototype.listenerEval = function listenerEval (str, cont) {
var id = Executive.nextId++;
this.pendingRequests[id] = cont;
this.activeRemote.evaluate(id, str);
};
Executive.prototype.listRemotes = function listRemotes () {
return this.remotes.map(
function (remote) {
return { index: remote.index(), kind: remote.kind(), id: remote.id(),
isActive: remote === this.activeRemote };
}, this);
};
Executive.prototype.selectRemote = function selectRemote (index, sticky, auto) {
// TBD: sticky support (should autoselect the remote with message upon attachment)
for (var i = 0; i < this.remotes.length; ++i) {
var remote = this.remotes[i];
if (remote.index() == index) {
if (remote == this.activeRemote) {
this.emit("output", "WARNING: remote already selected: " + remote.fullName() + "\n");
return;
}
this.activeRemote = remote;
if (!auto)
this.config.set("stickyRemote", sticky ? remote.fullName() : null);
this.emit("newPackage", remote.prompt());
this.emit("output", "Remote selected" + (auto ? " (auto)" : sticky ? " (sticky)" : "") +
": " + remote.fullName() + "\n");
return;
}
}
this.emit("output", "WARNING: bad remote index\n");
};
Executive.prototype.setTargetUrl = function setTargetUrl (targetUrl) {
var parsedUrl = null;
try {
parsedUrl = url.parse(targetUrl);
} catch (e) {}
if (parsedUrl && parsedUrl.hostname)
this.config.set("targetUrl", targetUrl);
else
this.emit("output", "WARNING: the URL must contain host and port\n");
};
Executive.prototype.setSlimeVersion = function setSlimeVersion (slimeVersion) {
this.config.set("slimeVersion", slimeVersion);
};
exports.Handler = Handler;
exports.Remote = Remote;
exports.Executive = Executive;