forked from qt-creator/qt-creator
ClangCodeModel: Move completion code to its own set of files
Change-Id: Iad856881ace80896f4aceab3fbf129d7635de642 Reviewed-by: <github-actions-qt-creator@cristianadam.eu> Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
@@ -17,6 +17,7 @@ add_qtc_plugin(ClangCodeModel
|
||||
clangconstants.h
|
||||
clangdast.cpp clangdast.h
|
||||
clangdclient.cpp clangdclient.h
|
||||
clangdcompletion.cpp clangdcompletion.h
|
||||
clangdfollowsymbol.cpp clangdfollowsymbol.h
|
||||
clangdiagnostictooltipwidget.cpp clangdiagnostictooltipwidget.h
|
||||
clangdquickfixes.cpp clangdquickfixes.h
|
||||
|
@@ -34,6 +34,8 @@ QtcPlugin {
|
||||
"clangdast.h",
|
||||
"clangdclient.cpp",
|
||||
"clangdclient.h",
|
||||
"clangdcompletion.cpp",
|
||||
"clangdcompletion.h",
|
||||
"clangdfollowsymbol.cpp",
|
||||
"clangdfollowsymbol.h",
|
||||
"clangdiagnostictooltipwidget.cpp",
|
||||
|
@@ -25,14 +25,13 @@
|
||||
|
||||
#include "clangdclient.h"
|
||||
|
||||
#include "clangcompletioncontextanalyzer.h"
|
||||
#include "clangconstants.h"
|
||||
#include "clangdast.h"
|
||||
#include "clangdcompletion.h"
|
||||
#include "clangdfollowsymbol.h"
|
||||
#include "clangdlocatorfilters.h"
|
||||
#include "clangdquickfixes.h"
|
||||
#include "clangdswitchdecldef.h"
|
||||
#include "clangpreprocessorassistproposalitem.h"
|
||||
#include "clangtextmark.h"
|
||||
#include "clangutils.h"
|
||||
#include "clangdsemantichighlighting.h"
|
||||
@@ -45,12 +44,7 @@
|
||||
#include <cplusplus/ASTPath.h>
|
||||
#include <cplusplus/FindUsages.h>
|
||||
#include <cplusplus/Icons.h>
|
||||
#include <cplusplus/MatchingText.h>
|
||||
#include <cppeditor/cppeditorconstants.h>
|
||||
#include <cppeditor/cppcodemodelsettings.h>
|
||||
#include <cppeditor/cppcompletionassistprocessor.h>
|
||||
#include <cppeditor/cppcompletionassistprovider.h>
|
||||
#include <cppeditor/cppdoxygen.h>
|
||||
#include <cppeditor/cppeditorwidget.h>
|
||||
#include <cppeditor/cppfindreferences.h>
|
||||
#include <cppeditor/cppmodelmanager.h>
|
||||
@@ -61,8 +55,6 @@
|
||||
#include <cppeditor/semantichighlighter.h>
|
||||
#include <cppeditor/cppsemanticinfo.h>
|
||||
#include <languageclient/diagnosticmanager.h>
|
||||
#include <languageclient/languageclientcompletionassist.h>
|
||||
#include <languageclient/languageclientfunctionhint.h>
|
||||
#include <languageclient/languageclienthoverhandler.h>
|
||||
#include <languageclient/languageclientinterface.h>
|
||||
#include <languageclient/languageclientmanager.h>
|
||||
@@ -79,7 +71,6 @@
|
||||
#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/fileutils.h>
|
||||
@@ -119,8 +110,6 @@ namespace Internal {
|
||||
Q_LOGGING_CATEGORY(clangdLog, "qtc.clangcodemodel.clangd", QtWarningMsg);
|
||||
Q_LOGGING_CATEGORY(clangdLogAst, "qtc.clangcodemodel.clangd.ast", QtWarningMsg);
|
||||
static Q_LOGGING_CATEGORY(clangdLogServer, "qtc.clangcodemodel.clangd.server", QtWarningMsg);
|
||||
static Q_LOGGING_CATEGORY(clangdLogCompletion, "qtc.clangcodemodel.clangd.completion",
|
||||
QtWarningMsg);
|
||||
static QString indexingToken() { return "backgroundIndexProgress"; }
|
||||
|
||||
static Usage::Type getUsageType(const ClangdAstPath &path)
|
||||
@@ -350,178 +339,6 @@ public:
|
||||
{ insert("publishDiagnostics", caps); }
|
||||
};
|
||||
|
||||
|
||||
enum class CustomAssistMode { Doxygen, Preprocessor, IncludePath };
|
||||
class CustomAssistProcessor : public IAssistProcessor
|
||||
{
|
||||
public:
|
||||
CustomAssistProcessor(ClangdClient *client, int position, int endPos,
|
||||
unsigned completionOperator, CustomAssistMode mode)
|
||||
: m_client(client)
|
||||
, m_position(position)
|
||||
, m_endPos(endPos)
|
||||
, m_completionOperator(completionOperator)
|
||||
, m_mode(mode)
|
||||
{}
|
||||
|
||||
private:
|
||||
IAssistProposal *perform(const AssistInterface *interface) override
|
||||
{
|
||||
QList<AssistProposalItemInterface *> completions;
|
||||
switch (m_mode) {
|
||||
case CustomAssistMode::Doxygen:
|
||||
for (int i = 1; i < CppEditor::T_DOXY_LAST_TAG; ++i) {
|
||||
completions << createItem(QLatin1String(CppEditor::doxygenTagSpell(i)),
|
||||
CPlusPlus::Icons::keywordIcon());
|
||||
}
|
||||
break;
|
||||
case CustomAssistMode::Preprocessor: {
|
||||
static QIcon macroIcon = Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::Macro);
|
||||
for (const QString &completion
|
||||
: CppEditor::CppCompletionAssistProcessor::preprocessorCompletions()) {
|
||||
completions << createItem(completion, macroIcon);
|
||||
}
|
||||
if (CppEditor::ProjectFile::isObjC(interface->filePath().toString()))
|
||||
completions << createItem("import", macroIcon);
|
||||
break;
|
||||
}
|
||||
case ClangCodeModel::Internal::CustomAssistMode::IncludePath: {
|
||||
HeaderPaths headerPaths;
|
||||
const CppEditor::ProjectPart::ConstPtr projectPart
|
||||
= projectPartForFile(interface->filePath().toString());
|
||||
if (projectPart)
|
||||
headerPaths = projectPart->headerPaths;
|
||||
completions = completeInclude(m_endPos, m_completionOperator, interface, headerPaths);
|
||||
break;
|
||||
}
|
||||
}
|
||||
GenericProposalModelPtr model(new GenericProposalModel);
|
||||
model->loadContent(completions);
|
||||
const auto proposal = new GenericProposal(m_position, model);
|
||||
if (m_client->testingEnabled()) {
|
||||
emit m_client->proposalReady(proposal);
|
||||
return nullptr;
|
||||
}
|
||||
return proposal;
|
||||
}
|
||||
|
||||
AssistProposalItemInterface *createItem(const QString &text, const QIcon &icon) const
|
||||
{
|
||||
const auto item = new ClangPreprocessorAssistProposalItem;
|
||||
item->setText(text);
|
||||
item->setIcon(icon);
|
||||
item->setCompletionOperator(m_completionOperator);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates completion proposals for #include and given cursor
|
||||
* @param position - cursor placed after opening bracked or quote
|
||||
* @param completionOperator - the type of token
|
||||
* @param interface - relevant document data
|
||||
* @param headerPaths - the include paths
|
||||
* @return the list of completion items
|
||||
*/
|
||||
static QList<AssistProposalItemInterface *> completeInclude(
|
||||
int position, unsigned completionOperator, const TextEditor::AssistInterface *interface,
|
||||
const ProjectExplorer::HeaderPaths &headerPaths)
|
||||
{
|
||||
QTextCursor cursor(interface->textDocument());
|
||||
cursor.setPosition(position);
|
||||
QString directoryPrefix;
|
||||
if (completionOperator == T_SLASH) {
|
||||
QTextCursor c = cursor;
|
||||
c.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor);
|
||||
QString sel = c.selectedText();
|
||||
int startCharPos = sel.indexOf(QLatin1Char('"'));
|
||||
if (startCharPos == -1) {
|
||||
startCharPos = sel.indexOf(QLatin1Char('<'));
|
||||
completionOperator = T_ANGLE_STRING_LITERAL;
|
||||
} else {
|
||||
completionOperator = T_STRING_LITERAL;
|
||||
}
|
||||
if (startCharPos != -1)
|
||||
directoryPrefix = sel.mid(startCharPos + 1, sel.length() - 1);
|
||||
}
|
||||
|
||||
// Make completion for all relevant includes
|
||||
ProjectExplorer::HeaderPaths allHeaderPaths = headerPaths;
|
||||
const auto currentFilePath = ProjectExplorer::HeaderPath::makeUser(
|
||||
interface->filePath().toFileInfo().path());
|
||||
if (!allHeaderPaths.contains(currentFilePath))
|
||||
allHeaderPaths.append(currentFilePath);
|
||||
|
||||
const ::Utils::MimeType mimeType = ::Utils::mimeTypeForName("text/x-c++hdr");
|
||||
const QStringList suffixes = mimeType.suffixes();
|
||||
|
||||
QList<AssistProposalItemInterface *> completions;
|
||||
for (const ProjectExplorer::HeaderPath &headerPath : qAsConst(allHeaderPaths)) {
|
||||
QString realPath = headerPath.path;
|
||||
if (!directoryPrefix.isEmpty()) {
|
||||
realPath += QLatin1Char('/');
|
||||
realPath += directoryPrefix;
|
||||
if (headerPath.type == ProjectExplorer::HeaderPathType::Framework)
|
||||
realPath += QLatin1String(".framework/Headers");
|
||||
}
|
||||
completions << completeIncludePath(realPath, suffixes, completionOperator);
|
||||
}
|
||||
|
||||
QList<QPair<AssistProposalItemInterface *, QString>> completionsForSorting;
|
||||
for (AssistProposalItemInterface * const item : qAsConst(completions)) {
|
||||
QString s = item->text();
|
||||
s.replace('/', QChar(0)); // The dir separator should compare less than anything else.
|
||||
completionsForSorting << qMakePair(item, s);
|
||||
}
|
||||
Utils::sort(completionsForSorting, [](const auto &left, const auto &right) {
|
||||
return left.second < right.second;
|
||||
});
|
||||
for (int i = 0; i < completionsForSorting.count(); ++i)
|
||||
completions[i] = completionsForSorting[i].first;
|
||||
|
||||
return completions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Finds #include completion proposals using given include path
|
||||
* @param realPath - one of directories where compiler searches includes
|
||||
* @param suffixes - file suffixes for C/C++ header files
|
||||
* @return a list of matching completion items
|
||||
*/
|
||||
static QList<AssistProposalItemInterface *> completeIncludePath(
|
||||
const QString &realPath, const QStringList &suffixes, unsigned completionOperator)
|
||||
{
|
||||
QList<AssistProposalItemInterface *> completions;
|
||||
QDirIterator i(realPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
//: Parent folder for proposed #include completion
|
||||
const QString hint = ClangdClient::tr("Location: %1")
|
||||
.arg(QDir::toNativeSeparators(QDir::cleanPath(realPath)));
|
||||
while (i.hasNext()) {
|
||||
const QString fileName = i.next();
|
||||
const QFileInfo fileInfo = i.fileInfo();
|
||||
const QString suffix = fileInfo.suffix();
|
||||
if (suffix.isEmpty() || suffixes.contains(suffix)) {
|
||||
QString text = fileName.mid(realPath.length() + 1);
|
||||
if (fileInfo.isDir())
|
||||
text += QLatin1Char('/');
|
||||
|
||||
auto *item = new ClangPreprocessorAssistProposalItem;
|
||||
item->setText(text);
|
||||
item->setDetail(hint);
|
||||
item->setIcon(CPlusPlus::Icons::keywordIcon());
|
||||
item->setCompletionOperator(completionOperator);
|
||||
completions.append(item);
|
||||
}
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
|
||||
ClangdClient * const m_client;
|
||||
const int m_position;
|
||||
const int m_endPos;
|
||||
const unsigned m_completionOperator;
|
||||
const CustomAssistMode m_mode;
|
||||
};
|
||||
|
||||
static qint64 getRevision(const TextDocument *doc)
|
||||
{
|
||||
return doc->document()->revision();
|
||||
@@ -663,118 +480,6 @@ 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.
|
||||
if (Utils::optional<CompletionItemCapbilities> completionItemCaps = completionItem()) {
|
||||
completionItemCaps->setSnippetSupport(false);
|
||||
setCompletionItem(*completionItemCaps);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class ClangdCompletionItem : public LanguageClientCompletionItem
|
||||
{
|
||||
public:
|
||||
using LanguageClientCompletionItem::LanguageClientCompletionItem;
|
||||
void apply(TextDocumentManipulatorInterface &manipulator,
|
||||
int basePosition) const override;
|
||||
|
||||
enum class SpecialQtType { Signal, Slot, None };
|
||||
static SpecialQtType getQtType(const CompletionItem &item);
|
||||
|
||||
private:
|
||||
QIcon icon() const override;
|
||||
QString text() const override;
|
||||
};
|
||||
|
||||
class ClangdClient::ClangdCompletionAssistProcessor : public LanguageClientCompletionAssistProcessor
|
||||
{
|
||||
public:
|
||||
ClangdCompletionAssistProcessor(ClangdClient *client, const QString &snippetsGroup)
|
||||
: LanguageClientCompletionAssistProcessor(client, snippetsGroup)
|
||||
, m_client(client)
|
||||
{
|
||||
m_timer.start();
|
||||
}
|
||||
|
||||
~ClangdCompletionAssistProcessor()
|
||||
{
|
||||
qCDebug(clangdLogTiming).noquote().nospace()
|
||||
<< "ClangdCompletionAssistProcessor took: " << m_timer.elapsed() << " ms";
|
||||
}
|
||||
|
||||
private:
|
||||
IAssistProposal *perform(const AssistInterface *interface) override
|
||||
{
|
||||
if (m_client->d->isTesting) {
|
||||
setAsyncCompletionAvailableHandler([this](IAssistProposal *proposal) {
|
||||
emit m_client->proposalReady(proposal);
|
||||
});
|
||||
}
|
||||
return LanguageClientCompletionAssistProcessor::perform(interface);
|
||||
}
|
||||
|
||||
QList<AssistProposalItemInterface *> generateCompletionItems(
|
||||
const QList<LanguageServerProtocol::CompletionItem> &items) const override;
|
||||
|
||||
ClangdClient * const m_client;
|
||||
QElapsedTimer m_timer;
|
||||
};
|
||||
|
||||
QList<AssistProposalItemInterface *>
|
||||
ClangdClient::ClangdCompletionAssistProcessor::generateCompletionItems(
|
||||
const QList<LanguageServerProtocol::CompletionItem> &items) const
|
||||
{
|
||||
qCDebug(clangdLog) << "received" << items.count() << "completions";
|
||||
|
||||
auto itemGenerator = [](const QList<LanguageServerProtocol::CompletionItem> &items) {
|
||||
return Utils::transform<QList<AssistProposalItemInterface *>>(
|
||||
items, [](const LanguageServerProtocol::CompletionItem &item) {
|
||||
return new ClangdCompletionItem(item);
|
||||
});
|
||||
};
|
||||
|
||||
// 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) {
|
||||
return ClangdCompletionItem::getQtType(ci) == ClangdCompletionItem::SpecialQtType::Signal;
|
||||
};
|
||||
const QTextDocument *doc = document();
|
||||
const int pos = basePos();
|
||||
if (!doc || pos < 0 || !Utils::anyOf(items, criterion))
|
||||
return itemGenerator(items);
|
||||
const QString content = doc->toPlainText();
|
||||
const bool requiresSignal = CppEditor::CppModelManager::instance()
|
||||
->getSignalSlotType(filePath().toString(), content.toUtf8(), pos)
|
||||
== CppEditor::SignalSlotType::NewStyleSignal;
|
||||
if (requiresSignal)
|
||||
return itemGenerator(Utils::filtered(items, criterion));
|
||||
return itemGenerator(items);
|
||||
}
|
||||
|
||||
class ClangdClient::ClangdCompletionAssistProvider : public LanguageClientCompletionAssistProvider
|
||||
{
|
||||
public:
|
||||
ClangdCompletionAssistProvider(ClangdClient *client);
|
||||
|
||||
private:
|
||||
IAssistProcessor *createProcessor(const AssistInterface *interface) const override;
|
||||
|
||||
int activationCharSequenceLength() const override { return 3; }
|
||||
bool isActivationCharSequence(const QString &sequence) const override;
|
||||
bool isContinuationChar(const QChar &c) const override;
|
||||
|
||||
bool isInCommentOrString(const AssistInterface *interface) const;
|
||||
|
||||
ClangdClient * const m_client;
|
||||
};
|
||||
|
||||
static void addToCompilationDb(QJsonObject &cdb,
|
||||
const CppEditor::ProjectPart &projectPart,
|
||||
CppEditor::UsePrecompiledHeaders usePch,
|
||||
@@ -2118,303 +1823,6 @@ QString ClangdDiagnostic::category() const
|
||||
return typedValue<QString>("category");
|
||||
}
|
||||
|
||||
class ClangdClient::ClangdFunctionHintProcessor : public FunctionHintProcessor
|
||||
{
|
||||
public:
|
||||
ClangdFunctionHintProcessor(ClangdClient *client)
|
||||
: FunctionHintProcessor(client)
|
||||
, m_client(client)
|
||||
{}
|
||||
|
||||
private:
|
||||
IAssistProposal *perform(const AssistInterface *interface) override
|
||||
{
|
||||
if (m_client->d->isTesting) {
|
||||
setAsyncCompletionAvailableHandler([this](IAssistProposal *proposal) {
|
||||
emit m_client->proposalReady(proposal);
|
||||
});
|
||||
}
|
||||
return FunctionHintProcessor::perform(interface);
|
||||
}
|
||||
|
||||
ClangdClient * const m_client;
|
||||
};
|
||||
|
||||
ClangdClient::ClangdCompletionAssistProvider::ClangdCompletionAssistProvider(ClangdClient *client)
|
||||
: LanguageClientCompletionAssistProvider(client)
|
||||
, m_client(client)
|
||||
{}
|
||||
|
||||
IAssistProcessor *ClangdClient::ClangdCompletionAssistProvider::createProcessor(
|
||||
const AssistInterface *interface) const
|
||||
{
|
||||
qCDebug(clangdLogCompletion) << "completion processor requested for" << interface->filePath();
|
||||
qCDebug(clangdLogCompletion) << "text before cursor is"
|
||||
<< interface->textAt(interface->position(), -10);
|
||||
qCDebug(clangdLogCompletion) << "text after cursor is"
|
||||
<< interface->textAt(interface->position(), 10);
|
||||
ClangCompletionContextAnalyzer contextAnalyzer(interface->textDocument(),
|
||||
interface->position(), false, {});
|
||||
contextAnalyzer.analyze();
|
||||
switch (contextAnalyzer.completionAction()) {
|
||||
case ClangCompletionContextAnalyzer::PassThroughToLibClangAfterLeftParen:
|
||||
qCDebug(clangdLogCompletion) << "creating function hint processor";
|
||||
return new ClangdFunctionHintProcessor(m_client);
|
||||
case ClangCompletionContextAnalyzer::CompleteDoxygenKeyword:
|
||||
qCDebug(clangdLogCompletion) << "creating doxygen processor";
|
||||
return new CustomAssistProcessor(m_client,
|
||||
contextAnalyzer.positionForProposal(),
|
||||
contextAnalyzer.positionEndOfExpression(),
|
||||
contextAnalyzer.completionOperator(),
|
||||
CustomAssistMode::Doxygen);
|
||||
case ClangCompletionContextAnalyzer::CompletePreprocessorDirective:
|
||||
qCDebug(clangdLogCompletion) << "creating macro processor";
|
||||
return new CustomAssistProcessor(m_client,
|
||||
contextAnalyzer.positionForProposal(),
|
||||
contextAnalyzer.positionEndOfExpression(),
|
||||
contextAnalyzer.completionOperator(),
|
||||
CustomAssistMode::Preprocessor);
|
||||
case ClangCompletionContextAnalyzer::CompleteSignal:
|
||||
case ClangCompletionContextAnalyzer::CompleteSlot:
|
||||
if (!interface->isBaseObject())
|
||||
return CppEditor::getCppCompletionAssistProcessor();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const QString snippetsGroup = contextAnalyzer.addSnippets() && !isInCommentOrString(interface)
|
||||
? CppEditor::Constants::CPP_SNIPPETS_GROUP_ID
|
||||
: QString();
|
||||
qCDebug(clangdLogCompletion) << "creating proper completion processor"
|
||||
<< (snippetsGroup.isEmpty() ? "without" : "with") << "snippets";
|
||||
return new ClangdCompletionAssistProcessor(m_client, snippetsGroup);
|
||||
}
|
||||
|
||||
bool ClangdClient::ClangdCompletionAssistProvider::isActivationCharSequence(const QString &sequence) const
|
||||
{
|
||||
const QChar &ch = sequence.at(2);
|
||||
const QChar &ch2 = sequence.at(1);
|
||||
const QChar &ch3 = sequence.at(0);
|
||||
unsigned kind = T_EOF_SYMBOL;
|
||||
const int pos = CppEditor::CppCompletionAssistProvider::activationSequenceChar(
|
||||
ch, ch2, ch3, &kind, false, false);
|
||||
if (pos == 0)
|
||||
return false;
|
||||
|
||||
// We want to minimize unneeded completion requests, as those trigger document updates,
|
||||
// which trigger re-highlighting and diagnostics, which we try to delay.
|
||||
// Therefore, we do not trigger on syntax elements that often occur in non-applicable
|
||||
// contexts, such as '(', '<' or '/'.
|
||||
switch (kind) {
|
||||
case T_DOT: case T_COLON_COLON: case T_ARROW: case T_DOT_STAR: case T_ARROW_STAR: case T_POUND:
|
||||
qCDebug(clangdLogCompletion) << "detected" << sequence << "as activation char sequence";
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ClangdClient::ClangdCompletionAssistProvider::isContinuationChar(const QChar &c) const
|
||||
{
|
||||
return CppEditor::isValidIdentifierChar(c);
|
||||
}
|
||||
|
||||
bool ClangdClient::ClangdCompletionAssistProvider::isInCommentOrString(
|
||||
const AssistInterface *interface) const
|
||||
{
|
||||
LanguageFeatures features = LanguageFeatures::defaultFeatures();
|
||||
features.objCEnabled = CppEditor::ProjectFile::isObjC(interface->filePath().toString());
|
||||
return CppEditor::isInCommentOrString(interface, features);
|
||||
}
|
||||
|
||||
void ClangdCompletionItem::apply(TextDocumentManipulatorInterface &manipulator,
|
||||
int /*basePosition*/) const
|
||||
{
|
||||
const LanguageServerProtocol::CompletionItem item = this->item();
|
||||
QChar typedChar = triggeredCommitCharacter();
|
||||
const auto edit = item.textEdit();
|
||||
if (!edit)
|
||||
return;
|
||||
|
||||
const int labelOpenParenOffset = item.label().indexOf('(');
|
||||
const int labelClosingParenOffset = item.label().indexOf(')');
|
||||
const auto kind = static_cast<CompletionItemKind::Kind>(
|
||||
item.kind().value_or(CompletionItemKind::Text));
|
||||
const bool isMacroCall = kind == CompletionItemKind::Text && labelOpenParenOffset != -1
|
||||
&& labelClosingParenOffset > labelOpenParenOffset; // Heuristic
|
||||
const bool isFunctionLike = kind == CompletionItemKind::Function
|
||||
|| kind == CompletionItemKind::Method || kind == CompletionItemKind::Constructor
|
||||
|| isMacroCall;
|
||||
|
||||
QString rawInsertText = edit->newText();
|
||||
|
||||
// Some preparation for our magic involving (non-)insertion of parentheses and
|
||||
// cursor placement.
|
||||
if (isFunctionLike && !rawInsertText.contains('(')) {
|
||||
if (labelOpenParenOffset != -1) {
|
||||
if (labelClosingParenOffset == labelOpenParenOffset + 1) // function takes no arguments
|
||||
rawInsertText += "()";
|
||||
else // function takes arguments
|
||||
rawInsertText += "( )";
|
||||
}
|
||||
}
|
||||
|
||||
const int firstParenOffset = rawInsertText.indexOf('(');
|
||||
const int lastParenOffset = rawInsertText.lastIndexOf(')');
|
||||
const QString detail = item.detail().value_or(QString());
|
||||
const CompletionSettings &completionSettings = TextEditorSettings::completionSettings();
|
||||
QString textToBeInserted = rawInsertText.left(firstParenOffset);
|
||||
QString extraCharacters;
|
||||
int extraLength = 0;
|
||||
int cursorOffset = 0;
|
||||
bool setAutoCompleteSkipPos = false;
|
||||
int currentPos = manipulator.currentPosition();
|
||||
const QTextDocument * const doc = manipulator.textCursorAt(currentPos).document();
|
||||
const Range range = edit->range();
|
||||
const int rangeStart = range.start().toPositionInDocument(doc);
|
||||
if (isFunctionLike && 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 && !isMacroCall && matchPreviousWord(manipulator, cursor, detail))
|
||||
abandonParen = true; // function definition
|
||||
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(currentPos);
|
||||
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(currentPos + 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;
|
||||
}
|
||||
|
||||
// Avoid inserting characters that are already there
|
||||
QTextCursor cursor = manipulator.textCursorAt(rangeStart);
|
||||
cursor.movePosition(QTextCursor::EndOfWord);
|
||||
const QString textAfterCursor = manipulator.textAt(currentPos, cursor.position() - currentPos);
|
||||
if (currentPos < cursor.position()
|
||||
&& textToBeInserted != textAfterCursor
|
||||
&& textToBeInserted.indexOf(textAfterCursor, currentPos - rangeStart) >= 0) {
|
||||
currentPos = cursor.position();
|
||||
}
|
||||
for (int i = 0; i < extraCharacters.length(); ++i) {
|
||||
const QChar a = extraCharacters.at(i);
|
||||
const QChar b = manipulator.characterAt(currentPos + i);
|
||||
if (a == b)
|
||||
++extraLength;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
textToBeInserted += extraCharacters;
|
||||
const int length = currentPos - rangeStart + extraLength;
|
||||
const bool isReplaced = manipulator.replace(rangeStart, length, 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);
|
||||
}
|
||||
}
|
||||
|
||||
ClangdCompletionItem::SpecialQtType ClangdCompletionItem::getQtType(const CompletionItem &item)
|
||||
{
|
||||
const Utils::optional<MarkupOrString> doc = item.documentation();
|
||||
if (!doc)
|
||||
return SpecialQtType::None;
|
||||
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();
|
||||
if (docText.contains("Annotation: qt_signal"))
|
||||
return SpecialQtType::Signal;
|
||||
if (docText.contains("Annotation: qt_slot"))
|
||||
return SpecialQtType::Slot;
|
||||
return SpecialQtType::None;
|
||||
}
|
||||
|
||||
QIcon ClangdCompletionItem::icon() const
|
||||
{
|
||||
if (isDeprecated())
|
||||
return Utils::Icons::WARNING.icon();
|
||||
const SpecialQtType qtType = getQtType(item());
|
||||
switch (qtType) {
|
||||
case SpecialQtType::Signal:
|
||||
return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::Signal);
|
||||
case SpecialQtType::Slot:
|
||||
// FIXME: Add visibility info to completion item tags in clangd?
|
||||
return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::SlotPublic);
|
||||
case SpecialQtType::None:
|
||||
break;
|
||||
}
|
||||
if (item().kind().value_or(CompletionItemKind::Text) == CompletionItemKind::Property)
|
||||
return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::VarPublicStatic);
|
||||
return LanguageClientCompletionItem::icon();
|
||||
}
|
||||
|
||||
QString ClangdCompletionItem::text() const
|
||||
{
|
||||
const QString clangdValue = LanguageClientCompletionItem::text();
|
||||
if (isDeprecated())
|
||||
return "[[deprecated]]" + clangdValue;
|
||||
return clangdValue;
|
||||
}
|
||||
|
||||
MessageId ClangdClient::Private::getAndHandleAst(const TextDocOrFile &doc,
|
||||
const AstHandler &astHandler,
|
||||
AstCallbackMode callbackMode, const Range &range)
|
||||
|
@@ -149,9 +149,6 @@ private:
|
||||
class Private;
|
||||
class VirtualFunctionAssistProcessor;
|
||||
class VirtualFunctionAssistProvider;
|
||||
class ClangdFunctionHintProcessor;
|
||||
class ClangdCompletionAssistProcessor;
|
||||
class ClangdCompletionAssistProvider;
|
||||
Private * const d;
|
||||
};
|
||||
|
||||
|
643
src/plugins/clangcodemodel/clangdcompletion.cpp
Normal file
643
src/plugins/clangcodemodel/clangdcompletion.cpp
Normal file
@@ -0,0 +1,643 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2022 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 "clangdcompletion.h"
|
||||
|
||||
#include "clangcompletioncontextanalyzer.h"
|
||||
#include "clangdclient.h"
|
||||
#include "clangpreprocessorassistproposalitem.h"
|
||||
#include "clangutils.h"
|
||||
#include "tasktimers.h"
|
||||
|
||||
#include <cppeditor/cppcompletionassistprocessor.h>
|
||||
#include <cppeditor/cppcompletionassistprovider.h>
|
||||
#include <cppeditor/cppdoxygen.h>
|
||||
#include <cppeditor/cppeditorconstants.h>
|
||||
#include <cppeditor/cppmodelmanager.h>
|
||||
#include <cppeditor/cppprojectfile.h>
|
||||
#include <cppeditor/projectpart.h>
|
||||
#include <cplusplus/Icons.h>
|
||||
#include <cplusplus/MatchingText.h>
|
||||
#include <languageclient/languageclientfunctionhint.h>
|
||||
#include <projectexplorer/headerpath.h>
|
||||
#include <texteditor/codeassist/assistinterface.h>
|
||||
#include <texteditor/codeassist/genericproposal.h>
|
||||
#include <texteditor/codeassist/genericproposalmodel.h>
|
||||
#include <texteditor/texteditorsettings.h>
|
||||
#include <utils/utilsicons.h>
|
||||
|
||||
using namespace CppEditor;
|
||||
using namespace CPlusPlus;
|
||||
using namespace LanguageClient;
|
||||
using namespace LanguageServerProtocol;
|
||||
using namespace ProjectExplorer;
|
||||
using namespace TextEditor;
|
||||
using namespace Utils;
|
||||
|
||||
namespace ClangCodeModel::Internal {
|
||||
static Q_LOGGING_CATEGORY(clangdLogCompletion, "qtc.clangcodemodel.clangd.completion",
|
||||
QtWarningMsg);
|
||||
|
||||
enum class CustomAssistMode { Doxygen, Preprocessor, IncludePath };
|
||||
class CustomAssistProcessor : public IAssistProcessor
|
||||
{
|
||||
public:
|
||||
CustomAssistProcessor(ClangdClient *client, int position, int endPos,
|
||||
unsigned completionOperator, CustomAssistMode mode);
|
||||
|
||||
private:
|
||||
IAssistProposal *perform(const AssistInterface *interface) override;
|
||||
|
||||
AssistProposalItemInterface *createItem(const QString &text, const QIcon &icon) const;
|
||||
|
||||
static QList<AssistProposalItemInterface *> completeInclude(
|
||||
int position, unsigned completionOperator, const AssistInterface *interface,
|
||||
const HeaderPaths &headerPaths);
|
||||
static QList<AssistProposalItemInterface *> completeIncludePath(
|
||||
const QString &realPath, const QStringList &suffixes, unsigned completionOperator);
|
||||
|
||||
ClangdClient * const m_client;
|
||||
const int m_position;
|
||||
const int m_endPos;
|
||||
const unsigned m_completionOperator;
|
||||
const CustomAssistMode m_mode;
|
||||
};
|
||||
|
||||
class ClangdCompletionItem : public LanguageClientCompletionItem
|
||||
{
|
||||
public:
|
||||
using LanguageClientCompletionItem::LanguageClientCompletionItem;
|
||||
void apply(TextDocumentManipulatorInterface &manipulator,
|
||||
int basePosition) const override;
|
||||
|
||||
enum class SpecialQtType { Signal, Slot, None };
|
||||
static SpecialQtType getQtType(const CompletionItem &item);
|
||||
|
||||
private:
|
||||
QIcon icon() const override;
|
||||
QString text() const override;
|
||||
};
|
||||
|
||||
class ClangdCompletionAssistProcessor : public LanguageClientCompletionAssistProcessor
|
||||
{
|
||||
public:
|
||||
ClangdCompletionAssistProcessor(ClangdClient *client, const QString &snippetsGroup);
|
||||
~ClangdCompletionAssistProcessor();
|
||||
|
||||
private:
|
||||
IAssistProposal *perform(const AssistInterface *interface) override;
|
||||
QList<AssistProposalItemInterface *> generateCompletionItems(
|
||||
const QList<LanguageServerProtocol::CompletionItem> &items) const override;
|
||||
|
||||
ClangdClient * const m_client;
|
||||
QElapsedTimer m_timer;
|
||||
};
|
||||
|
||||
class ClangdFunctionHintProcessor : public FunctionHintProcessor
|
||||
{
|
||||
public:
|
||||
ClangdFunctionHintProcessor(ClangdClient *client);
|
||||
|
||||
private:
|
||||
IAssistProposal *perform(const AssistInterface *interface) override;
|
||||
|
||||
ClangdClient * const m_client;
|
||||
};
|
||||
|
||||
ClangdCompletionAssistProvider::ClangdCompletionAssistProvider(ClangdClient *client)
|
||||
: LanguageClientCompletionAssistProvider(client)
|
||||
, m_client(client)
|
||||
{}
|
||||
|
||||
IAssistProcessor *ClangdCompletionAssistProvider::createProcessor(
|
||||
const AssistInterface *interface) const
|
||||
{
|
||||
qCDebug(clangdLogCompletion) << "completion processor requested for" << interface->filePath();
|
||||
qCDebug(clangdLogCompletion) << "text before cursor is"
|
||||
<< interface->textAt(interface->position(), -10);
|
||||
qCDebug(clangdLogCompletion) << "text after cursor is"
|
||||
<< interface->textAt(interface->position(), 10);
|
||||
ClangCompletionContextAnalyzer contextAnalyzer(interface->textDocument(),
|
||||
interface->position(), false, {});
|
||||
contextAnalyzer.analyze();
|
||||
switch (contextAnalyzer.completionAction()) {
|
||||
case ClangCompletionContextAnalyzer::PassThroughToLibClangAfterLeftParen:
|
||||
qCDebug(clangdLogCompletion) << "creating function hint processor";
|
||||
return new ClangdFunctionHintProcessor(m_client);
|
||||
case ClangCompletionContextAnalyzer::CompleteDoxygenKeyword:
|
||||
qCDebug(clangdLogCompletion) << "creating doxygen processor";
|
||||
return new CustomAssistProcessor(m_client,
|
||||
contextAnalyzer.positionForProposal(),
|
||||
contextAnalyzer.positionEndOfExpression(),
|
||||
contextAnalyzer.completionOperator(),
|
||||
CustomAssistMode::Doxygen);
|
||||
case ClangCompletionContextAnalyzer::CompletePreprocessorDirective:
|
||||
qCDebug(clangdLogCompletion) << "creating macro processor";
|
||||
return new CustomAssistProcessor(m_client,
|
||||
contextAnalyzer.positionForProposal(),
|
||||
contextAnalyzer.positionEndOfExpression(),
|
||||
contextAnalyzer.completionOperator(),
|
||||
CustomAssistMode::Preprocessor);
|
||||
case ClangCompletionContextAnalyzer::CompleteSignal:
|
||||
case ClangCompletionContextAnalyzer::CompleteSlot:
|
||||
if (!interface->isBaseObject())
|
||||
return CppEditor::getCppCompletionAssistProcessor();
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const QString snippetsGroup = contextAnalyzer.addSnippets() && !isInCommentOrString(interface)
|
||||
? CppEditor::Constants::CPP_SNIPPETS_GROUP_ID
|
||||
: QString();
|
||||
qCDebug(clangdLogCompletion) << "creating proper completion processor"
|
||||
<< (snippetsGroup.isEmpty() ? "without" : "with") << "snippets";
|
||||
return new ClangdCompletionAssistProcessor(m_client, snippetsGroup);
|
||||
}
|
||||
|
||||
bool ClangdCompletionAssistProvider::isActivationCharSequence(const QString &sequence) const
|
||||
{
|
||||
const QChar &ch = sequence.at(2);
|
||||
const QChar &ch2 = sequence.at(1);
|
||||
const QChar &ch3 = sequence.at(0);
|
||||
unsigned kind = T_EOF_SYMBOL;
|
||||
const int pos = CppCompletionAssistProvider::activationSequenceChar(
|
||||
ch, ch2, ch3, &kind, false, false);
|
||||
if (pos == 0)
|
||||
return false;
|
||||
|
||||
// We want to minimize unneeded completion requests, as those trigger document updates,
|
||||
// which trigger re-highlighting and diagnostics, which we try to delay.
|
||||
// Therefore, we do not trigger on syntax elements that often occur in non-applicable
|
||||
// contexts, such as '(', '<' or '/'.
|
||||
switch (kind) {
|
||||
case T_DOT: case T_COLON_COLON: case T_ARROW: case T_DOT_STAR: case T_ARROW_STAR: case T_POUND:
|
||||
qCDebug(clangdLogCompletion) << "detected" << sequence << "as activation char sequence";
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ClangdCompletionAssistProvider::isContinuationChar(const QChar &c) const
|
||||
{
|
||||
return isValidIdentifierChar(c);
|
||||
}
|
||||
|
||||
bool ClangdCompletionAssistProvider::isInCommentOrString(const AssistInterface *interface) const
|
||||
{
|
||||
LanguageFeatures features = LanguageFeatures::defaultFeatures();
|
||||
features.objCEnabled = ProjectFile::isObjC(interface->filePath().toString());
|
||||
return CppEditor::isInCommentOrString(interface, features);
|
||||
}
|
||||
|
||||
void ClangdCompletionItem::apply(TextDocumentManipulatorInterface &manipulator,
|
||||
int /*basePosition*/) const
|
||||
{
|
||||
const CompletionItem item = this->item();
|
||||
QChar typedChar = triggeredCommitCharacter();
|
||||
const auto edit = item.textEdit();
|
||||
if (!edit)
|
||||
return;
|
||||
|
||||
const int labelOpenParenOffset = item.label().indexOf('(');
|
||||
const int labelClosingParenOffset = item.label().indexOf(')');
|
||||
const auto kind = static_cast<CompletionItemKind::Kind>(
|
||||
item.kind().value_or(CompletionItemKind::Text));
|
||||
const bool isMacroCall = kind == CompletionItemKind::Text && labelOpenParenOffset != -1
|
||||
&& labelClosingParenOffset > labelOpenParenOffset; // Heuristic
|
||||
const bool isFunctionLike = kind == CompletionItemKind::Function
|
||||
|| kind == CompletionItemKind::Method || kind == CompletionItemKind::Constructor
|
||||
|| isMacroCall;
|
||||
|
||||
QString rawInsertText = edit->newText();
|
||||
|
||||
// Some preparation for our magic involving (non-)insertion of parentheses and
|
||||
// cursor placement.
|
||||
if (isFunctionLike && !rawInsertText.contains('(')) {
|
||||
if (labelOpenParenOffset != -1) {
|
||||
if (labelClosingParenOffset == labelOpenParenOffset + 1) // function takes no arguments
|
||||
rawInsertText += "()";
|
||||
else // function takes arguments
|
||||
rawInsertText += "( )";
|
||||
}
|
||||
}
|
||||
|
||||
const int firstParenOffset = rawInsertText.indexOf('(');
|
||||
const int lastParenOffset = rawInsertText.lastIndexOf(')');
|
||||
const QString detail = item.detail().value_or(QString());
|
||||
const CompletionSettings &completionSettings = TextEditorSettings::completionSettings();
|
||||
QString textToBeInserted = rawInsertText.left(firstParenOffset);
|
||||
QString extraCharacters;
|
||||
int extraLength = 0;
|
||||
int cursorOffset = 0;
|
||||
bool setAutoCompleteSkipPos = false;
|
||||
int currentPos = manipulator.currentPosition();
|
||||
const QTextDocument * const doc = manipulator.textCursorAt(currentPos).document();
|
||||
const Range range = edit->range();
|
||||
const int rangeStart = range.start().toPositionInDocument(doc);
|
||||
if (isFunctionLike && 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 && !isMacroCall && matchPreviousWord(manipulator, cursor, detail))
|
||||
abandonParen = true; // function definition
|
||||
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(currentPos);
|
||||
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(currentPos + 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;
|
||||
}
|
||||
|
||||
// Avoid inserting characters that are already there
|
||||
QTextCursor cursor = manipulator.textCursorAt(rangeStart);
|
||||
cursor.movePosition(QTextCursor::EndOfWord);
|
||||
const QString textAfterCursor = manipulator.textAt(currentPos, cursor.position() - currentPos);
|
||||
if (currentPos < cursor.position()
|
||||
&& textToBeInserted != textAfterCursor
|
||||
&& textToBeInserted.indexOf(textAfterCursor, currentPos - rangeStart) >= 0) {
|
||||
currentPos = cursor.position();
|
||||
}
|
||||
for (int i = 0; i < extraCharacters.length(); ++i) {
|
||||
const QChar a = extraCharacters.at(i);
|
||||
const QChar b = manipulator.characterAt(currentPos + i);
|
||||
if (a == b)
|
||||
++extraLength;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
textToBeInserted += extraCharacters;
|
||||
const int length = currentPos - rangeStart + extraLength;
|
||||
const bool isReplaced = manipulator.replace(rangeStart, length, 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);
|
||||
}
|
||||
}
|
||||
|
||||
ClangdCompletionItem::SpecialQtType ClangdCompletionItem::getQtType(const CompletionItem &item)
|
||||
{
|
||||
const Utils::optional<MarkupOrString> doc = item.documentation();
|
||||
if (!doc)
|
||||
return SpecialQtType::None;
|
||||
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();
|
||||
if (docText.contains("Annotation: qt_signal"))
|
||||
return SpecialQtType::Signal;
|
||||
if (docText.contains("Annotation: qt_slot"))
|
||||
return SpecialQtType::Slot;
|
||||
return SpecialQtType::None;
|
||||
}
|
||||
|
||||
QIcon ClangdCompletionItem::icon() const
|
||||
{
|
||||
if (isDeprecated())
|
||||
return Utils::Icons::WARNING.icon();
|
||||
const SpecialQtType qtType = getQtType(item());
|
||||
switch (qtType) {
|
||||
case SpecialQtType::Signal:
|
||||
return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::Signal);
|
||||
case SpecialQtType::Slot:
|
||||
// FIXME: Add visibility info to completion item tags in clangd?
|
||||
return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::SlotPublic);
|
||||
case SpecialQtType::None:
|
||||
break;
|
||||
}
|
||||
if (item().kind().value_or(CompletionItemKind::Text) == CompletionItemKind::Property)
|
||||
return Utils::CodeModelIcon::iconForType(Utils::CodeModelIcon::VarPublicStatic);
|
||||
return LanguageClientCompletionItem::icon();
|
||||
}
|
||||
|
||||
QString ClangdCompletionItem::text() const
|
||||
{
|
||||
const QString clangdValue = LanguageClientCompletionItem::text();
|
||||
if (isDeprecated())
|
||||
return "[[deprecated]]" + clangdValue;
|
||||
return clangdValue;
|
||||
}
|
||||
|
||||
CustomAssistProcessor::CustomAssistProcessor(ClangdClient *client, int position, int endPos,
|
||||
unsigned completionOperator, CustomAssistMode mode)
|
||||
: m_client(client)
|
||||
, m_position(position)
|
||||
, m_endPos(endPos)
|
||||
, m_completionOperator(completionOperator)
|
||||
, m_mode(mode)
|
||||
{}
|
||||
|
||||
IAssistProposal *CustomAssistProcessor::perform(const AssistInterface *interface)
|
||||
{
|
||||
QList<AssistProposalItemInterface *> completions;
|
||||
switch (m_mode) {
|
||||
case CustomAssistMode::Doxygen:
|
||||
for (int i = 1; i < T_DOXY_LAST_TAG; ++i) {
|
||||
completions << createItem(QLatin1String(doxygenTagSpell(i)),
|
||||
CPlusPlus::Icons::keywordIcon());
|
||||
}
|
||||
break;
|
||||
case CustomAssistMode::Preprocessor: {
|
||||
static QIcon macroIcon = Utils::CodeModelIcon::iconForType(CodeModelIcon::Macro);
|
||||
for (const QString &completion
|
||||
: CppCompletionAssistProcessor::preprocessorCompletions()) {
|
||||
completions << createItem(completion, macroIcon);
|
||||
}
|
||||
if (ProjectFile::isObjC(interface->filePath().toString()))
|
||||
completions << createItem("import", macroIcon);
|
||||
break;
|
||||
}
|
||||
case CustomAssistMode::IncludePath: {
|
||||
HeaderPaths headerPaths;
|
||||
const ProjectPart::ConstPtr projectPart
|
||||
= projectPartForFile(interface->filePath().toString());
|
||||
if (projectPart)
|
||||
headerPaths = projectPart->headerPaths;
|
||||
completions = completeInclude(m_endPos, m_completionOperator, interface, headerPaths);
|
||||
break;
|
||||
}
|
||||
}
|
||||
GenericProposalModelPtr model(new GenericProposalModel);
|
||||
model->loadContent(completions);
|
||||
const auto proposal = new GenericProposal(m_position, model);
|
||||
if (m_client->testingEnabled()) {
|
||||
emit m_client->proposalReady(proposal);
|
||||
return nullptr;
|
||||
}
|
||||
return proposal;
|
||||
}
|
||||
|
||||
AssistProposalItemInterface *CustomAssistProcessor::createItem(const QString &text,
|
||||
const QIcon &icon) const
|
||||
{
|
||||
const auto item = new ClangPreprocessorAssistProposalItem;
|
||||
item->setText(text);
|
||||
item->setIcon(icon);
|
||||
item->setCompletionOperator(m_completionOperator);
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates completion proposals for #include and given cursor
|
||||
* @param position - cursor placed after opening bracked or quote
|
||||
* @param completionOperator - the type of token
|
||||
* @param interface - relevant document data
|
||||
* @param headerPaths - the include paths
|
||||
* @return the list of completion items
|
||||
*/
|
||||
QList<AssistProposalItemInterface *> CustomAssistProcessor::completeInclude(
|
||||
int position, unsigned completionOperator, const AssistInterface *interface,
|
||||
const HeaderPaths &headerPaths)
|
||||
{
|
||||
QTextCursor cursor(interface->textDocument());
|
||||
cursor.setPosition(position);
|
||||
QString directoryPrefix;
|
||||
if (completionOperator == T_SLASH) {
|
||||
QTextCursor c = cursor;
|
||||
c.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor);
|
||||
QString sel = c.selectedText();
|
||||
int startCharPos = sel.indexOf(QLatin1Char('"'));
|
||||
if (startCharPos == -1) {
|
||||
startCharPos = sel.indexOf(QLatin1Char('<'));
|
||||
completionOperator = T_ANGLE_STRING_LITERAL;
|
||||
} else {
|
||||
completionOperator = T_STRING_LITERAL;
|
||||
}
|
||||
if (startCharPos != -1)
|
||||
directoryPrefix = sel.mid(startCharPos + 1, sel.length() - 1);
|
||||
}
|
||||
|
||||
// Make completion for all relevant includes
|
||||
HeaderPaths allHeaderPaths = headerPaths;
|
||||
const auto currentFilePath = HeaderPath::makeUser(
|
||||
interface->filePath().toFileInfo().path());
|
||||
if (!allHeaderPaths.contains(currentFilePath))
|
||||
allHeaderPaths.append(currentFilePath);
|
||||
|
||||
const MimeType mimeType = mimeTypeForName("text/x-c++hdr");
|
||||
const QStringList suffixes = mimeType.suffixes();
|
||||
|
||||
QList<AssistProposalItemInterface *> completions;
|
||||
for (const HeaderPath &headerPath : qAsConst(allHeaderPaths)) {
|
||||
QString realPath = headerPath.path;
|
||||
if (!directoryPrefix.isEmpty()) {
|
||||
realPath += QLatin1Char('/');
|
||||
realPath += directoryPrefix;
|
||||
if (headerPath.type == HeaderPathType::Framework)
|
||||
realPath += QLatin1String(".framework/Headers");
|
||||
}
|
||||
completions << completeIncludePath(realPath, suffixes, completionOperator);
|
||||
}
|
||||
|
||||
QList<QPair<AssistProposalItemInterface *, QString>> completionsForSorting;
|
||||
for (AssistProposalItemInterface * const item : qAsConst(completions)) {
|
||||
QString s = item->text();
|
||||
s.replace('/', QChar(0)); // The dir separator should compare less than anything else.
|
||||
completionsForSorting << qMakePair(item, s);
|
||||
}
|
||||
Utils::sort(completionsForSorting, [](const auto &left, const auto &right) {
|
||||
return left.second < right.second;
|
||||
});
|
||||
for (int i = 0; i < completionsForSorting.count(); ++i)
|
||||
completions[i] = completionsForSorting[i].first;
|
||||
|
||||
return completions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Finds #include completion proposals using given include path
|
||||
* @param realPath - one of directories where compiler searches includes
|
||||
* @param suffixes - file suffixes for C/C++ header files
|
||||
* @return a list of matching completion items
|
||||
*/
|
||||
QList<AssistProposalItemInterface *> CustomAssistProcessor::completeIncludePath(
|
||||
const QString &realPath, const QStringList &suffixes, unsigned completionOperator)
|
||||
{
|
||||
QList<AssistProposalItemInterface *> completions;
|
||||
QDirIterator i(realPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
//: Parent folder for proposed #include completion
|
||||
const QString hint = ClangdClient::tr("Location: %1")
|
||||
.arg(QDir::toNativeSeparators(QDir::cleanPath(realPath)));
|
||||
while (i.hasNext()) {
|
||||
const QString fileName = i.next();
|
||||
const QFileInfo fileInfo = i.fileInfo();
|
||||
const QString suffix = fileInfo.suffix();
|
||||
if (suffix.isEmpty() || suffixes.contains(suffix)) {
|
||||
QString text = fileName.mid(realPath.length() + 1);
|
||||
if (fileInfo.isDir())
|
||||
text += QLatin1Char('/');
|
||||
|
||||
auto *item = new ClangPreprocessorAssistProposalItem;
|
||||
item->setText(text);
|
||||
item->setDetail(hint);
|
||||
item->setIcon(CPlusPlus::Icons::keywordIcon());
|
||||
item->setCompletionOperator(completionOperator);
|
||||
completions.append(item);
|
||||
}
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
|
||||
ClangdCompletionAssistProcessor::ClangdCompletionAssistProcessor(ClangdClient *client,
|
||||
const QString &snippetsGroup)
|
||||
: LanguageClientCompletionAssistProcessor(client, snippetsGroup)
|
||||
, m_client(client)
|
||||
{
|
||||
m_timer.start();
|
||||
}
|
||||
|
||||
ClangdCompletionAssistProcessor::~ClangdCompletionAssistProcessor()
|
||||
{
|
||||
qCDebug(clangdLogTiming).noquote().nospace()
|
||||
<< "ClangdCompletionAssistProcessor took: " << m_timer.elapsed() << " ms";
|
||||
}
|
||||
|
||||
IAssistProposal *ClangdCompletionAssistProcessor::perform(const AssistInterface *interface)
|
||||
{
|
||||
if (m_client->testingEnabled()) {
|
||||
setAsyncCompletionAvailableHandler([this](IAssistProposal *proposal) {
|
||||
emit m_client->proposalReady(proposal);
|
||||
});
|
||||
}
|
||||
return LanguageClientCompletionAssistProcessor::perform(interface);
|
||||
}
|
||||
|
||||
QList<AssistProposalItemInterface *> ClangdCompletionAssistProcessor::generateCompletionItems(
|
||||
const QList<CompletionItem> &items) const
|
||||
{
|
||||
qCDebug(clangdLog) << "received" << items.count() << "completions";
|
||||
|
||||
auto itemGenerator = [](const QList<LanguageServerProtocol::CompletionItem> &items) {
|
||||
return Utils::transform<QList<AssistProposalItemInterface *>>(items,
|
||||
[](const LanguageServerProtocol::CompletionItem &item) {
|
||||
return new ClangdCompletionItem(item);
|
||||
});
|
||||
};
|
||||
|
||||
// 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) {
|
||||
return ClangdCompletionItem::getQtType(ci) == ClangdCompletionItem::SpecialQtType::Signal;
|
||||
};
|
||||
const QTextDocument *doc = document();
|
||||
const int pos = basePos();
|
||||
if (!doc || pos < 0 || !Utils::anyOf(items, criterion))
|
||||
return itemGenerator(items);
|
||||
const QString content = doc->toPlainText();
|
||||
const bool requiresSignal = CppModelManager::instance()->getSignalSlotType(
|
||||
filePath().toString(), content.toUtf8(), pos)
|
||||
== SignalSlotType::NewStyleSignal;
|
||||
if (requiresSignal)
|
||||
return itemGenerator(Utils::filtered(items, criterion));
|
||||
return itemGenerator(items);
|
||||
}
|
||||
|
||||
ClangdFunctionHintProcessor::ClangdFunctionHintProcessor(ClangdClient *client)
|
||||
: FunctionHintProcessor(client)
|
||||
, m_client(client)
|
||||
{}
|
||||
|
||||
IAssistProposal *ClangdFunctionHintProcessor::perform(const AssistInterface *interface)
|
||||
{
|
||||
if (m_client->testingEnabled()) {
|
||||
setAsyncCompletionAvailableHandler([this](IAssistProposal *proposal) {
|
||||
emit m_client->proposalReady(proposal);
|
||||
});
|
||||
}
|
||||
return FunctionHintProcessor::perform(interface);
|
||||
}
|
||||
|
||||
ClangdCompletionCapabilities::ClangdCompletionCapabilities(const JsonObject &object)
|
||||
: TextDocumentClientCapabilities::CompletionCapabilities(object)
|
||||
{
|
||||
insert("editsNearCursor", true); // For dot-to-arrow correction.
|
||||
if (Utils::optional<CompletionItemCapbilities> completionItemCaps = completionItem()) {
|
||||
completionItemCaps->setSnippetSupport(false);
|
||||
setCompletionItem(*completionItemCaps);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ClangCodeModel::Internal
|
62
src/plugins/clangcodemodel/clangdcompletion.h
Normal file
62
src/plugins/clangcodemodel/clangdcompletion.h
Normal file
@@ -0,0 +1,62 @@
|
||||
|
||||
#include <languageclient/languageclientcompletionassist.h>
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2022 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 <languageclient/languageclientcompletionassist.h>
|
||||
#include <languageserverprotocol/clientcapabilities.h>
|
||||
|
||||
namespace TextEditor { class IAssistProcessor; }
|
||||
namespace ClangCodeModel::Internal {
|
||||
class ClangdClient;
|
||||
|
||||
class ClangdCompletionAssistProvider : public LanguageClient::LanguageClientCompletionAssistProvider
|
||||
{
|
||||
public:
|
||||
ClangdCompletionAssistProvider(ClangdClient *client);
|
||||
|
||||
private:
|
||||
TextEditor::IAssistProcessor *createProcessor(
|
||||
const TextEditor::AssistInterface *interface) const override;
|
||||
|
||||
int activationCharSequenceLength() const override { return 3; }
|
||||
bool isActivationCharSequence(const QString &sequence) const override;
|
||||
bool isContinuationChar(const QChar &c) const override;
|
||||
|
||||
bool isInCommentOrString(const TextEditor::AssistInterface *interface) const;
|
||||
|
||||
ClangdClient * const m_client;
|
||||
};
|
||||
|
||||
class ClangdCompletionCapabilities
|
||||
: public LanguageServerProtocol::TextDocumentClientCapabilities::CompletionCapabilities
|
||||
{
|
||||
public:
|
||||
explicit ClangdCompletionCapabilities(const JsonObject &object);
|
||||
};
|
||||
|
||||
} // namespace ClangCodeModel::Internal
|
Reference in New Issue
Block a user