File indexing completed on 2024-04-14 04:31:08

0001 /*
0002  * This file is part of KDevelop
0003  * Copyright (C) 2012-2015 Miquel Sabaté Solà <mikisabate@gmail.com>
0004  *
0005  * This program is free software: you can redistribute it and/or modify
0006  * it under the terms of the GNU General Public License as published by
0007  * the Free Software Foundation, either version 3 of the License, or
0008  * (at your option) any later version.
0009  *
0010  * This program is distributed in the hope that it will be useful,
0011  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0012  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013  * GNU General Public License for more details.
0014  *
0015  * You should have received a copy of the GNU General Public License
0016  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0017  */
0018 
0019 // KDE + KDevelop
0020 #include <language/duchain/duchainlock.h>
0021 #include <language/duchain/duchain.h>
0022 #include <language/duchain/types/unsuretype.h>
0023 
0024 // Ruby
0025 #include <completiondebug.h>
0026 #include <parser/parser.h>
0027 #include <duchain/loader.h>
0028 #include <duchain/editorintegrator.h>
0029 #include <duchain/expressionvisitor.h>
0030 #include <duchain/declarations/moduledeclaration.h>
0031 #include <duchain/declarations/methoddeclaration.h>
0032 #include <completion/context.h>
0033 #include <completion/items/keyworditem.h>
0034 #include <completion/items/normalitem.h>
0035 #include <completion/items/requirefileitem.h>
0036 
0037 #define ADD_KEYWORD(name) list << CompletionTreeItemPointer(new KeywordItem(KDevelop::CodeCompletionContext::Ptr(this), name))
0038 #define ADD_KEYWORD2(name, desc) list << CompletionTreeItemPointer(new KeywordItem(KDevelop::CodeCompletionContext::Ptr(this), name, desc))
0039 #define ADD_ONE_LINER(name, desc) list << CompletionTreeItemPointer(new KeywordItem(KDevelop::CodeCompletionContext::Ptr(this), name, desc, true))
0040 #define ADD_NORMAL(decl, depth) list << CompletionTreeItemPointer(new NormalItem(DeclarationPointer(decl), KDevelop::CodeCompletionContext::Ptr(this), depth));
0041 
0042 using namespace KDevelop;
0043 
0044 namespace ruby
0045 {
0046 
0047 const QSet<QString> MEMBER_STRINGS = QString(". :: < include extend").split(' ').toSet();
0048 const int MEMBER_STRINGS_MAX = 7; // include
0049 
0050 /**
0051  * The diferent types of contexts in code completion.
0052  */
0053 enum class ContextType {
0054     NoMemberAccess,         /// A global completion should be done.
0055     MemberAccess,           /// obj.
0056     ModuleMemberAccess,     /// MyModule::
0057     BaseClassAccess,        /// After "class Klass <".
0058     ModuleMixinAccess,      /// After "include" or "extend".
0059     FileChoose              /// Autocompletion for files.
0060 };
0061 
0062 /**
0063  * It compresses the given @p text by removing spaces.
0064  */
0065 void compressText(QString &text)
0066 {
0067     for (int i = text.length() - 1; i >= 0; --i) {
0068         if (!text[i].isSpace()) {
0069             break;
0070         }
0071         text.remove(i, 1);
0072     }
0073 }
0074 
0075 /**
0076  * Get the ending string from the set of members for the given text.
0077  *
0078  * @param str The completion context text.
0079  * @returns the required ending string.
0080  */
0081 QString getEndingFromSet(const QString &str)
0082 {
0083     QString end;
0084 
0085     for (int i = qMin(str.length(), MEMBER_STRINGS_MAX); i > 0; --i) {
0086         end = str.right(i);
0087         if (MEMBER_STRINGS.contains(end)) {
0088             return end;
0089         }
0090     }
0091     return QString();
0092 }
0093 
0094 /**
0095  * Ugly method that tells if the given text is inside a class.
0096  *
0097  * @param text The given text.
0098  * @returns true if we're inside a class, false otherwise.
0099  */
0100 bool insideClass(const QString &text)
0101 {
0102     int idx = text.lastIndexOf(QStringLiteral("<"));
0103     int classIdx = text.lastIndexOf(QStringLiteral("class"), idx);
0104     int semicolon = text.lastIndexOf(QStringLiteral(";"), idx);
0105     return  (classIdx != -1 && (classIdx > semicolon));
0106 }
0107 
0108 /**
0109  * Returns the last n lines from the given text.
0110  *
0111  * @param str The given text.
0112  * @param n The number of lines to retrieve.
0113  */
0114 QString lastNLines(const QString &str, int n)
0115 {
0116     int curNewLine = str.lastIndexOf('\n');
0117     int nthLine = curNewLine;
0118 
0119     for (int i = 0; i < n; ++i) {
0120         if (curNewLine == -1) {
0121             break;
0122         } else {
0123             nthLine = curNewLine;
0124         }
0125         curNewLine = str.lastIndexOf('\n', curNewLine - 1);
0126     }
0127 
0128     // return the position after the newline, or whole str if no newline
0129     return str.mid(nthLine + 1);
0130 }
0131 
0132 /**
0133  * Guess the access kind depending on the given parameter.
0134  *
0135  * @param original The original text from the completion context.
0136  * @returns the proper CodeCompletionContext::CompletionContextType.
0137  */
0138 ContextType findAccessKind(const QString &original)
0139 {
0140     const QString text = getEndingFromSet(original);
0141 
0142     if (text == QStringLiteral(".")) {
0143         return ContextType::MemberAccess;
0144     }
0145     if (text == QStringLiteral("::")) {
0146         return ContextType::ModuleMemberAccess;
0147     }
0148     if (text == QStringLiteral("<") && insideClass(original)) {
0149         return ContextType::BaseClassAccess;
0150     }
0151     if (text == QStringLiteral("include") || text == QStringLiteral("extend")) {
0152         return ContextType::ModuleMixinAccess;
0153     }
0154     return ContextType::NoMemberAccess;
0155 }
0156 
0157 CodeCompletionContext::CodeCompletionContext(DUContextPointer ctxt, const QString &text,
0158                                              const QString &followingText,
0159                                              const CursorInRevision &pos, int depth)
0160     : KDevelop::CodeCompletionContext(ctxt, text, pos, depth)
0161     , m_valid(true), m_kind(ContextType::NoMemberAccess)
0162 {
0163     if (!m_duContext || !isValidPosition()) {
0164         m_valid = false;
0165         return;
0166     }
0167 
0168     m_following = followingText;
0169     m_closing = 0;
0170     if (doRequireCompletion()) {
0171         return;
0172     }
0173 
0174     compressText(m_text);
0175     m_kind = findAccessKind(m_text);
0176 }
0177 
0178 CodeCompletionContext::~CodeCompletionContext()
0179 {
0180 }
0181 
0182 QList<KDevelop::CompletionTreeItemPointer> CodeCompletionContext::completionItems(bool &abort, bool fullCompletion)
0183 {
0184     QList<CompletionTreeItemPointer> items;
0185     if (!m_valid) {
0186         return items;
0187     }
0188 
0189     switch(m_kind) {
0190     case ContextType::MemberAccess:
0191         items += memberAccessItems();
0192         break;
0193     case ContextType::ModuleMemberAccess:
0194         items += moduleMemberAccessItems();
0195         break;
0196     case ContextType::BaseClassAccess:
0197         items += baseClassItems();
0198         break;
0199     case ContextType::ModuleMixinAccess:
0200         items += moduleMixinItems();
0201         break;
0202     case ContextType::FileChoose:
0203         items += fileChooseItems();
0204         break;
0205     default:
0206         items += standardAccessItems();
0207         addRubyKeywords();
0208         addRubySpecialBuiltins();
0209     }
0210 
0211     if (shouldAddParentItems(fullCompletion)) {
0212         items.append(parentContext()->completionItems(abort, fullCompletion));
0213     }
0214     return items;
0215 }
0216 
0217 QList<CompletionTreeElementPointer> CodeCompletionContext::ungroupedElements()
0218 {
0219     return m_ungroupedItems;
0220 }
0221 
0222 bool CodeCompletionContext::isValidPosition()
0223 {
0224     if (m_text.isEmpty()) {
0225         return true;
0226     }
0227 
0228     for (QString::iterator it = m_text.end(); it != m_text.begin(); --it) {
0229         if (*it == '\n') {
0230             break;
0231         }
0232         if (*it == '#') {
0233             return false;
0234         }
0235     }
0236     return true;
0237 }
0238 
0239 bool CodeCompletionContext::doRequireCompletion()
0240 {
0241     QString line = lastNLines(m_text, 1).trimmed();
0242     KDevelop::Path relative;
0243     int idx = 8;
0244 
0245     if (!line.startsWith(QStringLiteral("require "))) {
0246         if (!line.startsWith(QStringLiteral("require_relative "))) {
0247             return false;
0248         }
0249         idx += 9;
0250         relative = KDevelop::Path(m_duContext->url().toUrl()).parent();
0251     }
0252     line = line.mid(idx).trimmed();
0253     m_closing = '\'';
0254     if ((idx = line.indexOf(QStringLiteral("'"))) < 0) {
0255         m_closing = '"';
0256         if ((idx = line.indexOf(QStringLiteral("\")")) < 0)) {
0257             m_closing = 0;
0258             return false;
0259         }
0260     }
0261     line = line.mid(idx + 1);
0262 
0263     m_includeItems = Loader::getFilesInSearchPath(line, m_following, relative);
0264     m_kind = ContextType::FileChoose;
0265     return true;
0266 }
0267 
0268 AbstractType::Ptr CodeCompletionContext::getExpressionType(const QString &token)
0269 {
0270     AbstractType::Ptr res;
0271     QString expr = m_text.left(m_text.lastIndexOf(token));
0272     EditorIntegrator e;
0273     ExpressionVisitor ev(m_duContext.data(), &e);
0274 
0275     DUChainReadLocker lock;
0276     Parser parser(IndexedString(), expr.toUtf8());
0277     Ast *ast = parser.parse();
0278     if (!ast || !ast->tree) {
0279         return AbstractType::Ptr(nullptr);
0280     }
0281     lock.unlock();
0282     ev.visitCode(ast);
0283     res = ev.lastType();
0284     lock.lock();
0285 
0286     return res;
0287 }
0288 
0289 QList<CompletionTreeItemPointer> CodeCompletionContext::getCompletionItemsFromType(AbstractType::Ptr type, bool scoped)
0290 {
0291     QList<CompletionTreeItemPointer> res;
0292     if (type->whichType() == AbstractType::TypeUnsure) {
0293         auto unsure = type.staticCast<UnsureType>();
0294         int count = unsure->typesSize();
0295         for (int i = 0; i < count; i++) {
0296             res.append(getCompletionItemsForOneType(unsure->types()[i].abstractType(), scoped));
0297         }
0298     } else {
0299         res = getCompletionItemsForOneType(type, scoped);
0300     }
0301     return res;
0302 }
0303 
0304 QList<CompletionTreeItemPointer> CodeCompletionContext::getCompletionItemsForOneType(AbstractType::Ptr type, bool scoped)
0305 {
0306     QList<CompletionTreeItemPointer> list;
0307     QVector<DeclarationPair> decls;
0308     auto sType = type.dynamicCast<StructureType>();
0309 
0310     {
0311         DUChainReadLocker lock;
0312         if (!sType || !sType->internalContext(m_duContext->topContext())) {
0313             return QList<CompletionTreeItemPointer>();
0314         }
0315         DUContext *current = sType->internalContext(m_duContext->topContext());
0316         decls = current->allDeclarations(CursorInRevision::invalid(), m_duContext->topContext(), false);
0317     }
0318 
0319     foreach (DeclarationPair d, decls) {
0320         MethodDeclaration *md = dynamic_cast<MethodDeclaration *>(d.first);
0321         if (md && md->accessPolicy() == Declaration::Public) {
0322             if (!scoped || (scoped && md->isClassMethod())) {
0323 
0324                 ADD_NORMAL(d.first, d.second);
0325             }
0326         } else if (scoped && dynamic_cast<ModuleDeclaration *>(d.first)) {
0327             ADD_NORMAL(d.first, d.second);
0328         }
0329     }
0330     return list;
0331 }
0332 
0333 bool CodeCompletionContext::shouldAddParentItems(bool fullCompletion)
0334 {
0335     return (m_parentContext && fullCompletion);
0336 }
0337 
0338 QList<CompletionTreeItemPointer> CodeCompletionContext::memberAccessItems()
0339 {
0340     QList<CompletionTreeItemPointer> list;
0341     AbstractType::Ptr type = getExpressionType(QStringLiteral("."));
0342     if (type) {
0343         list << getCompletionItemsFromType(type);
0344     } else {
0345         qCDebug(COMPLETION) << "Oops: cannot access at the member";
0346     }
0347     return list;
0348 }
0349 
0350 QList<CompletionTreeItemPointer> CodeCompletionContext::moduleMemberAccessItems()
0351 {
0352     QList<CompletionTreeItemPointer> list;
0353     AbstractType::Ptr type = getExpressionType(QStringLiteral("::"));
0354     if (type) {
0355         list << getCompletionItemsFromType(type, true);
0356     } else {
0357         qCDebug(COMPLETION) << "Oops: cannot access at the member";
0358     }
0359 
0360     return list;
0361 }
0362 
0363 QList<CompletionTreeItemPointer> CodeCompletionContext::baseClassItems()
0364 {
0365     QList<CompletionTreeItemPointer> list;
0366     QVector<DeclarationPair> decls;
0367 
0368     {
0369         DUChainReadLocker lock;
0370         decls = m_duContext->allDeclarations(m_position, m_duContext->topContext());
0371     }
0372 
0373     foreach (DeclarationPair d, decls) {
0374         ModuleDeclaration *mDecl = dynamic_cast<ModuleDeclaration *>(d.first);
0375         if (mDecl && !mDecl->isModule()) {
0376             ADD_NORMAL(d.first, d.second);
0377         }
0378     }
0379     return list;
0380 }
0381 
0382 QList<CompletionTreeItemPointer> CodeCompletionContext::moduleMixinItems()
0383 {
0384     QList<CompletionTreeItemPointer> list;
0385     QVector<DeclarationPair> decls;
0386 
0387     {
0388         DUChainReadLocker lock;
0389         decls = m_duContext->allDeclarations(m_position, m_duContext->topContext());
0390     }
0391 
0392     foreach (DeclarationPair d, decls) {
0393         if (dynamic_cast<ModuleDeclaration *>(d.first)) {
0394             ADD_NORMAL(d.first, d.second);
0395         }
0396     }
0397     return list;
0398 }
0399 
0400 QList<CompletionTreeItemPointer> CodeCompletionContext::fileChooseItems()
0401 {
0402     QList<CompletionTreeItemPointer> list;
0403 
0404     foreach (const KDevelop::IncludeItem &item, m_includeItems) {
0405         list << CompletionTreeItemPointer(new RequireFileItem(item, m_closing));
0406     }
0407     return list;
0408 }
0409 
0410 QList<CompletionTreeItemPointer> CodeCompletionContext::standardAccessItems()
0411 {
0412     QList<CompletionTreeItemPointer> list;
0413     QVector<DeclarationPair> decls;
0414 
0415     // Add one-liners (i.e. shebang)
0416     if (m_position.line == 0 && (m_text.startsWith(QStringLiteral("#"))
0417                 || m_text.isEmpty())) {
0418         ADD_ONE_LINER(QStringLiteral("#!/usr/bin/env ruby"), i18n("insert Shebang line"));
0419         ADD_ONE_LINER(QStringLiteral("# encoding: UTF-8"), i18n("insert encoding line"));
0420     }
0421 
0422     // Find everything that is accessible at this point
0423     {
0424         DUChainReadLocker lock;
0425         decls = m_duContext->allDeclarations(m_position, m_duContext->topContext());
0426     }
0427     foreach (DeclarationPair d, decls) {
0428         ADD_NORMAL(d.first, d.second);
0429     }
0430     return list;
0431 }
0432 
0433 void CodeCompletionContext::eventuallyAddGroup(const QString &name, int priority,
0434                                                QList<CompletionTreeItemPointer> items)
0435 {
0436     KDevelop::CompletionCustomGroupNode *node = new KDevelop::CompletionCustomGroupNode(name, priority);
0437     node->appendChildren(items);
0438     m_ungroupedItems << CompletionTreeElementPointer(node);
0439 }
0440 
0441 void CodeCompletionContext::addRubyKeywords()
0442 {
0443     QList<CompletionTreeItemPointer> list;
0444 
0445     // "Ultra-simple" statements. Some of them may not be *that* useful.
0446     ADD_KEYWORD("next");
0447     ADD_KEYWORD("break");
0448     ADD_KEYWORD("true");
0449     ADD_KEYWORD("false");
0450     ADD_KEYWORD("self");
0451     ADD_KEYWORD("then");
0452     ADD_KEYWORD("redo");
0453     ADD_KEYWORD("retry");
0454     ADD_KEYWORD("yield");
0455     ADD_KEYWORD("super");
0456     ADD_KEYWORD("return");
0457     ADD_KEYWORD("defined?");
0458     ADD_KEYWORD("__FILE__");
0459     ADD_KEYWORD("__LINE__");
0460     ADD_KEYWORD("__ENCODING__");
0461 
0462     // Simple statements
0463     ADD_KEYWORD2("alias", "alias ");
0464     ADD_KEYWORD2("undef", "undef ");
0465     ADD_KEYWORD2("rescue", "%UNINDENT%rescue ");
0466     ADD_KEYWORD2("ensure", "%UNINDENT%ensure%INDENT%%CURSOR%\n");
0467     ADD_KEYWORD2("BEGIN", "BEGIN {\n%INDENT%%CURSOR%\n}");
0468 
0469     // Take care of complex statements that can be just statement modifiers
0470     if (lastNLines(m_text, 1).isEmpty()) {
0471         ADD_KEYWORD2("if", "if %SELECT%condition%ENDSELECT%\n%END%");
0472         ADD_KEYWORD2("unless", "unless %SELECT%condition%ENDSELECT%\n%END%");
0473         ADD_KEYWORD2("while", "while %SELECT%condition%ENDSELECT%\n%END%");
0474         ADD_KEYWORD2("until", "until %SELECT%condition%ENDSELECT%\n%END%");
0475     } else {
0476         ADD_KEYWORD2("if", "if %SELECT%condition%ENDSELECT%");
0477         ADD_KEYWORD2("unless", "unless %SELECT%condition%ENDSELECT%");
0478         ADD_KEYWORD2("while", "while %SELECT%condition%ENDSELECT%");
0479         ADD_KEYWORD2("until", "until %SELECT%condition%ENDSELECT%");
0480     }
0481 
0482     // Complex constructions
0483     ADD_KEYWORD2("elsif", "%UNINDENT%elsif %SELECT%condition%ENDSELECT%");
0484     ADD_KEYWORD2("else", "%UNINDENT%else\n%INDENT%%CURSOR%");
0485     ADD_KEYWORD2("for", "for %SELECT%condition%ENDSELECT% in \n%END%");
0486     ADD_KEYWORD2("def", "def %SELECT%name%ENDSELECT%\n%END%");
0487     ADD_KEYWORD2("class", "class %SELECT%Name%ENDSELECT%\n%END%");
0488     ADD_KEYWORD2("module", "module %SELECT%Name%ENDSELECT%\n%END%");
0489     ADD_KEYWORD2("case", "case %SELECT%condition%ENDSELECT%\n%END%");
0490     ADD_KEYWORD2("when", "%UNINDENT%when %SELECT%condition%ENDSELECT%");
0491     ADD_KEYWORD2("begin", "begin\n%INDENT%%CURSOR%\n%END%");
0492     ADD_KEYWORD2("do", "do %SELECT%||%ENDSELECT%\n%END%");
0493 
0494     // Group all these keywords into the "Ruby Keyword" group.
0495     eventuallyAddGroup(i18n("Ruby Keyword"), 800, list);
0496 }
0497 
0498 void CodeCompletionContext::addRubySpecialBuiltins()
0499 {
0500     QList<CompletionTreeItemPointer> list;
0501 
0502     // Not really keywords, but who cares? ;)
0503     ADD_KEYWORD2("include", "include %SELECT%MyModule%ENDSELECT%");
0504     ADD_KEYWORD2("extend", "extend %SELECT%MyModule%ENDSELECT%");
0505     ADD_KEYWORD2("require", "require '%CURSOR%'");
0506     ADD_KEYWORD2("require_relative", "require_relative '%CURSOR%'");
0507 
0508     // Group all these special builtins
0509     eventuallyAddGroup(i18n("Ruby Builtins"), 800, list);
0510 }
0511 
0512 }