add swank-js
This commit is contained in:
parent
d2665ae0c5
commit
22be8e220f
18 changed files with 3917 additions and 0 deletions
48
emacs.d/swank-js/LICENSE
Normal file
48
emacs.d/swank-js/LICENSE
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
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.
|
||||
|
||||
Client file serving code was adapted from socket.io:
|
||||
|
||||
Copyright (c) 2010 LearnBoost <dev@learnboost.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
262
emacs.d/swank-js/README.md
Normal file
262
emacs.d/swank-js/README.md
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
swank-js
|
||||
========
|
||||
|
||||
swank-js provides [SLIME](http://common-lisp.net/project/slime/) REPL
|
||||
and other development tools for in-browser JavaScript and
|
||||
[Node.JS](http://nodejs.org). It consists of SWANK backend and
|
||||
accompanying SLIME contrib. [Socket.IO](http://socket.io/) is used to
|
||||
communicate with wide range of web browsers.
|
||||
|
||||
Motivation
|
||||
----------
|
||||
|
||||
From my experience an ability to use REPL for JavaScript debugging and
|
||||
interactive development is very helpful when developing Web
|
||||
applications. Previously I was using a heavily patched
|
||||
[MozRepl](https://github.com/bard/mozrepl/wiki/) version that was
|
||||
adapted for in-browser JavaScript. Primary downsides of that approach
|
||||
were extreme instability of communication between Emacs and the
|
||||
browser, the lack of cross-browser support and the lack of good RPC
|
||||
between Emacs and JS that can be used to develop some handy
|
||||
extensions.
|
||||
|
||||
I knew there exists [slime-proxy](https://github.com/3b/slime-proxy)
|
||||
project that provides such functionality for
|
||||
[Parenscript](http://common-lisp.net/project/parenscript/). The
|
||||
problem is that most of us including myself can't use Lisp all the
|
||||
time and a lot of code needs to be developed using languages like
|
||||
plain JavaScript (as opposed to Parenscript), Python and so on. My
|
||||
first thought was to adapt slime-proxy for use with plain JS, but then
|
||||
I decided to roll my own SWANK backend using Node.JS. I wanted to find
|
||||
out what this buzz around Node.JS is about and perhaps steal an
|
||||
idea or two from there for use in my Lisp projects. Another reason was
|
||||
availability of [Socket.IO](http://socket.io/) and an example of
|
||||
[tiny http server proxy](http://www.catonmat.net/http-proxy-in-nodejs).
|
||||
|
||||
Some people may prefer Firebug or built-in browser development tools
|
||||
to Emacs-based development, but for example in case of mobile browsers
|
||||
you don't have much choice. At some point I did try swank-js with an
|
||||
colleague's iPhone and it worked, which is not too unexpected given
|
||||
that Socket.IO has good cross-browser support.
|
||||
|
||||
Status
|
||||
------
|
||||
|
||||
As of now swank-js provides REPL with an ability to work with multiple
|
||||
browser connections, supports dynamic updates of JavaScript code using
|
||||
C-c C-c / C-M-x, provides debug output function and an ability to
|
||||
reload web pages in the browser or refresh their CSS using Emacs
|
||||
commands.
|
||||
|
||||
Many aspects of full-fledged SWANK backends aren't implemented yet,
|
||||
there's no debugger/completion/autodoc and so on, but as I plan to use
|
||||
swank-js a lot in future there's a good chance many of these features
|
||||
will be eventually added.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
1. Install [Node.JS](http://nodejs.org), [npm](http://npmjs.org/) and
|
||||
then [Socket.IO](http://socket.io/):
|
||||
|
||||
npm install socket.io
|
||||
2. Get recent [SLIME](http://common-lisp.net/project/slime/) from its CVS
|
||||
or the [git mirror](http://git.boinkor.net/gitweb/slime.git).
|
||||
3. Make sure you have latest [js2-mode](http://code.google.com/p/js2-mode/).
|
||||
Add it to your .emacs:
|
||||
|
||||
(add-to-list 'load-path "/path/to/js2-mode/directory")
|
||||
(autoload 'js2-mode "js2-mode" nil t)
|
||||
(add-to-list 'auto-mode-alist '("\\.js$" . js2-mode))
|
||||
3. Create symbolic link to slime-js.el in the contrib subdirectory of
|
||||
SLIME project.
|
||||
4. In your .emacs, add the following lines (you may use other key for
|
||||
slime-js-reload; also, if you're already using SLIME, just add slime-js
|
||||
to the list of contribs, otherwise adjust the load-path item):
|
||||
|
||||
(add-to-list 'load-path "/path/to/slime/installation")
|
||||
(require 'slime)
|
||||
(slime-setup '(slime-repl slime-js))
|
||||
|
||||
(global-set-key [f5] 'slime-js-reload)
|
||||
(add-hook 'js2-mode-hook
|
||||
(lambda ()
|
||||
(slime-js-minor-mode 1)))
|
||||
5. If you're using CSS mode, you may want to add the following lines too:
|
||||
|
||||
(add-hook 'css-mode-hook
|
||||
(lambda ()
|
||||
(define-key css-mode-map "\M-\C-x" 'slime-js-refresh-css)))
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Start swank-js with the following command in the project directory:
|
||||
|
||||
node swank.js
|
||||
|
||||
Make SLIME connect to the backend using M-x slime-connect and
|
||||
specifying localhost and port 4005. You will see REPL buffer with the
|
||||
following prompt:
|
||||
|
||||
NODE>
|
||||
|
||||
This means that you're currently talking to Node.JS. You may play
|
||||
around with it by running some JavaScript expressions.
|
||||
|
||||
If you get warning about SLIME version mismatch, you may make it
|
||||
disappear until the next SLIME upgrade by typing *,js-slime-version*
|
||||
at the REPL and entering your SLIME version (e.g. 2010-11-13).
|
||||
|
||||
Point your web browser to
|
||||
|
||||
http://localhost:8009/swank-js/test.html
|
||||
You will see the following message appear in the REPL (browser name
|
||||
and version may differ):
|
||||
|
||||
Remote attached: (browser) Firefox3.6:127.0.0.1
|
||||
|
||||
This means that the browser is now connected. Several browsers can
|
||||
connect simultaneously and you can switch between them and Node.JS
|
||||
REPL using *,select-remote* REPL shortcut. To use it, press ','
|
||||
(comma) and type *select-remote* (completion is supported). You will
|
||||
see "Remote:" prompt. Press TAB to see completions. Select your
|
||||
browser in the list by typing its name or clicking on the
|
||||
completion. The following message will appear:
|
||||
|
||||
NODE>
|
||||
Remote selected: (browser) Firefox3.6:127.0.0.1
|
||||
FIREFOX-3.6>
|
||||
|
||||
After that, you can interactively evaluate expressions in your
|
||||
browser. To go back to Node.JS repl, switch back to node.js/direct
|
||||
remote.
|
||||
|
||||
FIREFOX-3.6> document.body.nodeName
|
||||
BODY
|
||||
FIREFOX-3.6> alert("test!")
|
||||
FIREFOX-3.6>
|
||||
|
||||
When working with browser, you may use F5 to reload the page. swank-js
|
||||
connection with browser is lost in this case, but to solve this
|
||||
problem you may use *,sticky-select-remote* instead of
|
||||
*,select-remote*. This way swank-js will remember your selection and
|
||||
automagically attach to the browser whenever it connects. If you press
|
||||
F5 after using *,sticky-select-remote*, you will see that browser
|
||||
briefly disconnects but then connects again:
|
||||
|
||||
Remote detached: (browser) Firefox3.6:127.0.0.1
|
||||
FIREFOX-3.6>
|
||||
Remote selected (auto): (direct) node.js
|
||||
Remote attached: (browser) Firefox3.6:127.0.0.1
|
||||
NODE>
|
||||
Remote selected (auto): (browser) Firefox3.6:127.0.0.1
|
||||
FIREFOX-3.6>
|
||||
|
||||
The sticky remote selection is saved in the config file, ~/.swankjsrc,
|
||||
so you don't need to do *,sticky-select-remote* after restarting the
|
||||
browser.
|
||||
|
||||
Now, let's try to make it work with an actual site. swank-js acts as a
|
||||
proxy between your browser and the site so it can inject necessary
|
||||
script tags into HTML pages and avoid cross-domain HTTP request
|
||||
problems. Let's point it to [reddit](http://www.reddit.com). Type
|
||||
*,target-url* and then *http://www.reddit.com* (www. part is
|
||||
important, otherwise it will redirect to www.reddit.com skipping the
|
||||
proxy). Point your browser to http://localhost:8009, you'll see reddit
|
||||
frontpage load. If you didn't do *,select-remote* or
|
||||
*,sticky-select-remote* yet do it now and select your browser from the
|
||||
list of remotes. Now you can execute JavaScript in the context of
|
||||
reddit:
|
||||
|
||||
FIREFOX-3.6> $(".sitetable a.title").map(function(n) { return (n + 1) + ". " + $(this).text(); }).get().join("\n")
|
||||
1. Wikileaks currently under a mass DDOS attack
|
||||
2. Munich University - Jealous
|
||||
...
|
||||
|
||||
Let's make a function from it. Create a file test.js somewhere and
|
||||
make sure it uses js2-mode (if it doesn't, switch it to js2-mode using
|
||||
M-x js2-mode). Type the following there:
|
||||
|
||||
function listRedditTitles () {
|
||||
$(".sitetable a.title").map(
|
||||
function (n) {
|
||||
SwankJS.output((n + 1) + ". " + $(this).text() + "\n");
|
||||
}).get().join("\n");
|
||||
}
|
||||
|
||||
Note SwankJS.output() function being used there. It allows you to send
|
||||
debug print to SLIME REPL.
|
||||
|
||||
Move the point somewhere into the middle of the listRedditTitles() function
|
||||
and press C-M-x. Now you may try it out in the REPL:
|
||||
|
||||
FIREFOX-3.6> listRedditTitles()
|
||||
1. Wikileaks currently under a mass DDOS attack
|
||||
2. Munich University - Jealous
|
||||
...
|
||||
|
||||
You may edit the function definition and update it using C-M-x any
|
||||
number of times.
|
||||
|
||||
Now let's try some CSS hacking. Create a directory named zzz and start
|
||||
a Web server in it from your command prompt:
|
||||
|
||||
$ mkdir zzz && cd zzz && python -mSimpleHTTPServer
|
||||
|
||||
Create a file named a.css there and make sure it uses css-mode (like
|
||||
with js2-mode, you can force it with M-x css-mode). Add some CSS rules
|
||||
to this file:
|
||||
|
||||
body {
|
||||
background: green;
|
||||
}
|
||||
|
||||
Now let's add the stylesheet to the reddit page:
|
||||
|
||||
FIREFOX-3.6> $('head').append('<link rel="stylesheet" href="http://localhost:8000/a.css" type="text/css" />');
|
||||
[object Object]
|
||||
|
||||
You will see some parts of the page become green. Now, change green to
|
||||
blue in the CSS file and press C-M-x (it will save the file
|
||||
automatically):
|
||||
|
||||
body {
|
||||
background: blue;
|
||||
}
|
||||
|
||||
You will see the page become blue, maybe after some flickering as all
|
||||
CSS used on the page is reloaded. This way you may update CSS in an
|
||||
AJAX application without reloading the page, which is often rather
|
||||
handy. Unlike editing CSS in Firebug in case when you're editing CSS
|
||||
of your own application changes will not disappear upon page reload
|
||||
(with reddit page you'll have to readd the stylesheet).
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
I've noticed that flashsocket Socket.IO transport does exhibit some
|
||||
instability. You may want to try other transports by changing the
|
||||
socketio cookie, e.g.:
|
||||
|
||||
document.cookie = "socketio=xhr-polling"
|
||||
|
||||
Be careful not to lose connection to the browser though especially in
|
||||
case of REPL-less browser like IE6/7 or you'll have to type something
|
||||
like
|
||||
|
||||
javascript:void(document.cookie = "socketio=flashsocket")
|
||||
|
||||
in the address bar.
|
||||
|
||||
In case of IE, increasing the maximum number of HTTP connections may
|
||||
help with non-Flash transports, although I didn't try it yet. To do it
|
||||
add DWORD value MaxConnectionsPer1_0Server to the following registry
|
||||
key:
|
||||
|
||||
HKEY\_CURRENT\_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
swank-js is distributed under X11-style license. See LICENSE file.
|
||||
483
emacs.d/swank-js/client/json2.js
Normal file
483
emacs.d/swank-js/client/json2.js
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
/*
|
||||
http://www.JSON.org/json2.js
|
||||
2010-11-17
|
||||
|
||||
Public Domain.
|
||||
|
||||
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
||||
|
||||
See http://www.JSON.org/js.html
|
||||
|
||||
|
||||
This code should be minified before deployment.
|
||||
See http://javascript.crockford.com/jsmin.html
|
||||
|
||||
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
|
||||
NOT CONTROL.
|
||||
|
||||
|
||||
This file creates a global JSON object containing two methods: stringify
|
||||
and parse.
|
||||
|
||||
JSON.stringify(value, replacer, space)
|
||||
value any JavaScript value, usually an object or array.
|
||||
|
||||
replacer an optional parameter that determines how object
|
||||
values are stringified for objects. It can be a
|
||||
function or an array of strings.
|
||||
|
||||
space an optional parameter that specifies the indentation
|
||||
of nested structures. If it is omitted, the text will
|
||||
be packed without extra whitespace. If it is a number,
|
||||
it will specify the number of spaces to indent at each
|
||||
level. If it is a string (such as '\t' or ' '),
|
||||
it contains the characters used to indent at each level.
|
||||
|
||||
This method produces a JSON text from a JavaScript value.
|
||||
|
||||
When an object value is found, if the object contains a toJSON
|
||||
method, its toJSON method will be called and the result will be
|
||||
stringified. A toJSON method does not serialize: it returns the
|
||||
value represented by the name/value pair that should be serialized,
|
||||
or undefined if nothing should be serialized. The toJSON method
|
||||
will be passed the key associated with the value, and this will be
|
||||
bound to the value
|
||||
|
||||
For example, this would serialize Dates as ISO strings.
|
||||
|
||||
Date.prototype.toJSON = function (key) {
|
||||
function f(n) {
|
||||
// Format integers to have at least two digits.
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
return this.getUTCFullYear() + '-' +
|
||||
f(this.getUTCMonth() + 1) + '-' +
|
||||
f(this.getUTCDate()) + 'T' +
|
||||
f(this.getUTCHours()) + ':' +
|
||||
f(this.getUTCMinutes()) + ':' +
|
||||
f(this.getUTCSeconds()) + 'Z';
|
||||
};
|
||||
|
||||
You can provide an optional replacer method. It will be passed the
|
||||
key and value of each member, with this bound to the containing
|
||||
object. The value that is returned from your method will be
|
||||
serialized. If your method returns undefined, then the member will
|
||||
be excluded from the serialization.
|
||||
|
||||
If the replacer parameter is an array of strings, then it will be
|
||||
used to select the members to be serialized. It filters the results
|
||||
such that only members with keys listed in the replacer array are
|
||||
stringified.
|
||||
|
||||
Values that do not have JSON representations, such as undefined or
|
||||
functions, will not be serialized. Such values in objects will be
|
||||
dropped; in arrays they will be replaced with null. You can use
|
||||
a replacer function to replace those with JSON values.
|
||||
JSON.stringify(undefined) returns undefined.
|
||||
|
||||
The optional space parameter produces a stringification of the
|
||||
value that is filled with line breaks and indentation to make it
|
||||
easier to read.
|
||||
|
||||
If the space parameter is a non-empty string, then that string will
|
||||
be used for indentation. If the space parameter is a number, then
|
||||
the indentation will be that many spaces.
|
||||
|
||||
Example:
|
||||
|
||||
text = JSON.stringify(['e', {pluribus: 'unum'}]);
|
||||
// text is '["e",{"pluribus":"unum"}]'
|
||||
|
||||
|
||||
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
|
||||
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
|
||||
|
||||
text = JSON.stringify([new Date()], function (key, value) {
|
||||
return this[key] instanceof Date ?
|
||||
'Date(' + this[key] + ')' : value;
|
||||
});
|
||||
// text is '["Date(---current time---)"]'
|
||||
|
||||
|
||||
JSON.parse(text, reviver)
|
||||
This method parses a JSON text to produce an object or array.
|
||||
It can throw a SyntaxError exception.
|
||||
|
||||
The optional reviver parameter is a function that can filter and
|
||||
transform the results. It receives each of the keys and values,
|
||||
and its return value is used instead of the original value.
|
||||
If it returns what it received, then the structure is not modified.
|
||||
If it returns undefined then the member is deleted.
|
||||
|
||||
Example:
|
||||
|
||||
// Parse the text. Values that look like ISO date strings will
|
||||
// be converted to Date objects.
|
||||
|
||||
myData = JSON.parse(text, function (key, value) {
|
||||
var a;
|
||||
if (typeof value === 'string') {
|
||||
a =
|
||||
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
|
||||
if (a) {
|
||||
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
|
||||
+a[5], +a[6]));
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
|
||||
var d;
|
||||
if (typeof value === 'string' &&
|
||||
value.slice(0, 5) === 'Date(' &&
|
||||
value.slice(-1) === ')') {
|
||||
d = new Date(value.slice(5, -1));
|
||||
if (d) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
|
||||
This is a reference implementation. You are free to copy, modify, or
|
||||
redistribute.
|
||||
*/
|
||||
|
||||
/*jslint evil: true, strict: false, regexp: false */
|
||||
|
||||
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
|
||||
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
|
||||
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
|
||||
lastIndex, length, parse, prototype, push, replace, slice, stringify,
|
||||
test, toJSON, toString, valueOf
|
||||
*/
|
||||
|
||||
|
||||
// Create a JSON object only if one does not already exist. We create the
|
||||
// methods in a closure to avoid creating global variables.
|
||||
|
||||
if (!this.JSON) {
|
||||
this.JSON = {};
|
||||
}
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function f(n) {
|
||||
// Format integers to have at least two digits.
|
||||
return n < 10 ? '0' + n : n;
|
||||
}
|
||||
|
||||
if (typeof Date.prototype.toJSON !== 'function') {
|
||||
|
||||
Date.prototype.toJSON = function (key) {
|
||||
|
||||
return isFinite(this.valueOf()) ?
|
||||
this.getUTCFullYear() + '-' +
|
||||
f(this.getUTCMonth() + 1) + '-' +
|
||||
f(this.getUTCDate()) + 'T' +
|
||||
f(this.getUTCHours()) + ':' +
|
||||
f(this.getUTCMinutes()) + ':' +
|
||||
f(this.getUTCSeconds()) + 'Z' : null;
|
||||
};
|
||||
|
||||
String.prototype.toJSON =
|
||||
Number.prototype.toJSON =
|
||||
Boolean.prototype.toJSON = function (key) {
|
||||
return this.valueOf();
|
||||
};
|
||||
}
|
||||
|
||||
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
|
||||
gap,
|
||||
indent,
|
||||
meta = { // table of character substitutions
|
||||
'\b': '\\b',
|
||||
'\t': '\\t',
|
||||
'\n': '\\n',
|
||||
'\f': '\\f',
|
||||
'\r': '\\r',
|
||||
'"' : '\\"',
|
||||
'\\': '\\\\'
|
||||
},
|
||||
rep;
|
||||
|
||||
|
||||
function quote(string) {
|
||||
|
||||
// If the string contains no control characters, no quote characters, and no
|
||||
// backslash characters, then we can safely slap some quotes around it.
|
||||
// Otherwise we must also replace the offending characters with safe escape
|
||||
// sequences.
|
||||
|
||||
escapable.lastIndex = 0;
|
||||
return escapable.test(string) ?
|
||||
'"' + string.replace(escapable, function (a) {
|
||||
var c = meta[a];
|
||||
return typeof c === 'string' ? c :
|
||||
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
}) + '"' :
|
||||
'"' + string + '"';
|
||||
}
|
||||
|
||||
|
||||
function str(key, holder) {
|
||||
|
||||
// Produce a string from holder[key].
|
||||
|
||||
var i, // The loop counter.
|
||||
k, // The member key.
|
||||
v, // The member value.
|
||||
length,
|
||||
mind = gap,
|
||||
partial,
|
||||
value = holder[key];
|
||||
|
||||
// If the value has a toJSON method, call it to obtain a replacement value.
|
||||
|
||||
if (value && typeof value === 'object' &&
|
||||
typeof value.toJSON === 'function') {
|
||||
value = value.toJSON(key);
|
||||
}
|
||||
|
||||
// If we were called with a replacer function, then call the replacer to
|
||||
// obtain a replacement value.
|
||||
|
||||
if (typeof rep === 'function') {
|
||||
value = rep.call(holder, key, value);
|
||||
}
|
||||
|
||||
// What happens next depends on the value's type.
|
||||
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return quote(value);
|
||||
|
||||
case 'number':
|
||||
|
||||
// JSON numbers must be finite. Encode non-finite numbers as null.
|
||||
|
||||
return isFinite(value) ? String(value) : 'null';
|
||||
|
||||
case 'boolean':
|
||||
case 'null':
|
||||
|
||||
// If the value is a boolean or null, convert it to a string. Note:
|
||||
// typeof null does not produce 'null'. The case is included here in
|
||||
// the remote chance that this gets fixed someday.
|
||||
|
||||
return String(value);
|
||||
|
||||
// If the type is 'object', we might be dealing with an object or an array or
|
||||
// null.
|
||||
|
||||
case 'object':
|
||||
|
||||
// Due to a specification blunder in ECMAScript, typeof null is 'object',
|
||||
// so watch out for that case.
|
||||
|
||||
if (!value) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
// Make an array to hold the partial results of stringifying this object value.
|
||||
|
||||
gap += indent;
|
||||
partial = [];
|
||||
|
||||
// Is the value an array?
|
||||
|
||||
if (Object.prototype.toString.apply(value) === '[object Array]') {
|
||||
|
||||
// The value is an array. Stringify every element. Use null as a placeholder
|
||||
// for non-JSON values.
|
||||
|
||||
length = value.length;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
partial[i] = str(i, value) || 'null';
|
||||
}
|
||||
|
||||
// Join all of the elements together, separated with commas, and wrap them in
|
||||
// brackets.
|
||||
|
||||
v = partial.length === 0 ? '[]' :
|
||||
gap ? '[\n' + gap +
|
||||
partial.join(',\n' + gap) + '\n' +
|
||||
mind + ']' :
|
||||
'[' + partial.join(',') + ']';
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
|
||||
// If the replacer is an array, use it to select the members to be stringified.
|
||||
|
||||
if (rep && typeof rep === 'object') {
|
||||
length = rep.length;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
k = rep[i];
|
||||
if (typeof k === 'string') {
|
||||
v = str(k, value);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// Otherwise, iterate through all of the keys in the object.
|
||||
|
||||
for (k in value) {
|
||||
if (Object.hasOwnProperty.call(value, k)) {
|
||||
v = str(k, value);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (gap ? ': ' : ':') + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Join all of the member texts together, separated with commas,
|
||||
// and wrap them in braces.
|
||||
|
||||
v = partial.length === 0 ? '{}' :
|
||||
gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
|
||||
mind + '}' : '{' + partial.join(',') + '}';
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
// If the JSON object does not yet have a stringify method, give it one.
|
||||
|
||||
if (typeof JSON.stringify !== 'function') {
|
||||
JSON.stringify = function (value, replacer, space) {
|
||||
|
||||
// The stringify method takes a value and an optional replacer, and an optional
|
||||
// space parameter, and returns a JSON text. The replacer can be a function
|
||||
// that can replace values, or an array of strings that will select the keys.
|
||||
// A default replacer method can be provided. Use of the space parameter can
|
||||
// produce text that is more easily readable.
|
||||
|
||||
var i;
|
||||
gap = '';
|
||||
indent = '';
|
||||
|
||||
// If the space parameter is a number, make an indent string containing that
|
||||
// many spaces.
|
||||
|
||||
if (typeof space === 'number') {
|
||||
for (i = 0; i < space; i += 1) {
|
||||
indent += ' ';
|
||||
}
|
||||
|
||||
// If the space parameter is a string, it will be used as the indent string.
|
||||
|
||||
} else if (typeof space === 'string') {
|
||||
indent = space;
|
||||
}
|
||||
|
||||
// If there is a replacer, it must be a function or an array.
|
||||
// Otherwise, throw an error.
|
||||
|
||||
rep = replacer;
|
||||
if (replacer && typeof replacer !== 'function' &&
|
||||
(typeof replacer !== 'object' ||
|
||||
typeof replacer.length !== 'number')) {
|
||||
throw new Error('JSON.stringify');
|
||||
}
|
||||
|
||||
// Make a fake root object containing our value under the key of ''.
|
||||
// Return the result of stringifying the value.
|
||||
|
||||
return str('', {'': value});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// If the JSON object does not yet have a parse method, give it one.
|
||||
|
||||
if (typeof JSON.parse !== 'function') {
|
||||
JSON.parse = function (text, reviver) {
|
||||
|
||||
// The parse method takes a text and an optional reviver function, and returns
|
||||
// a JavaScript value if the text is a valid JSON text.
|
||||
|
||||
var j;
|
||||
|
||||
function walk(holder, key) {
|
||||
|
||||
// The walk method is used to recursively walk the resulting structure so
|
||||
// that modifications can be made.
|
||||
|
||||
var k, v, value = holder[key];
|
||||
if (value && typeof value === 'object') {
|
||||
for (k in value) {
|
||||
if (Object.hasOwnProperty.call(value, k)) {
|
||||
v = walk(value, k);
|
||||
if (v !== undefined) {
|
||||
value[k] = v;
|
||||
} else {
|
||||
delete value[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return reviver.call(holder, key, value);
|
||||
}
|
||||
|
||||
|
||||
// Parsing happens in four stages. In the first stage, we replace certain
|
||||
// Unicode characters with escape sequences. JavaScript handles many characters
|
||||
// incorrectly, either silently deleting them, or treating them as line endings.
|
||||
|
||||
text = String(text);
|
||||
cx.lastIndex = 0;
|
||||
if (cx.test(text)) {
|
||||
text = text.replace(cx, function (a) {
|
||||
return '\\u' +
|
||||
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
});
|
||||
}
|
||||
|
||||
// In the second stage, we run the text against regular expressions that look
|
||||
// for non-JSON patterns. We are especially concerned with '()' and 'new'
|
||||
// because they can cause invocation, and '=' because it can cause mutation.
|
||||
// But just to be safe, we want to reject all unexpected forms.
|
||||
|
||||
// We split the second stage into 4 regexp operations in order to work around
|
||||
// crippling inefficiencies in IE's and Safari's regexp engines. First we
|
||||
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
|
||||
// replace all simple value tokens with ']' characters. Third, we delete all
|
||||
// open brackets that follow a colon or comma or that begin the text. Finally,
|
||||
// we look to see that the remaining characters are only whitespace or ']' or
|
||||
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
|
||||
|
||||
if (/^[\],:{}\s]*$/
|
||||
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
|
||||
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
|
||||
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
|
||||
|
||||
// In the third stage we use the eval function to compile the text into a
|
||||
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
|
||||
// in JavaScript: it can begin a block or an object literal. We wrap the text
|
||||
// in parens to eliminate the ambiguity.
|
||||
|
||||
j = eval('(' + text + ')');
|
||||
|
||||
// In the optional fourth stage, we recursively walk the new structure, passing
|
||||
// each name/value pair to a reviver function for possible transformation.
|
||||
|
||||
return typeof reviver === 'function' ?
|
||||
walk({'': j}, '') : j;
|
||||
}
|
||||
|
||||
// If the text is not JSON parseable, then a SyntaxError is thrown.
|
||||
|
||||
throw new SyntaxError('JSON.parse');
|
||||
};
|
||||
}
|
||||
}());
|
||||
1
emacs.d/swank-js/client/load.js
Normal file
1
emacs.d/swank-js/client/load.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
SwankJS.setup();
|
||||
356
emacs.d/swank-js/client/stacktrace.js
Normal file
356
emacs.d/swank-js/client/stacktrace.js
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
// Domain Public by Eric Wendelin http://eriwen.com/ (2008)
|
||||
// Luke Smith http://lucassmith.name/ (2008)
|
||||
// Loic Dachary <loic@dachary.org> (2008)
|
||||
// Johan Euphrosine <proppy@aminche.com> (2008)
|
||||
// Øyvind Sean Kinsey http://kinsey.no/blog (2010)
|
||||
//
|
||||
// Information and discussions
|
||||
// http://jspoker.pokersource.info/skin/test-printstacktrace.html
|
||||
// http://eriwen.com/javascript/js-stack-trace/
|
||||
// http://eriwen.com/javascript/stacktrace-update/
|
||||
// http://pastie.org/253058
|
||||
//
|
||||
// guessFunctionNameFromLines comes from firebug
|
||||
//
|
||||
// Software License Agreement (BSD License)
|
||||
//
|
||||
// Copyright (c) 2007, Parakey Inc.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use of this software 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.
|
||||
//
|
||||
// * Neither the name of Parakey Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products
|
||||
// derived from this software without specific prior
|
||||
// written permission of Parakey Inc.
|
||||
//
|
||||
// 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 OWNER 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.
|
||||
|
||||
/**
|
||||
* Main function giving a function stack trace with a forced or passed in Error
|
||||
*
|
||||
* @cfg {Error} e The error to create a stacktrace from (optional)
|
||||
* @cfg {Boolean} guess If we should try to resolve the names of anonymous functions
|
||||
* @return {Array} of Strings with functions, lines, files, and arguments where possible
|
||||
*/
|
||||
function swank_printStackTrace(options) {
|
||||
var ex = (options && options.e) ? options.e : null;
|
||||
var guess = options ? !!options.guess : true;
|
||||
|
||||
var p = new swank_printStackTrace.implementation();
|
||||
var result = p.run(ex);
|
||||
return (guess) ? p.guessFunctions(result) : result;
|
||||
}
|
||||
|
||||
swank_printStackTrace.implementation = function() {};
|
||||
|
||||
swank_printStackTrace.implementation.prototype = {
|
||||
run: function(ex) {
|
||||
// Use either the stored mode, or resolve it
|
||||
var mode = this._mode || this.mode();
|
||||
if (mode === 'other') {
|
||||
return this.other(arguments.callee);
|
||||
} else {
|
||||
ex = ex ||
|
||||
(function() {
|
||||
try {
|
||||
var _err = __undef__ << 1;
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
})();
|
||||
return this[mode](ex);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @return {String} mode of operation for the environment in question.
|
||||
*/
|
||||
mode: function() {
|
||||
try {
|
||||
var _err = __undef__ << 1;
|
||||
} catch (e) {
|
||||
if (e['arguments']) {
|
||||
return (this._mode = 'chrome');
|
||||
} else if (window.opera && e.stacktrace) {
|
||||
return (this._mode = 'opera10');
|
||||
} else if (e.stack) {
|
||||
return (this._mode = 'firefox');
|
||||
} else if (window.opera && !('stacktrace' in e)) { //Opera 9-
|
||||
return (this._mode = 'opera');
|
||||
}
|
||||
}
|
||||
return (this._mode = 'other');
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a context, function name, and callback function, overwrite it so that it calls
|
||||
* swank_printStackTrace() first with a callback and then runs the rest of the body.
|
||||
*
|
||||
* @param {Object} context of execution (e.g. window)
|
||||
* @param {String} functionName to instrument
|
||||
* @param {Function} function to call with a stack trace on invocation
|
||||
*/
|
||||
instrumentFunction: function(context, functionName, callback) {
|
||||
context = context || window;
|
||||
context['_old' + functionName] = context[functionName];
|
||||
context[functionName] = function() {
|
||||
callback.call(this, swank_printStackTrace());
|
||||
return context['_old' + functionName].apply(this, arguments);
|
||||
};
|
||||
context[functionName]._instrumented = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a context and function name of a function that has been
|
||||
* instrumented, revert the function to it's original (non-instrumented)
|
||||
* state.
|
||||
*
|
||||
* @param {Object} context of execution (e.g. window)
|
||||
* @param {String} functionName to de-instrument
|
||||
*/
|
||||
deinstrumentFunction: function(context, functionName) {
|
||||
if (context[functionName].constructor === Function &&
|
||||
context[functionName]._instrumented &&
|
||||
context['_old' + functionName].constructor === Function) {
|
||||
context[functionName] = context['_old' + functionName];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Given an Error object, return a formatted Array based on Chrome's stack string.
|
||||
*
|
||||
* @param e - Error object to inspect
|
||||
* @return Array<String> of function calls, files and line numbers
|
||||
*/
|
||||
chrome: function(e) {
|
||||
return e.stack.replace(/^[^\n]*\n/, '').replace(/^[^\n]*\n/, '').replace(/^[^\(]+?[\n$]/gm, '').replace(/^\s+at\s+/gm, '').replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@').split('\n');
|
||||
},
|
||||
|
||||
/**
|
||||
* Given an Error object, return a formatted Array based on Firefox's stack string.
|
||||
*
|
||||
* @param e - Error object to inspect
|
||||
* @return Array<String> of function calls, files and line numbers
|
||||
*/
|
||||
firefox: function(e) {
|
||||
return e.stack ? e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anonymous}(').split('\n') : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Given an Error object, return a formatted Array based on Opera 10's stacktrace string.
|
||||
*
|
||||
* @param e - Error object to inspect
|
||||
* @return Array<String> of function calls, files and line numbers
|
||||
*/
|
||||
opera10: function(e) {
|
||||
var stack = e.stacktrace;
|
||||
var lines = stack.split('\n'), ANON = '{anonymous}',
|
||||
lineRE = /.*line (\d+), column (\d+) in ((<anonymous function\:?\s*(\S+))|([^\(]+)\([^\)]*\))(?: in )?(.*)\s*$/i, i, j, len;
|
||||
for (i = 2, j = 0, len = lines.length; i < len - 2; i++) {
|
||||
if (lineRE.test(lines[i])) {
|
||||
var location = RegExp.$6 + ':' + RegExp.$1 + ':' + RegExp.$2;
|
||||
var fnName = RegExp.$3;
|
||||
fnName = fnName.replace(/<anonymous function\s?(\S+)?>/g, ANON);
|
||||
lines[j++] = fnName + '@' + location;
|
||||
}
|
||||
}
|
||||
|
||||
lines.splice(j, lines.length - j);
|
||||
return lines;
|
||||
},
|
||||
|
||||
// Opera 7.x-9.x only!
|
||||
opera: function(e) {
|
||||
var lines = e.message.split('\n'), ANON = '{anonymous}',
|
||||
lineRE = /Line\s+(\d+).*script\s+(http\S+)(?:.*in\s+function\s+(\S+))?/i,
|
||||
i, j, len;
|
||||
|
||||
for (i = 4, j = 0, len = lines.length; i < len; i += 2) {
|
||||
//TODO: RegExp.exec() would probably be cleaner here
|
||||
if (lineRE.test(lines[i])) {
|
||||
lines[j++] = (RegExp.$3 ? RegExp.$3 + '()@' + RegExp.$2 + RegExp.$1 : ANON + '()@' + RegExp.$2 + ':' + RegExp.$1) + ' -- ' + lines[i + 1].replace(/^\s+/, '');
|
||||
}
|
||||
}
|
||||
|
||||
lines.splice(j, lines.length - j);
|
||||
return lines;
|
||||
},
|
||||
|
||||
// Safari, IE, and others
|
||||
other: function(curr) {
|
||||
var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i,
|
||||
stack = [], j = 0, fn, args;
|
||||
|
||||
var maxStackSize = 10;
|
||||
while (curr && stack.length < maxStackSize) {
|
||||
fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON;
|
||||
args = Array.prototype.slice.call(curr['arguments']);
|
||||
stack[j++] = fn + '(' + this.stringifyArguments(args) + ')';
|
||||
curr = curr.caller;
|
||||
}
|
||||
return stack;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given arguments array as a String, subsituting type names for non-string types.
|
||||
*
|
||||
* @param {Arguments} object
|
||||
* @return {Array} of Strings with stringified arguments
|
||||
*/
|
||||
stringifyArguments: function(args) {
|
||||
for (var i = 0; i < args.length; ++i) {
|
||||
var arg = args[i];
|
||||
if (arg === undefined) {
|
||||
args[i] = 'undefined';
|
||||
} else if (arg === null) {
|
||||
args[i] = 'null';
|
||||
} else if (arg.constructor) {
|
||||
if (arg.constructor === Array) {
|
||||
if (arg.length < 3) {
|
||||
args[i] = '[' + this.stringifyArguments(arg) + ']';
|
||||
} else {
|
||||
args[i] = '[' + this.stringifyArguments(Array.prototype.slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(Array.prototype.slice.call(arg, -1)) + ']';
|
||||
}
|
||||
} else if (arg.constructor === Object) {
|
||||
args[i] = '#object';
|
||||
} else if (arg.constructor === Function) {
|
||||
args[i] = '#function';
|
||||
} else if (arg.constructor === String) {
|
||||
args[i] = '"' + arg + '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
return args.join(',');
|
||||
},
|
||||
|
||||
sourceCache: {},
|
||||
|
||||
/**
|
||||
* @return the text from a given URL.
|
||||
*/
|
||||
ajax: function(url) {
|
||||
var req = this.createXMLHTTPObject();
|
||||
if (!req) {
|
||||
return;
|
||||
}
|
||||
req.open('GET', url, false);
|
||||
req.setRequestHeader('User-Agent', 'XMLHTTP/1.0');
|
||||
req.send('');
|
||||
return req.responseText;
|
||||
},
|
||||
|
||||
/**
|
||||
* Try XHR methods in order and store XHR factory.
|
||||
*
|
||||
* @return <Function> XHR function or equivalent
|
||||
*/
|
||||
createXMLHTTPObject: function() {
|
||||
var xmlhttp, XMLHttpFactories = [
|
||||
function() {
|
||||
return new XMLHttpRequest();
|
||||
}, function() {
|
||||
return new ActiveXObject('Msxml2.XMLHTTP');
|
||||
}, function() {
|
||||
return new ActiveXObject('Msxml3.XMLHTTP');
|
||||
}, function() {
|
||||
return new ActiveXObject('Microsoft.XMLHTTP');
|
||||
}
|
||||
];
|
||||
for (var i = 0; i < XMLHttpFactories.length; i++) {
|
||||
try {
|
||||
xmlhttp = XMLHttpFactories[i]();
|
||||
// Use memoization to cache the factory
|
||||
this.createXMLHTTPObject = XMLHttpFactories[i];
|
||||
return xmlhttp;
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a URL, check if it is in the same domain (so we can get the source
|
||||
* via Ajax).
|
||||
*
|
||||
* @param url <String> source url
|
||||
* @return False if we need a cross-domain request
|
||||
*/
|
||||
isSameDomain: function(url) {
|
||||
return url.indexOf(location.hostname) !== -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get source code from given URL if in the same domain.
|
||||
*
|
||||
* @param url <String> JS source URL
|
||||
* @return <String> Source code
|
||||
*/
|
||||
getSource: function(url) {
|
||||
if (!(url in this.sourceCache)) {
|
||||
this.sourceCache[url] = this.ajax(url).split('\n');
|
||||
}
|
||||
return this.sourceCache[url];
|
||||
},
|
||||
|
||||
guessFunctions: function(stack) {
|
||||
for (var i = 0; i < stack.length; ++i) {
|
||||
var reStack = /\{anonymous\}\(.*\)@(\w+:\/\/([\-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/;
|
||||
var frame = stack[i], m = reStack.exec(frame);
|
||||
if (m) {
|
||||
var file = m[1], lineno = m[4]; //m[7] is character position in Chrome
|
||||
if (file && this.isSameDomain(file) && lineno) {
|
||||
var functionName = this.guessFunctionName(file, lineno);
|
||||
stack[i] = frame.replace('{anonymous}', functionName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return stack;
|
||||
},
|
||||
|
||||
guessFunctionName: function(url, lineNo) {
|
||||
try {
|
||||
return this.guessFunctionNameFromLines(lineNo, this.getSource(url));
|
||||
} catch (e) {
|
||||
return 'getSource failed with url: ' + url + ', exception: ' + e.toString();
|
||||
}
|
||||
},
|
||||
|
||||
guessFunctionNameFromLines: function(lineNo, source) {
|
||||
var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/;
|
||||
var reGuessFunction = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/;
|
||||
// Walk backwards from the first line in the function until we find the line which
|
||||
// matches the pattern above, which is the function definition
|
||||
var line = "", maxLines = 10;
|
||||
for (var i = 0; i < maxLines; ++i) {
|
||||
line = source[lineNo - i] + line;
|
||||
if (line !== undefined) {
|
||||
var m = reGuessFunction.exec(line);
|
||||
if (m && m[1]) {
|
||||
return m[1];
|
||||
} else {
|
||||
m = reFunctionArgNames.exec(line);
|
||||
if (m && m[1]) {
|
||||
return m[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return '(?)';
|
||||
}
|
||||
};
|
||||
139
emacs.d/swank-js/client/swank-js.js
Normal file
139
emacs.d/swank-js/client/swank-js.js
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
//
|
||||
// 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 SwankJS = { socket: null, connected: false, bufferedOutput: [] };
|
||||
|
||||
// TBD: check message contents
|
||||
// TBD: exception handling
|
||||
// TBD: trim stack trace excluding everything starting from swankjs_evaluate line
|
||||
|
||||
SwankJS.debug = function debug () {
|
||||
if (!window.console)
|
||||
return;
|
||||
var debug = console.debug || console.log;
|
||||
if (!debug)
|
||||
return;
|
||||
var args = [];
|
||||
for (var i = 0; i < arguments.length; ++i)
|
||||
args.push(arguments[i]);
|
||||
debug.apply(console, args);
|
||||
};
|
||||
|
||||
SwankJS.setup = function setup () {
|
||||
try {
|
||||
if (parent.window && parent.window.document !== document && parent.window.SwankJS)
|
||||
return;
|
||||
} catch (e) {}
|
||||
var self = this;
|
||||
// TBD: swank-js should proxy all requests to autoadd its scripts
|
||||
// (this way, the dynamic script loading stuff isn't necessary)
|
||||
// and to make flashsocket swf load from the same url as the
|
||||
// web app itself.
|
||||
// Don't forget about 'Host: ' header though!
|
||||
this.socket = new io.Socket();
|
||||
this.socket.on(
|
||||
"connect",
|
||||
function() {
|
||||
self.connected = true;
|
||||
self.debug("connected");
|
||||
self.socket.send({ op: "handshake", userAgent: navigator.userAgent });
|
||||
if (self.bufferedOutput.length > 0) {
|
||||
for (var i = 0; i < self.bufferedOutput.length; ++i)
|
||||
self.output(self.bufferedOutput[i]);
|
||||
self.bufferedOutput = [];
|
||||
}
|
||||
});
|
||||
this.socket.on(
|
||||
"message", function swankjs_evaluate (m) {
|
||||
self.debug("eval: %o", m);
|
||||
// var m = JSON.parse(message);
|
||||
try {
|
||||
var r = window.eval(m.code);
|
||||
} catch (e) {
|
||||
var message = String(e);
|
||||
if (message == "[object Error]") {
|
||||
try {
|
||||
message = "ERROR: " + e.message;
|
||||
} catch(e1) {}
|
||||
}
|
||||
self.debug("error = %s", message);
|
||||
self.socket.send({ op: "result", id: m.id,
|
||||
error: message + "\n" + swank_printStackTrace({ e: e }).join("\n") });
|
||||
return;
|
||||
}
|
||||
self.debug("result = %s", String(r));
|
||||
self.socket.send({ op: "result", id: m.id, error: null, values: r === undefined ? [] : [String(r)] }); });
|
||||
this.socket.on(
|
||||
"disconnect", function() {
|
||||
self.debug("connected");
|
||||
});
|
||||
this.socket.connect();
|
||||
};
|
||||
|
||||
// useful functions for the REPL / web apps
|
||||
|
||||
SwankJS.output = function output (str) {
|
||||
if (this.socket && this.connected)
|
||||
this.socket.send({ op: "output", str: str });
|
||||
else
|
||||
this.bufferedOutput.push(str);
|
||||
};
|
||||
|
||||
SwankJS.reload = function reload () {
|
||||
document.location.reload(true);
|
||||
};
|
||||
|
||||
SwankJS.refreshCSS = function refreshCSS () {
|
||||
// FIXME: this doesn't work in IE yet
|
||||
// FIXME: support refresh of individual CSS files
|
||||
var links = document.getElementsByTagName('link');
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
var link = links[i];
|
||||
if (link.rel.toLowerCase().indexOf('stylesheet') >=0 && link.href) {
|
||||
var h = link.href.replace(/(&|\\?)forceReload=\d+/, "");
|
||||
link.href = h + (h.indexOf('?') >= 0 ? '&' : '?') + 'forceReload=' + Date.now();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
// we may need this later
|
||||
|
||||
SwankJS.makeScriptElement = function makeScriptElement (src, content) {
|
||||
var script = document.createElement("script");
|
||||
script.type = "text/javascript";
|
||||
if (src)
|
||||
script.src = src;
|
||||
else {
|
||||
var text = document.createTextNode(content);
|
||||
script.appendChild(text);
|
||||
}
|
||||
return script;
|
||||
};
|
||||
*/
|
||||
|
||||
SwankJS.setup();
|
||||
10
emacs.d/swank-js/client/test.html
Normal file
10
emacs.d/swank-js/client/test.html
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript" src="/swank-js/json2.js"></script>
|
||||
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
|
||||
<script type="text/javascript" src="/swank-js/stacktrace.js"></script>
|
||||
<script type="text/javascript" src="/swank-js/swank-js.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
103
emacs.d/swank-js/config.js
Normal file
103
emacs.d/swank-js/config.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// -*- mode: js2 -*-
|
||||
//
|
||||
// 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 path = require("path"), fs = require("fs");
|
||||
|
||||
function Config (fileName) {
|
||||
this.fileName = fileName;
|
||||
if (/^~\//.test(this.fileName))
|
||||
this.fileName = path.join(process.env.HOME || "/", this.fileName.substring(2));
|
||||
this.config = null;
|
||||
}
|
||||
|
||||
Config.prototype.loadConfig = function loadConfig (cont) {
|
||||
var self = this;
|
||||
if (!this.config) {
|
||||
fs.readFile(
|
||||
self.fileName, "utf-8", function (err, data) {
|
||||
self.config = {};
|
||||
if (!err) {
|
||||
try {
|
||||
self.config = JSON.parse(data);
|
||||
} catch (e) {}
|
||||
}
|
||||
cont(self.config);
|
||||
});
|
||||
} else
|
||||
cont(this.config);
|
||||
};
|
||||
|
||||
Config.prototype.saveConfig = function saveConfig (cont) {
|
||||
if (!this.config)
|
||||
return;
|
||||
var self = this;
|
||||
fs.writeFile(
|
||||
this.fileName, JSON.stringify(this.config), "utf8",
|
||||
function (err) {
|
||||
if (err)
|
||||
console.warn("error writing config file %s: %s", self.fileName, err);
|
||||
cont();
|
||||
});
|
||||
};
|
||||
|
||||
Config.prototype.get = function get (name, cont) {
|
||||
this.loadConfig(
|
||||
function (cfg) {
|
||||
cont(cfg.hasOwnProperty(name) ? cfg[name] : undefined);
|
||||
});
|
||||
};
|
||||
|
||||
Config.prototype.set = function set (name, value, cont) {
|
||||
var self = this;
|
||||
cont = cont || function () {};
|
||||
this.loadConfig(
|
||||
function (cfg) {
|
||||
cfg[name] = value;
|
||||
self.saveConfig(cont);
|
||||
});
|
||||
};
|
||||
|
||||
function FakeConfig (values) {
|
||||
this.config = values || {};
|
||||
}
|
||||
|
||||
FakeConfig.prototype.getNow = function getNow (name) {
|
||||
return this.config.hasOwnProperty(name) ? this.config[name] : undefined;
|
||||
};
|
||||
|
||||
FakeConfig.prototype.get = function get (name, cont) {
|
||||
cont(this.config.hasOwnProperty(name) ? this.config[name] : undefined);
|
||||
};
|
||||
|
||||
FakeConfig.prototype.set = function set (name, value, cont) {
|
||||
this.config[name] = value;
|
||||
if (cont) cont();
|
||||
};
|
||||
|
||||
exports.Config = Config;
|
||||
exports.FakeConfig = FakeConfig;
|
||||
273
emacs.d/swank-js/lisp-tests.js
Normal file
273
emacs.d/swank-js/lisp-tests.js
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
// -*- mode: js2; js-run: t -*-
|
||||
//
|
||||
// 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 lisp = require("./lisp");
|
||||
var assert = require("assert");
|
||||
var S = lisp.S, cons = lisp.cons, consp = lisp.consp, car = lisp.car, cdr = lisp.cdr,
|
||||
nil = lisp.nil, nullp = lisp.nullp, listp = lisp.listp, list = lisp.list,
|
||||
reverse = lisp.reverse, append = lisp.append, repr = lisp.repr,
|
||||
StringInputStream = lisp.StringInputStream,
|
||||
readFromString = lisp.readFromString,
|
||||
fromLisp = lisp.fromLisp,
|
||||
toLisp = lisp.toLisp;
|
||||
|
||||
assert.equal(S("zzz"), S("zzz"));
|
||||
assert.deepEqual(cons(1, cons(2, cons(3, nil))), list(1, 2, 3));
|
||||
assert.equal("abc", car(cons("abc", "def")));
|
||||
assert.equal("def", cdr(cons("abc", "def")));
|
||||
assert.equal(nil, list());
|
||||
assert.ok(consp(cons(1, 2)));
|
||||
assert.ok(!consp(nil));
|
||||
assert.ok(listp(cons(1, 2)));
|
||||
assert.ok(listp(list(1, 2)));
|
||||
assert.ok(listp(nil));
|
||||
assert.ok(nullp(nil));
|
||||
assert.ok(!nullp(cons(1, 2)));
|
||||
assert.ok(!nullp(1));
|
||||
assert.deepEqual(list(), reverse(list()));
|
||||
assert.deepEqual(list(1), reverse(list(1)));
|
||||
assert.deepEqual(list(3, 2, 1), reverse(list(1, 2, 3)));
|
||||
assert.deepEqual(nil, append(nil, nil));
|
||||
assert.deepEqual(list(1), append(list(1), nil));
|
||||
assert.deepEqual(list(1), append(nil, list(1)));
|
||||
assert.deepEqual(list(1, 2, 3), append(list(1, 2), list(3)));
|
||||
assert.deepEqual(list(1, 2, 3, 4), append(list(1, 2), list(3, 4)));
|
||||
|
||||
var s = new StringInputStream("abc");
|
||||
assert.equal(0, s.pos());
|
||||
assert.equal("a", s.getc());
|
||||
assert.equal(1, s.pos());
|
||||
assert.equal("b", s.readchar());
|
||||
assert.equal(2, s.pos());
|
||||
assert.equal("c", s.readchar());
|
||||
assert.equal(3, s.pos());
|
||||
assert.equal(null, s.getc());
|
||||
assert.equal(3, s.pos());
|
||||
assert["throws"](function () { s.readchar(); });
|
||||
assert.equal(3, s.pos());
|
||||
s.ungetc("c");
|
||||
assert.equal(2, s.pos());
|
||||
assert.equal("c", s.getc());
|
||||
assert.equal(3, s.pos());
|
||||
assert["throws"](function () { s.ungetc("z"); });
|
||||
assert.equal(3, s.pos());
|
||||
s.ungetc("c");
|
||||
s.ungetc("b");
|
||||
assert.equal(1, s.pos());
|
||||
assert.equal("b", s.getc());
|
||||
assert.equal("c", s.getc());
|
||||
assert.equal(3, s.pos());
|
||||
s.ungetc("c");
|
||||
s.ungetc("b");
|
||||
s.ungetc("a");
|
||||
|
||||
assert.equal(0, s.pos());
|
||||
assert["throws"](function () { s.ungetc("z"); });
|
||||
assert["throws"](function () { s.ungetc(""); });
|
||||
assert.equal(0, s.pos());
|
||||
assert.equal("a", s.readchar());
|
||||
assert.equal("b", s.readchar());
|
||||
assert.equal("c", s.readchar());
|
||||
assert.equal(3, s.pos());
|
||||
|
||||
s = new StringInputStream("");
|
||||
assert.equal(0, s.pos());
|
||||
assert["throws"](function () { s.ungetc("z"); });
|
||||
assert["throws"](function () { s.ungetc(""); });
|
||||
assert.equal(null, s.getc());
|
||||
assert["throws"](function () { s.readchar(); });
|
||||
assert.equal(0, s.pos());
|
||||
|
||||
function test_read (str, o) {
|
||||
assert.equal(str, repr(o));
|
||||
var rs = readFromString(str);
|
||||
assert.deepEqual(o, rs);
|
||||
assert.equal(str, repr(rs));
|
||||
};
|
||||
|
||||
test_read("zzz", S("zzz"));
|
||||
test_read("'zzz", list(S("quote"), S("zzz")));
|
||||
test_read('"zzz"', "zzz");
|
||||
test_read('"zz\nz"', "zz\nz");
|
||||
test_read('\'"zzz"', list(S("quote"), "zzz"));
|
||||
test_read('"z\\"z\\\\z"', "z\"z\\z");
|
||||
test_read("nil", nil);
|
||||
test_read("(1)", list(1));
|
||||
test_read("(1 2)", list(1, 2));
|
||||
test_read("(1 2 4.25)", list(1, 2, 4.25));
|
||||
test_read("(1 2 eprst)", list(1, 2, S("eprst")));
|
||||
test_read('(1 2 eprst ("abra" "kodabra"))',
|
||||
list(1, 2, S("eprst"), list("abra", "kodabra")));
|
||||
test_read('(1 2 eprst ("abra" . "kodabra"))',
|
||||
list(1, 2, S("eprst"), cons("abra", "kodabra")));
|
||||
test_read('(1 2 eprst ("abra" "kodabra" .schwabbra))',
|
||||
list(1, 2, S("eprst"), list("abra", "kodabra", S(".schwabbra"))));
|
||||
test_read('(1 2 eprst ("abra" "kodabra" .schwabbra . QQQ))',
|
||||
list(1, 2, S("eprst"),
|
||||
cons("abra",cons("kodabra", cons(S(".schwabbra"), S("QQQ"))))));
|
||||
test_read('(1 2 eprst \'("abra" "kodabra" .schwabbra . QQQ))',
|
||||
list(1, 2, S("eprst"),
|
||||
list(S("quote"),
|
||||
cons("abra", cons("kodabra", cons(S(".schwabbra"), S("QQQ")))))));
|
||||
test_read("(1 2 3)", list(1, 2, 3));
|
||||
test_read("(1 2 3 (4 5 6))", list(1, 2, 3, list(4, 5, 6)));
|
||||
test_read("((4 5 6) . 7)", cons(list(4, 5, 6), 7));
|
||||
test_read("((4 5 6) 7 8 . :eprst)",
|
||||
cons(list(4, 5, 6), cons(7, cons(8, S(":eprst")))));
|
||||
test_read("((4 5 6) 7 8 . swank:connection-info)",
|
||||
cons(list(4, 5, 6), cons(7, cons(8, S("swank:connection-info")))));
|
||||
|
||||
var CONV_ERROR = {};
|
||||
|
||||
function test_conversion(spec, source, expectedResult, reconverted) {
|
||||
var r, l = readFromString(source);
|
||||
try {
|
||||
r = spec === null ? fromLisp(l) : fromLisp(l, spec);
|
||||
} catch (e) {
|
||||
if (e instanceof TypeError && /^error converting/.test(e.message))
|
||||
r = CONV_ERROR;
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
assert.deepEqual(expectedResult, r);
|
||||
if (r !== CONV_ERROR)
|
||||
assert.equal(reconverted || source, repr(spec === null ? toLisp(r) : toLisp(r, spec)));
|
||||
}
|
||||
|
||||
test_conversion("N", "1", 1);
|
||||
test_conversion("K", ":abc", "abc");
|
||||
test_conversion("K", "nil", null);
|
||||
test_conversion("B", "t", true);
|
||||
test_conversion("B", "nil", false);
|
||||
test_conversion("B", "123", true, "t");
|
||||
test_conversion("B", ":zzz", true, "t");
|
||||
test_conversion("@", '(test nil () 123 "456" :zzz (1 2 3) (4 . 5))',
|
||||
["test", null, null, 123, "456", ":zzz", [1, 2, 3], [4, 5]],
|
||||
'("test" nil nil 123 "456" ":zzz" (1 2 3) (4 5))');
|
||||
test_conversion(null, '(test nil () 123 "456" :zzz (1 2 3) (4 . 5))',
|
||||
["test", null, null, 123, "456", ":zzz", [1, 2, 3], [4, 5]],
|
||||
'("test" nil nil 123 "456" ":zzz" (1 2 3) (4 5))');
|
||||
test_conversion(["N:one"], "(1)", { one: 1 });
|
||||
test_conversion(["N:one", "N:two", "N:three"], "(1 2 3)", { one: 1, two: 2, three: 3 });
|
||||
test_conversion(["N:one", "N:two", "N:three"], "(1 2)", CONV_ERROR);
|
||||
test_conversion(["N:one", "N:two", "s:zzz"], '(1 2 "qqqq")', { one: 1, two: 2, zzz: "qqqq" });
|
||||
test_conversion(["N:one", "N:two", "s:zzz"], '(1 2 3)', CONV_ERROR);
|
||||
test_conversion(["N:one", "N:two", "s:zzz"], '(1 2 :RRR)', CONV_ERROR);
|
||||
test_conversion(["S:op", "_:form", "_:packageName", "_:threadId", "N:id"],
|
||||
'(:emacs-rex (swank:connection-info) "COMMON-LISP-USER" t 1)',
|
||||
{ op: ":emacs-rex",
|
||||
form: list(S("swank:connection-info")),
|
||||
packageName: "COMMON-LISP-USER",
|
||||
threadId: S("t"),
|
||||
id: 1 });
|
||||
test_conversion(["@:x"], "(test)", { x: "test" }, '("test")');
|
||||
test_conversion(["@:x"], '((test 123 "456" :zzz (1 2 3) (4 . 5)))',
|
||||
{ x: ["test", 123, "456", ":zzz", [1, 2, 3], [4, 5]] },
|
||||
'(("test" 123 "456" ":zzz" (1 2 3) (4 5)))');
|
||||
test_conversion(["S:name", "R:args"], '(test)',
|
||||
{ name: "test",
|
||||
args: [] });
|
||||
test_conversion(["S:name", "R:args"], '(test :abc :def "QQQ" 123)',
|
||||
{ name: "test",
|
||||
args: [":abc", ":def", "QQQ", 123] },
|
||||
'(test ":abc" ":def" "QQQ" 123)');
|
||||
test_conversion(["S:name", "R*:args"], '(test)',
|
||||
{ name: "test",
|
||||
args: [] });
|
||||
test_conversion(["S:name", "R*:args"], '(test :abc :def (123 . 456))',
|
||||
{ name: "test",
|
||||
args: [S(":abc"), S(":def"), cons(123, 456)] });
|
||||
|
||||
test_conversion(["N:n", "D:dict"], '(42.25 ())',
|
||||
{ n: 42.25, dict: {} },
|
||||
'(42.25 nil)');
|
||||
test_conversion(["N:n", "D:dict"], '(42.25 (:x 3))',
|
||||
{ n: 42.25, dict: { x: 3 } });
|
||||
test_conversion(["N:n", "D:dict"], '(42.25 (:x))', CONV_ERROR);
|
||||
test_conversion(["N:n", "D:dict"], '(42.25 (:x :y :z))', CONV_ERROR);
|
||||
test_conversion(["N:n", "D:dict"], '(42.25 (:x 3 :abc "fff" :zzz qwerty))',
|
||||
{ n: 42.25, dict: { x: 3, abc: "fff", zzz: "qwerty" }},
|
||||
'(42.25 (:abc "fff" :x 3 :zzz "qwerty"))');
|
||||
test_conversion(["N:n", "D*:dict"], '(42.25 ())',
|
||||
{ n: 42.25, dict: {} },
|
||||
'(42.25 nil)');
|
||||
test_conversion(["N:n", "D*:dict"], '(42.25 (:x 3))',
|
||||
{ n: 42.25, dict: { x: 3 } });
|
||||
test_conversion(["N:n", "D*:dict"], '(42.25 (:x))', CONV_ERROR);
|
||||
test_conversion(["N:n", "D*:dict"], '(42.25 (:x :y :z))', CONV_ERROR);
|
||||
test_conversion(["N:n", "D*:dict"], '(42.25 (:x 3 :abc "fff" :zzz qwerty))',
|
||||
{ n: 42.25, dict: { x: 3, abc: "fff", zzz: S("qwerty") } },
|
||||
'(42.25 (:abc "fff" :x 3 :zzz qwerty))');
|
||||
|
||||
test_conversion(["N:n", "RD:dict"], '(42.25)',
|
||||
{ n: 42.25, dict: {} });
|
||||
test_conversion(["N:n", "RD:dict"], '(42.25 :x 3)',
|
||||
{ n: 42.25, dict: { x: 3 } });
|
||||
test_conversion(["N:n", "RD:dict"], '(42.25 :x)', CONV_ERROR);
|
||||
test_conversion(["N:n", "RD:dict"], '(42.25 :x 3 :abc "fff" :zzz qwerty)',
|
||||
{ n: 42.25, dict: { x: 3, abc: "fff", zzz: "qwerty" }},
|
||||
'(42.25 :abc "fff" :x 3 :zzz "qwerty")');
|
||||
test_conversion(["N:n", "RD*:dict"], '(42.25)',
|
||||
{ n: 42.25, dict: {} });
|
||||
test_conversion(["N:n", "RD*:dict"], '(42.25 :x 3)',
|
||||
{ n: 42.25, dict: { x: 3 } });
|
||||
test_conversion(["N:n", "RD*:dict"], '(42.25 :x)', CONV_ERROR);
|
||||
test_conversion(["N:n", "RD*:dict"], '(42.25 :x 3 :abc "fff" :zzz qwerty)',
|
||||
{ n: 42.25, dict: { x: 3, abc: "fff", zzz: S("qwerty") } },
|
||||
'(42.25 :abc "fff" :x 3 :zzz qwerty)');
|
||||
|
||||
test_conversion({ x: "N", "abc-def": "D:abcDef", name: "S", rrr: "_:r1", qqq: "_" },
|
||||
'(:abc-def (:x 3 :y 9) :x 42 :name :abcd :rrr "whatever" :unused 99)',
|
||||
{ x: 42, abcDef: { x: 3, y: 9 }, name: ":abcd", r1: "whatever" },
|
||||
'(:abc-def (:x 3 :y 9) :name :abcd :rrr "whatever" :x 42)');
|
||||
|
||||
// > and >* tell arrayValue to consume the next argument as type value
|
||||
test_conversion(["S:name", ">:dict", { x: "N", y: "S" }],
|
||||
'(:somename (:x 32 :y :zzz))',
|
||||
{ name: ":somename", dict: { x: 32, y: ":zzz" } });
|
||||
test_conversion(["S:name", ">*:dict", { x: "N", y: "S" }],
|
||||
'(:somename :x 32 :y :zzz)',
|
||||
{ name: ":somename", dict: { x: 32, y: ":zzz" } });
|
||||
|
||||
test_conversion({ x: "N", l: { name: "theList", spec: ["S:name", "N:n", "K:keyword"] },
|
||||
d: { name: "dict1", spec: { a: "N", b: "N" } },
|
||||
d2: { spec: { a: "N", b: "N" } }},
|
||||
'(:x 99 :l (zzz 42 :eprst) :d (:a 11 :b 12) :d2 (:a 1 :b 2))',
|
||||
{ x: 99,
|
||||
theList: { name: "zzz", n: 42, keyword: "eprst" },
|
||||
dict1: { a: 11, b: 12 },
|
||||
d2: { a: 1, b : 2 } },
|
||||
'(:d (:a 11 :b 12) :d2 (:a 1 :b 2) :l (zzz 42 :eprst) :x 99)');
|
||||
|
||||
assert.equal("(:abc 12 :def 4242)", repr(toLisp({ abc: 12, def: 4242 }, "@")));
|
||||
assert.equal("(:abc 19)", repr(toLisp({ x: 19 }, [S(":abc"), "N:x"])));
|
||||
assert.equal("(abc 19 :def)", repr(toLisp({ x: 19 }, [S("abc"), "N:x", S(":def")])));
|
||||
assert.equal("nil", repr(toLisp(null, "@")));
|
||||
assert.equal("nil", repr(toLisp(null, "_")));
|
||||
|
||||
// TBD: toLisp should use "@" as spec by default
|
||||
600
emacs.d/swank-js/lisp.js
Normal file
600
emacs.d/swank-js/lisp.js
Normal file
|
|
@ -0,0 +1,600 @@
|
|||
// -*- mode: js2; js-run: "lisp-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 util = require("util");
|
||||
var assert = process.assert;
|
||||
|
||||
var I = {};
|
||||
|
||||
function _symbol (name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
_symbol.prototype.toString = function toString () {
|
||||
return this.name;
|
||||
};
|
||||
|
||||
function S(name) {
|
||||
if (I.hasOwnProperty(name))
|
||||
return I[name];
|
||||
return I[name] = new _symbol(name);
|
||||
};
|
||||
|
||||
function symbolp (o) {
|
||||
return o instanceof _symbol;
|
||||
};
|
||||
|
||||
var nil = S("nil");
|
||||
|
||||
function nullp (o) {
|
||||
return o === nil;
|
||||
};
|
||||
|
||||
function _cons (car, cdr) {
|
||||
this.car = car;
|
||||
this.cdr = cdr;
|
||||
}
|
||||
|
||||
_cons.prototype.toString = function toString () {
|
||||
var result = [];
|
||||
if (this.car == S("quote") && consp(this.cdr) && nullp(this.cdr.cdr))
|
||||
return "'" + repr(this.cdr.car);
|
||||
for (var c = this;; c = c.cdr) {
|
||||
if (consp(c))
|
||||
result.push(repr(c.car));
|
||||
else {
|
||||
if (!nullp(c))
|
||||
result.push(". " + repr(c));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return '(' + result.join(" ") + ')';
|
||||
};
|
||||
|
||||
function consp (o) {
|
||||
return o instanceof _cons;
|
||||
}
|
||||
|
||||
function cons (car, cdr) {
|
||||
return new _cons(car, cdr);
|
||||
}
|
||||
|
||||
function car (o) {
|
||||
return o.car;
|
||||
}
|
||||
|
||||
function cdr (o) {
|
||||
return o.cdr;
|
||||
}
|
||||
|
||||
function repr (x) {
|
||||
if (typeof(x) == "string")
|
||||
return '"' + x.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
||||
return String(x);
|
||||
};
|
||||
|
||||
function list () {
|
||||
var tail = nil;
|
||||
for (var i = arguments.length - 1; i >= 0; --i)
|
||||
tail = cons(arguments[i], tail);
|
||||
return tail;
|
||||
}
|
||||
|
||||
function listp (o) {
|
||||
return nullp(o) || consp(o);
|
||||
}
|
||||
|
||||
function reverse (l) {
|
||||
var r = nil;
|
||||
for (; !nullp(l); l = cdr(l))
|
||||
r = cons(car(l), r);
|
||||
return r;
|
||||
}
|
||||
|
||||
function append (l1, l2) {
|
||||
if (l1 === nil)
|
||||
return l2;
|
||||
var r = cons(car(l1), nil), tail = r;
|
||||
while ((l1 = cdr(l1)) !== nil) {
|
||||
tail.cdr = cons(car(l1));
|
||||
tail = tail.cdr;
|
||||
}
|
||||
tail.cdr = l2;
|
||||
return r;
|
||||
}
|
||||
|
||||
function StringInputStream (string) {
|
||||
this._string = string;
|
||||
this._pos = 0;
|
||||
this._max = this._string.length;
|
||||
}
|
||||
|
||||
StringInputStream.prototype.pos = function pos () {
|
||||
return this._pos;
|
||||
};
|
||||
|
||||
StringInputStream.prototype.getc = function getc () {
|
||||
if (this._pos == this._max)
|
||||
return null;
|
||||
return this._string.charAt(this._pos++);
|
||||
};
|
||||
|
||||
StringInputStream.prototype.readchar = function readchar () {
|
||||
var c = this.getc();
|
||||
if (c === null)
|
||||
throw new Error("StringInputStream.readchar(): EOF reached");
|
||||
return c;
|
||||
};
|
||||
|
||||
StringInputStream.prototype.ungetc = function ungetc (c) {
|
||||
if (this._pos > 0 && this._string[this._pos - 1] == c)
|
||||
--this._pos;
|
||||
else { /* FIXME: { is just to make nodejs repl happy */
|
||||
throw new Error("StringInputStream.ungetc(): invalid argument");
|
||||
}
|
||||
};
|
||||
|
||||
function LispReader (s) {
|
||||
this.s = s;
|
||||
}
|
||||
|
||||
LispReader.prototype.readNumberOrSymbol = function readNumberOrSymbol () {
|
||||
var token = this.readToken();
|
||||
if (token == "")
|
||||
throw new Error("LispReader.readNumberOrSymbol(): EOF reached");
|
||||
if (/^[-+]?[0-9]+$/.test(token))
|
||||
return parseInt(token);
|
||||
if (/^[-+]?[0-9]*\.?[0-9]+(?:[dDeE][-+]?[0-9]+)?/.test(token))
|
||||
return parseFloat(token.replace(/d/g, "e"));
|
||||
return S(token);
|
||||
};
|
||||
|
||||
LispReader.prototype.read = function read () {
|
||||
this.skipWhitespace();
|
||||
var c = this.s.getc();
|
||||
switch (c) {
|
||||
case "(":
|
||||
return this.readList();
|
||||
case '"':
|
||||
return this.readString();
|
||||
case "'":
|
||||
return this.readQuote();
|
||||
case null:
|
||||
throw new Error("LispReader.read(): EOF reached");
|
||||
default:
|
||||
this.s.ungetc(c);
|
||||
return this.readNumberOrSymbol();
|
||||
}
|
||||
};
|
||||
|
||||
LispReader.prototype.readList = function readList () {
|
||||
var l = nil;
|
||||
var head = nil;
|
||||
while (true) {
|
||||
this.skipWhitespace();
|
||||
var c = this.s.readchar();
|
||||
switch (c) {
|
||||
case ")":
|
||||
return l;
|
||||
case ".":
|
||||
var c1 = this.s.readchar();
|
||||
if (" \n\t".indexOf(c1) < 0)
|
||||
this.s.ungetc(c1); // process the default case
|
||||
else {
|
||||
if (nullp(l))
|
||||
throw new Error("LispReader.readList(): invalid placement of the dot");
|
||||
head.cdr = this.read();
|
||||
return l;
|
||||
}
|
||||
default:
|
||||
this.s.ungetc(c);
|
||||
if (nullp(l)) {
|
||||
l = list(this.read());
|
||||
head = l;
|
||||
} else {
|
||||
head.cdr = list(this.read());
|
||||
head = head.cdr;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null; /* never get there */
|
||||
};
|
||||
|
||||
LispReader.prototype.readString = function readString () {
|
||||
var r = [];
|
||||
while (true) {
|
||||
var c = this.s.readchar();
|
||||
switch (c) {
|
||||
case '"':
|
||||
return r.join("");
|
||||
case "\\":
|
||||
c = this.s.readchar();
|
||||
if (c != "\\" && c != '"')
|
||||
throw new Error("Invalid escape char " + c);
|
||||
}
|
||||
r.push(c);
|
||||
}
|
||||
return null; /* never get there */
|
||||
};
|
||||
|
||||
LispReader.prototype.readQuote = function readQuote () {
|
||||
return list(S("quote"), this.read());
|
||||
};
|
||||
|
||||
LispReader.prototype.readToken = function readToken () {
|
||||
var c, token = [];
|
||||
while ((c = this.s.getc()) != null) {
|
||||
if (this.isTerminating(c)) {
|
||||
this.s.ungetc(c);
|
||||
break;
|
||||
}
|
||||
token.push(c);
|
||||
}
|
||||
return token.join("");
|
||||
};
|
||||
|
||||
LispReader.prototype.skipWhitespace = function skipWhitespace () {
|
||||
while (true) {
|
||||
var c = this.s.getc();
|
||||
switch (c) {
|
||||
case " ":
|
||||
case "\n":
|
||||
case "\t":
|
||||
continue;
|
||||
case null:
|
||||
return;
|
||||
default:
|
||||
this.s.ungetc(c);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LispReader.prototype.isTerminating = function isTerminating (c) {
|
||||
return " \n\t()\"'".indexOf(c) >= 0;
|
||||
};
|
||||
|
||||
function readFromString (str) {
|
||||
return new LispReader(new StringInputStream(str)).read();
|
||||
}
|
||||
|
||||
function _conversionError (value, spec) {
|
||||
return new TypeError(
|
||||
"error converting " + util.inspect(value) + " using spec " + util.inspect(spec));
|
||||
};
|
||||
|
||||
function naturalValue (v) {
|
||||
if (typeof(v) == "number" || typeof(v) == "string")
|
||||
return v;
|
||||
else if (symbolp(v))
|
||||
return v === nil ? null : v.name;
|
||||
else if (consp(v)) {
|
||||
var result = [];
|
||||
for (; v !== nil; v = cdr(v)) {
|
||||
if (consp(v))
|
||||
result.push(naturalValue(car(v)));
|
||||
else {
|
||||
result.push(naturalValue(v));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else
|
||||
return v;
|
||||
};
|
||||
|
||||
function plistValue (l, useNatural, map) {
|
||||
assert(!map || !useNatural);
|
||||
var origList = l;
|
||||
var result = {};
|
||||
for (; l !== nil; l = cdr(cdr(l))) {
|
||||
if (!consp(l) || !consp(cdr(l)))
|
||||
throw _conversionError(origList, "<plist>");
|
||||
var nameSym = car(l);
|
||||
if (!symbolp(nameSym))
|
||||
throw _conversionError(origList, "<plist>");
|
||||
var value = car(cdr(l));
|
||||
var targetName = nameSym.name.replace(/^.*:/, "").toLowerCase();
|
||||
if (useNatural)
|
||||
result[targetName] = naturalValue(value);
|
||||
else if (map) {
|
||||
if (!map.hasOwnProperty(targetName))
|
||||
continue;
|
||||
var mapSpec = map[targetName];
|
||||
if (typeof(mapSpec) == "object") {
|
||||
assert(mapSpec.spec);
|
||||
result[mapSpec.hasOwnProperty("name") ? mapSpec.name : targetName] =
|
||||
fromLisp(value, mapSpec.spec);
|
||||
} else {
|
||||
var arg = mapSpec.split(/:/);
|
||||
if (arg.length > 1)
|
||||
result[arg[1]] = fromLisp(value, arg[0]);
|
||||
else
|
||||
result[targetName] = fromLisp(value, arg[0]);
|
||||
}
|
||||
} else
|
||||
result[targetName] = value;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
function plainList (l, spec) {
|
||||
var result = {};
|
||||
var origList = l;
|
||||
for (var i = 0; i < spec.length; ++i, l = cdr(l)) {
|
||||
if (l !== nil && !consp(l))
|
||||
throw _conversionError(origList, spec);
|
||||
var arg = spec[i].split(/:/);
|
||||
var type = arg[0];
|
||||
var name = arg[1];
|
||||
if (type == ">") {
|
||||
assert(i < spec.length - 1);
|
||||
type = spec[++i];
|
||||
}
|
||||
if (type == ">*") {
|
||||
assert(i < spec.length - 1);
|
||||
result[name] = fromLisp(l, spec[++i]);
|
||||
l = nil;
|
||||
break;
|
||||
}
|
||||
if (type == "R" || type == "R*") {
|
||||
result[name] = [];
|
||||
for (; l !== nil; l = cdr(l))
|
||||
result[name].push(type == "R*" ? car(l) : naturalValue(car(l)));
|
||||
break;
|
||||
}
|
||||
if (type == "RD" || type == "RD*") {
|
||||
result[name] = plistValue(l, type == "RD");
|
||||
l = nil;
|
||||
break;
|
||||
}
|
||||
if (l === nil)
|
||||
throw _conversionError(origList, spec);
|
||||
result[name] = fromLisp(car(l), type);
|
||||
}
|
||||
if (l !== nil)
|
||||
throw _conversionError(origList, spec);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
function fromLisp (o, spec) {
|
||||
spec = spec || "@";
|
||||
if (typeof(spec) == "string") {
|
||||
switch (spec) {
|
||||
case 'B':
|
||||
return naturalValue(o) !== null;
|
||||
case 'S':
|
||||
if (!symbolp(o))
|
||||
throw _conversionError(o, spec);
|
||||
return nullp(o) ? null : o.name;
|
||||
case 'K':
|
||||
if (!symbolp(o) || (!nullp(o) && !/:/.test(o.name)))
|
||||
throw _conversionError(o, spec);
|
||||
return nullp(o) ? null : o.name.replace(/^:/, "");
|
||||
case 's':
|
||||
if (typeof(o) != "string")
|
||||
throw _conversionError(o, spec);
|
||||
return o;
|
||||
case 'N':
|
||||
if (typeof(o) != "number")
|
||||
throw _conversionError(o, spec);
|
||||
return o;
|
||||
case 'D':
|
||||
case 'D*':
|
||||
return plistValue(o, spec == "D");
|
||||
case "@":
|
||||
return naturalValue(o);
|
||||
case '_':
|
||||
return o;
|
||||
}
|
||||
} else if (spec instanceof Array)
|
||||
return plainList(o, spec);
|
||||
else if (typeof(spec) == "object")
|
||||
return plistValue(o, false, spec);
|
||||
throw new Error("invalid destructuring type spec");
|
||||
}
|
||||
|
||||
function naturalValueToLisp (v) {
|
||||
if (v === null)
|
||||
return nil;
|
||||
if (typeof(v) == "number" || typeof(v) == "string" || symbolp(v) || consp(v))
|
||||
return v;
|
||||
if (v instanceof Array) {
|
||||
var r = nil;
|
||||
for (var i = 0; i < v.length; ++i)
|
||||
r = cons(naturalValueToLisp(v[i]), r);
|
||||
return reverse(r);
|
||||
}
|
||||
if (typeof(v) != "object")
|
||||
throw _conversionError(v, "<natural>");
|
||||
var keys = [];
|
||||
for (var k in v) {
|
||||
if (v.hasOwnProperty(k))
|
||||
keys.push(k);
|
||||
}
|
||||
keys.sort();
|
||||
var r = nil;
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
var keyNameSym = /:/.test(keys[i]) ? S(keys[i]) : S(":" + keys[i]);
|
||||
r = cons(naturalValueToLisp(v[keys[i]]), cons(keyNameSym, r));
|
||||
}
|
||||
return reverse(r);
|
||||
};
|
||||
|
||||
function plistValueToLisp (o, useNatural) {
|
||||
var r = nil;
|
||||
var keys = [];
|
||||
for (var k in o) {
|
||||
if (o.hasOwnProperty(k))
|
||||
keys.push(k);
|
||||
}
|
||||
keys.sort();
|
||||
for (var i = 0; i < keys.length; ++i) {
|
||||
var v = o[keys[i]];
|
||||
if (useNatural)
|
||||
v = naturalValueToLisp(v);
|
||||
var keyNameSym = /:/.test(keys[i]) ? S(keys[i]) : S(":" + keys[i]);
|
||||
r = cons(v, cons(keyNameSym, r));
|
||||
}
|
||||
|
||||
return reverse(r);
|
||||
}
|
||||
|
||||
function mappedPlistValueToLisp (o, map) {
|
||||
var items = [];
|
||||
for (var k in map) {
|
||||
if (!map.hasOwnProperty(k))
|
||||
continue;
|
||||
var mapSpec = map[k];
|
||||
if (typeof(mapSpec) == "object") {
|
||||
assert(mapSpec.spec);
|
||||
items.push({ jsName: mapSpec.hasOwnProperty("name") ? mapSpec.name : k,
|
||||
lispName: k, spec: mapSpec.spec });
|
||||
} else {
|
||||
var arg = mapSpec.split(/:/);
|
||||
items.push({ jsName: arg.length > 1 ? arg[1] : k,
|
||||
lispName: k, spec: arg[0] });
|
||||
}
|
||||
}
|
||||
|
||||
items.sort(function (a, b) {
|
||||
if (a.lispName < b.lispName)
|
||||
return -1;
|
||||
else if (a.lispName > b.lispName)
|
||||
return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
var r = nil;
|
||||
for (var i = 0; i < items.length; ++i) {
|
||||
if (!o.hasOwnProperty(items[i].jsName))
|
||||
continue;
|
||||
var v = toLisp(o[items[i].jsName], items[i].spec);
|
||||
var lispName = items[i].lispName;
|
||||
var keyNameSym = /:/.test(lispName) ? S(lispName) : S(":" + lispName);
|
||||
r = cons(v, cons(keyNameSym, r));
|
||||
}
|
||||
|
||||
return reverse(r);
|
||||
};
|
||||
|
||||
function plainListToLisp (o, spec) {
|
||||
var r = nil;
|
||||
if (typeof(o) != "object")
|
||||
throw _conversionError(o, spec);
|
||||
for (var i = 0; i < spec.length; ++i) {
|
||||
if (symbolp(spec[i])) {
|
||||
r = cons(spec[i], r);
|
||||
continue;
|
||||
}
|
||||
var arg = spec[i].split(/:/);
|
||||
var type = arg[0];
|
||||
var name = arg[1];
|
||||
if (!o.hasOwnProperty(name))
|
||||
throw _conversionError(o, spec);
|
||||
var v = o[name];
|
||||
|
||||
if (type == ">") {
|
||||
assert(i < spec.length - 1);
|
||||
type = spec[++i];
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case ">*":
|
||||
assert(i < spec.length - 1);
|
||||
return append(reverse(r), toLisp(v, spec[++i]));
|
||||
case "R":
|
||||
return append(reverse(r), naturalValueToLisp(v));
|
||||
case "R*":
|
||||
return append(reverse(r), list.apply(null, v));
|
||||
case "RD":
|
||||
case "RD*":
|
||||
return append(reverse(r), plistValueToLisp(v, type == "RD"));
|
||||
default:
|
||||
r = cons(toLisp(v, type), r);
|
||||
}
|
||||
}
|
||||
|
||||
return reverse(r);
|
||||
};
|
||||
|
||||
function toLisp (o, spec) {
|
||||
spec = spec || "@";
|
||||
if (typeof(spec) == "string") {
|
||||
switch (spec) {
|
||||
case 'B':
|
||||
return !o || o === nil ? nil : S("t");
|
||||
case 'S':
|
||||
case 'K':
|
||||
if (symbolp(o))
|
||||
return o;
|
||||
if (o === null)
|
||||
return nil;
|
||||
if (typeof(o) != "string")
|
||||
throw _conversionError(o, spec);
|
||||
return S(spec == "S" ? o : ":" + o);
|
||||
case 's':
|
||||
if (typeof(o) != "string")
|
||||
throw _conversionError(o, spec);
|
||||
return o;
|
||||
case 'N':
|
||||
if (typeof(o) != "number")
|
||||
throw _conversionError(o, spec);
|
||||
return o;
|
||||
case 'D':
|
||||
case 'D*':
|
||||
return plistValueToLisp(o, spec == "D");
|
||||
case "@":
|
||||
return naturalValueToLisp(o);
|
||||
case '_':
|
||||
return o === null ? nil : o;
|
||||
}
|
||||
} else if (spec instanceof Array)
|
||||
return plainListToLisp(o, spec);
|
||||
else if (typeof(spec) == "object")
|
||||
return mappedPlistValueToLisp(o, spec);
|
||||
throw new Error("invalid destructuring type spec");
|
||||
}
|
||||
|
||||
exports.S = S;
|
||||
exports.cons = cons;
|
||||
exports.consp = consp;
|
||||
exports.car = car;
|
||||
exports.cdr = cdr;
|
||||
exports.nil = nil;
|
||||
exports.nullp = nullp;
|
||||
exports.listp = listp;
|
||||
exports.list = list;
|
||||
exports.reverse = reverse;
|
||||
exports.append = append;
|
||||
exports.repr = repr;
|
||||
exports.StringInputStream = StringInputStream;
|
||||
exports.readFromString = readFromString;
|
||||
exports.fromLisp = fromLisp;
|
||||
exports.toLisp = toLisp;
|
||||
203
emacs.d/swank-js/slime-js.el
Normal file
203
emacs.d/swank-js/slime-js.el
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
;;;
|
||||
;;; 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.
|
||||
|
||||
(define-slime-contrib slime-js
|
||||
"Emacs-side support for Swank-JS."
|
||||
(:authors "Ivan Shvedunov")
|
||||
(:license "X11-style")
|
||||
(:on-load
|
||||
(add-hook 'slime-event-hooks 'slime-js-event-hook-function))
|
||||
(:on-unload
|
||||
(remove-hook 'slime-event-hooks 'slime-js-event-hook-function)))
|
||||
|
||||
(defun slime-js-repl-update-package ()
|
||||
(let ((name (slime-current-package)))
|
||||
(with-current-buffer (slime-output-buffer)
|
||||
(let ((previouse-point (- (point) slime-repl-input-start-mark)))
|
||||
(setf (slime-lisp-package) name
|
||||
(slime-lisp-package-prompt-string) name
|
||||
slime-buffer-package name)
|
||||
(slime-repl-insert-prompt)
|
||||
(when (plusp previouse-point)
|
||||
(goto-char (+ previouse-point slime-repl-input-start-mark)))))))
|
||||
|
||||
(defun slime-js-event-hook-function (event)
|
||||
(when (equal "JS" (slime-lisp-implementation-type))
|
||||
(destructure-case event
|
||||
((:new-package package prompt)
|
||||
(let ((buffer (slime-connection-output-buffer)))
|
||||
(setf (slime-lisp-package) package)
|
||||
(setf (slime-lisp-package-prompt-string) prompt)
|
||||
(when (buffer-live-p buffer)
|
||||
(with-current-buffer buffer
|
||||
(setq slime-buffer-package package)
|
||||
(slime-js-repl-update-package)
|
||||
(save-excursion
|
||||
(goto-char (marker-position slime-repl-prompt-start-mark))
|
||||
(slime-mark-output-start))))
|
||||
t))
|
||||
(t nil))))
|
||||
|
||||
(defvar slime-js-remote-history nil
|
||||
"History list for JS remote names.")
|
||||
|
||||
(defun slime-js-read-remote-index (&optional prompt)
|
||||
(let* ((completion-ignore-case nil)
|
||||
(remotes (slime-eval '(js:list-remotes)))
|
||||
(remote-names
|
||||
(loop for remote in remotes
|
||||
collect (concat (third remote)
|
||||
"/"
|
||||
(replace-regexp-in-string
|
||||
"^:" ""(symbol-name (second remote))))))
|
||||
(prompt (or prompt "Remote: "))
|
||||
(p (or (position
|
||||
(completing-read prompt (slime-bogus-completion-alist remote-names)
|
||||
nil nil nil
|
||||
'slime-remote-history nil)
|
||||
remote-names :test #'equal)
|
||||
(error "bad remote name"))))
|
||||
(first (elt remotes p))))
|
||||
|
||||
(defun slime-js-select-remote (n)
|
||||
"Select JS remote by number"
|
||||
(interactive (list (slime-js-read-remote-index)))
|
||||
(slime-eval-async `(js:select-remote ,n nil)))
|
||||
|
||||
(defslime-repl-shortcut slime-repl-js-select-remote ("select-remote")
|
||||
(:handler 'slime-js-select-remote)
|
||||
(:one-liner "Select JS remote."))
|
||||
|
||||
(defun slime-js-sticky-select-remote (n)
|
||||
"Select JS remote by number in sticky mode"
|
||||
(interactive (list (slime-js-read-remote-index)))
|
||||
(slime-eval-async `(js:select-remote ,n t)))
|
||||
|
||||
(defslime-repl-shortcut slime-repl-js-sticky-select-remote ("sticky-select-remote")
|
||||
(:handler 'slime-js-sticky-select-remote)
|
||||
(:one-liner "Select JS remote in sticky mode."))
|
||||
|
||||
(defun slime-js-set-target-url (url)
|
||||
"Set target URL for the proxy"
|
||||
(interactive "sTarget URL: ")
|
||||
(slime-eval-async `(js:set-target-url ,url)))
|
||||
|
||||
(defslime-repl-shortcut slime-repl-js-set-target-url ("target-url")
|
||||
(:handler 'slime-js-set-target-url)
|
||||
(:one-liner "Select target URL for the swank-js proxy"))
|
||||
|
||||
(defun slime-js-set-slime-version (url)
|
||||
"Set SLIME version for swank-js"
|
||||
(interactive "sVersion: ")
|
||||
(slime-eval-async `(js:set-slime-version ,url)))
|
||||
|
||||
(defslime-repl-shortcut slime-repl-js-set-slime-version ("js-slime-version")
|
||||
(:handler 'slime-js-set-slime-version)
|
||||
(:one-liner "Set SLIME version for swank-js"))
|
||||
|
||||
;; FIXME: should add an rpc command for browser-only eval
|
||||
|
||||
(defun slime-js-eval (str &optional cont)
|
||||
(slime-eval-async `(swank:interactive-eval ,str) cont))
|
||||
|
||||
(defun slime-js-reload ()
|
||||
(interactive)
|
||||
(slime-js-eval "SwankJS.reload()"
|
||||
#'(lambda (v)
|
||||
(message "Reloading the page"))))
|
||||
|
||||
(defun slime-js-refresh-css ()
|
||||
(interactive)
|
||||
(slime-js-eval "SwankJS.refreshCSS()"
|
||||
#'(lambda (v)
|
||||
(message "Refreshing CSS"))))
|
||||
|
||||
(defun slime-js-start-of-toplevel-form ()
|
||||
(interactive)
|
||||
(when js2-mode-buffer-dirty-p
|
||||
(js2-mode-wait-for-parse #'slime-js-start-of-toplevel-form))
|
||||
(js2-forward-sws)
|
||||
(if (= (point) (point-max))
|
||||
(js2-mode-forward-sexp -1)
|
||||
(let ((node (js2-node-at-point)))
|
||||
(when (or (null node)
|
||||
(js2-ast-root-p node))
|
||||
(error "cannot locate any toplevel form"))
|
||||
(while (and (js2-node-parent node)
|
||||
(not (js2-ast-root-p (js2-node-parent node))))
|
||||
(setf node (js2-node-parent node)))
|
||||
(goto-char (js2-node-abs-pos node))
|
||||
(js2-forward-sws)))
|
||||
(point))
|
||||
|
||||
(defun slime-js-end-of-toplevel-form ()
|
||||
(interactive)
|
||||
(js2-forward-sws)
|
||||
(let ((node (js2-node-at-point)))
|
||||
(unless (or (null node) (js2-ast-root-p node))
|
||||
(while (and (js2-node-parent node)
|
||||
(not (js2-ast-root-p (js2-node-parent node))))
|
||||
(setf node (js2-node-parent node)))
|
||||
(goto-char (js2-node-abs-end node)))
|
||||
(point)))
|
||||
|
||||
;; FIXME: this breaks if // comment directly precedes the function
|
||||
(defun slime-js-send-defun ()
|
||||
(interactive)
|
||||
(save-excursion
|
||||
(lexical-let ((start (slime-js-start-of-toplevel-form))
|
||||
(end (slime-js-end-of-toplevel-form)))
|
||||
;; FIXME: use slime-eval-region
|
||||
(slime-js-eval
|
||||
(buffer-substring-no-properties start end)
|
||||
#'(lambda (v)
|
||||
(save-excursion
|
||||
(goto-char start)
|
||||
(let ((sent-func "<...>"))
|
||||
(when (looking-at "[ \t]*\\([^ \t\n{}][^\n{}]*\\)")
|
||||
(setf sent-func (match-string 1)))
|
||||
(message "Sent: %s" sent-func))))))))
|
||||
|
||||
(define-minor-mode slime-js-minor-mode
|
||||
"Toggle slime-js minor mode
|
||||
With no argument, this command toggles the mode.
|
||||
Non-null prefix argument turns on the mode.
|
||||
Null prefix argument turns off the mode."
|
||||
nil
|
||||
" slime-js"
|
||||
'(("\C-\M-x" . slime-js-send-defun)
|
||||
("\C-c\C-c" . slime-js-send-defun)
|
||||
;; ("\C-c\C-r" . slime-eval-region)
|
||||
("\C-c\C-z" . slime-switch-to-output-buffer)))
|
||||
|
||||
;; TBD: dabbrev in repl:
|
||||
;; DABBREV--GOTO-START-OF-ABBREV function skips over REPL prompt
|
||||
;; because it has property 'intangible' and (forward-char -1) doesn't do
|
||||
;; what is expected at the propmpt edge. Must redefine this function
|
||||
;; or define and advice for it.
|
||||
;; TBD: lost continuations (pipelined request ...) - maybe when closing page
|
||||
(provide 'slime-js)
|
||||
292
emacs.d/swank-js/swank-handler-tests.js
Normal file
292
emacs.d/swank-js/swank-handler-tests.js
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
// -*- mode: js2; js-run: t -*-
|
||||
//
|
||||
// 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 swh = require("./swank-handler");
|
||||
var readFromString = require("./lisp").readFromString;
|
||||
var config = require("./config");
|
||||
var util = require("util");
|
||||
var assert = require("assert");
|
||||
|
||||
var cfg = new config.FakeConfig();
|
||||
var expected = [];
|
||||
var executive = new swh.Executive({ config: cfg, pid: 4242 });
|
||||
var handler = new swh.Handler(executive);
|
||||
|
||||
handler.on(
|
||||
"response", function (response) {
|
||||
// console.log("=> %s", response);
|
||||
assert.ok(expected.length > 0);
|
||||
assert.ok(typeof(response) == "string");
|
||||
// console.log("response: %s", response);
|
||||
var expectedResponse = expected.shift();
|
||||
if (expectedResponse instanceof RegExp)
|
||||
assert.ok(expectedResponse.test(response));
|
||||
else
|
||||
assert.equal(expectedResponse, response,
|
||||
"got response " + response + " instead of " + expectedResponse);
|
||||
});
|
||||
|
||||
function expect () {
|
||||
for (var i = 0; i < arguments.length; ++i)
|
||||
expected.push(arguments[i]);
|
||||
}
|
||||
|
||||
function verifyExpectations () {
|
||||
// console.log("expected: %s\n", expected.map(JSON.stringify).join("\n"));
|
||||
assert.equal(0, expected.length);
|
||||
}
|
||||
|
||||
function request (str) {
|
||||
for (var i = 1; i < arguments.length; ++i)
|
||||
expected.push(arguments[i]);
|
||||
handler.receive(readFromString(str));
|
||||
verifyExpectations();
|
||||
}
|
||||
|
||||
request('(:emacs-rex (swank:connection-info) "COMMON-LISP-USER" t 1)',
|
||||
'(:return (:ok (:encoding (:coding-system "utf-8" :external-format "UTF-8") ' +
|
||||
':lisp-implementation (:name "JS" :type "JS" :version "1.5") ' +
|
||||
':package (:name "NODE" :prompt "NODE") ' +
|
||||
':pid 4242 :version "2010-11-13")) ' +
|
||||
'1)');
|
||||
|
||||
// currently we just ignore swank-require
|
||||
request('(:emacs-rex (swank:swank-require \'(swank-listener-hooks swank-indentation)) "COMMON-LISP-USER" t 2)',
|
||||
'(:return (:ok nil) 2)');
|
||||
|
||||
request('(:emacs-rex (swank:create-repl nil) "COMMON-LISP-USER" t 3)',
|
||||
'(:return (:ok ("NODE" "NODE")) 3)');
|
||||
|
||||
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 4)',
|
||||
'(:return (:ok (:values "30")) 4)');
|
||||
|
||||
request('(:emacs-rex (swank:listener-eval "undefined") "NODE" :repl-thread 5)',
|
||||
'(:return (:ok nil) 5)');
|
||||
|
||||
request('(:emacs-rex (swank:autodoc \'("zzzz" swank::%cursor-marker%) :print-right-margin 236)' +
|
||||
' "COMMON-LISP-USER" :repl-thread 6)',
|
||||
'(:return (:ok :not-available) 6)');
|
||||
|
||||
request('(:emacs-rex (swank:listener-eval "_swank.output(\'hello world\\\\n\')") "NODE" :repl-thread 7)',
|
||||
'(:write-string "hello world\n")',
|
||||
'(:return (:ok nil) 7)');
|
||||
|
||||
request('(:emacs-rex (swank:listener-eval "_swank.output(1234)") "NODE" :repl-thread 8)',
|
||||
'(:write-string "1234")',
|
||||
'(:return (:ok nil) 8)');
|
||||
|
||||
request('(:emacs-rex (swank:listener-eval "zzz") "NODE" :repl-thread 9)',
|
||||
/^\(:write-string "ReferenceError: zzz is not defined(.|\n)*"\)$/,
|
||||
'(:return (:ok nil) 9)');
|
||||
|
||||
// TBD: debugger
|
||||
|
||||
function FakeRemote (name) {
|
||||
this.name = name;
|
||||
};
|
||||
|
||||
util.inherits(FakeRemote, swh.Remote);
|
||||
|
||||
FakeRemote.prototype.prompt = function prompt () {
|
||||
return "FAKE";
|
||||
};
|
||||
|
||||
FakeRemote.prototype.kind = function kind () {
|
||||
return "test";
|
||||
};
|
||||
|
||||
FakeRemote.prototype.id = function id () {
|
||||
return this.name;
|
||||
};
|
||||
|
||||
FakeRemote.prototype.evaluate = function evaluate (id, str) {
|
||||
this.sendResult(id, [ "R:" + this.name + ":" + str.replace(/^\s*|\s*$/g, "") ]);
|
||||
};
|
||||
|
||||
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 10)',
|
||||
'(:return (:ok ((1 :direct "node.js" t))) 10)');
|
||||
|
||||
expect('(:write-string "Remote attached: (test) test/localhost:8080\n")');
|
||||
var r1 = new FakeRemote("test/localhost:8080");
|
||||
executive.attachRemote(r1);
|
||||
verifyExpectations();
|
||||
|
||||
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 11)',
|
||||
'(:return (:ok ((1 :direct "node.js" t) (2 :test "test/localhost:8080" nil))) 11)');
|
||||
|
||||
expect('(:write-string "Remote attached: (test) test/localhost:9999\n")');
|
||||
var r2 = new FakeRemote("test/localhost:9999");
|
||||
executive.attachRemote(r2);
|
||||
verifyExpectations();
|
||||
|
||||
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 12)',
|
||||
'(:return (:ok ((1 :direct "node.js" t) ' +
|
||||
'(2 :test "test/localhost:8080" nil) ' +
|
||||
'(3 :test "test/localhost:9999" nil))) 12)');
|
||||
|
||||
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 13)',
|
||||
'(:return (:ok (:values "30")) 13)');
|
||||
|
||||
request('(:emacs-rex (js:select-remote 2 nil) "NODE" :repl-thread 14)',
|
||||
'(:new-package "FAKE" "FAKE")',
|
||||
'(:write-string "Remote selected: (test) test/localhost:8080\n")',
|
||||
'(:return (:ok nil) 14)');
|
||||
|
||||
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 15)',
|
||||
'(:return (:ok ((1 :direct "node.js" nil) ' +
|
||||
'(2 :test "test/localhost:8080" t) ' +
|
||||
'(3 :test "test/localhost:9999" nil))) 15)');
|
||||
|
||||
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 16)',
|
||||
'(:return (:ok (:values "R:test/localhost:8080:3 * 10")) 16)');
|
||||
|
||||
expect('(:write-string "Remote detached: (test) test/localhost:8080\n")',
|
||||
'(:new-package "NODE" "NODE")',
|
||||
'(:write-string "Remote selected (auto): (direct) node.js\n")');
|
||||
r1.disconnect();
|
||||
verifyExpectations();
|
||||
|
||||
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 17)',
|
||||
'(:return (:ok (:values "30")) 17)');
|
||||
|
||||
// TBD: add higher-level functions for testing remotes
|
||||
|
||||
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 18)',
|
||||
'(:return (:ok ((1 :direct "node.js" t) ' +
|
||||
'(3 :test "test/localhost:9999" nil))) 18)');
|
||||
|
||||
expect('(:write-string "Remote detached: (test) test/localhost:9999\n")');
|
||||
r2.disconnect();
|
||||
verifyExpectations();
|
||||
|
||||
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 19)',
|
||||
'(:return (:ok (:values "30")) 19)');
|
||||
|
||||
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 20)',
|
||||
'(:return (:ok ((1 :direct "node.js" t))) 20)');
|
||||
|
||||
request('(:emacs-rex (js:select-remote 2 nil) "NODE" :repl-thread 21)',
|
||||
'(:write-string "WARNING: bad remote index\n")',
|
||||
'(:return (:ok nil) 21)');
|
||||
|
||||
request('(:emacs-rex (swank:listener-eval "3 * 10\n") "NODE" :repl-thread 22)',
|
||||
'(:return (:ok (:values "30")) 22)');
|
||||
|
||||
request('(:emacs-rex (js:select-remote 1 nil) "NODE" :repl-thread 23)',
|
||||
'(:write-string "WARNING: remote already selected: (direct) node.js\n")',
|
||||
'(:return (:ok nil) 23)');
|
||||
|
||||
assert.equal(null, cfg.getNow("stickyRemote"));
|
||||
|
||||
// test sticky remote selection
|
||||
expect('(:write-string "Remote attached: (test) test/localhost:8001\n")');
|
||||
var r3 = new FakeRemote("test/localhost:8001");
|
||||
executive.attachRemote(r3);
|
||||
verifyExpectations();
|
||||
|
||||
request('(:emacs-rex (js:list-remotes) "NODE" :repl-thread 24)',
|
||||
'(:return (:ok ((1 :direct "node.js" t) (4 :test "test/localhost:8001" nil))) 24)');
|
||||
|
||||
request('(:emacs-rex (js:select-remote 4 t) "NODE" :repl-thread 25)',
|
||||
'(:new-package "FAKE" "FAKE")',
|
||||
'(:write-string "Remote selected (sticky): (test) test/localhost:8001\n")',
|
||||
'(:return (:ok nil) 25)');
|
||||
|
||||
assert.equal("(test) test/localhost:8001", cfg.getNow("stickyRemote"));
|
||||
|
||||
expect('(:write-string "Remote detached: (test) test/localhost:8001\n")',
|
||||
'(:new-package "NODE" "NODE")',
|
||||
'(:write-string "Remote selected (auto): (direct) node.js\n")');
|
||||
r3.disconnect();
|
||||
verifyExpectations();
|
||||
|
||||
expect('(:write-string "Remote attached: (test) test/localhost:8001\n")',
|
||||
'(:new-package "FAKE" "FAKE")',
|
||||
'(:write-string "Remote selected (auto): (test) test/localhost:8001\n")');
|
||||
var r5 = new FakeRemote("test/localhost:8001");
|
||||
executive.attachRemote(r5);
|
||||
verifyExpectations();
|
||||
|
||||
assert.equal("(test) test/localhost:8001", cfg.getNow("stickyRemote"));
|
||||
|
||||
request('(:emacs-rex (js:select-remote 1 nil) "NODE" :repl-thread 26)',
|
||||
'(:new-package "NODE" "NODE")',
|
||||
'(:write-string "Remote selected: (direct) node.js\n")',
|
||||
'(:return (:ok nil) 26)');
|
||||
|
||||
request('(:emacs-rex (js:select-remote 5 nil) "NODE" :repl-thread 27)',
|
||||
'(:new-package "FAKE" "FAKE")',
|
||||
'(:write-string "Remote selected: (test) test/localhost:8001\n")',
|
||||
'(:return (:ok nil) 27)');
|
||||
|
||||
assert.equal(null, cfg.getNow("stickyRemote"));
|
||||
|
||||
expect('(:write-string "Remote detached: (test) test/localhost:8001\n")',
|
||||
'(:new-package "NODE" "NODE")',
|
||||
'(:write-string "Remote selected (auto): (direct) node.js\n")');
|
||||
r5.disconnect();
|
||||
verifyExpectations();
|
||||
|
||||
expect('(:write-string "Remote attached: (test) test/localhost:8001\n")');
|
||||
var r6 = new FakeRemote("test/localhost:8001");
|
||||
executive.attachRemote(r6);
|
||||
verifyExpectations();
|
||||
|
||||
assert.equal(null, cfg.getNow("stickyRemote"));
|
||||
|
||||
request('(:emacs-rex (js:set-target-url "http://localhost:1234/") "NODE" :repl-thread 28)',
|
||||
'(:return (:ok nil) 28)');
|
||||
|
||||
assert.equal("http://localhost:1234/", cfg.getNow("targetUrl"));
|
||||
|
||||
request('(:emacs-rex (js:set-target-url "zzz") "NODE" :repl-thread 29)',
|
||||
'(:write-string "WARNING: the URL must contain host and port\n")',
|
||||
'(:return (:ok nil) 29)');
|
||||
|
||||
assert.equal("http://localhost:1234/", cfg.getNow("targetUrl"));
|
||||
|
||||
assert.equal(null, cfg.getNow("slimeVersion"));
|
||||
|
||||
request('(:emacs-rex (js:set-slime-version "2010-11-28") "NODE" :repl-thread 30)',
|
||||
'(:return (:ok nil) 30)');
|
||||
|
||||
assert.equal("2010-11-28", cfg.getNow("slimeVersion"));
|
||||
|
||||
// TBD: use ## instead of numbers in the tests above (request() should take care of it)
|
||||
// TBD: test output from an inactive remote
|
||||
// TBD: are out-of-order results for :emacs-rex ok? look at slime sources
|
||||
|
||||
/*
|
||||
|
||||
list/select remotes along the lines of
|
||||
|
||||
catching errors on the client: window.onerror
|
||||
http://stackoverflow.com/questions/951791/javascript-global-error-handling
|
||||
*/
|
||||
|
||||
// TBD: add \n to messages from remotes / executive
|
||||
416
emacs.d/swank-js/swank-handler.js
Normal file
416
emacs.d/swank-js/swank-handler.js
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
// -*- 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;
|
||||
76
emacs.d/swank-js/swank-protocol-tests.js
Normal file
76
emacs.d/swank-js/swank-protocol-tests.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// -*- mode: js2; js-run: t -*-
|
||||
//
|
||||
// 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 swp = require("./swank-protocol");
|
||||
var lisp = require("./lisp");
|
||||
var Buffer = require('buffer').Buffer;
|
||||
var assert = require("assert");
|
||||
var S = lisp.S, list = lisp.list, nil = lisp.nil;
|
||||
|
||||
var expected = [];
|
||||
|
||||
var parser = new swp.SwankParser(
|
||||
function onMessage (message) {
|
||||
assert.ok(expected.length > 0);
|
||||
var expectedMessage = expected.shift();
|
||||
assert.deepEqual(expectedMessage, message);
|
||||
});
|
||||
|
||||
function feed (text) {
|
||||
for (var i = 1; i < arguments.length; ++i)
|
||||
expected.push(arguments[i]);
|
||||
parser.execute(text);
|
||||
assert.equal(0, expected.length);
|
||||
}
|
||||
|
||||
// dispatch: see dispatch-event in swank.lisp
|
||||
feed("000");
|
||||
feed("03b");
|
||||
feed("(:emacs-rex (swank:connection-info) \"COMMON-LISP-USER\" t 1)",
|
||||
list(S(":emacs-rex"), list(S("swank:connection-info")),
|
||||
"COMMON-LISP-USER", S("t"), 1));
|
||||
|
||||
feed("0");
|
||||
feed("0");
|
||||
feed("0");
|
||||
feed("03b(:emacs-rex (swank:connection-info)");
|
||||
feed(" \"COMMON-LISP-USER\" t 1)000",
|
||||
list(S(":emacs-rex"), list(S("swank:connection-info")),
|
||||
"COMMON-LISP-USER", S("t"), 1));
|
||||
|
||||
feed("03b(:emacs-rex (swank:connection-info)");
|
||||
feed(" \"COMMON-LISP-USER\" t 1");
|
||||
feed(")",
|
||||
list(S(":emacs-rex"), list(S("swank:connection-info")),
|
||||
"COMMON-LISP-USER", S("t"), 1));
|
||||
|
||||
assert.equal(
|
||||
"000015(:return (:ok nil) 1)",
|
||||
swp.buildMessage(list(S(":return"), list(S(":ok"), nil), 1)));
|
||||
|
||||
// TBD: check unicode string handling (use \uxxxx notation)
|
||||
87
emacs.d/swank-js/swank-protocol.js
Normal file
87
emacs.d/swank-js/swank-protocol.js
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// -*- mode: js2; js-run: "swank-protocol-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 readFromString = require("./lisp").readFromString;
|
||||
|
||||
const HEADER_LEN = 6;
|
||||
const DUMMY_HEADER = "000000";
|
||||
const MAX_MESSAGE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
function SwankParser (onMessage) {
|
||||
this.needChars = HEADER_LEN;
|
||||
this.handleData = this.handleHeader;
|
||||
this.stash = "";
|
||||
this.onMessage = onMessage;
|
||||
};
|
||||
|
||||
// FIXME: proper error handling (handle both packet parsing and reader errors)
|
||||
|
||||
SwankParser.prototype.execute = function execute (text) {
|
||||
var offset = 0;
|
||||
while (offset < text.length)
|
||||
offset += this.handleContent(text, offset);
|
||||
};
|
||||
|
||||
SwankParser.prototype.handleContent = function handleContent (text, offset) {
|
||||
var stashLen = this.stash.length;
|
||||
var avail = Math.min(this.needChars, text.length + stashLen - offset);
|
||||
var message = this.stash + text.substring(offset, offset + avail - stashLen);
|
||||
if (avail < this.needChars)
|
||||
this.stash = message;
|
||||
else {
|
||||
this.stash = "";
|
||||
this.handleData(message);
|
||||
}
|
||||
return message.length - stashLen;
|
||||
};
|
||||
|
||||
SwankParser.prototype.handleHeader = function handleHeader (str) {
|
||||
var count = parseInt(str, 16) || 0;
|
||||
if (count > 0 && count < MAX_MESSAGE_SIZE) {
|
||||
this.needChars = count;
|
||||
this.handleData = this.handleMessage;
|
||||
} else
|
||||
this.needChars = HEADER_LEN; // FIXME: handle errors
|
||||
};
|
||||
|
||||
SwankParser.prototype.handleMessage = function handleMessage (str) {
|
||||
this.onMessage(readFromString(str)); // FIXME: handle errors
|
||||
this.needChars = HEADER_LEN;
|
||||
this.handleData = this.handleHeader;
|
||||
};
|
||||
|
||||
function buildMessage (obj) {
|
||||
var str = obj.toString();
|
||||
var lenStr = "" + str.length.toString(16);
|
||||
while (lenStr.length < HEADER_LEN)
|
||||
lenStr = "0" + lenStr;
|
||||
return lenStr + str;
|
||||
};
|
||||
|
||||
exports.SwankParser = SwankParser;
|
||||
exports.buildMessage = buildMessage;
|
||||
425
emacs.d/swank-js/swank.js
Normal file
425
emacs.d/swank-js/swank.js
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
// -*- mode: js2 -*-
|
||||
//
|
||||
// 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 net = require("net"), http = require('http'), io = require('socket.io'), util = require("util"),
|
||||
url = require('url'), fs = require('fs');
|
||||
var swh = require("./swank-handler");
|
||||
var swp = require("./swank-protocol");
|
||||
var ua = require("./user-agent");
|
||||
var config = require("./config");
|
||||
|
||||
var DEFAULT_TARGET_HOST = "localhost";
|
||||
var DEFAULT_TARGET_PORT = 8080;
|
||||
var CONFIG_FILE_NAME = "~/.swankjsrc";
|
||||
|
||||
var cfg = new config.Config(CONFIG_FILE_NAME);
|
||||
var executive = new swh.Executive({ config: cfg });
|
||||
|
||||
var swankServer = net.createServer(
|
||||
function (stream) {
|
||||
stream.setEncoding("utf-8");
|
||||
var handler = new swh.Handler(executive);
|
||||
var parser = new swp.SwankParser(
|
||||
function onMessage (message) {
|
||||
handler.receive(message);
|
||||
});
|
||||
handler.on(
|
||||
"response", function (response) {
|
||||
var responseText = swp.buildMessage(response);
|
||||
console.log("response: %s", responseText);
|
||||
stream.write(responseText);
|
||||
});
|
||||
stream.on(
|
||||
"data", function (data) {
|
||||
parser.execute(data);
|
||||
});
|
||||
stream.on(
|
||||
"end", function () {
|
||||
// FIXME: notify handler -> executive
|
||||
// TBD: destroy the handler
|
||||
handler.removeAllListeners("response");
|
||||
});
|
||||
});
|
||||
swankServer.listen(process.argv[2] || 4005, process.argv[3] || "localhost");
|
||||
|
||||
function BrowserRemote (clientInfo, client) {
|
||||
var userAgent = ua.recognize(clientInfo.userAgent);
|
||||
this.name = userAgent.replace(/ /g, "") + (clientInfo.address ? (":" + clientInfo.address) : "");
|
||||
this._prompt = userAgent.toUpperCase().replace(/ /g, '-');
|
||||
this.client = client;
|
||||
this.client.on(
|
||||
"message", function(m) {
|
||||
// TBD: handle parse errors
|
||||
// TBD: validate incoming message (id, etc.)
|
||||
console.log("message from browser: %s", JSON.stringify(m));
|
||||
switch(m.op) {
|
||||
case "output":
|
||||
this.output(m.str);
|
||||
break;
|
||||
case "result":
|
||||
if (m.error) {
|
||||
this.output(m.error + "\n");
|
||||
this.sendResult(m.id, []);
|
||||
break;
|
||||
}
|
||||
this.sendResult(m.id, m.values);
|
||||
break;
|
||||
default:
|
||||
console.log("WARNING: cannot interpret the client message");
|
||||
}
|
||||
}.bind(this));
|
||||
this.client.on(
|
||||
"disconnect", function() {
|
||||
console.log("client disconnected: %s", this.id());
|
||||
this.disconnect();
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
util.inherits(BrowserRemote, swh.Remote);
|
||||
|
||||
BrowserRemote.prototype.prompt = function prompt () {
|
||||
return this._prompt;
|
||||
};
|
||||
|
||||
BrowserRemote.prototype.kind = function kind () {
|
||||
return "browser";
|
||||
};
|
||||
|
||||
BrowserRemote.prototype.id = function id () {
|
||||
return this.name;
|
||||
};
|
||||
|
||||
BrowserRemote.prototype.evaluate = function evaluate (id, str) {
|
||||
this.client.send({ id: id, code: str });
|
||||
};
|
||||
|
||||
// proxy code from http://www.catonmat.net/http-proxy-in-nodejs
|
||||
|
||||
function HttpListener (cfg) {
|
||||
this.config = cfg;
|
||||
}
|
||||
|
||||
HttpListener.prototype.clientVersion = "0.1";
|
||||
|
||||
HttpListener.prototype.cachedFiles = {};
|
||||
|
||||
HttpListener.prototype.clientFiles = {
|
||||
'json2.js': 'json2.js',
|
||||
'stacktrace.js': 'stacktrace.js',
|
||||
'swank-js.js': 'swank-js.js',
|
||||
'load.js': 'load.js',
|
||||
'test.html': 'test.html'
|
||||
};
|
||||
|
||||
HttpListener.prototype.types = {
|
||||
html: "text/html; charset=utf-8",
|
||||
js: "text/javascript; charset=utf-8"
|
||||
};
|
||||
|
||||
HttpListener.prototype.scriptBlock =
|
||||
new Buffer(
|
||||
'<script type="text/javascript" src="/swank-js/json2.js"></script>' +
|
||||
'<script type="text/javascript" src="/socket.io/socket.io.js"></script>' +
|
||||
'<script type="text/javascript" src="/swank-js/stacktrace.js"></script>' +
|
||||
'<script type="text/javascript" src="/swank-js/swank-js.js"></script>');
|
||||
|
||||
HttpListener.prototype.findClosingTag = function findClosingTag (buffer, name) {
|
||||
// note: this function is suitable for <head> and <body> tags,
|
||||
// because they don't contain any repeating letters, but
|
||||
// it will not work for tags that have such letters
|
||||
var chars = [];
|
||||
var endChar = ">".charCodeAt(0);
|
||||
name = "</" + name.toLowerCase();
|
||||
for (var i = 0; i < name.length; ++i)
|
||||
chars.push(name.charCodeAt(i));
|
||||
var A_CODE = "A".charCodeAt(0), Z_CODE = "Z".charCodeAt(0), CODE_INC = "a".charCodeAt(0) - A_CODE;
|
||||
function codeToLower (x) {
|
||||
return x >= A_CODE && x <= Z_CODE ? x + CODE_INC : x;
|
||||
}
|
||||
for (i = 0; i < buffer.length - chars.length - 1;) {
|
||||
var found = true;
|
||||
if (buffer[i++] != chars[0]) // note: no lowercasing for matching against '<'
|
||||
continue;
|
||||
|
||||
for (var j = 1; j < chars.length; ++j, ++i) {
|
||||
if (codeToLower(buffer[i]) != chars[j]) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
for (var k = i; k < buffer.length; ++k) {
|
||||
if (buffer[k] == endChar)// note: no lowercasing for matching against '>'
|
||||
return i - chars.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
HttpListener.prototype.injectScripts = function injectScripts (buffer, url) {
|
||||
var p = this.findClosingTag(buffer, "head");
|
||||
if (p < 0) {
|
||||
p = this.findClosingTag(buffer, "body");
|
||||
if (p < 0) {
|
||||
// html blocks without head / body tags aren't that uncommon
|
||||
// console.log("WARNING: unable to inject script block: %s", url);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
var newBuf = new Buffer(buffer.length + this.scriptBlock.length);
|
||||
buffer.copy(newBuf, 0, 0, p);
|
||||
this.scriptBlock.copy(newBuf, p, 0);
|
||||
buffer.copy(newBuf, p + this.scriptBlock.length, p);
|
||||
return newBuf;
|
||||
};
|
||||
|
||||
HttpListener.prototype.proxyRequest = function proxyRequest (request, response) {
|
||||
var self = this;
|
||||
this.config.get(
|
||||
"targetUrl",
|
||||
function (targetUrl) {
|
||||
self.doProxyRequest(targetUrl, request, response);
|
||||
});
|
||||
};
|
||||
|
||||
HttpListener.prototype.doProxyRequest = function doProxyRequest (targetUrl, request, response) {
|
||||
var self = this;
|
||||
var headersSent = false;
|
||||
var done = false;
|
||||
|
||||
var hostname = DEFAULT_TARGET_HOST;
|
||||
var port = DEFAULT_TARGET_PORT;
|
||||
var parsedUrl = null;
|
||||
try {
|
||||
parsedUrl = url.parse(targetUrl);
|
||||
} catch (e) {}
|
||||
if (parsedUrl && parsedUrl.hostname) {
|
||||
hostname = parsedUrl.hostname;
|
||||
port = parsedUrl.port ? parsedUrl.port - 0 : 80;
|
||||
}
|
||||
|
||||
request.headers["host"] = hostname + (port == 80 ? "" : ":" + port);
|
||||
delete request.headers["accept-encoding"]; // we don't want gzipped pages, do we?
|
||||
|
||||
// note on http client error handling:
|
||||
// http://rentzsch.tumblr.com/post/664884799/node-js-handling-refused-http-client-connections
|
||||
var proxy = http.createClient(port, hostname);
|
||||
proxy.addListener(
|
||||
'error', function handleError (e) {
|
||||
console.log("proxy error: %s", e);
|
||||
if (done)
|
||||
return;
|
||||
if (headersSent) {
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
response.writeHead(502, {'Content-Type': 'text/plain; charset=utf-8'});
|
||||
response.end("swank-js: unable to forward the request");
|
||||
});
|
||||
|
||||
console.log("PROXY: %s %s", request.method, request.url);
|
||||
var proxyRequest = proxy.request(request.method, request.url, request.headers);
|
||||
|
||||
proxyRequest.addListener(
|
||||
'response', function (proxyResponse) {
|
||||
var contentType = proxyResponse.headers["content-type"];
|
||||
var statusCode = proxyResponse.statusCode;
|
||||
console.log("==> status %s", statusCode);
|
||||
var headers = {};
|
||||
for (k in proxyResponse.headers) {
|
||||
if (proxyResponse.headers.hasOwnProperty(k))
|
||||
headers[k] = proxyResponse.headers[k];
|
||||
}
|
||||
var chunks = proxyResponse.statusCode == 200 && contentType && /^text\/html\b|^application\/xhtml\+xml/.test(contentType) ?
|
||||
[] : null;
|
||||
if (chunks === null) {
|
||||
// FIXME: without this, there were problems with redirects.
|
||||
// I don't quite understand why...
|
||||
response.writeHead(statusCode, headers);
|
||||
headersSent = true;
|
||||
}
|
||||
proxyResponse.addListener(
|
||||
'data', function (chunk) {
|
||||
if (chunks !== null) {
|
||||
chunks.push(chunk);
|
||||
return;
|
||||
}
|
||||
if (!headersSent) {
|
||||
response.writeHead(statusCode, headers);
|
||||
headersSent = true;
|
||||
}
|
||||
response.write(chunk, 'binary');
|
||||
});
|
||||
proxyResponse.addListener(
|
||||
'end', function() {
|
||||
if (chunks !== null) {
|
||||
console.log("^^MOD: %s %s", request.method, request.url);
|
||||
var buf = new Buffer(chunks.reduce(function (s, chunk) { return s += chunk.length; }, 0));
|
||||
var p = 0;
|
||||
chunks.forEach(
|
||||
function (chunk) {
|
||||
chunk.copy(buf, p, 0);
|
||||
p += chunk.length;
|
||||
});
|
||||
buf = self.injectScripts(buf, request.url);
|
||||
headers["content-length"] = buf.length;
|
||||
response.writeHead(statusCode, headers);
|
||||
headersSent = true;
|
||||
response.write(buf, 'binary');
|
||||
} else if (!headersSent) {
|
||||
response.writeHead(statusCode, headers);
|
||||
headersSent = true;
|
||||
}
|
||||
|
||||
response.end();
|
||||
done = true;
|
||||
});
|
||||
});
|
||||
request.addListener(
|
||||
'data', function(chunk) {
|
||||
proxyRequest.write(chunk, 'binary');
|
||||
});
|
||||
request.addListener(
|
||||
'end', function() {
|
||||
proxyRequest.end();
|
||||
});
|
||||
};
|
||||
|
||||
HttpListener.prototype.sendCachedFile = function sendCachedFile (req, res, path) {
|
||||
if (req.headers['if-none-match'] == this.clientVersion) {
|
||||
res.writeHead(304);
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200, this.cachedFiles[path].headers);
|
||||
res.end(this.cachedFiles[path].content, this.cachedFiles[path].encoding);
|
||||
}
|
||||
};
|
||||
|
||||
HttpListener.prototype.notFound = function notFound (res) {
|
||||
res.writeHead(404, {'Content-Type': 'text/plain; charset=utf-8'});
|
||||
res.end("file not found");
|
||||
};
|
||||
|
||||
HttpListener.prototype.serveClient = function serveClient(req, res) {
|
||||
var self = this;
|
||||
var path = url.parse(req.url).pathname, parts, cn;
|
||||
// console.log("%s %s", req.method, req.url);
|
||||
if (path && path.indexOf("/swank-js/") != 0) {
|
||||
// console.log("--> proxy");
|
||||
this.proxyRequest(req, res);
|
||||
return;
|
||||
}
|
||||
var file = path.substr(1).split('/').slice(1);
|
||||
|
||||
var localPath = this.clientFiles[file];
|
||||
if (req.method == 'GET' && localPath !== undefined){
|
||||
// TBD: reenable caching, check datetime of the file
|
||||
// if (path in this.cachedFiles){
|
||||
// this.sendCachedFile(req, res, path);
|
||||
// return;
|
||||
// }
|
||||
|
||||
fs.readFile(
|
||||
__dirname + '/client/' + localPath, function(err, data) {
|
||||
if (err) {
|
||||
console.log("error: %s", err);
|
||||
self.notFound(res);
|
||||
} else {
|
||||
var ext = localPath.split('.').pop();
|
||||
self.cachedFiles[localPath] = {
|
||||
headers: {
|
||||
'Content-Length': data.length,
|
||||
'Content-Type': self.types[ext],
|
||||
'ETag': self.clientVersion
|
||||
},
|
||||
content: data,
|
||||
encoding: ext == 'swf' ? 'binary' : 'utf8'
|
||||
};
|
||||
self.sendCachedFile(req, res, localPath);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("bad request for /swank-js/ path");
|
||||
this.notFound(res);
|
||||
}
|
||||
};
|
||||
|
||||
var httpListener = new HttpListener(cfg);
|
||||
var httpServer = http.createServer(httpListener.serveClient.bind(httpListener));
|
||||
|
||||
httpServer.listen(8009);
|
||||
|
||||
var socket = io.listen(httpServer);
|
||||
socket.on(
|
||||
"connection", function (client) {
|
||||
// new client is here!
|
||||
console.log("client connected");
|
||||
function handleHandshake (message) {
|
||||
client.removeListener("message", handleHandshake);
|
||||
if (!message.hasOwnProperty("op") || !message.op == "handshake")
|
||||
console.warn("WARNING: bad handshake message: %j", message);
|
||||
else {
|
||||
var address = null;
|
||||
if (client.connection && client.connection.remoteAddress)
|
||||
address = client.connection.remoteAddress;
|
||||
var remote = new BrowserRemote({ address: address, userAgent: message.userAgent }, client);
|
||||
executive.attachRemote(remote);
|
||||
console.log("added remote: %s", remote.fullName());
|
||||
}
|
||||
};
|
||||
client.on("message", handleHandshake);
|
||||
});
|
||||
|
||||
// TBD: handle reader errors
|
||||
|
||||
// function location determination:
|
||||
// for code loaded from scripts: direct (if possible)
|
||||
// for 'compiled' code: load the code by adding <script> tag loaded from the swank-js' webserver, its name should encode the real path and line offset
|
||||
// for code entered via REPL: none
|
||||
// PREPROCESS STACK TRACES!!!
|
||||
// https://github.com/emwendelin/Javascript-Stacktrace
|
||||
// ALSO: http://blog.yoursway.com/2009/07/3-painful-ways-to-obtain-stack-trace-in.html -- onerror in ie gives the innermost frame
|
||||
// it should be also possible to 'soft-trace' functions so that they extend Exception objects with caller info as it passes through them
|
||||
// TBD: unix domain sockets, normal slime startup
|
||||
// TBD: http request logging (for specific remote)
|
||||
// TBD: sudden disconnections (flashsocket), sometimes after lots of output (?) --
|
||||
// Error: You are trying to call recursively into the Flash Player which is not allowed. In most cases the JavaScript setTimeout function, can be used as a workaround.
|
||||
// TBD: autoreconnect + connection error handling
|
||||
// ALSO: are htmlfile, jsonp-polling modes etc supposed to disconnect after each message?
|
||||
// TBD: add SwankJS scripts to all passing html pages (into <head> or <body>)
|
||||
// TBD: it should be possible to serve local files instead of proxying
|
||||
// (maybe using https://github.com/felixge/node-paperboy )
|
||||
// TBD: handle edge case: new sticky remote connects, old sticky remote disconnects
|
||||
// (late disconnect) - as of now, swank-js switches to node.js, but it should
|
||||
// instead upon remote detachment see whether another remote with the same name
|
||||
// is available
|
||||
// TBD: handle/add X-Forwarded-For headers
|
||||
// TBD: fix all assert calls: we need (actual, expected) not (expected, actual)
|
||||
// TBD: invoke SwankJS.setup() only when DOM is ready (at least in IE)
|
||||
// TBD: timeouts for browser requests
|
||||
89
emacs.d/swank-js/user-agent-tests.js
Normal file
89
emacs.d/swank-js/user-agent-tests.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// -*- mode: js2; js-run: t -*-
|
||||
//
|
||||
// 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 assert = require("assert");
|
||||
var useragent = require("./user-agent");
|
||||
|
||||
// for reference: http://www.pgts.com.au/cgi-bin/psql?agent_main
|
||||
var USER_AGENT_TESTS = [
|
||||
[ "Firefox 0.9", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7) Gecko/20040707 Firefox/0.9.2 StumbleUpon/1.998" ],
|
||||
[ "Firefox 1.5", "Mozilla/5.0 (Windows NT 5.1; U; SV1; MEGAUPLOAD 1.0; ru; rv:1.8.0) Gecko/20060728 Firefox/1.5.0 Opera 9.23" ],
|
||||
[ "Firefox 2.0", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.13) Gecko/20081108 Firefox/2.0.0.13" ],
|
||||
[ "Firefox 3.6", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3" ],
|
||||
[ "Firefox 3.5", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5" ],
|
||||
[ "Firefox 3.0", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.0.6) Gecko/2009011913 Firefox/3.0.6" ],
|
||||
[ "Firefox 4.0", "Mozilla/5.0 (Windows NT 5.1; rv:2.0b6) Gecko/20100101 Firefox/4.0b6" ],
|
||||
[ "Firefox", "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; ToolKit; rv:1.9.0.8) Gecko/2009032609 Firefox MRA 5.5 (build 02842);" ],
|
||||
[ "Opera 11.00", "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.6.37 Version/11.00" ],
|
||||
[ "Opera 10.62", "Opera/9.80 (Windows NT 5.1; U; ru) Presto/2.6.30 Version/10.62" ],
|
||||
[ "Opera 9.50", "Opera/9.50 (Windows NT 6.0; U; MRA 5.5 (build 02842); ru)" ],
|
||||
[ "Opera 8.01", "Opera/8.01 (J2ME/MIDP; Opera Mini/3.1.9427/1724; ru; U; ssr)" ],
|
||||
[ "Opera 7.0", "Mozilla/4.0 (compatible; MSIE 6.0; MSIE 5.5; Windows NT 4.0) Opera 7.0 [en]" ],
|
||||
[ "Opera 6.0", "Mozilla/4.0 (compatible; MSIE 5.0; Windows 2000) Opera 6.0 [en]" ],
|
||||
[ "Opera 5.11", "Mozilla/4.0 (compatible; MSIE 5.0; Windows ME) Opera 5.11 [en]" ],
|
||||
[ "Chrome 6.0", "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3" ],
|
||||
[ "ChromeFrame 6.0", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0) chromeframe/6.0.472.63" ],
|
||||
[ "Safari 4.0", "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_2; ru-ru) AppleWebKit/531.21.8 (KHTML, like Gecko) Version/4.0.4 Safari/531.21.10" ],
|
||||
[ "iPhone 3.1.3", "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_1_3 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Mobile/7E18" ],
|
||||
[ "iPad 3.2.2", "Mozilla/5.0 (iPad; U; CPU OS 3_2_2 like Mac OS X; ru-ru) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B500 Safari/531.21.10" ],
|
||||
[ "iPod", "Mozilla/5.0 (iPod; U; CPU like Mac OS X; en) AppleWebKit/420.1 (KHTML, like Gecko) Version/3.0 Mobile/3A101a Safari/419.3" ],
|
||||
[ "QtWeb 4.7", "Mozilla/5.0 (X11; U; Linux x86_64; C) AppleWebKit/533.3 (KHTML, like Gecko) Qt/4.7.0 Safari/533.3" ],
|
||||
[ "WebKit", "Mozilla/5.0 (X11; U; Linux x86_64; C) AppleWebKit/533.3 (KHTML, like Gecko) Safari/533.3" ], // made up
|
||||
[ "WebKit", "Mozilla/5.0 (X11; U; Linux x86_64; C) AppleWebKit/533.3 (KHTML, like Gecko)" ], // made up
|
||||
[ "WebKit", "Midori/0.2 (X11; Linux; U; en-us) WebKit/531.2+" ],
|
||||
[ "Conkeror 0.9", "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.10) Gecko/20100915 Conkeror/0.9.2" ],
|
||||
[ "Gecko", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.4b) Gecko/20030516 Mozilla Firebird/0.6" ],
|
||||
[ "SeaMonkey 2.0", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.4) Gecko/20091028 SeaMonkey/2.0" ],
|
||||
[ "Android 2.2", "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Droid Build/FRG22D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1"],
|
||||
[ "Android 1.6", "Mozilla/5.0 (Linux; U; Android 1.6; en-us; T-Mobile G1 Build/DMD64) AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1"],
|
||||
[ "MSIE 9.0", "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)" ],
|
||||
[ "MSIE 8.0", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; GTB5; MRSPUTNIK 2, 3, 0, 104; MRA 5.6 (build 03392); .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)" ],
|
||||
[ "MSIE 7.0", "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; InfoPath.2; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)" ],
|
||||
[ "MSIE 6.0", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; InfoPath.2; .NET CLR 2.0.50727)" ],
|
||||
[ "MSIE 5.01", "Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)" ],
|
||||
[ "MSIE 5.5", "Mozilla/4.0 (compatible; MSIE 5.5; Windows 98)" ],
|
||||
[ "MSIE 4.01", "Mozilla/4.0 (compatible; MSIE 4.01; Windows 95)" ],
|
||||
[ "MSIE 4.01", "Mozilla/4.0 (compatible; MSIE 4.01; Digital AlphaServer 1000A 4/233; Windows NT; Powered By 64-Bit Alpha Processor)" ],
|
||||
[ "MSIE 3.0", "Mozilla/2.0 (compatible; MSIE 3.0B; Windows NT)" ],
|
||||
[ "MSIE 3.0", "Mozilla/2.0 (compatible; MSIE 3.0B; Windows NT)" ],
|
||||
[ "MSIE 3.02", "Mozilla/2.0 (compatible; MSIE 3.02; Windows CE; 240x320)" ],
|
||||
[ "MSIE 3.01", "Mozilla/2.0 (compatible; MSIE 3.01; Windows 98)" ],
|
||||
[ "MSIE 2.0", "Mozilla/1.22 (compatible; MSIE 2.0d; Windows NT)" ],
|
||||
[ "MSIE 1.5", "Mozilla/1.22 (compatible; MSIE 1.5; Windows NT)" ],
|
||||
[ "MSIE 1.0", "Mozilla/1.0 (compatible; MSIE 1.0; Windows 3.11)" ], // made up
|
||||
[ "unknown", "don't know" ],
|
||||
[ "unknown", "" ]
|
||||
];
|
||||
|
||||
USER_AGENT_TESTS.forEach(
|
||||
function (item) {
|
||||
var actual = useragent.recognize(item[1]);
|
||||
assert.equal(
|
||||
item[0], actual,
|
||||
"got " + actual + " instead of " + item[0] + " for " + item[1]);
|
||||
});
|
||||
54
emacs.d/swank-js/user-agent.js
Normal file
54
emacs.d/swank-js/user-agent.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// -*- mode: js2; js-run: "user-agent-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 RX_TABLE = [
|
||||
[ /^.*(Firefox|Chrome|chromeframe|Conkeror|SeaMonkey)\/(\d+\.\d+).*$/, "$1 $2"],
|
||||
[ /^.*Firefox.*$/, "Firefox" ],
|
||||
[ /^.*Opera.*Version\/(\d+\.\d+).*$/, "Opera $1" ],
|
||||
[ /^.*Opera[/ ](\d+\.\d+).*$/, "Opera $1" ],
|
||||
[ /^.*Android (\d+\.\d+).*$/, "Android $1" ],
|
||||
[ /^.*iPhone;.*OS (\d+(?:_\d+)*).*$/, "iPhone $1" ],
|
||||
[ /^.*iPad;.*OS (\d+(?:_\d+)*).*$/, "iPad $1" ],
|
||||
[ /^.*iPod;.*$/, "iPod" ],
|
||||
[ /^.*Version\/(\d+\.\d+).*Safari.*$/, "Safari $1" ],
|
||||
[ /^.*Qt\/(\d+\.\d+).*$/, "QtWeb $1" ],
|
||||
[ /^.*WebKit.*$/, "WebKit" ],
|
||||
[ /^.*Gecko.*$/, "Gecko" ],
|
||||
[ /^.*MSIE (\d+\.\d+).*$/, "MSIE $1" ]
|
||||
];
|
||||
|
||||
exports.recognize = function recognize (name) {
|
||||
// console.log("name=%s", name);
|
||||
for (var i = 0; i < RX_TABLE.length; ++i) {
|
||||
var r = name.replace(RX_TABLE[i][0], RX_TABLE[i][1]);
|
||||
// console.log("m=%s r=%s", RX_TABLE[i][0], r);
|
||||
if (r != name)
|
||||
return r.replace(/chromeframe/, "ChromeFrame").replace(/_/g, ".");
|
||||
}
|
||||
return "unknown";
|
||||
};
|
||||
Loading…
Reference in a new issue