diff --git a/emacs.d/swank-js/LICENSE b/emacs.d/swank-js/LICENSE new file mode 100644 index 0000000..1d10642 --- /dev/null +++ b/emacs.d/swank-js/LICENSE @@ -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 + +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. diff --git a/emacs.d/swank-js/README.md b/emacs.d/swank-js/README.md new file mode 100644 index 0000000..b7589bb --- /dev/null +++ b/emacs.d/swank-js/README.md @@ -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(''); + [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. diff --git a/emacs.d/swank-js/client/json2.js b/emacs.d/swank-js/client/json2.js new file mode 100644 index 0000000..22b44d9 --- /dev/null +++ b/emacs.d/swank-js/client/json2.js @@ -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'); + }; + } +}()); diff --git a/emacs.d/swank-js/client/load.js b/emacs.d/swank-js/client/load.js new file mode 100644 index 0000000..ac6812f --- /dev/null +++ b/emacs.d/swank-js/client/load.js @@ -0,0 +1 @@ +SwankJS.setup(); diff --git a/emacs.d/swank-js/client/stacktrace.js b/emacs.d/swank-js/client/stacktrace.js new file mode 100644 index 0000000..a166181 --- /dev/null +++ b/emacs.d/swank-js/client/stacktrace.js @@ -0,0 +1,356 @@ +// Domain Public by Eric Wendelin http://eriwen.com/ (2008) +// Luke Smith http://lucassmith.name/ (2008) +// Loic Dachary (2008) +// Johan Euphrosine (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 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.\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 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 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 ((/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 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 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 JS source URL + * @return 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 '(?)'; + } +}; diff --git a/emacs.d/swank-js/client/swank-js.js b/emacs.d/swank-js/client/swank-js.js new file mode 100644 index 0000000..61ac373 --- /dev/null +++ b/emacs.d/swank-js/client/swank-js.js @@ -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(); diff --git a/emacs.d/swank-js/client/test.html b/emacs.d/swank-js/client/test.html new file mode 100644 index 0000000..f85a810 --- /dev/null +++ b/emacs.d/swank-js/client/test.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/emacs.d/swank-js/config.js b/emacs.d/swank-js/config.js new file mode 100644 index 0000000..54fb16c --- /dev/null +++ b/emacs.d/swank-js/config.js @@ -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; diff --git a/emacs.d/swank-js/lisp-tests.js b/emacs.d/swank-js/lisp-tests.js new file mode 100644 index 0000000..9b81f6a --- /dev/null +++ b/emacs.d/swank-js/lisp-tests.js @@ -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 diff --git a/emacs.d/swank-js/lisp.js b/emacs.d/swank-js/lisp.js new file mode 100644 index 0000000..db87af1 --- /dev/null +++ b/emacs.d/swank-js/lisp.js @@ -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, ""); + var nameSym = car(l); + if (!symbolp(nameSym)) + throw _conversionError(origList, ""); + 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, ""); + 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; diff --git a/emacs.d/swank-js/slime-js.el b/emacs.d/swank-js/slime-js.el new file mode 100644 index 0000000..8aef661 --- /dev/null +++ b/emacs.d/swank-js/slime-js.el @@ -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) diff --git a/emacs.d/swank-js/swank-handler-tests.js b/emacs.d/swank-js/swank-handler-tests.js new file mode 100644 index 0000000..9e0c27e --- /dev/null +++ b/emacs.d/swank-js/swank-handler-tests.js @@ -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 diff --git a/emacs.d/swank-js/swank-handler.js b/emacs.d/swank-js/swank-handler.js new file mode 100644 index 0000000..c4720d3 --- /dev/null +++ b/emacs.d/swank-js/swank-handler.js @@ -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; diff --git a/emacs.d/swank-js/swank-protocol-tests.js b/emacs.d/swank-js/swank-protocol-tests.js new file mode 100644 index 0000000..b5d56b7 --- /dev/null +++ b/emacs.d/swank-js/swank-protocol-tests.js @@ -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) diff --git a/emacs.d/swank-js/swank-protocol.js b/emacs.d/swank-js/swank-protocol.js new file mode 100644 index 0000000..376fbbb --- /dev/null +++ b/emacs.d/swank-js/swank-protocol.js @@ -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; diff --git a/emacs.d/swank-js/swank.js b/emacs.d/swank-js/swank.js new file mode 100644 index 0000000..3d49925 --- /dev/null +++ b/emacs.d/swank-js/swank.js @@ -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( + '' + + '' + + '' + + ''); + +HttpListener.prototype.findClosingTag = function findClosingTag (buffer, name) { + // note: this function is suitable for and 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 = "= 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