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