UNPKG

swank-client

Version:

A Javascript client for interacting with a Lisp swank server

961 lines (851 loc) 35.5 kB
var net = require('net'); var paredit = require('paredit.js'); var util = require('./utils.js') /***************************************************************** Client Configuration and Setup *****************************************************************/ /* The Client class manages connecting and talking to a Swank server. The constructor takes a host and port to connect to. There are three class fields that can be accessed: host (the host to connect to), port (the port to connect to), and connected (whether connected to a client). The host and port can be set, and the new values will be used the next time the Client trys to connect. */ function Client(host, port) { this.host = host; this.port = port; this.socket = null; this.connected = false; // Useful protocol information this.req_counter = 1; this.req_table = {}; this.on_handlers = { connect: function() {}, print_string: function(m) {}, presentation_start: function (pid) {}, presentation_end: function (pid) {}, new_package: function(p) {}, debug_activate: function(obj) {}, debug_setup: function(obj) {}, debug_return: function(obj) {}, read_from_minibuffer: function(prompt, initial_value) {}, y_or_n_p: function(question) {}, read_string: function(tag) {}, read_aborted: function(tag) {}, profile_command_complete: function(msg) {}, compiler_notes: function(notes) {}, disconnect: function() {} }; // Bootstrap the reading state this.setup_read(6, this.header_complete_callback); } /* Adds a listener to the given event. The possible events include: connect, disconnect, print_string, presentation_start, presentation_end, new_package, debug_activate, debug_setup, debug_return, read_from_minibuffer, y_or_n_p, read_string, read_aborted, and profile_command_complete, compiler_notes */ Client.prototype.on = function(event, fn) { this.on_handlers[event] = fn; } /* Attempts to connect ot the swank server, and returns a promise for when the connection is made. */ Client.prototype.connect = function() { var sc = this; // Because the 'this' operator changes scope return new Promise(function(resolve, reject) { // Create a socket sc.socket = net.connect({ host: sc.host, port: sc.port }, function() { sc.connected = true; resolve(); }); sc.socket.setNoDelay(true); sc.socket.on('error', function(err) { reject(); }); sc.socket.on('data', function(data) { sc.socket_data_handler(data); }); sc.socket.on('close', function() { sc.connected = false; sc.on_handlers.disconnect(); }); }); } /* Disconnects from the swank server, if connected */ Client.prototype.disconnect = function() { if (this.connected) { this.socket.end(); } } /* Returns whether the client is connected to a swank server */ Client.prototype.is_connected = function() { return this.connected; } /***************************************************************** Swank Data Structure Parsers *****************************************************************/ /* Parses a paredit sexp of either the form (:location <buffer> <position> <hints>) or the form (:error <message>) into a location object.*/ function parse_location(location_sexp) { srcloc = {}; if (location_sexp.children[0].source.toLowerCase() == ":error"){ srcloc.buffer_type = "error"; srcloc.error = util.from_lisp_string(location_sexp.children[1]); } else { var raw_buffer_sexp = location_sexp.children[1]; srcloc.buffer_type = raw_buffer_sexp.children[0].source.slice(1).toLowerCase(); if (srcloc.buffer_type == 'file') { srcloc.file = util.from_lisp_string(raw_buffer_sexp.children[1]); } else if (srcloc.buffer_type == 'buffer') { srcloc.buffer_name = util.from_lisp_string(raw_buffer_sexp.children[1]); } else if (srcloc.buffer_type == 'buffer-and-file') { srcloc.buffer_name = util.from_lisp_string(raw_buffer_sexp.children[1]); srcloc.file = util.from_lisp_string(raw_buffer_sexp.children[2]); } else if (srcloc.buffer_type == 'source-form') { srcloc.source_form = util.from_lisp_string(raw_buffer_sexp.children[1]); } else if (srcloc.buffer_type == 'zip') { srcloc.zip_file = util.from_lisp_string(raw_buffer_sexp.children[1]); srcloc.zip_entry = util.from_lisp_string(raw_buffer_sexp.children[1]); } var raw_position_sexp = location_sexp.children[2]; srcloc.position_type = raw_position_sexp.children[0].source.slice(1).toLowerCase(); if (srcloc.position_type == 'position') { srcloc.position_offset = Number(raw_position_sexp.children[1].source); } else if (srcloc.position_type == 'offset') { srcloc.position_type = 'position' srcloc.position_offset = Number(raw_position_sexp.children[1].source) + Number(raw_position_sexp.children[2].source); } else if (srcloc.position_type == 'line') { srcloc.position_line = Number(raw_position_sexp.children[1]); if (raw_position_sexp.children.length >= 3) { srcloc.position_column = Number(raw_position_sexp.children[2]); } } else if (srcloc.position_type == 'function-name') { srcloc.position_function = raw_position_sexp.children[1].source; } else if (srcloc.position_type == 'source_path') { if (raw_position_sexp.children[1].type.toLowerCase == 'list') { srcloc.position_source_path_list = raw_position_sexp.children[1].map(function(elt) { return elt.source; }); } else { srcloc.position_source_path_list = []; } srcloc.position_source_path_start = raw_position_sexp.children[2].source; } else if (srcloc.position_type == 'method') { srcloc.position_method_name = raw_position_sexp.children[1].source; if (raw_position_sexp.children[2].type.toLowerCase == 'list') { srcloc.position_specializers = raw_position_sexp.children[2].map(function(elt) { return elt.source; }); } else { srcloc.position_specializers = []; } srcloc.position_qualifiers = raw_position_sexp.children.slice(3).map(function(elt) { return elt.source; }); } srcloc.hints = location_sexp.children.slice(3); } return srcloc; } /***************************************************************** Low-level data handling protocol *****************************************************************/ Client.prototype.send_message = function(msg) { var len_str = Buffer.byteLength(msg).toString(16); len_str = Array((6 - len_str.length) + 1).join('0') + len_str; // Assemble overall message var msg_overall = len_str + msg; // Send it // console.log("Write:") // console.log(" Length: " + len_str + " (" + msg_utf8.length + ")"); // console.log(" Msg: ..."); // console.log(msg_overall) // Great for debugging! this.socket.write(msg_overall); } /* Some data just came in over the wire. Make sure to read it in message chunks with the length */ Client.prototype.socket_data_handler = function(source) { var byte_offset = 0; // offset into source that has been copied so far while (byte_offset < Buffer.byteLength(source)) { var amount_to_read = Math.min(this.len_remaining, Buffer.byteLength(source) - byte_offset); var copied = source.copy(this.buffer, Buffer.byteLength(this.buffer) - this.len_remaining, byte_offset, byte_offset + amount_to_read); this.len_remaining -= copied; byte_offset += copied; if (this.len_remaining == 0) { this.buffer_complete_callback(this.buffer.toString('utf8')); } } } Client.prototype.setup_read = function(length, fn) { this.buffer = Buffer.alloc(length); this.len_remaining = length; this.buffer_complete_callback = fn; } Client.prototype.header_complete_callback = function(data) { // Parse the length var len = parseInt(data, 16); // Set up to read data this.setup_read(len, this.data_complete_callback); } Client.prototype.data_complete_callback = function(data) { // Call the handler try { this.on_swank_message(data); } catch (e) { console.log("Error in swank-js callback"); console.log(e); } // Set up again to read the header this.setup_read(6, this.header_complete_callback); // It's 6 bytes long } Client.prototype.on_swank_message = function(msg) { // console.log(msg); // Great for debugging! var ast = paredit.parse(msg); var sexp = ast.children[0]; var cmd = sexp.children[0].source.toLowerCase(); if (cmd == ":return") { this.swank_message_rex_return_handler(sexp); } else if (cmd == ':write-string') { this.on_handlers.print_string(util.from_lisp_string(sexp.children[1])); if(sexp.children.length>3 && util.from_lisp_bool(sexp.children[3])) { var thread = sexp.children[3].source; this.send_message('(:write-done '+thread+')') } } else if (cmd == ':presentation-start') { var presentation_id = sexp.children[1].source; this.on_handlers.presentation_start(presentation_id); } else if (cmd == ':presentation-end') { var presentation_id = sexp.children[1].source; this.on_handlers.presentation_end(presentation_id); } else if (cmd == ":new-package") { this.on_handlers.new_package(util.from_lisp_string(sexp.children[1])); } else if (cmd == ":debug") { this.debug_setup_handler(sexp); } else if (cmd == ":debug-activate") { this.debug_activate_handler(sexp); } else if (cmd == ":debug-return") { this.debug_return_handler(sexp); } else if (cmd == ":read-from-minibuffer") { this.read_from_minibuffer_handler(sexp); } else if (cmd == ":y-or-n-p") { this.y_or_n_p_handler(sexp); } else if (cmd == ":read-string") { this.read_string_handler(sexp); } else if (cmd == ":read-aborted") { this.read_aborted_handler(sexp); } else if (cmd == ":ping") { this.ping_handler(sexp); } else { console.log("Ignoring command " + cmd); } } Client.prototype.rex = function(cmd, pkg, thread) { // Run an EMACS-REX command, and call the callback // when we have a return value, with the parsed paredit s-expression var sc = this; var resolve_fn = null; var id = sc.req_counter; var promise = new Promise(function(resolve, reject) { // Dispatch a command to swank resolve_fn = resolve; var rex_cmd = "(:EMACS-REX " + cmd + " \"" + pkg + "\" " + thread + " " + id + ")"; // console.log(rex_cmd); sc.send_message(rex_cmd); }); // Add an entry into our table! sc.req_counter = sc.req_counter + 1; sc.req_table[id] = { id: id, cmd: cmd, pkg: pkg, promise_resolve_fn: resolve_fn }; return promise; } Client.prototype.swank_message_rex_return_handler = function(cmd) { var status = cmd.children[1].children[0].source.toLowerCase(); var return_val = cmd.children[1].children[1]; var id = cmd.children[2].source; // Look up the appropriate callback and return it! if (id in this.req_table) { var req = this.req_table[id]; delete this.req_table[id]; // console.log("Resolving " + id); req.promise_resolve_fn(return_val); } else { console.error("Received REX response for unknown command ID"); } } Client.prototype.ping_handler = function(sexp) { // Swank occasionally send's ping messages to see if we're okay. // We must respond! var response = '(:EMACS-PONG ' + sexp.children[1].source + ' ' + sexp.children[2].source + ')'; this.send_message(response); } Client.prototype.read_from_minibuffer_handler = function(sexp) { var thread = sexp.children[1].source, tag = sexp.children[2].source, prompt = util.from_lisp_string(sexp.children[3]), initial_value = sexp.children[4].source; Promise.resolve(this.on_handlers.read_from_minibuffer(prompt, initial_value)) .then(function(answer) { this.send_message('(:EMACS-RETURN '+thread+' '+tag+' '+util.to_lisp_string(answer)+')') }.bind(this)); } Client.prototype.y_or_n_p_handler = function(sexp) { var thread = sexp.children[1].source, tag = sexp.children[2].source, question = util.from_lisp_string(sexp.children[3]); Promise.resolve(this.on_handlers.y_or_n_p(question)) .then(function(answer) { this.send_message('(:EMACS-RETURN '+thread+' '+tag+' '+util.to_lisp_bool(answer)+')'); }.bind(this)); } Client.prototype.read_string_handler = function(sexp) { var thread = sexp.children[1].source, tag = sexp.children[2].source; Promise.resolve(this.on_handlers.read_string(tag)) .then(function(string) { this.send_message('(:EMACS-RETURN-STRING '+thread+' '+tag+' '+util.to_lisp_string(string)+')'); }.bind(this)); } Client.prototype.read_aborted_handler = function(sexp) { var thread = sexp.children[1].source, tag = sexp.children[2].source; this.on_handlers.read_aborted(tag); } /***************************************************************** Higher-level commands *****************************************************************/ Client.prototype.initialize = function() { // Run these useful initialization commands one after another var self = this; return self.rex("(SWANK:SWANK-REQUIRE \ '(SWANK-IO-PACKAGE::SWANK-TRACE-DIALOG SWANK-IO-PACKAGE::SWANK-PACKAGE-FU \ SWANK-IO-PACKAGE::SWANK-PRESENTATIONS SWANK-IO-PACKAGE::SWANK-FUZZY \ SWANK-IO-PACKAGE::SWANK-FANCY-INSPECTOR SWANK-IO-PACKAGE::SWANK-C-P-C \ SWANK-IO-PACKAGE::SWANK-ARGLISTS SWANK-IO-PACKAGE::SWANK-REPL))", 'COMMON-LISP-USER', 'T') .then(function(response) { return self.rex("(SWANK:INIT-PRESENTATIONS)", 'COMMON-LISP-USER', 'T'); }).then(function(response) { return self.rex('(SWANK-REPL:CREATE-REPL NIL :CODING-SYSTEM "utf-8-unix")', 'COMMON-LISP-USER', 'T'); }); } /* Gets autodocumentation for the given sexp, given the cursor's position */ Client.prototype.autodoc = function(sexp_string, cursor_position, pkg) { var ast = paredit.parse(sexp_string); try { var forms = ast.children[0]; var output_forms = []; var didCursor = false; for(var i = 0; i < forms.children.length; i++) { var form = forms.children[i]; output_forms.push(util.to_lisp_string(sexp_string.substring(form.start, form.end))); if (cursor_position >= form.start && cursor_position <= form.end && !didCursor) { output_forms.push('SWANK::%CURSOR-MARKER%'); didCursor = true; break; } } if (!didCursor) { output_forms.push('""'); output_forms.push('SWANK::%CURSOR-MARKER%'); didCursor = true; } var cmd = '(SWANK:AUTODOC \'('; // '"potato" SWANK::%CURSOR-MARKER%) :PRINT-RIGHT-MARGIN 80)'; cmd += output_forms.join(' '); cmd += ') :PRINT-RIGHT-MARGIN 80)'; } catch (e) { // Return a promise with nothing then console.log("Error constructing command:"); console.log(e); return Promise.resolve({type: 'symbol', source: ':not-available'}); } // Return a promise that will yield the result. return this.rex(cmd, pkg, ':REPL-THREAD') .then(function (ast) { try { return ast.children[0]; } catch (e) { return {type: 'symbol', source: ':not-available'}; } }); } Client.prototype.autocomplete = function(prefix, pkg) { prefix = util.to_lisp_string(prefix); var cmd = '(SWANK:SIMPLE-COMPLETIONS ' + prefix + ' \'"' + pkg + '")'; return this.rex(cmd, pkg, "T") .then(function (ast) { try { return ast.children[0].children.map(function(competion) { return util.from_lisp_string(competion); }); } catch (e) { return []; } }); } Client.prototype.eval = function(sexp_string, pkg) { var cmd = '(SWANK-REPL:LISTENER-EVAL ' + util.to_lisp_string(sexp_string) +')'; return this.rex(cmd, pkg, ':REPL-THREAD'); } Client.prototype.debug_setup_handler = function(sexp) { var obj = {}; obj.thread = sexp.children[1].source; obj.level = sexp.children[2].source; obj.title = util.from_lisp_string(sexp.children[3].children[0]); obj.type = util.from_lisp_string(sexp.children[3].children[1]); obj.restarts = []; sexp.children[4].children.forEach(function(restart_sexp) { obj.restarts.push({ cmd: util.from_lisp_string(restart_sexp.children[0]), description: util.from_lisp_string(restart_sexp.children[1]) }); }); obj.stack_frames = []; sexp.children[5].children.forEach(function(frame_sexp){ if (frame_sexp.children.length >= 3) { restartable = util.from_lisp_bool(frame_sexp.children[2].children[1]); } else { restartable = false; } obj.stack_frames.push({ frame_number: Number(frame_sexp.children[0].source), description: util.from_lisp_string(frame_sexp.children[1]), restartable: restartable }); }); this.on_handlers.debug_setup(obj); } Client.prototype.debug_activate_handler = function(sexp) { var thread = sexp.children[1].source; var level = sexp.children[2].source; this.on_handlers.debug_activate({thread: thread, level: level}); } Client.prototype.debug_return_handler = function(sexp) { var thread = sexp.children[1].source; var level = sexp.children[2].source; this.on_handlers.debug_return({thread: thread, level: level}); } Client.prototype.debug_invoke_restart = function(level, restart, thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:INVOKE-NTH-RESTART-FOR-EMACS ' + level + ' ' + restart + ')'; return this.rex(cmd, pkg, thread); } /* Escape from all errors */ Client.prototype.debug_escape_all = function(thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:THROW-TO-TOPLEVEL)'; return this.rex(cmd, pkg, thread); } /* Use the continue restart */ Client.prototype.debug_continue = function(thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:SLDB-CONTINUE)'; return this.rex(cmd, pkg, thread) } /* Abort the current debug level */ Client.prototype.debug_abort_current_level = function (level, thread, pkg='COMMON-LISP-USER') { var cmd; if (level == 1) { cmd = '(SWANK:THROW-TO-TOPLEVEL)'; } else { cmd = '(SWANK:SLDB-ABORT)'; } return this.rex(cmd, pkg, thread) } /* Get the entire stack trace Returns a promise of the list of objects, each containing a stack frame*/ Client.prototype.debug_get_stack_trace = function(thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:BACKTRACE 0 NIL)'; return this.rex(cmd, pkg, thread).then(function(sexp) { stack_frames = []; sexp.children.forEach(function(frame_sexp){ if (frame_sexp.children.length >= 3) { restartable = util.from_lisp_bool(frame_sexp.children[2].children[1]); } else { restartable = false; } stack_frames.push({ frame_number: Number(frame_sexp.children[0].source), description: util.from_lisp_string(frame_sexp.children[1]), restartable: restartable }); }); return stack_frames; }); } /* Retrieve the stack frame details for the specified frame. The existing stack frame object will be updated. Returns a promise of the updated stack frame object. */ Client.prototype.debug_stack_frame_details = function(index, stack_frames, thread, pkg='COMMON-LISP-USER') { frame_info = stack_frames.find(function(frame) { return Number(frame.frame_number) === Number(index); }); if (frame_info.hasOwnProperty('locals')) { //frame details have already been fetched return Promise.resolve(frame_info); } else { var cmd = '(SWANK:FRAME-LOCALS-AND-CATCH-TAGS ' + index + ')'; return this.rex(cmd, pkg, thread).then(function(sexp) { if (util.from_lisp_bool(sexp.children[0])) { frame_info.locals = sexp.children[0].children.map(function(local_sexp) { return { name: util.from_lisp_string(local_sexp.children[1]), id: Number(local_sexp.children[3].source), value: util.from_lisp_string(local_sexp.children[5]) }; }); } else { frame_info.locals = []; } if (util.from_lisp_bool(sexp.children[1])) { frame_info.catch_tags = sexp.children[1].children.map(function(tag_sexp) { return util.from_lisp_string(tag_sexp); }); } else { frame_info.catch_tags = [] } return frame_info; }); } } /* Restart the specified frame. May not be supported on some implementations. */ Client.prototype.debug_restart_frame = function(frame, thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:RESTART-FRAME ' + frame + ')'; return this.rex(cmd, pkg, thread); } /* Return the given value from the specified frame. May not be supported on some implementations. Returns a promise that will throw an error if Lisp can't return from this frame. */ Client.prototype.debug_return_from_frame = function(frame, value, thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:SLDB-RETURN-FROM-FRAME ' + frame + ' ' + util.to_lisp_string(value) + ')'; return this.rex(cmd, pkg, thread).then(function(rawErrorMessage) { if (util.from_lisp_bool(rawErrorMessage)){ throw new Error(util.from_lisp_string(rawErrorMessage)); } }); } /* Gets information to display the frame's source */ Client.prototype.debug_frame_source = function(frame, thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:FRAME-SOURCE-LOCATION ' + frame + ')'; return this.rex(cmd, pkg, thread).then(parse_location); } /* Disassembles the specified frame. */ Client.prototype.debug_disassemble_frame = function(frame, thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:SLDB-DISASSEMBLE ' + frame + ')'; return this.rex(cmd, pkg, thread).then(function(sexp) { return util.from_lisp_string(sexp); }); } /* Evaluate the given string in the specified frame. Returns a promise of the results of the evaluation as a string. */ Client.prototype.debug_eval_in_frame = function(frame, expr, thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:FRAME-PACKAGE-NAME ' + frame + ')'; _this = this; return this.rex(cmd, pkg, thread).then(function(package) { var cmd = '(SWANK:EVAL-STRING-IN-FRAME ' + util.to_lisp_string(expr) + ' ' + frame + ' ' + package.source + ')'; return _this.rex(cmd, pkg, thread) }).then(function(result) { return util.from_lisp_string(result); }); } /* Steps the debugger to the next expression in the frame. */ Client.prototype.debug_step = function(frame, thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:SLDB-STEP ' + frame + ')'; return this.rex(cmd, pkg, thread); } /* Steps the debugger to the next form in the function. */ Client.prototype.debug_next = function(frame, thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:SLDB-NEXT ' + frame + ')'; return this.rex(cmd, pkg, thread); } /* Complete the current function then resume stepping. */ Client.prototype.debug_step_out = function(frame, thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:SLDB-OUT ' + frame + ')'; return this.rex(cmd, pkg, thread); } /* Insert a breakpoint at the end of the frame. */ Client.prototype.debug_break_on_return = function(frame, thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:SLDB-BREAK-ON-RETURN ' + frame + ')'; return this.rex(cmd, pkg, thread); } /* Insert a breakpoint at the specified function. */ Client.prototype.debug_break = function(function_name, thread, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:SLDB-BREAK ' + util.to_lisp_string(function_name) + ')'; return this.rex(cmd, pkg, thread); } /************* * Profiling * *************/ Client.prototype.profile_invoke_toggle_function = function(func, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:TOGGLE-PROFILE-FDEFINITION "' + func + '")'; var prof_func = this.on_handlers.profile_command_complete; return this.rex(cmd, pkg, ":REPL-THREAD").then(function(sexp) { prof_func(sexp.source.slice(1,-1)); }); } Client.prototype.profile_invoke_toggle_package = function(pack, rec_calls, prof_meth, pkg='COMMON-LISP-USER') { var cmd = '(SWANK:SWANK-PROFILE-PACKAGE ' + util.to_lisp_string(pack) + ' ' + util.to_lisp_bool(rec_calls) + ' ' + util.to_lisp_bool(prof_meth) + ')'; var prof_func = this.on_handlers.profile_command_complete; return this.rex(cmd, pkg, ":REPL-THREAD").then(function(sexp) { prof_func("Attempting to profile package " + pack + "..."); }); } Client.prototype.profile_invoke_unprofile_all = function(func, pkg='COMMON-LISP-USER') { var cmd = '(SWANK/BACKEND:UNPROFILE-ALL)'; var prof_func = this.on_handlers.profile_command_complete; return this.rex(cmd, pkg, ":REPL-THREAD").then(function(sexp) { prof_func(sexp.source.slice(1,-1)); }); } Client.prototype.profile_invoke_reset = function(func, pkg='COMMON-LISP-USER') { var cmd = '(SWANK/BACKEND:PROFILE-RESET)'; var prof_func = this.on_handlers.profile_command_complete; return this.rex(cmd, pkg, ":REPL-THREAD").then(function(sexp) { prof_func(sexp.source.slice(1,-1)); }); } Client.prototype.profile_invoke_report = function(func, pkg='COMMON-LISP-USER') { var cmd = '(SWANK/BACKEND:PROFILE-REPORT)'; var prof_func = this.on_handlers.profile_command_complete; return this.rex(cmd, pkg, ":REPL-THREAD").then(function(sexp) { prof_func("Profile report printed to REPL"); }); } /* Gets function definitions. Returns a promise of a list of objects, each of which has a label property, and a location object */ Client.prototype.find_definitions = function(fn, pkg) { var cmd = '(SWANK:FIND-DEFINITIONS-FOR-EMACS "' + fn + '")'; return this.rex(cmd, pkg, "T") .then(function (ast) { var refs = []; for(var i = 0; i < ast.children.length; i++) { try { child_ast = ast.children[i]; location_sexp = child_ast.children[1]; srcloc = parse_location(location_sexp); if (srcloc.buffer_type != 'error') { // Push the reference if swank could find a location refs.push({ label: util.from_lisp_string(child_ast.children[0]), location: srcloc }); } } catch (e) { // Don't add the reference - it didn't parse correctly } } return refs; }); } // Compiles the given string Client.prototype.compile_string = function(compile_string, filename, filename_full, position, line, column, package) { var cmd = '(SWANK:COMPILE-STRING-FOR-EMACS ' + util.to_lisp_string(compile_string) + ' ' + util.to_lisp_string(filename) + " '((:POSITION " + position + "))" + util.to_lisp_string(filename_full) + " 'NIL)"; return this.rex(cmd, package, "T").then((result) => this.on_compilation(package, result)); } // Compiles the given file Client.prototype.compile_file = function(filename, package, load=true) { var cmd = "(SWANK:COMPILE-FILE-FOR-EMACS " + util.to_lisp_string(filename) + " " + util.to_lisp_bool(load) + ")"; return this.rex(cmd, package, "T").then((result) => this.on_compilation(package, result)); } // Called after a compilation command to ensure things are loaded as needed Client.prototype.on_compilation = function(package, result) { compilation_notes = result.children[1]; // plist: :message (description) // :severity {:note :redefinition :style-warning :warning :early-deprecation-warning :late-deprecation-warning, :final-deprecation-warning :read-error :error} // :location (:location ...) // :references {nil} // :source-context (Optinal, structural location) notes = []; if (util.from_lisp_bool(compilation_notes)) { for (j = 0; j < compilation_notes.children.length; j++) { raw_note = compilation_notes.children[j]; note = {source_context: ''}; notes.push(note); for (i = 0; i < raw_note.children.length; i+=2) { switch(raw_note.children[i].source.toLowerCase()) { case ':message': note.message = util.from_lisp_string(raw_note.children[i+1]); break; case ':severity': note.severity = raw_note.children[i+1].source.toLowerCase().substring(1); break; case ':location': note.location = parse_location(raw_note.children[i+1]); break; case ':source-context': if (util.from_lisp_bool(raw_note.children[i+1])) { note.source_context = util.from_lisp_string(raw_note.children[i+1]); } break; // TODO add support for references default: console.warn('Unknown key in compilation note: "' + raw_note.children[i].source +'"'); } } } } this.on_handlers.compiler_notes(notes); if (result.type == 'list' && util.from_lisp_bool(result.children[2]) && util.from_lisp_bool(result.children[4])) { return this.rex("(SWANK:LOAD-FILE " + result.children[5].source + ")", package, "T") } } /* Computes the expansion for the given form. A promise of the expanded text is returned. When the repeatedly argument is true, the expansion is applied until the form can no longer be expanded; when false, the form is only expanded once. If macros is true and compiler_macros is false, repeatedly can be set as "all" to walk the code expanding macros.*/ Client.prototype.expand = function(form, package, repeatedly=true, macros=true, compiler_macros=true) { var func; if (macros) { if (compiler_macros) { func = "EXPAND"; } else { func = "MACROEXPAND"; } } else { if (compiler_macros) { func = "COMPILER-MACROEXPAND"; } else { // Expand nothing console.warn("Using trivial expansion"); return Promise.resolve(form); } } if (repeatedly=="all"){ if (macros && !compiler_macros) { func += "-ALL" } else { throw "Only macroexpand can use a repetition of all"; } } else if (!repeatedly) { func += "-1" } var cmd = "(SWANK:SWANK-"+func+" "+util.to_lisp_string(form)+")"; return this.rex(cmd, package, "T").then(util.from_lisp_string); } Client.prototype.load_inspection_content = function (pkg, description, raw_content) { raw_content_description = raw_content.children[0]; content_length = Number(raw_content.children[1].source); content_start = Number(raw_content.children[2].source); content_end = Number(raw_content.children[3].source); if (util.from_lisp_bool(raw_content_description)) { description = description.concat(raw_content_description.children); } if (content_end < content_length) { var cmd = '(SWANK:INSPECTOR-RANGE ' + content_end + ' ' + content_length + ')' return this.rex(cmd, pkg, 'T') .then(this.load_inspection_content.bind(this, pkg, description)); } else { return Promise.resolve(description); } } /* Helper function that parses the inspector sexp. It return a object with the following fields: title, id, and content. The content is a list containing strings and arrays of string-id pairs. The strings are designed to be printed as the UI, with id's corresponding to actions. If Swank indicates no updates should happen, null is returned. */ Client.prototype.parse_inspection = function(pkg, result) { if (!util.from_lisp_bool(result)) { return null; } var inspection = {}, raw_content = []; for (var i = 0; i < result.children.length; i += 2) { var key = result.children[i].source; var val = result.children[i+1]; if (key == ':title') { // string inspection.title = util.from_lisp_string(val); } else if (key == ':id') { // integer inspection.id = val.source; } else if (key == ':content') { raw_content = val; } else { console.warn('Found key of ' + key + ' in presentation results.'); } } return this.load_inspection_content(pkg, [], raw_content) .then(function (raw_content) { inspection.content = raw_content.map(function(elt) { if (elt.type == 'string') { return util.from_lisp_string(elt); } else { //type == 'list' return [elt.children[0].source, util.from_lisp_string(elt.children[1]), Number(elt.children[2].source)]; } }); return inspection; }); } Client.prototype.inspect_evaluation = function(expression, pkg='COMMON-LISP-USER') { var cmd = "(SWANK:INIT-INSPECTOR " + util.to_lisp_string(expression) + ")"; return this.rex(cmd, pkg, ':REPL-THREAD') .then(this.parse_inspection.bind(this, pkg)); } /* Gets the inspector information for the given presentation id.*/ Client.prototype.inspect_presentation = function(presentation_id, reset_p=false, pkg='COMMON-LISP-USER') { var cmd = "(SWANK:INSPECT-PRESENTATION '" + presentation_id + " " + util.to_lisp_bool(reset_p) + ")"; return this.rex(cmd, pkg, ':REPL-THREAD') .then(this.parse_inspection.bind(this, pkg)); } /* Inspects the specified variable in the given stack frame */ Client.prototype.inspect_frame_var = function(frame_index, var_num, thread, pkg='COMMON-LISP-USER') { var cmd = "(SWANK:INSPECT-FRAME-VAR " + frame_index + " " + var_num + ")"; return this.rex(cmd, pkg, thread) .then(this.parse_inspection.bind(this, pkg)); } /* Evaluates the given expression, then inspects it */ Client.prototype.inspect_in_frame = function(frame_index, expression, thread, pkg='COMMON-LISP-USER') { var cmd = "(SWANK:INSPECT-IN-FRAME " + util.to_lisp_string(expression) + " " + frame_index + ")"; return this.rex(cmd, pkg, thread) .then(this.parse_inspection.bind(this, pkg)); } /* Inspects the current condition */ Client.prototype.inspect_current_condition = function(thread, pkg='COMMON-LISP-USER') { var cmd = "(SWANK:INSPECT-CURRENT-CONDITION)"; return this.rex(cmd, pkg, thread) .then(this.parse_inspection.bind(this, pkg)); } /* Inspects the nth part of the current inspection */ Client.prototype.inspect_nth_part = function(n, pkg='COMMON-LISP-USER') { var cmd = "(SWANK:INSPECT-NTH-PART "+ n + ")"; return this.rex(cmd, pkg, ':REPL-THREAD') .then(this.parse_inspection.bind(this, pkg)); } /* Calls the nth action of the current inspector */ Client.prototype.inspector_call_nth_action = function(n, pkg='COMMON-LISP-USER') { var cmd = "(SWANK:INSPECTOR-CALL-NTH-ACTION "+ n + ")"; return this.rex(cmd, pkg, ':REPL-THREAD') .then(this.parse_inspection.bind(this, pkg)); } /* Shows the previous inspected object */ Client.prototype.inspect_previous_object = function(pkg='COMMON-LISP-USER') { var cmd = "(SWANK:INSPECTOR-POP)"; return this.rex(cmd, pkg, ':REPL-THREAD') .then(this.parse_inspection.bind(this, pkg)); } /* Shows the next inspected object */ Client.prototype.inspect_next_object = function(pkg='COMMON-LISP-USER') { var cmd = "(SWANK:INSPECTOR-NEXT)"; return this.rex(cmd, pkg, ':REPL-THREAD') .then(this.parse_inspection.bind(this, pkg)); } Client.prototype.interrupt = function() { var cmd = '(:EMACS-INTERRUPT :REPL-THREAD)'; this.send_message(cmd); } Client.prototype.quit = function() { var cmd = '(SWANK/BACKEND:QUIT-LISP)'; return this.rex(cmd, 'COMMON-LISP-USER', "T") } /***************************************************************** Exports *****************************************************************/ module.exports.Client = Client; module.exports.util = util;