backend/wbpublic/sqlide/sql_editor_be.cpp (914 lines of code) (raw):

/* * Copyright (c) 2007, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, * as published by the Free Software Foundation. * * This program is designed to work with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, as * designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an additional * permission to link the program and your derivative works with the * separately licensed software that they have either included with * the program or referenced in the documentation. * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License, version 2.0, for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "base/boost_smart_ptr_helpers.h" #include "base/log.h" #include "base/string_utilities.h" #include "base/threaded_timer.h" #include "base/util_functions.h" #include "grt/grt_manager.h" #include "grt/grt_threaded_task.h" #include "objimpl/db.query/db_query_QueryBuffer.h" #include "grtui/file_charset_dialog.h" #include "mforms/code_editor.h" #include "mforms/find_panel.h" #include "mforms/toolbar.h" #include "mforms/menu.h" #include "mforms/filechooser.h" #include "grts/structs.db.mysql.h" #include "SymbolTable.h" #include "sql_editor_be.h" #include <mutex> DEFAULT_LOG_DOMAIN("MySQL editor"); using namespace bec; using namespace grt; using namespace base; using namespace parsers; //---------------------------------------------------------------------------------------------------------------------- class MySQLEditor::Private { public: // ref to the GRT object representing this object // it will be used in Db_sql_editor queryBuffer list and in standalone // editors for plugin support db_query_QueryBufferRef grtobj; mforms::Box *container; mforms::Menu *editorContextMenu; mforms::Menu *editorTextSubmenu; mforms::ToolBar *toolbar; int lastTypedChar; MySQLParserContext::Ref parserContext; MySQLParserContext::Ref autocompletionContext; MySQLParserServices::Ref services; SymbolTable symbolTable; // Entries determined the last time we started auto completion. The actually shown list // is derived from these entries filtered by the current input. std::vector<std::pair<int, std::string>> codeCompletionCandidates; base::RecMutex sqlCheckerMutex; MySQLParseUnit parseUnit; // The type of query we want to limit our parsing to. // We use 2 timers here for delayed work. One is a grt timer to run a task in // the main thread after a certain delay. // The other one is to run the actual work task in a background thread. bec::GRTManager::Timer *currentDelayTimer; int currentWorkTimerID; std::pair<const char *, size_t> textInfo; // Only valid during a parse run. std::vector<ParserErrorInfo> recognitionErrors; // List of errors from the last sql check run. std::set<size_t> errorMarkerLines; bool splittingRequired; bool updatingStatementMarkers; std::set<size_t> statementMarkerLines; base::RecMutex sqlStatementBordersMutex; std::vector<StatementRange> statementRanges; bool isRefreshEnabled; // Whether the FE control is permitted to replace its contents from the BE. bool isSQLCheckEnabled; // Enables automatic syntax checks. bool stopProcessing; // To stop ongoing syntax checks (because of text changes). bool ownsToolbar; boost::signals2::signal<void()> textChangeSignal; mforms::CodeEditor *codeEditor = nullptr; std::string currentSchema; std::string sqlMode; Private(MySQLParserContext::Ref syntaxcheck_context, MySQLParserContext::Ref autocompleteContext) : grtobj(grt::Initialized), stopProcessing(false) { ownsToolbar = false; parseUnit = MySQLParseUnit::PuGeneric; isRefreshEnabled = true; splittingRequired = false; parserContext = syntaxcheck_context; autocompletionContext = autocompleteContext; services = MySQLParserServices::get(); currentDelayTimer = nullptr; currentWorkTimerID = -1; isSQLCheckEnabled = true; container = nullptr; editorTextSubmenu = nullptr; editorContextMenu = nullptr; toolbar = nullptr; lastTypedChar = 0; updatingStatementMarkers = false; } //-------------------------------------------------------------------------------------------------------------------- /** * Determines ranges for all statements in the current text. */ void splitStatementsIfRequired() { // If we have restricted content (e.g. for object editors) then we don't split and handle the entire content // as a single statement. This will then show syntax errors for any invalid additional input. if (splittingRequired) { logDebug3("Start splitting\n"); splittingRequired = false; base::RecMutexLock lock(sqlStatementBordersMutex); statementRanges.clear(); if (parseUnit == MySQLParseUnit::PuGeneric) { double start = timestamp(); services->determineStatementRanges(textInfo.first, textInfo.second, ";", statementRanges); logDebug3("Splitting ended after %f ticks\n", timestamp() - start); } else statementRanges.push_back({ 0, 0, textInfo.second }); } } //-------------------------------------------------------------------------------------------------------------------- /** * One or more markers on that line where changed. We have to stay in sync with our statement markers list * to make the optimized add/remove algorithm working. */ void markerChanged(const mforms::LineMarkupChangeset &changeset, bool deleted) { if (updatingStatementMarkers || changeset.size() == 0) return; if (deleted) { for (mforms::LineMarkupChangeset::const_iterator iterator = changeset.begin(); iterator != changeset.end(); ++iterator) { if ((iterator->markup & mforms::LineMarkupStatement) != 0) statementMarkerLines.erase(iterator->original_line); if ((iterator->markup & mforms::LineMarkupError) != 0) errorMarkerLines.erase(iterator->original_line); } } else { for (mforms::LineMarkupChangeset::const_iterator iterator = changeset.begin(); iterator != changeset.end(); ++iterator) { if ((iterator->markup & mforms::LineMarkupStatement) != 0) statementMarkerLines.erase(iterator->original_line); if ((iterator->markup & mforms::LineMarkupError) != 0) errorMarkerLines.erase(iterator->original_line); } for (mforms::LineMarkupChangeset::const_iterator iterator = changeset.begin(); iterator != changeset.end(); ++iterator) { if ((iterator->markup & mforms::LineMarkupStatement) != 0) statementMarkerLines.insert(iterator->new_line); if ((iterator->markup & mforms::LineMarkupError) != 0) errorMarkerLines.insert(iterator->new_line); } } } //-------------------------------------------------------------------------------------------------------------------- }; //---------------------------------------------------------------------------------------------------------------------- MySQLEditor::Ref MySQLEditor::create(MySQLParserContext::Ref syntax_check_context, MySQLParserContext::Ref autocompleteContext, std::vector<SymbolTable *> const &globalSymbols, db_query_QueryBufferRef grtobj) { Ref editor = MySQLEditor::Ref(new MySQLEditor(syntax_check_context, autocompleteContext)); editor->d->symbolTable.addDependencies(globalSymbols); // Replace the default object with the custom one. if (grtobj.is_valid()) editor->set_grtobj(grtobj); // setup the GRT object db_query_QueryBuffer::ImplData *data = new db_query_QueryBuffer::ImplData(editor->grtobj(), editor); editor->grtobj()->set_data(data); return editor; } //---------------------------------------------------------------------------------------------------------------------- MySQLEditor::MySQLEditor(MySQLParserContext::Ref syntax_check_context, MySQLParserContext::Ref autocompleteContext) { d = new Private(syntax_check_context, autocompleteContext); d->codeEditor = new mforms::CodeEditor(this); d->codeEditor->set_font(bec::GRTManager::get()->get_app_option_string("workbench.general.Editor:Font")); d->codeEditor->set_features(mforms::FeatureUsePopup, false); d->codeEditor->set_features(mforms::FeatureConvertEolOnPaste | mforms::FeatureAutoIndent, true); d->codeEditor->set_name("Code Editor"); setServerVersion(syntax_check_context->serverVersion()); d->codeEditor->send_editor(SCI_SETTABWIDTH, bec::GRTManager::get()->get_app_option_int("Editor:TabWidth", 4), 0); d->codeEditor->send_editor(SCI_SETINDENT, bec::GRTManager::get()->get_app_option_int("Editor:IndentWidth", 4), 0); d->codeEditor->send_editor(SCI_SETUSETABS, !bec::GRTManager::get()->get_app_option_int("Editor:TabIndentSpaces", 0), 0); scoped_connect(d->codeEditor->signal_changed(), std::bind(&MySQLEditor::text_changed, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); scoped_connect(d->codeEditor->signal_char_added(), std::bind(&MySQLEditor::char_added, this, std::placeholders::_1)); scoped_connect(d->codeEditor->signal_dwell(), std::bind(&MySQLEditor::dwell_event, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); scoped_connect(d->codeEditor->signal_marker_changed(), std::bind(&MySQLEditor::Private::markerChanged, d, std::placeholders::_1, std::placeholders::_2)); setup_auto_completion(); setup_editor_menu(); } //---------------------------------------------------------------------------------------------------------------------- MySQLEditor::~MySQLEditor() { stop_processing(); { d->isSQLCheckEnabled = false; // We lock all mutexes for a moment here to ensure no background thread is // still holding them. base::RecMutexLock lock1(d->sqlCheckerMutex); base::RecMutexLock lock2(d->sqlStatementBordersMutex); } if (d->editorTextSubmenu != nullptr) delete d->editorTextSubmenu; delete d->editorContextMenu; if (d->ownsToolbar && d->toolbar != nullptr) d->toolbar->release(); delete d->codeEditor; delete d; } //---------------------------------------------------------------------------------------------------------------------- db_query_QueryBufferRef MySQLEditor::grtobj() { return d->grtobj; } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::set_grtobj(db_query_QueryBufferRef grtobj) { d->grtobj = grtobj; } //---------------------------------------------------------------------------------------------------------------------- mforms::CodeEditor *MySQLEditor::get_editor_control() { return d->codeEditor; }; //---------------------------------------------------------------------------------------------------------------------- static void toggle_show_special_chars(mforms::ToolBarItem *item, MySQLEditor *sql_editor) { sql_editor->show_special_chars(item->get_checked()); } //---------------------------------------------------------------------------------------------------------------------- static void toggle_word_wrap(mforms::ToolBarItem *item, MySQLEditor *sql_editor) { sql_editor->enable_word_wrap(item->get_checked()); } //---------------------------------------------------------------------------------------------------------------------- static void show_find_panel_for_active_editor(MySQLEditor *sql_editor) { sql_editor->get_editor_control()->show_find_panel(false); } //---------------------------------------------------------------------------------------------------------------------- static void beautify_script(MySQLEditor *sql_editor) { grt::BaseListRef args(true); args.ginsert(sql_editor->grtobj()); grt::GRT::get()->call_module_function("SQLIDEUtils", "enbeautificate", args); } //---------------------------------------------------------------------------------------------------------------------- static void open_file(MySQLEditor *sql_editor) { mforms::FileChooser fc(mforms::OpenFile); if (fc.run_modal()) { std::string file = fc.get_path(); gchar *contents; gsize length; GError *error = nullptr; if (g_file_get_contents(file.c_str(), &contents, &length, &error)) { char *converted; mforms::CodeEditor *code_editor = sql_editor->get_editor_control(); if (FileCharsetDialog::ensure_filedata_utf8(contents, length, "", file, converted)) { code_editor->set_text_keeping_state(converted ? converted : contents); g_free(contents); g_free(converted); } else { g_free(contents); code_editor->set_text(_("Data is not UTF8 encoded and cannot be displayed.")); } } else if (error) { mforms::Utilities::show_error("Load File", base::strfmt("Could not load file %s:\n%s", file.c_str(), error->message), "OK"); g_error_free(error); } } } //---------------------------------------------------------------------------------------------------------------------- static void save_file(MySQLEditor *sql_editor) { mforms::FileChooser fc(mforms::SaveFile); fc.set_extensions("SQL Scripts (*.sql)|*.sql", "sql"); if (fc.run_modal()) { GError *error = nullptr; std::string file = fc.get_path(); mforms::CodeEditor *code_editor = sql_editor->get_editor_control(); std::pair<const char *, size_t> data = code_editor->get_text_ptr(); if (!g_file_set_contents(file.c_str(), data.first, (gssize)data.second, &error) && error) { mforms::Utilities::show_error("Save File", base::strfmt("Could not save to file %s:\n%s", file.c_str(), error->message), "OK"); g_error_free(error); } } } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::set_base_toolbar(mforms::ToolBar *toolbar) { /* TODO: that is a crude implementation as the toolbar is sometimes directly created and set and *then* also set here, so deleting it crashs. if (d->toolbar != nullptr && d->owns_toolbar) delete d->toolbar; */ d->toolbar = toolbar; d->ownsToolbar = false; mforms::ToolBarItem *item; if (d->isSQLCheckEnabled) { item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem)); item->set_name("Beautify"); item->setInternalName("query.beautify"); item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_beautifier.png")); item->set_tooltip(_("Beautify/reformat the SQL script")); scoped_connect(item->signal_activated(), std::bind(beautify_script, this)); d->toolbar->add_item(item); } item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem)); item->set_name("Search"); item->setInternalName("query.search"); item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_find.png")); item->set_tooltip(_("Show the Find panel for the editor")); scoped_connect(item->signal_activated(), std::bind(show_find_panel_for_active_editor, this)); d->toolbar->add_item(item); item = mforms::manage(new mforms::ToolBarItem(mforms::ToggleItem)); item->set_name("Toggle Invisible"); item->setInternalName("query.toggleInvisible"); item->set_alt_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_special-chars-on.png")); item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_special-chars-off.png")); item->set_tooltip(_("Toggle display of invisible characters (spaces, tabs, newlines)")); scoped_connect(item->signal_activated(), std::bind(toggle_show_special_chars, item, this)); d->toolbar->add_item(item); item = mforms::manage(new mforms::ToolBarItem(mforms::ToggleItem)); item->set_name("Toggle Word Wrap"); item->setInternalName("query.toggleWordWrap"); item->set_alt_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_word-wrap-on.png")); item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_word-wrap-off.png")); item->set_tooltip(_("Toggle wrapping of long lines (keep this off for large files)")); scoped_connect(item->signal_activated(), std::bind(toggle_word_wrap, item, this)); d->toolbar->add_item(item); } //---------------------------------------------------------------------------------------------------------------------- static void embed_find_panel(mforms::CodeEditor *editor, bool show, mforms::Box *container) { mforms::View *panel = editor->get_find_panel(); if (show) { if (!panel->get_parent()) container->add(panel, false, true); } else { container->remove(panel); editor->focus(); } } //---------------------------------------------------------------------------------------------------------------------- mforms::View *MySQLEditor::get_container() { if (d->container == nullptr) { d->container = new mforms::Box(false); d->container->add(get_toolbar(), false, true); get_editor_control()->set_show_find_panel_callback( std::bind(embed_find_panel, std::placeholders::_1, std::placeholders::_2, d->container)); d->container->add_end(get_editor_control(), true, true); } return d->container; }; //---------------------------------------------------------------------------------------------------------------------- mforms::ToolBar *MySQLEditor::get_toolbar(bool include_file_actions) { if (!d->toolbar) { d->ownsToolbar = true; d->toolbar = mforms::manage(new mforms::ToolBar(mforms::SecondaryToolBar)); #ifdef _MSC_VER d->toolbar->set_size(-1, 27); #endif if (include_file_actions) { mforms::ToolBarItem *item; item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem)); item->set_name("Open File"); item->setInternalName("query.openFile"); item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_open.png")); item->set_tooltip(_("Open a script file in this editor")); scoped_connect(item->signal_activated(), std::bind(open_file, this)); d->toolbar->add_item(item); item = mforms::manage(new mforms::ToolBarItem(mforms::ActionItem)); item->set_name("Save File"); item->setInternalName("query.saveFile"); item->set_icon(IconManager::get_instance()->get_icon_path("qe_sql-editor-tb-icon_save.png")); item->set_tooltip(_("Save the script to a file.")); scoped_connect(item->signal_activated(), std::bind(save_file, this)); d->toolbar->add_item(item); d->toolbar->add_item(mforms::manage(new mforms::ToolBarItem(mforms::SeparatorItem))); } set_base_toolbar(d->toolbar); } return d->toolbar; }; //---------------------------------------------------------------------------------------------------------------------- bool MySQLEditor::is_refresh_enabled() const { return d->isRefreshEnabled; } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::set_refresh_enabled(bool val) { d->isRefreshEnabled = val; } //---------------------------------------------------------------------------------------------------------------------- bool MySQLEditor::is_sql_check_enabled() const { return d->isSQLCheckEnabled; } //---------------------------------------------------------------------------------------------------------------------- /** * Returns the text of the editor. Usage of this function is discouraged because * it copies the * (potentially) large editor content. Use text_ptr() instead. */ std::string MySQLEditor::sql() { return d->codeEditor->get_text(false); } //---------------------------------------------------------------------------------------------------------------------- /** * Returns a direct pointer to the editor content, which is only valid until the * next change. * So if you want to keep it for longer copy the text. * Note: since the text can be large don't do this unless absolutely necessary. */ std::pair<const char *, size_t> MySQLEditor::text_ptr() { return d->codeEditor->get_text_ptr(); } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::set_current_schema(const std::string &schema) { d->currentSchema = schema; } //---------------------------------------------------------------------------------------------------------------------- bool MySQLEditor::empty() { return d->codeEditor->text_length() == 0; } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::append_text(const std::string &text) { d->codeEditor->append_text(text.data(), text.size()); } //---------------------------------------------------------------------------------------------------------------------- /** * Used to the set the content of the editor from outside (e.g. when loading a * file or for tests). */ void MySQLEditor::sql(const char *sql) { d->codeEditor->set_text(sql); d->splittingRequired = true; d->statementMarkerLines.clear(); d->codeEditor->set_eol_mode(mforms::EolLF, true); } //---------------------------------------------------------------------------------------------------------------------- std::size_t MySQLEditor::cursor_pos() { return d->codeEditor->get_caret_pos(); } //---------------------------------------------------------------------------------------------------------------------- /** * Returns the caret position as column/row pair. The returned column (char * index) is utf-8 safe and computes the actual character index as displayed in the editor, not the byte index in * a std::string. If @local is true then the line position is relative to the statement, * otherwise that in the entire editor. */ std::pair<std::size_t, std::size_t> MySQLEditor::cursor_pos_row_column(bool local) { size_t position = d->codeEditor->get_caret_pos(); ssize_t line = d->codeEditor->line_from_position(position); ssize_t line_start, line_end; d->codeEditor->get_range_of_line(line, line_start, line_end); ssize_t offset = position - line_start; // This is a byte offset. std::string line_text = d->codeEditor->get_text_in_range(line_start, line_end); offset = g_utf8_pointer_to_offset(line_text.c_str(), line_text.c_str() + offset); if (local) { size_t min, max; if (get_current_statement_range(min, max)) line -= d->codeEditor->line_from_position(min); } return { offset, line }; } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::set_cursor_pos(std::size_t position) { d->codeEditor->set_caret_pos(position); } //---------------------------------------------------------------------------------------------------------------------- bool MySQLEditor::selected_range(std::size_t &start, std::size_t &end) { size_t length; d->codeEditor->get_selection(start, length); end = start + length; return length > 0; } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::set_selected_range(std::size_t start, std::size_t end) { d->codeEditor->set_selection(start, end - start); } ///---------------------------------------------------------------------------------------------------------------------- boost::signals2::signal<void()> *MySQLEditor::text_change_signal() { return &d->textChangeSignal; } //---------------------------------------------------------------------------------------------------------------------- std::string MySQLEditor::sql_mode() { return d->sqlMode; }; //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::set_sql_mode(const std::string &value) { d->sqlMode = value; d->parserContext->updateSqlMode(value); } //---------------------------------------------------------------------------------------------------------------------- /** * Update the parser's server version in case of external changes (e.g. model * settings). */ void MySQLEditor::setServerVersion(GrtVersionRef version) { mforms::SyntaxHighlighterLanguage lang = mforms::LanguageMySQL; if (version.is_valid()) { switch (version->majorNumber()) { case 5: switch (version->minorNumber()) { case 6: lang = mforms::LanguageMySQL56; break; case 7: lang = mforms::LanguageMySQL57; break; } break; case 8: switch (version->minorNumber()) { case 0: lang = mforms::LanguageMySQL80; break; } break; } } d->codeEditor->set_language(lang); d->parserContext->updateServerVersion(version); start_sql_processing(); } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::restrict_content_to(ContentType type) { switch (type) { case ContentTypeTrigger: d->parseUnit = MySQLParseUnit::PuCreateTrigger; break; case ContentTypeView: d->parseUnit = MySQLParseUnit::PuCreateView; break; case ContentTypeFunction: d->parseUnit = MySQLParseUnit::PuCreateFunction; break; case ContentTypeProcedure: d->parseUnit = MySQLParseUnit::PuCreateProcedure; break; case ContentTypeUdf: d->parseUnit = MySQLParseUnit::PuCreateUdf; break; case ContentTypeRoutine: d->parseUnit = MySQLParseUnit::PuCreateRoutine; break; case ContentTypeEvent: d->parseUnit = MySQLParseUnit::PuCreateEvent; break; default: d->parseUnit = MySQLParseUnit::PuGeneric; break; } } //---------------------------------------------------------------------------------------------------------------------- bool MySQLEditor::has_sql_errors() const { return d->recognitionErrors.size() > 0; } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::text_changed(Sci_Position position, Sci_Position length, Sci_Position lines_changed, bool added) { stop_processing(); if (d->codeEditor->auto_completion_active() && !added) { // Update auto completion list if a char was removed, but not added. // When adding a char the caret is not yet updated leading to strange // behavior. // So we use a different notification for adding chars. std::string text = getWrittenPart(position); update_auto_completion(text); } d->splittingRequired = true; d->textInfo = d->codeEditor->get_text_ptr(); if (d->isSQLCheckEnabled) d->currentDelayTimer = bec::GRTManager::get()->run_every(std::bind(&MySQLEditor::start_sql_processing, this), 0.001); else d->textChangeSignal(); // If there is no timer set up then trigger // change signals directly. } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::char_added(int char_code) { if (!d->codeEditor->auto_completion_active()) d->lastTypedChar = char_code; // UTF32 encoded char. else { std::string text = getWrittenPart(d->codeEditor->get_caret_pos()); update_auto_completion(text); } } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::dwell_event(bool started, size_t position, int x, int y) { if (started) { if (d->codeEditor->indicator_at(position) == mforms::RangeIndicatorError) { // TODO: sort by position and do a binary search. for (size_t i = 0; i < d->recognitionErrors.size(); ++i) { ParserErrorInfo entry = d->recognitionErrors[i]; if (entry.charOffset <= position && position <= entry.charOffset + entry.length) { d->codeEditor->show_calltip(true, position, entry.message); break; } } } } else d->codeEditor->show_calltip(false, 0, ""); } //---------------------------------------------------------------------------------------------------------------------- /** * Prepares and triggers an sql check run. Runs in the context of the main * thread. */ bool MySQLEditor::start_sql_processing() { // Here we trigger our text change signal, to avoid frequent signals for each // key press. // Consumers are expected to use this signal for UI updates, so we need to // coalesce messages. d->textChangeSignal(); d->currentDelayTimer = nullptr; // The timer will be deleted by the grt manager. { RecMutexLock sql_errors_mutex(d->sqlCheckerMutex); d->recognitionErrors.clear(); } d->stopProcessing = false; d->codeEditor->set_status_text(""); if (d->textInfo.first != nullptr && d->textInfo.second > 0) d->currentWorkTimerID = ThreadedTimer::get()->add_task( TimerTimeSpan, 0.05, true, std::bind(&MySQLEditor::do_statement_split_and_check, this, std::placeholders::_1)); return false; // Don't re-run this task, it's a single-shot. } //---------------------------------------------------------------------------------------------------------------------- bool MySQLEditor::do_statement_split_and_check(int id) { d->splitStatementsIfRequired(); // Start tasks that depend on the statement ranges (markers + auto completion). bec::GRTManager::get()->run_once_when_idle(this, std::bind(&MySQLEditor::splitting_done, this)); if (d->stopProcessing) return false; base::RecMutexLock lock(d->sqlCheckerMutex); // Now do error checking for each of the statements, collecting error // positions for later markup. for (auto &range : d->statementRanges) { if (d->stopProcessing) return false; if (d->services->checkSqlSyntax(d->parserContext, d->textInfo.first + range.start, range.length, d->parseUnit) > 0) { std::vector<ParserErrorInfo> errors = d->parserContext->errorsWithOffset(range.start); d->recognitionErrors.insert(d->recognitionErrors.end(), errors.begin(), errors.end()); } } bec::GRTManager::get()->run_once_when_idle(this, std::bind(&MySQLEditor::update_error_markers, this)); return false; } //---------------------------------------------------------------------------------------------------------------------- /** * Updates the statement markup and starts auto completion if enabled. This is called in the context of the main thread. */ void *MySQLEditor::splitting_done() { // Trigger auto completion for certain keys (if enabled). // This has to be done after our statement splitter has completed (which is // the case when we appear here). if (auto_start_code_completion() && !d->codeEditor->auto_completion_active() && (g_unichar_isalnum(d->lastTypedChar) || d->lastTypedChar == '.' || d->lastTypedChar == '@')) { d->lastTypedChar = 0; show_auto_completion(false); } std::set<size_t> removal_candidates; std::set<size_t> insert_candidates; std::set<size_t> lines; for (auto &range : d->statementRanges) lines.insert(d->codeEditor->line_from_position(range.start)); std::set_difference(lines.begin(), lines.end(), d->statementMarkerLines.begin(), d->statementMarkerLines.end(), inserter(insert_candidates, insert_candidates.begin())); std::set_difference(d->statementMarkerLines.begin(), d->statementMarkerLines.end(), lines.begin(), lines.end(), inserter(removal_candidates, removal_candidates.begin())); d->statementMarkerLines.swap(lines); d->updatingStatementMarkers = true; for (std::set<size_t>::const_iterator iterator = removal_candidates.begin(); iterator != removal_candidates.end(); ++iterator) d->codeEditor->remove_markup(mforms::LineMarkupStatement, *iterator); for (std::set<size_t>::const_iterator iterator = insert_candidates.begin(); iterator != insert_candidates.end(); ++iterator) d->codeEditor->show_markup(mforms::LineMarkupStatement, *iterator); d->updatingStatementMarkers = false; return nullptr; } //---------------------------------------------------------------------------------------------------------------------- void *MySQLEditor::update_error_markers() { std::set<size_t> removal_candidates; std::set<size_t> insert_candidates; std::set<size_t> lines; d->codeEditor->remove_indicator(mforms::RangeIndicatorError, 0, d->codeEditor->text_length()); if (d->recognitionErrors.size() > 0) { if (d->recognitionErrors.size() == 1) d->codeEditor->set_status_text(_("1 error found")); else d->codeEditor->set_status_text(base::strfmt(_("%lu errors found"), static_cast<unsigned long>(d->recognitionErrors.size()))); for (size_t i = 0; i < d->recognitionErrors.size(); ++i) { d->codeEditor->show_indicator(mforms::RangeIndicatorError, d->recognitionErrors[i].charOffset, d->recognitionErrors[i].length); lines.insert(d->codeEditor->line_from_position(d->recognitionErrors[i].charOffset)); } } else d->codeEditor->set_status_text(""); std::set_difference(lines.begin(), lines.end(), d->errorMarkerLines.begin(), d->errorMarkerLines.end(), inserter(insert_candidates, insert_candidates.begin())); std::set_difference(d->errorMarkerLines.begin(), d->errorMarkerLines.end(), lines.begin(), lines.end(), inserter(removal_candidates, removal_candidates.begin())); d->errorMarkerLines.swap(lines); for (std::set<size_t>::const_iterator iterator = removal_candidates.begin(); iterator != removal_candidates.end(); ++iterator) d->codeEditor->remove_markup(mforms::LineMarkupError, *iterator); for (std::set<size_t>::const_iterator iterator = insert_candidates.begin(); iterator != insert_candidates.end(); ++iterator) d->codeEditor->show_markup(mforms::LineMarkupError, *iterator); return nullptr; } //---------------------------------------------------------------------------------------------------------------------- std::string MySQLEditor::selected_text() { return d->codeEditor->get_text(true); } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::set_selected_text(const std::string &new_text) { d->codeEditor->replace_selected_text(new_text); } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::insert_text(const std::string &new_text) { d->codeEditor->clear_selection(); d->codeEditor->replace_selected_text(new_text); } //---------------------------------------------------------------------------------------------------------------------- /** * Returns the statement at the current caret position. */ std::string MySQLEditor::current_statement() { size_t min, max; if (get_current_statement_range(min, max)) return d->codeEditor->get_text_in_range(min, max); return ""; } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::setup_editor_menu() { d->editorContextMenu = new mforms::Menu(); scoped_connect(d->editorContextMenu->signal_will_show(), std::bind(&MySQLEditor::editor_menu_opening, this)); d->editorContextMenu->add_item(_("Undo"), "undo"); d->editorContextMenu->add_item(_("Redo"), "redo"); d->editorContextMenu->add_separator(); d->editorContextMenu->add_item(_("Cut"), "cut"); d->editorContextMenu->add_item(_("Copy"), "copy"); d->editorContextMenu->add_item(_("Paste"), "paste"); d->editorContextMenu->add_item(_("Delete"), "delete"); d->editorContextMenu->add_separator(); d->editorContextMenu->add_item(_("Select All"), "select_all"); std::list<std::string> groups; groups.push_back("Menu/Text"); { bec::ArgumentPool argpool; argpool.add_entries_for_object("activeQueryBuffer", grtobj()); argpool.add_entries_for_object("", grtobj()); bec::MenuItemList plugin_items = bec::GRTManager::get()->get_plugin_context_menu_items(groups, argpool); if (!plugin_items.empty()) { d->editorContextMenu->add_separator(); d->editorContextMenu->add_items_from_list(plugin_items); } } bec::MenuItemList plugin_items; bec::ArgumentPool argpool; argpool.add_simple_value("selectedText", grt::StringRef("")); argpool.add_simple_value("document", grt::StringRef("")); groups.clear(); groups.push_back("Filter"); plugin_items = bec::GRTManager::get()->get_plugin_context_menu_items(groups, argpool); if (!plugin_items.empty()) { d->editorContextMenu->add_separator(); d->editorTextSubmenu = new mforms::Menu(); d->editorTextSubmenu->add_items_from_list(plugin_items); d->editorContextMenu->add_submenu(_("Text"), d->editorTextSubmenu); } d->codeEditor->set_context_menu(d->editorContextMenu); scoped_connect(d->editorContextMenu->signal_on_action(), std::bind(&MySQLEditor::activate_context_menu_item, this, std::placeholders::_1)); } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::editor_menu_opening() { int index = d->editorContextMenu->get_item_index("undo"); d->editorContextMenu->set_item_enabled(index, d->codeEditor->can_undo()); index = d->editorContextMenu->get_item_index("redo"); d->editorContextMenu->set_item_enabled(index, d->codeEditor->can_redo()); index = d->editorContextMenu->get_item_index("cut"); d->editorContextMenu->set_item_enabled(index, d->codeEditor->can_cut()); index = d->editorContextMenu->get_item_index("copy"); d->editorContextMenu->set_item_enabled(index, d->codeEditor->can_copy()); index = d->editorContextMenu->get_item_index("paste"); d->editorContextMenu->set_item_enabled(index, d->codeEditor->can_paste()); index = d->editorContextMenu->get_item_index("delete"); d->editorContextMenu->set_item_enabled(index, d->codeEditor->can_delete()); } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::activate_context_menu_item(const std::string &name) { // Standard commands first. if (name == "undo") d->codeEditor->undo(); else if (name == "redo") d->codeEditor->redo(); else if (name == "cut") d->codeEditor->cut(); else if (name == "copy") d->codeEditor->copy(); else if (name == "paste") d->codeEditor->paste(); else if (name == "delete") d->codeEditor->replace_selected_text(""); else if (name == "select_all") d->codeEditor->set_selection(0, d->codeEditor->text_length()); else { std::vector<std::string> parts = base::split(name, ":", 1); if (parts.size() == 2 && parts[0] == "plugin") { app_PluginRef plugin(bec::GRTManager::get()->get_plugin_manager()->get_plugin(parts[1])); if (!plugin.is_valid()) throw std::runtime_error("Invalid plugin " + name); bec::ArgumentPool argpool; argpool.add_entries_for_object("activeQueryBuffer", grtobj()); argpool.add_entries_for_object("", grtobj()); bool input_was_selection = false; if (bec::ArgumentPool::needs_simple_input(plugin, "selectedText")) { argpool.add_simple_value("selectedText", grt::StringRef(selected_text())); input_was_selection = true; } if (bec::ArgumentPool::needs_simple_input(plugin, "document")) argpool.add_simple_value("document", grt::StringRef(sql())); bool is_filter = false; if (plugin->groups().get_index("Filter") != grt::BaseListRef::npos) is_filter = true; grt::BaseListRef fargs(argpool.build_argument_list(plugin)); grt::ValueRef result = bec::GRTManager::get()->get_plugin_manager()->execute_plugin_function(plugin, fargs); if (is_filter) { if (!result.is_valid() || !grt::StringRef::can_wrap(result)) throw std::runtime_error(base::strfmt("plugin %s returned unexpected value", plugin->name().c_str())); grt::StringRef str(grt::StringRef::cast_from(result)); if (input_was_selection) d->codeEditor->replace_selected_text(str.c_str()); else d->codeEditor->set_text(str.c_str()); } } else { logWarning("Unhandled context menu item %s", name.c_str()); } } } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::show_special_chars(bool flag) { d->codeEditor->set_features(mforms::FeatureShowSpecial, flag); } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::enable_word_wrap(bool flag) { d->codeEditor->set_features(mforms::FeatureWrapText, flag); } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::set_sql_check_enabled(bool flag) { if (d->isSQLCheckEnabled != flag) { d->isSQLCheckEnabled = flag; if (flag) { ThreadedTimer::get()->remove_task(d->currentWorkTimerID); // Does nothing if the id is -1. if (d->currentDelayTimer == nullptr) d->currentDelayTimer = bec::GRTManager::get()->run_every(std::bind(&MySQLEditor::start_sql_processing, this), 0.01); } else stop_processing(); } } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::setup_auto_completion() { d->codeEditor->auto_completion_max_size(80, 15); static std::vector<std::pair<int, std::string>> ccImages = { {AC_KEYWORD_IMAGE, "ac_keyword.png"}, {AC_SCHEMA_IMAGE, "ac_schema.png"}, {AC_TABLE_IMAGE, "ac_table.png"}, {AC_ROUTINE_IMAGE, "ac_routine.png"}, {AC_FUNCTION_IMAGE, "ac_function.png"}, {AC_VIEW_IMAGE, "ac_view.png"}, {AC_COLUMN_IMAGE, "ac_column.png"}, {AC_OPERATOR_IMAGE, "ac_operator.png"}, {AC_ENGINE_IMAGE, "ac_engine.png"}, {AC_TRIGGER_IMAGE, "ac_trigger.png"}, {AC_LOGFILE_GROUP_IMAGE, "ac_logfilegroup.png"}, {AC_USER_VAR_IMAGE, "ac_uservar.png"}, {AC_SYSTEM_VAR_IMAGE, "ac_sysvar.png"}, {AC_TABLESPACE_IMAGE, "ac_tablespace.png"}, {AC_EVENT_IMAGE, "ac_event.png"}, {AC_INDEX_IMAGE, "ac_index.png"}, {AC_USER_IMAGE, "ac_user.png"}, {AC_CHARSET_IMAGE, "ac_charset.png"}, {AC_COLLATION_IMAGE, "ac_collation.png"}}; d->codeEditor->auto_completion_register_images(ccImages); d->codeEditor->auto_completion_stops("\t,.*;) "); // Will close ac even if we are in an identifier. d->codeEditor->auto_completion_fillups(""); } //---------------------------------------------------------------------------------------------------------------------- /** * Returns the text in the editor starting at the given position backwards until * the line start or the first non alphanumeric char is found. */ std::string MySQLEditor::getWrittenPart(size_t position) { ssize_t line = d->codeEditor->line_from_position(position); ssize_t start, stop; d->codeEditor->get_range_of_line(line, start, stop); std::string text = d->codeEditor->get_text_in_range(start, position); if (text.empty()) return ""; const char *head = text.c_str(); const char *run = head; std::string lastQuotedText; while (*run != '\0') { if (*run == '\'' || *run == '"' || *run == '`') { // Entering a quoted text. head = run + 1; char quote_char = *run; while (true) { run = g_utf8_next_char(run); if (*run == quote_char || *run == '\0') break; // If there's an escape char skip it and the next char too (if we didn't // reach the end). if (*run == '\\') { run++; if (*run != '\0') run = g_utf8_next_char(run); } } if (*run == '\0') // Unfinished quoted text. Return everything. return head; lastQuotedText = std::string(head - 1, run - head); // Include the quotes or scintilla will mess up head = run + 1; // Skip over this quoted text and start over. } run++; } // If we come here then we are outside any quoted text. Scan back for anything we consider to be a word stopper. // There is a special case however: if we are directly after a quoted part, this part is used as typed text // (treating it so as if it wasn't quoted). if (head == run && (*(head - 1) == '\'' || *(head - 1) == '\'' || *(head - 1) == '\'')) return lastQuotedText; while (head < run--) { if (!std::isalnum(*run) && *run != '_' && *run != '$' && *run != '@') // Allowed parts in an unquoted identifier. return run + 1; } return head; } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::show_auto_completion(bool auto_choose_single) { if (!code_completion_enabled()) return; d->codeEditor->auto_completion_options(true, auto_choose_single, false, true, false); // Get the statement and its absolute position. size_t caretPosition = d->codeEditor->get_caret_pos(); size_t caretLine = d->codeEditor->line_from_position(caretPosition); ssize_t lineStart, lineEnd; d->codeEditor->get_range_of_line(caretLine, lineStart, lineEnd); size_t caretOffset = caretPosition - lineStart; // This is a byte offset. size_t min, max; std::string statement; bool fixedCaretPos = false; if (get_current_statement_range(min, max, true)) { // If the caret is in the whitespaces before the query we would get a wrong line number // (because the statement splitter doesn't include these whitespaces in the determined ranges). // We set the caret pos to the first position in the query, which has the same effect for // code completion (we don't generate error line numbers). uint32_t codeStartLine = (uint32_t)d->codeEditor->line_from_position(min); if (codeStartLine > caretLine) { caretLine = 0; caretOffset = 0; fixedCaretPos = true; } else caretLine -= codeStartLine; statement = d->codeEditor->get_text_in_range(min, max); } else { // No query, means we have nothing typed yet in the current query (except whitespaces/comments). caretLine = 0; caretOffset = 0; fixedCaretPos = true; } // Convert current caret position into a position of the single statement. // The byte-based offset in the line must be converted to a character offset. if (!fixedCaretPos) { std::string line_text = d->codeEditor->get_text_in_range(lineStart, lineEnd); caretOffset = g_utf8_pointer_to_offset(line_text.c_str(), line_text.c_str() + caretOffset); } d->codeCompletionCandidates = d->services->getCodeCompletionCandidates( d->autocompletionContext, { caretOffset, caretLine }, statement, d->currentSchema, make_keywords_uppercase(), d->symbolTable); update_auto_completion(getWrittenPart(caretPosition)); } //---------------------------------------------------------------------------------------------------------------------- /** * Updates the auto completion list by filtering the determined entries by the * text the user * already typed. If auto completion is not yet active it becomes active here. * Returns the list sent to the editor for unit tests to validate them. */ std::vector<std::pair<int, std::string>> MySQLEditor::update_auto_completion(const std::string &typed_part) { logDebug2("Updating auto completion popup in editor\n"); // Remove all entries that don't start with the typed text before showing the // list. if (!typed_part.empty()) { gchar *prefix = g_utf8_casefold(typed_part.c_str(), -1); std::vector<std::pair<int, std::string>> filteredEntries; for (auto &entry : d->codeCompletionCandidates) { gchar *folded = g_utf8_casefold(entry.second.c_str(), -1); if (g_str_has_prefix(folded, prefix)) filteredEntries.push_back(entry); g_free(folded); } switch (filteredEntries.size()) { case 0: logDebug2("Nothing to autocomplete - hiding popup if it was active\n"); d->codeEditor->auto_completion_cancel(); break; case 1: // See if that single entry matches the typed part. If so we don't need // to show ac either. if (base::same_string(filteredEntries[0].second, prefix, false)) { logDebug2( "The only match is the same as the written input - hiding popup " "if it was active\n"); d->codeEditor->auto_completion_cancel(); break; } // Fall through. default: logDebug2("Showing auto completion popup\n"); d->codeEditor->auto_completion_show(typed_part.size(), filteredEntries); break; } g_free(prefix); return filteredEntries; } else { if (!d->codeCompletionCandidates.empty()) { logDebug2("Showing auto completion popup\n"); d->codeEditor->auto_completion_show(0, d->codeCompletionCandidates); } else { logDebug2("Nothing to autocomplete - hiding popup if it was active\n"); d->codeEditor->auto_completion_cancel(); } } return d->codeCompletionCandidates; } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::cancel_auto_completion() { // Make sure a pending timed autocompletion won't kick in after we cancel it. d->lastTypedChar = 0; d->codeEditor->auto_completion_cancel(); } //---------------------------------------------------------------------------------------------------------------------- bool MySQLEditor::code_completion_enabled() { return bec::GRTManager::get()->get_app_option_int("DbSqlEditor:CodeCompletionEnabled") == 1; } //---------------------------------------------------------------------------------------------------------------------- bool MySQLEditor::auto_start_code_completion() { return (bec::GRTManager::get()->get_app_option_int("DbSqlEditor:AutoStartCodeCompletion") == 1) && (d->autocompletionContext != nullptr); } //---------------------------------------------------------------------------------------------------------------------- bool MySQLEditor::make_keywords_uppercase() { return bec::GRTManager::get()->get_app_option_int("DbSqlEditor:CodeCompletionUpperCaseKeywords") == 1; } //---------------------------------------------------------------------------------------------------------------------- /** * Determines the start and end position of the current statement, that is, the statement where the caret is in. * For effective search in a large set binary search is used. * * Note: search can be done in two modes: * - strict: whitespaces before a statement belong to that statement. * - loose: such whitespaces belong to the previous statement (and are ignored). * Loose mode allows to have the caret in the whitespaces after a statement and execute that, * while strict mode is needed for code completion (should be done for the following statement then). * * @returns true if a statement could be found at the caret position, otherwise false. */ bool MySQLEditor::get_current_statement_range(size_t &start, size_t &end, bool strict) { // In case the splitter is right now processing the text we wait here until its done. // If the splitter wasn't triggered yet (e.g. when typing fast and then immediately running a statement) // then we do the splitting here instead. RecMutexLock sql_statement_borders_mutex(d->sqlStatementBordersMutex); d->splitStatementsIfRequired(); if (d->statementRanges.empty()) return false; typedef std::vector<StatementRange>::iterator RangeIterator; size_t caret_position = d->codeEditor->get_caret_pos(); RangeIterator low = d->statementRanges.begin(); RangeIterator high = d->statementRanges.end() - 1; while (low < high) { RangeIterator middle = low + (high - low + 1) / 2; if (middle->start > caret_position) high = middle - 1; else { size_t end = low->start + low->length; if (end >= caret_position) break; low = middle; } } if (low == d->statementRanges.end()) return false; // If we are between two statements (in white spaces) then the algorithm above // returns the lower one. if (strict) { if (low->start + low->length < caret_position) ++low; if (low == d->statementRanges.end()) return false; } start = low->start; end = low->start + low->length; return true; } //---------------------------------------------------------------------------------------------------------------------- /** * Stops any ongoing processing like splitting, syntax checking etc. */ void MySQLEditor::stop_processing() { d->stopProcessing = true; ThreadedTimer::get()->remove_task(d->currentWorkTimerID); d->currentWorkTimerID = -1; if (d->currentDelayTimer != nullptr) { bec::GRTManager::get()->cancel_timer(d->currentDelayTimer); d->currentDelayTimer = nullptr; } } //---------------------------------------------------------------------------------------------------------------------- void MySQLEditor::focus() { d->codeEditor->focus(); } //---------------------------------------------------------------------------------------------------------------------- /** * Register a target for file drop operations which will handle these cases. */ void MySQLEditor::register_file_drop_for(mforms::DropDelegate *target) { std::vector<std::string> formats; formats.push_back(mforms::DragFormatFileName); d->codeEditor->register_drop_formats(target, formats); } //----------------------------------------------------------------------------------------------------------------------