From fbd630489efc2536a304d9ca952e91a110007c62 Mon Sep 17 00:00:00 2001 From: eelke Date: Mon, 19 Aug 2019 11:38:04 +0200 Subject: [PATCH] Added paste lang option for pasting programming code. Expects you to paste only string literals with possible concatenation operators like . or + --- pglab/DatabaseWindow.cpp | 21 +- pglab/DatabaseWindow.h | 2 + pglab/QueryTool.cpp | 7 + pglab/QueryTool.h | 1 + pglablib/util.cpp | 208 ++++++++++++++++++ pglablib/util.h | 1 + tests/pglabtests/pglabtests.pro | 1 + .../pglabtests/tst_ConvertLangToSqlString.cpp | 63 ++++++ 8 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 tests/pglabtests/tst_ConvertLangToSqlString.cpp diff --git a/pglab/DatabaseWindow.cpp b/pglab/DatabaseWindow.cpp index 0993d04..2e24156 100644 --- a/pglab/DatabaseWindow.cpp +++ b/pglab/DatabaseWindow.cpp @@ -141,14 +141,14 @@ void DatabaseWindow::createActions() icon.addFile(QString::fromUtf8(":/icons/token_shortland_character.png"), QSize(), QIcon::Normal, QIcon::On); auto action = actionCopyAsCString = new QAction(icon, tr("Copy as C string"), this); action->setObjectName("actionCopyAsCString"); - action->setShortcut(QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_C)); + action->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_C)); } { QIcon icon; icon.addFile(QString::fromUtf8(":/icons/token_shortland_character.png"), QSize(), QIcon::Normal, QIcon::On); auto action = actionCopyAsRawCppString = new QAction(icon, tr("Copy as raw C++-string"), this); action->setObjectName("actionCopyAsRawCppString"); - action->setShortcut(QKeySequence(Qt::CTRL + Qt::ALT + Qt::Key_C)); + } { QIcon icon; @@ -213,6 +213,13 @@ void DatabaseWindow::createActions() action->setObjectName("actionOpenSql"); action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_O)); } + { + QIcon icon; + //icon.addFile(QString::fromUtf8(":/icons/folder.png"), QSize(), QIcon::Normal, QIcon::On); + auto action = actionPasteLangString = new QAction(icon, tr("Paste lang string"), this); + action->setObjectName("actionPasteLangString"); + action->setShortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_V)); + } { QIcon icon; icon.addFile(QString::fromUtf8(":/icons/script_save.png"), QSize(), QIcon::Normal, QIcon::On); @@ -257,6 +264,8 @@ void DatabaseWindow::initMenus() actionCopy, actionCopyAsCString, actionCopyAsRawCppString, + // standard Paste missing Ctrl+V works however by default + actionPasteLangString, actionGenerateCode }); @@ -482,6 +491,14 @@ void DatabaseWindow::on_actionOpenSql_triggered() } } +void DatabaseWindow::on_actionPasteLangString_triggered() +{ + auto query_tool = GetActiveQueryTool(); + if (query_tool) { + query_tool->pasteLangString(); + } +} + void DatabaseWindow::on_actionSaveSql_triggered() { auto query_tool = GetActiveQueryTool(); diff --git a/pglab/DatabaseWindow.h b/pglab/DatabaseWindow.h index b7f62cc..333e18a 100644 --- a/pglab/DatabaseWindow.h +++ b/pglab/DatabaseWindow.h @@ -77,6 +77,7 @@ private: QAction *actionInspectUserSchemas = nullptr; ///< Create or switch to pgcatalog tab QAction *actionNewSql = nullptr; QAction *actionOpenSql = nullptr; + QAction *actionPasteLangString = nullptr; QAction *actionSaveSql = nullptr; QAction *actionSaveSqlAs = nullptr; QAction *actionSaveCopyOfSqlAs = nullptr; @@ -138,6 +139,7 @@ private slots: void on_actionInspectUserSchemas_triggered(); void on_actionNewSql_triggered(); void on_actionOpenSql_triggered(); + void on_actionPasteLangString_triggered(); void on_actionSaveSql_triggered(); void on_actionSaveSqlAs_triggered(); void on_actionSaveCopyOfSqlAs_triggered(); diff --git a/pglab/QueryTool.cpp b/pglab/QueryTool.cpp index eb2b0c3..f18a1ee 100644 --- a/pglab/QueryTool.cpp +++ b/pglab/QueryTool.cpp @@ -559,6 +559,13 @@ void QueryTool::copyQueryAsRawCppString() QApplication::clipboard()->setText(cs); } +void QueryTool::pasteLangString() +{ + QString s = QApplication::clipboard()->text(); + s = ConvertLangToSqlString(s); + ui->queryEdit->insertPlainText(s); +} + void QueryTool::generateCode() { QString command = getCommand(); diff --git a/pglab/QueryTool.h b/pglab/QueryTool.h index f36d63b..1e836a4 100644 --- a/pglab/QueryTool.h +++ b/pglab/QueryTool.h @@ -63,6 +63,7 @@ public slots: void saveCopyAs(); void copyQueryAsCString(); void copyQueryAsRawCppString(); + void pasteLangString(); void cancel(); void exportData(); diff --git a/pglablib/util.cpp b/pglablib/util.cpp index c7b43ff..647f944 100644 --- a/pglablib/util.cpp +++ b/pglablib/util.cpp @@ -146,3 +146,211 @@ QString ConvertToMultiLineRawCppString(const QString &in) out.append("\n)" + delim + "\""); return out; } + +QString ConvertLangToSqlString(const QString &in) +{ + // Assume mostly C++ for now but allow some other things like + // - single quotes (php) + // - concattenation operators . (php) and + (java) + + // Allow cpp prefixes L u8 u U + + // Parser flow, we start in whitespace state and search for prefix|opening quote + // parse string and process escapes + // if escape is \r strip it if \n go to new line + // until we reach matching end quote + // skip whitespace and + or . + + + QString output; + + enum { + WHITESPACE, + PREFIX, + IN_STRING, + END, + ERROR + } state = WHITESPACE; + int index = 0; + QChar quote = '\0'; + while (state != ERROR && state != END && index < in.length()) { + if (state == WHITESPACE) { + // skip all whitespace untill we encounter something else + // we also skip concatenation operators. Note this code is not trying to validate + // for correct syntax so it will quite happily accept many incorrect constructs + // that doesn't matter however as we are just trying to strip everything which is not SQL. + while (index < in.length() && (in[index].isSpace() || in[index] == '+' || in[index] == '.')) ++index; + if (index == in.length()) { + state == END; + break; + } + // Assume quotes can vary + if (in[index] == '\'' || in[index] == '\"') { + quote = in[index]; + ++index; + state = IN_STRING; + } + else { + state = PREFIX; + } + } + else if (state == PREFIX) { + auto c = in[index]; + if (c == 'L' || c == 'U') { + // C++ prefix expect C++ double quote + if (in.length() > index+ 1 && in[index+1] == '"') { + index += 2; + state = IN_STRING; + } + else { + state = ERROR; + break; + } + } + if (c == 'u') { + // C++ prefix expect C++ double quote + if (in.length() > index+ 2 && in[index+1] == '8' && in[index+2] == '"') { + index += 3; + state = IN_STRING; + } + else if (in.length() > index+ 1 && in[index+1] == '"') { + index += 2; + state = IN_STRING; + } + else { + state = ERROR; + break; + } + } + else { + state = ERROR; + break; + } + } + else if (state == IN_STRING) { + // scan contents of string and process any escapes encountered + bool escape = false; + while (state != ERROR && index < in.length()) { + QChar c = in[index]; + if (escape) { + if (c == 'a') output += '\x07'; + else if (c == 'a') output += '\x07'; + else if (c == 'b') output += '\x08'; + else if (c == 'f') output += '\x0c'; + else if (c == 'n') output += '\n'; + else if (c == 'r') ; + else if (c == 'v') ; + else if (c >= '0' && c <= '7') { + // process octal escape + if (in.length() > index + 2) { + char buf[4]; + buf[0] = c.toLatin1(); + buf[1] = in[++index].toLatin1(); + buf[2] = in[++index].toLatin1(); + buf[3] = 0; + long int v = strtol(buf, nullptr, 8); + if (v < 0x80) { + output += static_cast(static_cast(v)); + } + else { + state = ERROR; + break; + } + } + else { + state = ERROR; + break; + } + } + else if (c == 'x') { + // hex byte + if (in.length() > index + 2) { + char buf[3]; + buf[0] = in[++index].toLatin1(); + buf[1] = in[++index].toLatin1(); + buf[2] = 0; + long int v = strtol(buf, nullptr, 16); + output += static_cast(static_cast(v)); + } + else { + state = ERROR; + break; + } + } + else if (c == 'u') { + // 4 digit hax unicode codepoint + // hex byte + if (in.length() > index + 4) { + char buf[5]; + buf[0] = in[++index].toLatin1(); + buf[1] = in[++index].toLatin1(); + buf[2] = in[++index].toLatin1(); + buf[3] = in[++index].toLatin1(); + buf[4] = 0; + long int v = strtol(buf, nullptr, 16); + output += static_cast(static_cast(v)); + } + else { + state = ERROR; + break; + } + + } + else if (c == 'U') { + // 8 digit hax unicode codepoint + if (in.length() > index + 8) { + char buf[9]; + buf[0] = in[++index].toLatin1(); + buf[1] = in[++index].toLatin1(); + buf[2] = in[++index].toLatin1(); + buf[3] = in[++index].toLatin1(); + buf[4] = in[++index].toLatin1(); + buf[5] = in[++index].toLatin1(); + buf[6] = in[++index].toLatin1(); + buf[7] = in[++index].toLatin1(); + buf[8] = 0; + uint v = static_cast(strtol(buf, nullptr, 16)); + if (QChar::requiresSurrogates(v)) { + output += QChar(QChar::highSurrogate(v)); + output += QChar(QChar::lowSurrogate(v)); + } + } + else { + state = ERROR; + break; + } + + } + else { + output += c; + } + escape = false; + } + else { + if (c == quote) { + state = WHITESPACE; + ++index; + break; + } + // Is there any language where string literals do not have to be terminated before the end of the line + // Not considering string literals that explicitly allow for multiline strings as these are often raw strings + // and can be copy pasted normally. + else if (c == '\n') { + state = WHITESPACE; + ++index; + break; + } + else if (c == '\\') { + escape = true; + } + else { + output += c; + } + } + ++index; + } + } + } + + return output; +} diff --git a/pglablib/util.h b/pglablib/util.h index 90d1fbb..6fcfd96 100644 --- a/pglablib/util.h +++ b/pglablib/util.h @@ -9,6 +9,7 @@ QString msfloatToHumanReadableString(float ms); void copySelectionToClipboard(const QTableView *view); QString ConvertToMultiLineCString(const QString &in); QString ConvertToMultiLineRawCppString(const QString &in); +QString ConvertLangToSqlString(const QString &in); void exportTable(const QTableView *view, QTextStream &out); inline QString stdStrToQ(const std::string &s) diff --git a/tests/pglabtests/pglabtests.pro b/tests/pglabtests/pglabtests.pro index eeafb33..2ab510b 100644 --- a/tests/pglabtests/pglabtests.pro +++ b/tests/pglabtests/pglabtests.pro @@ -14,6 +14,7 @@ QT += core widgets HEADERS += SOURCES += main.cpp \ + tst_ConvertLangToSqlString.cpp \ tst_ExplainJsonParser.cpp \ tst_expected.cpp \ tst_SqlLexer.cpp \ diff --git a/tests/pglabtests/tst_ConvertLangToSqlString.cpp b/tests/pglabtests/tst_ConvertLangToSqlString.cpp new file mode 100644 index 0000000..1fa28af --- /dev/null +++ b/tests/pglabtests/tst_ConvertLangToSqlString.cpp @@ -0,0 +1,63 @@ +#include +#include +#include "util.h" +#include "PrintTo_Qt.h" + +using namespace testing; + + +TEST(ConvertLangToSqlString, simple) +{ + QString in(R"__( "SELECT" )__"); + QString expected(R"__(SELECT)__"); + + auto output = ConvertLangToSqlString(in); + ASSERT_EQ(output, expected); +} + +TEST(ConvertLangToSqlString, testEscapedQuote) +{ + QString in(R"__( "SELECT\"" )__"); + QString expected(R"__(SELECT")__"); + + auto output = ConvertLangToSqlString(in); + ASSERT_EQ(output, expected); +} + +TEST(ConvertLangToSqlString, testEscapedNewLine) +{ + QString in(R"__( "SELECT\nFROM" )__"); + QString expected(R"__(SELECT +FROM)__"); + + auto output = ConvertLangToSqlString(in); + ASSERT_EQ(output, expected); +} + +TEST(ConvertLangToSqlString, testConcatPlus) +{ + QString in(R"__( "SELECT" + " FROM" )__"); + QString expected(R"__(SELECT FROM)__"); + + auto output = ConvertLangToSqlString(in); + ASSERT_EQ(output, expected); +} + +TEST(ConvertLangToSqlString, testConcatDot) +{ + QString in(R"__( "SELECT"." FROM" )__"); + QString expected(R"__(SELECT FROM)__"); + + auto output = ConvertLangToSqlString(in); + ASSERT_EQ(output, expected); +} + +TEST(ConvertLangToSqlString, testSemiColon) +{ + QString in(R"__( "SELECT"." FROM"; )__"); + QString expected(R"__(SELECT FROM)__"); + + auto output = ConvertLangToSqlString(in); + ASSERT_EQ(output, expected); +} +