Files
vim-netbeans-server/lib/controler.js

389 lines
13 KiB
JavaScript

var Transform = require("stream").Transform;
var EventEmitter = require("events").EventEmitter;
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();
};
}
MessageTransform.prototype = Transform.prototype;
//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";
EventEmitter.call(this);
var self = this;
var callbacks = {};
var currentSeqno = 0;
var connected = false;
socket.setEncoding("utf8");
var messageTransform = new MessageTransform();
messageTransform.on("data",function (data) {
var message = data.toString();
console.log("MSG : "+message);
/* 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();
console.log("MSG "+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(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] === "T" || message[i] === "F")) {
args.push(message[i] === "T");
}
else {
var argument = message.substr(argStartOffset, i);
if (Number.isNaN(argument)) {
if (argument.contains("/")) {
args.push(argument.split("/"));
}
else {
args.push(argument);
}
}
else {
args.push(parseInt(argument));
}
}
}
}
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, currentSeqno, {value: 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);
}
NetbeansClient.prototype = EventEmitter.prototype;
//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;
EventEmitter.call(this);
var client = new NetbeansClient(socket, authentication, function(name, buffId) {
switch (name) {
case events.disconnect :
socket.destroy();
self.emit("disconnected");
break;
case events.killed :
if (buffers.hasOwnProperty(buffId)) {
buffers[buffId].emit.apply(buffers[buffId], Array.prototype.slice(arguments, 1));
delete buffers[buffId];
}
break;
default :
var target = self;
if (buffId > 0 && buffers.hasOwnProperty(buffId)) {
target = buffers[buffId];
}
target.emit.apply(target, Array.prototype.slice(arguments, 1));
break;
}
},
function (error) {
console.log("ERROR : "+error);
self.emit("error",error);
},
function () {
console.log("DISCONNECTED");
self.emit("disconnected");
});
var buffId = 0;
var buffers = {};
function getBuffer() {
buffId += 1;
var buffer = new Buffer(client, buffId);
Object.defineProperty(buffers, buffId, {value: buffer});
return buffer;
}
/* GLOBAL COMMANDS */
//Bring Vim to the front (GVim only)
this.raise = function () {
client.sendCommand("raise", 0);
};
//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() {
var buffer = getBuffer();
client.sendCommand("create", buffId);
return buffer;
};
//Associate already opened buffer on Vim to a controler owned buffer
//Return the newly created buffer
this.putBufferNumber = function (pathname) {
var buffer = getBuffer();
client.sendCommand("putBufferNumber", buffId, Types.string(pathname)); //TODO : May be better on the buffer
return buffer;
};
//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) {
var buffer = getBuffer();
client.sendCommand("setBufferNumber", buffId, Types.string(pathname));
return buffer;
};
/* GLOBAL FUNCTIONS */
//Ask Vim to save and exit
this.saveAndExit = function (callback) {
client.callFunction("saveAndExit", 0, callback);
};
//Get the numbr of modified buffers
this.getModified = function (callback) {
client.callFunction("getModified", 0, callback);
};
/* CONNTROLER 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");
};
this.getBuffer = function(buffId) {
return buffers[buffId];
};
}
Controler.prototype = EventEmitter.prototype;
module.exports.Controler = Controler;