mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-20 17:58:18 +03:00
HackStudio: Add ClassView tab for viewing classes in a tree structure
This enables the user to view and navigate classes with a TreeView that is updated by the LanguageServer as it parses the code. It offers a new neat way to view the project's structure :^)
This commit is contained in:
parent
5adfcd54d8
commit
f52c3cabcf
Notes:
sideshowbarker
2024-07-18 20:25:52 +09:00
Author: https://github.com/itamar8910 Commit: https://github.com/SerenityOS/serenity/commit/f52c3cabcf3 Pull-request: https://github.com/SerenityOS/serenity/pull/6216
@ -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
|
||||
|
198
Userland/DevTools/HackStudio/ClassViewWidget.cpp
Normal file
198
Userland/DevTools/HackStudio/ClassViewWidget.cpp
Normal file
@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright (c) 2021, Itamar S. <itamar8910@gmail.com>
|
||||
* 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 <LibGUI/BoxLayout.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
ClassViewWidget::ClassViewWidget()
|
||||
{
|
||||
set_layout<GUI::VerticalBoxLayout>();
|
||||
m_class_tree = add<GUI::TreeView>();
|
||||
|
||||
m_class_tree->on_selection = [this](auto& index) {
|
||||
if (!index.is_valid())
|
||||
return;
|
||||
|
||||
auto* node = static_cast<const ClassViewNode*>(index.internal_data());
|
||||
if (!node->declaration)
|
||||
return;
|
||||
|
||||
open_file(node->declaration->position.file, node->declaration->position.line, node->declaration->position.column);
|
||||
};
|
||||
}
|
||||
|
||||
RefPtr<ClassViewModel> 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<ClassViewNode*>(index.internal_data());
|
||||
return node->children.size();
|
||||
}
|
||||
|
||||
GUI::Variant ClassViewModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
|
||||
{
|
||||
auto* node = static_cast<const ClassViewNode*>(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<const ClassViewNode*>(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<const ClassViewNode*>(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<ClassViewNode>(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<ClassViewNode>(scope));
|
||||
parent->children.last().parent = parent;
|
||||
parent = &parent->children.last();
|
||||
}
|
||||
}
|
||||
|
||||
NonnullOwnPtrVector<ClassViewNode>* 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<ClassViewNode>(decl.name));
|
||||
children_of_parent->last().declaration = &decl;
|
||||
children_of_parent->last().parent = parent;
|
||||
}
|
||||
}
|
||||
|
||||
void ClassViewWidget::refresh()
|
||||
{
|
||||
m_class_tree->set_model(ClassViewModel::create());
|
||||
}
|
||||
|
||||
}
|
78
Userland/DevTools/HackStudio/ClassViewWidget.h
Normal file
78
Userland/DevTools/HackStudio/ClassViewWidget.h
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2021, Itamar S. <itamar8910@gmail.com>
|
||||
* 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 <AK/StringView.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGUI/AutocompleteProvider.h>
|
||||
#include <LibGUI/TreeView.h>
|
||||
#include <LibGUI/Widget.h>
|
||||
|
||||
namespace HackStudio {
|
||||
|
||||
class ClassViewWidget final : public GUI::Widget {
|
||||
C_OBJECT(ClassViewWidget)
|
||||
public:
|
||||
virtual ~ClassViewWidget() override { }
|
||||
ClassViewWidget();
|
||||
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
RefPtr<GUI::TreeView> 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<ClassViewNode> children;
|
||||
ClassViewNode* parent { nullptr };
|
||||
|
||||
explicit ClassViewNode(const StringView& name)
|
||||
: name(name) {};
|
||||
};
|
||||
|
||||
class ClassViewModel final : public GUI::Model {
|
||||
public:
|
||||
static RefPtr<ClassViewModel> 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<ClassViewNode> m_root_scope;
|
||||
};
|
||||
|
||||
}
|
@ -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<GUI::VerticalSplitter>();
|
||||
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<GitWidget*>(git_widget)->refresh();
|
||||
if (StringView { "GitWidget" } == active_widget->class_name())
|
||||
reinterpret_cast<GitWidget*>(active_widget)->refresh();
|
||||
}
|
||||
|
||||
void HackStudioWidget::open_project(const String& root_path)
|
||||
@ -743,9 +743,8 @@ void HackStudioWidget::set_current_editor_wrapper(RefPtr<EditorWrapper> 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<GUI::TreeView>();
|
||||
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<GUI::TabWidget>();
|
||||
m_project_tab->set_tab_position(GUI::TabWidget::TabPosition::Bottom);
|
||||
m_project_tree_view = m_project_tab->add_tab<GUI::TreeView>("Files");
|
||||
configure_project_tree_view();
|
||||
|
||||
m_class_view = m_project_tab->add_tab<ClassViewWidget>("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");
|
||||
|
@ -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<GUI::TreeView> m_form_widget_tree_view;
|
||||
RefPtr<DiffViewer> m_diff_viewer;
|
||||
RefPtr<GitWidget> m_git_widget;
|
||||
RefPtr<ClassViewWidget> m_class_view;
|
||||
RefPtr<GUI::Menu> m_project_tree_view_context_menu;
|
||||
RefPtr<GUI::TabWidget> m_action_tab_widget;
|
||||
RefPtr<GUI::TabWidget> m_project_tab;
|
||||
RefPtr<TerminalWrapper> m_terminal_wrapper;
|
||||
RefPtr<Locator> m_locator;
|
||||
RefPtr<FindInFilesWidget> m_find_in_files_widget;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<void(Vector<Entry>)>) = 0;
|
||||
|
Loading…
Reference in New Issue
Block a user