diff --git a/Userland/DevTools/HackStudio/CMakeLists.txt b/Userland/DevTools/HackStudio/CMakeLists.txt index 35288c2233..e2f41f2a10 100644 --- a/Userland/DevTools/HackStudio/CMakeLists.txt +++ b/Userland/DevTools/HackStudio/CMakeLists.txt @@ -5,6 +5,7 @@ compile_gml(Dialogs/NewProjectDialog.gml Dialogs/NewProjectDialogGML.h new_proje set(SOURCES CodeDocument.cpp + ClassViewWidget.cpp CursorTool.cpp Debugger/BacktraceModel.cpp Debugger/DebugInfoWidget.cpp diff --git a/Userland/DevTools/HackStudio/ClassViewWidget.cpp b/Userland/DevTools/HackStudio/ClassViewWidget.cpp new file mode 100644 index 0000000000..bfd98ee39a --- /dev/null +++ b/Userland/DevTools/HackStudio/ClassViewWidget.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2021, Itamar S. + * 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 "ClassViewWidget.h" +#include "HackStudio.h" +#include "ProjectDeclarations.h" +#include + +namespace HackStudio { + +ClassViewWidget::ClassViewWidget() +{ + set_layout(); + m_class_tree = add(); + + m_class_tree->on_selection = [this](auto& index) { + if (!index.is_valid()) + return; + + auto* node = static_cast(index.internal_data()); + if (!node->declaration) + return; + + open_file(node->declaration->position.file, node->declaration->position.line, node->declaration->position.column); + }; +} + +RefPtr ClassViewModel::create() +{ + return adopt(*new ClassViewModel()); +} + +int ClassViewModel::row_count(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return m_root_scope.size(); + auto* node = static_cast(index.internal_data()); + return node->children.size(); +} + +GUI::Variant ClassViewModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const +{ + auto* node = static_cast(index.internal_data()); + switch (role) { + case GUI::ModelRole::Display: { + return node->name; + } + case GUI::ModelRole::Icon: { + if (!node->declaration) + return {}; + auto icon = ProjectDeclarations::get_icon_for(node->declaration->type); + if (icon.has_value()) + return icon.value(); + return {}; + } + default: + return {}; + } +} + +GUI::ModelIndex ClassViewModel::parent_index(const GUI::ModelIndex& index) const +{ + if (!index.is_valid()) + return {}; + auto* child = static_cast(index.internal_data()); + auto* parent = child->parent; + if (parent == nullptr) + return {}; + + if (parent->parent == nullptr) { + for (size_t row = 0; row < m_root_scope.size(); row++) { + if (m_root_scope.ptr_at(row).ptr() == parent) + return create_index(row, 0, parent); + } + VERIFY_NOT_REACHED(); + } + for (size_t row = 0; row < parent->parent->children.size(); row++) { + ClassViewNode* child_at_row = parent->parent->children.ptr_at(row).ptr(); + if (child_at_row == parent) + return create_index(row, 0, parent); + } + VERIFY_NOT_REACHED(); +} + +GUI::ModelIndex ClassViewModel::index(int row, int column, const GUI::ModelIndex& parent_index) const +{ + if (!parent_index.is_valid()) + return create_index(row, column, &m_root_scope[row]); + auto* parent = static_cast(parent_index.internal_data()); + auto* child = &parent->children[row]; + return create_index(row, column, child); +} + +ClassViewModel::ClassViewModel() +{ + m_root_scope.clear(); + ProjectDeclarations::the().for_each_declared_symbol([this](auto& decl) { + if (decl.type == GUI::AutocompleteProvider::DeclarationType::Class + || decl.type == GUI::AutocompleteProvider::DeclarationType::Struct + || decl.type == GUI::AutocompleteProvider::DeclarationType::Member + || decl.type == GUI::AutocompleteProvider::DeclarationType::Namespace) { + add_declaration(decl); + } + }); +} + +void ClassViewModel::add_declaration(const GUI::AutocompleteProvider::Declaration& decl) +{ + ClassViewNode* parent = nullptr; + auto scope_parts = decl.scope.view().split_view("::"); + + if (!scope_parts.is_empty()) { + // Traverse declarations tree to the parent of 'decl' + for (auto& node : m_root_scope) { + if (node.name == scope_parts.first()) + parent = &node; + } + + if (parent == nullptr) { + m_root_scope.append(make(scope_parts.first())); + parent = &m_root_scope.last(); + } + + for (size_t i = 1; i < scope_parts.size(); ++i) { + auto& scope = scope_parts[i]; + ClassViewNode* next { nullptr }; + for (auto& child : parent->children) { + VERIFY(child.declaration); + if (child.declaration->name == scope) { + next = &child; + break; + } + } + + if (next) { + parent = next; + continue; + } + + parent->children.append(make(scope)); + parent->children.last().parent = parent; + parent = &parent->children.last(); + } + } + + NonnullOwnPtrVector* children_of_parent = nullptr; + if (parent) { + children_of_parent = &parent->children; + } else { + children_of_parent = &m_root_scope; + } + + bool already_exists = false; + for (auto& child : *children_of_parent) { + if (child.name == decl.name) { + already_exists = true; + if (!child.declaration) { + child.declaration = &decl; + } + break; + } + } + if (!already_exists) { + children_of_parent->append(make(decl.name)); + children_of_parent->last().declaration = &decl; + children_of_parent->last().parent = parent; + } +} + +void ClassViewWidget::refresh() +{ + m_class_tree->set_model(ClassViewModel::create()); +} + +} diff --git a/Userland/DevTools/HackStudio/ClassViewWidget.h b/Userland/DevTools/HackStudio/ClassViewWidget.h new file mode 100644 index 0000000000..0a903dbd04 --- /dev/null +++ b/Userland/DevTools/HackStudio/ClassViewWidget.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2021, Itamar S. + * 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 +#include + +namespace HackStudio { + +class ClassViewWidget final : public GUI::Widget { + C_OBJECT(ClassViewWidget) +public: + virtual ~ClassViewWidget() override { } + ClassViewWidget(); + + void refresh(); + +private: + RefPtr m_class_tree; +}; + +// Note: A ClassViewNode stores a raw pointer to the Declaration from ProjectDeclarations and a StringView into its name. +// We should take care to update the ClassViewModel whenever the declarations change, because otherwise we may be holding pointers to freed memory. +// This is currently achieved with the on_update callback of ProjectDeclarations. +struct ClassViewNode { + StringView name; + const GUI::AutocompleteProvider::Declaration* declaration { nullptr }; + NonnullOwnPtrVector children; + ClassViewNode* parent { nullptr }; + + explicit ClassViewNode(const StringView& name) + : name(name) {}; +}; + +class ClassViewModel final : public GUI::Model { +public: + static RefPtr create(); + virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override; + virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override { return 1; } + virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole role) const override; + virtual void update() override { did_update(); } + virtual GUI::ModelIndex parent_index(const GUI::ModelIndex&) const override; + virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& parent_index = GUI::ModelIndex()) const override; + +private: + explicit ClassViewModel(); + void add_declaration(const GUI::AutocompleteProvider::Declaration&); + NonnullOwnPtrVector m_root_scope; +}; + +} diff --git a/Userland/DevTools/HackStudio/HackStudioWidget.cpp b/Userland/DevTools/HackStudio/HackStudioWidget.cpp index 623643f63c..cbb599eca3 100644 --- a/Userland/DevTools/HackStudio/HackStudioWidget.cpp +++ b/Userland/DevTools/HackStudio/HackStudioWidget.cpp @@ -43,6 +43,7 @@ #include "HackStudioWidget.h" #include "Locator.h" #include "Project.h" +#include "ProjectDeclarations.h" #include "TerminalWrapper.h" #include "WidgetTool.h" #include "WidgetTreeModel.h" @@ -108,7 +109,7 @@ HackStudioWidget::HackStudioWidget(const String& path_to_project) auto& left_hand_splitter = outer_splitter.add(); left_hand_splitter.set_fixed_width(150); - create_project_tree_view(left_hand_splitter); + create_project_tab(left_hand_splitter); m_project_tree_view_context_menu = create_project_tree_view_context_menu(); create_open_files_view(left_hand_splitter); @@ -177,12 +178,11 @@ void HackStudioWidget::update_actions() void HackStudioWidget::on_action_tab_change() { update_actions(); - auto git_widget = m_action_tab_widget->active_widget(); - if (!git_widget) + auto active_widget = m_action_tab_widget->active_widget(); + if (!active_widget) return; - if (StringView { "GitWidget" } != git_widget->class_name()) - return; - reinterpret_cast(git_widget)->refresh(); + if (StringView { "GitWidget" } == active_widget->class_name()) + reinterpret_cast(active_widget)->refresh(); } void HackStudioWidget::open_project(const String& root_path) @@ -743,9 +743,8 @@ void HackStudioWidget::set_current_editor_wrapper(RefPtr editor_w m_current_editor_wrapper = editor_wrapper; } -void HackStudioWidget::create_project_tree_view(GUI::Widget& parent) +void HackStudioWidget::configure_project_tree_view() { - m_project_tree_view = parent.add(); m_project_tree_view->set_model(m_project->model()); m_project_tree_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection); @@ -937,6 +936,20 @@ void HackStudioWidget::create_action_tab(GUI::Widget& parent) }); } +void HackStudioWidget::create_project_tab(GUI::Widget& parent) +{ + m_project_tab = parent.add(); + m_project_tab->set_tab_position(GUI::TabWidget::TabPosition::Bottom); + m_project_tree_view = m_project_tab->add_tab("Files"); + configure_project_tree_view(); + + m_class_view = m_project_tab->add_tab("ClassView"); + + ProjectDeclarations::the().on_update = [this]() { + m_class_view->refresh(); + }; +} + void HackStudioWidget::create_app_menubar(GUI::MenuBar& menubar) { auto& file_menu = menubar.add_menu("&File"); diff --git a/Userland/DevTools/HackStudio/HackStudioWidget.h b/Userland/DevTools/HackStudio/HackStudioWidget.h index 1b363ca199..210fc964fb 100644 --- a/Userland/DevTools/HackStudio/HackStudioWidget.h +++ b/Userland/DevTools/HackStudio/HackStudioWidget.h @@ -28,6 +28,7 @@ #pragma once +#include "ClassViewWidget.h" #include "Debugger/DebugInfoWidget.h" #include "Debugger/DisassemblyWidget.h" #include "EditorWrapper.h" @@ -112,7 +113,6 @@ private: void reveal_action_tab(GUI::Widget&); void initialize_debugger(); - void create_project_tree_view(GUI::Widget& parent); void create_open_files_view(GUI::Widget& parent); void create_form_editor(GUI::Widget& parent); void create_toolbar(GUI::Widget& parent); @@ -123,6 +123,8 @@ private: void create_build_menubar(GUI::MenuBar&); void create_view_menubar(GUI::MenuBar&); void create_help_menubar(GUI::MenuBar&); + void create_project_tab(GUI::Widget& parent); + void configure_project_tree_view(); void run(TerminalWrapper& wrapper); void build(TerminalWrapper& wrapper); @@ -150,8 +152,10 @@ private: RefPtr m_form_widget_tree_view; RefPtr m_diff_viewer; RefPtr m_git_widget; + RefPtr m_class_view; RefPtr m_project_tree_view_context_menu; RefPtr m_action_tab_widget; + RefPtr m_project_tab; RefPtr m_terminal_wrapper; RefPtr m_locator; RefPtr m_find_in_files_widget; diff --git a/Userland/Libraries/LibGUI/AutocompleteProvider.cpp b/Userland/Libraries/LibGUI/AutocompleteProvider.cpp index 48cde66e64..9c19d6c919 100644 --- a/Userland/Libraries/LibGUI/AutocompleteProvider.cpp +++ b/Userland/Libraries/LibGUI/AutocompleteProvider.cpp @@ -200,4 +200,14 @@ void AutocompleteBox::apply_suggestion() m_editor->insert_at_cursor_or_replace_selection(completion); } +bool AutocompleteProvider::Declaration::operator==(const AutocompleteProvider::Declaration& other) const +{ + return name == other.name && position == other.position && type == other.type && scope == other.scope; +} + +bool AutocompleteProvider::ProjectLocation::operator==(const ProjectLocation& other) const +{ + return file == other.file && line == other.line && column == other.column; +} + } diff --git a/Userland/Libraries/LibGUI/AutocompleteProvider.h b/Userland/Libraries/LibGUI/AutocompleteProvider.h index aed7a4d6d9..03facd0556 100644 --- a/Userland/Libraries/LibGUI/AutocompleteProvider.h +++ b/Userland/Libraries/LibGUI/AutocompleteProvider.h @@ -61,6 +61,8 @@ public: String file; size_t line { 0 }; size_t column { 0 }; + + bool operator==(const ProjectLocation&) const; }; enum class DeclarationType { @@ -78,6 +80,8 @@ public: ProjectLocation position; DeclarationType type; String scope; + + bool operator==(const Declaration&) const; }; virtual void provide_completions(Function)>) = 0;