forked from qt-creator/qt-creator
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:
@@ -93,6 +93,7 @@ add_qtc_library(Utils
|
||||
mimetypes/mimeprovider.cpp mimetypes/mimeprovider_p.h
|
||||
mimetypes/mimetype.cpp mimetypes/mimetype.h mimetypes/mimetype_p.h
|
||||
mimetypes/mimetypeparser.cpp mimetypes/mimetypeparser_p.h
|
||||
multitextcursor.cpp multitextcursor.h
|
||||
namevaluedictionary.cpp namevaluedictionary.h
|
||||
namevaluedictionary.cpp namevaluedictionary.h
|
||||
namevalueitem.cpp namevalueitem.h
|
||||
|
@@ -26,9 +26,13 @@
|
||||
|
||||
#include "camelcasecursor.h"
|
||||
|
||||
#include "multitextcursor.h"
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QPlainTextEdit>
|
||||
|
||||
namespace Utils {
|
||||
|
||||
template<typename C, typename E>
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
QTextCursor temp;
|
||||
@@ -334,8 +347,20 @@ bool CamelCaseCursor::right(QTextCursor *cursor, QPlainTextEdit *edit, QTextCurs
|
||||
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)
|
||||
{
|
||||
QTextCursor temp;
|
||||
return camelCaseRight(&temp, edit, mode);
|
||||
}
|
||||
|
||||
} // namespace Utils
|
||||
|
||||
|
@@ -35,11 +35,19 @@ class QLineEdit;
|
||||
class QPlainTextEdit;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
namespace Utils {
|
||||
|
||||
class MultiTextCursor;
|
||||
|
||||
class QTCREATOR_UTILS_EXPORT CamelCaseCursor
|
||||
{
|
||||
public:
|
||||
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 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);
|
||||
};
|
||||
|
||||
} // namespace Utils
|
||||
|
446
src/libs/utils/multitextcursor.cpp
Normal file
446
src/libs/utils/multitextcursor.cpp
Normal 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
|
106
src/libs/utils/multitextcursor.h
Normal file
106
src/libs/utils/multitextcursor.h
Normal 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
|
@@ -24,10 +24,14 @@
|
||||
****************************************************************************/
|
||||
|
||||
#include "uncommentselection.h"
|
||||
|
||||
#include "qtcassert.h"
|
||||
#include "utils/multitextcursor.h"
|
||||
|
||||
#include <QPlainTextEdit>
|
||||
#include <QTextBlock>
|
||||
|
||||
using namespace Utils;
|
||||
namespace Utils {
|
||||
|
||||
CommentDefinition CommentDefinition::CppStyle = CommentDefinition("//", "/*", "*/");
|
||||
CommentDefinition CommentDefinition::HashStyle = CommentDefinition("#");
|
||||
@@ -74,9 +78,9 @@ static bool isComment(const QString &text, int index,
|
||||
}
|
||||
|
||||
|
||||
QTextCursor Utils::unCommentSelection(const QTextCursor &cursorIn,
|
||||
const CommentDefinition &definition,
|
||||
bool preferSingleLine)
|
||||
QTextCursor unCommentSelection(const QTextCursor &cursorIn,
|
||||
const CommentDefinition &definition,
|
||||
bool preferSingleLine)
|
||||
{
|
||||
if (!definition.isValid())
|
||||
return cursorIn;
|
||||
@@ -244,3 +248,31 @@ QTextCursor Utils::unCommentSelection(const QTextCursor &cursorIn,
|
||||
}
|
||||
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
|
||||
|
@@ -36,6 +36,8 @@ QT_END_NAMESPACE
|
||||
|
||||
namespace Utils {
|
||||
|
||||
class MultiTextCursor;
|
||||
|
||||
class QTCREATOR_UTILS_EXPORT CommentDefinition
|
||||
{
|
||||
public:
|
||||
@@ -62,4 +64,9 @@ QTextCursor unCommentSelection(const QTextCursor &cursor,
|
||||
const CommentDefinition &definiton = CommentDefinition(),
|
||||
bool preferSingleLine = false);
|
||||
|
||||
QTCREATOR_UTILS_EXPORT
|
||||
MultiTextCursor unCommentSelection(const MultiTextCursor &cursor,
|
||||
const CommentDefinition &definiton = CommentDefinition(),
|
||||
bool preferSingleLine = false);
|
||||
|
||||
} // namespace Utils
|
||||
|
@@ -142,6 +142,7 @@ SOURCES += \
|
||||
$$PWD/qtcsettings.cpp \
|
||||
$$PWD/link.cpp \
|
||||
$$PWD/linecolumn.cpp \
|
||||
$$PWD/multitextcursor.cpp \
|
||||
|
||||
HEADERS += \
|
||||
$$PWD/environmentfwd.h \
|
||||
@@ -307,6 +308,7 @@ HEADERS += \
|
||||
$$PWD/launcherpackets.h \
|
||||
$$PWD/launchersocket.h \
|
||||
$$PWD/qtcsettings.h
|
||||
$$PWD/multitextcursor.h \
|
||||
|
||||
FORMS += $$PWD/filewizardpage.ui \
|
||||
$$PWD/projectintropage.ui \
|
||||
|
@@ -177,6 +177,8 @@ Project {
|
||||
"macroexpander.cpp",
|
||||
"macroexpander.h",
|
||||
"mapreduce.h",
|
||||
"multitextcursor.cpp",
|
||||
"multitextcursor.h",
|
||||
"namevaluedictionary.cpp",
|
||||
"namevaluedictionary.h",
|
||||
"namevalueitem.cpp",
|
||||
|
@@ -25,6 +25,7 @@
|
||||
|
||||
#include "basetextfind.h"
|
||||
|
||||
#include <utils/algorithm.h>
|
||||
#include <utils/qtcassert.h>
|
||||
#include <utils/filesearch.h>
|
||||
|
||||
@@ -36,13 +37,13 @@
|
||||
|
||||
namespace Core {
|
||||
|
||||
static QRegularExpression regularExpression(const QString &txt, FindFlags flags)
|
||||
QRegularExpression BaseTextFind::regularExpression(const QString &txt, FindFlags flags)
|
||||
{
|
||||
return QRegularExpression(
|
||||
(flags & FindRegularExpression) ? txt
|
||||
: QRegularExpression::escape(txt),
|
||||
(flags & FindCaseSensitively) ? QRegularExpression::NoPatternOption
|
||||
: QRegularExpression::CaseInsensitiveOption);
|
||||
return QRegularExpression((flags & FindRegularExpression) ? txt
|
||||
: QRegularExpression::escape(txt),
|
||||
(flags & FindCaseSensitively)
|
||||
? QRegularExpression::NoPatternOption
|
||||
: QRegularExpression::CaseInsensitiveOption);
|
||||
}
|
||||
|
||||
struct BaseTextFindPrivate
|
||||
@@ -53,10 +54,8 @@ struct BaseTextFindPrivate
|
||||
QPointer<QTextEdit> m_editor;
|
||||
QPointer<QPlainTextEdit> m_plaineditor;
|
||||
QPointer<QWidget> m_widget;
|
||||
QTextCursor m_findScopeStart;
|
||||
QTextCursor m_findScopeEnd;
|
||||
int m_findScopeVerticalBlockSelectionFirstColumn;
|
||||
int m_findScopeVerticalBlockSelectionLastColumn;
|
||||
Utils::MultiTextCursor m_scope;
|
||||
std::function<Utils::MultiTextCursor()> m_cursorProvider;
|
||||
int m_incrementalStartPos;
|
||||
bool m_incrementalWrappedState;
|
||||
};
|
||||
@@ -64,8 +63,6 @@ struct BaseTextFindPrivate
|
||||
BaseTextFindPrivate::BaseTextFindPrivate(QTextEdit *editor)
|
||||
: m_editor(editor)
|
||||
, m_widget(editor)
|
||||
, m_findScopeVerticalBlockSelectionFirstColumn(-1)
|
||||
, m_findScopeVerticalBlockSelectionLastColumn(-1)
|
||||
, m_incrementalStartPos(-1)
|
||||
, m_incrementalWrappedState(false)
|
||||
{
|
||||
@@ -74,8 +71,6 @@ BaseTextFindPrivate::BaseTextFindPrivate(QTextEdit *editor)
|
||||
BaseTextFindPrivate::BaseTextFindPrivate(QPlainTextEdit *editor)
|
||||
: m_plaineditor(editor)
|
||||
, m_widget(editor)
|
||||
, m_findScopeVerticalBlockSelectionFirstColumn(-1)
|
||||
, m_findScopeVerticalBlockSelectionLastColumn(-1)
|
||||
, m_incrementalStartPos(-1)
|
||||
, m_incrementalWrappedState(false)
|
||||
{
|
||||
@@ -93,15 +88,10 @@ BaseTextFindPrivate::BaseTextFindPrivate(QPlainTextEdit *editor)
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn void Core::BaseTextFind::findScopeChanged(const QTextCursor &start,
|
||||
const QTextCursor &end,
|
||||
int verticalBlockSelectionFirstColumn,
|
||||
int verticalBlockSelectionLastColumn)
|
||||
\fn void Core::BaseTextFind::findScopeChanged(const Utils::MultiTextCursor &cursor)
|
||||
|
||||
This signal is emitted when the search
|
||||
scope changes to \a start, \a end,
|
||||
\a verticalBlockSelectionFirstColumn, and
|
||||
\a verticalBlockSelectionLastColumn.
|
||||
scope changes to \a cursor.
|
||||
*/
|
||||
|
||||
/*!
|
||||
@@ -324,6 +314,13 @@ QTextCursor BaseTextFind::replaceInternal(const QString &before, const QString &
|
||||
return cursor;
|
||||
}
|
||||
|
||||
Utils::MultiTextCursor BaseTextFind::multiTextCursor() const
|
||||
{
|
||||
if (d->m_cursorProvider)
|
||||
return d->m_cursorProvider();
|
||||
return Utils::MultiTextCursor({textCursor()});
|
||||
}
|
||||
|
||||
/*!
|
||||
\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)
|
||||
{
|
||||
QTextCursor editCursor = textCursor();
|
||||
if (!d->m_findScopeStart.isNull())
|
||||
editCursor.setPosition(d->m_findScopeStart.position());
|
||||
if (findFlags.testFlag(FindBackward))
|
||||
editCursor.movePosition(QTextCursor::End);
|
||||
else
|
||||
editCursor.movePosition(QTextCursor::Start);
|
||||
editCursor.movePosition(QTextCursor::Start);
|
||||
editCursor.beginEditBlock();
|
||||
int count = 0;
|
||||
bool usesRegExp = (findFlags & FindRegularExpression);
|
||||
@@ -355,7 +353,7 @@ int BaseTextFind::replaceAll(const QString &before, const QString &after, FindFl
|
||||
QRegularExpression regexp = regularExpression(before, findFlags);
|
||||
QTextCursor found = findOne(regexp, editCursor, textDocumentFlagsForFindFlags(findFlags));
|
||||
bool first = true;
|
||||
while (!found.isNull() && inScope(found.selectionStart(), found.selectionEnd())) {
|
||||
while (!found.isNull()) {
|
||||
if (found == editCursor && !first) {
|
||||
if (editCursor.atEnd())
|
||||
break;
|
||||
@@ -390,8 +388,7 @@ int BaseTextFind::replaceAll(const QString &before, const QString &after, FindFl
|
||||
return count;
|
||||
}
|
||||
|
||||
bool BaseTextFind::find(const QString &txt, FindFlags findFlags,
|
||||
QTextCursor start, bool *wrapped)
|
||||
bool BaseTextFind::find(const QString &txt, FindFlags findFlags, QTextCursor start, bool *wrapped)
|
||||
{
|
||||
if (txt.isEmpty()) {
|
||||
setTextCursor(start);
|
||||
@@ -402,85 +399,52 @@ bool BaseTextFind::find(const QString &txt, FindFlags findFlags,
|
||||
if (wrapped)
|
||||
*wrapped = false;
|
||||
|
||||
if (!d->m_findScopeStart.isNull()) {
|
||||
|
||||
// scoped
|
||||
if (found.isNull() || !inScope(found.selectionStart(), found.selectionEnd())) {
|
||||
if ((findFlags & FindBackward) == 0)
|
||||
start.setPosition(d->m_findScopeStart.position());
|
||||
else
|
||||
start.setPosition(d->m_findScopeEnd.position());
|
||||
found = findOne(regexp, start, textDocumentFlagsForFindFlags(findFlags));
|
||||
if (found.isNull() || !inScope(found.selectionStart(), found.selectionEnd()))
|
||||
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()) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// helper function. Works just like QTextDocument::find() but supports vertical block selection
|
||||
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);
|
||||
if (candidate.isNull())
|
||||
return candidate;
|
||||
|
||||
if (d->m_findScopeVerticalBlockSelectionFirstColumn < 0)
|
||||
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 found = document()->find(expr, from, options);
|
||||
while (!found.isNull() && !inScope(found)) {
|
||||
if (!found.hasSelection()) {
|
||||
from = found;
|
||||
found.movePosition(options & QTextDocument::FindBackward
|
||||
? QTextCursor::PreviousCharacter
|
||||
: QTextCursor::NextCharacter);
|
||||
candidate = document()->find(expr, candidate, options);
|
||||
} 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 (d->m_findScopeStart.position() <= startPosition
|
||||
&& d->m_findScopeEnd.position() >= endPosition);
|
||||
return Utils::anyOf(d->m_scope, [candidate](const QTextCursor &scope){
|
||||
return candidate.selectionStart() >= scope.selectionStart()
|
||||
&& candidate.selectionEnd() <= scope.selectionEnd();
|
||||
});
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -488,30 +452,24 @@ bool BaseTextFind::inScope(int startPosition, int endPosition) const
|
||||
*/
|
||||
void BaseTextFind::defineFindScope()
|
||||
{
|
||||
QTextCursor cursor = textCursor();
|
||||
if (cursor.hasSelection() && cursor.block() != cursor.document()->findBlock(cursor.anchor())) {
|
||||
d->m_findScopeStart = cursor;
|
||||
d->m_findScopeStart.setPosition(qMax(0, cursor.selectionStart()));
|
||||
d->m_findScopeEnd = cursor;
|
||||
d->m_findScopeEnd.setPosition(cursor.selectionEnd());
|
||||
d->m_findScopeVerticalBlockSelectionFirstColumn = -1;
|
||||
d->m_findScopeVerticalBlockSelectionLastColumn = -1;
|
||||
|
||||
if (d->m_plaineditor && d->m_plaineditor->metaObject()->indexOfProperty("verticalBlockSelectionFirstColumn") >= 0) {
|
||||
d->m_findScopeVerticalBlockSelectionFirstColumn
|
||||
= d->m_plaineditor->property("verticalBlockSelectionFirstColumn").toInt();
|
||||
d->m_findScopeVerticalBlockSelectionLastColumn
|
||||
= d->m_plaineditor->property("verticalBlockSelectionLastColumn").toInt();
|
||||
Utils::MultiTextCursor multiCursor = multiTextCursor();
|
||||
bool foundSelection = false;
|
||||
for (const QTextCursor &c : multiCursor) {
|
||||
if (c.hasSelection()) {
|
||||
if (foundSelection || c.block() != c.document()->findBlock(c.anchor())) {
|
||||
QList<QTextCursor> sortedCursors = multiCursor.cursors();
|
||||
Utils::sort(sortedCursors);
|
||||
d->m_scope = Utils::MultiTextCursor(sortedCursors);
|
||||
QTextCursor cursor = textCursor();
|
||||
cursor.clearSelection();
|
||||
setTextCursor(cursor);
|
||||
emit findScopeChanged(d->m_scope);
|
||||
return;
|
||||
}
|
||||
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()
|
||||
{
|
||||
d->m_findScopeStart = QTextCursor();
|
||||
d->m_findScopeEnd = QTextCursor();
|
||||
d->m_findScopeVerticalBlockSelectionFirstColumn = -1;
|
||||
d->m_findScopeVerticalBlockSelectionLastColumn = -1;
|
||||
emit findScopeChanged(d->m_findScopeStart, d->m_findScopeEnd,
|
||||
d->m_findScopeVerticalBlockSelectionFirstColumn,
|
||||
d->m_findScopeVerticalBlockSelectionLastColumn);
|
||||
d->m_scope = Utils::MultiTextCursor();
|
||||
emit findScopeChanged(d->m_scope);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -537,4 +490,9 @@ void BaseTextFind::highlightAll(const QString &txt, FindFlags findFlags)
|
||||
emit highlightAllRequested(txt, findFlags);
|
||||
}
|
||||
|
||||
void BaseTextFind::setMultiTextCursorProvider(const CursorProvider &provider)
|
||||
{
|
||||
d->m_cursorProvider = provider;
|
||||
}
|
||||
|
||||
} // namespace Core
|
||||
|
@@ -27,6 +27,10 @@
|
||||
|
||||
#include "ifindsupport.h"
|
||||
|
||||
#include <utils/multitextcursor.h>
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QPlainTextEdit;
|
||||
class QTextEdit;
|
||||
@@ -63,22 +67,26 @@ public:
|
||||
|
||||
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:
|
||||
void highlightAllRequested(const QString &txt, Core::FindFlags findFlags);
|
||||
void findScopeChanged(const QTextCursor &start, const QTextCursor &end,
|
||||
int verticalBlockSelectionFirstColumn,
|
||||
int verticalBlockSelectionLastColumn);
|
||||
void findScopeChanged(const Utils::MultiTextCursor &cursor);
|
||||
|
||||
private:
|
||||
bool find(const QString &txt, FindFlags findFlags, QTextCursor start, bool *wrapped);
|
||||
QTextCursor replaceInternal(const QString &before, const QString &after, FindFlags findFlags);
|
||||
|
||||
Utils::MultiTextCursor multiTextCursor() const;
|
||||
QTextCursor textCursor() const;
|
||||
void setTextCursor(const QTextCursor&);
|
||||
QTextDocument *document() const;
|
||||
bool isReadOnly() const;
|
||||
bool inScope(int startPosition, int endPosition) const;
|
||||
QTextCursor findOne(const QRegularExpression &expr, const QTextCursor &from, QTextDocument::FindFlags options) const;
|
||||
QTextCursor findOne(const QRegularExpression &expr, QTextCursor from, QTextDocument::FindFlags options) const;
|
||||
|
||||
BaseTextFindPrivate *d;
|
||||
};
|
||||
|
@@ -337,6 +337,9 @@ static bool trySplitComment(TextEditor::TextEditorWidget *editorWidget,
|
||||
if (!settings.m_enableDoxygen && !settings.m_leadingAsterisks)
|
||||
return false;
|
||||
|
||||
if (editorWidget->multiTextCursor().hasMultipleCursors())
|
||||
return false;
|
||||
|
||||
QTextCursor cursor = editorWidget->textCursor();
|
||||
if (!CPlusPlus::MatchingText::isInCommentHelper(cursor))
|
||||
return false;
|
||||
|
@@ -396,7 +396,6 @@ int SideDiffEditorWidget::chunkRowsCountForBlockNumber(int blockNumber) const
|
||||
|
||||
void SideDiffEditorWidget::clearAll(const QString &message)
|
||||
{
|
||||
setBlockSelection(false);
|
||||
clear();
|
||||
clearAllData();
|
||||
setExtraSelections(TextEditorWidget::OtherSelection,
|
||||
|
@@ -1690,22 +1690,44 @@ void FakeVimPluginPrivate::editorOpened(IEditor *editor)
|
||||
|
||||
handler->requestDisableBlockSelection.connect([tew] {
|
||||
if (tew)
|
||||
tew->setBlockSelection(false);
|
||||
tew->setTextCursor(tew->textCursor());
|
||||
});
|
||||
|
||||
handler->requestSetBlockSelection.connect([tew](const QTextCursor &cursor) {
|
||||
if (tew)
|
||||
tew->setBlockSelection(cursor);
|
||||
if (tew) {
|
||||
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) {
|
||||
if (tew && cursor)
|
||||
*cursor = tew->blockSelection();
|
||||
if (tew && cursor) {
|
||||
MultiTextCursor mtc = tew->multiTextCursor();
|
||||
*cursor = mtc.cursors().first();
|
||||
cursor->setPosition(mtc.mainCursor().position(), QTextCursor::KeepAnchor);
|
||||
}
|
||||
});
|
||||
|
||||
handler->requestHasBlockSelection.connect([tew](bool *on) {
|
||||
if (tew && on)
|
||||
*on = tew->hasBlockSelection();
|
||||
*on = tew->multiTextCursor().hasMultipleCursors();
|
||||
});
|
||||
|
||||
handler->simpleCompletionRequested.connect([this, handler](const QString &needle, bool forward) {
|
||||
|
@@ -164,6 +164,8 @@ void CodeAssistantPrivate::invoke(AssistKind kind, IAssistProvider *provider)
|
||||
|
||||
bool CodeAssistantPrivate::requestActivationCharProposal()
|
||||
{
|
||||
if (m_editorWidget->multiTextCursor().hasMultipleCursors())
|
||||
return false;
|
||||
if (m_assistKind == Completion && m_settings.m_completionTrigger != ManualCompletion) {
|
||||
if (CompletionAssistProvider *provider = identifyActivationSequence()) {
|
||||
requestProposal(ActivationCharacter, Completion, provider);
|
||||
@@ -197,9 +199,6 @@ void CodeAssistantPrivate::requestProposal(AssistReason reason,
|
||||
if (isWaitingForProposal())
|
||||
cancelCurrentRequest();
|
||||
|
||||
if (m_editorWidget->hasBlockSelection())
|
||||
return; // TODO
|
||||
|
||||
if (!provider) {
|
||||
if (kind == Completion)
|
||||
provider = m_editorWidget->textDocument()->completionAssistProvider();
|
||||
@@ -528,8 +527,11 @@ void CodeAssistantPrivate::startAutomaticProposalTimer()
|
||||
|
||||
void CodeAssistantPrivate::automaticProposalTimeout()
|
||||
{
|
||||
if (isWaitingForProposal() || (isDisplayingProposal() && !m_proposal->isFragile()))
|
||||
if (isWaitingForProposal()
|
||||
|| m_editorWidget->multiTextCursor().hasMultipleCursors()
|
||||
|| (isDisplayingProposal() && !m_proposal->isFragile())) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestProposal(IdleEditor, Completion);
|
||||
}
|
||||
|
@@ -81,10 +81,7 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
QTextCursor indentOrUnindent(const QTextCursor &textCursor, bool doIndent,
|
||||
const TabSettings &tabSettings,
|
||||
bool blockSelection = false, int column = 0,
|
||||
int *offset = nullptr);
|
||||
MultiTextCursor indentOrUnindent(const MultiTextCursor &cursor, bool doIndent, const TabSettings &tabSettings);
|
||||
void resetRevisions();
|
||||
void updateRevisions();
|
||||
|
||||
@@ -112,146 +109,90 @@ public:
|
||||
Utils::Guard m_modificationChangedGuard;
|
||||
};
|
||||
|
||||
QTextCursor TextDocumentPrivate::indentOrUnindent(const QTextCursor &textCursor, bool doIndent,
|
||||
const TabSettings &tabSettings,
|
||||
bool blockSelection, int columnIn, int *offset)
|
||||
MultiTextCursor TextDocumentPrivate::indentOrUnindent(const MultiTextCursor &cursors,
|
||||
bool doIndent,
|
||||
const TabSettings &tabSettings)
|
||||
{
|
||||
QTextCursor cursor = textCursor;
|
||||
cursor.beginEditBlock();
|
||||
|
||||
// Indent or unindent the selected lines
|
||||
int pos = cursor.position();
|
||||
int column = blockSelection ? columnIn
|
||||
: tabSettings.columnAt(cursor.block().text(), cursor.positionInBlock());
|
||||
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);
|
||||
MultiTextCursor result;
|
||||
bool first = true;
|
||||
for (const QTextCursor &textCursor : cursors) {
|
||||
QTextCursor cursor = textCursor;
|
||||
if (first) {
|
||||
cursor.beginEditBlock();
|
||||
first = false;
|
||||
} 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
|
||||
{
|
||||
public:
|
||||
BlockIndenter(const QTextBlock &_block,
|
||||
const int column,
|
||||
const TabSettings &_tabSettings)
|
||||
: block(_block)
|
||||
, text(block.text())
|
||||
, tabSettings(_tabSettings)
|
||||
{
|
||||
indentPosition = tabSettings.positionAtColumn(text, column, nullptr, true);
|
||||
spaces = TabSettings::spacesLeftFromPosition(text, indentPosition);
|
||||
}
|
||||
// Indent or unindent the selected lines
|
||||
int pos = cursor.position();
|
||||
int column = tabSettings.columnAt(cursor.block().text(), cursor.positionInBlock());
|
||||
int anchor = cursor.anchor();
|
||||
int start = qMin(anchor, pos);
|
||||
int end = qMax(anchor, pos);
|
||||
|
||||
void indent(const int targetColumn) const
|
||||
{
|
||||
const int startColumn = tabSettings.columnAt(text, indentPosition - spaces);
|
||||
QTextCursor cursor(block);
|
||||
cursor.setPosition(block.position() + indentPosition);
|
||||
cursor.setPosition(block.position() + indentPosition - spaces, QTextCursor::KeepAnchor);
|
||||
QTextBlock startBlock = m_document.findBlock(start);
|
||||
QTextBlock endBlock = m_document.findBlock(qMax(end - 1, 0)).next();
|
||||
const bool cursorAtBlockStart = (cursor.position() == startBlock.position());
|
||||
const bool anchorAtBlockStart = (cursor.anchor() == startBlock.position());
|
||||
const bool oneLinePartial = (startBlock.next() == endBlock)
|
||||
&& (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.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);
|
||||
}
|
||||
}
|
||||
|
||||
int targetColumn(bool doIndent) const
|
||||
{
|
||||
const int optimumTargetColumn
|
||||
= tabSettings.indentedColumn(tabSettings.columnAt(block.text(), indentPosition),
|
||||
doIndent);
|
||||
const int minimumTargetColumn = tabSettings.columnAt(text, indentPosition - spaces);
|
||||
return std::max(optimumTargetColumn, minimumTargetColumn);
|
||||
}
|
||||
|
||||
const QTextBlock &textBlock() { return block; }
|
||||
|
||||
private:
|
||||
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));
|
||||
} else {
|
||||
QString text = startBlock.text();
|
||||
int indentPosition = tabSettings.positionAtColumn(text, column, nullptr, true);
|
||||
int spaces = tabSettings.spacesLeftFromPosition(text, indentPosition);
|
||||
int startColumn = tabSettings.columnAt(text, indentPosition - spaces);
|
||||
int targetColumn = tabSettings.indentedColumn(tabSettings.columnAt(text, indentPosition),
|
||||
doIndent);
|
||||
cursor.setPosition(startBlock.position() + indentPosition);
|
||||
cursor.setPosition(startBlock.position() + indentPosition - spaces,
|
||||
QTextCursor::KeepAnchor);
|
||||
cursor.removeSelectedText();
|
||||
cursor.insertText(
|
||||
tabSettings.indentationString(startColumn, targetColumn, 0, startBlock));
|
||||
}
|
||||
for (const BlockIndenter &blockIndenter : blocks)
|
||||
blockIndenter.indent(maxTargetColumn);
|
||||
|
||||
// Preserve initial anchor of block selection
|
||||
if (blockSelection) {
|
||||
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();
|
||||
result.addCursor(cursor);
|
||||
}
|
||||
|
||||
cursor.endEditBlock();
|
||||
|
||||
return modified ? cursor : textCursor;
|
||||
return result;
|
||||
}
|
||||
|
||||
void TextDocumentPrivate::resetRevisions()
|
||||
@@ -504,16 +445,14 @@ void TextDocument::autoFormatOrIndent(const QTextCursor &cursor)
|
||||
d->m_indenter->autoIndent(cursor, tabSettings());
|
||||
}
|
||||
|
||||
QTextCursor TextDocument::indent(const QTextCursor &cursor, bool blockSelection, int column,
|
||||
int *offset)
|
||||
Utils::MultiTextCursor TextDocument::indent(const Utils::MultiTextCursor &cursor)
|
||||
{
|
||||
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,
|
||||
int *offset)
|
||||
Utils::MultiTextCursor TextDocument::unindent(const Utils::MultiTextCursor &cursor)
|
||||
{
|
||||
return d->indentOrUnindent(cursor, false, tabSettings(), blockSelection, column, offset);
|
||||
return d->indentOrUnindent(cursor, false, tabSettings());
|
||||
}
|
||||
|
||||
void TextDocument::setFormatter(Formatter *formatter)
|
||||
|
@@ -33,6 +33,7 @@
|
||||
|
||||
#include <utils/id.h>
|
||||
#include <utils/link.h>
|
||||
#include <utils/multitextcursor.h>
|
||||
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
@@ -95,9 +96,8 @@ public:
|
||||
int currentCursorPosition = -1);
|
||||
void autoReindent(const QTextCursor &cursor, int currentCursorPosition = -1);
|
||||
void autoFormatOrIndent(const QTextCursor &cursor);
|
||||
QTextCursor indent(const QTextCursor &cursor, bool blockSelection, int column, int *offset);
|
||||
QTextCursor unindent(const QTextCursor &cursor, bool blockSelection = false, int column = 0,
|
||||
int *offset = nullptr);
|
||||
Utils::MultiTextCursor indent(const Utils::MultiTextCursor &cursor);
|
||||
Utils::MultiTextCursor unindent(const Utils::MultiTextCursor &cursor);
|
||||
|
||||
void setFormatter(Formatter *indenter); // transfers ownership
|
||||
void autoFormat(const QTextCursor &cursor);
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,7 @@
|
||||
|
||||
#include <utils/elidinglabel.h>
|
||||
#include <utils/link.h>
|
||||
#include <utils/multitextcursor.h>
|
||||
#include <utils/uncommentselection.h>
|
||||
|
||||
#include <QPlainTextEdit>
|
||||
@@ -177,9 +178,6 @@ private:
|
||||
class TEXTEDITOR_EXPORT TextEditorWidget : public QPlainTextEdit
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int verticalBlockSelectionFirstColumn READ verticalBlockSelectionFirstColumn)
|
||||
Q_PROPERTY(int verticalBlockSelectionLastColumn READ verticalBlockSelectionLastColumn)
|
||||
|
||||
public:
|
||||
explicit TextEditorWidget(QWidget *parent = nullptr);
|
||||
~TextEditorWidget() override;
|
||||
@@ -271,15 +269,8 @@ public:
|
||||
const QString &snippet,
|
||||
const SnippetParser &parse);
|
||||
|
||||
void setBlockSelection(bool on);
|
||||
void setBlockSelection(int positionBlock, int positionColumn, int anchhorBlock,
|
||||
int anchorColumn);
|
||||
void setBlockSelection(const QTextCursor &cursor);
|
||||
QTextCursor blockSelection() const;
|
||||
bool hasBlockSelection() const;
|
||||
|
||||
int verticalBlockSelectionFirstColumn() const;
|
||||
int verticalBlockSelectionLastColumn() const;
|
||||
Utils::MultiTextCursor multiTextCursor() const;
|
||||
void setMultiTextCursor(const Utils::MultiTextCursor &cursor);
|
||||
|
||||
QRegion translatedLineRegion(int lineStart, int lineEnd) const;
|
||||
|
||||
@@ -325,6 +316,7 @@ public:
|
||||
static Utils::Id AutoCompleteSelection;
|
||||
static Utils::Id CodeWarningsSelection;
|
||||
static Utils::Id CodeSemanticsSelection;
|
||||
static Utils::Id CursorSelection;
|
||||
static Utils::Id UndefinedSymbolSelection;
|
||||
static Utils::Id UnusedSymbolSelection;
|
||||
static Utils::Id OtherSelection;
|
||||
@@ -512,7 +504,6 @@ protected:
|
||||
QTextBlock blockForVerticalOffset(int offset) const;
|
||||
bool event(QEvent *e) override;
|
||||
void contextMenuEvent(QContextMenuEvent *e) override;
|
||||
void inputMethodEvent(QInputMethodEvent *e) override;
|
||||
void keyPressEvent(QKeyEvent *e) override;
|
||||
void wheelEvent(QWheelEvent *e) override;
|
||||
void changeEvent(QEvent *e) override;
|
||||
@@ -542,6 +533,7 @@ protected:
|
||||
void dropEvent(QDropEvent *e) override;
|
||||
|
||||
virtual QString plainTextFromSelection(const QTextCursor &cursor) const;
|
||||
virtual QString plainTextFromSelection(const Utils::MultiTextCursor &cursor) const;
|
||||
static QString convertToPlainText(const QString &txt);
|
||||
|
||||
virtual QString lineNumber(int blockNumber) const;
|
||||
@@ -602,7 +594,7 @@ protected:
|
||||
const QRect &clip);
|
||||
int visibleFoldedBlockNumber() const;
|
||||
void doSetTextCursor(const QTextCursor &cursor) override;
|
||||
void doSetTextCursor(const QTextCursor &cursor, bool keepBlockSelection);
|
||||
void doSetTextCursor(const QTextCursor &cursor, bool keepMultiSelection);
|
||||
|
||||
signals:
|
||||
void markRequested(TextEditor::TextEditorWidget *widget,
|
||||
@@ -619,7 +611,6 @@ protected:
|
||||
virtual void slotCodeStyleSettingsChanged(const QVariant &); // Used in CppEditor
|
||||
|
||||
Q_INVOKABLE bool inFindScope(const QTextCursor &cursor);
|
||||
Q_INVOKABLE bool inFindScope(int selectionStart, int selectionEnd);
|
||||
|
||||
private:
|
||||
Internal::TextEditorWidgetPrivate *d;
|
||||
|
@@ -37,37 +37,6 @@ class TextDocument;
|
||||
|
||||
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
|
||||
//
|
||||
|
@@ -40,464 +40,6 @@
|
||||
|
||||
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)
|
||||
{
|
||||
switch (policy) {
|
||||
|
@@ -57,15 +57,6 @@ private slots:
|
||||
void testSnippetParsing_data();
|
||||
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();
|
||||
#endif
|
||||
|
@@ -89,7 +89,9 @@ bool TypingSettings::equals(const TypingSettings &ts) const
|
||||
&& 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)
|
||||
return false;
|
||||
|
@@ -8,3 +8,4 @@ add_subdirectory(settings)
|
||||
add_subdirectory(stringutils)
|
||||
add_subdirectory(templateengine)
|
||||
add_subdirectory(treemodel)
|
||||
add_subdirectory(multicursor)
|
||||
|
4
tests/auto/utils/multicursor/CMakeLists.txt
Normal file
4
tests/auto/utils/multicursor/CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
add_qtc_test(tst_utils_multicursor
|
||||
DEPENDS Utils
|
||||
SOURCES tst_multicursor.cpp
|
||||
)
|
4
tests/auto/utils/multicursor/multicursor.pro
Normal file
4
tests/auto/utils/multicursor/multicursor.pro
Normal file
@@ -0,0 +1,4 @@
|
||||
QTC_LIB_DEPENDS += utils
|
||||
include(../../qttest.pri)
|
||||
|
||||
SOURCES += tst_multicursor.cpp
|
7
tests/auto/utils/multicursor/multicursor.qbs
Normal file
7
tests/auto/utils/multicursor/multicursor.qbs
Normal file
@@ -0,0 +1,7 @@
|
||||
import qbs
|
||||
|
||||
QtcAutotest {
|
||||
name: "MultiTextCursor autotest"
|
||||
Depends { name: "Utils" }
|
||||
files: "tst_multicursor.cpp"
|
||||
}
|
352
tests/auto/utils/multicursor/tst_multicursor.cpp
Normal file
352
tests/auto/utils/multicursor/tst_multicursor.cpp
Normal 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"
|
@@ -10,4 +10,5 @@ SUBDIRS = \
|
||||
settings \
|
||||
stringutils \
|
||||
templateengine \
|
||||
treemodel
|
||||
treemodel \
|
||||
multicursor
|
||||
|
@@ -13,5 +13,6 @@ Project {
|
||||
"stringutils/stringutils.qbs",
|
||||
"templateengine/templateengine.qbs",
|
||||
"treemodel/treemodel.qbs",
|
||||
"multicursor/multicursor.qbs",
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user