Serenity Operating System
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}