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.
This commit is contained in:
Le Tan 2017-06-20 22:53:59 +08:00
parent 878264b8fc
commit e305024a58
4 changed files with 322 additions and 30 deletions

View File

@ -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<QChar, VVim::Register> &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<QChar, VVim::Mark> &VVim::Marks::getMarks() const
{
return m_marks;
}
QChar VVim::Marks::getLastUsedMark() const
{
return m_lastUsedMark;
}

View File

@ -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<QChar, VVim::Mark> &getMarks() const;
QChar getLastUsedMark() const;
private:
QMap<QChar, Mark> 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;

View File

@ -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("<span style=\"font-weight:bold;\">%1</span>")
.arg(p_vim->getPendingKeys());
m_keyLabel->setText(keyText);
@ -172,3 +192,37 @@ void VVimIndicator::updateRegistersTree(QWidget *p_widget)
const QMap<QChar, VVim::Register> &regs = m_vim->getRegisters();
fillTreeItemsWithRegisters(regTree, regs);
}
static void fillTreeItemsWithMarks(QTreeWidget *p_tree,
const QMap<QChar, VVim::Mark> &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<QTreeWidget *>(p_widget);
if (!m_vim) {
markTree->clear();
return;
}
const QMap<QChar, VVim::Mark> &marks = m_vim->getMarks().getMarks();
fillTreeItemsWithMarks(markTree, marks);
}

View File

@ -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;