/**************************************************************************** ** ** Copyright (C) 2015 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms and ** conditions see http://www.qt.io/terms-conditions. For further information ** use the contact form at http://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 or version 3 as published by the Free ** Software Foundation and appearing in the file LICENSE.LGPLv21 and ** LICENSE.LGPLv3 included in the packaging of this file. Please review the ** following information to ensure the GNU Lesser General Public License ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, The Qt Company gives you certain additional ** rights. These rights are described in The Qt Company LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ****************************************************************************/ #include "clangassistproposalitem.h" #include "activationsequenceprocessor.h" #include "clangassistproposal.h" #include "clangassistproposalmodel.h" #include "clangcompletionassistprocessor.h" #include "clangcompletioncontextanalyzer.h" #include "clangfunctionhintmodel.h" #include "clangutils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace ClangCodeModel { namespace Internal { using TextEditor::AssistProposalItem; namespace { const char SNIPPET_ICON_PATH[] = ":/texteditor/images/snippet.png"; QList toAssistProposalItems(const CodeCompletions &completions) { static CPlusPlus::Icons m_icons; // de-deduplicate QList result; bool signalCompletion = false; // TODO bool slotCompletion = false; // TODO const QIcon snippetIcon = QIcon(QLatin1String(SNIPPET_ICON_PATH)); QHash items; foreach (const CodeCompletion &ccr, completions) { if (ccr.text().isEmpty()) // TODO: Make isValid()? continue; if (signalCompletion && ccr.completionKind() != CodeCompletion::SignalCompletionKind) continue; if (slotCompletion && ccr.completionKind() != CodeCompletion::SlotCompletionKind) continue; const QString txt(ccr.text().toString()); ClangAssistProposalItem *item = items.value(txt, 0); if (item) { item->addOverload(ccr); } else { item = new ClangAssistProposalItem; items.insert(txt, item); item->setText(txt); item->setDetail(ccr.hint().toString()); item->setOrder(ccr.priority()); const QString snippet = ccr.snippet().toString(); if (!snippet.isEmpty()) item->setData(snippet); else item->setData(qVariantFromValue(ccr)); } // FIXME: show the effective accessebility instead of availability using ClangBackEnd::CodeCompletion; using CPlusPlus::Icons; switch (ccr.completionKind()) { case CodeCompletion::ClassCompletionKind: case CodeCompletion::TemplateClassCompletionKind: item->setIcon(m_icons.iconForType(Icons::ClassIconType)); break; case CodeCompletion::EnumerationCompletionKind: item->setIcon(m_icons.iconForType(Icons::EnumIconType)); break; case CodeCompletion::EnumeratorCompletionKind: item->setIcon(m_icons.iconForType(Icons::EnumeratorIconType)); break; case CodeCompletion::ConstructorCompletionKind: // fall through case CodeCompletion::DestructorCompletionKind: // fall through case CodeCompletion::FunctionCompletionKind: case CodeCompletion::TemplateFunctionCompletionKind: case CodeCompletion::ObjCMessageCompletionKind: switch (ccr.availability()) { case CodeCompletion::Available: case CodeCompletion::Deprecated: item->setIcon(m_icons.iconForType(Icons::FuncPublicIconType)); break; default: item->setIcon(m_icons.iconForType(Icons::FuncPrivateIconType)); break; } break; case CodeCompletion::SignalCompletionKind: item->setIcon(m_icons.iconForType(Icons::SignalIconType)); break; case CodeCompletion::SlotCompletionKind: switch (ccr.availability()) { case CodeCompletion::Available: case CodeCompletion::Deprecated: item->setIcon(m_icons.iconForType(Icons::SlotPublicIconType)); break; case CodeCompletion::NotAccessible: case CodeCompletion::NotAvailable: item->setIcon(m_icons.iconForType(Icons::SlotPrivateIconType)); break; } break; case CodeCompletion::NamespaceCompletionKind: item->setIcon(m_icons.iconForType(Icons::NamespaceIconType)); break; case CodeCompletion::PreProcessorCompletionKind: item->setIcon(m_icons.iconForType(Icons::MacroIconType)); break; case CodeCompletion::VariableCompletionKind: switch (ccr.availability()) { case CodeCompletion::Available: case CodeCompletion::Deprecated: item->setIcon(m_icons.iconForType(Icons::VarPublicIconType)); break; default: item->setIcon(m_icons.iconForType(Icons::VarPrivateIconType)); break; } break; case CodeCompletion::KeywordCompletionKind: item->setIcon(m_icons.iconForType(Icons::KeywordIconType)); break; case CodeCompletion::ClangSnippetKind: item->setIcon(snippetIcon); break; case CodeCompletion::Other: break; } } foreach (ClangAssistProposalItem *item, items.values()) result.append(item); return result; } bool isFunctionHintLikeCompletion(CodeCompletion::Kind kind) { return kind == CodeCompletion::FunctionCompletionKind || kind == CodeCompletion::ConstructorCompletionKind || kind == CodeCompletion::DestructorCompletionKind || kind == CodeCompletion::SignalCompletionKind || kind == CodeCompletion::SlotCompletionKind; } QVector matchingFunctionCompletions(const QVector completions, const QString &functionName) { QVector matching; foreach (const CodeCompletion &completion, completions) { if (isFunctionHintLikeCompletion(completion.completionKind()) && completion.text().toString() == functionName) { matching.append(completion); } } return matching; } } // Anonymous using namespace CPlusPlus; using namespace TextEditor; ClangCompletionAssistProcessor::ClangCompletionAssistProcessor() : m_completionOperator(T_EOF_SYMBOL) { } ClangCompletionAssistProcessor::~ClangCompletionAssistProcessor() { } IAssistProposal *ClangCompletionAssistProcessor::perform(const AssistInterface *interface) { m_interface.reset(static_cast(interface)); if (interface->reason() != ExplicitlyInvoked && !accepts()) return 0; return startCompletionHelper(); // == 0 if results are calculated asynchronously } void ClangCompletionAssistProcessor::asyncCompletionsAvailable(const CodeCompletions &completions) { switch (m_sentRequestType) { case CompletionRequestType::NormalCompletion: onCompletionsAvailable(completions); break; case CompletionRequestType::FunctionHintCompletion: onFunctionHintCompletionsAvailable(completions); break; default: QTC_CHECK(!"Unhandled ClangCompletionAssistProcessor::CompletionRequestType"); break; } } const TextEditorWidget *ClangCompletionAssistProcessor::textEditorWidget() const { return m_interface->textEditorWidget(); } /// Seach backwards in the document starting from pos to find the first opening /// parenthesis. Nested parenthesis are skipped. static int findOpenParen(QTextDocument *document, int start) { unsigned parenCount = 1; for (int position = start; position >= 0; --position) { const QChar ch = document->characterAt(position); if (ch == QLatin1Char('(')) { --parenCount; if (parenCount == 0) return position; } else if (ch == QLatin1Char(')')) { ++parenCount; } } return -1; } static QByteArray modifyInput(QTextDocument *doc, int endOfExpression) { int comma = endOfExpression; while (comma > 0) { const QChar ch = doc->characterAt(comma); if (ch == QLatin1Char(',')) break; if (ch == QLatin1Char(';') || ch == QLatin1Char('{') || ch == QLatin1Char('}')) { // Safety net: we don't seem to have "connect(pointer, SIGNAL(" as // input, so stop searching. comma = -1; break; } --comma; } if (comma < 0) return QByteArray(); const int openBrace = findOpenParen(doc, comma); if (openBrace < 0) return QByteArray(); QByteArray modifiedInput = doc->toPlainText().toUtf8(); const int len = endOfExpression - comma; QByteArray replacement(len - 4, ' '); replacement.append(")->"); modifiedInput.replace(comma, len, replacement); modifiedInput.insert(openBrace, '('); return modifiedInput; } IAssistProposal *ClangCompletionAssistProcessor::startCompletionHelper() { sendFileContent(Utils::projectFilePathForFile(m_interface->fileName()), QByteArray()); // TODO: Remoe ClangCompletionContextAnalyzer analyzer(m_interface.data(), m_interface->languageFeatures()); analyzer.analyze(); m_completionOperator = analyzer.completionOperator(); m_positionForProposal = analyzer.positionForProposal(); QByteArray modifiedFileContent; const ClangCompletionContextAnalyzer::CompletionAction action = analyzer.completionAction(); switch (action) { case ClangCompletionContextAnalyzer::CompleteDoxygenKeyword: if (completeDoxygenKeywords()) return createProposal(); break; case ClangCompletionContextAnalyzer::CompleteIncludePath: if (completeInclude(analyzer.positionEndOfExpression())) return createProposal(); break; case ClangCompletionContextAnalyzer::CompletePreprocessorDirective: if (completePreprocessorDirectives()) return createProposal(); break; case ClangCompletionContextAnalyzer::CompleteSignal: case ClangCompletionContextAnalyzer::CompleteSlot: modifiedFileContent = modifyInput(m_interface->textDocument(), analyzer.positionEndOfExpression()); // Fall through! case ClangCompletionContextAnalyzer::PassThroughToLibClang: { m_addSnippets = m_completionOperator == T_EOF_SYMBOL; m_sentRequestType = NormalCompletion; sendCompletionRequest(analyzer.positionForClang(), modifiedFileContent); break; } case ClangCompletionContextAnalyzer::PassThroughToLibClangAfterLeftParen: { m_sentRequestType = FunctionHintCompletion; m_functionName = analyzer.functionName(); sendCompletionRequest(analyzer.positionForClang(), QByteArray()); break; } default: break; } return 0; } // TODO: Extract duplicated logic from InternalCppCompletionAssistProcessor::startOfOperator int ClangCompletionAssistProcessor::startOfOperator(int positionInDocument, unsigned *kind, bool wantFunctionCall) const { auto activationSequence = m_interface->textAt(positionInDocument - 3, 3); ActivationSequenceProcessor activationSequenceProcessor(activationSequence, positionInDocument, wantFunctionCall); *kind = activationSequenceProcessor.completionKind(); int start = activationSequenceProcessor.position(); if (start != positionInDocument) { QTextCursor tc(m_interface->textDocument()); tc.setPosition(positionInDocument); // Include completion: make sure the quote character is the first one on the line if (*kind == T_STRING_LITERAL) { QTextCursor s = tc; s.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor); QString sel = s.selectedText(); if (sel.indexOf(QLatin1Char('"')) < sel.length() - 1) { *kind = T_EOF_SYMBOL; start = positionInDocument; } } if (*kind == T_COMMA) { ExpressionUnderCursor expressionUnderCursor(m_interface->languageFeatures()); if (expressionUnderCursor.startOfFunctionCall(tc) == -1) { *kind = T_EOF_SYMBOL; start = positionInDocument; } } SimpleLexer tokenize; tokenize.setLanguageFeatures(m_interface->languageFeatures()); tokenize.setSkipComments(false); const Tokens &tokens = tokenize(tc.block().text(), BackwardsScanner::previousBlockState(tc.block())); const int tokenIdx = SimpleLexer::tokenBefore(tokens, qMax(0, tc.positionInBlock() - 1)); // get the token at the left of the cursor const Token tk = (tokenIdx == -1) ? Token() : tokens.at(tokenIdx); if (*kind == T_DOXY_COMMENT && !(tk.is(T_DOXY_COMMENT) || tk.is(T_CPP_DOXY_COMMENT))) { *kind = T_EOF_SYMBOL; start = positionInDocument; } // Don't complete in comments or strings, but still check for include completion else if (tk.is(T_COMMENT) || tk.is(T_CPP_COMMENT) || tk.is(T_CPP_DOXY_COMMENT) || tk.is(T_DOXY_COMMENT) || (tk.isLiteral() && (*kind != T_STRING_LITERAL && *kind != T_ANGLE_STRING_LITERAL && *kind != T_SLASH))) { *kind = T_EOF_SYMBOL; start = positionInDocument; } // Include completion: can be triggered by slash, but only in a string else if (*kind == T_SLASH && (tk.isNot(T_STRING_LITERAL) && tk.isNot(T_ANGLE_STRING_LITERAL))) { *kind = T_EOF_SYMBOL; start = positionInDocument; } else if (*kind == T_LPAREN) { if (tokenIdx > 0) { const Token &previousToken = tokens.at(tokenIdx - 1); // look at the token at the left of T_LPAREN switch (previousToken.kind()) { case T_IDENTIFIER: case T_GREATER: case T_SIGNAL: case T_SLOT: break; // good default: // that's a bad token :) *kind = T_EOF_SYMBOL; start = positionInDocument; } } } // Check for include preprocessor directive else if (*kind == T_STRING_LITERAL || *kind == T_ANGLE_STRING_LITERAL || *kind == T_SLASH) { bool include = false; if (tokens.size() >= 3) { if (tokens.at(0).is(T_POUND) && tokens.at(1).is(T_IDENTIFIER) && (tokens.at(2).is(T_STRING_LITERAL) || tokens.at(2).is(T_ANGLE_STRING_LITERAL))) { const Token &directiveToken = tokens.at(1); QString directive = tc.block().text().mid(directiveToken.bytesBegin(), directiveToken.bytes()); if (directive == QLatin1String("include") || directive == QLatin1String("include_next") || directive == QLatin1String("import")) { include = true; } } } if (!include) { *kind = T_EOF_SYMBOL; start = positionInDocument; } } } return start; } int ClangCompletionAssistProcessor::findStartOfName(int pos) const { if (pos == -1) pos = m_interface->position(); QChar chr; // Skip to the start of a name do { chr = m_interface->characterAt(--pos); } while (chr.isLetterOrNumber() || chr == QLatin1Char('_')); return pos + 1; } bool ClangCompletionAssistProcessor::accepts() const { const int pos = m_interface->position(); unsigned token = T_EOF_SYMBOL; const int start = startOfOperator(pos, &token, /*want function call=*/ true); if (start != pos) { if (token == T_POUND) { const int column = pos - m_interface->textDocument()->findBlock(start).position(); if (column != 1) return false; } return true; } else { // Trigger completion after three characters of a name have been typed, when not editing an existing name QChar characterUnderCursor = m_interface->characterAt(pos); if (!characterUnderCursor.isLetterOrNumber() && characterUnderCursor != QLatin1Char('_')) { const int startOfName = findStartOfName(pos); if (pos - startOfName >= 3) { const QChar firstCharacter = m_interface->characterAt(startOfName); if (firstCharacter.isLetter() || firstCharacter == QLatin1Char('_')) { // Finally check that we're not inside a comment or string (code copied from startOfOperator) QTextCursor tc(m_interface->textDocument()); tc.setPosition(pos); SimpleLexer tokenize; LanguageFeatures lf = tokenize.languageFeatures(); lf.qtMocRunEnabled = true; lf.objCEnabled = true; tokenize.setLanguageFeatures(lf); tokenize.setSkipComments(false); const Tokens &tokens = tokenize(tc.block().text(), BackwardsScanner::previousBlockState(tc.block())); const int tokenIdx = SimpleLexer::tokenBefore(tokens, qMax(0, tc.positionInBlock() - 1)); const Token tk = (tokenIdx == -1) ? Token() : tokens.at(tokenIdx); if (!tk.isComment() && !tk.isLiteral()) { return true; } else if (tk.isLiteral() && tokens.size() == 3 && tokens.at(0).kind() == T_POUND && tokens.at(1).kind() == T_IDENTIFIER) { const QString &line = tc.block().text(); const Token &idToken = tokens.at(1); const QStringRef &identifier = line.midRef(idToken.bytesBegin(), idToken.bytesEnd() - idToken.bytesBegin()); if (identifier == QLatin1String("include") || identifier == QLatin1String("include_next") || (m_interface->objcEnabled() && identifier == QLatin1String("import"))) { return true; } } } } } } return false; } /** * @brief Creates completion proposals for #include and given cursor * @param cursor - cursor placed after opening bracked or quote * @return false if completions list is empty */ bool ClangCompletionAssistProcessor::completeInclude(const QTextCursor &cursor) { QString directoryPrefix; if (m_completionOperator == T_SLASH) { QTextCursor c = cursor; c.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor); QString sel = c.selectedText(); int startCharPos = sel.indexOf(QLatin1Char('"')); if (startCharPos == -1) { startCharPos = sel.indexOf(QLatin1Char('<')); m_completionOperator = T_ANGLE_STRING_LITERAL; } else { m_completionOperator = T_STRING_LITERAL; } if (startCharPos != -1) directoryPrefix = sel.mid(startCharPos + 1, sel.length() - 1); } // Make completion for all relevant includes CppTools::ProjectPart::HeaderPaths headerPaths = m_interface->headerPaths(); const CppTools::ProjectPart::HeaderPath currentFilePath(QFileInfo(m_interface->fileName()).path(), CppTools::ProjectPart::HeaderPath::IncludePath); if (!headerPaths.contains(currentFilePath)) headerPaths.append(currentFilePath); ::Utils::MimeDatabase mdb; const ::Utils::MimeType mimeType = mdb.mimeTypeForName(QLatin1String("text/x-c++hdr")); const QStringList suffixes = mimeType.suffixes(); foreach (const CppTools::ProjectPart::HeaderPath &headerPath, headerPaths) { QString realPath = headerPath.path; if (!directoryPrefix.isEmpty()) { realPath += QLatin1Char('/'); realPath += directoryPrefix; if (headerPath.isFrameworkPath()) realPath += QLatin1String(".framework/Headers"); } completeIncludePath(realPath, suffixes); } return !m_completions.isEmpty(); } bool ClangCompletionAssistProcessor::completeInclude(int position) { QTextCursor textCursor(m_interface->textDocument()); // TODO: Simplify, move into function textCursor.setPosition(position); return completeInclude(textCursor); } /** * @brief Adds #include completion proposals using given include path * @param realPath - one of directories where compiler searches includes * @param suffixes - file suffixes for C/C++ header files */ void ClangCompletionAssistProcessor::completeIncludePath(const QString &realPath, const QStringList &suffixes) { QDirIterator i(realPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); //: Parent folder for proposed #include completion const QString hint = tr("Location: %1").arg(QDir::toNativeSeparators(QDir::cleanPath(realPath))); while (i.hasNext()) { const QString fileName = i.next(); const QFileInfo fileInfo = i.fileInfo(); const QString suffix = fileInfo.suffix(); if (suffix.isEmpty() || suffixes.contains(suffix)) { QString text = fileName.mid(realPath.length() + 1); if (fileInfo.isDir()) text += QLatin1Char('/'); ClangAssistProposalItem *item = new ClangAssistProposalItem; item->setText(text); item->setDetail(hint); item->setIcon(m_icons.keywordIcon()); item->keepCompletionOperator(m_completionOperator); m_completions.append(item); } } } bool ClangCompletionAssistProcessor::completePreprocessorDirectives() { foreach (const QString &preprocessorCompletion, m_preprocessorCompletions) addCompletionItem(preprocessorCompletion, m_icons.iconForType(Icons::MacroIconType)); if (m_interface->objcEnabled()) addCompletionItem(QLatin1String("import"), m_icons.iconForType(Icons::MacroIconType)); return !m_completions.isEmpty(); } bool ClangCompletionAssistProcessor::completeDoxygenKeywords() { for (int i = 1; i < CppTools::T_DOXY_LAST_TAG; ++i) addCompletionItem(QString::fromLatin1(CppTools::doxygenTagSpell(i)), m_icons.keywordIcon()); return !m_completions.isEmpty(); } void ClangCompletionAssistProcessor::addCompletionItem(const QString &text, const QIcon &icon, int order, const QVariant &data) { ClangAssistProposalItem *item = new ClangAssistProposalItem; item->setText(text); item->setIcon(icon); item->setOrder(order); item->setData(data); item->keepCompletionOperator(m_completionOperator); m_completions.append(item); } void ClangCompletionAssistProcessor::sendFileContent(const QString &projectFilePath, const QByteArray &modifiedFileContent) { const QString filePath = m_interface->fileName(); const QByteArray unsavedContent = modifiedFileContent.isEmpty() ? m_interface->textDocument()->toPlainText().toUtf8() : modifiedFileContent; const bool hasUnsavedContent = true; // TODO IpcCommunicator &ipcCommunicator = m_interface->ipcCommunicator(); ipcCommunicator.registerFilesForCodeCompletion( {ClangBackEnd::FileContainer(filePath, projectFilePath, Utf8String::fromByteArray(unsavedContent), hasUnsavedContent)}); } void ClangCompletionAssistProcessor::sendCompletionRequest(int position, const QByteArray &modifiedFileContent) { int line, column; TextEditor::Convenience::convertPosition(m_interface->textDocument(), position, &line, &column); ++column; const QString filePath = m_interface->fileName(); const QString projectFilePath = Utils::projectFilePathForFile(filePath); sendFileContent(projectFilePath, modifiedFileContent); m_interface->ipcCommunicator().completeCode(this, filePath, line, column, projectFilePath); } TextEditor::IAssistProposal *ClangCompletionAssistProcessor::createProposal() const { ClangAssistProposalModel *model = new ClangAssistProposalModel; model->loadContent(m_completions); return new ClangAssistProposal(m_positionForProposal, model); } void ClangCompletionAssistProcessor::onCompletionsAvailable(const CodeCompletions &completions) { QTC_CHECK(m_completions.isEmpty()); m_completions = toAssistProposalItems(completions); if (m_addSnippets) addSnippets(); setAsyncProposalAvailable(createProposal()); } void ClangCompletionAssistProcessor::onFunctionHintCompletionsAvailable( const CodeCompletions &completions) { QTC_CHECK(!m_functionName.isEmpty()); const auto relevantCompletions = matchingFunctionCompletions(completions, m_functionName); if (!relevantCompletions.isEmpty()) { TextEditor::IFunctionHintProposalModel *model = new ClangFunctionHintModel(relevantCompletions); TextEditor::FunctionHintProposal *proposal = new FunctionHintProposal(m_positionForProposal, model); setAsyncProposalAvailable(proposal); } else { QTC_CHECK(!"Function completion failed. Would fallback to global completion here..."); // TODO: If we need this, the processor can't be deleted in IpcClient. } } } // namespace Internal } // namespace ClangCodeModel