diff options
author | Christian Kandeler <christian.kandeler@qt.io> | 2023-07-10 12:58:26 +0200 |
---|---|---|
committer | Christian Kandeler <christian.kandeler@qt.io> | 2023-08-08 07:33:41 +0000 |
commit | f93836b25d57fed5888b1376e1e1b2d084fcb98d (patch) | |
tree | 764d7dfd66a13aa5cf758da24619f4b17e032788 /src/plugins/cppeditor/cppquickfixes.cpp | |
parent | d201899a0a8de506af3db9d576e10e848df3ebdf (diff) |
CppEditor: Add quickfix for converting comments from C++ to C style
... and vice versa.
Fixes: QTCREATORBUG-27501
Change-Id: I8584cc1e86718b3fe0f0ead2a3436495303ca3c8
Reviewed-by: <github-actions-qt-creator@cristianadam.eu>
Reviewed-by: David Schulz <david.schulz@qt.io>
Diffstat (limited to 'src/plugins/cppeditor/cppquickfixes.cpp')
-rw-r--r-- | src/plugins/cppeditor/cppquickfixes.cpp | 226 |
1 files changed, 226 insertions, 0 deletions
diff --git a/src/plugins/cppeditor/cppquickfixes.cpp b/src/plugins/cppeditor/cppquickfixes.cpp index 43c96cf383..727f0c8497 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<Token> &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<Token> 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<Token> &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() |