Serenity Operating System
at master 961 lines 41 kB view raw
1/* 2 * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2022, the SerenityOS developers. 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include "MainWidget.h" 9#include <AK/Optional.h> 10#include <AK/StringBuilder.h> 11#include <AK/URL.h> 12#include <Applications/TextEditor/TextEditorWindowGML.h> 13#include <LibCMake/CMakeCache/SyntaxHighlighter.h> 14#include <LibCMake/SyntaxHighlighter.h> 15#include <LibConfig/Client.h> 16#include <LibCore/Debounce.h> 17#include <LibCpp/SyntaxHighlighter.h> 18#include <LibDesktop/Launcher.h> 19#include <LibGUI/Action.h> 20#include <LibGUI/BoxLayout.h> 21#include <LibGUI/Button.h> 22#include <LibGUI/CheckBox.h> 23#include <LibGUI/FilePicker.h> 24#include <LibGUI/FontPicker.h> 25#include <LibGUI/GML/SyntaxHighlighter.h> 26#include <LibGUI/GitCommitSyntaxHighlighter.h> 27#include <LibGUI/GroupBox.h> 28#include <LibGUI/INISyntaxHighlighter.h> 29#include <LibGUI/Menu.h> 30#include <LibGUI/Menubar.h> 31#include <LibGUI/MessageBox.h> 32#include <LibGUI/RegularEditingEngine.h> 33#include <LibGUI/Statusbar.h> 34#include <LibGUI/TextBox.h> 35#include <LibGUI/TextEditor.h> 36#include <LibGUI/Toolbar.h> 37#include <LibGUI/ToolbarContainer.h> 38#include <LibGUI/VimEditingEngine.h> 39#include <LibGfx/Font/Font.h> 40#include <LibGfx/Painter.h> 41#include <LibJS/SyntaxHighlighter.h> 42#include <LibMarkdown/Document.h> 43#include <LibSQL/AST/SyntaxHighlighter.h> 44#include <LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h> 45#include <LibWeb/HTML/SyntaxHighlighter/SyntaxHighlighter.h> 46#include <LibWebView/OutOfProcessWebView.h> 47#include <Shell/SyntaxHighlighter.h> 48 49namespace TextEditor { 50 51MainWidget::MainWidget() 52{ 53 load_from_gml(text_editor_window_gml).release_value_but_fixme_should_propagate_errors(); 54 55 m_toolbar = *find_descendant_of_type_named<GUI::Toolbar>("toolbar"); 56 m_toolbar_container = *find_descendant_of_type_named<GUI::ToolbarContainer>("toolbar_container"); 57 58 m_editor = *find_descendant_of_type_named<GUI::TextEditor>("editor"); 59 m_editor->set_ruler_visible(true); 60 m_editor->set_automatic_indentation_enabled(true); 61 if (m_editor->editing_engine()->is_regular()) 62 m_editor->set_editing_engine(make<GUI::RegularEditingEngine>()); 63 else if (m_editor->editing_engine()->is_vim()) 64 m_editor->set_editing_engine(make<GUI::VimEditingEngine>()); 65 else 66 VERIFY_NOT_REACHED(); 67 68 auto font_entry = Config::read_string("TextEditor"sv, "Text"sv, "Font"sv, "default"sv); 69 if (font_entry != "default") 70 m_editor->set_font(Gfx::FontDatabase::the().get_by_name(font_entry)); 71 72 m_editor->on_change = Core::debounce([this] { 73 update_preview(); 74 }, 75 100); 76 77 m_editor->on_modified_change = [this](bool modified) { 78 window()->set_modified(modified); 79 }; 80 81 m_find_replace_widget = *find_descendant_of_type_named<GUI::GroupBox>("find_replace_widget"); 82 m_find_widget = *find_descendant_of_type_named<GUI::Widget>("find_widget"); 83 m_replace_widget = *find_descendant_of_type_named<GUI::Widget>("replace_widget"); 84 85 m_find_textbox = *find_descendant_of_type_named<GUI::TextBox>("find_textbox"); 86 m_find_textbox->set_placeholder("Find"sv); 87 88 m_replace_textbox = *find_descendant_of_type_named<GUI::TextBox>("replace_textbox"); 89 m_replace_textbox->set_placeholder("Replace"sv); 90 91 m_match_case_checkbox = *find_descendant_of_type_named<GUI::CheckBox>("match_case_checkbox"); 92 m_match_case_checkbox->on_checked = [this](auto is_checked) { 93 m_match_case = is_checked; 94 }; 95 m_match_case_checkbox->set_checked(true); 96 97 m_regex_checkbox = *find_descendant_of_type_named<GUI::CheckBox>("regex_checkbox"); 98 m_regex_checkbox->on_checked = [this](auto is_checked) { 99 m_use_regex = is_checked; 100 }; 101 m_regex_checkbox->set_checked(false); 102 103 m_wrap_around_checkbox = *find_descendant_of_type_named<GUI::CheckBox>("wrap_around_checkbox"); 104 m_wrap_around_checkbox->on_checked = [this](auto is_checked) { 105 m_should_wrap = is_checked; 106 }; 107 m_wrap_around_checkbox->set_checked(true); 108 109 m_find_next_action = GUI::Action::create("Find &Next", { Mod_Ctrl, Key_G }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find-next.png"sv).release_value_but_fixme_should_propagate_errors(), [this](auto&) { 110 find_text(GUI::TextEditor::SearchDirection::Forward, ShowMessageIfNoResults::Yes); 111 }); 112 113 m_find_previous_action = GUI::Action::create("Find Pr&evious", { Mod_Ctrl | Mod_Shift, Key_G }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find-previous.png"sv).release_value_but_fixme_should_propagate_errors(), [this](auto&) { 114 find_text(GUI::TextEditor::SearchDirection::Backward, ShowMessageIfNoResults::Yes); 115 }); 116 117 m_replace_action = GUI::Action::create("Rep&lace", { Mod_Ctrl, Key_F1 }, [&](auto&) { 118 auto needle = m_find_textbox->text(); 119 auto substitute = m_replace_textbox->text(); 120 if (needle.is_empty()) 121 return; 122 if (m_use_regex) 123 m_editor->document().update_regex_matches(needle); 124 125 auto found_range = m_editor->document().find_next(needle, m_editor->normalized_selection().start(), m_should_wrap ? GUI::TextDocument::SearchShouldWrap::Yes : GUI::TextDocument::SearchShouldWrap::No, m_use_regex, m_match_case); 126 if (found_range.is_valid()) { 127 m_editor->set_selection(found_range); 128 m_editor->insert_at_cursor_or_replace_selection(substitute); 129 } else { 130 GUI::MessageBox::show(window(), 131 DeprecatedString::formatted("Not found: \"{}\"", needle), 132 "Not found"sv, 133 GUI::MessageBox::Type::Information); 134 } 135 }); 136 137 m_replace_all_action = GUI::Action::create("Replace &All", { Mod_Ctrl, Key_F2 }, [&](auto&) { 138 auto needle = m_find_textbox->text(); 139 auto substitute = m_replace_textbox->text(); 140 auto length_delta = substitute.length() - needle.length(); 141 if (needle.is_empty()) 142 return; 143 if (m_use_regex) 144 m_editor->document().update_regex_matches(needle); 145 146 auto found_range = m_editor->document().find_next(needle, {}, GUI::TextDocument::SearchShouldWrap::No, m_use_regex, m_match_case); 147 if (found_range.is_valid()) { 148 while (found_range.is_valid()) { 149 m_editor->set_selection(found_range); 150 m_editor->insert_at_cursor_or_replace_selection(substitute); 151 auto next_start = GUI::TextPosition(found_range.end().line(), found_range.end().column() + length_delta); 152 found_range = m_editor->document().find_next(needle, next_start, GUI::TextDocument::SearchShouldWrap::No, m_use_regex, m_match_case); 153 } 154 } else { 155 GUI::MessageBox::show(window(), 156 DeprecatedString::formatted("Not found: \"{}\"", needle), 157 "Not found"sv, 158 GUI::MessageBox::Type::Information); 159 } 160 }); 161 162 m_find_previous_button = *find_descendant_of_type_named<GUI::Button>("find_previous_button"); 163 m_find_previous_button->set_action(*m_find_previous_action); 164 m_find_previous_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/find-previous.png"sv).release_value_but_fixme_should_propagate_errors()); 165 166 m_find_next_button = *find_descendant_of_type_named<GUI::Button>("find_next_button"); 167 m_find_next_button->set_action(*m_find_next_action); 168 m_find_next_button->set_icon(Gfx::Bitmap::load_from_file("/res/icons/16x16/find-next.png"sv).release_value_but_fixme_should_propagate_errors()); 169 170 m_find_textbox->on_return_pressed = [this] { 171 m_find_next_button->click(); 172 }; 173 174 m_find_textbox->on_escape_pressed = [this] { 175 m_find_replace_widget->set_visible(false); 176 m_editor->set_focus(true); 177 m_editor->reset_search_results(); 178 }; 179 180 m_find_textbox->on_change = [this] { 181 m_editor->reset_search_results(); 182 find_text(GUI::TextEditor::SearchDirection::Forward, ShowMessageIfNoResults::No); 183 }; 184 185 m_replace_button = *find_descendant_of_type_named<GUI::Button>("replace_button"); 186 m_replace_button->set_action(*m_replace_action); 187 188 m_replace_all_button = *find_descendant_of_type_named<GUI::Button>("replace_all_button"); 189 m_replace_all_button->set_action(*m_replace_all_action); 190 191 m_replace_textbox->on_return_pressed = [this] { 192 m_replace_button->click(); 193 }; 194 195 m_replace_textbox->on_escape_pressed = [this] { 196 m_find_replace_widget->set_visible(false); 197 m_editor->set_focus(true); 198 }; 199 200 m_vim_emulation_setting_action = GUI::Action::create_checkable("&Vim Emulation", { Mod_Ctrl | Mod_Shift | Mod_Alt, Key_V }, [&](auto& action) { 201 if (action.is_checked()) 202 m_editor->set_editing_engine(make<GUI::VimEditingEngine>()); 203 else 204 m_editor->set_editing_engine(make<GUI::RegularEditingEngine>()); 205 }); 206 m_vim_emulation_setting_action->set_checked(false); 207 208 m_find_replace_action = GUI::Action::create("&Find/Replace...", { Mod_Ctrl | Mod_Shift, Key_F }, Gfx::Bitmap::load_from_file("/res/icons/16x16/find.png"sv).release_value_but_fixme_should_propagate_errors(), [this](auto&) { 209 m_find_replace_widget->set_visible(true); 210 m_find_widget->set_visible(true); 211 m_replace_widget->set_visible(true); 212 m_find_textbox->set_focus(true); 213 214 if (m_editor->has_selection()) { 215 auto selected_text = m_editor->document().text_in_range(m_editor->normalized_selection()); 216 m_find_textbox->set_text(selected_text); 217 } 218 m_find_textbox->select_all(); 219 }); 220 221 m_editor->add_custom_context_menu_action(*m_find_replace_action); 222 m_editor->add_custom_context_menu_action(*m_find_next_action); 223 m_editor->add_custom_context_menu_action(*m_find_previous_action); 224 225 m_line_column_statusbar_menu = GUI::Menu::construct(); 226 m_syntax_statusbar_menu = GUI::Menu::construct(); 227 228 m_statusbar = *find_descendant_of_type_named<GUI::Statusbar>("statusbar"); 229 m_statusbar->segment(1).set_mode(GUI::Statusbar::Segment::Mode::Auto); 230 m_statusbar->segment(1).set_clickable(true); 231 m_statusbar->segment(1).set_menu(m_syntax_statusbar_menu); 232 m_statusbar->segment(2).set_mode(GUI::Statusbar::Segment::Mode::Fixed); 233 auto width = font().width("Ln 0000, Col 000"sv) + font().max_glyph_width(); 234 m_statusbar->segment(2).set_fixed_width(width); 235 m_statusbar->segment(2).set_clickable(true); 236 m_statusbar->segment(2).set_menu(m_line_column_statusbar_menu); 237 238 GUI::Application::the()->on_action_enter = [this](GUI::Action& action) { 239 auto text = action.status_tip(); 240 if (text.is_empty()) 241 text = Gfx::parse_ampersand_string(action.text()); 242 m_statusbar->set_override_text(move(text)); 243 }; 244 245 GUI::Application::the()->on_action_leave = [this](GUI::Action&) { 246 m_statusbar->set_override_text({}); 247 }; 248 249 m_editor->on_cursor_change = [this] { update_statusbar(); }; 250 m_editor->on_selection_change = [this] { update_statusbar(); }; 251 m_editor->on_highlighter_change = [this] { update_statusbar(); }; 252 253 m_new_action = GUI::Action::create("&New", { Mod_Ctrl, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/new.png"sv).release_value_but_fixme_should_propagate_errors(), [this](GUI::Action const&) { 254 if (editor().document().is_modified()) { 255 auto save_document_first_result = GUI::MessageBox::ask_about_unsaved_changes(window(), m_path, editor().document().undo_stack().last_unmodified_timestamp()); 256 if (save_document_first_result == GUI::Dialog::ExecResult::Yes) 257 m_save_action->activate(); 258 if (save_document_first_result != GUI::Dialog::ExecResult::No && editor().document().is_modified()) 259 return; 260 } 261 262 m_editor->set_text(StringView()); 263 set_path({}); 264 update_title(); 265 }); 266 267 m_open_action = GUI::CommonActions::make_open_action([this](auto&) { 268 if (editor().document().is_modified()) { 269 auto save_document_first_result = GUI::MessageBox::ask_about_unsaved_changes(window(), m_path, editor().document().undo_stack().last_unmodified_timestamp()); 270 if (save_document_first_result == GUI::Dialog::ExecResult::Yes) 271 m_save_action->activate(); 272 if (save_document_first_result != GUI::Dialog::ExecResult::No && editor().document().is_modified()) 273 return; 274 } 275 276 auto response = FileSystemAccessClient::Client::the().open_file(window()); 277 if (response.is_error()) 278 return; 279 280 if (auto result = read_file(response.value().filename(), response.value().stream()); result.is_error()) 281 GUI::MessageBox::show(window(), "Unable to open file.\n"sv, "Error"sv, GUI::MessageBox::Type::Error); 282 }); 283 284 m_save_as_action = GUI::CommonActions::make_save_as_action([&](auto&) { 285 auto extension = m_extension; 286 if (extension.is_null() && m_editor->syntax_highlighter()) 287 extension = Syntax::common_language_extension(m_editor->syntax_highlighter()->language()); 288 289 auto response = FileSystemAccessClient::Client::the().save_file(window(), m_name, extension); 290 if (response.is_error()) 291 return; 292 293 auto file = response.release_value(); 294 if (auto result = m_editor->write_to_file(file.stream()); result.is_error()) { 295 GUI::MessageBox::show(window(), "Unable to save file.\n"sv, "Error"sv, GUI::MessageBox::Type::Error); 296 return; 297 } 298 299 set_path(file.filename()); 300 GUI::Application::the()->set_most_recently_open_file(file.filename()); 301 dbgln("Wrote document to {}", file.filename()); 302 }); 303 304 m_save_action = GUI::CommonActions::make_save_action([&](auto&) { 305 if (m_path.is_empty()) { 306 m_save_as_action->activate(); 307 return; 308 } 309 auto response = FileSystemAccessClient::Client::the().request_file(window(), m_path, Core::File::OpenMode::Truncate | Core::File::OpenMode::Write); 310 if (response.is_error()) 311 return; 312 313 if (auto result = m_editor->write_to_file(response.value().stream()); result.is_error()) { 314 GUI::MessageBox::show(window(), "Unable to save file.\n"sv, "Error"sv, GUI::MessageBox::Type::Error); 315 } 316 }); 317 318 auto file_manager_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/app-file-manager.png"sv).release_value_but_fixme_should_propagate_errors(); 319 m_open_folder_action = GUI::Action::create("Reveal in File Manager", { Mod_Ctrl | Mod_Shift, Key_O }, file_manager_icon, [&](auto&) { 320 auto lexical_path = LexicalPath(m_path); 321 Desktop::Launcher::open(URL::create_with_file_scheme(lexical_path.dirname(), lexical_path.basename())); 322 }); 323 m_open_folder_action->set_enabled(!m_path.is_empty()); 324 m_open_folder_action->set_status_tip("Open the current file location in File Manager"); 325 326 m_toolbar->add_action(*m_new_action); 327 m_toolbar->add_action(*m_open_action); 328 m_toolbar->add_action(*m_save_action); 329 330 m_toolbar->add_separator(); 331 332 m_toolbar->add_action(*m_open_folder_action); 333 334 m_toolbar->add_separator(); 335 336 m_toolbar->add_action(m_editor->cut_action()); 337 m_toolbar->add_action(m_editor->copy_action()); 338 m_toolbar->add_action(m_editor->paste_action()); 339 340 m_toolbar->add_separator(); 341 342 m_toolbar->add_action(m_editor->undo_action()); 343 m_toolbar->add_action(m_editor->redo_action()); 344} 345 346WebView::OutOfProcessWebView& MainWidget::ensure_web_view() 347{ 348 if (!m_page_view) { 349 auto& web_view_container = *find_descendant_of_type_named<GUI::Widget>("web_view_container"); 350 m_page_view = web_view_container.add<WebView::OutOfProcessWebView>(); 351 m_page_view->on_link_hover = [this](auto& url) { 352 if (url.is_valid()) 353 m_statusbar->set_text(url.to_deprecated_string()); 354 else 355 update_statusbar(); 356 }; 357 m_page_view->on_link_click = [&](auto& url, auto&, unsigned) { 358 if (!Desktop::Launcher::open(url)) { 359 GUI::MessageBox::show( 360 window(), 361 DeprecatedString::formatted("The link to '{}' could not be opened.", url), 362 "Failed to open link"sv, 363 GUI::MessageBox::Type::Error); 364 } 365 }; 366 } 367 return *m_page_view; 368} 369 370void MainWidget::initialize_menubar(GUI::Window& window) 371{ 372 auto& file_menu = window.add_menu("&File"); 373 file_menu.add_action(*m_new_action); 374 file_menu.add_action(*m_open_action); 375 file_menu.add_action(*m_save_action); 376 file_menu.add_action(*m_save_as_action); 377 file_menu.add_separator(); 378 file_menu.add_action(*m_open_folder_action); 379 file_menu.add_separator(); 380 381 // FIXME: Propagate errors. 382 (void)file_menu.add_recent_files_list([&](auto& action) { 383 if (editor().document().is_modified()) { 384 auto save_document_first_result = GUI::MessageBox::ask_about_unsaved_changes(&window, m_path, editor().document().undo_stack().last_unmodified_timestamp()); 385 if (save_document_first_result == GUI::Dialog::ExecResult::Yes) 386 m_save_action->activate(); 387 if (save_document_first_result != GUI::Dialog::ExecResult::No && editor().document().is_modified()) 388 return; 389 } 390 391 auto response = FileSystemAccessClient::Client::the().request_file(&window, action.text(), Core::File::OpenMode::Read); 392 if (response.is_error()) 393 return; 394 395 if (auto result = read_file(response.value().filename(), response.value().stream()); result.is_error()) 396 GUI::MessageBox::show(&window, "Unable to open file.\n"sv, "Error"sv, GUI::MessageBox::Type::Error); 397 }); 398 file_menu.add_action(GUI::CommonActions::make_quit_action([this](auto&) { 399 if (!request_close()) 400 return; 401 GUI::Application::the()->quit(); 402 })); 403 404 auto& edit_menu = window.add_menu("&Edit"); 405 edit_menu.add_action(m_editor->undo_action()); 406 edit_menu.add_action(m_editor->redo_action()); 407 edit_menu.add_separator(); 408 edit_menu.add_action(m_editor->cut_action()); 409 edit_menu.add_action(m_editor->copy_action()); 410 edit_menu.add_action(m_editor->paste_action()); 411 edit_menu.add_separator(); 412 edit_menu.add_action(m_editor->insert_emoji_action()); 413 edit_menu.add_action(*m_vim_emulation_setting_action); 414 edit_menu.add_separator(); 415 edit_menu.add_action(*m_find_replace_action); 416 edit_menu.add_action(*m_find_next_action); 417 edit_menu.add_action(*m_find_previous_action); 418 edit_menu.add_action(*m_replace_action); 419 edit_menu.add_action(*m_replace_all_action); 420 421 m_no_preview_action = GUI::Action::create_checkable( 422 "&No Preview", [this](auto&) { 423 set_preview_mode(PreviewMode::None); 424 }); 425 426 m_markdown_preview_action = GUI::Action::create_checkable( 427 "&Markdown Preview", [this](auto&) { 428 set_preview_mode(PreviewMode::Markdown); 429 }, 430 this); 431 432 m_html_preview_action = GUI::Action::create_checkable( 433 "&HTML Preview", [this](auto&) { 434 set_preview_mode(PreviewMode::HTML); 435 }, 436 this); 437 438 m_preview_actions.add_action(*m_no_preview_action); 439 m_preview_actions.add_action(*m_markdown_preview_action); 440 m_preview_actions.add_action(*m_html_preview_action); 441 m_preview_actions.set_exclusive(true); 442 443 m_layout_toolbar_action = GUI::Action::create_checkable("&Toolbar", [&](auto& action) { 444 action.is_checked() ? m_toolbar_container->set_visible(true) : m_toolbar_container->set_visible(false); 445 Config::write_bool("TextEditor"sv, "Layout"sv, "ShowToolbar"sv, action.is_checked()); 446 }); 447 auto show_toolbar = Config::read_bool("TextEditor"sv, "Layout"sv, "ShowToolbar"sv, true); 448 m_layout_toolbar_action->set_checked(show_toolbar); 449 m_toolbar_container->set_visible(show_toolbar); 450 451 m_layout_statusbar_action = GUI::Action::create_checkable("&Status Bar", [&](auto& action) { 452 action.is_checked() ? m_statusbar->set_visible(true) : m_statusbar->set_visible(false); 453 Config::write_bool("TextEditor"sv, "Layout"sv, "ShowStatusbar"sv, action.is_checked()); 454 update_statusbar(); 455 }); 456 auto show_statusbar = Config::read_bool("TextEditor"sv, "Layout"sv, "ShowStatusbar"sv, true); 457 m_layout_statusbar_action->set_checked(show_statusbar); 458 m_statusbar->set_visible(show_statusbar); 459 460 m_layout_ruler_action = GUI::Action::create_checkable("&Ruler", [&](auto& action) { 461 action.is_checked() ? m_editor->set_ruler_visible(true) : m_editor->set_ruler_visible(false); 462 Config::write_bool("TextEditor"sv, "Layout"sv, "ShowRuler"sv, action.is_checked()); 463 }); 464 auto show_ruler = Config::read_bool("TextEditor"sv, "Layout"sv, "ShowRuler"sv, true); 465 m_layout_ruler_action->set_checked(show_ruler); 466 m_editor->set_ruler_visible(show_ruler); 467 468 auto& view_menu = window.add_menu("&View"); 469 auto& layout_menu = view_menu.add_submenu("&Layout"); 470 layout_menu.add_action(*m_layout_toolbar_action); 471 layout_menu.add_action(*m_layout_statusbar_action); 472 layout_menu.add_action(*m_layout_ruler_action); 473 474 view_menu.add_separator(); 475 476 view_menu.add_action(GUI::Action::create("Editor &Font...", Gfx::Bitmap::load_from_file("/res/icons/16x16/app-font-editor.png"sv).release_value_but_fixme_should_propagate_errors(), 477 [&](auto&) { 478 auto picker = GUI::FontPicker::construct(&window, &m_editor->font(), false); 479 if (picker->exec() == GUI::Dialog::ExecResult::OK) { 480 dbgln("setting font {}", picker->font()->qualified_name()); 481 m_editor->set_font(picker->font()); 482 Config::write_string("TextEditor"sv, "Text"sv, "Font"sv, picker->font()->qualified_name()); 483 } 484 })); 485 486 view_menu.add_separator(); 487 488 m_wrapping_mode_actions.set_exclusive(true); 489 auto& wrapping_mode_menu = view_menu.add_submenu("&Wrapping Mode"); 490 m_no_wrapping_action = GUI::Action::create_checkable("&No Wrapping", [&](auto&) { 491 m_editor->set_wrapping_mode(GUI::TextEditor::WrappingMode::NoWrap); 492 Config::write_string("TextEditor"sv, "View"sv, "WrappingMode"sv, "None"sv); 493 }); 494 m_wrap_anywhere_action = GUI::Action::create_checkable("Wrap &Anywhere", [&](auto&) { 495 m_editor->set_wrapping_mode(GUI::TextEditor::WrappingMode::WrapAnywhere); 496 Config::write_string("TextEditor"sv, "View"sv, "WrappingMode"sv, "Anywhere"sv); 497 }); 498 m_wrap_at_words_action = GUI::Action::create_checkable("Wrap at &Words", [&](auto&) { 499 m_editor->set_wrapping_mode(GUI::TextEditor::WrappingMode::WrapAtWords); 500 Config::write_string("TextEditor"sv, "View"sv, "WrappingMode"sv, "Words"sv); 501 }); 502 503 m_wrapping_mode_actions.add_action(*m_no_wrapping_action); 504 m_wrapping_mode_actions.add_action(*m_wrap_anywhere_action); 505 m_wrapping_mode_actions.add_action(*m_wrap_at_words_action); 506 507 wrapping_mode_menu.add_action(*m_no_wrapping_action); 508 wrapping_mode_menu.add_action(*m_wrap_anywhere_action); 509 wrapping_mode_menu.add_action(*m_wrap_at_words_action); 510 511 auto word_wrap = Config::read_string("TextEditor"sv, "View"sv, "WrappingMode"sv, "Words"sv); 512 if (word_wrap == "None") { 513 m_no_wrapping_action->set_checked(true); 514 m_editor->set_wrapping_mode(GUI::TextEditor::WrappingMode::NoWrap); 515 } else if (word_wrap == "Anywhere") { 516 m_wrap_anywhere_action->set_checked(true); 517 m_editor->set_wrapping_mode(GUI::TextEditor::WrappingMode::WrapAnywhere); 518 } else { 519 m_wrap_at_words_action->set_checked(true); 520 m_editor->set_wrapping_mode(GUI::TextEditor::WrappingMode::WrapAtWords); 521 } 522 523 m_soft_tab_width_actions.set_exclusive(true); 524 auto& soft_tab_width_menu = view_menu.add_submenu("&Tab Width"); 525 m_soft_tab_1_width_action = GUI::Action::create_checkable("1", [&](auto&) { 526 m_editor->set_soft_tab_width(1); 527 }); 528 m_soft_tab_2_width_action = GUI::Action::create_checkable("2", [&](auto&) { 529 m_editor->set_soft_tab_width(2); 530 }); 531 m_soft_tab_4_width_action = GUI::Action::create_checkable("4", [&](auto&) { 532 m_editor->set_soft_tab_width(4); 533 }); 534 m_soft_tab_8_width_action = GUI::Action::create_checkable("8", [&](auto&) { 535 m_editor->set_soft_tab_width(8); 536 }); 537 m_soft_tab_16_width_action = GUI::Action::create_checkable("16", [&](auto&) { 538 m_editor->set_soft_tab_width(16); 539 }); 540 541 m_soft_tab_width_actions.add_action(*m_soft_tab_1_width_action); 542 m_soft_tab_width_actions.add_action(*m_soft_tab_2_width_action); 543 m_soft_tab_width_actions.add_action(*m_soft_tab_4_width_action); 544 m_soft_tab_width_actions.add_action(*m_soft_tab_8_width_action); 545 m_soft_tab_width_actions.add_action(*m_soft_tab_16_width_action); 546 547 soft_tab_width_menu.add_action(*m_soft_tab_1_width_action); 548 soft_tab_width_menu.add_action(*m_soft_tab_2_width_action); 549 soft_tab_width_menu.add_action(*m_soft_tab_4_width_action); 550 soft_tab_width_menu.add_action(*m_soft_tab_8_width_action); 551 soft_tab_width_menu.add_action(*m_soft_tab_16_width_action); 552 553 m_soft_tab_4_width_action->set_checked(true); 554 555 view_menu.add_separator(); 556 557 m_visualize_trailing_whitespace_action = GUI::Action::create_checkable("T&railing Whitespace", [&](auto&) { 558 m_editor->set_visualize_trailing_whitespace(m_visualize_trailing_whitespace_action->is_checked()); 559 }); 560 m_visualize_leading_whitespace_action = GUI::Action::create_checkable("L&eading Whitespace", [&](auto&) { 561 m_editor->set_visualize_leading_whitespace(m_visualize_leading_whitespace_action->is_checked()); 562 }); 563 564 m_visualize_trailing_whitespace_action->set_checked(true); 565 m_visualize_trailing_whitespace_action->set_status_tip("Visualize trailing whitespace"); 566 m_visualize_leading_whitespace_action->set_status_tip("Visualize leading whitespace"); 567 568 view_menu.add_action(*m_visualize_trailing_whitespace_action); 569 view_menu.add_action(*m_visualize_leading_whitespace_action); 570 571 m_cursor_line_highlighting_action = GUI::Action::create_checkable("L&ine Highlighting", [&](auto&) { 572 m_editor->set_cursor_line_highlighting(m_cursor_line_highlighting_action->is_checked()); 573 }); 574 575 m_cursor_line_highlighting_action->set_checked(true); 576 m_cursor_line_highlighting_action->set_status_tip("Highlight the current line"); 577 578 view_menu.add_action(*m_cursor_line_highlighting_action); 579 580 m_relative_line_number_action = GUI::Action::create_checkable("R&elative Line Number", [&](auto& action) { 581 m_editor->set_relative_line_number(action.is_checked()); 582 Config::write_bool("TextEditor"sv, "View"sv, "RelativeLineNumber"sv, action.is_checked()); 583 }); 584 585 auto show_relative_line_number = Config::read_bool("TextEditor"sv, "View"sv, "RelativeLineNumber"sv, false); 586 m_relative_line_number_action->set_checked(show_relative_line_number); 587 m_editor->set_relative_line_number(show_relative_line_number); 588 589 m_relative_line_number_action->set_status_tip("Set relative line number"); 590 591 view_menu.add_action(*m_relative_line_number_action); 592 593 view_menu.add_separator(); 594 view_menu.add_action(*m_no_preview_action); 595 view_menu.add_action(*m_markdown_preview_action); 596 view_menu.add_action(*m_html_preview_action); 597 m_no_preview_action->set_checked(true); 598 view_menu.add_separator(); 599 600 syntax_actions.set_exclusive(true); 601 602 auto& syntax_menu = view_menu.add_submenu("&Syntax"); 603 m_plain_text_highlight = GUI::Action::create_checkable("&Plain Text", [&](auto&) { 604 m_statusbar->set_text(1, "Plain Text"); 605 m_editor->set_syntax_highlighter({}); 606 m_editor->update(); 607 }); 608 m_plain_text_highlight->set_checked(true); 609 m_statusbar->set_text(1, "Plain Text"); 610 syntax_actions.add_action(*m_plain_text_highlight); 611 syntax_menu.add_action(*m_plain_text_highlight); 612 613 m_cpp_highlight = GUI::Action::create_checkable("&C++", [&](auto&) { 614 m_editor->set_syntax_highlighter(make<Cpp::SyntaxHighlighter>()); 615 m_editor->update(); 616 }); 617 syntax_actions.add_action(*m_cpp_highlight); 618 syntax_menu.add_action(*m_cpp_highlight); 619 620 m_cmake_highlight = GUI::Action::create_checkable("C&Make", [&](auto&) { 621 m_editor->set_syntax_highlighter(make<CMake::SyntaxHighlighter>()); 622 m_editor->update(); 623 }); 624 syntax_actions.add_action(*m_cmake_highlight); 625 syntax_menu.add_action(*m_cmake_highlight); 626 627 m_cmakecache_highlight = GUI::Action::create_checkable("CM&akeCache", [&](auto&) { 628 m_editor->set_syntax_highlighter(make<CMake::Cache::SyntaxHighlighter>()); 629 m_editor->update(); 630 }); 631 syntax_actions.add_action(*m_cmakecache_highlight); 632 syntax_menu.add_action(*m_cmakecache_highlight); 633 634 m_js_highlight = GUI::Action::create_checkable("&JavaScript", [&](auto&) { 635 m_editor->set_syntax_highlighter(make<JS::SyntaxHighlighter>()); 636 m_editor->update(); 637 }); 638 syntax_actions.add_action(*m_js_highlight); 639 syntax_menu.add_action(*m_js_highlight); 640 641 m_css_highlight = GUI::Action::create_checkable("C&SS", [&](auto&) { 642 m_editor->set_syntax_highlighter(make<Web::CSS::SyntaxHighlighter>()); 643 m_editor->update(); 644 }); 645 syntax_actions.add_action(*m_css_highlight); 646 syntax_menu.add_action(*m_css_highlight); 647 648 m_html_highlight = GUI::Action::create_checkable("&HTML File", [&](auto&) { 649 m_editor->set_syntax_highlighter(make<Web::HTML::SyntaxHighlighter>()); 650 m_editor->update(); 651 }); 652 syntax_actions.add_action(*m_html_highlight); 653 syntax_menu.add_action(*m_html_highlight); 654 655 m_git_highlight = GUI::Action::create_checkable("Gi&t Commit", [&](auto&) { 656 m_editor->set_syntax_highlighter(make<GUI::GitCommitSyntaxHighlighter>()); 657 m_editor->update(); 658 }); 659 syntax_actions.add_action(*m_git_highlight); 660 syntax_menu.add_action(*m_git_highlight); 661 662 m_gml_highlight = GUI::Action::create_checkable("&GML", [&](auto&) { 663 m_editor->set_syntax_highlighter(make<GUI::GML::SyntaxHighlighter>()); 664 m_editor->update(); 665 }); 666 syntax_actions.add_action(*m_gml_highlight); 667 syntax_menu.add_action(*m_gml_highlight); 668 669 m_ini_highlight = GUI::Action::create_checkable("&INI File", [&](auto&) { 670 m_editor->set_syntax_highlighter(make<GUI::IniSyntaxHighlighter>()); 671 m_editor->update(); 672 }); 673 syntax_actions.add_action(*m_ini_highlight); 674 syntax_menu.add_action(*m_ini_highlight); 675 676 m_shell_highlight = GUI::Action::create_checkable("Sh&ell File", [&](auto&) { 677 m_editor->set_syntax_highlighter(make<Shell::SyntaxHighlighter>()); 678 m_editor->update(); 679 }); 680 syntax_actions.add_action(*m_shell_highlight); 681 syntax_menu.add_action(*m_shell_highlight); 682 683 m_sql_highlight = GUI::Action::create_checkable("S&QL File", [&](auto&) { 684 m_editor->set_syntax_highlighter(make<SQL::AST::SyntaxHighlighter>()); 685 m_editor->update(); 686 }); 687 syntax_actions.add_action(*m_sql_highlight); 688 syntax_menu.add_action(*m_sql_highlight); 689 690 auto& help_menu = window.add_menu("&Help"); 691 help_menu.add_action(GUI::CommonActions::make_command_palette_action(&window)); 692 help_menu.add_action(GUI::CommonActions::make_help_action([](auto&) { 693 Desktop::Launcher::open(URL::create_with_file_scheme("/usr/share/man/man1/TextEditor.md"), "/bin/Help"); 694 })); 695 help_menu.add_action(GUI::CommonActions::make_about_action("Text Editor", GUI::Icon::default_icon("app-text-editor"sv), &window)); 696 697 auto& wrapping_statusbar_menu = m_line_column_statusbar_menu->add_submenu("&Wrapping Mode"); 698 wrapping_statusbar_menu.add_action(*m_no_wrapping_action); 699 wrapping_statusbar_menu.add_action(*m_wrap_anywhere_action); 700 wrapping_statusbar_menu.add_action(*m_wrap_at_words_action); 701 702 auto& tab_width_statusbar_menu = m_line_column_statusbar_menu->add_submenu("&Tab Width"); 703 tab_width_statusbar_menu.add_action(*m_soft_tab_1_width_action); 704 tab_width_statusbar_menu.add_action(*m_soft_tab_2_width_action); 705 tab_width_statusbar_menu.add_action(*m_soft_tab_4_width_action); 706 tab_width_statusbar_menu.add_action(*m_soft_tab_8_width_action); 707 tab_width_statusbar_menu.add_action(*m_soft_tab_16_width_action); 708 709 m_line_column_statusbar_menu->add_separator(); 710 m_line_column_statusbar_menu->add_action(*m_cursor_line_highlighting_action); 711 712 m_syntax_statusbar_menu->add_action(*m_plain_text_highlight); 713 m_syntax_statusbar_menu->add_action(*m_cpp_highlight); 714 m_syntax_statusbar_menu->add_action(*m_cmake_highlight); 715 m_syntax_statusbar_menu->add_action(*m_cmakecache_highlight); 716 m_syntax_statusbar_menu->add_action(*m_css_highlight); 717 m_syntax_statusbar_menu->add_action(*m_git_highlight); 718 m_syntax_statusbar_menu->add_action(*m_gml_highlight); 719 m_syntax_statusbar_menu->add_action(*m_html_highlight); 720 m_syntax_statusbar_menu->add_action(*m_ini_highlight); 721 m_syntax_statusbar_menu->add_action(*m_js_highlight); 722 m_syntax_statusbar_menu->add_action(*m_shell_highlight); 723 m_syntax_statusbar_menu->add_action(*m_sql_highlight); 724} 725 726void MainWidget::set_path(StringView path) 727{ 728 if (path.is_empty()) { 729 m_path = {}; 730 m_name = {}; 731 m_extension = {}; 732 } else { 733 auto lexical_path = LexicalPath(path); 734 m_path = lexical_path.string(); 735 m_name = lexical_path.title(); 736 m_extension = lexical_path.extension(); 737 } 738 739 if (m_extension == "c" || m_extension == "cc" || m_extension == "cxx" || m_extension == "cpp" || m_extension == "c++" 740 || m_extension == "h" || m_extension == "hh" || m_extension == "hxx" || m_extension == "hpp" || m_extension == "h++") { 741 m_cpp_highlight->activate(); 742 } else if (m_extension == "cmake" || (m_extension == "txt" && m_name == "CMakeLists")) { 743 m_cmake_highlight->activate(); 744 } else if (m_extension == "txt" && m_name == "CMakeCache") { 745 m_cmakecache_highlight->activate(); 746 } else if (m_extension == "js" || m_extension == "mjs" || m_extension == "json") { 747 m_js_highlight->activate(); 748 } else if (m_name == "COMMIT_EDITMSG") { 749 m_git_highlight->activate(); 750 } else if (m_extension == "gml") { 751 m_gml_highlight->activate(); 752 } else if (m_extension == "ini" || m_extension == "af") { 753 m_ini_highlight->activate(); 754 } else if (m_extension == "sh" || m_extension == "bash") { 755 m_shell_highlight->activate(); 756 } else if (m_extension == "sql") { 757 m_sql_highlight->activate(); 758 } else if (m_extension == "html" || m_extension == "htm") { 759 m_html_highlight->activate(); 760 } else if (m_extension == "css") { 761 m_css_highlight->activate(); 762 } else { 763 m_plain_text_highlight->activate(); 764 } 765 766 if (m_auto_detect_preview_mode) { 767 if (m_extension == "md") 768 set_preview_mode(PreviewMode::Markdown); 769 else if (m_extension == "html" || m_extension == "htm") 770 set_preview_mode(PreviewMode::HTML); 771 else 772 set_preview_mode(PreviewMode::None); 773 } 774 775 m_open_folder_action->set_enabled(!path.is_empty()); 776 update_title(); 777} 778 779void MainWidget::update_title() 780{ 781 StringBuilder builder; 782 if (m_path.is_empty()) 783 builder.append("Untitled"sv); 784 else 785 builder.append(m_path); 786 builder.append("[*] - Text Editor"sv); 787 window()->set_title(builder.to_deprecated_string()); 788} 789 790ErrorOr<void> MainWidget::read_file(String const& filename, Core::File& file) 791{ 792 m_editor->set_text(TRY(file.read_until_eof())); 793 set_path(filename); 794 GUI::Application::the()->set_most_recently_open_file(filename); 795 m_editor->set_focus(true); 796 return {}; 797} 798 799void MainWidget::open_nonexistent_file(DeprecatedString const& path) 800{ 801 m_editor->set_text({}); 802 set_path(path); 803 m_editor->set_focus(true); 804} 805 806bool MainWidget::request_close() 807{ 808 if (!editor().document().is_modified()) 809 return true; 810 auto result = GUI::MessageBox::ask_about_unsaved_changes(window(), m_path, editor().document().undo_stack().last_unmodified_timestamp()); 811 812 if (result == GUI::MessageBox::ExecResult::Yes) { 813 m_save_action->activate(); 814 if (editor().document().is_modified()) 815 return false; 816 return true; 817 } 818 819 if (result == GUI::MessageBox::ExecResult::No) 820 return true; 821 822 return false; 823} 824 825void MainWidget::drag_enter_event(GUI::DragEvent& event) 826{ 827 auto const& mime_types = event.mime_types(); 828 if (mime_types.contains_slow("text/uri-list")) 829 event.accept(); 830} 831 832void MainWidget::drop_event(GUI::DropEvent& event) 833{ 834 event.accept(); 835 window()->move_to_front(); 836 837 if (event.mime_data().has_urls()) { 838 auto urls = event.mime_data().urls(); 839 if (urls.is_empty()) 840 return; 841 if (urls.size() > 1) { 842 GUI::MessageBox::show(window(), "TextEditor can only open one file at a time!"sv, "One at a time please!"sv, GUI::MessageBox::Type::Error); 843 return; 844 } 845 if (!request_close()) 846 return; 847 848 auto response = FileSystemAccessClient::Client::the().request_file_read_only_approved(window(), urls.first().path()); 849 if (response.is_error()) 850 return; 851 if (auto result = read_file(response.value().filename(), response.value().stream()); result.is_error()) 852 GUI::MessageBox::show(window(), "Unable to open file.\n"sv, "Error"sv, GUI::MessageBox::Type::Error); 853 } 854} 855 856void MainWidget::set_web_view_visible(bool visible) 857{ 858 if (!visible && !m_page_view) 859 return; 860 ensure_web_view(); 861 auto& web_view_container = *find_descendant_of_type_named<GUI::Widget>("web_view_container"); 862 web_view_container.set_visible(visible); 863} 864 865void MainWidget::set_preview_mode(PreviewMode mode) 866{ 867 if (m_preview_mode == mode) 868 return; 869 m_preview_mode = mode; 870 871 if (m_preview_mode == PreviewMode::HTML) { 872 m_html_preview_action->set_checked(true); 873 set_web_view_visible(true); 874 update_html_preview(); 875 } else if (m_preview_mode == PreviewMode::Markdown) { 876 m_markdown_preview_action->set_checked(true); 877 set_web_view_visible(true); 878 update_markdown_preview(); 879 } else { 880 m_no_preview_action->set_checked(true); 881 set_web_view_visible(false); 882 } 883} 884 885void MainWidget::update_preview() 886{ 887 switch (m_preview_mode) { 888 case PreviewMode::Markdown: 889 update_markdown_preview(); 890 break; 891 case PreviewMode::HTML: 892 update_html_preview(); 893 break; 894 default: 895 break; 896 } 897} 898 899void MainWidget::update_markdown_preview() 900{ 901 auto document = Markdown::Document::parse(m_editor->text()); 902 if (document) { 903 auto html = document->render_to_html(); 904 auto current_scroll_pos = m_page_view->visible_content_rect(); 905 m_page_view->load_html(html, URL::create_with_file_scheme(m_path)); 906 m_page_view->scroll_into_view(current_scroll_pos, true, true); 907 } 908} 909 910void MainWidget::update_html_preview() 911{ 912 auto current_scroll_pos = m_page_view->visible_content_rect(); 913 m_page_view->load_html(m_editor->text(), URL::create_with_file_scheme(m_path)); 914 m_page_view->scroll_into_view(current_scroll_pos, true, true); 915} 916 917void MainWidget::update_statusbar() 918{ 919 if (!m_statusbar->is_visible()) 920 return; 921 922 StringBuilder builder; 923 if (m_editor->has_selection()) { 924 DeprecatedString selected_text = m_editor->selected_text(); 925 auto word_count = m_editor->number_of_selected_words(); 926 builder.appendff("{} {} ({} {}) selected", selected_text.length(), selected_text.length() == 1 ? "character" : "characters", word_count, word_count != 1 ? "words" : "word"); 927 } else { 928 DeprecatedString text = m_editor->text(); 929 auto word_count = m_editor->number_of_words(); 930 builder.appendff("{} {} ({} {})", text.length(), text.length() == 1 ? "character" : "characters", word_count, word_count != 1 ? "words" : "word"); 931 } 932 m_statusbar->set_text(0, builder.to_deprecated_string()); 933 934 if (m_editor && m_editor->syntax_highlighter()) { 935 auto language = m_editor->syntax_highlighter()->language(); 936 m_statusbar->set_text(1, Syntax::language_to_string(language)); 937 } 938 m_statusbar->set_text(2, DeprecatedString::formatted("Ln {}, Col {}", m_editor->cursor().line() + 1, m_editor->cursor().column())); 939} 940 941void MainWidget::find_text(GUI::TextEditor::SearchDirection direction, ShowMessageIfNoResults show_message) 942{ 943 auto needle = m_find_textbox->text(); 944 if (needle.is_empty()) 945 return; 946 if (m_use_regex) 947 m_editor->document().update_regex_matches(needle); 948 949 auto result = m_editor->find_text(needle, direction, 950 m_should_wrap ? GUI::TextDocument::SearchShouldWrap::Yes : GUI::TextDocument::SearchShouldWrap::No, 951 m_use_regex, m_match_case); 952 953 if (!result.is_valid() && show_message == ShowMessageIfNoResults::Yes) { 954 GUI::MessageBox::show(window(), 955 DeprecatedString::formatted("Not found: \"{}\"", needle), 956 "Not found"sv, 957 GUI::MessageBox::Type::Information); 958 } 959} 960 961}