diff --git a/source/api.d b/source/api.d new file mode 100644 index 0000000..724854c --- /dev/null +++ b/source/api.d @@ -0,0 +1,74 @@ +module workspaced.api; + +import std.conv; +import std.json; +import painlessjson; + +/// +alias AsyncCallback = void delegate(Throwable, JSONValue); + +/// Will get called asynchronously (Must prepend AsyncCallback as argument) +enum async = 2603248026; + +/// Will get called for loading components +enum load = 2603248027; + +/// Will get called for unloading components +enum unload = 2603248028; + +/// Will call this function in any case (cmd: component) +enum any = 2603248029; + +/// Component call +struct component +{ + /// Name of the component + string name; +} + +/// Will get called when some argument matches +struct Arguments +{ + /// Arguments to match + Argument[] arguments; +} + +private struct Argument +{ + /// Key in JSON object node at root level to match + string key; + /// Value in JSON object node at root level to match + JSONValue value; +} + +private template ArgumentPair(size_t i) +{ + static if (i > 0) + enum ArgumentPair = "ret.arguments[" ~ (i / 2 - 1).to!string ~ "] = Argument(args[" ~ (i - 2).to!string ~ "], args[" ~ (i - 1).to!string ~ "].toJSON);" ~ ArgumentPair!( + i - 2); + else + enum ArgumentPair = ""; +} + +Arguments arguments(T...)(T args) +{ + if (args.length < 2) + return Arguments.init; + Arguments ret; + ret.arguments.length = args.length / 2; + mixin(ArgumentPair!(args.length)); + return ret; +} + +unittest +{ + Arguments args = arguments("foo", 5, "bar", "str"); + assert(args.arguments[0].key == "foo"); + assert(args.arguments[0].value.integer == 5); + assert(args.arguments[1].key == "bar"); + assert(args.arguments[1].value.str == "str"); +} + +alias ImportPathProvider = string[]function(); + +ImportPathProvider importPathProvider, stringImportPathProvider; diff --git a/source/app.d b/source/app.d index b43e22a..9afede3 100644 --- a/source/app.d +++ b/source/app.d @@ -1,20 +1,48 @@ module workspaced.app; -import workspaced.com.component; - +import core.sync.mutex; import core.exception; + +import painlessjson; + +import workspaced.api; + import std.exception; import std.bitmanip; import std.process; +import std.traits; import std.stdio; import std.json; +import std.meta; +import std.conv; -static immutable Version = [1, 2, 0]; +static immutable Version = [2, 0, 0]; +__gshared Mutex writeMutex; -void send(int id, JSONValue value) +void sendFinal(int id, JSONValue value) { ubyte[] data = nativeToBigEndian(id) ~ (cast(ubyte[]) value.toString()); - stdout.rawWrite(nativeToBigEndian(cast(int) data.length) ~ data); + synchronized (writeMutex) + { + stdout.rawWrite(nativeToBigEndian(cast(int) data.length) ~ data); + stdout.flush(); + } +} + +void send(int id, JSONValue[] values) +{ + if (values.length == 0) + { + throw new Exception("Unknown arguments!"); + } + else if (values.length == 1) + { + sendFinal(id, values[0]); + } + else + { + sendFinal(id, JSONValue(values)); + } } JSONValue toJSONArray(T)(T value) @@ -27,6 +55,7 @@ JSONValue toJSONArray(T)(T value) return JSONValue(vals); } +/* JSONValue handleRequest(JSONValue value) { assert(value.type == JSON_TYPE.OBJECT, "Request must be an object!"); @@ -122,60 +151,258 @@ JSONValue handleRequest(JSONValue value) throw new Exception("Unknown command: " ~ command); } } +}*/ + +alias Identity(I...) = I; + +template JSONCallBody(alias T, string fn, string jsonvar, size_t i, Args...) +{ + static if (Args.length == 1 && Args[0] == "request" && is(Parameters!T[0] == JSONValue)) + enum JSONCallBody = jsonvar; + else static if (Args.length == i) + enum JSONCallBody = ""; + else static if (is(ParameterDefaults!T[i] == void)) + enum JSONCallBody = "(assert(`" ~ Args[i] ~ "` in " ~ jsonvar ~ ", `" ~ Args[i] ~ " has no default value and is not in the JSON request`), fromJSON!(Parameters!(" ~ fn ~ ")[" ~ i + .to!string ~ "])(" ~ jsonvar ~ "[`" ~ Args[i] ~ "`]" ~ "))," ~ JSONCallBody!(T, fn, jsonvar, i + 1, Args); + else + enum JSONCallBody = "(`" ~ Args[i] ~ "` in " ~ jsonvar ~ ") ? fromJSON!(Parameters!(" ~ fn ~ ")[" ~ i.to!string ~ "])(" ~ jsonvar ~ "[`" ~ Args[i] ~ "`]" ~ ") : ParameterDefaults!(" ~ fn ~ ")[" ~ i + .to!string ~ "]," ~ JSONCallBody!(T, fn, jsonvar, i + 1, Args); +} + +template JSONCallNoRet(alias T, string fn, string jsonvar, bool async) +{ + alias Args = ParameterIdentifierTuple!T; + static if (Args.length > 0) + enum JSONCallNoRet = fn ~ "(" ~ (async ? "asyncCallback," : "") ~ JSONCallBody!(T, fn, jsonvar, async ? 1 : 0, Args) ~ ")"; + else + enum JSONCallNoRet = fn ~ "(" ~ (async ? "asyncCallback" : "") ~ ")"; +} + +template JSONCall(alias T, string fn, string jsonvar, bool async) +{ + static if (async) + enum JSONCall = JSONCallNoRet!(T, fn, jsonvar, async) ~ ";"; + else + { + alias Ret = ReturnType!T; + static if (is(Ret == void)) + enum JSONCall = JSONCallNoRet!(T, fn, jsonvar, async) ~ ";"; + else + enum JSONCall = "values ~= " ~ JSONCallNoRet!(T, fn, jsonvar, async) ~ ".toJSON;"; + } +} + +void handleRequestMod(alias T)(int id, JSONValue request, ref JSONValue[] values, ref int asyncWaiting, ref bool isAsync, ref bool hasArgs, ref AsyncCallback asyncCallback) +{ + foreach (name; __traits(allMembers, T)) + { + static if (__traits(compiles, __traits(getMember, T, name))) + { + alias symbol = Identity!(__traits(getMember, T, name)); + static if (isSomeFunction!symbol) + { + bool matches = false; + foreach (Arguments args; getUDAs!(symbol, Arguments)) + { + if (!matches) + { + foreach (arg; args.arguments) + { + if (!matches) + { + auto nodeptr = arg.key in request; + if (nodeptr && *nodeptr == arg.value) + matches = true; + } + } + } + } + static if (hasUDA!(symbol, any)) + matches = true; + static if (hasUDA!(symbol, component)) + { + if (("cmd" in request) !is null && request["cmd"].type == JSON_TYPE.STRING && getUDAs!(symbol, component)[0].name != request["cmd"].str) + matches = false; + } + static if (hasUDA!(symbol, load) && hasUDA!(symbol, component)) + { + if (("components" in request) !is null && ("cmd" in request) !is null && request["cmd"].type == JSON_TYPE.STRING && request["cmd"].str == "load") + { + if (request["components"].type == JSON_TYPE.ARRAY) + { + foreach (com; request["components"].array) + if (com.type == JSON_TYPE.STRING && com.str == getUDAs!(symbol, component)[0].name) + matches = true; + } + else if (request["components"].type == JSON_TYPE.STRING && request["components"].str == getUDAs!(symbol, component)[0].name) + matches = true; + } + } + static if (hasUDA!(symbol, unload) && hasUDA!(symbol, component)) + { + if (("components" in request) !is null && ("cmd" in request) !is null && request["cmd"].type == JSON_TYPE.STRING && request["cmd"].str == "unload") + { + if (request["components"].type == JSON_TYPE.ARRAY) + { + foreach (com; request["components"].array) + if (com.type == JSON_TYPE.STRING && com.str == getUDAs!(symbol, component)[0].name) + matches = true; + } + else if (request["components"].type == JSON_TYPE.STRING && request["components"].str == getUDAs!(symbol, component)[0].name) + matches = true; + } + } + if (matches) + { + static if (hasUDA!(symbol, async)) + { + assert(!hasArgs); + isAsync = true; + asyncWaiting++; + mixin(JSONCall!(symbol[0], "symbol[0]", "request", true)); + } + else + { + assert(!isAsync); + hasArgs = true; + mixin(JSONCall!(symbol[0], "symbol[0]", "request", false)); + } + } + } + } + } +} + +void handleRequest(int id, JSONValue request) +{ + JSONValue[] values; + int asyncWaiting = 0; + bool isAsync = false; + bool hasArgs = false; + Mutex asyncMutex = new Mutex; + + AsyncCallback asyncCallback = (err, value) { + synchronized (asyncMutex) + { + try + { + assert(isAsync); + if (err) + throw err; + values ~= value; + asyncWaiting--; + if (asyncWaiting == 0) + send(id, values); + } + catch (Exception e) + { + processException(id, e); + } + catch (AssertError e) + { + processException(id, e); + } + } + }; + + handleRequestMod!(workspaced.com.dub)(id, request, values, asyncWaiting, isAsync, hasArgs, asyncCallback); + handleRequestMod!(workspaced.com.dcd)(id, request, values, asyncWaiting, isAsync, hasArgs, asyncCallback); + handleRequestMod!(workspaced.com.dfmt)(id, request, values, asyncWaiting, isAsync, hasArgs, asyncCallback); + handleRequestMod!(workspaced.com.dscanner)(id, request, values, asyncWaiting, isAsync, hasArgs, asyncCallback); + + if (isAsync) + { + if (values.length > 0) + throw new Exception("Cannot mix sync and async functions!"); + } + else + { + if (hasArgs && values.length == 0) + sendFinal(id, JSONValue(null)); + else + send(id, values); + } +} + +void processException(int id, Throwable e) +{ + stderr.writeln(e); + // dfmt off + sendFinal(id, JSONValue([ + "error": JSONValue(true), + "msg": JSONValue(e.msg), + "exception": JSONValue(e.toString()) + ])); + // dfmt on +} + +void processException(int id, JSONValue request, Throwable e) +{ + stderr.writeln(e); + // dfmt off + sendFinal(id, JSONValue([ + "error": JSONValue(true), + "msg": JSONValue(e.msg), + "exception": JSONValue(e.toString()), + "request": request + ])); + // dfmt on } int main(string[] args) { + import std.file; import etc.linux.memoryerror; static if (is(typeof(registerMemoryErrorHandler))) registerMemoryErrorHandler(); + writeMutex = new Mutex; + int length = 0; int id = 0; ubyte[4] intBuffer; ubyte[] dataBuffer; + JSONValue data; while (stdin.isOpen && stdout.isOpen && !stdin.eof) { - try - { - dataBuffer = stdin.rawRead(intBuffer); - assert(dataBuffer.length == 4, "Unexpected buffer data"); - length = bigEndianToNative!int(dataBuffer[0 .. 4]); + dataBuffer = stdin.rawRead(intBuffer); + assert(dataBuffer.length == 4, "Unexpected buffer data"); + length = bigEndianToNative!int(dataBuffer[0 .. 4]); - assert(length >= 4, "Invalid request"); + assert(length >= 4, "Invalid request"); - dataBuffer = stdin.rawRead(intBuffer); - assert(dataBuffer.length == 4, "Unexpected buffer data"); - id = bigEndianToNative!int(dataBuffer[0 .. 4]); + dataBuffer = stdin.rawRead(intBuffer); + assert(dataBuffer.length == 4, "Unexpected buffer data"); + id = bigEndianToNative!int(dataBuffer[0 .. 4]); - dataBuffer.length = length - 4; - dataBuffer = stdin.rawRead(dataBuffer); + dataBuffer.length = length - 4; + dataBuffer = stdin.rawRead(dataBuffer); - auto data = parseJSON(cast(string) dataBuffer); - send(id, handleRequest(data)); + try + { + data = parseJSON(cast(string) dataBuffer); + } + catch (Exception e) + { + processException(id, e); + } + catch (AssertError e) + { + processException(id, e); + } + + try + { + handleRequest(id, data); } catch (Exception e) { - stderr.writeln(e); - // dfmt off - send(id, JSONValue([ - "error": JSONValue(true), - "msg": JSONValue(e.msg), - "exception": JSONValue(e.toString()) - ])); - // dfmt on + processException(id, data, e); } catch (AssertError e) { - stderr.writeln(e); - // dfmt off - send(id, JSONValue([ - "error": JSONValue(true), - "msg": JSONValue(e.msg), - "exception": JSONValue(e.toString()) - ])); - // dfmt on + processException(id, data, e); } stdout.flush(); } diff --git a/source/com/component.d b/source/com/component.d deleted file mode 100644 index d8d9476..0000000 --- a/source/com/component.d +++ /dev/null @@ -1,95 +0,0 @@ -module workspaced.com.component; - -import std.json; - -class Component -{ -public: - abstract void load(JSONValue args); - abstract void unload(JSONValue args); - abstract JSONValue process(JSONValue args); - - @property auto initialized() - { - return _initialized; - } - - void initialize(JSONValue args) - { - if (_initialized) - return; - load(args); - _initialized = true; - } - - void deinitialize(JSONValue args) - { - if (!_initialized) - return; - unload(args); - _initialized = false; - } - -private: - bool _initialized = false; -} - -interface IImportPathProvider -{ - string[] importPaths(); -} - -interface IStringImportPathProvider -{ - string[] stringImportPaths(); -} - -static Component[string] components; -private static IImportPathProvider importPathProvider; -private static IStringImportPathProvider stringImportPathProvider; - -void setImportPathProvider(IImportPathProvider provider) -{ - assert(importPathProvider is null, "Another import path provider is already set!"); - importPathProvider = provider; -} - -void setStringImportPathProvider(IStringImportPathProvider provider) -{ - assert(stringImportPathProvider is null, "Another string import path provider is already set!"); - stringImportPathProvider = provider; -} - -IImportPathProvider getImportPathProvider() -{ - return importPathProvider; -} - -IStringImportPathProvider getStringImportPathProvider() -{ - return stringImportPathProvider; -} - -string getString(JSONValue value, string key) -{ - auto ptr = key in value; - assert(ptr, key ~ " not specified!"); - assert(ptr.type == JSON_TYPE.STRING, key ~ " must be a string!"); - return ptr.str; -} - -auto getInt(JSONValue value, string key) -{ - auto ptr = key in value; - assert(ptr, key ~ " not specified!"); - assert(ptr.type == JSON_TYPE.INTEGER, key ~ " must be a string!"); - return ptr.integer; -} - -JSONValue get(JSONValue value, string key) -{ - auto ptr = key in value; - assert(ptr, key ~ " not specified!"); - assert(ptr.type == JSON_TYPE.OBJECT, key ~ " must be an object!"); - return *ptr; -} diff --git a/source/com/dcd.d b/source/com/dcd.d index 43bc6ff..b06fa9b 100644 --- a/source/com/dcd.d +++ b/source/com/dcd.d @@ -1,7 +1,5 @@ module workspaced.com.dcd; -import workspaced.com.component; - import std.json; import std.conv; import std.stdio; @@ -11,183 +9,226 @@ import core.thread; import painlessjson; -private struct DCDInit +import workspaced.api; + +@component("dcd") : + +@load void start(string dir, string clientPath = "dcd-client", string serverPath = "dcd-server", ushort port = 9166, bool autoStart = true) { - ushort port = 9166; - string clientPath = "dcd-client"; - string serverPath = "dcd-server"; - string dir; - bool autoStart = true; + .cwd = dir; + .serverPath = serverPath; + .clientPath = clientPath; + .port = port; + if (autoStart) + startServer(); } -private struct DCDServerStatus +@unload void stop() { - bool isRunning; + stopServerSync(); } -private struct DCDIdentifier +@arguments("subcmd", "setup-server") +void setupServer() { - string identifier; - string type; + startServer(); + updateImports(); } -private struct DCDSearchResult +@arguments("subcmd", "start-server") +void startServer() { - string file; - int position; - string type; + if (isPortRunning(port)) + throw new Exception("Already running dcd on port " ~ port.to!string); + runningPort = port; + serverPipes = raw([serverPath, "--port", runningPort.to!string], Redirect.stdin | Redirect.stdoutToStderr); + new Thread({ + while (!serverPipes.stderr.eof) + { + stderr.writeln("Server: ", serverPipes.stderr.readln()); + } + stderr.writeln("DCD-Server stopped with code ", serverPipes.pid.wait()); + }).start(); } -class DCDComponent : Component +void stopServerSync() { -public: - override void load(JSONValue args) - { - DCDInit value = fromJSON!DCDInit(args); - assert(value.dir, "dcd initialization requires a 'dir' field"); - cwd = value.dir; - clientPath = value.clientPath; - serverPath = value.serverPath; - port = value.port; - if (value.autoStart) - startServer(); - } + doClient(["--shutdown"]).pid.wait; +} - override void unload(JSONValue args) - { - auto pipes = stopServer(); - pipes.pid.wait(); - } +@async @arguments("subcmd", "stop-server") +void stopServer(AsyncCallback cb) +{ + new Thread({ /**/ + try + { + stopServerSync(); + cb(null, JSONValue(null)); + } + catch (Throwable t) + { + cb(t, JSONValue(null)); + } + }).start(); +} - void startServer() - { - if (isPortRunning(port)) - throw new Exception("Already running dcd on port " ~ to!string(port)); - runningPort = port; - serverPipes = raw([serverPath, "--port", to!string(runningPort)], Redirect.stdin | Redirect.stdoutToStderr); - new Thread({ - while (!serverPipes.stderr.eof) - { - stderr.writeln("Server: ", serverPipes.stderr.readln()); - } - stderr.writeln("DCD-Server stopped with code ", serverPipes.pid.wait()); - }).start(); - } +@arguments("subcmd", "kill-server") +void killServer() +{ + if (!serverPipes.pid.tryWait().terminated) + serverPipes.pid.kill(); +} - auto stopServer() - { - return doClient(["--shutdown"]); - } +@async @arguments("subcmd", "restart-server") +void restartServer(AsyncCallback cb) +{ + new Thread({ /**/ + try + { + stopServerSync(); + setupServer(); + cb(null, JSONValue(null)); + } + catch (Throwable t) + { + cb(t, JSONValue(null)); + } + }).start(); +} - void killServer() - { - if (!serverPipes.pid.tryWait().terminated) - serverPipes.pid.kill(); - } +@arguments("subcmd", "status") +auto serverStatus() @property +{ + DCDServerStatus status; + if (serverPipes.pid.tryWait().terminated) + status.isRunning = false; + else + status.isRunning = isPortRunning(runningPort) == 0; + return status; +} - void addImports(string[] imports) - { - string[] args; - foreach (path; knownImports) - args ~= "-I" ~ path; - foreach (path; imports) - args ~= "-I" ~ path; - knownImports ~= imports; - doClient(args).pid.wait(); - } +@arguments("subcmd", "search-symbol") +@async auto searchSymbol(AsyncCallback cb, string query) +{ + new Thread({ + try + { + auto pipes = doClient(["--search", query]); + scope (exit) + pipes.pid.wait(); + pipes.stdin.close(); + DCDSearchResult[] results; + while (pipes.stdout.isOpen && !pipes.stdout.eof) + { + string line = pipes.stdout.readln(); + if (line.length == 0) + continue; + string[] splits = line[0 .. $ - 1].split('\t'); + results ~= DCDSearchResult(splits[0], toImpl!(int)(splits[2]), splits[1]); + } + cb(null, results.toJSON); + } + catch (Throwable t) + { + cb(t, JSONValue(null)); + } + }).start(); +} - @property auto serverStatus() - { - DCDServerStatus status; - if (serverPipes.pid.tryWait().terminated) - status.isRunning = false; - else - status.isRunning = isPortRunning(runningPort) == 0; - return status; - } +@arguments("subcmd", "refresh-imports") +void refreshImports() +{ + addImports(importPathProvider()); +} - string getDocumentation(string code, int location) - { - auto pipes = doClient(["--doc", "-c", to!string(location)]); - pipes.stdin.write(code); - pipes.stdin.close(); - string data; - while (pipes.stdout.isOpen && !pipes.stdout.eof) +@arguments("subcmd", "add-imports") +void addImports(string[] imports) +{ + knownImports ~= imports; + updateImports(); +} + +@arguments("subcmd", "find-and-select-port") +@async void findAndSelectPort(AsyncCallback cb, ushort port = 9166) +{ + new Thread({ /**/ + try { - string line = pipes.stdout.readln(); - if (line.length) - data ~= line[0 .. $ - 1]; + auto newPort = findOpen(port); + port = newPort; + cb(null, port.toJSON()); } - return data.replace("\\n", "\n"); - } - - JSONValue findDeclaration(string code, int location) - { - auto pipes = doClient(["-c", to!string(location), "--symbolLocation"]); - scope (exit) - pipes.pid.wait(); - pipes.stdin.write(code); - pipes.stdin.close(); - string line = pipes.stdout.readln(); - if (line.length == 0) - return JSONValue(null); - string[] splits = line[0 .. $ - 1].split('\t'); - if (splits.length != 2) - return JSONValue(null); - return JSONValue([JSONValue(splits[0]), JSONValue(toImpl!int(splits[1]))]); - } + catch (Throwable t) + { + cb(t, JSONValue(null)); + } + }).start(); +} - DCDSearchResult[] searchSymbol(string query) - { - auto pipes = doClient(["--search", query]); - scope (exit) - pipes.pid.wait(); - pipes.stdin.close(); - DCDSearchResult[] results; - while (pipes.stdout.isOpen && !pipes.stdout.eof) +@arguments("subcmd", "find-declaration") +@async void findDeclaration(AsyncCallback cb, string code, int pos) +{ + new Thread({ + try { + auto pipes = doClient(["-c", pos.to!string, "--symbolLocation"]); + scope (exit) + pipes.pid.wait(); + pipes.stdin.write(code); + pipes.stdin.close(); string line = pipes.stdout.readln(); if (line.length == 0) - continue; + { + cb(null, JSONValue(null)); + return; + } string[] splits = line[0 .. $ - 1].split('\t'); - results ~= DCDSearchResult(splits[0], toImpl!(int)(splits[2]), splits[1]); + if (splits.length != 2) + { + cb(null, JSONValue(null)); + return; + } + cb(null, JSONValue([JSONValue(splits[0]), JSONValue(splits[1].to!int)])); } - return results; - } + catch (Throwable t) + { + cb(t, JSONValue(null)); + } + }).start(); +} - override JSONValue process(JSONValue args) - { - string cmd = args.getString("subcmd"); - switch (cmd) +@arguments("subcmd", "get-documentation") +@async void getDocumentation(AsyncCallback cb, string code, int pos) +{ + new Thread({ + try { - case "status": - return serverStatus.toJSON(); - case "setup-server": - startServer(); - addImports(getImportPathProvider().importPaths); - break; - case "start-server": - startServer(); - break; - case "stop-server": - stopServer().pid.wait(); - break; - case "kill-server": - killServer(); - break; - case "restart-server": - auto pipes = stopServer(); - pipes.pid.wait(); - startServer(); - break; - case "find-and-select-port": - auto newPort = findOpen(cast(ushort) args.getInt("port")); - port = newPort; - return port.toJSON(); - case "list-completion": - string code = args.getString("code"); - auto pos = args.getInt("pos"); - auto pipes = doClient(["-c", to!string(pos)]); + auto pipes = doClient(["--doc", "-c", pos.to!string]); + pipes.stdin.write(code); + pipes.stdin.close(); + string data; + while (pipes.stdout.isOpen && !pipes.stdout.eof) + { + string line = pipes.stdout.readln(); + if (line.length) + data ~= line[0 .. $ - 1]; + } + cb(null, JSONValue(data.replace("\\n", "\n"))); + } + catch (Throwable t) + { + cb(t, JSONValue(null)); + } + }).start(); +} + +@arguments("subcmd", "list-completion") +@async void listCompletion(AsyncCallback cb, string code, int pos) +{ + new Thread({ + try + { + auto pipes = doClient(["-c", pos.to!string]); scope (exit) pipes.pid.wait(); pipes.stdin.write(code); @@ -202,82 +243,99 @@ public: } int[] emptyArr; if (data.length == 0) - return JSONValue(["type" : JSONValue("identifiers"), "identifiers" : emptyArr.toJSON()]); + { + cb(null, JSONValue(["type" : JSONValue("identifiers"), "identifiers" : emptyArr.toJSON()])); + return; + } if (data[0] == "calltips") { - return JSONValue(["type" : JSONValue("calltips"), "calltips" : data[1 .. $].toJSON()]); + cb(null, JSONValue(["type" : JSONValue("calltips"), "calltips" : data[1 .. $].toJSON()])); + return; } else if (data[0] == "identifiers") { DCDIdentifier[] identifiers; - foreach (line; data[1 .. $]) + foreach (line; + data[1 .. $]) { string[] splits = line.split('\t'); identifiers ~= DCDIdentifier(splits[0], splits[1]); } - return JSONValue(["type" : JSONValue("identifiers"), "identifiers" : identifiers.toJSON()]); + cb(null, JSONValue(["type" : JSONValue("identifiers"), "identifiers" : identifiers.toJSON()])); + return; } else { - return JSONValue(["type" : JSONValue("raw"), "raw" : data.toJSON()]); + cb(null, JSONValue(["type" : JSONValue("raw"), "raw" : data.toJSON()])); + return; } - case "get-documentation": - return getDocumentation(args.getString("code"), cast(int) args.getInt("pos")).toJSON(); - case "find-declaration": - return findDeclaration(args.getString("code"), cast(int) args.getInt("pos")); - case "search-symbol": - return searchSymbol(args.getString("query")).toJSON(); - case "refresh-imports": - addImports(getImportPathProvider().importPaths); - break; - case "add-imports": - assert("imports" in args, "No import paths specified"); - addImports(fromJSON!(string[])(args["imports"])); - break; - default: - throw new Exception("Unknown command: '" ~ cmd ~ "'"); } - return JSONValue(null); - } + catch (Throwable e) + { + cb(e, JSONValue(null)); + } + }).start(); +} -private: - auto doClient(string[] args) - { - return raw([clientPath, "--port", to!string(runningPort)] ~ args); - } +void updateImports() +{ + string[] args; + foreach (path; knownImports) + args ~= "-I" ~ path; + doClient(args).pid.wait(); +} - auto raw(string[] args, Redirect redirect = Redirect.all) - { - auto pipes = pipeProcess(args, redirect, null, Config.none, cwd); - return pipes; - } +private __gshared: - bool isPortRunning(ushort port) - { - auto pipes = raw([clientPath, "-q", "--port", to!string(port)]); - return wait(pipes.pid) == 0; - } +string clientPath, serverPath, cwd; +ProcessPipes serverPipes; +ushort port, runningPort; +string[] knownImports; + +auto doClient(string[] args) +{ + return raw([clientPath, "--port", runningPort.to!string] ~ args); +} + +auto raw(string[] args, Redirect redirect = Redirect.all) +{ + auto pipes = pipeProcess(args, redirect, null, Config.none, cwd); + return pipes; +} + +bool isPortRunning(ushort port) +{ + auto pipes = raw([clientPath, "-q", "--port", port.to!string]); + return wait(pipes.pid) == 0; +} - ushort findOpen(ushort port) +ushort findOpen(ushort port) +{ + port--; + bool isRunning; + do { - port--; - bool isRunning; - do - { - port++; - isRunning = isPortRunning(port); - } - while (isRunning); - return port; + port++; + isRunning = isPortRunning(port); } + while (isRunning); + return port; +} + +private struct DCDServerStatus +{ + bool isRunning; +} - string clientPath, serverPath, cwd; - ProcessPipes serverPipes; - ushort port, runningPort; - string[] knownImports; +private struct DCDIdentifier +{ + string identifier; + string type; } -shared static this() +private struct DCDSearchResult { - components["dcd"] = new DCDComponent(); + string file; + int position; + string type; } diff --git a/source/com/dfmt.d b/source/com/dfmt.d index 376a625..cd88afa 100644 --- a/source/com/dfmt.d +++ b/source/com/dfmt.d @@ -1,72 +1,53 @@ module workspaced.com.dfmt; -import workspaced.com.component; - import std.json; -import std.conv; -import std.stdio; -import std.regex; -import std.string; import std.process; -import std.algorithm; import core.thread; import painlessjson; -private struct DFMTInit +import workspaced.api; + +@component("dfmt") : + +@load void start(string dir, string dfmtPath = "dfmt") { - string dfmtPath = "dfmt"; - string dir; + cwd = dir; + execPath = dfmtPath; } -class DFMTComponent : Component +@unload void stop() { -public: - override void load(JSONValue args) - { - DFMTInit value = fromJSON!DFMTInit(args); - assert(value.dir, "dfmt initialization requires a 'dir' field"); - - execPath = value.dfmtPath; - cwd = value.dir; - } - - override void unload(JSONValue args) - { - } +} - override JSONValue process(JSONValue args) - { - string code = args.getString("code"); - ProcessPipes pipes = raw([execPath]); - scope (exit) - pipes.pid.wait(); - pipes.stdin.write(code); - pipes.stdin.close(); - ubyte[4096] buffer; - ubyte[] data; - size_t len; - do +@any @async void format(AsyncCallback cb, string code) +{ + new Thread({ + try { - auto appended = pipes.stdout.rawRead(buffer); - len = appended.length; - data ~= appended; + auto pipes = pipeProcess([execPath], Redirect.all, null, Config.none, cwd); + scope (exit) + pipes.pid.wait(); + pipes.stdin.write(code); + pipes.stdin.close(); + ubyte[4096] buffer; + ubyte[] data; + size_t len; + do + { + auto appended = pipes.stdout.rawRead(buffer); + len = appended.length; + data ~= appended; + } + while (len == 4096); + cb(null, JSONValue(cast(string) data)); } - while (len == 4096); - return JSONValue(cast(string) data); - } - -private: - auto raw(string[] args, Redirect redirect = Redirect.all) - { - auto pipes = pipeProcess(args, redirect, null, Config.none, cwd); - return pipes; - } - - string cwd, execPath; + catch (Throwable e) + { + cb(e, JSONValue(null)); + } + }).start(); } -shared static this() -{ - components["dfmt"] = new DFMTComponent(); -} +__gshared: +string cwd, execPath; diff --git a/source/com/dscanner.d b/source/com/dscanner.d index 96b00f5..3faef94 100644 --- a/source/com/dscanner.d +++ b/source/com/dscanner.d @@ -1,7 +1,5 @@ module workspaced.com.dscanner; -import workspaced.com.component; - import std.json; import std.conv; import std.path; @@ -14,136 +12,130 @@ import core.thread; import painlessjson; -private struct DScannerInit -{ - string dscannerPath = "dscanner"; - string dir; -} +import workspaced.api; -private auto dscannerIssueRegex = ctRegex!`^(.+?)\((\d+)\:(\d+)\)\[(.*?)\]: (.*)`; -private struct DScannerIssue -{ - string file; - int line, column; - string type; - string description; -} +@component("dscanner") : -private struct OutlineTreeNode +@load void start(string dir, string dscannerPath = "dscanner") { - string definition; - int line; - OutlineTreeNode[] children; + cwd = dir; + execPath = dscannerPath; } -private struct DefinitionElement +@unload void stop() { - string name; - int line; - string type; - string[string] attributes; } -class DScannerComponent : Component +@arguments("subcmd", "lint") +@async void lint(AsyncCallback cb, string file) { -public: - override void load(JSONValue args) - { - DScannerInit value = fromJSON!DScannerInit(args); - assert(value.dir, "dub initialization requires a 'dir' field"); - - execPath = value.dscannerPath; - cwd = value.dir; - } - - override void unload(JSONValue args) - { - } - - override JSONValue process(JSONValue args) - { - string cmd = args.getString("subcmd"); - switch (cmd) + new Thread({ + try { - case "lint": + ProcessPipes pipes = raw([execPath, "-S", file, "--config", buildPath(cwd, "dscanner.ini")]); + scope (exit) + pipes.pid.wait(); + string[] res; + while (pipes.stdout.isOpen && !pipes.stdout.eof) + res ~= pipes.stdout.readln(); + DScannerIssue[] issues; + foreach (line; + res) { - string file = args.getString("file"); - ProcessPipes pipes = raw([execPath, "-S", file, "--config", buildPath(cwd, "dscanner.ini")]); - scope (exit) - pipes.pid.wait(); - string[] res; - while (pipes.stdout.isOpen && !pipes.stdout.eof) - res ~= pipes.stdout.readln(); - DScannerIssue[] issues; - foreach (line; res) - { - if (!line.length) - continue; - auto match = line[0 .. $ - 1].matchFirst(dscannerIssueRegex); - if (!match) - continue; - DScannerIssue issue; - issue.file = match[1]; - issue.line = toImpl!int(match[2]); - issue.column = toImpl!int(match[3]); - issue.type = match[4]; - issue.description = match[5]; - issues ~= issue; - } - return issues.toJSON(); + if (!line.length) + continue; + auto match = line[0 .. $ - 1].matchFirst(dscannerIssueRegex); + if (!match) + continue; + DScannerIssue issue; + issue.file = match[1]; + issue.line = toImpl!int(match[2]); + issue.column = toImpl!int(match[3]); + issue.type = match[4]; + issue.description = match[5]; + issues ~= issue; } - case "list-definitions": + cb(null, issues.toJSON); + } + catch (Throwable e) + { + cb(e, JSONValue(null)); + } + }).start(); +} + +@arguments("subcmd", "list-definitions") +@async void listDefinitions(AsyncCallback cb, string file) +{ + new Thread({ + try + { + ProcessPipes pipes = raw([execPath, "-c", file]); + scope (exit) + pipes.pid.wait(); + string[] res; + while (pipes.stdout.isOpen && !pipes.stdout.eof) + res ~= pipes.stdout.readln(); + DefinitionElement[] definitions; + foreach (line; + res) { - string file = args.getString("file"); - ProcessPipes pipes = raw([execPath, "-c", file]); - scope (exit) - pipes.pid.wait(); - string[] res; - while (pipes.stdout.isOpen && !pipes.stdout.eof) - res ~= pipes.stdout.readln(); - DefinitionElement[] definitions; - foreach (line; res) + if (!line.length || line[0] == '!') + continue; + line = line[0 .. $ - 1]; + string[] splits = line.split('\t'); + DefinitionElement definition; + definition.name = splits[0]; + definition.type = splits[3]; + definition.line = toImpl!int(splits[4][5 .. $]); + if (splits.length > 5) + foreach (attribute; + splits[5 .. $]) { - if (!line.length || line[0] == '!') - continue; - line = line[0 .. $ - 1]; - string[] splits = line.split('\t'); - DefinitionElement definition; - definition.name = splits[0]; - definition.type = splits[3]; - definition.line = toImpl!int(splits[4][5 .. $]); - if (splits.length > 5) - foreach (attribute; splits[5 .. $]) - { - string[] sides = attribute.split(':'); - definition.attributes[sides[0]] = sides[1 .. $].join(':'); - } - definitions ~= definition; + string[] sides = attribute.split(':'); + definition.attributes[sides[0]] = sides[1 .. $].join(':'); } - return definitions.toJSON(); - } - case "outline": - { - OutlineTreeNode[] outline; - return outline.toJSON(); + definitions ~= definition; } - default: - throw new Exception("Unknown command: '" ~ cmd ~ "'"); + cb(null, definitions.toJSON); } - //return JSONValue(null); - } + catch (Throwable e) + { + cb(e, JSONValue(null)); + } + }).start(); +} -private: - auto raw(string[] args, Redirect redirect = Redirect.all) - { - auto pipes = pipeProcess(args, redirect, null, Config.none, cwd); - return pipes; - } +private __gshared: + +string cwd, execPath; + +auto raw(string[] args, Redirect redirect = Redirect.all) +{ + auto pipes = pipeProcess(args, redirect, null, Config.none, cwd); + return pipes; +} + +auto dscannerIssueRegex = ctRegex!`^(.+?)\((\d+)\:(\d+)\)\[(.*?)\]: (.*)`; +struct DScannerIssue +{ + string file; + int line, column; + string type; + string description; +} - string execPath, cwd; +struct OutlineTreeNode +{ + string definition; + int line; + OutlineTreeNode[] children; } -shared static this() +struct DefinitionElement { - components["dscanner"] = new DScannerComponent(); + string name; + int line; + string type; + string[string] attributes; } diff --git a/source/com/dub.d b/source/com/dub.d index fc26b30..7d56991 100644 --- a/source/com/dub.d +++ b/source/com/dub.d @@ -1,13 +1,16 @@ module workspaced.com.dub; -import workspaced.com.component; +import core.sync.mutex; +import core.thread; -import std.json; +import std.json : JSONValue; +import std.stdio; +import std.parallelism; import std.algorithm; import painlessjson; -import core.thread; +import workspaced.api; import dub.dub; import dub.project; @@ -15,215 +18,204 @@ import dub.package_; import dub.description; import dub.compilers.compiler; import dub.compilers.buildsettings; -import dub.internal.vibecompat.core.log; import dub.internal.vibecompat.inet.url; -private struct DubInit +@component("dub") : + +@load void startup(string dir, bool registerImportProvider = true, bool registerStringImportProvider = true) { - string dir; - bool watchFile = true; - bool registerImportProvider = true; - bool registerStringImportProvider = true; + if (registerImportProvider) + importPathProvider = &imports; + if (registerStringImportProvider) + stringImportPathProvider = &stringImports; + + _cwdStr = dir; + _cwd = Path(dir); + + start(); + + string compilerName = defaultCompiler; + _compiler = getCompiler(compilerName); + BuildSettings settings; + _platform = _compiler.determinePlatform(settings, compilerName); + _settings = settings; + + setConfiguration(_dub.project.getDefaultConfiguration(_platform)); } -private struct DubPackageInfo +@unload void stop() { - string[string] dependencies; - string ver; - string name; + _dub.shutdown(); } -class DubComponent : Component, IImportPathProvider, IStringImportPathProvider +private void start() { -public: - override void load(JSONValue args) - { - DubInit value = fromJSON!DubInit(args); - assert(value.dir, "dub initialization requires a 'dir' field"); - - if (value.registerImportProvider) - setImportPathProvider(this); - if (value.registerStringImportProvider) - setStringImportPathProvider(this); - - _dub = new Dub(null, value.dir, SkipRegistry.none); - _cwdStr = value.dir; - _cwd = Path(value.dir); - _dub.packageManager.getOrLoadPackage(_cwd); - _dub.loadPackageFromCwd(); - _dub.project.validate(); - string compilerName = defaultCompiler; - _compiler = getCompiler(compilerName); - BuildPlatform platform = _compiler.determinePlatform(_settings, compilerName); - _platform = platform; // Workaround for strange bug - setConfiguration(dub.project.getDefaultConfiguration(_platform)); - } + _dub = new Dub(null, _cwdStr, SkipRegistry.none); + _dub.packageManager.getOrLoadPackage(_cwd); + _dub.loadPackageFromCwd(); + _dub.project.validate(); +} - bool updateImportPaths(bool restartDub = true) - { - if(restartDub) - restart(); - - ProjectDescription desc = dub.project.describe(_platform, _configuration, _buildType); - if (desc.targets.length > 0 && desc.targetLookup.length > 0 && (desc.rootPackage in desc.targetLookup) !is null) +private void restart() +{ + stop(); + start(); +} + +@arguments("subcmd", "update") +@async void update(AsyncCallback callback) +{ + restart(); + new Thread({ /**/ + try { - // target-type: none (no import paths) - _importPaths = dub.project.listImportPaths(_platform, _configuration, _buildType, false); - _stringImportPaths = dub.project.listStringImportPaths(_platform, _configuration, _buildType, false); - return _importPaths.length > 0; + auto result = updateImportPaths(false); + callback(null, result.toJSON); } - else + catch (Throwable t) { - _importPaths = []; - _stringImportPaths = []; - return false; + callback(t, null.toJSON); } - } + }).start(); +} + +bool updateImportPaths(bool restartDub = true) +{ + if (restartDub) + restart(); + + ProjectDescription desc = _dub.project.describe(_platform, _configuration, _buildType); - override void unload(JSONValue args) + // target-type: none (no import paths) + if (desc.targets.length > 0 && desc.targetLookup.length > 0 && (desc.rootPackage in desc.targetLookup) !is null) { - _dub.shutdown(); + _importPaths = _dub.project.listImportPaths(_platform, _configuration, _buildType, false); + _stringImportPaths = _dub.project.listStringImportPaths(_platform, _configuration, _buildType, false); + return _importPaths.length > 0; } - - void restart() + else { - _dub.shutdown(); - - _dub = new Dub(null, _cwdStr, SkipRegistry.none); - _dub.packageManager.getOrLoadPackage(_cwd); - _dub.loadPackageFromCwd(); - _dub.project.validate(); + _importPaths = []; + _stringImportPaths = []; + return false; } +} - @property auto dependencies() - { - return dub.project.listDependencies(); - } +@arguments("subcmd", "upgrade") +void upgrade() +{ + _dub.upgrade(UpgradeOptions.upgrade); +} - @property string[] importPaths() - { - return _importPaths; - } +@arguments("subcmd", "list:dep") +auto dependencies() @property +{ + return _dub.project.listDependencies(); +} - @property string[] stringImportPaths() - { - return _stringImportPaths; - } +@arguments("subcmd", "list:import") +auto imports() @property +{ + return _importPaths; +} - @property auto configurations() - { - return dub.project.configurations; - } +@arguments("subcmd", "list:string-import") +auto stringImports() @property +{ + return _stringImportPaths; +} - void upgrade() - { - _dub.upgrade(UpgradeOptions.upgrade); - } +@arguments("subcmd", "list:configurations") +auto configurations() @property +{ + return _dub.project.configurations; +} - @property auto dub() - { - return _dub; - } +@arguments("subcmd", "get:configuration") +auto configuration() @property +{ + return _configuration; +} - @property auto configuration() - { - return _configuration; - } +@arguments("subcmd", "set:configuration") +bool setConfiguration(string configuration) +{ + if (!_dub.project.configurations.canFind(configuration)) + return false; + _configuration = configuration; + return updateImportPaths(false); +} - bool setConfiguration(string value) +@arguments("subcmd", "get:build-type") +auto buildType() @property +{ + return _buildType; +} + +@arguments("subcmd", "set:build-type") +bool setBuildType(JSONValue request) +{ + try { - if (!dub.project.configurations.canFind(value)) - return false; - _configuration = value; + assert("build-type" in request, "build-type not in request"); + _buildType = request["build-type"].str; return updateImportPaths(false); } - - @property auto buildType() + catch (Exception e) { - return _buildType; + return false; } +} - bool setBuildType(string value) - { - try - { - _buildType = value; - return updateImportPaths(false); - } - catch (Exception e) - { - return false; - } - } +@arguments("subcmd", "get:compiler") +auto compiler() @property +{ + return _compiler.name; +} - @property auto compiler() +@arguments("subcmd", "set:compiler") +bool setCompiler(string compiler) +{ + try { - return _compiler.name; + _compiler = getCompiler(compiler); + return true; } - - bool setCompiler(string value) + catch (Exception e) { - try - { - _compiler = getCompiler(value); - return true; - } - catch (Exception e) - { // No public function to get compilers - return false; - } + return false; } +} - override JSONValue process(JSONValue args) - { - string cmd = args.getString("subcmd"); - switch (cmd) - { - case "update": - return updateImportPaths().toJSON(); - case "upgrade": - upgrade(); - break; - case "list:dep": - return dependencies.toJSON(); - case "list:import": - return importPaths.toJSON(); - case "list:string-import": - return stringImportPaths.toJSON(); - case "list:configurations": - return configurations.toJSON(); - case "set:configuration": - return setConfiguration(args.getString("configuration")).toJSON(); - case "get:configuration": - return configuration.toJSON(); - case "set:build-type": - return setBuildType(args.getString("build-type")).toJSON(); - case "get:build-type": - return buildType.toJSON(); - case "set:compiler": - return setCompiler(args.getString("compiler")).toJSON(); - case "get:compiler": - return compiler.toJSON(); - case "get:name": - return dub.projectName.toJSON(); - case "get:path": - return dub.projectPath.toJSON(); - default: - throw new Exception("Unknown command: '" ~ cmd ~ "'"); - } - return JSONValue(null); - } +@arguments("subcmd", "get:name") +string name() @property +{ + return _dub.projectName; +} -private: - Dub _dub; - Path _cwd; - string _configuration; - string _buildType = "debug"; - string _cwdStr; - BuildSettings _settings; - Compiler _compiler; - BuildPlatform _platform; - string[] _importPaths, _stringImportPaths; +@arguments("subcmd", "get:path") +auto path() @property +{ + return _dub.projectPath; +} + +private __gshared: + +Dub _dub; +Path _cwd; +string _configuration; +string _buildType = "debug"; +string _cwdStr; +BuildSettings _settings; +Compiler _compiler; +BuildPlatform _platform; +string[] _importPaths, _stringImportPaths; + +struct DubPackageInfo +{ + string[string] dependencies; + string ver; + string name; } DubPackageInfo getInfo(in Package dep) @@ -249,9 +241,4 @@ auto listDependencies(Project project) dependencies ~= getInfo(dep); } return dependencies; -} - -shared static this() -{ - components["dub"] = new DubComponent(); -} +} \ No newline at end of file diff --git a/test/tester.d b/test/tester.d index 4976732..8cfc935 100644 --- a/test/tester.d +++ b/test/tester.d @@ -68,7 +68,7 @@ void main(string[] args) assert(dataBuffer.length == 4, "Invalid buffer data"); int receivedID = bigEndianToNative!int(dataBuffer[0 .. 4]); - assert(requestID == receivedID, "Processed invalid id!"); + assert(requestID == receivedID, "Processed invalid id! Got those bytes instead: " ~ cast(string) dataBuffer); dataBuffer.length = length - 4; dataBuffer = pipes.stdout.rawRead(dataBuffer);