diff --git a/src/resources/docs/shortcuts_en.md b/src/resources/docs/shortcuts_en.md index efadd552..5a0c2948 100644 --- a/src/resources/docs/shortcuts_en.md +++ b/src/resources/docs/shortcuts_en.md @@ -49,13 +49,17 @@ Save current changes and exit edit mode. #### Text Editing - `Ctrl+B` -Insert bold. Press `Ctrl+B` again to exit. Current selected text will be changed to bold if exist. +Insert bold. Press `Ctrl+B` again to exit. Current selected text will be changed to bold if exists. - `Ctrl+I` -Insert italic. Press `Ctrl+I` again to exit. Current selected text will be changed to italic if exist. +Insert italic. Press `Ctrl+I` again to exit. Current selected text will be changed to italic if exists. - `Ctrl+D` -Insert strikethrought. Press `Ctrl+D` again to exit. Current selected text will be changed to strikethrough if exist. +Insert strikethrought. Press `Ctrl+D` again to exit. Current selected text will be changed to strikethrough if exists. - `Ctrl+O` -Insert inline code. Press `Ctrl+O` again to exit. Current selected text will be changed to inline code if exist. +Insert inline code. Press `Ctrl+O` again to exit. Current selected text will be changed to inline code if exists. +- `Ctrl+M` +Insert fenced code block. Press `Ctrl+M` again to exit. Current selected text will be wrapped into a code block if exists. +- `Ctrl+L` +Insert link. - `Ctrl+H` Backspace. Delete a character backward. - `Ctrl+W` @@ -63,7 +67,7 @@ Delete all the characters from current cursor to the first space backward. - `Ctrl+U` Delete all the characters from current cursor to the beginning of current line. - `Ctrl+` -Insert title at level ``. `` should be 1 to 6. Current selected text will be changed to title if exist. +Insert title at level ``. `` should be 1 to 6. Current selected text will be changed to title if exists. - `Tab`/`Shift+Tab` Increase or decrease the indentation. If any text is selected, the indentation will operate on all these selected lines. - `Shift+Enter` diff --git a/src/resources/docs/shortcuts_zh.md b/src/resources/docs/shortcuts_zh.md index 431e0ee3..adb72f1f 100644 --- a/src/resources/docs/shortcuts_zh.md +++ b/src/resources/docs/shortcuts_zh.md @@ -56,6 +56,10 @@ 插入删除线;再次按`Ctrl+D`退出。如果已经选择文本,则将当前选择文本改为删除线。 - `Ctrl+O` 插入行内代码;再次按`Ctrl+O`退出。如果已经选择文本,则将当前选择文本改为行内代码。 +- `Ctrl+M` +插入代码块;再次按`Ctrl+M`退出。如果已经选择文本,则将当前选择文本嵌入到代码块中。 +- `Ctrl+L` +插入链接。 - `Ctrl+H` 退格键,向前删除一个字符。 - `Ctrl+W` diff --git a/src/resources/icons/code_block.svg b/src/resources/icons/code_block.svg new file mode 100644 index 00000000..82e44a14 --- /dev/null +++ b/src/resources/icons/code_block.svg @@ -0,0 +1,6 @@ + + + Layer 1 + # + + diff --git a/src/utils/veditutils.cpp b/src/utils/veditutils.cpp index 3dd81fab..36d08495 100644 --- a/src/utils/veditutils.cpp +++ b/src/utils/veditutils.cpp @@ -48,7 +48,7 @@ bool VEditUtils::insertBlockWithIndent(QTextCursor &p_cursor) { V_ASSERT(!p_cursor.hasSelection()); p_cursor.insertBlock(); - return indentBlockAsPreviousBlock(p_cursor); + return indentBlockAsBlock(p_cursor, false); } bool VEditUtils::insertListMarkAsPreviousBlock(QTextCursor &p_cursor) @@ -85,21 +85,16 @@ bool VEditUtils::insertListMarkAsPreviousBlock(QTextCursor &p_cursor) } -bool VEditUtils::indentBlockAsPreviousBlock(QTextCursor &p_cursor) +bool VEditUtils::indentBlockAsBlock(QTextCursor &p_cursor, bool p_next) { bool changed = false; QTextBlock block = p_cursor.block(); - if (block.blockNumber() == 0) { - // The first block. + QTextBlock refBlock = p_next ? block.next() : block.previous(); + if (!refBlock.isValid()) { return false; } - QTextBlock preBlock = block.previous(); - QString text = preBlock.text(); - QRegExp regExp("(^\\s*)"); - regExp.indexIn(text); - V_ASSERT(regExp.captureCount() == 1); - QString leadingSpaces = regExp.capturedTexts()[1]; + QString leadingSpaces = fetchIndentSpaces(refBlock); moveCursorFirstNonSpaceCharacter(p_cursor, QTextCursor::MoveAnchor); if (!p_cursor.atBlockStart()) { @@ -789,3 +784,30 @@ void VEditUtils::findCurrentWORD(const QTextCursor &p_cursor, p_end += block.position(); } +QString VEditUtils::fetchIndentSpaces(const QTextBlock &p_block) +{ + QString text = p_block.text(); + QRegExp regExp("(^\\s*)"); + regExp.indexIn(text); + Q_ASSERT(regExp.captureCount() == 1); + return regExp.capturedTexts()[1]; +} + +void VEditUtils::insertBlock(QTextCursor &p_cursor, + bool p_above) +{ + p_cursor.movePosition(p_above ? QTextCursor::StartOfBlock + : QTextCursor::EndOfBlock, + QTextCursor::MoveAnchor, + 1); + + p_cursor.insertBlock(); + + if (p_above) { + p_cursor.movePosition(QTextCursor::PreviousBlock, + QTextCursor::MoveAnchor, + 1); + } + + p_cursor.movePosition(QTextCursor::EndOfBlock); +} diff --git a/src/utils/veditutils.h b/src/utils/veditutils.h index 1272d44f..b630180d 100644 --- a/src/utils/veditutils.h +++ b/src/utils/veditutils.h @@ -24,10 +24,11 @@ public: // Need to call setTextCursor() to make it take effect. static void moveCursorFirstNonSpaceCharacter(QTextCursor &p_cursor, QTextCursor::MoveMode p_mode); - // Indent current block as previous block. + // Indent current block as next/previous block. // Return true if some changes have been made. // @p_cursor will be placed at the position after inserting leading spaces. - static bool indentBlockAsPreviousBlock(QTextCursor &p_cursor); + // @p_next: indent as next block or previous block. + static bool indentBlockAsBlock(QTextCursor &p_cursor, bool p_next); // Returns true if two blocks has the same indent. static bool hasSameIndent(const QTextBlock &p_blocka, const QTextBlock &p_blockb); @@ -157,6 +158,14 @@ public: int &p_start, int &p_end); + // Return the leading spaces of @p_block. + static QString fetchIndentSpaces(const QTextBlock &p_block); + + // Insert a block above/below current block. Move the cursor to the start of + // the new block after insertion. + static void insertBlock(QTextCursor &p_cursor, + bool p_above); + private: VEditUtils() {} }; diff --git a/src/utils/vvim.cpp b/src/utils/vvim.cpp index 51c6d941..5d9e1c6a 100644 --- a/src/utils/vvim.cpp +++ b/src/utils/vvim.cpp @@ -296,7 +296,7 @@ static void insertChangeBlockAfterDeletion(QTextCursor &p_cursor, int p_deletion } if (g_config->getAutoIndent()) { - VEditUtils::indentBlockAsPreviousBlock(p_cursor); + VEditUtils::indentBlockAsBlock(p_cursor, false); } } @@ -911,7 +911,7 @@ bool VVim::handleKeyPressEvent(int key, int modifiers, int *p_autoIndentPos) bool textInserted = false; if (g_config->getAutoIndent()) { - textInserted = VEditUtils::indentBlockAsPreviousBlock(cursor); + textInserted = VEditUtils::indentBlockAsBlock(cursor, false); if (g_config->getAutoList()) { textInserted = VEditUtils::insertListMarkAsPreviousBlock(cursor) || textInserted; diff --git a/src/vcaptain.h b/src/vcaptain.h index 5d961a80..98bc19d8 100644 --- a/src/vcaptain.h +++ b/src/vcaptain.h @@ -9,6 +9,7 @@ class QKeyEvent; class VNavigationMode; +class QShortcut; // void func(void *p_target, void *p_data); typedef std::function CaptainFunc; diff --git a/src/vconstants.h b/src/vconstants.h index 6a326d37..217e7b4b 100644 --- a/src/vconstants.h +++ b/src/vconstants.h @@ -52,12 +52,16 @@ namespace DirConfig static const QString c_emptyHeaderName = "[EMPTY]"; -enum class TextDecoration { None, - Bold, - Italic, - Underline, - Strikethrough, - InlineCode }; +enum class TextDecoration +{ + None, + Bold, + Italic, + Underline, + Strikethrough, + InlineCode, + CodeBlock +}; enum FindOption { diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index 688da0ea..e737d32a 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -463,6 +463,21 @@ void VMainWindow::initEditToolBar(QSize p_iconSize) m_editToolBar->addAction(inlineCodeAct); + QAction *codeBlockAct = new QAction(QIcon(":/resources/icons/code_block.svg"), + tr("Code Block (Ctrl+M)"), + this); + codeBlockAct->setStatusTip(tr("Insert fenced code block text or wrap selected text into a fenced code block")); + connect(codeBlockAct, &QAction::triggered, + this, [this](){ + if (m_curTab) { + m_curTab->decorateText(TextDecoration::CodeBlock); + } + }); + + m_editToolBar->addAction(codeBlockAct); + + m_editToolBar->addSeparator(); + // Insert link. QAction *insetLinkAct = new QAction(QIcon(":/resources/icons/link.svg"), tr("Insert Link (Ctrl+L)"), this); diff --git a/src/vmdeditoperations.cpp b/src/vmdeditoperations.cpp index d8c5d926..0f73de01 100644 --- a/src/vmdeditoperations.cpp +++ b/src/vmdeditoperations.cpp @@ -289,6 +289,17 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event) break; } + case Qt::Key_M: + { + if (modifiers == Qt::ControlModifier) { + decorateCodeBlock(); + p_event->accept(); + ret = true; + } + + break; + } + case Qt::Key_O: { if (modifiers == Qt::ControlModifier) { @@ -684,6 +695,10 @@ void VMdEditOperations::decorateText(TextDecoration p_decoration) decorateInlineCode(); break; + case TextDecoration::CodeBlock: + decorateCodeBlock(); + break; + default: validDecoration = false; qDebug() << "decoration" << (int)p_decoration << "is not implemented yet"; @@ -807,6 +822,86 @@ void VMdEditOperations::decorateInlineCode() m_editor->setTextCursor(cursor); } +void VMdEditOperations::decorateCodeBlock() +{ + const QString marker("```"); + + QTextCursor cursor = m_editor->textCursor(); + cursor.beginEditBlock(); + if (cursor.hasSelection()) { + // Insert ``` around the selected text. + int start = cursor.selectionStart(); + int end = cursor.selectionEnd(); + + QString indentation = VEditUtils::fetchIndentSpaces(cursor.block()); + + // Insert the end marker first. + cursor.setPosition(end, QTextCursor::MoveAnchor); + VEditUtils::insertBlock(cursor, false); + VEditUtils::indentBlock(cursor, indentation); + cursor.insertText(marker); + + // Insert the start marker. + cursor.setPosition(start, QTextCursor::MoveAnchor); + VEditUtils::insertBlock(cursor, true); + VEditUtils::indentBlock(cursor, indentation); + cursor.insertText(marker); + } else { + // Insert ``` ``` and place cursor after the first marker. + // Or if current block or next block is ```, we will skip it. + QTextBlock block = cursor.block(); + int state = block.userState(); + if (state == HighlightBlockState::CodeBlock + || state == HighlightBlockState::CodeBlockStart + || state == HighlightBlockState::CodeBlockEnd) { + // Find the block end. + while (block.isValid()) { + if (block.userState() == HighlightBlockState::CodeBlockEnd) { + break; + } + + block = block.next(); + } + + if (block.isValid()) { + // It is CodeBlockEnd. + cursor.setPosition(block.position()); + if (block.next().isValid()) { + cursor.movePosition(QTextCursor::NextBlock); + cursor.movePosition(QTextCursor::StartOfBlock); + } else { + cursor.movePosition(QTextCursor::EndOfBlock); + } + } else { + // Reach the end of the document. + cursor.movePosition(QTextCursor::End); + } + } else { + bool insertInline = false; + if (!cursor.atBlockEnd()) { + cursor.insertBlock(); + cursor.movePosition(QTextCursor::PreviousBlock); + } else if (cursor.atBlockStart()) { + insertInline = true; + } + + if (!insertInline) { + VEditUtils::insertBlock(cursor, false); + VEditUtils::indentBlockAsBlock(cursor, false); + } + + cursor.insertText(marker); + + VEditUtils::insertBlock(cursor, true); + VEditUtils::indentBlockAsBlock(cursor, true); + cursor.insertText(marker); + } + } + + cursor.endEditBlock(); + m_editor->setTextCursor(cursor); +} + void VMdEditOperations::decorateStrikethrough() { QTextCursor cursor = m_editor->textCursor(); diff --git a/src/vmdeditoperations.h b/src/vmdeditoperations.h index 5f96367b..d8609339 100644 --- a/src/vmdeditoperations.h +++ b/src/vmdeditoperations.h @@ -72,6 +72,9 @@ private: // Insert inline-code marker or set selected text inline-coded. void decorateInlineCode(); + // Insert inline-code marker or set selected text inline-coded. + void decorateCodeBlock(); + // Insert strikethrough marker or set selected text strikethrough. void decorateStrikethrough(); diff --git a/src/vnote.qrc b/src/vnote.qrc index d62efb59..80d83209 100644 --- a/src/vnote.qrc +++ b/src/vnote.qrc @@ -133,5 +133,6 @@ resources/icons/compact_mode.svg resources/icons/heading_sequence.svg resources/icons/link.svg + resources/icons/code_block.svg