forked from qt-creator/qt-creator
... for clangd >= 16, which has the configurable rename limit. Still allow users to opt out for now. Change-Id: I86d6809b66ffbf80245b61cf57c648842d2e17af Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: <github-actions-qt-creator@cristianadam.eu> Reviewed-by: David Schulz <david.schulz@qt.io>
1504 lines
60 KiB
C++
1504 lines
60 KiB
C++
// Copyright (C) 2021 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
|
|
|
#include "clangdclient.h"
|
|
|
|
#include "clangconstants.h"
|
|
#include "clangdast.h"
|
|
#include "clangdcompletion.h"
|
|
#include "clangdfindreferences.h"
|
|
#include "clangdfollowsymbol.h"
|
|
#include "clangdlocatorfilters.h"
|
|
#include "clangdmemoryusagewidget.h"
|
|
#include "clangdquickfixes.h"
|
|
#include "clangdswitchdecldef.h"
|
|
#include "clangtextmark.h"
|
|
#include "clangutils.h"
|
|
#include "clangdsemantichighlighting.h"
|
|
#include "tasktimers.h"
|
|
|
|
#include <coreplugin/editormanager/editormanager.h>
|
|
#include <coreplugin/icore.h>
|
|
#include <cplusplus/AST.h>
|
|
#include <cplusplus/ASTPath.h>
|
|
#include <cplusplus/Icons.h>
|
|
#include <cppeditor/cppcodemodelsettings.h>
|
|
#include <cppeditor/cppeditorconstants.h>
|
|
#include <cppeditor/cppeditorwidget.h>
|
|
#include <cppeditor/cppmodelmanager.h>
|
|
#include <cppeditor/cpprefactoringchanges.h>
|
|
#include <cppeditor/cppsemanticinfo.h>
|
|
#include <cppeditor/cpptoolsreuse.h>
|
|
#include <cppeditor/cppvirtualfunctionassistprovider.h>
|
|
#include <cppeditor/cppvirtualfunctionproposalitem.h>
|
|
#include <cppeditor/semantichighlighter.h>
|
|
#include <languageclient/diagnosticmanager.h>
|
|
#include <languageclient/languageclienthoverhandler.h>
|
|
#include <languageclient/languageclientinterface.h>
|
|
#include <languageclient/languageclientmanager.h>
|
|
#include <languageclient/languageclientsymbolsupport.h>
|
|
#include <languageclient/languageclientutils.h>
|
|
#include <languageserverprotocol/clientcapabilities.h>
|
|
#include <languageserverprotocol/progresssupport.h>
|
|
#include <projectexplorer/project.h>
|
|
#include <projectexplorer/projecttree.h>
|
|
#include <projectexplorer/session.h>
|
|
#include <projectexplorer/taskhub.h>
|
|
#include <texteditor/codeassist/assistinterface.h>
|
|
#include <texteditor/codeassist/iassistprocessor.h>
|
|
#include <texteditor/codeassist/iassistprovider.h>
|
|
#include <texteditor/codeassist/textdocumentmanipulatorinterface.h>
|
|
#include <texteditor/texteditor.h>
|
|
#include <utils/algorithm.h>
|
|
#include <utils/environment.h>
|
|
#include <utils/fileutils.h>
|
|
#include <utils/itemviews.h>
|
|
#include <utils/runextensions.h>
|
|
#include <utils/utilsicons.h>
|
|
|
|
#include <QAction>
|
|
#include <QElapsedTimer>
|
|
#include <QFile>
|
|
#include <QHash>
|
|
#include <QPair>
|
|
#include <QPointer>
|
|
#include <QRegularExpression>
|
|
|
|
#include <cmath>
|
|
#include <new>
|
|
#include <set>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
|
|
using namespace CPlusPlus;
|
|
using namespace Core;
|
|
using namespace LanguageClient;
|
|
using namespace LanguageServerProtocol;
|
|
using namespace ProjectExplorer;
|
|
using namespace TextEditor;
|
|
|
|
namespace ClangCodeModel {
|
|
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 QString indexingToken() { return "backgroundIndexProgress"; }
|
|
|
|
class SymbolDetails : public JsonObject
|
|
{
|
|
public:
|
|
using JsonObject::JsonObject;
|
|
|
|
static constexpr char16_t usrKey[] = u"usr";
|
|
|
|
// the unqualified name of the symbol
|
|
QString name() const { return typedValue<QString>(nameKey); }
|
|
|
|
// the enclosing namespace, class etc (without trailing ::)
|
|
// [NOTE: This is not true, the trailing colons are included]
|
|
QString containerName() const { return typedValue<QString>(containerNameKey); }
|
|
|
|
// the clang-specific “unified symbol resolution” identifier
|
|
QString usr() const { return typedValue<QString>(usrKey); }
|
|
|
|
// the clangd-specific opaque symbol ID
|
|
std::optional<QString> id() const { return optionalValue<QString>(idKey); }
|
|
|
|
bool isValid() const override
|
|
{
|
|
return contains(nameKey) && contains(containerNameKey) && contains(usrKey);
|
|
}
|
|
};
|
|
|
|
class SymbolInfoRequest : public Request<LanguageClientArray<SymbolDetails>, std::nullptr_t, TextDocumentPositionParams>
|
|
{
|
|
public:
|
|
using Request::Request;
|
|
explicit SymbolInfoRequest(const TextDocumentPositionParams ¶ms)
|
|
: Request("textDocument/symbolInfo", params) {}
|
|
};
|
|
|
|
void setupClangdConfigFile()
|
|
{
|
|
const Utils::FilePath targetConfigFile = CppEditor::ClangdSettings::clangdUserConfigFilePath();
|
|
const Utils::FilePath baseDir = targetConfigFile.parentDir();
|
|
baseDir.ensureWritableDir();
|
|
Utils::FileReader configReader;
|
|
const QByteArray firstLine = "# This file was generated by Qt Creator and will be overwritten "
|
|
"unless you remove this line.";
|
|
if (!configReader.fetch(targetConfigFile) || configReader.data().startsWith(firstLine)) {
|
|
Utils::FileSaver saver(targetConfigFile);
|
|
saver.write(firstLine + '\n');
|
|
saver.write("Hover:\n");
|
|
saver.write(" ShowAKA: Yes\n");
|
|
saver.write("Diagnostics:\n");
|
|
saver.write(" UnusedIncludes: Strict\n");
|
|
QTC_CHECK(saver.finalize());
|
|
}
|
|
}
|
|
|
|
static BaseClientInterface *clientInterface(Project *project, const Utils::FilePath &jsonDbDir)
|
|
{
|
|
using CppEditor::ClangdSettings;
|
|
QString indexingOption = "--background-index";
|
|
const ClangdSettings settings(CppEditor::ClangdProjectSettings(project).settings());
|
|
const ClangdSettings::IndexingPriority indexingPriority = settings.indexingPriority();
|
|
const bool indexingEnabled = indexingPriority != ClangdSettings::IndexingPriority::Off;
|
|
if (!indexingEnabled)
|
|
indexingOption += "=0";
|
|
const QString headerInsertionOption = QString("--header-insertion=")
|
|
+ (settings.autoIncludeHeaders() ? "iwyu" : "never");
|
|
const QString limitResults = QString("--limit-results=%1").arg(settings.completionResults());
|
|
Utils::CommandLine cmd{settings.clangdFilePath(),
|
|
{indexingOption,
|
|
headerInsertionOption,
|
|
limitResults,
|
|
"--limit-references=0",
|
|
"--clang-tidy=0"}};
|
|
if (settings.workerThreadLimit() != 0)
|
|
cmd.addArg("-j=" + QString::number(settings.workerThreadLimit()));
|
|
if (indexingEnabled && settings.clangdVersion() >= QVersionNumber(15)) {
|
|
cmd.addArg("--background-index-priority="
|
|
+ ClangdSettings::priorityToString(indexingPriority));
|
|
}
|
|
if (settings.clangdVersion() >= QVersionNumber(16))
|
|
cmd.addArg("--rename-file-limit=0");
|
|
if (!jsonDbDir.isEmpty())
|
|
cmd.addArg("--compile-commands-dir=" + jsonDbDir.toString());
|
|
if (clangdLogServer().isDebugEnabled())
|
|
cmd.addArgs({"--log=verbose", "--pretty"});
|
|
cmd.addArg("--use-dirty-headers");
|
|
const auto interface = new StdIOClientInterface;
|
|
interface->setCommandLine(cmd);
|
|
return interface;
|
|
}
|
|
|
|
class DiagnosticsCapabilities : public JsonObject
|
|
{
|
|
public:
|
|
using JsonObject::JsonObject;
|
|
void enableCategorySupport() { insert(u"categorySupport", true); }
|
|
void enableCodeActionsInline() {insert(u"codeActionsInline", true);}
|
|
};
|
|
|
|
class ClangdTextDocumentClientCapabilities : public TextDocumentClientCapabilities
|
|
{
|
|
public:
|
|
using TextDocumentClientCapabilities::TextDocumentClientCapabilities;
|
|
|
|
|
|
void setPublishDiagnostics(const DiagnosticsCapabilities &caps)
|
|
{ insert(u"publishDiagnostics", caps); }
|
|
};
|
|
|
|
static qint64 getRevision(const TextDocument *doc)
|
|
{
|
|
return doc->document()->revision();
|
|
}
|
|
static qint64 getRevision(const Utils::FilePath &fp)
|
|
{
|
|
return fp.lastModified().toMSecsSinceEpoch();
|
|
}
|
|
|
|
template<typename DocType, typename DataType> class VersionedDocData
|
|
{
|
|
public:
|
|
VersionedDocData(const DocType &doc, const DataType &data) :
|
|
revision(getRevision(doc)), data(data) {}
|
|
|
|
const qint64 revision;
|
|
const DataType data;
|
|
};
|
|
|
|
template<typename DocType, typename DataType> class VersionedDataCache
|
|
{
|
|
public:
|
|
void insert(const DocType &doc, const DataType &data)
|
|
{
|
|
m_data.emplace(doc, VersionedDocData(doc, data));
|
|
}
|
|
void remove(const DocType &doc) { m_data.erase(doc); }
|
|
std::optional<VersionedDocData<DocType, DataType>> take(const DocType &doc)
|
|
{
|
|
const auto it = m_data.find(doc);
|
|
if (it == m_data.end())
|
|
return {};
|
|
const auto data = it->second;
|
|
m_data.erase(it);
|
|
return data;
|
|
}
|
|
std::optional<DataType> get(const DocType &doc)
|
|
{
|
|
const auto it = m_data.find(doc);
|
|
if (it == m_data.end())
|
|
return {};
|
|
if (it->second.revision != getRevision(doc)) {
|
|
m_data.erase(it);
|
|
return {};
|
|
}
|
|
return it->second.data;
|
|
}
|
|
private:
|
|
std::unordered_map<DocType, VersionedDocData<DocType, DataType>> m_data;
|
|
};
|
|
|
|
class HighlightingData
|
|
{
|
|
public:
|
|
// For all QPairs, the int member is the corresponding document version.
|
|
QPair<QList<ExpandedSemanticToken>, int> previousTokens;
|
|
|
|
// The ranges of symbols referring to virtual functions,
|
|
// as extracted by the highlighting procedure.
|
|
QPair<QList<Range>, int> virtualRanges;
|
|
|
|
// The highlighter is owned by its document.
|
|
CppEditor::SemanticHighlighter *highlighter = nullptr;
|
|
};
|
|
|
|
class ClangdClient::Private
|
|
{
|
|
public:
|
|
Private(ClangdClient *q, Project *project)
|
|
: q(q), settings(CppEditor::ClangdProjectSettings(project).settings()) {}
|
|
|
|
void findUsages(TextDocument *document, const QTextCursor &cursor,
|
|
const QString &searchTerm, const std::optional<QString> &replacement,
|
|
bool categorize);
|
|
|
|
void handleDeclDefSwitchReplies();
|
|
|
|
static CppEditor::CppEditorWidget *widgetFromDocument(const TextDocument *doc);
|
|
QString searchTermFromCursor(const QTextCursor &cursor) const;
|
|
QTextCursor adjustedCursor(const QTextCursor &cursor, const TextDocument *doc);
|
|
|
|
void setHelpItemForTooltip(const MessageId &token,
|
|
const DocumentUri &uri,
|
|
const QString &fqn = {},
|
|
HelpItem::Category category = HelpItem::Unknown,
|
|
const QString &type = {});
|
|
|
|
void handleSemanticTokens(TextDocument *doc, const QList<ExpandedSemanticToken> &tokens,
|
|
int version, bool force);
|
|
|
|
MessageId getAndHandleAst(const TextDocOrFile &doc, const AstHandler &astHandler,
|
|
AstCallbackMode callbackMode, const Range &range = {});
|
|
|
|
ClangdClient * const q;
|
|
const CppEditor::ClangdSettings::Data settings;
|
|
ClangdFollowSymbol *followSymbol = nullptr;
|
|
ClangdSwitchDeclDef *switchDeclDef = nullptr;
|
|
ClangdFindLocalReferences *findLocalRefs = nullptr;
|
|
std::optional<QVersionNumber> versionNumber;
|
|
|
|
QHash<TextDocument *, HighlightingData> highlightingData;
|
|
QHash<Utils::FilePath, CppEditor::BaseEditorDocumentParser::Configuration> parserConfigs;
|
|
QHash<Utils::FilePath, Tasks> issuePaneEntries;
|
|
QHash<Utils::FilePath, int> openedExtraFiles;
|
|
|
|
VersionedDataCache<const TextDocument *, ClangdAstNode> astCache;
|
|
VersionedDataCache<Utils::FilePath, ClangdAstNode> externalAstCache;
|
|
TaskTimer highlightingTimer{"highlighting"};
|
|
bool isFullyIndexed = false;
|
|
bool isTesting = false;
|
|
};
|
|
|
|
static void addToCompilationDb(QJsonObject &cdb,
|
|
const CppEditor::ProjectPart &projectPart,
|
|
CppEditor::UsePrecompiledHeaders usePch,
|
|
const QJsonArray &projectPartOptions,
|
|
const Utils::FilePath &workingDir,
|
|
const CppEditor::ProjectFile &sourceFile,
|
|
bool clStyle)
|
|
{
|
|
QJsonArray args = clangOptionsForFile(sourceFile, projectPart, projectPartOptions, usePch,
|
|
clStyle);
|
|
|
|
// TODO: clangd seems to apply some heuristics depending on what we put here.
|
|
// Should we make use of them or keep using our own?
|
|
args.prepend("clang");
|
|
|
|
const QString fileString = Utils::FilePath::fromString(sourceFile.path).toUserOutput();
|
|
args.append(fileString);
|
|
QJsonObject value;
|
|
value.insert("workingDirectory", workingDir.toString());
|
|
value.insert("compilationCommand", args);
|
|
cdb.insert(fileString, value);
|
|
}
|
|
|
|
static void addCompilationDb(QJsonObject &parentObject, const QJsonObject &cdb)
|
|
{
|
|
parentObject.insert("compilationDatabaseChanges", cdb);
|
|
}
|
|
|
|
ClangdClient::ClangdClient(Project *project, const Utils::FilePath &jsonDbDir)
|
|
: Client(clientInterface(project, jsonDbDir)), d(new Private(this, project))
|
|
{
|
|
setName(tr("clangd"));
|
|
LanguageFilter langFilter;
|
|
langFilter.mimeTypes = QStringList{"text/x-chdr", "text/x-csrc",
|
|
"text/x-c++hdr", "text/x-c++src", "text/x-objc++src", "text/x-objcsrc"};
|
|
setSupportedLanguage(langFilter);
|
|
setActivateDocumentAutomatically(true);
|
|
setLogTarget(LogTarget::Console);
|
|
setCompletionAssistProvider(new ClangdCompletionAssistProvider(this));
|
|
setQuickFixAssistProvider(new ClangdQuickFixProvider(this));
|
|
symbolSupport().setDefaultRenamingSymbolMapper(
|
|
[](const QString &oldSymbol) { return oldSymbol + "_new"; });
|
|
symbolSupport().setLimitRenamingToProjects(true);
|
|
if (!project) {
|
|
QJsonObject initOptions;
|
|
const Utils::FilePath includeDir
|
|
= CppEditor::ClangdSettings(d->settings).clangdIncludePath();
|
|
CppEditor::CompilerOptionsBuilder optionsBuilder = clangOptionsBuilder(
|
|
*CppEditor::CppModelManager::instance()->fallbackProjectPart(),
|
|
warningsConfigForProject(nullptr), includeDir, {});
|
|
const CppEditor::UsePrecompiledHeaders usePch = CppEditor::getPchUsage();
|
|
const QJsonArray projectPartOptions = fullProjectPartOptions(
|
|
optionsBuilder, globalClangOptions());
|
|
const QJsonArray clangOptions = clangOptionsForFile({}, optionsBuilder.projectPart(),
|
|
projectPartOptions, usePch,
|
|
optionsBuilder.isClStyle());
|
|
initOptions.insert("fallbackFlags", clangOptions);
|
|
setInitializationOptions(initOptions);
|
|
}
|
|
auto isRunningClangdClient = [](const LanguageClient::Client *c) {
|
|
return qobject_cast<const ClangdClient *>(c) && c->state() != Client::ShutdownRequested
|
|
&& c->state() != Client::Shutdown;
|
|
};
|
|
const QList<Client *> clients =
|
|
Utils::filtered(LanguageClientManager::clientsForProject(project), isRunningClangdClient);
|
|
QTC_CHECK(clients.isEmpty());
|
|
for (const Client *client : clients)
|
|
qCWarning(clangdLog) << client->name() << client->stateString();
|
|
ClientCapabilities caps = Client::defaultClientCapabilities();
|
|
std::optional<TextDocumentClientCapabilities> textCaps = caps.textDocument();
|
|
if (textCaps) {
|
|
ClangdTextDocumentClientCapabilities clangdTextCaps(*textCaps);
|
|
clangdTextCaps.clearDocumentHighlight();
|
|
DiagnosticsCapabilities diagnostics;
|
|
diagnostics.enableCategorySupport();
|
|
diagnostics.enableCodeActionsInline();
|
|
clangdTextCaps.setPublishDiagnostics(diagnostics);
|
|
std::optional<TextDocumentClientCapabilities::CompletionCapabilities> completionCaps
|
|
= textCaps->completion();
|
|
if (completionCaps)
|
|
clangdTextCaps.setCompletion(ClangdCompletionCapabilities(*completionCaps));
|
|
caps.setTextDocument(clangdTextCaps);
|
|
}
|
|
caps.clearExperimental();
|
|
setClientCapabilities(caps);
|
|
setLocatorsEnabled(false);
|
|
setAutoRequestCodeActions(false); // clangd sends code actions inside diagnostics
|
|
setProgressTitleForToken(indexingToken(),
|
|
project ? tr("Indexing %1 with clangd").arg(project->displayName())
|
|
: tr("Indexing session with clangd"));
|
|
setClickHandlerForToken(indexingToken(), [] {
|
|
ICore::showOptionsDialog(CppEditor::Constants::CPP_CLANGD_SETTINGS_ID);
|
|
});
|
|
setCurrentProject(project);
|
|
setDocumentChangeUpdateThreshold(d->settings.documentUpdateThreshold);
|
|
setSymbolStringifier(displayNameFromDocumentSymbol);
|
|
setSemanticTokensHandler([this](TextDocument *doc, const QList<ExpandedSemanticToken> &tokens,
|
|
int version, bool force) {
|
|
d->handleSemanticTokens(doc, tokens, version, force);
|
|
});
|
|
hoverHandler()->setHelpItemProvider([this](const HoverRequest::Response &response,
|
|
const DocumentUri &uri) {
|
|
gatherHelpItemForTooltip(response, uri);
|
|
});
|
|
|
|
connect(this, &Client::workDone, this,
|
|
[this, p = QPointer(project)](const ProgressToken &token) {
|
|
const QString * const val = std::get_if<QString>(&token);
|
|
if (val && *val == indexingToken()) {
|
|
d->isFullyIndexed = true;
|
|
emit indexingFinished();
|
|
#ifdef WITH_TESTS
|
|
if (p)
|
|
emit p->indexingFinished("Indexer.Clangd");
|
|
#endif
|
|
}
|
|
});
|
|
|
|
connect(this, &Client::initialized, this, [this] {
|
|
auto currentDocumentFilter = static_cast<ClangdCurrentDocumentFilter *>(
|
|
CppEditor::CppModelManager::instance()->currentDocumentFilter());
|
|
currentDocumentFilter->updateCurrentClient();
|
|
d->openedExtraFiles.clear();
|
|
});
|
|
|
|
start();
|
|
}
|
|
|
|
ClangdClient::~ClangdClient()
|
|
{
|
|
if (d->followSymbol)
|
|
d->followSymbol->clear();
|
|
delete d;
|
|
}
|
|
|
|
bool ClangdClient::isFullyIndexed() const
|
|
{
|
|
return d->isFullyIndexed;
|
|
}
|
|
|
|
void ClangdClient::openExtraFile(const Utils::FilePath &filePath, const QString &content)
|
|
{
|
|
const auto it = d->openedExtraFiles.find(filePath);
|
|
if (it != d->openedExtraFiles.end()) {
|
|
QTC_CHECK(it.value() > 0);
|
|
++it.value();
|
|
return;
|
|
}
|
|
|
|
QFile cxxFile(filePath.toString());
|
|
if (content.isEmpty() && !cxxFile.open(QIODevice::ReadOnly))
|
|
return;
|
|
TextDocumentItem item;
|
|
item.setLanguageId("cpp");
|
|
item.setUri(DocumentUri::fromFilePath(filePath));
|
|
item.setText(!content.isEmpty() ? content : QString::fromUtf8(cxxFile.readAll()));
|
|
item.setVersion(0);
|
|
sendMessage(DidOpenTextDocumentNotification(DidOpenTextDocumentParams(item)),
|
|
SendDocUpdates::Ignore);
|
|
|
|
d->openedExtraFiles.insert(filePath, 1);
|
|
}
|
|
|
|
void ClangdClient::closeExtraFile(const Utils::FilePath &filePath)
|
|
{
|
|
const auto it = d->openedExtraFiles.find(filePath);
|
|
QTC_ASSERT(it != d->openedExtraFiles.end(), return);
|
|
QTC_CHECK(it.value() > 0);
|
|
if (--it.value() > 0)
|
|
return;
|
|
d->openedExtraFiles.erase(it);
|
|
sendMessage(DidCloseTextDocumentNotification(DidCloseTextDocumentParams(
|
|
TextDocumentIdentifier{DocumentUri::fromFilePath(filePath)})),
|
|
SendDocUpdates::Ignore);
|
|
}
|
|
|
|
void ClangdClient::findUsages(TextDocument *document, const QTextCursor &cursor,
|
|
const std::optional<QString> &replacement)
|
|
{
|
|
// Quick check: Are we even on anything searchable?
|
|
const QTextCursor adjustedCursor = d->adjustedCursor(cursor, document);
|
|
const QString searchTerm = d->searchTermFromCursor(adjustedCursor);
|
|
if (searchTerm.isEmpty())
|
|
return;
|
|
|
|
if (replacement && versionNumber() >= QVersionNumber(16)
|
|
&& Utils::qtcEnvironmentVariable("QTC_CLANGD_RENAMING") != "0") {
|
|
symbolSupport().renameSymbol(document, adjustedCursor, *replacement,
|
|
CppEditor::preferLowerCaseFileNames());
|
|
return;
|
|
}
|
|
|
|
const bool categorize = CppEditor::codeModelSettings()->categorizeFindReferences();
|
|
|
|
// If it's a "normal" symbol, go right ahead.
|
|
if (searchTerm != "operator" && Utils::allOf(searchTerm, [](const QChar &c) {
|
|
return c.isLetterOrNumber() || c == '_';
|
|
})) {
|
|
d->findUsages(document, adjustedCursor, searchTerm, replacement, categorize);
|
|
return;
|
|
}
|
|
|
|
// Otherwise get the proper spelling of the search term from clang, so we can put it into the
|
|
// search widget.
|
|
const auto symbolInfoHandler = [this, doc = QPointer(document), adjustedCursor, replacement, categorize]
|
|
(const QString &name, const QString &, const MessageId &) {
|
|
if (!doc)
|
|
return;
|
|
if (name.isEmpty())
|
|
return;
|
|
d->findUsages(doc.data(), adjustedCursor, name, replacement, categorize);
|
|
};
|
|
requestSymbolInfo(document->filePath(), Range(adjustedCursor).start(), symbolInfoHandler);
|
|
}
|
|
|
|
void ClangdClient::handleDiagnostics(const PublishDiagnosticsParams ¶ms)
|
|
{
|
|
const DocumentUri &uri = params.uri();
|
|
Client::handleDiagnostics(params);
|
|
const int docVersion = documentVersion(uri.toFilePath());
|
|
if (params.version().value_or(docVersion) != docVersion)
|
|
return;
|
|
for (const Diagnostic &diagnostic : params.diagnostics()) {
|
|
const ClangdDiagnostic clangdDiagnostic(diagnostic);
|
|
auto codeActions = clangdDiagnostic.codeActions();
|
|
if (codeActions && !codeActions->isEmpty()) {
|
|
for (CodeAction &action : *codeActions)
|
|
action.setDiagnostics({diagnostic});
|
|
LanguageClient::updateCodeActionRefactoringMarker(this, *codeActions, uri);
|
|
} else {
|
|
// We know that there's only one kind of diagnostic for which clangd has
|
|
// a quickfix tweak, so let's not be wasteful.
|
|
const Diagnostic::Code code = diagnostic.code().value_or(Diagnostic::Code());
|
|
const QString * const codeString = std::get_if<QString>(&code);
|
|
if (codeString && *codeString == "-Wswitch")
|
|
requestCodeActions(uri, diagnostic);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ClangdClient::handleDocumentOpened(TextDocument *doc)
|
|
{
|
|
const auto data = d->externalAstCache.take(doc->filePath());
|
|
if (!data)
|
|
return;
|
|
if (data->revision == getRevision(doc->filePath()))
|
|
d->astCache.insert(doc, data->data);
|
|
}
|
|
|
|
void ClangdClient::handleDocumentClosed(TextDocument *doc)
|
|
{
|
|
d->highlightingData.remove(doc);
|
|
d->astCache.remove(doc);
|
|
d->parserConfigs.remove(doc->filePath());
|
|
}
|
|
|
|
QTextCursor ClangdClient::adjustedCursorForHighlighting(const QTextCursor &cursor,
|
|
TextEditor::TextDocument *doc)
|
|
{
|
|
return d->adjustedCursor(cursor, doc);
|
|
}
|
|
|
|
const LanguageClient::Client::CustomInspectorTabs ClangdClient::createCustomInspectorTabs()
|
|
{
|
|
return {{new ClangdMemoryUsageWidget(this), tr("Memory Usage")}};
|
|
}
|
|
|
|
class ClangdDiagnosticManager : public LanguageClient::DiagnosticManager
|
|
{
|
|
using LanguageClient::DiagnosticManager::DiagnosticManager;
|
|
|
|
ClangdClient *getClient() const { return qobject_cast<ClangdClient *>(client()); }
|
|
|
|
bool isCurrentDocument(const Utils::FilePath &filePath) const
|
|
{
|
|
const IDocument * const doc = EditorManager::currentDocument();
|
|
return doc && doc->filePath() == filePath;
|
|
}
|
|
|
|
void showDiagnostics(const DocumentUri &uri, int version) override
|
|
{
|
|
const Utils::FilePath filePath = uri.toFilePath();
|
|
getClient()->clearTasks(filePath);
|
|
DiagnosticManager::showDiagnostics(uri, version);
|
|
if (isCurrentDocument(filePath))
|
|
getClient()->switchIssuePaneEntries(filePath);
|
|
}
|
|
|
|
void hideDiagnostics(const Utils::FilePath &filePath) override
|
|
{
|
|
DiagnosticManager::hideDiagnostics(filePath);
|
|
if (isCurrentDocument(filePath))
|
|
TaskHub::clearTasks(Constants::TASK_CATEGORY_DIAGNOSTICS);
|
|
}
|
|
|
|
QList<Diagnostic> filteredDiagnostics(const QList<Diagnostic> &diagnostics) const override
|
|
{
|
|
return Utils::filtered(diagnostics, [](const Diagnostic &diag){
|
|
const Diagnostic::Code code = diag.code().value_or(Diagnostic::Code());
|
|
const QString * const codeString = std::get_if<QString>(&code);
|
|
return !codeString || *codeString != "drv_unknown_argument";
|
|
});
|
|
}
|
|
|
|
TextMark *createTextMark(const Utils::FilePath &filePath,
|
|
const Diagnostic &diagnostic,
|
|
bool isProjectFile) const override
|
|
{
|
|
return new ClangdTextMark(filePath, diagnostic, isProjectFile, getClient());
|
|
}
|
|
};
|
|
|
|
DiagnosticManager *ClangdClient::createDiagnosticManager()
|
|
{
|
|
auto diagnosticManager = new ClangdDiagnosticManager(this);
|
|
if (d->isTesting) {
|
|
connect(diagnosticManager, &DiagnosticManager::textMarkCreated,
|
|
this, &ClangdClient::textMarkCreated);
|
|
}
|
|
return diagnosticManager;
|
|
}
|
|
|
|
bool ClangdClient::referencesShadowFile(const TextEditor::TextDocument *doc,
|
|
const Utils::FilePath &candidate)
|
|
{
|
|
const QRegularExpression includeRex("#include.*" + candidate.fileName() + R"([>"])");
|
|
const QTextCursor includePos = doc->document()->find(includeRex);
|
|
return !includePos.isNull();
|
|
}
|
|
|
|
bool ClangdClient::fileBelongsToProject(const Utils::FilePath &filePath) const
|
|
{
|
|
if (CppEditor::ClangdSettings::instance().granularity()
|
|
== CppEditor::ClangdSettings::Granularity::Session) {
|
|
return SessionManager::projectForFile(filePath);
|
|
}
|
|
return Client::fileBelongsToProject(filePath);
|
|
}
|
|
|
|
RefactoringChangesData *ClangdClient::createRefactoringChangesBackend() const
|
|
{
|
|
return new CppEditor::CppRefactoringChangesData(
|
|
CppEditor::CppModelManager::instance()->snapshot());
|
|
}
|
|
|
|
QVersionNumber ClangdClient::versionNumber() const
|
|
{
|
|
if (d->versionNumber)
|
|
return d->versionNumber.value();
|
|
|
|
const QRegularExpression versionPattern("^clangd version (\\d+)\\.(\\d+)\\.(\\d+).*$");
|
|
QTC_CHECK(versionPattern.isValid());
|
|
const QRegularExpressionMatch match = versionPattern.match(serverVersion());
|
|
if (match.isValid()) {
|
|
d->versionNumber.emplace({match.captured(1).toInt(), match.captured(2).toInt(),
|
|
match.captured(3).toInt()});
|
|
} else {
|
|
qCWarning(clangdLog) << "Failed to parse clangd server string" << serverVersion();
|
|
d->versionNumber.emplace({0});
|
|
}
|
|
return d->versionNumber.value();
|
|
}
|
|
|
|
CppEditor::ClangdSettings::Data ClangdClient::settingsData() const { return d->settings; }
|
|
|
|
void ClangdClient::Private::findUsages(TextDocument *document,
|
|
const QTextCursor &cursor, const QString &searchTerm,
|
|
const std::optional<QString> &replacement, bool categorize)
|
|
{
|
|
const auto findRefs = new ClangdFindReferences(q, document, cursor, searchTerm, replacement,
|
|
categorize);
|
|
if (isTesting) {
|
|
connect(findRefs, &ClangdFindReferences::foundReferences,
|
|
q, &ClangdClient::foundReferences);
|
|
connect(findRefs, &ClangdFindReferences::done, q, &ClangdClient::findUsagesDone);
|
|
}
|
|
}
|
|
|
|
void ClangdClient::enableTesting()
|
|
{
|
|
d->isTesting = true;
|
|
}
|
|
|
|
bool ClangdClient::testingEnabled() const
|
|
{
|
|
return d->isTesting;
|
|
}
|
|
|
|
QString ClangdClient::displayNameFromDocumentSymbol(SymbolKind kind, const QString &name,
|
|
const QString &detail)
|
|
{
|
|
switch (kind) {
|
|
case SymbolKind::Constructor:
|
|
return name + detail;
|
|
case SymbolKind::Method:
|
|
case SymbolKind::Function: {
|
|
const int lastParenOffset = detail.lastIndexOf(')');
|
|
if (lastParenOffset == -1)
|
|
return name;
|
|
int leftParensNeeded = 1;
|
|
int i = -1;
|
|
for (i = lastParenOffset - 1; i >= 0; --i) {
|
|
switch (detail.at(i).toLatin1()) {
|
|
case ')':
|
|
++leftParensNeeded;
|
|
break;
|
|
case '(':
|
|
--leftParensNeeded;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (leftParensNeeded == 0)
|
|
break;
|
|
}
|
|
if (leftParensNeeded > 0)
|
|
return name;
|
|
return name + detail.mid(i) + " -> " + detail.left(i);
|
|
}
|
|
case SymbolKind::Variable:
|
|
case SymbolKind::Field:
|
|
case SymbolKind::Constant:
|
|
if (detail.isEmpty())
|
|
return name;
|
|
return name + " -> " + detail;
|
|
default:
|
|
return name;
|
|
}
|
|
}
|
|
|
|
// Force re-parse of all open files that include the changed ui header.
|
|
// Otherwise, we potentially have stale diagnostics.
|
|
void ClangdClient::handleUiHeaderChange(const QString &fileName)
|
|
{
|
|
const QRegularExpression includeRex("#include.*" + fileName + R"([>"])");
|
|
const QList<Client *> &allClients = LanguageClientManager::clients();
|
|
for (Client * const client : allClients) {
|
|
if (!client->reachable() || !qobject_cast<ClangdClient *>(client))
|
|
continue;
|
|
for (IDocument * const doc : DocumentModel::openedDocuments()) {
|
|
const auto textDoc = qobject_cast<TextDocument *>(doc);
|
|
if (!textDoc || !client->documentOpen(textDoc))
|
|
continue;
|
|
const QTextCursor includePos = textDoc->document()->find(includeRex);
|
|
if (includePos.isNull())
|
|
continue;
|
|
qCDebug(clangdLog) << "updating" << textDoc->filePath() << "due to change in UI header"
|
|
<< fileName;
|
|
client->documentContentsChanged(textDoc, 0, 0, 0);
|
|
break; // No sane project includes the same UI header twice.
|
|
}
|
|
}
|
|
}
|
|
|
|
void ClangdClient::updateParserConfig(const Utils::FilePath &filePath,
|
|
const CppEditor::BaseEditorDocumentParser::Configuration &config)
|
|
{
|
|
// TODO: Also handle usePrecompiledHeaders?
|
|
// TODO: Should we write the editor defines into the json file? It seems strange
|
|
// that they should affect the index only while the file is open in the editor.
|
|
const auto projectPart = !config.preferredProjectPartId.isEmpty()
|
|
? CppEditor::CppModelManager::instance()->projectPartForId(
|
|
config.preferredProjectPartId)
|
|
: projectPartForFile(filePath.toString());
|
|
if (!projectPart)
|
|
return;
|
|
|
|
CppEditor::BaseEditorDocumentParser::Configuration fullConfig = config;
|
|
fullConfig.preferredProjectPartId = projectPart->id();
|
|
auto cachedConfig = d->parserConfigs.find(filePath);
|
|
if (cachedConfig == d->parserConfigs.end()) {
|
|
cachedConfig = d->parserConfigs.insert(filePath, fullConfig);
|
|
if (config.preferredProjectPartId.isEmpty() && config.editorDefines.isEmpty())
|
|
return;
|
|
} else if (cachedConfig.value() == fullConfig) {
|
|
return;
|
|
}
|
|
cachedConfig.value() = fullConfig;
|
|
|
|
QJsonObject cdbChanges;
|
|
const Utils::FilePath includeDir = CppEditor::ClangdSettings(d->settings).clangdIncludePath();
|
|
CppEditor::CompilerOptionsBuilder optionsBuilder = clangOptionsBuilder(
|
|
*projectPart, warningsConfigForProject(project()), includeDir,
|
|
ProjectExplorer::Macro::toMacros(config.editorDefines));
|
|
const CppEditor::ProjectFile file(filePath.toString(),
|
|
CppEditor::ProjectFile::classify(filePath.toString()));
|
|
const QJsonArray projectPartOptions = fullProjectPartOptions(
|
|
optionsBuilder, globalClangOptions());
|
|
addToCompilationDb(cdbChanges, *projectPart, CppEditor::getPchUsage(), projectPartOptions,
|
|
filePath.parentDir(), file, optionsBuilder.isClStyle());
|
|
QJsonObject settings;
|
|
addCompilationDb(settings, cdbChanges);
|
|
DidChangeConfigurationParams configChangeParams;
|
|
configChangeParams.setSettings(settings);
|
|
sendMessage(DidChangeConfigurationNotification(configChangeParams));
|
|
emit configChanged();
|
|
}
|
|
|
|
void ClangdClient::switchIssuePaneEntries(const Utils::FilePath &filePath)
|
|
{
|
|
TaskHub::clearTasks(Constants::TASK_CATEGORY_DIAGNOSTICS);
|
|
const Tasks tasks = d->issuePaneEntries.value(filePath);
|
|
for (const Task &t : tasks)
|
|
TaskHub::addTask(t);
|
|
}
|
|
|
|
void ClangdClient::addTask(const ProjectExplorer::Task &task)
|
|
{
|
|
d->issuePaneEntries[task.file] << task;
|
|
}
|
|
|
|
void ClangdClient::clearTasks(const Utils::FilePath &filePath)
|
|
{
|
|
d->issuePaneEntries[filePath].clear();
|
|
}
|
|
|
|
std::optional<bool> ClangdClient::hasVirtualFunctionAt(TextDocument *doc, int revision,
|
|
const Range &range)
|
|
{
|
|
const auto highlightingData = d->highlightingData.constFind(doc);
|
|
if (highlightingData == d->highlightingData.constEnd()
|
|
|| highlightingData->virtualRanges.second != revision) {
|
|
return {};
|
|
}
|
|
const auto matcher = [range](const Range &r) { return range.overlaps(r); };
|
|
return Utils::contains(highlightingData->virtualRanges.first, matcher);
|
|
}
|
|
|
|
MessageId ClangdClient::getAndHandleAst(const TextDocOrFile &doc, const AstHandler &astHandler,
|
|
AstCallbackMode callbackMode, const Range &range)
|
|
{
|
|
return d->getAndHandleAst(doc, astHandler, callbackMode, range);
|
|
}
|
|
|
|
MessageId ClangdClient::requestSymbolInfo(const Utils::FilePath &filePath, const Position &position,
|
|
const SymbolInfoHandler &handler)
|
|
{
|
|
const TextDocumentIdentifier docId(DocumentUri::fromFilePath(filePath));
|
|
const TextDocumentPositionParams params(docId, position);
|
|
SymbolInfoRequest symReq(params);
|
|
symReq.setResponseCallback([handler, reqId = symReq.id()]
|
|
(const SymbolInfoRequest::Response &response) {
|
|
const auto result = response.result();
|
|
if (!result) {
|
|
handler({}, {}, reqId);
|
|
return;
|
|
}
|
|
|
|
// According to the documentation, we should receive a single
|
|
// object here, but it's a list. No idea what it means if there's
|
|
// more than one entry. We choose the first one.
|
|
const auto list = std::get_if<QList<SymbolDetails>>(&result.value());
|
|
if (!list || list->isEmpty()) {
|
|
handler({}, {}, reqId);
|
|
return;
|
|
}
|
|
|
|
const SymbolDetails &sd = list->first();
|
|
handler(sd.name(), sd.containerName(), reqId);
|
|
});
|
|
sendMessage(symReq);
|
|
return symReq.id();
|
|
}
|
|
|
|
|
|
void ClangdClient::followSymbol(TextDocument *document,
|
|
const QTextCursor &cursor,
|
|
CppEditor::CppEditorWidget *editorWidget,
|
|
const Utils::LinkHandler &callback,
|
|
bool resolveTarget,
|
|
FollowTo followTo,
|
|
bool openInSplit
|
|
)
|
|
{
|
|
QTC_ASSERT(documentOpen(document), openDocument(document));
|
|
|
|
delete d->followSymbol;
|
|
d->followSymbol = nullptr;
|
|
|
|
const QTextCursor adjustedCursor = d->adjustedCursor(cursor, document);
|
|
if (followTo == FollowTo::SymbolDef && !resolveTarget) {
|
|
symbolSupport().findLinkAt(document, adjustedCursor, callback, false);
|
|
return;
|
|
}
|
|
|
|
qCDebug(clangdLog) << "follow symbol requested" << document->filePath()
|
|
<< adjustedCursor.blockNumber() << adjustedCursor.positionInBlock();
|
|
d->followSymbol = new ClangdFollowSymbol(this, adjustedCursor, editorWidget, document, callback,
|
|
followTo, openInSplit);
|
|
connect(d->followSymbol, &ClangdFollowSymbol::done, this, [this] {
|
|
d->followSymbol->deleteLater();
|
|
d->followSymbol = nullptr;
|
|
});
|
|
}
|
|
|
|
void ClangdClient::switchDeclDef(TextDocument *document, const QTextCursor &cursor,
|
|
CppEditor::CppEditorWidget *editorWidget,
|
|
const Utils::LinkHandler &callback)
|
|
{
|
|
QTC_ASSERT(documentOpen(document), openDocument(document));
|
|
|
|
qCDebug(clangdLog) << "switch decl/dev requested" << document->filePath()
|
|
<< cursor.blockNumber() << cursor.positionInBlock();
|
|
if (d->switchDeclDef)
|
|
delete d->switchDeclDef;
|
|
d->switchDeclDef = new ClangdSwitchDeclDef(this, document, cursor, editorWidget, callback);
|
|
connect(d->switchDeclDef, &ClangdSwitchDeclDef::done, this, [this] {
|
|
d->switchDeclDef->deleteLater();
|
|
d->switchDeclDef = nullptr;
|
|
});
|
|
}
|
|
|
|
void ClangdClient::switchHeaderSource(const Utils::FilePath &filePath, bool inNextSplit)
|
|
{
|
|
class SwitchSourceHeaderRequest : public Request<QJsonValue, std::nullptr_t, TextDocumentIdentifier>
|
|
{
|
|
public:
|
|
using Request::Request;
|
|
explicit SwitchSourceHeaderRequest(const Utils::FilePath &filePath)
|
|
: Request("textDocument/switchSourceHeader",
|
|
TextDocumentIdentifier(DocumentUri::fromFilePath(filePath))) {}
|
|
};
|
|
SwitchSourceHeaderRequest req(filePath);
|
|
req.setResponseCallback([inNextSplit](const SwitchSourceHeaderRequest::Response &response) {
|
|
if (const std::optional<QJsonValue> result = response.result()) {
|
|
const DocumentUri uri = DocumentUri::fromProtocol(result->toString());
|
|
const Utils::FilePath filePath = uri.toFilePath();
|
|
if (!filePath.isEmpty())
|
|
CppEditor::openEditor(filePath, inNextSplit);
|
|
}
|
|
});
|
|
sendMessage(req);
|
|
}
|
|
|
|
void ClangdClient::findLocalUsages(TextDocument *document, const QTextCursor &cursor,
|
|
CppEditor::RenameCallback &&callback)
|
|
{
|
|
QTC_ASSERT(documentOpen(document), openDocument(document));
|
|
|
|
qCDebug(clangdLog) << "local references requested" << document->filePath()
|
|
<< (cursor.blockNumber() + 1) << (cursor.positionInBlock() + 1);
|
|
|
|
if (d->findLocalRefs) {
|
|
d->findLocalRefs->disconnect(this);
|
|
d->findLocalRefs->deleteLater();
|
|
d->findLocalRefs = nullptr;
|
|
}
|
|
|
|
const QString searchTerm = d->searchTermFromCursor(cursor);
|
|
if (searchTerm.isEmpty()) {
|
|
callback({}, {}, document->document()->revision());
|
|
return;
|
|
}
|
|
|
|
d->findLocalRefs = new ClangdFindLocalReferences(this, document, cursor, callback);
|
|
connect(d->findLocalRefs, &ClangdFindLocalReferences::done, this, [this] {
|
|
d->findLocalRefs->deleteLater();
|
|
d->findLocalRefs = nullptr;
|
|
});
|
|
}
|
|
|
|
void ClangdClient::gatherHelpItemForTooltip(const HoverRequest::Response &hoverResponse,
|
|
const DocumentUri &uri)
|
|
{
|
|
if (const std::optional<HoverResult> result = hoverResponse.result()) {
|
|
if (auto hover = std::get_if<Hover>(&(*result))) {
|
|
const HoverContent content = hover->content();
|
|
const MarkupContent *const markup = std::get_if<MarkupContent>(&content);
|
|
if (markup) {
|
|
const QString markupString = markup->content();
|
|
|
|
// Macros aren't locatable via the AST, so parse the formatted string.
|
|
static const QString magicMacroPrefix = "### macro `";
|
|
if (markupString.startsWith(magicMacroPrefix)) {
|
|
const int nameStart = magicMacroPrefix.length();
|
|
const int closingQuoteIndex = markupString.indexOf('`', nameStart);
|
|
if (closingQuoteIndex != -1) {
|
|
const QString macroName = markupString.mid(nameStart,
|
|
closingQuoteIndex - nameStart);
|
|
d->setHelpItemForTooltip(hoverResponse.id(),
|
|
uri,
|
|
macroName,
|
|
HelpItem::Macro);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Is it the file path for an include directive?
|
|
QString cleanString = markupString;
|
|
cleanString.remove('`');
|
|
const QStringList lines = cleanString.trimmed().split('\n');
|
|
if (!lines.isEmpty()) {
|
|
const auto filePath = Utils::FilePath::fromUserInput(lines.last().simplified());
|
|
if (filePath.exists()) {
|
|
d->setHelpItemForTooltip(hoverResponse.id(),
|
|
uri,
|
|
filePath.fileName(),
|
|
HelpItem::Brief);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const TextDocument * const doc = documentForFilePath(uri.toFilePath());
|
|
QTC_ASSERT(doc, return);
|
|
const auto astHandler = [this, uri, hoverResponse](const ClangdAstNode &ast, const MessageId &) {
|
|
const MessageId id = hoverResponse.id();
|
|
Range range;
|
|
if (const std::optional<HoverResult> result = hoverResponse.result()) {
|
|
if (auto hover = std::get_if<Hover>(&(*result)))
|
|
range = hover->range().value_or(Range());
|
|
}
|
|
const ClangdAstPath path = getAstPath(ast, range);
|
|
if (path.isEmpty()) {
|
|
d->setHelpItemForTooltip(id, uri);
|
|
return;
|
|
}
|
|
ClangdAstNode node = path.last();
|
|
if (node.role() == "expression" && node.kind() == "ImplicitCast") {
|
|
const std::optional<QList<ClangdAstNode>> children = node.children();
|
|
if (children && !children->isEmpty())
|
|
node = children->first();
|
|
}
|
|
while (node.kind() == "Qualified") {
|
|
const std::optional<QList<ClangdAstNode>> children = node.children();
|
|
if (children && !children->isEmpty())
|
|
node = children->first();
|
|
}
|
|
if (clangdLogAst().isDebugEnabled())
|
|
node.print(0);
|
|
|
|
QString type = node.type();
|
|
const auto stripTemplatePartOffType = [&type] {
|
|
const int angleBracketIndex = type.indexOf('<');
|
|
if (angleBracketIndex != -1)
|
|
type = type.left(angleBracketIndex);
|
|
};
|
|
|
|
if (gatherMemberFunctionOverrideHelpItemForTooltip(id, uri, path))
|
|
return;
|
|
|
|
const bool isMemberFunction = node.role() == "expression" && node.kind() == "Member"
|
|
&& (node.arcanaContains("member function") || type.contains('('));
|
|
const bool isFunction = node.role() == "expression" && node.kind() == "DeclRef"
|
|
&& type.contains('(');
|
|
if (isMemberFunction || isFunction) {
|
|
const auto symbolInfoHandler = [this, id, uri, type, isFunction](const QString &name,
|
|
const QString &prefix,
|
|
const MessageId &) {
|
|
qCDebug(clangdLog) << "handling symbol info reply";
|
|
const QString fqn = prefix + name;
|
|
|
|
// Unfortunately, the arcana string contains the signature only for
|
|
// free functions, so we can't distinguish member function overloads.
|
|
// But since HtmlDocExtractor::getFunctionDescription() is always called
|
|
// with mainOverload = true, such information would get ignored anyway.
|
|
if (!fqn.isEmpty())
|
|
d->setHelpItemForTooltip(id,
|
|
uri,
|
|
fqn,
|
|
HelpItem::Function,
|
|
isFunction ? type : "()");
|
|
};
|
|
requestSymbolInfo(uri.toFilePath(), range.start(), symbolInfoHandler);
|
|
return;
|
|
}
|
|
if ((node.role() == "expression" && node.kind() == "DeclRef")
|
|
|| (node.role() == "declaration"
|
|
&& (node.kind() == "Var" || node.kind() == "ParmVar"
|
|
|| node.kind() == "Field"))) {
|
|
if (node.arcanaContains("EnumConstant")) {
|
|
d->setHelpItemForTooltip(id,
|
|
uri,
|
|
node.detail().value_or(QString()),
|
|
HelpItem::Enum,
|
|
type);
|
|
return;
|
|
}
|
|
stripTemplatePartOffType();
|
|
type.remove("&").remove("*").remove("const ").remove(" const")
|
|
.remove("volatile ").remove(" volatile");
|
|
type = type.simplified();
|
|
if (type != "int" && !type.contains(" int")
|
|
&& type != "char" && !type.contains(" char")
|
|
&& type != "double" && !type.contains(" double")
|
|
&& type != "float" && type != "bool") {
|
|
d->setHelpItemForTooltip(id,
|
|
uri,
|
|
type,
|
|
node.qdocCategoryForDeclaration(
|
|
HelpItem::ClassOrNamespace));
|
|
} else {
|
|
d->setHelpItemForTooltip(id, uri);
|
|
}
|
|
return;
|
|
}
|
|
if (node.isNamespace()) {
|
|
QString ns = node.detail().value_or(QString());
|
|
for (auto it = path.rbegin() + 1; it != path.rend(); ++it) {
|
|
if (it->isNamespace()) {
|
|
const QString name = it->detail().value_or(QString());
|
|
if (!name.isEmpty())
|
|
ns.prepend("::").prepend(name);
|
|
}
|
|
}
|
|
d->setHelpItemForTooltip(id, uri, ns, HelpItem::ClassOrNamespace);
|
|
return;
|
|
}
|
|
if (node.role() == "type") {
|
|
if (node.kind() == "Enum") {
|
|
d->setHelpItemForTooltip(id, uri, node.detail().value_or(QString()), HelpItem::Enum);
|
|
} else if (node.kind() == "Record" || node.kind() == "TemplateSpecialization") {
|
|
stripTemplatePartOffType();
|
|
d->setHelpItemForTooltip(id, uri, type, HelpItem::ClassOrNamespace);
|
|
} else if (node.kind() == "Typedef") {
|
|
d->setHelpItemForTooltip(id, uri, type, HelpItem::Typedef);
|
|
} else {
|
|
d->setHelpItemForTooltip(id, uri);
|
|
}
|
|
return;
|
|
}
|
|
if (node.role() == "expression" && node.kind() == "CXXConstruct") {
|
|
const QString name = node.detail().value_or(QString());
|
|
if (!name.isEmpty())
|
|
type = name;
|
|
d->setHelpItemForTooltip(id, uri, type, HelpItem::ClassOrNamespace);
|
|
}
|
|
if (node.role() == "specifier" && node.kind() == "NamespaceAlias") {
|
|
d->setHelpItemForTooltip(id,
|
|
uri,
|
|
node.detail().value_or(QString()).chopped(2),
|
|
HelpItem::ClassOrNamespace);
|
|
return;
|
|
}
|
|
d->setHelpItemForTooltip(id, uri);
|
|
};
|
|
d->getAndHandleAst(doc, astHandler, AstCallbackMode::SyncIfPossible);
|
|
}
|
|
|
|
bool ClangdClient::gatherMemberFunctionOverrideHelpItemForTooltip(
|
|
const LanguageServerProtocol::MessageId &token,
|
|
const DocumentUri &uri,
|
|
const QList<ClangdAstNode> &path)
|
|
{
|
|
// Heuristic: If we encounter a member function re-declaration, continue under the
|
|
// assumption that the base class holds the documentation.
|
|
if (path.length() < 3 || path.last().kind() != "FunctionProto")
|
|
return false;
|
|
const ClangdAstNode &methodNode = path.at(path.length() - 2);
|
|
if (methodNode.kind() != "CXXMethod" || !methodNode.detail())
|
|
return false;
|
|
bool hasOverride = false;
|
|
for (const ClangdAstNode &methodNodeChild
|
|
: methodNode.children().value_or(QList<ClangdAstNode>())) {
|
|
if (methodNodeChild.kind() == "Override") {
|
|
hasOverride = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!hasOverride)
|
|
return false;
|
|
const ClangdAstNode &classNode = path.at(path.length() - 3);
|
|
if (classNode.kind() != "CXXRecord")
|
|
return false;
|
|
const ClangdAstNode baseNode = classNode.children()->first();
|
|
if (baseNode.role() != "base")
|
|
return false;
|
|
const auto baseNodeChildren = baseNode.children();
|
|
if (!baseNodeChildren || baseNodeChildren->isEmpty())
|
|
return false;
|
|
const ClangdAstNode baseTypeNode = baseNodeChildren->first();
|
|
if (baseTypeNode.role() != "type")
|
|
return false;
|
|
const auto baseTypeNodeChildren = baseTypeNode.children();
|
|
if (!baseTypeNodeChildren || baseTypeNodeChildren->isEmpty())
|
|
return false;
|
|
const ClangdAstNode baseClassNode = baseTypeNodeChildren->first();
|
|
if (!baseClassNode.detail())
|
|
return false;
|
|
d->setHelpItemForTooltip(token,
|
|
uri,
|
|
*baseClassNode.detail() + "::" + *methodNode.detail(),
|
|
HelpItem::Function,
|
|
"()");
|
|
return true;
|
|
}
|
|
|
|
void ClangdClient::setVirtualRanges(const Utils::FilePath &filePath, const QList<Range> &ranges,
|
|
int revision)
|
|
{
|
|
TextDocument * const doc = documentForFilePath(filePath);
|
|
if (doc && doc->document()->revision() == revision)
|
|
d->highlightingData[doc].virtualRanges = {ranges, revision};
|
|
}
|
|
|
|
CppEditor::CppEditorWidget *ClangdClient::Private::widgetFromDocument(const TextDocument *doc)
|
|
{
|
|
IEditor * const editor = Utils::findOrDefault(EditorManager::visibleEditors(),
|
|
[doc](const IEditor *editor) { return editor->document() == doc; });
|
|
return qobject_cast<CppEditor::CppEditorWidget *>(TextEditorWidget::fromEditor(editor));
|
|
}
|
|
|
|
QString ClangdClient::Private::searchTermFromCursor(const QTextCursor &cursor) const
|
|
{
|
|
QTextCursor termCursor(cursor);
|
|
termCursor.select(QTextCursor::WordUnderCursor);
|
|
return termCursor.selectedText();
|
|
}
|
|
|
|
// https://github.com/clangd/clangd/issues/936
|
|
QTextCursor ClangdClient::Private::adjustedCursor(const QTextCursor &cursor,
|
|
const TextDocument *doc)
|
|
{
|
|
CppEditor::CppEditorWidget * const widget = widgetFromDocument(doc);
|
|
if (!widget)
|
|
return cursor;
|
|
const Document::Ptr cppDoc = widget->semanticInfo().doc;
|
|
if (!cppDoc)
|
|
return cursor;
|
|
const QList<AST *> builtinAstPath = ASTPath(cppDoc)(cursor);
|
|
if (builtinAstPath.isEmpty())
|
|
return cursor;
|
|
const TranslationUnit * const tu = cppDoc->translationUnit();
|
|
const auto posForToken = [doc, tu](int tok) {
|
|
return tu->getTokenPositionInDocument(tok, doc->document());
|
|
};
|
|
const auto endPosForToken = [doc, tu](int tok) {
|
|
return tu->getTokenEndPositionInDocument(tok, doc->document());
|
|
};
|
|
const auto leftMovedCursor = [cursor] {
|
|
QTextCursor c = cursor;
|
|
c.setPosition(cursor.position() - 1);
|
|
return c;
|
|
};
|
|
|
|
// enum E { v1|, v2 };
|
|
if (const EnumeratorAST * const enumAst = builtinAstPath.last()->asEnumerator()) {
|
|
if (endPosForToken(enumAst->identifier_token) == cursor.position())
|
|
return leftMovedCursor();
|
|
return cursor;
|
|
}
|
|
|
|
for (auto it = builtinAstPath.rbegin(); it != builtinAstPath.rend(); ++it) {
|
|
|
|
// s|.x or s|->x
|
|
if (const MemberAccessAST * const memberAccess = (*it)->asMemberAccess()) {
|
|
switch (tu->tokenAt(memberAccess->access_token).kind()) {
|
|
case T_DOT:
|
|
break;
|
|
case T_ARROW: {
|
|
const std::optional<ClangdAstNode> clangdAst = astCache.get(doc);
|
|
if (!clangdAst)
|
|
return cursor;
|
|
const ClangdAstPath clangdAstPath = getAstPath(*clangdAst, Range(cursor));
|
|
for (auto it = clangdAstPath.rbegin(); it != clangdAstPath.rend(); ++it) {
|
|
if (it->detailIs("operator->") && it->arcanaContains("CXXMethod"))
|
|
return cursor;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
return cursor;
|
|
}
|
|
if (posForToken(memberAccess->access_token) != cursor.position())
|
|
return cursor;
|
|
return leftMovedCursor();
|
|
}
|
|
|
|
// f(arg1|, arg2)
|
|
if (const CallAST *const callAst = (*it)->asCall()) {
|
|
const int tok = builtinAstPath.last()->lastToken();
|
|
if (posForToken(tok) != cursor.position())
|
|
return cursor;
|
|
if (tok == callAst->rparen_token)
|
|
return leftMovedCursor();
|
|
if (tu->tokenKind(tok) != T_COMMA)
|
|
return cursor;
|
|
|
|
// Guard against edge case of overloaded comma operator.
|
|
for (auto list = callAst->expression_list; list; list = list->next) {
|
|
if (list->value->lastToken() == tok)
|
|
return leftMovedCursor();
|
|
}
|
|
return cursor;
|
|
}
|
|
|
|
// ~My|Class
|
|
if (const DestructorNameAST * const destrAst = (*it)->asDestructorName()) {
|
|
QTextCursor c = cursor;
|
|
c.setPosition(posForToken(destrAst->tilde_token));
|
|
return c;
|
|
}
|
|
|
|
// QVector<QString|>
|
|
if (const TemplateIdAST * const templAst = (*it)->asTemplateId()) {
|
|
if (posForToken(templAst->greater_token) == cursor.position())
|
|
return leftMovedCursor();
|
|
return cursor;
|
|
}
|
|
}
|
|
return cursor;
|
|
}
|
|
|
|
void ClangdClient::Private::setHelpItemForTooltip(const MessageId &token,
|
|
const DocumentUri &uri,
|
|
const QString &fqn,
|
|
HelpItem::Category category,
|
|
const QString &type)
|
|
{
|
|
QStringList helpIds;
|
|
QString mark;
|
|
if (!fqn.isEmpty()) {
|
|
helpIds << fqn;
|
|
int sepSearchStart = 0;
|
|
while (true) {
|
|
sepSearchStart = fqn.indexOf("::", sepSearchStart);
|
|
if (sepSearchStart == -1)
|
|
break;
|
|
sepSearchStart += 2;
|
|
helpIds << fqn.mid(sepSearchStart);
|
|
}
|
|
mark = helpIds.last();
|
|
if (category == HelpItem::Function)
|
|
mark += type.mid(type.indexOf('('));
|
|
}
|
|
if (category == HelpItem::Enum && !type.isEmpty())
|
|
mark = type;
|
|
|
|
const HelpItem helpItem(helpIds, uri.toFilePath(), mark, category);
|
|
if (isTesting)
|
|
emit q->helpItemGathered(helpItem);
|
|
else
|
|
q->hoverHandler()->setHelpItem(token, helpItem);
|
|
}
|
|
|
|
// Unfortunately, clangd ignores almost everything except symbols when sending
|
|
// semantic token info, so we need to consult the AST for additional information.
|
|
// In particular, we inspect the following constructs:
|
|
// - Raw string literals, because our built-in lexer does not parse them properly.
|
|
// While we're at it, we also handle other types of literals.
|
|
// - Ternary expressions (for the matching of "?" and ":").
|
|
// - Template declarations and instantiations (for the matching of "<" and ">").
|
|
// - Function declarations, to find out whether a declaration is also a definition.
|
|
// - Function arguments, to find out whether they correspond to output parameters.
|
|
// - We consider most other tokens to be simple enough to be handled by the built-in code model.
|
|
// Sometimes we have no choice, as for #include directives, which appear neither
|
|
// in the semantic tokens nor in the AST.
|
|
void ClangdClient::Private::handleSemanticTokens(TextDocument *doc,
|
|
const QList<ExpandedSemanticToken> &tokens,
|
|
int version, bool force)
|
|
{
|
|
SubtaskTimer t(highlightingTimer);
|
|
qCInfo(clangdLogHighlight) << "handling LSP tokens" << doc->filePath()
|
|
<< version << tokens.size();
|
|
if (version != q->documentVersion(doc->filePath())) {
|
|
qCInfo(clangdLogHighlight) << "LSP tokens outdated; aborting highlighting procedure"
|
|
<< version << q->documentVersion(doc->filePath());
|
|
return;
|
|
}
|
|
force = force || isTesting;
|
|
const auto data = highlightingData.find(doc);
|
|
if (data != highlightingData.end()) {
|
|
if (!force && data->previousTokens.first == tokens
|
|
&& data->previousTokens.second == version) {
|
|
qCInfo(clangdLogHighlight) << "tokens and version same as last time; nothing to do";
|
|
return;
|
|
}
|
|
data->previousTokens.first = tokens;
|
|
data->previousTokens.second = version;
|
|
} else {
|
|
highlightingData.insert(doc, {{tokens, version}, {}});
|
|
}
|
|
for (const ExpandedSemanticToken &t : tokens)
|
|
qCDebug(clangdLogHighlight()) << '\t' << t.line << t.column << t.length << t.type
|
|
<< t.modifiers;
|
|
|
|
const auto astHandler = [this, tokens, doc, version](const ClangdAstNode &ast, const MessageId &) {
|
|
FinalizingSubtaskTimer t(highlightingTimer);
|
|
if (!q->documentOpen(doc))
|
|
return;
|
|
if (version != q->documentVersion(doc->filePath())) {
|
|
qCInfo(clangdLogHighlight) << "AST not up to date; aborting highlighting procedure"
|
|
<< version << q->documentVersion(doc->filePath());
|
|
return;
|
|
}
|
|
if (clangdLogAst().isDebugEnabled())
|
|
ast.print();
|
|
|
|
const auto runner = [tokens, filePath = doc->filePath(),
|
|
text = doc->document()->toPlainText(), ast,
|
|
doc = QPointer(doc), rev = doc->document()->revision(),
|
|
clangdVersion = q->versionNumber(),
|
|
this] {
|
|
try {
|
|
return Utils::runAsync(doSemanticHighlighting, filePath, tokens, text, ast, doc,
|
|
rev, clangdVersion, highlightingTimer);
|
|
} catch (const std::exception &e) {
|
|
qWarning() << "caught" << e.what() << "in main highlighting thread";
|
|
return QFuture<HighlightingResult>();
|
|
}
|
|
};
|
|
|
|
if (isTesting) {
|
|
const auto watcher = new QFutureWatcher<HighlightingResult>(q);
|
|
connect(watcher, &QFutureWatcher<HighlightingResult>::finished,
|
|
q, [this, watcher, fp = doc->filePath()] {
|
|
emit q->highlightingResultsReady(watcher->future().results(), fp);
|
|
watcher->deleteLater();
|
|
});
|
|
watcher->setFuture(runner());
|
|
return;
|
|
}
|
|
|
|
auto &data = highlightingData[doc];
|
|
if (!data.highlighter)
|
|
data.highlighter = new CppEditor::SemanticHighlighter(doc);
|
|
else
|
|
data.highlighter->updateFormatMapFromFontSettings();
|
|
data.highlighter->setHighlightingRunner(runner);
|
|
data.highlighter->run();
|
|
};
|
|
getAndHandleAst(doc, astHandler, AstCallbackMode::SyncIfPossible);
|
|
}
|
|
|
|
std::optional<QList<CodeAction> > ClangdDiagnostic::codeActions() const
|
|
{
|
|
auto actions = optionalArray<LanguageServerProtocol::CodeAction>(u"codeActions");
|
|
if (!actions)
|
|
return actions;
|
|
static const QStringList badCodeActions{
|
|
"remove constant to silence this warning", // QTCREATORBUG-18593
|
|
};
|
|
for (auto it = actions->begin(); it != actions->end();) {
|
|
if (badCodeActions.contains(it->title()))
|
|
it = actions->erase(it);
|
|
else
|
|
++it;
|
|
}
|
|
return actions;
|
|
}
|
|
|
|
QString ClangdDiagnostic::category() const
|
|
{
|
|
return typedValue<QString>(u"category");
|
|
}
|
|
|
|
MessageId ClangdClient::Private::getAndHandleAst(const TextDocOrFile &doc,
|
|
const AstHandler &astHandler,
|
|
AstCallbackMode callbackMode, const Range &range)
|
|
{
|
|
const auto textDocPtr = std::get_if<const TextDocument *>(&doc);
|
|
const TextDocument * const textDoc = textDocPtr ? *textDocPtr : nullptr;
|
|
const Utils::FilePath filePath = textDoc ? textDoc->filePath()
|
|
: std::get<Utils::FilePath>(doc);
|
|
|
|
// If the entire AST is requested and the document's AST is in the cache and it is up to date,
|
|
// call the handler.
|
|
const bool fullAstRequested = !range.isValid();
|
|
if (fullAstRequested) {
|
|
if (const auto ast = textDoc ? astCache.get(textDoc) : externalAstCache.get(filePath)) {
|
|
qCDebug(clangdLog) << "using AST from cache";
|
|
switch (callbackMode) {
|
|
case AstCallbackMode::SyncIfPossible:
|
|
astHandler(*ast, {});
|
|
break;
|
|
case AstCallbackMode::AlwaysAsync:
|
|
QMetaObject::invokeMethod(q, [ast, astHandler] { astHandler(*ast, {}); },
|
|
Qt::QueuedConnection);
|
|
break;
|
|
}
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Otherwise retrieve the AST from clangd.
|
|
const auto wrapperHandler = [this, filePath, guardedTextDoc = QPointer(textDoc), astHandler,
|
|
fullAstRequested, docRev = textDoc ? getRevision(textDoc) : -1,
|
|
fileRev = getRevision(filePath)](const ClangdAstNode &ast, const MessageId &reqId) {
|
|
qCDebug(clangdLog) << "retrieved AST from clangd";
|
|
if (fullAstRequested) {
|
|
if (guardedTextDoc) {
|
|
if (docRev == getRevision(guardedTextDoc))
|
|
astCache.insert(guardedTextDoc, ast);
|
|
} else if (fileRev == getRevision(filePath) && !q->documentForFilePath(filePath)) {
|
|
externalAstCache.insert(filePath, ast);
|
|
}
|
|
}
|
|
astHandler(ast, reqId);
|
|
};
|
|
qCDebug(clangdLog) << "requesting AST for" << filePath;
|
|
return requestAst(q, filePath, range, wrapperHandler);
|
|
}
|
|
|
|
} // namespace Internal
|
|
} // namespace ClangCodeModel
|