From e305024a5827296881d5b0923ac9457b477471f2 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Tue, 20 Jun 2017 22:53:59 +0800 Subject: [PATCH] vim-mode: support simple marks(a-z) Different behaviors from Vim: after deleting the line with a mark set, VNote could not detect if this mark is set or not. VNote just simply jumps to the same line. --- src/utils/vvim.cpp | 199 ++++++++++++++++++++++++++++++++++++++++-- src/utils/vvim.h | 92 ++++++++++++++----- src/vvimindicator.cpp | 56 +++++++++++- src/vvimindicator.h | 5 ++ 4 files changed, 322 insertions(+), 30 deletions(-) diff --git a/src/utils/vvim.cpp b/src/utils/vvim.cpp index 4976da20..7c95c910 100644 --- a/src/utils/vvim.cpp +++ b/src/utils/vvim.cpp @@ -419,6 +419,33 @@ bool VVim::handleKeyPressEvent(int key, int modifiers) goto clear_accept; } + if (expectingMarkName()) { + // Expecting a mark name to create a mark. + if (keyInfo.isAlphabet() && modifiers == Qt::NoModifier) { + m_keys.clear(); + m_marks.setMark(keyToChar(key, modifiers), m_editor->textCursor()); + } + + goto clear_accept; + } + + if (expectingMarkTarget()) { + // Expecting a mark name as the target. + Movement mm = Movement::Invalid; + const Key &aKey = m_keys.first(); + if (aKey == Key(Qt::Key_Apostrophe)) { + mm = Movement::MarkJumpLine; + } else { + Q_ASSERT(aKey == Key(Qt::Key_QuoteLeft)); + mm = Movement::MarkJump; + } + + tryAddMoveAction(); + addMovementToken(mm, keyInfo); + processCommand(m_tokens); + goto clear_accept; + } + if (expectingCharacterTarget()) { // Expecting a target character for f/F/t/T. Movement mm = Movement::Invalid; @@ -437,8 +464,6 @@ bool VVim::handleKeyPressEvent(int key, int modifiers) } } - V_ASSERT(mm != Movement::Invalid); - tryAddMoveAction(); addMovementToken(mm, keyInfo); m_lastFindToken = m_tokens.last(); @@ -625,7 +650,8 @@ bool VVim::handleKeyPressEvent(int key, int modifiers) setMode(VimMode::Insert); } else if (modifiers == Qt::ControlModifier) { // Ctrl+I, jump to next location. - if (!m_tokens.isEmpty()) { + if (!m_tokens.isEmpty() + || !checkMode(VimMode::Normal)) { break; } @@ -729,7 +755,8 @@ bool VVim::handleKeyPressEvent(int key, int modifiers) break; } else if (modifiers == Qt::ControlModifier) { // Ctrl+O, jump to previous location. - if (!m_tokens.isEmpty()) { + if (!m_tokens.isEmpty() + || !checkMode(VimMode::Normal)) { break; } @@ -1565,6 +1592,52 @@ bool VVim::handleKeyPressEvent(int key, int modifiers) break; } + case Qt::Key_M: + { + if (modifiers == Qt::NoModifier) { + if (m_keys.isEmpty() && m_tokens.isEmpty()) { + // m, creating a mark. + // We can create marks in Visual mode, too. + m_keys.append(keyInfo); + goto accept; + } + } + + break; + } + + case Qt::Key_Apostrophe: + { + if (modifiers == Qt::NoModifier) { + // ', jump to the start of line of a mark. + // Repeat is useless. + tryGetRepeatToken(m_keys, m_tokens); + + if (m_keys.isEmpty()) { + m_keys.append(keyInfo); + goto accept; + } + } + + break; + } + + case Qt::Key_QuoteLeft: + { + if (modifiers == Qt::NoModifier) { + // `, jump to a mark. + // Repeat is useless. + tryGetRepeatToken(m_keys, m_tokens); + + if (m_keys.isEmpty()) { + m_keys.append(keyInfo); + goto accept; + } + } + + break; + } + default: break; } @@ -2238,6 +2311,40 @@ handle_target: break; } + case Movement::MarkJump: + // Fall through. + case Movement::MarkJumpLine: + { + // repeat is useless here. + const Key &key = p_token.m_key; + QChar target = keyToChar(key.m_key, key.m_modifiers); + Location loc = m_marks.getMarkLocation(target); + if (loc.isValid()) { + if (loc.m_blockNumber >= p_doc->blockCount()) { + // Invalid block number. + message(tr("Mark not set")); + m_marks.clearMark(target); + break; + } + + // Different from Vim: + // We just use the block number for mark, so if we delete the line + // where the mark locates, we could not detect if it is set or not. + QTextBlock block = p_doc->findBlockByNumber(loc.m_blockNumber); + p_cursor.setPosition(block.position(), p_moveMode); + if (p_token.m_movement == Movement::MarkJump) { + setCursorPositionInBlock(p_cursor, loc.m_positionInBlock, p_moveMode); + } else { + // Jump to the first non-space character. + VEditUtils::moveCursorFirstNonSpaceCharacter(p_cursor, p_moveMode); + } + + hasMoved = true; + } + + break; + } + default: break; } @@ -3475,6 +3582,17 @@ bool VVim::expectingLeaderSequence() const return m_keys.first() == Key(Qt::Key_Backslash); } +bool VVim::expectingMarkName() const +{ + return checkPendingKey(Key(Qt::Key_M)) && m_tokens.isEmpty(); +} + +bool VVim::expectingMarkTarget() const +{ + return checkPendingKey(Key(Qt::Key_Apostrophe)) + || checkPendingKey(Key(Qt::Key_QuoteLeft)); +} + QChar VVim::keyToRegisterName(const Key &p_key) const { if (p_key.isAlphabet()) { @@ -3816,6 +3934,11 @@ const QMap &VVim::getRegisters() const return m_registers; } +const VVim::Marks &VVim::getMarks() const +{ + return m_marks; +} + QChar VVim::getCurrentRegisterName() const { return m_regName; @@ -4026,7 +4149,7 @@ bool VVim::processLeaderSequence(const Key &p_key) } VVim::LocationStack::LocationStack(int p_maximum) - : c_maximumLocations(p_maximum < 10 ? 10 : p_maximum), m_pointer(0) + : m_pointer(0), c_maximumLocations(p_maximum < 10 ? 10 : p_maximum) { } @@ -4102,3 +4225,69 @@ const VVim::Location &VVim::LocationStack::nextLocation() return m_locations.at(m_pointer); } + +VVim::Marks::Marks() +{ + for (char ch = 'a'; ch <= 'z'; ++ch) { + m_marks[QChar(ch)] = Mark(); + } +} + +void VVim::Marks::setMark(QChar p_name, const QTextCursor &p_cursor) +{ + auto it = m_marks.find(p_name); + if (it == m_marks.end()) { + return; + } + + it->m_location.m_blockNumber = p_cursor.block().blockNumber(); + it->m_location.m_positionInBlock = p_cursor.positionInBlock(); + it->m_text = p_cursor.block().text().trimmed(); + it->m_name = p_name; + + m_lastUsedMark = p_name; + + qDebug() << QString("set mark %1 to (%2,%3)") + .arg(p_name) + .arg(it->m_location.m_blockNumber) + .arg(it->m_location.m_positionInBlock); +} + +void VVim::Marks::clearMark(QChar p_name) +{ + auto it = m_marks.find(p_name); + if (it == m_marks.end()) { + return; + } + + it->m_location = Location(); + it->m_text.clear(); + + if (m_lastUsedMark == p_name) { + m_lastUsedMark = QChar(); + } +} + +VVim::Location VVim::Marks::getMarkLocation(QChar p_name) +{ + auto it = m_marks.find(p_name); + if (it == m_marks.end()) { + return Location(); + } + + if (it->m_location.isValid()) { + m_lastUsedMark = p_name; + } + + return it->m_location; +} + +const QMap &VVim::Marks::getMarks() const +{ + return m_marks; +} + +QChar VVim::Marks::getLastUsedMark() const +{ + return m_lastUsedMark; +} diff --git a/src/utils/vvim.h b/src/utils/vvim.h index 0a5cdcf2..6ddba224 100644 --- a/src/utils/vvim.h +++ b/src/utils/vvim.h @@ -28,6 +28,30 @@ class VVim : public QObject public: explicit VVim(VEdit *p_editor); + // Struct for a location. + struct Location + { + Location() : m_blockNumber(-1), m_positionInBlock(0) + { + } + + Location(int p_blockNumber, int p_positionInBlock) + : m_blockNumber(p_blockNumber), m_positionInBlock(p_positionInBlock) + { + } + + bool isValid() const + { + return m_blockNumber > -1; + } + + // Block number of the location, based on 0. + int m_blockNumber; + + // Position in block, based on 0. + int m_positionInBlock; + }; + struct Register { Register(QChar p_name, const QString &p_value) @@ -91,6 +115,37 @@ public: bool m_append; }; + struct Mark + { + QChar m_name; + Location m_location; + QString m_text; + }; + + // We only support simple local marks a-z. + class Marks + { + public: + Marks(); + + // Set mark @p_name to point to @p_cursor. + void setMark(QChar p_name, const QTextCursor &p_cursor); + + void clearMark(QChar p_name); + + // Return the location of mark @p_name. + Location getMarkLocation(QChar p_name); + + const QMap &getMarks() const; + + QChar getLastUsedMark() const; + + private: + QMap m_marks; + + QChar m_lastUsedMark; + }; + // Handle key press event. // Returns true if the event is consumed and need no more handling. bool handleKeyPressEvent(QKeyEvent *p_event); @@ -113,6 +168,9 @@ public: // Turn m_pendingKeys to a string. QString getPendingKeys() const; + // Get m_marks. + const VVim::Marks &getMarks() const; + signals: // Emit when current mode has been changed. void modeChanged(VimMode p_mode); @@ -237,6 +295,8 @@ private: FindBackward, TillForward, TillBackward, + MarkJump, + MarkJumpLine, Invalid }; @@ -350,30 +410,6 @@ private: Key m_key; }; - // Struct for a location. - struct Location - { - Location() : m_blockNumber(-1), m_positionInBlock(0) - { - } - - Location(int p_blockNumber, int p_positionInBlock) - : m_blockNumber(p_blockNumber), m_positionInBlock(p_positionInBlock) - { - } - - bool isValid() const - { - return m_blockNumber > -1; - } - - // Block number of the location, based on 0. - int m_blockNumber; - - // Position in block, based on 0. - int m_positionInBlock; - }; - // Stack for all the jump locations. // When we execute a jump action, we push current location to the stack and // remove older location with the same block number. @@ -492,6 +528,12 @@ private: // Check if we are in a leader sequence. bool expectingLeaderSequence() const; + // Check m_keys to see if we are expecting a mark name to create a mark. + bool expectingMarkName() const; + + // Check m_keys to see if we are expecting a mark name as the target. + bool expectingMarkTarget() const; + // Return the corresponding register name of @p_key. // If @p_key is not a valid register name, return a NULL QChar. QChar keyToRegisterName(const Key &p_key) const; @@ -633,6 +675,8 @@ private: LocationStack m_locations; + Marks m_marks; + static const QChar c_unnamedRegister; static const QChar c_blackHoleRegister; static const QChar c_selectionRegister; diff --git a/src/vvimindicator.cpp b/src/vvimindicator.cpp index 0bcd8c3d..6ed0c8be 100644 --- a/src/vvimindicator.cpp +++ b/src/vvimindicator.cpp @@ -31,7 +31,6 @@ void VVimIndicator::setupUI() QTreeWidget *regTree = new QTreeWidget(this); regTree->setColumnCount(2); regTree->header()->setStretchLastSection(true); - regTree->header()->setProperty("RegisterTree", true); QStringList headers; headers << tr("Register") << tr("Value"); regTree->setHeaderLabels(headers); @@ -39,6 +38,21 @@ void VVimIndicator::setupUI() connect(m_regBtn, &VButtonWithWidget::popupWidgetAboutToShow, this, &VVimIndicator::updateRegistersTree); + m_markBtn = new VButtonWithWidget(QIcon(":/resources/icons/arrow_dropup.svg"), + "[]", + this); + m_markBtn->setProperty("StatusBtn", true); + m_markBtn->setFocusPolicy(Qt::NoFocus); + QTreeWidget *markTree = new QTreeWidget(this); + markTree->setColumnCount(4); + markTree->header()->setStretchLastSection(true); + headers.clear(); + headers << tr("Mark") << tr("Line") << tr("Column") << tr("Text"); + markTree->setHeaderLabels(headers); + m_markBtn->setPopupWidget(markTree); + connect(m_markBtn, &VButtonWithWidget::popupWidgetAboutToShow, + this, &VVimIndicator::updateMarksTree); + m_keyLabel = new QLabel(this); QFontMetrics metric(font()); m_keyLabel->setMinimumWidth(metric.width('A') * 5); @@ -46,6 +60,7 @@ void VVimIndicator::setupUI() QHBoxLayout *mainLayout = new QHBoxLayout(this); mainLayout->addWidget(m_modeLabel); mainLayout->addWidget(m_regBtn); + mainLayout->addWidget(m_markBtn); mainLayout->addWidget(m_keyLabel); mainLayout->setContentsMargins(0, 0, 0, 0); @@ -148,6 +163,7 @@ void VVimIndicator::update(const VVim *p_vim) VimMode mode = p_vim->getMode(); QChar curRegName = p_vim->getCurrentRegisterName(); + QChar lastUsedMark = p_vim->getMarks().getLastUsedMark(); QString style = QString("QLabel { padding: 0px 2px 0px 2px; font: bold; background-color: %1; }") .arg(modeBackgroundColor(mode)); @@ -156,6 +172,10 @@ void VVimIndicator::update(const VVim *p_vim) m_regBtn->setText(curRegName); + QString markText = QString("[%1]") + .arg(lastUsedMark.isNull() ? QChar(' ') : lastUsedMark); + m_markBtn->setText(markText); + QString keyText = QString("%1") .arg(p_vim->getPendingKeys()); m_keyLabel->setText(keyText); @@ -172,3 +192,37 @@ void VVimIndicator::updateRegistersTree(QWidget *p_widget) const QMap ®s = m_vim->getRegisters(); fillTreeItemsWithRegisters(regTree, regs); } + +static void fillTreeItemsWithMarks(QTreeWidget *p_tree, + const QMap &p_marks) +{ + p_tree->clear(); + for (auto const &mark : p_marks) { + if (!mark.m_location.isValid()) { + continue; + } + + QStringList itemStr; + itemStr << mark.m_name << QString::number(mark.m_location.m_blockNumber + 1) + << QString::number(mark.m_location.m_positionInBlock) << mark.m_text; + QTreeWidgetItem *item = new QTreeWidgetItem(p_tree, itemStr); + item->setFlags(item->flags() | Qt::ItemIsSelectable | Qt::ItemIsEditable); + } + + p_tree->resizeColumnToContents(0); + p_tree->resizeColumnToContents(1); + p_tree->resizeColumnToContents(2); + p_tree->resizeColumnToContents(3); +} + +void VVimIndicator::updateMarksTree(QWidget *p_widget) +{ + QTreeWidget *markTree = dynamic_cast(p_widget); + if (!m_vim) { + markTree->clear(); + return; + } + + const QMap &marks = m_vim->getMarks().getMarks(); + fillTreeItemsWithMarks(markTree, marks); +} diff --git a/src/vvimindicator.h b/src/vvimindicator.h index 5930ae66..6353a51c 100644 --- a/src/vvimindicator.h +++ b/src/vvimindicator.h @@ -20,6 +20,8 @@ public slots: private slots: void updateRegistersTree(QWidget *p_widget); + void updateMarksTree(QWidget *p_widget); + private: void setupUI(); @@ -31,6 +33,9 @@ private: // Indicate the registers. VButtonWithWidget *m_regBtn; + // Indicate the marks. + VButtonWithWidget *m_markBtn; + // Indicate the pending keys. QLabel *m_keyLabel;