var Transform = require("stream").Transform; var EventEmitter = require("events").EventEmitter; var util = require("util"); var Types = require("./types.js"); var Buffer = require("./buffer.js"); var events = require("./event.js"); //A stream Transform implementation returning full netbeans messages function MessageTransform(opts) { "use strict"; Transform.call(this,opts); var buffer; this._transform = function(data, encoding, callback) { var offstart = 0; for (var i = 0 ; i < data.length ; ++i) { if (data[i] === 10) { if (buffer !== undefined) { this.push(buffer); buffer = undefined; } this.push(data.slice(offstart, i)); offstart = i + 1; } } if (offstart < i) { buffer = data.slice(offstart); } callback(); }; this._flush = function (callback) { buffer = undefined; callback(); }; } util.inherits(MessageTransform, Transform); //Main netbeans object. Needs the socket of the incomming connexion and an authentication callback //This callback just get the password sent by Vim. It must return true to allow connexion function NetbeansClient(socket, authentication, onEvent, onError, onDisconnected) { "use strict"; var callbacks = {}; var currentSeqno = 0; var connected = false; socket.setEncoding("utf8"); var messageTransform = new MessageTransform(); messageTransform.on("data",function (data) { var message = data.toString(); /* The first message must be an authentication one */ if (message.indexOf("AUTH ") === 0 && authentication(message.substring(message.indexOf(" ") + 1))) { connected = true; messageTransform.removeAllListeners("data"); messageTransform.on("data", function(data) { var message = data.toString(); try { var iName = 0; /*Index of the name separator in case of an Event */ var iSeqno = 0; /*Index of the seqno separator in case of an Event */ /* Header of the message */ for (var i = 0 ; i < message.length && message[i] !== " "; ++i) { if (message[i] === ":") { iName = i; } else if (message[i] === "=") { iSeqno = i; } } var isEvent = false; var args = []; var seqno = 0; if (iName > 0 && iSeqno > 0) { isEvent = true; args.push(parseInt(message.substr(0, iName), 10)); args.push(message.substring(iName + 1, iSeqno)); //args.push(parseInt(message.substring(iSeqno + 1, i), 10)); } else if (iName === 0 && iSeqno === 0) { seqno = parseInt(message.substr(0, i), 10); } else { throw "Unknown message type"; } var argStartOffset = ++i; //Arguments of the message for ( ; i < message.length ; ++i) { if (message[i] === "\"" || message[i] === "!") { var parsedString = ""; i += 1; while (i < message.length && message[i] !== "\"") { if (message[i] === "\\") { i += 1; if (i === message.length) { throw "Unfinished string in message"; } else { switch (message[i]) { case "n" : parsedString += "\n"; break; case "\\" : parsedString += "\\"; break; case "\"" : parsedString += "\""; break; case "r" : parsedString += "\r"; break; case "t" : parsedString += "\t"; break; default : throw "Invalid escaped character in string"; } } } else { parsedString += message[i]; } ++i; } args.push(parsedString); i += 1; } else { while (i < message.length && message[i] !== " ") { i += 1; } if (i - argStartOffset === 1 && (message[i - 1] === "T" || message[i - 1] === "F")) { args.push(message[i - 1] === "T"); } else { var argument = message.substring(argStartOffset, i); if (Number.isNaN(argument)) { if (argument.contains("/")) { args.push(argument.split("/")); } else { args.push(argument); } } else { args.push(parseInt(argument)); } } } argStartOffset = ++i; } if (isEvent) { onEvent.apply(null, args); } else if (callbacks.hasOwnProperty(seqno)) { callbacks[seqno].apply(null, args); delete callbacks[seqno]; } else { throw "Received reply to unknown function call"; } } catch (ex) { onError.call(null, ex.message); } }); } else { //TODO send a message to client ? socket.destroy(); } }); messageTransform.on("error", function (error) { onError.call(null, error); }); messageTransform.on("end",function () { onDisconnected.call(null); }); messageTransform.on("close", function () { onDisconnected.call(null); }); //Send a command message. //Needs the name of the buffer, the buffer id and the parameters of the command this.sendCommand = function(name, buffer) { var seqno = currentSeqno; currentSeqno += 1; var data = buffer + ":" + name + "!" + seqno; for (var i = 2 ; i < arguments.length ; ++i) { data += " " + arguments[i]; } data += "\n"; socket.write(data); }; //Call a function. //Needs the name of the function, the bufferid, the parametres of the command and the callback to retreive the reply this.callFunction = function(name, buffer) { var seqno = currentSeqno; currentSeqno += 1; var lastArg = arguments.length - 1; if (typeof(arguments[lastArg]) === "function") { Object.defineProperty(callbacks, seqno, {value: arguments[lastArg], configurable: true}); lastArg -= 1; } var data = buffer + ":" + name + "/" + seqno; for (var i = 2 ; i <= lastArg ; ++i) { data += " " + arguments[i]; } data += "\n"; socket.write(data); }; socket.pipe(messageTransform); } //Wrapper around the controler exposing functions and commands as methods //Takes an incomming connexion (socket:Socket), an authentication callback (authentication:function(string)) //Returns a Controler (Controler) function Controler(socket, authentication) { "use strict"; var self = this; var unmanagedFiles=[]; EventEmitter.call(this); var client = new NetbeansClient(socket, authentication, function(buffId, name) { switch (name) { case events.disconnect : //Connexion closed by Vim socket.destroy(); self.emit("disconnected"); break; case events.killed : //Remove the buffer if it as been killed if (buffers.hasOwnProperty(buffId)) { buffers[buffId].emit.apply(buffers[buffId], Array.prototype.slice.call(arguments, 1)); delete buffers[buffId]; } break; case events.keyAtPos : //Call the key registered callback if it exists. var buffer = self.getBuffer(buffId); var pos = arguments[3].split("/"); if ((!buffer || !buffer.handleKey(arguments, pos[0], pos[1])) && keysCallback.hasOwnProperty(arguments[2])){ keysCallback[arguments[2]].call(null, buffer, pos[0], pos[1]); } break; case events.fileOpened : //File opened. Check if it's already managed by controler. if (self.getBuffer(arguments[2]) === undefined) { unmanagedFiles.push(arguments[2]); } /* fall through */ default : var target = self; if (buffId > 0 && buffers.hasOwnProperty(buffId)) { target = buffers[buffId]; } target.emit.apply(target, Array.prototype.slice.call(arguments, 1)); break; } }, function (error) { self.emit("error",error); }, function () { self.emit("disconnected"); }); var buffId = 0; var buffers = {}; //Internal function to create a new buffer function newBuffer(pathname) { if (pathname !== undefined) { var indexFile = unmanagedFiles.indexOf(pathname); if (indexFile < 0 ) { throw "File not opened by client"; } unmanagedFiles.splice(indexFile, 1); } buffId += 1; var buffer = new Buffer(client, buffId, pathname); Object.defineProperty(buffers, buffId, {value: buffer, configurable: true}); return buffer; } function error (exception) { self.emit("error", exception); } /* GLOBAL COMMANDS */ //Bring Vim to the front (GVim only) this.raise = function () { client.sendCommand("raise", 0); }; //FAILING //Set an exiting delay, allowing the controler to handle things this.setExitDelay = function (seconds) { client.sendCommand("setExitDelay", 0, seconds); }; //Set key binding. Absolutely not documented feature... this.specialKeys = function (keys) { client.sendCommand("specialKeys", 0,Types.string(keys)); }; //Create a new buffer. Return the buffer created this.create = function() { try { var buffer = newBuffer(); client.sendCommand("create", buffId); return buffer; } catch (exception) { error(exception); } }; //Associate already opened buffer on Vim to a controler owned buffer //Return the newly created buffer this.putBufferNumber = function (pathname) { try { var buffer = newBuffer(pathname); client.sendCommand("putBufferNumber", buffId, Types.string(pathname)); return buffer; } catch (exception) { error(exception); } }; //Associate already opened buffer on Vim to a controler owned buffer and bring it to front //Return the newly created buffer this.setBufferNumber = function (pathname) { try { var buffer = newBuffer(pathname); client.sendCommand("setBufferNumber", buffId, Types.string(pathname)); return buffer; } catch (exception) { error(exception); } }; /* GLOBAL FUNCTIONS */ //Ask Vim to save and exit this.saveAndExit = function (callback) { client.callFunction("saveAndExit", 0, callback); }; //Get the number of modified buffers this.getModified = function (callback) { client.callFunction("getModified", 0, callback); }; //Get cursor position. Callback function get the line, column and offset this.getCursor = function (callback) { client.callFunction("getCursor", 0, callback); }; /* CONTROLER CONTROL */ //Close the underlying connexion with Vim, but not Vim this.detach = function () { socket.end("DETACH\n"); self.emit("disconnected"); }; //Close the underlying connexion with Vim and Vim. Ensure that data has been saved before this.close = function () { socket.end("DISCONNECT\n"); self.emit("disconnected"); }; //Get a buffer using its name or its id this.getBuffer = function(buffId) { if (typeof(buffId) === "string" || buffId instanceof String) { for (var id in buffers) { if (buffers[id].name === buffId) { return buffers[id]; } } return undefined; } else { return buffers[buffId]; } }; var keysCallback = {}; this.registerKey = function (key, callback) { self.specialKeys(key); Object.defineProperty(keysCallback, key, { value: callback, configurable: true}); }; } util.inherits(Controler, EventEmitter); module.exports.Controler = Controler;