Revision control

++ // Copyright (C) 2024 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++
++ #include "qqmllsutils_p.h"
++ #include "documentsymbolutils_p.h"
++ #include <QtLanguageServer/private/qlanguageserverspectypes_p.h>
++ #include <QtQmlDom/private/qqmldomitem_p.h>
++ #include <QtQmlDom/private/qqmldomoutwriter_p.h>
++ #include <stack>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace DocumentSymbolUtils {
++ using QLspSpecification::SymbolKind;
++ using namespace QQmlJS::Dom;
++
++ struct TypeSymbolRelation
++ {
++ DomType domType;
++ SymbolKind symbolKind;
++ };
++
++ constexpr static std::array<TypeSymbolRelation, 9> s_TypeSymbolRelations = { {
++ { DomType::Binding, SymbolKind::Variable },
++ { DomType::PropertyDefinition, SymbolKind::Property },
++ // Although MethodInfo simply relates to Method, SymbolKind requires special handling:
++ // When MethodInfo represents a Signal, its SymbolKind is set to Event.
++ // This distinction is explicitly managed in the symbolKindOf() helper function.
++ // see also QTBUG-128423
++ { DomType::MethodInfo, SymbolKind::Method },
++ { DomType::Id, SymbolKind::Key },
++ { DomType::QmlObject, SymbolKind::Object },
++ { DomType::EnumDecl, SymbolKind::Enum },
++ { DomType::EnumItem, SymbolKind::EnumMember },
++ { DomType::QmlComponent, SymbolKind::Module },
++ { DomType::QmlFile, SymbolKind::File },
++ } };
++
++ [[nodiscard]] constexpr static inline SymbolKind symbolKindFor(const DomType &type)
++ {
++ // constexpr std::find_if is only from c++20
++ for (const auto &mapping : s_TypeSymbolRelations) {
++ if (mapping.domType == type) {
++ return mapping.symbolKind;
++ }
++ }
++ return SymbolKind::Null;
++ }
++
++ constexpr static inline bool documentSymbolNotSupportedFor(const DomType &type)
++ {
++ return symbolKindFor(type) == SymbolKind::Null;
++ }
++
++ static bool propertyBoundAtDefinitionLine(const DomItem &propertyDefinition)
++ {
++ Q_ASSERT(propertyDefinition.internalKind() == DomType::PropertyDefinition);
++ return FileLocations::treeOf(propertyDefinition)->info().regions[ColonTokenRegion].isValid();
++ }
++
++ static inline bool shouldFilterOut(const DomItem &item)
++ {
++ const auto itemType = item.internalKind();
++ if (documentSymbolNotSupportedFor(itemType)) {
++ return true;
++ }
++ if (itemType == DomType::PropertyDefinition && propertyBoundAtDefinitionLine(item)) {
++ // without this check there is a "duplication" of symbols.
++ // one representing PropertyDefinition another one - Binding
++ return true;
++ }
++ return false;
++ }
++
++ static std::optional<QByteArray> tryGetQmlObjectDetail(const DomItem &qmlObj)
++ {
++ using namespace QQmlJS::Dom;
++ Q_ASSERT(qmlObj.internalKind() == DomType::QmlObject);
++ bool hasId = !qmlObj.idStr().isEmpty();
++ if (hasId) {
++ return qmlObj.idStr().toUtf8();
++ }
++ const bool isRootObject = qmlObj.component().field(Fields::objects).index(0) == qmlObj;
++ if (isRootObject) {
++ return "root";
++ }
++ return std::nullopt;
++ }
++
++ static std::optional<QByteArray> tryGetBindingDetail(const DomItem &bItem)
++ {
++ const auto *bindingPtr = bItem.as<Binding>();
++ Q_ASSERT(bindingPtr);
++ switch (bindingPtr->valueKind()) {
++ case BindingValueKind::ScriptExpression: {
++ auto exprCode = bindingPtr->scriptExpressionValue()->code();
++ if (exprCode.length() > 25) {
++ return QStringView(exprCode).first(22).toUtf8().append("...");
++ }
++ if (exprCode.endsWith(QStringLiteral(";"))) {
++ exprCode.chop(1);
++ }
++ return exprCode.toUtf8();
++ }
++ default:
++ // Value is QmlObject or QList<QmlObject> => no detail
++ return std::nullopt;
++ }
++ }
++
++ static inline QByteArray getMethodDetail(const DomItem &mItem)
++ {
++ const auto *methodInfoPtr = mItem.as<MethodInfo>();
++ Q_ASSERT(methodInfoPtr);
++ return methodInfoPtr->signature(mItem).toUtf8();
++ }
++
++ std::optional<QByteArray> tryGetDetailOf(const DomItem &item)
++ {
++ switch (item.internalKind()) {
++ case DomType::Id: {
++ const auto name = item.name();
++ return name.isEmpty() ? std::nullopt : std::make_optional(name.toUtf8());
++ }
++ case DomType::EnumItem:
++ return QByteArray::number(item.as<EnumItem>()->value());
++ case DomType::QmlObject:
++ return tryGetQmlObjectDetail(item);
++ case DomType::MethodInfo:
++ return getMethodDetail(item);
++ case DomType::Binding:
++ return tryGetBindingDetail(item);
++ default:
++ return std::nullopt;
++ }
++ }
++
++ // TODO move to qmllsUtils?
++ static inline bool isSubRange(const QLspSpecification::Range &potentialSubRange,
++ const QLspSpecification::Range &range)
++ {
++ // Check if the start of a is greater than or equal to the start of b
++ bool startContained = (potentialSubRange.start.line > range.start.line
++ || (potentialSubRange.start.line == range.start.line
++ && potentialSubRange.start.character >= range.start.character));
++
++ // Check if the end of a is less than or equal to the end of b
++ bool endContained = (potentialSubRange.end.line < range.end.line
++ || (potentialSubRange.end.line == range.end.line
++ && potentialSubRange.end.character <= range.end.character));
++
++ return startContained && endContained;
++ }
++
++ using MutableRefToDocumentSymbol = QLspSpecification::DocumentSymbol &;
++ [[nodiscard]] static MutableRefToDocumentSymbol
++ findDirectParentFor(const QLspSpecification::DocumentSymbol &child,
++ MutableRefToDocumentSymbol currentParent)
++ {
++ const auto containsChildRange =
++ [&range = child.range](const QLspSpecification::DocumentSymbol &symbol) {
++ return isSubRange(range, symbol.range);
++ };
++ // Parent's Range covers children's Ranges
++ // all children, grand-children, grand-grand-children and so forth
++ // are not supposed to have overlapping Ranges, hence it's just "gready" approach
++ // 1. find a Symbol among children, containing a Range
++ // 2. set it as currentCandidate
++ // 3. repeat
++ std::reference_wrapper<QLspSpecification::DocumentSymbol> currentCandidate(currentParent);
++ while (containsChildRange(currentCandidate) && currentCandidate.get().children.has_value()) {
++ auto newCandidate =
++ std::find_if(currentCandidate.get().children->begin(),
++ currentCandidate.get().children->end(), containsChildRange);
++ if (newCandidate == currentCandidate.get().children->end()) {
++ break;
++ }
++ currentCandidate = std::ref(*newCandidate);
++ }
++ return currentCandidate;
++ }
++
++ using DocumentSymbolPredicate =
++ qxp::function_ref<bool(const QLspSpecification::DocumentSymbol &) const>;
++ [[nodiscard]] static SymbolsList extractChildrenIf(const DocumentSymbolPredicate shouldBeReadopted,
++ MutableRefToDocumentSymbol currentParent)
++ {
++ if (!currentParent.children.has_value()) {
++ return {};
++ }
++ auto &parentsChildren = currentParent.children.value();
++ SymbolsList extractedChildren;
++ extractedChildren.reserve(parentsChildren.size());
++ auto [_, toBeRemoved] = std::partition_copy(parentsChildren.cbegin(), parentsChildren.cend(),
++ std::back_inserter(extractedChildren),
++ parentsChildren.begin(), shouldBeReadopted);
++ parentsChildren.erase(toBeRemoved, parentsChildren.end());
++ return extractedChildren;
++ }
++
++ static inline void adopt(QLspSpecification::DocumentSymbol &&child,
++ MutableRefToDocumentSymbol parent)
++ {
++ if (!parent.children.has_value()) {
++ parent.children.emplace({ std::move(child) });
++ return;
++ }
++ parent.children->emplace_back(std::move(child));
++ }
++
++ static void readoptChildrenIf(const DocumentSymbolPredicate unaryPred,
++ MutableRefToDocumentSymbol currentParent)
++ {
++ auto childrenToBeReadopted = extractChildrenIf(unaryPred, currentParent);
++ for (auto &&child : childrenToBeReadopted) {
++ auto &newParentRef = findDirectParentFor(child, currentParent);
++ adopt(std::move(child), newParentRef);
++ }
++ }
++
++ // Readopts all Enum-s and Id-s
++ static void reorganizeQmlComponentSymbol(MutableRefToDocumentSymbol qmlCompSymbol)
++ {
++ Q_ASSERT(qmlCompSymbol.kind == symbolKindFor(DomType::QmlComponent));
++ if (!qmlCompSymbol.children.has_value()) {
++ // nothing to reorganize
++ return;
++ }
++
++ constexpr auto enumDeclSymbolKind = symbolKindFor(DomType::EnumDecl);
++ const auto symbolIsEnumDecl = [](const QLspSpecification::DocumentSymbol &symbol) -> bool {
++ return symbol.kind == enumDeclSymbolKind;
++ };
++ readoptChildrenIf(symbolIsEnumDecl, qmlCompSymbol);
++ }
++
++ /*! \internal
++ * This function reorganizes \c qmlFileSymbols (result of assembleSymbolsForQmlFile)
++ * in the following way:
++ * 1. Moves Symbol-s representing Enum-s and inline QmlComponent-s
++ * to their respective range-containing parents , a.k.a. direct structural parents.
++ * 2. Reassignes head to the DocumentSymbol representing root QmlObject of the main
++ * QmlComponent
++ */
++ void reorganizeForOutlineView(SymbolsList &qmlFileSymbols)
++ {
++ Q_ASSERT(qmlFileSymbols.at(0).kind == symbolKindFor(DomType::QmlFile)
++ && qmlFileSymbols.at(0).children.has_value());
++
++ auto &qmlFileSymbol = qmlFileSymbols[0];
++ constexpr auto qmlCompSymbolKind = symbolKindFor(DomType::QmlComponent);
++ for (auto &childSymbol : qmlFileSymbol.children.value()) {
++ if (childSymbol.kind == qmlCompSymbolKind) {
++ reorganizeQmlComponentSymbol(childSymbol);
++ }
++ }
++
++ const auto symbolIsInlineComp = [](const QLspSpecification::DocumentSymbol &symbol) -> bool {
++ return symbol.kind == qmlCompSymbolKind && symbol.name.contains(".");
++ };
++ readoptChildrenIf(symbolIsInlineComp, qmlFileSymbol);
++
++ // move pointer from the documentSymbol representing QmlFile
++ // to the documentSymbols representing children of main QmlComponent
++ // a.k.a. ignore / not to show QmlFile and mainComponent symbols
++ qmlFileSymbols = qmlFileSymbol.children->at(0).children.value();
++ }
++
++ /*! \internal
++ * Constructs a \c DocumentSymbol for an \c Item with the provided \c children.
++ * Returns \c children if the current \c Item should not be represented via a \c DocumentSymbol.
++ */
++ SymbolsList buildSymbolOrReturnChildren(const DomItem &item, SymbolsList &&children)
++ {
++ if (shouldFilterOut(item)) {
++ // nothing to build, just returning children
++ return std::move(children);
++ }
++
++ const auto buildPartialSymbol = [](const DomItem &item) {
++ QLspSpecification::DocumentSymbol symbol;
++ symbol.kind = symbolKindOf(item);
++ symbol.name = symbolNameOf(item);
++ symbol.detail = tryGetDetailOf(item);
++ std::tie(symbol.range, symbol.selectionRange) = symbolRangesOf(item);
++ return symbol;
++ };
++
++ auto symbol = buildPartialSymbol(item);
++ if (!children.empty()) {
++ symbol.children.emplace(std::move(children));
++ }
++ /*
++ To avoid pushing down Id items through the DocumentSymbol tree,
++ as part of rearrangement step, it was decided to handle them here explicitly.
++ That Id issue atm only affects objects
++ If / when Id is moving from component level to Object level this should be reflected
++ also in the visiting logic.
++ TODO(QTBUG-128274)
++ */
++ if (const auto objPtr = item.as<QmlObject>()) {
++ if (const auto idItem = item.component().field(Fields::ids).key(objPtr->idStr()).index(0)) {
++ auto idSymbol = buildPartialSymbol(idItem);
++ adopt(std::move(idSymbol), symbol);
++ }
++ }
++ return SymbolsList{ std::move(symbol) };
++ }
++
++ std::pair<QLspSpecification::Range, QLspSpecification::Range> symbolRangesOf(const DomItem &item)
++ {
++ const auto &fLoc = FileLocations::treeOf(item)->info();
++ const auto fullRangeSourceloc = fLoc.fullRegion;
++ const auto selectionRangeSourceLoc = fLoc.regions[IdentifierRegion].isValid()
++ ? fLoc.regions[IdentifierRegion]
++ : fullRangeSourceloc;
++
++ auto fItem = item.containingFile();
++ Q_ASSERT(fItem);
++ const QString &code = fItem.ownerAs<QmlFile>()->code();
++ return { QQmlLSUtils::qmlLocationToLspLocation(
++ QQmlLSUtils::Location::from({}, fullRangeSourceloc, code)),
++ QQmlLSUtils::qmlLocationToLspLocation(
++ QQmlLSUtils::Location::from({}, selectionRangeSourceLoc, code)) };
++ }
++
++ QByteArray symbolNameOf(const DomItem &item)
++ {
++ if (item.internalKind() == DomType::Id) {
++ return "id";
++ }
++ return (item.name().isEmpty() ? item.internalKindStr() : item.name()).toUtf8();
++ }
++
++ QLspSpecification::SymbolKind symbolKindOf(const DomItem &item)
++ {
++ if (item.internalKind() == DomType::MethodInfo) {
++ const auto *methodInfoPtr = item.as<MethodInfo>();
++ Q_ASSERT(methodInfoPtr);
++ return methodInfoPtr->methodType == MethodInfo::MethodType::Signal
++ ? SymbolKind::Event
++ : symbolKindFor(DomType::MethodInfo);
++ }
++ return symbolKindFor(item.internalKind());
++ }
++
++ /*! \internal
++ * Design decisions behind this class are the following:
++ * 1. It is an implementation detail of the free \c assembleSymbolsForQmlFile function
++ * 2. It can only be initialized and used once per \c Item.
++ * This is enforced by its \c refToRootItem reference member.
++ * 3. It is tested via the public \c assembleSymbolsForQmlFile function.
++ */
++ class DocumentSymbolVisitor
++ {
++ public:
++ DocumentSymbolVisitor(const DomItem &item, const AssemblingFunction af)
++ : m_assemble(af), m_refToRootItem(item) {};
++
++ static const FieldFilter &fieldsFilter();
++
++ [[nodiscard]] SymbolsList assembleSymbols();
++
++ private:
++ [[nodiscard]] SymbolsList popAndAssembleSymbolsFor(const DomItem &item);
++
++ void appendToTop(const SymbolsList &symbols);
++
++ private:
++ const AssemblingFunction m_assemble;
++ const DomItem &m_refToRootItem;
++ std::stack<SymbolsList> m_stackOfChildrenSymbols;
++ };
++
++ const FieldFilter &DocumentSymbolVisitor::fieldsFilter()
++ {
++ // TODO(QTBUG-128118) add only fields to be visited and not the ones
++ // to be removed.
++ static const FieldFilter ff{
++ {}, // to add
++ {
++ // to remove
++ { QString(), Fields::code.toString() },
++ { QString(), Fields::postCode.toString() },
++ { QString(), Fields::preCode.toString() },
++ { QString(), Fields::importScope.toString() },
++ { QString(), Fields::fileLocationsTree.toString() },
++ { QString(), Fields::astComments.toString() },
++ { QString(), Fields::comments.toString() },
++ { QString(), Fields::exports.toString() },
++ { QString(), Fields::propertyInfos.toString() },
++ { QLatin1String("FileLocationsNode"), Fields::parent.toString() },
++ //^^^ FieldFilter::default
++ { QString(), Fields::errors.toString() },
++ { QString(), Fields::imports.toString() },
++ { QString(), Fields::prototypes.toString() },
++ { QString(), Fields::annotations.toString() },
++ { QString(), Fields::attachedType.toString() },
++ { QString(), Fields::canonicalFilePath.toString() },
++ { QString(), Fields::isValid.toString() },
++ { QString(), Fields::isSingleton.toString() },
++ { QString(), Fields::isCreatable.toString() },
++ { QString(), Fields::isComposite.toString() },
++ { QString(), Fields::attachedTypeName.toString() },
++ { QString(), Fields::pragmas.toString() },
++ { QString(), Fields::defaultPropertyName.toString() },
++ { QString(), Fields::name.toString() },
++ { QString(), Fields::nameIdentifiers.toString() },
++ { QString(), Fields::prototypes.toString() },
++ { QString(), Fields::nextScope.toString() },
++ { QString(), Fields::parameters.toString() },
++ { QString(), Fields::methodType.toString() },
++ { QString(), Fields::type.toString() },
++ { QString(), Fields::isConstructor.toString() },
++ { QString(), Fields::returnType.toString() },
++ { QString(), Fields::body.toString() },
++ { QString(), Fields::access.toString() },
++ { QString(), Fields::typeName.toString() },
++ { QString(), Fields::isReadonly.toString() },
++ { QString(), Fields::isList.toString() },
++ { QString(), Fields::bindingIdentifiers.toString() },
++ { QString(), Fields::bindingType.toString() },
++ { QString(), Fields::isSignalHandler.toString() },
++ // prop def?
++ { QString(), Fields::isPointer.toString() },
++ { QString(), Fields::isFinal.toString() },
++ { QString(), Fields::isAlias.toString() },
++ { QString(), Fields::isDefaultMember.toString() },
++ { QString(), Fields::isRequired.toString() },
++ { QString(), Fields::read.toString() },
++ { QString(), Fields::write.toString() },
++ { QString(), Fields::bindable.toString() },
++ { QString(), Fields::notify.toString() },
++ { QString(), Fields::type.toString() },
++ // scriptExpr
++ { QString(), Fields::scriptElement.toString() },
++ { QString(), Fields::localOffset.toString() },
++ { QString(), Fields::astRelocatableDump.toString() },
++ { QString(), Fields::expressionType.toString() },
++ // components
++ { QString(), Fields::subComponents.toString() },
++ // BEWARE
++ // Ids and IdStr are filtered out during the visit, because
++ // documentSymbol-s for them will be explicitly handled as part of the
++ // creation of symbol for QmlObject
++ { QString(), Fields::ids.toString() },
++ { QString(), Fields::idStr.toString() },
++
++ // id
++ { QString(), Fields::referredObject.toString() },
++ // enum item
++ { QLatin1String("EnumItem"), Fields::value.toString() },
++ }
++ };
++ return ff;
++ }
++
++ SymbolsList DocumentSymbolVisitor::assembleSymbols()
++ {
++ using namespace QQmlJS::Dom;
++ auto openingVisitor = [this](const Path &, const DomItem &, bool) -> bool {
++ m_stackOfChildrenSymbols.emplace();
++ return true;
++ };
++ auto closingVisitor = [this](const Path &, const DomItem &item, bool) -> bool {
++ // it's closing Visitor, openingVisitor must have pushed something
++ Q_ASSERT(!m_stackOfChildrenSymbols.empty());
++ if (m_stackOfChildrenSymbols.size() == 1) {
++ // reached children of root, nothing to do
++ return false;
++ }
++ auto symbols = popAndAssembleSymbolsFor(item);
++ appendToTop(symbols);
++ return true;
++ };
++ m_refToRootItem.visitTree(Path(), emptyChildrenVisitor, VisitOption::Default, openingVisitor,
++ closingVisitor, fieldsFilter());
++ return popAndAssembleSymbolsFor(m_refToRootItem);
++ }
++
++ SymbolsList DocumentSymbolVisitor::popAndAssembleSymbolsFor(const DomItem &item)
++ {
++ Q_ASSERT(!m_stackOfChildrenSymbols.empty());
++ auto atEnd = qScopeGuard([this]() { m_stackOfChildrenSymbols.pop(); });
++ return m_assemble(item, std::move(m_stackOfChildrenSymbols.top()));
++ }
++
++ void DocumentSymbolVisitor::appendToTop(const SymbolsList &symbols)
++ {
++ Q_ASSERT(!m_stackOfChildrenSymbols.empty());
++ m_stackOfChildrenSymbols.top().append(symbols);
++ }
++
++ SymbolsList assembleSymbolsForQmlFile(const DomItem &item, const AssemblingFunction af)
++ {
++ Q_ASSERT(item.internalKind() == DomType::QmlFile);
++ DocumentSymbolVisitor visitor(item, af);
++ return visitor.assembleSymbols();
++ }
++ } // namespace DocumentSymbolUtils
++
++ QT_END_NAMESPACE