diff --git a/AK/JsonPath.cpp b/AK/JsonPath.cpp new file mode 100644 index 0000000000..612c69cd2b --- /dev/null +++ b/AK/JsonPath.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include + +namespace AK { + +JsonPathElement JsonPathElement::any_array_element { Kind::AnyIndex }; +JsonPathElement JsonPathElement::any_object_element { Kind::AnyKey }; + +JsonValue JsonPath::resolve(const JsonValue& top_root) const +{ + auto root = top_root; + for (auto& element : *this) { + switch (element.kind()) { + case JsonPathElement::Kind::Key: + root = JsonValue { root.as_object().get(element.key()) }; + break; + case JsonPathElement::Kind::Index: + root = JsonValue { root.as_array().at(element.index()) }; + break; + default: + ASSERT_NOT_REACHED(); + } + } + return root; +} + +String JsonPath::to_string() const +{ + StringBuilder builder; + builder.append("{ ."); + for (auto& el : *this) { + builder.append(" > "); + builder.append(el.to_string()); + } + builder.append(" }"); + return builder.to_string(); +} + +} diff --git a/AK/JsonPath.h b/AK/JsonPath.h new file mode 100644 index 0000000000..f6c7432b44 --- /dev/null +++ b/AK/JsonPath.h @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include +#include +#include +#include + +namespace AK { + +class JsonPathElement { +public: + enum class Kind { + Key, + Index, + AnyIndex, + AnyKey, + }; + + JsonPathElement(size_t index) + : m_kind(Kind::Index) + , m_index(index) + { + } + + JsonPathElement(const StringView& key) + : m_kind(Kind::Key) + , m_key(key) + { + } + + Kind kind() const { return m_kind; } + const String& key() const + { + ASSERT(m_kind == Kind::Key); + return m_key; + } + + size_t index() const + { + ASSERT(m_kind == Kind::Index); + return m_index; + } + + String to_string() const + { + switch (m_kind) { + case Kind::Key: + return key(); + case Kind::Index: + return String::number(index()); + default: + return "*"; + } + } + + static JsonPathElement any_array_element; + static JsonPathElement any_object_element; + + bool operator==(const JsonPathElement& other) const + { + switch (other.kind()) { + case Kind::Key: + return (m_kind == Kind::Key && other.key() == key()) || m_kind == Kind::AnyKey; + case Kind::Index: + return (m_kind == Kind::Index && other.index() == index()) || m_kind == Kind::AnyIndex; + case Kind::AnyKey: + return m_kind == Kind::Key; + case Kind::AnyIndex: + return m_kind == Kind::Index; + } + return false; + } + bool operator!=(const JsonPathElement& other) const + { + return !(*this == other); + } + +private: + Kind m_kind; + String m_key; + size_t m_index { 0 }; + + JsonPathElement(Kind kind) + : m_kind(kind) + { + } +}; + +class JsonPath : public Vector { +public: + JsonValue resolve(const JsonValue&) const; + String to_string() const; +}; + +} + +using AK::JsonPath; +using AK::JsonPathElement; diff --git a/DevTools/Inspector/RemoteObjectPropertyModel.cpp b/DevTools/Inspector/RemoteObjectPropertyModel.cpp index adb650235d..78f39bff05 100644 --- a/DevTools/Inspector/RemoteObjectPropertyModel.cpp +++ b/DevTools/Inspector/RemoteObjectPropertyModel.cpp @@ -33,9 +33,22 @@ RemoteObjectPropertyModel::RemoteObjectPropertyModel(RemoteObject& object) { } -int RemoteObjectPropertyModel::row_count(const GUI::ModelIndex&) const +int RemoteObjectPropertyModel::row_count(const GUI::ModelIndex& index) const { - return m_properties.size(); + Function do_count = [&](const JsonValue& value) { + if (value.is_array()) + return value.as_array().size(); + else if (value.is_object()) + return value.as_object().size(); + return 0; + }; + + if (index.is_valid()) { + auto* path = static_cast(index.internal_data()); + return do_count(path->resolve(m_object.json)); + } else { + return do_count(m_object.json); + } } String RemoteObjectPropertyModel::column_name(int column) const @@ -51,13 +64,22 @@ String RemoteObjectPropertyModel::column_name(int column) const GUI::Variant RemoteObjectPropertyModel::data(const GUI::ModelIndex& index, Role role) const { - auto& property = m_properties[index.row()]; + auto* path = static_cast(index.internal_data()); + if (!path) + return {}; + if (role == Role::Display) { switch (index.column()) { case Column::Name: - return property.name; - case Column::Value: - return property.value.to_string(); + return path->last().to_string(); + case Column::Value: { + auto data = path->resolve(m_object.json); + if (data.is_array()) + return String::format("" : "s>"); + if (data.is_object()) + return String::format("" : "s>"); + return data; + } } } return {}; @@ -65,18 +87,151 @@ GUI::Variant RemoteObjectPropertyModel::data(const GUI::ModelIndex& index, Role void RemoteObjectPropertyModel::update() { - m_properties.clear(); - m_object.json.for_each_member([this](auto& name, auto& value) { - m_properties.append({ name, value }); - }); did_update(); } void RemoteObjectPropertyModel::set_data(const GUI::ModelIndex& index, const GUI::Variant& new_value) { - auto& property = m_properties[index.row()]; + if (!index.is_valid()) + return; + + auto* path = static_cast(index.internal_data()); + if (path->size() != 1) + return; + FlatPtr address = m_object.address; - RemoteProcess::the().set_property(address, property.name.to_string(), new_value.to_string()); - property.value = new_value.to_string(); + RemoteProcess::the().set_property(address, path->first().to_string(), new_value.to_string()); did_update(); } + +GUI::ModelIndex RemoteObjectPropertyModel::index(int row, int column, const GUI::ModelIndex& parent) const +{ + const auto& parent_path = parent.is_valid() ? *static_cast(parent.internal_data()) : JsonPath {}; + + auto nth_child = [&](int n, const JsonValue& value) -> const JsonPath* { + auto path = make(); + path->append(parent_path); + int row_index = n; + if (value.is_object()) { + String property_name; + auto& object = value.as_object(); + object.for_each_member([&](auto& name, auto&) { + if (row_index > 0) { + --row_index; + } else if (row_index == 0) { + property_name = name; + --row_index; + } + }); + if (property_name.is_null()) + return nullptr; + + path->append({ property_name }); + m_paths.append(move(path)); + } else if (value.is_array()) { + path->append(JsonPathElement { (size_t)n }); + m_paths.append(move(path)); + } else { + return nullptr; + } + return &m_paths.last(); + }; + + if (!parent.is_valid()) { + if (m_object.json.is_empty()) + return {}; + } + + auto index_path = cached_path_at(row, parent_path); + + if (!index_path) + index_path = nth_child(row, parent_path.resolve(m_object.json)); + + if (!index_path) + return {}; + + return create_index(row, column, index_path); +} + +GUI::ModelIndex RemoteObjectPropertyModel::parent_index(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return index; + + auto path = *static_cast(index.internal_data()); + if (path.is_empty()) + return {}; + + path.take_last(); + if (path.is_empty()) + return {}; + + auto* cpath = find_cached_path(path); + if (cpath) { + int index_in_parent = 0; + if (cpath->last().kind() == JsonPathElement::Kind::Index) + index_in_parent = cpath->last().index(); + else if (cpath->last().kind() == JsonPathElement::Kind::Key) { + auto path_copy = path; + auto last = path_copy.take_last(); + bool found = false; + path_copy.resolve(m_object.json).as_object().for_each_member([&](auto& name, auto&) { + if (!found) { + if (last.key() == name) + found = true; + else + index_in_parent++; + } + }); + } + return create_index(index_in_parent, 0, cpath); + } + + dbg() << "No cached path found for path " << path.to_string(); + return {}; +} + +const JsonPath* RemoteObjectPropertyModel::cached_path_at(int n, const Vector& prefix) const +{ + // FIXME: ModelIndex wants a void*, so we have to keep these + // indices alive, but allocating a new path every time + // we're asked for an index is silly, so we have to look for existing ones first. + const JsonPath* index_path = nullptr; + int row_index = n; + for (auto& path : m_paths) { + if (path.size() != prefix.size() + 1) + continue; + + for (size_t i = 0; i < prefix.size(); ++i) { + if (path[i] != prefix[i]) + goto do_continue; + } + + if (row_index == 0) { + index_path = &path; + break; + } + --row_index; + do_continue:; + } + + return index_path; +}; + +const JsonPath* RemoteObjectPropertyModel::find_cached_path(const Vector& path) const +{ + for (auto& cpath : m_paths) { + if (cpath.size() != path.size()) + continue; + + for (size_t i = 0; i < cpath.size(); ++i) { + if (cpath[i] != path[i]) + goto do_continue; + } + + return &cpath; + do_continue:; + } + + return nullptr; +} diff --git a/DevTools/Inspector/RemoteObjectPropertyModel.h b/DevTools/Inspector/RemoteObjectPropertyModel.h index 377f386cec..36a01b32e6 100644 --- a/DevTools/Inspector/RemoteObjectPropertyModel.h +++ b/DevTools/Inspector/RemoteObjectPropertyModel.h @@ -26,14 +26,16 @@ #pragma once +#include #include +#include #include class RemoteObject; class RemoteObjectPropertyModel final : public GUI::Model { public: - virtual ~RemoteObjectPropertyModel() override {} + virtual ~RemoteObjectPropertyModel() override { } static NonnullRefPtr create(RemoteObject& object) { return adopt(*new RemoteObjectPropertyModel(object)); @@ -52,14 +54,15 @@ public: virtual void set_data(const GUI::ModelIndex&, const GUI::Variant&) override; virtual void update() override; virtual bool is_editable(const GUI::ModelIndex& index) const override { return index.column() == Column::Value; } + virtual GUI::ModelIndex index(int row, int column, const GUI::ModelIndex& parent = GUI::ModelIndex()) const override; + virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override; private: explicit RemoteObjectPropertyModel(RemoteObject&); + const JsonPath* cached_path_at(int n, const Vector& prefix) const; + const JsonPath* find_cached_path(const Vector& path) const; + RemoteObject& m_object; - struct NameAndValue { - JsonValue name; - JsonValue value; - }; - Vector m_properties; + mutable NonnullOwnPtrVector m_paths; }; diff --git a/DevTools/Inspector/main.cpp b/DevTools/Inspector/main.cpp index 49cea918d9..ddded558db 100644 --- a/DevTools/Inspector/main.cpp +++ b/DevTools/Inspector/main.cpp @@ -94,15 +94,15 @@ int main(int argc, char** argv) tree_view.set_model(remote_process.object_graph_model()); tree_view.set_activates_on_selection(true); - auto& properties_table_view = splitter.add(); - properties_table_view.set_editable(true); - properties_table_view.aid_create_editing_delegate = [](auto&) { + auto& properties_tree_view = splitter.add(); + properties_tree_view.set_editable(true); + properties_tree_view.aid_create_editing_delegate = [](auto&) { return make(); }; tree_view.on_activation = [&](auto& index) { auto* remote_object = static_cast(index.internal_data()); - properties_table_view.set_model(remote_object->property_model()); + properties_tree_view.set_model(remote_object->property_model()); remote_process.set_inspected_object(remote_object->address); };