Serenity Operating System
at master 1922 lines 78 kB view raw
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, &regs] { 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}