458 lines
15 KiB
JavaScript
458 lines
15 KiB
JavaScript
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;
|