Files
vim-netbeans-server/lib/controler.js
2014-11-10 13:40:24 +01:00

458 lines
15 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 event = 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 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();
/* 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 event.disconnect :
//Connexion closed by Vim
socket.destroy();
self.emit("disconnected");
break;
case event.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 event.keyAtPos :
//Call the key registered callback if it exists.
var buffer = getBuffer(buffId);
if (keysCallback.indexOf(arguments[2]) >= 0) {
keysCallback.call(null, buffer, arguments[3].split("/")[0], arguments[3].split("/")[1]);
}
else if (buffer === undefined) {
}
break;
case event.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 = {};
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);
};
//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 numbr 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[i];
}
}
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;