DocumentSymbol add Detail deriving functionality

This commit introduces support for detail field of DocumentSymbol.
For Dom::Id it's the .name()
For EnumMember is implicit or explicit .value()
For QmlObject it's
- .idStr if it exists
- "root" if .idStr doesnt's exist + object is indeed root
- null otherwise
For MethodInfo it's a signature, however, because this field
is absent from the MethodInfo struct, it needs to be derived here.
Dom lacks unified support for querying signatures, esp. of "Singal"-s,
hence it's manually handled and a bit messy.
As for Binding, detail is constructed using BindingValue

Task-number: QTBUG-120002
Change-Id: I96ed605d96caaa778e09a156bed51ac4a4f09b50
Reviewed-by: Sami Shalayel <sami.shalayel@qt.io>
This commit is contained in:
Dmitrii Akshintsev 2024-07-16 19:30:22 +02:00
parent 751beaff29
commit 86a3a0fbf7
5 changed files with 190 additions and 9 deletions

View File

@ -745,6 +745,10 @@ public:
TypeAnnotationStyle typeAnnotationStyle = TypeAnnotationStyle::Suffix;
};
// TODO (QTBUG-128423)
// Refactor to differentiate between Signals and Methods easily,
// considering their distinct handling and formatting needs.
// Explore separating Signal functionality or unifying shared methods.
class QMLDOM_EXPORT MethodInfo : public AttributeInfo
{
Q_GADGET

View File

@ -5,6 +5,7 @@
#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
@ -22,11 +23,11 @@ struct TypeSymbolRelation
constexpr static std::array<TypeSymbolRelation, 9> s_TypeSymbolRelations = { {
{ DomType::Binding, SymbolKind::Variable },
{ DomType::PropertyDefinition, SymbolKind::Property },
{ DomType::MethodInfo, SymbolKind::Method }, // BEWARE.
// Even though it's just a relation MethodInfo -> Method for the sake of simplicity,
// atm SymbolKind for MethodInfo requires special handling:
// in cases when MethodInfo is Signal, the SymbolKind will be Event,
// this is explicitly handled in the symbolKindOf() helper
// 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 },
@ -51,6 +52,69 @@ constexpr static inline bool documentSymbolNotSupportedFor(const DomType &type)
return symbolKindFor(type) == SymbolKind::Null;
}
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;
}
}
/*! \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.
@ -65,6 +129,7 @@ SymbolsList buildSymbolOrReturnChildren(const DomItem &item, SymbolsList &&child
QLspSpecification::DocumentSymbol symbol;
symbol.kind = symbolKindOf(item);
symbol.name = symbolNameOf(item);
symbol.detail = tryGetDetailOf(item);
std::tie(symbol.range, symbol.selectionRange) = symbolRangesOf(item);
if (!children.empty()) {
symbol.children.emplace(std::move(children));

View File

@ -42,6 +42,8 @@ symbolRangesOf(const DomItem &item);
[[nodiscard]] QLspSpecification::SymbolKind symbolKindOf(const DomItem &item);
[[nodiscard]] std::optional<QByteArray> tryGetDetailOf(const DomItem &item);
[[nodiscard]] SymbolsList
assembleSymbolsForQmlFile(const DomItem &item,
const AssemblingFunction af = buildSymbolOrReturnChildren);

View File

@ -18,20 +18,21 @@ QT_BEGIN_NAMESPACE
template <typename DomClass>
static inline DomItem domItem(DomClass &&c)
{
DomItem domEnv(DomEnvironment::create({}));
constexpr auto kind = std::decay<DomClass>::type::kindValue;
if constexpr (domTypeIsDomElement(kind)) {
c.updatePathFromOwner(
Path::Root()); // necessary for querying fields, a.k.a. init DomElement
Path::Current()); // necessary for querying fields, a.k.a. init DomElement
// For the "DomElement"-s owners are required,
// hence creating Env as an owner and then call copy on it
DomItem domEnv(DomEnvironment::create({}));
return domEnv.copy(&c);
}
if constexpr (kind == DomType::QmlFile) {
DomItem domEnv(DomEnvironment::create({}));
return domEnv.copy(&c);
}
return DomItem().wrap(QQmlJS::Dom::PathEls::PathComponent(), c);
// it's helpful to use wrap on DomEnv instead of just DomItem() to make
// .canonicalPath() usable
return domEnv.wrap(QQmlJS::Dom::PathEls::PathComponent(), c);
}
QT_END_NAMESPACE
@ -291,4 +292,112 @@ void tst_document_symbol_utils::symbolKindOf()
QCOMPARE(DocumentSymbolUtils::symbolKindOf(DomItem()), QLspSpecification::SymbolKind::Null);
}
void tst_document_symbol_utils::tryGetDetailOf()
{
{ // Id
Id id;
auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(id));
QVERIFY(!detail.has_value());
const QString name("name");
id.name = name;
const auto expectedDetail = name.toUtf8();
detail = DocumentSymbolUtils::tryGetDetailOf(domItem(id));
QVERIFY(detail.has_value());
QCOMPARE(detail, expectedDetail);
}
{ // EnumItem
const int value = 4;
const auto expectedDetail = QByteArray::number(value);
EnumItem enumItem("a", value);
const auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(enumItem));
QCOMPARE(detail, expectedDetail);
}
{ // Binding
Binding b;
auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
QCOMPARE(detail, std::nullopt);
b.setValue(std::make_unique<BindingValue>());
detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
QCOMPARE(detail, std::nullopt);
b.setValue(std::make_unique<BindingValue>(QmlObject()));
detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
QCOMPARE(detail, std::nullopt);
b.setValue(std::make_unique<BindingValue>(QList<QmlObject>()));
detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
QCOMPARE(detail, std::nullopt);
{
const QString bindingValue("4");
const auto expectedDetail = bindingValue.toUtf8();
const QString bindingExpr = bindingValue + ";";
const auto exprPtr = std::make_shared<ScriptExpression>(
bindingExpr, ScriptExpression::ExpressionType::BindingExpression);
b.setValue(std::make_unique<BindingValue>(exprPtr));
detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
QCOMPARE(detail, expectedDetail);
}
{
const QString bindingExpr("12345678901234567890123456"); // 26 symbols
const auto expectedDetail = QString("1234567890123456789012...").toUtf8();
const auto exprPtr = std::make_shared<ScriptExpression>(
bindingExpr, ScriptExpression::ExpressionType::BindingExpression);
b.setValue(std::make_unique<BindingValue>(exprPtr));
detail = DocumentSymbolUtils::tryGetDetailOf(domItem(b));
QCOMPARE(detail, expectedDetail);
}
}
{ // MethodInfo
{ // Method
MethodParameter intA;
intA.name = "a";
intA.typeName = "int";
MethodParameter stringB;
stringB.name = "b";
stringB.typeName = "string";
MethodInfo method;
method.parameters = { intA, stringB };
method.typeName = "bool";
const auto expectedDetail = QString("(a: int, b: string): bool").toUtf8();
const auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(method));
QVERIFY(detail.has_value());
QCOMPARE(detail.value(), expectedDetail);
}
{ // Signal
MethodParameter intA;
intA.name = "a";
intA.typeName = "int";
MethodInfo method;
method.methodType = MethodInfo::MethodType::Signal;
method.parameters = { intA };
const auto expectedDetail = QString("(a: int)").toUtf8();
const auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(method));
QVERIFY(detail.has_value());
QCOMPARE(detail.value(), expectedDetail);
}
}
{ // QmlObject
QmlObject obj;
auto detail = DocumentSymbolUtils::tryGetDetailOf(domItem(obj));
QCOMPARE(detail, std::nullopt);
const QString objId("objId");
obj.setIdStr(objId);
detail = DocumentSymbolUtils::tryGetDetailOf(domItem(obj));
QCOMPARE(detail, objId.toUtf8());
/*
* Unfortunately because of the way DomItem::component() and filterUp() are working
* (using full path from the root(env/top) and non-trivially trimming it)
* I can't properly construct a fake hierarchy of components and objects
* to verify the "root" case.
*/
}
}
QTEST_MAIN(tst_document_symbol_utils)

View File

@ -27,4 +27,5 @@ private slots:
void assembleSymbolsForQmlFile();
void symbolNameOf();
void symbolKindOf();
void tryGetDetailOf();
};