// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "multitextcursor.h" #include "algorithm.h" #include "camelcasecursor.h" #include "hostosinfo.h" #include "qtcassert.h" #include #include namespace Utils { MultiTextCursor::MultiTextCursor() {} MultiTextCursor::MultiTextCursor(const QList &cursors) { setCursors(cursors); } void MultiTextCursor::fillMapWithList() { m_cursorMap.clear(); for (auto it = m_cursorList.begin(); it != m_cursorList.end(); ++it) m_cursorMap[it->selectionStart()] = it; } MultiTextCursor& MultiTextCursor::operator=(const MultiTextCursor &multiCursor) { m_cursorList = multiCursor.m_cursorList; fillMapWithList(); return *this; } MultiTextCursor::MultiTextCursor(const MultiTextCursor &multiCursor) { *this = multiCursor; } MultiTextCursor& MultiTextCursor::operator=(const MultiTextCursor &&multiCursor) { m_cursorList = std::move(multiCursor.m_cursorList); fillMapWithList(); return *this; } MultiTextCursor::MultiTextCursor(const MultiTextCursor &&multiCursor) { *this = std::move(multiCursor); } MultiTextCursor::~MultiTextCursor() = default; 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::addCursor(const QTextCursor &cursor) { QTC_ASSERT(!cursor.isNull(), return); QTextCursor c1 = cursor; const int pos = c1.selectionStart(); auto found = m_cursorMap.lower_bound(pos); if (found != m_cursorMap.begin()) --found; for (; !m_cursorMap.empty() && found != m_cursorMap.end() && found->second->selectionStart() <= cursor.selectionEnd();) { const QTextCursor &c2 = *found->second; if (cursorsOverlap(c1, c2)) { Utils::mergeCursors(c1, c2); m_cursorList.erase(found->second); found = m_cursorMap.erase(found); continue; } ++found; } m_cursorMap[pos] = m_cursorList.insert(m_cursorList.end(), c1); } void MultiTextCursor::addCursors(const QList &cursors) { for (const QTextCursor &c : cursors) addCursor(c); } void MultiTextCursor::setCursors(const QList &cursors) { m_cursorList.clear(); m_cursorMap.clear(); addCursors(cursors); } const QList MultiTextCursor::cursors() const { return QList(m_cursorList.begin(), m_cursorList.end()); } void MultiTextCursor::replaceMainCursor(const QTextCursor &cursor) { QTC_ASSERT(!cursor.isNull(), return); takeMainCursor(); addCursor(cursor); } QTextCursor MultiTextCursor::mainCursor() const { if (m_cursorList.empty()) return {}; return m_cursorList.back(); } QTextCursor MultiTextCursor::takeMainCursor() { if (m_cursorList.empty()) return {}; QTextCursor cursor = m_cursorList.back(); auto it = m_cursorList.end(); --it; m_cursorMap.erase(it->selectionStart()); m_cursorList.erase(it); return cursor; } void MultiTextCursor::beginEditBlock() { QTC_ASSERT(!m_cursorList.empty(), return); m_cursorList.back().beginEditBlock(); } void MultiTextCursor::endEditBlock() { QTC_ASSERT(!m_cursorList.empty(), return); m_cursorList.back().endEditBlock(); } bool MultiTextCursor::isNull() const { return m_cursorList.empty(); } bool MultiTextCursor::hasMultipleCursors() const { return m_cursorList.size() > 1; } int MultiTextCursor::cursorCount() const { return static_cast(m_cursorList.size()); } void MultiTextCursor::movePosition(QTextCursor::MoveOperation operation, QTextCursor::MoveMode mode, int n) { for (auto &cursor : m_cursorList) cursor.movePosition(operation, mode, n); mergeCursors(); } bool MultiTextCursor::hasSelection() const { return Utils::anyOf(m_cursorList, &QTextCursor::hasSelection); } QString MultiTextCursor::selectedText() const { QString text; for (const auto &element : std::as_const(m_cursorMap)) { const QTextCursor &cursor = *element.second; const QString &cursorText = cursor.selectedText(); if (cursorText.isEmpty()) continue; if (!text.isEmpty()) { if (text.endsWith(QChar::ParagraphSeparator)) text.chop(1); text.append('\n'); } text.append(cursorText); } return text; } void MultiTextCursor::removeSelectedText() { beginEditBlock(); for (auto cursor = m_cursorList.begin(); cursor != m_cursorList.end(); ++cursor) cursor->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_cursorList.empty()) return; m_cursorList.back().beginEditBlock(); if (hasMultipleCursors()) { QStringList lines = text.split('\n'); if (!lines.isEmpty() && lines.last().isEmpty()) lines.pop_back(); int index = 0; if (static_cast(lines.count()) == m_cursorList.size()) { for (const auto &element : std::as_const(m_cursorMap)) { QTextCursor &cursor = *element.second; insertAndSelect(cursor, lines.at(index++), selectNewText); } m_cursorList.back().endEditBlock(); return; } } for (auto cursor = m_cursorList.begin(); cursor != m_cursorList.end(); ++cursor) insertAndSelect(*cursor, text, selectNewText); m_cursorList.back().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_cursorList.size() != other.m_cursorList.size()) return false; if (m_cursorList.empty()) return true; if (!equalCursors(m_cursorList.back(), other.m_cursorList.back())) return false; auto it = m_cursorMap.begin(); auto otherIt = other.m_cursorMap.begin(); for (;it != m_cursorMap.end() && otherIt != other.m_cursorMap.end(); ++it, ++otherIt) { const QTextCursor &cursor = *it->second; const QTextCursor &otherCursor = *otherIt->second; if (it->first != otherIt->first || cursor != otherCursor || cursor.anchor() != otherCursor.anchor()) return false; } return true; } bool MultiTextCursor::operator!=(const MultiTextCursor &other) const { return !operator==(other); } void MultiTextCursor::mergeCursors() { QList cursors(m_cursorList.begin(), m_cursorList.end()); setCursors(cursors); } // 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 MultiTextCursor::multiCursorAddEvent(QKeyEvent *e, QKeySequence::StandardKey matchKey) { uint searchkey = (e->modifiers() | e->key()) & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier | Qt::AltModifier | Qt::ShiftModifier); const QList bindings = QKeySequence::keyBindings(matchKey); return bindings.contains(QKeySequence(searchkey)); } bool MultiTextCursor::handleMoveKeyEvent(QKeyEvent *e, QPlainTextEdit *edit, bool camelCaseNavigationEnabled) { if (e->modifiers() & Qt::AltModifier && !Utils::HostOsInfo::isMacHost()) { 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 std::list cursors = m_cursorList; 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); addCursor(cursor); } return true; } for (auto it = m_cursorList.begin(); it != m_cursorList.end(); ++it) { QTextCursor &cursor = *it; 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