Commit b5717a53 authored by Ulf Hermann's avatar Ulf Hermann
Browse files

QmlJS: Lazy-load console items to allow for recursion



Using Utils:TreeView automatically gives us the capability for loading
item as they are expanded. This way we can show recursive structure in
the console as well as load data from the debug server on demand.

Also, properly print error messages received from unsuccessful
command evaluations.

Task-number: QTCREATORBUG-14931
Change-Id: I66d440eedd9723b04670169b27db1ee18f3f2891
Reviewed-by: default avatarhjk <hjk@theqtcompany.com>
parent 0b328328
......@@ -38,122 +38,130 @@ namespace QmlJS {
//
///////////////////////////////////////////////////////////////////////
ConsoleItem::ConsoleItem(ConsoleItem *parent, ConsoleItem::ItemType itemType,
const QString &text)
: m_parentItem(parent),
itemType(itemType),
line(-1)
QString addZeroWidthSpace(QString text)
{
setText(text);
for (int i = 0; i < text.length(); ++i) {
if (text.at(i).isPunct())
text.insert(++i, QChar(0x200b)); // ZERO WIDTH SPACE
}
return text;
}
ConsoleItem::~ConsoleItem()
ConsoleItem::ConsoleItem(ItemType itemType, const QString &expression, const QString &file,
int line) :
m_itemType(itemType), m_text(addZeroWidthSpace(expression)), m_file(file), m_line(line)
{
qDeleteAll(m_childItems);
setFlags(Qt::ItemFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable |
(itemType == InputType ? Qt::ItemIsEditable : Qt::NoItemFlags)));
}
ConsoleItem *ConsoleItem::child(int number)
ConsoleItem::ConsoleItem(ConsoleItem::ItemType itemType, const QString &expression,
std::function<void(ConsoleItem *)> doFetch) :
m_itemType(itemType), m_text(addZeroWidthSpace(expression)), m_line(-1), m_doFetch(doFetch)
{
return m_childItems.value(number);
setFlags(Qt::ItemFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable |
(itemType == InputType ? Qt::ItemIsEditable : Qt::NoItemFlags)));
}
int ConsoleItem::childCount() const
ConsoleItem::ItemType ConsoleItem::itemType() const
{
return m_childItems.size();
return m_itemType;
}
int ConsoleItem::childNumber() const
QString ConsoleItem::text() const
{
if (m_parentItem)
return m_parentItem->m_childItems.indexOf(const_cast<ConsoleItem *>(this));
return 0;
return m_text;
}
bool ConsoleItem::insertChildren(int position, int count)
QString ConsoleItem::file() const
{
if (position < 0 || position > m_childItems.size())
return false;
for (int row = 0; row < count; ++row) {
ConsoleItem *item = new ConsoleItem(this, ConsoleItem::UndefinedType,
QString());
m_childItems.insert(position, item);
}
return true;
return m_file;
}
void ConsoleItem::insertChild(ConsoleItem *item, bool sorted)
int ConsoleItem::line() const
{
if (!sorted) {
m_childItems.insert(m_childItems.count(), item);
return;
}
int i = 0;
for (; i < m_childItems.count(); i++) {
if (item->m_text < m_childItems[i]->m_text)
break;
}
m_childItems.insert(i, item);
return m_line;
}
bool ConsoleItem::insertChild(int position, ConsoleItem *item)
QVariant ConsoleItem::data(int column, int role) const
{
if (position < 0 || position > m_childItems.size())
return false;
m_childItems.insert(position, item);
return true;
}
ConsoleItem *ConsoleItem::parent()
{
return m_parentItem;
if (column != 0)
return QVariant();
switch (role)
{
case TypeRole:
return m_itemType;
case FileRole:
return m_file;
case LineRole:
return m_line;
case ExpressionRole:
return expression();
case Qt::DisplayRole:
return m_text;
default:
return TreeItem::data(column, role);
}
}
bool ConsoleItem::removeChildren(int position, int count)
bool ConsoleItem::setData(int column, const QVariant &data, int role)
{
if (position < 0 || position + count > m_childItems.size())
if (column != 0)
return false;
for (int row = 0; row < count; ++row)
delete m_childItems.takeAt(position);
return true;
switch (role)
{
case TypeRole:
m_itemType = ItemType(data.toInt());
return true;
case FileRole:
m_file = data.toString();
return true;
case LineRole:
m_line = data.toInt();
return true;
case ExpressionRole:
m_text = addZeroWidthSpace(data.toString());
return true;
case Qt::DisplayRole:
m_text = data.toString();
return true;
default:
return TreeItem::setData(column, data, role);
}
}
bool ConsoleItem::detachChild(int position)
bool ConsoleItem::canFetchMore() const
{
if (position < 0 || position > m_childItems.size())
return false;
m_childItems.removeAt(position);
// Always fetch all children, too, as the labels depend on them.
foreach (TreeItem *child, children()) {
if (static_cast<ConsoleItem *>(child)->m_doFetch)
return true;
}
return true;
return bool(m_doFetch);
}
void ConsoleItem::setText(const QString &text)
void ConsoleItem::fetchMore()
{
m_text = text;
for (int i = 0; i < m_text.length(); ++i) {
if (m_text.at(i).isPunct())
m_text.insert(++i, QChar(0x200b)); // ZERO WIDTH SPACE
if (m_doFetch) {
m_doFetch(this);
m_doFetch = std::function<void(ConsoleItem *)>();
}
}
QString ConsoleItem::text() const
{
return m_text;
foreach (TreeItem *child, children()) {
ConsoleItem *item = static_cast<ConsoleItem *>(child);
if (item->m_doFetch) {
item->m_doFetch(item);
item->m_doFetch = m_doFetch;
}
}
}
QString ConsoleItem::expression() const
{
QString text = m_text;
return text.remove(QChar(0x200b)); // ZERO WIDTH SPACE
return text().remove(QChar(0x200b)); // ZERO WIDTH SPACE
}
} // QmlJS
......@@ -32,53 +32,56 @@
#define CONSOLEITEM_H
#include "qmljs_global.h"
#include <utils/treemodel.h>
#include <QList>
#include <QString>
#include <functional>
namespace QmlJS {
class QMLJS_EXPORT ConsoleItem
class QMLJS_EXPORT ConsoleItem : public Utils::TreeItem
{
public:
enum Roles {
TypeRole = Qt::UserRole,
FileRole,
LineRole,
ExpressionRole
};
enum ItemType
{
UndefinedType = 0x01, // Can be used for unknown and for Return values
DebugType = 0x02,
WarningType = 0x04,
ErrorType = 0x08,
InputType = 0x10,
DefaultTypes = InputType | UndefinedType
DefaultType = 0x01, // Can be used for unknown and for Return values
DebugType = 0x02,
WarningType = 0x04,
ErrorType = 0x08,
InputType = 0x10,
};
Q_DECLARE_FLAGS(ItemTypes, ItemType)
ConsoleItem(ConsoleItem *parent,
ConsoleItem::ItemType type = ConsoleItem::UndefinedType,
const QString &data = QString());
~ConsoleItem();
ConsoleItem(ItemType itemType = ConsoleItem::DefaultType, const QString &expression = QString(),
const QString &file = QString(), int line = -1);
ConsoleItem(ItemType itemType, const QString &expression,
std::function<void(ConsoleItem *)> doFetch);
ConsoleItem *child(int number);
int childCount() const;
bool insertChildren(int position, int count);
void insertChild(ConsoleItem *item, bool sorted);
bool insertChild(int position, ConsoleItem *item);
ConsoleItem *parent();
bool removeChildren(int position, int count);
bool detachChild(int position);
int childNumber() const;
void setText(const QString &text);
QString text() const;
ItemType itemType() const;
QString expression() const;
QString text() const;
QString file() const;
int line() const;
QVariant data(int column, int role) const;
bool setData(int column, const QVariant &data, int role);
bool canFetchMore() const;
void fetchMore();
private:
ConsoleItem *m_parentItem;
QList<ConsoleItem *> m_childItems;
ItemType m_itemType;
QString m_text;
QString m_file;
int m_line;
public:
ConsoleItem::ItemType itemType;
QString file;
int line;
std::function<void(ConsoleItem *)> m_doFetch;
};
} // QmlJS
......
......@@ -50,8 +50,6 @@ public:
virtual void showConsolePane() = 0;
virtual ConsoleItem *rootItem() const = 0;
virtual void setScriptEvaluator(IScriptEvaluator *scriptEvaluator) = 0;
virtual void setContext(const QString &context) = 0;
......
......@@ -530,7 +530,7 @@ void DebuggerEngine::showMessage(const QString &msg, int channel, int timeout) c
// qDebug() << qPrintable(msg) << "IN STATE" << state();
QmlJS::ConsoleManagerInterface *consoleManager = QmlJS::ConsoleManagerInterface::instance();
if (channel == ConsoleOutput && consoleManager)
consoleManager->printToConsolePane(QmlJS::ConsoleItem::UndefinedType, msg);
consoleManager->printToConsolePane(QmlJS::ConsoleItem::DefaultType, msg);
Internal::showMessage(msg, channel, timeout);
if (d->m_runControl) {
......
......@@ -182,7 +182,7 @@ public:
QmlV8ObjectData extractData(const QVariant &data) const;
void insertSubItems(WatchItem *parent, const QVariantList &properties);
void checkForFinishedUpdate();
ConsoleItem *constructLogItemTree(ConsoleItem *parent, const QmlV8ObjectData &objectData);
ConsoleItem *constructLogItemTree(const QmlV8ObjectData &objectData);
public:
QHash<int, QmlV8ObjectData> refVals; // The mapping of target object handles to retrieved values.
......@@ -220,6 +220,10 @@ public:
QmlDebug::QDebugMessageClient *msgClient = 0;
QHash<int, QmlCallback> callbackForToken;
private:
ConsoleItem *constructLogItemTree(const QmlV8ObjectData &objectData, QList<int> &seenHandles);
void constructChildLogItems(ConsoleItem *item, const QmlV8ObjectData &objectData,
QList<int> &seenHandles);
};
static void updateDocument(IDocument *document, const QTextDocument *textDocument)
......@@ -1012,44 +1016,73 @@ void QmlEngine::selectWatchData(const QByteArray &iname)
d->inspectorAdapter.agent()->watchDataSelected(item->id);
}
static ConsoleItem *constructLogItemTree(ConsoleItem *parent,
const QVariant &result,
bool compareConsoleItems(const ConsoleItem *a, const ConsoleItem *b)
{
if (a == 0)
return true;
if (b == 0)
return false;
return a->text() < b->text();
}
static ConsoleItem *constructLogItemTree(const QVariant &result,
const QString &key = QString())
{
bool sorted = boolSetting(SortStructMembers);
if (!result.isValid())
return 0;
ConsoleItem *item = new ConsoleItem(parent);
QString text;
ConsoleItem *item = 0;
if (result.type() == QVariant::Map) {
if (key.isEmpty())
item->setText(_("Object"));
text = _("Object");
else
item->setText(key + _(" : Object"));
text = key + _(" : Object");
QMap<QString, QVariant> resultMap = result.toMap();
QVarLengthArray<ConsoleItem *> children(resultMap.size());
QMapIterator<QString, QVariant> i(result.toMap());
auto it = children.begin();
while (i.hasNext()) {
i.next();
ConsoleItem *child = constructLogItemTree(item, i.value(), i.key());
*(it++) = constructLogItemTree(i.value(), i.key());
}
// Sort before inserting as ConsoleItem::sortChildren causes a whole cascade of changes we
// may not want to handle here.
if (sorted)
std::sort(children.begin(), children.end(), compareConsoleItems);
item = new ConsoleItem(ConsoleItem::DefaultType, text);
foreach (ConsoleItem *child, children) {
if (child)
item->insertChild(child, sorted);
item->appendChild(child);
}
} else if (result.type() == QVariant::List) {
if (key.isEmpty())
item->setText(_("List"));
text = _("List");
else
item->setText(QString(_("[%1] : List")).arg(key));
text = QString(_("[%1] : List")).arg(key);
QVariantList resultList = result.toList();
for (int i = 0; i < resultList.count(); i++) {
ConsoleItem *child = constructLogItemTree(item, resultList.at(i),
QString::number(i));
QVarLengthArray<ConsoleItem *> children(resultList.size());
for (int i = 0; i < resultList.count(); i++)
children[i] = constructLogItemTree(resultList.at(i), QString::number(i));
if (sorted)
std::sort(children.begin(), children.end(), compareConsoleItems);
item = new ConsoleItem(ConsoleItem::DefaultType, text);
foreach (ConsoleItem *child, children) {
if (child)
item->insertChild(child, sorted);
item->appendChild(child);
}
} else if (result.canConvert(QVariant::String)) {
item->setText(result.toString());
item = new ConsoleItem(ConsoleItem::DefaultType, result.toString());
} else {
item->setText(_("Unknown Value"));
item = new ConsoleItem(ConsoleItem::DefaultType, _("Unknown Value"));
}
return item;
......@@ -1060,7 +1093,7 @@ void QmlEngine::expressionEvaluated(quint32 queryId, const QVariant &result)
if (d->queryIds.contains(queryId)) {
d->queryIds.removeOne(queryId);
if (auto consoleManager = ConsoleManagerInterface::instance()) {
if (ConsoleItem *item = constructLogItemTree(consoleManager->rootItem(), result))
if (ConsoleItem *item = constructLogItemTree(result))
consoleManager->printToConsolePane(item);
}
}
......@@ -2034,13 +2067,10 @@ void QmlEnginePrivate::messageReceived(const QByteArray &data)
//This is most probably due to a wrong eval expression.
//Redirect output to console.
if (eventType.isEmpty()) {
QmlV8ObjectData entry;
entry.type = "string";
entry.value = resp.value(_("message"));
if (auto consoleManager = ConsoleManagerInterface::instance()) {
if (ConsoleItem *item = constructLogItemTree(consoleManager->rootItem(), entry))
consoleManager->printToConsolePane(item);
}
if (auto consoleManager = ConsoleManagerInterface::instance())
consoleManager->printToConsolePane(new ConsoleItem(
ConsoleItem::ErrorType,
resp.value(_(MESSAGE)).toString()));
}
} //EVENT
......@@ -2319,37 +2349,99 @@ void QmlEnginePrivate::checkForFinishedUpdate()
engine->watchHandler()->notifyUpdateFinished();
}
ConsoleItem *QmlEnginePrivate::constructLogItemTree(ConsoleItem *parent,
const QmlV8ObjectData &objectData)
ConsoleItem *QmlEnginePrivate::constructLogItemTree(const QmlV8ObjectData &objectData)
{
bool sorted = boolSetting(SortStructMembers);
if (!objectData.value.isValid())
return 0;
QList<int> handles;
return constructLogItemTree(objectData, handles);
}
void QmlEnginePrivate::constructChildLogItems(ConsoleItem *item, const QmlV8ObjectData &objectData,
QList<int> &seenHandles)
{
// We cannot sort the children after attaching them to the parent as that would cause layout
// changes, invalidating cached indices. So we presort them before inserting.
QVarLengthArray<ConsoleItem *> children(objectData.properties.size());
auto it = children.begin();
foreach (const QVariant &property, objectData.properties)
*(it++) = constructLogItemTree(extractData(property), seenHandles);
if (boolSetting(SortStructMembers))
std::sort(children.begin(), children.end(), compareConsoleItems);
foreach (ConsoleItem *child, children)
item->appendChild(child);
}
ConsoleItem *QmlEnginePrivate::constructLogItemTree(const QmlV8ObjectData &objectData,
QList<int> &seenHandles)
{
QString text;
if (objectData.name.isEmpty())
if (objectData.value.isValid()) {
text = objectData.value.toString();
else
text = QString(_("%1: %2")).arg(QString::fromLatin1(objectData.name))
.arg(objectData.value.toString());
} else if (!objectData.type.isEmpty()) {
text = QString::fromLatin1(objectData.type);
} else {
int handle = objectData.handle;
ConsoleItem *item = new ConsoleItem(ConsoleItem::DefaultType,
QString::fromLatin1(objectData.name),
[this, handle](ConsoleItem *item)
{
DebuggerCommand cmd(LOOKUP);
cmd.arg(HANDLES, QList<int>() << handle);
runCommand(cmd, [this, item, handle](const QVariantMap &response) {
const QVariantMap body = response.value(_(BODY)).toMap();
QStringList handlesList = body.keys();
foreach (const QString &handleString, handlesList) {
if (handle != handleString.toInt())
continue;
QmlV8ObjectData objectData = extractData(body.value(handleString));
// keep original name, if possible
QString name = item->expression();
if (name.isEmpty())
name = QString::fromLatin1(objectData.name);
QString value = objectData.value.isValid() ?
objectData.value.toString() : QString::fromLatin1(objectData.type);
// We can do setData() and cause dataChanged() here, but only because this
// callback is executed after fetchMore() has returned.
item->model()->setData(item->index(),
QString::fromLatin1("%1: %2").arg(name).arg(value),
ConsoleItem::ExpressionRole);
QList<int> newHandles;
constructChildLogItems(item, objectData, newHandles);
break;
}
});
});
return item;
}
ConsoleItem *item = new ConsoleItem(parent, ConsoleItem::UndefinedType, text);
if (!objectData.name.isEmpty())
text = QString(_("%1: %2")).arg(QString::fromLatin1(objectData.name)).arg(text);
QSet<QString> childrenFetched;
foreach (const QVariant &property, objectData.properties) {
const QmlV8ObjectData childObjectData = extractData(property);
if (childObjectData.handle == objectData.handle)
continue;
ConsoleItem *child = constructLogItemTree(item, childObjectData);
if (child) {
const QString text = child->text();
if (childrenFetched.contains(text))
continue;
childrenFetched.insert(text);
item->insertChild(child, sorted);
}
if (objectData.properties.isEmpty())
return new ConsoleItem(ConsoleItem::DefaultType, text);
if (seenHandles.contains(objectData.handle)) {
ConsoleItem *item = new ConsoleItem(ConsoleItem::DefaultType, text,
[this, objectData](ConsoleItem *item)
{
QList<int> newHandles;
constructChildLogItems(item, objectData, newHandles);
});
return item;
}
seenHandles.append(objectData.handle);
ConsoleItem *item = new ConsoleItem(ConsoleItem::DefaultType, text);
constructChildLogItems(item, objectData, seenHandles);
seenHandles.removeLast();
return item;
}
......@@ -2397,14 +2489,22 @@ void QmlEnginePrivate::insertSubItems(WatchItem *parent, const QVariantList &pro
void QmlEnginePrivate::handleExecuteDebuggerCommand(const QVariantMap &response)
{
QmlV8ObjectData body = extractData(response.value(_(BODY)));
if (auto consoleManager = ConsoleManagerInterface::instance()) {
if (ConsoleItem *item = constructLogItemTree(consoleManager->rootItem(), body))
consoleManager->printToConsolePane(item);
auto consoleManager = ConsoleManagerInterface::instance();
if (!consoleManager)
return;
auto it = response.constFind(_(SUCCESS));
if (it != response.constEnd() && it.value().toBool()) {
consoleManager->printToConsolePane(constructLogItemTree(
extractData(response.value(_(BODY)))));