Serenity Operating System
at master 568 lines 20 kB view raw
1/* 2 * Copyright (c) 2022, Dylan Katz <dykatz@uw.edu> 3 * Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org> 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include <DevTools/SQLStudio/SQLStudioGML.h> 9#include <LibCore/DeprecatedFile.h> 10#include <LibCore/DirIterator.h> 11#include <LibCore/StandardPaths.h> 12#include <LibDesktop/Launcher.h> 13#include <LibGUI/Action.h> 14#include <LibGUI/Application.h> 15#include <LibGUI/BoxLayout.h> 16#include <LibGUI/ComboBox.h> 17#include <LibGUI/FilePicker.h> 18#include <LibGUI/GroupBox.h> 19#include <LibGUI/ItemListModel.h> 20#include <LibGUI/JsonArrayModel.h> 21#include <LibGUI/Menu.h> 22#include <LibGUI/MessageBox.h> 23#include <LibGUI/SortingProxyModel.h> 24#include <LibGUI/Statusbar.h> 25#include <LibGUI/TabWidget.h> 26#include <LibGUI/TableView.h> 27#include <LibGUI/TextDocument.h> 28#include <LibGUI/TextEditor.h> 29#include <LibGUI/Toolbar.h> 30#include <LibGUI/ToolbarContainer.h> 31#include <LibSQL/AST/Lexer.h> 32#include <LibSQL/AST/Token.h> 33#include <LibSQL/SQLClient.h> 34#include <LibSQL/Value.h> 35 36#include "MainWidget.h" 37#include "ScriptEditor.h" 38 39REGISTER_WIDGET(SQLStudio, MainWidget); 40 41namespace SQLStudio { 42 43static Vector<DeprecatedString> lookup_database_names() 44{ 45 static constexpr auto database_extension = ".db"sv; 46 47 auto database_path = DeprecatedString::formatted("{}/sql", Core::StandardPaths::data_directory()); 48 if (!Core::DeprecatedFile::exists(database_path)) 49 return {}; 50 51 Core::DirIterator iterator(move(database_path), Core::DirIterator::SkipParentAndBaseDir); 52 Vector<DeprecatedString> database_names; 53 54 while (iterator.has_next()) { 55 if (auto database = iterator.next_path(); database.ends_with(database_extension)) 56 database_names.append(database.substring(0, database.length() - database_extension.length())); 57 } 58 59 return database_names; 60} 61 62MainWidget::MainWidget() 63{ 64 load_from_gml(sql_studio_gml).release_value_but_fixme_should_propagate_errors(); 65 66 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](auto&) { 67 open_new_script(); 68 }); 69 70 m_open_action = GUI::CommonActions::make_open_action([&](auto&) { 71 if (auto result = GUI::FilePicker::get_open_filepath(window()); result.has_value()) 72 open_script_from_file(LexicalPath { result.release_value() }); 73 }); 74 75 m_save_action = GUI::CommonActions::make_save_action([&](auto&) { 76 auto* editor = active_editor(); 77 VERIFY(editor); 78 79 if (auto result = editor->save(); result.is_error()) 80 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Failed to save {}\n{}", editor->path(), result.error())); 81 }); 82 83 m_save_as_action = GUI::CommonActions::make_save_as_action([&](auto&) { 84 auto* editor = active_editor(); 85 VERIFY(editor); 86 87 if (auto result = editor->save_as(); result.is_error()) 88 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Failed to save {}\n{}", editor->path(), result.error())); 89 }); 90 91 m_save_all_action = GUI::Action::create("Save All", { Mod_Ctrl | Mod_Alt, Key_S }, [this](auto&) { 92 auto* editor = active_editor(); 93 VERIFY(editor); 94 95 m_tab_widget->for_each_child_widget([&](auto& child) { 96 auto& editor = verify_cast<ScriptEditor>(child); 97 m_tab_widget->set_active_widget(&editor); 98 99 if (auto result = editor.save(); result.is_error()) { 100 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Failed to save {}\n{}", editor.path(), result.error())); 101 return IterationDecision::Break; 102 } else if (!result.value()) { 103 return IterationDecision::Break; 104 } 105 106 return IterationDecision::Continue; 107 }); 108 109 m_tab_widget->set_active_widget(editor); 110 }); 111 112 m_copy_action = GUI::CommonActions::make_copy_action([&](auto&) { 113 auto* editor = active_editor(); 114 VERIFY(editor); 115 116 editor->copy_action().activate(); 117 update_editor_actions(editor); 118 }); 119 120 m_cut_action = GUI::CommonActions::make_cut_action([&](auto&) { 121 auto* editor = active_editor(); 122 VERIFY(editor); 123 124 editor->cut_action().activate(); 125 update_editor_actions(editor); 126 }); 127 128 m_paste_action = GUI::CommonActions::make_paste_action([&](auto&) { 129 auto* editor = active_editor(); 130 VERIFY(editor); 131 132 editor->paste_action().activate(); 133 update_editor_actions(editor); 134 }); 135 136 m_undo_action = GUI::CommonActions::make_undo_action([&](auto&) { 137 auto* editor = active_editor(); 138 VERIFY(editor); 139 140 editor->document().undo(); 141 update_editor_actions(editor); 142 }); 143 144 m_redo_action = GUI::CommonActions::make_redo_action([&](auto&) { 145 auto* editor = active_editor(); 146 VERIFY(editor); 147 148 editor->document().redo(); 149 update_editor_actions(editor); 150 }); 151 152 m_connect_to_database_action = GUI::Action::create("Connect to Database"sv, { Mod_Alt, Key_C }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"sv).release_value_but_fixme_should_propagate_errors(), [this](auto&) { 153 auto database_name = m_databases_combo_box->text().trim_whitespace(); 154 if (database_name.is_empty()) 155 return; 156 157 m_run_script_action->set_enabled(false); 158 m_statusbar->set_text(1, "Disconnected"sv); 159 160 if (m_connection_id.has_value()) { 161 m_sql_client->disconnect(*m_connection_id); 162 m_connection_id.clear(); 163 } 164 165 if (auto connection_id = m_sql_client->connect(database_name); connection_id.has_value()) { 166 m_statusbar->set_text(1, DeprecatedString::formatted("Connected to: {}", database_name)); 167 m_connection_id = *connection_id; 168 m_run_script_action->set_enabled(true); 169 } else { 170 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Could not connect to {}", database_name)); 171 } 172 }); 173 174 m_run_script_action = GUI::Action::create("Run script", { Mod_Alt, Key_F9 }, Gfx::Bitmap::load_from_file("/res/icons/16x16/play.png"sv).release_value_but_fixme_should_propagate_errors(), [&](auto&) { 175 m_results.clear(); 176 m_current_line_for_parsing = 0; 177 read_next_sql_statement_of_editor(); 178 }); 179 m_run_script_action->set_enabled(false); 180 181 static auto database_names = lookup_database_names(); 182 m_databases_combo_box = GUI::ComboBox::construct(); 183 m_databases_combo_box->set_editor_placeholder("Enter new database or select existing database"sv); 184 m_databases_combo_box->set_max_width(font().width(m_databases_combo_box->editor_placeholder()) + font().max_glyph_width() + 16); 185 m_databases_combo_box->set_model(*GUI::ItemListModel<DeprecatedString>::create(database_names)); 186 m_databases_combo_box->on_return_pressed = [this]() { 187 m_connect_to_database_action->activate(m_databases_combo_box); 188 }; 189 190 auto& toolbar = *find_descendant_of_type_named<GUI::Toolbar>("toolbar"sv); 191 toolbar.add_action(*m_new_action); 192 toolbar.add_action(*m_open_action); 193 toolbar.add_action(*m_save_action); 194 toolbar.add_action(*m_save_as_action); 195 toolbar.add_separator(); 196 toolbar.add_action(*m_copy_action); 197 toolbar.add_action(*m_cut_action); 198 toolbar.add_action(*m_paste_action); 199 toolbar.add_separator(); 200 toolbar.add_action(*m_undo_action); 201 toolbar.add_action(*m_redo_action); 202 toolbar.add_separator(); 203 toolbar.add_child(*m_databases_combo_box); 204 toolbar.add_action(*m_connect_to_database_action); 205 toolbar.add_separator(); 206 toolbar.add_action(*m_run_script_action); 207 208 m_tab_widget = find_descendant_of_type_named<GUI::TabWidget>("script_tab_widget"sv); 209 210 m_tab_widget->on_tab_close_click = [&](auto& widget) { 211 auto& editor = verify_cast<ScriptEditor>(widget); 212 213 if (auto result = editor.attempt_to_close(); result.is_error()) { 214 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Failed to save {}\n{}", editor.path(), result.error())); 215 } else if (result.value()) { 216 m_tab_widget->remove_tab(editor); 217 update_title(); 218 on_editor_change(); 219 } 220 }; 221 222 m_tab_widget->on_change = [&](auto&) { 223 update_title(); 224 on_editor_change(); 225 }; 226 227 m_action_tab_widget = find_descendant_of_type_named<GUI::TabWidget>("action_tab_widget"sv); 228 229 m_query_results_widget = m_action_tab_widget->add_tab<GUI::Widget>("Results"); 230 m_query_results_widget->set_layout<GUI::VerticalBoxLayout>(6); 231 m_query_results_table_view = m_query_results_widget->add<GUI::TableView>(); 232 233 m_action_tab_widget->on_tab_close_click = [this](auto&) { 234 m_action_tab_widget->set_visible(false); 235 }; 236 237 m_statusbar = find_descendant_of_type_named<GUI::Statusbar>("statusbar"sv); 238 m_statusbar->segment(1).set_mode(GUI::Statusbar::Segment::Mode::Auto); 239 m_statusbar->set_text(1, "Disconnected"sv); 240 m_statusbar->segment(2).set_mode(GUI::Statusbar::Segment::Mode::Fixed); 241 m_statusbar->segment(2).set_fixed_width(font().width("Ln 0000, Col 000"sv) + font().max_glyph_width()); 242 243 GUI::Application::the()->on_action_enter = [this](GUI::Action& action) { 244 auto text = action.status_tip(); 245 if (text.is_empty()) 246 text = Gfx::parse_ampersand_string(action.text()); 247 m_statusbar->set_override_text(move(text)); 248 }; 249 250 GUI::Application::the()->on_action_leave = [this](GUI::Action&) { 251 m_statusbar->set_override_text({}); 252 }; 253 254 m_sql_client = SQL::SQLClient::try_create().release_value_but_fixme_should_propagate_errors(); 255 m_sql_client->on_execution_success = [this](auto result) { 256 m_result_column_names = move(result.column_names); 257 read_next_sql_statement_of_editor(); 258 }; 259 m_sql_client->on_execution_error = [this](auto result) { 260 auto* editor = active_editor(); 261 VERIFY(editor); 262 263 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Error executing {}\n{}", editor->path(), result.error_message)); 264 }; 265 m_sql_client->on_next_result = [this](auto result) { 266 m_results.append({}); 267 m_results.last().ensure_capacity(result.values.size()); 268 269 for (auto const& value : result.values) 270 m_results.last().unchecked_append(value.to_deprecated_string()); 271 }; 272 m_sql_client->on_results_exhausted = [this](auto) { 273 if (m_results.size() == 0) 274 return; 275 if (m_results[0].size() == 0) 276 return; 277 278 Vector<GUI::JsonArrayModel::FieldSpec> query_result_fields; 279 for (auto& column_name : m_result_column_names) 280 query_result_fields.empend(column_name, column_name, Gfx::TextAlignment::CenterLeft); 281 282 auto query_results_model = GUI::JsonArrayModel::create("{}", move(query_result_fields)); 283 m_query_results_table_view->set_model(MUST(GUI::SortingProxyModel::create(*query_results_model))); 284 for (auto& result_row : m_results) { 285 Vector<JsonValue> individual_result_as_json; 286 for (auto& result_row_column : result_row) 287 individual_result_as_json.append(result_row_column); 288 query_results_model->add(move(individual_result_as_json)); 289 } 290 m_action_tab_widget->set_visible(true); 291 }; 292} 293 294void MainWidget::initialize_menu(GUI::Window* window) 295{ 296 auto& file_menu = window->add_menu("&File"); 297 file_menu.add_action(*m_new_action); 298 file_menu.add_action(*m_open_action); 299 file_menu.add_action(*m_save_action); 300 file_menu.add_action(*m_save_as_action); 301 file_menu.add_action(*m_save_all_action); 302 file_menu.add_separator(); 303 file_menu.add_action(GUI::CommonActions::make_quit_action([](auto&) { 304 GUI::Application::the()->quit(); 305 })); 306 307 auto& edit_menu = window->add_menu("&Edit"); 308 edit_menu.add_action(*m_copy_action); 309 edit_menu.add_action(*m_cut_action); 310 edit_menu.add_action(*m_paste_action); 311 edit_menu.add_separator(); 312 edit_menu.add_action(*m_undo_action); 313 edit_menu.add_action(*m_redo_action); 314 edit_menu.add_separator(); 315 edit_menu.add_action(*m_run_script_action); 316 317 auto& help_menu = window->add_menu("&Help"); 318 help_menu.add_action(GUI::CommonActions::make_command_palette_action(window)); 319 help_menu.add_action(GUI::CommonActions::make_help_action([](auto&) { 320 Desktop::Launcher::open(URL::create_with_file_scheme("/usr/share/man/man1/SQLStudio.md"), "/bin/Help"); 321 })); 322 help_menu.add_action(GUI::CommonActions::make_about_action("SQL Studio", GUI::Icon::default_icon("app-sql-studio"sv), window)); 323} 324 325void MainWidget::open_new_script() 326{ 327 auto new_script_name = DeprecatedString::formatted("New Script - {}", m_new_script_counter); 328 ++m_new_script_counter; 329 330 auto& editor = m_tab_widget->add_tab<ScriptEditor>(new_script_name); 331 editor.new_script_with_temp_name(new_script_name); 332 333 editor.on_cursor_change = [this] { on_editor_change(); }; 334 editor.on_selection_change = [this] { on_editor_change(); }; 335 editor.on_highlighter_change = [this] { on_editor_change(); }; 336 337 m_tab_widget->set_active_widget(&editor); 338} 339 340void MainWidget::open_script_from_file(LexicalPath const& file_path) 341{ 342 auto& editor = m_tab_widget->add_tab<ScriptEditor>(file_path.title()); 343 344 if (auto result = editor.open_script_from_file(file_path); result.is_error()) { 345 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Failed to open {}\n{}", file_path, result.error())); 346 return; 347 } 348 349 editor.on_cursor_change = [this] { on_editor_change(); }; 350 editor.on_selection_change = [this] { on_editor_change(); }; 351 editor.on_highlighter_change = [this] { on_editor_change(); }; 352 353 m_tab_widget->set_active_widget(&editor); 354} 355 356bool MainWidget::request_close() 357{ 358 auto any_scripts_modified { false }; 359 auto is_script_modified = [&](auto& child) { 360 auto& editor = verify_cast<ScriptEditor>(child); 361 362 if (editor.document().is_modified()) { 363 any_scripts_modified = true; 364 return IterationDecision::Break; 365 } 366 367 return IterationDecision::Continue; 368 }; 369 370 m_tab_widget->for_each_child_widget(is_script_modified); 371 if (!any_scripts_modified) 372 return true; 373 374 auto result = GUI::MessageBox::ask_about_unsaved_changes(window(), {}); 375 switch (result) { 376 case GUI::Dialog::ExecResult::Yes: 377 break; 378 case GUI::Dialog::ExecResult::No: 379 return true; 380 default: 381 return false; 382 } 383 384 m_save_all_action->activate(); 385 any_scripts_modified = false; 386 387 m_tab_widget->for_each_child_widget(is_script_modified); 388 return !any_scripts_modified; 389} 390 391ScriptEditor* MainWidget::active_editor() 392{ 393 if (!m_tab_widget || !m_tab_widget->active_widget()) 394 return nullptr; 395 return verify_cast<ScriptEditor>(m_tab_widget->active_widget()); 396} 397 398void MainWidget::update_title() 399{ 400 if (auto* editor = active_editor()) 401 window()->set_title(DeprecatedString::formatted("{} - SQL Studio", editor->name())); 402 else 403 window()->set_title("SQL Studio"); 404} 405 406void MainWidget::on_editor_change() 407{ 408 auto* editor = active_editor(); 409 update_statusbar(editor); 410 update_editor_actions(editor); 411} 412 413void MainWidget::update_statusbar(ScriptEditor* editor) 414{ 415 if (!editor) { 416 m_statusbar->set_text(0, ""); 417 m_statusbar->set_text(2, ""); 418 return; 419 } 420 421 StringBuilder builder; 422 if (editor->has_selection()) { 423 auto character_count = editor->selected_text().length(); 424 auto word_count = editor->number_of_selected_words(); 425 builder.appendff("Selected: {} {} ({} {})", character_count, character_count == 1 ? "character" : "characters", word_count, word_count != 1 ? "words" : "word"); 426 } 427 428 m_statusbar->set_text(0, builder.to_deprecated_string()); 429 m_statusbar->set_text(2, DeprecatedString::formatted("Ln {}, Col {}", editor->cursor().line() + 1, editor->cursor().column())); 430} 431 432void MainWidget::update_editor_actions(ScriptEditor* editor) 433{ 434 if (!editor) { 435 m_save_action->set_enabled(false); 436 m_save_as_action->set_enabled(false); 437 m_save_all_action->set_enabled(false); 438 m_run_script_action->set_enabled(false); 439 440 m_copy_action->set_enabled(false); 441 m_cut_action->set_enabled(false); 442 m_paste_action->set_enabled(false); 443 m_undo_action->set_enabled(false); 444 m_redo_action->set_enabled(false); 445 446 return; 447 } 448 449 m_save_action->set_enabled(true); 450 m_save_as_action->set_enabled(true); 451 m_save_all_action->set_enabled(true); 452 m_run_script_action->set_enabled(m_connection_id.has_value()); 453 454 m_copy_action->set_enabled(editor->copy_action().is_enabled()); 455 m_cut_action->set_enabled(editor->cut_action().is_enabled()); 456 m_paste_action->set_enabled(editor->paste_action().is_enabled()); 457 m_undo_action->set_enabled(editor->undo_action().is_enabled()); 458 m_redo_action->set_enabled(editor->redo_action().is_enabled()); 459} 460 461void MainWidget::drag_enter_event(GUI::DragEvent& event) 462{ 463 auto const& mime_types = event.mime_types(); 464 if (mime_types.contains_slow("text/uri-list")) 465 event.accept(); 466} 467 468void MainWidget::drop_event(GUI::DropEvent& drop_event) 469{ 470 drop_event.accept(); 471 window()->move_to_front(); 472 473 if (drop_event.mime_data().has_urls()) { 474 auto urls = drop_event.mime_data().urls(); 475 if (urls.is_empty()) 476 return; 477 478 for (auto& url : urls) { 479 auto& scheme = url.scheme(); 480 if (!scheme.equals_ignoring_ascii_case("file"sv)) 481 continue; 482 483 auto lexical_path = LexicalPath(url.path()); 484 open_script_from_file(lexical_path); 485 } 486 } 487} 488 489void MainWidget::read_next_sql_statement_of_editor() 490{ 491 if (!m_connection_id.has_value()) 492 return; 493 494 StringBuilder piece; 495 do { 496 if (!piece.is_empty()) 497 piece.append('\n'); 498 499 auto line_maybe = read_next_line_of_editor(); 500 501 if (!line_maybe.has_value()) 502 return; 503 504 auto& line = line_maybe.value(); 505 auto lexer = SQL::AST::Lexer(line); 506 507 piece.append(line); 508 509 bool is_first_token = true; 510 bool is_command = false; 511 bool last_token_ended_statement = false; 512 bool tokens_found = false; 513 514 for (SQL::AST::Token token = lexer.next(); token.type() != SQL::AST::TokenType::Eof; token = lexer.next()) { 515 tokens_found = true; 516 switch (token.type()) { 517 case SQL::AST::TokenType::ParenOpen: 518 ++m_editor_line_level; 519 break; 520 case SQL::AST::TokenType::ParenClose: 521 --m_editor_line_level; 522 break; 523 case SQL::AST::TokenType::SemiColon: 524 last_token_ended_statement = true; 525 break; 526 case SQL::AST::TokenType::Period: 527 if (is_first_token) 528 is_command = true; 529 break; 530 default: 531 last_token_ended_statement = is_command; 532 break; 533 } 534 535 is_first_token = false; 536 } 537 538 if (tokens_found) 539 m_editor_line_level = last_token_ended_statement ? 0 : (m_editor_line_level > 0 ? m_editor_line_level : 1); 540 } while ((m_editor_line_level > 0) || piece.is_empty()); 541 542 auto sql_statement = piece.to_deprecated_string(); 543 544 if (auto statement_id = m_sql_client->prepare_statement(*m_connection_id, sql_statement); statement_id.has_value()) { 545 m_sql_client->async_execute_statement(*statement_id, {}); 546 } else { 547 auto* editor = active_editor(); 548 VERIFY(editor); 549 550 GUI::MessageBox::show_error(window(), DeprecatedString::formatted("Could not parse {}\n{}", editor->path(), sql_statement)); 551 } 552} 553 554Optional<DeprecatedString> MainWidget::read_next_line_of_editor() 555{ 556 auto* editor = active_editor(); 557 if (!editor) 558 return {}; 559 560 if (m_current_line_for_parsing >= editor->document().line_count()) 561 return {}; 562 563 auto result = editor->document().line(m_current_line_for_parsing).to_utf8(); 564 ++m_current_line_for_parsing; 565 return result; 566} 567 568}