Files
qt-creator/src/plugins/clangcodemodel/clangcompletion.cpp

1170 lines
44 KiB
C++
Raw Normal View History

/****************************************************************************
**
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** 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 Digia. For licensing terms and
** conditions see http://www.qt.io/licensing. 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, Digia gives you certain additional
** rights. These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
****************************************************************************/
#include "clangcompletion.h"
#include "clangutils.h"
#include "pchmanager.h"
#include <coreplugin/icore.h>
#include <coreplugin/idocument.h>
#include <coreplugin/mimedatabase.h>
#include <cplusplus/BackwardsScanner.h>
#include <cplusplus/ExpressionUnderCursor.h>
#include <cplusplus/Token.h>
#include <cplusplus/MatchingText.h>
#include <cppeditor/cppeditorconstants.h>
#include <cpptools/cppdoxygen.h>
#include <cpptools/cppmodelmanager.h>
#include <cpptools/cppworkingcopy.h>
#include <texteditor/texteditor.h>
#include <texteditor/convenience.h>
#include <texteditor/codeassist/genericproposalmodel.h>
#include <texteditor/codeassist/assistproposalitem.h>
#include <texteditor/codeassist/functionhintproposal.h>
#include <texteditor/codeassist/genericproposal.h>
#include <texteditor/codeassist/ifunctionhintproposalmodel.h>
#include <texteditor/texteditorsettings.h>
#include <texteditor/completionsettings.h>
#include <utils/algorithm.h>
#include <QCoreApplication>
#include <QDirIterator>
#include <QLoggingCategory>
#include <QTextCursor>
#include <QTextDocument>
using namespace ClangCodeModel;
using namespace ClangCodeModel::Internal;
using namespace CPlusPlus;
using namespace CppTools;
using namespace TextEditor;
static const char SNIPPET_ICON_PATH[] = ":/texteditor/images/snippet.png";
static Q_LOGGING_CATEGORY(log, "qtc.clangcodemodel.completion")
namespace {
int activationSequenceChar(const QChar &ch,
const QChar &ch2,
const QChar &ch3,
unsigned *kind,
bool wantFunctionCall)
{
int referencePosition = 0;
int completionKind = T_EOF_SYMBOL;
switch (ch.toLatin1()) {
case '.':
if (ch2 != QLatin1Char('.')) {
completionKind = T_DOT;
referencePosition = 1;
}
break;
case ',':
completionKind = T_COMMA;
referencePosition = 1;
break;
case '(':
if (wantFunctionCall) {
completionKind = T_LPAREN;
referencePosition = 1;
}
break;
case ':':
if (ch3 != QLatin1Char(':') && ch2 == QLatin1Char(':')) {
completionKind = T_COLON_COLON;
referencePosition = 2;
}
break;
case '>':
if (ch2 == QLatin1Char('-')) {
completionKind = T_ARROW;
referencePosition = 2;
}
break;
case '*':
if (ch2 == QLatin1Char('.')) {
completionKind = T_DOT_STAR;
referencePosition = 2;
} else if (ch3 == QLatin1Char('-') && ch2 == QLatin1Char('>')) {
completionKind = T_ARROW_STAR;
referencePosition = 3;
}
break;
case '\\':
case '@':
if (ch2.isNull() || ch2.isSpace()) {
completionKind = T_DOXY_COMMENT;
referencePosition = 1;
}
break;
case '<':
completionKind = T_ANGLE_STRING_LITERAL;
referencePosition = 1;
break;
case '"':
completionKind = T_STRING_LITERAL;
referencePosition = 1;
break;
case '/':
completionKind = T_SLASH;
referencePosition = 1;
break;
case '#':
completionKind = T_POUND;
referencePosition = 1;
break;
}
if (kind)
*kind = completionKind;
return referencePosition;
}
static QList<CodeCompletionResult> unfilteredCompletion(const ClangCompletionAssistInterface* interface,
const QString &fileName,
unsigned line, unsigned column,
QByteArray modifiedInput = QByteArray(),
bool isSignalSlotCompletion = false)
{
ClangCompleter::Ptr wrapper = interface->clangWrapper();
QMutexLocker lock(wrapper->mutex());
//### TODO: check if we're cancelled after we managed to acquire the mutex
wrapper->setFileName(fileName);
wrapper->setOptions(interface->options());
wrapper->setSignalSlotCompletion(isSignalSlotCompletion);
UnsavedFiles unsavedFiles = interface->unsavedFiles();
if (!modifiedInput.isEmpty())
unsavedFiles.insert(fileName, modifiedInput);
qCDebug(log) << "Starting completion...";
QTime t; t.start();
QList<CodeCompletionResult> result = wrapper->codeCompleteAt(line, column + 1, unsavedFiles);
::Utils::sort(result);
qCDebug(log) << "Completion done in" << t.elapsed() << "ms, with" << result.count() << "items.";
return result;
}
} // Anonymous
namespace ClangCodeModel {
namespace Internal {
// -----------------------------
// ClangCompletionAssistProvider
// -----------------------------
ClangCompletionAssistProvider::ClangCompletionAssistProvider()
: m_clangCompletionWrapper(new ClangCodeModel::ClangCompleter)
{
}
IAssistProcessor *ClangCompletionAssistProvider::createProcessor() const
{
return new ClangCompletionAssistProcessor;
}
AssistInterface *ClangCompletionAssistProvider::createAssistInterface(
const QString &filePath,
QTextDocument *document, bool isObjCEnabled, int position, AssistReason reason) const
{
Q_UNUSED(isObjCEnabled);
CppModelManager *modelManager = CppModelManager::instance();
C++: Base parsing on editor document instead of widget This mainly takes CppEditorSupport apart. * Parsing is now invoked by CPPEditorDocument itself by listening to QTextDocument::contentsChanged(). * Upon construction and destruction CPPEditorDocument creates and deletes an EditorDocumentHandle for (un)registration in the model manager. This handle provides everything to generate the working copy and to access the editor document processor. * A CPPEditorDocument owns a BaseEditorDocumentProcessor instance that controls parsing, semantic info recalculation and the semantic highlighting for the document. This is more or less what is left from CppEditorSupport and can be considered as the backend of a CPPEditorDocument. CPPEditorDocument itself is quite small. * BuiltinEditorDocumentProcessor and ClangEditorDocumentProcessor derive from BaseEditorDocumentProcessor and implement the gaps. * Since the semantic info calculation was bound to the widget, it also calculated the local uses, which depend on the cursor position. This calculation got moved into the extracted class UseSeletionsUpdater in the cppeditor plugin, which is run once the cursor position changes or the semantic info document is updated. * Some more logic got extracted: - SemanticInfoUpdater (logic was in CppEditorSupport) - SemanticHighlighter (logic was in CppEditorSupport) * The *Parser and *Processor classes can be easily accessed by the static function get(). * CppHighlightingSupport is gone since it turned out to be useless. * The editor dependency in CompletionAssistProviders is gone since we actually only need the file path now. Change-Id: I49d3a7bd138c5ed9620123e34480772535156508 Reviewed-by: Orgad Shaneh <orgads@gmail.com> Reviewed-by: Erik Verbruggen <erik.verbruggen@digia.com>
2014-08-19 15:59:29 +02:00
QList<ProjectPart::Ptr> parts = modelManager->projectPart(filePath);
if (parts.isEmpty())
parts += modelManager->fallbackProjectPart();
ProjectPart::HeaderPaths headerPaths;
QStringList options;
PchInfo::Ptr pchInfo;
foreach (ProjectPart::Ptr part, parts) {
if (part.isNull())
continue;
C++: Base parsing on editor document instead of widget This mainly takes CppEditorSupport apart. * Parsing is now invoked by CPPEditorDocument itself by listening to QTextDocument::contentsChanged(). * Upon construction and destruction CPPEditorDocument creates and deletes an EditorDocumentHandle for (un)registration in the model manager. This handle provides everything to generate the working copy and to access the editor document processor. * A CPPEditorDocument owns a BaseEditorDocumentProcessor instance that controls parsing, semantic info recalculation and the semantic highlighting for the document. This is more or less what is left from CppEditorSupport and can be considered as the backend of a CPPEditorDocument. CPPEditorDocument itself is quite small. * BuiltinEditorDocumentProcessor and ClangEditorDocumentProcessor derive from BaseEditorDocumentProcessor and implement the gaps. * Since the semantic info calculation was bound to the widget, it also calculated the local uses, which depend on the cursor position. This calculation got moved into the extracted class UseSeletionsUpdater in the cppeditor plugin, which is run once the cursor position changes or the semantic info document is updated. * Some more logic got extracted: - SemanticInfoUpdater (logic was in CppEditorSupport) - SemanticHighlighter (logic was in CppEditorSupport) * The *Parser and *Processor classes can be easily accessed by the static function get(). * CppHighlightingSupport is gone since it turned out to be useless. * The editor dependency in CompletionAssistProviders is gone since we actually only need the file path now. Change-Id: I49d3a7bd138c5ed9620123e34480772535156508 Reviewed-by: Orgad Shaneh <orgads@gmail.com> Reviewed-by: Erik Verbruggen <erik.verbruggen@digia.com>
2014-08-19 15:59:29 +02:00
options = ClangCodeModel::Utils::createClangOptions(part, filePath);
pchInfo = PchManager::instance()->pchInfo(part);
if (!pchInfo.isNull())
options.append(ClangCodeModel::Utils::createPCHInclusionOptions(pchInfo->fileName()));
headerPaths = part->headerPaths;
break;
}
return new ClangCodeModel::ClangCompletionAssistInterface(
m_clangCompletionWrapper,
C++: Base parsing on editor document instead of widget This mainly takes CppEditorSupport apart. * Parsing is now invoked by CPPEditorDocument itself by listening to QTextDocument::contentsChanged(). * Upon construction and destruction CPPEditorDocument creates and deletes an EditorDocumentHandle for (un)registration in the model manager. This handle provides everything to generate the working copy and to access the editor document processor. * A CPPEditorDocument owns a BaseEditorDocumentProcessor instance that controls parsing, semantic info recalculation and the semantic highlighting for the document. This is more or less what is left from CppEditorSupport and can be considered as the backend of a CPPEditorDocument. CPPEditorDocument itself is quite small. * BuiltinEditorDocumentProcessor and ClangEditorDocumentProcessor derive from BaseEditorDocumentProcessor and implement the gaps. * Since the semantic info calculation was bound to the widget, it also calculated the local uses, which depend on the cursor position. This calculation got moved into the extracted class UseSeletionsUpdater in the cppeditor plugin, which is run once the cursor position changes or the semantic info document is updated. * Some more logic got extracted: - SemanticInfoUpdater (logic was in CppEditorSupport) - SemanticHighlighter (logic was in CppEditorSupport) * The *Parser and *Processor classes can be easily accessed by the static function get(). * CppHighlightingSupport is gone since it turned out to be useless. * The editor dependency in CompletionAssistProviders is gone since we actually only need the file path now. Change-Id: I49d3a7bd138c5ed9620123e34480772535156508 Reviewed-by: Orgad Shaneh <orgads@gmail.com> Reviewed-by: Erik Verbruggen <erik.verbruggen@digia.com>
2014-08-19 15:59:29 +02:00
document, position, filePath, reason,
options, headerPaths, pchInfo);
}
// ------------------------
// ClangAssistProposalModel
// ------------------------
class ClangAssistProposalModel : public GenericProposalModel
{
public:
ClangAssistProposalModel()
: m_sortable(false)
, m_completionOperator(T_EOF_SYMBOL)
, m_replaceDotForArrow(false)
{}
virtual bool isSortable(const QString &prefix) const;
bool m_sortable;
unsigned m_completionOperator;
bool m_replaceDotForArrow;
};
// -------------------
// ClangAssistProposal
// -------------------
class ClangAssistProposal : public GenericProposal
{
public:
ClangAssistProposal(int cursorPos, GenericProposalModel *model)
: GenericProposal(cursorPos, model)
, m_replaceDotForArrow(static_cast<ClangAssistProposalModel *>(model)->m_replaceDotForArrow)
{}
virtual bool isCorrective() const { return m_replaceDotForArrow; }
virtual void makeCorrection(TextEditorWidget *editorWidget)
{
editorWidget->setCursorPosition(basePosition() - 1);
editorWidget->replace(1, QLatin1String("->"));
moveBasePosition(1);
}
private:
bool m_replaceDotForArrow;
};
// ----------------------
// ClangFunctionHintModel
// ----------------------
class ClangFunctionHintModel : public IFunctionHintProposalModel
{
public:
ClangFunctionHintModel(const QList<CodeCompletionResult> functionSymbols)
: m_functionSymbols(functionSymbols)
, m_currentArg(-1)
{}
virtual void reset() {}
virtual int size() const { return m_functionSymbols.size(); }
virtual QString text(int index) const;
virtual int activeArgument(const QString &prefix) const;
private:
QList<ClangCodeModel::CodeCompletionResult> m_functionSymbols;
mutable int m_currentArg;
};
QString ClangFunctionHintModel::text(int index) const
{
#if 0
// TODO: add the boldening to the result
Overview overview;
overview.setShowReturnTypes(true);
overview.setShowArgumentNames(true);
overview.setMarkedArgument(m_currentArg + 1);
Function *f = m_functionSymbols.at(index);
const QString prettyMethod = overview(f->type(), f->name());
const int begin = overview.markedArgumentBegin();
const int end = overview.markedArgumentEnd();
QString hintText;
hintText += prettyMethod.left(begin).toHtmlEscaped());
hintText += "<b>";
hintText += prettyMethod.mid(begin, end - begin).toHtmlEscaped());
hintText += "</b>";
hintText += prettyMethod.mid(end).toHtmlEscaped());
return hintText;
#endif
return m_functionSymbols.at(index).hint();
}
int ClangFunctionHintModel::activeArgument(const QString &prefix) const
{
int argnr = 0;
int parcount = 0;
SimpleLexer tokenize;
Tokens tokens = tokenize(prefix);
for (int i = 0; i < tokens.count(); ++i) {
const CPlusPlus::Token &tk = tokens.at(i);
if (tk.is(T_LPAREN))
++parcount;
else if (tk.is(T_RPAREN))
--parcount;
else if (! parcount && tk.is(T_COMMA))
++argnr;
}
if (parcount < 0)
return -1;
if (argnr != m_currentArg)
m_currentArg = argnr;
return argnr;
}
class ClangAssistProposalItem : public AssistProposalItem
{
public:
ClangAssistProposalItem() {}
virtual bool prematurelyApplies(const QChar &c) const;
virtual void applyContextualContent(TextEditorWidget *editorWidget,
int basePosition) const;
void keepCompletionOperator(unsigned compOp) { m_completionOperator = compOp; }
bool isOverloaded() const
{ return !m_overloads.isEmpty(); }
void addOverload(const CodeCompletionResult &ccr)
{ m_overloads.append(ccr); }
CodeCompletionResult originalItem() const
{
const QVariant &v = data();
if (v.canConvert<CodeCompletionResult>())
return v.value<CodeCompletionResult>();
else
return CodeCompletionResult();
}
bool isCodeCompletionResult() const
{ return data().canConvert<CodeCompletionResult>(); }
private:
unsigned m_completionOperator;
mutable QChar m_typedChar;
QList<CodeCompletionResult> m_overloads;
};
/// @return True, because clang always returns priorities for sorting
bool ClangAssistProposalModel::isSortable(const QString &prefix) const
{
Q_UNUSED(prefix)
return true;
}
} // namespace Internal
} // namespace ClangCodeModel
bool ClangAssistProposalItem::prematurelyApplies(const QChar &typedChar) const
{
bool ok = false;
if (m_completionOperator == T_SIGNAL || m_completionOperator == T_SLOT)
ok = QString::fromLatin1("(,").contains(typedChar);
else if (m_completionOperator == T_STRING_LITERAL || m_completionOperator == T_ANGLE_STRING_LITERAL)
ok = (typedChar == QLatin1Char('/')) && text().endsWith(QLatin1Char('/'));
else if (!isCodeCompletionResult())
ok = (typedChar == QLatin1Char('(')); /* && data().canConvert<CompleteFunctionDeclaration>()*/ //###
else if (originalItem().completionKind() == CodeCompletionResult::ObjCMessageCompletionKind)
ok = QString::fromLatin1(";.,").contains(typedChar);
else
ok = QString::fromLatin1(";.,:(").contains(typedChar);
if (ok)
m_typedChar = typedChar;
return ok;
}
void ClangAssistProposalItem::applyContextualContent(TextEditorWidget *editorWidget,
int basePosition) const
{
const CodeCompletionResult ccr = originalItem();
QString toInsert = text();
QString extraChars;
int extraLength = 0;
int cursorOffset = 0;
bool autoParenthesesEnabled = true;
if (m_completionOperator == T_SIGNAL || m_completionOperator == T_SLOT) {
extraChars += QLatin1Char(')');
if (m_typedChar == QLatin1Char('(')) // Eat the opening parenthesis
m_typedChar = QChar();
} else if (m_completionOperator == T_STRING_LITERAL || m_completionOperator == T_ANGLE_STRING_LITERAL) {
if (!toInsert.endsWith(QLatin1Char('/'))) {
extraChars += QLatin1Char((m_completionOperator == T_ANGLE_STRING_LITERAL) ? '>' : '"');
} else {
if (m_typedChar == QLatin1Char('/')) // Eat the slash
m_typedChar = QChar();
}
} else if (ccr.isValid()) {
const CompletionSettings &completionSettings =
TextEditorSettings::instance()->completionSettings();
const bool autoInsertBrackets = completionSettings.m_autoInsertBrackets;
if (autoInsertBrackets &&
(ccr.completionKind() == CodeCompletionResult::FunctionCompletionKind
|| ccr.completionKind() == CodeCompletionResult::DestructorCompletionKind
|| ccr.completionKind() == CodeCompletionResult::SignalCompletionKind
|| ccr.completionKind() == CodeCompletionResult::SlotCompletionKind)) {
// When the user typed the opening parenthesis, he'll likely also type the closing one,
// in which case it would be annoying if we put the cursor after the already automatically
// inserted closing parenthesis.
const bool skipClosingParenthesis = m_typedChar != QLatin1Char('(');
if (completionSettings.m_spaceAfterFunctionName)
extraChars += QLatin1Char(' ');
extraChars += QLatin1Char('(');
if (m_typedChar == QLatin1Char('('))
m_typedChar = QChar();
// If the function doesn't return anything, automatically place the semicolon,
// unless we're doing a scope completion (then it might be function definition).
const QChar characterAtCursor = editorWidget->characterAt(editorWidget->position());
bool endWithSemicolon = m_typedChar == QLatin1Char(';')/*
|| (function->returnType()->isVoidType() && m_completionOperator != T_COLON_COLON)*/; //###
const QChar semicolon = m_typedChar.isNull() ? QLatin1Char(';') : m_typedChar;
if (endWithSemicolon && characterAtCursor == semicolon) {
endWithSemicolon = false;
m_typedChar = QChar();
}
// If the function takes no arguments, automatically place the closing parenthesis
if (!isOverloaded() && !ccr.hasParameters() && skipClosingParenthesis) {
extraChars += QLatin1Char(')');
if (endWithSemicolon) {
extraChars += semicolon;
m_typedChar = QChar();
}
} else if (autoParenthesesEnabled) {
const QChar lookAhead = editorWidget->characterAt(editorWidget->position() + 1);
if (MatchingText::shouldInsertMatchingText(lookAhead)) {
extraChars += QLatin1Char(')');
--cursorOffset;
if (endWithSemicolon) {
extraChars += semicolon;
--cursorOffset;
m_typedChar = QChar();
}
}
}
}
#if 0
if (autoInsertBrackets && data().canConvert<CompleteFunctionDeclaration>()) {
if (m_typedChar == QLatin1Char('('))
m_typedChar = QChar();
// everything from the closing parenthesis on are extra chars, to
// make sure an auto-inserted ")" gets replaced by ") const" if necessary
int closingParen = toInsert.lastIndexOf(QLatin1Char(')'));
extraChars = toInsert.mid(closingParen);
toInsert.truncate(closingParen);
}
#endif
}
// Append an unhandled typed character, adjusting cursor offset when it had been adjusted before
if (!m_typedChar.isNull()) {
extraChars += m_typedChar;
if (cursorOffset != 0)
--cursorOffset;
}
// Avoid inserting characters that are already there
const int endsPosition = editorWidget->position(EndOfLinePosition);
const QString existingText = editorWidget->textAt(editorWidget->position(), endsPosition - editorWidget->position());
int existLength = 0;
if (!existingText.isEmpty()) {
// Calculate the exist length in front of the extra chars
existLength = toInsert.length() - (editorWidget->position() - basePosition);
while (!existingText.startsWith(toInsert.right(existLength))) {
if (--existLength == 0)
break;
}
}
for (int i = 0; i < extraChars.length(); ++i) {
const QChar a = extraChars.at(i);
const QChar b = editorWidget->characterAt(editorWidget->position() + i + existLength);
if (a == b)
++extraLength;
else
break;
}
toInsert += extraChars;
// Insert the remainder of the name
const int length = editorWidget->position() - basePosition + existLength + extraLength;
editorWidget->setCursorPosition(basePosition);
editorWidget->replace(length, toInsert);
if (cursorOffset)
editorWidget->setCursorPosition(editorWidget->position() + cursorOffset);
}
bool ClangCompletionAssistInterface::objcEnabled() const
{
return m_clangWrapper->objcEnabled();
}
ClangCompletionAssistInterface::ClangCompletionAssistInterface(ClangCompleter::Ptr clangWrapper,
QTextDocument *document,
int position,
const QString &fileName,
AssistReason reason,
const QStringList &options,
const QList<CppTools::ProjectPart::HeaderPath> &headerPaths,
const PchInfo::Ptr &pchInfo)
: AssistInterface(document, position, fileName, reason)
, m_clangWrapper(clangWrapper)
, m_options(options)
, m_headerPaths(headerPaths)
, m_savedPchPointer(pchInfo)
{
Q_ASSERT(!clangWrapper.isNull());
CppModelManager *mmi = CppModelManager::instance();
Q_ASSERT(mmi);
m_unsavedFiles = Utils::createUnsavedFiles(mmi->workingCopy());
}
ClangCompletionAssistProcessor::ClangCompletionAssistProcessor()
: m_model(new ClangAssistProposalModel)
{
}
ClangCompletionAssistProcessor::~ClangCompletionAssistProcessor()
{
}
IAssistProposal *ClangCompletionAssistProcessor::perform(const AssistInterface *interface)
{
m_interface.reset(static_cast<const ClangCompletionAssistInterface *>(interface));
if (interface->reason() != ExplicitlyInvoked && !accepts())
return 0;
int index = startCompletionHelper();
if (index != -1) {
if (m_hintProposal)
return m_hintProposal;
m_model->m_sortable = (m_model->m_completionOperator != T_EOF_SYMBOL);
return createContentProposal();
}
return 0;
}
int ClangCompletionAssistProcessor::startCompletionHelper()
{
//### TODO: clean-up this method, some calculated values might not be used anymore.
Q_ASSERT(m_model);
const int startOfName = findStartOfName();
m_startPosition = startOfName;
m_model->m_completionOperator = T_EOF_SYMBOL;
int endOfOperator = m_startPosition;
// Skip whitespace preceding this position
while (m_interface->characterAt(endOfOperator - 1).isSpace())
--endOfOperator;
const QString fileName = m_interface->fileName();
int endOfExpression = startOfOperator(endOfOperator,
&m_model->m_completionOperator,
/*want function call =*/ true);
if (m_model->m_completionOperator == T_EOF_SYMBOL) {
endOfOperator = m_startPosition;
} else if (m_model->m_completionOperator == T_DOXY_COMMENT) {
for (int i = 1; i < T_DOXY_LAST_TAG; ++i)
addCompletionItem(QString::fromLatin1(doxygenTagSpell(i)),
m_icons.keywordIcon());
return m_startPosition;
}
// Pre-processor completion
//### TODO: check if clang can do pp completion
if (m_model->m_completionOperator == T_POUND) {
completePreprocessor();
m_startPosition = startOfName;
return m_startPosition;
}
// Include completion
if (m_model->m_completionOperator == T_STRING_LITERAL
|| m_model->m_completionOperator == T_ANGLE_STRING_LITERAL
|| m_model->m_completionOperator == T_SLASH) {
QTextCursor c(m_interface->textDocument());
c.setPosition(endOfExpression);
if (completeInclude(c))
m_startPosition = startOfName;
return m_startPosition;
}
ExpressionUnderCursor expressionUnderCursor;
QTextCursor tc(m_interface->textDocument());
if (m_model->m_completionOperator == T_COMMA) {
tc.setPosition(endOfExpression);
const int start = expressionUnderCursor.startOfFunctionCall(tc);
if (start == -1) {
m_model->m_completionOperator = T_EOF_SYMBOL;
return -1;
}
endOfExpression = start;
m_startPosition = start + 1;
m_model->m_completionOperator = T_LPAREN;
}
tc.setPosition(endOfExpression);
if (m_model->m_completionOperator) {
const QString expression = expressionUnderCursor(tc);
if (m_model->m_completionOperator == T_LPAREN) {
if (expression.endsWith(QLatin1String("SIGNAL")))
m_model->m_completionOperator = T_SIGNAL;
else if (expression.endsWith(QLatin1String("SLOT")))
m_model->m_completionOperator = T_SLOT;
else if (m_interface->position() != endOfOperator) {
// We don't want a function completion when the cursor isn't at the opening brace
m_model->m_completionOperator = T_EOF_SYMBOL;
m_startPosition = startOfName;
}
}
}
int line = 0, column = 0;
Convenience::convertPosition(m_interface->textDocument(), endOfOperator, &line, &column);
return startCompletionInternal(fileName, line, column, endOfOperator);
}
int ClangCompletionAssistProcessor::startOfOperator(int pos,
unsigned *kind,
bool wantFunctionCall) const
{
const QChar ch = pos > -1 ? m_interface->characterAt(pos - 1) : QChar();
const QChar ch2 = pos > 0 ? m_interface->characterAt(pos - 2) : QChar();
const QChar ch3 = pos > 1 ? m_interface->characterAt(pos - 3) : QChar();
int start = pos - activationSequenceChar(ch, ch2, ch3, kind, wantFunctionCall);
if (start != pos) {
QTextCursor tc(m_interface->textDocument());
tc.setPosition(pos);
// 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 = pos;
}
}
if (*kind == T_COMMA) {
ExpressionUnderCursor expressionUnderCursor;
if (expressionUnderCursor.startOfFunctionCall(tc) == -1) {
*kind = T_EOF_SYMBOL;
start = 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)); // get the token at the left of the cursor
const CPlusPlus::Token tk = (tokenIdx == -1) ? CPlusPlus::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 = pos;
}
// 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.isLiteral() && (*kind != T_STRING_LITERAL
&& *kind != T_ANGLE_STRING_LITERAL
&& *kind != T_SLASH))) {
*kind = T_EOF_SYMBOL;
start = pos;
}
// 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 = pos;
}
else if (*kind == T_LPAREN) {
if (tokenIdx > 0) {
const CPlusPlus::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 = pos;
}
}
}
// 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 CPlusPlus::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 = pos;
}
}
}
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 CPlusPlus::Token tk = (tokenIdx == -1) ? CPlusPlus::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 CPlusPlus::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;
}
IAssistProposal *ClangCompletionAssistProcessor::createContentProposal()
{
m_model->loadContent(m_completions);
return new ClangAssistProposal(m_startPosition, m_model.take());
}
/// Seach backwards in the document starting from pos to find the first opening
/// parenthesis. Nested parenthesis are skipped.
static int findOpenParen(QTextDocument *doc, int start)
{
unsigned parenCount = 1;
for (int pos = start; pos >= 0; --pos) {
const QChar ch = doc->characterAt(pos);
if (ch == QLatin1Char('(')) {
--parenCount;
if (parenCount == 0)
return pos;
} 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;
}
int ClangCompletionAssistProcessor::startCompletionInternal(const QString fileName,
unsigned line,
unsigned column,
int endOfExpression)
{
bool signalCompletion = false;
bool slotCompletion = false;
QByteArray modifiedInput;
if (m_model->m_completionOperator == T_SIGNAL) {
signalCompletion = true;
modifiedInput = modifyInput(m_interface->textDocument(), endOfExpression);
} else if (m_model->m_completionOperator == T_SLOT) {
slotCompletion = true;
modifiedInput = modifyInput(m_interface->textDocument(), endOfExpression);
} else if (m_model->m_completionOperator == T_LPAREN) {
// Find the expression that precedes the current name
int index = endOfExpression;
while (m_interface->characterAt(index - 1).isSpace())
--index;
QTextCursor tc(m_interface->textDocument());
tc.setPosition(index);
ExpressionUnderCursor euc;
index = euc.startOfFunctionCall(tc);
int nameStart = findStartOfName(index);
QTextCursor tc2(m_interface->textDocument());
tc2.setPosition(nameStart);
tc2.setPosition(index, QTextCursor::KeepAnchor);
const QString functionName = tc2.selectedText().trimmed();
int l = line, c = column;
Convenience::convertPosition(m_interface->textDocument(), nameStart, &l, &c);
qCDebug(log)<<"complete constructor or function @" << line<<":"<<column << "->"<<l<<":"<<c;
const QList<CodeCompletionResult> completions = unfilteredCompletion(
m_interface.data(), fileName, l, c, QByteArray(), signalCompletion || slotCompletion);
QList<CodeCompletionResult> functionCompletions;
foreach (const CodeCompletionResult &ccr, completions) {
if (ccr.completionKind() == CodeCompletionResult::FunctionCompletionKind
|| ccr.completionKind() == CodeCompletionResult::ConstructorCompletionKind
|| ccr.completionKind() == CodeCompletionResult::DestructorCompletionKind
|| ccr.completionKind() == CodeCompletionResult::SignalCompletionKind
|| ccr.completionKind() == CodeCompletionResult::SlotCompletionKind)
if (ccr.text() == functionName)
functionCompletions.append(ccr);
}
if (!functionCompletions.isEmpty()) {
IFunctionHintProposalModel *model = new ClangFunctionHintModel(functionCompletions);
m_hintProposal = new FunctionHintProposal(m_startPosition, model);
return m_startPosition;
}
}
const QIcon snippetIcon = QIcon(QLatin1String(SNIPPET_ICON_PATH));
QList<CodeCompletionResult> completions = unfilteredCompletion(
m_interface.data(), fileName, line, column, modifiedInput, signalCompletion || slotCompletion);
QHash<QString, ClangAssistProposalItem *> items;
foreach (const CodeCompletionResult &ccr, completions) {
if (!ccr.isValid())
continue;
if (signalCompletion && ccr.completionKind() != CodeCompletionResult::SignalCompletionKind)
continue;
if (slotCompletion && ccr.completionKind() != CodeCompletionResult::SlotCompletionKind)
continue;
const QString txt(ccr.text());
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());
item->setOrder(ccr.priority());
const QString snippet = ccr.snippet();
if (!snippet.isEmpty())
item->setData(snippet);
else
item->setData(qVariantFromValue(ccr));
}
// FIXME: show the effective accessebility instead of availability
switch (ccr.completionKind()) {
case CodeCompletionResult::ClassCompletionKind: item->setIcon(m_icons.iconForType(Icons::ClassIconType)); break;
case CodeCompletionResult::EnumCompletionKind: item->setIcon(m_icons.iconForType(Icons::EnumIconType)); break;
case CodeCompletionResult::EnumeratorCompletionKind: item->setIcon(m_icons.iconForType(Icons::EnumeratorIconType)); break;
case CodeCompletionResult::ConstructorCompletionKind: // fall through
case CodeCompletionResult::DestructorCompletionKind: // fall through
case CodeCompletionResult::FunctionCompletionKind:
case CodeCompletionResult::ObjCMessageCompletionKind:
switch (ccr.availability()) {
case CodeCompletionResult::Available:
case CodeCompletionResult::Deprecated:
item->setIcon(m_icons.iconForType(Icons::FuncPublicIconType));
break;
default:
item->setIcon(m_icons.iconForType(Icons::FuncPrivateIconType));
break;
}
break;
case CodeCompletionResult::SignalCompletionKind:
item->setIcon(m_icons.iconForType(Icons::SignalIconType));
break;
case CodeCompletionResult::SlotCompletionKind:
switch (ccr.availability()) {
case CodeCompletionResult::Available:
case CodeCompletionResult::Deprecated:
item->setIcon(m_icons.iconForType(Icons::SlotPublicIconType));
break;
case CodeCompletionResult::NotAccessible:
case CodeCompletionResult::NotAvailable:
item->setIcon(m_icons.iconForType(Icons::SlotPrivateIconType));
break;
}
break;
case CodeCompletionResult::NamespaceCompletionKind: item->setIcon(m_icons.iconForType(Icons::NamespaceIconType)); break;
case CodeCompletionResult::PreProcessorCompletionKind: item->setIcon(m_icons.iconForType(Icons::MacroIconType)); break;
case CodeCompletionResult::VariableCompletionKind:
switch (ccr.availability()) {
case CodeCompletionResult::Available:
case CodeCompletionResult::Deprecated:
item->setIcon(m_icons.iconForType(Icons::VarPublicIconType));
break;
default:
item->setIcon(m_icons.iconForType(Icons::VarPrivateIconType));
break;
}
break;
case CodeCompletionResult::KeywordCompletionKind:
item->setIcon(m_icons.iconForType(Icons::KeywordIconType));
break;
case CodeCompletionResult::ClangSnippetKind:
item->setIcon(snippetIcon);
break;
default:
break;
}
}
foreach (ClangAssistProposalItem *item, items.values())
m_completions.append(item);
return m_startPosition;
}
/**
* @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_model->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_model->m_completionOperator = T_ANGLE_STRING_LITERAL;
} else {
m_model->m_completionOperator = T_STRING_LITERAL;
}
if (startCharPos != -1)
directoryPrefix = sel.mid(startCharPos + 1, sel.length() - 1);
}
// Make completion for all relevant includes
ProjectPart::HeaderPaths headerPaths = m_interface->headerPaths();
const ProjectPart::HeaderPath currentFilePath(QFileInfo(m_interface->fileName()).path(),
ProjectPart::HeaderPath::IncludePath);
if (!headerPaths.contains(currentFilePath))
headerPaths.append(currentFilePath);
const Core::MimeType mimeType = Core::MimeDatabase::findByType(QLatin1String("text/x-c++hdr"));
const QStringList suffixes = mimeType.suffixes();
foreach (const 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();
}
/**
* @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_model->m_completionOperator);
m_completions.append(item);
}
}
}
void ClangCompletionAssistProcessor::completePreprocessor()
{
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));
}
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_model->m_completionOperator);
m_completions.append(item);
}