diff --git a/doc/src/editors/creator-editors.qdoc b/doc/src/editors/creator-editors.qdoc index efb08173b17..b1c1fedd5e4 100644 --- a/doc/src/editors/creator-editors.qdoc +++ b/doc/src/editors/creator-editors.qdoc @@ -2060,6 +2060,18 @@ \endcode \li for + \row + \li Escape String Literal as UTF-8 + \li Escapes non-ASCII characters in a string literal to hexadecimal escape sequences. + String Literals are handled as UTF-8. + \li String literal + + \row + \li Unescape String Literal as UTF-8 + \li Unescapes octal or hexadecimal escape sequences in a string literal. + String Literals are handled as UTF-8. + \li String literal + \endtable \section2 Refactoring QML Code diff --git a/src/plugins/cppeditor/cppquickfix_test.cpp b/src/plugins/cppeditor/cppquickfix_test.cpp index 88516ad276e..5b55c610f1e 100644 --- a/src/plugins/cppeditor/cppquickfix_test.cpp +++ b/src/plugins/cppeditor/cppquickfix_test.cpp @@ -1271,6 +1271,43 @@ void CppEditorPlugin::test_quickfix_data() " int m_it;\n" "};\n" ); + + // Escape String Literal as UTF-8 (no-trigger) + QTest::newRow("EscapeStringLiteral_notrigger") + << CppQuickFixFactoryPtr(new EscapeStringLiteral) + << _("const char *notrigger = \"@abcdef \\a\\n\\\\\";\n") + << _(); + + // Escape String Literal as UTF-8 + QTest::newRow("EscapeStringLiteral") + << CppQuickFixFactoryPtr(new EscapeStringLiteral) + << _("const char *utf8 = \"@\xe3\x81\x82\xe3\x81\x84\";\n") + << _("const char *utf8 = \"\\xe3\\x81\\x82\\xe3\\x81\\x84\";\n"); + + // Unescape String Literal as UTF-8 (from hexdecimal escape sequences) + QTest::newRow("UnescapeStringLiteral_hex") + << CppQuickFixFactoryPtr(new EscapeStringLiteral) + << _("const char *hex_escaped = \"@\\xe3\\x81\\x82\\xe3\\x81\\x84\";\n") + << _("const char *hex_escaped = \"\xe3\x81\x82\xe3\x81\x84\";\n"); + + // Unescape String Literal as UTF-8 (from octal escape sequences) + QTest::newRow("UnescapeStringLiteral_oct") + << CppQuickFixFactoryPtr(new EscapeStringLiteral) + << _("const char *oct_escaped = \"@\\343\\201\\202\\343\\201\\204\";\n") + << _("const char *oct_escaped = \"\xe3\x81\x82\xe3\x81\x84\";\n"); + + // Unescape String Literal as UTF-8 (triggered but no change) + QTest::newRow("UnescapeStringLiteral_noconv") + << CppQuickFixFactoryPtr(new EscapeStringLiteral) + << _("const char *escaped_ascii = \"@\\x1b\";\n") + << _("const char *escaped_ascii = \"\\x1b\";\n"); + + // Unescape String Literal as UTF-8 (no conversion because of invalid utf-8) + QTest::newRow("UnescapeStringLiteral_invalid") + << CppQuickFixFactoryPtr(new EscapeStringLiteral) + << _("const char *escaped = \"@\\xe3\\x81\";\n") + << _("const char *escaped = \"\\xe3\\x81\";\n"); + } void CppEditorPlugin::test_quickfix() diff --git a/src/plugins/cppeditor/cppquickfixes.cpp b/src/plugins/cppeditor/cppquickfixes.cpp index fb88ef661b3..b466b66f9f1 100644 --- a/src/plugins/cppeditor/cppquickfixes.cpp +++ b/src/plugins/cppeditor/cppquickfixes.cpp @@ -63,6 +63,7 @@ #include #include #include +#include #include @@ -118,6 +119,8 @@ void CppEditor::Internal::registerQuickFixes(ExtensionSystem::IPlugin *plugIn) plugIn->addAutoReleasedObject(new InsertVirtualMethods); plugIn->addAutoReleasedObject(new OptimizeForLoop); + + plugIn->addAutoReleasedObject(new EscapeStringLiteral); } // In the following anonymous namespace all functions are collected, which could be of interest for @@ -4863,3 +4866,176 @@ void OptimizeForLoop::match(const CppQuickFixInterface &interface, QuickFixOpera result.append(QuickFixOperation::Ptr(op)); } } + +namespace { + +class EscapeStringLiteralOperation: public CppQuickFixOperation +{ +public: + EscapeStringLiteralOperation(const CppQuickFixInterface &interface, + ExpressionAST *literal, bool escape) + : CppQuickFixOperation(interface) + , m_literal(literal) + , m_escape(escape) + { + if (m_escape) { + setDescription(QApplication::translate("CppTools::QuickFix", + "Escape String Literal as UTF-8")); + } else { + setDescription(QApplication::translate("CppTools::QuickFix", + "Unescape String Literal as UTF-8")); + } + } + +private: + static inline bool isDigit(quint8 ch, int base) + { + if (base == 8) + return ch >= '0' && ch < '8'; + if (base == 16) + return isxdigit(ch); + return false; + } + + static QByteArray escapeString(const QByteArray &contents) + { + QByteArray newContents; + for (int i = 0; i < contents.length(); ++i) { + quint8 c = contents.at(i); + if (isascii(c) && isprint(c)) { + newContents += c; + } else { + newContents += QByteArray("\\x") + + QByteArray::number(c, 16).rightJustified(2, '0'); + } + } + return newContents; + } + + static QByteArray unescapeString(const QByteArray &contents) + { + QByteArray newContents; + const int len = contents.length(); + for (int i = 0; i < len; ++i) { + quint8 c = contents.at(i); + if (c == '\\' && i < len - 1) { + int idx = i + 1; + quint8 ch = contents.at(idx); + int base = 0; + int maxlen = 0; + if (isDigit(ch, 8)) { + base = 8; + maxlen = 3; + } else if ((ch == 'x' || ch == 'X') && idx < len - 1) { + base = 16; + maxlen = 2; + ch = contents.at(++idx); + } + if (base > 0) { + QByteArray buf; + while (isDigit(ch, base) && idx < len && buf.length() < maxlen) { + buf += ch; + ++idx; + if (idx == len) + break; + ch = contents.at(idx); + } + if (!buf.isEmpty()) { + bool ok; + uint value = buf.toUInt(&ok, base); + // Don't unescape isascii() && !isprint() + if (ok && (!isascii(value) || isprint(value))) { + newContents += value; + i = idx - 1; + continue; + } + } + } + newContents += c; + c = contents.at(++i); + } + newContents += c; + } + return newContents; + } + + // QuickFixOperation interface +public: + void perform() + { + CppRefactoringChanges refactoring(snapshot()); + CppRefactoringFilePtr currentFile = refactoring.file(fileName()); + + const int startPos = currentFile->startOf(m_literal); + const int endPos = currentFile->endOf(m_literal); + + StringLiteralAST *stringLiteral = m_literal->asStringLiteral(); + QTC_ASSERT(stringLiteral, return); + const QByteArray oldContents(currentFile->tokenAt(stringLiteral->literal_token). + identifier->chars()); + QByteArray newContents; + if (m_escape) + newContents = escapeString(oldContents); + else + newContents = unescapeString(oldContents); + + if (oldContents != newContents) { + // Check UTF-8 byte array is correct or not. + QTextCodec *utf8codec = QTextCodec::codecForName("UTF-8"); + QScopedPointer decoder(utf8codec->makeDecoder()); + const QString str = decoder->toUnicode(newContents); + const QByteArray utf8buf = str.toUtf8(); + if (utf8codec->canEncode(str) && newContents == utf8buf) { + ChangeSet changes; + changes.replace(startPos + 1, endPos - 1, str); + currentFile->setChangeSet(changes); + currentFile->apply(); + } + } + } + +private: + ExpressionAST *m_literal; + bool m_escape; +}; + +} // anonymous namespace + +void EscapeStringLiteral::match(const CppQuickFixInterface &interface, QuickFixOperations &result) +{ + const QList &path = interface->path(); + + AST * const lastAst = path.last(); + ExpressionAST *literal = lastAst->asStringLiteral(); + if (!literal) + return; + + StringLiteralAST *stringLiteral = literal->asStringLiteral(); + CppRefactoringFilePtr file = interface->currentFile(); + const QByteArray contents(file->tokenAt(stringLiteral->literal_token).identifier->chars()); + + bool canEscape = false; + bool canUnescape = false; + for (int i = 0; i < contents.length(); ++i) { + quint8 c = contents.at(i); + if (!isascii(c) || !isprint(c)) { + canEscape = true; + } else if (c == '\\' && i < contents.length() - 1) { + c = contents.at(++i); + if ((c >= '0' && c < '8') || c == 'x' || c == 'X') + canUnescape = true; + } + } + + if (canEscape) { + QuickFixOperation::Ptr op( + new EscapeStringLiteralOperation(interface, literal, true)); + result.append(op); + } + + if (canUnescape) { + QuickFixOperation::Ptr op( + new EscapeStringLiteralOperation(interface, literal, false)); + result.append(op); + } +} diff --git a/src/plugins/cppeditor/cppquickfixes.h b/src/plugins/cppeditor/cppquickfixes.h index 01ae9229c81..c690c8e1d51 100644 --- a/src/plugins/cppeditor/cppquickfixes.h +++ b/src/plugins/cppeditor/cppquickfixes.h @@ -524,6 +524,19 @@ public: void match(const CppQuickFixInterface &interface, TextEditor::QuickFixOperations &result); }; +/*! + Escapes or unescapes a string literal as UTF-8. + + Escapes non-ASCII characters in a string literal to hexadecimal escape sequences. + Unescapes octal or hexadecimal escape sequences in a string literal. + String literals are handled as UTF-8 even if file's encoding is not UTF-8. + */ +class EscapeStringLiteral : public CppQuickFixFactory +{ +public: + void match(const CppQuickFixInterface &interface, TextEditor::QuickFixOperations &result); +}; + } // namespace Internal } // namespace CppEditor