// Copyright (C) 2020 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "diagnosticmanager.h" #include "client.h" #include "languageclientmanager.h" #include "languageclienttr.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace LanguageServerProtocol; using namespace ProjectExplorer; using namespace TextEditor; using namespace Utils; namespace LanguageClient { class TextMark : public TextEditor::TextMark { public: TextMark(TextDocument *doc, const Diagnostic &diag, const Client *client) : TextEditor::TextMark(doc, diag.range().start().line() + 1, {client->name(), client->id()}) { setLineAnnotation(diag.message()); setToolTip(diag.message()); const bool isError = diag.severity().value_or(DiagnosticSeverity::Hint) == DiagnosticSeverity::Error; setColor(isError ? Theme::CodeModel_Error_TextMarkColor : Theme::CodeModel_Warning_TextMarkColor); setIcon(isError ? Icons::CODEMODEL_ERROR.icon() : Icons::CODEMODEL_WARNING.icon()); } }; struct VersionedDiagnostics { std::optional version; QList diagnostics; }; class Marks { public: ~Marks() { qDeleteAll(marks); } bool enabled = true; QList marks; }; class DiagnosticManager::DiagnosticManagerPrivate { public: DiagnosticManagerPrivate(Client *client) : m_client(client) {} void showTasks(TextDocument *doc) { if (!doc || m_client != LanguageClientManager::clientForDocument(doc)) return; TaskHub::clearTasks(m_taskCategory); const Tasks tasks = m_issuePaneEntries.value(doc->filePath()); for (const Task &t : tasks) TaskHub::addTask(t); } QMap m_diagnostics; QMap m_marks; Client *m_client; QHash m_issuePaneEntries; Id m_extraSelectionsId = TextEditorWidget::CodeWarningsSelection; bool m_forceCreateTasks = true; Id m_taskCategory = Constants::TASK_CATEGORY_DIAGNOSTICS; }; DiagnosticManager::DiagnosticManager(Client *client) : d(std::make_unique(client)) { auto updateCurrentEditor = [this](Core::IEditor *editor) { if (editor) d->showTasks(qobject_cast(editor->document())); }; connect(Core::EditorManager::instance(), &Core::EditorManager::currentEditorChanged, this, updateCurrentEditor); } DiagnosticManager::~DiagnosticManager() { clearDiagnostics(); } void DiagnosticManager::setDiagnostics(const FilePath &filePath, const QList &diagnostics, const std::optional &version) { hideDiagnostics(filePath); d->m_diagnostics[filePath] = {version, filteredDiagnostics(diagnostics)}; } void DiagnosticManager::hideDiagnostics(const Utils::FilePath &filePath) { if (auto doc = TextDocument::textDocumentForFilePath(filePath)) { if (doc == TextDocument::currentTextDocument()) TaskHub::clearTasks(d->m_taskCategory); for (BaseTextEditor *editor : BaseTextEditor::textEditorsForDocument(doc)) editor->editorWidget()->setExtraSelections(d->m_extraSelectionsId, {}); } d->m_marks.remove(filePath); d->m_issuePaneEntries.remove(filePath); } QList DiagnosticManager::filteredDiagnostics(const QList &diagnostics) const { return diagnostics; } void DiagnosticManager::disableDiagnostics(TextEditor::TextDocument *document) { Marks &marks = d->m_marks[document->filePath()]; if (!marks.enabled) return; for (TextEditor::TextMark *mark : marks.marks) mark->setColor(Utils::Theme::Color::IconsDisabledColor); marks.enabled = false; } void DiagnosticManager::showDiagnostics(const FilePath &filePath, int version) { d->m_issuePaneEntries.remove(filePath); if (TextDocument *doc = TextDocument::textDocumentForFilePath(filePath)) { QList extraSelections; const VersionedDiagnostics &versionedDiagnostics = d->m_diagnostics.value(filePath); if (versionedDiagnostics.version.value_or(version) == version && !versionedDiagnostics.diagnostics.isEmpty()) { Marks &marks = d->m_marks[filePath]; const bool isProjectFile = d->m_client->fileBelongsToProject(filePath); for (const Diagnostic &diagnostic : versionedDiagnostics.diagnostics) { const QTextEdit::ExtraSelection selection = createDiagnosticSelection(diagnostic, doc->document()); if (!selection.cursor.isNull()) extraSelections << selection; if (TextEditor::TextMark *mark = createTextMark(doc, diagnostic, isProjectFile)) marks.marks.append(mark); if (std::optional task = createTask(doc, diagnostic, isProjectFile)) d->m_issuePaneEntries[filePath].append(*task); } if (!marks.marks.isEmpty()) emit textMarkCreated(filePath); } for (BaseTextEditor *editor : BaseTextEditor::textEditorsForDocument(doc)) editor->editorWidget()->setExtraSelections(d->m_extraSelectionsId, extraSelections); if (doc == TextDocument::currentTextDocument()) d->showTasks(doc); } } Client *DiagnosticManager::client() const { return d->m_client; } TextEditor::TextMark *DiagnosticManager::createTextMark(TextDocument *doc, const Diagnostic &diagnostic, bool /*isProjectFile*/) const { static const QIcon icon = Icon::fromTheme("edit-copy"); static const QString tooltip = Tr::tr("Copy to Clipboard"); auto mark = new TextMark(doc, diagnostic, d->m_client); mark->setActionsProvider([text = diagnostic.message()] { QAction *action = new QAction(); action->setIcon(icon); action->setToolTip(tooltip); QObject::connect(action, &QAction::triggered, [text] { setClipboardAndSelection(text); }); return QList{action}; }); return mark; } std::optional DiagnosticManager::createTask( TextDocument *doc, const LanguageServerProtocol::Diagnostic &diagnostic, bool isProjectFile) const { if (!isProjectFile && !d->m_forceCreateTasks) return {}; Task::TaskType taskType = Task::TaskType::Unknown; QIcon icon; if (const std::optional severity = diagnostic.severity()) { switch (*severity) { case DiagnosticSeverity::Error: taskType = Task::TaskType::Error; icon = Icons::CODEMODEL_ERROR.icon(); break; case DiagnosticSeverity::Warning: taskType = Task::TaskType::Warning; icon = Icons::CODEMODEL_WARNING.icon(); break; default: break; } } return Task(taskType, taskText(diagnostic), doc->filePath(), diagnostic.range().start().line(), d->m_taskCategory, icon, Task::NoOptions); } QString DiagnosticManager::taskText(const LanguageServerProtocol::Diagnostic &diagnostic) const { return diagnostic.message(); } void DiagnosticManager::setTaskCategory(const Utils::Id &taskCategory) { d->m_taskCategory = taskCategory; } void DiagnosticManager::setForceCreateTasks(bool forceCreateTasks) { d->m_forceCreateTasks = forceCreateTasks; } QTextEdit::ExtraSelection DiagnosticManager::createDiagnosticSelection( const LanguageServerProtocol::Diagnostic &diagnostic, QTextDocument *textDocument) const { QTextCursor cursor(textDocument); cursor.setPosition(diagnostic.range().start().toPositionInDocument(textDocument)); cursor.setPosition(diagnostic.range().end().toPositionInDocument(textDocument), QTextCursor::KeepAnchor); const FontSettings &fontSettings = TextEditorSettings::fontSettings(); const DiagnosticSeverity severity = diagnostic.severity().value_or(DiagnosticSeverity::Warning); const TextStyle style = severity == DiagnosticSeverity::Error ? C_ERROR : C_WARNING; return QTextEdit::ExtraSelection{cursor, fontSettings.toTextCharFormat(style)}; } void DiagnosticManager::setExtraSelectionsId(const Utils::Id &extraSelectionsId) { // this function should be called before any diagnostics are handled QTC_CHECK(d->m_diagnostics.isEmpty()); d->m_extraSelectionsId = extraSelectionsId; } void DiagnosticManager::forAllMarks(std::function func) { for (const Marks &marks : std::as_const(d->m_marks)) { for (TextEditor::TextMark *mark : marks.marks) func(mark); } } void DiagnosticManager::clearDiagnostics() { for (const Utils::FilePath &path : d->m_diagnostics.keys()) hideDiagnostics(path); d->m_diagnostics.clear(); QTC_ASSERT(d->m_marks.isEmpty(), d->m_marks.clear()); } QList DiagnosticManager::diagnosticsAt(const FilePath &filePath, const QTextCursor &cursor) const { const int documentRevision = d->m_client->documentVersion(filePath); auto it = d->m_diagnostics.find(filePath); if (it == d->m_diagnostics.end()) return {}; if (documentRevision != it->version.value_or(documentRevision)) return {}; return Utils::filtered(it->diagnostics, [range = Range(cursor)](const Diagnostic &diagnostic) { return diagnostic.range().overlaps(range); }); } bool DiagnosticManager::hasDiagnostic(const FilePath &filePath, const TextDocument *doc, const LanguageServerProtocol::Diagnostic &diag) const { if (!doc) return false; const auto it = d->m_diagnostics.find(filePath); if (it == d->m_diagnostics.end()) return {}; const int revision = d->m_client->documentVersion(filePath); if (revision != it->version.value_or(revision)) return false; return it->diagnostics.contains(diag); } bool DiagnosticManager::hasDiagnostics(const TextDocument *doc) const { const FilePath docPath = doc->filePath(); const auto it = d->m_diagnostics.find(docPath); if (it == d->m_diagnostics.end()) return {}; const int revision = d->m_client->documentVersion(docPath); if (revision != it->version.value_or(revision)) return false; return !it->diagnostics.isEmpty(); } } // namespace LanguageClient