1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-23 18:55:08 +00:00

HackStudio: Rethink the "project" concept to be about a directory

Instead of having .hsp files that determine which files are members
of a project, a project is now an entire directory tree instead.

This feels a lot less cumbersome to work with, and removes a fair
amount of busywork that would otherwise be expected from the user.

This patch refactors large parts of HackStudio to implement the new
way of thinking. I've probably missed some details here and there,
but generally I think it's pretty OK.
This commit is contained in:
Andreas Kling 2020-12-10 18:59:03 +01:00
parent 5d0fda3d39
commit dd3e6451ac
6 changed files with 86 additions and 468 deletions

View file

@ -26,368 +26,59 @@
#include "Project.h"
#include "HackStudio.h"
#include <AK/LexicalPath.h>
#include <AK/QuickSort.h>
#include <AK/StringBuilder.h>
#include <LibCore/DirIterator.h>
#include <LibCore/File.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
namespace HackStudio {
struct Project::ProjectTreeNode : public RefCounted<ProjectTreeNode> {
enum class Type {
Invalid,
Project,
Directory,
File,
};
ProjectTreeNode& find_or_create_subdirectory(const String& name)
{
for (auto& child : children) {
if (child->type == Type::Directory && child->name == name)
return *child;
}
auto new_child = adopt(*new ProjectTreeNode);
new_child->type = Type::Directory;
new_child->name = name;
new_child->parent = this;
auto* ptr = new_child.ptr();
children.append(move(new_child));
return *ptr;
}
void sort()
{
if (type == Type::File)
return;
quick_sort(children, [](auto& a, auto& b) {
return a->name < b->name;
});
for (auto& child : children)
child->sort();
}
Type type { Type::Invalid };
String name;
String path;
Vector<NonnullRefPtr<ProjectTreeNode>> children;
ProjectTreeNode* parent { nullptr };
};
class ProjectModel final : public GUI::Model {
public:
explicit ProjectModel(Project& project)
: m_project(project)
{
}
virtual int row_count(const GUI::ModelIndex& index) const override
{
if (!index.is_valid())
return 1;
auto* node = static_cast<Project::ProjectTreeNode*>(index.internal_data());
return node->children.size();
}
virtual int column_count(const GUI::ModelIndex&) const override
{
return 1;
}
virtual GUI::Variant data(const GUI::ModelIndex& index, GUI::ModelRole role) const override
{
auto* node = static_cast<Project::ProjectTreeNode*>(index.internal_data());
if (role == GUI::ModelRole::Display) {
return node->name;
}
if (role == GUI::ModelRole::Custom) {
return node->path;
}
if (role == GUI::ModelRole::Icon) {
if (node->type == Project::ProjectTreeNode::Type::Project)
return m_project.m_project_icon;
if (node->type == Project::ProjectTreeNode::Type::Directory)
return m_project.m_directory_icon;
if (node->name.ends_with(".cpp"))
return m_project.m_cplusplus_icon;
if (node->name.ends_with(".frm"))
return m_project.m_form_icon;
if (node->name.ends_with(".h"))
return m_project.m_header_icon;
if (node->name.ends_with(".hsp"))
return m_project.m_hackstudio_icon;
if (node->name.ends_with(".js"))
return m_project.m_javascript_icon;
return m_project.m_file_icon;
}
if (role == GUI::ModelRole::Font) {
if (node->name == currently_open_file())
return Gfx::Font::default_bold_font();
return {};
}
return {};
}
virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& parent = GUI::ModelIndex()) const override
{
if (!parent.is_valid()) {
return create_index(row, column, &m_project.root_node());
}
auto& node = *static_cast<Project::ProjectTreeNode*>(parent.internal_data());
return create_index(row, column, node.children.at(row).ptr());
}
GUI::ModelIndex parent_index(const GUI::ModelIndex& index) const override
{
if (!index.is_valid())
return {};
auto& node = *static_cast<Project::ProjectTreeNode*>(index.internal_data());
if (!node.parent)
return {};
if (!node.parent->parent) {
return create_index(0, 0, &m_project.root_node());
ASSERT_NOT_REACHED();
return {};
}
for (size_t row = 0; row < node.parent->parent->children.size(); ++row) {
if (node.parent->parent->children[row].ptr() == node.parent)
return create_index(row, 0, node.parent);
}
ASSERT_NOT_REACHED();
return {};
}
virtual void update() override
{
did_update();
}
private:
Project& m_project;
};
Project::Project(const String& path, Vector<String>&& filenames)
: m_path(path)
Project::Project(const String& root_path)
: m_root_path(root_path)
{
m_name = LexicalPath(m_path).basename();
m_file_icon = GUI::Icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-unknown.png"));
m_cplusplus_icon = GUI::Icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-cplusplus.png"));
m_header_icon = GUI::Icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-header.png"));
m_directory_icon = GUI::Icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-folder.png"));
m_project_icon = GUI::Icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/hackstudio-project.png"));
m_javascript_icon = GUI::Icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-javascript.png"));
m_hackstudio_icon = GUI::Icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-hackstudio.png"));
m_form_icon = GUI::Icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-form.png"));
for (auto& filename : filenames) {
m_files.append(ProjectFile::construct_with_name(filename));
}
m_model = adopt(*new ProjectModel(*this));
rebuild_tree();
m_model = GUI::FileSystemModel::create(root_path, GUI::FileSystemModel::Mode::FilesAndDirectories);
}
Project::~Project()
{
}
OwnPtr<Project> Project::load_from_file(const String& path)
OwnPtr<Project> Project::open_with_root_path(const String& root_path)
{
auto file = Core::File::construct(path);
if (!file->open(Core::File::ReadOnly))
if (!Core::File::is_directory(root_path))
return nullptr;
auto type = ProjectType::Cpp;
Vector<String> files;
auto add_glob = [&](String path) {
auto split = path.split('*', true);
for (auto& item : split) {
dbg() << item;
}
ASSERT(split.size() == 2);
auto cwd = getcwd(nullptr, 0);
Core::DirIterator it(cwd, Core::DirIterator::Flags::SkipParentAndBaseDir);
while (it.has_next()) {
auto path = it.next_path();
if (!split[0].is_empty() && !path.starts_with(split[0]))
continue;
if (!split[1].is_empty() && !path.ends_with(split[1]))
continue;
files.append(path);
}
};
for (;;) {
auto line = file->read_line(1024);
if (line.is_null())
break;
auto path = String::copy(line, Chomp);
if (path.contains("*"))
add_glob(path);
else
files.append(path);
}
for (auto& file : files) {
if (file.ends_with(".js")) {
type = ProjectType::JavaScript;
break;
}
}
quick_sort(files);
auto project = adopt_own(*new Project(path, move(files)));
project->m_type = type;
return project;
return adopt_own(*new Project(root_path));
}
bool Project::add_file(const String& filename)
template<typename Callback>
static void traverse_model(const GUI::FileSystemModel& model, const GUI::ModelIndex& index, Callback callback)
{
m_files.append(ProjectFile::construct_with_name(filename));
rebuild_tree();
m_model->update();
return save();
}
bool Project::remove_file(const String& filename)
{
if (!get_file(filename))
return false;
m_files.remove_first_matching([filename](auto& file) { return file->name() == filename; });
rebuild_tree();
m_model->update();
return save();
}
bool Project::save()
{
auto project_file = Core::File::construct(m_path);
if (!project_file->open(Core::File::WriteOnly))
return false;
for (auto& file : m_files) {
// FIXME: Check for error here. IODevice::printf() needs some work on error reporting.
project_file->printf("%s\n", file.name().characters());
if (index.is_valid())
callback(index);
auto row_count = model.row_count(index);
if (!row_count)
return;
for (int row = 0; row < row_count; ++row) {
auto child_index = model.index(row, GUI::FileSystemModel::Column::Name, index);
traverse_model(model, child_index, callback);
}
if (!project_file->close())
return false;
return true;
}
RefPtr<ProjectFile> Project::get_file(const String& filename)
void Project::for_each_text_file(Function<void(const ProjectFile&)> callback) const
{
traverse_model(model(), {}, [&](auto& index) {
auto file = get_file(model().full_path(index));
if (file)
callback(*file);
});
}
RefPtr<ProjectFile> Project::get_file(const String& path) const
{
for (auto& file : m_files) {
if (LexicalPath(file.name()).string() == LexicalPath(filename).string())
return &file;
if (file.name() == path)
return file;
}
return nullptr;
}
String Project::default_file() const
{
if (m_files.size() > 0) {
if (m_type != ProjectType::Unknown) {
StringView extension;
switch (m_type) {
case ProjectType::Cpp:
extension = ".cpp";
break;
case ProjectType::JavaScript:
extension = ".js";
break;
default:
ASSERT_NOT_REACHED();
}
auto project_file = m_files.find([&](auto project_file) {
return project_file->name().ends_with(extension);
});
if (!project_file.is_end()) {
auto& file = *project_file;
return file->name();
}
}
return m_files.first().name();
}
ASSERT_NOT_REACHED();
}
void Project::rebuild_tree()
{
auto root = adopt(*new ProjectTreeNode);
root->name = m_name;
root->type = ProjectTreeNode::Type::Project;
for (auto& file : m_files) {
LexicalPath path(file.name());
ProjectTreeNode* current = root.ptr();
StringBuilder partial_path;
for (size_t i = 0; i < path.parts().size(); ++i) {
auto& part = path.parts().at(i);
if (part == ".")
continue;
if (i != path.parts().size() - 1) {
current = &current->find_or_create_subdirectory(part);
continue;
}
struct stat st;
if (lstat(path.string().characters(), &st) == 0) {
if (S_ISDIR(st.st_mode)) {
current = &current->find_or_create_subdirectory(part);
continue;
}
}
auto file_node = adopt(*new ProjectTreeNode);
file_node->name = part;
file_node->path = path.string();
file_node->type = Project::ProjectTreeNode::Type::File;
file_node->parent = current;
current->children.append(move(file_node));
break;
}
}
root->sort();
#if 0
Function<void(ProjectTreeNode&, int indent)> dump_tree = [&](ProjectTreeNode& node, int indent) {
for (int i = 0; i < indent; ++i)
out(" ");
if (node.name.is_null())
outln("(null)");
else
outln("{}", node.name);
for (auto& child : node.children) {
dump_tree(*child, indent + 2);
}
};
dump_tree(*root, 0);
#endif
m_root_node = move(root);
m_model->update();
auto file = ProjectFile::construct_with_name(path);
m_files.append(file);
return file;
}
}