From 92781499412a2f86e9652f41a1db446c40363424 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Fri, 30 Jun 2017 20:31:46 +0800 Subject: [PATCH] vim-mode: support shorcuts to jump between titles - `[[`: jump to previous title; - `]]`: jump to next title; - `[]`: jump to previous title at the same level; - `][`: jump to next title at the same level; - `[{`: jump to previous title at a higher level; - `]}`: jump to next title at a higher level; --- src/resources/docs/shortcuts_en.md | 9 +++ src/resources/docs/shortcuts_zh.md | 9 +++ src/utils/vvim.cpp | 65 +++++++++++++++++ src/utils/vvim.h | 4 ++ src/vedit.cpp | 8 +++ src/vedit.h | 8 +++ src/vmdedit.cpp | 111 +++++++++++++++++++++++++++-- src/vmdedit.h | 8 +++ 8 files changed, 217 insertions(+), 5 deletions(-) diff --git a/src/resources/docs/shortcuts_en.md b/src/resources/docs/shortcuts_en.md index 83fc9e12..cf00c186 100644 --- a/src/resources/docs/shortcuts_en.md +++ b/src/resources/docs/shortcuts_en.md @@ -57,6 +57,8 @@ Delete all the characters from current cursor to the beginning of current line. Insert title at level ``. `` should be 1 to 6. Currently selected text will be changed to title if exist. - `Tab`/`Shift+Tab` Increase or decrease the indentation. If any text is selected, the indentation will operate on all these selected lines. +- `Shift+Enter` +Insert two spaces followed by a new line, namely a soft linebreak in Markdown. - `Shift+Left`, `Shift+Right`, `Shift+Up`, `Shift+Down` Expand the selection one character left or right, or one line up or down. - `Ctrl+Shift+Left`, `Ctrl+Shift+Right` @@ -132,6 +134,13 @@ VNote supports following features of Vim: - `u` and `Ctrl+R` for undo and redo; - Text objects `i/a`: word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, and `{}`; - Command line `:w`, `:wq`, `:x`, `:q`, and `:q!`; +- Jump between titles + - `[[`: jump to previous title; + - `]]`: jump to next title; + - `[]`: jump to previous title at the same level; + - `][`: jump to next title at the same level; + - `[{`: jump to previous title at a higher level; + - `]}`: jump to next title at a higher level; For now, VNote does **NOT** support the macro and repeat(`.`) features of Vim. diff --git a/src/resources/docs/shortcuts_zh.md b/src/resources/docs/shortcuts_zh.md index bc15ccb7..e401e186 100644 --- a/src/resources/docs/shortcuts_zh.md +++ b/src/resources/docs/shortcuts_zh.md @@ -57,6 +57,8 @@ 插入级别为``的标题。``应该是1到6的一个数字。如果已经选择文本,则将当前选择文本改为标题。 - `Tab`/`Shift+Tab` 增加或减小缩进。如果已经选择文本,则对所有选择的行进行缩进操作。 +- `Shift+Enter` +插入两个空格然后换行,在Markdown中类似于软换行的概念。 - `Shift+Left`, `Shift+Right`, `Shift+Up`, `Shift+Down` 扩展选定左右一个字符,或上下一行。 - `Ctrl+Shift+Left`, `Ctrl+Shift+Right` @@ -131,6 +133,13 @@ VNote支持以下几个Vim的特性: - `u` 和 `Ctrl+R` 撤销和重做; - 文本对象 `i/a`:word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, and `{}`; - 命令行 `:w`, `:wq`, `:x`, `:q`, and `:q!`; +- 标题跳转 + - `[[`:跳转到上一个标题; + - `]]`: 跳转到下一个标题; + - `[]`:跳转到上一个同层级的标题; + - `][`:跳转到下一个同层级的标题; + - `[{`:跳转到上一个高一层级的标题; + - `]}`:跳转到下一个高一层级的标题; VNote目前暂时不支持Vim的宏和重复(`.`)特性。 diff --git a/src/utils/vvim.cpp b/src/utils/vvim.cpp index bcd84c1f..b8e72d52 100644 --- a/src/utils/vvim.cpp +++ b/src/utils/vvim.cpp @@ -1173,7 +1173,22 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) addRangeToken(range); processCommand(m_tokens); break; + } else if (hasActionToken() || !checkMode(VimMode::Normal)) { + // Invalid sequence. + break; + } else if (m_keys.isEmpty()) { + // First ], pend it. + m_keys.append(keyInfo); + goto accept; + } else if (checkPendingKey(keyInfo)) { + // ]], goto next title, regardless of level. + processTitleJump(m_tokens, true, 1); + } else if (checkPendingKey(Key(Qt::Key_BracketLeft))) { + // [], goto previous title at the same level. + processTitleJump(m_tokens, false, 0); } + + break; } break; @@ -1197,6 +1212,19 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) addRangeToken(range); processCommand(m_tokens); break; + } else if (hasActionToken() || !checkMode(VimMode::Normal)) { + // Invalid sequence. + break; + } else if (m_keys.isEmpty()) { + // First [, pend it. + m_keys.append(keyInfo); + goto accept; + } else if (checkPendingKey(keyInfo)) { + // [[, goto previous title, regardless of level. + processTitleJump(m_tokens, false, 1); + } else if (checkPendingKey(Key(Qt::Key_BracketRight))) { + // ][, goto next title at the same level. + processTitleJump(m_tokens, true, 0); } break; @@ -1855,7 +1883,15 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) addRangeToken(range); processCommand(m_tokens); break; + } else if (hasActionToken() || !checkMode(VimMode::Normal)) { + // Invalid sequence. + break; + } else if (checkPendingKey(Key(Qt::Key_BracketLeft))) { + // [{, goto previous title at one higher level. + processTitleJump(m_tokens, false, -1); } + + break; } break; @@ -1876,7 +1912,15 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) addRangeToken(range); processCommand(m_tokens); break; + } else if (hasActionToken() || !checkMode(VimMode::Normal)) { + // Invalid sequence. + break; + } else if (checkPendingKey(Key(Qt::Key_BracketRight))) { + // ]}, goto next title at one higher level. + processTitleJump(m_tokens, true, -1); } + + break; } break; @@ -4881,3 +4925,24 @@ QChar VVim::Marks::getLastUsedMark() const { return m_lastUsedMark; } + +void VVim::processTitleJump(const QList &p_tokens, bool p_forward, int p_relativeLevel) +{ + int repeat = 1; + if (p_tokens.size() == 1) { + Token to = p_tokens.first(); + if (to.isRepeat()) { + repeat = to.m_repeat; + } else { + return; + } + } else if (!p_tokens.isEmpty()) { + return; + } + + QTextCursor cursor = m_editor->textCursor(); + if (m_editor->jumpTitle(p_forward, p_relativeLevel, repeat)) { + // Record current location. + m_locations.addLocation(cursor); + } +} diff --git a/src/utils/vvim.h b/src/utils/vvim.h index 0e0c9c90..1cc35644 100644 --- a/src/utils/vvim.h +++ b/src/utils/vvim.h @@ -662,6 +662,10 @@ private: // P: "+P bool processLeaderSequence(const Key &p_key); + // Jump across titles. + // [[, ]], [], ][, [{, ]}. + void processTitleJump(const QList &p_tokens, bool p_forward, int p_relativeLevel); + VEdit *m_editor; const VEditConfig *m_editConfig; VimMode m_mode; diff --git a/src/vedit.cpp b/src/vedit.cpp index 3077666c..5fa887de 100644 --- a/src/vedit.cpp +++ b/src/vedit.cpp @@ -790,3 +790,11 @@ void VEdit::requestUpdateVimStatus() emit vimStatusUpdated(NULL); } } + +bool VEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) +{ + Q_UNUSED(p_forward); + Q_UNUSED(p_relativeLevel); + Q_UNUSED(p_repeat); + return false; +} diff --git a/src/vedit.h b/src/vedit.h index fb12f578..48a477b4 100644 --- a/src/vedit.h +++ b/src/vedit.h @@ -110,6 +110,14 @@ signals: public slots: virtual void highlightCurrentLine(); + // Jump to a title. + // @p_forward: jump forward or backward. + // @p_relativeLevel: 0 for the same level as current header; + // negative value for upper level; + // positive value is ignored. + // Returns true if the jump succeeded. + virtual bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat); + private slots: void labelTimerTimeout(); void highlightSelectedWord(); diff --git a/src/vmdedit.cpp b/src/vmdedit.cpp index 42f5c23b..3a211d04 100644 --- a/src/vmdedit.cpp +++ b/src/vmdedit.cpp @@ -264,10 +264,10 @@ void VMdEdit::clearUnusedImages() m_initImages.clear(); } -void VMdEdit::updateCurHeader() +int VMdEdit::currentCursorHeader() const { if (m_headers.isEmpty()) { - return; + return -1; } int curLine = textCursor().block().firstLineNumber(); @@ -281,13 +281,26 @@ void VMdEdit::updateCurHeader() } if (i == -1) { + return -1; + } else { + Q_ASSERT(m_headers[i].index == i); + return i; + } +} + +void VMdEdit::updateCurHeader() +{ + if (m_headers.isEmpty()) { + return; + } + + int idx = currentCursorHeader(); + if (idx == -1) { emit curHeaderChanged(VAnchor(m_file, "", -1, -1)); return; } - V_ASSERT(m_headers[i].index == i); - - emit curHeaderChanged(VAnchor(m_file, "", m_headers[i].lineNumber, m_headers[i].index)); + emit curHeaderChanged(VAnchor(m_file, "", m_headers[idx].lineNumber, m_headers[idx].index)); } void VMdEdit::generateEditOutline() @@ -454,3 +467,91 @@ const QVector &VMdEdit::getHeaders() const { return m_headers; } + +bool VMdEdit::jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) +{ + if (m_headers.isEmpty()) { + return false; + } + + QTextCursor cursor = textCursor(); + int cursorLine = cursor.block().firstLineNumber(); + int targetIdx = -1; + // -1: skip level check. + int targetLevel = 0; + int idx = currentCursorHeader(); + if (idx == -1) { + // Cursor locates at the beginning, before any headers. + if (p_relativeLevel < 0 || !p_forward) { + return false; + } + } + + int delta = 1; + if (!p_forward) { + delta = -1; + } + + bool firstHeader = true; + for (targetIdx = idx == -1 ? 0 : idx; + targetIdx >= 0 && targetIdx < m_headers.size(); + targetIdx += delta) { + const VHeader &header = m_headers[targetIdx]; + if (header.isEmpty()) { + continue; + } + + if (targetLevel == 0) { + // The target level has not been init yet. + Q_ASSERT(firstHeader); + targetLevel = header.level; + if (p_relativeLevel < 0) { + targetLevel += p_relativeLevel; + if (targetLevel < 1) { + // Invalid level. + return false; + } + } else if (p_relativeLevel > 0) { + targetLevel = -1; + } + } + + if (targetLevel == -1 || header.level == targetLevel) { + if (firstHeader + && (cursorLine == header.lineNumber + || p_forward) + && idx != -1) { + // This header is not counted for the repeat. + firstHeader = false; + continue; + } + + if (--p_repeat == 0) { + // Found. + break; + } + } else if (header.level < targetLevel) { + // Stop by higher level. + return false; + } + + firstHeader = false; + } + + if (targetIdx < 0 || targetIdx >= m_headers.size()) { + return false; + } + + // Jump to target header. + int line = m_headers[targetIdx].lineNumber; + if (line > -1) { + QTextBlock block = document()->findBlockByLineNumber(line); + if (block.isValid()) { + cursor.setPosition(block.position()); + setTextCursor(cursor); + return true; + } + } + + return false; +} diff --git a/src/vmdedit.h b/src/vmdedit.h index 6ed62ddd..ecb0930f 100644 --- a/src/vmdedit.h +++ b/src/vmdedit.h @@ -39,6 +39,9 @@ public: const QVector &getHeaders() const; +public slots: + bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE; + signals: void headersChanged(const QVector &headers); @@ -68,13 +71,18 @@ protected: private: void initInitImages(); void clearUnusedImages(); + // p_text[p_index] is QChar::ObjectReplacementCharacter. Remove the line containing it. // Returns the index of previous line's '\n'. int removeObjectReplacementLine(QString &p_text, int p_index) const; + // There is a QChar::ObjectReplacementCharacter in the selection. // Get the QImage. QImage selectedImage(); + // Return the header index in m_headers where current cursor locates. + int currentCursorHeader() const; + HGMarkdownHighlighter *m_mdHighlighter; VCodeBlockHighlightHelper *m_cbHighlighter; VImagePreviewer *m_imagePreviewer;