// Copyright Brian Schott (Sir Alaran) 2012. // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE_1_0.txt or copy at // http://www.boost.org/LICENSE_1_0.txt) module autocomplete; import std.range; import std.algorithm; import std.array; import std.conv; import std.stdio; import std.typecons; import std.path; import std.file; import parser; import langutils; import types; import tokenizer; immutable string[] versions = ["AIX", "all", "Alpha", "ARM", "BigEndian", "BSD", "Cygwin", "D_Coverage", "D_Ddoc", "DigitalMars", "D_InlineAsm_X86", "D_InlineAsm_X86_64", "D_LP64", "D_NET", "D_PIC", "D_Version2", "FreeBSD", "GNU", "HPPA", "HPPA64", "Hurd", "IA64", "LDC", "linux", "LittleEndian", "MinGW", "MIPS", "MIPS64", "none", "OpenBSD", "OSX", "Posix", "PPC", "PPC64", "S390", "S390X", "SDC", "SH", "SH64", "SkyOS", "Solaris", "SPARC", "SPARC64", "SysV3", "SysV4", "unittest", "Win32", "Win64", "Windows", "X86", "X86_64" ]; immutable string[] scopes = ["exit", "failure", "success"]; /** * Returns: indicies into the token array */ size_t findEndOfExpression(const Token[] tokens, const size_t index) out (result) { assert (result < tokens.length); assert (result >= index); } body { size_t i = index; loop: while (i < tokens.length) { switch (tokens[i].type) { case TokenType.Return: case TokenType.New: case TokenType.Delete: case TokenType.Comma: case TokenType.RBrace: case TokenType.RParen: case TokenType.RBracket: case TokenType.Semicolon: break loop; case TokenType.LParen: skipParens(tokens, i); break; case TokenType.LBrace: skipBraces(tokens, i); break; case TokenType.LBracket: skipBrackets(tokens, i); break; default: ++i; break; } } return i; } size_t findBeginningOfExpression(const Token[] tokens, const size_t index) in { assert (index < tokens.length); assert (tokens.length > 0); } out (result) { import std.string; assert (result < tokens.length); assert (result <= index, format("%d, %d", result, index)); } body { size_t i = index; loop: while (i < tokens.length) { switch (tokens[i].type) { case TokenType.Return: case TokenType.New: case TokenType.Case: case TokenType.Assign: case TokenType.Delete: case TokenType.LBrace: case TokenType.LParen: case TokenType.Equals: case TokenType.Plus: case TokenType.Import: case TokenType.LBracket: case TokenType.Comma: case TokenType.Semicolon: case TokenType.RBrace: return i + 1; case TokenType.RParen: if (i == 0) break loop; skipParens(tokens, i); break; case TokenType.RBracket: if (i == 0) break loop; skipBrackets(tokens, i); break; default: if (i == 0) break loop; i--; break; } } return i + 1; } const(Token)[] splitCallChain(const(Token)[] tokens) { auto app = appender!(Token[])(); size_t i = 0; while (i < tokens.length) { app.put(tokens[i++]); while (i < tokens.length && tokens[i] == TokenType.LParen) skipParens(tokens, i); while (i < tokens.length && tokens[i] == TokenType.LBracket) skipBrackets(tokens, i); while (i < tokens.length && tokens[i] == TokenType.Dot) ++i; } return app.data; } unittest { auto code = `a.b[10].c("grcl").x`; auto tokens = tokenize(code); assert (splitCallChain(tokens) == ["a", "b", "c", "x"]); } struct AutoComplete { this(const (Token)[] tokens, CompletionContext context) { this.tokens = tokens; this.context = context; } string getTypeOfExpression(const(Token)[] expression, const Token[] tokens, size_t cursor) { stderr.writeln("getting type of ", expression); if (expression.length == 0) return "void"; auto type = typeOfVariable(expression[0], cursor); if (type is null) return "void"; size_t index = 1; while (index < expression.length) { const Tuple!(string, string)[string] typeMap = context.getMembersOfType( type); const Tuple!(string, string)* memberType = expression[index].value in typeMap; if (memberType is null) return "void"; else type = (*memberType)[0]; index++; } return type; } string typeOfVariable(Token symbol, size_t cursor) { // int is of type int, double of type double, and so on if (symbol.value in typeProperties) return symbol.value; string tokenType = getTypeFromToken(symbol); if (tokenType !is null) return tokenType; if (context.getMembersOfType(symbol.value)) return symbol.value; // Arbitrarily define the depth of the cursor position as zero // iterate backwards through the code to try to find the variable int depth = 0; auto preceedingTokens = assumeSorted(tokens).lowerBound(cursor); auto index = preceedingTokens.length - 1; while (true) { if (preceedingTokens[index] == TokenType.LBrace) --depth; else if (preceedingTokens[index] == TokenType.RBrace) ++depth; else if (depth <= 0 && preceedingTokens[index].value == symbol) { // Found the symbol, now determine if it was declared here. auto p = preceedingTokens[index - 1]; if ((p == TokenType.Auto || p == TokenType.Immutable || p == TokenType.Const) && preceedingTokens[index + 1] == TokenType.Assign) { // Try to determine the type of a variable declared as "auto" return getTypeOfExpression( tokens[index + 2 .. findEndOfExpression(tokens, index + 2)], tokens, cursor); } else if (p == TokenType.Identifier || (p.type > TokenType.TYPES_BEGIN && p.type < TokenType.TYPES_END)) { // Handle simple cases like "int a;" or "Someclass instance;" return p.value; } else if (p == TokenType.RBracket || p == TokenType.RParen) { return combineTokens(tokens[findBeginningOfExpression(tokens, index) .. index]); } } if (index == 0) break; else --index; } // Find all struct or class bodies that we're in. // Check for the symbol in those class/struct/interface bodies // if match is found, return it auto structs = context.getStructsContaining(cursor); if (symbol == "this" && structs.length > 0) { return minCount!("a.bodyStart > b.bodyStart")(structs)[0].name; } foreach (s; structs) { auto t = s.getMemberType(symbol.value); if (t !is null) return t; } return "void"; } string symbolAt(size_t cursor) const { auto r = assumeSorted(tokens).lowerBound(cursor)[$ - 1]; if (r.value.length + r.startIndex > cursor) return r.value; else return null; } string parenComplete(size_t cursor) { auto index = assumeSorted(tokens).lowerBound(cursor).length - 2; Token t = tokens[index]; switch (tokens[index].type) { case TokenType.Version: return "completions\n" ~ to!string(join(map!`a ~ " k"`(versions), "\n").array()); case TokenType.Scope: return "completions\n" ~ to!string(join(map!`a ~ " k"`(scopes), "\n").array()); case TokenType.If: case TokenType.Cast: case TokenType.While: case TokenType.For: case TokenType.Foreach: case TokenType.Switch: return ""; default: size_t startIndex = findBeginningOfExpression(tokens, index); auto callChain = splitCallChain(tokens[startIndex .. index + 1]); auto expressionType = getTypeOfExpression( callChain[0 .. $ - 1], tokens, cursor); return "calltips\n" ~ to!string(context.getCallTipsFor(expressionType, callChain[$ - 1].value, cursor).join("\n").array()); } } string dotComplete(size_t cursor) { stderr.writeln("dotComplete"); auto index = assumeSorted(tokens).lowerBound(cursor).length - 1; Token t = tokens[index]; size_t startIndex = findBeginningOfExpression(tokens, index); if (startIndex - 1 < tokens.length && tokens[startIndex - 1] == TokenType.Import) { return importComplete(splitCallChain(tokens[startIndex .. index])); } auto expressionType = getTypeOfExpression( splitCallChain(tokens[startIndex .. index]), tokens, cursor); stderr.writeln("expression type is ", expressionType); // Complete pointers and references the same way if (expressionType[$ - 1] == '*') expressionType = expressionType[0 .. $ - 1]; const Tuple!(string, string)[string] typeMap = context.getMembersOfType( expressionType); if (typeMap is null) return ""; auto app = appender!(string[])(); foreach (k, t; typeMap) app.put(k ~ " " ~ t[1]); return to!string(array(join(sort!("a.toLower() < b.toLower()")(app.data), "\n"))); } string importComplete(const(Token)[] tokens) { stderr.writeln("importComplete"); auto app = appender!(string[])(); string part = to!string(map!"a.value.dup"(tokens).join("/").array()); foreach (path; context.importDirectories) { stderr.writeln("Searching for ", path, "/", part); if (!exists(buildPath(path, part))) continue; stderr.writeln("found it"); foreach (DirEntry dirEntry; dirEntries(buildPath(path, part), SpanMode.shallow)) { if (dirEntry.isDir) app.put(baseName(dirEntry.name) ~ " P"); else if (dirEntry.name.endsWith(".d", ".di")) app.put(stripExtension(baseName(dirEntry.name)) ~ " M"); } } return to!string(sort!("a.toLower() < b.toLower()")(app.data).join("\n").array()); } const(Token)[] tokens; CompletionContext context; } unittest { auto code = q{ struct TestStruct { int a; int b; } TestStruct ts; ts.a. }; auto tokens = tokenize(code); auto mod = parseModule(tokens); auto context = new CompletionContext(mod); auto completion = AutoComplete(tokens, context); assert (completion.getTypeOfExpression(splitCallChain(tokens[13 .. 16]), tokens, 56) == "int"); }