diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0cc7023 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "msgpack-d"] + path = msgpack-d + url = https://github.com/msgpack/msgpack-d.git diff --git a/autocomplete.d b/autocomplete.d new file mode 100644 index 0000000..59fa457 --- /dev/null +++ b/autocomplete.d @@ -0,0 +1,68 @@ +module autocomplete; + +import std.array; +import std.stdio; +import std.d.lexer; +import std.d.parser; +import std.d.ast; +import std.range; + +import messages; +import importutils; +import constants; + +AutocompleteResponse complete(AutocompleteRequest request, string[] importPaths) +{ + writeln("Got a completion request"); + AutocompleteResponse response; + + LexerConfig config; + auto tokens = request.sourceCode.byToken(config); + auto tokenArray = tokens.array(); + auto sortedTokens = assumeSorted(tokenArray); + + auto beforeTokens = sortedTokens.lowerBound(cast(size_t) request.cursorPosition); + if (beforeTokens[$ - 1] == TokenType.lParen) + { + if (beforeTokens[$ - 2] == TokenType.traits) + { + response.completionType = CompletionType.identifiers; + for (size_t i = 0; i < traits.length; i++) + { + response.completions ~= traits[i]; + response.completionKinds ~= CompletionKind.keyword; + } + } + else if (beforeTokens[$ - 2] == TokenType.scope_) + { + response.completionType = CompletionType.identifiers; + for (size_t i = 0; i < scopes.length; i++) + { + response.completions ~= scopes[i]; + response.completionKinds ~= CompletionKind.keyword; + } + } + else if (beforeTokens[$ - 2] == TokenType.version_) + { + response.completionType = CompletionType.identifiers; + for (size_t i = 0; i < versions.length; i++) + { + response.completions ~= versions[i]; + response.completionKinds ~= CompletionKind.keyword; + } + } + } + else + { + Module mod = parseModule(tokenArray, request.fileName, &messageFunction); + + writeln("Resolved imports: ", getImportedFiles(mod, importPaths ~ request.importPaths)); + } + + return response; +} + +void messageFunction(string fileName, int line, int column, string message) +{ + // does nothing +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..ab4a88b --- /dev/null +++ b/build.sh @@ -0,0 +1,2 @@ +dmd client.d messages.d msgpack-d/src/msgpack.d -Imsgpack-d/src -ofdcd-client +dmd server.d messages.d constants.d importutils.d autocomplete.d ../dscanner/std/d/ast.d ../dscanner/std/d/parser.d ../dscanner/std/d/lexer.d ../dscanner/std/d/entities.d msgpack-d/src/msgpack.d -Imsgpack-d/src -I../dscanner/ -ofdcd-server diff --git a/client.d b/client.d new file mode 100644 index 0000000..e472cf9 --- /dev/null +++ b/client.d @@ -0,0 +1,123 @@ +module client; + +import std.socket; +import std.stdio; +import std.getopt; +import std.array; + +import msgpack; +import messages; + +int main(string[] args) +{ + int cursorPos = -1; + string[] importPaths; + ushort port = 9090; + bool help; + + try + { + getopt(args, "cursorPos|c", &cursorPos, "I", &importPaths, + "port|p", &port, "help|h", &help); + } + catch (Exception e) + { + stderr.writeln(e.msg); + } + + if (help) + { + printHelp(args[0]); + return 0; + } + + // cursor position is a required argument + if (cursorPos == -1) + { + printHelp(args[0]); + return 1; + } + + // Read in the source + bool usingStdin = args.length <= 1; + string fileName = usingStdin ? "stdin" : args[1]; + File f = usingStdin ? stdin : File(args[1]); + ubyte[] sourceCode = usingStdin ? cast(ubyte[]) [] : uninitializedArray!(ubyte[])(f.size); + f.rawRead(sourceCode); + + // Create message + AutocompleteRequest request; + request.fileName = fileName; + request.importPaths = importPaths; + request.sourceCode = sourceCode; + request.cursorPosition = cursorPos; + ubyte[] message = msgpack.pack(request); + + // Send message to server + auto socket = new TcpSocket(AddressFamily.INET); + scope (exit) socket.close(); + socket.connect(new InternetAddress("127.0.0.1", port)); + socket.blocking = true; + stderr.writeln("Sending ", message.length, " bytes"); + auto bytesSent = socket.send(message); + stderr.writeln(bytesSent, " bytes sent"); + + // Get response and write it out + ubyte[1024 * 16] buffer; + auto bytesReceived = socket.receive(buffer); + if (bytesReceived == Socket.ERROR) + { + return 1; + } + + AutocompleteResponse response; + msgpack.unpack(buffer[0..bytesReceived], response); + + writeln(response.completionType); + if (response.completionType == CompletionType.identifiers) + { + for (size_t i = 0; i < response.completions.length; i++) + { + writefln("%s\t%s", response.completions[i], response.completionKinds[i]); + } + } + else + { + foreach (completion; response.completions) + { + writeln(completion); + } + } + stderr.writeln("completed"); + return 0; +} + +void printHelp(string programName) +{ + writefln( +` + Usage: %1$s --cursorPos NUMBER [options] [FILENAME] + or: %1$s -cNUMBER [options] [FILENAME] + + A file name is optional. If it is given, autocomplete information will be + given for the file specified. If it is missing, input will be read from + stdin instead. + + Source code is assumed to be UTF-8 encoded. + +Mandatory Arguments: + --cursorPos | -c position + Provides auto-completion at the given cursor position. The cursor + position is measured in bytes from the beginning of the source code. + +Options: + --help | -h + Displays this help message + + -IPATH + Includes PATH in the listing of paths that are searched for file imports + + --port PORTNUMBER | -pPORTNUMBER + Uses PORTNUMBER to communicate with the server instead of the default + port 9091.`, programName); +} diff --git a/constants.d b/constants.d new file mode 100644 index 0000000..222331d --- /dev/null +++ b/constants.d @@ -0,0 +1,130 @@ +module constants; + +immutable string[] traits = [ + "allMembers", + "classInstanceSize", + "compiles" + "derivedMembers", + "getAttributes", + "getMember", + "getOverloads", + "getProtection", + "getVirtualFunctions", + "getVirtualMethods", + "hasMember", + "identifier", + "isAbstractClass", + "isAbstractFunction", + "isArithmetic", + "isAssociativeArray", + "isFinalClass", + "isFinalFunction", + "isFloating", + "isIntegral", + "isLazy", + "isNested", + "isOut", + "isPOD", + "isRef", + "isSame", + "isScalar", + "isStaticArray", + "isStaticFunction", + "isUnsigned", + "isVirtualFunction", + "isVirtualMethod", + "parent" +]; + +/** + * Scope conditions + */ +immutable string[] scopes = [ + "exit", + "failure", + "success" +]; + +/** + * Predefined version identifiers + */ +immutable string[] versions = [ + "AArch64", + "AIX", + "all", + "Alpha", + "Alpha_HardFloat", + "Alpha_SoftFloat", + "Android", + "ARM", + "ARM_HardFloat", + "ARM_SoftFloat", + "ARM_SoftFP", + "ARM_Thumb", + "assert", + "BigEndian", + "BSD", + "Cygwin", + "D_Coverage", + "D_Ddoc", + "D_HardFloat", + "DigitalMars", + "D_InlineAsm_X86", + "D_InlineAsm_X86_64", + "D_LP64", + "D_NoBoundsChecks", + "D_PIC", + "DragonFlyBSD", + "D_SIMD", + "D_SoftFloat", + "D_Version2", + "D_X32", + "FreeBSD", + "GNU", + "Haiku", + "HPPA", + "HPPA64", + "Hurd", + "IA64", + "LDC", + "linux", + "LittleEndian", + "MIPS32", + "MIPS64", + "MIPS_EABI", + "MIPS_HardFloat", + "MIPS_N32", + "MIPS_N64", + "MIPS_O32", + "MIPS_O64", + "MIPS_SoftFloat", + "NetBSD", + "none", + "OpenBSD", + "OSX", + "Posix", + "PPC", + "PPC64", + "PPC_HardFloat", + "PPC_SoftFloat", + "S390", + "S390X", + "SDC", + "SH", + "SH64", + "SkyOS", + "Solaris", + "SPARC", + "SPARC64", + "SPARC_HardFloat", + "SPARC_SoftFloat", + "SPARC_V8Plus", + "SysV3", + "SysV4", + "unittest", + "Win32", + "Win64", + "Windows", + "X86", + "X86_64", +]; diff --git a/importutils.d b/importutils.d new file mode 100644 index 0000000..caa9a2f --- /dev/null +++ b/importutils.d @@ -0,0 +1,71 @@ +module importutils; + +import std.file; +import std.d.parser; +import std.d.ast; +import std.stdio; + +class ImportCollector : ASTVisitor +{ + alias ASTVisitor.visit visit; + + override void visit(ImportDeclaration dec) + { + foreach (singleImport; dec.singleImports) + { + imports ~= flattenIdentifierChain(singleImport.identifierChain); + } + if (dec.importBindings !is null) + { + imports ~= flattenIdentifierChain(dec.importBindings.singleImport.identifierChain); + } + } + + private static string flattenIdentifierChain(IdentifierChain chain) + { + string rVal; + bool first = true; + foreach (identifier; chain.identifiers) + { + if (!first) + rVal ~= "/"; + rVal ~= identifier.value; + first = false; + } + rVal ~= ".d"; + return rVal; + } + + string[] imports; +} + +string[] getImportedFiles(Module mod, string[] importPaths) +{ + auto collector = new ImportCollector; + collector.visit(mod); + string[] importedFiles; + foreach (imp; collector.imports) + { + bool found = false; + foreach (path; importPaths) + { + string filePath = path ~ "/" ~ imp; + if (filePath.exists()) + { + importedFiles ~= filePath; + found = true; + break; + } + filePath ~= "i"; // check for x.di if x.d isn't found + if (filePath.exists()) + { + importedFiles ~= filePath; + found = true; + break; + } + } + if (!found) + writeln("Could not locate ", imp); + } + return importedFiles; +} diff --git a/messages.d b/messages.d new file mode 100644 index 0000000..fa172a0 --- /dev/null +++ b/messages.d @@ -0,0 +1,102 @@ +module messages; + +/** + * Identifies the kind of the item in an identifier completion list + */ +enum CompletionKind : char +{ + /// class names + className = 'c', + + /// interface names + interfaceName = 'i', + + /// structure names + structName = 's', + + /// variable name + variableName = 'v', + + /// member variable + memberVariableName = 'm', + + /// keyword, built-in version, scope statement + keyword = 'k', + + /// function or method + functionName = 'f', + + /// enum name + enumName = 'g', + + /// package name + packageName = 'P', + + // module name + moduleName = 'M' +} + +/** + * The type of completion list being returned + */ +enum CompletionType : string +{ + /** + * The completion list contains a listing of identifier/kind pairs. + */ + identifiers = "identifiers", + + /** + * The auto-completion list consists of a listing of functions and their + * parameters. + */ + calltips = "calltips" +} + +/** + * Autocompletion request message + */ +struct AutocompleteRequest +{ + /** + * File name used for error reporting + */ + string fileName; + + /** + * Paths to be searched for import files + */ + string[] importPaths; + + /** + * The source code to auto complete + */ + ubyte[] sourceCode; + + /** + * The cursor position + */ + int cursorPosition; +} + +/** + * Autocompletion response message + */ +struct AutocompleteResponse +{ + /** + * The autocompletion type. (Parameters or identifier) + */ + string completionType; + + /** + * The completions + */ + string[] completions; + + /** + * The kinds of the items in the completions array. Will be empty if the + * completion type is a function argument list. + */ + char[] completionKinds; +} diff --git a/msgpack-d b/msgpack-d new file mode 160000 index 0000000..40c797c --- /dev/null +++ b/msgpack-d @@ -0,0 +1 @@ +Subproject commit 40c797cb8ae3eb56cf88399ef3532fc29abd238a diff --git a/server.d b/server.d new file mode 100644 index 0000000..2202af3 --- /dev/null +++ b/server.d @@ -0,0 +1,69 @@ +module server; + +import std.socket; +import std.stdio; +import std.getopt; + +import msgpack; + +import messages; +import autocomplete; + +void main(string[] args) +{ + ushort port = 9090; + bool help; + string[] importPaths; + + try + { + getopt(args, "port|p", &port, "I", &importPaths, "help|h", &help); + } + catch (Exception e) + { + stderr.writeln(e.msg); + } + + auto socket = new TcpSocket(AddressFamily.INET); + socket.blocking = true; + socket.bind(new InternetAddress("127.0.0.1", port)); + socket.listen(0); + scope (exit) socket.close(); + ubyte[1024 * 1024 * 4] buffer = void; // 4 megabytes should be enough for anybody... + while (true) + { + auto s = socket.accept(); + s.blocking = true; + scope (exit) s.close(); + ptrdiff_t bytesReceived = s.receive(buffer); + + if (bytesReceived == Socket.ERROR) + { + writeln("Socket recieve failed"); + break; + } + else + { + AutocompleteRequest request; + writeln("Unpacking ", bytesReceived, "/", buffer.length, " bytes into a request"); + msgpack.unpack(buffer[0 .. bytesReceived], request); + AutocompleteResponse response = complete(request, importPaths); + ubyte[] responseBytes = msgpack.pack(response); + assert(s.send(responseBytes) == responseBytes.length); + } + } +} + +void printHelp(string programName) +{ + writefln( +` + Usage: %s options + +options: + -I path + Includes path in the listing of paths that are searched for file imports + + --port PORTNUMBER | -pPORTNUMBER + Listens on PORTNUMBER instead of the default port 9091.`, programName); +}