diff --git a/codecompletion/CMakeLists.txt b/codecompletion/CMakeLists.txt index 9e378dc..bc3c971 100644 --- a/codecompletion/CMakeLists.txt +++ b/codecompletion/CMakeLists.txt @@ -1,30 +1,31 @@ include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/duchain ${KDEVPGQT_INCLUDE_DIR} ) set(completion_SRCS model.cpp worker.cpp context.cpp items/completionitem.cpp items/functionitem.cpp items/importcompletionitem.cpp completiondebug.cpp + typematch.cpp ) add_library(kdevgocompletion SHARED ${completion_SRCS}) generate_export_header(kdevgocompletion BASE_NAME kdevgocompletion EXPORT_MACRO_NAME KDEVGOCOMPLETION_EXPORT) target_link_libraries(kdevgocompletion LINK_PRIVATE KDev::Language KDev::Interfaces KDev::Project kdevgoduchain kdevgoparser ) install(TARGETS kdevgocompletion DESTINATION ${INSTALL_TARGETS_DEFAULT_ARGS}) add_subdirectory(tests) diff --git a/codecompletion/context.cpp b/codecompletion/context.cpp index 864671b..5f27431 100644 --- a/codecompletion/context.cpp +++ b/codecompletion/context.cpp @@ -1,531 +1,540 @@ /************************************************************************************* * Copyright (C) 2014 by Pavel Petrushkov * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * *************************************************************************************/ //Completion code is mostly based on KDevelop QmlJS plugin which should be referenced for more details amd comments #include "context.h" #include #include #include #include "parser/golexer.h" #include "parser/goparser.h" #include "expressionvisitor.h" #include "types/gostructuretype.h" #include "items/completionitem.h" #include "items/functionitem.h" #include "items/importcompletionitem.h" #include "helper.h" #include "completiondebug.h" using namespace KDevelop; namespace go { CodeCompletionContext::CodeCompletionContext(const KDevelop::DUContextPointer& context, const QString& text, const KDevelop::CursorInRevision& position, int depth): KDevelop::CodeCompletionContext(context, extractLastExpression(text), position, depth), m_fullText(text) { } bool CodeCompletionContext::isImportAndMemberCompletion() { int pos = m_text.size() - 1; QChar lastChar; do { lastChar = m_text.size() > 0 ? m_text.at(pos) : QLatin1Char('\0'); --pos; } while((lastChar == QLatin1Char(' ') || lastChar == QLatin1Char('\n') || lastChar == QLatin1Char('\t')) && pos >= 0); return lastChar == QLatin1Char('.'); } QList< CompletionTreeItemPointer > CodeCompletionContext::completionItems(bool& abort, bool fullCompletion) { qCDebug(COMPLETION) << m_text; QList items; //We shouldn't need anything before last semicolon (previous statements) if(m_text.lastIndexOf(';') != -1) m_text = m_text.mid(m_text.lastIndexOf(';')); //"import" + [optional package alias] + [opening double quote] + [cursor at EOL] if(m_text.contains(QRegExp("import[^\"]*\"[^\"]*$"))) { items << importCompletion(); return items; } if(isInsideCommentOrString()) return items; setTypeToMatch(); items << functionCallTips(); items << (isImportAndMemberCompletion() ? importAndMemberCompletion() : normalCompletion()); return items; } QList< CompletionTreeItemPointer > CodeCompletionContext::normalCompletion() { //all declarations QList items; DUChainReadLocker lock; auto declarations = m_duContext->allDeclarations(CursorInRevision::invalid(), m_duContext->topContext()); for(const QPair &decl : declarations) { if(decl.first->topContext() != m_duContext->topContext()) continue; if(decl.first->identifier() == globalImportIdentifier() || decl.first->identifier() == globalAliasIdentifier() || decl.first->identifier() == Identifier()) continue; items << itemForDeclaration(decl); } return items; } QList< CompletionTreeItemPointer > CodeCompletionContext::functionCallTips() { QStack stack = expressionStack(m_text); QList items; int depth = 1; bool isTopOfStack = true; while(!stack.empty()) { ExpressionStackEntry entry = stack.pop(); if (entry.startPosition > 0 && m_text.at(entry.startPosition - 1) == QLatin1Char('(')) { DeclarationPointer function = lastDeclaration(m_text.left(entry.startPosition - 1)); if(function && fastCast(function->abstractType().constData())) { FunctionCompletionItem* item = new FunctionCompletionItem(function, depth, entry.commas); depth++; items << CompletionTreeItemPointer(item); - if(isTopOfStack && !m_typeToMatch) + if(isTopOfStack && !m_typeMatch.singleType()) { GoFunctionType::Ptr ftype(fastCast(function->abstractType().constData())); auto args = ftype->arguments(); if(args.count() != 0) { int argument = entry.commas >= args.count() ? args.count()-1 : entry.commas; - m_typeToMatch = args.at(argument); + m_typeMatch.setSingleType(args.at(argument)); } } } } isTopOfStack = false; } return items; } void CodeCompletionContext::setTypeToMatch() { QStack stack = expressionStack(m_text); if(stack.empty()) { return; } ExpressionStackEntry entry = stack.pop(); if(entry.operatorStart <= entry.startPosition) { return; } auto operatorText = m_text.mid(entry.operatorStart, entry.operatorEnd-entry.operatorStart); if(operatorText == "=") { auto leftText = m_text.mid(entry.startPosition, entry.operatorStart - entry.startPosition); auto rightText = m_text.mid(entry.operatorEnd); // On left side we have comma-separated declarations while on right side we can have function calls // whose can have extra commas. Since entry.commas contains *all* commas in *current* expression, // (e.g. commas inside function calls will not be counted) use it and amount of commas on left side to determine // amount of commas on right side. auto pos = entry.commas - leftText.count(','); auto declarations = leftText.split(','); + if(pos == 0 && declarations.size() > 1) + { + QList types; + for(auto i = 0; i < declarations.size(); ++i) + { + types.append(lastType(declarations[i])); + } + m_typeMatch.setMultipleTypes(types); + } if(declarations.size() > pos) { if(auto type = lastType(declarations[pos])) { - m_typeToMatch = type; + m_typeMatch.setSingleType(type); } } } //don't show matching in var declarations e.g. "a := b" //and expression lists e.g. "a(), b() else if(operatorText != "," && operatorText != ":=") { if(auto type = lastType(m_text.left(entry.operatorStart))) { - m_typeToMatch = type; + m_typeMatch.setSingleType(type); } } } QList CodeCompletionContext::getImportableDeclarations(Declaration *sourceDeclaration) { QList items; auto declarations = getDeclarations(sourceDeclaration->qualifiedIdentifier(), m_duContext.data()); for(Declaration* declaration : declarations) { DUContext* context = declaration->internalContext(); if(!context) continue; auto innerDeclarations = context->allDeclarations(CursorInRevision::invalid(), sourceDeclaration->topContext(), false); for(const QPair innerDeclaration : innerDeclarations) { if(innerDeclaration.first == declaration) continue; QualifiedIdentifier fullname = innerDeclaration.first->qualifiedIdentifier(); Identifier identifier = fullname.last(); if(identifier.toString().size() <= 0) continue; //import only declarations that start with capital letter(Go language rule) if(m_duContext->topContext() != declaration->topContext()) if(!identifier.toString().at(0).isLetter() || (identifier.toString().at(0) != identifier.toString().at(0).toUpper())) continue; items << itemForDeclaration(innerDeclaration); } } return items; } QList< CompletionTreeItemPointer > CodeCompletionContext::importAndMemberCompletion() { QList items; int pos = m_text.size(); QChar lastChar; do { --pos; lastChar = m_text.size() > 0 ? m_text.at(pos) : QLatin1Char('\0'); } while(lastChar != QLatin1Char('.') && lastChar != QLatin1Char('\0')); AbstractType::Ptr type = lastType(m_text.left(pos)); if(type) { if(auto ptype = fastCast(type.constData())) { DUChainReadLocker lock; if(ptype->baseType()) { type = ptype->baseType(); } } if(auto structure = fastCast(type.constData())) { DUChainReadLocker lock; Declaration* declaration = structure->declaration(m_duContext->topContext()); if(declaration) { items << getImportableDeclarations(declaration); } } if(auto structure = dynamic_cast(type.data())) { DUContext* context = structure->context(); DUChainReadLocker lock; auto declarations = context->allDeclarations(CursorInRevision::invalid(), m_duContext->topContext(), false); lock.unlock(); for(const QPair &decl : declarations) { items << itemForDeclaration(decl); } } } return items; } QList CodeCompletionContext::importCompletion() { auto searchPaths = Helper::getSearchPaths(); QList items; QString fullPath = m_text.mid(m_text.lastIndexOf('"')+1); //import "parentPackage/childPackage" QStringList pathChain = fullPath.split('/', QString::SkipEmptyParts); qCDebug(COMPLETION) << pathChain; for(const QString& path : searchPaths) { QDir dir(path); if(dir.exists()) { bool isValid = true; for(const QString& nextDir : pathChain) { isValid = dir.cd(nextDir); if(!isValid) break; } if(!dir.exists() || !isValid) continue; for(const QString& package : dir.entryList(QDir::Dirs)) { if(package.startsWith('.')) continue; items << CompletionTreeItemPointer(new ImportCompletionItem(package)); } } } return items; } QStack< CodeCompletionContext::ExpressionStackEntry > CodeCompletionContext::expressionStack(const QString& expression) { //for details see similar function in QmlJS KDevelop plugin QStack stack; QByteArray expr(expression.toUtf8()); KDevPG::QByteArrayIterator iter(expr); Lexer lexer(iter); bool atEnd=false; ExpressionStackEntry entry; entry.startPosition = 0; entry.operatorStart = 0; entry.operatorEnd = 0; entry.commas = 0; stack.push(entry); qint64 line, lineEnd, column, columnEnd; while(!atEnd) { KDevPG::Token token(lexer.read()); switch(token.kind) { case Parser::Token_EOF: atEnd=true; break; case Parser::Token_LBRACE: case Parser::Token_LBRACKET: case Parser::Token_LPAREN: qint64 sline, scolumn; lexer.locationTable()->positionAt(token.begin, &sline, &scolumn); entry.startPosition = scolumn+1; entry.operatorStart = entry.startPosition; entry.operatorEnd = entry.startPosition; entry.commas = 0; stack.push(entry); break; case Parser::Token_RBRACE: case Parser::Token_RBRACKET: case Parser::Token_RPAREN: if (stack.count() > 1) { stack.pop(); } break; case Parser::Token_IDENT: //temporary hack to allow completion in variable declarations //two identifiers in a row is not possible? if(lexer.size() > 1 && lexer.at(lexer.index()-2).kind == Parser::Token_IDENT) { lexer.locationTable()->positionAt(lexer.at(lexer.index()-2).begin, &line, &column); lexer.locationTable()->positionAt(lexer.at(lexer.index()-2).end+1, &lineEnd, &columnEnd); stack.top().operatorStart = column; stack.top().operatorEnd = columnEnd; } break; case Parser::Token_DOT: break; case Parser::Token_COMMA: stack.top().commas++; break; default: // The last operator of every sub-expression is stored on the stack // so that "A = foo." can know that attributes of foo having the same // type as A should be highlighted. qCDebug(COMPLETION) << token.kind; lexer.locationTable()->positionAt(token.begin, &line, &column); lexer.locationTable()->positionAt(token.end+1, &lineEnd, &columnEnd); stack.top().operatorStart = column; stack.top().operatorEnd = columnEnd; } } return stack; } AbstractType::Ptr CodeCompletionContext::lastType(const QString& expression) { QStack stack = expressionStack(expression); QString lastExpression(expression.mid(stack.top().operatorEnd)); qCDebug(COMPLETION) << lastExpression; ParseSession session(lastExpression.toUtf8(), 0, false); ExpressionAst* expressionAst; if(!session.parseExpression(&expressionAst)) return AbstractType::Ptr(); ExpressionVisitor expVisitor(&session, this->m_duContext.data()); expVisitor.visitExpression(expressionAst); if(expVisitor.lastTypes().size() != 0) { AbstractType::Ptr type = expVisitor.lastTypes().first(); return type; } return AbstractType::Ptr(); } DeclarationPointer CodeCompletionContext::lastDeclaration(const QString& expression) { QStack stack = expressionStack(expression); QString lastExpression(expression.mid(stack.top().operatorEnd)); qCDebug(COMPLETION) << lastExpression; ParseSession session(lastExpression.toUtf8(), 0, false); ExpressionAst* expressionAst; if(!session.parseExpression(&expressionAst)) return DeclarationPointer(); ExpressionVisitor expVisitor(&session, this->m_duContext.data()); expVisitor.visitExpression(expressionAst); if(expVisitor.lastDeclaration()) return expVisitor.lastDeclaration(); return DeclarationPointer(); } CompletionTreeItemPointer CodeCompletionContext::itemForDeclaration(QPair declaration) { if(declaration.first->isFunctionDeclaration()) return CompletionTreeItemPointer(new FunctionCompletionItem(DeclarationPointer(declaration.first))); return CompletionTreeItemPointer(new go::CompletionItem(DeclarationPointer(declaration.first), QExplicitlySharedDataPointer(), declaration.second)); } bool CodeCompletionContext::isInsideCommentOrString() { bool inLineComment = false; bool inComment = false; bool inQuotes = false; bool inDoubleQuotes = false; bool inBackQuotes = false; QString text = ' ' + m_fullText; for(int index = 0; index < text.size()-1; ++index) { const QChar c = text.at(index); const QChar next = text.at(index + 1); if(inLineComment) { if(c == QLatin1Char('\n')) { inLineComment = false; continue; } } if(inComment) { if(c == QLatin1Char('*') && next == QLatin1Char('/')) { inComment = false; continue; } } else if(inQuotes) { if(c != QLatin1Char('\\') && next == QLatin1Char('\'')) { inQuotes = false; continue; } } else if(inDoubleQuotes) { if(c != QLatin1Char('\\') && next == QLatin1Char('\"')) { inDoubleQuotes = false; continue; } } else if(inBackQuotes) { if(c != QLatin1Char('\\') && next == QLatin1Char('\`')) { inBackQuotes = false; continue; } } else { if(c == QLatin1Char('/') && next == QLatin1Char('/')) inLineComment = true; if(c == QLatin1Char('/') && next == QLatin1Char('*')) inComment = true; if(next == QLatin1Char('\'')) inQuotes = true; if(next == QLatin1Char('\"')) inDoubleQuotes = true; if(next == QLatin1Char('\`')) inBackQuotes = true; } } if(inLineComment || inComment || inQuotes || inDoubleQuotes || inBackQuotes) return true; return false; } QString CodeCompletionContext::extractLastExpression(const QString &str) { QString result; int prevLineEnd = str.lastIndexOf('\n'); if(prevLineEnd != -1) { result = str.mid(prevLineEnd+1); if(prevLineEnd - 1 >= 0) { auto previousText = str.left(prevLineEnd); if(endsWithDot(previousText)) { result = extractLastExpression(previousText) + "\n" + result; } } } else { result = str; } return result; } bool CodeCompletionContext::endsWithDot(const QString &str) { auto dotPos = str.lastIndexOf('.'); if(dotPos == -1) { return false; } for(auto i = dotPos + 1; i < str.size(); ++i) { if(!str[i].isSpace()) { return false; } } return true; } } \ No newline at end of file diff --git a/codecompletion/context.h b/codecompletion/context.h index 5d659cf..25b4e60 100644 --- a/codecompletion/context.h +++ b/codecompletion/context.h @@ -1,90 +1,91 @@ /************************************************************************************* * Copyright (C) 2014 by Pavel Petrushkov * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * *************************************************************************************/ #ifndef GOLANGCOMPLETIONCONTEXT_H #define GOLANGCOMPLETIONCONTEXT_H #include #include +#include "typematch.h" #include "kdevgocompletion_export.h" #include #include namespace go { class KDEVGOCOMPLETION_EXPORT CodeCompletionContext : public KDevelop::CodeCompletionContext { public: CodeCompletionContext(const KDevelop::DUContextPointer& context, const QString& text, const KDevelop::CursorInRevision& position, int depth = 0); QList completionItems(bool& abort, bool fullCompletion = true) override; - KDevelop::AbstractType::Ptr typeToMatch() { return m_typeToMatch; } + TypeMatch typeToMatch() { return m_typeMatch; } private: //See QmlJS plugin completion for details struct ExpressionStackEntry { int startPosition; int operatorStart; int operatorEnd; int commas; }; QStack expressionStack(const QString& expression); KDevelop::AbstractType::Ptr lastType(const QString& expression); KDevelop::DeclarationPointer lastDeclaration(const QString& expression); QList getImportableDeclarations(KDevelop::Declaration *sourceDeclaration); QList importAndMemberCompletion(); QList normalCompletion(); /** * Creates FunctionCallTips and sets m_typeToMatch **/ QList functionCallTips(); void setTypeToMatch(); QList importCompletion(); /** * Return completion item for declaration. **/ KDevelop::CompletionTreeItemPointer itemForDeclaration(QPair declaration); /** * returns true if cursor is in comment and completion is not needed **/ bool isInsideCommentOrString(); bool isImportAndMemberCompletion(); - KDevelop::AbstractType::Ptr m_typeToMatch; + TypeMatch m_typeMatch; QString m_fullText; QString extractLastExpression(const QString &str); bool endsWithDot(const QString &str); }; } #endif \ No newline at end of file diff --git a/codecompletion/items/completionitem.cpp b/codecompletion/items/completionitem.cpp index b16a4b9..4f8a3c7 100644 --- a/codecompletion/items/completionitem.cpp +++ b/codecompletion/items/completionitem.cpp @@ -1,97 +1,127 @@ /************************************************************************************* * Copyright (C) 2014 by Pavel Petrushkov * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * *************************************************************************************/ #include "completionitem.h" #include #include #include #include "context.h" #include "types/gofunctiontype.h" namespace go { CompletionItem::CompletionItem(KDevelop::DeclarationPointer decl, QExplicitlySharedDataPointer< KDevelop::CodeCompletionContext > context, int inheritanceDepth): NormalDeclarationCompletionItem(decl, QExplicitlySharedDataPointer(), 0), m_prefix("") { DUChainReadLocker lock; if(!decl) return; //NormalDeclarationCompletionItem fails to get a meaningful prefix in these cases if(decl->abstractType() && decl->abstractType()->whichType() == KDevelop::AbstractType::TypeFunction) m_prefix = decl->abstractType()->toString(); if(decl->kind() == KDevelop::Declaration::Import || decl->kind() == KDevelop::Declaration::NamespaceAlias) m_prefix = "namespace"; } QVariant CompletionItem::data(const QModelIndex& index, int role, const KDevelop::CodeCompletionModel* model) const { switch(role) { case Qt::DisplayRole: { if (index.column() == CodeCompletionModel::Prefix && m_prefix != "") { return m_prefix; } break; } case CodeCompletionModel::BestMatchesCount: return 5; case CodeCompletionModel::MatchQuality: { if(!declaration()) return QVariant(); //type aliases are actually different types if(declaration()->isTypeAlias()) return QVariant(); - AbstractType::Ptr typeToMatch = static_cast(model->completionContext().data())->typeToMatch(); + auto codeCompletionContext = static_cast(model->completionContext().data()); + if(!codeCompletionContext->typeToMatch().singleType()) + { + return QVariant(); + } + AbstractType::Ptr typeToMatch = codeCompletionContext->typeToMatch().singleType(); if(!typeToMatch) return QVariant(); AbstractType::Ptr declType = declaration()->abstractType(); if (!declType) return QVariant(); //ignore constants typeToMatch->setModifiers(AbstractType::NoModifiers); declType->setModifiers(AbstractType::NoModifiers); if(declType->equals(typeToMatch.constData())) { - return QVariant(10); + return 10; } else if(declType->whichType() == AbstractType::TypeFunction) { GoFunctionType* function = fastCast(declType.constData()); auto args = function->returnArguments(); if(args.size() != 0) { + auto multipleTypesMatch = codeCompletionContext->typeToMatch().multipleTypes(); + if(args.size() == multipleTypesMatch.size()) + { + bool allTypesMatches = true; + for(auto pos = 0; pos < args.size(); ++pos) + { + auto expected = multipleTypesMatch[pos]; + auto actual = args[pos]; + expected->setModifiers(AbstractType::NoModifiers); + actual->setModifiers(AbstractType::NoModifiers); + if(!actual->equals(expected.constData()) && expected->toString() != "_") + { + allTypesMatches = false; + break; + } + } + if(allTypesMatches) + { + return 20; + } + } + AbstractType::Ptr first = args.first(); first->setModifiers(AbstractType::NoModifiers); - if(first->equals(typeToMatch.constData())) - return QVariant(10); + if(args.size() == 1 && first->equals(typeToMatch.constData())) + { + return 10; + } } - }else + } + else { return QVariant(); } } } return NormalDeclarationCompletionItem::data(index, role, model); } } diff --git a/codecompletion/tests/testcompletion.cpp b/codecompletion/tests/testcompletion.cpp index 7ea5115..89f1b20 100644 --- a/codecompletion/tests/testcompletion.cpp +++ b/codecompletion/tests/testcompletion.cpp @@ -1,280 +1,281 @@ /************************************************************************************* * Copyright (C) 2014 by Pavel Petrushkov * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License * * as published by the Free Software Foundation; either version 2 * * of the License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the Free Software * * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA * *************************************************************************************/ #include "testcompletion.h" #include "context.h" #include "model.h" #include "parser/parsesession.h" #include "builders/declarationbuilder.h" #include #include #include #include #include #include using namespace KDevelop; QTEST_MAIN(TestCompletion); QStandardItemModel& fakeModel() { static QStandardItemModel model; model.setColumnCount(10); model.setRowCount(10); return model; } go::CodeCompletionModel* model = 0; DUContext* getPackageContext(const QString& code) { ParseSession session(code.toUtf8(), 0); static int testNumber = 0; session.setCurrentDocument(IndexedString(QString("file:///temp/%1").arg(testNumber++))); if(!session.startParsing()) return 0; DeclarationBuilder builder(&session, false); ReferencedTopDUContext context = builder.build(session.currentDocument(), session.ast()); if(!context) return 0; DUChainReadLocker lock; auto decls = context->localDeclarations(); if(decls.size() != 1) return 0; Declaration* packageDeclaration = decls.first(); DUContext* packageContext = packageDeclaration->internalContext(); return packageContext; } QList getCompletions(QString code) { int cursorIndex = code.indexOf("%CURSOR"); Q_ASSERT(cursorIndex != -1); DUContext* context = getPackageContext(code.replace(cursorIndex, 7, "")); CursorInRevision cursor(0, cursorIndex); DUChainReadLocker lock; context = context->findContextAt(cursor); Q_ASSERT(context); go::CodeCompletionContext* completion = new go::CodeCompletionContext(DUContextPointer(context), code.mid(0, cursorIndex), cursor); model = new go::CodeCompletionModel(nullptr); model->setCompletionContext(QExplicitlySharedDataPointer(completion)); bool abort = false; return completion->completionItems(abort, true); } void debugPrint(const QList& completions, bool showQuality=false) { qDebug() << "Completions debug print"; QModelIndex prefixIdx = fakeModel().index(0, KDevelop::CodeCompletionModel::Prefix); QModelIndex nameIdx = fakeModel().index(0, KDevelop::CodeCompletionModel::Name); QModelIndex argsIdx = fakeModel().index(0, KDevelop::CodeCompletionModel::Arguments); for(const CompletionTreeItemPointer item : completions) { QStringList name; name << item->data(prefixIdx, Qt::DisplayRole, nullptr).toString(); name << item->data(nameIdx, Qt::DisplayRole, nullptr).toString(); name << item->data(argsIdx, Qt::DisplayRole, nullptr).toString(); if(!showQuality) qDebug() << name.join(' '); else { int quality = item->data(nameIdx, KDevelop::CodeCompletionModel::MatchQuality, model).toInt(); qDebug() << name.join(' ') << quality; } } } bool containsDeclaration(QString declaration, const QList& completions, int depth=-1, int quality=-1) { QModelIndex prefixIdx = fakeModel().index(0, KDevelop::CodeCompletionModel::Prefix); QModelIndex nameIdx = fakeModel().index(0, KDevelop::CodeCompletionModel::Name); QModelIndex argsIdx = fakeModel().index(0, KDevelop::CodeCompletionModel::Arguments); for(const CompletionTreeItemPointer item : completions) { //if(item->declaration()->toString() == declaration) QStringList name; name << item->data(prefixIdx, Qt::DisplayRole, nullptr).toString(); name << item->data(nameIdx, Qt::DisplayRole, nullptr).toString(); name << item->data(argsIdx, Qt::DisplayRole, nullptr).toString(); int itemQuality = item->data(nameIdx, KDevelop::CodeCompletionModel::MatchQuality, model).toInt(); if(name.join(' ') == declaration && (depth == -1 || item->argumentHintDepth() == depth) && (quality == -1 || quality == itemQuality) ) return true; } //print completions if we haven't found anything debugPrint(completions, quality == -1 ? false : true); return false; } void TestCompletion::initTestCase() { AutoTestShell::init(); TestCore::initialize(Core::NoUi); } void TestCompletion::cleanupTestCase() { TestCore::shutdown(); } void TestCompletion::test_basicCompletion() { QString code("package main; func main() { var a int; %CURSOR }"); auto completions = getCompletions(code); QCOMPARE(completions.size(), 3); QVERIFY(containsDeclaration(" main ()", completions)); //function QVERIFY(containsDeclaration("namespace main ", completions)); //package QVERIFY(containsDeclaration("int a ", completions)); //variable } void TestCompletion::test_functionCallTips_data() { QTest::addColumn("declaration"); QTest::addColumn("expression"); QTest::addColumn("result"); QTest::addColumn("depth"); QTest::addColumn("size"); QTest::newRow("normal") << "func myfunc() {};" << "myfunc(%CURSOR)" << " myfunc ()" << 1 << 4; QTest::newRow("args") << "func myfunc(a int) {};" << "myfunc(%CURSOR)" << " myfunc (int a)" << 1 << 4; QTest::newRow("args 2") << "type mytype int; func myfunc(a, b rune, c mytype) {};" << "myfunc(%CURSOR)" << " myfunc (rune a, rune b, main::mytype c)" << 1 << 5; QTest::newRow("rargs") << "func myfunc(a int) float32 {};" << "myfunc(%CURSOR)" << "float32 myfunc (int a)" << 1 << 4; QTest::newRow("rargs 2") << "func myfunc(a int) (float32, bool) {};" << "myfunc(%CURSOR)" << "float32, bool myfunc (int a)" << 1 << 4; QTest::newRow("rargs 3") << "func myfunc(a int) (r rune, b bool) {};" << "myfunc(%CURSOR)" << "rune r, bool b myfunc (int a)" << 1 << 4; QTest::newRow("variadic args") << "func myfunc(a ...int) {};" << "myfunc(%CURSOR)" << " myfunc (int... a)" << 1 << 4; QTest::newRow("variadic args 2") << "func myfunc(a int, i ...interface{}) {};" << "myfunc(%CURSOR)" << " myfunc (int a, interface{}... i)" << 1 << 4; QTest::newRow("var") << "var myvar func(a int);" << "myvar(%CURSOR)" << " myvar (int)" << 1 << 4; QTest::newRow("var 2") << "func functest(t int) rune;" << "myvar := functest; myvar(%CURSOR)" << "rune myvar (int)" << 1 << 5; QTest::newRow("struct") << "type mytype struct { f func(t int) rune; };" << "var myvar mytype; myvar.f(%CURSOR)" << "rune f (int)" << 1 << 5; QTest::newRow("method") << "type mytype int; func (m mytype) myfunc(c rune) {};" << "var myvar mytype; myvar.myfunc(%CURSOR)" << " myfunc (rune c)" << 1 << 5; QTest::newRow("method of embedded struct declared after method") << "type mytype struct {}; func (m mytype) myfunc(c rune) {}; type mytype2 struct {*mytype};" << "var myvar mytype2; myvar.myfunc(%CURSOR)" << " myfunc (rune c)" << 1 << 6; QTest::newRow("method of embedded struct declared before method") << "type mytype struct {}; func (m mytype) myfunc(c rune) {}; type mytype2 struct {*mytype};" << "var myvar mytype2; myvar.myfunc(%CURSOR)" << " myfunc (rune c)" << 1 << 6; QTest::newRow("paren") << "func myfunc(a int) {};" << "(myfunc)(%CURSOR)" << " myfunc (int a)" << 1 << 4; QTest::newRow("nested") << "func myfunc(a int) {};" << "f := myfunc; myfunc(f(%CURSOR))" << " myfunc (int a)" << 2 << 6; QTest::newRow("nested 2") << "func myfunc(a int) {};" << "f := myfunc; myfunc(f(%CURSOR))" << " f (int)" << 1 << 6; //TODO these don't work yet(they still pass, but they're wrong) //QTest::newRow("lambda") << "" << "f := func() int { \n}(%CURSOR)" << "int ()" << 0 << 3; QTest::newRow("multiple lines") << "func myfunc(a, b int) {};" << "myfunc(5, \n %CURSOR)" << " myfunc (int a, int b)" << 0 << 3; } void TestCompletion::test_functionCallTips() { QFETCH(QString, declaration); QFETCH(QString, expression); QFETCH(QString, result); QFETCH(int, depth); QFETCH(int, size); QString code(QString("package main; %1 func main() { %2 }").arg(declaration).arg(expression)); auto completions = getCompletions(code); //debugPrint(completions); QCOMPARE(completions.size(), size); QVERIFY(containsDeclaration(result, completions, depth)); } void TestCompletion::test_typeMatching_data() { QTest::addColumn("declaration"); QTest::addColumn("expression"); QTest::addColumn("result"); QTest::addColumn("size"); QTest::addColumn("quality"); QTest::newRow("assignment") << "var pi int = 3.14" << "var a int; a = %CURSOR a" << "int pi " << 4 << 10; QTest::newRow("const") << "const pi int = 3.14" << "var a int; a = %CURSOR a" << "int pi " << 4 << 10; QTest::newRow("function") << "func test() int {}" << "var a int; a = %CURSOR a" << "int test ()" << 4 << 10; QTest::newRow("function var") << "func test() int {}" << "f := test; var a int; a = %CURSOR a" << "function () int f " << 5 << 10; QTest::newRow("function type") << "func test() int {}" << "var a func() int; a = %CURSOR a" << "int test ()" << 4 << 10; QTest::newRow("function type var") << "func test() int {}" << "f := test; var a func() int; a = %CURSOR a" << "function () int f " << 5 << 10; QTest::newRow("arguments") << "func test(a int) {}" << "var a int; test(%CURSOR);" << "int a " << 5 << 10; QTest::newRow("sum") << "var a int" << "var b int; b + %CURSOR c" << "int a " << 4 << 10; QTest::newRow("multiple assignments") << "var a int = 1; var b bool = true" << "a, b = %CURSOR a, b" << "int a " << 4 << 10; QTest::newRow("multiple assignments 2") << "var a int = 1; var b bool = true" << "a, b = a, %CURSOR b" << "bool b " << 4 << 10; QTest::newRow("multiple assignments with function return args assigment") << "var a int = 1; var b bool = true" << "a, b = test(1, 3, 4), %CURSOR b" << "bool b " << 4 << 10; + QTest::newRow("multiple assignments with function returning multiple args") << "var a int = 1; var b bool = true; func test() (x int, y bool) {}" << "a, b = %CURSOR b" << "int x, bool y test ()" << 5 << 20; } void TestCompletion::test_typeMatching() { QFETCH(QString, declaration); QFETCH(QString, expression); QFETCH(QString, result); QFETCH(int, size); QFETCH(int, quality); QString code(QString("package main; %1; func main() { %2 }").arg(declaration).arg(expression)); auto completions = getCompletions(code); QCOMPARE(completions.size(), size); QVERIFY(containsDeclaration(result, completions, -1, quality)); } void TestCompletion::test_commentCompletion_data() { QTest::addColumn("expression"); QTest::addColumn("size"); QTest::newRow("line comment") << "// %CURSOR \n" << 0; QTest::newRow("line comment 2") << "// \n %CURSOR" << 2; QTest::newRow("comment") << "/* \"asdf \" %CURSOR */" << 0; QTest::newRow("comment 2") << "/* \'asdf \' */ %CURSOR" << 2; QTest::newRow("comment 3") << "/* ` %CURSOR */" << 0; QTest::newRow("comment 4") << "a := \"/* ` \" %CURSOR " << 3; QTest::newRow("string") << " a := \" %CURSOR \"" << 0; QTest::newRow("string 2") << " a := \" \" %CURSOR" << 3; QTest::newRow("string 3") << " a := ` %CURSOR `" << 0; QTest::newRow("string 4") << " a := ` ` %CURSOR" << 3; QTest::newRow("rune") << " a := \' %CURSOR\'" << 0; QTest::newRow("rune 2") << " a := \'a\' %CURSOR" << 3; } void TestCompletion::test_commentCompletion() { QFETCH(QString, expression); QFETCH(int, size); QString code(QString("package main; func main() { %1 }").arg(expression)); auto completions = getCompletions(code); QCOMPARE(completions.size(), size); } void TestCompletion::test_membersCompletion_data() { QTest::addColumn("declaration"); QTest::addColumn("expression"); QTest::addColumn("result"); QTest::addColumn("size"); QTest::newRow("method multiple lines") << "type mytype int; func (m mytype) myfunc() (t mytype) {}; func (m mytype) myfunc2(c rune) {}" << "var myvar mytype; myvar.myfunc().\n%CURSOR x;" << " myfunc2 (rune c)" << 2; } void TestCompletion::test_membersCompletion() { QFETCH(QString, declaration); QFETCH(QString, expression); QFETCH(QString, result); QFETCH(int, size); QString code(QString("package main; %1; func main() { %2 }").arg(declaration).arg(expression)); auto completions = getCompletions(code); QCOMPARE(completions.size(), size); QVERIFY(containsDeclaration(result, completions)); } \ No newline at end of file diff --git a/codecompletion/typematch.cpp b/codecompletion/typematch.cpp new file mode 100644 index 0000000..df7d144 --- /dev/null +++ b/codecompletion/typematch.cpp @@ -0,0 +1,47 @@ +/* KDevelop Go codecompletion support + * + * Copyright 2017 Mikhail Ivchenko + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + */ + +#include "typematch.h" + +namespace go +{ + +TypeMatch::TypeMatch() : m_singleType(nullptr), m_multipleTypes({}) +{ +} + +TypeMatch::TypeMatch(const KDevelop::AbstractType::Ptr &m_singleType, + const QList &m_multipleTypes) + : m_singleType(m_singleType), m_multipleTypes(m_multipleTypes) +{ + +} + +KDevelop::AbstractType::Ptr TypeMatch::singleType() const +{ + return m_singleType; +} + +QList TypeMatch::multipleTypes() const +{ + return m_multipleTypes; +} + +void TypeMatch::setSingleType(const KDevelop::AbstractType::Ptr &singleType) +{ + m_singleType = singleType; +} + +void TypeMatch::setMultipleTypes(const QList &multipleTypes) +{ + m_multipleTypes = multipleTypes; +} + +} diff --git a/codecompletion/typematch.h b/codecompletion/typematch.h new file mode 100644 index 0000000..63326ea --- /dev/null +++ b/codecompletion/typematch.h @@ -0,0 +1,43 @@ +/* KDevelop Go codecompletion support + * + * Copyright 2017 Mikhail Ivchenko + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + */ + +#ifndef GOLANGTYPEMATCH_H +#define GOLANGTYPEMATCH_H + +#include +#include + +#include "kdevgocompletion_export.h" +#include + +namespace go +{ + +class KDEVGOCOMPLETION_EXPORT TypeMatch +{ +public: + TypeMatch(); + + TypeMatch(const KDevelop::AbstractType::Ptr &m_singleType, const QList &m_multipleTypes); + + KDevelop::AbstractType::Ptr singleType() const; + QList multipleTypes() const; + + void setSingleType(const KDevelop::AbstractType::Ptr &singleType); + void setMultipleTypes(const QList &multipleTypes); + +private: + KDevelop::AbstractType::Ptr m_singleType; + QList m_multipleTypes; +}; + +} + +#endif //GOLANGTYPEMATCH_H