diff --git a/src/libs/3rdparty/cplusplus/TranslationUnit.cpp b/src/libs/3rdparty/cplusplus/TranslationUnit.cpp index d5c7a5fc9b8..848f61285cb 100644 --- a/src/libs/3rdparty/cplusplus/TranslationUnit.cpp +++ b/src/libs/3rdparty/cplusplus/TranslationUnit.cpp @@ -105,6 +105,35 @@ int TranslationUnit::commentCount() const const Token &TranslationUnit::commentAt(int index) const { return _comments->at(index); } +std::vector TranslationUnit::allTokens() const +{ + std::vector all; + int tokIndex = 0; + int commentIndex = 0; + while (true) { + if (tokIndex == tokenCount()) { + for (int i = commentIndex; i < commentCount(); ++i) + all.push_back(commentAt(i)); + break; + } + if (commentIndex == commentCount()) { + for (int i = tokIndex; i < tokenCount(); ++i) + all.push_back(tokenAt(i)); + break; + } + const Token &tok = tokenAt(tokIndex); + const Token &comment = commentAt(commentIndex); + if (tok.utf16charsBegin() < comment.utf16charsBegin()) { + all.push_back(tok); + ++tokIndex; + } else { + all.push_back(comment); + ++commentIndex; + } + } + return all; +} + const Identifier *TranslationUnit::identifier(int index) const { return tokenAt(index).identifier; } @@ -381,27 +410,55 @@ int TranslationUnit::findColumnNumber(int utf16CharOffset, int lineNumber) const int TranslationUnit::getTokenPositionInDocument(int index, const QTextDocument *doc) const { - int line, column; - getTokenPosition(index, &line, &column); - return Utils::Text::positionInText(doc, line, column); + return getTokenPositionInDocument(_tokens->at(index), doc); } int TranslationUnit::getTokenEndPositionInDocument(int index, const QTextDocument *doc) const { - int line, column; - getTokenEndPosition(index, &line, &column); - return Utils::Text::positionInText(doc, line, column); + return getTokenEndPositionInDocument(_tokens->at(index), doc); } void TranslationUnit::getTokenPosition(int index, int *line, int *column, const StringLiteral **fileName) const -{ return getPosition(tokenAt(index).utf16charsBegin(), line, column, fileName); } +{ + return getTokenPosition(_tokens->at(index), line, column, fileName); +} void TranslationUnit::getTokenEndPosition(int index, int *line, int *column, const StringLiteral **fileName) const -{ return getPosition(tokenAt(index).utf16charsEnd(), line, column, fileName); } +{ + return getTokenEndPosition(_tokens->at(index), line, column, fileName); +} + +void TranslationUnit::getTokenPosition(const Token &token, int *line, int *column, + const StringLiteral **fileName) const +{ + return getPosition(token.utf16charsBegin(), line, column, fileName); +} + +void TranslationUnit::getTokenEndPosition(const Token &token, int *line, + int *column, const StringLiteral **fileName) const +{ + return getPosition(token.utf16charsEnd(), line, column, fileName); +} + +int TranslationUnit::getTokenPositionInDocument(const Token token, + const QTextDocument *doc) const +{ + int line, column; + getTokenPosition(token, &line, &column); + return Utils::Text::positionInText(doc, line, column); +} + +int TranslationUnit::getTokenEndPositionInDocument(const Token &token, + const QTextDocument *doc) const +{ + int line, column; + getTokenEndPosition(token, &line, &column); + return Utils::Text::positionInText(doc, line, column); +} void TranslationUnit::getPosition(int utf16charOffset, int *line, diff --git a/src/libs/3rdparty/cplusplus/TranslationUnit.h b/src/libs/3rdparty/cplusplus/TranslationUnit.h index 9ae01755fb5..40f79d0091d 100644 --- a/src/libs/3rdparty/cplusplus/TranslationUnit.h +++ b/src/libs/3rdparty/cplusplus/TranslationUnit.h @@ -65,6 +65,9 @@ public: int commentCount() const; const Token &commentAt(int index) const; + // Including comments. + std::vector allTokens() const; + int matchingBrace(int index) const; const Identifier *identifier(int index) const; const Literal *literal(int index) const; @@ -120,9 +123,17 @@ public: int *line, int *column = nullptr, const StringLiteral **fileName = nullptr) const; + int getTokenPositionInDocument(int index, const QTextDocument *doc) const; int getTokenEndPositionInDocument(int index, const QTextDocument *doc) const; + void getTokenPosition(const Token &token, int *line, int *column = nullptr, + const StringLiteral **fileName = nullptr) const; + void getTokenEndPosition(const Token &token, int *line, int *column = nullptr, + const StringLiteral **fileName = nullptr) const; + int getTokenPositionInDocument(const Token token, const QTextDocument *doc) const; + int getTokenEndPositionInDocument(const Token &token, const QTextDocument *doc) const; + void pushLineOffset(int offset); void pushPreprocessorLine(int utf16charOffset, int line, diff --git a/src/plugins/cppeditor/cppquickfix_test.cpp b/src/plugins/cppeditor/cppquickfix_test.cpp index 3f292769f1e..72f4f1b0833 100644 --- a/src/plugins/cppeditor/cppquickfix_test.cpp +++ b/src/plugins/cppeditor/cppquickfix_test.cpp @@ -8982,4 +8982,201 @@ void QuickfixTest::testGenerateConstructor() QuickFixOperationTest(testDocuments, &factory); } +void QuickfixTest::testChangeCommentType_data() +{ + QTest::addColumn("input"); + QTest::addColumn("expectedOutput"); + + QTest::newRow("C -> C++ / no selection / single line") << R"( +int var1; +/* Other comment, unaffected */ +/* Our @comment */ +/* Another unaffected comment */ +int var2;)" << R"( +int var1; +/* Other comment, unaffected */ +// Our comment +/* Another unaffected comment */ +int var2;)"; + + QTest::newRow("C -> C++ / no selection / multi-line / preserved header and footer") << R"( +/**************************************************** + * some info + * more @info + ***************************************************/)" << R"( +///////////////////////////////////////////////////// +// some info +// more info +/////////////////////////////////////////////////////)"; + + QTest::newRow("C -> C++ / no selection / multi-line / non-preserved header and footer") << R"( +/* + * some info + * more @info + */)" << R"( +// some info +// more info +)"; + + QTest::newRow("C -> C++ / no selection / qdoc") << R"( +/*! + \qmlproperty string Type::element.name + \qmlproperty int Type::element.id + + \brief Holds the @element name and id. +*/)" << R"( +//! \qmlproperty string Type::element.name +//! \qmlproperty @int Type::element.id +//! +//! \brief Holds the element name and id. +)"; + + QTest::newRow("C -> C++ / no selection / doxygen") << R"( +/*! \class Test + \brief A test class. + + A more detailed @class description. +*/)" << R"( +//! \class Test +//! \brief A test class. +//! +//! A more detailed class description. +)"; + + QTest::newRow("C -> C++ / selection / single line") << R"( +int var1; +/* Other comment, unaffected */ +@{start}/* Our comment */@{end} +/* Another unaffected comment */ +int var2;)" << R"( +int var1; +/* Other comment, unaffected */ +// Our comment +/* Another unaffected comment */ +int var2;)"; + + QTest::newRow("C -> C++ / selection / multi-line / preserved header and footer") << R"( +/**************************************************** + * @{start}some info + * more info@{end} + ***************************************************/)" << R"( +///////////////////////////////////////////////////// +// some info +// more info +/////////////////////////////////////////////////////)"; + + QTest::newRow("C -> C++ / selection / multi-line / non-preserved header and footer") << R"( +/*@{start} + * some in@{end}fo + * more info + */)" << R"( +// some info +// more info +)"; + + QTest::newRow("C -> C++ / selection / qdoc") << R"( +/*!@{start} + \qmlproperty string Type::element.name + \qmlproperty int Type::element.id + + \brief Holds the element name and id. +*/@{end})" << R"( +//! \qmlproperty string Type::element.name +//! \qmlproperty int Type::element.id +//! +//! \brief Holds the element name and id. +)"; + + QTest::newRow("C -> C++ / selection / doxygen") << R"( +/** Expand envi@{start}ronment variables in a string. + * + * Environment variables are accepted in the @{end}following forms: + * $SOMEVAR, ${SOMEVAR} on Unix and %SOMEVAR% on Windows. + * No escapes and quoting are supported. + * If a variable is not found, it is not substituted. + */)" << R"( +//! Expand environment variables in a string. +//! +//! Environment variables are accepted in the following forms: +//! $SOMEVAR, ${SOMEVAR} on Unix and %SOMEVAR% on Windows. +//! No escapes and quoting are supported. +//! If a variable is not found, it is not substituted. +)"; + + QTest::newRow("C -> C++ / selection / multiple comments") << R"( +@{start}/* Affected comment */ +/* Another affected comment */ +/* A third affected comment */@{end} +/* An unaffected comment */)" << R"( +// Affected comment +// Another affected comment +// A third affected comment +/* An unaffected comment */)"; + + QTest::newRow("C++ -> C / no selection / single line") << R"( +// Other comment, unaffected +// Our @comment +// Another unaffected comment)" << R"( +// Other comment, unaffected +/* Our comment */ +// Another unaffected comment)"; + + QTest::newRow("C++ -> C / selection / single line") << R"( +// Other comment, unaffected +@{start}// Our comment@{end} +// Another unaffected comment)" << R"( +// Other comment, unaffected +/* Our comment */ +// Another unaffected comment)"; + + QTest::newRow("C++ -> C / selection / multi-line / preserved header and footer") << R"( +@{start}///////////////////////////////////////////////////// +// some info +// more info +/////////////////////////////////////////////////////@{end})" << R"( +/****************************************************/ +/* some info */ +/* more info */ +/****************************************************/)"; + + QTest::newRow("C++ -> C / selection / qdoc") << R"( +@{start}//! \qmlproperty string Type::element.name +//! \qmlproperty int Type::element.id +//! +//! \brief Holds the element name and id.@{end} +)" << R"( +/*! + \qmlproperty string Type::element.name + \qmlproperty int Type::element.id + + \brief Holds the element name and id. +*/ +)"; + + QTest::newRow("C++ -> C / selection / doxygen") << R"( +@{start}//! \class Test +//! \brief A test class. +//! +//! A more detailed class description.@{end} +)" << R"( +/*! + \class Test + \brief A test class. + + A more detailed class description. +*/ +)"; +} + +void QuickfixTest::testChangeCommentType() +{ + QFETCH(QString, input); + QFETCH(QString, expectedOutput); + + ConvertCommentStyle factory; + QuickFixOperationTest( + {CppTestDocument::create("file.h", input.toUtf8(), expectedOutput.toUtf8())}, + &factory); +} + } // namespace CppEditor::Internal::Tests diff --git a/src/plugins/cppeditor/cppquickfix_test.h b/src/plugins/cppeditor/cppquickfix_test.h index 53926630cab..ef1af4440b3 100644 --- a/src/plugins/cppeditor/cppquickfix_test.h +++ b/src/plugins/cppeditor/cppquickfix_test.h @@ -219,6 +219,9 @@ private slots: void testGenerateConstructor_data(); void testGenerateConstructor(); + + void testChangeCommentType_data(); + void testChangeCommentType(); }; } // namespace Tests diff --git a/src/plugins/cppeditor/cppquickfixes.cpp b/src/plugins/cppeditor/cppquickfixes.cpp index 43c96cf383b..727f0c84970 100644 --- a/src/plugins/cppeditor/cppquickfixes.cpp +++ b/src/plugins/cppeditor/cppquickfixes.cpp @@ -9306,6 +9306,231 @@ void GenerateConstructor::match(const CppQuickFixInterface &interface, QuickFixO result << op; } +namespace { +class ConvertCommentStyleOp : public CppQuickFixOperation +{ +public: + ConvertCommentStyleOp(const CppQuickFixInterface &interface, const QList &tokens, + Kind kind) + : CppQuickFixOperation(interface), + m_tokens(tokens), + m_kind(kind), + m_wasCxxStyle(m_kind == T_CPP_COMMENT || m_kind == T_CPP_DOXY_COMMENT), + m_isDoxygen(m_kind == T_DOXY_COMMENT || m_kind == T_CPP_DOXY_COMMENT) + { + setDescription(m_wasCxxStyle ? Tr::tr("Convert comment to C style") + : Tr::tr("Convert comment to C++ style")); + } + +private: + // Turns every line of a C-style comment into a C++-style comment and vice versa. + // For C++ -> C, we use one /* */ comment block per line. However, doxygen + // requires a single comment, so there we just replace the prefix with whitespace and + // add the start and end comment in extra lines. + // For cosmetic reasons, we offer some convenience functionality: + // - Turn /***** ... into ////// ... and vice versa + // - With C -> C++, remove leading asterisks. + // - With C -> C++, remove the first and last line of a block if they have no content + // other than the comment start and end characters. + // - With C++ -> C, try to align the end comment characters. + // These are obviously heuristics; we do not guarantee perfect results for everybody. + // We also don't second-guess the users's selection: E.g. if there is an empty + // line between the tokens, then it's not the same doxygen comment, but we merge + // it anyway in C++ to C mode. + void perform() override + { + TranslationUnit * const tu = currentFile()->cppDocument()->translationUnit(); + const QString newCommentStart = getNewCommentStart(); + ChangeSet changeSet; + int endCommentColumn = -1; + const QChar oldFillChar = m_wasCxxStyle ? '/' : '*'; + const QChar newFillChar = m_wasCxxStyle ? '*' : '/'; + + for (const Token &token : m_tokens) { + const int startPos = tu->getTokenPositionInDocument(token, textDocument()); + const int endPos = tu->getTokenEndPositionInDocument(token, textDocument()); + + if (m_wasCxxStyle && m_isDoxygen) { + // Replace "///" characters with whitespace (to keep alignment). + // The insertion of "/*" and "*/" is done once after the loop. + changeSet.replace(startPos, startPos + 3, " "); + continue; + } + + const QTextBlock firstBlock = textDocument()->findBlock(startPos); + const QTextBlock lastBlock = textDocument()->findBlock(endPos); + for (QTextBlock block = firstBlock; block.isValid() && block.position() <= endPos; + block = block.next()) { + const QString &blockText = block.text(); + const int firstColumn = block == firstBlock ? startPos - block.position() : 0; + const int endColumn = block == lastBlock ? endPos - block.position() + : block.length(); + + // Returns true if the current line looks like "/********/" or "//////////", + // as is often the case at the start and end of comment blocks. + const auto fillChecker = [&] { + if (m_isDoxygen) + return false; + QString textToCheck = blockText; + if (block == firstBlock) + textToCheck.remove(0, 1); + if (block == lastBlock) + textToCheck.chop(block.length() - endColumn); + return Utils::allOf(textToCheck, [oldFillChar](const QChar &c) + { return c == oldFillChar || c == ' '; + }) && textToCheck.count(oldFillChar) > 2; + }; + + // Returns the index of the first character of actual comment content, + // as opposed to visual stuff like slashes, stars or whitespace. + const auto indexOfActualContent = [&] { + const int offset = block == firstBlock ? firstColumn + newCommentStart.length() + : firstColumn; + + for (int i = offset, lastFillChar = -1; i < blockText.length(); ++i) { + if (blockText.at(i) == oldFillChar) { + lastFillChar = i; + continue; + } + if (!blockText.at(i).isSpace()) + return lastFillChar + 1; + } + return -1; + }; + + if (fillChecker()) { + const QString replacement = QString(endColumn - 1 - firstColumn, newFillChar); + changeSet.replace(block.position() + firstColumn, + block.position() + endColumn - 1, + replacement); + if (m_wasCxxStyle) { + changeSet.replace(block.position() + firstColumn, + block.position() + firstColumn + 1, "/"); + changeSet.insert(block.position() + endColumn - 1, "*"); + endCommentColumn = endColumn - 1; + } + continue; + } + + // Remove leading noise or even the entire block, if applicable. + const bool blockIsRemovable = (block == firstBlock || block == lastBlock) + && firstBlock != lastBlock; + const auto removeBlock = [&] { + changeSet.remove(block.position() + firstColumn, block.position() + endColumn); + }; + const int contentIndex = indexOfActualContent(); + if (contentIndex == -1) { + if (blockIsRemovable) { + removeBlock(); + continue; + } else if (!m_wasCxxStyle) { + changeSet.replace(block.position() + firstColumn, + block.position() + endColumn - 1, newCommentStart); + continue; + } + } else if (block == lastBlock && contentIndex == endColumn - 1) { + if (blockIsRemovable) { + removeBlock(); + break; + } + } else { + changeSet.remove(block.position() + firstColumn, + block.position() + firstColumn + contentIndex); + } + + if (block == firstBlock) { + changeSet.replace(startPos, startPos + newCommentStart.length(), + newCommentStart); + } else { + // If the line starts with enough whitespace, replace it with the + // comment start characters, so we don't move the content to the right + // unnecessarily. Otherwise, insert the comment start characters. + if (blockText.startsWith(QString(newCommentStart.size() + 1, ' '))) { + changeSet.replace(block.position(), + block.position() + newCommentStart.length(), + newCommentStart); + } else { + changeSet.insert(block.position(), newCommentStart); + } + } + + if (block == lastBlock) { + if (m_wasCxxStyle) { + // This is for proper alignment of the end comment character. + if (endCommentColumn != -1) { + const int endCommentPos = block.position() + endCommentColumn; + if (endPos < endCommentPos) + changeSet.insert(endPos, QString(endCommentPos - endPos - 1, ' ')); + } + changeSet.insert(endPos, " */"); + } else { + changeSet.remove(endPos - 2, endPos); + } + } + } + } + + if (m_wasCxxStyle && m_isDoxygen) { + const int startPos = tu->getTokenPositionInDocument(m_tokens.first(), textDocument()); + const int endPos = tu->getTokenEndPositionInDocument(m_tokens.last(), textDocument()); + changeSet.insert(startPos, "/*!\n"); + changeSet.insert(endPos, "\n*/"); + } + + changeSet.apply(textDocument()); + } + + QString getNewCommentStart() const + { + if (m_wasCxxStyle) { + if (m_isDoxygen) + return "/*!"; + return "/*"; + } + if (m_isDoxygen) + return "//!"; + return "//"; + } + + const QList m_tokens; + const Kind m_kind; + const bool m_wasCxxStyle; + const bool m_isDoxygen; +}; +} // namespace + +void ConvertCommentStyle::match(const CppQuickFixInterface &interface, + TextEditor::QuickFixOperations &result) +{ + // If there's a selection, then it must entirely consist of comment tokens. + // If there's no selection, the cursor must be on a comment. + const QList &cursorTokens = interface.currentFile()->tokensForCursor(); + if (cursorTokens.empty()) + return; + if (!cursorTokens.front().isComment()) + return; + + // All tokens must be the same kind of comment, but we make an exception for doxygen comments + // that start with "///", as these are often not intended to be doxygen. For our purposes, + // we treat them as normal comments. + const auto effectiveKind = [&interface](const Token &token) { + if (token.kind() != T_CPP_DOXY_COMMENT) + return token.kind(); + TranslationUnit * const tu = interface.currentFile()->cppDocument()->translationUnit(); + const int startPos = tu->getTokenPositionInDocument(token, interface.textDocument()); + const QString commentStart = interface.textAt(startPos, 3); + return commentStart == "///" ? T_CPP_COMMENT : T_CPP_DOXY_COMMENT; + }; + const Kind kind = effectiveKind(cursorTokens.first()); + for (int i = 1; i < cursorTokens.count(); ++i) { + if (effectiveKind(cursorTokens.at(i)) != kind) + return; + } + + // Ok, all tokens are of same(ish) comment type, offer quickfix. + result << new ConvertCommentStyleOp(interface, cursorTokens, kind); +} + void createCppQuickFixes() { new AddIncludeForUndefinedIdentifier; @@ -9362,6 +9587,7 @@ void createCppQuickFixes() new RemoveUsingNamespace; new GenerateConstructor; + new ConvertCommentStyle; } void destroyCppQuickFixes() diff --git a/src/plugins/cppeditor/cppquickfixes.h b/src/plugins/cppeditor/cppquickfixes.h index 1b19c23b60d..62e1b1f981c 100644 --- a/src/plugins/cppeditor/cppquickfixes.h +++ b/src/plugins/cppeditor/cppquickfixes.h @@ -585,5 +585,13 @@ private: bool m_test = false; }; +//! Converts C-style to C++-style comments and vice versa +class ConvertCommentStyle : public CppQuickFixFactory +{ +private: + void match(const CppQuickFixInterface &interface, + TextEditor::QuickFixOperations &result) override; +}; + } // namespace Internal } // namespace CppEditor diff --git a/src/plugins/cppeditor/cpprefactoringchanges.cpp b/src/plugins/cppeditor/cpprefactoringchanges.cpp index 9ed1fc284cf..32b6b9d88c9 100644 --- a/src/plugins/cppeditor/cpprefactoringchanges.cpp +++ b/src/plugins/cppeditor/cpprefactoringchanges.cpp @@ -16,6 +16,8 @@ #include +#include + using namespace CPlusPlus; using namespace Utils; @@ -138,6 +140,31 @@ bool CppRefactoringFile::isCursorOn(const AST *ast) const return cursorBegin >= start && cursorBegin <= end; } +QList CppRefactoringFile::tokensForCursor() const +{ + QTextCursor c = cursor(); + int pos = c.selectionStart(); + int endPos = c.selectionEnd(); + if (pos > endPos) + std::swap(pos, endPos); + + const std::vector &allTokens = m_cppDocument->translationUnit()->allTokens(); + const int firstIndex = tokenIndexForPosition(allTokens, pos, 0); + if (firstIndex == -1) + return {}; + + const int lastIndex = pos == endPos + ? firstIndex + : tokenIndexForPosition(allTokens, endPos, firstIndex); + if (lastIndex == -1) + return {}; + QTC_ASSERT(lastIndex >= firstIndex, return {}); + QList result; + for (int i = firstIndex; i <= lastIndex; ++i) + result.push_back(allTokens.at(i)); + return result; +} + ChangeSet::Range CppRefactoringFile::range(unsigned tokenIndex) const { const Token &token = tokenAt(tokenIndex); @@ -215,6 +242,29 @@ void CppRefactoringFile::fileChanged() RefactoringFile::fileChanged(); } +int CppRefactoringFile::tokenIndexForPosition(const std::vector &tokens, + int pos, int startIndex) const +{ + const TranslationUnit * const tu = m_cppDocument->translationUnit(); + + // Binary search + for (int l = startIndex, u = int(tokens.size()) - 1; l <= u; ) { + const int i = (l + u) / 2; + const int tokenPos = tu->getTokenPositionInDocument(tokens.at(i), document()); + if (pos < tokenPos) { + u = i - 1; + continue; + } + const int tokenEndPos = tu->getTokenEndPositionInDocument(tokens.at(i), document()); + if (pos > tokenEndPos) { + l = i + 1; + continue; + } + return i; + } + return -1; +} + CppRefactoringChangesData::CppRefactoringChangesData(const Snapshot &snapshot) : m_snapshot(snapshot) , m_workingCopy(CppModelManager::workingCopy()) diff --git a/src/plugins/cppeditor/cpprefactoringchanges.h b/src/plugins/cppeditor/cpprefactoringchanges.h index 5dca8a9ca51..602efaf6d04 100644 --- a/src/plugins/cppeditor/cpprefactoringchanges.h +++ b/src/plugins/cppeditor/cpprefactoringchanges.h @@ -12,6 +12,8 @@ #include +#include + namespace CppEditor { class CppRefactoringChanges; @@ -44,6 +46,8 @@ public: void startAndEndOf(unsigned index, int *start, int *end) const; + QList tokensForCursor() const; + using TextEditor::RefactoringFile::textOf; QString textOf(const CPlusPlus::AST *ast) const; @@ -55,6 +59,9 @@ protected: CppRefactoringChangesData *data() const; void fileChanged() override; + int tokenIndexForPosition(const std::vector &tokens, int pos, + int startIndex) const; + mutable CPlusPlus::Document::Ptr m_cppDocument; friend class CppRefactoringChanges; // for access to constructor