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;
This commit is contained in:
Le Tan 2017-06-30 20:31:46 +08:00
parent e1acd6e9a2
commit 9278149941
8 changed files with 217 additions and 5 deletions

View File

@ -57,6 +57,8 @@ Delete all the characters from current cursor to the beginning of current line.
Insert title at level `<Num>`. `<Num>` should be 1 to 6. Currently selected text will be changed to title if exist. Insert title at level `<Num>`. `<Num>` should be 1 to 6. Currently selected text will be changed to title if exist.
- `Tab`/`Shift+Tab` - `Tab`/`Shift+Tab`
Increase or decrease the indentation. If any text is selected, the indentation will operate on all these selected lines. 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` - `Shift+Left`, `Shift+Right`, `Shift+Up`, `Shift+Down`
Expand the selection one character left or right, or one line up or down. Expand the selection one character left or right, or one line up or down.
- `Ctrl+Shift+Left`, `Ctrl+Shift+Right` - `Ctrl+Shift+Left`, `Ctrl+Shift+Right`
@ -132,6 +134,13 @@ VNote supports following features of Vim:
- `u` and `Ctrl+R` for undo and redo; - `u` and `Ctrl+R` for undo and redo;
- Text objects `i/a`: word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, and `{}`; - Text objects `i/a`: word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, and `{}`;
- Command line `:w`, `:wq`, `:x`, `:q`, and `:q!`; - 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. For now, VNote does **NOT** support the macro and repeat(`.`) features of Vim.

View File

@ -57,6 +57,8 @@
插入级别为`<Num>`的标题。`<Num>`应该是1到6的一个数字。如果已经选择文本则将当前选择文本改为标题。 插入级别为`<Num>`的标题。`<Num>`应该是1到6的一个数字。如果已经选择文本则将当前选择文本改为标题。
- `Tab`/`Shift+Tab` - `Tab`/`Shift+Tab`
增加或减小缩进。如果已经选择文本,则对所有选择的行进行缩进操作。 增加或减小缩进。如果已经选择文本,则对所有选择的行进行缩进操作。
- `Shift+Enter`
插入两个空格然后换行在Markdown中类似于软换行的概念。
- `Shift+Left`, `Shift+Right`, `Shift+Up`, `Shift+Down` - `Shift+Left`, `Shift+Right`, `Shift+Up`, `Shift+Down`
扩展选定左右一个字符,或上下一行。 扩展选定左右一个字符,或上下一行。
- `Ctrl+Shift+Left`, `Ctrl+Shift+Right` - `Ctrl+Shift+Left`, `Ctrl+Shift+Right`
@ -131,6 +133,13 @@ VNote支持以下几个Vim的特性
- `u``Ctrl+R` 撤销和重做; - `u``Ctrl+R` 撤销和重做;
- 文本对象 `i/a`word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, and `{}`; - 文本对象 `i/a`word, WORD, `''`, `""`, `` ` ` ``, `()`, `[]`, `<>`, and `{}`;
- 命令行 `:w`, `:wq`, `:x`, `:q`, and `:q!`; - 命令行 `:w`, `:wq`, `:x`, `:q`, and `:q!`;
- 标题跳转
- `[[`:跳转到上一个标题;
- `]]`: 跳转到下一个标题;
- `[]`:跳转到上一个同层级的标题;
- `][`:跳转到下一个同层级的标题;
- `[{`:跳转到上一个高一层级的标题;
- `]}`:跳转到下一个高一层级的标题;
VNote目前暂时不支持Vim的宏和重复(`.`)特性。 VNote目前暂时不支持Vim的宏和重复(`.`)特性。

View File

@ -1173,7 +1173,22 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
addRangeToken(range); addRangeToken(range);
processCommand(m_tokens); processCommand(m_tokens);
break; 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; break;
@ -1197,6 +1212,19 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
addRangeToken(range); addRangeToken(range);
processCommand(m_tokens); processCommand(m_tokens);
break; 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; break;
@ -1855,7 +1883,15 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
addRangeToken(range); addRangeToken(range);
processCommand(m_tokens); processCommand(m_tokens);
break; 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; break;
@ -1876,7 +1912,15 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos)
addRangeToken(range); addRangeToken(range);
processCommand(m_tokens); processCommand(m_tokens);
break; 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; break;
@ -4881,3 +4925,24 @@ QChar VVim::Marks::getLastUsedMark() const
{ {
return m_lastUsedMark; return m_lastUsedMark;
} }
void VVim::processTitleJump(const QList<Token> &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);
}
}

View File

@ -662,6 +662,10 @@ private:
// P: "+P // P: "+P
bool processLeaderSequence(const Key &p_key); bool processLeaderSequence(const Key &p_key);
// Jump across titles.
// [[, ]], [], ][, [{, ]}.
void processTitleJump(const QList<Token> &p_tokens, bool p_forward, int p_relativeLevel);
VEdit *m_editor; VEdit *m_editor;
const VEditConfig *m_editConfig; const VEditConfig *m_editConfig;
VimMode m_mode; VimMode m_mode;

View File

@ -790,3 +790,11 @@ void VEdit::requestUpdateVimStatus()
emit vimStatusUpdated(NULL); 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;
}

View File

@ -110,6 +110,14 @@ signals:
public slots: public slots:
virtual void highlightCurrentLine(); 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: private slots:
void labelTimerTimeout(); void labelTimerTimeout();
void highlightSelectedWord(); void highlightSelectedWord();

View File

@ -264,10 +264,10 @@ void VMdEdit::clearUnusedImages()
m_initImages.clear(); m_initImages.clear();
} }
void VMdEdit::updateCurHeader() int VMdEdit::currentCursorHeader() const
{ {
if (m_headers.isEmpty()) { if (m_headers.isEmpty()) {
return; return -1;
} }
int curLine = textCursor().block().firstLineNumber(); int curLine = textCursor().block().firstLineNumber();
@ -281,13 +281,26 @@ void VMdEdit::updateCurHeader()
} }
if (i == -1) { 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)); emit curHeaderChanged(VAnchor(m_file, "", -1, -1));
return; return;
} }
V_ASSERT(m_headers[i].index == i); emit curHeaderChanged(VAnchor(m_file, "", m_headers[idx].lineNumber, m_headers[idx].index));
emit curHeaderChanged(VAnchor(m_file, "", m_headers[i].lineNumber, m_headers[i].index));
} }
void VMdEdit::generateEditOutline() void VMdEdit::generateEditOutline()
@ -454,3 +467,91 @@ const QVector<VHeader> &VMdEdit::getHeaders() const
{ {
return m_headers; 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;
}

View File

@ -39,6 +39,9 @@ public:
const QVector<VHeader> &getHeaders() const; const QVector<VHeader> &getHeaders() const;
public slots:
bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE;
signals: signals:
void headersChanged(const QVector<VHeader> &headers); void headersChanged(const QVector<VHeader> &headers);
@ -68,13 +71,18 @@ protected:
private: private:
void initInitImages(); void initInitImages();
void clearUnusedImages(); void clearUnusedImages();
// p_text[p_index] is QChar::ObjectReplacementCharacter. Remove the line containing it. // p_text[p_index] is QChar::ObjectReplacementCharacter. Remove the line containing it.
// Returns the index of previous line's '\n'. // Returns the index of previous line's '\n'.
int removeObjectReplacementLine(QString &p_text, int p_index) const; int removeObjectReplacementLine(QString &p_text, int p_index) const;
// There is a QChar::ObjectReplacementCharacter in the selection. // There is a QChar::ObjectReplacementCharacter in the selection.
// Get the QImage. // Get the QImage.
QImage selectedImage(); QImage selectedImage();
// Return the header index in m_headers where current cursor locates.
int currentCursorHeader() const;
HGMarkdownHighlighter *m_mdHighlighter; HGMarkdownHighlighter *m_mdHighlighter;
VCodeBlockHighlightHelper *m_cbHighlighter; VCodeBlockHighlightHelper *m_cbHighlighter;
VImagePreviewer *m_imagePreviewer; VImagePreviewer *m_imagePreviewer;