Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2020-2022, Itamar S. <itamar8910@gmail.com>
4 * Copyright (c) 2020-2022, the SerenityOS developers.
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include "HackStudioWidget.h"
10#include "Debugger/DebugInfoWidget.h"
11#include "Debugger/Debugger.h"
12#include "Debugger/DisassemblyWidget.h"
13#include "Dialogs/NewProjectDialog.h"
14#include "Editor.h"
15#include "EditorWrapper.h"
16#include "FindInFilesWidget.h"
17#include "Git/DiffViewer.h"
18#include "Git/GitWidget.h"
19#include "HackStudio.h"
20#include "Locator.h"
21#include "Project.h"
22#include "ProjectDeclarations.h"
23#include "TerminalWrapper.h"
24#include "ToDoEntries.h"
25#include <AK/JsonParser.h>
26#include <AK/LexicalPath.h>
27#include <AK/StringBuilder.h>
28#include <Kernel/API/InodeWatcherEvent.h>
29#include <LibConfig/Client.h>
30#include <LibCore/DeprecatedFile.h>
31#include <LibCore/Event.h>
32#include <LibCore/EventLoop.h>
33#include <LibCore/FileWatcher.h>
34#include <LibCore/System.h>
35#include <LibDebug/DebugSession.h>
36#include <LibDesktop/Launcher.h>
37#include <LibGUI/Action.h>
38#include <LibGUI/ActionGroup.h>
39#include <LibGUI/Application.h>
40#include <LibGUI/BoxLayout.h>
41#include <LibGUI/Button.h>
42#include <LibGUI/Dialog.h>
43#include <LibGUI/EditingEngine.h>
44#include <LibGUI/FilePicker.h>
45#include <LibGUI/FontPicker.h>
46#include <LibGUI/InputBox.h>
47#include <LibGUI/ItemListModel.h>
48#include <LibGUI/Label.h>
49#include <LibGUI/Menu.h>
50#include <LibGUI/Menubar.h>
51#include <LibGUI/MessageBox.h>
52#include <LibGUI/ModelEditingDelegate.h>
53#include <LibGUI/RegularEditingEngine.h>
54#include <LibGUI/Splitter.h>
55#include <LibGUI/StackWidget.h>
56#include <LibGUI/Statusbar.h>
57#include <LibGUI/TabWidget.h>
58#include <LibGUI/TableView.h>
59#include <LibGUI/TextBox.h>
60#include <LibGUI/TextEditor.h>
61#include <LibGUI/Toolbar.h>
62#include <LibGUI/ToolbarContainer.h>
63#include <LibGUI/TreeView.h>
64#include <LibGUI/VimEditingEngine.h>
65#include <LibGUI/Widget.h>
66#include <LibGUI/Window.h>
67#include <LibGfx/Font/FontDatabase.h>
68#include <LibGfx/Palette.h>
69#include <LibThreading/Mutex.h>
70#include <LibThreading/Thread.h>
71#include <LibVT/TerminalWidget.h>
72#include <fcntl.h>
73#include <spawn.h>
74#include <stdio.h>
75#include <sys/stat.h>
76#include <sys/types.h>
77#include <sys/wait.h>
78#include <unistd.h>
79
80namespace HackStudio {
81
82ErrorOr<NonnullRefPtr<HackStudioWidget>> HackStudioWidget::create(DeprecatedString path_to_project)
83{
84 auto widget = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) HackStudioWidget));
85
86 widget->m_editor_font = widget->read_editor_font_from_config();
87 widget->set_fill_with_background_color(true);
88 widget->set_layout<GUI::VerticalBoxLayout>(GUI::Margins {}, 2);
89
90 auto& toolbar_container = widget->add<GUI::ToolbarContainer>();
91
92 auto& outer_splitter = widget->add<GUI::HorizontalSplitter>();
93 outer_splitter.layout()->set_spacing(4);
94
95 auto& left_hand_splitter = outer_splitter.add<GUI::VerticalSplitter>();
96 left_hand_splitter.layout()->set_spacing(6);
97 left_hand_splitter.set_preferred_width(150);
98
99 widget->m_project_tree_view_context_menu = TRY(widget->create_project_tree_view_context_menu());
100
101 widget->m_right_hand_splitter = outer_splitter.add<GUI::VerticalSplitter>();
102 widget->m_right_hand_stack = widget->m_right_hand_splitter->add<GUI::StackWidget>();
103
104 TRY(widget->create_action_tab(*widget->m_right_hand_splitter));
105
106 widget->open_project(path_to_project);
107 widget->create_project_tab(left_hand_splitter);
108 widget->create_open_files_view(left_hand_splitter);
109
110 // Put a placeholder widget front & center since we don't have a file open yet.
111 widget->m_right_hand_stack->add<GUI::Widget>();
112
113 widget->m_diff_viewer = widget->m_right_hand_stack->add<DiffViewer>();
114
115 widget->m_editors_splitter = widget->m_right_hand_stack->add<GUI::VerticalSplitter>();
116 widget->m_editors_splitter->layout()->set_margins({ 3, 0, 0 });
117 widget->add_new_editor_tab_widget(*widget->m_editors_splitter);
118
119 widget->m_switch_to_next_editor_tab_widget = widget->create_switch_to_next_editor_tab_widget_action();
120 widget->m_switch_to_next_editor = widget->create_switch_to_next_editor_action();
121 widget->m_switch_to_previous_editor = widget->create_switch_to_previous_editor_action();
122
123 widget->m_remove_current_editor_tab_widget_action = widget->create_remove_current_editor_tab_widget_action();
124 widget->m_remove_current_editor_action = TRY(widget->create_remove_current_editor_action());
125 widget->m_open_action = TRY(widget->create_open_action());
126 widget->m_save_action = widget->create_save_action();
127 widget->m_save_as_action = widget->create_save_as_action();
128 widget->m_new_project_action = TRY(widget->create_new_project_action());
129
130 widget->m_add_editor_tab_widget_action = widget->create_add_editor_tab_widget_action();
131 widget->m_add_editor_action = TRY(widget->create_add_editor_action());
132 widget->m_add_terminal_action = TRY(widget->create_add_terminal_action());
133 widget->m_remove_current_terminal_action = TRY(widget->create_remove_current_terminal_action());
134
135 widget->m_locator = widget->add<Locator>();
136
137 widget->m_terminal_wrapper->on_command_exit = [widget] {
138 widget->m_stop_action->set_enabled(false);
139 };
140
141 widget->m_open_project_configuration_action = TRY(widget->create_open_project_configuration_action());
142 widget->m_build_action = TRY(widget->create_build_action());
143 widget->m_run_action = TRY(widget->create_run_action());
144 widget->m_stop_action = TRY(widget->create_stop_action());
145 widget->m_debug_action = TRY(widget->create_debug_action());
146
147 widget->initialize_debugger();
148
149 widget->create_toolbar(toolbar_container);
150
151 widget->m_statusbar = widget->add<GUI::Statusbar>(3);
152 widget->m_statusbar->segment(1).set_mode(GUI::Statusbar::Segment::Mode::Auto);
153 widget->m_statusbar->segment(2).set_mode(GUI::Statusbar::Segment::Mode::Fixed);
154 auto width = widget->font().width("Ln 0000, Col 000"sv) + widget->font().max_glyph_width();
155 widget->m_statusbar->segment(2).set_fixed_width(width);
156 widget->update_statusbar();
157
158 GUI::Application::the()->on_action_enter = [widget](GUI::Action& action) {
159 auto text = action.status_tip();
160 if (text.is_empty())
161 text = Gfx::parse_ampersand_string(action.text());
162 widget->m_statusbar->set_override_text(move(text));
163 };
164
165 GUI::Application::the()->on_action_leave = [widget](GUI::Action&) {
166 widget->m_statusbar->set_override_text({});
167 };
168
169 auto maybe_watcher = Core::FileWatcher::create();
170 if (maybe_watcher.is_error()) {
171 warnln("Couldn't create a file watcher, deleted files won't be noticed! Error: {}", maybe_watcher.error());
172 } else {
173 widget->m_file_watcher = maybe_watcher.release_value();
174 widget->m_file_watcher->on_change = [widget](Core::FileWatcherEvent const& event) {
175 if (event.type != Core::FileWatcherEvent::Type::Deleted)
176 return;
177
178 if (event.event_path.starts_with(widget->project().root_path())) {
179 DeprecatedString relative_path = LexicalPath::relative_path(event.event_path, widget->project().root_path());
180 widget->handle_external_file_deletion(relative_path);
181 } else {
182 widget->handle_external_file_deletion(event.event_path);
183 }
184 };
185 }
186
187 widget->project().model().set_should_show_dotfiles(Config::read_bool("HackStudio"sv, "Global"sv, "ShowDotfiles"sv, false));
188
189 return widget;
190}
191
192void HackStudioWidget::update_actions()
193{
194 auto is_remove_terminal_enabled = [this]() {
195 auto widget = m_action_tab_widget->active_widget();
196 if (!widget)
197 return false;
198 if ("TerminalWrapper"sv != widget->class_name())
199 return false;
200 if (!reinterpret_cast<TerminalWrapper*>(widget)->user_spawned())
201 return false;
202 return true;
203 };
204
205 m_remove_current_editor_action->set_enabled(m_all_editor_wrappers.size() > 1);
206 m_remove_current_terminal_action->set_enabled(is_remove_terminal_enabled());
207}
208
209void HackStudioWidget::on_action_tab_change()
210{
211 update_actions();
212 if (auto* active_widget = m_action_tab_widget->active_widget()) {
213 if (is<GitWidget>(*active_widget))
214 static_cast<GitWidget&>(*active_widget).refresh();
215 }
216}
217
218Vector<DeprecatedString> HackStudioWidget::read_recent_projects()
219{
220 auto json = Config::read_string("HackStudio"sv, "Global"sv, "RecentProjects"sv);
221 AK::JsonParser parser(json);
222 auto value_or_error = parser.parse();
223 if (value_or_error.is_error())
224 return {};
225
226 auto value = value_or_error.release_value();
227 if (!value.is_array())
228 return {};
229
230 Vector<DeprecatedString> paths;
231 for (auto& json_value : value.as_array().values()) {
232 if (!json_value.is_string())
233 return {};
234 paths.append(json_value.as_string());
235 }
236
237 return paths;
238}
239
240void HackStudioWidget::open_project(DeprecatedString const& root_path)
241{
242 if (warn_unsaved_changes("There are unsaved changes, do you want to save before closing current project?") == ContinueDecision::No)
243 return;
244 if (chdir(root_path.characters()) < 0) {
245 perror("chdir");
246 exit(1);
247 }
248 if (m_project) {
249 close_current_project();
250 }
251 m_project = Project::open_with_root_path(root_path);
252 VERIFY(m_project);
253 m_project_builder = make<ProjectBuilder>(*m_terminal_wrapper, *m_project);
254 if (m_project_tree_view) {
255 m_project_tree_view->set_model(m_project->model());
256 m_project_tree_view->update();
257 }
258 if (m_git_widget->initialized()) {
259 m_git_widget->change_repo(root_path);
260 m_git_widget->refresh();
261 }
262 if (Debugger::is_initialized()) {
263 auto& debugger = Debugger::the();
264 debugger.reset_breakpoints();
265 debugger.set_source_root(m_project->root_path());
266 }
267 for (auto& editor_wrapper : m_all_editor_wrappers)
268 editor_wrapper->set_project_root(m_project->root_path());
269
270 m_locations_history.clear();
271 m_locations_history_end_index = 0;
272
273 m_project->model().on_rename_successful = [this](auto& absolute_old_path, auto& absolute_new_path) {
274 file_renamed(
275 LexicalPath::relative_path(absolute_old_path, m_project->root_path()),
276 LexicalPath::relative_path(absolute_new_path, m_project->root_path()));
277 };
278
279 auto recent_projects = read_recent_projects();
280 recent_projects.remove_all_matching([&](auto& p) { return p == root_path; });
281 recent_projects.insert(0, root_path);
282 if (recent_projects.size() > recent_projects_history_size)
283 recent_projects.shrink(recent_projects_history_size);
284
285 Config::write_string("HackStudio"sv, "Global"sv, "RecentProjects"sv, JsonArray(recent_projects).to_deprecated_string());
286 update_recent_projects_submenu();
287}
288
289Vector<DeprecatedString> HackStudioWidget::selected_file_paths() const
290{
291 Vector<DeprecatedString> files;
292 m_project_tree_view->selection().for_each_index([&](const GUI::ModelIndex& index) {
293 DeprecatedString sub_path = index.data().as_string();
294
295 GUI::ModelIndex parent_or_invalid = index.parent();
296
297 while (parent_or_invalid.is_valid()) {
298 sub_path = DeprecatedString::formatted("{}/{}", parent_or_invalid.data().as_string(), sub_path);
299
300 parent_or_invalid = parent_or_invalid.parent();
301 }
302
303 files.append(sub_path);
304 });
305 return files;
306}
307
308bool HackStudioWidget::open_file(DeprecatedString const& full_filename, size_t line, size_t column)
309{
310 DeprecatedString filename = full_filename;
311 if (full_filename.starts_with(project().root_path())) {
312 filename = LexicalPath::relative_path(full_filename, project().root_path());
313 }
314 if (Core::DeprecatedFile::is_directory(filename) || !Core::DeprecatedFile::exists(filename))
315 return false;
316
317 auto editor_wrapper_or_none = m_all_editor_wrappers.first_matching([&](auto& wrapper) {
318 return wrapper->filename() == filename;
319 });
320
321 if (editor_wrapper_or_none.has_value()) {
322 set_current_editor_wrapper(editor_wrapper_or_none.release_value());
323 return true;
324 } else if (active_file().is_empty() && !current_editor().document().is_modified() && !full_filename.is_empty()) {
325 // Replace "Untitled" blank file since it hasn't been modified
326 } else {
327 add_new_editor(*m_current_editor_tab_widget);
328 }
329
330 if (!active_file().is_empty()) {
331 // Since the file is previously open, it should always be in m_open_files.
332 VERIFY(m_open_files.find(active_file()) != m_open_files.end());
333 auto previous_open_project_file = m_open_files.get(active_file()).value();
334
335 // Update the scrollbar values of the previous_open_project_file and save them to m_open_files.
336 previous_open_project_file->vertical_scroll_value(current_editor().vertical_scrollbar().value());
337 previous_open_project_file->horizontal_scroll_value(current_editor().horizontal_scrollbar().value());
338 }
339
340 RefPtr<ProjectFile> new_project_file = nullptr;
341 if (auto it = m_open_files.find(filename); it != m_open_files.end()) {
342 new_project_file = it->value;
343 } else {
344 new_project_file = m_project->create_file(filename);
345 m_open_files.set(filename, *new_project_file);
346 m_open_files_vector.append(filename);
347
348 if (!m_file_watcher.is_null()) {
349 auto watch_result = m_file_watcher->add_watch(filename, Core::FileWatcherEvent::Type::Deleted);
350 if (watch_result.is_error()) {
351 warnln("Couldn't watch '{}'", filename);
352 }
353 }
354 m_open_files_view->model()->invalidate();
355 }
356
357 current_editor().on_cursor_change = nullptr; // Disable callback while we're swapping the document.
358 current_editor().set_document(const_cast<GUI::TextDocument&>(new_project_file->document()));
359 if (new_project_file->could_render_text()) {
360 current_editor_wrapper().set_mode_displayable();
361 } else {
362 current_editor_wrapper().set_mode_non_displayable();
363 }
364 current_editor().horizontal_scrollbar().set_value(new_project_file->horizontal_scroll_value());
365 current_editor().vertical_scrollbar().set_value(new_project_file->vertical_scroll_value());
366 if (current_editor().editing_engine()->is_regular())
367 current_editor().set_editing_engine(make<GUI::RegularEditingEngine>());
368 else if (current_editor().editing_engine()->is_vim())
369 current_editor().set_editing_engine(make<GUI::VimEditingEngine>());
370 else
371 VERIFY_NOT_REACHED();
372
373 set_edit_mode(EditMode::Text);
374
375 DeprecatedString relative_file_path = filename;
376 if (filename.starts_with(m_project->root_path()))
377 relative_file_path = filename.substring(m_project->root_path().length() + 1);
378
379 m_project_tree_view->update();
380
381 current_editor_wrapper().set_filename(filename);
382 update_current_editor_title();
383 current_editor().set_focus(true);
384
385 current_editor().on_cursor_change = [this] { on_cursor_change(); };
386 current_editor().on_change = [this] { update_window_title(); };
387 current_editor_wrapper().on_change = [this] { update_gml_preview(); };
388 current_editor().set_cursor(line, column);
389 update_gml_preview();
390
391 return true;
392}
393
394void HackStudioWidget::close_file_in_all_editors(DeprecatedString const& filename)
395{
396 m_open_files.remove(filename);
397 m_open_files_vector.remove_all_matching(
398 [&filename](DeprecatedString const& element) { return element == filename; });
399
400 for (auto& editor_wrapper : m_all_editor_wrappers) {
401 Editor& editor = editor_wrapper->editor();
402 DeprecatedString editor_file_path = editor.code_document().file_path();
403 DeprecatedString relative_editor_file_path = LexicalPath::relative_path(editor_file_path, project().root_path());
404
405 if (relative_editor_file_path == filename) {
406 if (m_open_files_vector.is_empty()) {
407 editor.set_document(CodeDocument::create());
408 editor_wrapper->set_filename("");
409 } else {
410 auto& first_path = m_open_files_vector[0];
411 auto& document = m_open_files.get(first_path).value()->code_document();
412 editor.set_document(document);
413 editor_wrapper->set_filename(first_path);
414 }
415 }
416 }
417
418 m_open_files_view->model()->invalidate();
419}
420
421GUI::TabWidget& HackStudioWidget::current_editor_tab_widget()
422{
423 VERIFY(m_current_editor_tab_widget);
424 return *m_current_editor_tab_widget;
425}
426
427GUI::TabWidget const& HackStudioWidget::current_editor_tab_widget() const
428{
429 VERIFY(m_current_editor_tab_widget);
430 return *m_current_editor_tab_widget;
431}
432
433EditorWrapper& HackStudioWidget::current_editor_wrapper()
434{
435 VERIFY(m_current_editor_wrapper);
436 return *m_current_editor_wrapper;
437}
438
439EditorWrapper const& HackStudioWidget::current_editor_wrapper() const
440{
441 VERIFY(m_current_editor_wrapper);
442 return *m_current_editor_wrapper;
443}
444
445GUI::TextEditor& HackStudioWidget::current_editor()
446{
447 return current_editor_wrapper().editor();
448}
449
450GUI::TextEditor const& HackStudioWidget::current_editor() const
451{
452 return current_editor_wrapper().editor();
453}
454
455void HackStudioWidget::set_edit_mode(EditMode mode)
456{
457 if (mode == EditMode::Text) {
458 m_right_hand_stack->set_active_widget(m_editors_splitter);
459 } else if (mode == EditMode::Diff) {
460 m_right_hand_stack->set_active_widget(m_diff_viewer);
461 } else {
462 VERIFY_NOT_REACHED();
463 }
464 m_right_hand_stack->active_widget()->update();
465}
466
467ErrorOr<NonnullRefPtr<GUI::Menu>> HackStudioWidget::create_project_tree_view_context_menu()
468{
469 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("&C++ Source File", "/res/icons/16x16/filetype-cplusplus.png", "cpp"))));
470 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("C++ &Header File", "/res/icons/16x16/filetype-header.png", "h"))));
471 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("&GML File", "/res/icons/16x16/filetype-gml.png", "gml"))));
472 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("P&ython Source File", "/res/icons/16x16/filetype-python.png", "py"))));
473 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("Ja&va Source File", "/res/icons/16x16/filetype-java.png", "java"))));
474 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("C Source File", "/res/icons/16x16/filetype-c.png", "c"))));
475 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("&JavaScript Source File", "/res/icons/16x16/filetype-javascript.png", "js"))));
476 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("HT&ML File", "/res/icons/16x16/filetype-html.png", "html"))));
477 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("C&SS File", "/res/icons/16x16/filetype-css.png", "css"))));
478 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("&PHP File", "/res/icons/16x16/filetype-php.png", "php"))));
479 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("&Wasm File", "/res/icons/16x16/filetype-wasm.png", "wasm"))));
480 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("&INI File", "/res/icons/16x16/filetype-ini.png", "ini"))));
481 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("JS&ON File", "/res/icons/16x16/filetype-json.png", "json"))));
482 TRY(m_new_file_actions.try_append(TRY(create_new_file_action("Mark&down File", "/res/icons/16x16/filetype-markdown.png", "md"))));
483
484 m_new_plain_file_action = TRY(create_new_file_action("Plain &File", "/res/icons/16x16/new.png", ""));
485
486 m_open_selected_action = TRY(create_open_selected_action());
487 m_show_in_file_manager_action = create_show_in_file_manager_action();
488 m_copy_relative_path_action = create_copy_relative_path_action();
489 m_copy_full_path_action = create_copy_full_path_action();
490
491 m_new_directory_action = TRY(create_new_directory_action());
492 m_delete_action = create_delete_action();
493 m_tree_view_rename_action = GUI::CommonActions::make_rename_action([this](GUI::Action const&) {
494 m_project_tree_view->begin_editing(m_project_tree_view->cursor_index());
495 });
496 auto project_tree_view_context_menu = GUI::Menu::construct("Project Files");
497
498 auto& new_file_submenu = project_tree_view_context_menu->add_submenu("N&ew...");
499 for (auto& new_file_action : m_new_file_actions) {
500 new_file_submenu.add_action(new_file_action);
501 }
502 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"sv));
503 new_file_submenu.set_icon(icon);
504 new_file_submenu.add_action(*m_new_plain_file_action);
505 new_file_submenu.add_separator();
506 new_file_submenu.add_action(*m_new_directory_action);
507
508 project_tree_view_context_menu->add_action(*m_open_selected_action);
509 project_tree_view_context_menu->add_action(*m_show_in_file_manager_action);
510 project_tree_view_context_menu->add_action(*m_copy_relative_path_action);
511 project_tree_view_context_menu->add_action(*m_copy_full_path_action);
512 // TODO: Cut, copy, duplicate with new name...
513 project_tree_view_context_menu->add_separator();
514 project_tree_view_context_menu->add_action(*m_tree_view_rename_action);
515 project_tree_view_context_menu->add_action(*m_delete_action);
516 return project_tree_view_context_menu;
517}
518
519ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_new_file_action(DeprecatedString const& label, DeprecatedString const& icon, DeprecatedString const& extension)
520{
521 auto icon_no_shadow = TRY(Gfx::Bitmap::load_from_file(icon));
522 return GUI::Action::create(label, icon_no_shadow, [this, extension](const GUI::Action&) {
523 DeprecatedString filename;
524 if (GUI::InputBox::show(window(), filename, "Enter name of new file:"sv, "Add new file to project"sv) != GUI::InputBox::ExecResult::OK)
525 return;
526
527 if (!extension.is_empty() && !filename.ends_with(DeprecatedString::formatted(".{}", extension))) {
528 filename = DeprecatedString::formatted("{}.{}", filename, extension);
529 }
530
531 auto path_to_selected = selected_file_paths();
532
533 DeprecatedString filepath;
534
535 if (!path_to_selected.is_empty()) {
536 VERIFY(Core::DeprecatedFile::exists(path_to_selected.first()));
537
538 LexicalPath selected(path_to_selected.first());
539
540 DeprecatedString dir_path;
541
542 if (Core::DeprecatedFile::is_directory(selected.string()))
543 dir_path = selected.string();
544 else
545 dir_path = selected.dirname();
546
547 filepath = DeprecatedString::formatted("{}/", dir_path);
548 }
549
550 filepath = DeprecatedString::formatted("{}{}", filepath, filename);
551
552 auto file_or_error = Core::File::open(filepath, Core::File::OpenMode::Write | Core::File::OpenMode::MustBeNew);
553 if (file_or_error.is_error()) {
554 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Failed to create '{}': {}", filepath, file_or_error.error()));
555 return;
556 }
557 open_file(filepath);
558 });
559}
560
561ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_new_directory_action()
562{
563 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"sv));
564 return GUI::Action::create("&Directory...", { Mod_Ctrl | Mod_Shift, Key_N }, icon, [this](const GUI::Action&) {
565 DeprecatedString directory_name;
566 if (GUI::InputBox::show(window(), directory_name, "Enter name of new directory:"sv, "Add new folder to project"sv) != GUI::InputBox::ExecResult::OK)
567 return;
568
569 auto path_to_selected = selected_file_paths();
570
571 if (!path_to_selected.is_empty()) {
572 LexicalPath selected(path_to_selected.first());
573
574 DeprecatedString dir_path;
575
576 if (Core::DeprecatedFile::is_directory(selected.string()))
577 dir_path = selected.string();
578 else
579 dir_path = selected.dirname();
580
581 directory_name = DeprecatedString::formatted("{}/{}", dir_path, directory_name);
582 }
583
584 auto formatted_dir_name = LexicalPath::canonicalized_path(DeprecatedString::formatted("{}/{}", m_project->model().root_path(), directory_name));
585 int rc = mkdir(formatted_dir_name.characters(), 0755);
586 if (rc < 0) {
587 GUI::MessageBox::show(window(), "Failed to create new directory"sv, "Error"sv, GUI::MessageBox::Type::Error);
588 return;
589 }
590 });
591}
592
593ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_open_selected_action()
594{
595 auto open_selected_action = GUI::Action::create("&Open", [this](const GUI::Action&) {
596 auto files = selected_file_paths();
597 for (auto& file : files)
598 open_file(file);
599 });
600 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"sv));
601 open_selected_action->set_icon(icon);
602 open_selected_action->set_enabled(true);
603 return open_selected_action;
604}
605
606NonnullRefPtr<GUI::Action> HackStudioWidget::create_show_in_file_manager_action()
607{
608 auto show_in_file_manager_action = GUI::Action::create("Show in File &Manager", [this](const GUI::Action&) {
609 auto files = selected_file_paths();
610 for (auto& file : files)
611 Desktop::Launcher::open(URL::create_with_file_scheme(m_project->root_path(), file));
612 });
613 show_in_file_manager_action->set_enabled(true);
614 show_in_file_manager_action->set_icon(GUI::Icon::default_icon("app-file-manager"sv).bitmap_for_size(16));
615
616 return show_in_file_manager_action;
617}
618
619NonnullRefPtr<GUI::Action> HackStudioWidget::create_copy_relative_path_action()
620{
621 auto copy_relative_path_action = GUI::Action::create("Copy &Relative Path", [this](const GUI::Action&) {
622 auto paths = selected_file_paths();
623 VERIFY(!paths.is_empty());
624 auto paths_string = DeprecatedString::join('\n', paths);
625 GUI::Clipboard::the().set_plain_text(paths_string);
626 });
627 copy_relative_path_action->set_enabled(true);
628 copy_relative_path_action->set_icon(GUI::Icon::default_icon("hard-disk"sv).bitmap_for_size(16));
629
630 return copy_relative_path_action;
631}
632
633NonnullRefPtr<GUI::Action> HackStudioWidget::create_copy_full_path_action()
634{
635 auto copy_full_path_action = GUI::Action::create("Copy &Full Path", [this](const GUI::Action&) {
636 auto paths = selected_file_paths();
637 VERIFY(!paths.is_empty());
638 Vector<DeprecatedString> full_paths;
639 for (auto& path : paths)
640 full_paths.append(get_absolute_path(path));
641 auto paths_string = DeprecatedString::join('\n', full_paths);
642 GUI::Clipboard::the().set_plain_text(paths_string);
643 });
644 copy_full_path_action->set_enabled(true);
645 copy_full_path_action->set_icon(GUI::Icon::default_icon("hard-disk"sv).bitmap_for_size(16));
646
647 return copy_full_path_action;
648}
649
650NonnullRefPtr<GUI::Action> HackStudioWidget::create_delete_action()
651{
652 auto delete_action = GUI::CommonActions::make_delete_action([this](const GUI::Action&) {
653 auto files = selected_file_paths();
654 if (files.is_empty())
655 return;
656
657 DeprecatedString message;
658 if (files.size() == 1) {
659 LexicalPath file(files[0]);
660 message = DeprecatedString::formatted("Really remove {} from disk?", file.basename());
661 } else {
662 message = DeprecatedString::formatted("Really remove {} files from disk?", files.size());
663 }
664
665 auto result = GUI::MessageBox::show(window(),
666 message,
667 "Confirm deletion"sv,
668 GUI::MessageBox::Type::Warning,
669 GUI::MessageBox::InputType::OKCancel);
670 if (result == GUI::MessageBox::ExecResult::Cancel)
671 return;
672
673 for (auto& file : files) {
674 struct stat st;
675 if (lstat(file.characters(), &st) < 0) {
676 GUI::MessageBox::show(window(),
677 DeprecatedString::formatted("lstat ({}) failed: {}", file, strerror(errno)),
678 "Removal failed"sv,
679 GUI::MessageBox::Type::Error);
680 break;
681 }
682
683 bool is_directory = S_ISDIR(st.st_mode);
684 if (auto result = Core::DeprecatedFile::remove(file, Core::DeprecatedFile::RecursionMode::Allowed); result.is_error()) {
685 auto& error = result.error();
686 if (is_directory) {
687 GUI::MessageBox::show(window(),
688 DeprecatedString::formatted("Removing directory {} from the project failed: {}", file, error),
689 "Removal failed"sv,
690 GUI::MessageBox::Type::Error);
691 } else {
692 GUI::MessageBox::show(window(),
693 DeprecatedString::formatted("Removing file {} from the project failed: {}", file, error),
694 "Removal failed"sv,
695 GUI::MessageBox::Type::Error);
696 }
697 }
698 }
699 },
700 m_project_tree_view);
701 delete_action->set_enabled(false);
702 return delete_action;
703}
704
705ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_new_project_action()
706{
707 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/hackstudio-project.png"sv));
708 return GUI::Action::create(
709 "&Project...", icon,
710 [this](const GUI::Action&) {
711 if (warn_unsaved_changes("There are unsaved changes. Would you like to save before creating a new project?") == ContinueDecision::No)
712 return;
713 // If the user wishes to save the changes, this occurs in warn_unsaved_changes. If they do not,
714 // we need to mark the documents as clean so open_project works properly without asking again.
715 for (auto& editor_wrapper : m_all_editor_wrappers)
716 editor_wrapper->editor().document().set_unmodified();
717 auto dialog = NewProjectDialog::construct(window());
718 dialog->set_icon(window()->icon());
719 auto result = dialog->exec();
720
721 if (result == GUI::Dialog::ExecResult::OK && dialog->created_project_path().has_value())
722 open_project(dialog->created_project_path().value());
723 });
724}
725
726NonnullRefPtr<GUI::Action> HackStudioWidget::create_remove_current_editor_tab_widget_action()
727{
728 return GUI::Action::create("Switch to Next Editor Group", { Mod_Alt | Mod_Shift, Key_Backslash }, [this](auto&) {
729 if (m_all_editor_tab_widgets.size() <= 1)
730 return;
731 auto tab_widget = m_current_editor_tab_widget;
732 while (tab_widget->children().size() > 1) {
733 auto active_wrapper = tab_widget->active_widget();
734 tab_widget->remove_tab(*active_wrapper);
735 m_all_editor_wrappers.remove_first_matching([&active_wrapper](auto& entry) { return entry == active_wrapper; });
736 }
737 tab_widget->on_tab_close_click(*tab_widget->active_widget());
738 });
739}
740
741void HackStudioWidget::add_new_editor_tab_widget(GUI::Widget& parent)
742{
743 auto tab_widget = GUI::TabWidget::construct();
744 if (m_action_tab_widget) {
745 parent.insert_child_before(tab_widget, *m_action_tab_widget);
746 } else {
747 parent.add_child(tab_widget);
748 }
749
750 m_current_editor_tab_widget = tab_widget;
751 m_all_editor_tab_widgets.append(tab_widget);
752
753 tab_widget->set_reorder_allowed(true);
754
755 if (m_all_editor_tab_widgets.size() > 1) {
756 for (auto& widget : m_all_editor_tab_widgets)
757 widget->set_close_button_enabled(true);
758 }
759
760 tab_widget->on_change = [&](auto& widget) {
761 auto& wrapper = static_cast<EditorWrapper&>(widget);
762 set_current_editor_wrapper(wrapper);
763 current_editor().set_focus(true);
764 };
765
766 tab_widget->on_middle_click = [](auto& widget) {
767 auto& wrapper = static_cast<EditorWrapper&>(widget);
768 wrapper.on_tab_close_request(wrapper);
769 };
770
771 tab_widget->on_tab_close_click = [](auto& widget) {
772 auto& wrapper = static_cast<EditorWrapper&>(widget);
773 wrapper.on_tab_close_request(wrapper);
774 };
775
776 add_new_editor(*m_current_editor_tab_widget);
777}
778
779void HackStudioWidget::add_new_editor(GUI::TabWidget& parent)
780{
781 auto& wrapper = parent.add_tab<EditorWrapper>("(Untitled)");
782 parent.set_active_widget(&wrapper);
783 if (parent.children().size() > 1 || m_all_editor_tab_widgets.size() > 1)
784 parent.set_close_button_enabled(true);
785
786 auto previous_editor_wrapper = m_current_editor_wrapper;
787 m_current_editor_wrapper = wrapper;
788 m_all_editor_wrappers.append(wrapper);
789 wrapper.editor().set_focus(true);
790 wrapper.editor().set_font(*m_editor_font);
791 wrapper.editor().set_wrapping_mode(m_wrapping_mode);
792 wrapper.set_project_root(m_project->root_path());
793 wrapper.editor().on_cursor_change = [this] { on_cursor_change(); };
794 wrapper.on_change = [this] { update_gml_preview(); };
795 set_edit_mode(EditMode::Text);
796 if (previous_editor_wrapper && previous_editor_wrapper->editor().editing_engine()->is_regular())
797 wrapper.editor().set_editing_engine(make<GUI::RegularEditingEngine>());
798 else if (previous_editor_wrapper && previous_editor_wrapper->editor().editing_engine()->is_vim())
799 wrapper.editor().set_editing_engine(make<GUI::VimEditingEngine>());
800 else
801 wrapper.editor().set_editing_engine(make<GUI::RegularEditingEngine>());
802
803 wrapper.on_tab_close_request = [this, &parent](auto& tab) {
804 parent.deferred_invoke([this, &parent, &tab] {
805 set_current_editor_wrapper(tab);
806 parent.remove_tab(tab);
807 m_all_editor_wrappers.remove_first_matching([&tab](auto& entry) { return entry == &tab; });
808 if (parent.children().is_empty() && m_all_editor_tab_widgets.size() > 1) {
809 m_switch_to_next_editor_tab_widget->activate();
810 m_editors_splitter->remove_child(parent);
811 m_all_editor_tab_widgets.remove_first_matching([&parent](auto& entry) { return entry == &parent; });
812 }
813 update_actions();
814 });
815 };
816}
817
818NonnullRefPtr<GUI::Action> HackStudioWidget::create_switch_to_next_editor_tab_widget_action()
819{
820 return GUI::Action::create("Switch to Next Editor Group", { Mod_Ctrl | Mod_Shift, Key_T }, [this](auto&) {
821 if (m_all_editor_tab_widgets.size() <= 1)
822 return;
823 Vector<GUI::TabWidget&> tab_widgets;
824 m_editors_splitter->for_each_child_of_type<GUI::TabWidget>([&tab_widgets](auto& child) {
825 tab_widgets.append(child);
826 return IterationDecision::Continue;
827 });
828 for (size_t i = 0; i < tab_widgets.size(); ++i) {
829 if (m_current_editor_tab_widget.ptr() == &tab_widgets[i]) {
830 if (i == tab_widgets.size() - 1)
831 m_current_editor_tab_widget = tab_widgets[0];
832 else
833 m_current_editor_tab_widget = tab_widgets[i + 1];
834 auto wrapper = static_cast<EditorWrapper*>(m_current_editor_tab_widget->active_widget());
835 set_current_editor_wrapper(wrapper);
836 current_editor().set_focus(true);
837 break;
838 }
839 }
840 });
841}
842
843NonnullRefPtr<GUI::Action> HackStudioWidget::create_switch_to_next_editor_action()
844{
845 return GUI::Action::create("Switch to &Next Editor", { Mod_Ctrl, Key_E }, [this](auto&) {
846 m_current_editor_tab_widget->activate_next_tab();
847 });
848}
849
850NonnullRefPtr<GUI::Action> HackStudioWidget::create_switch_to_previous_editor_action()
851{
852 return GUI::Action::create("Switch to &Previous Editor", { Mod_Ctrl | Mod_Shift, Key_E }, [this](auto&) {
853 m_current_editor_tab_widget->activate_previous_tab();
854 });
855}
856
857ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_remove_current_editor_action()
858{
859 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/hackstudio/remove-editor.png"sv));
860 return GUI::Action::create("&Remove Current Editor", { Mod_Alt | Mod_Shift, Key_E }, icon, [this](auto&) {
861 if (m_all_editor_wrappers.size() <= 1)
862 return;
863 auto tab_widget = m_current_editor_tab_widget;
864 auto* active_wrapper = tab_widget->active_widget();
865 VERIFY(active_wrapper);
866 tab_widget->on_tab_close_click(*active_wrapper);
867 update_actions();
868 });
869}
870
871ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_open_action()
872{
873 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"sv));
874 return GUI::Action::create("&Open Project...", { Mod_Ctrl | Mod_Shift, Key_O }, icon, [this](auto&) {
875 auto open_path = GUI::FilePicker::get_open_filepath(window(), "Open project", m_project->root_path(), true);
876 if (!open_path.has_value())
877 return;
878 open_project(open_path.value());
879 update_actions();
880 });
881}
882
883NonnullRefPtr<GUI::Action> HackStudioWidget::create_save_action()
884{
885 return GUI::CommonActions::make_save_action([&](auto&) {
886 if (active_file().is_empty())
887 m_save_as_action->activate();
888
889 // NOTE active_file() could still be empty after a cancelled save_as_action
890 if (!active_file().is_empty())
891 current_editor_wrapper().save();
892
893 if (m_git_widget->initialized())
894 m_git_widget->refresh();
895 });
896}
897
898NonnullRefPtr<GUI::Action> HackStudioWidget::create_save_as_action()
899{
900 return GUI::CommonActions::make_save_as_action([&](auto&) {
901 auto const old_filename = current_editor_wrapper().filename();
902 LexicalPath const old_path(old_filename);
903
904 Optional<DeprecatedString> save_path = GUI::FilePicker::get_save_filepath(window(),
905 old_filename.is_null() ? "Untitled"sv : old_path.title(),
906 old_filename.is_null() ? "txt"sv : old_path.extension(),
907 Core::DeprecatedFile::absolute_path(old_path.dirname()));
908 if (!save_path.has_value()) {
909 return;
910 }
911
912 DeprecatedString const relative_file_path = LexicalPath::relative_path(save_path.value(), m_project->root_path());
913 if (current_editor_wrapper().filename().is_null()) {
914 current_editor_wrapper().set_filename(relative_file_path);
915 } else {
916 for (auto& editor_wrapper : m_all_editor_wrappers) {
917 if (editor_wrapper->filename() == old_filename)
918 editor_wrapper->set_filename(relative_file_path);
919 }
920 }
921 current_editor_wrapper().save();
922
923 auto new_project_file = m_project->create_file(relative_file_path);
924 m_open_files.set(relative_file_path, *new_project_file);
925 m_open_files.remove(old_filename);
926
927 m_open_files_vector.append(relative_file_path);
928 m_open_files_vector.remove_all_matching([&old_filename](auto const& element) { return element == old_filename; });
929
930 update_window_title();
931 update_current_editor_title();
932
933 m_project->model().invalidate();
934 update_tree_view();
935 });
936}
937
938ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_remove_current_terminal_action()
939{
940 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/hackstudio/remove-terminal.png"sv));
941 return GUI::Action::create("Remove &Current Terminal", { Mod_Alt | Mod_Shift, Key_T }, icon, [this](auto&) {
942 auto widget = m_action_tab_widget->active_widget();
943 if (!widget)
944 return;
945 if (!is<TerminalWrapper>(widget))
946 return;
947 auto& terminal = *static_cast<TerminalWrapper*>(widget);
948 if (!terminal.user_spawned())
949 return;
950 m_action_tab_widget->remove_tab(terminal);
951 update_actions();
952 });
953}
954
955NonnullRefPtr<GUI::Action> HackStudioWidget::create_add_editor_tab_widget_action()
956{
957 return GUI::Action::create("Add New Editor Group", { Mod_Ctrl | Mod_Alt, Key_Backslash },
958 [this](auto&) {
959 add_new_editor_tab_widget(*m_editors_splitter);
960 update_actions();
961 });
962}
963
964ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_add_editor_action()
965{
966 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/hackstudio/add-editor.png"sv));
967 return GUI::Action::create("Add New &Editor", { Mod_Ctrl | Mod_Alt, Key_E },
968 icon,
969 [this](auto&) {
970 add_new_editor(*m_current_editor_tab_widget);
971 update_actions();
972 });
973}
974
975ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_add_terminal_action()
976{
977 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/hackstudio/add-terminal.png"sv));
978 return GUI::Action::create("Add New &Terminal", { Mod_Ctrl | Mod_Alt, Key_T },
979 icon,
980 [this](auto&) {
981 auto& terminal_wrapper = m_action_tab_widget->add_tab<TerminalWrapper>("Terminal");
982 terminal_wrapper.on_command_exit = [&]() {
983 deferred_invoke([this]() {
984 m_action_tab_widget->remove_tab(*m_action_tab_widget->active_widget());
985 });
986 };
987 reveal_action_tab(terminal_wrapper);
988 update_actions();
989 terminal_wrapper.terminal().set_focus(true);
990 });
991}
992
993void HackStudioWidget::reveal_action_tab(GUI::Widget& widget)
994{
995 if (m_action_tab_widget->effective_min_size().height().as_int() < 200)
996 m_action_tab_widget->set_preferred_height(200);
997 m_action_tab_widget->set_active_widget(&widget);
998}
999
1000ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_debug_action()
1001{
1002 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-run.png"sv));
1003 return GUI::Action::create("&Debug", icon, [this](auto&) {
1004 if (!Core::DeprecatedFile::exists(get_project_executable_path())) {
1005 GUI::MessageBox::show(window(), DeprecatedString::formatted("Could not find file: {}. (did you build the project?)", get_project_executable_path()), "Error"sv, GUI::MessageBox::Type::Error);
1006 return;
1007 }
1008 if (Debugger::the().session()) {
1009 GUI::MessageBox::show(window(), "Debugger is already running"sv, "Error"sv, GUI::MessageBox::Type::Error);
1010 return;
1011 }
1012
1013 Debugger::the().set_executable_path(get_project_executable_path());
1014
1015 m_terminal_wrapper->clear_including_history();
1016
1017 // The debugger calls wait() on the debuggee, so the TerminalWrapper can't do that.
1018 auto ptm_res = m_terminal_wrapper->setup_master_pseudoterminal(TerminalWrapper::WaitForChildOnExit::No);
1019 if (ptm_res.is_error()) {
1020 perror("setup_master_pseudoterminal");
1021 return;
1022 }
1023
1024 Debugger::the().set_child_setup_callback([this, ptm_res = ptm_res.release_value()]() {
1025 return m_terminal_wrapper->setup_slave_pseudoterminal(ptm_res);
1026 });
1027
1028 m_debugger_thread = Threading::Thread::construct(Debugger::start_static);
1029 m_debugger_thread->start();
1030 m_stop_action->set_enabled(true);
1031 m_run_action->set_enabled(false);
1032
1033 for (auto& editor_wrapper : m_all_editor_wrappers) {
1034 editor_wrapper->set_debug_mode(true);
1035 }
1036 });
1037}
1038
1039void HackStudioWidget::initialize_debugger()
1040{
1041 Debugger::initialize(
1042 m_project->root_path(),
1043 [this](PtraceRegisters const& regs) {
1044 VERIFY(Debugger::the().session());
1045 const auto& debug_session = *Debugger::the().session();
1046 auto source_position = debug_session.get_source_position(regs.ip());
1047 if (!source_position.has_value()) {
1048 dbgln("Could not find source position for address: {:p}", regs.ip());
1049 return Debugger::HasControlPassedToUser::No;
1050 }
1051 dbgln("Debugger stopped at source position: {}:{}", source_position.value().file_path, source_position.value().line_number);
1052
1053 GUI::Application::the()->event_loop().deferred_invoke([this, source_position, ®s] {
1054 m_current_editor_in_execution = get_editor_of_file(source_position.value().file_path);
1055 if (m_current_editor_in_execution)
1056 m_current_editor_in_execution->editor().set_execution_position(source_position.value().line_number - 1);
1057 m_debug_info_widget->update_state(*Debugger::the().session(), regs);
1058 m_debug_info_widget->set_debug_actions_enabled(true, DebugInfoWidget::DebugActionsState::DebuggeeStopped);
1059 m_disassembly_widget->update_state(*Debugger::the().session(), regs);
1060 HackStudioWidget::reveal_action_tab(*m_debug_info_widget);
1061 });
1062 GUI::Application::the()->event_loop().wake();
1063
1064 return Debugger::HasControlPassedToUser::Yes;
1065 },
1066 [this]() {
1067 GUI::Application::the()->event_loop().deferred_invoke([this] {
1068 m_debug_info_widget->set_debug_actions_enabled(true, DebugInfoWidget::DebugActionsState::DebuggeeRunning);
1069 if (m_current_editor_in_execution)
1070 m_current_editor_in_execution->editor().clear_execution_position();
1071 });
1072 GUI::Application::the()->event_loop().wake();
1073 },
1074 [this]() {
1075 GUI::Application::the()->event_loop().deferred_invoke([this] {
1076 m_debug_info_widget->set_debug_actions_enabled(false, {});
1077 if (m_current_editor_in_execution)
1078 m_current_editor_in_execution->editor().clear_execution_position();
1079 m_debug_info_widget->program_stopped();
1080 m_disassembly_widget->program_stopped();
1081 m_stop_action->set_enabled(false);
1082 m_run_action->set_enabled(true);
1083 m_debugger_thread.clear();
1084
1085 for (auto& editor_wrapper : m_all_editor_wrappers) {
1086 editor_wrapper->set_debug_mode(false);
1087 }
1088
1089 HackStudioWidget::hide_action_tabs();
1090 GUI::MessageBox::show(window(), "Program Exited"sv, "Debugger"sv, GUI::MessageBox::Type::Information);
1091 });
1092 GUI::Application::the()->event_loop().wake();
1093 },
1094 [](float progress) {
1095 if (GUI::Application::the()->active_window())
1096 GUI::Application::the()->active_window()->set_progress(progress * 100);
1097 });
1098}
1099
1100DeprecatedString HackStudioWidget::get_full_path_of_serenity_source(DeprecatedString const& file)
1101{
1102 auto path_parts = LexicalPath(file).parts();
1103 while (!path_parts.is_empty() && path_parts[0] == "..") {
1104 path_parts.remove(0);
1105 }
1106 StringBuilder relative_path_builder;
1107 relative_path_builder.join('/', path_parts);
1108 constexpr char SERENITY_LIBS_PREFIX[] = "/usr/src/serenity";
1109 LexicalPath serenity_sources_base(SERENITY_LIBS_PREFIX);
1110 return DeprecatedString::formatted("{}/{}", serenity_sources_base, relative_path_builder.to_deprecated_string());
1111}
1112
1113DeprecatedString HackStudioWidget::get_absolute_path(DeprecatedString const& path) const
1114{
1115 // TODO: We can probably do a more specific condition here, something like
1116 // "if (file.starts_with("../Libraries/") || file.starts_with("../AK/"))"
1117 if (path.starts_with(".."sv)) {
1118 return get_full_path_of_serenity_source(path);
1119 }
1120 return m_project->to_absolute_path(path);
1121}
1122
1123RefPtr<EditorWrapper> HackStudioWidget::get_editor_of_file(DeprecatedString const& filename)
1124{
1125 DeprecatedString file_path = filename;
1126
1127 if (filename.starts_with("../"sv)) {
1128 file_path = get_full_path_of_serenity_source(filename);
1129 }
1130
1131 if (!open_file(file_path))
1132 return nullptr;
1133 return current_editor_wrapper();
1134}
1135
1136DeprecatedString HackStudioWidget::get_project_executable_path() const
1137{
1138 // FIXME: Dumb heuristic ahead!
1139 // e.g /my/project => /my/project/project
1140 // TODO: Perhaps a Makefile rule for getting the value of $(PROGRAM) would be better?
1141 return DeprecatedString::formatted("{}/{}", m_project->root_path(), LexicalPath::basename(m_project->root_path()));
1142}
1143
1144void HackStudioWidget::build()
1145{
1146 auto result = m_project_builder->build(active_file());
1147 if (result.is_error()) {
1148 GUI::MessageBox::show(window(), DeprecatedString::formatted("{}", result.error()), "Build failed"sv, GUI::MessageBox::Type::Error);
1149 m_build_action->set_enabled(true);
1150 m_stop_action->set_enabled(false);
1151 } else {
1152 m_stop_action->set_enabled(true);
1153 }
1154}
1155
1156void HackStudioWidget::run()
1157{
1158 auto result = m_project_builder->run(active_file());
1159 if (result.is_error()) {
1160 GUI::MessageBox::show(window(), DeprecatedString::formatted("{}", result.error()), "Run failed"sv, GUI::MessageBox::Type::Error);
1161 m_run_action->set_enabled(true);
1162 m_stop_action->set_enabled(false);
1163 } else {
1164 m_stop_action->set_enabled(true);
1165 }
1166}
1167
1168void HackStudioWidget::hide_action_tabs()
1169{
1170 m_action_tab_widget->set_preferred_height(24);
1171};
1172
1173Project& HackStudioWidget::project()
1174{
1175 return *m_project;
1176}
1177
1178void HackStudioWidget::set_current_editor_tab_widget(RefPtr<GUI::TabWidget> tab_widget)
1179{
1180 m_current_editor_tab_widget = tab_widget;
1181}
1182
1183void HackStudioWidget::set_current_editor_wrapper(RefPtr<EditorWrapper> editor_wrapper)
1184{
1185 if (m_current_editor_wrapper)
1186 m_current_editor_wrapper->editor().hide_autocomplete();
1187
1188 m_current_editor_wrapper = editor_wrapper;
1189 update_window_title();
1190 update_current_editor_title();
1191 update_tree_view();
1192 update_toolbar_actions();
1193 set_current_editor_tab_widget(static_cast<GUI::TabWidget*>(m_current_editor_wrapper->parent()));
1194 m_current_editor_tab_widget->set_active_widget(editor_wrapper);
1195 update_statusbar();
1196}
1197
1198void HackStudioWidget::file_renamed(DeprecatedString const& old_name, DeprecatedString const& new_name)
1199{
1200 auto editor_or_none = m_all_editor_wrappers.first_matching([&old_name](auto const& editor) {
1201 return editor->filename() == old_name;
1202 });
1203 if (editor_or_none.has_value()) {
1204 (*editor_or_none)->set_filename(new_name);
1205 (*editor_or_none)->set_name(new_name);
1206 }
1207
1208 if (m_open_files.contains(old_name)) {
1209 VERIFY(m_open_files_vector.remove_first_matching([&old_name](auto const& file) { return file == old_name; }));
1210 m_open_files_vector.append(new_name);
1211
1212 ProjectFile* f = m_open_files.get(old_name).release_value();
1213 m_open_files.set(new_name, *f);
1214 m_open_files.remove(old_name);
1215 m_open_files_view->model()->invalidate();
1216 }
1217
1218 if (m_file_watcher->is_watching(old_name)) {
1219 VERIFY(!m_file_watcher->remove_watch(old_name).is_error());
1220 VERIFY(!m_file_watcher->add_watch(new_name, Core::FileWatcherEvent::Type::Deleted).is_error());
1221 }
1222
1223 update_window_title();
1224 update_current_editor_title();
1225}
1226
1227void HackStudioWidget::configure_project_tree_view()
1228{
1229 m_project_tree_view->set_model(m_project->model());
1230 m_project_tree_view->set_selection_mode(GUI::AbstractView::SelectionMode::MultiSelection);
1231 m_project_tree_view->set_editable(true);
1232 m_project_tree_view->aid_create_editing_delegate = [](auto&) {
1233 return make<GUI::StringModelEditingDelegate>();
1234 };
1235
1236 for (int column_index = 0; column_index < m_project->model().column_count(); ++column_index)
1237 m_project_tree_view->set_column_visible(column_index, false);
1238
1239 m_project_tree_view->set_column_visible(GUI::FileSystemModel::Column::Name, true);
1240
1241 m_project_tree_view->on_context_menu_request = [this](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
1242 if (index.is_valid()) {
1243 m_project_tree_view_context_menu->popup(event.screen_position(), m_open_selected_action);
1244 }
1245 };
1246
1247 m_project_tree_view->on_selection_change = [this] {
1248 m_open_selected_action->set_enabled(!m_project_tree_view->selection().is_empty());
1249
1250 auto selections = m_project_tree_view->selection().indices();
1251 auto it = selections.find_if([&](auto selected_file) {
1252 return Core::DeprecatedFile::can_delete_or_move(m_project->model().full_path(selected_file));
1253 });
1254 bool has_permissions = it != selections.end();
1255 m_tree_view_rename_action->set_enabled(has_permissions);
1256 m_delete_action->set_enabled(has_permissions);
1257 };
1258
1259 m_project_tree_view->on_activation = [this](auto& index) {
1260 auto full_path_to_file = m_project->model().full_path(index);
1261 open_file(full_path_to_file);
1262 };
1263}
1264
1265void HackStudioWidget::create_open_files_view(GUI::Widget& parent)
1266{
1267 m_open_files_view = parent.add<GUI::ListView>();
1268 auto open_files_model = GUI::ItemListModel<DeprecatedString>::create(m_open_files_vector);
1269 m_open_files_view->set_model(open_files_model);
1270
1271 m_open_files_view->on_activation = [this](auto& index) {
1272 open_file(index.data().to_deprecated_string());
1273 };
1274}
1275
1276void HackStudioWidget::create_toolbar(GUI::Widget& parent)
1277{
1278 auto& toolbar = parent.add<GUI::Toolbar>();
1279 toolbar.add_action(*m_new_plain_file_action);
1280 toolbar.add_action(*m_new_directory_action);
1281 toolbar.add_action(*m_save_action);
1282 toolbar.add_action(*m_delete_action);
1283 toolbar.add_separator();
1284
1285 m_cut_button = toolbar.add_action(current_editor().cut_action());
1286 m_copy_button = toolbar.add_action(current_editor().copy_action());
1287 m_paste_button = toolbar.add_action(current_editor().paste_action());
1288 toolbar.add_separator();
1289 toolbar.add_action(GUI::CommonActions::make_undo_action([this](auto&) { current_editor().undo_action().activate(); }, m_editors_splitter));
1290 toolbar.add_action(GUI::CommonActions::make_redo_action([this](auto&) { current_editor().redo_action().activate(); }, m_editors_splitter));
1291 toolbar.add_separator();
1292
1293 toolbar.add_action(*m_build_action);
1294 toolbar.add_separator();
1295
1296 toolbar.add_action(*m_run_action);
1297 toolbar.add_action(*m_stop_action);
1298 toolbar.add_separator();
1299
1300 toolbar.add_action(*m_debug_action);
1301}
1302
1303ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_build_action()
1304{
1305 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/build.png"sv));
1306 return GUI::Action::create("&Build", { Mod_Ctrl, Key_B }, icon, [this](auto&) {
1307 if (warn_unsaved_changes("There are unsaved changes, do you want to save before building?") == ContinueDecision::No)
1308 return;
1309
1310 reveal_action_tab(*m_terminal_wrapper);
1311 build();
1312 });
1313}
1314
1315ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_run_action()
1316{
1317 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/program-run.png"sv));
1318 return GUI::Action::create("&Run", { Mod_Ctrl, Key_R }, icon, [this](auto&) {
1319 reveal_action_tab(*m_terminal_wrapper);
1320 run();
1321 });
1322}
1323
1324ErrorOr<void> HackStudioWidget::create_action_tab(GUI::Widget& parent)
1325{
1326 m_action_tab_widget = parent.add<GUI::TabWidget>();
1327
1328 m_action_tab_widget->set_preferred_height(24);
1329 m_action_tab_widget->on_change = [this](auto&) {
1330 on_action_tab_change();
1331
1332 static bool first_time = true;
1333 if (!first_time)
1334 m_action_tab_widget->set_preferred_height(200);
1335 first_time = false;
1336 };
1337
1338 m_find_in_files_widget = m_action_tab_widget->add_tab<FindInFilesWidget>("Find in files");
1339 m_todo_entries_widget = m_action_tab_widget->add_tab<ToDoEntriesWidget>("TODO");
1340 m_terminal_wrapper = m_action_tab_widget->add_tab<TerminalWrapper>("Console", false);
1341 auto debug_info_widget = TRY(DebugInfoWidget::create());
1342 TRY(m_action_tab_widget->add_tab(debug_info_widget, "Debug"));
1343 m_debug_info_widget = debug_info_widget;
1344
1345 m_debug_info_widget->on_backtrace_frame_selection = [this](Debug::DebugInfo::SourcePosition const& source_position) {
1346 open_file(get_absolute_path(source_position.file_path), source_position.line_number - 1);
1347 };
1348
1349 m_disassembly_widget = m_action_tab_widget->add_tab<DisassemblyWidget>("Disassembly");
1350 m_git_widget = m_action_tab_widget->add_tab<GitWidget>("Git");
1351 m_git_widget->set_view_diff_callback([this](auto const& original_content, auto const& diff) {
1352 m_diff_viewer->set_content(original_content, diff);
1353 set_edit_mode(EditMode::Diff);
1354 });
1355 m_gml_preview_widget = m_action_tab_widget->add_tab<GMLPreviewWidget>("GML Preview", "");
1356
1357 ToDoEntries::the().on_update = [this]() {
1358 m_todo_entries_widget->refresh();
1359 };
1360
1361 return {};
1362}
1363
1364void HackStudioWidget::create_project_tab(GUI::Widget& parent)
1365{
1366 m_project_tab = parent.add<GUI::TabWidget>();
1367 m_project_tab->set_tab_position(GUI::TabWidget::TabPosition::Bottom);
1368
1369 auto& tree_view_container = m_project_tab->add_tab<GUI::Widget>("Files");
1370 tree_view_container.set_layout<GUI::VerticalBoxLayout>(GUI::Margins {}, 2);
1371
1372 m_project_tree_view = tree_view_container.add<GUI::TreeView>();
1373 configure_project_tree_view();
1374
1375 auto& class_view_container = m_project_tab->add_tab<GUI::Widget>("Classes");
1376 class_view_container.set_layout<GUI::VerticalBoxLayout>(2);
1377
1378 m_class_view = class_view_container.add<ClassViewWidget>();
1379
1380 ProjectDeclarations::the().on_update = [this]() {
1381 m_class_view->refresh();
1382 };
1383}
1384
1385void HackStudioWidget::update_recent_projects_submenu()
1386{
1387 if (!m_recent_projects_submenu)
1388 return;
1389
1390 m_recent_projects_submenu->remove_all_actions();
1391 auto recent_projects = read_recent_projects();
1392
1393 if (recent_projects.size() <= 1) {
1394 auto empty_action = GUI::Action::create("Empty...", [](auto&) {});
1395 empty_action->set_enabled(false);
1396 m_recent_projects_submenu->add_action(empty_action);
1397 return;
1398 }
1399
1400 for (size_t i = 1; i < recent_projects.size(); i++) {
1401 auto project_path = recent_projects[i];
1402 m_recent_projects_submenu->add_action(GUI::Action::create(recent_projects[i], [this, project_path](auto&) {
1403 open_project(project_path);
1404 }));
1405 }
1406}
1407
1408ErrorOr<void> HackStudioWidget::create_file_menu(GUI::Window& window)
1409{
1410 auto& file_menu = window.add_menu("&File");
1411
1412 auto& new_submenu = file_menu.add_submenu("&New...");
1413 new_submenu.add_action(*m_new_project_action);
1414 new_submenu.add_separator();
1415 for (auto& new_file_action : m_new_file_actions) {
1416 new_submenu.add_action(new_file_action);
1417 }
1418
1419 {
1420 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"sv));
1421 new_submenu.set_icon(icon);
1422 }
1423 new_submenu.add_action(*m_new_plain_file_action);
1424 new_submenu.add_separator();
1425 new_submenu.add_action(*m_new_directory_action);
1426
1427 file_menu.add_action(*m_open_action);
1428 m_recent_projects_submenu = &file_menu.add_submenu("Open &Recent");
1429 {
1430 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/open-recent.png"sv));
1431 m_recent_projects_submenu->set_icon(icon);
1432 }
1433 update_recent_projects_submenu();
1434 file_menu.add_action(*m_save_action);
1435 file_menu.add_action(*m_save_as_action);
1436 file_menu.add_separator();
1437 file_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
1438 GUI::Application::the()->quit();
1439 }));
1440 return {};
1441}
1442
1443ErrorOr<void> HackStudioWidget::create_edit_menu(GUI::Window& window)
1444{
1445 auto& edit_menu = window.add_menu("&Edit");
1446 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"sv));
1447 edit_menu.add_action(GUI::Action::create("&Find in Files...", { Mod_Ctrl | Mod_Shift, Key_F }, icon, [this](auto&) {
1448 reveal_action_tab(*m_find_in_files_widget);
1449 m_find_in_files_widget->focus_textbox_and_select_all();
1450 }));
1451
1452 edit_menu.add_separator();
1453
1454 auto vim_emulation_setting_action = GUI::Action::create_checkable("&Vim Emulation", { Mod_Ctrl | Mod_Shift | Mod_Alt, Key_V }, [this](auto& action) {
1455 if (action.is_checked()) {
1456 for (auto& editor_wrapper : m_all_editor_wrappers)
1457 editor_wrapper->editor().set_editing_engine(make<GUI::VimEditingEngine>());
1458 } else {
1459 for (auto& editor_wrapper : m_all_editor_wrappers)
1460 editor_wrapper->editor().set_editing_engine(make<GUI::RegularEditingEngine>());
1461 }
1462 });
1463 vim_emulation_setting_action->set_checked(false);
1464 edit_menu.add_action(vim_emulation_setting_action);
1465
1466 edit_menu.add_separator();
1467 edit_menu.add_action(*m_open_project_configuration_action);
1468 return {};
1469}
1470
1471void HackStudioWidget::create_build_menu(GUI::Window& window)
1472{
1473 auto& build_menu = window.add_menu("&Build");
1474 build_menu.add_action(*m_build_action);
1475 build_menu.add_separator();
1476 build_menu.add_action(*m_run_action);
1477 build_menu.add_action(*m_stop_action);
1478 build_menu.add_separator();
1479 build_menu.add_action(*m_debug_action);
1480}
1481
1482ErrorOr<void> HackStudioWidget::create_view_menu(GUI::Window& window)
1483{
1484 auto hide_action_tabs_action = GUI::Action::create("&Hide Action Tabs", { Mod_Ctrl | Mod_Shift, Key_X }, [this](auto&) {
1485 hide_action_tabs();
1486 });
1487 auto open_locator_action = GUI::Action::create("Open &Locator", { Mod_Ctrl, Key_K }, [this](auto&) {
1488 m_locator->open();
1489 });
1490 auto show_dotfiles_action = GUI::Action::create_checkable("S&how Dotfiles", { Mod_Ctrl, Key_H }, [&](auto& checked) {
1491 project().model().set_should_show_dotfiles(checked.is_checked());
1492 Config::write_bool("HackStudio"sv, "Global"sv, "ShowDotfiles"sv, checked.is_checked());
1493 });
1494 show_dotfiles_action->set_checked(Config::read_bool("HackStudio"sv, "Global"sv, "ShowDotfiles"sv, false));
1495
1496 auto& view_menu = window.add_menu("&View");
1497 view_menu.add_action(hide_action_tabs_action);
1498 view_menu.add_action(open_locator_action);
1499 view_menu.add_action(show_dotfiles_action);
1500 m_toggle_semantic_highlighting_action = TRY(create_toggle_syntax_highlighting_mode_action());
1501 view_menu.add_action(*m_toggle_semantic_highlighting_action);
1502 view_menu.add_separator();
1503
1504 m_wrapping_mode_actions.set_exclusive(true);
1505 auto& wrapping_mode_menu = view_menu.add_submenu("&Wrapping Mode");
1506 m_no_wrapping_action = GUI::Action::create_checkable("&No Wrapping", [&](auto&) {
1507 m_wrapping_mode = GUI::TextEditor::WrappingMode::NoWrap;
1508 for (auto& wrapper : m_all_editor_wrappers)
1509 wrapper->editor().set_wrapping_mode(GUI::TextEditor::WrappingMode::NoWrap);
1510 });
1511 m_wrap_anywhere_action = GUI::Action::create_checkable("Wrap &Anywhere", [&](auto&) {
1512 m_wrapping_mode = GUI::TextEditor::WrappingMode::WrapAnywhere;
1513 for (auto& wrapper : m_all_editor_wrappers)
1514 wrapper->editor().set_wrapping_mode(GUI::TextEditor::WrappingMode::WrapAnywhere);
1515 });
1516 m_wrap_at_words_action = GUI::Action::create_checkable("Wrap at &Words", [&](auto&) {
1517 m_wrapping_mode = GUI::TextEditor::WrappingMode::WrapAtWords;
1518 for (auto& wrapper : m_all_editor_wrappers)
1519 wrapper->editor().set_wrapping_mode(GUI::TextEditor::WrappingMode::WrapAtWords);
1520 });
1521
1522 m_wrapping_mode_actions.add_action(*m_no_wrapping_action);
1523 m_wrapping_mode_actions.add_action(*m_wrap_anywhere_action);
1524 m_wrapping_mode_actions.add_action(*m_wrap_at_words_action);
1525
1526 wrapping_mode_menu.add_action(*m_no_wrapping_action);
1527 wrapping_mode_menu.add_action(*m_wrap_anywhere_action);
1528 wrapping_mode_menu.add_action(*m_wrap_at_words_action);
1529
1530 switch (m_wrapping_mode) {
1531 case GUI::TextEditor::NoWrap:
1532 m_no_wrapping_action->set_checked(true);
1533 break;
1534 case GUI::TextEditor::WrapAtWords:
1535 m_wrap_at_words_action->set_checked(true);
1536 break;
1537 case GUI::TextEditor::WrapAnywhere:
1538 m_wrap_anywhere_action->set_checked(true);
1539 break;
1540 }
1541
1542 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/app-font-editor.png"sv));
1543 m_editor_font_action = GUI::Action::create("Editor &Font...", icon,
1544 [&](auto&) {
1545 auto picker = GUI::FontPicker::construct(&window, m_editor_font, false);
1546 if (picker->exec() == GUI::Dialog::ExecResult::OK) {
1547 change_editor_font(picker->font());
1548 }
1549 });
1550 view_menu.add_action(*m_editor_font_action);
1551
1552 view_menu.add_separator();
1553 view_menu.add_action(*m_add_editor_tab_widget_action);
1554 view_menu.add_action(*m_add_editor_action);
1555 view_menu.add_action(*m_remove_current_editor_action);
1556 view_menu.add_action(*m_add_terminal_action);
1557 view_menu.add_action(*m_remove_current_terminal_action);
1558
1559 view_menu.add_separator();
1560
1561 TRY(create_location_history_actions());
1562 view_menu.add_action(*m_locations_history_back_action);
1563 view_menu.add_action(*m_locations_history_forward_action);
1564
1565 view_menu.add_separator();
1566
1567 view_menu.add_action(GUI::CommonActions::make_fullscreen_action([&](auto&) {
1568 window.set_fullscreen(!window.is_fullscreen());
1569 }));
1570 return {};
1571}
1572
1573void HackStudioWidget::create_help_menu(GUI::Window& window)
1574{
1575 auto& help_menu = window.add_menu("&Help");
1576 help_menu.add_action(GUI::CommonActions::make_command_palette_action(&window));
1577 help_menu.add_action(GUI::CommonActions::make_about_action("Hack Studio", GUI::Icon::default_icon("app-hack-studio"sv), &window));
1578}
1579
1580ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_stop_action()
1581{
1582 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/program-stop.png"sv));
1583 auto action = GUI::Action::create("&Stop", icon, [this](auto&) {
1584 if (!Debugger::the().session()) {
1585 if (auto result = m_terminal_wrapper->kill_running_command(); result.is_error())
1586 warnln("{}", result.error());
1587 return;
1588 }
1589
1590 Debugger::the().stop();
1591 });
1592
1593 action->set_enabled(false);
1594 return action;
1595}
1596
1597ErrorOr<void> HackStudioWidget::initialize_menubar(GUI::Window& window)
1598{
1599 TRY(create_file_menu(window));
1600 TRY(create_edit_menu(window));
1601 create_build_menu(window);
1602 TRY(create_view_menu(window));
1603 create_help_menu(window);
1604 return {};
1605}
1606
1607void HackStudioWidget::update_statusbar()
1608{
1609 StringBuilder builder;
1610 if (current_editor().has_selection()) {
1611 DeprecatedString selected_text = current_editor().selected_text();
1612 auto word_count = current_editor().number_of_selected_words();
1613 builder.appendff("Selected: {} {} ({} {})", selected_text.length(), selected_text.length() == 1 ? "character" : "characters", word_count, word_count != 1 ? "words" : "word");
1614 }
1615
1616 m_statusbar->set_text(0, builder.to_deprecated_string());
1617 m_statusbar->set_text(1, Syntax::language_to_string(current_editor_wrapper().editor().code_document().language().value_or(Syntax::Language::PlainText)));
1618 m_statusbar->set_text(2, DeprecatedString::formatted("Ln {}, Col {}", current_editor().cursor().line() + 1, current_editor().cursor().column()));
1619}
1620
1621void HackStudioWidget::handle_external_file_deletion(DeprecatedString const& filepath)
1622{
1623 close_file_in_all_editors(filepath);
1624}
1625
1626void HackStudioWidget::stop_debugger_if_running()
1627{
1628 if (!m_debugger_thread.is_null()) {
1629 Debugger::the().stop();
1630 dbgln("Waiting for debugger thread to terminate");
1631 auto rc = m_debugger_thread->join();
1632 if (rc.is_error()) {
1633 warnln("pthread_join: {}", strerror(rc.error().value()));
1634 dbgln("error joining debugger thread");
1635 }
1636 }
1637}
1638
1639void HackStudioWidget::close_current_project()
1640{
1641 m_editors_splitter->remove_all_children();
1642 m_all_editor_tab_widgets.clear();
1643 m_all_editor_wrappers.clear();
1644 add_new_editor_tab_widget(*m_editors_splitter);
1645 m_open_files.clear();
1646 m_open_files_vector.clear();
1647 m_find_in_files_widget->reset();
1648 m_todo_entries_widget->clear();
1649 m_terminal_wrapper->clear_including_history();
1650 stop_debugger_if_running();
1651 update_gml_preview();
1652}
1653
1654HackStudioWidget::~HackStudioWidget()
1655{
1656 stop_debugger_if_running();
1657}
1658
1659HackStudioWidget::ContinueDecision HackStudioWidget::warn_unsaved_changes(DeprecatedString const& prompt)
1660{
1661 if (!any_document_is_dirty())
1662 return ContinueDecision::Yes;
1663
1664 auto result = GUI::MessageBox::show(window(), prompt, "Unsaved changes"sv, GUI::MessageBox::Type::Warning, GUI::MessageBox::InputType::YesNoCancel);
1665
1666 if (result == GUI::MessageBox::ExecResult::Cancel)
1667 return ContinueDecision::No;
1668
1669 if (result == GUI::MessageBox::ExecResult::Yes) {
1670 for (auto& editor_wrapper : m_all_editor_wrappers) {
1671 if (editor_wrapper->editor().document().is_modified()) {
1672 editor_wrapper->save();
1673 }
1674 }
1675 }
1676
1677 return ContinueDecision::Yes;
1678}
1679
1680bool HackStudioWidget::any_document_is_dirty() const
1681{
1682 return any_of(m_all_editor_wrappers, [](auto& editor_wrapper) {
1683 return editor_wrapper->editor().document().is_modified();
1684 });
1685}
1686
1687void HackStudioWidget::update_gml_preview()
1688{
1689 auto gml_content = current_editor_wrapper().filename().ends_with(".gml"sv) ? current_editor_wrapper().editor().text().view() : ""sv;
1690 m_gml_preview_widget->load_gml(gml_content);
1691}
1692
1693void HackStudioWidget::update_tree_view()
1694{
1695 auto index = m_project->model().index(m_current_editor_wrapper->filename(), GUI::FileSystemModel::Column::Name);
1696 if (index.is_valid()) {
1697 m_project_tree_view->expand_all_parents_of(index);
1698 m_project_tree_view->set_cursor(index, GUI::AbstractView::SelectionUpdate::Set);
1699 }
1700}
1701
1702void HackStudioWidget::update_toolbar_actions()
1703{
1704 m_copy_button->set_action(current_editor().copy_action());
1705 m_paste_button->set_action(current_editor().paste_action());
1706 m_cut_button->set_action(current_editor().cut_action());
1707}
1708
1709void HackStudioWidget::update_window_title()
1710{
1711 window()->set_title(DeprecatedString::formatted("{} - {} - Hack Studio", m_current_editor_wrapper->filename_title(), m_project->name()));
1712 window()->set_modified(any_document_is_dirty());
1713}
1714
1715void HackStudioWidget::update_current_editor_title()
1716{
1717 current_editor_tab_widget().set_tab_title(current_editor_wrapper(), current_editor_wrapper().filename_title());
1718}
1719
1720void HackStudioWidget::on_cursor_change()
1721{
1722 update_statusbar();
1723 if (current_editor_wrapper().filename().is_null())
1724 return;
1725
1726 auto current_location = current_project_location();
1727
1728 if (m_locations_history_end_index != 0) {
1729 auto last = m_locations_history[m_locations_history_end_index - 1];
1730 if (current_location.filename == last.filename && current_location.line == last.line)
1731 return;
1732 }
1733
1734 // Clear "Go Forward" locations
1735 VERIFY(m_locations_history_end_index <= m_locations_history.size());
1736 m_locations_history.remove(m_locations_history_end_index, m_locations_history.size() - m_locations_history_end_index);
1737
1738 m_locations_history.append(current_location);
1739
1740 constexpr size_t max_locations = 30;
1741 if (m_locations_history.size() > max_locations)
1742 m_locations_history.take_first();
1743
1744 m_locations_history_end_index = m_locations_history.size();
1745
1746 update_history_actions();
1747}
1748
1749ErrorOr<void> HackStudioWidget::create_location_history_actions()
1750{
1751 {
1752 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/go-back.png"sv));
1753 m_locations_history_back_action = GUI::Action::create("Go Back", { Mod_Alt | Mod_Shift, Key_Left }, icon, [this](auto&) {
1754 if (m_locations_history_end_index <= 1)
1755 return;
1756
1757 auto location = m_locations_history[m_locations_history_end_index - 2];
1758 --m_locations_history_end_index;
1759
1760 m_locations_history_disabled = true;
1761 open_file(location.filename, location.line, location.column);
1762 m_locations_history_disabled = false;
1763
1764 update_history_actions();
1765 });
1766 }
1767
1768 {
1769 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"sv));
1770 m_locations_history_forward_action = GUI::Action::create("Go Forward", { Mod_Alt | Mod_Shift, Key_Right }, icon, [this](auto&) {
1771 if (m_locations_history_end_index == m_locations_history.size())
1772 return;
1773
1774 auto location = m_locations_history[m_locations_history_end_index];
1775 ++m_locations_history_end_index;
1776
1777 m_locations_history_disabled = true;
1778 open_file(location.filename, location.line, location.column);
1779 m_locations_history_disabled = false;
1780
1781 update_history_actions();
1782 });
1783 }
1784 m_locations_history_forward_action->set_enabled(false);
1785 return {};
1786}
1787
1788ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_open_project_configuration_action()
1789{
1790 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/settings.png"sv));
1791 return GUI::Action::create("Project Configuration", icon, [&](auto&) {
1792 auto parent_directory = LexicalPath::dirname(Project::config_file_path);
1793 auto absolute_config_file_path = LexicalPath::absolute_path(m_project->root_path(), Project::config_file_path);
1794
1795 DeprecatedString formatted_error_string_holder;
1796 auto save_configuration_or_error = [&]() -> ErrorOr<void> {
1797 if (Core::DeprecatedFile::exists(absolute_config_file_path))
1798 return {};
1799
1800 if (Core::DeprecatedFile::exists(parent_directory) && !Core::DeprecatedFile::is_directory(parent_directory)) {
1801 formatted_error_string_holder = DeprecatedString::formatted("Cannot create the '{}' directory because there is already a file with that name", parent_directory);
1802 return Error::from_string_view(formatted_error_string_holder);
1803 }
1804
1805 auto maybe_error = Core::System::mkdir(LexicalPath::absolute_path(m_project->root_path(), parent_directory), 0755);
1806 if (maybe_error.is_error() && maybe_error.error().code() != EEXIST)
1807 return maybe_error.release_error();
1808
1809 auto file = TRY(Core::File::open(absolute_config_file_path, Core::File::OpenMode::Write));
1810 TRY(file->write_until_depleted(
1811 "{\n"
1812 " \"build_command\": \"your build command here\",\n"
1813 " \"run_command\": \"your run command here\"\n"
1814 "}\n"sv.bytes()));
1815 return {};
1816 }();
1817 if (save_configuration_or_error.is_error()) {
1818 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Saving configuration failed: {}.", save_configuration_or_error.error()));
1819 return;
1820 }
1821
1822 open_file(Project::config_file_path);
1823 });
1824}
1825
1826HackStudioWidget::ProjectLocation HackStudioWidget::current_project_location() const
1827{
1828 return ProjectLocation { current_editor_wrapper().filename(), current_editor().cursor().line(), current_editor().cursor().column() };
1829}
1830
1831void HackStudioWidget::update_history_actions()
1832{
1833 if (m_locations_history_end_index <= 1)
1834 m_locations_history_back_action->set_enabled(false);
1835 else
1836 m_locations_history_back_action->set_enabled(true);
1837
1838 if (m_locations_history_end_index == m_locations_history.size())
1839 m_locations_history_forward_action->set_enabled(false);
1840 else
1841 m_locations_history_forward_action->set_enabled(true);
1842}
1843
1844RefPtr<Gfx::Font const> HackStudioWidget::read_editor_font_from_config()
1845{
1846 auto font_family = Config::read_string("HackStudio"sv, "EditorFont"sv, "Family"sv);
1847 auto font_variant = Config::read_string("HackStudio"sv, "EditorFont"sv, "Variant"sv);
1848 auto font_size = Config::read_i32("HackStudio"sv, "EditorFont"sv, "Size"sv);
1849
1850 auto font = Gfx::FontDatabase::the().get(font_family, font_variant, font_size);
1851 if (font.is_null())
1852 return Gfx::FontDatabase::the().default_fixed_width_font();
1853
1854 return font;
1855}
1856
1857void HackStudioWidget::change_editor_font(RefPtr<Gfx::Font const> font)
1858{
1859 m_editor_font = move(font);
1860 for (auto& editor_wrapper : m_all_editor_wrappers) {
1861 editor_wrapper->editor().set_font(*m_editor_font);
1862 }
1863
1864 Config::write_string("HackStudio"sv, "EditorFont"sv, "Family"sv, m_editor_font->family());
1865 Config::write_string("HackStudio"sv, "EditorFont"sv, "Variant"sv, m_editor_font->variant());
1866 Config::write_i32("HackStudio"sv, "EditorFont"sv, "Size"sv, m_editor_font->presentation_size());
1867}
1868
1869void HackStudioWidget::open_coredump(DeprecatedString const& coredump_path)
1870{
1871 open_project("/usr/src/serenity");
1872 m_mode = Mode::Coredump;
1873
1874 m_coredump_inspector = Coredump::Inspector::create(coredump_path, [this](float progress) {
1875 window()->set_progress(progress * 100);
1876 });
1877 window()->set_progress(0);
1878
1879 if (m_coredump_inspector) {
1880 m_debug_info_widget->update_state(*m_coredump_inspector, m_coredump_inspector->get_registers());
1881 reveal_action_tab(*m_debug_info_widget);
1882 }
1883}
1884
1885void HackStudioWidget::debug_process(pid_t pid)
1886{
1887 open_project("/usr/src/serenity");
1888 Debugger::the().set_pid_to_attach(pid);
1889
1890 m_debugger_thread = Threading::Thread::construct(Debugger::start_static);
1891 m_debugger_thread->start();
1892 m_stop_action->set_enabled(true);
1893 m_run_action->set_enabled(false);
1894
1895 for (auto& editor_wrapper : m_all_editor_wrappers) {
1896 editor_wrapper->set_debug_mode(true);
1897 }
1898}
1899
1900void HackStudioWidget::for_each_open_file(Function<void(ProjectFile const&)> func)
1901{
1902 for (auto& open_file : m_open_files) {
1903 func(*open_file.value);
1904 }
1905}
1906
1907ErrorOr<NonnullRefPtr<GUI::Action>> HackStudioWidget::create_toggle_syntax_highlighting_mode_action()
1908{
1909 auto icon = TRY(Gfx::Bitmap::load_from_file("/res/icons/16x16/filetype-cplusplus.png"sv));
1910 auto action = GUI::Action::create_checkable("&Semantic Highlighting", icon, [this](auto& action) {
1911 for (auto& editor_wrapper : m_all_editor_wrappers)
1912 editor_wrapper->editor().set_semantic_syntax_highlighting(action.is_checked());
1913 });
1914
1915 return action;
1916}
1917
1918bool HackStudioWidget::semantic_syntax_highlighting_is_enabled() const
1919{
1920 return m_toggle_semantic_highlighting_action->is_checked();
1921}
1922}