ClangCodeModel: Use clangd for completion and function hint

Change-Id: I80160f3a40da18ac178682afe6caba5e5af6e3eb
Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
Christian Kandeler
2021-06-18 16:30:03 +02:00
parent 67d2a4186b
commit e0e8fda580
62 changed files with 1569 additions and 187 deletions

View File

@@ -276,7 +276,7 @@ Position::Position(const QTextCursor &cursor)
: Position(cursor.blockNumber(), cursor.positionInBlock())
{ }
int Position::toPositionInDocument(QTextDocument *doc) const
int Position::toPositionInDocument(const QTextDocument *doc) const
{
const QTextBlock block = doc->findBlockByNumber(line());
if (!block.isValid())

View File

@@ -88,7 +88,7 @@ public:
bool isValid() const override
{ return contains(lineKey) && contains(characterKey); }
int toPositionInDocument(QTextDocument *doc) const;
int toPositionInDocument(const QTextDocument *doc) const;
QTextCursor toTextCursor(QTextDocument *doc) const;
};

View File

@@ -30,16 +30,19 @@
#include <cplusplus/BackwardsScanner.h>
#include <cplusplus/ExpressionUnderCursor.h>
#include <cplusplus/SimpleLexer.h>
#include <utils/textutils.h>
#include <QTextDocument>
namespace ClangCodeModel {
namespace Internal {
ActivationSequenceContextProcessor::ActivationSequenceContextProcessor(const ClangCompletionAssistInterface *assistInterface)
: m_textCursor(assistInterface->textDocument()),
m_assistInterface(assistInterface),
m_positionInDocument(assistInterface->position()),
ActivationSequenceContextProcessor::ActivationSequenceContextProcessor(
QTextDocument *document, int position, CPlusPlus::LanguageFeatures languageFeatures)
: m_textCursor(document),
m_document(document),
m_languageFeatures(languageFeatures),
m_positionInDocument(position),
m_startOfNamePosition(m_positionInDocument),
m_operatorStartPosition(m_positionInDocument)
@@ -49,6 +52,13 @@ ActivationSequenceContextProcessor::ActivationSequenceContextProcessor(const Cla
process();
}
ActivationSequenceContextProcessor::ActivationSequenceContextProcessor(
const ClangCompletionAssistInterface *interface)
: ActivationSequenceContextProcessor(interface->textDocument(), interface->position(),
interface->languageFeatures())
{
}
CPlusPlus::Kind ActivationSequenceContextProcessor::completionKind() const
{
return m_completionKind;
@@ -91,8 +101,9 @@ void ActivationSequenceContextProcessor::process()
void ActivationSequenceContextProcessor::processActivationSequence()
{
const int nonSpacePosition = skipPrecedingWhitespace(m_assistInterface, m_startOfNamePosition);
const auto activationSequence = m_assistInterface->textAt(nonSpacePosition - 3, 3);
const int nonSpacePosition = skipPrecedingWhitespace(m_document, m_startOfNamePosition);
const auto activationSequence = Utils::Text::textAt(QTextCursor(m_document),
nonSpacePosition - 3, 3);
ActivationSequenceProcessor activationSequenceProcessor(activationSequence,
nonSpacePosition,
true);
@@ -115,7 +126,7 @@ void ActivationSequenceContextProcessor::processStringLiteral()
void ActivationSequenceContextProcessor::processComma()
{
if (m_completionKind == CPlusPlus::T_COMMA) {
CPlusPlus::ExpressionUnderCursor expressionUnderCursor(m_assistInterface->languageFeatures());
CPlusPlus::ExpressionUnderCursor expressionUnderCursor(m_languageFeatures);
if (expressionUnderCursor.startOfFunctionCall(m_textCursor) == -1)
m_completionKind = CPlusPlus::T_EOF_SYMBOL;
}
@@ -124,7 +135,7 @@ void ActivationSequenceContextProcessor::processComma()
void ActivationSequenceContextProcessor::generateTokens()
{
CPlusPlus::SimpleLexer tokenize;
tokenize.setLanguageFeatures(m_assistInterface->languageFeatures());
tokenize.setLanguageFeatures(m_languageFeatures);
tokenize.setSkipComments(false);
auto state = CPlusPlus::BackwardsScanner::previousBlockState(m_textCursor.block());
m_tokens = tokenize(m_textCursor.block().text(), state);
@@ -222,12 +233,11 @@ void ActivationSequenceContextProcessor::resetPositionsForEOFCompletionKind()
m_operatorStartPosition = m_positionInDocument;
}
int ActivationSequenceContextProcessor::skipPrecedingWhitespace(
const TextEditor::AssistInterface *assistInterface,
int startPosition)
int ActivationSequenceContextProcessor::skipPrecedingWhitespace(const QTextDocument *document,
int startPosition)
{
int position = startPosition;
while (assistInterface->characterAt(position - 1).isSpace())
while (document->characterAt(position - 1).isSpace())
--position;
return position;
}
@@ -241,7 +251,7 @@ static bool isValidIdentifierChar(const QChar &character)
}
int ActivationSequenceContextProcessor::findStartOfName(
const TextEditor::AssistInterface *assistInterface,
const QTextDocument *document,
int startPosition,
NameCategory category)
{
@@ -249,32 +259,32 @@ int ActivationSequenceContextProcessor::findStartOfName(
QChar character;
if (category == NameCategory::Function
&& position > 2 && assistInterface->characterAt(position - 1) == '>'
&& assistInterface->characterAt(position - 2) != '-') {
&& position > 2 && document->characterAt(position - 1) == '>'
&& document->characterAt(position - 2) != '-') {
uint unbalancedLessGreater = 1;
--position;
while (unbalancedLessGreater > 0 && position > 2) {
character = assistInterface->characterAt(--position);
character = document->characterAt(--position);
// Do not count -> usage inside temlate argument list
if (character == '<')
--unbalancedLessGreater;
else if (character == '>' && assistInterface->characterAt(position-1) != '-')
else if (character == '>' && document->characterAt(position-1) != '-')
++unbalancedLessGreater;
}
position = skipPrecedingWhitespace(assistInterface, position) - 1;
position = skipPrecedingWhitespace(document, position) - 1;
}
do {
character = assistInterface->characterAt(--position);
character = document->characterAt(--position);
} while (isValidIdentifierChar(character));
int prevPosition = skipPrecedingWhitespace(assistInterface, position);
int prevPosition = skipPrecedingWhitespace(document, position);
if (category == NameCategory::Function
&& assistInterface->characterAt(prevPosition) == ':'
&& assistInterface->characterAt(prevPosition - 1) == ':') {
&& document->characterAt(prevPosition) == ':'
&& document->characterAt(prevPosition - 1) == ':') {
// Handle :: case - go recursive
prevPosition = skipPrecedingWhitespace(assistInterface, prevPosition - 2);
return findStartOfName(assistInterface, prevPosition + 1, category);
prevPosition = skipPrecedingWhitespace(document, prevPosition - 2);
return findStartOfName(document, prevPosition + 1, category);
}
return position + 1;
@@ -283,7 +293,7 @@ int ActivationSequenceContextProcessor::findStartOfName(
void ActivationSequenceContextProcessor::goBackToStartOfName()
{
CPlusPlus::SimpleLexer tokenize;
tokenize.setLanguageFeatures(m_assistInterface->languageFeatures());
tokenize.setLanguageFeatures(m_languageFeatures);
tokenize.setSkipComments(false);
const int state = CPlusPlus::BackwardsScanner::previousBlockState(m_textCursor.block());
const CPlusPlus::Tokens tokens = tokenize(m_textCursor.block().text(), state);
@@ -297,7 +307,7 @@ void ActivationSequenceContextProcessor::goBackToStartOfName()
m_startOfNamePosition = m_textCursor.block().position() + std::max(slashIndex, tokenStart)
+ 1;
} else {
m_startOfNamePosition = findStartOfName(m_assistInterface, m_positionInDocument);
m_startOfNamePosition = findStartOfName(m_document, m_positionInDocument);
}
if (m_startOfNamePosition != m_positionInDocument)

View File

@@ -41,7 +41,9 @@ namespace Internal {
class ActivationSequenceContextProcessor
{
public:
ActivationSequenceContextProcessor(const ClangCompletionAssistInterface *assistInterface);
ActivationSequenceContextProcessor(QTextDocument *document, int position,
CPlusPlus::LanguageFeatures languageFeatures);
ActivationSequenceContextProcessor(const ClangCompletionAssistInterface *interface);
CPlusPlus::Kind completionKind() const;
int startOfNamePosition() const; // e.g. points to 'b' in "foo.bar<CURSOR>"
@@ -50,10 +52,10 @@ public:
const QTextCursor &textCursor_forTestOnly() const;
enum class NameCategory { Function, NonFunction };
static int findStartOfName(const TextEditor::AssistInterface *assistInterface,
static int findStartOfName(const QTextDocument *document,
int startPosition,
NameCategory category = NameCategory::NonFunction);
static int skipPrecedingWhitespace(const TextEditor::AssistInterface *assistInterface,
static int skipPrecedingWhitespace(const QTextDocument *document,
int startPosition);
protected:
@@ -78,7 +80,8 @@ private:
QVector<CPlusPlus::Token> m_tokens;
QTextCursor m_textCursor;
CPlusPlus::Token m_token;
const ClangCompletionAssistInterface *m_assistInterface;
QTextDocument * const m_document;
const CPlusPlus::LanguageFeatures m_languageFeatures;
int m_tokenIndex;
const int m_positionInDocument;
int m_startOfNamePosition;

View File

@@ -79,41 +79,6 @@ bool ClangAssistProposalItem::implicitlyApplies() const
return true;
}
static QString textUntilPreviousStatement(TextDocumentManipulatorInterface &manipulator,
int startPosition)
{
static const QString stopCharacters(";{}#");
int endPosition = 0;
for (int i = startPosition; i >= 0 ; --i) {
if (stopCharacters.contains(manipulator.characterAt(i))) {
endPosition = i + 1;
break;
}
}
return manipulator.textAt(endPosition, startPosition - endPosition);
}
// 7.3.3: using typename(opt) nested-name-specifier unqualified-id ;
static bool isAtUsingDeclaration(TextDocumentManipulatorInterface &manipulator,
int basePosition)
{
SimpleLexer lexer;
lexer.setLanguageFeatures(LanguageFeatures::defaultFeatures());
const QString textToLex = textUntilPreviousStatement(manipulator, basePosition);
const Tokens tokens = lexer(textToLex);
if (tokens.empty())
return false;
// The nested-name-specifier always ends with "::", so check for this first.
const Token lastToken = tokens[tokens.size() - 1];
if (lastToken.kind() != T_COLON_COLON)
return false;
return contains(tokens, [](const Token &token) { return token.kind() == T_USING; });
}
static QString methodDefinitionParameters(const CodeCompletionChunks &chunks)
{
QString result;

View File

@@ -210,6 +210,7 @@ QVector<QObject *> ClangCodeModelPlugin::createTestObjects() const
{
return {
new Tests::ClangCodeCompletionTest,
new Tests::ClangdTestCompletion,
new Tests::ClangdTestFindReferences,
new Tests::ClangdTestFollowSymbol,
new Tests::ClangdTestHighlighting,

View File

@@ -68,24 +68,33 @@ namespace Internal {
ClangCompletionContextAnalyzer::ClangCompletionContextAnalyzer(
const ClangCompletionAssistInterface *assistInterface,
CPlusPlus::LanguageFeatures languageFeatures)
: m_interface(assistInterface)
, m_languageFeatures(languageFeatures)
: ClangCompletionContextAnalyzer(assistInterface->textDocument(), assistInterface->position(),
assistInterface->type() == CompletionType::FunctionHint,
languageFeatures)
{
}
ClangCompletionContextAnalyzer::ClangCompletionContextAnalyzer(
QTextDocument *document, int position, bool isFunctionHint,
CPlusPlus::LanguageFeatures languageFeatures)
: m_document(document), m_position(position), m_isFunctionHint(isFunctionHint),
m_languageFeatures(languageFeatures)
{
}
void ClangCompletionContextAnalyzer::analyze()
{
QTC_ASSERT(m_interface, return);
QTC_ASSERT(m_document, return);
setActionAndClangPosition(PassThroughToLibClang, -1);
ActivationSequenceContextProcessor activationSequenceContextProcessor(m_interface);
ActivationSequenceContextProcessor activationSequenceContextProcessor(
m_document, m_position, m_languageFeatures);
m_completionOperator = activationSequenceContextProcessor.completionKind();
int afterOperatorPosition = activationSequenceContextProcessor.startOfNamePosition();
m_positionEndOfExpression = activationSequenceContextProcessor.operatorStartPosition();
m_positionForProposal = activationSequenceContextProcessor.startOfNamePosition();
const bool actionIsSet = m_interface->type() != CompletionType::FunctionHint
&& handleNonFunctionCall(afterOperatorPosition);
const bool actionIsSet = !m_isFunctionHint && handleNonFunctionCall(afterOperatorPosition);
if (!actionIsSet) {
handleCommaInFunctionCall();
handleFunctionCall(afterOperatorPosition);
@@ -94,20 +103,20 @@ void ClangCompletionContextAnalyzer::analyze()
int ClangCompletionContextAnalyzer::startOfFunctionCall(int endOfOperator) const
{
int index = ActivationSequenceContextProcessor::skipPrecedingWhitespace(m_interface,
int index = ActivationSequenceContextProcessor::skipPrecedingWhitespace(m_document,
endOfOperator);
QTextCursor textCursor(m_interface->textDocument());
QTextCursor textCursor(m_document);
textCursor.setPosition(index);
ExpressionUnderCursor euc(m_languageFeatures);
index = euc.startOfFunctionCall(textCursor);
index = ActivationSequenceContextProcessor::skipPrecedingWhitespace(m_interface, index);
index = ActivationSequenceContextProcessor::skipPrecedingWhitespace(m_document, index);
const int functionNameStart = ActivationSequenceContextProcessor::findStartOfName(
m_interface, index, ActivationSequenceContextProcessor::NameCategory::Function);
m_document, index, ActivationSequenceContextProcessor::NameCategory::Function);
if (functionNameStart == -1)
return -1;
QTextCursor functionNameSelector(m_interface->textDocument());
QTextCursor functionNameSelector(m_document);
functionNameSelector.setPosition(functionNameStart);
functionNameSelector.setPosition(index, QTextCursor::KeepAnchor);
const QString functionName = functionNameSelector.selectedText().trimmed();
@@ -137,12 +146,12 @@ void ClangCompletionContextAnalyzer::handleCommaInFunctionCall()
{
if (m_completionOperator == T_COMMA) {
ExpressionUnderCursor expressionUnderCursor(m_languageFeatures);
QTextCursor textCursor(m_interface->textDocument());
QTextCursor textCursor(m_document);
textCursor.setPosition(m_positionEndOfExpression);
const int start = expressionUnderCursor.startOfFunctionCall(textCursor);
m_positionEndOfExpression = start;
m_positionForProposal = start + 1; // After '(' of function call
if (m_interface->characterAt(start) == '(')
if (m_document->characterAt(start) == '(')
m_completionOperator = T_LPAREN;
else
m_completionOperator = T_LBRACE;
@@ -151,7 +160,7 @@ void ClangCompletionContextAnalyzer::handleCommaInFunctionCall()
void ClangCompletionContextAnalyzer::handleFunctionCall(int afterOperatorPosition)
{
if (m_interface->type() == CompletionType::FunctionHint) {
if (m_isFunctionHint) {
const int functionNameStart = startOfFunctionCall(afterOperatorPosition);
if (functionNameStart >= 0) {
m_addSnippets = functionNameStart == afterOperatorPosition;
@@ -166,15 +175,20 @@ void ClangCompletionContextAnalyzer::handleFunctionCall(int afterOperatorPositio
if (m_completionOperator == T_LPAREN || m_completionOperator == T_LBRACE) {
ExpressionUnderCursor expressionUnderCursor(m_languageFeatures);
QTextCursor textCursor(m_interface->textDocument());
QTextCursor textCursor(m_document);
textCursor.setPosition(m_positionEndOfExpression);
const QString expression = expressionUnderCursor(textCursor);
const QString trimmedExpression = expression.trimmed();
const QChar lastExprChar = trimmedExpression.isEmpty()
? QChar() : trimmedExpression.at(trimmedExpression.length() - 1);
const bool mightBeConstructorCall = lastExprChar != ')';
if (expression.endsWith(QLatin1String("SIGNAL"))) {
setActionAndClangPosition(CompleteSignal, afterOperatorPosition);
} else if (expression.endsWith(QLatin1String("SLOT"))) {
setActionAndClangPosition(CompleteSlot, afterOperatorPosition);
} else if (m_interface->position() != afterOperatorPosition) {
} else if (m_position != afterOperatorPosition
|| (m_completionOperator == T_LBRACE && !mightBeConstructorCall)) {
// No function completion if cursor is not after '(' or ','
m_addSnippets = true;
m_positionForProposal = afterOperatorPosition;

View File

@@ -29,6 +29,10 @@
#include <QString>
QT_BEGIN_NAMESPACE
class QTextDocument;
QT_END_NAMESPACE
namespace TextEditor { class AssistInterface; }
namespace ClangCodeModel {
@@ -42,6 +46,8 @@ public:
ClangCompletionContextAnalyzer() = delete;
ClangCompletionContextAnalyzer(const ClangCompletionAssistInterface *assistInterface,
CPlusPlus::LanguageFeatures languageFeatures);
ClangCompletionContextAnalyzer(QTextDocument *document, int position, bool isFunctionHint,
CPlusPlus::LanguageFeatures languageFeatures);
void analyze();
enum CompletionAction {
@@ -75,8 +81,10 @@ private:
void handleFunctionCall(int endOfOperator);
private:
const ClangCompletionAssistInterface *m_interface; // Not owned
const CPlusPlus::LanguageFeatures m_languageFeatures; // TODO: Get from assistInterface?!
QTextDocument * const m_document;
const int m_position;
const bool m_isFunctionHint;
const CPlusPlus::LanguageFeatures m_languageFeatures;
// Results
CompletionAction m_completionAction = PassThroughToLibClang;

View File

@@ -25,7 +25,9 @@
#include "clangdclient.h"
#include "clangcompletioncontextanalyzer.h"
#include "clangdiagnosticmanager.h"
#include "clangpreprocessorassistproposalitem.h"
#include "clangtextmark.h"
#include "clangutils.h"
@@ -34,6 +36,11 @@
#include <coreplugin/find/searchresultitem.h>
#include <coreplugin/find/searchresultwindow.h>
#include <cplusplus/FindUsages.h>
#include <cplusplus/Icons.h>
#include <cplusplus/MatchingText.h>
#include <cppeditor/cppeditorconstants.h>
#include <cpptools/cppcodemodelsettings.h>
#include <cpptools/cppdoxygen.h>
#include <cpptools/cppeditorwidgetinterface.h>
#include <cpptools/cppfindreferences.h>
#include <cpptools/cppmodelmanager.h>
@@ -50,6 +57,8 @@
#include <texteditor/codeassist/assistinterface.h>
#include <texteditor/codeassist/iassistprocessor.h>
#include <texteditor/codeassist/iassistprovider.h>
#include <texteditor/codeassist/textdocumentmanipulatorinterface.h>
#include <texteditor/texteditorsettings.h>
#include <texteditor/texteditor.h>
#include <utils/algorithm.h>
#include <utils/runextensions.h>
@@ -676,6 +685,62 @@ public:
{ insert("publishDiagnostics", caps); }
};
class DoxygenAssistProcessor : public TextEditor::IAssistProcessor
{
public:
DoxygenAssistProcessor(int position, unsigned completionOperator,
const ProposalHandler &handler)
: m_position(position), m_completionOperator(completionOperator), m_handler(handler) {}
private:
TextEditor::IAssistProposal *perform(const TextEditor::AssistInterface *) override
{
QList<TextEditor::AssistProposalItemInterface *> completions;
for (int i = 1; i < CppTools::T_DOXY_LAST_TAG; ++i) {
const auto item = new ClangPreprocessorAssistProposalItem;
item->setText(QLatin1String(CppTools::doxygenTagSpell(i)));
item->setIcon(CPlusPlus::Icons::keywordIcon());
item->setCompletionOperator(m_completionOperator);
completions.append(item);
}
TextEditor::GenericProposalModelPtr model(new TextEditor::GenericProposalModel);
model->loadContent(completions);
const auto proposal = new TextEditor::GenericProposal(m_position, model);
if (m_handler) {
m_handler(proposal);
return nullptr;
}
return proposal;
}
const int m_position;
const unsigned m_completionOperator;
const ProposalHandler m_handler;
};
class DoxygenAssistProvider : public TextEditor::IAssistProvider
{
public:
void setProposalHandler(const ProposalHandler &handler) { m_proposalHandler = handler; }
void setParameters(int position, unsigned completionOperator)
{
m_position = position;
m_completionOperator = completionOperator;
}
private:
RunType runType() const override { return Synchronous; }
TextEditor::IAssistProcessor *createProcessor() const override
{
return new DoxygenAssistProcessor(m_position, m_completionOperator, m_proposalHandler);
}
ProposalHandler m_proposalHandler;
int m_position = 0;
unsigned m_completionOperator = 0;
};
class ClangdClient::Private
{
public:
@@ -711,8 +776,13 @@ public:
void handleSemanticTokens(TextEditor::TextDocument *doc,
const QList<ExpandedSemanticToken> &tokens);
void applyCompletionItem(const CompletionItem &item,
TextEditor::TextDocumentManipulatorInterface &manipulator,
QChar typedChar);
ClangdClient * const q;
const CppTools::ClangdSettings::Data settings;
DoxygenAssistProvider doxygenAssistProvider;
QHash<quint64, ReferencesData> runningFindUsages;
Utils::optional<FollowSymbolData> followSymbolData;
Utils::optional<SwitchDeclDefData> switchDeclDefData;
@@ -724,6 +794,16 @@ public:
bool isTesting = false;
};
class ClangdCompletionCapabilities : public TextDocumentClientCapabilities::CompletionCapabilities
{
public:
explicit ClangdCompletionCapabilities(const JsonObject &object)
: TextDocumentClientCapabilities::CompletionCapabilities(object)
{
insert("editsNearCursor", true); // For dot-to-arrow correction.
}
};
ClangdClient::ClangdClient(Project *project, const Utils::FilePath &jsonDbDir)
: Client(clientInterface(project, jsonDbDir)), d(new Private(this, project))
{
@@ -745,12 +825,15 @@ ClangdClient::ClangdClient(Project *project, const Utils::FilePath &jsonDbDir)
Utils::optional<TextDocumentClientCapabilities> textCaps = caps.textDocument();
if (textCaps) {
ClangdTextDocumentClientCapabilities clangdTextCaps(*textCaps);
clangdTextCaps.clearCompletion();
clangdTextCaps.clearDocumentHighlight();
DiagnosticsCapabilities diagnostics;
diagnostics.enableCategorySupport();
diagnostics.enableCodeActionsInline();
clangdTextCaps.setPublishDiagnostics(diagnostics);
Utils::optional<TextDocumentClientCapabilities::CompletionCapabilities> completionCaps
= textCaps->completion();
if (completionCaps)
clangdTextCaps.setCompletion(ClangdCompletionCapabilities(*completionCaps));
caps.setTextDocument(clangdTextCaps);
}
caps.clearExperimental();
@@ -800,6 +883,34 @@ ClangdClient::ClangdClient(Project *project, const Utils::FilePath &jsonDbDir)
const DocumentUri &uri) {
gatherHelpItemForTooltip(response, uri);
});
setCompletionItemsTransformer([](const Utils::FilePath &filePath, const QString &content,
int pos, const QList<CompletionItem> &items) {
qCDebug(clangdLog) << "received" << items.count() << "completions";
// If there are signals among the candidates, we employ the built-in code model to find out
// whether the cursor was on the second argument of a (dis)connect() call.
// If so, we offer only signals, as nothing else makes sense in that context.
static const auto criterion = [](const CompletionItem &ci) {
const Utils::optional<MarkupOrString> doc = ci.documentation();
if (!doc)
return false;
QString docText;
if (Utils::holds_alternative<QString>(*doc))
docText = Utils::get<QString>(*doc);
else if (Utils::holds_alternative<MarkupContent>(*doc))
docText = Utils::get<MarkupContent>(*doc).content();
return docText.contains("Annotation: qt_signal");
};
if (pos != -1 && Utils::anyOf(items, criterion) && CppTools::CppModelManager::instance()
->positionRequiresSignal(filePath.toString(), content.toUtf8(), pos)) {
return Utils::filtered(items, criterion);
}
return items;
});
setCompletionApplyHelper([this](const CompletionItem &item,
TextEditor::TextDocumentManipulatorInterface &manipulator, QChar typedChar) {
d->applyCompletionItem(item, manipulator, typedChar);
});
connect(this, &Client::workDone, this,
[this, p = QPointer(project)](const ProgressToken &token) {
@@ -900,8 +1011,6 @@ void ClangdClient::findUsages(TextEditor::TextDocument *document, const QTextCur
sendContent(symReq);
}
void ClangdClient::enableTesting() { d->isTesting = true; }
void ClangdClient::handleDiagnostics(const PublishDiagnosticsParams &params)
{
const DocumentUri &uri = params.uri();
@@ -1003,6 +1112,55 @@ void ClangdClient::Private::findUsages(TextEditor::TextDocument *document,
});
}
void ClangdClient::enableTesting()
{
d->isTesting = true;
setCompletionProposalHandler([this](TextEditor::IAssistProposal *proposal) {
emit proposalReady(proposal);
});
setFunctionHintProposalHandler([this](TextEditor::IAssistProposal *proposal) {
emit proposalReady(proposal);
});
d->doxygenAssistProvider.setProposalHandler([this](TextEditor::IAssistProposal *proposal) {
QMetaObject::invokeMethod(this, [this, proposal] { emit proposalReady(proposal); },
Qt::QueuedConnection);
});
}
void ClangdClient::openEditorDocument(TextEditor::BaseTextEditor *editor)
{
if (!documentOpen(editor->textDocument()))
openDocument(editor->textDocument());
const auto assistRequestHandler = [self = QPointer(this)](
TextEditor::TextEditorWidget *editorWidget, TextEditor::AssistKind kind,
TextEditor::IAssistProvider *provider) {
if (!self)
return false;
if (kind != TextEditor::Completion || provider)
return false;
ClangCompletionContextAnalyzer contextAnalyzer(editorWidget->document(),
editorWidget->position(), false, {});
contextAnalyzer.analyze();
self->setSnippetsGroup(contextAnalyzer.addSnippets()
? CppEditor::Constants::CPP_SNIPPETS_GROUP_ID : QString());
switch (contextAnalyzer.completionAction()) {
case ClangCompletionContextAnalyzer::PassThroughToLibClangAfterLeftParen:
qCDebug(clangdLog) << "completion changed to function hint";
editorWidget->invokeAssist(TextEditor::FunctionHint, provider);
return true;
case ClangCompletionContextAnalyzer::CompleteDoxygenKeyword:
self->d->doxygenAssistProvider.setParameters(contextAnalyzer.positionForProposal(),
contextAnalyzer.completionOperator());
editorWidget->invokeAssist(kind, &self->d->doxygenAssistProvider);
return true;
default:
break;
}
return false;
};
editor->editorWidget()->setAssistRequestHandler(assistRequestHandler);
}
void ClangdClient::Private::handleFindUsagesResult(quint64 key, const QList<Location> &locations)
{
const auto refData = runningFindUsages.find(key);
@@ -2414,7 +2572,7 @@ static void semanticHighlighter(QFutureInterface<TextEditor::HighlightingResult>
void ClangdClient::Private::handleSemanticTokens(TextEditor::TextDocument *doc,
const QList<ExpandedSemanticToken> &tokens)
{
qCDebug(clangdLog()) << "handling LSP tokens" << tokens.size();
qCDebug(clangdLog()) << "handling LSP tokens" << doc->filePath() << tokens.size();
for (const ExpandedSemanticToken &t : tokens)
qCDebug(clangdLogHighlight()) << '\t' << t.line << t.column << t.length << t.type
<< t.modifiers;
@@ -2437,8 +2595,8 @@ void ClangdClient::Private::handleSemanticTokens(TextEditor::TextDocument *doc,
if (isTesting) {
const auto watcher = new QFutureWatcher<TextEditor::HighlightingResult>(q);
connect(watcher, &QFutureWatcher<TextEditor::HighlightingResult>::finished,
q, [this, watcher] {
emit q->highlightingResultsReady(watcher->future().results());
q, [this, watcher, fp = doc->filePath()] {
emit q->highlightingResultsReady(watcher->future().results(), fp);
watcher->deleteLater();
});
watcher->setFuture(runner());
@@ -2457,6 +2615,125 @@ void ClangdClient::Private::handleSemanticTokens(TextEditor::TextDocument *doc,
q->sendContent(astReq, SendDocUpdates::Ignore);
}
void ClangdClient::Private::applyCompletionItem(const CompletionItem &item,
TextEditor::TextDocumentManipulatorInterface &manipulator, QChar typedChar)
{
const auto edit = item.textEdit();
if (!edit)
return;
const auto kind = static_cast<CompletionItemKind::Kind>(
item.kind().value_or(CompletionItemKind::Text));
if (kind != CompletionItemKind::Function && kind != CompletionItemKind::Method
&& kind != CompletionItemKind::Constructor) {
applyTextEdit(manipulator, *edit, true);
return;
}
const QString rawInsertText = edit->newText();
const int firstParenOffset = rawInsertText.indexOf('(');
const int lastParenOffset = rawInsertText.lastIndexOf(')');
if (firstParenOffset == -1 || lastParenOffset == -1) {
applyTextEdit(manipulator, *edit, true);
return;
}
const QString detail = item.detail().value_or(QString());
const TextEditor::CompletionSettings &completionSettings
= TextEditor::TextEditorSettings::completionSettings();
QString textToBeInserted = rawInsertText.left(firstParenOffset);
QString extraCharacters;
int cursorOffset = 0;
bool setAutoCompleteSkipPos = false;
const QTextDocument * const doc = manipulator.textCursorAt(
manipulator.currentPosition()).document();
const Range range = edit->range();
const int rangeStart = range.start().toPositionInDocument(doc);
const int rangeLength = range.end().toPositionInDocument(doc) - rangeStart;
if (completionSettings.m_autoInsertBrackets) {
// If the user typed the opening parenthesis, they'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 = typedChar != '(';
QTextCursor cursor = manipulator.textCursorAt(rangeStart);
bool abandonParen = false;
if (matchPreviousWord(manipulator, cursor, "&")) {
moveToPreviousWord(manipulator, cursor);
moveToPreviousChar(manipulator, cursor);
const QChar prevChar = manipulator.characterAt(cursor.position());
cursor.setPosition(rangeStart);
abandonParen = QString("(;,{}=").contains(prevChar);
}
if (!abandonParen)
abandonParen = isAtUsingDeclaration(manipulator, rangeStart);
if (!abandonParen && matchPreviousWord(manipulator, cursor, detail)) // function definition?
abandonParen = true;
if (!abandonParen) {
if (completionSettings.m_spaceAfterFunctionName)
extraCharacters += ' ';
extraCharacters += '(';
if (typedChar == '(')
typedChar = {};
// 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 = manipulator.characterAt(manipulator.currentPosition());
bool endWithSemicolon = typedChar == ';';
const QChar semicolon = typedChar.isNull() ? QLatin1Char(';') : typedChar;
if (endWithSemicolon && characterAtCursor == semicolon) {
endWithSemicolon = false;
typedChar = {};
}
// If the function takes no arguments, automatically place the closing parenthesis
if (firstParenOffset + 1 == lastParenOffset && skipClosingParenthesis) {
extraCharacters += QLatin1Char(')');
if (endWithSemicolon) {
extraCharacters += semicolon;
typedChar = {};
}
} else {
const QChar lookAhead = manipulator.characterAt(manipulator.currentPosition() + 1);
if (MatchingText::shouldInsertMatchingText(lookAhead)) {
extraCharacters += ')';
--cursorOffset;
setAutoCompleteSkipPos = true;
if (endWithSemicolon) {
extraCharacters += semicolon;
--cursorOffset;
typedChar = {};
}
}
}
}
}
// Append an unhandled typed character, adjusting cursor offset when it had been adjusted before
if (!typedChar.isNull()) {
extraCharacters += typedChar;
if (cursorOffset != 0)
--cursorOffset;
}
textToBeInserted += extraCharacters;
const bool isReplaced = manipulator.replace(rangeStart, rangeLength, textToBeInserted);
manipulator.setCursorPosition(rangeStart + textToBeInserted.length());
if (isReplaced) {
if (cursorOffset)
manipulator.setCursorPosition(manipulator.currentPosition() + cursorOffset);
if (setAutoCompleteSkipPos)
manipulator.setAutoCompleteSkipPosition(manipulator.currentPosition());
}
if (auto additionalEdits = item.additionalTextEdits()) {
for (const auto &edit : *additionalEdits)
applyTextEdit(manipulator, edit);
}
}
void ClangdClient::VirtualFunctionAssistProcessor::cancel()
{
resetData();

View File

@@ -36,7 +36,7 @@
namespace Core { class SearchResultItem; }
namespace CppTools { class CppEditorWidgetInterface; }
namespace ProjectExplorer { class Project; }
namespace TextEditor { class TextDocument; }
namespace TextEditor { class BaseTextEditor; }
namespace ClangCodeModel {
namespace Internal {
@@ -52,6 +52,8 @@ public:
QVersionNumber versionNumber() const;
CppTools::ClangdSettings::Data settingsData() const;
void openEditorDocument(TextEditor::BaseTextEditor *editor);
void openExtraFile(const Utils::FilePath &filePath, const QString &content = {});
void closeExtraFile(const Utils::FilePath &filePath);
@@ -83,7 +85,9 @@ signals:
void foundReferences(const QList<Core::SearchResultItem> &items);
void findUsagesDone();
void helpItemGathered(const Core::HelpItem &helpItem);
void highlightingResultsReady(const TextEditor::HighlightingResults &results);
void highlightingResultsReady(const TextEditor::HighlightingResults &results,
const Utils::FilePath &file);
void proposalReady(TextEditor::IAssistProposal *proposal);
private:
void handleDiagnostics(const LanguageServerProtocol::PublishDiagnosticsParams &params) override;

View File

@@ -334,8 +334,7 @@ void ClangModelManagerSupport::updateLanguageClient(ProjectExplorer::Project *pr
continue;
if (fallbackClient && fallbackClient->documentOpen(editor->textDocument()))
fallbackClient->closeDocument(editor->textDocument());
if (!client->documentOpen(editor->textDocument()))
client->openDocument(editor->textDocument());
client->openEditorDocument(editor);
ClangEditorDocumentProcessor::clearTextMarks(editor->textDocument()->filePath());
hasDocuments = true;
}
@@ -429,8 +428,8 @@ void ClangModelManagerSupport::onEditorOpened(Core::IEditor *editor)
// instead. Is this feasible?
ProjectExplorer::Project * const project
= ProjectExplorer::SessionManager::projectForFile(document->filePath());
if (Client * const client = clientForProject(project))
client->openDocument(textDocument);
if (ClangdClient * const client = clientForProject(project))
client->openEditorDocument(qobject_cast<TextEditor::BaseTextEditor *>(editor));
}
}

View File

@@ -44,7 +44,9 @@
#include <projectexplorer/kitinformation.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <projectexplorer/target.h>
#include <texteditor/codeassist/textdocumentmanipulatorinterface.h>
#include <cplusplus/SimpleLexer.h>
#include <utils/algorithm.h>
#include <utils/fileutils.h>
#include <utils/qtcassert.h>
@@ -608,5 +610,41 @@ const QStringList optionsForProject(ProjectExplorer::Project *project)
return ClangProjectSettings::globalCommandLineOptions();
}
// 7.3.3: using typename(opt) nested-name-specifier unqualified-id ;
bool isAtUsingDeclaration(TextEditor::TextDocumentManipulatorInterface &manipulator,
int basePosition)
{
using namespace CPlusPlus;
SimpleLexer lexer;
lexer.setLanguageFeatures(LanguageFeatures::defaultFeatures());
const QString textToLex = textUntilPreviousStatement(manipulator, basePosition);
const Tokens tokens = lexer(textToLex);
if (tokens.empty())
return false;
// The nested-name-specifier always ends with "::", so check for this first.
const Token lastToken = tokens[tokens.size() - 1];
if (lastToken.kind() != T_COLON_COLON)
return false;
return contains(tokens, [](const Token &token) { return token.kind() == T_USING; });
}
QString textUntilPreviousStatement(TextEditor::TextDocumentManipulatorInterface &manipulator,
int startPosition)
{
static const QString stopCharacters(";{}#");
int endPosition = 0;
for (int i = startPosition; i >= 0 ; --i) {
if (stopCharacters.contains(manipulator.characterAt(i))) {
endPosition = i + 1;
break;
}
}
return manipulator.textAt(endPosition, startPosition - endPosition);
}
} // namespace Internal
} // namespace Clang

View File

@@ -42,6 +42,8 @@ class ClangDiagnosticConfig;
class CppEditorDocumentHandle;
}
namespace TextEditor { class TextDocumentManipulatorInterface; }
namespace ClangBackEnd { class TokenInfoContainer; }
namespace ProjectExplorer { class Project; }
@@ -107,7 +109,7 @@ private:
};
template <class CharacterProvider>
void moveToPreviousChar(CharacterProvider &provider, QTextCursor &cursor)
void moveToPreviousChar(const CharacterProvider &provider, QTextCursor &cursor)
{
cursor.movePosition(QTextCursor::PreviousCharacter);
while (provider.characterAt(cursor.position()).isSpace())
@@ -123,7 +125,7 @@ void moveToPreviousWord(CharacterProvider &provider, QTextCursor &cursor)
}
template <class CharacterProvider>
bool matchPreviousWord(CharacterProvider &provider, QTextCursor cursor, QString pattern)
bool matchPreviousWord(const CharacterProvider &provider, QTextCursor cursor, QString pattern)
{
cursor.movePosition(QTextCursor::PreviousWord);
while (provider.characterAt(cursor.position()) == ':')
@@ -151,5 +153,11 @@ bool matchPreviousWord(CharacterProvider &provider, QTextCursor cursor, QString
return pattern.isEmpty();
}
QString textUntilPreviousStatement(TextEditor::TextDocumentManipulatorInterface &manipulator,
int startPosition);
bool isAtUsingDeclaration(TextEditor::TextDocumentManipulatorInterface &manipulator,
int basePosition);
} // namespace Internal
} // namespace Clang

View File

@@ -105,7 +105,7 @@ public:
TestDocument(const QByteArray &fileName, CppTools::Tests::TemporaryDir *temporaryDir = nullptr)
{
QTC_ASSERT(!fileName.isEmpty(), return);
const QResource resource(qrcPath(fileName));
const QResource resource(qrcPath("completion/" + fileName));
QTC_ASSERT(resource.isValid(), return);
const QByteArray contents = QByteArray(reinterpret_cast<const char*>(resource.data()),
resource.size());
@@ -534,7 +534,7 @@ void ClangCodeCompletionTest::testCompletePreprocessorKeywords()
void ClangCodeCompletionTest::testCompleteIncludeDirective()
{
CppTools::Tests::TemporaryCopiedDir testDir(qrcPath("exampleIncludeDir"));
CppTools::Tests::TemporaryCopiedDir testDir(qrcPath("completion/exampleIncludeDir"));
ProjectLessCompletionTest t("includeDirectiveCompletion.cpp",
QString(),
QStringList(testDir.path()));

View File

@@ -37,17 +37,22 @@
#include <cpptools/cpptoolstestcase.h>
#include <cpptools/semantichighlighter.h>
#include <coreplugin/editormanager/editormanager.h>
#include <languageclient/languageclientmanager.h>
#include <projectexplorer/kitmanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/target.h>
#include <qtsupport/qtkitinformation.h>
#include <texteditor/codeassist/assistproposaliteminterface.h>
#include <texteditor/codeassist/textdocumentmanipulatorinterface.h>
#include <utils/algorithm.h>
#include <utils/filepath.h>
#include <utils/textutils.h>
#include <QElapsedTimer>
#include <QEventLoop>
#include <QFileInfo>
#include <QPair>
#include <QScopedPointer>
#include <QTimer>
#include <QtTest>
@@ -78,6 +83,7 @@ namespace Tests {
ClangdTest::~ClangdTest()
{
EditorManager::closeAllEditors(false);
if (m_project)
ProjectExplorerPlugin::unloadProject(m_project);
delete m_projectDir;
@@ -88,6 +94,40 @@ Utils::FilePath ClangdTest::filePath(const QString &fileName) const
return Utils::FilePath::fromString(m_projectDir->absolutePath(fileName.toLocal8Bit()));
}
void ClangdTest::waitForNewClient(bool withIndex)
{
// Setting up the project should result in a clangd client being created.
// Wait until that has happened.
m_client = nullptr;
const auto modelManagerSupport = ClangModelManagerSupport::instance();
m_client = modelManagerSupport->clientForProject(project());
if (!m_client) {
QVERIFY(waitForSignalOrTimeout(modelManagerSupport,
&ClangModelManagerSupport::createdClient, timeOutInMs()));
m_client = modelManagerSupport->clientForProject(m_project);
}
QVERIFY(m_client);
m_client->enableTesting();
// Wait until the client is fully initialized, i.e. it's completed the handshake
// with the server.
if (!m_client->reachable())
QVERIFY(waitForSignalOrTimeout(m_client, &ClangdClient::initialized, timeOutInMs()));
QVERIFY(m_client->reachable());
if (m_minVersion != -1 && m_client->versionNumber() < QVersionNumber(m_minVersion))
QSKIP("clangd is too old");
// Wait for index to build.
if (withIndex) {
if (!m_client->isFullyIndexed()) {
QVERIFY(waitForSignalOrTimeout(m_client, &ClangdClient::indexingFinished,
clangdIndexingTimeout()));
}
QVERIFY(m_client->isFullyIndexed());
}
}
void ClangdTest::initTestCase()
{
const QString clangdFromEnv = qEnvironmentVariable("QTC_CLANGD");
@@ -115,34 +155,8 @@ void ClangdTest::initTestCase()
m_project = openProjectResult.project();
m_project->configureAsExampleProject(m_kit);
// Setting up the project should result in a clangd client being created.
// Wait until that has happened.
const auto modelManagerSupport = ClangModelManagerSupport::instance();
m_client = modelManagerSupport->clientForProject(openProjectResult.project());
if (!m_client) {
QVERIFY(waitForSignalOrTimeout(modelManagerSupport,
&ClangModelManagerSupport::createdClient, timeOutInMs()));
m_client = modelManagerSupport->clientForProject(m_project);
}
waitForNewClient();
QVERIFY(m_client);
m_client->enableTesting();
// Wait until the client is fully initialized, i.e. it's completed the handshake
// with the server.
if (!m_client->reachable())
QVERIFY(waitForSignalOrTimeout(m_client, &ClangdClient::initialized, timeOutInMs()));
QVERIFY(m_client->reachable());
// The kind of AST support we need was introduced in LLVM 13.
if (m_minVersion != -1 && m_client->versionNumber() < QVersionNumber(m_minVersion))
QSKIP("clangd is too old");
// Wait for index to build.
if (!m_client->isFullyIndexed()) {
QVERIFY(waitForSignalOrTimeout(m_client, &ClangdClient::indexingFinished,
clangdIndexingTimeout()));
}
QVERIFY(m_client->isFullyIndexed());
// Open cpp documents.
for (const QString &sourceFileName : qAsConst(m_sourceFileNames)) {
@@ -153,7 +167,8 @@ void ClangdTest::initTestCase()
QVERIFY(editor);
const auto doc = qobject_cast<TextEditor::TextDocument *>(editor->document());
QVERIFY(doc);
QVERIFY(m_client->documentForFilePath(sourceFilePath) == doc);
QVERIFY2(m_client->documentForFilePath(sourceFilePath) == doc,
qPrintable(sourceFilePath.toUserOutput()));
m_sourceDocuments.insert(sourceFileName, doc);
}
}
@@ -1328,6 +1343,586 @@ void ClangdTestHighlighting::test()
QCOMPARE(result.kind, expectedKind);
}
class Manipulator : public TextDocumentManipulatorInterface
{
public:
Manipulator()
{
const auto textEditor = static_cast<BaseTextEditor *>(EditorManager::currentEditor());
QVERIFY(textEditor);
m_doc = textEditor->textDocument()->document();
m_cursor = textEditor->editorWidget()->textCursor();
}
int currentPosition() const override { return m_cursor.position(); }
int positionAt(TextPositionOperation) const override { return 0; }
QChar characterAt(int position) const override { return m_doc->characterAt(position); }
QString textAt(int position, int length) const override
{
return m_doc->toPlainText().mid(position, length);
}
QTextCursor textCursorAt(int position) const override
{
QTextCursor cursor(m_doc);
cursor.setPosition(position);
return cursor;
}
void setCursorPosition(int position) override { m_cursor.setPosition(position); }
void setAutoCompleteSkipPosition(int position) override { m_skipPos = position; }
bool replace(int position, int length, const QString &text) override
{
QTextCursor cursor = textCursorAt(position);
cursor.setPosition(position + length, QTextCursor::KeepAnchor);
cursor.insertText(text);
return true;
}
void insertCodeSnippet(int pos, const QString &text, const SnippetParser &parser) override
{
const auto parseResult = parser(text);
if (const auto snippet = Utils::get_if<ParsedSnippet>(&parseResult)) {
if (!snippet->parts.isEmpty())
textCursorAt(pos).insertText(snippet->parts.first().text);
}
}
void paste() override {}
void encourageApply() override {}
void autoIndent(int, int) override {}
QString getLine(int line) const { return m_doc->findBlockByNumber(line - 1).text(); }
QPair<int, int> cursorPos() const
{
const int pos = currentPosition();
QPair<int, int> lineAndColumn;
Utils::Text::convertPosition(m_doc, pos, &lineAndColumn.first, &lineAndColumn.second);
return lineAndColumn;
}
int skipPos() const { return m_skipPos; }
private:
QTextDocument *m_doc;
QTextCursor m_cursor;
int m_skipPos = -1;
};
ClangdTestCompletion::ClangdTestCompletion()
{
setProjectFileName("completion.pro");
setSourceFileNames({"completionWithProject.cpp", "constructorCompletion.cpp",
"classAndConstructorCompletion.cpp", "dotToArrowCorrection.cpp",
"doxygenKeywordsCompletion.cpp", "functionAddress.cpp",
"functionCompletion.cpp", "functionCompletionFiltered2.cpp",
"functionCompletionFiltered.cpp", "globalCompletion.cpp",
"includeDirectiveCompletion.cpp", "mainwindow.cpp",
"memberCompletion.cpp", "membercompletion-friend.cpp",
"membercompletion-inside.cpp", "membercompletion-outside.cpp",
"noDotToArrowCorrectionForFloats.cpp",
"preprocessorKeywordsCompletion.cpp", "preprocessorKeywordsCompletion2.cpp",
"preprocessorKeywordsCompletion3.cpp", "signalCompletion.cpp"});
}
void ClangdTestCompletion::initTestCase()
{
ClangdTest::initTestCase();
client()->forceHighlightingOnEmptyDelta();
startCollectingHighlightingInfo();
}
void ClangdTestCompletion::testCompleteDoxygenKeywords()
{
ProposalModelPtr proposal;
getProposal("doxygenKeywordsCompletion.cpp", proposal);
QVERIFY(proposal);
QVERIFY(hasItem(proposal, "brief"));
QVERIFY(hasItem(proposal, "param"));
QVERIFY(hasItem(proposal, "return"));
QVERIFY(!hasSnippet(proposal, "class "));
}
void ClangdTestCompletion::testCompletePreprocessorKeywords()
{
ProposalModelPtr proposal;
getProposal("preprocessorKeywordsCompletion.cpp", proposal);
QVERIFY(proposal);
QVERIFY(hasItem(proposal, " ifdef macro"));
QVERIFY(!hasSnippet(proposal, "class "));
proposal.clear();
getProposal("preprocessorKeywordsCompletion2.cpp", proposal);
QVERIFY(proposal);
QVERIFY(hasItem(proposal, " endif"));
QVERIFY(!hasSnippet(proposal, "class "));
proposal.clear();
getProposal("preprocessorKeywordsCompletion3.cpp", proposal);
QVERIFY(proposal);
QEXPECT_FAIL("", "TODO: Fix in clangd", Continue);
QVERIFY(hasItem(proposal, " endif"));
QVERIFY(!hasSnippet(proposal, "class "));
}
void ClangdTestCompletion::testCompleteIncludeDirective()
{
ProposalModelPtr proposal;
getProposal("includeDirectiveCompletion.cpp", proposal);
QVERIFY(proposal);
QVERIFY(hasItem(proposal, " file.h>"));
QVERIFY(hasItem(proposal, " otherFile.h>"));
QVERIFY(hasItem(proposal, " mylib/"));
QVERIFY(!hasSnippet(proposal, "class "));
}
void ClangdTestCompletion::testCompleteGlobals()
{
ProposalModelPtr proposal;
int cursorPos = -1;
getProposal("globalCompletion.cpp", proposal, {}, &cursorPos);
QVERIFY(proposal);
QVERIFY(hasItem(proposal, " globalVariable", "int"));
QVERIFY(hasItem(proposal, " GlobalClass"));
QVERIFY(hasItem(proposal, " class")); // Keyword
QVERIFY(hasSnippet(proposal, "class ")); // Snippet
const AssistProposalItemInterface * const item = getItem(proposal, " globalFunction()", "void");
QVERIFY(item);
Manipulator manipulator;
item->apply(manipulator, cursorPos);
QCOMPARE(manipulator.getLine(7), " globalFunction() /* COMPLETE HERE */");
QCOMPARE(manipulator.cursorPos(), qMakePair(7, 20));
QCOMPARE(manipulator.skipPos(), -1);
}
void ClangdTestCompletion::testCompleteMembers()
{
ProposalModelPtr proposal;
int cursorPos = -1;
getProposal("memberCompletion.cpp", proposal, {}, &cursorPos);
QVERIFY(proposal);
QVERIFY(!hasItem(proposal, " globalVariable"));
QVERIFY(!hasItem(proposal, " class")); // Keyword
QVERIFY(!hasSnippet(proposal, "class ")); // Snippet
const AssistProposalItemInterface * const item = getItem(proposal, " member", "int");
QVERIFY(item);
Manipulator manipulator;
item->apply(manipulator, cursorPos);
QCOMPARE(manipulator.getLine(7), " s.member /* COMPLETE HERE */");
QCOMPARE(manipulator.cursorPos(), qMakePair(7, 13));
QCOMPARE(manipulator.skipPos(), -1);
}
void ClangdTestCompletion::testCompleteMembersFromInside()
{
ProposalModelPtr proposal;
int cursorPos = -1;
getProposal("membercompletion-inside.cpp", proposal, {}, &cursorPos);
QVERIFY(proposal);
QVERIFY(hasItem(proposal, " publicFunc()", "void"));
const AssistProposalItemInterface * const item = getItem(proposal, " privateFunc()", "void");
QVERIFY(item);
Manipulator manipulator;
item->apply(manipulator, cursorPos);
QCOMPARE(manipulator.getLine(4), " privateFunc() /* COMPLETE HERE */");
QCOMPARE(manipulator.cursorPos(), qMakePair(4, 22));
QCOMPARE(manipulator.skipPos(), -1);
}
void ClangdTestCompletion::testCompleteMembersFromOutside()
{
ProposalModelPtr proposal;
int cursorPos = -1;
getProposal("membercompletion-outside.cpp", proposal, {}, &cursorPos);
QVERIFY(proposal);
QVERIFY(!hasItem(proposal, " privateFunc", "void"));
const AssistProposalItemInterface * const item = getItem(proposal, " publicFunc()", "void");
QVERIFY(item);
Manipulator manipulator;
item->apply(manipulator, cursorPos);
QCOMPARE(manipulator.getLine(13), " c.publicFunc() /* COMPLETE HERE */");
QCOMPARE(manipulator.cursorPos(), qMakePair(13, 19));
QCOMPARE(manipulator.skipPos(), -1);
}
void ClangdTestCompletion::testCompleteMembersFromFriend()
{
ProposalModelPtr proposal;
int cursorPos = -1;
getProposal("membercompletion-friend.cpp", proposal, {}, &cursorPos);
QVERIFY(proposal);
QVERIFY(hasItem(proposal, " publicFunc()", "void"));
const AssistProposalItemInterface * const item = getItem(proposal, " privateFunc()", "void");
QVERIFY(item);
Manipulator manipulator;
item->apply(manipulator, cursorPos);
QCOMPARE(manipulator.getLine(14), " C().privateFunc() /* COMPLETE HERE */");
QCOMPARE(manipulator.cursorPos(), qMakePair(14, 22));
QCOMPARE(manipulator.skipPos(), -1);
}
void ClangdTestCompletion::testFunctionAddress()
{
ProposalModelPtr proposal;
int cursorPos = -1;
getProposal("functionAddress.cpp", proposal, {}, &cursorPos);
QVERIFY(proposal);
const AssistProposalItemInterface * const item = getItem(proposal, " memberFunc()", "void");
QVERIFY(item);
Manipulator manipulator;
item->apply(manipulator, cursorPos);
QCOMPARE(manipulator.getLine(7), " const auto p = &S::memberFunc /* COMPLETE HERE */;");
QCOMPARE(manipulator.cursorPos(), qMakePair(7, 34));
QCOMPARE(manipulator.skipPos(), -1);
}
void ClangdTestCompletion::testFunctionHints()
{
ProposalModelPtr proposal;
getProposal("functionCompletion.cpp", proposal);
QVERIFY(proposal);
QVERIFY(hasItem(proposal, "f() -> void"));
QVERIFY(hasItem(proposal, "f(int a) -> void"));
QVERIFY(hasItem(proposal, "f(const QString &s) -> void"));
QVERIFY(hasItem(proposal, "f(char c, int optional = 3) -> void"));
QVERIFY(hasItem(proposal, "f(char c, int optional1 = 3, int optional2 = 3) -> void"));
QVERIFY(hasItem(proposal, "f(const TType<QString> *t) -> void"));
QVERIFY(hasItem(proposal, "f(bool) -> TType<QString>"));
}
void ClangdTestCompletion::testFunctionHintsFiltered()
{
ProposalModelPtr proposal;
getProposal("functionCompletionFiltered.cpp", proposal);
QVERIFY(proposal);
QCOMPARE(proposal->size(), 1);
QVERIFY(hasItem(proposal, "func(int i, <b>int j</b>) -&gt; void"));
proposal.clear();
getProposal("functionCompletionFiltered2.cpp", proposal);
QVERIFY(proposal);
QCOMPARE(proposal->size(), 2);
QVERIFY(hasItem(proposal, "func(const S &amp;s, <b>int j</b>) -&gt; void"));
QEXPECT_FAIL("", "FIXME: LanguageClient handles active parameter only in active signature", Abort);
QVERIFY(hasItem(proposal, "func(const S &amp;s, <b>int j</b>, int k) -&gt; void"));
}
void ClangdTestCompletion::testFunctionHintConstructor()
{
ProposalModelPtr proposal;
getProposal("constructorCompletion.cpp", proposal);
QVERIFY(proposal);
QVERIFY(!hasItem(proposal, "globalVariable"));
QVERIFY(!hasItem(proposal, " class"));
QVERIFY(hasItem(proposal, "Foo(<b>int</b>)"));
QEXPECT_FAIL("", "FIXME: LanguageClient handles active parameter only in active signature", Abort);
QVERIFY(hasItem(proposal, "Foo(<b>int</b>, double)"));
}
void ClangdTestCompletion::testCompleteClassAndConstructor()
{
ProposalModelPtr proposal;
int cursorPos = -1;
getProposal("classAndConstructorCompletion.cpp", proposal, {}, &cursorPos);
QVERIFY(proposal);
QVERIFY(hasItem(proposal, " Foo"));
const AssistProposalItemInterface * const item
= getItem(proposal, QString::fromUtf8(" Foo(…)"), "[2 overloads]");
QVERIFY(item);
Manipulator manipulator;
item->apply(manipulator, cursorPos);
QCOMPARE(manipulator.getLine(7), " Foo( /* COMPLETE HERE */");
QCOMPARE(manipulator.cursorPos(), qMakePair(7, 9));
QCOMPARE(manipulator.skipPos(), -1);
}
void ClangdTestCompletion::testCompleteWithDotToArrowCorrection()
{
ProposalModelPtr proposal;
int cursorPos = -1;
getProposal("dotToArrowCorrection.cpp", proposal, ".", &cursorPos);
QVERIFY(proposal);
const AssistProposalItemInterface * const item = getItem(proposal, " member", "int");
QVERIFY(item);
Manipulator manipulator;
item->apply(manipulator, cursorPos);
QCOMPARE(manipulator.getLine(4), " bar->member /* COMPLETE HERE */");
QCOMPARE(manipulator.cursorPos(), qMakePair(4, 16));
QCOMPARE(manipulator.skipPos(), -1);
}
void ClangdTestCompletion::testDontCompleteWithDotToArrowCorrectionForFloats()
{
ProposalModelPtr proposal;
getProposal("noDotToArrowCorrectionForFloats.cpp", proposal, ".");
QVERIFY(proposal);
for (int i = 0; i < proposal->size(); ++i) // Offer only snippets.
QVERIFY2(hasSnippet(proposal, proposal->text(i)), qPrintable(proposal->text(i)));
}
void ClangdTestCompletion::testCompleteCodeInGeneratedUiFile()
{
ProposalModelPtr proposal;
int cursorPos = -1;
getProposal("mainwindow.cpp", proposal, {}, &cursorPos);
QVERIFY(proposal);
QVERIFY(hasItem(proposal, " menuBar"));
QVERIFY(hasItem(proposal, " statusBar"));
QVERIFY(hasItem(proposal, " centralWidget"));
const AssistProposalItemInterface * const item = getItem(
proposal, " setupUi(QMainWindow *MainWindow)", "void");
QVERIFY(item);
Manipulator manipulator;
item->apply(manipulator, cursorPos);
QCOMPARE(manipulator.getLine(34), " ui->setupUi( /* COMPLETE HERE */");
QCOMPARE(manipulator.cursorPos(), qMakePair(34, 17));
QCOMPARE(manipulator.skipPos(), -1);
}
void ClangdTestCompletion::testSignalCompletion_data()
{
QTest::addColumn<QString>("customCode");
QTest::addColumn<QStringList>("expectedSuggestions");
QTest::addRow("positive: connect() on QObject class")
<< QString("QObject::connect(dummy, &QObject::")
<< QStringList{"aSignal()", "anotherSignal()"};
QTest::addRow("positive: connect() on QObject object")
<< QString("QObject o; o.connect(dummy, &QObject::")
<< QStringList{"aSignal()", "anotherSignal()"};
QTest::addRow("positive: connect() on QObject pointer")
<< QString("QObject *o; o->connect(dummy, &QObject::")
<< QStringList{"aSignal()", "anotherSignal()"};
QTest::addRow("positive: connect() on QObject rvalue")
<< QString("QObject().connect(dummy, &QObject::")
<< QStringList{"aSignal()", "anotherSignal()"};
QTest::addRow("positive: connect() on QObject pointer rvalue")
<< QString("(new QObject)->connect(dummy, &QObject::")
<< QStringList{"aSignal()", "anotherSignal()"};
QTest::addRow("positive: disconnect() on QObject")
<< QString("QObject::disconnect(dummy, &QObject::")
<< QStringList{"aSignal()", "anotherSignal()"};
QTest::addRow("positive: connect() in member function of derived class")
<< QString("}void DerivedFromQObject::alsoNotASignal() { connect(this, &DerivedFromQObject::")
<< QStringList{"aSignal()", "anotherSignal()", "myOwnSignal()"};
const QStringList allQObjectFunctions{"aSignal()", "anotherSignal()", "notASignal()",
"connect()", "disconnect()", "QObject", "~QObject()",
"operator=(…)"};
QTest::addRow("negative: different function name")
<< QString("QObject::notASignal(dummy, &QObject::")
<< allQObjectFunctions;
QTest::addRow("negative: connect function from other class")
<< QString("NotAQObject::connect(dummy, &QObject::")
<< allQObjectFunctions;
QTest::addRow("negative: first argument")
<< QString("QObject::connect(QObject::")
<< allQObjectFunctions;
QTest::addRow("negative: third argument")
<< QString("QObject::connect(dummy1, dummy2, QObject::")
<< allQObjectFunctions;
QTest::addRow("negative: not a QObject")
<< QString("QObject::connect(dummy, &NotAQObject::")
<< QStringList{"notASignal()", "alsoNotASignal()", "connect()", "NotAQObject",
"~NotAQObject()", "operator=(…)"};
}
void ClangdTestCompletion::testSignalCompletion()
{
QFETCH(QString, customCode);
QFETCH(QStringList, expectedSuggestions);
ProposalModelPtr proposal;
getProposal("signalCompletion.cpp", proposal, customCode);
QVERIFY(proposal);
if (client()->versionNumber() < QVersionNumber(14)
&& QString::fromLatin1(QTest::currentDataTag()).startsWith("positive:")) {
QEXPECT_FAIL("", "Signal info in completions requires clangd >= 14", Abort);
}
QCOMPARE(proposal->size(), expectedSuggestions.size());
for (const QString &expectedSuggestion : qAsConst(expectedSuggestions))
QVERIFY2(hasItem(proposal, ' ' + expectedSuggestion), qPrintable(expectedSuggestion));
}
void ClangdTestCompletion::testCompleteAfterProjectChange()
{
// No defines set, completion must come from #else branch.
ProposalModelPtr proposal;
getProposal("completionWithProject.cpp", proposal);
QVERIFY(proposal);
QVERIFY(!hasItem(proposal, " projectConfiguration1"));
QVERIFY(!hasItem(proposal, " projectConfiguration2"));
QVERIFY(hasItem(proposal, " noProjectConfigurationDetected"));
// Set define in project file, completion must come from #if branch.
const auto proFileEditor = qobject_cast<BaseTextEditor *>(
EditorManager::openEditor(project()->projectFilePath()));
QVERIFY(proFileEditor);
proFileEditor->insert("DEFINES += PROJECT_CONFIGURATION_1\n");
QString saveError;
QVERIFY2(proFileEditor->document()->save(&saveError), qPrintable(saveError));
QVERIFY(waitForSignalOrTimeout(project(), &Project::anyParsingFinished, timeOutInMs()));
QVERIFY(waitForSignalOrTimeout(LanguageClient::LanguageClientManager::instance(),
&LanguageClient::LanguageClientManager::clientRemoved,
timeOutInMs()));
// Waiting for the index will cause highlighting info collection to start too late,
// so skip it.
waitForNewClient(false);
QVERIFY(client());
startCollectingHighlightingInfo();
proposal.clear();
getProposal("completionWithProject.cpp", proposal);
QVERIFY(proposal);
QVERIFY(hasItem(proposal, " projectConfiguration1"));
QVERIFY(!hasItem(proposal, " projectConfiguration2"));
QVERIFY(!hasItem(proposal, " noProjectConfigurationDetected"));
}
void ClangdTestCompletion::startCollectingHighlightingInfo()
{
m_documentsWithHighlighting.clear();
connect(client(), &ClangdClient::highlightingResultsReady, this,
[this](const HighlightingResults &, const Utils::FilePath &file) {
m_documentsWithHighlighting.insert(file);
});
}
void ClangdTestCompletion::getProposal(const QString &fileName,
TextEditor::ProposalModelPtr &proposalModel,
const QString &insertString,
int *cursorPos)
{
const TextDocument * const doc = document(fileName);
QVERIFY(doc);
const int pos = doc->document()->toPlainText().indexOf(" /* COMPLETE HERE */");
QVERIFY(pos != -1);
if (cursorPos)
*cursorPos = pos;
int line, column;
Utils::Text::convertPosition(doc->document(), pos, &line, &column);
const auto editor = qobject_cast<BaseTextEditor *>(
EditorManager::openEditorAt(doc->filePath().toString(), line, column - 1));
QVERIFY(editor);
QCOMPARE(EditorManager::currentEditor(), editor);
QCOMPARE(editor->textDocument(), doc);
if (!insertString.isEmpty()) {
m_documentsWithHighlighting.remove(doc->filePath());
editor->insert(insertString);
if (cursorPos)
*cursorPos += insertString.length();
}
// Once clangd has sent highlighting information for a file, we know it is also
// ready for completion.
QElapsedTimer highlightingTimer;
highlightingTimer.start();
while (!m_documentsWithHighlighting.contains(doc->filePath())
&& highlightingTimer.elapsed() < timeOutInMs()) {
waitForSignalOrTimeout(client(), &ClangdClient::highlightingResultsReady, 1000);
};
QVERIFY2(m_documentsWithHighlighting.contains(doc->filePath()), qPrintable(fileName));
QTimer timer;
timer.setSingleShot(true);
QEventLoop loop;
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
IAssistProposal *proposal = nullptr;
connect(client(), &ClangdClient::proposalReady, &loop, [&proposal, &loop](IAssistProposal *p) {
proposal = p;
loop.quit();
});
editor->editorWidget()->invokeAssist(Completion, nullptr);
timer.start(5000);
loop.exec();
QVERIFY(timer.isActive());
QVERIFY(proposal);
proposalModel = proposal->model();
delete proposal;
// The "dot" test files are only used once.
if (!insertString.isEmpty() && insertString != ".") {
m_documentsWithHighlighting.remove(doc->filePath());
editor->editorWidget()->undo();
}
}
bool ClangdTestCompletion::hasItem(TextEditor::ProposalModelPtr model, const QString &text,
const QString &detail)
{
for (int i = 0; i < model->size(); ++i) {
if (model->text(i) == text) {
if (detail.isEmpty())
return true;
const auto genericModel = model.dynamicCast<GenericProposalModel>();
return genericModel && genericModel->detail(i) == detail;
}
}
return false;
}
bool ClangdTestCompletion::hasSnippet(TextEditor::ProposalModelPtr model, const QString &text)
{
const auto genericModel = model.dynamicCast<GenericProposalModel>();
if (!genericModel)
return false;
for (int i = 0, size = genericModel->size(); i < size; ++i) {
const TextEditor::AssistProposalItemInterface * const item = genericModel->proposalItem(i);
if (item->text() == text)
return item->isSnippet();
}
return false;
}
AssistProposalItemInterface *ClangdTestCompletion::getItem(
TextEditor::ProposalModelPtr model, const QString &text, const QString &detail)
{
const auto genericModel = model.dynamicCast<GenericProposalModel>();
if (!genericModel)
return nullptr;
for (int i = 0; i < genericModel->size(); ++i) {
if (genericModel->text(i) == text &&
(detail.isEmpty() || detail == genericModel->detail(i))) {
return genericModel->proposalItem(i);
}
}
return nullptr;
}
} // namespace Tests
} // namespace Internal
} // namespace ClangCodeModel

View File

@@ -27,11 +27,13 @@
#include <cpptools/cpptoolstestcase.h>
#include <coreplugin/find/searchresultitem.h>
#include <texteditor/codeassist/genericproposal.h>
#include <texteditor/semantichighlighter.h>
#include <utils/fileutils.h>
#include <QHash>
#include <QObject>
#include <QSet>
#include <QStringList>
namespace ProjectExplorer {
@@ -63,6 +65,8 @@ protected:
TextEditor::TextDocument *document(const QString &fileName) const {
return m_sourceDocuments.value(fileName);
}
ProjectExplorer::Project *project() const { return m_project; }
void waitForNewClient(bool withIndex = true);
protected slots:
virtual void initTestCase();
@@ -142,6 +146,54 @@ private:
TextEditor::HighlightingResults m_results;
};
class ClangdTestCompletion : public ClangdTest
{
Q_OBJECT
public:
ClangdTestCompletion();
private slots:
void initTestCase() override;
void testCompleteDoxygenKeywords();
void testCompletePreprocessorKeywords();
void testCompleteIncludeDirective();
void testCompleteGlobals();
void testCompleteMembers();
void testCompleteMembersFromInside();
void testCompleteMembersFromOutside();
void testCompleteMembersFromFriend();
void testFunctionAddress();
void testFunctionHints();
void testFunctionHintsFiltered();
void testFunctionHintConstructor();
void testCompleteClassAndConstructor();
void testCompleteWithDotToArrowCorrection();
void testDontCompleteWithDotToArrowCorrectionForFloats();
void testCompleteCodeInGeneratedUiFile();
void testSignalCompletion_data();
void testSignalCompletion();
void testCompleteAfterProjectChange();
private:
void startCollectingHighlightingInfo();
void getProposal(const QString &fileName, TextEditor::ProposalModelPtr &proposalModel,
const QString &insertString = {}, int *cursorPos = nullptr);
static bool hasItem(TextEditor::ProposalModelPtr model, const QString &text,
const QString &detail = {});
static bool hasSnippet(TextEditor::ProposalModelPtr model, const QString &text);
static int itemsWithText(TextEditor::ProposalModelPtr model, const QString &text);
static TextEditor::AssistProposalItemInterface *getItem(
TextEditor::ProposalModelPtr model, const QString &text, const QString &detail = {});
QSet<Utils::FilePath> m_documentsWithHighlighting;
};
} // namespace Tests
} // namespace Internal
} // namespace ClangCodeModel

View File

@@ -5,30 +5,11 @@
<file>qt-widgets-app/mainwindow.h</file>
<file>qt-widgets-app/mainwindow.ui</file>
<file>qt-widgets-app/qt-widgets-app.pro</file>
<file>exampleIncludeDir/file.h</file>
<file>exampleIncludeDir/mylib/mylib.h</file>
<file>exampleIncludeDir/otherFile.h</file>
<file>completionWithProject.cpp</file>
<file>constructorCompletion.cpp</file>
<file>doxygenKeywordsCompletion.cpp</file>
<file>functionCompletion.cpp</file>
<file>globalCompletion.cpp</file>
<file>includeDirectiveCompletion.cpp</file>
<file>memberCompletion.cpp</file>
<file>myheader.h</file>
<file>mysource.cpp</file>
<file>objc_messages_1.mm</file>
<file>objc_messages_2.mm</file>
<file>objc_messages_3.mm</file>
<file>preprocessorKeywordsCompletion.cpp</file>
<file>dotToArrowCorrection.cpp</file>
<file>noDotToArrowCorrectionForFloats.cpp</file>
<file>classAndConstructorCompletion.cpp</file>
<file>membercompletion-outside.cpp</file>
<file>membercompletion-inside.cpp</file>
<file>membercompletion-friend.cpp</file>
<file>functionCompletionFiltered.cpp</file>
<file>functionCompletionFiltered2.cpp</file>
<file>find-usages/defs.h</file>
<file>find-usages/main.cpp</file>
<file>find-usages/find-usages.pro</file>
@@ -39,10 +20,38 @@
<file>follow-symbol/main.cpp</file>
<file>local-references/local-references.pro</file>
<file>local-references/references.cpp</file>
<file>tooltips/subdir/tooltipinfo.h</file>
<file>tooltips/tooltips.cpp</file>
<file>tooltips/tooltips.pro</file>
<file>highlighting/highlighting.cpp</file>
<file>highlighting/highlighting.pro</file>
<file>tooltips/subdir/tooltipinfo.h</file>
<file>completion/completion.pro</file>
<file>completion/classAndConstructorCompletion.cpp</file>
<file>completion/completionWithProject.cpp</file>
<file>completion/constructorCompletion.cpp</file>
<file>completion/doxygenKeywordsCompletion.cpp</file>
<file>completion/functionCompletion.cpp</file>
<file>completion/functionCompletionFiltered.cpp</file>
<file>completion/functionCompletionFiltered2.cpp</file>
<file>completion/globalCompletion.cpp</file>
<file>completion/includeDirectiveCompletion.cpp</file>
<file>completion/membercompletion-friend.cpp</file>
<file>completion/membercompletion-inside.cpp</file>
<file>completion/membercompletion-outside.cpp</file>
<file>completion/memberCompletion.cpp</file>
<file>completion/preprocessorKeywordsCompletion.cpp</file>
<file>completion/exampleIncludeDir/file.h</file>
<file>completion/exampleIncludeDir/otherFile.h</file>
<file>completion/exampleIncludeDir/mylib/mylib.h</file>
<file>completion/dotToArrowCorrection.cpp</file>
<file>completion/noDotToArrowCorrectionForFloats.cpp</file>
<file>completion/main.cpp</file>
<file>completion/mainwindow.cpp</file>
<file>completion/mainwindow.h</file>
<file>completion/mainwindow.ui</file>
<file>completion/signalCompletion.cpp</file>
<file>completion/functionAddress.cpp</file>
<file>completion/preprocessorKeywordsCompletion2.cpp</file>
<file>completion/preprocessorKeywordsCompletion3.cpp</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,30 @@
QT += widgets
INCLUDEPATH += $$PWD/exampleIncludeDir
SOURCES = \
classAndConstructorCompletion.cpp \
completionWithProject.cpp \
constructorCompletion.cpp \
dotToArrowCorrection.cpp \
doxygenKeywordsCompletion.cpp \
functionAddress.cpp \
functionCompletion.cpp \
functionCompletionFiltered2.cpp \
functionCompletionFiltered.cpp \
globalCompletion.cpp \
includeDirectiveCompletion.cpp \
main.cpp \
mainwindow.cpp \
memberCompletion.cpp \
membercompletion-friend.cpp \
membercompletion-inside.cpp \
membercompletion-outside.cpp \
noDotToArrowCorrectionForFloats.cpp \
preprocessorKeywordsCompletion.cpp \
preprocessorKeywordsCompletion2.cpp \
preprocessorKeywordsCompletion3.cpp \
signalCompletion.cpp
HEADERS = mainwindow.h
FORMS = mainwindow.ui

View File

@@ -0,0 +1,8 @@
struct S {
void memberFunc();
};
void func()
{
const auto p = &S::mem /* COMPLETE HERE */;
}

View File

@@ -0,0 +1,36 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://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 https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}

View File

@@ -0,0 +1,40 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://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 https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui-> /* COMPLETE HERE */
}
MainWindow::~MainWindow()
{
delete ui;
}

View File

@@ -0,0 +1,44 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://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 https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#pragma once
#include <QMainWindow>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private:
Ui::MainWindow *ui;
};

View File

@@ -0,0 +1,20 @@
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow" >
<property name="geometry" >
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<widget class="QMenuBar" name="menuBar" />
<widget class="QToolBar" name="mainToolBar" />
<widget class="QWidget" name="centralWidget" />
<widget class="QStatusBar" name="statusBar" />
</widget>
<layoutDefault spacing="6" margin="11" />
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,3 @@
#if 1
int x;
#en /* COMPLETE HERE */

View File

@@ -0,0 +1,3 @@
#if 0
int x;
#en /* COMPLETE HERE */

View File

@@ -0,0 +1,23 @@
class QObject {
public:
void aSignal() __attribute__((annotate("qt_signal")));
void anotherSignal() __attribute__((annotate("qt_signal")));
void notASignal();
static void connect();
static void disconnect();
};
class DerivedFromQObject : public QObject {
public:
void myOwnSignal() __attribute__((annotate("qt_signal")));
void alsoNotASignal();
};
class NotAQObject {
public:
void notASignal();
void alsoNotASignal();
static void connect();
};
void someFunction()
{
/* COMPLETE HERE */

View File

@@ -137,14 +137,28 @@ bool CppEditorDocument::isObjCEnabled() const
return m_isObjCEnabled;
}
CppTools::CppCompletionAssistProvider *CppEditorDocument::completionAssistProvider() const
void CppEditorDocument::setCompletionAssistProvider(TextEditor::CompletionAssistProvider *provider)
{
return m_completionAssistProvider;
TextDocument::setCompletionAssistProvider(provider);
m_completionAssistProvider = nullptr;
}
CppTools::CppCompletionAssistProvider *CppEditorDocument::functionHintAssistProvider() const
void CppEditorDocument::setFunctionHintAssistProvider(TextEditor::CompletionAssistProvider *provider)
{
return m_functionHintAssistProvider;
TextDocument::setFunctionHintAssistProvider(provider);
m_functionHintAssistProvider = nullptr;
}
CompletionAssistProvider *CppEditorDocument::completionAssistProvider() const
{
return m_completionAssistProvider
? m_completionAssistProvider : TextDocument::completionAssistProvider();
}
CompletionAssistProvider *CppEditorDocument::functionHintAssistProvider() const
{
return m_functionHintAssistProvider
? m_functionHintAssistProvider : TextDocument::functionHintAssistProvider();
}
TextEditor::IAssistProvider *CppEditorDocument::quickFixAssistProvider() const

View File

@@ -52,8 +52,10 @@ public:
explicit CppEditorDocument();
bool isObjCEnabled() const;
CppTools::CppCompletionAssistProvider *completionAssistProvider() const override;
CppTools::CppCompletionAssistProvider *functionHintAssistProvider() const override;
void setCompletionAssistProvider(TextEditor::CompletionAssistProvider *provider) override;
void setFunctionHintAssistProvider(TextEditor::CompletionAssistProvider *provider) override;
TextEditor::CompletionAssistProvider *completionAssistProvider() const override;
TextEditor::CompletionAssistProvider *functionHintAssistProvider() const override;
TextEditor::IAssistProvider *quickFixAssistProvider() const override;
void recalculateSemanticInfoDetached();

View File

@@ -1007,8 +1007,8 @@ AssistInterface *CppEditorWidget::createAssistInterface(AssistKind kind, AssistR
{
if (kind == Completion || kind == FunctionHint) {
CppCompletionAssistProvider * const cap = kind == Completion
? cppEditorDocument()->completionAssistProvider()
: cppEditorDocument()->functionHintAssistProvider();
? qobject_cast<CppCompletionAssistProvider *>(cppEditorDocument()->completionAssistProvider())
: qobject_cast<CppCompletionAssistProvider *>(cppEditorDocument()->functionHintAssistProvider());
if (cap) {
LanguageFeatures features = LanguageFeatures::defaultFeatures();
if (Document::Ptr doc = d->m_lastSemanticInfo.doc)
@@ -1019,6 +1019,8 @@ AssistInterface *CppEditorWidget::createAssistInterface(AssistKind kind, AssistR
features,
position(),
reason);
} else {
return TextEditorWidget::createAssistInterface(kind, reason);
}
} else if (kind == QuickFix) {
if (isSemanticInfoValid())

View File

@@ -1017,6 +1017,13 @@ void Client::setSemanticTokensHandler(const SemanticTokensHandler &handler)
m_tokenSupport.setTokensHandler(handler);
}
#ifdef WITH_TESTS
void Client::forceHighlightingOnEmptyDelta()
{
m_tokenSupport.forceHighlightingOnEmptyDelta();
}
#endif
void Client::setSymbolStringifier(const LanguageServerProtocol::SymbolStringifier &stringifier)
{
m_symbolStringifier = stringifier;
@@ -1027,6 +1034,46 @@ SymbolStringifier Client::symbolStringifier() const
return m_symbolStringifier;
}
void Client::setCompletionItemsTransformer(const CompletionItemsTransformer &transformer)
{
if (const auto provider = qobject_cast<LanguageClientCompletionAssistProvider *>(
m_clientProviders.completionAssistProvider)) {
provider->setItemsTransformer(transformer);
}
}
void Client::setCompletionApplyHelper(const CompletionApplyHelper &applyHelper)
{
if (const auto provider = qobject_cast<LanguageClientCompletionAssistProvider *>(
m_clientProviders.completionAssistProvider)) {
provider->setApplyHelper(applyHelper);
}
}
void Client::setCompletionProposalHandler(const ProposalHandler &handler)
{
if (const auto provider = qobject_cast<LanguageClientCompletionAssistProvider *>(
m_clientProviders.completionAssistProvider)) {
provider->setProposalHandler(handler);
}
}
void Client::setFunctionHintProposalHandler(const ProposalHandler &handler)
{
if (const auto provider = qobject_cast<FunctionHintAssistProvider *>(
m_clientProviders.functionHintProvider)) {
provider->setProposalHandler(handler);
}
}
void Client::setSnippetsGroup(const QString &group)
{
if (const auto provider = qobject_cast<LanguageClientCompletionAssistProvider *>(
m_clientProviders.completionAssistProvider)) {
provider->setSnippetsGroup(group);
}
}
void Client::start()
{
if (m_clientInterface->start())

View File

@@ -183,6 +183,11 @@ public:
void setSemanticTokensHandler(const SemanticTokensHandler &handler);
void setSymbolStringifier(const LanguageServerProtocol::SymbolStringifier &stringifier);
LanguageServerProtocol::SymbolStringifier symbolStringifier() const;
void setCompletionItemsTransformer(const CompletionItemsTransformer &transformer);
void setCompletionApplyHelper(const CompletionApplyHelper &applyHelper);
void setCompletionProposalHandler(const ProposalHandler &handler);
void setFunctionHintProposalHandler(const ProposalHandler &handler);
void setSnippetsGroup(const QString &group);
// logging
void log(const QString &message) const;
@@ -190,6 +195,10 @@ public:
void log(const LanguageServerProtocol::ResponseError<Error> &responseError) const
{ log(responseError.toString()); }
#ifdef WITH_TESTS
void forceHighlightingOnEmptyDelta();
#endif
signals:
void initialized(const LanguageServerProtocol::ServerCapabilities &capabilities);
void capabilitiesChanged(const DynamicCapabilities &capabilities);

View File

@@ -36,6 +36,7 @@
#include <texteditor/codeassist/genericproposalmodel.h>
#include <texteditor/codeassist/iassistprocessor.h>
#include <texteditor/snippets/snippet.h>
#include <texteditor/snippets/snippetassistcollector.h>
#include <texteditor/texteditorsettings.h>
#include <utils/algorithm.h>
#include <utils/textutils.h>
@@ -58,7 +59,7 @@ namespace LanguageClient {
class LanguageClientCompletionItem : public AssistProposalItemInterface
{
public:
LanguageClientCompletionItem(CompletionItem item);
LanguageClientCompletionItem(CompletionItem item, const CompletionApplyHelper &applyHelper);
// AssistProposalItemInterface interface
QString text() const override;
@@ -80,13 +81,15 @@ public:
private:
CompletionItem m_item;
const CompletionApplyHelper m_applyHelper;
mutable QChar m_triggeredCommitCharacter;
mutable QString m_sortText;
mutable QString m_filterText;
};
LanguageClientCompletionItem::LanguageClientCompletionItem(CompletionItem item)
: m_item(std::move(item))
LanguageClientCompletionItem::LanguageClientCompletionItem(CompletionItem item,
const CompletionApplyHelper &applyHelper)
: m_item(std::move(item)), m_applyHelper(applyHelper)
{ }
QString LanguageClientCompletionItem::text() const
@@ -107,10 +110,15 @@ bool LanguageClientCompletionItem::prematurelyApplies(const QChar &typedCharacte
void LanguageClientCompletionItem::apply(TextDocumentManipulatorInterface &manipulator,
int /*basePosition*/) const
{
const int pos = manipulator.currentPosition();
if (m_applyHelper) {
m_applyHelper(m_item, manipulator, m_triggeredCommitCharacter);
return;
}
if (auto edit = m_item.textEdit()) {
applyTextEdit(manipulator, *edit, isSnippet());
} else {
const int pos = manipulator.currentPosition();
const QString textToInsert(m_item.insertText().value_or(text()));
int length = 0;
for (auto it = textToInsert.crbegin(), end = textToInsert.crend(); it != end; ++it) {
@@ -253,16 +261,22 @@ public:
void sort(const QString &/*prefix*/) override;
bool supportsPrefixExpansion() const override { return false; }
QList<LanguageClientCompletionItem *> items() const
{ return Utils::static_container_cast<LanguageClientCompletionItem *>(m_currentItems); }
QList<AssistProposalItemInterface *> items() const { return m_currentItems; }
};
void LanguageClientCompletionModel::sort(const QString &/*prefix*/)
{
std::sort(m_currentItems.begin(), m_currentItems.end(),
[] (AssistProposalItemInterface *a, AssistProposalItemInterface *b){
return *(dynamic_cast<LanguageClientCompletionItem *>(a)) < *(
dynamic_cast<LanguageClientCompletionItem *>(b));
const auto lca = dynamic_cast<LanguageClientCompletionItem *>(a);
const auto lcb = dynamic_cast<LanguageClientCompletionItem *>(b);
if (!lca && !lcb)
return a->text() < b->text();
if (lca && lcb)
return *lca < *lcb;
if (lca && !lcb)
return true;
return false;
});
}
@@ -281,8 +295,10 @@ public:
return false;
return m_model->keepPerfectMatch(reason)
|| !Utils::anyOf(m_model->items(), [this](LanguageClientCompletionItem *item){
return item->isPerfectMatch(m_pos, m_document);
|| !Utils::anyOf(m_model->items(), [this](AssistProposalItemInterface *item){
if (const auto lcItem = dynamic_cast<LanguageClientCompletionItem *>(item))
return lcItem->isPerfectMatch(m_pos, m_document);
return false;
});
}
@@ -295,7 +311,11 @@ public:
class LanguageClientCompletionAssistProcessor final : public IAssistProcessor
{
public:
LanguageClientCompletionAssistProcessor(Client *client);
LanguageClientCompletionAssistProcessor(Client *client,
const CompletionItemsTransformer &itemsTransformer,
const CompletionApplyHelper &applyHelper,
const ProposalHandler &proposalHandler,
const QString &snippetsGroup);
~LanguageClientCompletionAssistProcessor() override;
IAssistProposal *perform(const AssistInterface *interface) override;
bool running() override;
@@ -306,15 +326,23 @@ private:
void handleCompletionResponse(const CompletionRequest::Response &response);
QPointer<QTextDocument> m_document;
Utils::FilePath m_filePath;
QPointer<Client> m_client;
Utils::optional<MessageId> m_currentRequest;
QMetaObject::Connection m_postponedUpdateConnection;
const CompletionItemsTransformer m_itemsTransformer;
const CompletionApplyHelper m_applyHelper;
const ProposalHandler m_proposalHandler;
const QString m_snippetsGroup;
int m_pos = -1;
int m_basePos = -1;
};
LanguageClientCompletionAssistProcessor::LanguageClientCompletionAssistProcessor(Client *client)
: m_client(client)
LanguageClientCompletionAssistProcessor::LanguageClientCompletionAssistProcessor(Client *client,
const CompletionItemsTransformer &itemsTransformer, const CompletionApplyHelper &applyHelper,
const ProposalHandler &proposalHandler, const QString &snippetsGroup)
: m_client(client), m_itemsTransformer(itemsTransformer), m_applyHelper(applyHelper),
m_proposalHandler(proposalHandler), m_snippetsGroup(snippetsGroup)
{ }
LanguageClientCompletionAssistProcessor::~LanguageClientCompletionAssistProcessor()
@@ -384,6 +412,7 @@ IAssistProposal *LanguageClientCompletionAssistProcessor::perform(const AssistIn
m_client->addAssistProcessor(this);
m_currentRequest = completionRequest.id();
m_document = interface->textDocument();
m_filePath = interface->filePath();
qCDebug(LOGLSPCOMPLETION) << QTime::currentTime()
<< " : request completions at " << m_pos
<< " by " << assistReasonString(interface->reason());
@@ -430,17 +459,28 @@ void LanguageClientCompletionAssistProcessor::handleCompletionResponse(
} else if (Utils::holds_alternative<QList<CompletionItem>>(*result)) {
items = Utils::get<QList<CompletionItem>>(*result);
}
if (m_itemsTransformer && m_document)
items = m_itemsTransformer(m_filePath, m_document->toPlainText(), m_basePos, items);
auto model = new LanguageClientCompletionModel();
model->loadContent(Utils::transform(items, [](const CompletionItem &item){
return static_cast<AssistProposalItemInterface *>(new LanguageClientCompletionItem(item));
}));
auto proposalItems = Utils::transform<QList<AssistProposalItemInterface *>>(items,
[this](const CompletionItem &item) {
return new LanguageClientCompletionItem(item, m_applyHelper);
});
if (!m_snippetsGroup.isEmpty()) {
proposalItems << TextEditor::SnippetAssistCollector(
m_snippetsGroup, QIcon(":/texteditor/images/snippet.png")).collect();
}
model->loadContent(proposalItems);
LanguageClientCompletionProposal *proposal = new LanguageClientCompletionProposal(m_basePos,
model);
proposal->m_document = m_document;
proposal->m_pos = m_pos;
proposal->setFragile(true);
proposal->setSupportsPrefix(false);
setAsyncProposalAvailable(proposal);
if (m_proposalHandler)
m_proposalHandler(proposal);
else
setAsyncProposalAvailable(proposal);
m_client->removeAssistProcessor(this);
qCDebug(LOGLSPCOMPLETION) << QTime::currentTime() << " : "
<< items.count() << " completions handled";
@@ -453,7 +493,8 @@ LanguageClientCompletionAssistProvider::LanguageClientCompletionAssistProvider(C
IAssistProcessor *LanguageClientCompletionAssistProvider::createProcessor() const
{
return new LanguageClientCompletionAssistProcessor(m_client);
return new LanguageClientCompletionAssistProcessor(m_client, m_itemsTransformer, m_applyHelper,
m_proposalHandler, m_snippetsGroup);
}
IAssistProvider::RunType LanguageClientCompletionAssistProvider::runType() const
@@ -484,4 +525,16 @@ void LanguageClientCompletionAssistProvider::setTriggerCharacters(
}
}
void LanguageClientCompletionAssistProvider::setItemsTransformer(
const CompletionItemsTransformer &transformer)
{
m_itemsTransformer = transformer;
}
void LanguageClientCompletionAssistProvider::setApplyHelper(
const CompletionApplyHelper &applyHelper)
{
m_applyHelper = applyHelper;
}
} // namespace LanguageClient

View File

@@ -25,14 +25,30 @@
#pragma once
#include <languageserverprotocol/completion.h>
#include <texteditor/codeassist/completionassistprovider.h>
#include <utils/optional.h>
#include <functional>
namespace TextEditor {
class IAssistProposal;
class TextDocumentManipulatorInterface;
}
namespace LanguageClient {
class Client;
using CompletionItemsTransformer = std::function<QList<LanguageServerProtocol::CompletionItem>(
const Utils::FilePath &, const QString &, int,
const QList<LanguageServerProtocol::CompletionItem> &)>;
using CompletionApplyHelper = std::function<void(
const LanguageServerProtocol::CompletionItem &,
TextEditor::TextDocumentManipulatorInterface &, QChar)>;
using ProposalHandler = std::function<void(TextEditor::IAssistProposal *)>;
class LanguageClientCompletionAssistProvider : public TextEditor::CompletionAssistProvider
{
Q_OBJECT
@@ -49,8 +65,17 @@ public:
void setTriggerCharacters(const Utils::optional<QList<QString>> triggerChars);
void setItemsTransformer(const CompletionItemsTransformer &transformer);
void setApplyHelper(const CompletionApplyHelper &applyHelper);
void setProposalHandler(const ProposalHandler &handler) { m_proposalHandler = handler; }
void setSnippetsGroup(const QString &group) { m_snippetsGroup = group; }
private:
QList<QString> m_triggerChars;
CompletionItemsTransformer m_itemsTransformer;
CompletionApplyHelper m_applyHelper;
ProposalHandler m_proposalHandler;
QString m_snippetsGroup;
int m_activationCharSequenceLength = 0;
Client *m_client = nullptr; // not owned
};

View File

@@ -84,7 +84,8 @@ QString FunctionHintProposalModel::text(int index) const
class FunctionHintProcessor : public IAssistProcessor
{
public:
explicit FunctionHintProcessor(Client *client) : m_client(client) {}
explicit FunctionHintProcessor(Client *client, const ProposalHandler &proposalHandler)
: m_client(client), m_proposalHandler(proposalHandler) {}
IAssistProposal *perform(const AssistInterface *interface) override;
bool running() override { return m_currentRequest.has_value(); }
bool needsRestart() const override { return true; }
@@ -92,8 +93,10 @@ public:
private:
void handleSignatureResponse(const SignatureHelpRequest::Response &response);
void processProposal(TextEditor::IAssistProposal *proposal);
QPointer<Client> m_client;
const ProposalHandler m_proposalHandler;
Utils::optional<MessageId> m_currentRequest;
int m_pos = -1;
};
@@ -129,18 +132,26 @@ void FunctionHintProcessor::handleSignatureResponse(const SignatureHelpRequest::
m_client->removeAssistProcessor(this);
auto result = response.result().value_or(LanguageClientValue<SignatureHelp>());
if (result.isNull()) {
setAsyncProposalAvailable(nullptr);
processProposal(nullptr);
return;
}
const SignatureHelp &signatureHelp = result.value();
if (signatureHelp.signatures().isEmpty()) {
setAsyncProposalAvailable(nullptr);
processProposal(nullptr);
} else {
FunctionHintProposalModelPtr model(new FunctionHintProposalModel(signatureHelp));
setAsyncProposalAvailable(new FunctionHintProposal(m_pos, model));
processProposal(new FunctionHintProposal(m_pos, model));
}
}
void FunctionHintProcessor::processProposal(IAssistProposal *proposal)
{
if (m_proposalHandler)
m_proposalHandler(proposal);
else
setAsyncProposalAvailable(proposal);
}
FunctionHintAssistProvider::FunctionHintAssistProvider(Client *client)
: CompletionAssistProvider(client)
, m_client(client)
@@ -148,7 +159,7 @@ FunctionHintAssistProvider::FunctionHintAssistProvider(Client *client)
TextEditor::IAssistProcessor *FunctionHintAssistProvider::createProcessor() const
{
return new FunctionHintProcessor(m_client);
return new FunctionHintProcessor(m_client, m_proposalHandler);
}
IAssistProvider::RunType FunctionHintAssistProvider::runType() const

View File

@@ -28,10 +28,14 @@
#include <texteditor/codeassist/completionassistprovider.h>
#include <utils/optional.h>
namespace TextEditor { class IAssistProposal; }
namespace LanguageClient {
class Client;
using ProposalHandler = std::function<void(TextEditor::IAssistProposal *)>;
class FunctionHintAssistProvider : public TextEditor::CompletionAssistProvider
{
Q_OBJECT
@@ -47,8 +51,12 @@ public:
bool isContinuationChar(const QChar &c) const override;
void setTriggerCharacters(const Utils::optional<QList<QString>> &triggerChars);
void setProposalHandler(const ProposalHandler &handler) { m_proposalHandler = handler; }
private:
QList<QString> m_triggerChars;
ProposalHandler m_proposalHandler;
int m_activationCharSequenceLength = 0;
Client *m_client = nullptr; // not owned
};

View File

@@ -218,10 +218,12 @@ void LanguageClientManager::deleteClient(Client *client)
managerInstance->m_clients.removeAll(client);
for (QVector<Client *> &clients : managerInstance->m_clientsForSetting)
clients.removeAll(client);
if (managerInstance->m_shuttingDown)
if (managerInstance->m_shuttingDown) {
delete client;
else
} else {
client->deleteLater();
emit instance()->clientRemoved(client);
}
}
void LanguageClientManager::shutdown()

View File

@@ -99,6 +99,7 @@ public:
static void showInspector();
signals:
void clientRemoved(Client *client);
void shutdownFinished();
private:

View File

@@ -386,8 +386,11 @@ void SemanticTokenSupport::handleSemanticTokensDelta(
m_tokens[filePath] = *tokens;
} else if (auto tokensDelta = Utils::get_if<SemanticTokensDelta>(&result)) {
QList<SemanticTokensEdit> edits = tokensDelta->edits();
if (edits.isEmpty())
if (edits.isEmpty()) {
if (m_highlightOnEmptyDelta)
highlight(filePath);
return;
}
Utils::sort(edits, &SemanticTokensEdit::start);

View File

@@ -81,6 +81,7 @@ public:
// void setAdditionalTokenModifierStyles(const QHash<int, TextEditor::TextStyle> &modifierStyles);
void setTokensHandler(const SemanticTokensHandler &handler) { m_tokensHandler = handler; }
void forceHighlightingOnEmptyDelta() { m_highlightOnEmptyDelta = true; }
private:
LanguageServerProtocol::SemanticRequestTypes supportedSemanticRequests(
@@ -106,6 +107,7 @@ private:
SemanticTokensHandler m_tokensHandler;
QStringList m_tokenTypeStrings;
QStringList m_tokenModifierStrings;
bool m_highlightOnEmptyDelta = false;
};
} // namespace LanguageClient

View File

@@ -142,9 +142,9 @@ public:
virtual void triggerPendingUpdates();
void setCompletionAssistProvider(CompletionAssistProvider *provider);
virtual void setCompletionAssistProvider(CompletionAssistProvider *provider);
virtual CompletionAssistProvider *completionAssistProvider() const;
void setFunctionHintAssistProvider(CompletionAssistProvider *provider);
virtual void setFunctionHintAssistProvider(CompletionAssistProvider *provider);
virtual CompletionAssistProvider *functionHintAssistProvider() const;
void setQuickFixAssistProvider(IAssistProvider *provider) const;
virtual IAssistProvider *quickFixAssistProvider() const;

View File

@@ -781,6 +781,7 @@ public:
TextMark* m_dragMark = nullptr;
QScopedPointer<ClipboardAssistProvider> m_clipboardAssistProvider;
TextEditorWidget::AssistRequestHandler m_assistRequestHandler;
QScopedPointer<AutoCompleter> m_autoCompleter;
CommentDefinition m_commentDefinition;
@@ -3531,6 +3532,11 @@ void TextEditorWidget::showTextMarksToolTip(const QPoint &pos,
d->showTextMarksToolTip(pos, marks, mainTextMark);
}
void TextEditorWidget::setAssistRequestHandler(const AssistRequestHandler &handler)
{
d->m_assistRequestHandler = handler;
}
void TextEditorWidgetPrivate::processTooltipRequest(const QTextCursor &c)
{
const QPoint toolTipPoint = q->toolTipPosition(c);
@@ -8526,6 +8532,9 @@ QTextBlock TextEditorWidget::blockForVerticalOffset(int offset) const
void TextEditorWidget::invokeAssist(AssistKind kind, IAssistProvider *provider)
{
if (d->m_assistRequestHandler && d->m_assistRequestHandler(this, kind, provider))
return;
if (kind == QuickFix && d->m_snippetOverlay->isVisible())
d->m_snippetOverlay->accept();

View File

@@ -288,6 +288,10 @@ public:
const TextMarks &marks,
const TextMark *mainTextMark = nullptr) const;
using AssistRequestHandler = std::function<bool(TextEditorWidget *editorWidget,
AssistKind assistKind, IAssistProvider *provider)>;
void setAssistRequestHandler(const AssistRequestHandler &handler);
void invokeAssist(AssistKind assistKind, IAssistProvider *provider = nullptr);
virtual TextEditor::AssistInterface *createAssistInterface(AssistKind assistKind,

View File

@@ -147,7 +147,7 @@ TEST(ActivationSequenceContextProcessor, TemplateFunctionLeftParen)
TEST(ActivationSequenceContextProcessor, TemplateFunctionSecondParameter)
{
ClangCompletionAssistInterface interface("foo<X>(", 7);
int startOfname = ContextProcessor::findStartOfName(&interface,
int startOfname = ContextProcessor::findStartOfName(interface.textDocument(),
6,
ContextProcessor::NameCategory::Function);