backend/wbprivate/sqlide/wb_sql_editor_buffer.cpp (522 lines of code) (raw):
/*
* Copyright (c) 2011, 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 "wb_sql_editor_form.h"
#include "wb_sql_editor_buffer.h"
#include "wb_sql_editor_panel.h"
#include "wb_sql_editor_tree_controller.h"
#include <boost/foreach.hpp>
#include <fstream>
#include <errno.h>
#include "base/util_functions.h"
#include "base/file_functions.h"
#include "base/string_utilities.h"
#include "base/log.h"
#include "mforms/utilities.h"
#include "mforms/filechooser.h"
#include "mforms/code_editor.h"
#include "query_side_palette.h"
#include "grtsqlparser/mysql_parser_services.h"
DEFAULT_LOG_DOMAIN("SqlEditor")
using namespace bec;
using namespace base;
void SqlEditorForm::auto_save() {
if (!_autosave_disabled && _startup_done) {
logDebug("Auto saving workspace\n");
try {
save_workspace(sanitize_file_name(_connection.is_valid() ? _connection->name() : "unconnected"), true);
} catch (std::exception &exc) {
std::string message = strfmt(_("An error occurred during auto-save:\n%s"), exc.what());
logError("Auto saving editors failed: %s\n", message.c_str());
if (mforms::Utilities::show_error(_("Error on Auto Save"), message, _("Continue"), _("Skip Auto Saving")) !=
mforms::ResultOk) {
_autosave_disabled = true;
}
}
}
}
// Save all script buffers, including scratch buffers
void SqlEditorForm::save_workspace(const std::string &workspace_name, bool is_autosave) {
std::string path;
// if we're autosaving, just use the same path from previous saves
if (!is_autosave || _autosave_path.empty()) {
std::string path_prefix = base::makePath(bec::GRTManager::get()->get_user_datadir(), "sql_workspaces");
if (!g_file_test(path_prefix.c_str(), G_FILE_TEST_EXISTS)) {
if (g_mkdir_with_parents(path_prefix.c_str(), 0700) < 0)
throw std::runtime_error(strfmt("Could not create directory %s: %s", path_prefix.c_str(), g_strerror(errno)));
}
int i = 1;
do {
path = base::makePath(path_prefix,
strfmt("%s-%i%s", workspace_name.c_str(), i++, (is_autosave ? ".autosave" : ".workspace")));
} while (!create_directory(path, 0700)); // returns false if dir exists, exception on other errors
if (is_autosave) {
_autosave_lock = new base::LockFile(base::makePath(path, "lock"));
_autosave_path = path;
}
} else
path = _autosave_path;
// save the real id of the connection
if (_connection.is_valid())
g_file_set_contents(base::makePath(path, "connection_id").c_str(), _connection->id().c_str(),
(gssize)_connection->id().size(), NULL);
// save some of the state of the schema tree
{
std::string info;
info.append("active_schema=").append(active_schema()).append("\n");
// save the expansion state of the active schema only, since saving everything could be very slow when restoring
mforms::TreeNodeRef schema_node =
_live_tree->get_schema_tree()->get_node_for_object(active_schema(), wb::LiveSchemaTree::Schema, "");
if (schema_node) {
std::string expand_state;
if (schema_node->is_expanded()) {
expand_state = active_schema();
expand_state.append(":schema");
if (schema_node->get_child(0) && schema_node->get_child(0)->is_expanded())
expand_state.append(",tables");
if (schema_node->get_child(1) && schema_node->get_child(1)->is_expanded())
expand_state.append(",views");
if (schema_node->get_child(2) && schema_node->get_child(2)->is_expanded())
expand_state.append(",procedures");
if (schema_node->get_child(3) && schema_node->get_child(3)->is_expanded())
expand_state.append(",functions");
} else
expand_state = "";
info.append("expanded=").append(expand_state).append("\n");
}
g_file_set_contents(base::makePath(path, "schema_tree").c_str(), info.c_str(), info.size(), NULL);
}
if (_tabdock) {
for (int c = _tabdock->view_count(), i = 0; i < c; i++) {
SqlEditorPanel *editor = sql_editor_panel(i);
if (!editor)
continue;
try {
editor->auto_save(path);
} catch (std::exception &e) {
logError("Could not auto-save editor %s\n", editor->get_title().c_str());
mforms::Utilities::show_error(
"Auto save", base::strfmt("Could not save contents of tab %s.\n%s", editor->get_title().c_str(), e.what()),
"OK");
}
}
}
save_workspace_order(path);
}
std::string SqlEditorForm::find_workspace_state(const std::string &workspace_name,
std::unique_ptr<base::LockFile> &lock_file) {
std::string path_prefix = base::makePath(bec::GRTManager::get()->get_user_datadir(), "sql_workspaces");
// find workspaces on disk
std::string workspace_path;
bool restoring_autosave = false;
{
GDir *dir = g_dir_open(path_prefix.c_str(), 0, NULL);
if (!dir)
return "";
int lowest_index = 9999999;
const gchar *name;
while ((name = g_dir_read_name(dir)) != NULL) {
if (g_str_has_prefix(name, workspace_name.c_str())) {
const char *end = strrchr(name, '.');
int new_index = 0;
if (end)
new_index =
base::atoi<int>(std::string(name + workspace_name.size() + 1, end - name - (workspace_name.size() + 1)), 0);
if (g_str_has_suffix(name, ".autosave")) {
if (LockFile::check(base::makePath(base::makePath(path_prefix, name), "lock")) != LockFile::NotLocked)
continue;
if (!restoring_autosave) {
try {
lock_file.reset(new base::LockFile(base::makePath(base::makePath(path_prefix, name), "lock")));
} catch (const base::file_locked_error &) {
continue;
}
lowest_index = new_index;
restoring_autosave = true;
workspace_path = name;
} else if (new_index < lowest_index) {
try {
lock_file.reset(new base::LockFile(base::makePath(base::makePath(path_prefix, name), "lock")));
} catch (const base::file_locked_error &) {
continue;
}
lowest_index = new_index;
workspace_path = name;
}
} else if (!restoring_autosave && g_str_has_suffix(name, ".workspace")) {
if (new_index < lowest_index) {
try {
lock_file.reset(new base::LockFile(base::makePath(base::makePath(path_prefix, name), "lock")));
} catch (const base::file_locked_error &) {
continue;
}
workspace_path = name;
lowest_index = new_index;
}
}
}
}
g_dir_close(dir);
}
return workspace_path;
}
struct GuardBoolFlag {
bool *flag;
GuardBoolFlag(bool *ptr) : flag(ptr) {
if (flag)
*flag = true;
}
~GuardBoolFlag() {
if (flag)
*flag = false;
}
};
// Restore a previously saved workspace for this connection. The loaded data is deleted immediately after loading
// (unless its an autosave)
bool SqlEditorForm::load_workspace(const std::string &workspace_name) {
std::string path_prefix = base::makePath(bec::GRTManager::get()->get_user_datadir(), "sql_workspaces");
GuardBoolFlag flag(&_loading_workspace);
std::unique_ptr<base::LockFile> lock_file;
std::string workspace_path = find_workspace_state(workspace_name, lock_file);
if (workspace_path.empty())
return false;
workspace_path = base::makePath(path_prefix, workspace_path);
if (base::file_exists(base::makePath(workspace_path, "tab_order"))) {
// new WB 6.2 format workspace
std::wifstream f = openTextInputStream(base::makePath(workspace_path, "tab_order"));
std::vector<std::string> editor_files;
while (!f.eof()) {
std::wstring suffix;
f >> suffix;
if (!suffix.empty())
editor_files.push_back(base::wstring_to_string(suffix));
}
SqlEditorPanel *editor(add_sql_editor());
for (std::string file : editor_files) {
std::string info_file = base::makePath(workspace_path, file + ".info");
std::string text_file = base::makePath(workspace_path, file + ".scratch");
SqlEditorPanel::AutoSaveInfo info(info_file);
try {
if (editor->load_autosave(info, text_file)) {
// editor load was successful, create tab for the next editor
editor = add_sql_editor();
}
} catch (std::exception &e) {
if (!text_file.empty()) {
int rc;
rc = mforms::Utilities::show_error(
"Restore Workspace", strfmt("Could not read contents of '%s'.\n%s", text_file.c_str(), e.what()), "Ignore",
"Cancel", "");
if (rc != mforms::ResultOk)
return false;
}
}
// delete the autosaves
try {
base::remove(info_file);
} catch (std::exception &e) {
logError("Could not delete autosave file %s\n%s\n", info_file.c_str(), e.what());
}
try {
base::remove(text_file);
} catch (std::exception &e) {
logError("Could not delete autosave file %s\n%s\n", text_file.c_str(), e.what());
}
}
// remove the pre-created editor
remove_sql_editor(editor);
} else {
typedef std::pair<std::string, SqlEditorPanel::AutoSaveInfo> FileItem;
std::vector<FileItem> editor_files;
// old format workspace
{
GDir *dir = g_dir_open(workspace_path.c_str(), 0, NULL);
if (!dir)
return false;
const gchar *name;
while ((name = g_dir_read_name(dir)) != NULL) {
SqlEditorPanel::AutoSaveInfo info;
std::string path = base::makePath(workspace_path, name);
if (base::hasSuffix(name, ".scratch")) {
editor_files.push_back(std::make_pair(path, SqlEditorPanel::AutoSaveInfo::old_scratch(path)));
} else if (base::hasSuffix(name, ".autosave")) {
editor_files.push_back(std::make_pair(path, SqlEditorPanel::AutoSaveInfo::old_autosave(path)));
}
}
g_dir_close(dir);
}
SqlEditorPanel *editor(add_sql_editor());
for (FileItem file : editor_files) {
try {
if (editor->load_autosave(file.second, file.first)) {
// editor load was successful, create tab for the next editor
editor = add_sql_editor();
}
} catch (std::exception &e) {
if (!file.first.empty()) {
int rc;
rc = mforms::Utilities::show_error(
"Restore Workspace", strfmt("Could not read contents of '%s'.\n%s", file.first.c_str(), e.what()), "Ignore",
"Cancel", "");
if (rc != mforms::ResultOk)
return false;
}
}
// delete the autosaves
try {
base::remove(file.first);
} catch (std::exception &e) {
logError("Could not delete autosave file %s\n%s\n", file.first.c_str(), e.what());
}
}
// remove the pre-created editor
remove_sql_editor(editor);
}
// load schema tree state
{
gchar *data;
gsize length;
if (g_file_get_contents(base::makePath(workspace_path, "schema_tree").c_str(), &data, &length, NULL)) {
char *line = strtok(data, "\n");
while (line) {
if (base::hasPrefix(line, "expanded=")) {
char *value = strchr(line, '=');
if (value) {
_pending_expand_nodes = value + 1; // expanded=<schema-name>:schema,tables,views,etc
break;
}
}
line = strtok(NULL, "\n");
}
g_free(data);
}
}
if (base::hasSuffix(workspace_path, ".autosave")) {
_autosave_lock = lock_file.release();
_autosave_path = workspace_path;
bec::GRTManager::get()->replace_status_text(_("Restored last session state"));
} else {
lock_file.reset(0);
base_rmdir_recursively(workspace_path.c_str());
}
return true;
}
SqlEditorPanel *SqlEditorForm::add_sql_editor(bool scratch, bool start_collapsed) {
SqlEditorPanel *editor(mforms::manage(new SqlEditorPanel(this, scratch, start_collapsed)));
editor->editor_be()->register_file_drop_for(this);
editor->grtobj()->owner(grtobj());
grtobj()->queryEditors().insert(editor->grtobj());
_tabdock->dock_view(editor);
_tabdock->select_view(editor);
if (!scratch)
editor->set_title(strfmt("SQL File %i", ++_sql_editors_serial));
else
editor->set_title(strfmt("Query %i", ++_scratch_editors_serial));
if (!_loading_workspace)
auto_save();
return editor;
}
//--------------------------------------------------------------------------------------------------
mforms::DragOperation SqlEditorForm::drag_over(mforms::View *sender, base::Point p,
mforms::DragOperation allowedOperations,
const std::vector<std::string> &formats) {
// We can accept dropped files.
if (std::find(formats.begin(), formats.end(), mforms::DragFormatFileName) != formats.end())
return allowedOperations & mforms::DragOperationCopy; // Copy to indicate we don't do anything with the files.
return mforms::DragOperationNone;
}
//--------------------------------------------------------------------------------------------------
mforms::DragOperation SqlEditorForm::files_dropped(mforms::View *sender, base::Point p,
mforms::DragOperation allowedOperations,
const std::vector<std::string> &file_names) {
if ((allowedOperations & mforms::DragOperationCopy) != mforms::DragOperationCopy)
return mforms::DragOperationNone;
#ifdef _MSC_VER
bool case_sensitive = false; // TODO: on Mac case sensitivity depends on the file system.
#else
bool case_sensitive = true;
#endif
std::vector<std::string> file_names_to_open;
for (size_t i = 0; i < file_names.size(); ++i) {
// First see if we have already a tab with that file open.
bool found = false;
for (size_t c = _tabdock->view_count(), j = 0; j < c; j++) {
SqlEditorPanel *panel = sql_editor_panel((int)j);
if (panel && base::same_string(panel->filename(), file_names[i], case_sensitive)) {
found = true;
// Ignore this file name, but if this is the only one dropped the activate the particular
// sql editor.
if (file_names.size() == 1)
_tabdock->select_view(panel);
break;
}
}
if (!found)
file_names_to_open.push_back(file_names[i]);
}
for (std::vector<std::string>::const_iterator f = file_names_to_open.begin(); f != file_names_to_open.end(); ++f)
open_file(*f, true);
return mforms::DragOperationCopy;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorForm::remove_sql_editor(SqlEditorPanel *panel) {
panel->grtobj()->owner().clear();
grtobj()->queryEditors().remove_value(panel->grtobj());
// if we're removing editor, just cancel query side timer cause there is only one timer
if (_side_palette)
_side_palette->cancel_timer();
if (!_closing && !_autosave_path.empty()) // if autosave_path is empty then it means we're not ready yet
{
panel->delete_auto_save(_autosave_path);
save_workspace_order(_autosave_path);
}
_tabdock->undock_view(panel);
// no need to delete, undock_view will release the reference and delete it because panel is managed
// delete panel;
}
SqlEditorPanel *SqlEditorForm::active_sql_editor_panel() {
if (_tabdock)
return dynamic_cast<SqlEditorPanel *>(_tabdock->selected_view());
return nullptr;
}
void SqlEditorForm::sql_editor_panel_switched() {
SqlEditorPanel *panel = active_sql_editor_panel();
if (panel)
bec::GRTManager::get()->run_once_when_idle(
(bec::UIForm *)panel, std::bind(&mforms::View::focus, panel->editor_be()->get_editor_control()));
validate_menubar();
}
void SqlEditorForm::sql_editor_panel_closed(mforms::AppView *view) {
if (!_closing) {
if (_tabdock->view_count() == 0)
new_sql_scratch_area();
else if (dynamic_cast<SqlEditorPanel *>(view)) {
// We check if the tab was closed is SqlEditorPanel if so,
// we need to find if there's at least one query tab left if not,
// we create new one
bool panel_found = false;
for (int i = 0; i < _tabdock->view_count(); ++i) {
if (sql_editor_panel(i)) {
panel_found = true;
break;
}
}
if (!panel_found)
new_sql_scratch_area();
}
}
}
void SqlEditorForm::save_workspace_order(const std::string &prefix) {
if (prefix.empty())
logError("save with empty path\n");
if (_tabdock) {
std::wofstream orderFile = openTextOutputStream(base::makePath(prefix, "tab_order"));
for (int c = _tabdock->view_count(), i = 0; i < c; i++) {
SqlEditorPanel *editor = sql_editor_panel(i);
if (editor && orderFile.good())
orderFile << base::string_to_wstring(editor->autosave_file_suffix()) << std::endl;
}
orderFile.close();
}
}
void SqlEditorForm::sql_editor_reordered(SqlEditorPanel *panel, int to) {
if (!panel || to < 0)
return;
/// Reorder the GRT lists
int from_index = (int)grtobj()->queryEditors().get_index(panel->grtobj());
if (from_index == (int)grt::BaseListRef::npos)
logFatal("Could not find reordered editor in GRT object list\n");
// first build an array of result panel objects, in the same order as the tabview
std::vector<std::pair<db_query_QueryEditorRef, int> > panels;
for (int panel_order = 0, i = 0; i < sql_editor_count(); i++) {
SqlEditorPanel *p = sql_editor_panel(i);
if (p)
panels.push_back(std::make_pair(p->grtobj(), panel_order++));
else
panels.push_back(std::make_pair(db_query_QueryEditorRef(), 0));
}
int to_index = -1;
// now find out where we have to move to
if ((int)from_index < to) {
for (int i = to; i > (int)from_index; i--) {
if (panels[i].first.is_valid()) {
to_index = panels[i].second;
break;
}
}
} else {
for (int i = to; i < (int)from_index; i++) {
if (panels[i].first.is_valid()) {
to_index = panels[i].second;
break;
}
}
}
if (to_index < 0) {
to_index = panels.back().second;
}
grtobj()->queryEditors()->reorder(from_index, to_index);
if (!_autosave_path.empty()) {
/// Rename autosave files to keep the order
save_workspace_order(_autosave_path);
}
}
//--------------------------------------------------------------------------------------------------
SqlEditorPanel *SqlEditorForm::sql_editor_panel(int index) {
if (index >= 0 && index < _tabdock->view_count())
return dynamic_cast<SqlEditorPanel *>(_tabdock->view_at_index(index));
return NULL;
}
int SqlEditorForm::sql_editor_panel_index(SqlEditorPanel *panel) {
for (int c = _tabdock->view_count(), i = 0; i < c; i++) {
if (sql_editor_panel(i) == panel)
return i;
}
return -1;
}
int SqlEditorForm::sql_editor_count() {
if (_tabdock)
return _tabdock->view_count();
return 0;
}
//--------------------------------------------------------------------------------------------------
SqlEditorPanel *SqlEditorForm::new_sql_script_file() {
SqlEditorPanel *panel = add_sql_editor(false);
bec::GRTManager::get()->replace_status_text(_("Added new script editor"));
update_menu_and_toolbar();
return panel;
}
SqlEditorPanel *SqlEditorForm::new_sql_scratch_area(bool start_collapsed) {
SqlEditorPanel *panel = add_sql_editor(true, start_collapsed);
bec::GRTManager::get()->replace_status_text(_("Added new scratch query editor"));
update_menu_and_toolbar();
return panel;
}
//--------------------------------------------------------------------------------------------------
void SqlEditorForm::open_file(const std::string &path, bool in_new_tab, bool askForFile) {
std::string file_path = path;
bec::GRTManager::get()->replace_status_text(base::strfmt(_("Opening %s..."), path.c_str()));
if (askForFile) {
if (file_path.empty()) {
mforms::FileChooser opendlg(mforms::OpenFile);
opendlg.set_title(_("Open SQL Script"));
opendlg.set_extensions("SQL Files (*.sql)|*.sql|Query Browser Files (*.qbquery)|*.qbquery", "sql");
if (opendlg.run_modal())
file_path = opendlg.get_path();
}
if (file_path.empty()) {
bec::GRTManager::get()->replace_status_text(_("Cancelled open file"));
return;
}
}
SqlEditorPanel *panel = NULL;
if (!in_new_tab)
panel = active_sql_editor_panel();
if (!panel)
panel = new_sql_script_file();
if (panel->is_dirty()) {
int r = mforms::Utilities::show_warning(_("Open File"), strfmt(_("SQL script %s has unsaved changes.\n"
"Would you like to Save these changes?"),
panel->get_title().c_str()),
_("Save"), _("Cancel"), _("Don't Save"));
if (r == mforms::ResultCancel)
return;
else if (r == mforms::ResultOk)
if (!panel->save())
return;
}
try {
if (askForFile && panel->load_from(file_path) == SqlEditorPanel::RunInstead) {
if (in_new_tab)
remove_sql_editor(panel);
grt::BaseListRef args(true);
args.ginsert(grtobj());
args.ginsert(grt::StringRef(file_path));
grt::GRT::get()->call_module_function("SQLIDEUtils", "runSQLScriptFile", args);
return;
}
} catch (std::exception &exc) {
logError("Cannot open file %s: %s\n", file_path.c_str(), exc.what());
if (in_new_tab)
remove_sql_editor(panel);
mforms::Utilities::show_error(_("Open File"),
strfmt(_("Could not open file %s\n%s"), file_path.c_str(), exc.what()), _("OK"));
return;
}
{
base::NotificationInfo info;
info["opener"] = "SqlEditorForm";
info["path"] = file_path;
base::NotificationCenter::get()->send("GNDocumentOpened", this, info);
}
auto_save();
}
//--------------------------------------------------------------------------------------------------
std::string SqlEditorForm::restore_sql_from_history(int entry_index, std::list<int> &detail_indexes) {
return _history->restore_sql_from_history(entry_index, detail_indexes);
}
void SqlEditorForm::set_autosave_disabled(const bool autosave_disabled) {
_autosave_disabled = autosave_disabled;
}
bool SqlEditorForm::get_autosave_disabled(void) {
return _autosave_disabled;
}