mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-05 22:09:52 +08:00
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:
parent
e1acd6e9a2
commit
9278149941
@ -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.
|
||||||
|
|
||||||
|
@ -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的宏和重复(`.`)特性。
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
|
111
src/vmdedit.cpp
111
src/vmdedit.cpp
@ -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;
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user