Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include "Project.h"
28#include <AK/FileSystemPath.h>
29#include <AK/QuickSort.h>
30#include <AK/StringBuilder.h>
31#include <LibCore/File.h>
32#include <stdio.h>
33#include <string.h>
34#include <sys/stat.h>
35
36struct Project::ProjectTreeNode : public RefCounted<ProjectTreeNode> {
37 enum class Type {
38 Invalid,
39 Project,
40 Directory,
41 File,
42 };
43
44 ProjectTreeNode& find_or_create_subdirectory(const String& name)
45 {
46 for (auto& child : children) {
47 if (child->type == Type::Directory && child->name == name)
48 return *child;
49 }
50 auto new_child = adopt(*new ProjectTreeNode);
51 new_child->type = Type::Directory;
52 new_child->name = name;
53 new_child->parent = this;
54 auto* ptr = new_child.ptr();
55 children.append(move(new_child));
56 return *ptr;
57 }
58
59 void sort()
60 {
61 if (type == Type::File)
62 return;
63 quick_sort(children.begin(), children.end(), [](auto& a, auto& b) {
64 return a->name < b->name;
65 });
66 for (auto& child : children)
67 child->sort();
68 }
69
70 Type type { Type::Invalid };
71 String name;
72 String path;
73 Vector<NonnullRefPtr<ProjectTreeNode>> children;
74 ProjectTreeNode* parent { nullptr };
75};
76
77class ProjectModel final : public GUI::Model {
78public:
79 explicit ProjectModel(Project& project)
80 : m_project(project)
81 {
82 }
83
84 virtual int row_count(const GUI::ModelIndex& index) const override
85 {
86 if (!index.is_valid())
87 return 1;
88 auto* node = static_cast<Project::ProjectTreeNode*>(index.internal_data());
89 return node->children.size();
90 }
91
92 virtual int column_count(const GUI::ModelIndex&) const override
93 {
94 return 1;
95 }
96
97 virtual GUI::Variant data(const GUI::ModelIndex& index, Role role = Role::Display) const override
98 {
99 auto* node = static_cast<Project::ProjectTreeNode*>(index.internal_data());
100 if (role == Role::Display) {
101 return node->name;
102 }
103 if (role == Role::Custom) {
104 return node->path;
105 }
106 if (role == Role::Icon) {
107 if (node->type == Project::ProjectTreeNode::Type::Project)
108 return m_project.m_project_icon;
109 if (node->type == Project::ProjectTreeNode::Type::Directory)
110 return m_project.m_directory_icon;
111 if (node->name.ends_with(".cpp"))
112 return m_project.m_cplusplus_icon;
113 if (node->name.ends_with(".h"))
114 return m_project.m_header_icon;
115 return m_project.m_file_icon;
116 }
117 if (role == Role::Font) {
118 extern String g_currently_open_file;
119 if (node->name == g_currently_open_file)
120 return Gfx::Font::default_bold_font();
121 return {};
122 }
123 return {};
124 }
125
126 virtual GUI::ModelIndex index(int row, int column = 0, const GUI::ModelIndex& parent = GUI::ModelIndex()) const override
127 {
128 if (!parent.is_valid()) {
129 return create_index(row, column, &m_project.root_node());
130 }
131 auto& node = *static_cast<Project::ProjectTreeNode*>(parent.internal_data());
132 return create_index(row, column, node.children.at(row).ptr());
133 }
134
135 GUI::ModelIndex parent_index(const GUI::ModelIndex& index) const override
136 {
137 if (!index.is_valid())
138 return {};
139 auto& node = *static_cast<Project::ProjectTreeNode*>(index.internal_data());
140 if (!node.parent)
141 return {};
142
143 if (!node.parent->parent) {
144 return create_index(0, 0, &m_project.root_node());
145 ASSERT_NOT_REACHED();
146 return {};
147 }
148
149 for (size_t row = 0; row < node.parent->parent->children.size(); ++row) {
150 if (node.parent->parent->children[row].ptr() == node.parent)
151 return create_index(row, 0, node.parent);
152 }
153
154 ASSERT_NOT_REACHED();
155 return {};
156 }
157
158 virtual void update() override
159 {
160 did_update();
161 }
162
163private:
164 Project& m_project;
165};
166
167Project::Project(const String& path, Vector<String>&& filenames)
168 : m_path(path)
169{
170 m_name = FileSystemPath(m_path).basename();
171
172 m_file_icon = GIcon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-unknown.png"));
173 m_cplusplus_icon = GIcon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-cplusplus.png"));
174 m_header_icon = GIcon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-header.png"));
175 m_directory_icon = GIcon(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-folder.png"));
176 m_project_icon = GIcon(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-hack-studio.png"));
177
178 for (auto& filename : filenames) {
179 m_files.append(ProjectFile::construct_with_name(filename));
180 }
181
182 m_model = adopt(*new ProjectModel(*this));
183
184 rebuild_tree();
185}
186
187Project::~Project()
188{
189}
190
191OwnPtr<Project> Project::load_from_file(const String& path)
192{
193 auto file = Core::File::construct(path);
194 if (!file->open(Core::File::ReadOnly))
195 return nullptr;
196
197 Vector<String> files;
198 for (;;) {
199 auto line = file->read_line(1024);
200 if (line.is_null())
201 break;
202 files.append(String::copy(line, Chomp));
203 }
204
205 return OwnPtr(new Project(path, move(files)));
206}
207
208bool Project::add_file(const String& filename)
209{
210 m_files.append(ProjectFile::construct_with_name(filename));
211 rebuild_tree();
212 m_model->update();
213 return save();
214}
215
216bool Project::remove_file(const String& filename)
217{
218 if (!get_file(filename))
219 return false;
220 m_files.remove_first_matching([filename](auto& file) { return file->name() == filename; });
221 rebuild_tree();
222 m_model->update();
223 return save();
224}
225
226bool Project::save()
227{
228 auto project_file = Core::File::construct(m_path);
229 if (!project_file->open(Core::File::WriteOnly))
230 return false;
231
232 for (auto& file : m_files) {
233 // FIXME: Check for error here. IODevice::printf() needs some work on error reporting.
234 project_file->printf("%s\n", file.name().characters());
235 }
236
237 if (!project_file->close())
238 return false;
239
240 return true;
241}
242
243ProjectFile* Project::get_file(const String& filename)
244{
245 for (auto& file : m_files) {
246 if (FileSystemPath(file.name()).string() == FileSystemPath(filename).string())
247 return &file;
248 }
249 return nullptr;
250}
251
252void Project::rebuild_tree()
253{
254 auto root = adopt(*new ProjectTreeNode);
255 root->name = m_name;
256 root->type = ProjectTreeNode::Type::Project;
257
258 for (auto& file : m_files) {
259 FileSystemPath path(file.name());
260 ProjectTreeNode* current = root.ptr();
261 StringBuilder partial_path;
262
263 for (size_t i = 0; i < path.parts().size(); ++i) {
264 auto& part = path.parts().at(i);
265 if (part == ".")
266 continue;
267 if (i != path.parts().size() - 1) {
268 current = ¤t->find_or_create_subdirectory(part);
269 continue;
270 }
271 struct stat st;
272 if (lstat(path.string().characters(), &st) < 0)
273 continue;
274
275 if (S_ISDIR(st.st_mode)) {
276 current = ¤t->find_or_create_subdirectory(part);
277 continue;
278 }
279 auto file_node = adopt(*new ProjectTreeNode);
280 file_node->name = part;
281 file_node->path = path.string();
282 file_node->type = Project::ProjectTreeNode::Type::File;
283 file_node->parent = current;
284 current->children.append(move(file_node));
285 break;
286 }
287 }
288
289 root->sort();
290
291#if 0
292 Function<void(ProjectTreeNode&, int indent)> dump_tree = [&](ProjectTreeNode& node, int indent) {
293 for (int i = 0; i < indent; ++i)
294 printf(" ");
295 if (node.name.is_null())
296 printf("(null)\n");
297 else
298 printf("%s\n", node.name.characters());
299 for (auto& child : node.children) {
300 dump_tree(*child, indent + 2);
301 }
302 };
303
304 dump_tree(*root, 0);
305#endif
306
307 m_root_node = move(root);
308 m_model->update();
309}