// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "clangdsemantichighlighting.h"

#include "clangdast.h"
#include "clangdclient.h"
#include "clangdqpropertyhighlighter.h"
#include "clangmodelmanagersupport.h"
#include "tasktimers.h"

#include <cppeditor/semantichighlighter.h>
#include <languageclient/languageclientmanager.h>
#include <languageclient/semantichighlightsupport.h>
#include <languageserverprotocol/lsptypes.h>
#include <texteditor/blockrange.h>
#include <texteditor/textstyles.h>

#include <QtConcurrent>
#include <QTextDocument>

#include <new>

using namespace LanguageClient;
using namespace LanguageServerProtocol;
using namespace TextEditor;

namespace ClangCodeModel::Internal {
Q_LOGGING_CATEGORY(clangdLogHighlight, "qtc.clangcodemodel.clangd.highlight", QtWarningMsg);

// clangd reports also the #ifs, #elses and #endifs around the disabled code as disabled,
// and not even in a consistent manner. We don't want this, so we have to clean up here.
// But note that we require this behavior, as otherwise we would not be able to grey out
// e.g. empty lines after an #ifdef, due to the lack of symbols.
static QList<BlockRange> cleanupDisabledCode(HighlightingResults &results, const QTextDocument *doc,
                                             const QString &docContent)
{
    QList<BlockRange> ifdefedOutRanges;
    int rangeStartPos = -1;
    for (auto it = results.begin(); it != results.end();) {
        const bool wasIfdefedOut = rangeStartPos != -1;
        const bool isIfDefedOut = it->textStyles.mainStyle == C_DISABLED_CODE;
        if (!isIfDefedOut) {
            if (wasIfdefedOut) {
                const QTextBlock block = doc->findBlockByNumber(it->line - 1);
                ifdefedOutRanges << BlockRange(rangeStartPos, block.position());
                rangeStartPos = -1;
            }
            ++it;
            continue;
        }

        if (!wasIfdefedOut)
            rangeStartPos = doc->findBlockByNumber(it->line - 1).position();

        // Does the current line contain a potential "ifdefed-out switcher"?
        // If not, no state change is possible and we continue with the next line.
        const auto isPreprocessorControlStatement = [&] {
            const int pos = Utils::Text::positionInText(doc, it->line, it->column);
            const QStringView content = subViewLen(docContent, pos, it->length).trimmed();
            if (content.isEmpty() || content.first() != '#')
                return false;
            int offset = 1;
            while (offset < content.size() && content.at(offset).isSpace())
                ++offset;
            if (offset == content.size())
                return false;
            const QStringView ppDirective = content.mid(offset);
            return ppDirective.startsWith(QLatin1String("if"))
                    || ppDirective.startsWith(QLatin1String("elif"))
                    || ppDirective.startsWith(QLatin1String("else"))
                    || ppDirective.startsWith(QLatin1String("endif"));
        };
        if (!isPreprocessorControlStatement()) {
            ++it;
            continue;
        }

        if (!wasIfdefedOut) {
            // The #if or #else that starts disabled code should not be disabled.
            const QTextBlock nextBlock = doc->findBlockByNumber(it->line);
            rangeStartPos = nextBlock.isValid() ? nextBlock.position() : -1;
            it = results.erase(it);
            continue;
        }

        if (wasIfdefedOut && (it + 1 == results.end()
                || (it + 1)->textStyles.mainStyle != C_DISABLED_CODE
                || (it + 1)->line != it->line + 1)) {
            // The #else or #endif that ends disabled code should not be disabled.
            const QTextBlock block = doc->findBlockByNumber(it->line - 1);
            ifdefedOutRanges << BlockRange(rangeStartPos, block.position());
            rangeStartPos = -1;
            it = results.erase(it);
            continue;
        }
        ++it;
    }

    if (rangeStartPos != -1)
        ifdefedOutRanges << BlockRange(rangeStartPos, doc->characterCount());

    qCDebug(clangdLogHighlight) << "found" << ifdefedOutRanges.size() << "ifdefed-out ranges";
    if (clangdLogHighlight().isDebugEnabled()) {
        for (const BlockRange &r : std::as_const(ifdefedOutRanges))
            qCDebug(clangdLogHighlight) << r.first() << r.last();
    }

    return ifdefedOutRanges;
}

class ExtraHighlightingResultsCollector
{
public:
    ExtraHighlightingResultsCollector(QFutureInterface<HighlightingResult> &future,
                                      HighlightingResults &results,
                                      const Utils::FilePath &filePath, const ClangdAstNode &ast,
                                      const QTextDocument *doc, const QString &docContent,
                                      const QVersionNumber &clangdVersion);

    void collect();
private:
    static bool lessThan(const HighlightingResult &r1, const HighlightingResult &r2);
    static int onlyIndexOf(const QStringView &text, const QStringView &subString, int from = 0);
    int posForNodeStart(const ClangdAstNode &node) const;
    int posForNodeEnd(const ClangdAstNode &node) const;
    void insertResult(const HighlightingResult &result);
    void insertResult(const ClangdAstNode &node, TextStyle style);
    void insertAngleBracketInfo(int searchStart1, int searchEnd1, int searchStart2, int searchEnd2);
    void setResultPosFromRange(HighlightingResult &result, const Range &range);
    void collectFromNode(const ClangdAstNode &node);
    void visitNode(const ClangdAstNode&node);

    QFutureInterface<HighlightingResult> &m_future;
    HighlightingResults &m_results;
    const Utils::FilePath m_filePath;
    const ClangdAstNode &m_ast;
    const QTextDocument * const m_doc;
    const QString &m_docContent;
    const int m_clangdVersion;
    ClangdAstNode::FileStatus m_currentFileStatus = ClangdAstNode::FileStatus::Unknown;
};

void doSemanticHighlighting(
        QFutureInterface<HighlightingResult> &future,
        const Utils::FilePath &filePath,
        const QList<ExpandedSemanticToken> &tokens,
        const QString &docContents,
        const ClangdAstNode &ast,
        const QPointer<TextDocument> &textDocument,
        int docRevision,
        const QVersionNumber &clangdVersion,
        const TaskTimer &taskTimer)
{
    ThreadedSubtaskTimer t("highlighting", taskTimer);
    if (future.isCanceled()) {
        future.reportFinished();
        return;
    }

    const QTextDocument doc(docContents);
    const auto tokenRange = [&doc](const ExpandedSemanticToken &token) {
        const Position startPos(token.line - 1, token.column - 1);
        const Position endPos = startPos.withOffset(token.length, &doc);
        return Range(startPos, endPos);
    };
    const int clangdMajorVersion = clangdVersion.majorVersion();
    const auto isOutputParameter = [&ast, &tokenRange, clangdMajorVersion]
            (const ExpandedSemanticToken &token) {
        if (token.modifiers.contains(QLatin1String("usedAsMutableReference")))
            return true;
        if (token.modifiers.contains(QLatin1String("usedAsMutablePointer")))
            return true;
        if (clangdMajorVersion >= 16)
            return false;
        if (token.type != "variable" && token.type != "property" && token.type != "parameter")
            return false;
        const Range range = tokenRange(token);
        const ClangdAstPath path = getAstPath(ast, range);
        if (path.size() < 2)
            return false;
        if (token.type == "property"
                && (path.rbegin()->kind() == "MemberInitializer"
                    || path.rbegin()->kind() == "CXXConstruct")) {
            return false;
        }
        if (path.rbegin()->hasConstType())
            return false;
        for (auto it = path.rbegin() + 1; it != path.rend(); ++it) {
            if (it->kind() == "CXXConstruct" || it->kind() == "MemberInitializer")
                return true;

            if (it->kind() == "Call") {
                // The first child is e.g. a called lambda or an object on which
                // the call happens, and should not be highlighted as an output argument.
                // If the call is not fully resolved (as in templates), we don't
                // know whether the argument is passed as const or not.
                if (it->arcanaContains("dependent type"))
                    return false;
                const QList<ClangdAstNode> children = it->children().value_or(QList<ClangdAstNode>());
                return children.isEmpty()
                        || (children.first().range() != (it - 1)->range()
                        && children.first().kind() != "UnresolvedLookup");
            }

            // The token should get marked for e.g. lambdas, but not for assignment operators,
            // where the user sees that it's being written.
            if (it->kind() == "CXXOperatorCall") {
                const QList<ClangdAstNode> children = it->children().value_or(QList<ClangdAstNode>());

                // Child 1 is the call itself, Child 2 is the named entity on which the call happens
                // (a lambda or a class instance), after that follow the actual call arguments.
                if (children.size() < 2)
                    return false;

                // The call itself is never modifiable.
                if (children.first().range() == range)
                    return false;

                // The callable is never displayed as an output parameter.
                // TODO: A good argument can be made to display objects on which a non-const
                //       operator or function is called as output parameters.
                if (children.at(1).range().contains(range))
                    return false;

                QList<ClangdAstNode> firstChildTree{children.first()};
                while (!firstChildTree.isEmpty()) {
                    const ClangdAstNode n = firstChildTree.takeFirst();
                    const QString detail = n.detail().value_or(QString());
                    if (detail.startsWith("operator")) {
                        return !detail.contains('=')
                                && !detail.contains("++") && !detail.contains("--")
                                && !detail.contains("<<") && !detail.contains(">>")
                                && !detail.contains("*");
                    }
                    firstChildTree << n.children().value_or(QList<ClangdAstNode>());
                }
                return true;
            }

            if (it->kind() == "Lambda")
                return false;
            if (it->kind() == "BinaryOperator")
                return false;
            if (it->hasConstType())
                return false;

            if (it->kind() == "CXXMemberCall") {
                if (it == path.rbegin())
                    return false;
                const QList<ClangdAstNode> children = it->children().value_or(QList<ClangdAstNode>());
                QTC_ASSERT(!children.isEmpty(), return false);

                // The called object is never displayed as an output parameter.
                // TODO: A good argument can be made to display objects on which a non-const
                //       operator or function is called as output parameters.
                return (it - 1)->range() != children.first().range();
            }

            if (it->kind() == "Member" && it->arcanaContains("(")
                    && !it->arcanaContains("bound member function type")) {
                return false;
            }
        }
        return false;
    };

    const std::function<HighlightingResult(const ExpandedSemanticToken &)> toResult
            = [&](const ExpandedSemanticToken &token) {
        TextStyles styles;
        if (token.type == "variable") {
            if (token.modifiers.contains(QLatin1String("functionScope"))) {
                styles.mainStyle = C_LOCAL;
            } else if (token.modifiers.contains(QLatin1String("classScope"))) {
                styles.mainStyle = C_FIELD;
            } else if (token.modifiers.contains(QLatin1String("fileScope"))
                       || token.modifiers.contains(QLatin1String("globalScope"))) {
                styles.mainStyle = C_GLOBAL;
            }
        } else if (token.type == "function" || token.type == "method") {
            styles.mainStyle = token.modifiers.contains(QLatin1String("virtual"))
                    ? C_VIRTUAL_METHOD : C_FUNCTION;
            if (token.modifiers.contains("definition")) {
                styles.mixinStyles.push_back(C_FUNCTION_DEFINITION);
            } else if (clangdMajorVersion < 16 && ast.isValid()) {
                const ClangdAstPath path = getAstPath(ast, tokenRange(token));
                if (path.length() > 1) {
                    const ClangdAstNode declNode = path.at(path.length() - 2);
                    if ((declNode.kind() == "Function" || declNode.kind() == "CXXMethod")
                            && declNode.hasChildWithRole("statement")) {
                        styles.mixinStyles.push_back(C_FUNCTION_DEFINITION);
                    }
                }
            }
        } else if (token.type == "class") {
            styles.mainStyle = C_TYPE;
            if (token.modifiers.contains("constructorOrDestructor")) {
                styles.mainStyle = C_FUNCTION;
            } else if (clangdMajorVersion < 16 && ast.isValid()) {
                const ClangdAstPath path = getAstPath(ast, tokenRange(token));
                if (!path.isEmpty()) {
                    if (path.last().kind() == "CXXConstructor") {
                        if (!path.last().arcanaContains("implicit"))
                            styles.mainStyle = C_FUNCTION;
                    } else if (path.last().kind() == "Record" && path.length() > 1) {
                        const ClangdAstNode node = path.at(path.length() - 2);
                        if (node.kind() == "CXXDestructor" && !node.arcanaContains("implicit")) {
                            styles.mainStyle = C_FUNCTION;

                            // https://github.com/clangd/clangd/issues/872
                            if (node.role() == "declaration")
                                styles.mixinStyles.push_back(C_DECLARATION);
                        }
                    }
                }
            }
        } else if (token.type == "comment") { // "comment" means code disabled via the preprocessor
            styles.mainStyle = C_DISABLED_CODE;
        } else if (token.type == "namespace") {
            styles.mainStyle = C_NAMESPACE;
        } else if (token.type == "property") {
            styles.mainStyle = C_FIELD;
        } else if (token.type == "enum") {
            styles.mainStyle = C_TYPE;
        } else if (token.type == "enumMember") {
            styles.mainStyle = C_ENUMERATION;
        } else if (token.type == "parameter") {
            styles.mainStyle = C_PARAMETER;
        } else if (token.type == "macro") {
            styles.mainStyle = C_MACRO;
        } else if (token.type == "type" || token.type == "concept") {
            styles.mainStyle = C_TYPE;
        } else if (token.type == "modifier") {
            styles.mainStyle = C_KEYWORD;
        } else if (token.type == "typeParameter") {
            // clangd reports both type and non-type template parameters as type parameters,
            // but the latter can be distinguished by the readonly modifier.
            styles.mainStyle = token.modifiers.contains(QLatin1String("readonly"))
                    ? C_PARAMETER : C_TYPE;
        } else if (token.type == "operator") {
            const int pos = Utils::Text::positionInText(&doc, token.line, token.column);
            QTC_ASSERT(pos >= 0 || pos < docContents.size(), return HighlightingResult());
            const QChar firstChar = docContents.at(pos);
            if (firstChar.isLetter())
                styles.mainStyle = C_KEYWORD;
            else
                styles.mainStyle = C_PUNCTUATION;
            styles.mixinStyles.push_back(C_OPERATOR);
            if (token.modifiers.contains("userDefined"))
                styles.mixinStyles.push_back(C_OVERLOADED_OPERATOR);
            else if (token.modifiers.contains("declaration")) {
                styles.mixinStyles.push_back(C_OVERLOADED_OPERATOR);
                styles.mixinStyles.push_back(C_DECLARATION);
            }
            HighlightingResult result(token.line, token.column, token.length, styles);
            if (token.length == 1) {
                if (firstChar == '?')
                    result.kind = CppEditor::SemanticHighlighter::TernaryIf;
                else if (firstChar == ':')
                    result.kind = CppEditor::SemanticHighlighter::TernaryElse;
            }
            return result;
        } else if (token.type == "bracket") {
            styles.mainStyle = C_PUNCTUATION;
            HighlightingResult result(token.line, token.column, token.length, styles);
            const int pos = Utils::Text::positionInText(&doc, token.line, token.column);
            QTC_ASSERT(pos >= 0 || pos < docContents.size(), return HighlightingResult());
            const char symbol = docContents.at(pos).toLatin1();
            QTC_ASSERT(symbol == '<' || symbol == '>', return HighlightingResult());
            result.kind = symbol == '<'
                    ? CppEditor::SemanticHighlighter::AngleBracketOpen
                    : CppEditor::SemanticHighlighter::AngleBracketClose;
            return result;
        }
        if (token.modifiers.contains(QLatin1String("declaration")))
            styles.mixinStyles.push_back(C_DECLARATION);
        if (token.modifiers.contains(QLatin1String("static"))) {
            if (styles.mainStyle == C_FUNCTION) {
                styles.mainStyle = C_STATIC_MEMBER;
                styles.mixinStyles.push_back(C_FUNCTION);
            } else if (styles.mainStyle == C_FIELD) {
                styles.mainStyle = C_STATIC_MEMBER;
            }
        }
        if (isOutputParameter(token))
            styles.mixinStyles.push_back(C_OUTPUT_ARGUMENT);
        qCDebug(clangdLogHighlight) << "adding highlighting result"
                                    << token.line << token.column << token.length << int(styles.mainStyle);
        return HighlightingResult(token.line, token.column, token.length, styles);
    };

    const auto safeToResult = [&toResult](const ExpandedSemanticToken &token) {
        try {
            return toResult(token);
        } catch (const std::exception &e) {
            qWarning() << "caught" << e.what() << "in toResult()";
            return HighlightingResult();
        }
    };
    auto results = QtConcurrent::blockingMapped<HighlightingResults>(tokens, safeToResult);
    const QList<BlockRange> ifdefedOutBlocks = cleanupDisabledCode(results, &doc, docContents);
    ExtraHighlightingResultsCollector(future, results, filePath, ast, &doc, docContents,
                                      clangdVersion).collect();
    Utils::erase(results, [](const HighlightingResult &res) {
        // QTCREATORBUG-28639
        return res.textStyles.mainStyle == C_TEXT && res.textStyles.mixinStyles.empty();
    });
    if (!future.isCanceled()) {
        qCInfo(clangdLogHighlight) << "reporting" << results.size() << "highlighting results";
        QMetaObject::invokeMethod(textDocument, [textDocument, ifdefedOutBlocks, docRevision] {
            if (textDocument && textDocument->document()->revision() == docRevision)
                textDocument->setIfdefedOutBlocks(ifdefedOutBlocks);
        }, Qt::QueuedConnection);
        QList<Range> virtualRanges;
        for (const HighlightingResult &r : results) {
            if (r.textStyles.mainStyle != C_VIRTUAL_METHOD)
                continue;
            const Position startPos(r.line - 1, r.column - 1);
            virtualRanges << Range(startPos, startPos.withOffset(r.length, &doc));
        }
        QMetaObject::invokeMethod(LanguageClientManager::instance(),
                                  [filePath, virtualRanges, docRevision] {
            if (ClangdClient * const client = ClangModelManagerSupport::clientForFile(filePath))
                client->setVirtualRanges(filePath, virtualRanges, docRevision);
        }, Qt::QueuedConnection);
        future.reportResults(QVector<HighlightingResult>(results.cbegin(), results.cend()));
    }
    future.reportFinished();
}

ExtraHighlightingResultsCollector::ExtraHighlightingResultsCollector(
        QFutureInterface<HighlightingResult> &future, HighlightingResults &results,
        const Utils::FilePath &filePath, const ClangdAstNode &ast, const QTextDocument *doc,
        const QString &docContent, const QVersionNumber &clangdVersion)
    : m_future(future), m_results(results), m_filePath(filePath), m_ast(ast), m_doc(doc),
      m_docContent(docContent), m_clangdVersion(clangdVersion.majorVersion())
{
}

void ExtraHighlightingResultsCollector::collect()
{
    for (int i = 0; i < m_results.length(); ++i) {
        const HighlightingResult res = m_results.at(i);
        if (res.textStyles.mainStyle != TextEditor::C_MACRO || res.length != 10)
            continue;
        const int pos = Utils::Text::positionInText(m_doc, res.line, res.column);
        if (subViewLen(m_docContent, pos, 10) != QLatin1String("Q_PROPERTY"))
            continue;
        int endPos;
        if (i < m_results.length() - 1) {
            const HighlightingResult nextRes = m_results.at(i + 1);
            endPos = Utils::Text::positionInText(m_doc, nextRes.line, nextRes.column);
        } else {
            endPos = m_docContent.length();
        }
        const QString qPropertyString = m_docContent.mid(pos, endPos - pos);
        QPropertyHighlighter propHighlighter(m_doc, qPropertyString, pos);
        for (const HighlightingResult &newRes : propHighlighter.highlight())
            m_results.insert(++i, newRes);
    }

    if (!m_ast.isValid())
        return;
    QTC_ASSERT(m_clangdVersion < 17, return);
    visitNode(m_ast);
}

bool ExtraHighlightingResultsCollector::lessThan(const HighlightingResult &r1,
                                                 const HighlightingResult &r2)
{
    return r1.line < r2.line || (r1.line == r2.line && r1.column < r2.column)
            || (r1.line == r2.line && r1.column == r2.column && r1.length < r2.length);
}

int ExtraHighlightingResultsCollector::onlyIndexOf(const QStringView &text,
                                                   const QStringView &subString, int from)
{
    const int firstIndex = text.indexOf(subString, from);
    if (firstIndex == -1)
        return -1;
    const int nextIndex = text.indexOf(subString, firstIndex + 1);

    // The second condion deals with the off-by-one error in TemplateSpecialization nodes;
    // see collectFromNode().
    return nextIndex == -1 || nextIndex == firstIndex + 1 ? firstIndex : -1;
}

// Unfortunately, the exact position of a specific token is usually not
// recorded in the AST, so if we need that, we have to search for it textually.
// In corner cases, this might get sabotaged by e.g. comments, in which case we give up.
int ExtraHighlightingResultsCollector::posForNodeStart(const ClangdAstNode &node) const
{
    return Utils::Text::positionInText(m_doc, node.range().start().line() + 1,
                                       node.range().start().character() + 1);
}

int ExtraHighlightingResultsCollector::posForNodeEnd(const ClangdAstNode &node) const
{
    return Utils::Text::positionInText(m_doc, node.range().end().line() + 1,
                                       node.range().end().character() + 1);
}

void ExtraHighlightingResultsCollector::insertResult(const HighlightingResult &result)
{
    if (!result.isValid()) // Some nodes don't have a range.
        return;
    const auto it = std::lower_bound(m_results.begin(), m_results.end(), result, lessThan);
    if (it == m_results.end() || *it != result) {

        // Prevent inserting expansions for function-like macros. For instance:
        //     #define TEST() "blubb"
        //     const char *s = TEST();
        // The macro name is always shorter than the expansion and starts at the same
        // location, so it should occur right before the insertion position.
        if (it > m_results.begin() && (it - 1)->line == result.line
                && (it - 1)->column == result.column
                && (it - 1)->textStyles.mainStyle == C_MACRO) {
            return;
        }

        // Bogus ranges; e.g. QTCREATORBUG-27601
        if (it != m_results.end()) {
            const int nextStartPos = Utils::Text::positionInText(m_doc, it->line, it->column);
            const int resultEndPos = Utils::Text::positionInText(m_doc, result.line, result.column)
                    + result.length;
            if (resultEndPos > nextStartPos)
                return;
        }

        qCDebug(clangdLogHighlight) << "adding additional highlighting result"
                                    << result.line << result.column << result.length;
        m_results.insert(it, result);
        return;
    }
}

void ExtraHighlightingResultsCollector::insertResult(const ClangdAstNode &node, TextStyle style)
{
    HighlightingResult result;
    result.useTextSyles = true;
    result.textStyles.mainStyle = style;
    setResultPosFromRange(result, node.range());
    insertResult(result);
    return;
}

// For matching the "<" and ">" brackets of template declarations, specializations
// and instantiations.
void ExtraHighlightingResultsCollector::insertAngleBracketInfo(int searchStart1, int searchEnd1,
                                                               int searchStart2, int searchEnd2)
{
    const int openingAngleBracketPos = onlyIndexOf(
                subViewEnd(m_docContent, searchStart1, searchEnd1),
                QStringView(QStringLiteral("<")));
    if (openingAngleBracketPos == -1)
        return;
    const int absOpeningAngleBracketPos = searchStart1 + openingAngleBracketPos;
    if (absOpeningAngleBracketPos > searchStart2)
        searchStart2 = absOpeningAngleBracketPos + 1;
    if (searchStart2 >= searchEnd2)
        return;
    const int closingAngleBracketPos = onlyIndexOf(
                subViewEnd(m_docContent, searchStart2, searchEnd2),
                QStringView(QStringLiteral(">")));
    if (closingAngleBracketPos == -1)
        return;

    const int absClosingAngleBracketPos = searchStart2 + closingAngleBracketPos;
    if (absOpeningAngleBracketPos > absClosingAngleBracketPos)
        return;

    HighlightingResult result;
    result.useTextSyles = true;
    result.textStyles.mainStyle = C_PUNCTUATION;
    Utils::Text::convertPosition(m_doc, absOpeningAngleBracketPos, &result.line, &result.column);
    result.length = 1;
    result.kind = CppEditor::SemanticHighlighter::AngleBracketOpen;
    insertResult(result);
    Utils::Text::convertPosition(m_doc, absClosingAngleBracketPos, &result.line, &result.column);
    result.kind = CppEditor::SemanticHighlighter::AngleBracketClose;
    insertResult(result);
}

void ExtraHighlightingResultsCollector::setResultPosFromRange(HighlightingResult &result,
                                                              const Range &range)
{
    if (!range.isValid())
        return;
    const Position startPos = range.start();
    const Position endPos = range.end();
    result.line = startPos.line() + 1;
    result.column = startPos.character() + 1;
    result.length = endPos.toPositionInDocument(m_doc) - startPos.toPositionInDocument(m_doc);
}

void ExtraHighlightingResultsCollector::collectFromNode(const ClangdAstNode &node)
{
    if (node.kind().endsWith("Literal"))
        return;
    if (node.role() == "type" && node.kind() == "Builtin")
        return;

    if (m_clangdVersion < 16 && node.role() == "attribute"
            && (node.kind() == "Override" || node.kind() == "Final")) {
        insertResult(node, C_KEYWORD);
        return;
    }

    const bool isExpression = node.role() == "expression";
    if (m_clangdVersion < 16 && isExpression && node.kind() == "Predefined") {
        insertResult(node, C_LOCAL);
        return;
    }

    const bool isDeclaration = node.role() == "declaration";
    const int nodeStartPos = posForNodeStart(node);
    const int nodeEndPos = posForNodeEnd(node);
    const QList<ClangdAstNode> children = node.children().value_or(QList<ClangdAstNode>());

    // Match question mark and colon in ternary operators.
    if (m_clangdVersion < 16 && isExpression && node.kind() == "ConditionalOperator") {
        if (children.size() != 3)
            return;

        // The question mark is between sub-expressions 1 and 2, the colon is between
        // sub-expressions 2 and 3.
        const int searchStartPosQuestionMark = posForNodeEnd(children.first());
        const int searchEndPosQuestionMark = posForNodeStart(children.at(1));
        QStringView content = subViewEnd(m_docContent, searchStartPosQuestionMark,
                                         searchEndPosQuestionMark);
        const int questionMarkPos = onlyIndexOf(content, QStringView(QStringLiteral("?")));
        if (questionMarkPos == -1)
            return;
        const int searchStartPosColon = posForNodeEnd(children.at(1));
        const int searchEndPosColon = posForNodeStart(children.at(2));
        content = subViewEnd(m_docContent, searchStartPosColon, searchEndPosColon);
        const int colonPos = onlyIndexOf(content, QStringView(QStringLiteral(":")));
        if (colonPos == -1)
            return;

        const int absQuestionMarkPos = searchStartPosQuestionMark + questionMarkPos;
        const int absColonPos = searchStartPosColon + colonPos;
        if (absQuestionMarkPos > absColonPos)
            return;

        HighlightingResult result;
        result.useTextSyles = true;
        result.textStyles.mainStyle = C_PUNCTUATION;
        result.textStyles.mixinStyles.push_back(C_OPERATOR);
        Utils::Text::convertPosition(m_doc, absQuestionMarkPos, &result.line, &result.column);
        result.length = 1;
        result.kind = CppEditor::SemanticHighlighter::TernaryIf;
        insertResult(result);
        Utils::Text::convertPosition(m_doc, absColonPos, &result.line, &result.column);
        result.kind = CppEditor::SemanticHighlighter::TernaryElse;
        insertResult(result);
        return;
    }

    if (isDeclaration && (node.kind() == "FunctionTemplate" || node.kind() == "ClassTemplate")) {
        // The child nodes are the template parameters and and the function or class.
        // The opening angle bracket is before the first child node, the closing angle
        // bracket is before the function child node and after the last param node.
        const QString classOrFunctionKind = QLatin1String(node.kind() == "FunctionTemplate"
                                                          ? "Function" : "CXXRecord");
        const auto functionOrClassIt = std::find_if(children.begin(), children.end(),
                                                    [&classOrFunctionKind](const ClangdAstNode &n) {
            return n.role() == "declaration" && n.kind() == classOrFunctionKind;
        });
        if (functionOrClassIt == children.end() || functionOrClassIt == children.begin())
            return;
        const int firstTemplateParamStartPos = posForNodeStart(children.first());
        const int lastTemplateParamEndPos = posForNodeEnd(*(functionOrClassIt - 1));
        const int functionOrClassStartPos = posForNodeStart(*functionOrClassIt);
        insertAngleBracketInfo(nodeStartPos, firstTemplateParamStartPos,
                               lastTemplateParamEndPos, functionOrClassStartPos);
        return;
    }

    const auto isTemplateParamDecl = [](const ClangdAstNode &node) {
        return node.isTemplateParameterDeclaration();
    };
    if (isDeclaration && node.kind() == "TypeAliasTemplate") {
        // Children are one node of type TypeAlias and the template parameters.
        // The opening angle bracket is before the first parameter and the closing
        // angle bracket is after the last parameter.
        // The TypeAlias node seems to appear first in the AST, even though lexically
        // is comes after the parameters. We don't rely on the order here.
        // Note that there is a second pair of angle brackets. That one is part of
        // a TemplateSpecialization, which is handled further below.
        const auto firstTemplateParam = std::find_if(children.begin(), children.end(),
                                                     isTemplateParamDecl);
        if (firstTemplateParam == children.end())
            return;
        const auto lastTemplateParam = std::find_if(children.rbegin(), children.rend(),
                                                    isTemplateParamDecl);
        QTC_ASSERT(lastTemplateParam != children.rend(), return);
        const auto typeAlias = std::find_if(children.begin(), children.end(),
                [](const ClangdAstNode &n) { return n.kind() == "TypeAlias"; });
        if (typeAlias == children.end())
            return;

        const int firstTemplateParamStartPos = posForNodeStart(*firstTemplateParam);
        const int lastTemplateParamEndPos = posForNodeEnd(*lastTemplateParam);
        const int searchEndPos = posForNodeStart(*typeAlias);
        insertAngleBracketInfo(nodeStartPos, firstTemplateParamStartPos,
                               lastTemplateParamEndPos, searchEndPos);
        return;
    }

    if (isDeclaration && node.kind() == "ClassTemplateSpecialization") {
        // There is one child of kind TemplateSpecialization. The first pair
        // of angle brackets comes before that.
        if (children.size() == 1) {
            const int childNodePos = posForNodeStart(children.first());
            insertAngleBracketInfo(nodeStartPos, childNodePos, nodeStartPos, childNodePos);
        }
        return;
    }

    if (isDeclaration && node.kind() == "TemplateTemplateParm") {
        // The child nodes are template arguments and template parameters.
        // Arguments seem to appear before parameters in the AST, even though they
        // come after them in the source code. We don't rely on the order here.
        const auto firstTemplateParam = std::find_if(children.begin(), children.end(),
                                                     isTemplateParamDecl);
        if (firstTemplateParam == children.end())
            return;
        const auto lastTemplateParam = std::find_if(children.rbegin(), children.rend(),
                                                    isTemplateParamDecl);
        QTC_ASSERT(lastTemplateParam != children.rend(), return);
        const auto templateArg = std::find_if(children.begin(), children.end(),
                [](const ClangdAstNode &n) { return n.role() == "template argument"; });

        const int firstTemplateParamStartPos = posForNodeStart(*firstTemplateParam);
        const int lastTemplateParamEndPos = posForNodeEnd(*lastTemplateParam);
        const int searchEndPos = templateArg == children.end()
                ? nodeEndPos : posForNodeStart(*templateArg);
        insertAngleBracketInfo(nodeStartPos, firstTemplateParamStartPos,
                               lastTemplateParamEndPos, searchEndPos);
        return;
    }

    // {static,dynamic,reinterpret}_cast<>().
    if (isExpression && node.kind().startsWith("CXX") && node.kind().endsWith("Cast")) {
        // First child is type, second child is expression.
        // The opening angle bracket is before the first child, the closing angle bracket
        // is between the two children.
        if (children.size() == 2) {
            insertAngleBracketInfo(nodeStartPos, posForNodeStart(children.first()),
                                   posForNodeEnd(children.first()),
                                   posForNodeStart(children.last()));
        }
        return;
    }

    if (node.kind() == "TemplateSpecialization") {
        // First comes the template type, then the template arguments.
        // The opening angle bracket is before the first template argument,
        // the closing angle bracket is after the last template argument.
        // The first child node has no range, so we start searching at the parent node.
        if (children.size() >= 2) {
            int searchStart2 = posForNodeEnd(children.last());
            int searchEnd2 = nodeEndPos;

            // There is a weird off-by-one error on the clang side: If there is a
            // nested template instantiation *and* there is no space between
            // the closing angle brackets, then the inner TemplateSpecialization node's range
            // will extend one character too far, covering the outer's closing angle bracket.
            // This is what we are correcting for here.
            // This issue is tracked at https://github.com/clangd/clangd/issues/871.
            if (searchStart2 == searchEnd2)
                --searchStart2;
            insertAngleBracketInfo(nodeStartPos, posForNodeStart(children.at(1)),
                                   searchStart2, searchEnd2);
        }
        return;
    }

    if (!isExpression && !isDeclaration)
        return;

    if (m_clangdVersion >= 16)
        return;

    // Operators, overloaded ones in particular.
    static const QString operatorPrefix = "operator";
    QString detail = node.detail().value_or(QString());
    const bool isCallToNew = node.kind() == "CXXNew";
    const bool isCallToDelete = node.kind() == "CXXDelete";
    const auto isProperOperator = [&] {
        if (isCallToNew || isCallToDelete)
            return true;
        if (!detail.startsWith(operatorPrefix))
            return false;
        if (detail == operatorPrefix)
            return false;
        const QChar nextChar = detail.at(operatorPrefix.length());
        return !nextChar.isLetterOrNumber() && nextChar != '_';
    };
    if (!isProperOperator())
        return;

    if (!isCallToNew && !isCallToDelete)
        detail.remove(0, operatorPrefix.length());

    if (node.kind() == "CXXConversion")
        return;

    HighlightingResult result;
    result.useTextSyles = true;
    const bool isOverloaded = isDeclaration || ((!isCallToNew && !isCallToDelete)
                                                || node.arcanaContains("CXXMethod"));
    result.textStyles.mainStyle = isCallToNew || isCallToDelete || detail.at(0).isSpace()
              ? C_KEYWORD : C_PUNCTUATION;
    result.textStyles.mixinStyles.push_back(C_OPERATOR);
    if (isOverloaded)
        result.textStyles.mixinStyles.push_back(C_OVERLOADED_OPERATOR);
    if (isDeclaration)
        result.textStyles.mixinStyles.push_back(C_DECLARATION);

    const QStringView nodeText = subViewEnd(m_docContent, nodeStartPos, nodeEndPos);

    if (isCallToNew || isCallToDelete) {
        result.line = node.range().start().line() + 1;
        result.column = node.range().start().character() + 1;
        result.length = isCallToNew ? 3 : 6;
        insertResult(result);
        if (node.arcanaContains("array")) {
            const int openingBracketOffset = nodeText.indexOf('[');
            if (openingBracketOffset == -1)
                return;
            const int closingBracketOffset = nodeText.lastIndexOf(']');
            if (closingBracketOffset == -1 || closingBracketOffset < openingBracketOffset)
                return;

            result.textStyles.mainStyle = C_PUNCTUATION;
            result.length = 1;
            Utils::Text::convertPosition(m_doc,
                                         nodeStartPos + openingBracketOffset,
                                         &result.line, &result.column);
            insertResult(result);
            Utils::Text::convertPosition(m_doc,
                                         nodeStartPos + closingBracketOffset,
                                         &result.line, &result.column);
            insertResult(result);
        }
        return;
    }

    if (isExpression && (detail == QLatin1String("()") || detail == QLatin1String("[]"))) {
        result.line = node.range().start().line() + 1;
        result.column = node.range().start().character() + 1;
        result.length = 1;
        insertResult(result);
        result.line = node.range().end().line() + 1;
        result.column = node.range().end().character();
        insertResult(result);
        return;
    }

    const int opStringLen = detail.at(0).isSpace() ? detail.length() - 1 : detail.length();

    // The simple case: Call to operator+, +=, * etc.
    if (nodeEndPos - nodeStartPos == opStringLen) {
        setResultPosFromRange(result, node.range());
        insertResult(result);
        return;
    }

    const int prefixOffset = nodeText.indexOf(operatorPrefix);
    if (prefixOffset == -1)
        return;

    const bool isArray = detail == "[]";
    const bool isCall = detail == "()";
    const bool isArrayNew = detail == " new[]";
    const bool isArrayDelete = detail == " delete[]";
    const QStringView searchTerm = isArray || isCall
            ? QStringView(detail).chopped(1) : isArrayNew || isArrayDelete
              ? QStringView(detail).chopped(2) : detail;
    const int opStringOffset = nodeText.indexOf(searchTerm, prefixOffset
                                                + operatorPrefix.length());
    if (opStringOffset == -1 || nodeText.indexOf(operatorPrefix, opStringOffset) != -1)
        return;

    const int opStringOffsetInDoc = nodeStartPos + opStringOffset
            + detail.length() - opStringLen;
    Utils::Text::convertPosition(m_doc, opStringOffsetInDoc, &result.line, &result.column);
    result.length = opStringLen;
    if (isArray || isCall)
        result.length = 1;
    else if (isArrayNew || isArrayDelete)
        result.length -= 2;
    if (!isArray && !isCall)
        insertResult(result);
    if (!isArray && !isCall && !isArrayNew && !isArrayDelete)
        return;

    result.textStyles.mainStyle = C_PUNCTUATION;
    result.length = 1;
    const int openingParenOffset = nodeText.indexOf(
                isCall ? '(' : '[', prefixOffset + operatorPrefix.length());
    if (openingParenOffset == -1)
        return;
    const int closingParenOffset = nodeText.indexOf(isCall ? ')' : ']', openingParenOffset + 1);
    if (closingParenOffset == -1 || closingParenOffset < openingParenOffset)
        return;
    Utils::Text::convertPosition(m_doc, nodeStartPos + openingParenOffset,
                                 &result.line, &result.column);
    insertResult(result);
    Utils::Text::convertPosition(m_doc, nodeStartPos + closingParenOffset,
                                 &result.line, &result.column);
    insertResult(result);
}

void ExtraHighlightingResultsCollector::visitNode(const ClangdAstNode &node)
{
    if (m_future.isCanceled())
        return;
    const ClangdAstNode::FileStatus prevFileStatus = m_currentFileStatus;
    m_currentFileStatus = node.fileStatus(m_filePath);
    if (m_currentFileStatus == ClangdAstNode::FileStatus::Unknown
            && prevFileStatus != ClangdAstNode::FileStatus::Ours) {
        m_currentFileStatus = prevFileStatus;
    }
    switch (m_currentFileStatus) {
    case ClangdAstNode::FileStatus::Ours:
    case ClangdAstNode::FileStatus::Unknown:
        collectFromNode(node);
        [[fallthrough]];
    case ClangdAstNode::FileStatus::Foreign:
    case ClangCodeModel::Internal::ClangdAstNode::FileStatus::Mixed: {
        const auto children = node.children();
        if (!children)
            return;
        for (const ClangdAstNode &childNode : *children)
            visitNode(childNode);
        break;
    }
    }
    m_currentFileStatus = prevFileStatus;
}

} // namespace ClangCodeModel::Internal
