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