diff --git a/src/plugins/cppeditor/cppeditor.cpp b/src/plugins/cppeditor/cppeditor.cpp index a0e4e5e0ea6..b0c9389b693 100644 --- a/src/plugins/cppeditor/cppeditor.cpp +++ b/src/plugins/cppeditor/cppeditor.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,7 @@ #include #include +#include #include #include #include @@ -119,6 +121,8 @@ public: QScopedPointer m_followSymbolUnderCursor; QToolButton *m_preprocessorButton; + + CppSelectionChanger m_cppSelectionChanger; }; CppEditorWidgetPrivate::CppEditorWidgetPrivate(CppEditorWidget *q) @@ -130,6 +134,7 @@ CppEditorWidgetPrivate::CppEditorWidgetPrivate(CppEditorWidget *q) , m_declDefLinkFinder(new FunctionDeclDefLinkFinder(q)) , m_followSymbolUnderCursor(new FollowSymbolUnderCursor(q)) , m_preprocessorButton(0) + , m_cppSelectionChanger() { } @@ -201,6 +206,9 @@ void CppEditorWidget::finalizeInitialization() connect(this, &CppEditorWidget::cursorPositionChanged, [this]() { if (!d->m_localRenaming.isActive()) d->m_useSelectionsUpdater.scheduleUpdate(); + + // Notify selection expander about the changed cursor. + d->m_cppSelectionChanger.onCursorPositionChanged(textCursor()); }); // Tool bar creation @@ -328,6 +336,44 @@ void CppEditorWidget::renameUsages(const QString &replacement) } } +bool CppEditorWidget::selectBlockUp() +{ + if (!behaviorSettings().m_smartSelectionChanging) + return TextEditorWidget::selectBlockUp(); + + QTextCursor cursor = textCursor(); + d->m_cppSelectionChanger.startChangeSelection(); + const bool changed = + d->m_cppSelectionChanger.changeSelection( + CppSelectionChanger::ExpandSelection, + cursor, + d->m_lastSemanticInfo.doc); + if (changed) + setTextCursor(cursor); + d->m_cppSelectionChanger.stopChangeSelection(); + + return changed; +} + +bool CppEditorWidget::selectBlockDown() +{ + if (!behaviorSettings().m_smartSelectionChanging) + return TextEditorWidget::selectBlockDown(); + + QTextCursor cursor = textCursor(); + d->m_cppSelectionChanger.startChangeSelection(); + const bool changed = + d->m_cppSelectionChanger.changeSelection( + CppSelectionChanger::ShrinkSelection, + cursor, + d->m_lastSemanticInfo.doc); + if (changed) + setTextCursor(cursor); + d->m_cppSelectionChanger.stopChangeSelection(); + + return changed; +} + void CppEditorWidget::renameSymbolUnderCursor() { d->m_useSelectionsUpdater.abortSchedule(); diff --git a/src/plugins/cppeditor/cppeditor.h b/src/plugins/cppeditor/cppeditor.h index 62c50ccc10d..8b74d74d402 100644 --- a/src/plugins/cppeditor/cppeditor.h +++ b/src/plugins/cppeditor/cppeditor.h @@ -92,6 +92,9 @@ public slots: void renameSymbolUnderCursor(); void renameUsages(const QString &replacement = QString()); + bool selectBlockUp() override; + bool selectBlockDown() override; + protected: bool event(QEvent *e) override; void contextMenuEvent(QContextMenuEvent *) override; diff --git a/src/plugins/cpptools/cppselectionchanger.cpp b/src/plugins/cpptools/cppselectionchanger.cpp new file mode 100644 index 00000000000..cee56aa21f5 --- /dev/null +++ b/src/plugins/cpptools/cppselectionchanger.cpp @@ -0,0 +1,1113 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "cppselectionchanger.h" + +#include +#include + +#include +#include +#include +#include + +using namespace CPlusPlus; +using namespace TextEditor::Convenience; + +enum { + debug = false +}; + +namespace CppTools { + +namespace Internal { +const int kChangeSelectionNodeIndexNotSet = -1; +const int kChangeSelectionNodeIndexWholeDocoument = -2; +} + +using namespace CppTools::Internal; + +CppSelectionChanger::CppSelectionChanger(QObject *parent) + : QObject(parent) + , m_initialChangeSelectionCursor() + , m_workingCursor() + , m_doc(0) + , m_unit(0) + , m_direction(ExpandSelection) + , m_changeSelectionNodeIndex(kChangeSelectionNodeIndexNotSet) + , m_nodeCurrentStep(kChangeSelectionNodeIndexNotSet) + , m_inChangeSelection(false) +{ +} + +void CppSelectionChanger::onCursorPositionChanged(const QTextCursor &newCursor) +{ + // Reset the text cursor to be used for initial change selection behavior, only in the case + // that the cursor is not being modified by the actual change selection methods. + if (!m_inChangeSelection) { + m_initialChangeSelectionCursor = newCursor; + setNodeIndexAndStep(NodeIndexAndStepNotSet); + if (debug) + qDebug() << "Updating change selection cursor position:" << newCursor.position(); + } +} + +namespace { + +bool hasNoSelectionAndShrinking( + CppSelectionChanger::Direction direction, + const QTextCursor &cursor) +{ + if (direction == CppSelectionChanger::ShrinkSelection && !cursor.hasSelection()) { + if (debug) + qDebug() << "No selection to shrink, exiting early."; + return true; + } + return false; +} + +void ensureCursorSelectionIsNotFlipped(QTextCursor &cursor) +{ + if (cursor.hasSelection() && (cursor.anchor() > cursor.position())) + cursor = flippedCursor(cursor); + + if (debug) { + int l, c; + convertPosition(cursor.document(), cursor.position(), &l, &c); + + qDebug() << "Cursor details: " << cursor.anchor() << cursor.position() + << " l,c:" << l << ":" << c; + } +} + +bool isDocumentAvailable(const CPlusPlus::Document::Ptr doc) +{ + if (!doc) { + if (debug) + qDebug() << "Document is not available."; + return false; + } + return true; +} + +QTextCursor getWholeDocumentCursor(const QTextCursor &cursor) +{ + QTextCursor newWholeDocumentCursor(cursor); + newWholeDocumentCursor.setPosition(0, QTextCursor::MoveAnchor); + newWholeDocumentCursor.setPosition(cursor.document()->characterCount() - 1, + QTextCursor::KeepAnchor); + return newWholeDocumentCursor; +} + +bool isWholeDocumentSelectedAndExpanding( + CppSelectionChanger::Direction direction, + const QTextCursor &cursor) +{ + if (direction == CppSelectionChanger::ExpandSelection && cursor.hasSelection()) { + const QTextCursor wholeDocumentCursor = getWholeDocumentCursor(cursor); + if (wholeDocumentCursor == cursor) { + if (debug) + qDebug() << "Selection is whole document, nothing to expand, exiting early."; + return true; + } + } + return false; +} + +} // end of anonymous namespace + +int CppSelectionChanger::getTokenStartCursorPosition( + unsigned tokenIndex, + const QTextCursor &cursor) const +{ + unsigned startLine, startColumn; + m_unit->getTokenStartPosition(tokenIndex, &startLine, &startColumn); + + const QTextDocument *document = cursor.document(); + const int startPosition = + document->findBlockByNumber(static_cast(startLine) - 1).position() + + static_cast(startColumn) - 1; + + return startPosition; +} + +int CppSelectionChanger::getTokenEndCursorPosition( + unsigned tokenIndex, + const QTextCursor &cursor) const +{ + unsigned endLine, endColumn; + m_unit->getTokenEndPosition(tokenIndex, &endLine, &endColumn); + + const QTextDocument *document = cursor.document(); + const int endPosition = + document->findBlockByNumber(static_cast(endLine) - 1).position() + + static_cast(endColumn) - 1; + + return endPosition; +} + +void CppSelectionChanger::printTokenDebugInfo( + unsigned tokenIndex, + const QTextCursor &cursor, + QString prefix) const +{ + unsigned line, column; + const Token token = m_unit->tokenAt(tokenIndex); + m_unit->getTokenStartPosition(tokenIndex, &line, &column); + const int startPos = getTokenStartCursorPosition(tokenIndex, cursor); + const int endPos = getTokenEndCursorPosition(tokenIndex, cursor); + + qDebug() << qSetFieldWidth(20) << prefix << qSetFieldWidth(0) + << token.spell() << tokenIndex + << " l, c:" << line << ":" << column + << " offset: " << token.utf16chars() << startPos << endPos; +} + +bool CppSelectionChanger::shouldSkipASTNodeBasedOnPosition( + const ASTNodePositions &positions, + const QTextCursor &cursor) const +{ + bool shouldSkipNode = false; + + bool isEqual = cursor.anchor() == positions.astPosStart + && cursor.position() == positions.astPosEnd; + + // New selections should include initial selection. + bool includesInitialSelection = + m_initialChangeSelectionCursor.anchor() >= positions.astPosStart && + m_initialChangeSelectionCursor.position() <= positions.astPosEnd; + + // Prefer new selections to start with initial cursor if anchor == position. + if (!m_initialChangeSelectionCursor.hasSelection()) { + includesInitialSelection = + m_initialChangeSelectionCursor.position() < positions.astPosEnd; + } + + // When expanding: Skip if new selection is smaller than current cursor selection. + // When shrinking: Skip if new selection is bigger than current cursor selection. + bool isNewSelectionSmaller = positions.astPosStart > cursor.anchor() + || positions.astPosEnd < cursor.position(); + bool isNewSelectionBigger = positions.astPosStart < cursor.anchor() + || positions.astPosEnd > cursor.position(); + + if (m_direction == CppSelectionChanger::ExpandSelection + && (isNewSelectionSmaller || isEqual || !includesInitialSelection)) { + shouldSkipNode = true; + } else if (m_direction == CppSelectionChanger::ShrinkSelection + && (isNewSelectionBigger || isEqual || !includesInitialSelection)) { + shouldSkipNode = true; + } + + if (debug && shouldSkipNode) { + qDebug() << "isEqual:" << isEqual << "includesInitialSelection:" << includesInitialSelection + << "isNewSelectionSmaller:" << isNewSelectionSmaller << "isNewSelectionBigger:" + << isNewSelectionBigger; + } + + return shouldSkipNode; +} + +ASTNodePositions CppSelectionChanger::getASTPositions(AST *ast, const QTextCursor &cursor) const +{ + ASTNodePositions positions(ast); + + // An AST node's contents is bound by its first token start position inclusively, + // and its last token start position exclusively. + // So we are also interested in the second to last token, which is actually + // included in the bounds. + positions.firstTokenIndex = ast->firstToken(); + positions.lastTokenIndex = ast->lastToken(); + positions.secondToLastTokenIndex = positions.lastTokenIndex - 1; + + // The AST position start is the start of the first token. + positions.astPosStart = getTokenStartCursorPosition(positions.firstTokenIndex, cursor); + + // The end position depends on whether, there is only one token involved in the current AST + // node or multiple ones. + // Default we assume that there is only one token, so the end position of the AST node + // is the start of the last token. + // If there is more than one (second to last token will be different to the first token) + // use the second to last token end position as the AST node end position. + positions.astPosEnd = getTokenStartCursorPosition(positions.lastTokenIndex, cursor); + if (positions.lastTokenIndex != positions.firstTokenIndex) + positions.astPosEnd = getTokenEndCursorPosition(positions.secondToLastTokenIndex, cursor); + + if (debug) { + qDebug() << "Token positions start and end:" + << positions.astPosStart << positions.astPosEnd; + } + + return positions; +} + +void CppSelectionChanger::updateCursorSelection( + QTextCursor &cursorToModify, + ASTNodePositions positions) +{ + m_workingCursor.setPosition(positions.astPosStart, QTextCursor::MoveAnchor); + m_workingCursor.setPosition(positions.astPosEnd, QTextCursor::KeepAnchor); + cursorToModify = m_workingCursor; + + if (debug) { + printTokenDebugInfo(positions.firstTokenIndex, m_workingCursor, + QString::fromLatin1("First token:")); + printTokenDebugInfo(positions.lastTokenIndex, m_workingCursor, + QString::fromLatin1("Last token:")); + printTokenDebugInfo(positions.secondToLastTokenIndex, m_workingCursor, + QString::fromLatin1("Second to last:")); + + qDebug() << "Anchor is now: " << m_workingCursor.anchor(); + qDebug() << "Position is now: " << m_workingCursor.position(); + } +} + +int CppSelectionChanger::getFirstCurrentStepForASTNode(AST *ast) const +{ + if (m_direction == ExpandSelection) + return 1; + else + return possibleASTStepCount(ast); +} + +bool CppSelectionChanger::isLastPossibleStepForASTNode(AST *ast) const +{ + if (m_direction == ExpandSelection) + return currentASTStep() == possibleASTStepCount(ast); + else + return currentASTStep() == 1; +} + +ASTNodePositions CppSelectionChanger::getFineTunedASTPositions(AST *ast, + const QTextCursor &cursor) const +{ + ASTNodePositions positions = getASTPositions(ast, cursor); + fineTuneASTNodePositions(positions); + return positions; +} + +ASTNodePositions CppSelectionChanger::findRelevantASTPositionsFromCursor( + const QList &astPath, + const QTextCursor &cursor, + int startingFromNodeIndex) +{ + ASTNodePositions currentNodePositions; + const int size = astPath.size(); + int currentAstIndex = m_direction == ExpandSelection ? size - 1 : 0; + + // Adjust starting node index, if a valid value was passed. + if (startingFromNodeIndex != kChangeSelectionNodeIndexNotSet) + currentAstIndex = startingFromNodeIndex; + + if (currentAstIndex < size && currentAstIndex >= 0) { + AST *ast = astPath.at(currentAstIndex); + m_changeSelectionNodeIndex = currentAstIndex; + m_nodeCurrentStep = getFirstCurrentStepForASTNode(ast); + currentNodePositions = getFineTunedASTPositions(ast, cursor); + + if (debug && startingFromNodeIndex == kChangeSelectionNodeIndexNotSet) + qDebug() << "Setting AST index for the first time."; + } + + if (!currentNodePositions.ast) + setNodeIndexAndStep(NodeIndexAndStepNotSet); + + return currentNodePositions; +} + +ASTNodePositions CppSelectionChanger::findRelevantASTPositionsFromCursorWhenNodeIndexNotSet( + const QList astPath, + const QTextCursor &cursor) +{ + // Find relevant AST node from cursor, when the user expands for the first time. + return findRelevantASTPositionsFromCursor(astPath, cursor); +} + +ASTNodePositions CppSelectionChanger::findRelevantASTPositionsFromCursorWhenWholeDocumentSelected( + const QList astPath, + const QTextCursor &cursor) +{ + // Can't expand more, because whole document is selected. + if (m_direction == ExpandSelection) + return 0; + + // In case of shrink, select the next smaller selection. + return findRelevantASTPositionsFromCursor(astPath, cursor); +} + +ASTNodePositions CppSelectionChanger::findRelevantASTPositionsFromCursorFromPreviousNodeIndex( + const QList astPath, + const QTextCursor &cursor) +{ + ASTNodePositions nodePositions; + + // This is not the first expansion, use the previous node index. + nodePositions.ast = astPath.at(m_changeSelectionNodeIndex); + + // We reached the last possible step for the current AST node, so we move to the + // next / previous one depending on the direction. + if (isLastPossibleStepForASTNode(nodePositions.ast)) { + int newAstIndex = m_changeSelectionNodeIndex; + if (m_direction == ExpandSelection) + --newAstIndex; + else + ++newAstIndex; + + if (newAstIndex < 0 || newAstIndex >= astPath.count()) { + if (debug) + qDebug() << "Skipping expansion because there is no available next AST node."; + return 0; + } + + // Switch to next AST and set the first step. + nodePositions = findRelevantASTPositionsFromCursor(astPath, cursor, newAstIndex); + if (!nodePositions) + return 0; + + if (debug) + qDebug() << "Moved to next AST node."; + } else { + // There are possible steps available for current node, so move to the next / previous + // step. + if (m_direction == ExpandSelection) + ++m_nodeCurrentStep; + else + --m_nodeCurrentStep; + nodePositions = getFineTunedASTPositions(nodePositions.ast, cursor); + + if (debug) + qDebug() << "Moved to next AST step."; + } + + return nodePositions; +} + +ASTNodePositions CppSelectionChanger::findNextASTStepPositions(const QTextCursor &cursor) +{ + // Find AST node path starting from the initial change selection cursor. + // The ASTPath class, only takes into consideration the position of the cursor, but not the + // anchor. We make up for that later in the code. + QTextCursor cursorToStartFrom(m_initialChangeSelectionCursor); + + ASTPath astPathFinder(m_doc); + const QList astPath = astPathFinder(cursorToStartFrom); + +#ifdef WITH_AST_PATH_DUMP + if (debug) + ASTPath::dump(astPath); +#endif + + if (astPath.size() == 0) + return 0; + + ASTNodePositions currentNodePositions; + if (m_changeSelectionNodeIndex == kChangeSelectionNodeIndexNotSet) { + currentNodePositions = findRelevantASTPositionsFromCursorWhenNodeIndexNotSet(astPath, + cursor); + } else if (m_changeSelectionNodeIndex == kChangeSelectionNodeIndexWholeDocoument) { + currentNodePositions = findRelevantASTPositionsFromCursorWhenWholeDocumentSelected(astPath, + cursor); + } else { + currentNodePositions = findRelevantASTPositionsFromCursorFromPreviousNodeIndex(astPath, + cursor); + } + + if (debug) { + qDebug() << "m_changeSelectionNodeIndex:" << m_changeSelectionNodeIndex + << "possible step count:" << possibleASTStepCount(currentNodePositions.ast) + << "current step:" << m_nodeCurrentStep; + } + + QTC_ASSERT(m_nodeCurrentStep >= 1, return 0); + + return currentNodePositions; +} + +void CppSelectionChanger::fineTuneForStatementPositions(unsigned firstParenTokenIndex, + unsigned lastParenTokenIndex, + ASTNodePositions &positions) const +{ + Token firstParenToken = m_unit->tokenAt(firstParenTokenIndex); + Token lastParenToken = m_unit->tokenAt(lastParenTokenIndex); + if (debug) { + qDebug() << "firstParenToken:" << firstParenToken.spell(); + qDebug() << "lastParenToken:" << lastParenToken.spell(); + } + + int newPosStart = getTokenStartCursorPosition(firstParenTokenIndex, m_workingCursor); + int newPosEnd = getTokenEndCursorPosition(lastParenTokenIndex, m_workingCursor); + + bool isOutsideParen = + m_initialChangeSelectionCursor.position() <= newPosStart; + + if (currentASTStep() == 1 && !isOutsideParen) { + if (debug) + qDebug() << "Selecting Paren contents of for statement."; + positions.astPosStart = newPosStart + 1; + positions.astPosEnd = newPosEnd - 1; + } + if (currentASTStep() == 2 && !isOutsideParen) { + if (debug) + qDebug() << "Selecting Paren of for statement together with contents."; + positions.astPosStart = newPosStart; + positions.astPosEnd = newPosEnd; + } +} + +void CppSelectionChanger::fineTuneASTNodePositions(ASTNodePositions &positions) const +{ + AST *ast = positions.ast; + + if (ast->asCompoundStatement()) { + // Allow first selecting the contents of the scope, without selecting the braces, and + // afterwards select the contents together with braces. + if (currentASTStep() == 1) { + if (debug) + qDebug() << "Selecting inner contents of compound statement."; + + unsigned firstInnerTokenIndex = positions.firstTokenIndex + 1; + unsigned lastInnerTokenIndex = positions.lastTokenIndex - 2; + Token firstInnerToken = m_unit->tokenAt(firstInnerTokenIndex); + Token lastInnerToken = m_unit->tokenAt(lastInnerTokenIndex); + if (debug) { + qDebug() << "LastInnerToken:" << lastInnerToken.spell(); + qDebug() << "FirstInnerToken:" << firstInnerToken.spell(); + } + + // Check if compound statement is empty, then select just the blank space inside it. + int newPosStart, newPosEnd; + if (positions.secondToLastTokenIndex - positions.firstTokenIndex <= 1) { + // TODO: If the empty space has a new tab character, or spaces, and the document is + // not saved, the last semantic info is not updated, and the selection is not + // properly computed. Figure out how to work around this. + newPosStart = getTokenEndCursorPosition(positions.firstTokenIndex, m_workingCursor); + newPosEnd = getTokenStartCursorPosition(positions.secondToLastTokenIndex, + m_workingCursor); + if (debug) + qDebug() << "Selecting inner contents of compound statement which is empty."; + } else { + // Select the inner contents of the scope, without the braces. + newPosStart = getTokenStartCursorPosition(firstInnerTokenIndex, m_workingCursor); + newPosEnd = getTokenEndCursorPosition(lastInnerTokenIndex, m_workingCursor); + } + + if (debug) { + qDebug() << "New" << newPosStart << newPosEnd + << "Old" << m_workingCursor.anchor() << m_workingCursor.position(); + } + + positions.astPosStart = newPosStart; + positions.astPosEnd = newPosEnd; + } + // Next time, we select the braces as well. Reverse for shrinking. + // The positions already have the correct selection, so no need to set them. + } else if (CallAST *callAST = ast->asCall()) { + unsigned firstParenTokenIndex = callAST->lparen_token; + unsigned lastParenTokenIndex = callAST->rparen_token; + Token firstParenToken = m_unit->tokenAt(firstParenTokenIndex); + Token lastParenToken = m_unit->tokenAt(lastParenTokenIndex); + if (debug) { + qDebug() << "firstParenToken:" << firstParenToken.spell(); + qDebug() << "lastParenToken:" << lastParenToken.spell(); + } + + // Select the parenthesis of the call, and everything between. + int newPosStart = getTokenStartCursorPosition(firstParenTokenIndex, m_workingCursor); + int newPosEnd = getTokenEndCursorPosition(lastParenTokenIndex, m_workingCursor); + + bool isInFunctionName = + m_initialChangeSelectionCursor.position() <= newPosStart; + + // If cursor is inside the function name, select the name implicitly (because it's a + // different AST node), and then the whole call expression (so just one step). + // If cursor is inside parentheses, on first step select everything inside them, + // on second step select the everything inside parentheses including them, + // on third step select the whole call expression. + if (currentASTStep() == 1 && !isInFunctionName) { + if (debug) + qDebug() << "Selecting everything inside parentheses."; + positions.astPosStart = newPosStart + 1; + positions.astPosEnd = newPosEnd - 1; + } + if (currentASTStep() == 2 && !isInFunctionName) { + if (debug) + qDebug() << "Selecting everything inside and including " + "the parentheses of the function call."; + positions.astPosStart = newPosStart; + positions.astPosEnd = newPosEnd; + } + } else if (StringLiteralAST *stringLiteralAST = ast->asStringLiteral()) { + // Select literal without quotes on first step, and the whole literal on next step. + if (currentASTStep() == 1) { + Token firstToken = m_unit->tokenAt(stringLiteralAST->firstToken()); + bool isRawLiteral = firstToken.f.kind >= T_FIRST_RAW_STRING_LITERAL + && firstToken.f.kind <= T_RAW_UTF32_STRING_LITERAL; + if (debug && isRawLiteral) + qDebug() << "Is raw literal."; + + // Start from positions that include quotes. + int newPosStart = positions.astPosStart; + int newPosEnd = positions.astPosEnd; + + // Decrement last position to skip last quote. + --newPosEnd; + + // If raw literal also skip parenthesis. + if (isRawLiteral) + --newPosEnd; + + // Start position will be the end position minus the size of the actual contents of the + // literal. + newPosStart = newPosEnd - static_cast(firstToken.string->size()); + + // Skip raw literal parentheses. + if (isRawLiteral) + newPosStart += 2; + + positions.astPosStart = newPosStart; + positions.astPosEnd = newPosEnd; + if (debug) + qDebug() << "Selecting inner contents of string literal."; + } + } else if (NumericLiteralAST *numericLiteralAST = ast->asNumericLiteral()) { + Token firstToken = m_unit->tokenAt(numericLiteralAST->firstToken()); + // If char literal, select it without quotes on first step. + if (firstToken.isCharLiteral()) { + if (currentASTStep() == 1) { + if (debug) + qDebug() << "Selecting inner contents of char literal."; + + int newPosStart = positions.astPosStart; + int newPosEnd = positions.astPosEnd; + newPosEnd = newPosEnd - 1; + newPosStart = newPosEnd - static_cast(firstToken.literal->size()); + + positions.astPosStart = newPosStart; + positions.astPosEnd = newPosEnd; + } + } + } else if (ForStatementAST *forStatementAST = ast->asForStatement()) { + unsigned firstParenTokenIndex = forStatementAST->lparen_token; + unsigned lastParenTokenIndex = forStatementAST->rparen_token; + fineTuneForStatementPositions(firstParenTokenIndex, lastParenTokenIndex, positions); + } else if (RangeBasedForStatementAST *rangeForStatementAST = ast->asRangeBasedForStatement()) { + unsigned firstParenTokenIndex = rangeForStatementAST->lparen_token; + unsigned lastParenTokenIndex = rangeForStatementAST->rparen_token; + fineTuneForStatementPositions(firstParenTokenIndex, lastParenTokenIndex, positions); + } else if (ClassSpecifierAST *classSpecificerAST = ast->asClassSpecifier()) { + + unsigned firstBraceTokenIndex = classSpecificerAST->lbrace_token; + unsigned lastBraceTokenIndex = classSpecificerAST->rbrace_token; + unsigned classKeywordTokenIndex = classSpecificerAST->classkey_token; + + Token firstBraceToken = m_unit->tokenAt(firstBraceTokenIndex); + Token lastBraceToken = m_unit->tokenAt(lastBraceTokenIndex); + Token classKeywordToken = m_unit->tokenAt(classKeywordTokenIndex); + + if (debug) { + qDebug() << "firstBraceToken:" << firstBraceToken.spell(); + qDebug() << "lastBraceToken:" << lastBraceToken.spell(); + qDebug() << "classKeywordToken:" << classKeywordToken.spell(); + + } + + int newPosStart = getTokenStartCursorPosition(firstBraceTokenIndex, m_workingCursor); + int newPosEnd = getTokenEndCursorPosition(lastBraceTokenIndex, m_workingCursor); + + bool isOutsideBraces = + m_initialChangeSelectionCursor.position() <= newPosStart; + bool isInsideBraces = !isOutsideBraces; + + int classKeywordPosStart = getTokenStartCursorPosition(classKeywordTokenIndex, + m_workingCursor); + + int classKeywordPosEnd = getTokenEndCursorPosition(classKeywordTokenIndex, m_workingCursor); + + bool isInClassKeyword = m_initialChangeSelectionCursor.anchor() >= classKeywordPosStart && + m_initialChangeSelectionCursor.position() <= classKeywordPosEnd; + + bool isInClassName = false; + int classNamePosEnd = newPosEnd; + NameAST *nameAST = classSpecificerAST->name; + if (nameAST) { + SimpleNameAST *classNameAST = nameAST->asSimpleName(); + if (classNameAST) { + unsigned identifierTokenIndex = classNameAST->identifier_token; + Token identifierToken = m_unit->tokenAt(identifierTokenIndex); + if (debug) + qDebug() << "identifierToken:" << identifierToken.spell(); + + int classNamePosStart = getTokenStartCursorPosition(identifierTokenIndex, + m_workingCursor); + classNamePosEnd = getTokenEndCursorPosition(identifierTokenIndex, + m_workingCursor); + + isInClassName = m_initialChangeSelectionCursor.anchor() >= classNamePosStart && + m_initialChangeSelectionCursor.position() <= classNamePosEnd; + } + } + + if (currentASTStep() == 1 && isInsideBraces) { + if (debug) + qDebug() << "Selecting everything inside braces of class statement."; + positions.astPosStart = newPosStart + 1; + positions.astPosEnd = newPosEnd - 1; + } + if (currentASTStep() == 2 && isInsideBraces) { + if (debug) + qDebug() << "Selecting braces of class statement."; + positions.astPosStart = newPosStart; + positions.astPosEnd = newPosEnd; + } + if (currentASTStep() == 1 && isInClassKeyword) { + if (debug) + qDebug() << "Selecting class keyword."; + positions.astPosStart = classKeywordPosStart; + positions.astPosEnd = classKeywordPosEnd; + } + if (currentASTStep() == 2 && isInClassKeyword) { + if (debug) + qDebug() << "Selecting class keyword and name."; + positions.astPosStart = classKeywordPosStart; + positions.astPosEnd = classNamePosEnd; + } + if (currentASTStep() == 1 && isInClassName) { + if (debug) + qDebug() << "Selecting class keyword and name."; + positions.astPosStart = classKeywordPosStart; + positions.astPosEnd = classNamePosEnd; + } + } else if (NamespaceAST *namespaceAST = ast->asNamespace()) { + unsigned namespaceTokenIndex = namespaceAST->namespace_token; + unsigned identifierTokenIndex = namespaceAST->identifier_token; + Token namespaceToken = m_unit->tokenAt(namespaceTokenIndex); + Token identifierToken = m_unit->tokenAt(identifierTokenIndex); + if (debug) { + qDebug() << "namespace token:" << namespaceToken.spell(); + qDebug() << "identifier token:" << identifierToken.spell(); + } + + int namespacePosStart = getTokenStartCursorPosition(namespaceTokenIndex, m_workingCursor); + int namespacePosEnd = getTokenEndCursorPosition(namespaceTokenIndex, m_workingCursor); + + int identifierPosStart = getTokenStartCursorPosition(identifierTokenIndex, m_workingCursor); + int identifierPosEnd = getTokenEndCursorPosition(identifierTokenIndex, m_workingCursor); + + bool isInNamespaceKeyword = + m_initialChangeSelectionCursor.position() <= namespacePosEnd; + + bool isInNamespaceIdentifier = + m_initialChangeSelectionCursor.anchor() >= identifierPosStart && + m_initialChangeSelectionCursor.position() <= identifierPosEnd; + + if (currentASTStep() == 1) { + if (isInNamespaceKeyword) { + if (debug) + qDebug() << "Selecting namespace keyword."; + positions.astPosStart = namespacePosStart; + positions.astPosEnd = namespacePosEnd; + } + else if (isInNamespaceIdentifier) { + if (debug) + qDebug() << "Selecting namespace identifier."; + positions.astPosStart = identifierPosStart; + positions.astPosEnd = identifierPosEnd; + } + } + else if (currentASTStep() == 2) { + if (isInNamespaceKeyword || isInNamespaceIdentifier) { + if (debug) + qDebug() << "Selecting namespace keyword and identifier."; + positions.astPosStart = namespacePosStart; + positions.astPosEnd = identifierPosEnd; + + } + } + } else if (ExpressionListParenAST *parenAST = ast->asExpressionListParen()) { + unsigned firstParenTokenIndex = parenAST->lparen_token; + unsigned lastParenTokenIndex = parenAST->rparen_token; + Token firstParenToken = m_unit->tokenAt(firstParenTokenIndex); + Token lastParenToken = m_unit->tokenAt(lastParenTokenIndex); + if (debug) { + qDebug() << "firstParenToken:" << firstParenToken.spell(); + qDebug() << "lastParenToken:" << lastParenToken.spell(); + } + + // Select the parentheses, and everything between. + int newPosStart = getTokenStartCursorPosition(firstParenTokenIndex, m_workingCursor); + int newPosEnd = getTokenEndCursorPosition(lastParenTokenIndex, m_workingCursor); + + if (currentASTStep() == 1) { + if (debug) + qDebug() << "Selecting everything inside parentheses."; + positions.astPosStart = newPosStart + 1; + positions.astPosEnd = newPosEnd - 1; + } + if (currentASTStep() == 2) { + if (debug) + qDebug() << "Selecting everything inside including the parentheses."; + positions.astPosStart = newPosStart; + positions.astPosEnd = newPosEnd; + } + } else if (FunctionDeclaratorAST* functionDeclaratorAST = ast->asFunctionDeclarator()) { + unsigned firstParenTokenIndex = functionDeclaratorAST->lparen_token; + unsigned lastParenTokenIndex = functionDeclaratorAST->rparen_token; + Token firstParenToken = m_unit->tokenAt(firstParenTokenIndex); + Token lastParenToken = m_unit->tokenAt(lastParenTokenIndex); + if (debug) { + qDebug() << "firstParenToken:" << firstParenToken.spell(); + qDebug() << "lastParenToken:" << lastParenToken.spell(); + } + + int newPosStart = getTokenStartCursorPosition(firstParenTokenIndex, m_workingCursor); + int newPosEnd = getTokenEndCursorPosition(lastParenTokenIndex, m_workingCursor); + + if (currentASTStep() == 1) { + if (debug) + qDebug() << "Selecting everything inside and including the parentheses."; + positions.astPosStart = newPosStart; + positions.astPosEnd = newPosEnd; + } + } else if (FunctionDefinitionAST *functionDefinitionAST = ast->asFunctionDefinition()) { + if (!functionDefinitionAST->function_body) + return; + + CompoundStatementAST *compoundStatementAST = + functionDefinitionAST->function_body->asCompoundStatement(); + if (!compoundStatementAST) + return; + + if (!functionDefinitionAST->decl_specifier_list + || !functionDefinitionAST->decl_specifier_list->value) + return; + + SimpleSpecifierAST *simpleSpecifierAST = + functionDefinitionAST->decl_specifier_list->value->asSimpleSpecifier(); + if (!simpleSpecifierAST) + return; + + unsigned firstBraceTokenIndex = compoundStatementAST->lbrace_token; + unsigned specifierTokenIndex = simpleSpecifierAST->firstToken(); + Token firstBraceToken = m_unit->tokenAt(firstBraceTokenIndex); + Token specifierToken = m_unit->tokenAt(specifierTokenIndex); + if (debug) { + qDebug() << "firstBraceToken:" << firstBraceToken.spell(); + qDebug() << "specifierToken:" << specifierToken.spell(); + } + + int firstBracePosEnd = getTokenStartCursorPosition(firstBraceTokenIndex, m_workingCursor); + + bool isOutsideBraces = + m_initialChangeSelectionCursor.position() <= firstBracePosEnd; + + if (currentASTStep() == 1 && isOutsideBraces) { + int newPosStart = getTokenStartCursorPosition(specifierTokenIndex, m_workingCursor); + + if (debug) + qDebug() << "Selecting everything to the left of the function braces."; + positions.astPosStart = newPosStart; + positions.astPosEnd = firstBracePosEnd - 1; + } + } else if (DeclaratorAST *declaratorAST = ast->asDeclarator()) { + PostfixDeclaratorListAST *list = declaratorAST->postfix_declarator_list; + if (!list) + return; + + PostfixDeclaratorAST *postfixDeclarator = list->value; + if (!postfixDeclarator) + return; + + FunctionDeclaratorAST *functionDeclarator = postfixDeclarator->asFunctionDeclarator(); + if (!functionDeclarator) + return; + + SpecifierListAST *cv_list = functionDeclarator->cv_qualifier_list; + if (!cv_list) + return; + + SpecifierAST *first_cv = cv_list->value; + if (!first_cv) + return; + + unsigned firstCVTokenIndex = first_cv->firstToken(); + Token firstCVToken = m_unit->tokenAt(firstCVTokenIndex); + if (debug) { + qDebug() << "firstCVTokenIndex:" << firstCVToken.spell(); + } + + int cvPosStart = getTokenStartCursorPosition(firstCVTokenIndex, m_workingCursor); + bool isBeforeCVList = m_initialChangeSelectionCursor.position() < cvPosStart; + + if (currentASTStep() == 1 && isBeforeCVList) { + if (debug) + qDebug() << "Selecting function declarator without CV qualifiers."; + + int newPosEnd = cvPosStart; + positions.astPosEnd = newPosEnd - 1; + } + + } else if (TemplateIdAST *templateIdAST = ast->asTemplateId()) { + unsigned identifierTokenIndex = templateIdAST->identifier_token; + Token identifierToken = m_unit->tokenAt(identifierTokenIndex); + if (debug) { + qDebug() << "identifierTokenIndex:" << identifierToken.spell(); + } + + int newPosStart = getTokenStartCursorPosition(identifierTokenIndex, m_workingCursor); + int newPosEnd = getTokenEndCursorPosition(identifierTokenIndex, m_workingCursor); + + bool isInsideIdentifier = m_initialChangeSelectionCursor.anchor() >= newPosStart && + m_initialChangeSelectionCursor.position() <= newPosEnd; + + if (currentASTStep() == 1 && isInsideIdentifier) { + if (debug) + qDebug() << "Selecting just identifier before selecting template id."; + positions.astPosStart = newPosStart; + positions.astPosEnd = newPosEnd; + } + } else if (TemplateDeclarationAST *templateDeclarationAST = ast->asTemplateDeclaration()) { + unsigned templateKeywordTokenIndex = templateDeclarationAST->template_token; + unsigned greaterTokenIndex = templateDeclarationAST->greater_token; + Token templateKeywordToken = m_unit->tokenAt(templateKeywordTokenIndex); + Token greaterToken = m_unit->tokenAt(greaterTokenIndex); + if (debug) { + qDebug() << "templateKeywordTokenIndex:" << templateKeywordToken.spell(); + qDebug() << "greaterTokenIndex:" << greaterToken.spell(); + } + + int templateKeywordPosStart = getTokenStartCursorPosition(templateKeywordTokenIndex, + m_workingCursor); + int templateKeywordPosEnd = getTokenEndCursorPosition(templateKeywordTokenIndex, + m_workingCursor); + + int templateParametersPosEnd = getTokenEndCursorPosition(greaterTokenIndex, + m_workingCursor); + + bool isInsideTemplateKeyword = + m_initialChangeSelectionCursor.anchor() >= templateKeywordPosStart && + m_initialChangeSelectionCursor.position() <= templateKeywordPosEnd; + + if (currentASTStep() == 1 && isInsideTemplateKeyword) { + if (debug) + qDebug() << "Selecting template keyword."; + positions.astPosStart = templateKeywordPosStart; + positions.astPosEnd = templateKeywordPosEnd; + } + if (currentASTStep() == 2 && isInsideTemplateKeyword) { + if (debug) + qDebug() << "Selecting template keyword and parameters."; + positions.astPosStart = templateKeywordPosStart; + positions.astPosEnd = templateParametersPosEnd; + } + } else if (LambdaExpressionAST *lambdaExpressionAST = ast->asLambdaExpression()) { + // TODO: Fix more lambda cases. + LambdaIntroducerAST *lambdaIntroducerAST = lambdaExpressionAST->lambda_introducer; + LambdaDeclaratorAST *lambdaDeclaratorAST = lambdaExpressionAST->lambda_declarator; + TrailingReturnTypeAST *trailingReturnTypeAST = lambdaDeclaratorAST->trailing_return_type; + unsigned firstSquareBracketTokenIndex = lambdaIntroducerAST->lbracket_token; + unsigned lastParenTokenIndex = lambdaDeclaratorAST->rparen_token; + + Token firstSquareBracketToken = m_unit->tokenAt(firstSquareBracketTokenIndex); + Token lastParenToken = m_unit->tokenAt(lastParenTokenIndex); + if (debug) { + qDebug() << "firstSquareBracketToken:" << firstSquareBracketToken.spell(); + qDebug() << "lastParenToken:" << lastParenToken.spell(); + } + + int firstSquareBracketPosStart = getTokenStartCursorPosition(firstSquareBracketTokenIndex, + m_workingCursor); + int lastParenPosEnd = getTokenEndCursorPosition(lastParenTokenIndex, m_workingCursor); + + + bool isInsideDeclarator = + m_initialChangeSelectionCursor.anchor() >= firstSquareBracketPosStart && + m_initialChangeSelectionCursor.position() <= lastParenPosEnd; + + if (currentASTStep() == 1 && isInsideDeclarator) { + if (debug) + qDebug() << "Selecting lambda capture group and arguments."; + positions.astPosStart = firstSquareBracketPosStart; + positions.astPosEnd = lastParenPosEnd; + } + if (currentASTStep() == 2 && isInsideDeclarator && trailingReturnTypeAST) { + if (debug) + qDebug() << "Selecting lambda prototype."; + + unsigned lastReturnTypeTokenIndex = trailingReturnTypeAST->lastToken(); + Token lastReturnTypeToken = m_unit->tokenAt(lastReturnTypeTokenIndex); + if (debug) + qDebug() << "lastReturnTypeToken:" << lastReturnTypeToken.spell(); + int lastReturnTypePosEnd = getTokenEndCursorPosition(lastReturnTypeTokenIndex, + m_workingCursor); + + positions.astPosStart = firstSquareBracketPosStart; + positions.astPosEnd = lastReturnTypePosEnd - 2; + } + } +} + +bool CppSelectionChanger::performSelectionChange(QTextCursor &cursorToModify) +{ + forever { + if (ASTNodePositions positions = findNextASTStepPositions(m_workingCursor)) { + if (!shouldSkipASTNodeBasedOnPosition(positions, m_workingCursor)) { + updateCursorSelection(cursorToModify, positions); + return true; + } else { + if (debug) + qDebug() << "Skipping node."; + } + } else if (m_direction == ShrinkSelection) { + // The last possible action to do, if there was no step with a smaller selection, is + // to set the cursor to the initial change selection cursor, without an anchor. + QTextCursor finalCursor(m_initialChangeSelectionCursor); + finalCursor.setPosition(finalCursor.position(), QTextCursor::MoveAnchor); + cursorToModify = finalCursor; + setNodeIndexAndStep(NodeIndexAndStepNotSet); + if (debug) + qDebug() << "Final shrink selection case."; + return true; + } else if (m_direction == ExpandSelection) { + // The last possible action to do, if there was no step with a bigger selection, is + // to set the cursor to the whole document including header inclusions. + QTextCursor finalCursor = getWholeDocumentCursor(m_initialChangeSelectionCursor); + cursorToModify = finalCursor; + setNodeIndexAndStep(NodeIndexAndStepWholeDocument); + if (debug) + qDebug() << "Final expand selection case."; + return true; + } + // Break out of the loop, because no further modification of the selection can be done. + else break; + } + + // No next step found for given direction, return early without modifying the cursor. + return false; +} + +void CppSelectionChanger::setNodeIndexAndStep(NodeIndexAndStepState state) +{ + switch (state) { + case NodeIndexAndStepWholeDocument: + m_changeSelectionNodeIndex = kChangeSelectionNodeIndexWholeDocoument; + m_nodeCurrentStep = kChangeSelectionNodeIndexWholeDocoument; + break; + case NodeIndexAndStepNotSet: + default: + m_changeSelectionNodeIndex = kChangeSelectionNodeIndexNotSet; + m_nodeCurrentStep = kChangeSelectionNodeIndexNotSet; + break; + } +} + +bool CppSelectionChanger::changeSelection( + Direction direction, + QTextCursor &cursorToModify, + const CPlusPlus::Document::Ptr doc) +{ + m_workingCursor = cursorToModify; + + if (hasNoSelectionAndShrinking(direction, m_workingCursor)) + return false; + + if (isWholeDocumentSelectedAndExpanding(direction, m_workingCursor)) + return false; + + if (!isDocumentAvailable(doc)) { + return false; + } + + ensureCursorSelectionIsNotFlipped(m_workingCursor); + + m_doc = doc; + m_unit = m_doc->translationUnit(); + m_direction = direction; + + return performSelectionChange(cursorToModify); +} + +void CppSelectionChanger::startChangeSelection() +{ + // Stop cursorPositionChanged signal handler from setting the initial + // change selection cursor, when the cursor is being changed as a result of the change + // selection operation. + m_inChangeSelection = true; +} + +void CppSelectionChanger::stopChangeSelection() +{ + m_inChangeSelection = false; +} + +int CppSelectionChanger::possibleASTStepCount(CPlusPlus::AST *ast) const +{ + // Different AST nodes, have a different number of steps though which they can go. + // For example in a string literal, we first want to select the literal contents on the first + // step, and then the quotes + the literal content in the second step. + if (!ast) + return 1; + if (ast->asCompoundStatement()) + return 2; + if (ast->asCall()) + return 3; + if (ast->asStringLiteral()) + return 2; + if (NumericLiteralAST* numericLiteralAST = ast->asNumericLiteral()) { + Token firstToken = m_unit->tokenAt(numericLiteralAST->firstToken()); + if (firstToken.isCharLiteral()) + return 2; + return 1; + } + if (ast->asForStatement()) + return 3; + if (ast->asRangeBasedForStatement()) + return 3; + if (ast->asClassSpecifier()) + return 3; + if (ast->asNamespace()) + return 3; + if (ast->asExpressionListParen()) + return 2; + if (ast->asFunctionDeclarator()) + return 1; + if (ast->asFunctionDefinition()) + return 2; + if (ast->asTemplateId()) + return 2; + if (ast->asDeclarator()) + return 2; + if (ast->asTemplateDeclaration()) + return 3; + if (ast->asLambdaExpression()) + return 3; + + return 1; +} + +int CppSelectionChanger::currentASTStep() const +{ + return m_nodeCurrentStep; +} + +} // namespace CppTools diff --git a/src/plugins/cpptools/cppselectionchanger.h b/src/plugins/cpptools/cppselectionchanger.h new file mode 100644 index 00000000000..8e64fbdee4e --- /dev/null +++ b/src/plugins/cpptools/cppselectionchanger.h @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#ifndef CPPSELECTIONCHANGER_H +#define CPPSELECTIONCHANGER_H + +#include "cpptools_global.h" + +#include +#include +#include + +#include +#include + +namespace CppTools { + +class ASTNodePositions { +public: + ASTNodePositions() {} + ASTNodePositions(CPlusPlus::AST *_ast) : ast(_ast) {} + operator bool() const { return ast; } + + CPlusPlus::AST *ast = 0; + unsigned firstTokenIndex = 0; + unsigned lastTokenIndex = 0; + unsigned secondToLastTokenIndex = 0; + int astPosStart = -1; + int astPosEnd = -1; +}; + +class CPPTOOLS_EXPORT CppSelectionChanger : public QObject +{ + Q_OBJECT +public: + explicit CppSelectionChanger(QObject *parent = 0); + + enum Direction { + ExpandSelection, + ShrinkSelection + }; + + enum NodeIndexAndStepState { + NodeIndexAndStepNotSet, + NodeIndexAndStepWholeDocument, + }; + + bool changeSelection(Direction direction, + QTextCursor &cursorToModify, + const CPlusPlus::Document::Ptr doc); + void startChangeSelection(); + void stopChangeSelection(); + +public slots: + void onCursorPositionChanged(const QTextCursor &newCursor); + +protected slots: + void fineTuneForStatementPositions(unsigned firstParensTokenIndex, + unsigned lastParensTokenIndex, + ASTNodePositions &positions) const; + +private: + bool performSelectionChange(QTextCursor &cursorToModify); + ASTNodePositions getASTPositions(CPlusPlus::AST *ast, const QTextCursor &cursor) const; + void updateCursorSelection(QTextCursor &cursorToModify, ASTNodePositions positions); + + int possibleASTStepCount(CPlusPlus::AST *ast) const; + int currentASTStep() const; + ASTNodePositions findNextASTStepPositions(const QTextCursor &cursor); + + void fineTuneASTNodePositions(ASTNodePositions &positions) const; + ASTNodePositions getFineTunedASTPositions(CPlusPlus::AST *ast, const QTextCursor &cursor) const; + int getFirstCurrentStepForASTNode(CPlusPlus::AST *ast) const; + bool isLastPossibleStepForASTNode(CPlusPlus::AST *ast) const; + ASTNodePositions findRelevantASTPositionsFromCursor(const QList &astPath, + const QTextCursor &cursor, + int startingFromNodeIndex = -1); + ASTNodePositions findRelevantASTPositionsFromCursorWhenNodeIndexNotSet( + const QList astPath, + const QTextCursor &cursor); + ASTNodePositions findRelevantASTPositionsFromCursorWhenWholeDocumentSelected( + const QList astPath, + const QTextCursor &cursor); + ASTNodePositions findRelevantASTPositionsFromCursorFromPreviousNodeIndex( + const QList astPath, + const QTextCursor &cursor); + bool shouldSkipASTNodeBasedOnPosition(const ASTNodePositions &positions, + const QTextCursor &cursor) const; + void setNodeIndexAndStep(NodeIndexAndStepState state); + int getTokenStartCursorPosition(unsigned tokenIndex, const QTextCursor &cursor) const; + int getTokenEndCursorPosition(unsigned tokenIndex, const QTextCursor &cursor) const; + void printTokenDebugInfo(unsigned tokenIndex, const QTextCursor &cursor, QString prefix) const; + + QTextCursor m_initialChangeSelectionCursor; + QTextCursor m_workingCursor; + CPlusPlus::Document::Ptr m_doc; + CPlusPlus::TranslationUnit *m_unit; + Direction m_direction; + int m_changeSelectionNodeIndex; + int m_nodeCurrentStep; + bool m_inChangeSelection; +}; + +} // namespace CppTools + +#endif // CPPSELECTIONCHANGER_H diff --git a/src/plugins/cpptools/cpptools.pro b/src/plugins/cpptools/cpptools.pro index d31bfe85188..67e8d165713 100644 --- a/src/plugins/cpptools/cpptools.pro +++ b/src/plugins/cpptools/cpptools.pro @@ -43,6 +43,7 @@ HEADERS += \ cppprojectfile.h \ cppqtstyleindenter.h \ cpprefactoringchanges.h \ + cppselectionchanger.h \ cppsemanticinfo.h \ cppsemanticinfoupdater.h \ cppsourceprocessor.h \ @@ -117,6 +118,7 @@ SOURCES += \ cppprojectfile.cpp \ cppqtstyleindenter.cpp \ cpprefactoringchanges.cpp \ + cppselectionchanger.cpp \ cppsemanticinfo.cpp \ cppsemanticinfoupdater.cpp \ cppsourceprocessor.cpp \ diff --git a/src/plugins/cpptools/cpptools.qbs b/src/plugins/cpptools/cpptools.qbs index 4974b01215e..f424237957e 100644 --- a/src/plugins/cpptools/cpptools.qbs +++ b/src/plugins/cpptools/cpptools.qbs @@ -66,6 +66,7 @@ QtcPlugin { "cppprojectfile.cpp", "cppprojectfile.h", "cppqtstyleindenter.cpp", "cppqtstyleindenter.h", "cpprefactoringchanges.cpp", "cpprefactoringchanges.h", + "cppselectionchanger.cpp", "cppselectionchanger.h", "cppsemanticinfo.cpp", "cppsemanticinfo.h", "cppsemanticinfoupdater.cpp", "cppsemanticinfoupdater.h", "cppsourceprocessor.cpp", "cppsourceprocessor.h", diff --git a/src/plugins/texteditor/behaviorsettings.cpp b/src/plugins/texteditor/behaviorsettings.cpp index 1a217cd76a7..f969c1b588b 100644 --- a/src/plugins/texteditor/behaviorsettings.cpp +++ b/src/plugins/texteditor/behaviorsettings.cpp @@ -37,6 +37,7 @@ static const char constrainTooltips[] = "ConstrainTooltips"; static const char camelCaseNavigationKey[] = "CamelCaseNavigation"; static const char keyboardTooltips[] = "KeyboardTooltips"; static const char groupPostfix[] = "BehaviorSettings"; +static const char smartSelectionChanging[] = "SmartSelectionChanging"; namespace TextEditor { @@ -46,7 +47,8 @@ BehaviorSettings::BehaviorSettings() : m_scrollWheelZooming(true), m_constrainHoverTooltips(false), m_camelCaseNavigation(true), - m_keyboardTooltips(false) + m_keyboardTooltips(false), + m_smartSelectionChanging(true) { } @@ -69,6 +71,7 @@ void BehaviorSettings::toMap(const QString &prefix, QVariantMap *map) const map->insert(prefix + QLatin1String(constrainTooltips), m_constrainHoverTooltips); map->insert(prefix + QLatin1String(camelCaseNavigationKey), m_camelCaseNavigation); map->insert(prefix + QLatin1String(keyboardTooltips), m_keyboardTooltips); + map->insert(prefix + QLatin1String(smartSelectionChanging), m_smartSelectionChanging); } void BehaviorSettings::fromMap(const QString &prefix, const QVariantMap &map) @@ -85,6 +88,9 @@ void BehaviorSettings::fromMap(const QString &prefix, const QVariantMap &map) map.value(prefix + QLatin1String(camelCaseNavigationKey), m_camelCaseNavigation).toBool(); m_keyboardTooltips = map.value(prefix + QLatin1String(keyboardTooltips), m_keyboardTooltips).toBool(); + m_smartSelectionChanging = + map.value(prefix + QLatin1String(smartSelectionChanging), m_smartSelectionChanging) + .toBool(); } bool BehaviorSettings::equals(const BehaviorSettings &ds) const @@ -95,6 +101,7 @@ bool BehaviorSettings::equals(const BehaviorSettings &ds) const && m_constrainHoverTooltips == ds.m_constrainHoverTooltips && m_camelCaseNavigation == ds.m_camelCaseNavigation && m_keyboardTooltips == ds.m_keyboardTooltips + && m_smartSelectionChanging == ds.m_smartSelectionChanging ; } diff --git a/src/plugins/texteditor/behaviorsettings.h b/src/plugins/texteditor/behaviorsettings.h index f88a3064bba..f45fd4a9c07 100644 --- a/src/plugins/texteditor/behaviorsettings.h +++ b/src/plugins/texteditor/behaviorsettings.h @@ -59,6 +59,7 @@ public: bool m_constrainHoverTooltips; bool m_camelCaseNavigation; bool m_keyboardTooltips; + bool m_smartSelectionChanging; }; inline bool operator==(const BehaviorSettings &t1, const BehaviorSettings &t2) { return t1.equals(t2); } diff --git a/src/plugins/texteditor/behaviorsettingswidget.cpp b/src/plugins/texteditor/behaviorsettingswidget.cpp index 187ba904066..f3410915e45 100644 --- a/src/plugins/texteditor/behaviorsettingswidget.cpp +++ b/src/plugins/texteditor/behaviorsettingswidget.cpp @@ -111,6 +111,8 @@ BehaviorSettingsWidget::BehaviorSettingsWidget(QWidget *parent) this, &BehaviorSettingsWidget::slotBehaviorSettingsChanged); connect(d->m_ui.keyboardTooltips, &QAbstractButton::clicked, this, &BehaviorSettingsWidget::slotBehaviorSettingsChanged); + connect(d->m_ui.smartSelectionChanging, &QAbstractButton::clicked, + this, &BehaviorSettingsWidget::slotBehaviorSettingsChanged); } BehaviorSettingsWidget::~BehaviorSettingsWidget() @@ -198,6 +200,7 @@ void BehaviorSettingsWidget::setAssignedBehaviorSettings(const BehaviorSettings d->m_ui.constrainTooltipsBox->setCurrentIndex(behaviorSettings.m_constrainHoverTooltips ? 1 : 0); d->m_ui.camelCaseNavigation->setChecked(behaviorSettings.m_camelCaseNavigation); d->m_ui.keyboardTooltips->setChecked(behaviorSettings.m_keyboardTooltips); + d->m_ui.smartSelectionChanging->setChecked(behaviorSettings.m_smartSelectionChanging); updateConstrainTooltipsBoxTooltip(); } @@ -209,6 +212,7 @@ void BehaviorSettingsWidget::assignedBehaviorSettings(BehaviorSettings *behavior behaviorSettings->m_constrainHoverTooltips = (d->m_ui.constrainTooltipsBox->currentIndex() == 1); behaviorSettings->m_camelCaseNavigation = d->m_ui.camelCaseNavigation->isChecked(); behaviorSettings->m_keyboardTooltips = d->m_ui.keyboardTooltips->isChecked(); + behaviorSettings->m_smartSelectionChanging = d->m_ui.smartSelectionChanging->isChecked(); } void BehaviorSettingsWidget::setAssignedExtraEncodingSettings( diff --git a/src/plugins/texteditor/behaviorsettingswidget.ui b/src/plugins/texteditor/behaviorsettingswidget.ui index 05cee857946..3668cfd2d7a 100644 --- a/src/plugins/texteditor/behaviorsettingswidget.ui +++ b/src/plugins/texteditor/behaviorsettingswidget.ui @@ -357,6 +357,16 @@ Specifies how backspace interacts with indentation. + + + + Using Select Block Up / Down actions will now provide smarter selections. + + + Enable smart selection changing + + + diff --git a/src/plugins/texteditor/convenience.cpp b/src/plugins/texteditor/convenience.cpp index 4509c2fa338..9d969d72ca7 100644 --- a/src/plugins/texteditor/convenience.cpp +++ b/src/plugins/texteditor/convenience.cpp @@ -61,5 +61,13 @@ QString textAt(QTextCursor tc, int pos, int length) return tc.selectedText().replace(QChar::ParagraphSeparator, QLatin1Char('\n')); } +QTextCursor flippedCursor(const QTextCursor &cursor) +{ + QTextCursor flipped = cursor; + flipped.clearSelection(); + flipped.setPosition(cursor.anchor(), QTextCursor::KeepAnchor); + return flipped; +} + } // Util } // TextEditor diff --git a/src/plugins/texteditor/convenience.h b/src/plugins/texteditor/convenience.h index 4ef8d4d58c8..49a4f581300 100644 --- a/src/plugins/texteditor/convenience.h +++ b/src/plugins/texteditor/convenience.h @@ -44,6 +44,8 @@ TEXTEDITOR_EXPORT bool convertPosition(const QTextDocument *document, TEXTEDITOR_EXPORT QString textAt(QTextCursor tc, int pos, int length); +TEXTEDITOR_EXPORT QTextCursor flippedCursor(const QTextCursor &cursor); + } // Util } // TextEditor diff --git a/src/plugins/texteditor/texteditor.cpp b/src/plugins/texteditor/texteditor.cpp index a065d191ad9..57b89fb5d8d 100644 --- a/src/plugins/texteditor/texteditor.cpp +++ b/src/plugins/texteditor/texteditor.cpp @@ -1300,14 +1300,6 @@ void TextEditorWidget::gotoNextWordCamelCaseWithSelection() setTextCursor(c); } -static QTextCursor flippedCursor(const QTextCursor &cursor) -{ - QTextCursor flipped = cursor; - flipped.clearSelection(); - flipped.setPosition(cursor.anchor(), QTextCursor::KeepAnchor); - return flipped; -} - bool TextEditorWidget::selectBlockUp() { QTextCursor cursor = textCursor(); @@ -1321,7 +1313,7 @@ bool TextEditorWidget::selectBlockUp() if (!TextBlockUserData::findNextClosingParenthesis(&cursor, true)) return false; - setTextCursor(flippedCursor(cursor)); + setTextCursor(Convenience::flippedCursor(cursor)); d->_q_matchParentheses(); return true; } @@ -1346,7 +1338,7 @@ bool TextEditorWidget::selectBlockDown() if ( cursor != d->m_selectBlockAnchor) TextBlockUserData::findNextClosingParenthesis(&cursor, true); - setTextCursor(flippedCursor(cursor)); + setTextCursor(Convenience::flippedCursor(cursor)); d->_q_matchParentheses(); return true; } @@ -5346,6 +5338,11 @@ const MarginSettings &TextEditorWidget::marginSettings() const return d->m_marginSettings; } +const BehaviorSettings &TextEditorWidget::behaviorSettings() const +{ + return d->m_behaviorSettings; +} + void TextEditorWidgetPrivate::handleHomeKey(bool anchor) { QTextCursor cursor = q->textCursor(); diff --git a/src/plugins/texteditor/texteditor.h b/src/plugins/texteditor/texteditor.h index eaf999d6f16..d29c3250147 100644 --- a/src/plugins/texteditor/texteditor.h +++ b/src/plugins/texteditor/texteditor.h @@ -294,6 +294,7 @@ public: const DisplaySettings &displaySettings() const; const MarginSettings &marginSettings() const; + const BehaviorSettings &behaviorSettings() const; void ensureCursorVisible(); @@ -389,8 +390,8 @@ public: void gotoNextWordCamelCase(); void gotoNextWordCamelCaseWithSelection(); - bool selectBlockUp(); - bool selectBlockDown(); + virtual bool selectBlockUp(); + virtual bool selectBlockDown(); void moveLineUp(); void moveLineDown(); @@ -498,6 +499,7 @@ protected: void showDefaultContextMenu(QContextMenuEvent *e, Core::Id menuContextId); virtual void finalizeInitialization() {} virtual void finalizeInitializationAfterDuplication(TextEditorWidget *) {} + static QTextCursor flippedCursor(const QTextCursor &cursor); public: struct Link diff --git a/src/plugins/texteditor/texteditoractionhandler.cpp b/src/plugins/texteditor/texteditoractionhandler.cpp index 71d51550a68..45815477bb5 100644 --- a/src/plugins/texteditor/texteditoractionhandler.cpp +++ b/src/plugins/texteditor/texteditoractionhandler.cpp @@ -449,7 +449,7 @@ void TextEditorActionHandlerPrivate::createActions() G_EDIT_BLOCKS, advancedEditMenu); m_selectBlockDownAction = registerAction(SELECT_BLOCK_DOWN, [this] (TextEditorWidget *w) { w->selectBlockDown(); }, true, tr("Select Block Down"), - QKeySequence(), + QKeySequence(tr("Ctrl+Shift+Alt+U")), G_EDIT_BLOCKS, advancedEditMenu); // register GOTO Actions diff --git a/tests/auto/cplusplus/cplusplus.pro b/tests/auto/cplusplus/cplusplus.pro index edb96501f35..239d179ecb3 100644 --- a/tests/auto/cplusplus/cplusplus.pro +++ b/tests/auto/cplusplus/cplusplus.pro @@ -11,6 +11,7 @@ SUBDIRS = \ typeprettyprinter \ misc \ c99 \ + cppselectionchanger\ cxx11 \ checksymbols \ lexer \ diff --git a/tests/auto/cplusplus/cplusplus.qbs b/tests/auto/cplusplus/cplusplus.qbs index de1907d0c45..e50b6881ef5 100644 --- a/tests/auto/cplusplus/cplusplus.qbs +++ b/tests/auto/cplusplus/cplusplus.qbs @@ -7,6 +7,7 @@ Project { "c99/c99.qbs", "checksymbols/checksymbols.qbs", "codeformatter/codeformatter.qbs", + "cppselectionchanger/cppselectionchanger.qbs", "cxx11/cxx11.qbs", "fileiterationorder/fileiterationorder.qbs", "findusages/findusages.qbs", diff --git a/tests/auto/cplusplus/cppselectionchanger/cppselectionchanger.pro b/tests/auto/cplusplus/cppselectionchanger/cppselectionchanger.pro new file mode 100644 index 00000000000..ab4b96d90e2 --- /dev/null +++ b/tests/auto/cplusplus/cppselectionchanger/cppselectionchanger.pro @@ -0,0 +1,9 @@ +include(../shared/shared.pri) + +# Inject the source dir for referencing test data from shadow builds. +DEFINES += SRCDIR=\\\"$$PWD\\\" + +SOURCES += tst_cppselectionchangertest.cpp + +DISTFILES += testCppFile.cpp \ + cppselectionchanger.qbs diff --git a/tests/auto/cplusplus/cppselectionchanger/cppselectionchanger.qbs b/tests/auto/cplusplus/cppselectionchanger/cppselectionchanger.qbs new file mode 100644 index 00000000000..07d112f8c7d --- /dev/null +++ b/tests/auto/cplusplus/cppselectionchanger/cppselectionchanger.qbs @@ -0,0 +1,21 @@ +import qbs +import "../cplusplusautotest.qbs" as CPlusPlusAutotest + +CPlusPlusAutotest { + name: "CPlusPlus selection changer autotest" + + Group { + name: "Source Files" + files: "tst_cppselectionchangertest.cpp" + } + + Group { + name: "Data Files" + fileTags: ["data"] + files: [ + "testCppFile.cpp", + ] + } + + cpp.defines: base.concat(['SRCDIR="' + path + '"']) +} diff --git a/tests/auto/cplusplus/cppselectionchanger/testCppFile.cpp b/tests/auto/cplusplus/cppselectionchanger/testCppFile.cpp new file mode 100644 index 00000000000..fad74cc6332 --- /dev/null +++ b/tests/auto/cplusplus/cppselectionchanger/testCppFile.cpp @@ -0,0 +1,124 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include +#include +#include +#include + +struct TestClass { + int a; + char b; + char x; + char y; + char z; + std::string c; + double d; + std::map e; +}; + +int add(int a, int b) { + return a + b; +} + +template +int subtract(T a, T b) { + return a - b; +} + +namespace CustomNamespace { + extern int insideNamespace; + int foo() { + insideNamespace = 2; + return insideNamespace; + } +} + +class CustomClass { + bool customClassMethod(const int ¶meter) const volatile; +}; + +class SecondCustomClass { +public: + SecondCustomClass(int argc, char *argv[]); + void secondCustomClassFunction(); +}; + +bool CustomClass::customClassMethod(const int ¶meter) const volatile { + int secondParameter = parameter; + ++secondParameter; + return secondParameter; +} + +int main(int argc, char *argv[]) +{ + std::map a; + + SecondCustomClass secondCustomClass(argc, argv); + secondCustomClass.secondCustomClassFunction(); + + TestClass bla; + bla.a = 1; + bla.b = 65; + bla.c = "Hello"; + bla.d = 3.14f; + bla.e[3] = 3; + + a[3] = bla; + + std::vector v; + v.push_back(1); + v.push_back(2); + + if (5 == 5) { + std::cout << "Hello" << 'c' << 54545 << u8"utf8string" << U"unicodeString"; + for (int i = 0; i < 3; ++i) { + std::cout << i; + } + + for (auto val :v) { + std::cout << val; + } + } + + std::cout << "\n After exec"; + std::cout << argc << argv; + + std::cout << add(1, add(2, add(10, 20))); + + auto res = std::find(v.begin(), v.end(), 1); + if (res != v.end()) + std::cout << *res; + std::cout << static_cast(3); + + auto aLambda = [=, &a](int lambdaArgument) -> int { + return lambdaArgument + 1; + }; + aLambda(1); + + std::cout << R"( + Raw literal + )"; +} diff --git a/tests/auto/cplusplus/cppselectionchanger/tst_cppselectionchangertest.cpp b/tests/auto/cplusplus/cppselectionchanger/tst_cppselectionchangertest.cpp new file mode 100644 index 00000000000..df7f6754496 --- /dev/null +++ b/tests/auto/cplusplus/cppselectionchanger/tst_cppselectionchangertest.cpp @@ -0,0 +1,1156 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include + +#include +#include +#include +#include +#include + +#include +#include + +using namespace CPlusPlus; +using namespace CppTools; + +//TESTED_COMPONENT=src/plugins/cpptools + +enum { + debug = false +}; + +class ChangeResult { +public: + ChangeResult() : m_string(""), m_result(false) {} + ChangeResult(const QString &s) : m_string(s), m_result(true) {} + ChangeResult(const QString &s, bool r) : m_string(s), m_result(r) {} + + QString string() const { return m_string; } + bool result() const { return m_result; } + + bool operator==(const ChangeResult& rhs) const + { + return string().simplified() == rhs.string().simplified() && result() == rhs.result(); + } + +private: + QString m_string; + bool m_result; +}; + +namespace QTest { + template<> + char *toString(const ChangeResult &changeResult) + { + QByteArray ba = QString("\n").toUtf8(); + ba.append(changeResult.string().trimmed().toUtf8()); + ba.append(QString("\nResult: ").toUtf8()); + ba.append(changeResult.result() ? '1' : '0'); + return qstrdup(ba.data()); + } +} + +class tst_CppSelectionChanger : public QObject +{ + Q_OBJECT +public: + +private Q_SLOTS: + void initTestCase(); + void testStructSelection(); + void testFunctionDeclaration(); + void testTemplatedFunctionDeclaration(); + void testNamespace(); + void testClassSelection(); + void testFunctionDefinition(); + + void testInstanceDeclaration(); + void testIfStatement(); + void testForSelection(); + void testRangeForSelection(); + void testStringLiteral(); + void testCharLiteral(); + void testFunctionCall(); + void testTemplateId(); + void testLambda(); + + void testWholeDocumentSelection(); + +private: + static QByteArray preprocess(const QByteArray &source, const QString &filename); + QTextCursor getCursorStartPositionForFoundString(const QString &stringToSearchFor); + + bool expand(QTextCursor &cursor); + bool shrink(QTextCursor &cursor); + + bool verifyCursorBeforeChange(const QTextCursor &cursor); + void doExpand(QTextCursor &cursor, const QString &expectedString); + void doShrink(QTextCursor &cursor, const QString &expectedString); + + CppSelectionChanger changer; + QString cppFileString; + QTextDocument textDocument; + Document::Ptr cppDocument; + ChangeResult expected; + ChangeResult computed; +}; + +#define STRING_COMPARE_SIMPLIFIED(a, b) QVERIFY(a.simplified() == b.simplified()); + +QTextCursor tst_CppSelectionChanger::getCursorStartPositionForFoundString( + const QString &stringToSearchFor) +{ + QTextCursor cursor = textDocument.find(stringToSearchFor); + // Returns only the start position of the selection, not the whole selection. + if (!cursor.isNull()) + cursor.setPosition(cursor.anchor()); + return cursor; +} + +bool tst_CppSelectionChanger::expand(QTextCursor &cursor) +{ + changer.startChangeSelection(); + bool result = + changer.changeSelection( + CppSelectionChanger::ExpandSelection, + cursor, + cppDocument); + changer.stopChangeSelection(); + + if (debug) + qDebug() << "After expansion:" << cursor.selectedText(); + + return result; +} + +bool tst_CppSelectionChanger::shrink(QTextCursor &cursor) +{ + changer.startChangeSelection(); + bool result = + changer.changeSelection( + CppSelectionChanger::ShrinkSelection, + cursor, + cppDocument); + changer.stopChangeSelection(); + + if (debug) + qDebug() << "After shrinking:" << cursor.selectedText(); + + return result; +} + +bool tst_CppSelectionChanger::verifyCursorBeforeChange(const QTextCursor &cursor) +{ + if (cursor.isNull()) + return false; + + if (debug) + qDebug() << "Before expansion:" << cursor.selectedText(); + + // Set initial cursor, never forget. + changer.onCursorPositionChanged(cursor); + return true; +} + +#define VERIFY_CHANGE() QCOMPARE(computed, expected) + +inline void tst_CppSelectionChanger::doExpand(QTextCursor& cursor, const QString& expectedString) +{ + bool result = expand(cursor); + computed = ChangeResult(cursor.selectedText(), result); + expected = ChangeResult(expectedString); +} + +inline void tst_CppSelectionChanger::doShrink(QTextCursor& cursor, const QString& expectedString) +{ + bool result = shrink(cursor); + computed = ChangeResult(cursor.selectedText(), result); + expected = ChangeResult(expectedString); +} + +QByteArray tst_CppSelectionChanger::preprocess(const QByteArray &source, const QString &fileName) +{ + Client *client = 0; // no client. + Environment env; + Preprocessor preprocess(client, &env); + preprocess.setKeepComments(true); + return preprocess.run(fileName, source); +} + +void tst_CppSelectionChanger::initTestCase() +{ + // Read cpp file contents into QTextDocument and CppTools::Document::Ptr. + QString fileName(SRCDIR "/testCppFile.cpp"); + QFile file(fileName); + file.open(QIODevice::ReadOnly); + QTextStream s(&file); + cppFileString = s.readAll(); + file.close(); + textDocument.setPlainText(cppFileString); + + // Create the CPP document and preprocess the source, just like how the CppEditor does it. + cppDocument = Document::create(fileName); + const QByteArray preprocessedSource = preprocess(cppFileString.toUtf8(), fileName); + cppDocument->setUtf8Source(preprocessedSource); + cppDocument->parse(); + cppDocument->check(); +} + +void tst_CppSelectionChanger::testStructSelection() +{ + bool result; + QTextCursor cursor = getCursorStartPositionForFoundString("int a;"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "int"); + VERIFY_CHANGE(); + doExpand(cursor, "int a;"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + int a; + char b; + char x; + char y; + char z; + std::string c; + double d; + std::map e; + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + { + int a; + char b; + char x; + char y; + char z; + std::string c; + double d; + std::map e; + } + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + struct TestClass { + int a; + char b; + char x; + char y; + char z; + std::string c; + double d; + std::map e; + } + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + struct TestClass { + int a; + char b; + char x; + char y; + char z; + std::string c; + double d; + std::map e; + }; + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + struct TestClass { + int a; + char b; + char x; + char y; + char z; + std::string c; + double d; + std::map e; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + { + int a; + char b; + char x; + char y; + char z; + std::string c; + double d; + std::map e; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + int a; + char b; + char x; + char y; + char z; + std::string c; + double d; + std::map e; + )"); + VERIFY_CHANGE(); + doShrink(cursor, "int a;"); + VERIFY_CHANGE(); + doShrink(cursor, "int"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + // Final shrink shouldn't do anything, and return false. + result = shrink(cursor); + STRING_COMPARE_SIMPLIFIED(cursor.selectedText(), QString("")); + QVERIFY(!result); +} + +void tst_CppSelectionChanger::testClassSelection() +{ + // Start from class keyword. + QTextCursor cursor = getCursorStartPositionForFoundString("class CustomClass"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "class"); + VERIFY_CHANGE(); + doExpand(cursor, "class CustomClass"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + class CustomClass { + bool customClassMethod(const int ¶meter) const volatile; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "class CustomClass"); + VERIFY_CHANGE(); + doShrink(cursor, "class"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + // Start from class name. + cursor = getCursorStartPositionForFoundString("class CustomClass"); + cursor.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "CustomClass"); + VERIFY_CHANGE(); + doExpand(cursor, "class CustomClass"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + class CustomClass { + bool customClassMethod(const int ¶meter) const volatile; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "class CustomClass"); + VERIFY_CHANGE(); + doShrink(cursor, "CustomClass"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testFunctionDefinition() +{ + QTextCursor cursor = getCursorStartPositionForFoundString("const int ¶meter"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "const"); + VERIFY_CHANGE(); + doExpand(cursor, "const int ¶meter"); + VERIFY_CHANGE(); + doExpand(cursor, "(const int ¶meter)"); + VERIFY_CHANGE(); + doExpand(cursor, "customClassMethod(const int ¶meter)"); + VERIFY_CHANGE(); + doExpand(cursor, "customClassMethod(const int ¶meter) const volatile"); + VERIFY_CHANGE(); + doExpand(cursor, "bool customClassMethod(const int ¶meter) const volatile;"); + VERIFY_CHANGE(); + doShrink(cursor, "customClassMethod(const int ¶meter) const volatile"); + VERIFY_CHANGE(); + doShrink(cursor, "customClassMethod(const int ¶meter)"); + VERIFY_CHANGE(); + doShrink(cursor, "(const int ¶meter)"); + VERIFY_CHANGE(); + doShrink(cursor, "const int ¶meter"); + VERIFY_CHANGE(); + doShrink(cursor, "const"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + // Test CV qualifiers. + cursor = getCursorStartPositionForFoundString("const volatile"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "const"); + VERIFY_CHANGE(); + doExpand(cursor, "customClassMethod(const int ¶meter) const volatile"); + VERIFY_CHANGE(); + doExpand(cursor, "bool customClassMethod(const int ¶meter) const volatile;"); + VERIFY_CHANGE(); + doShrink(cursor, "customClassMethod(const int ¶meter) const volatile"); + VERIFY_CHANGE(); + doShrink(cursor, "const"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + // Test selection starting from qualified name. + cursor = getCursorStartPositionForFoundString("CustomClass::customClassMethod"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "CustomClass"); + VERIFY_CHANGE(); + doExpand(cursor, "CustomClass::"); + VERIFY_CHANGE(); + doExpand(cursor, "CustomClass::customClassMethod"); + VERIFY_CHANGE(); + doExpand(cursor, "CustomClass::customClassMethod(const int ¶meter)"); + VERIFY_CHANGE(); + doExpand(cursor, "CustomClass::customClassMethod(const int ¶meter) const volatile"); + VERIFY_CHANGE(); + doExpand(cursor, + "bool CustomClass::customClassMethod(const int ¶meter) const volatile"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + bool CustomClass::customClassMethod(const int ¶meter) const volatile { + int secondParameter = parameter; + ++secondParameter; + return secondParameter; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, + "bool CustomClass::customClassMethod(const int ¶meter) const volatile"); + VERIFY_CHANGE(); + doShrink(cursor, "CustomClass::customClassMethod(const int ¶meter) const volatile"); + VERIFY_CHANGE(); + doShrink(cursor, "CustomClass::customClassMethod(const int ¶meter)"); + VERIFY_CHANGE(); + doShrink(cursor, "CustomClass::customClassMethod"); + VERIFY_CHANGE(); + doShrink(cursor, "CustomClass::"); + VERIFY_CHANGE(); + doShrink(cursor, "CustomClass"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + // Test selection starting from return value. + cursor = getCursorStartPositionForFoundString("bool CustomClass::customClassMethod"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "bool"); + VERIFY_CHANGE(); + doExpand(cursor, + "bool CustomClass::customClassMethod(const int ¶meter) const volatile"); + VERIFY_CHANGE(); + doShrink(cursor, "bool"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testInstanceDeclaration() +{ + QTextCursor cursor = getCursorStartPositionForFoundString("argc, argv"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "argc"); + VERIFY_CHANGE(); + doExpand(cursor, "argc, argv"); + VERIFY_CHANGE(); + doExpand(cursor, "(argc, argv)"); + VERIFY_CHANGE(); + doExpand(cursor, "secondCustomClass(argc, argv)"); + VERIFY_CHANGE(); + doExpand(cursor, "SecondCustomClass secondCustomClass(argc, argv);"); + VERIFY_CHANGE(); + doShrink(cursor, "secondCustomClass(argc, argv)"); + VERIFY_CHANGE(); + doShrink(cursor, "(argc, argv)"); + VERIFY_CHANGE(); + doShrink(cursor, "argc, argv"); + VERIFY_CHANGE(); + doShrink(cursor, "argc"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + cursor = getCursorStartPositionForFoundString("SecondCustomClass secondCustomClass"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "SecondCustomClass"); + VERIFY_CHANGE(); + doExpand(cursor, "SecondCustomClass secondCustomClass(argc, argv);"); + VERIFY_CHANGE(); + doShrink(cursor, "SecondCustomClass"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + cursor = getCursorStartPositionForFoundString("secondCustomClass(argc, argv)"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "secondCustomClass"); + VERIFY_CHANGE(); + doExpand(cursor, "secondCustomClass(argc, argv)"); + VERIFY_CHANGE(); + doExpand(cursor, "SecondCustomClass secondCustomClass(argc, argv);"); + VERIFY_CHANGE(); + doShrink(cursor, "secondCustomClass(argc, argv)"); + VERIFY_CHANGE(); + doShrink(cursor, "secondCustomClass"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testForSelection() +{ + QTextCursor cursor = getCursorStartPositionForFoundString("int i = 0"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "int"); + VERIFY_CHANGE(); + doExpand(cursor, "int i = 0;"); + VERIFY_CHANGE(); + doExpand(cursor, "int i = 0; i < 3; ++i"); + VERIFY_CHANGE(); + doExpand(cursor, "(int i = 0; i < 3; ++i)"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + for (int i = 0; i < 3; ++i) { + std::cout << i; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "(int i = 0; i < 3; ++i)"); + VERIFY_CHANGE(); + doShrink(cursor, "int i = 0; i < 3; ++i"); + VERIFY_CHANGE(); + doShrink(cursor, "int i = 0;"); + VERIFY_CHANGE(); + doShrink(cursor, "int"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testRangeForSelection() +{ + QTextCursor cursor = getCursorStartPositionForFoundString("auto val"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "auto"); + VERIFY_CHANGE(); + doExpand(cursor, "auto val :v"); + VERIFY_CHANGE(); + doExpand(cursor, "(auto val :v)"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + for (auto val :v) { + std::cout << val; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "(auto val :v)"); + VERIFY_CHANGE(); + doShrink(cursor, "auto val :v"); + VERIFY_CHANGE(); + doShrink(cursor, "auto"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testStringLiteral() +{ + QTextCursor cursor = getCursorStartPositionForFoundString("Hello"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "Hello"); + VERIFY_CHANGE(); + doExpand(cursor, "\"Hello\""); + VERIFY_CHANGE(); + doShrink(cursor, "Hello"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + // Test raw string literal. + cursor = getCursorStartPositionForFoundString("Raw literal"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, R"( + + Raw literal + + )"); + VERIFY_CHANGE(); + doExpand(cursor, "\ + R\"(\ + Raw literal\ + )\"\ + "); + VERIFY_CHANGE(); + doShrink(cursor, R"( + + Raw literal + + )"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testCharLiteral() +{ + QTextCursor cursor = getCursorStartPositionForFoundString("'c'"); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, 1); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "c"); + VERIFY_CHANGE(); + doExpand(cursor, "'c'"); + VERIFY_CHANGE(); + doShrink(cursor, "c"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testFunctionCall() +{ + QTextCursor cursor = getCursorStartPositionForFoundString("10, 20"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "10"); + VERIFY_CHANGE(); + doExpand(cursor, "10, 20"); + VERIFY_CHANGE(); + doExpand(cursor, "(10, 20)"); + VERIFY_CHANGE(); + doExpand(cursor, "add(10, 20)"); + VERIFY_CHANGE(); + doExpand(cursor, "2, add(10, 20)"); + VERIFY_CHANGE(); + doExpand(cursor, "(2, add(10, 20))"); + VERIFY_CHANGE(); + doExpand(cursor, "add(2, add(10, 20))"); + VERIFY_CHANGE(); + doExpand(cursor, "1, add(2, add(10, 20))"); + VERIFY_CHANGE(); + doExpand(cursor, "(1, add(2, add(10, 20)))"); + VERIFY_CHANGE(); + doExpand(cursor, "add(1, add(2, add(10, 20)))"); + VERIFY_CHANGE(); + doShrink(cursor, "(1, add(2, add(10, 20)))"); + VERIFY_CHANGE(); + doShrink(cursor, "1, add(2, add(10, 20))"); + VERIFY_CHANGE(); + doShrink(cursor, "add(2, add(10, 20))"); + VERIFY_CHANGE(); + doShrink(cursor, "(2, add(10, 20))"); + VERIFY_CHANGE(); + doShrink(cursor, "2, add(10, 20)"); + VERIFY_CHANGE(); + doShrink(cursor, "add(10, 20)"); + VERIFY_CHANGE(); + doShrink(cursor, "(10, 20)"); + VERIFY_CHANGE(); + doShrink(cursor, "10, 20"); + VERIFY_CHANGE(); + doShrink(cursor, "10"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testTemplateId() +{ + // Start from map keyword. + QTextCursor cursor = getCursorStartPositionForFoundString("std::map"); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, 5); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "map"); + VERIFY_CHANGE(); + doExpand(cursor, "map"); + VERIFY_CHANGE(); + doExpand(cursor, "std::map"); + VERIFY_CHANGE(); + doExpand(cursor, "std::map a;"); + VERIFY_CHANGE(); + doShrink(cursor, "std::map"); + VERIFY_CHANGE(); + doShrink(cursor, "map"); + VERIFY_CHANGE(); + doShrink(cursor, "map"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testLambda() +{ + QTextCursor cursor = getCursorStartPositionForFoundString("=, &a"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "=, &a"); + VERIFY_CHANGE(); + doExpand(cursor, "[=, &a]"); + VERIFY_CHANGE(); + doExpand(cursor, "[=, &a](int lambdaArgument)"); + VERIFY_CHANGE(); + doExpand(cursor, "[=, &a](int lambdaArgument) -> int"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + [=, &a](int lambdaArgument) -> int { + return lambdaArgument + 1; + } + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + aLambda = [=, &a](int lambdaArgument) -> int { + return lambdaArgument + 1; + } + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + auto aLambda = [=, &a](int lambdaArgument) -> int { + return lambdaArgument + 1; + }; + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + aLambda = [=, &a](int lambdaArgument) -> int { + return lambdaArgument + 1; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + [=, &a](int lambdaArgument) -> int { + return lambdaArgument + 1; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "[=, &a](int lambdaArgument) -> int"); + VERIFY_CHANGE(); + doShrink(cursor, "[=, &a](int lambdaArgument)"); + VERIFY_CHANGE(); + doShrink(cursor, "[=, &a]"); + VERIFY_CHANGE(); + doShrink(cursor, "=, &a"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + // Start from inside lambda. + cursor = getCursorStartPositionForFoundString("return lambdaArgument + 1;"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "return lambdaArgument + 1;"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + { + return lambdaArgument + 1; + } + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + [=, &a](int lambdaArgument) -> int { + return lambdaArgument + 1; + } + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + aLambda = [=, &a](int lambdaArgument) -> int { + return lambdaArgument + 1; + } + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + auto aLambda = [=, &a](int lambdaArgument) -> int { + return lambdaArgument + 1; + }; + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + aLambda = [=, &a](int lambdaArgument) -> int { + return lambdaArgument + 1; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + [=, &a](int lambdaArgument) -> int { + return lambdaArgument + 1; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + { + return lambdaArgument + 1; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "return lambdaArgument + 1;"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testIfStatement() +{ + // Inside braces. + QTextCursor cursor = getCursorStartPositionForFoundString("for (int i = 0"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, R"( + for (int i = 0; i < 3; ++i) { + std::cout << i; + } + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + std::cout << "Hello" << 'c' << 54545 << u8"utf8string" << U"unicodeString"; + for (int i = 0; i < 3; ++i) { + std::cout << i; + } + + for (auto val :v) { + std::cout << val; + } + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + { + std::cout << "Hello" << 'c' << 54545 << u8"utf8string" << U"unicodeString"; + for (int i = 0; i < 3; ++i) { + std::cout << i; + } + + for (auto val :v) { + std::cout << val; + } + } + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + if (5 == 5) { + std::cout << "Hello" << 'c' << 54545 << u8"utf8string" << U"unicodeString"; + for (int i = 0; i < 3; ++i) { + std::cout << i; + } + + for (auto val :v) { + std::cout << val; + } + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + { + std::cout << "Hello" << 'c' << 54545 << u8"utf8string" << U"unicodeString"; + for (int i = 0; i < 3; ++i) { + std::cout << i; + } + + for (auto val :v) { + std::cout << val; + } + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + std::cout << "Hello" << 'c' << 54545 << u8"utf8string" << U"unicodeString"; + for (int i = 0; i < 3; ++i) { + std::cout << i; + } + + for (auto val :v) { + std::cout << val; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + for (int i = 0; i < 3; ++i) { + std::cout << i; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + // Inside condition paranthesis. + cursor = getCursorStartPositionForFoundString("5 == 5"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "5"); + VERIFY_CHANGE(); + doExpand(cursor, "5 == 5"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + if (5 == 5) { + std::cout << "Hello" << 'c' << 54545 << u8"utf8string" << U"unicodeString"; + for (int i = 0; i < 3; ++i) { + std::cout << i; + } + + for (auto val :v) { + std::cout << val; + } + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "5 == 5"); + VERIFY_CHANGE(); + doShrink(cursor, "5"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testFunctionDeclaration() +{ + QTextCursor cursor = getCursorStartPositionForFoundString("int a, int b"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "int"); + VERIFY_CHANGE(); + doExpand(cursor, "int a"); + VERIFY_CHANGE(); + doExpand(cursor, "int a, int b"); + VERIFY_CHANGE(); + doExpand(cursor, "(int a, int b)"); + VERIFY_CHANGE(); + doExpand(cursor, "add(int a, int b)"); + VERIFY_CHANGE(); + doExpand(cursor, "int add(int a, int b)"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + int add(int a, int b) { + return a + b; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "int add(int a, int b)"); + VERIFY_CHANGE(); + doShrink(cursor, "add(int a, int b)"); + VERIFY_CHANGE(); + doShrink(cursor, "(int a, int b)"); + VERIFY_CHANGE(); + doShrink(cursor, "int a, int b"); + VERIFY_CHANGE(); + doShrink(cursor, "int a"); + VERIFY_CHANGE(); + doShrink(cursor, "int"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testTemplatedFunctionDeclaration() +{ + QTextCursor cursor = getCursorStartPositionForFoundString("T a, T b"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "T"); + VERIFY_CHANGE(); + doExpand(cursor, "T a"); + VERIFY_CHANGE(); + doExpand(cursor, "T a, T b"); + VERIFY_CHANGE(); + doExpand(cursor, "(T a, T b)"); + VERIFY_CHANGE(); + doExpand(cursor, "subtract(T a, T b)"); + VERIFY_CHANGE(); + doExpand(cursor, "int subtract(T a, T b)"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + int subtract(T a, T b) { + return a - b; + } + )"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + template + int subtract(T a, T b) { + return a - b; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, R"( + int subtract(T a, T b) { + return a - b; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "int subtract(T a, T b)"); + VERIFY_CHANGE(); + doShrink(cursor, "subtract(T a, T b)"); + VERIFY_CHANGE(); + doShrink(cursor, "(T a, T b)"); + VERIFY_CHANGE(); + doShrink(cursor, "T a, T b"); + VERIFY_CHANGE(); + doShrink(cursor, "T a"); + VERIFY_CHANGE(); + doShrink(cursor, "T"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + // Test template selection. + cursor = getCursorStartPositionForFoundString("template "); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "template"); + VERIFY_CHANGE(); + doExpand(cursor, "template "); + VERIFY_CHANGE(); + doExpand(cursor, R"( + template + int subtract(T a, T b) { + return a - b; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "template "); + VERIFY_CHANGE(); + doShrink(cursor, "template"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + // Test template parameter selection. + cursor = getCursorStartPositionForFoundString("class T"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "class T"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + template + int subtract(T a, T b) { + return a - b; + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "class T"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testNamespace() +{ + QTextCursor cursor = getCursorStartPositionForFoundString("namespace"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "namespace"); + VERIFY_CHANGE(); + doExpand(cursor, "namespace CustomNamespace"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + namespace CustomNamespace { + extern int insideNamespace; + int foo() { + insideNamespace = 2; + return insideNamespace; + } + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "namespace CustomNamespace"); + VERIFY_CHANGE(); + doShrink(cursor, "namespace"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); + + cursor = getCursorStartPositionForFoundString("CustomNamespace"); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + doExpand(cursor, "CustomNamespace"); + VERIFY_CHANGE(); + doExpand(cursor, "namespace CustomNamespace"); + VERIFY_CHANGE(); + doExpand(cursor, R"( + namespace CustomNamespace { + extern int insideNamespace; + int foo() { + insideNamespace = 2; + return insideNamespace; + } + } + )"); + VERIFY_CHANGE(); + doShrink(cursor, "namespace CustomNamespace"); + VERIFY_CHANGE(); + doShrink(cursor, "CustomNamespace"); + VERIFY_CHANGE(); + doShrink(cursor, ""); + VERIFY_CHANGE(); +} + +void tst_CppSelectionChanger::testWholeDocumentSelection() +{ + bool result; + QTextCursor cursor = getCursorStartPositionForFoundString("#include "); + QCOMPARE(verifyCursorBeforeChange(cursor), true); + + // Get whole document contents as a string. + QTextCursor wholeDocumentCursor(cursor); + wholeDocumentCursor.setPosition(0, QTextCursor::MoveAnchor); + wholeDocumentCursor.setPosition( + wholeDocumentCursor.document()->characterCount() - 1, QTextCursor::KeepAnchor); + + // Verify if cursor selected whole document. + doExpand(cursor, wholeDocumentCursor.selectedText()); + VERIFY_CHANGE(); + + // Should fail, because whole document was selected. + result = expand(cursor); + STRING_COMPARE_SIMPLIFIED(cursor.selectedText(), wholeDocumentCursor.selectedText()); + QVERIFY(!result); + + // Shrink to no contents. + doShrink(cursor, ""); + VERIFY_CHANGE(); + + // Last shrink should fail. + result = shrink(cursor); + STRING_COMPARE_SIMPLIFIED(cursor.selectedText(), QString("")); + QVERIFY(!result); +} + +QTEST_MAIN(tst_CppSelectionChanger) +#include "tst_cppselectionchangertest.moc"