File indexing completed on 2024-05-19 15:46:42
0001 /* 0002 SPDX-FileCopyrightText: 2013 Sven Brauch <svenbrauch@googlemail.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 #include "context.h" 0008 0009 #include "items/modulecompletionitem.h" 0010 #include "items/functioncalltipcompletionitem.h" 0011 0012 #include <language/codecompletion/codecompletionitem.h> 0013 #include <language/codecompletion/normaldeclarationcompletionitem.h> 0014 #include <language/duchain/declaration.h> 0015 #include <language/duchain/duchainlock.h> 0016 #include <language/duchain/classdeclaration.h> 0017 #include <language/duchain/namespacealiasdeclaration.h> 0018 #include <language/duchain/codemodel.h> 0019 0020 #include <qmljs/qmljsdocument.h> 0021 #include <qmljs/parser/qmljslexer_p.h> 0022 #include "../duchain/expressionvisitor.h" 0023 #include "../duchain/helper.h" 0024 #include "../duchain/cache.h" 0025 #include "../duchain/frameworks/nodejs.h" 0026 0027 #include <QDir> 0028 #include <QRegExp> 0029 0030 using namespace KDevelop; 0031 0032 using DeclarationDepthPair = QPair<Declaration*, int>; 0033 0034 namespace QmlJS { 0035 0036 CodeCompletionContext::CodeCompletionContext(const DUContextPointer& context, const QString& text, 0037 const CursorInRevision& position, int depth) 0038 : KDevelop::CodeCompletionContext(context, extractLastLine(text), position, depth), 0039 m_completionKind(NormalCompletion) 0040 { 0041 // Detect "import ..." and provide import completions 0042 if (m_text.startsWith(QLatin1String("import "))) { 0043 m_completionKind = ImportCompletion; 0044 } 0045 0046 // Node.js module completions 0047 if (m_text.endsWith(QLatin1String("require("))) { 0048 m_completionKind = NodeModulesCompletion; 0049 } 0050 0051 // Detect whether the cursor is in a comment 0052 bool isLastLine = true; 0053 bool inString = false; 0054 0055 for (int index = text.size()-1; index > 0; --index) { 0056 const QChar c = text.at(index); 0057 const QChar prev = text.at(index - 1); 0058 0059 if (c == QLatin1Char('\n')) { 0060 isLastLine = false; 0061 } else if (isLastLine && prev == QLatin1Char('/') && c == QLatin1Char('/')) { 0062 // Single-line comment on the current line, we are in a comment 0063 m_completionKind = CommentCompletion; 0064 break; 0065 } else if (prev == QLatin1Char('/') && c == QLatin1Char('*')) { 0066 // Start of a multi-line comment encountered 0067 m_completionKind = CommentCompletion; 0068 break; 0069 } else if (prev == QLatin1Char('*') && c == QLatin1Char('/')) { 0070 // End of a multi-line comment. Because /* and */ cannot be nested, 0071 // encountering a */ is enough to know that the cursor is outside a 0072 // comment 0073 break; 0074 } else if (prev != QLatin1Char('\\') && (c == QLatin1Char('"') || c == QLatin1Char('\''))) { 0075 // Toggle whether we are in a string or not 0076 inString = !inString; 0077 } 0078 } 0079 0080 if (inString) { 0081 m_completionKind = StringCompletion; 0082 } 0083 0084 // Some specific constructs don't need any code-completion at all (mainly 0085 // because the user will declare new things, not use ones) 0086 if (m_text.contains(QRegExp(QLatin1String("(var|function)\\s+$"))) || // "var |" or "function |" 0087 m_text.contains(QRegExp(QLatin1String("property\\s+[a-zA-Z0-9_]+\\s+$"))) || // "property <type> |" 0088 m_text.contains(QRegExp(QLatin1String("function(\\s+[a-zA-Z0-9_]+)?\\s*\\($"))) || // "function (|" or "function <name> (|" 0089 m_text.contains(QRegExp(QLatin1String("id:\\s*"))) // "id: |" 0090 ) { 0091 m_completionKind = NoCompletion; 0092 } 0093 } 0094 0095 QList<CompletionTreeItemPointer> CodeCompletionContext::completionItems(bool& abort, bool fullCompletion) 0096 { 0097 Q_UNUSED (fullCompletion); 0098 0099 if (abort) { 0100 return QList<CompletionTreeItemPointer>(); 0101 } 0102 0103 switch (m_completionKind) { 0104 case NormalCompletion: 0105 return normalCompletion(); 0106 case CommentCompletion: 0107 return commentCompletion(); 0108 case ImportCompletion: 0109 return importCompletion(); 0110 case NodeModulesCompletion: 0111 return nodeModuleCompletions(); 0112 case StringCompletion: 0113 case NoCompletion: 0114 break; 0115 } 0116 0117 return QList<CompletionTreeItemPointer>(); 0118 } 0119 0120 AbstractType::Ptr CodeCompletionContext::typeToMatch() const 0121 { 0122 return m_typeToMatch; 0123 } 0124 0125 QList<KDevelop::CompletionTreeItemPointer> CodeCompletionContext::normalCompletion() 0126 { 0127 QList<CompletionTreeItemPointer> items; 0128 QChar lastChar = m_text.size() > 0 ? m_text.at(m_text.size() - 1) : QLatin1Char('\0'); 0129 bool inQmlObjectScope = (m_duContext->type() == DUContext::Class); 0130 0131 // Start with the function call-tips, because functionCallTips is also responsible 0132 // for setting m_declarationForTypeMatch 0133 items << functionCallTips(); 0134 0135 if (lastChar == QLatin1Char('.') || lastChar == QLatin1Char('[')) { 0136 // Offer completions for object members and array subscripts 0137 items << fieldCompletions( 0138 m_text.left(m_text.size() - 1), 0139 lastChar == QLatin1Char('[') ? CompletionItem::QuotesAndBracket : 0140 CompletionItem::NoDecoration 0141 ); 0142 } 0143 0144 // "object." must only display the members of object, the declarations 0145 // available in the current context. 0146 if (lastChar != QLatin1Char('.')) { 0147 if (inQmlObjectScope) { 0148 DUChainReadLocker lock; 0149 0150 // The cursor is in a QML object and there is nothing before it. Display 0151 // a list of properties and signals that can be used in a script binding. 0152 // Note that the properties/signals of parent QML objects are not displayed here 0153 items << completionsInContext(m_duContext, 0154 CompletionOnlyLocal | CompletionHideWrappers, 0155 CompletionItem::ColonOrBracket); 0156 items << completionsFromImports(CompletionHideWrappers); 0157 items << completionsInContext(DUContextPointer(m_duContext->topContext()), 0158 CompletionHideWrappers, 0159 CompletionItem::NoDecoration); 0160 } else { 0161 items << completionsInContext(m_duContext, 0162 CompletionInContextFlags(), 0163 CompletionItem::NoDecoration); 0164 items << completionsFromImports(CompletionInContextFlags()); 0165 items << completionsFromNodeModule(CompletionInContextFlags(), QStringLiteral("__builtin_ecmascript")); 0166 0167 if (!QmlJS::isQmlFile(m_duContext.data())) { 0168 items << completionsFromNodeModule(CompletionInContextFlags(), QStringLiteral("__builtin_dom")); 0169 } 0170 } 0171 } 0172 0173 return items; 0174 } 0175 0176 QList<CompletionTreeItemPointer> CodeCompletionContext::commentCompletion() 0177 { 0178 return QList<CompletionTreeItemPointer>(); 0179 } 0180 0181 QList<CompletionTreeItemPointer> CodeCompletionContext::importCompletion() 0182 { 0183 QList<CompletionTreeItemPointer> items; 0184 const auto fragment = m_text.section(QLatin1Char(' '), -1, -1); 0185 0186 auto addModules = [&items, &fragment](const QString& dataDir) { 0187 if (dataDir.isEmpty()) 0188 return; 0189 0190 QDir dir(dataDir); 0191 if (!dir.exists()) 0192 return; 0193 0194 const auto dirEntries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name); 0195 items.reserve(dirEntries.size()); 0196 for (const QString& entry : dirEntries) { 0197 items.append(CompletionTreeItemPointer(new ModuleCompletionItem( 0198 fragment + entry.section(QLatin1Char('.'), 0, 0), 0199 ModuleCompletionItem::Import 0200 ))); 0201 } 0202 }; 0203 0204 // Use the cache to find the directory corresponding to the fragment 0205 // (org.kde is, for instance, /usr/lib64/kde4/imports/org/kde), and list 0206 // its subdirectories 0207 addModules(Cache::instance().modulePath(m_duContext->url(), fragment)); 0208 0209 if (items.isEmpty()) { 0210 // fallback to list all directories we can find 0211 const auto paths = Cache::instance().libraryPaths(m_duContext->url()); 0212 auto fragmentPath = fragment; 0213 fragmentPath.replace(QLatin1Char('.'), QLatin1Char('/')); 0214 for (const auto& path : paths) { 0215 addModules(path.cd(fragmentPath).path()); 0216 } 0217 } 0218 0219 return items; 0220 } 0221 0222 QList<CompletionTreeItemPointer> CodeCompletionContext::nodeModuleCompletions() 0223 { 0224 QList<CompletionTreeItemPointer> items; 0225 QDir dir; 0226 0227 const auto& paths = NodeJS::instance().moduleDirectories(m_duContext->url().str()); 0228 for (auto& path : paths) { 0229 dir.setPath(path.toLocalFile()); 0230 0231 const auto& entries = dir.entryList(QDir::Files, QDir::Name); 0232 for (QString entry : entries) { 0233 entry.remove(QLatin1String(".js")); 0234 0235 if (entry.startsWith(QLatin1String("__"))) { 0236 // Internal module, don't show 0237 continue; 0238 } 0239 0240 items.append(CompletionTreeItemPointer( 0241 new ModuleCompletionItem(entry, ModuleCompletionItem::Quotes) 0242 )); 0243 } 0244 } 0245 0246 return items; 0247 } 0248 0249 QList<CompletionTreeItemPointer> CodeCompletionContext::functionCallTips() 0250 { 0251 Stack<ExpressionStackEntry> stack = expressionStack(m_text); 0252 QList<CompletionTreeItemPointer> items; 0253 int argumentHintDepth = 1; 0254 bool isTopOfStack = true; 0255 DUChainReadLocker lock; 0256 0257 while (!stack.isEmpty()) { 0258 ExpressionStackEntry entry = stack.pop(); 0259 0260 if (isTopOfStack && entry.operatorStart > entry.startPosition) { 0261 // Deduce the declaration for type matching using operatorStart: 0262 // 0263 // table[document.base + 0264 // [ ^ 0265 // 0266 // ^ = operatorStart. Just before operatorStart is a code snippet that ends 0267 // with the declaration whose type should be used. 0268 DeclarationPointer decl = declarationAtEndOfString(m_text.left(entry.operatorStart)); 0269 0270 if (decl) { 0271 m_typeToMatch = decl->abstractType(); 0272 } 0273 } 0274 0275 if (entry.startPosition > 0 && m_text.at(entry.startPosition - 1) == QLatin1Char('(')) { 0276 // The current entry represents a function call, create a call-tip for it 0277 DeclarationPointer functionDecl = declarationAtEndOfString(m_text.left(entry.startPosition - 1)); 0278 0279 if (functionDecl) { 0280 auto item = new FunctionCalltipCompletionItem( 0281 functionDecl, 0282 argumentHintDepth, 0283 entry.commas 0284 ); 0285 0286 items << CompletionTreeItemPointer(item); 0287 argumentHintDepth++; 0288 0289 if (isTopOfStack && !m_typeToMatch) { 0290 m_typeToMatch = item->currentArgumentType(); 0291 } 0292 } 0293 } 0294 0295 isTopOfStack = false; 0296 } 0297 0298 return items; 0299 } 0300 0301 QList<CompletionTreeItemPointer> CodeCompletionContext::completionsFromImports(CompletionInContextFlags flags) 0302 { 0303 QList<CompletionTreeItemPointer> items; 0304 0305 // Iterate over all the imported namespaces and add their definitions 0306 DUChainReadLocker lock; 0307 const QList<Declaration*> imports = m_duContext->findDeclarations(globalImportIdentifier()); 0308 QList<Declaration*> realImports; 0309 0310 for (Declaration* import : imports) { 0311 if (import->kind() != Declaration::NamespaceAlias) { 0312 continue; 0313 } 0314 0315 auto* decl = static_cast<NamespaceAliasDeclaration *>(import); 0316 realImports << m_duContext->findDeclarations(decl->importIdentifier()); 0317 } 0318 0319 items.reserve(realImports.size()); 0320 foreach (Declaration* import, realImports) { 0321 items << completionsInContext( 0322 DUContextPointer(import->internalContext()), 0323 flags, 0324 CompletionItem::NoDecoration 0325 ); 0326 } 0327 0328 return items; 0329 } 0330 0331 QList<CompletionTreeItemPointer> CodeCompletionContext::completionsFromNodeModule(CompletionInContextFlags flags, 0332 const QString& module) 0333 { 0334 return completionsInContext( 0335 DUContextPointer(QmlJS::getInternalContext( 0336 QmlJS::NodeJS::instance().moduleExports(module, m_duContext->url()) 0337 )), 0338 flags | CompletionOnlyLocal, 0339 CompletionItem::NoDecoration 0340 ); 0341 } 0342 0343 QList<CompletionTreeItemPointer> CodeCompletionContext::completionsInContext(const DUContextPointer& context, 0344 CompletionInContextFlags flags, 0345 CompletionItem::Decoration decoration) 0346 { 0347 QList<CompletionTreeItemPointer> items; 0348 DUChainReadLocker lock; 0349 0350 if (context) { 0351 const auto declarations = context->allDeclarations( 0352 CursorInRevision::invalid(), 0353 context->topContext(), 0354 !flags.testFlag(CompletionOnlyLocal) 0355 ); 0356 0357 for (const DeclarationDepthPair& decl : declarations) { 0358 DeclarationPointer declaration(decl.first); 0359 CompletionItem::Decoration decorationOfThisItem = decoration; 0360 0361 if (declaration->identifier() == globalImportIdentifier()) { 0362 continue; 0363 } if (declaration->qualifiedIdentifier().isEmpty()) { 0364 continue; 0365 } else if (context->owner() && ( 0366 context->owner()->kind() == Declaration::Namespace || 0367 context->owner()->kind() == Declaration::NamespaceAlias 0368 ) && decl.second != 0 && decl.second != 1001) { 0369 // Only show the local declarations of modules, or the declarations 0370 // immediately in its imported parent contexts (that are global 0371 // contexts, hence the distance of 1001). This prevents "String()", 0372 // "QtQuick1.0" and "builtins" from being listed when the user 0373 // types "PlasmaCore.". 0374 continue; 0375 } else if (decorationOfThisItem == CompletionItem::NoDecoration && 0376 declaration->abstractType() && 0377 declaration->abstractType()->whichType() == AbstractType::TypeFunction) { 0378 // Decorate function calls with brackets 0379 decorationOfThisItem = CompletionItem::Brackets; 0380 } else if (flags.testFlag(CompletionHideWrappers)) { 0381 auto* classDecl = dynamic_cast<ClassDeclaration*>(declaration.data()); 0382 0383 if (classDecl && classDecl->classType() == ClassDeclarationData::Interface) { 0384 continue; 0385 } 0386 } 0387 0388 items << CompletionTreeItemPointer(new CompletionItem(declaration, decl.second, decorationOfThisItem)); 0389 } 0390 } 0391 0392 return items; 0393 } 0394 0395 QList<CompletionTreeItemPointer> CodeCompletionContext::fieldCompletions(const QString& expression, 0396 CompletionItem::Decoration decoration) 0397 { 0398 // The statement given to this method ends with an expression that may identify 0399 // a declaration ("foo" in "test(1, 2, foo"). List the declarations of this 0400 // inner context 0401 DUContext* context = getInternalContext(declarationAtEndOfString(expression)); 0402 0403 if (context) { 0404 return completionsInContext(DUContextPointer(context), 0405 CompletionOnlyLocal, 0406 decoration); 0407 } else { 0408 return QList<CompletionTreeItemPointer>(); 0409 } 0410 } 0411 0412 Stack<CodeCompletionContext::ExpressionStackEntry> CodeCompletionContext::expressionStack(const QString& expression) 0413 { 0414 Stack<CodeCompletionContext::ExpressionStackEntry> stack; 0415 ExpressionStackEntry entry; 0416 QmlJS::Lexer lexer(nullptr); 0417 bool atEnd = false; 0418 0419 lexer.setCode(expression, 1, false); 0420 0421 entry.startPosition = 0; 0422 entry.operatorStart = 0; 0423 entry.operatorEnd = 0; 0424 entry.commas = 0; 0425 0426 stack.push(entry); 0427 0428 // NOTE: KDevelop uses 0-indexed columns while QMLJS uses 1-indexed columns 0429 while (!atEnd) { 0430 switch (lexer.lex()) { 0431 case QmlJSGrammar::EOF_SYMBOL: 0432 atEnd = true; 0433 break; 0434 case QmlJSGrammar::T_LBRACE: 0435 case QmlJSGrammar::T_LBRACKET: 0436 case QmlJSGrammar::T_LPAREN: 0437 entry.startPosition = lexer.tokenEndColumn() - 1; 0438 entry.operatorStart = entry.startPosition; 0439 entry.operatorEnd = entry.startPosition; 0440 entry.commas = 0; 0441 0442 stack.push(entry); 0443 break; 0444 case QmlJSGrammar::T_RBRACE: 0445 case QmlJSGrammar::T_RBRACKET: 0446 case QmlJSGrammar::T_RPAREN: 0447 if (stack.count() > 1) { 0448 stack.pop(); 0449 } 0450 break; 0451 case QmlJSGrammar::T_IDENTIFIER: 0452 case QmlJSGrammar::T_DOT: 0453 case QmlJSGrammar::T_THIS: 0454 break; 0455 case QmlJSGrammar::T_COMMA: 0456 stack.top().commas++; 0457 break; 0458 default: 0459 // The last operator of every sub-expression is stored on the stack 0460 // so that "A = foo." can know that attributes of foo having the same 0461 // type as A should be highlighted. 0462 stack.top().operatorStart = lexer.tokenStartColumn() - 1; 0463 stack.top().operatorEnd = lexer.tokenEndColumn() - 1; 0464 break; 0465 } 0466 } 0467 0468 return stack; 0469 } 0470 0471 DeclarationPointer CodeCompletionContext::declarationAtEndOfString(const QString& expression) 0472 { 0473 // Build the expression stack of expression and use the valid portion of the 0474 // top sub-expression to find the right-most declaration that can be found 0475 // in expression. 0476 QmlJS::Document::MutablePtr doc = QmlJS::Document::create(QStringLiteral("inline"), Dialect::JavaScript); 0477 ExpressionStackEntry topEntry = expressionStack(expression).top(); 0478 0479 doc->setSource(expression.mid(topEntry.operatorEnd + topEntry.commas)); 0480 doc->parseExpression(); 0481 0482 if (!doc || !doc->isParsedCorrectly()) { 0483 return DeclarationPointer(); 0484 } 0485 0486 // Use ExpressionVisitor to find the type (and associated declaration) of 0487 // the snippet that has been parsed. The inner context of the declaration 0488 // can be used to get the list of completions 0489 ExpressionVisitor visitor(m_duContext.data()); 0490 doc->ast()->accept(&visitor); 0491 0492 return visitor.lastDeclaration(); 0493 } 0494 }