HackStudioWidget.cpp 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  1. /*
  2. * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
  3. * Copyright (c) 2020, Itamar S. <itamar8910@gmail.com>
  4. * Copyright (c) 2020, the SerenityOS developers
  5. * All rights reserved.
  6. *
  7. * Redistribution and use in source and binary forms, with or without
  8. * modification, are permitted provided that the following conditions are met:
  9. *
  10. * 1. Redistributions of source code must retain the above copyright notice, this
  11. * list of conditions and the following disclaimer.
  12. *
  13. * 2. Redistributions in binary form must reproduce the above copyright notice,
  14. * this list of conditions and the following disclaimer in the documentation
  15. * and/or other materials provided with the distribution.
  16. *
  17. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  18. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  19. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  20. * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  21. * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  22. * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  23. * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  24. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  25. * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  26. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  27. */
  28. #include "HackStudioWidget.h"
  29. #include "CursorTool.h"
  30. #include "Debugger/DebugInfoWidget.h"
  31. #include "Debugger/Debugger.h"
  32. #include "Debugger/DisassemblyWidget.h"
  33. #include "Editor.h"
  34. #include "EditorWrapper.h"
  35. #include "FindInFilesWidget.h"
  36. #include "FormEditorWidget.h"
  37. #include "FormWidget.h"
  38. #include "Git/DiffViewer.h"
  39. #include "Git/GitWidget.h"
  40. #include "HackStudio.h"
  41. #include "HackStudioWidget.h"
  42. #include "Locator.h"
  43. #include "Project.h"
  44. #include "TerminalWrapper.h"
  45. #include "WidgetTool.h"
  46. #include "WidgetTreeModel.h"
  47. #include <AK/StringBuilder.h>
  48. #include <LibCore/ArgsParser.h>
  49. #include <LibCore/Event.h>
  50. #include <LibCore/EventLoop.h>
  51. #include <LibCore/File.h>
  52. #include <LibDebug/DebugSession.h>
  53. #include <LibGUI/Action.h>
  54. #include <LibGUI/ActionGroup.h>
  55. #include <LibGUI/Application.h>
  56. #include <LibGUI/BoxLayout.h>
  57. #include <LibGUI/Button.h>
  58. #include <LibGUI/EditingEngine.h>
  59. #include <LibGUI/FilePicker.h>
  60. #include <LibGUI/InputBox.h>
  61. #include <LibGUI/ItemListModel.h>
  62. #include <LibGUI/Label.h>
  63. #include <LibGUI/Menu.h>
  64. #include <LibGUI/MenuBar.h>
  65. #include <LibGUI/MessageBox.h>
  66. #include <LibGUI/RegularEditingEngine.h>
  67. #include <LibGUI/Splitter.h>
  68. #include <LibGUI/StackWidget.h>
  69. #include <LibGUI/TabWidget.h>
  70. #include <LibGUI/TableView.h>
  71. #include <LibGUI/TextBox.h>
  72. #include <LibGUI/TextEditor.h>
  73. #include <LibGUI/ToolBar.h>
  74. #include <LibGUI/ToolBarContainer.h>
  75. #include <LibGUI/TreeView.h>
  76. #include <LibGUI/VimEditingEngine.h>
  77. #include <LibGUI/Widget.h>
  78. #include <LibGUI/Window.h>
  79. #include <LibGfx/FontDatabase.h>
  80. #include <LibThread/Lock.h>
  81. #include <LibThread/Thread.h>
  82. #include <LibVT/TerminalWidget.h>
  83. #include <fcntl.h>
  84. #include <spawn.h>
  85. #include <stdio.h>
  86. #include <sys/types.h>
  87. #include <sys/wait.h>
  88. #include <unistd.h>
  89. namespace HackStudio {
  90. HackStudioWidget::HackStudioWidget(const String& path_to_project)
  91. {
  92. set_fill_with_background_color(true);
  93. set_layout<GUI::VerticalBoxLayout>();
  94. layout()->set_spacing(2);
  95. open_project(path_to_project);
  96. auto& toolbar_container = add<GUI::ToolBarContainer>();
  97. auto& outer_splitter = add<GUI::HorizontalSplitter>();
  98. auto& left_hand_splitter = outer_splitter.add<GUI::VerticalSplitter>();
  99. left_hand_splitter.set_fixed_width(150);
  100. create_project_tree_view(left_hand_splitter);
  101. m_project_tree_view_context_menu = create_project_tree_view_context_menu();
  102. create_open_files_view(left_hand_splitter);
  103. m_right_hand_splitter = outer_splitter.add<GUI::VerticalSplitter>();
  104. m_right_hand_stack = m_right_hand_splitter->add<GUI::StackWidget>();
  105. // Put a placeholder widget front & center since we don't have a file open yet.
  106. m_right_hand_stack->add<GUI::Widget>();
  107. create_form_editor(*m_right_hand_stack);
  108. m_diff_viewer = m_right_hand_stack->add<DiffViewer>();
  109. m_editors_splitter = m_right_hand_stack->add<GUI::VerticalSplitter>();
  110. m_editors_splitter->layout()->set_margins({ 0, 3, 0, 0 });
  111. add_new_editor(*m_editors_splitter);
  112. m_switch_to_next_editor = create_switch_to_next_editor_action();
  113. m_switch_to_previous_editor = create_switch_to_previous_editor_action();
  114. m_remove_current_editor_action = create_remove_current_editor_action();
  115. m_open_action = create_open_action();
  116. m_save_action = create_save_action();
  117. create_action_tab(*m_right_hand_splitter);
  118. m_add_editor_action = create_add_editor_action();
  119. m_add_terminal_action = create_add_terminal_action();
  120. m_remove_current_terminal_action = create_remove_current_terminal_action();
  121. m_locator = add<Locator>();
  122. m_terminal_wrapper->on_command_exit = [this] {
  123. m_stop_action->set_enabled(false);
  124. };
  125. m_build_action = create_build_action();
  126. m_run_action = create_run_action();
  127. m_stop_action = create_stop_action();
  128. m_debug_action = create_debug_action();
  129. initialize_debugger();
  130. create_toolbar(toolbar_container);
  131. }
  132. void HackStudioWidget::update_actions()
  133. {
  134. auto is_remove_terminal_enabled = [this]() {
  135. auto widget = m_action_tab_widget->active_widget();
  136. if (!widget)
  137. return false;
  138. if (StringView { "TerminalWrapper" } != widget->class_name())
  139. return false;
  140. if (!reinterpret_cast<TerminalWrapper*>(widget)->user_spawned())
  141. return false;
  142. return true;
  143. };
  144. m_remove_current_editor_action->set_enabled(m_all_editor_wrappers.size() > 1);
  145. m_remove_current_terminal_action->set_enabled(is_remove_terminal_enabled());
  146. }
  147. void HackStudioWidget::on_action_tab_change()
  148. {
  149. update_actions();
  150. auto git_widget = m_action_tab_widget->active_widget();
  151. if (!git_widget)
  152. return;
  153. if (StringView { "GitWidget" } != git_widget->class_name())
  154. return;
  155. reinterpret_cast<GitWidget*>(git_widget)->refresh();
  156. }
  157. void HackStudioWidget::open_project(const String& root_path)
  158. {
  159. if (chdir(root_path.characters()) < 0) {
  160. perror("chdir");
  161. exit(1);
  162. }
  163. m_project = Project::open_with_root_path(root_path);
  164. ASSERT(m_project);
  165. if (m_project_tree_view) {
  166. m_project_tree_view->set_model(m_project->model());
  167. m_project_tree_view->update();
  168. }
  169. if (Debugger::is_initialized()) {
  170. Debugger::the().reset_breakpoints();
  171. }
  172. }
  173. Vector<String> HackStudioWidget::selected_file_names() const
  174. {
  175. Vector<String> files;
  176. m_project_tree_view->selection().for_each_index([&](const GUI::ModelIndex& index) {
  177. files.append(index.data().as_string());
  178. });
  179. return files;
  180. }
  181. void HackStudioWidget::open_file(const String& filename)
  182. {
  183. if (!currently_open_file().is_empty()) {
  184. // Since the file is previously open, it should always be in m_open_files.
  185. ASSERT(m_open_files.find(currently_open_file()) != m_open_files.end());
  186. auto previous_open_project_file = m_open_files.get(currently_open_file()).value();
  187. // Update the scrollbar values of the previous_open_project_file and save them to m_open_files.
  188. previous_open_project_file->vertical_scroll_value(current_editor().vertical_scrollbar().value());
  189. previous_open_project_file->horizontal_scroll_value(current_editor().horizontal_scrollbar().value());
  190. m_open_files.set(currently_open_file(), previous_open_project_file);
  191. }
  192. RefPtr<ProjectFile> new_project_file = nullptr;
  193. if (auto it = m_open_files.find(filename); it != m_open_files.end()) {
  194. new_project_file = it->value;
  195. } else {
  196. new_project_file = m_project->get_file(filename);
  197. if (!new_project_file) {
  198. new_project_file = ProjectFile::construct_with_name(filename);
  199. }
  200. m_open_files.set(filename, *new_project_file);
  201. m_open_files_vector.append(filename);
  202. m_open_files_view->model()->update();
  203. }
  204. current_editor().set_document(const_cast<GUI::TextDocument&>(new_project_file->document()));
  205. current_editor().set_mode(GUI::TextEditor::Editable);
  206. current_editor().horizontal_scrollbar().set_value(new_project_file->horizontal_scroll_value());
  207. current_editor().vertical_scrollbar().set_value(new_project_file->vertical_scroll_value());
  208. current_editor().set_editing_engine(make<GUI::RegularEditingEngine>());
  209. if (filename.ends_with(".frm")) {
  210. set_edit_mode(EditMode::Form);
  211. } else {
  212. set_edit_mode(EditMode::Text);
  213. }
  214. m_currently_open_file = filename;
  215. String relative_file_path = m_currently_open_file;
  216. if (m_currently_open_file.starts_with(m_project->root_path()))
  217. relative_file_path = m_currently_open_file.substring(m_project->root_path().length() + 1);
  218. window()->set_title(String::formatted("{} - {} - Hack Studio", relative_file_path, m_project->name()));
  219. m_project_tree_view->update();
  220. current_editor_wrapper().filename_label().set_text(filename);
  221. current_editor().set_focus(true);
  222. }
  223. EditorWrapper& HackStudioWidget::current_editor_wrapper()
  224. {
  225. ASSERT(m_current_editor_wrapper);
  226. return *m_current_editor_wrapper;
  227. }
  228. GUI::TextEditor& HackStudioWidget::current_editor()
  229. {
  230. return current_editor_wrapper().editor();
  231. }
  232. void HackStudioWidget::set_edit_mode(EditMode mode)
  233. {
  234. if (mode == EditMode::Text) {
  235. m_right_hand_stack->set_active_widget(m_editors_splitter);
  236. } else if (mode == EditMode::Form) {
  237. m_right_hand_stack->set_active_widget(m_form_inner_container);
  238. } else if (mode == EditMode::Diff) {
  239. m_right_hand_stack->set_active_widget(m_diff_viewer);
  240. } else {
  241. ASSERT_NOT_REACHED();
  242. }
  243. m_right_hand_stack->active_widget()->update();
  244. }
  245. NonnullRefPtr<GUI::Menu> HackStudioWidget::create_project_tree_view_context_menu()
  246. {
  247. m_open_selected_action = create_open_selected_action();
  248. m_new_action = create_new_action();
  249. m_delete_action = create_delete_action();
  250. auto project_tree_view_context_menu = GUI::Menu::construct("Project Files");
  251. project_tree_view_context_menu->add_action(*m_open_selected_action);
  252. // TODO: Rename, cut, copy, duplicate with new name, show containing folder ...
  253. project_tree_view_context_menu->add_separator();
  254. project_tree_view_context_menu->add_action(*m_new_action);
  255. project_tree_view_context_menu->add_action(*m_delete_action);
  256. return project_tree_view_context_menu;
  257. }
  258. NonnullRefPtr<GUI::Action> HackStudioWidget::create_new_action()
  259. {
  260. return GUI::Action::create("Add new file to project...", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"), [this](const GUI::Action&) {
  261. String filename;
  262. if (GUI::InputBox::show(filename, window(), "Enter name of new file:", "Add new file to project") != GUI::InputBox::ExecOK)
  263. return;
  264. auto file = Core::File::construct(filename);
  265. if (!file->open((Core::IODevice::OpenMode)(Core::IODevice::WriteOnly | Core::IODevice::MustBeNew))) {
  266. GUI::MessageBox::show(window(), String::formatted("Failed to create '{}'", filename), "Error", GUI::MessageBox::Type::Error);
  267. return;
  268. }
  269. open_file(filename);
  270. });
  271. }
  272. NonnullRefPtr<GUI::Action> HackStudioWidget::create_open_selected_action()
  273. {
  274. auto open_selected_action = GUI::Action::create("Open", [this](const GUI::Action&) {
  275. auto files = selected_file_names();
  276. for (auto& file : files)
  277. open_file(file);
  278. });
  279. open_selected_action->set_enabled(true);
  280. return open_selected_action;
  281. }
  282. NonnullRefPtr<GUI::Action> HackStudioWidget::create_delete_action()
  283. {
  284. auto delete_action = GUI::CommonActions::make_delete_action([this](const GUI::Action&) {
  285. auto files = selected_file_names();
  286. if (files.is_empty())
  287. return;
  288. String message;
  289. if (files.size() == 1) {
  290. message = String::formatted("Really remove {} from disk?", LexicalPath(files[0]).basename());
  291. } else {
  292. message = String::formatted("Really remove {} files from disk?", files.size());
  293. }
  294. auto result = GUI::MessageBox::show(window(),
  295. message,
  296. "Confirm deletion",
  297. GUI::MessageBox::Type::Warning,
  298. GUI::MessageBox::InputType::OKCancel);
  299. if (result == GUI::MessageBox::ExecCancel)
  300. return;
  301. for (auto& file : files) {
  302. if (1) {
  303. // FIXME: Remove `file` from disk
  304. } else {
  305. GUI::MessageBox::show(window(),
  306. String::formatted("Removing file {} from the project failed.", file),
  307. "Removal failed",
  308. GUI::MessageBox::Type::Error);
  309. break;
  310. }
  311. }
  312. });
  313. delete_action->set_enabled(false);
  314. return delete_action;
  315. }
  316. void HackStudioWidget::add_new_editor(GUI::Widget& parent)
  317. {
  318. auto wrapper = EditorWrapper::construct();
  319. if (m_action_tab_widget) {
  320. parent.insert_child_before(wrapper, *m_action_tab_widget);
  321. } else {
  322. parent.add_child(wrapper);
  323. }
  324. m_current_editor_wrapper = wrapper;
  325. m_all_editor_wrappers.append(wrapper);
  326. wrapper->editor().set_focus(true);
  327. }
  328. NonnullRefPtr<GUI::Action> HackStudioWidget::create_switch_to_next_editor_action()
  329. {
  330. return GUI::Action::create("Switch to next editor", { Mod_Ctrl, Key_E }, [this](auto&) {
  331. if (m_all_editor_wrappers.size() <= 1)
  332. return;
  333. Vector<EditorWrapper*> wrappers;
  334. m_editors_splitter->for_each_child_of_type<EditorWrapper>([this, &wrappers](auto& child) {
  335. wrappers.append(&child);
  336. return IterationDecision::Continue;
  337. });
  338. for (size_t i = 0; i < wrappers.size(); ++i) {
  339. if (m_current_editor_wrapper.ptr() == wrappers[i]) {
  340. if (i == wrappers.size() - 1)
  341. wrappers[0]->editor().set_focus(true);
  342. else
  343. wrappers[i + 1]->editor().set_focus(true);
  344. }
  345. }
  346. });
  347. }
  348. NonnullRefPtr<GUI::Action> HackStudioWidget::create_switch_to_previous_editor_action()
  349. {
  350. return GUI::Action::create("Switch to previous editor", { Mod_Ctrl | Mod_Shift, Key_E }, [this](auto&) {
  351. if (m_all_editor_wrappers.size() <= 1)
  352. return;
  353. Vector<EditorWrapper*> wrappers;
  354. m_editors_splitter->for_each_child_of_type<EditorWrapper>([this, &wrappers](auto& child) {
  355. wrappers.append(&child);
  356. return IterationDecision::Continue;
  357. });
  358. for (int i = wrappers.size() - 1; i >= 0; --i) {
  359. if (m_current_editor_wrapper.ptr() == wrappers[i]) {
  360. if (i == 0)
  361. wrappers.last()->editor().set_focus(true);
  362. else
  363. wrappers[i - 1]->editor().set_focus(true);
  364. }
  365. }
  366. });
  367. }
  368. NonnullRefPtr<GUI::Action> HackStudioWidget::create_remove_current_editor_action()
  369. {
  370. return GUI::Action::create("Remove current editor", { Mod_Alt | Mod_Shift, Key_E }, [this](auto&) {
  371. if (m_all_editor_wrappers.size() <= 1)
  372. return;
  373. auto wrapper = m_current_editor_wrapper;
  374. m_switch_to_next_editor->activate();
  375. m_editors_splitter->remove_child(*wrapper);
  376. m_all_editor_wrappers.remove_first_matching([&wrapper](auto& entry) { return entry == wrapper.ptr(); });
  377. update_actions();
  378. });
  379. }
  380. NonnullRefPtr<GUI::Action> HackStudioWidget::create_open_action()
  381. {
  382. return GUI::Action::create("Open project...", { Mod_Ctrl | Mod_Shift, Key_O }, Gfx::Bitmap::load_from_file("/res/icons/16x16/open.png"), [this](auto&) {
  383. auto open_path = GUI::FilePicker::get_open_filepath(window(), "Open project");
  384. if (!open_path.has_value())
  385. return;
  386. open_project(open_path.value());
  387. update_actions();
  388. });
  389. }
  390. NonnullRefPtr<GUI::Action> HackStudioWidget::create_save_action()
  391. {
  392. return GUI::Action::create("Save", { Mod_Ctrl, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"), [this](auto&) {
  393. if (m_currently_open_file.is_empty())
  394. return;
  395. current_editor().write_to_file(m_currently_open_file);
  396. if (m_git_widget->initialized())
  397. m_git_widget->refresh();
  398. });
  399. }
  400. NonnullRefPtr<GUI::Action> HackStudioWidget::create_remove_current_terminal_action()
  401. {
  402. return GUI::Action::create("Remove current Terminal", { Mod_Alt | Mod_Shift, Key_T }, [this](auto&) {
  403. auto widget = m_action_tab_widget->active_widget();
  404. if (!widget)
  405. return;
  406. if (!is<TerminalWrapper>(widget))
  407. return;
  408. auto& terminal = *static_cast<TerminalWrapper*>(widget);
  409. if (!terminal.user_spawned())
  410. return;
  411. m_action_tab_widget->remove_tab(terminal);
  412. update_actions();
  413. });
  414. }
  415. NonnullRefPtr<GUI::Action> HackStudioWidget::create_add_editor_action()
  416. {
  417. return GUI::Action::create("Add new editor", { Mod_Ctrl | Mod_Alt, Key_E },
  418. Gfx::Bitmap::load_from_file("/res/icons/16x16/app-text-editor.png"),
  419. [this](auto&) {
  420. add_new_editor(*m_editors_splitter);
  421. update_actions();
  422. });
  423. }
  424. NonnullRefPtr<GUI::Action> HackStudioWidget::create_add_terminal_action()
  425. {
  426. return GUI::Action::create("Add new Terminal", { Mod_Ctrl | Mod_Alt, Key_T },
  427. Gfx::Bitmap::load_from_file("/res/icons/16x16/app-terminal.png"),
  428. [this](auto&) {
  429. auto& terminal_wrapper = m_action_tab_widget->add_tab<TerminalWrapper>("Terminal");
  430. reveal_action_tab(terminal_wrapper);
  431. update_actions();
  432. terminal_wrapper.terminal().set_focus(true);
  433. });
  434. }
  435. void HackStudioWidget::reveal_action_tab(GUI::Widget& widget)
  436. {
  437. if (m_action_tab_widget->min_height() < 200)
  438. m_action_tab_widget->set_fixed_height(200);
  439. m_action_tab_widget->set_active_widget(&widget);
  440. }
  441. NonnullRefPtr<GUI::Action> HackStudioWidget::create_debug_action()
  442. {
  443. return GUI::Action::create("Debug", Gfx::Bitmap::load_from_file("/res/icons/16x16/debug-run.png"), [this](auto&) {
  444. if (!GUI::FilePicker::file_exists(get_project_executable_path())) {
  445. GUI::MessageBox::show(window(), String::formatted("Could not find file: {}. (did you build the project?)", get_project_executable_path()), "Error", GUI::MessageBox::Type::Error);
  446. return;
  447. }
  448. if (Debugger::the().session()) {
  449. GUI::MessageBox::show(window(), "Debugger is already running", "Error", GUI::MessageBox::Type::Error);
  450. return;
  451. }
  452. Debugger::the().set_executable_path(get_project_executable_path());
  453. m_debugger_thread = LibThread::Thread::construct(Debugger::start_static);
  454. m_debugger_thread->start();
  455. });
  456. }
  457. void HackStudioWidget::initialize_debugger()
  458. {
  459. Debugger::initialize(
  460. m_project->root_path(),
  461. [this](const PtraceRegisters& regs) {
  462. ASSERT(Debugger::the().session());
  463. const auto& debug_session = *Debugger::the().session();
  464. auto source_position = debug_session.get_source_position(regs.eip);
  465. if (!source_position.has_value()) {
  466. dbgln("Could not find source position for address: {:p}", regs.eip);
  467. return Debugger::HasControlPassedToUser::No;
  468. }
  469. dbgln("Debugger stopped at source position: {}:{}", source_position.value().file_path, source_position.value().line_number);
  470. Core::EventLoop::main().post_event(
  471. *window(),
  472. make<Core::DeferredInvocationEvent>(
  473. [this, source_position, &regs](auto&) {
  474. m_current_editor_in_execution = get_editor_of_file(source_position.value().file_path);
  475. m_current_editor_in_execution->editor().set_execution_position(source_position.value().line_number - 1);
  476. m_debug_info_widget->update_state(*Debugger::the().session(), regs);
  477. m_debug_info_widget->set_debug_actions_enabled(true);
  478. m_disassembly_widget->update_state(*Debugger::the().session(), regs);
  479. HackStudioWidget::reveal_action_tab(*m_debug_info_widget);
  480. }));
  481. Core::EventLoop::wake();
  482. return Debugger::HasControlPassedToUser::Yes;
  483. },
  484. [this]() {
  485. Core::EventLoop::main().post_event(*window(), make<Core::DeferredInvocationEvent>([this](auto&) {
  486. m_debug_info_widget->set_debug_actions_enabled(false);
  487. if (m_current_editor_in_execution) {
  488. m_current_editor_in_execution->editor().clear_execution_position();
  489. }
  490. }));
  491. Core::EventLoop::wake();
  492. },
  493. [this]() {
  494. Core::EventLoop::main().post_event(*window(), make<Core::DeferredInvocationEvent>([this](auto&) {
  495. m_debug_info_widget->program_stopped();
  496. m_disassembly_widget->program_stopped();
  497. HackStudioWidget::hide_action_tabs();
  498. GUI::MessageBox::show(window(), "Program Exited", "Debugger", GUI::MessageBox::Type::Information);
  499. }));
  500. Core::EventLoop::wake();
  501. });
  502. }
  503. String HackStudioWidget::get_full_path_of_serenity_source(const String& file)
  504. {
  505. auto path_parts = LexicalPath(file).parts();
  506. ASSERT(path_parts[0] == "..");
  507. path_parts.remove(0);
  508. StringBuilder relative_path_builder;
  509. relative_path_builder.join("/", path_parts);
  510. constexpr char SERENITY_LIBS_PREFIX[] = "/usr/src/serenity";
  511. LexicalPath serenity_sources_base(SERENITY_LIBS_PREFIX);
  512. return String::formatted("{}/{}", serenity_sources_base, relative_path_builder.to_string());
  513. }
  514. NonnullRefPtr<EditorWrapper> HackStudioWidget::get_editor_of_file(const String& file_name)
  515. {
  516. String file_path = file_name;
  517. // TODO: We can probably do a more specific condition here, something like
  518. // "if (file.starts_with("../Libraries/") || file.starts_with("../AK/"))"
  519. if (file_name.starts_with("../")) {
  520. file_path = get_full_path_of_serenity_source(file_name);
  521. }
  522. open_file(file_path);
  523. return current_editor_wrapper();
  524. }
  525. String HackStudioWidget::get_project_executable_path() const
  526. {
  527. // FIXME: Dumb heuristic ahead!
  528. // e.g /my/project => /my/project/project
  529. // TODO: Perhaps a Makefile rule for getting the value of $(PROGRAM) would be better?
  530. return String::formatted("{}/{}", m_project->root_path(), LexicalPath(m_project->root_path()).basename());
  531. }
  532. void HackStudioWidget::build(TerminalWrapper& wrapper)
  533. {
  534. if (m_currently_open_file.ends_with(".js"))
  535. wrapper.run_command(String::formatted("js -A {}", m_currently_open_file));
  536. else
  537. wrapper.run_command("make");
  538. }
  539. void HackStudioWidget::run(TerminalWrapper& wrapper)
  540. {
  541. if (m_currently_open_file.ends_with(".js"))
  542. wrapper.run_command(String::formatted("js {}", m_currently_open_file));
  543. else
  544. wrapper.run_command("make run");
  545. }
  546. void HackStudioWidget::hide_action_tabs()
  547. {
  548. m_action_tab_widget->set_fixed_height(24);
  549. };
  550. Project& HackStudioWidget::project()
  551. {
  552. return *m_project;
  553. }
  554. void HackStudioWidget::set_current_editor_wrapper(RefPtr<EditorWrapper> editor_wrapper)
  555. {
  556. m_current_editor_wrapper = editor_wrapper;
  557. }
  558. void HackStudioWidget::create_project_tree_view(GUI::Widget& parent)
  559. {
  560. m_project_tree_view = parent.add<GUI::TreeView>();
  561. m_project_tree_view->set_model(m_project->model());
  562. for (int column_index = 0; column_index < m_project->model().column_count(); ++column_index)
  563. m_project_tree_view->set_column_hidden(column_index, true);
  564. m_project_tree_view->set_column_hidden(GUI::FileSystemModel::Column::Name, false);
  565. m_project_tree_view->on_context_menu_request = [this](const GUI::ModelIndex& index, const GUI::ContextMenuEvent& event) {
  566. if (index.is_valid()) {
  567. m_project_tree_view_context_menu->popup(event.screen_position(), m_open_selected_action);
  568. }
  569. };
  570. m_project_tree_view->on_selection_change = [this] {
  571. m_open_selected_action->set_enabled(!m_project_tree_view->selection().is_empty());
  572. m_delete_action->set_enabled(!m_project_tree_view->selection().is_empty());
  573. };
  574. m_project_tree_view->on_activation = [this](auto& index) {
  575. auto filename = index.data(GUI::ModelRole::Custom).to_string();
  576. open_file(filename);
  577. };
  578. }
  579. void HackStudioWidget::create_open_files_view(GUI::Widget& parent)
  580. {
  581. m_open_files_view = parent.add<GUI::ListView>();
  582. auto open_files_model = GUI::ItemListModel<String>::create(m_open_files_vector);
  583. m_open_files_view->set_model(open_files_model);
  584. m_open_files_view->on_activation = [this](auto& index) {
  585. open_file(index.data().to_string());
  586. };
  587. }
  588. void HackStudioWidget::create_form_editor(GUI::Widget& parent)
  589. {
  590. m_form_inner_container = parent.add<GUI::Widget>();
  591. m_form_inner_container->set_layout<GUI::HorizontalBoxLayout>();
  592. auto& form_widgets_toolbar = m_form_inner_container->add<GUI::ToolBar>(Orientation::Vertical, 26);
  593. form_widgets_toolbar.set_fixed_width(38);
  594. GUI::ActionGroup tool_actions;
  595. tool_actions.set_exclusive(true);
  596. auto cursor_tool_action = GUI::Action::create_checkable("Cursor", Gfx::Bitmap::load_from_file("/res/icons/hackstudio/Cursor.png"), [this](auto&) {
  597. m_form_editor_widget->set_tool(make<CursorTool>(*m_form_editor_widget));
  598. });
  599. cursor_tool_action->set_checked(true);
  600. tool_actions.add_action(cursor_tool_action);
  601. form_widgets_toolbar.add_action(cursor_tool_action);
  602. GUI::WidgetClassRegistration::for_each([&, this](const GUI::WidgetClassRegistration& reg) {
  603. constexpr size_t gui_namespace_prefix_length = sizeof("GUI::") - 1;
  604. auto icon_path = String::formatted("/res/icons/hackstudio/G{}.png",
  605. reg.class_name().substring(gui_namespace_prefix_length, reg.class_name().length() - gui_namespace_prefix_length));
  606. if (!Core::File::exists(icon_path))
  607. return;
  608. auto action = GUI::Action::create_checkable(reg.class_name(), Gfx::Bitmap::load_from_file(icon_path), [&reg, this](auto&) {
  609. m_form_editor_widget->set_tool(make<WidgetTool>(*m_form_editor_widget, reg));
  610. auto widget = reg.construct();
  611. m_form_editor_widget->form_widget().add_child(widget);
  612. widget->set_relative_rect(30, 30, 30, 30);
  613. m_form_editor_widget->model().update();
  614. });
  615. action->set_checked(false);
  616. tool_actions.add_action(action);
  617. form_widgets_toolbar.add_action(move(action));
  618. });
  619. auto& form_editor_inner_splitter = m_form_inner_container->add<GUI::HorizontalSplitter>();
  620. m_form_editor_widget = form_editor_inner_splitter.add<FormEditorWidget>();
  621. auto& form_editing_pane_container = form_editor_inner_splitter.add<GUI::VerticalSplitter>();
  622. form_editing_pane_container.set_fixed_width(190);
  623. form_editing_pane_container.set_layout<GUI::VerticalBoxLayout>();
  624. auto add_properties_pane = [&](auto& text, auto& pane_widget) {
  625. auto& wrapper = form_editing_pane_container.add<GUI::Widget>();
  626. wrapper.set_layout<GUI::VerticalBoxLayout>();
  627. auto& label = wrapper.add<GUI::Label>(text);
  628. label.set_fill_with_background_color(true);
  629. label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
  630. label.set_font(Gfx::FontDatabase::default_bold_font());
  631. label.set_fixed_height(16);
  632. wrapper.add_child(pane_widget);
  633. };
  634. m_form_widget_tree_view = GUI::TreeView::construct();
  635. m_form_widget_tree_view->set_model(m_form_editor_widget->model());
  636. m_form_widget_tree_view->on_selection_change = [this] {
  637. m_form_editor_widget->selection().disable_hooks();
  638. m_form_editor_widget->selection().clear();
  639. m_form_widget_tree_view->selection().for_each_index([this](auto& index) {
  640. // NOTE: Make sure we don't add the FormWidget itself to the selection,
  641. // since that would allow you to drag-move the FormWidget.
  642. if (index.internal_data() != &m_form_editor_widget->form_widget())
  643. m_form_editor_widget->selection().add(*(GUI::Widget*)index.internal_data());
  644. });
  645. m_form_editor_widget->update();
  646. m_form_editor_widget->selection().enable_hooks();
  647. };
  648. m_form_editor_widget->selection().on_add = [this](auto& widget) {
  649. m_form_widget_tree_view->selection().add(m_form_editor_widget->model().index_for_widget(widget));
  650. };
  651. m_form_editor_widget->selection().on_remove = [this](auto& widget) {
  652. m_form_widget_tree_view->selection().remove(m_form_editor_widget->model().index_for_widget(widget));
  653. };
  654. m_form_editor_widget->selection().on_clear = [this] {
  655. m_form_widget_tree_view->selection().clear();
  656. };
  657. add_properties_pane("Form widget tree:", *m_form_widget_tree_view);
  658. add_properties_pane("Widget properties:", *GUI::TableView::construct());
  659. }
  660. void HackStudioWidget::create_toolbar(GUI::Widget& parent)
  661. {
  662. auto& toolbar = parent.add<GUI::ToolBar>();
  663. toolbar.add_action(*m_new_action);
  664. toolbar.add_action(*m_save_action);
  665. toolbar.add_action(*m_delete_action);
  666. toolbar.add_separator();
  667. toolbar.add_action(GUI::CommonActions::make_cut_action([this](auto&) { current_editor().cut_action().activate(); }));
  668. toolbar.add_action(GUI::CommonActions::make_copy_action([this](auto&) { current_editor().copy_action().activate(); }));
  669. toolbar.add_action(GUI::CommonActions::make_paste_action([this](auto&) { current_editor().paste_action().activate(); }));
  670. toolbar.add_separator();
  671. toolbar.add_action(GUI::CommonActions::make_undo_action([this](auto&) { current_editor().undo_action().activate(); }));
  672. toolbar.add_action(GUI::CommonActions::make_redo_action([this](auto&) { current_editor().redo_action().activate(); }));
  673. toolbar.add_separator();
  674. toolbar.add_action(*m_build_action);
  675. toolbar.add_separator();
  676. toolbar.add_action(*m_run_action);
  677. toolbar.add_action(*m_stop_action);
  678. toolbar.add_separator();
  679. toolbar.add_action(*m_debug_action);
  680. }
  681. NonnullRefPtr<GUI::Action> HackStudioWidget::create_build_action()
  682. {
  683. return GUI::Action::create("Build", { Mod_Ctrl, Key_B }, Gfx::Bitmap::load_from_file("/res/icons/16x16/build.png"), [this](auto&) {
  684. reveal_action_tab(*m_terminal_wrapper);
  685. build(*m_terminal_wrapper);
  686. m_stop_action->set_enabled(true);
  687. });
  688. }
  689. NonnullRefPtr<GUI::Action> HackStudioWidget::create_run_action()
  690. {
  691. return GUI::Action::create("Run", { Mod_Ctrl, Key_R }, Gfx::Bitmap::load_from_file("/res/icons/16x16/program-run.png"), [this](auto&) {
  692. reveal_action_tab(*m_terminal_wrapper);
  693. run(*m_terminal_wrapper);
  694. m_stop_action->set_enabled(true);
  695. });
  696. }
  697. void HackStudioWidget::create_action_tab(GUI::Widget& parent)
  698. {
  699. m_action_tab_widget = parent.add<GUI::TabWidget>();
  700. m_action_tab_widget->set_fixed_height(24);
  701. m_action_tab_widget->on_change = [this](auto&) {
  702. on_action_tab_change();
  703. static bool first_time = true;
  704. if (!first_time)
  705. m_action_tab_widget->set_fixed_height(200);
  706. first_time = false;
  707. };
  708. m_find_in_files_widget = m_action_tab_widget->add_tab<FindInFilesWidget>("Find in files");
  709. m_terminal_wrapper = m_action_tab_widget->add_tab<TerminalWrapper>("Build", false);
  710. m_debug_info_widget = m_action_tab_widget->add_tab<DebugInfoWidget>("Debug");
  711. m_disassembly_widget = m_action_tab_widget->add_tab<DisassemblyWidget>("Disassembly");
  712. m_git_widget = m_action_tab_widget->add_tab<GitWidget>("Git", LexicalPath(m_project->root_path()));
  713. m_git_widget->set_view_diff_callback([this](const auto& original_content, const auto& diff) {
  714. m_diff_viewer->set_content(original_content, diff);
  715. set_edit_mode(EditMode::Diff);
  716. });
  717. }
  718. void HackStudioWidget::create_app_menubar(GUI::MenuBar& menubar)
  719. {
  720. auto& app_menu = menubar.add_menu("Hack Studio");
  721. app_menu.add_action(*m_open_action);
  722. app_menu.add_action(*m_save_action);
  723. app_menu.add_separator();
  724. app_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) {
  725. GUI::Application::the()->quit();
  726. }));
  727. }
  728. void HackStudioWidget::create_project_menubar(GUI::MenuBar& menubar)
  729. {
  730. auto& project_menu = menubar.add_menu("Project");
  731. project_menu.add_action(*m_new_action);
  732. project_menu.add_action(*create_set_autocomplete_mode_action());
  733. }
  734. void HackStudioWidget::create_edit_menubar(GUI::MenuBar& menubar)
  735. {
  736. auto& edit_menu = menubar.add_menu("Edit");
  737. edit_menu.add_action(GUI::Action::create("Find in files...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"), [this](auto&) {
  738. reveal_action_tab(*m_find_in_files_widget);
  739. m_find_in_files_widget->focus_textbox_and_select_all();
  740. }));
  741. edit_menu.add_separator();
  742. auto line_wrapping_action = GUI::Action::create_checkable("Line wrapping", [this](auto& action) {
  743. for (auto& wrapper : m_all_editor_wrappers) {
  744. wrapper.editor().set_line_wrapping_enabled(action.is_checked());
  745. }
  746. });
  747. line_wrapping_action->set_checked(current_editor().is_line_wrapping_enabled());
  748. edit_menu.add_action(line_wrapping_action);
  749. edit_menu.add_separator();
  750. auto vim_emulation_setting_action = GUI::Action::create_checkable("Vim emulation", { Mod_Ctrl | Mod_Shift | Mod_Alt, Key_V }, [this](auto& action) {
  751. if (action.is_checked())
  752. current_editor().set_editing_engine(make<GUI::VimEditingEngine>());
  753. else
  754. current_editor().set_editing_engine(make<GUI::RegularEditingEngine>());
  755. });
  756. vim_emulation_setting_action->set_checked(false);
  757. edit_menu.add_action(vim_emulation_setting_action);
  758. }
  759. void HackStudioWidget::create_build_menubar(GUI::MenuBar& menubar)
  760. {
  761. auto& build_menu = menubar.add_menu("Build");
  762. build_menu.add_action(*m_build_action);
  763. build_menu.add_separator();
  764. build_menu.add_action(*m_run_action);
  765. build_menu.add_action(*m_stop_action);
  766. build_menu.add_separator();
  767. build_menu.add_action(*m_debug_action);
  768. }
  769. void HackStudioWidget::create_view_menubar(GUI::MenuBar& menubar)
  770. {
  771. auto hide_action_tabs_action = GUI::Action::create("Hide action tabs", { Mod_Ctrl | Mod_Shift, Key_X }, [this](auto&) {
  772. hide_action_tabs();
  773. });
  774. auto open_locator_action = GUI::Action::create("Open locator", { Mod_Ctrl, Key_K }, [this](auto&) {
  775. m_locator->open();
  776. });
  777. auto& view_menu = menubar.add_menu("View");
  778. view_menu.add_action(hide_action_tabs_action);
  779. view_menu.add_action(open_locator_action);
  780. view_menu.add_separator();
  781. view_menu.add_action(*m_add_editor_action);
  782. view_menu.add_action(*m_remove_current_editor_action);
  783. view_menu.add_action(*m_add_terminal_action);
  784. view_menu.add_action(*m_remove_current_terminal_action);
  785. }
  786. void HackStudioWidget::create_help_menubar(GUI::MenuBar& menubar)
  787. {
  788. auto& help_menu = menubar.add_menu("Help");
  789. help_menu.add_action(GUI::CommonActions::make_about_action("Hack Studio", GUI::Icon::default_icon("app-hack-studio"), window()));
  790. }
  791. NonnullRefPtr<GUI::Action> HackStudioWidget::create_stop_action()
  792. {
  793. auto action = GUI::Action::create("Stop", Gfx::Bitmap::load_from_file("/res/icons/16x16/program-stop.png"), [this](auto&) {
  794. m_terminal_wrapper->kill_running_command();
  795. });
  796. action->set_enabled(false);
  797. return action;
  798. }
  799. NonnullRefPtr<GUI::Action> HackStudioWidget::create_set_autocomplete_mode_action()
  800. {
  801. auto action = GUI::Action::create_checkable("AutoComplete C++ with Parser", [this](auto& action) {
  802. get_language_client<LanguageClients::Cpp::ServerConnection>(project().root_path())->set_autocomplete_mode(action.is_checked() ? "Parser" : "Lexer");
  803. });
  804. return action;
  805. }
  806. void HackStudioWidget::initialize_menubar(GUI::MenuBar& menubar)
  807. {
  808. create_app_menubar(menubar);
  809. create_project_menubar(menubar);
  810. create_edit_menubar(menubar);
  811. create_build_menubar(menubar);
  812. create_view_menubar(menubar);
  813. create_help_menubar(menubar);
  814. }
  815. HackStudioWidget::~HackStudioWidget()
  816. {
  817. if (!m_debugger_thread.is_null()) {
  818. Debugger::the().set_requested_debugger_action(Debugger::DebuggerAction::Exit);
  819. dbgln("Waiting for debugger thread to terminate");
  820. auto rc = m_debugger_thread->join();
  821. if (rc.is_error()) {
  822. warnln("pthread_join: {}", strerror(rc.error().value()));
  823. dbgln("error joining debugger thread");
  824. }
  825. }
  826. }
  827. }