var Transform = require("stream").Transform; var EventEmitter = require("events").EventEmitter; var Types = require("types"); var Buffer = require("buffer"); /** * A Transform Stream implementation that output full VNB protocol messages * @constructor * @param {object} opts Options passed to the Transform constructor */ function MessageTransform(opts) { "use strict"; Transform.call(this,opts); var buffer; /** * Transform stream Write implementation * @function * @param {string} data Upstream data already converted as string * @param {string} encoding String encoding (should be utf8, but should be checked upward) * @param {function} callback Downstream Stream receiving only full VNB messages **/ this._transform = function(data, encoding, callback) { var endpos = data.indexOf("\n"); if (endpos > -1) { if (buffer !== undefined) { this.push(buffer); buffer = undefined; } if (endpos > 0) { this.push(data.substr(0, endpos)); } callback(); if (endpos < data.length - 1) { buffer = data.substr(endpos + 1); } } else { buffer = buffer.concat(data); } }; /** * Flush still data. Remaining data is lost since it can only be an incomplete message * @function * @param {function} callback Callback to call when data has been flushed **/ this._flush = function (callback) { buffer = undefined; callback(); }; } /** * Authentication callback * @callback authenticationCallback * @param {string} password Password given by the authenticating client * @return {bool} Result of the authentication */ /** * Client implementation for vim netbeans protocol (communication from vim to the netbeans server) * @constructor * @param {Socket} socket Incomming stream * @param {authenticationCallback} authentication Callback function called when the AUTH message is received */ function Controler(socket, authentication) { "use strict"; EventEmitter.call(this); var self = this; var callbacks = new Map(); var currentSeqno = 0; var connected = false; //TODO check if this is needed socket.setEncoding("utf8"); var messageTransform = new MessageTransform(); messageTransform.on("data",function (message) { //The first message must be an authentication one if (message.startsWith("AUTH ") && authentication(message.substring(message.indexOf(" ") + 1))) { connected = true; messageTransform.removeAllListeners("data"); messageTransform.on("data", function(message) { 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("event"); args.push(Number.parseInt(message.substr(0, iName), 10)); args.push(message.substring(iName + 1, iSeqno)); //args.push(Number.parseInt(message.substring(iSeqno + 1, i), 10)); } else if (iName === 0 && iSeqno === 0) { seqno = Number.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] === "T" || message[i] === "F")) { args.push(message[i] === "T"); } else { var argument = message.substr(argStartOffset, i); if (Number.NaN(argument)) { if (argument.contains("/")) { args.push(argument.split("/")); } else { args.push(argument); } } else { args.push(Number.parseInt(argument)); } } } } if (isEvent) { //Emit the event self.emit(...args); } else if (callbacks.has(seqno)) { callbacks.get(seqno)(...args); callbacks.delete(seqno); } else { throw "Received reply to unknown function call"; } } catch (ex) { self.emit("error", ex.message); } }); } else { //TODO send a message to client ? socket.end(); } }); messageTransform.on("error", function (error) { }); messageTransform.on("end",function () { }); messageTransform.on("close", function () { }); /** * Send a command message * @function sendCommand * @param {string} name Name of the command * @param {number} buffer Name of the buffer * @param {...string} arguments Parameters of the command */ this.sendCommand = function(name, buffer) { let 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); }; /** * Send a function message * @function callFunction * @param {string} name Name of the function * @param {number} buffer ID of the buffer * @param {...string} arguments Parameters of the function. Use the string, bool, color and position function * of the method to convert javascript types to string * @param {function} callback Callback function called with reply value * */ this.callFunction = function(name, buffer) { let seqno = currentSeqno; currentSeqno += 1; var lastArg = arguments.length - 1; //If the function is called with a callback (wich should always be the case) //add the calback to the map so it's fired when the reply is received if (typeof(arguments[lastArg]) === "function") { callbacks[currentSeqno] = arguments[lastArg]; 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); } InternalClient.prototype = EventEmitter; /** * Connexion with a vim client * @constructor * @param {Socket} socket Connexion's socket * @param {authenticationCallback} authentication Callback used to authenticate the client */ function Client(socket, authentication) { "use strict"; var client = new Controler(socket, authentication); var buffId = 0; /* GLOBAL COMMANDS */ this.startAtomic = function () { client.sendCommand("startAtomic", 0); }; this.endAtomic = function() { client.sendCommand("endAtmoic", 0); }; this.raise = function () { client.sendCommand("raise", 0); }; this.setExitDelay = function (seconds) { client.sendCommand("setExitDelay", 0, seconds); }; this.specialKeys = function (keys) { client.sendCommand("specialKeys", 0,Types.string(keys)); //TODO : Wild guess, no documentation about this one }; this.create = function() { var buffer = new Buffer(client, buffId); buffId += 1; client.sendCommand("create", buffId); return buffer; }; /** * putBufferNumber * @function * @param {string} pathname Path of the file whose buffer is identified * @return {undefined} */ this.putBufferNumber = function (pathname) { var buffer = new Buffer(client, buffId); buffId += 1; client.sendCommand("putBufferNumber", buffId, Types.string(pathname)); //TODO : May be better on the buffer return buffer; }; /** * Set a buffer number to a file opened by Vim and sets its buffer as current buffer * @function * @param {string} pathname Path of the file */ this.setBufferNumber = function (pathname) { var buffer = new Buffer(client, buffId); buffId += 1; client.sendCommand("setBufferNumber", buffId, Types.string(pathname)); return buffer; }; /* GLOBAL FUNCTIONS */ /** * saveAndExit * * @param callback * @return {undefined} */ this.saveAndExit = function (callback) { client.callFunction("saveAndExit", 0, callback); }; this.getModified = function (callback) { client.callFunction("getModified", 0, callback); }; } module.exports.Controler = Client;