Editor: multi cursor support

Adding a way to create multiple cursors that can insert/remove text at
arbitrary positions in the document. Adding cursors is done by pressing
alt + up/down or by clicking into the editor while holding the alt key.

Fixes: QTCREATORBUG-16013
Change-Id: I495d27d95a3d277220946616ef30efc241da0120
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Artem Sokolovskii <artem.sokolovskii@qt.io>
Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
David Schulz
2021-06-28 09:13:57 +02:00
parent eefb385918
commit c00330f905
30 changed files with 2204 additions and 2459 deletions

View File

@@ -93,6 +93,7 @@ add_qtc_library(Utils
mimetypes/mimeprovider.cpp mimetypes/mimeprovider_p.h mimetypes/mimeprovider.cpp mimetypes/mimeprovider_p.h
mimetypes/mimetype.cpp mimetypes/mimetype.h mimetypes/mimetype_p.h mimetypes/mimetype.cpp mimetypes/mimetype.h mimetypes/mimetype_p.h
mimetypes/mimetypeparser.cpp mimetypes/mimetypeparser_p.h mimetypes/mimetypeparser.cpp mimetypes/mimetypeparser_p.h
multitextcursor.cpp multitextcursor.h
namevaluedictionary.cpp namevaluedictionary.h namevaluedictionary.cpp namevaluedictionary.h
namevaluedictionary.cpp namevaluedictionary.h namevaluedictionary.cpp namevaluedictionary.h
namevalueitem.cpp namevalueitem.h namevalueitem.cpp namevalueitem.h

View File

@@ -26,9 +26,13 @@
#include "camelcasecursor.h" #include "camelcasecursor.h"
#include "multitextcursor.h"
#include <QLineEdit> #include <QLineEdit>
#include <QPlainTextEdit> #include <QPlainTextEdit>
namespace Utils {
template<typename C, typename E> template<typename C, typename E>
bool moveCursor(C *cursor, E *edit, QTextCursor::MoveOperation direction, QTextCursor::MoveMode mode); bool moveCursor(C *cursor, E *edit, QTextCursor::MoveOperation direction, QTextCursor::MoveMode mode);
@@ -323,6 +327,15 @@ bool CamelCaseCursor::left(QTextCursor *cursor, QPlainTextEdit *edit, QTextCurso
return camelCaseLeft(cursor, edit, mode); return camelCaseLeft(cursor, edit, mode);
} }
bool CamelCaseCursor::left(MultiTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode)
{
bool result = false;
for (QTextCursor &c : *cursor)
result |= CamelCaseCursor::left(&c, edit, mode);
cursor->mergeCursors();
return result;
}
bool CamelCaseCursor::left(QLineEdit *edit, QTextCursor::MoveMode mode) bool CamelCaseCursor::left(QLineEdit *edit, QTextCursor::MoveMode mode)
{ {
QTextCursor temp; QTextCursor temp;
@@ -334,8 +347,20 @@ bool CamelCaseCursor::right(QTextCursor *cursor, QPlainTextEdit *edit, QTextCurs
return camelCaseRight(cursor, edit, mode); return camelCaseRight(cursor, edit, mode);
} }
bool CamelCaseCursor::right(MultiTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode)
{
bool result = false;
for (QTextCursor &c : *cursor)
result |= CamelCaseCursor::right(&c, edit, mode);
cursor->mergeCursors();
return result;
}
bool CamelCaseCursor::right(QLineEdit *edit, QTextCursor::MoveMode mode) bool CamelCaseCursor::right(QLineEdit *edit, QTextCursor::MoveMode mode)
{ {
QTextCursor temp; QTextCursor temp;
return camelCaseRight(&temp, edit, mode); return camelCaseRight(&temp, edit, mode);
} }
} // namespace Utils

View File

@@ -35,11 +35,19 @@ class QLineEdit;
class QPlainTextEdit; class QPlainTextEdit;
QT_END_NAMESPACE QT_END_NAMESPACE
namespace Utils {
class MultiTextCursor;
class QTCREATOR_UTILS_EXPORT CamelCaseCursor class QTCREATOR_UTILS_EXPORT CamelCaseCursor
{ {
public: public:
static bool left(QTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode); static bool left(QTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode);
static bool left(MultiTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode);
static bool left(QLineEdit *edit, QTextCursor::MoveMode mode); static bool left(QLineEdit *edit, QTextCursor::MoveMode mode);
static bool right(QTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode); static bool right(QTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode);
static bool right(MultiTextCursor *cursor, QPlainTextEdit *edit, QTextCursor::MoveMode mode);
static bool right(QLineEdit *edit, QTextCursor::MoveMode mode); static bool right(QLineEdit *edit, QTextCursor::MoveMode mode);
}; };
} // namespace Utils

View File

@@ -0,0 +1,446 @@
/****************************************************************************
**
** Copyright (C) 2021 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 "multitextcursor.h"
#include "algorithm.h"
#include "camelcasecursor.h"
#include "qtcassert.h"
#include <QKeyEvent>
#include <QTextBlock>
namespace Utils {
MultiTextCursor::MultiTextCursor() {}
MultiTextCursor::MultiTextCursor(const QList<QTextCursor> &cursors)
: m_cursors(cursors)
{
mergeCursors();
}
void MultiTextCursor::addCursor(const QTextCursor &cursor)
{
QTC_ASSERT(!cursor.isNull(), return);
m_cursors.append(cursor);
mergeCursors();
}
void MultiTextCursor::setCursors(const QList<QTextCursor> &cursors)
{
m_cursors = cursors;
mergeCursors();
}
const QList<QTextCursor> MultiTextCursor::cursors() const
{
return m_cursors;
}
void MultiTextCursor::replaceMainCursor(const QTextCursor &cursor)
{
QTC_ASSERT(!cursor.isNull(), return);
takeMainCursor();
addCursor(cursor);
}
QTextCursor MultiTextCursor::mainCursor() const
{
if (m_cursors.isEmpty())
return {};
return m_cursors.last();
}
QTextCursor MultiTextCursor::takeMainCursor()
{
if (m_cursors.isEmpty())
return {};
return m_cursors.takeLast();
}
void MultiTextCursor::beginEditBlock()
{
QTC_ASSERT(!m_cursors.empty(), return);
m_cursors.last().beginEditBlock();
}
void MultiTextCursor::endEditBlock()
{
QTC_ASSERT(!m_cursors.empty(), return);
m_cursors.last().endEditBlock();
}
bool MultiTextCursor::isNull() const
{
return m_cursors.isEmpty();
}
bool MultiTextCursor::hasMultipleCursors() const
{
return m_cursors.size() > 1;
}
int MultiTextCursor::cursorCount() const
{
return m_cursors.size();
}
void MultiTextCursor::movePosition(QTextCursor::MoveOperation operation,
QTextCursor::MoveMode mode,
int n)
{
for (QTextCursor &cursor : m_cursors)
cursor.movePosition(operation, mode, n);
mergeCursors();
}
bool MultiTextCursor::hasSelection() const
{
return Utils::anyOf(m_cursors, &QTextCursor::hasSelection);
}
QString MultiTextCursor::selectedText() const
{
QString text;
for (const QTextCursor &cursor : m_cursors) {
const QString &cursorText = cursor.selectedText();
if (cursorText.isEmpty())
continue;
if (!text.isEmpty())
text.append('\n');
text.append(cursorText);
}
return text;
}
void MultiTextCursor::removeSelectedText()
{
beginEditBlock();
for (QTextCursor &c : m_cursors)
c.removeSelectedText();
endEditBlock();
mergeCursors();
}
static void insertAndSelect(QTextCursor &cursor, const QString &text, bool selectNewText)
{
if (selectNewText) {
const int anchor = cursor.position();
cursor.insertText(text);
const int pos = cursor.position();
cursor.setPosition(anchor);
cursor.setPosition(pos, QTextCursor::KeepAnchor);
} else {
cursor.insertText(text);
}
}
void MultiTextCursor::insertText(const QString &text, bool selectNewText)
{
if (m_cursors.isEmpty())
return;
m_cursors.last().beginEditBlock();
if (hasMultipleCursors()) {
QStringList lines = text.split('\n');
if (!lines.isEmpty() && lines.last().isEmpty())
lines.pop_back();
int index = 0;
if (lines.count() == m_cursors.count()) {
for (QTextCursor &cursor : m_cursors)
insertAndSelect(cursor, lines.at(index++), selectNewText);
m_cursors.last().endEditBlock();
return;
}
}
for (QTextCursor &cursor : m_cursors)
insertAndSelect(cursor, text, selectNewText);
m_cursors.last().endEditBlock();
}
bool equalCursors(const QTextCursor &lhs, const QTextCursor &rhs)
{
return lhs == rhs && lhs.anchor() == rhs.anchor();
}
bool MultiTextCursor::operator==(const MultiTextCursor &other) const
{
if (m_cursors.size() != other.m_cursors.size())
return false;
if (m_cursors.isEmpty())
return true;
QList<QTextCursor> thisCursors = m_cursors;
QList<QTextCursor> otherCursors = other.m_cursors;
if (!equalCursors(thisCursors.takeLast(), otherCursors.takeLast()))
return false;
for (const QTextCursor &oc : otherCursors) {
auto compare = [oc](const QTextCursor &c) { return equalCursors(oc, c); };
if (!Utils::contains(thisCursors, compare))
return false;
}
return true;
}
bool MultiTextCursor::operator!=(const MultiTextCursor &other) const
{
return !operator==(other);
}
static bool cursorsOverlap(const QTextCursor &c1, const QTextCursor &c2)
{
if (c1.hasSelection()) {
if (c2.hasSelection()) {
return c2.selectionEnd() > c1.selectionStart()
&& c2.selectionStart() < c1.selectionEnd();
}
const int c2Pos = c2.position();
return c2Pos > c1.selectionStart() && c2Pos < c1.selectionEnd();
}
if (c2.hasSelection()) {
const int c1Pos = c1.position();
return c1Pos > c2.selectionStart() && c1Pos < c2.selectionEnd();
}
return c1 == c2;
};
static void mergeCursors(QTextCursor &c1, const QTextCursor &c2)
{
if (c1.position() == c2.position() && c1.anchor() == c2.anchor())
return;
if (c1.hasSelection()) {
if (!c2.hasSelection())
return;
int pos = c1.position();
int anchor = c1.anchor();
if (c1.selectionStart() > c2.selectionStart()) {
if (pos < anchor)
pos = c2.selectionStart();
else
anchor = c2.selectionStart();
}
if (c1.selectionEnd() < c2.selectionEnd()) {
if (pos < anchor)
anchor = c2.selectionEnd();
else
pos = c2.selectionEnd();
}
c1.setPosition(anchor);
c1.setPosition(pos, QTextCursor::KeepAnchor);
} else {
c1 = c2;
}
}
void MultiTextCursor::mergeCursors()
{
std::list<QTextCursor> cursors(m_cursors.begin(), m_cursors.end());
cursors = Utils::filtered(cursors, [](const QTextCursor &c){
return !c.isNull();
});
for (auto it = cursors.begin(); it != cursors.end(); ++it) {
QTextCursor &c1 = *it;
for (auto other = std::next(it); other != cursors.end();) {
const QTextCursor &c2 = *other;
if (cursorsOverlap(c1, c2)) {
Utils::mergeCursors(c1, c2);
other = cursors.erase(other);
continue;
}
++other;
}
}
m_cursors = QList<QTextCursor>(cursors.begin(), cursors.end());
}
// could go into QTextCursor...
static QTextLine currentTextLine(const QTextCursor &cursor)
{
const QTextBlock block = cursor.block();
if (!block.isValid())
return {};
const QTextLayout *layout = block.layout();
if (!layout)
return {};
const int relativePos = cursor.position() - block.position();
return layout->lineForTextPosition(relativePos);
}
bool multiCursorAddEvent(QKeyEvent *e, QKeySequence::StandardKey matchKey)
{
uint searchkey = (e->modifiers() | e->key())
& ~(Qt::KeypadModifier | Qt::GroupSwitchModifier | Qt::AltModifier);
const QList<QKeySequence> bindings = QKeySequence::keyBindings(matchKey);
return bindings.contains(QKeySequence(searchkey));
}
bool MultiTextCursor::handleMoveKeyEvent(QKeyEvent *e,
QPlainTextEdit *edit,
bool camelCaseNavigationEnabled)
{
if (e->modifiers() & Qt::AltModifier) {
QTextCursor::MoveOperation op = QTextCursor::NoMove;
if (multiCursorAddEvent(e, QKeySequence::MoveToNextWord)) {
op = QTextCursor::WordRight;
} else if (multiCursorAddEvent(e, QKeySequence::MoveToPreviousWord)) {
op = QTextCursor::WordLeft;
} else if (multiCursorAddEvent(e, QKeySequence::MoveToEndOfBlock)) {
op = QTextCursor::EndOfBlock;
} else if (multiCursorAddEvent(e, QKeySequence::MoveToStartOfBlock)) {
op = QTextCursor::StartOfBlock;
} else if (multiCursorAddEvent(e, QKeySequence::MoveToNextLine)) {
op = QTextCursor::Down;
} else if (multiCursorAddEvent(e, QKeySequence::MoveToPreviousLine)) {
op = QTextCursor::Up;
} else if (multiCursorAddEvent(e, QKeySequence::MoveToStartOfLine)) {
op = QTextCursor::StartOfLine;
} else if (multiCursorAddEvent(e, QKeySequence::MoveToEndOfLine)) {
op = QTextCursor::EndOfLine;
} else if (multiCursorAddEvent(e, QKeySequence::MoveToStartOfDocument)) {
op = QTextCursor::Start;
} else if (multiCursorAddEvent(e, QKeySequence::MoveToEndOfDocument)) {
op = QTextCursor::End;
} else {
return false;
}
const QList<QTextCursor> cursors = m_cursors;
for (QTextCursor cursor : cursors) {
if (camelCaseNavigationEnabled && op == QTextCursor::WordRight)
CamelCaseCursor::right(&cursor, edit, QTextCursor::MoveAnchor);
else if (camelCaseNavigationEnabled && op == QTextCursor::WordLeft)
CamelCaseCursor::left(&cursor, edit, QTextCursor::MoveAnchor);
else
cursor.movePosition(op, QTextCursor::MoveAnchor);
m_cursors << cursor;
}
mergeCursors();
return true;
}
for (QTextCursor &cursor : m_cursors) {
QTextCursor::MoveMode mode = QTextCursor::MoveAnchor;
QTextCursor::MoveOperation op = QTextCursor::NoMove;
if (e == QKeySequence::MoveToNextChar) {
op = QTextCursor::Right;
} else if (e == QKeySequence::MoveToPreviousChar) {
op = QTextCursor::Left;
} else if (e == QKeySequence::SelectNextChar) {
op = QTextCursor::Right;
mode = QTextCursor::KeepAnchor;
} else if (e == QKeySequence::SelectPreviousChar) {
op = QTextCursor::Left;
mode = QTextCursor::KeepAnchor;
} else if (e == QKeySequence::SelectNextWord) {
op = QTextCursor::WordRight;
mode = QTextCursor::KeepAnchor;
} else if (e == QKeySequence::SelectPreviousWord) {
op = QTextCursor::WordLeft;
mode = QTextCursor::KeepAnchor;
} else if (e == QKeySequence::SelectStartOfLine) {
op = QTextCursor::StartOfLine;
mode = QTextCursor::KeepAnchor;
} else if (e == QKeySequence::SelectEndOfLine) {
op = QTextCursor::EndOfLine;
mode = QTextCursor::KeepAnchor;
} else if (e == QKeySequence::SelectStartOfBlock) {
op = QTextCursor::StartOfBlock;
mode = QTextCursor::KeepAnchor;
} else if (e == QKeySequence::SelectEndOfBlock) {
op = QTextCursor::EndOfBlock;
mode = QTextCursor::KeepAnchor;
} else if (e == QKeySequence::SelectStartOfDocument) {
op = QTextCursor::Start;
mode = QTextCursor::KeepAnchor;
} else if (e == QKeySequence::SelectEndOfDocument) {
op = QTextCursor::End;
mode = QTextCursor::KeepAnchor;
} else if (e == QKeySequence::SelectPreviousLine) {
op = QTextCursor::Up;
mode = QTextCursor::KeepAnchor;
} else if (e == QKeySequence::SelectNextLine) {
op = QTextCursor::Down;
mode = QTextCursor::KeepAnchor;
{
QTextBlock block = cursor.block();
QTextLine line = currentTextLine(cursor);
if (!block.next().isValid() && line.isValid()
&& line.lineNumber() == block.layout()->lineCount() - 1)
op = QTextCursor::End;
}
} else if (e == QKeySequence::MoveToNextWord) {
op = QTextCursor::WordRight;
} else if (e == QKeySequence::MoveToPreviousWord) {
op = QTextCursor::WordLeft;
} else if (e == QKeySequence::MoveToEndOfBlock) {
op = QTextCursor::EndOfBlock;
} else if (e == QKeySequence::MoveToStartOfBlock) {
op = QTextCursor::StartOfBlock;
} else if (e == QKeySequence::MoveToNextLine) {
op = QTextCursor::Down;
} else if (e == QKeySequence::MoveToPreviousLine) {
op = QTextCursor::Up;
} else if (e == QKeySequence::MoveToStartOfLine) {
op = QTextCursor::StartOfLine;
} else if (e == QKeySequence::MoveToEndOfLine) {
op = QTextCursor::EndOfLine;
} else if (e == QKeySequence::MoveToStartOfDocument) {
op = QTextCursor::Start;
} else if (e == QKeySequence::MoveToEndOfDocument) {
op = QTextCursor::End;
} else {
return false;
}
// Except for pageup and pagedown, macOS has very different behavior, we don't do it all, but
// here's the breakdown:
// Shift still works as an anchor, but only one of the other keys can be down Ctrl (Command),
// Alt (Option), or Meta (Control).
// Command/Control + Left/Right -- Move to left or right of the line
// + Up/Down -- Move to top bottom of the file. (Control doesn't move the cursor)
// Option + Left/Right -- Move one word Left/right.
// + Up/Down -- Begin/End of Paragraph.
// Home/End Top/Bottom of file. (usually don't move the cursor, but will select)
bool visualNavigation = cursor.visualNavigation();
cursor.setVisualNavigation(true);
if (camelCaseNavigationEnabled && op == QTextCursor::WordRight)
CamelCaseCursor::right(&cursor, edit, mode);
else if (camelCaseNavigationEnabled && op == QTextCursor::WordLeft)
CamelCaseCursor::left(&cursor, edit, mode);
else if (!cursor.movePosition(op, mode) && mode == QTextCursor::MoveAnchor)
cursor.clearSelection();
cursor.setVisualNavigation(visualNavigation);
}
mergeCursors();
return true;
}
} // namespace Utils

View File

@@ -0,0 +1,106 @@
/****************************************************************************
**
** Copyright (C) 2021 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.
**
****************************************************************************/
#pragma once
#include "utils_global.h"
#include <QTextCursor>
QT_BEGIN_NAMESPACE
class QKeyEvent;
class QPlainTextEdit;
QT_END_NAMESPACE
namespace Utils {
class QTCREATOR_UTILS_EXPORT MultiTextCursor
{
public:
MultiTextCursor();
explicit MultiTextCursor(const QList<QTextCursor> &cursors);
/// replace all cursors with \param cursors and the last one will be the new main cursors
void setCursors(const QList<QTextCursor> &cursors);
const QList<QTextCursor> cursors() const;
/// \returns whether this multi cursor contains any cursor
bool isNull() const;
/// \returns whether this multi cursor contains more than one cursor
bool hasMultipleCursors() const;
/// \returns the number of cursors handled by this cursor
int cursorCount() const;
/// the \param cursor that is appended by added by \brief addCursor
/// will be interpreted as the new main cursor
void addCursor(const QTextCursor &cursor);
/// convenience function that removes the old main cursor and appends
/// \param cursor as the new main cursor
void replaceMainCursor(const QTextCursor &cursor);
/// \returns the main cursor
QTextCursor mainCursor() const;
/// \returns the main cursor and removes it from this multi cursor
QTextCursor takeMainCursor();
void beginEditBlock();
void endEditBlock();
/// merges overlapping cursors together
void mergeCursors();
/// applies the move key event \param e to all cursors in this multi cursor
bool handleMoveKeyEvent(QKeyEvent *e, QPlainTextEdit *edit, bool camelCaseNavigationEnabled);
/// applies the move \param operation to all cursors in this multi cursor \param n times
/// with the move \param mode
void movePosition(QTextCursor::MoveOperation operation, QTextCursor::MoveMode mode, int n = 1);
/// \returns whether any cursor has a selection
bool hasSelection() const;
/// \returns the selected text of all cursors that have a selection separated by
/// a newline character
QString selectedText() const;
/// removes the selected text of all cursors that have a selection from the document
void removeSelectedText();
/// inserts \param text into all cursors, potentially removing correctly selected text
void insertText(const QString &text, bool selectNewText = false);
bool operator==(const MultiTextCursor &other) const;
bool operator!=(const MultiTextCursor &other) const;
using iterator = QList<QTextCursor>::iterator;
using const_iterator = QList<QTextCursor>::const_iterator;
iterator begin() { return m_cursors.begin(); }
iterator end() { return m_cursors.end(); }
const_iterator begin() const { return m_cursors.begin(); }
const_iterator end() const { return m_cursors.end(); }
const_iterator constBegin() const { return m_cursors.constBegin(); }
const_iterator constEnd() const { return m_cursors.constEnd(); }
private:
QList<QTextCursor> m_cursors;
};
} // namespace Utils

View File

@@ -24,10 +24,14 @@
****************************************************************************/ ****************************************************************************/
#include "uncommentselection.h" #include "uncommentselection.h"
#include "qtcassert.h"
#include "utils/multitextcursor.h"
#include <QPlainTextEdit> #include <QPlainTextEdit>
#include <QTextBlock> #include <QTextBlock>
using namespace Utils; namespace Utils {
CommentDefinition CommentDefinition::CppStyle = CommentDefinition("//", "/*", "*/"); CommentDefinition CommentDefinition::CppStyle = CommentDefinition("//", "/*", "*/");
CommentDefinition CommentDefinition::HashStyle = CommentDefinition("#"); CommentDefinition CommentDefinition::HashStyle = CommentDefinition("#");
@@ -74,9 +78,9 @@ static bool isComment(const QString &text, int index,
} }
QTextCursor Utils::unCommentSelection(const QTextCursor &cursorIn, QTextCursor unCommentSelection(const QTextCursor &cursorIn,
const CommentDefinition &definition, const CommentDefinition &definition,
bool preferSingleLine) bool preferSingleLine)
{ {
if (!definition.isValid()) if (!definition.isValid())
return cursorIn; return cursorIn;
@@ -244,3 +248,31 @@ QTextCursor Utils::unCommentSelection(const QTextCursor &cursorIn,
} }
return cursor; return cursor;
} }
MultiTextCursor unCommentSelection(const MultiTextCursor &cursorIn,
const CommentDefinition &definiton,
bool preferSingleLine)
{
if (cursorIn.isNull())
return cursorIn;
if (!cursorIn.hasMultipleCursors())
return MultiTextCursor({unCommentSelection(cursorIn.mainCursor(), definiton, preferSingleLine)});
QMap<int, QTextCursor> cursors;
for (const QTextCursor &c : cursorIn) {
QTextBlock block = c.document()->findBlock(c.selectionStart());
QTC_ASSERT(block.isValid(), continue);
QTextBlock end = c.document()->findBlock(c.selectionEnd());
QTC_ASSERT(end.isValid(), continue);
end = end.next();
while (block != end && block.isValid()) {
if (!cursors.contains(block.blockNumber()))
cursors.insert(block.blockNumber(), QTextCursor(block));
block = block.next();
}
}
for (const QTextCursor &c : cursors)
unCommentSelection(c, definiton, /*always prefer single line for multi cursor*/ true);
return cursorIn;
}
} // namespace Utils

View File

@@ -36,6 +36,8 @@ QT_END_NAMESPACE
namespace Utils { namespace Utils {
class MultiTextCursor;
class QTCREATOR_UTILS_EXPORT CommentDefinition class QTCREATOR_UTILS_EXPORT CommentDefinition
{ {
public: public:
@@ -62,4 +64,9 @@ QTextCursor unCommentSelection(const QTextCursor &cursor,
const CommentDefinition &definiton = CommentDefinition(), const CommentDefinition &definiton = CommentDefinition(),
bool preferSingleLine = false); bool preferSingleLine = false);
QTCREATOR_UTILS_EXPORT
MultiTextCursor unCommentSelection(const MultiTextCursor &cursor,
const CommentDefinition &definiton = CommentDefinition(),
bool preferSingleLine = false);
} // namespace Utils } // namespace Utils

View File

@@ -142,6 +142,7 @@ SOURCES += \
$$PWD/qtcsettings.cpp \ $$PWD/qtcsettings.cpp \
$$PWD/link.cpp \ $$PWD/link.cpp \
$$PWD/linecolumn.cpp \ $$PWD/linecolumn.cpp \
$$PWD/multitextcursor.cpp \
HEADERS += \ HEADERS += \
$$PWD/environmentfwd.h \ $$PWD/environmentfwd.h \
@@ -307,6 +308,7 @@ HEADERS += \
$$PWD/launcherpackets.h \ $$PWD/launcherpackets.h \
$$PWD/launchersocket.h \ $$PWD/launchersocket.h \
$$PWD/qtcsettings.h $$PWD/qtcsettings.h
$$PWD/multitextcursor.h \
FORMS += $$PWD/filewizardpage.ui \ FORMS += $$PWD/filewizardpage.ui \
$$PWD/projectintropage.ui \ $$PWD/projectintropage.ui \

View File

@@ -177,6 +177,8 @@ Project {
"macroexpander.cpp", "macroexpander.cpp",
"macroexpander.h", "macroexpander.h",
"mapreduce.h", "mapreduce.h",
"multitextcursor.cpp",
"multitextcursor.h",
"namevaluedictionary.cpp", "namevaluedictionary.cpp",
"namevaluedictionary.h", "namevaluedictionary.h",
"namevalueitem.cpp", "namevalueitem.cpp",

View File

@@ -25,6 +25,7 @@
#include "basetextfind.h" #include "basetextfind.h"
#include <utils/algorithm.h>
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include <utils/filesearch.h> #include <utils/filesearch.h>
@@ -36,13 +37,13 @@
namespace Core { namespace Core {
static QRegularExpression regularExpression(const QString &txt, FindFlags flags) QRegularExpression BaseTextFind::regularExpression(const QString &txt, FindFlags flags)
{ {
return QRegularExpression( return QRegularExpression((flags & FindRegularExpression) ? txt
(flags & FindRegularExpression) ? txt : QRegularExpression::escape(txt),
: QRegularExpression::escape(txt), (flags & FindCaseSensitively)
(flags & FindCaseSensitively) ? QRegularExpression::NoPatternOption ? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption); : QRegularExpression::CaseInsensitiveOption);
} }
struct BaseTextFindPrivate struct BaseTextFindPrivate
@@ -53,10 +54,8 @@ struct BaseTextFindPrivate
QPointer<QTextEdit> m_editor; QPointer<QTextEdit> m_editor;
QPointer<QPlainTextEdit> m_plaineditor; QPointer<QPlainTextEdit> m_plaineditor;
QPointer<QWidget> m_widget; QPointer<QWidget> m_widget;
QTextCursor m_findScopeStart; Utils::MultiTextCursor m_scope;
QTextCursor m_findScopeEnd; std::function<Utils::MultiTextCursor()> m_cursorProvider;
int m_findScopeVerticalBlockSelectionFirstColumn;
int m_findScopeVerticalBlockSelectionLastColumn;
int m_incrementalStartPos; int m_incrementalStartPos;
bool m_incrementalWrappedState; bool m_incrementalWrappedState;
}; };
@@ -64,8 +63,6 @@ struct BaseTextFindPrivate
BaseTextFindPrivate::BaseTextFindPrivate(QTextEdit *editor) BaseTextFindPrivate::BaseTextFindPrivate(QTextEdit *editor)
: m_editor(editor) : m_editor(editor)
, m_widget(editor) , m_widget(editor)
, m_findScopeVerticalBlockSelectionFirstColumn(-1)
, m_findScopeVerticalBlockSelectionLastColumn(-1)
, m_incrementalStartPos(-1) , m_incrementalStartPos(-1)
, m_incrementalWrappedState(false) , m_incrementalWrappedState(false)
{ {
@@ -74,8 +71,6 @@ BaseTextFindPrivate::BaseTextFindPrivate(QTextEdit *editor)
BaseTextFindPrivate::BaseTextFindPrivate(QPlainTextEdit *editor) BaseTextFindPrivate::BaseTextFindPrivate(QPlainTextEdit *editor)
: m_plaineditor(editor) : m_plaineditor(editor)
, m_widget(editor) , m_widget(editor)
, m_findScopeVerticalBlockSelectionFirstColumn(-1)
, m_findScopeVerticalBlockSelectionLastColumn(-1)
, m_incrementalStartPos(-1) , m_incrementalStartPos(-1)
, m_incrementalWrappedState(false) , m_incrementalWrappedState(false)
{ {
@@ -93,15 +88,10 @@ BaseTextFindPrivate::BaseTextFindPrivate(QPlainTextEdit *editor)
*/ */
/*! /*!
\fn void Core::BaseTextFind::findScopeChanged(const QTextCursor &start, \fn void Core::BaseTextFind::findScopeChanged(const Utils::MultiTextCursor &cursor)
const QTextCursor &end,
int verticalBlockSelectionFirstColumn,
int verticalBlockSelectionLastColumn)
This signal is emitted when the search This signal is emitted when the search
scope changes to \a start, \a end, scope changes to \a cursor.
\a verticalBlockSelectionFirstColumn, and
\a verticalBlockSelectionLastColumn.
*/ */
/*! /*!
@@ -324,6 +314,13 @@ QTextCursor BaseTextFind::replaceInternal(const QString &before, const QString &
return cursor; return cursor;
} }
Utils::MultiTextCursor BaseTextFind::multiTextCursor() const
{
if (d->m_cursorProvider)
return d->m_cursorProvider();
return Utils::MultiTextCursor({textCursor()});
}
/*! /*!
\reimp \reimp
*/ */
@@ -344,10 +341,11 @@ bool BaseTextFind::replaceStep(const QString &before, const QString &after, Find
int BaseTextFind::replaceAll(const QString &before, const QString &after, FindFlags findFlags) int BaseTextFind::replaceAll(const QString &before, const QString &after, FindFlags findFlags)
{ {
QTextCursor editCursor = textCursor(); QTextCursor editCursor = textCursor();
if (!d->m_findScopeStart.isNull()) if (findFlags.testFlag(FindBackward))
editCursor.setPosition(d->m_findScopeStart.position()); editCursor.movePosition(QTextCursor::End);
else else
editCursor.movePosition(QTextCursor::Start); editCursor.movePosition(QTextCursor::Start);
editCursor.movePosition(QTextCursor::Start);
editCursor.beginEditBlock(); editCursor.beginEditBlock();
int count = 0; int count = 0;
bool usesRegExp = (findFlags & FindRegularExpression); bool usesRegExp = (findFlags & FindRegularExpression);
@@ -355,7 +353,7 @@ int BaseTextFind::replaceAll(const QString &before, const QString &after, FindFl
QRegularExpression regexp = regularExpression(before, findFlags); QRegularExpression regexp = regularExpression(before, findFlags);
QTextCursor found = findOne(regexp, editCursor, textDocumentFlagsForFindFlags(findFlags)); QTextCursor found = findOne(regexp, editCursor, textDocumentFlagsForFindFlags(findFlags));
bool first = true; bool first = true;
while (!found.isNull() && inScope(found.selectionStart(), found.selectionEnd())) { while (!found.isNull()) {
if (found == editCursor && !first) { if (found == editCursor && !first) {
if (editCursor.atEnd()) if (editCursor.atEnd())
break; break;
@@ -390,8 +388,7 @@ int BaseTextFind::replaceAll(const QString &before, const QString &after, FindFl
return count; return count;
} }
bool BaseTextFind::find(const QString &txt, FindFlags findFlags, bool BaseTextFind::find(const QString &txt, FindFlags findFlags, QTextCursor start, bool *wrapped)
QTextCursor start, bool *wrapped)
{ {
if (txt.isEmpty()) { if (txt.isEmpty()) {
setTextCursor(start); setTextCursor(start);
@@ -402,85 +399,52 @@ bool BaseTextFind::find(const QString &txt, FindFlags findFlags,
if (wrapped) if (wrapped)
*wrapped = false; *wrapped = false;
if (!d->m_findScopeStart.isNull()) { if (found.isNull()) {
if ((findFlags & FindBackward) == 0)
// scoped start.movePosition(QTextCursor::Start);
if (found.isNull() || !inScope(found.selectionStart(), found.selectionEnd())) { else
if ((findFlags & FindBackward) == 0) start.movePosition(QTextCursor::End);
start.setPosition(d->m_findScopeStart.position()); found = findOne(regexp, start, textDocumentFlagsForFindFlags(findFlags));
else if (found.isNull())
start.setPosition(d->m_findScopeEnd.position()); return false;
found = findOne(regexp, start, textDocumentFlagsForFindFlags(findFlags)); if (wrapped)
if (found.isNull() || !inScope(found.selectionStart(), found.selectionEnd())) *wrapped = true;
return false;
if (wrapped)
*wrapped = true;
}
} else {
// entire document
if (found.isNull()) {
if ((findFlags & FindBackward) == 0)
start.movePosition(QTextCursor::Start);
else
start.movePosition(QTextCursor::End);
found = findOne(regexp, start, textDocumentFlagsForFindFlags(findFlags));
if (found.isNull())
return false;
if (wrapped)
*wrapped = true;
}
} }
if (!found.isNull()) setTextCursor(found);
setTextCursor(found);
return true; return true;
} }
// helper function. Works just like QTextDocument::find() but supports vertical block selection
QTextCursor BaseTextFind::findOne(const QRegularExpression &expr, QTextCursor BaseTextFind::findOne(const QRegularExpression &expr,
const QTextCursor &from, QTextDocument::FindFlags options) const QTextCursor from,
QTextDocument::FindFlags options) const
{ {
QTextCursor candidate = document()->find(expr, from, options); QTextCursor found = document()->find(expr, from, options);
if (candidate.isNull()) while (!found.isNull() && !inScope(found)) {
return candidate; if (!found.hasSelection()) {
from = found;
if (d->m_findScopeVerticalBlockSelectionFirstColumn < 0) found.movePosition(options & QTextDocument::FindBackward
return candidate;
forever {
if (!inScope(candidate.selectionStart(), candidate.selectionEnd()))
return candidate;
bool inVerticalFindScope = false;
// This code relies on the fact, that we have to keep TextEditorWidget subclass
// inside d->m_plaineditor which is of QPlainTextEdit class. So we can't
// transform it into a typed version now, as it relies on a dynamic match.
QMetaObject::invokeMethod(d->m_plaineditor, "inFindScope", Qt::DirectConnection,
Q_RETURN_ARG(bool, inVerticalFindScope),
Q_ARG(QTextCursor, candidate));
if (inVerticalFindScope)
return candidate;
QTextCursor newCandidate = document()->find(expr, candidate, options);
if (newCandidate == candidate) {
// When searching for regular expressions that match "zero length" strings (like ^ or \b)
// we need to move away from the match before searching for the next one.
candidate.movePosition(options & QTextDocument::FindBackward
? QTextCursor::PreviousCharacter ? QTextCursor::PreviousCharacter
: QTextCursor::NextCharacter); : QTextCursor::NextCharacter);
candidate = document()->find(expr, candidate, options);
} else { } else {
candidate = newCandidate; from.setPosition(options & QTextDocument::FindBackward ? found.selectionStart()
: found.selectionEnd());
} }
found = document()->find(expr, from, options);
} }
return candidate;
return found;
} }
bool BaseTextFind::inScope(int startPosition, int endPosition) const bool BaseTextFind::inScope(const QTextCursor &candidate) const
{ {
if (d->m_findScopeStart.isNull()) if (candidate.isNull())
return false;
if (d->m_scope.isNull())
return true; return true;
return (d->m_findScopeStart.position() <= startPosition return Utils::anyOf(d->m_scope, [candidate](const QTextCursor &scope){
&& d->m_findScopeEnd.position() >= endPosition); return candidate.selectionStart() >= scope.selectionStart()
&& candidate.selectionEnd() <= scope.selectionEnd();
});
} }
/*! /*!
@@ -488,30 +452,24 @@ bool BaseTextFind::inScope(int startPosition, int endPosition) const
*/ */
void BaseTextFind::defineFindScope() void BaseTextFind::defineFindScope()
{ {
QTextCursor cursor = textCursor(); Utils::MultiTextCursor multiCursor = multiTextCursor();
if (cursor.hasSelection() && cursor.block() != cursor.document()->findBlock(cursor.anchor())) { bool foundSelection = false;
d->m_findScopeStart = cursor; for (const QTextCursor &c : multiCursor) {
d->m_findScopeStart.setPosition(qMax(0, cursor.selectionStart())); if (c.hasSelection()) {
d->m_findScopeEnd = cursor; if (foundSelection || c.block() != c.document()->findBlock(c.anchor())) {
d->m_findScopeEnd.setPosition(cursor.selectionEnd()); QList<QTextCursor> sortedCursors = multiCursor.cursors();
d->m_findScopeVerticalBlockSelectionFirstColumn = -1; Utils::sort(sortedCursors);
d->m_findScopeVerticalBlockSelectionLastColumn = -1; d->m_scope = Utils::MultiTextCursor(sortedCursors);
QTextCursor cursor = textCursor();
if (d->m_plaineditor && d->m_plaineditor->metaObject()->indexOfProperty("verticalBlockSelectionFirstColumn") >= 0) { cursor.clearSelection();
d->m_findScopeVerticalBlockSelectionFirstColumn setTextCursor(cursor);
= d->m_plaineditor->property("verticalBlockSelectionFirstColumn").toInt(); emit findScopeChanged(d->m_scope);
d->m_findScopeVerticalBlockSelectionLastColumn return;
= d->m_plaineditor->property("verticalBlockSelectionLastColumn").toInt(); }
foundSelection = true;
} }
emit findScopeChanged(d->m_findScopeStart, d->m_findScopeEnd,
d->m_findScopeVerticalBlockSelectionFirstColumn,
d->m_findScopeVerticalBlockSelectionLastColumn);
cursor.setPosition(d->m_findScopeStart.position());
setTextCursor(cursor);
} else {
clearFindScope();
} }
clearFindScope();
} }
/*! /*!
@@ -519,13 +477,8 @@ void BaseTextFind::defineFindScope()
*/ */
void BaseTextFind::clearFindScope() void BaseTextFind::clearFindScope()
{ {
d->m_findScopeStart = QTextCursor(); d->m_scope = Utils::MultiTextCursor();
d->m_findScopeEnd = QTextCursor(); emit findScopeChanged(d->m_scope);
d->m_findScopeVerticalBlockSelectionFirstColumn = -1;
d->m_findScopeVerticalBlockSelectionLastColumn = -1;
emit findScopeChanged(d->m_findScopeStart, d->m_findScopeEnd,
d->m_findScopeVerticalBlockSelectionFirstColumn,
d->m_findScopeVerticalBlockSelectionLastColumn);
} }
/*! /*!
@@ -537,4 +490,9 @@ void BaseTextFind::highlightAll(const QString &txt, FindFlags findFlags)
emit highlightAllRequested(txt, findFlags); emit highlightAllRequested(txt, findFlags);
} }
void BaseTextFind::setMultiTextCursorProvider(const CursorProvider &provider)
{
d->m_cursorProvider = provider;
}
} // namespace Core } // namespace Core

View File

@@ -27,6 +27,10 @@
#include "ifindsupport.h" #include "ifindsupport.h"
#include <utils/multitextcursor.h>
#include <QRegularExpression>
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
class QPlainTextEdit; class QPlainTextEdit;
class QTextEdit; class QTextEdit;
@@ -63,22 +67,26 @@ public:
void highlightAll(const QString &txt, FindFlags findFlags) override; void highlightAll(const QString &txt, FindFlags findFlags) override;
using CursorProvider = std::function<Utils::MultiTextCursor ()>;
void setMultiTextCursorProvider(const CursorProvider &provider);
bool inScope(const QTextCursor &candidate) const;
static QRegularExpression regularExpression(const QString &txt, FindFlags flags);
signals: signals:
void highlightAllRequested(const QString &txt, Core::FindFlags findFlags); void highlightAllRequested(const QString &txt, Core::FindFlags findFlags);
void findScopeChanged(const QTextCursor &start, const QTextCursor &end, void findScopeChanged(const Utils::MultiTextCursor &cursor);
int verticalBlockSelectionFirstColumn,
int verticalBlockSelectionLastColumn);
private: private:
bool find(const QString &txt, FindFlags findFlags, QTextCursor start, bool *wrapped); bool find(const QString &txt, FindFlags findFlags, QTextCursor start, bool *wrapped);
QTextCursor replaceInternal(const QString &before, const QString &after, FindFlags findFlags); QTextCursor replaceInternal(const QString &before, const QString &after, FindFlags findFlags);
Utils::MultiTextCursor multiTextCursor() const;
QTextCursor textCursor() const; QTextCursor textCursor() const;
void setTextCursor(const QTextCursor&); void setTextCursor(const QTextCursor&);
QTextDocument *document() const; QTextDocument *document() const;
bool isReadOnly() const; bool isReadOnly() const;
bool inScope(int startPosition, int endPosition) const; QTextCursor findOne(const QRegularExpression &expr, QTextCursor from, QTextDocument::FindFlags options) const;
QTextCursor findOne(const QRegularExpression &expr, const QTextCursor &from, QTextDocument::FindFlags options) const;
BaseTextFindPrivate *d; BaseTextFindPrivate *d;
}; };

View File

@@ -337,6 +337,9 @@ static bool trySplitComment(TextEditor::TextEditorWidget *editorWidget,
if (!settings.m_enableDoxygen && !settings.m_leadingAsterisks) if (!settings.m_enableDoxygen && !settings.m_leadingAsterisks)
return false; return false;
if (editorWidget->multiTextCursor().hasMultipleCursors())
return false;
QTextCursor cursor = editorWidget->textCursor(); QTextCursor cursor = editorWidget->textCursor();
if (!CPlusPlus::MatchingText::isInCommentHelper(cursor)) if (!CPlusPlus::MatchingText::isInCommentHelper(cursor))
return false; return false;

View File

@@ -396,7 +396,6 @@ int SideDiffEditorWidget::chunkRowsCountForBlockNumber(int blockNumber) const
void SideDiffEditorWidget::clearAll(const QString &message) void SideDiffEditorWidget::clearAll(const QString &message)
{ {
setBlockSelection(false);
clear(); clear();
clearAllData(); clearAllData();
setExtraSelections(TextEditorWidget::OtherSelection, setExtraSelections(TextEditorWidget::OtherSelection,

View File

@@ -1690,22 +1690,44 @@ void FakeVimPluginPrivate::editorOpened(IEditor *editor)
handler->requestDisableBlockSelection.connect([tew] { handler->requestDisableBlockSelection.connect([tew] {
if (tew) if (tew)
tew->setBlockSelection(false); tew->setTextCursor(tew->textCursor());
}); });
handler->requestSetBlockSelection.connect([tew](const QTextCursor &cursor) { handler->requestSetBlockSelection.connect([tew](const QTextCursor &cursor) {
if (tew) if (tew) {
tew->setBlockSelection(cursor); const TabSettings &tabs = tew->textDocument()->tabSettings();
MultiTextCursor mtc;
const bool forwardSelection = cursor.anchor() < cursor.position();
QTextBlock block = cursor.document()->findBlock(cursor.anchor());
const QTextBlock end = forwardSelection ? cursor.block().next() : cursor.block().previous();
const int anchor = tabs.columnAt(block.text(), cursor.anchor() - block.position());
const int pos = tabs.columnAt(cursor.block().text(), cursor.positionInBlock());
while (block.isValid() && block != end) {
const int columns = tabs.columnCountForText(block.text());
if (columns >= anchor || columns >= pos) {
QTextCursor c(block);
c.setPosition(block.position() + tabs.positionAtColumn(block.text(), anchor));
c.setPosition(block.position() + tabs.positionAtColumn(block.text(), pos),
QTextCursor::KeepAnchor);
mtc.addCursor(c);
}
block = forwardSelection ? block.next() : block.previous();
}
tew->setMultiTextCursor(mtc);
}
}); });
handler->requestBlockSelection.connect([tew](QTextCursor *cursor) { handler->requestBlockSelection.connect([tew](QTextCursor *cursor) {
if (tew && cursor) if (tew && cursor) {
*cursor = tew->blockSelection(); MultiTextCursor mtc = tew->multiTextCursor();
*cursor = mtc.cursors().first();
cursor->setPosition(mtc.mainCursor().position(), QTextCursor::KeepAnchor);
}
}); });
handler->requestHasBlockSelection.connect([tew](bool *on) { handler->requestHasBlockSelection.connect([tew](bool *on) {
if (tew && on) if (tew && on)
*on = tew->hasBlockSelection(); *on = tew->multiTextCursor().hasMultipleCursors();
}); });
handler->simpleCompletionRequested.connect([this, handler](const QString &needle, bool forward) { handler->simpleCompletionRequested.connect([this, handler](const QString &needle, bool forward) {

View File

@@ -164,6 +164,8 @@ void CodeAssistantPrivate::invoke(AssistKind kind, IAssistProvider *provider)
bool CodeAssistantPrivate::requestActivationCharProposal() bool CodeAssistantPrivate::requestActivationCharProposal()
{ {
if (m_editorWidget->multiTextCursor().hasMultipleCursors())
return false;
if (m_assistKind == Completion && m_settings.m_completionTrigger != ManualCompletion) { if (m_assistKind == Completion && m_settings.m_completionTrigger != ManualCompletion) {
if (CompletionAssistProvider *provider = identifyActivationSequence()) { if (CompletionAssistProvider *provider = identifyActivationSequence()) {
requestProposal(ActivationCharacter, Completion, provider); requestProposal(ActivationCharacter, Completion, provider);
@@ -197,9 +199,6 @@ void CodeAssistantPrivate::requestProposal(AssistReason reason,
if (isWaitingForProposal()) if (isWaitingForProposal())
cancelCurrentRequest(); cancelCurrentRequest();
if (m_editorWidget->hasBlockSelection())
return; // TODO
if (!provider) { if (!provider) {
if (kind == Completion) if (kind == Completion)
provider = m_editorWidget->textDocument()->completionAssistProvider(); provider = m_editorWidget->textDocument()->completionAssistProvider();
@@ -528,8 +527,11 @@ void CodeAssistantPrivate::startAutomaticProposalTimer()
void CodeAssistantPrivate::automaticProposalTimeout() void CodeAssistantPrivate::automaticProposalTimeout()
{ {
if (isWaitingForProposal() || (isDisplayingProposal() && !m_proposal->isFragile())) if (isWaitingForProposal()
|| m_editorWidget->multiTextCursor().hasMultipleCursors()
|| (isDisplayingProposal() && !m_proposal->isFragile())) {
return; return;
}
requestProposal(IdleEditor, Completion); requestProposal(IdleEditor, Completion);
} }

View File

@@ -81,10 +81,7 @@ public:
{ {
} }
QTextCursor indentOrUnindent(const QTextCursor &textCursor, bool doIndent, MultiTextCursor indentOrUnindent(const MultiTextCursor &cursor, bool doIndent, const TabSettings &tabSettings);
const TabSettings &tabSettings,
bool blockSelection = false, int column = 0,
int *offset = nullptr);
void resetRevisions(); void resetRevisions();
void updateRevisions(); void updateRevisions();
@@ -112,146 +109,90 @@ public:
Utils::Guard m_modificationChangedGuard; Utils::Guard m_modificationChangedGuard;
}; };
QTextCursor TextDocumentPrivate::indentOrUnindent(const QTextCursor &textCursor, bool doIndent, MultiTextCursor TextDocumentPrivate::indentOrUnindent(const MultiTextCursor &cursors,
const TabSettings &tabSettings, bool doIndent,
bool blockSelection, int columnIn, int *offset) const TabSettings &tabSettings)
{ {
QTextCursor cursor = textCursor; MultiTextCursor result;
cursor.beginEditBlock(); bool first = true;
for (const QTextCursor &textCursor : cursors) {
// Indent or unindent the selected lines QTextCursor cursor = textCursor;
int pos = cursor.position(); if (first) {
int column = blockSelection ? columnIn cursor.beginEditBlock();
: tabSettings.columnAt(cursor.block().text(), cursor.positionInBlock()); first = false;
int anchor = cursor.anchor();
int start = qMin(anchor, pos);
int end = qMax(anchor, pos);
bool modified = true;
QTextBlock startBlock = m_document.findBlock(start);
QTextBlock endBlock = m_document.findBlock(blockSelection ? end : qMax(end - 1, 0)).next();
const bool cursorAtBlockStart = (textCursor.position() == startBlock.position());
const bool anchorAtBlockStart = (textCursor.anchor() == startBlock.position());
const bool oneLinePartial = (startBlock.next() == endBlock)
&& (start > startBlock.position() || end < endBlock.position() - 1);
// Make sure one line selection will get processed in "for" loop
if (startBlock == endBlock)
endBlock = endBlock.next();
if (cursor.hasSelection() && !blockSelection && !oneLinePartial) {
for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
const QString text = block.text();
int indentPosition = tabSettings.lineIndentPosition(text);
if (!doIndent && !indentPosition)
indentPosition = TabSettings::firstNonSpace(text);
int targetColumn = tabSettings.indentedColumn(
tabSettings.columnAt(text, indentPosition), doIndent);
cursor.setPosition(block.position() + indentPosition);
cursor.insertText(tabSettings.indentationString(0, targetColumn, 0, block));
cursor.setPosition(block.position());
cursor.setPosition(block.position() + indentPosition, QTextCursor::KeepAnchor);
cursor.removeSelectedText();
}
// make sure that selection that begins in first column stays at first column
// even if we insert text at first column
if (cursorAtBlockStart) {
cursor = textCursor;
cursor.setPosition(startBlock.position(), QTextCursor::KeepAnchor);
} else if (anchorAtBlockStart) {
cursor = textCursor;
cursor.setPosition(startBlock.position(), QTextCursor::MoveAnchor);
cursor.setPosition(textCursor.position(), QTextCursor::KeepAnchor);
} else { } else {
modified = false; cursor.joinPreviousEditBlock();
} }
} else if (cursor.hasSelection() && !blockSelection && oneLinePartial) {
// Only one line partially selected.
cursor.removeSelectedText();
} else {
// Indent or unindent at cursor position
int maxTargetColumn = -1;
class BlockIndenter // Indent or unindent the selected lines
{ int pos = cursor.position();
public: int column = tabSettings.columnAt(cursor.block().text(), cursor.positionInBlock());
BlockIndenter(const QTextBlock &_block, int anchor = cursor.anchor();
const int column, int start = qMin(anchor, pos);
const TabSettings &_tabSettings) int end = qMax(anchor, pos);
: block(_block)
, text(block.text())
, tabSettings(_tabSettings)
{
indentPosition = tabSettings.positionAtColumn(text, column, nullptr, true);
spaces = TabSettings::spacesLeftFromPosition(text, indentPosition);
}
void indent(const int targetColumn) const QTextBlock startBlock = m_document.findBlock(start);
{ QTextBlock endBlock = m_document.findBlock(qMax(end - 1, 0)).next();
const int startColumn = tabSettings.columnAt(text, indentPosition - spaces); const bool cursorAtBlockStart = (cursor.position() == startBlock.position());
QTextCursor cursor(block); const bool anchorAtBlockStart = (cursor.anchor() == startBlock.position());
cursor.setPosition(block.position() + indentPosition); const bool oneLinePartial = (startBlock.next() == endBlock)
cursor.setPosition(block.position() + indentPosition - spaces, QTextCursor::KeepAnchor); && (start > startBlock.position()
|| end < endBlock.position() - 1)
&& !cursors.hasMultipleCursors();
// Make sure one line selection will get processed in "for" loop
if (startBlock == endBlock)
endBlock = endBlock.next();
if (cursor.hasSelection()) {
if (oneLinePartial) {
cursor.removeSelectedText(); cursor.removeSelectedText();
cursor.insertText(tabSettings.indentationString(startColumn, targetColumn, 0, block)); } else {
for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
const QString text = block.text();
int indentPosition = tabSettings.lineIndentPosition(text);
if (!doIndent && !indentPosition)
indentPosition = TabSettings::firstNonSpace(text);
int targetColumn
= tabSettings.indentedColumn(tabSettings.columnAt(text, indentPosition),
doIndent);
cursor.setPosition(block.position() + indentPosition);
cursor.insertText(tabSettings.indentationString(0, targetColumn, 0, block));
cursor.setPosition(block.position());
cursor.setPosition(block.position() + indentPosition, QTextCursor::KeepAnchor);
cursor.removeSelectedText();
}
// make sure that selection that begins in first column stays at first column
// even if we insert text at first column
if (cursorAtBlockStart) {
cursor = textCursor;
cursor.setPosition(startBlock.position(), QTextCursor::KeepAnchor);
} else if (anchorAtBlockStart) {
cursor = textCursor;
cursor.setPosition(startBlock.position(), QTextCursor::MoveAnchor);
cursor.setPosition(textCursor.position(), QTextCursor::KeepAnchor);
}
} }
} else {
int targetColumn(bool doIndent) const QString text = startBlock.text();
{ int indentPosition = tabSettings.positionAtColumn(text, column, nullptr, true);
const int optimumTargetColumn int spaces = tabSettings.spacesLeftFromPosition(text, indentPosition);
= tabSettings.indentedColumn(tabSettings.columnAt(block.text(), indentPosition), int startColumn = tabSettings.columnAt(text, indentPosition - spaces);
doIndent); int targetColumn = tabSettings.indentedColumn(tabSettings.columnAt(text, indentPosition),
const int minimumTargetColumn = tabSettings.columnAt(text, indentPosition - spaces); doIndent);
return std::max(optimumTargetColumn, minimumTargetColumn); cursor.setPosition(startBlock.position() + indentPosition);
} cursor.setPosition(startBlock.position() + indentPosition - spaces,
QTextCursor::KeepAnchor);
const QTextBlock &textBlock() { return block; } cursor.removeSelectedText();
cursor.insertText(
private: tabSettings.indentationString(startColumn, targetColumn, 0, startBlock));
QTextBlock block;
const QString text;
int indentPosition;
int spaces;
const TabSettings &tabSettings;
};
std::vector<BlockIndenter> blocks;
for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
QString text = block.text();
const int blockColumn = tabSettings.columnAt(text, text.size());
if (blockColumn < column) {
cursor.setPosition(block.position() + text.size());
cursor.insertText(tabSettings.indentationString(blockColumn, column, 0, block));
text = block.text();
}
blocks.emplace_back(BlockIndenter(block, column, tabSettings));
maxTargetColumn = std::max(maxTargetColumn, blocks.back().targetColumn(doIndent));
} }
for (const BlockIndenter &blockIndenter : blocks)
blockIndenter.indent(maxTargetColumn);
// Preserve initial anchor of block selection cursor.endEditBlock();
if (blockSelection) { result.addCursor(cursor);
if (offset)
*offset = maxTargetColumn - column;
startBlock = pos < anchor ? blocks.front().textBlock() : blocks.back().textBlock();
start = startBlock.position()
+ tabSettings.positionAtColumn(startBlock.text(), maxTargetColumn);
endBlock = pos > anchor ? blocks.front().textBlock() : blocks.back().textBlock();
end = endBlock.position()
+ tabSettings.positionAtColumn(endBlock.text(), maxTargetColumn);
cursor.setPosition(end);
cursor.setPosition(start, QTextCursor::KeepAnchor);
}
} }
cursor.endEditBlock(); return result;
return modified ? cursor : textCursor;
} }
void TextDocumentPrivate::resetRevisions() void TextDocumentPrivate::resetRevisions()
@@ -504,16 +445,14 @@ void TextDocument::autoFormatOrIndent(const QTextCursor &cursor)
d->m_indenter->autoIndent(cursor, tabSettings()); d->m_indenter->autoIndent(cursor, tabSettings());
} }
QTextCursor TextDocument::indent(const QTextCursor &cursor, bool blockSelection, int column, Utils::MultiTextCursor TextDocument::indent(const Utils::MultiTextCursor &cursor)
int *offset)
{ {
return d->indentOrUnindent(cursor, true, tabSettings(), blockSelection, column, offset); return d->indentOrUnindent(cursor, true, tabSettings());
} }
QTextCursor TextDocument::unindent(const QTextCursor &cursor, bool blockSelection, int column, Utils::MultiTextCursor TextDocument::unindent(const Utils::MultiTextCursor &cursor)
int *offset)
{ {
return d->indentOrUnindent(cursor, false, tabSettings(), blockSelection, column, offset); return d->indentOrUnindent(cursor, false, tabSettings());
} }
void TextDocument::setFormatter(Formatter *formatter) void TextDocument::setFormatter(Formatter *formatter)

View File

@@ -33,6 +33,7 @@
#include <utils/id.h> #include <utils/id.h>
#include <utils/link.h> #include <utils/link.h>
#include <utils/multitextcursor.h>
#include <QList> #include <QList>
#include <QMap> #include <QMap>
@@ -95,9 +96,8 @@ public:
int currentCursorPosition = -1); int currentCursorPosition = -1);
void autoReindent(const QTextCursor &cursor, int currentCursorPosition = -1); void autoReindent(const QTextCursor &cursor, int currentCursorPosition = -1);
void autoFormatOrIndent(const QTextCursor &cursor); void autoFormatOrIndent(const QTextCursor &cursor);
QTextCursor indent(const QTextCursor &cursor, bool blockSelection, int column, int *offset); Utils::MultiTextCursor indent(const Utils::MultiTextCursor &cursor);
QTextCursor unindent(const QTextCursor &cursor, bool blockSelection = false, int column = 0, Utils::MultiTextCursor unindent(const Utils::MultiTextCursor &cursor);
int *offset = nullptr);
void setFormatter(Formatter *indenter); // transfers ownership void setFormatter(Formatter *indenter); // transfers ownership
void autoFormat(const QTextCursor &cursor); void autoFormat(const QTextCursor &cursor);

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,7 @@
#include <utils/elidinglabel.h> #include <utils/elidinglabel.h>
#include <utils/link.h> #include <utils/link.h>
#include <utils/multitextcursor.h>
#include <utils/uncommentselection.h> #include <utils/uncommentselection.h>
#include <QPlainTextEdit> #include <QPlainTextEdit>
@@ -177,9 +178,6 @@ private:
class TEXTEDITOR_EXPORT TextEditorWidget : public QPlainTextEdit class TEXTEDITOR_EXPORT TextEditorWidget : public QPlainTextEdit
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(int verticalBlockSelectionFirstColumn READ verticalBlockSelectionFirstColumn)
Q_PROPERTY(int verticalBlockSelectionLastColumn READ verticalBlockSelectionLastColumn)
public: public:
explicit TextEditorWidget(QWidget *parent = nullptr); explicit TextEditorWidget(QWidget *parent = nullptr);
~TextEditorWidget() override; ~TextEditorWidget() override;
@@ -271,15 +269,8 @@ public:
const QString &snippet, const QString &snippet,
const SnippetParser &parse); const SnippetParser &parse);
void setBlockSelection(bool on); Utils::MultiTextCursor multiTextCursor() const;
void setBlockSelection(int positionBlock, int positionColumn, int anchhorBlock, void setMultiTextCursor(const Utils::MultiTextCursor &cursor);
int anchorColumn);
void setBlockSelection(const QTextCursor &cursor);
QTextCursor blockSelection() const;
bool hasBlockSelection() const;
int verticalBlockSelectionFirstColumn() const;
int verticalBlockSelectionLastColumn() const;
QRegion translatedLineRegion(int lineStart, int lineEnd) const; QRegion translatedLineRegion(int lineStart, int lineEnd) const;
@@ -325,6 +316,7 @@ public:
static Utils::Id AutoCompleteSelection; static Utils::Id AutoCompleteSelection;
static Utils::Id CodeWarningsSelection; static Utils::Id CodeWarningsSelection;
static Utils::Id CodeSemanticsSelection; static Utils::Id CodeSemanticsSelection;
static Utils::Id CursorSelection;
static Utils::Id UndefinedSymbolSelection; static Utils::Id UndefinedSymbolSelection;
static Utils::Id UnusedSymbolSelection; static Utils::Id UnusedSymbolSelection;
static Utils::Id OtherSelection; static Utils::Id OtherSelection;
@@ -512,7 +504,6 @@ protected:
QTextBlock blockForVerticalOffset(int offset) const; QTextBlock blockForVerticalOffset(int offset) const;
bool event(QEvent *e) override; bool event(QEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override; void contextMenuEvent(QContextMenuEvent *e) override;
void inputMethodEvent(QInputMethodEvent *e) override;
void keyPressEvent(QKeyEvent *e) override; void keyPressEvent(QKeyEvent *e) override;
void wheelEvent(QWheelEvent *e) override; void wheelEvent(QWheelEvent *e) override;
void changeEvent(QEvent *e) override; void changeEvent(QEvent *e) override;
@@ -542,6 +533,7 @@ protected:
void dropEvent(QDropEvent *e) override; void dropEvent(QDropEvent *e) override;
virtual QString plainTextFromSelection(const QTextCursor &cursor) const; virtual QString plainTextFromSelection(const QTextCursor &cursor) const;
virtual QString plainTextFromSelection(const Utils::MultiTextCursor &cursor) const;
static QString convertToPlainText(const QString &txt); static QString convertToPlainText(const QString &txt);
virtual QString lineNumber(int blockNumber) const; virtual QString lineNumber(int blockNumber) const;
@@ -602,7 +594,7 @@ protected:
const QRect &clip); const QRect &clip);
int visibleFoldedBlockNumber() const; int visibleFoldedBlockNumber() const;
void doSetTextCursor(const QTextCursor &cursor) override; void doSetTextCursor(const QTextCursor &cursor) override;
void doSetTextCursor(const QTextCursor &cursor, bool keepBlockSelection); void doSetTextCursor(const QTextCursor &cursor, bool keepMultiSelection);
signals: signals:
void markRequested(TextEditor::TextEditorWidget *widget, void markRequested(TextEditor::TextEditorWidget *widget,
@@ -619,7 +611,6 @@ protected:
virtual void slotCodeStyleSettingsChanged(const QVariant &); // Used in CppEditor virtual void slotCodeStyleSettingsChanged(const QVariant &); // Used in CppEditor
Q_INVOKABLE bool inFindScope(const QTextCursor &cursor); Q_INVOKABLE bool inFindScope(const QTextCursor &cursor);
Q_INVOKABLE bool inFindScope(int selectionStart, int selectionEnd);
private: private:
Internal::TextEditorWidgetPrivate *d; Internal::TextEditorWidgetPrivate *d;

View File

@@ -37,37 +37,6 @@ class TextDocument;
namespace Internal { namespace Internal {
class TEXTEDITOR_EXPORT TextBlockSelection
{
public:
TextBlockSelection() = default;
TextBlockSelection(const TextBlockSelection &other);
void clear();
QTextCursor selection(const TextDocument *baseTextDocument) const;
QTextCursor cursor(const TextDocument *baseTextDocument) const;
void fromPostition(int positionBlock, int positionColumn, int anchorBlock, int anchorColumn);
bool hasSelection() { return !(positionBlock == anchorBlock && positionColumn == anchorColumn); }
// defines the first block
inline int firstBlockNumber() const { return qMin(positionBlock, anchorBlock); }
// defines the last block
inline int lastBlockNumber() const { return qMax(positionBlock, anchorBlock); }
// defines the first visual column of the selection
inline int firstVisualColumn() const { return qMin(positionColumn, anchorColumn); }
// defines the last visual column of the selection
inline int lastVisualColumn() const { return qMax(positionColumn, anchorColumn); }
public:
int positionBlock = 0;
int positionColumn = 0;
int anchorBlock = 0;
int anchorColumn = 0;
private:
QTextCursor cursor(const TextDocument *baseTextDocument, bool fullSelection) const;
};
// //
// TextEditorPrivate // TextEditorPrivate
// //

View File

@@ -40,464 +40,6 @@
using namespace TextEditor; using namespace TextEditor;
enum TransFormationType { Uppercase, Lowercase };
struct TestBlockSelection
{
int positionBlock = 0;
int positionColumn = 0;
int anchorBlock = 0;
int anchorColumn = 0;
TestBlockSelection(int positionBlock, int positionColumn, int anchorBlock, int anchorColumn)
: positionBlock(positionBlock), positionColumn(positionColumn)
, anchorBlock(anchorBlock), anchorColumn(anchorColumn) {}
TestBlockSelection() = default;
};
Q_DECLARE_METATYPE(TransFormationType)
Q_DECLARE_METATYPE(TestBlockSelection)
void Internal::TextEditorPlugin::testBlockSelectionTransformation_data()
{
QTest::addColumn<QString>("input");
QTest::addColumn<QString>("transformedText");
QTest::addColumn<TestBlockSelection>("selection");
QTest::addColumn<TransFormationType>("type");
QTest::newRow("uppercase")
<< QString::fromLatin1("aaa\nbbb\nccc\n")
<< QString::fromLatin1("aAa\nbBb\ncCc\n")
<< TestBlockSelection(0, 1, 2, 2)
<< Uppercase;
QTest::newRow("lowercase")
<< QString::fromLatin1("AAA\nBBB\nCCC\n")
<< QString::fromLatin1("AaA\nBbB\nCcC\n")
<< TestBlockSelection(0, 1, 2, 2)
<< Lowercase;
QTest::newRow("uppercase_leading_tabs")
<< QString::fromLatin1("\taaa\n" "\tbbb\n" "\tccc\n")
<< QString::fromLatin1("\taAa\n" "\tbBb\n" "\tcCc\n")
<< TestBlockSelection(0, 9, 2, 10)
<< Uppercase;
QTest::newRow("lowercase_leading_tabs")
<< QString::fromLatin1("\tAAA\n\tBBB\n\tCCC\n")
<< QString::fromLatin1("\tAaA\n\tBbB\n\tCcC\n")
<< TestBlockSelection(0, 9, 2, 10)
<< Lowercase;
QTest::newRow("uppercase_mixed_leading_whitespace")
<< QString::fromLatin1("\taaa\n bbbbb\n ccccc\n")
<< QString::fromLatin1("\tAaa\n bbbbB\n ccccC\n")
<< TestBlockSelection(0, 8, 2, 9)
<< Uppercase;
QTest::newRow("lowercase_mixed_leading_whitespace")
<< QString::fromLatin1("\tAAA\n BBBBB\n CCCCC\n")
<< QString::fromLatin1("\taAA\n BBBBb\n CCCCc\n")
<< TestBlockSelection(0, 8, 2, 9)
<< Lowercase;
QTest::newRow("uppercase_mid_tabs1")
<< QString::fromLatin1("a\ta\nbbbbbbbbb\nccccccccc\n")
<< QString::fromLatin1("a\ta\nbBBBBBbbb\ncCCCCCccc\n")
<< TestBlockSelection(0, 1, 2, 6)
<< Uppercase;
QTest::newRow("lowercase_mid_tabs2")
<< QString::fromLatin1("AA\taa\n\t\t\nccccCCCCCC\n")
<< QString::fromLatin1("Aa\taa\n\t\t\ncccccccccC\n")
<< TestBlockSelection(0, 1, 2, 9)
<< Lowercase;
QTest::newRow("uppercase_over_line_ending")
<< QString::fromLatin1("aaaaa\nbbb\nccccc\n")
<< QString::fromLatin1("aAAAa\nbBB\ncCCCc\n")
<< TestBlockSelection(0, 1, 2, 4)
<< Uppercase;
QTest::newRow("lowercase_over_line_ending")
<< QString::fromLatin1("AAAAA\nBBB\nCCCCC\n")
<< QString::fromLatin1("AaaaA\nBbb\nCcccC\n")
<< TestBlockSelection(0, 1, 2, 4)
<< Lowercase;
}
void Internal::TextEditorPlugin::testBlockSelectionTransformation()
{
// fetch test data
QFETCH(QString, input);
QFETCH(QString, transformedText);
QFETCH(TestBlockSelection, selection);
QFETCH(TransFormationType, type);
// open editor
Core::IEditor *editor = Core::EditorManager::openEditorWithContents(
Core::Constants::K_DEFAULT_TEXT_EDITOR_ID, nullptr, input.toLatin1());
QVERIFY(editor);
if (auto textEditor = qobject_cast<BaseTextEditor*>(editor)) {
TextEditorWidget *editorWidget = textEditor->editorWidget();
editorWidget->setBlockSelection(selection.positionBlock,
selection.positionColumn,
selection.anchorBlock,
selection.anchorColumn);
editorWidget->update();
// transform blockselection
switch (type) {
case Uppercase:
editorWidget->uppercaseSelection();
break;
case Lowercase:
editorWidget->lowercaseSelection();
break;
}
QCOMPARE(textEditor->textDocument()->plainText(), transformedText);
}
Core::EditorManager::closeDocuments({editor->document()}, false);
}
static const char text[] =
"first line\n"
"second line\n"
"third line\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n";
void Internal::TextEditorPlugin::testBlockSelectionInsert_data()
{
QTest::addColumn<QString>("transformedText");
QTest::addColumn<TestBlockSelection>("selection");
QTest::addColumn<QString>("insertText");
QTest::newRow("insert")
<< QString::fromLatin1(".first line\n"
".second line\n"
".third line\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(0, 0, 2, 0)
<< QString::fromLatin1(".");
QTest::newRow("insert after line end 1")
<< QString::fromLatin1("first line\n"
"second line\n"
"third. line\n"
" .\n"
"longe.st line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(2, 5, 4, 5)
<< QString::fromLatin1(".");
QTest::newRow("insert after line end 2")
<< QString::fromLatin1("first line .\n"
"second line .\n"
"third line .\n"
" .\n"
"longest line in this text .\n"
" leading space .\n"
"" "\tleading tab .\n"
"mid\t" "tab .\n"
"endtab\t .\n")
<< TestBlockSelection(0, 32, 8, 32)
<< QString::fromLatin1(".");
QTest::newRow("insert in leading tab")
<< QString::fromLatin1("first line\n"
"second line\n"
"third line\n"
"\n"
"lo.ngest line in this text\n"
" . leading space\n"
" . leading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(4, 2, 6, 2)
<< QString::fromLatin1(".");
QTest::newRow("insert in middle tab")
<< QString::fromLatin1("first line\n"
"second line\n"
"third line\n"
"\n"
"longest line in this text\n"
" leading space\n"
" . leading tab\n"
"mid . tab\n"
"endta.b\t\n")
<< TestBlockSelection(6, 5, 8, 5)
<< QString::fromLatin1(".");
QTest::newRow("insert same block count with all blocks same length")
<< QString::fromLatin1(".first line\n"
".second line\n"
".third line\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(0, 0, 2, 0)
<< QString::fromLatin1(".\n.\n.");
QTest::newRow("insert same block count with all blocks different length")
<< QString::fromLatin1(". first line\n"
".. second line\n"
"...third line\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(0, 0, 2, 0)
<< QString::fromLatin1(".\n..\n...");
QTest::newRow("insert same block count with some blocks containing tabs")
<< QString::fromLatin1(". first line\n"
".\t second line\n"
".\t.third line\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(0, 0, 2, 0)
<< QString::fromLatin1(".\n.\t\n.\t.");
QTest::newRow("insert same block count with some blocks containing tabs in mid line")
<< QString::fromLatin1("fi. rst line\n"
"se.\t cond line\n"
"th.\t.ird line\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(0, 2, 2, 2)
<< QString::fromLatin1(".\n.\t\n.\t.");
QTest::newRow("insert different block count with all blocks same length")
<< QString::fromLatin1(".first line\n"
".\n"
".second line\n"
".\n"
".third line\n"
".\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(0, 0, 2, 0)
<< QString::fromLatin1(".\n.");
QTest::newRow("insert different block count with all blocks different length")
<< QString::fromLatin1(". first line\n"
"..\n"
". second line\n"
"..\n"
". third line\n"
"..\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(0, 0, 2, 0)
<< QString::fromLatin1(".\n..");
QTest::newRow("insert different block count with some blocks containing tabs")
<< QString::fromLatin1(". first line\n"
".\t \n"
".\t.\n"
". second line\n"
".\t \n"
".\t.\n"
"third line\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(0, 0, 1, 0)
<< QString::fromLatin1(".\n.\t\n.\t.");
QTest::newRow("insert different block count with some blocks containing tabs in mid line")
<< QString::fromLatin1("fi. rst line\n"
" .\t \n"
" .\t.\n"
"se. cond line\n"
" .\t \n"
" .\t.\n"
"third line\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(0, 2, 1, 2)
<< QString::fromLatin1(".\n.\t\n.\t.");
}
void Internal::TextEditorPlugin::testBlockSelectionInsert()
{
// fetch test data
QFETCH(QString, transformedText);
QFETCH(TestBlockSelection, selection);
QFETCH(QString, insertText);
// open editor
Core::IEditor *editor = Core::EditorManager::openEditorWithContents(
Core::Constants::K_DEFAULT_TEXT_EDITOR_ID, nullptr, text);
QVERIFY(editor);
if (auto textEditor = qobject_cast<BaseTextEditor*>(editor)) {
TextEditorWidget *editorWidget = textEditor->editorWidget();
editorWidget->setBlockSelection(selection.positionBlock,
selection.positionColumn,
selection.anchorBlock,
selection.anchorColumn);
editorWidget->update();
editorWidget->insertPlainText(insertText);
QCOMPARE(textEditor->textDocument()->plainText(), transformedText);
}
Core::EditorManager::closeDocuments({editor->document()}, false);
}
void Internal::TextEditorPlugin::testBlockSelectionRemove_data()
{
QTest::addColumn<QString>("transformedText");
QTest::addColumn<TestBlockSelection>("selection");
QTest::newRow("Delete")
<< QString::fromLatin1("first ine\n"
"second ine\n"
"third ine\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(0, 7, 2, 8);
QTest::newRow("Delete All")
<< QString::fromLatin1("\n\n\n\n\n\n\n\n\n")
<< TestBlockSelection(0, 0, 8, 30);
QTest::newRow("Delete Inside Tab")
<< QString::fromLatin1("first line\n"
"second line\n"
"third line\n"
"\n"
"longest line in this text\n"
" leading space\n"
" leading tab\n"
"mi\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(5, 2, 7, 3);
QTest::newRow("Delete around Tab")
<< QString::fromLatin1("first line\n"
"second line\n"
"third line\n"
"\n"
"longest line in this text\n"
" ng space\n"
" eading tab\n"
"miab\n"
"endtab\t\n")
<< TestBlockSelection(5, 2, 7, 9);
QTest::newRow("Delete behind text")
<< QString::fromLatin1("first line\n"
"second line\n"
"third line\n"
"\n"
"longest line in this text\n"
" leading space\n"
"" "\tleading tab\n"
"mid\t" "tab\n"
"endtab\t\n")
<< TestBlockSelection(0, 30, 8, 35);
}
void Internal::TextEditorPlugin::testBlockSelectionRemove()
{
// fetch test data
QFETCH(QString, transformedText);
QFETCH(TestBlockSelection, selection);
// open editor
Core::IEditor *editor = Core::EditorManager::openEditorWithContents(
Core::Constants::K_DEFAULT_TEXT_EDITOR_ID, nullptr, text);
QVERIFY(editor);
if (auto textEditor = qobject_cast<BaseTextEditor*>(editor)) {
TextEditorWidget *editorWidget = textEditor->editorWidget();
editorWidget->setBlockSelection(selection.positionBlock,
selection.positionColumn,
selection.anchorBlock,
selection.anchorColumn);
editorWidget->update();
editorWidget->insertPlainText(QString());
QCOMPARE(textEditor->textDocument()->plainText(), transformedText);
}
Core::EditorManager::closeDocuments({editor->document()}, false);
}
void Internal::TextEditorPlugin::testBlockSelectionCopy_data()
{
QTest::addColumn<QString>("copiedText");
QTest::addColumn<TestBlockSelection>("selection");
QTest::newRow("copy")
<< QString::fromLatin1("lin\n"
"lin\n"
"lin")
<< TestBlockSelection(0, 7, 2, 10);
QTest::newRow("copy over line end")
<< QString::fromLatin1("ond line \n"
"rd line \n"
" ")
<< TestBlockSelection(1, 3, 3, 15);
QTest::newRow("copy inside tab")
<< QString::fromLatin1("ond line \n"
"rd line \n"
" ")
<< TestBlockSelection(1, 3, 3, 15);
QTest::newRow("copy start in tab")
<< QString::fromLatin1("gest lin\n"
" leading\n"
" lea")
<< TestBlockSelection(4, 3, 6, 11);
QTest::newRow("copy around tab")
<< QString::fromLatin1(" leadin\n"
" le\n"
"d\tta")
<< TestBlockSelection(5, 2, 7, 10);
}
void Internal::TextEditorPlugin::testBlockSelectionCopy()
{
// fetch test data
QFETCH(QString, copiedText);
QFETCH(TestBlockSelection, selection);
// open editor
Core::IEditor *editor = Core::EditorManager::openEditorWithContents(
Core::Constants::K_DEFAULT_TEXT_EDITOR_ID, nullptr, text);
QVERIFY(editor);
if (auto textEditor = qobject_cast<BaseTextEditor*>(editor)) {
TextEditorWidget *editorWidget = textEditor->editorWidget();
editorWidget->setBlockSelection(selection.positionBlock,
selection.positionColumn,
selection.anchorBlock,
selection.anchorColumn);
editorWidget->update();
editorWidget->copy();
QCOMPARE(QGuiApplication::clipboard()->text(), copiedText);
}
Core::EditorManager::closeDocuments({editor->document()}, false);
}
QString tabPolicyToString(TabSettings::TabPolicy policy) QString tabPolicyToString(TabSettings::TabPolicy policy)
{ {
switch (policy) { switch (policy) {

View File

@@ -57,15 +57,6 @@ private slots:
void testSnippetParsing_data(); void testSnippetParsing_data();
void testSnippetParsing(); void testSnippetParsing();
void testBlockSelectionTransformation_data();
void testBlockSelectionTransformation();
void testBlockSelectionInsert_data();
void testBlockSelectionInsert();
void testBlockSelectionRemove_data();
void testBlockSelectionRemove();
void testBlockSelectionCopy_data();
void testBlockSelectionCopy();
void testIndentationClean_data(); void testIndentationClean_data();
void testIndentationClean(); void testIndentationClean();
#endif #endif

View File

@@ -89,7 +89,9 @@ bool TypingSettings::equals(const TypingSettings &ts) const
&& m_preferSingleLineComments == ts.m_preferSingleLineComments; && m_preferSingleLineComments == ts.m_preferSingleLineComments;
} }
bool TypingSettings::tabShouldIndent(const QTextDocument *document, const QTextCursor &cursor, int *suggestedPosition) const bool TypingSettings::tabShouldIndent(const QTextDocument *document,
const QTextCursor &cursor,
int *suggestedPosition) const
{ {
if (m_tabKeyBehavior == TabNeverIndents) if (m_tabKeyBehavior == TabNeverIndents)
return false; return false;

View File

@@ -8,3 +8,4 @@ add_subdirectory(settings)
add_subdirectory(stringutils) add_subdirectory(stringutils)
add_subdirectory(templateengine) add_subdirectory(templateengine)
add_subdirectory(treemodel) add_subdirectory(treemodel)
add_subdirectory(multicursor)

View File

@@ -0,0 +1,4 @@
add_qtc_test(tst_utils_multicursor
DEPENDS Utils
SOURCES tst_multicursor.cpp
)

View File

@@ -0,0 +1,4 @@
QTC_LIB_DEPENDS += utils
include(../../qttest.pri)
SOURCES += tst_multicursor.cpp

View File

@@ -0,0 +1,7 @@
import qbs
QtcAutotest {
name: "MultiTextCursor autotest"
Depends { name: "Utils" }
files: "tst_multicursor.cpp"
}

View File

@@ -0,0 +1,352 @@
/****************************************************************************
**
** 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 <utils/algorithm.h>
#include <utils/multitextcursor.h>
#include <QPlainTextEdit>
#include <QtTest>
using namespace Utils;
class tst_MultiCursor : public QObject
{
Q_OBJECT
public:
tst_MultiCursor();
private slots:
void init();
void initTestCase() { init(); }
void testMultiCursor_data();
void testMultiCursor();
void testMultiCursorMerge_data();
void testMultiCursorMerge();
void testMultiCursorMove_data();
void testMultiCursorMove();
void testMultiCursorSelection_data();
void testMultiCursorSelection();
void testMultiCursorInsert_data();
void testMultiCursorInsert();
private:
QString m_text;
QPlainTextEdit m_edit;
};
tst_MultiCursor::tst_MultiCursor()
{
m_text =
R"(You can move directly to the definition or the declaration of a symbol in the Edit mode
by holding the Ctrl key and clicking the symbol.
If you have multiple splits opened, you can open the link in the next split by holding
Ctrl and Alt while clicking the symbol.)";
}
void tst_MultiCursor::init()
{
m_edit.setPlainText(m_text);
}
class Cursor : public QPair<int, int>
{
public:
Cursor() = default;
explicit Cursor(const QTextCursor &c) : p(c.position(), c.anchor()) {}
QTextCursor toTextCursor(QTextDocument *doc)
{
if (p.first < 0)
return QTextCursor();
QTextCursor c(doc);
c.setPosition(p.second);
c.setPosition(p.first, QTextCursor::KeepAnchor);
return c;
};
private:
QPair<int, int> p;
};
class Cursors
{
public:
Cursors() = default;
explicit Cursors(QList<QTextCursor> cursors)
{ m_cursors = Utils::transform(cursors, [](const QTextCursor &c) { return Cursor(c); }); }
void append(const QTextCursor &c)
{ m_cursors << Cursor(c); }
QList<QTextCursor> toTextCurors(QTextDocument *doc)
{ return Utils::transform(m_cursors, [doc](Cursor c){ return c.toTextCursor(doc); }); }
private:
QList<Cursor> m_cursors;
};
Cursors _(const QList<QTextCursor> &cursors)
{
return Cursors(cursors);
}
Q_DECLARE_METATYPE(Cursor);
Q_DECLARE_METATYPE(Cursors);
void tst_MultiCursor::testMultiCursor_data()
{
QTest::addColumn<Cursors>("cursors");
QTest::addColumn<bool>("null");
QTest::addColumn<bool>("multiple");
QTest::addColumn<int>("cursorCount");
QTest::addColumn<Cursor>("mainCursor");
Cursors cursors;
QTest::addRow("No Cursor") << cursors << true << false << 0 << Cursor(QTextCursor());
QTest::addRow("Null Cursor") << _({QTextCursor()}) << true << false << 0 << Cursor(QTextCursor());
QTextCursor c1 = m_edit.textCursor();
c1.movePosition(QTextCursor::Start);
cursors.append(c1);
QTest::addRow("Single Cursor") << cursors << false << false << 1 << Cursor(c1);
QString plainText = m_edit.toPlainText();
QTextCursor c2 = m_edit.textCursor();
c2.movePosition(QTextCursor::End);
cursors.append(c2);
QTest::addRow("Multiple Cursors") << cursors << false << true << 2 << Cursor(c2);
}
void tst_MultiCursor::testMultiCursor()
{
QFETCH(Cursors, cursors);
QFETCH(bool, null);
QFETCH(bool, multiple);
QFETCH(int, cursorCount);
QFETCH(Cursor, mainCursor);
MultiTextCursor cursor(cursors.toTextCurors(m_edit.document()));
QCOMPARE(cursor.isNull(), null);
QCOMPARE(cursor.hasMultipleCursors(), multiple);
QCOMPARE(cursor.cursorCount(), cursorCount);
QCOMPARE(cursor.mainCursor(), mainCursor.toTextCursor(m_edit.document()));
MultiTextCursor cursor2;
for (const auto &c : cursors.toTextCurors(m_edit.document()))
cursor2.addCursor(c);
QCOMPARE(cursor2, cursor);
}
void tst_MultiCursor::testMultiCursorMerge_data()
{
QTest::addColumn<Cursors>("input");
QTest::addColumn<Cursors>("output");
QTextCursor null;
QTest::addRow("Null Cursor") << _({null, null}) << Cursors{};
QTextCursor c1 = m_edit.textCursor();
QTest::addRow("Null and Valid Cursor") << _({null, c1}) << _({c1});
QTest::addRow("Identical Cursors") << _({c1, c1}) << _({c1});
QTextCursor c2 = m_edit.textCursor();
c2.movePosition(QTextCursor::End);
QTest::addRow("Different Cursors") << _({c1, c2}) << _({c1, c2});
c1.select(QTextCursor::LineUnderCursor);
c2.movePosition(QTextCursor::Start);
QTest::addRow("Position at Selection Start") << _({c1, c2}) << _({c1, c2});
c2.movePosition(QTextCursor::EndOfBlock);
QTest::addRow("Position at Selection End") << _({c1, c2}) << _({c1, c2});
c2.movePosition(QTextCursor::PreviousCharacter);
QTest::addRow("Position in Selection") << _({c1, c2}) << _({c1});
auto setCursor = [](QTextCursor &c, int pos, int anchor){
c.setPosition(anchor);
c.setPosition(pos, QTextCursor::KeepAnchor);
};
setCursor(c1, 3, 6);
setCursor(c2, 1, 2);
QTest::addRow("Different Selections") << _({c1, c2}) << _({c1, c2});
setCursor(c2, 2, 3);
QTest::addRow("Adjacent Selections") << _({c1, c2}) << _({c1, c2});
setCursor(c2, 2, 4);
QTextCursor mc = m_edit.textCursor();
setCursor(mc, 2, 6);
QTest::addRow("Overlapping Selections") << _({c1, c2}) << _({mc});
setCursor(c2, 4, 5);
QTest::addRow("Enclosing Selection") << _({c1, c2}) << _({c1});
}
void tst_MultiCursor::testMultiCursorMerge()
{
QFETCH(Cursors, input);
QFETCH(Cursors, output);
QCOMPARE(MultiTextCursor(input.toTextCurors(m_edit.document())).cursors(),
output.toTextCurors(m_edit.document()));
}
Q_DECLARE_METATYPE(QTextCursor::MoveOperation)
Q_DECLARE_METATYPE(QTextCursor::MoveMode)
void tst_MultiCursor::testMultiCursorMove_data()
{
QTest::addColumn<Cursors>("input");
QTest::addColumn<QTextCursor::MoveOperation>("op");
QTest::addColumn<QTextCursor::MoveMode>("mode");
QTest::addColumn<int>("n");
QTest::addColumn<Cursors>("output");
QTextCursor c1 = m_edit.textCursor();
QTextCursor cend = c1;
cend.movePosition(QTextCursor::End);
QTest::addRow("Single Cursor")
<< _({c1}) << QTextCursor::End << QTextCursor::MoveAnchor << 1 << _({cend});
QTextCursor c1endl = c1;
c1endl.movePosition(QTextCursor::EndOfLine);
QTest::addRow("Double Cursor") << _({c1, cend}) << QTextCursor::EndOfLine
<< QTextCursor::MoveAnchor << 1 << _({c1endl, cend});
QTextCursor cnn = c1;
cnn.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, 2);
QTextCursor c1endlnn = c1endl;
c1endlnn.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, 2);
QTest::addRow("Double Cursor Move Twice")
<< _({c1, c1endl}) << QTextCursor::NextCharacter << QTextCursor::MoveAnchor << 2
<< _({cnn, c1endlnn});
QTest::addRow("Double Cursor Merge")
<< Cursors({c1, cend}) << QTextCursor::End << QTextCursor::MoveAnchor << 1 << _({cend});
QTextCursor cdoc = c1;
cdoc.select(QTextCursor::Document);
QTest::addRow("Double Cursor KeepAnchor")
<< _({c1, cend}) << QTextCursor::End << QTextCursor::KeepAnchor << 1 << _({cdoc, cend});
}
void tst_MultiCursor::testMultiCursorMove()
{
QFETCH(Cursors, input);
QFETCH(QTextCursor::MoveOperation, op);
QFETCH(QTextCursor::MoveMode, mode);
QFETCH(int, n);
QFETCH(Cursors, output);
MultiTextCursor cursor(input.toTextCurors(m_edit.document()));
cursor.movePosition(op, mode, n);
QCOMPARE(cursor.cursors(), output.toTextCurors(m_edit.document()));
}
void tst_MultiCursor::testMultiCursorSelection_data()
{
QTest::addColumn<Cursors>("input");
QTest::addColumn<bool>("hasSelection");
QTest::addColumn<QString>("selectedText");
QTest::addColumn<QString>("removedText");
QTest::addRow("No Cursor") << Cursors() << false << QString() << m_text;
QTextCursor c1 = m_edit.textCursor();
QTest::addRow("One Cursor No Selection") << _({c1}) << false << QString() << m_text;
QTextCursor c2 = c1;
c2.movePosition(QTextCursor::NextBlock);
QTest::addRow("Multiple Cursor No Selection") << _({c1, c2}) << false << QString() << m_text;
c1.select(QTextCursor::WordUnderCursor);
QTest::addRow("One Cursor With Selection") << _({c1}) << true << "You" << m_text.mid(3);
QTest::addRow("Two Cursor Mixed Selection") << _({c1, c2}) << true << "You" << m_text.mid(3);
c2.select(QTextCursor::WordUnderCursor);
QTest::addRow("Two Cursor With Selection") << _({c1, c2}) << true << "You\nby" << R"( can move directly to the definition or the declaration of a symbol in the Edit mode
holding the Ctrl key and clicking the symbol.
If you have multiple splits opened, you can open the link in the next split by holding
Ctrl and Alt while clicking the symbol.)";
}
void tst_MultiCursor::testMultiCursorSelection()
{
QFETCH(Cursors, input);
QFETCH(bool, hasSelection);
QFETCH(QString, selectedText);
QFETCH(QString, removedText);
MultiTextCursor cursor(input.toTextCurors(m_edit.document()));
QCOMPARE(cursor.hasSelection(), hasSelection);
QCOMPARE(cursor.selectedText(), selectedText);
if (cursor.cursorCount() > 0) // prevent triggering an assert in MultiTextCursor::beginEditBlock
cursor.removeSelectedText();
QCOMPARE(m_edit.toPlainText(), removedText);
}
void tst_MultiCursor::testMultiCursorInsert_data()
{
QTest::addColumn<Cursors>("input");
QTest::addColumn<QString>("insertText");
QTest::addColumn<QString>("editedText");
QString foo = "foo";
QString bar = "bar";
QString foobar = foo + '\n' + bar;
QTest::addRow("No Cursor") << Cursors() << foo << m_text;
QTextCursor c1 = m_edit.textCursor();
QTest::addRow("Single Cursor") << _({c1}) << foo << QString(foo + m_text);
QTextCursor c2 = m_edit.textCursor();
c2.movePosition(QTextCursor::NextCharacter);
QTest::addRow("Multi Cursor") << _({c1, c2}) << foo
<< QString(foo + m_text.at(0) + foo + m_text.mid(1));
QTest::addRow("Single Cursor Multi Line Insert Text")
<< _({c1}) << foobar << QString(foobar + m_text);
QTest::addRow("Multi Cursor Multi Line Insert Text")
<< _({c1, c2}) << foobar << QString(foo + m_text.at(0) + bar + m_text.mid(1));
}
void tst_MultiCursor::testMultiCursorInsert()
{
QFETCH(Cursors, input);
QFETCH(QString, insertText);
QFETCH(QString, editedText);
MultiTextCursor cursor(input.toTextCurors(m_edit.document()));
cursor.insertText(insertText);
QString pt = m_edit.toPlainText();
QCOMPARE(m_edit.toPlainText(), editedText);
}
QTEST_MAIN(tst_MultiCursor)
#include "tst_multicursor.moc"

View File

@@ -10,4 +10,5 @@ SUBDIRS = \
settings \ settings \
stringutils \ stringutils \
templateengine \ templateengine \
treemodel treemodel \
multicursor

View File

@@ -13,5 +13,6 @@ Project {
"stringutils/stringutils.qbs", "stringutils/stringutils.qbs",
"templateengine/templateengine.qbs", "templateengine/templateengine.qbs",
"treemodel/treemodel.qbs", "treemodel/treemodel.qbs",
"multicursor/multicursor.qbs",
] ]
} }