Skip to content

Commit

Permalink
Merge pull request #48 from RSDuck/master
Browse files Browse the repository at this point in the history
Reimplemented the elrpc client
  • Loading branch information
kosz78 authored Apr 19, 2017
2 parents 6fe5cb9 + 822aa11 commit 367c0a9
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 54 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@
"lint": "node ./node_modules/tslint/bin/tslint ./src/*.ts ./test/*.ts"
},
"dependencies": {
"nedb": "1.8.0",
"elrpc": "0.1.0"
"nedb": "1.8.0"
},
"devDependencies": {
"typescript": "^2.0.3",
Expand Down
105 changes: 105 additions & 0 deletions src/elrpc/elrpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*---------------------------------------------------------
* Copyright (C) Xored Software Inc., RSDuck All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
import net = require("net");
import sexp = require("./sexp");

function envelope(content: string): string {
return ("000000" + content.length.toString(16)).slice(-6) + content;
}

function generateUID(): number {
return Math.floor(Math.random() * 10000);
}

export class EPCPeer {
private socket: net.Socket;

private receivedBuffer: Buffer;
private sessions = new Map<number, (data: any) => void>();

private socketClosed = false;

constructor(socket: net.Socket) {
this.socket = socket;
this.receivedBuffer = new Buffer(0);
this.socket.on('data', data => {
this.receivedBuffer = Buffer.concat([this.receivedBuffer, data]);
while (this.receivedBuffer.length > 0) {
if (this.receivedBuffer.length >= 6) {
let length = parseInt(this.receivedBuffer.toString("utf8", 0, 6), 16);
if (this.receivedBuffer.length >= length + 6) {
let content = <any[]>sexp.parseSExp(this.receivedBuffer.toString("utf8", 6, 6 + length));
if (content) {
let guid = content[0][1];
this.sessions[guid](content[0]);
this.sessions.delete(guid);

let endTime = Date.now();
} else {
this.sessions.forEach(session => {
session("Received invalid SExp data")
});
}

this.receivedBuffer = this.receivedBuffer.slice(6 + length);
} else
return;
}
}
});
this.socket.on("close", (error) => {
console.error("Connection closed" + (error ? " due to an error" : ""));
this.sessions.forEach(session => {
session("Connection closed");
});
this.socketClosed = true;
});
}

callMethod(method: string, ...parameter: sexp.SExp[]): Promise<any[]> {
return new Promise<any[]>((resolve, reject) => {
if (this.socketClosed)
reject("Connection closed");

let guid = generateUID();

let payload = "(call " + guid + " " + method + " " + sexp.toString({ kind: "list", elements: parameter }) + ")";

this.sessions[guid] = (data) => {
if (!(data instanceof Array)) {
reject(data);
} else {
switch (data[0]) {
case "return":
resolve(data[2]);
break;
case "return-error":
case "epc-error":
reject(data[2]);
break;
}
}
};
this.socket.write(envelope(payload));
});
}

stop() {
if (!this.socketClosed)
this.socket.destroy();
}
}

export function startClient(port: number): Promise<EPCPeer> {
return new Promise<EPCPeer>((resolve, reject) => {
try {
let socket = net.createConnection(port, "localhost", () => {
resolve(new EPCPeer(socket));
});
} catch (e) {
reject(e);
}
});
}
145 changes: 145 additions & 0 deletions src/elrpc/sexp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*---------------------------------------------------------
* Copyright (C) Xored Software Inc., RSDuck All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
export interface SExpCons { kind: "cons"; car, cdr: SExp }
export interface SExpList { kind: "list"; elements: Array<SExp> }
export interface SExpNumber { kind: "number"; n: number }
export interface SExpIdent { kind: "ident"; ident: string }
export interface SExpString { kind: "string"; str: string }
export interface SExpNil { kind: "nil"; }

export type SExp = SExpCons | SExpList | SExpNumber | SExpIdent | SExpString | SExpNil

export function toJS(sexp: SExp): any {
switch (sexp.kind) {
case "cons":
return [toJS(sexp.car), toJS(sexp.cdr)];
case "list":
return sexp.elements.map(element => toJS(element));
case "number":
return sexp.n;
case "ident":
return sexp.ident;
case "string":
return sexp.str;
case "nil":
return null;
}
}

export function toString(sexp: SExp) {
switch (sexp.kind) {
case "cons":
return "(" + toString(sexp.car) + " . " + toString(sexp.cdr) + ")";
case "list":
let stringRepr = "(";
sexp.elements.forEach(element => {
stringRepr += toString(element) + " ";
});
return stringRepr.substr(0, stringRepr.length - 1) + ")";
case "number":
return sexp.n.toString();
case "ident":
return sexp.ident;
case "string":
return "\"" + sexp.str.replace("\n", "\\n").replace("\r", "\\r").replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
case "nil":
return "nil";
}
}

//outputs the SExp directly as a JS object to reduce the processing time
export function parseSExp(input: string): /*SExp*/ any[] | string {
let ptr = 0;

function parseSymbol(): /*SExpIdent | SExpNumber | SExpNil*/ string | number | null {
let symbolStart = ptr;
while (ptr < input.length && !input[ptr].match(/[ )]/)) {
if (input[ptr] == "\\" && input[ptr + 1] == " ")
ptr += 2;
else
ptr++;
}
let sym = input.substring(symbolStart, ptr);

if (/^-?\d+$/.test(sym))
//return { kind: "number", n: parseInt(sym) };
return parseInt(sym);
//else if (/^-?\d+.\d+$/.test(sym))
// return { kind: "number", n: parseFloat(sym) };
else if (sym == "nil")
//return { kind: "nil" };
return null;
//else if (/^#x-?[\da-fA-F]+$/.test(sym))
// return { kind: "number", n: parseInt(sym.substr(2), 16) };
//return { kind: "ident", ident: sym };
return sym;
}

function parseString(): /*SExpString*/ string {
let hasEscapes = false;
let startPos = ptr;
while (ptr < input.length && input[ptr] != "\"") {
if (input[ptr] == "\\") {
if (ptr + 1 >= input.length)
throw "Expected character after a escape seqence introducing backslash";
//string += input[ptr] + input[ptr + 1];
ptr += 2;
hasEscapes = true;
} else
//string += input[ptr++];
ptr++;
}
let string = input.substring(startPos, ptr);
//return { kind: "string", str: hasEscapes ? JSON.parse("\"" + string + "\"") : string };
return hasEscapes ? JSON.parse("\"" + string + "\"") : string;
}

function parseListOrCon(root?: boolean): /*SExpList | SExpCons*/ any[] {
//let items: SExp[] = [];
let items = [];
let cons = false;
while (ptr < input.length) {
if (/[^() "]/.test(input[ptr])) {
let sym = parseSymbol();
//if (sym.kind == "ident" && sym.ident == ".") {
if (sym === ".") {
if (items.length == 1)
cons = true;
else
throw "Invalid cons cell syntax";
}
items.push(sym);
} else if (input[ptr] == "(") {
ptr++;
items.push(parseListOrCon());
} else if (input[ptr] == "\"") {
ptr++;
items.push(parseString());
}
if (input[ptr++] == ")")
break;
//ptr++;
}
if (input[ptr - 1] != ")" && !root)
throw "Premature end, expected closing bracket";

if (cons) {
if (items.length == 3) {
//return { kind: "cons", car: items[0], cdr: items[2] };
return [items[0], items[2]];
} else
throw "Invalid cons cell syntax";
}

//return { kind: "list", elements: items };
return items;
}

try {
return parseListOrCon(true);
} catch (e) {
return e;
}
}
2 changes: 1 addition & 1 deletion src/nimSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class NimSignatureHelpProvider implements vscode.SignatureHelpProvider {
}
}

execNimSuggest(NimSuggestType.con, filename, position.line + 1, position.character - 1, getDirtyFile(document))
execNimSuggest(NimSuggestType.con, filename, position.line + 1, position.character, getDirtyFile(document))
.then(items => {
var signatures = new vscode.SignatureHelp();
var isModule = 0;
Expand Down
27 changes: 14 additions & 13 deletions src/nimSuggestExec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import path = require('path');
import os = require('os');
import fs = require('fs');
import net = require('net');
import elrpc = require('elrpc');
import elparser = require('elparser');
import elrpc = require('./elrpc/elrpc');
import sexp = require('./elrpc/sexp');
import { prepareConfig, getProjectFile, isProjectMode, getNimExecPath, removeDirSync, correctBinname } from './nimUtils';
import { hideNimStatus, showNimStatus } from './nimStatus';

class NimSuggestProcessDescription {
process: cp.ChildProcess;
rpc: elrpc.RPCServer;
rpc: elrpc.EPCPeer;
}

let nimSuggestProcessCache: { [project: string]: PromiseLike<NimSuggestProcessDescription> } = {};
Expand Down Expand Up @@ -157,7 +157,7 @@ export function initNimSuggest(ctx: vscode.ExtensionContext) {
let versionOutput = cp.spawnSync(getNimSuggestPath(), ['--version'], { cwd: vscode.workspace.rootPath }).output.toString();
let versionArgs = /.+Version\s([\d|\.]+)\s\(.+/g.exec(versionOutput);
if (versionArgs && versionArgs.length === 2) {
_nimSuggestVersion = versionArgs[1];
_nimSuggestVersion = versionArgs[1];
}

console.log(versionOutput);
Expand Down Expand Up @@ -186,7 +186,7 @@ export async function execNimSuggest(suggestType: NimSuggestType, filename: stri
let desc = await getNimSuggestProcess(projectFile);
let suggestCmd = NimSuggestType[suggestType];
trace(desc.process.pid, projectFile, suggestCmd + ' ' + normalizedFilename + ':' + line + ':' + column);
let ret = await desc.rpc.callMethod(new elparser.ast.SExpSymbol(suggestCmd), normalizedFilename, line, column, dirtyFile);
let ret = await desc.rpc.callMethod(suggestCmd, { kind: "string", str: normalizedFilename }, { kind: "number", n: line }, { kind: "number", n: column }, { kind: "string", str: dirtyFile });
trace(desc.process.pid, projectFile + '=' + suggestCmd + ' ' + normalizedFilename, ret);

var result: NimSuggestResult[] = [];
Expand All @@ -205,8 +205,8 @@ export async function execNimSuggest(suggestType: NimSuggestType, filename: stri
item.column = parts[6];
var doc = parts[7];
if (doc !== '') {
doc = doc.replace(/\\,u000A|\\,u000D\\,u000A/g, '\n');
doc = doc.replace(/\`\`/g, '`');
doc = doc.replace(/\.\. code-block:: (\w+)\r?\n(( .*\r?\n?)+)/g, '```$1\n$2\n```\n');
doc = doc.replace(/\`([^\<\`]+)\<([^\>]+)\>\`\_/g, '\[$1\]\($2\)');
}
item.documentation = doc;
Expand Down Expand Up @@ -272,16 +272,17 @@ async function getNimSuggestProcess(nimProject: string): Promise<NimSuggestProce
if (isNaN(portNumber)) {
reject('Nimsuggest returns unknown port number: ' + dataStr);
} else {
elrpc.startClient(portNumber).then((client) => {
client.socket.on('error', err => {
console.error(err);
});
resolve({ process: process, rpc: client });
}, (reason: any) => {
reject(reason);
elrpc.startClient(portNumber).then((peer) => {
resolve({ process: process, rpc: peer });
});
}
});
process.stdout.once('data', (data) => {
console.log(data.toString());
});
process.stderr.once('data', (data) => {
console.log(data.toString());
});
process.on('close', (code: number, signal: string) => {
if (code !== 0) {
console.error('nimsuggest closed with code: ' + code + ', signal: ' + signal);
Expand Down
8 changes: 0 additions & 8 deletions typings/elparser.d.ts

This file was deleted.

30 changes: 0 additions & 30 deletions typings/elrpc.d.ts

This file was deleted.

0 comments on commit 367c0a9

Please sign in to comment.