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.
- `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.

View File

@ -57,6 +57,8 @@
插入级别为`<Num>`的标题。`<Num>`应该是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的宏和重复(`.`)特性。

View File

@ -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<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
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;
const VEditConfig *m_editConfig;
VimMode m_mode;

View File

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

View File

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

View File

@ -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<VHeader> &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;
}

View File

@ -39,6 +39,9 @@ public:
const QVector<VHeader> &getHeaders() const;
public slots:
bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE;
signals:
void headersChanged(const QVector<VHeader> &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;