From 6c69d8af49e2e5bcfc628835efc21d1a3bcb48f7 Mon Sep 17 00:00:00 2001 From: Hackerpilot Date: Tue, 16 Jul 2013 23:57:41 -0700 Subject: [PATCH] Can sometimes autocomplete version, scope, and __traits --- .gitmodules | 3 ++ autocomplete.d | 68 ++++++++++++++++++++++++++ build.sh | 2 + client.d | 123 ++++++++++++++++++++++++++++++++++++++++++++++ constants.d | 130 +++++++++++++++++++++++++++++++++++++++++++++++++ importutils.d | 71 +++++++++++++++++++++++++++ messages.d | 102 ++++++++++++++++++++++++++++++++++++++ msgpack-d | 1 + server.d | 69 ++++++++++++++++++++++++++ 9 files changed, 569 insertions(+) create mode 100644 .gitmodules create mode 100644 autocomplete.d create mode 100755 build.sh create mode 100644 client.d create mode 100644 constants.d create mode 100644 importutils.d create mode 100644 messages.d create mode 160000 msgpack-d create mode 100644 server.d 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); +}