diff --git a/src/resources/docs/shortcuts_en.md b/src/resources/docs/shortcuts_en.md index cf245590..53a4d353 100644 --- a/src/resources/docs/shortcuts_en.md +++ b/src/resources/docs/shortcuts_en.md @@ -66,7 +66,18 @@ Zoom in/out the page through the mouse scroll. Recover the page zoom factor to 100%. - `Ctrl+J/K` Scroll page down/up without changing cursor. - +- `Ctrl+N/P` +Activate auto-completion. + - `Ctrl+N/P` + Navigate through the completion list and insert current completion. + - `Ctrl+J/K` + Navigate through the completion list. + - `Ctrl+E` + Cancel completion. + - `Enter` + Insert current completion. + - `Ctrl+[` or `Escape` + Finish completion. #### Text Editing - `Ctrl+B` diff --git a/src/resources/docs/shortcuts_zh.md b/src/resources/docs/shortcuts_zh.md index ae518798..b786a0f8 100644 --- a/src/resources/docs/shortcuts_zh.md +++ b/src/resources/docs/shortcuts_zh.md @@ -66,6 +66,18 @@ 恢复页面大小为100%。 - `Ctrl+J/K` 向下/向上滚动页面,不会改变光标。 +- `Ctrl+N/P` +激活自动补全。 + - `Ctrl+N/P` + 浏览补全列表并插入当前补全。 + - `Ctrl+J/K` + 浏览补全列表。 + - `Ctrl+E` + 取消补全。 + - `Enter` + 插入补全。 + - `Ctrl+[` or `Escape` + 结束补全。 #### 文本编辑 - `Ctrl+B` diff --git a/src/resources/themes/v_detorte/v_detorte.palette b/src/resources/themes/v_detorte/v_detorte.palette index 8fdaec01..8ba2bcab 100644 --- a/src/resources/themes/v_detorte/v_detorte.palette +++ b/src/resources/themes/v_detorte/v_detorte.palette @@ -9,7 +9,7 @@ mdhl_file=v_detorte.mdhl css_file=v_detorte.css codeblock_css_file=v_detorte_codeblock.css mermaid_css_file=v_detorte_mermaid.css -version=7 +version=8 ; This mapping will be used to translate colors when the content of HTML is copied ; without background. You could just specify the foreground colors mapping here. @@ -308,6 +308,18 @@ listview_item_selected_avtive_bg=@active_bg listview_item_selected_inactive_fg=@inactive_fg listview_item_selected_inactive_bg=@inactive_bg +; QAbstractItemView for TextEdit Completer. +abstractitemview_textedit_fg=#000000 +abstractitemview_textedit_bg=#BCBCBC +abstractitemview_textedit_item_hover_fg=@abstractitemview_textedit_fg +abstractitemview_textedit_item_hover_bg=@master_hover_bg +abstractitemview_textedit_item_selected_fg=@abstractitemview_textedit_fg +abstractitemview_textedit_item_selected_bg=@master_light_bg +abstractitemview_textedit_item_selected_avtive_fg=@abstractitemview_textedit_fg +abstractitemview_textedit_item_selected_avtive_bg=@master_focus_bg +abstractitemview_textedit_item_selected_inactive_fg=@inactive_fg +abstractitemview_textedit_item_selected_inactive_bg=@inactive_bg + ; Splitter. splitter_handle_bg=@border_bg splitter_handle_pressed_bg=@pressed_bg diff --git a/src/resources/themes/v_detorte/v_detorte.qss b/src/resources/themes/v_detorte/v_detorte.qss index 290c54aa..e5fbfef4 100644 --- a/src/resources/themes/v_detorte/v_detorte.qss +++ b/src/resources/themes/v_detorte/v_detorte.qss @@ -930,6 +930,46 @@ QListView::item:disabled { } /* End QListView */ +/* QAbstractItemView for TextEdit Completer popup*/ +QAbstractItemView[TextEdit="true"] { + color: @abstractitemview_textedit_fg; + background: @abstractitemview_textedit_bg; + show-decoration-selected: 0; + border: none; + selection-background-color: transparent; + outline: none; +} + +QAbstractItemView[TextEdit="true"]::item { + padding-top: 5px; + padding-bottom: 5px; +} + +QAbstractItemView[TextEdit="true"]::item:hover { + color: @abstractitemview_textedit_item_hover_fg; + background: @abstractitemview_textedit_item_hover_bg; +} + +QAbstractItemView[TextEdit="true"]::item:selected { + color: @abstractitemview_textedit_item_selected_fg; + background: @abstractitemview_textedit_item_selected_bg; +} + +QAbstractItemView[TextEdit="true"]::item:selected:active { + color: @abstractitemview_textedit_item_selected_active_fg; + background: @abstractitemview_textedit_item_selected_active_bg; +} + +QAbstractItemView[TextEdit="true"]::item:selected:!active { + color: @abstractitemview_textedit_item_selected_inactive_fg; + background: @abstractitemview_textedit_item_selected_inactive_bg; +} + +QAbstractItemView[TextEdit="true"]::item:disabled { + background: transparent; +} +/* End QAbstractItemView */ + /* QSplitter */ QSplitter#MainSplitter { border: none; diff --git a/src/resources/themes/v_moonlight/v_moonlight.palette b/src/resources/themes/v_moonlight/v_moonlight.palette index 25d71527..63cbec8c 100644 --- a/src/resources/themes/v_moonlight/v_moonlight.palette +++ b/src/resources/themes/v_moonlight/v_moonlight.palette @@ -7,7 +7,7 @@ mdhl_file=v_moonlight.mdhl css_file=v_moonlight.css codeblock_css_file=v_moonlight_codeblock.css mermaid_css_file=v_moonlight_mermaid.css -version=18 +version=19 ; This mapping will be used to translate colors when the content of HTML is copied ; without background. You could just specify the foreground colors mapping here. @@ -306,6 +306,18 @@ listview_item_selected_avtive_bg=@active_bg listview_item_selected_inactive_fg=@inactive_fg listview_item_selected_inactive_bg=@inactive_bg +; QAbstractItemView for TextEdit Completer. +abstractitemview_textedit_fg=@content_fg +abstractitemview_textedit_bg=#323841 +abstractitemview_textedit_item_hover_fg=@master_fg +abstractitemview_textedit_item_hover_bg=@master_hover_bg +abstractitemview_textedit_item_selected_fg=@master_fg +abstractitemview_textedit_item_selected_bg=@master_bg +abstractitemview_textedit_item_selected_avtive_fg=@master_fg +abstractitemview_textedit_item_selected_avtive_bg=@master_focus_bg +abstractitemview_textedit_item_selected_inactive_fg=@inactive_fg +abstractitemview_textedit_item_selected_inactive_bg=@inactive_bg + ; Splitter. splitter_handle_bg=@border_bg splitter_handle_pressed_bg=@pressed_bg diff --git a/src/resources/themes/v_moonlight/v_moonlight.qss b/src/resources/themes/v_moonlight/v_moonlight.qss index 23e02e47..124d7d01 100644 --- a/src/resources/themes/v_moonlight/v_moonlight.qss +++ b/src/resources/themes/v_moonlight/v_moonlight.qss @@ -930,6 +930,46 @@ QListView::item:disabled { } /* End QListView */ +/* QAbstractItemView for TextEdit Completer popup*/ +QAbstractItemView[TextEdit="true"] { + color: @abstractitemview_textedit_fg; + background: @abstractitemview_textedit_bg; + show-decoration-selected: 0; + border: none; + selection-background-color: transparent; + outline: none; +} + +QAbstractItemView[TextEdit="true"]::item { + padding-top: 5px; + padding-bottom: 5px; +} + +QAbstractItemView[TextEdit="true"]::item:hover { + color: @abstractitemview_textedit_item_hover_fg; + background: @abstractitemview_textedit_item_hover_bg; +} + +QAbstractItemView[TextEdit="true"]::item:selected { + color: @abstractitemview_textedit_item_selected_fg; + background: @abstractitemview_textedit_item_selected_bg; +} + +QAbstractItemView[TextEdit="true"]::item:selected:active { + color: @abstractitemview_textedit_item_selected_active_fg; + background: @abstractitemview_textedit_item_selected_active_bg; +} + +QAbstractItemView[TextEdit="true"]::item:selected:!active { + color: @abstractitemview_textedit_item_selected_inactive_fg; + background: @abstractitemview_textedit_item_selected_inactive_bg; +} + +QAbstractItemView[TextEdit="true"]::item:disabled { + background: transparent; +} +/* End QAbstractItemView */ + /* QSplitter */ QSplitter#MainSplitter { border: none; diff --git a/src/resources/themes/v_native/v_native.palette b/src/resources/themes/v_native/v_native.palette index 62ad198c..936830bb 100644 --- a/src/resources/themes/v_native/v_native.palette +++ b/src/resources/themes/v_native/v_native.palette @@ -7,7 +7,7 @@ mdhl_file=v_native.mdhl css_file=v_native.css codeblock_css_file=v_native_codeblock.css mermaid_css_file=v_native_mermaid.css -version=15 +version=16 [phony] ; Abstract color attributes. diff --git a/src/resources/themes/v_native/v_native.qss b/src/resources/themes/v_native/v_native.qss index 83be9bcf..fff3a1f4 100644 --- a/src/resources/themes/v_native/v_native.qss +++ b/src/resources/themes/v_native/v_native.qss @@ -510,6 +510,12 @@ QListView::item { } /* End QListView */ +/* QAbstractItemView for TextEdit Completer popup*/ +QAbstractItemView[TextEdit="true"] { + border: 1px solid @border_bg; +} +/* End QAbstractItemView */ + /* QSplitter */ QSplitter { border: none; diff --git a/src/resources/themes/v_pure/v_pure.palette b/src/resources/themes/v_pure/v_pure.palette index ff3c8b72..e17f4b64 100644 --- a/src/resources/themes/v_pure/v_pure.palette +++ b/src/resources/themes/v_pure/v_pure.palette @@ -7,7 +7,7 @@ mdhl_file=v_pure.mdhl css_file=v_pure.css codeblock_css_file=v_pure_codeblock.css mermaid_css_file=v_pure_mermaid.css -version=16 +version=17 [phony] ; Abstract color attributes. @@ -300,6 +300,18 @@ listview_item_selected_avtive_bg=@active_bg listview_item_selected_inactive_fg=@inactive_fg listview_item_selected_inactive_bg=@inactive_bg +; QAbstractItemView for TextEdit Completer. +abstractitemview_textedit_fg=#content_fg +abstractitemview_textedit_bg=#DADADA +abstractitemview_textedit_item_hover_fg=@abstractitemview_textedit_fg +abstractitemview_textedit_item_hover_bg=@master_hover_bg +abstractitemview_textedit_item_selected_fg=@abstractitemview_textedit_fg +abstractitemview_textedit_item_selected_bg=@master_light_bg +abstractitemview_textedit_item_selected_avtive_fg=@abstractitemview_textedit_fg +abstractitemview_textedit_item_selected_avtive_bg=@master_focus_bg +abstractitemview_textedit_item_selected_inactive_fg=@inactive_fg +abstractitemview_textedit_item_selected_inactive_bg=@inactive_bg + ; Splitter. splitter_handle_bg=@border_bg splitter_handle_pressed_bg=@pressed_bg diff --git a/src/resources/themes/v_pure/v_pure.qss b/src/resources/themes/v_pure/v_pure.qss index 568325dc..381b68b1 100644 --- a/src/resources/themes/v_pure/v_pure.qss +++ b/src/resources/themes/v_pure/v_pure.qss @@ -930,6 +930,46 @@ QListView::item:disabled { } /* End QListView */ +/* QAbstractItemView for TextEdit Completer popup*/ +QAbstractItemView[TextEdit="true"] { + color: @abstractitemview_textedit_fg; + background: @abstractitemview_textedit_bg; + show-decoration-selected: 0; + border: none; + selection-background-color: transparent; + outline: none; +} + +QAbstractItemView[TextEdit="true"]::item { + padding-top: 5px; + padding-bottom: 5px; +} + +QAbstractItemView[TextEdit="true"]::item:hover { + color: @abstractitemview_textedit_item_hover_fg; + background: @abstractitemview_textedit_item_hover_bg; +} + +QAbstractItemView[TextEdit="true"]::item:selected { + color: @abstractitemview_textedit_item_selected_fg; + background: @abstractitemview_textedit_item_selected_bg; +} + +QAbstractItemView[TextEdit="true"]::item:selected:active { + color: @abstractitemview_textedit_item_selected_active_fg; + background: @abstractitemview_textedit_item_selected_active_bg; +} + +QAbstractItemView[TextEdit="true"]::item:selected:!active { + color: @abstractitemview_textedit_item_selected_inactive_fg; + background: @abstractitemview_textedit_item_selected_inactive_bg; +} + +QAbstractItemView[TextEdit="true"]::item:disabled { + background: transparent; +} +/* End QAbstractItemView */ + /* QSplitter */ QSplitter#MainSplitter { border: none; diff --git a/src/src.pro b/src/src.pro index 16291498..39d7975c 100644 --- a/src/src.pro +++ b/src/src.pro @@ -142,7 +142,8 @@ SOURCES += main.cpp\ vtagexplorer.cpp \ pegmarkdownhighlighter.cpp \ pegparser.cpp \ - peghighlighterresult.cpp + peghighlighterresult.cpp \ + vtexteditcompleter.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -279,7 +280,8 @@ HEADERS += vmainwindow.h \ markdownhighlighterdata.h \ pegmarkdownhighlighter.h \ pegparser.h \ - peghighlighterresult.h + peghighlighterresult.h \ + vtexteditcompleter.h RESOURCES += \ vnote.qrc \ diff --git a/src/veditarea.h b/src/veditarea.h index 19a93e7e..15db55dc 100644 --- a/src/veditarea.h +++ b/src/veditarea.h @@ -10,10 +10,13 @@ #include #include #include +#include + #include "vnotebook.h" #include "veditwindow.h" #include "vnavigationmode.h" #include "vfilesessioninfo.h" +#include "vtexteditcompleter.h" class VFile; class VDirectory; @@ -95,6 +98,8 @@ public: // Distribute all the splits evenly. void distributeSplits(); + QSharedPointer getCompleter() const; + signals: // Emit when current window's tab status updated. void tabStatusUpdated(const VEditTabInfo &p_info); @@ -244,6 +249,8 @@ private: bool m_autoSave; VMathJaxPreviewHelper *m_mathPreviewHelper; + + QSharedPointer m_completer; }; inline VEditWindow* VEditArea::getWindow(int windowIndex) const @@ -266,4 +273,14 @@ inline VMathJaxPreviewHelper *VEditArea::getMathJaxPreviewHelper() const { return m_mathPreviewHelper; } + +inline QSharedPointer VEditArea::getCompleter() const +{ + if (m_completer.isNull()) { + VEditArea *ea = const_cast(this); + ea->m_completer.reset(new VTextEditCompleter(ea)); + } + + return m_completer; +} #endif // VEDITAREA_H diff --git a/src/veditor.cpp b/src/veditor.cpp index cf7477df..e4d71cc8 100644 --- a/src/veditor.cpp +++ b/src/veditor.cpp @@ -15,7 +15,9 @@ extern VConfigManager *g_config; extern VMetaWordManager *g_mwMgr; -VEditor::VEditor(VFile *p_file, QWidget *p_editor) +VEditor::VEditor(VFile *p_file, + QWidget *p_editor, + const QSharedPointer &p_completer) : m_editor(p_editor), m_object(new VEditorObject(this, p_editor)), m_file(p_file), @@ -23,12 +25,16 @@ VEditor::VEditor(VFile *p_file, QWidget *p_editor) m_document(nullptr), m_enableInputMethod(true), m_timeStamp(0), - m_trailingSpaceSelectionTS(0) + m_trailingSpaceSelectionTS(0), + m_completer(p_completer) { } VEditor::~VEditor() { + if (m_completer->widget() == m_editor) { + m_completer->setWidget(NULL); + } } void VEditor::init() @@ -161,8 +167,19 @@ void VEditor::highlightOnCursorPositionChanged() } else { // Judge whether we have trailing space at current line. QString text = cursor.block().text(); - if (text.rbegin()->isSpace()) { - updateTrailingSpaceHighlights(); + if (!text.isEmpty()) { + auto it = text.rbegin(); + bool needUpdate = it->isSpace(); + if (!needUpdate + && cursor.atBlockEnd() + && text.size() > 1) { + ++it; + needUpdate = it->isSpace(); + } + + if (needUpdate) { + updateTrailingSpaceHighlights(); + } } // Handle word-wrap in one block. @@ -228,14 +245,15 @@ void VEditor::filterTrailingSpace(QList &p_selects, const QList &p_src) { QTextCursor cursor = textCursorW(); - if (!cursor.atBlockEnd()) { - p_selects.append(p_src); - return; - } - - int cursorPos = cursor.position(); + bool blockEnd = cursor.atBlockEnd(); + int blockNum = cursor.blockNumber(); for (auto it = p_src.begin(); it != p_src.end(); ++it) { - if (it->cursor.selectionEnd() == cursorPos) { + if (blockEnd && it->cursor.blockNumber() == blockNum) { + // When cursor is at block end, we do not display any trailing space + // at current line. + continue; + } else if (!it->cursor.atBlockEnd()) { + // Obsolete trailing space. continue; } else { p_selects.append(*it); @@ -1100,3 +1118,91 @@ bool VEditor::setCursorPosition(int p_blockNumber, int p_posInBlock) setTextCursorW(cursor); return true; } + +static Qt::CaseSensitivity completionCaseSensitivity(const QString &p_text) +{ + bool upperCase = false; + for (int i = 0; i < p_text.size(); ++i) { + if (p_text[i].isUpper()) { + upperCase = true; + break; + } + } + + return upperCase ? Qt::CaseSensitive : Qt::CaseInsensitive; +} + +void VEditor::requestCompletion(bool p_reversed) +{ + QTextCursor cursor = textCursorW(); + cursor.clearSelection(); + setTextCursorW(cursor); + + QStringList words = generateCompletionCandidates(); + QString prefix = fetchCompletionPrefix(); + // Smart case. + Qt::CaseSensitivity cs = completionCaseSensitivity(prefix); + + cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, prefix.size()); + QRect popupRect = cursorRectW(cursor); + popupRect.setLeft(popupRect.left() + lineNumberAreaWidth()); + + m_completer->performCompletion(words, prefix, cs, p_reversed, popupRect, this); +} + +bool VEditor::isCompletionActivated() const +{ + if (m_completer->widget() == m_editor + && m_completer->isPopupVisible()) { + return true; + } + + return false; +} + +QStringList VEditor::generateCompletionCandidates() const +{ + QString content = getContent(); + QTextCursor cursor = textCursorW(); + int start, end; + VEditUtils::findCurrentWord(cursor, start, end); + + QRegExp reg("\\W+"); + + QStringList ret = content.mid(end).split(reg, QString::SkipEmptyParts); + ret.append(content.left(start).split(reg, QString::SkipEmptyParts)); + ret.removeDuplicates(); + return ret; +} + +QString VEditor::fetchCompletionPrefix() const +{ + QTextCursor cursor = textCursorW(); + if (cursor.atBlockStart()) { + return QString(); + } + + int pos = cursor.position() - 1; + int blockPos = cursor.block().position(); + QString prefix; + while (pos >= blockPos) { + QChar ch = m_document->characterAt(pos); + if (ch.isSpace()) { + break; + } + + prefix.prepend(ch); + --pos; + } + + return prefix; +} + +// @p_prefix may be longer than @p_completion. +void VEditor::insertCompletion(const QString &p_prefix, const QString &p_completion) +{ + QTextCursor cursor = textCursorW(); + cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, p_prefix.size()); + cursor.insertText(p_completion); + setTextCursorW(cursor); +} diff --git a/src/veditor.h b/src/veditor.h index c3bdb6bf..fb929b4f 100644 --- a/src/veditor.h +++ b/src/veditor.h @@ -6,11 +6,13 @@ #include #include #include +#include #include "veditconfig.h" #include "vconstants.h" #include "vfile.h" #include "vwordcountinfo.h" +#include "vtexteditcompleter.h" class QWidget; class VEditorObject; @@ -39,7 +41,9 @@ enum class SelectionId { class VEditor { public: - explicit VEditor(VFile *p_file, QWidget *p_editor); + explicit VEditor(VFile *p_file, + QWidget *p_editor, + const QSharedPointer &p_completer); virtual ~VEditor(); @@ -154,6 +158,15 @@ public: virtual bool setCursorPosition(int p_blockNumber, int p_posInBlock); + QString fetchCompletionPrefix() const; + + // Request text completion. + virtual void requestCompletion(bool p_reversed); + + virtual bool isCompletionActivated() const; + + virtual void insertCompletion(const QString &p_prefix, const QString &p_completion); + // Wrapper functions for QPlainTextEdit/QTextEdit. // Ends with W to distinguish it from the original interfaces. public: @@ -201,6 +214,10 @@ public: virtual void ensureCursorVisibleW() = 0; + virtual QRect cursorRectW() = 0; + + virtual QRect cursorRectW(const QTextCursor &p_cursor) = 0; + protected: void init(); @@ -235,6 +252,8 @@ protected: bool handleWheelEvent(QWheelEvent *p_event); + virtual int lineNumberAreaWidth() const = 0; + QWidget *m_editor; VEditorObject *m_object; @@ -288,6 +307,8 @@ private: // Highlight @p_cursor as the searched keyword under cursor. void highlightSearchedWordUnderCursor(const QTextCursor &p_cursor); + QStringList generateCompletionCandidates() const; + QLabel *m_wrapLabel; QTimer *m_labelTimer; @@ -329,6 +350,8 @@ private: TimeStamp m_trailingSpaceSelectionTS; + QSharedPointer m_completer; + // Functions for private slots. private: void labelTimerTimeout(); diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index 8256ceb1..2e4b6067 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -3343,3 +3343,8 @@ void VMainWindow::focusEditArea() const widget->setFocus(); } + +void VMainWindow::setCaptainModeEnabled(bool p_enabled) +{ + m_captain->setCaptainModeEnabled(p_enabled); +} diff --git a/src/vmainwindow.h b/src/vmainwindow.h index f60151ce..243d56b1 100644 --- a/src/vmainwindow.h +++ b/src/vmainwindow.h @@ -128,6 +128,8 @@ public: VExplorer *getExplorer() const; + void setCaptainModeEnabled(bool p_enabled); + signals: // Emit when editor related configurations were changed by user. void editorConfigUpdated(); diff --git a/src/vmdeditoperations.cpp b/src/vmdeditoperations.cpp index 3273cdb9..84c0bc9d 100644 --- a/src/vmdeditoperations.cpp +++ b/src/vmdeditoperations.cpp @@ -431,6 +431,30 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event) break; } + case Qt::Key_N: + { + if (VUtils::isControlModifierForVim(modifiers)) { + // Completion. + if (!m_editor->isCompletionActivated()) { + m_editor->requestCompletion(false); + } + } + + break; + } + + case Qt::Key_P: + { + if (VUtils::isControlModifierForVim(modifiers)) { + // Completion. + if (!m_editor->isCompletionActivated()) { + m_editor->requestCompletion(true); + } + } + + break; + } + default: break; } diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp index 9fcba38e..675e965f 100644 --- a/src/vmdeditor.cpp +++ b/src/vmdeditor.cpp @@ -32,9 +32,10 @@ extern VConfigManager *g_config; VMdEditor::VMdEditor(VFile *p_file, VDocument *p_doc, MarkdownConverterType p_type, + const QSharedPointer &p_completer, QWidget *p_parent) : VTextEdit(p_parent), - VEditor(p_file, this), + VEditor(p_file, this, p_completer), m_pegHighlighter(NULL), m_freshEdit(true), m_textToHtmlDialog(NULL), @@ -1411,3 +1412,8 @@ void VMdEditor::setFontAndPaletteByStyleSheet(const QFont &p_font, const QPalett .arg(p_palette.color(QPalette::Text).name()) .arg(p_palette.color(QPalette::Base).name())); } + +int VMdEditor::lineNumberAreaWidth() const +{ + return VTextEdit::lineNumberAreaWidth(); +} diff --git a/src/vmdeditor.h b/src/vmdeditor.h index 74e06e15..4a0eda5b 100644 --- a/src/vmdeditor.h +++ b/src/vmdeditor.h @@ -28,6 +28,7 @@ public: VMdEditor(VFile *p_file, VDocument *p_doc, MarkdownConverterType p_type, + const QSharedPointer &p_completer, QWidget *p_parent = nullptr); void beginEdit() Q_DECL_OVERRIDE; @@ -189,6 +190,16 @@ public: ensureCursorVisible(); } + QRect cursorRectW() Q_DECL_OVERRIDE + { + return cursorRect(); + } + + QRect cursorRectW(const QTextCursor &p_cursor) Q_DECL_OVERRIDE + { + return cursorRect(p_cursor); + } + signals: // Signal when headers change. void headersChanged(const QVector &p_headers); @@ -223,6 +234,8 @@ protected: void wheelEvent(QWheelEvent *p_event) Q_DECL_OVERRIDE; + int lineNumberAreaWidth() const Q_DECL_OVERRIDE; + private slots: // Update m_headers according to elements. void updateHeaders(const QVector &p_headerRegions); diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index 7a364286..a90d3642 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -473,7 +473,11 @@ void VMdTab::setupMarkdownEditor() { Q_ASSERT(!m_editor); - m_editor = new VMdEditor(m_file, m_document, m_mdConType, this); + m_editor = new VMdEditor(m_file, + m_document, + m_mdConType, + m_editArea->getCompleter(), + this); m_editor->setProperty("MainEditor", true); m_editor->setEditTab(this); int delta = g_config->getEditorZoomDelta(); diff --git a/src/vtextedit.cpp b/src/vtextedit.cpp index 306b586f..a6c6f473 100644 --- a/src/vtextedit.cpp +++ b/src/vtextedit.cpp @@ -459,3 +459,8 @@ void VTextEdit::dragMoveEvent(QDragMoveEvent *p_event) // TODO: find out the rect of current cursor to update that rect only. update(); } + +int VTextEdit::lineNumberAreaWidth() const +{ + return m_lineNumberArea->width(); +} diff --git a/src/vtextedit.h b/src/vtextedit.h index 4cb5a64f..6db3841e 100644 --- a/src/vtextedit.h +++ b/src/vtextedit.h @@ -81,6 +81,8 @@ protected: // Return the Y offset of the content via the scrollbar. int contentOffsetY() const; + int lineNumberAreaWidth() const; + void updateLineNumberAreaWidth(const QFontMetrics &p_metrics); void dragMoveEvent(QDragMoveEvent *p_event) Q_DECL_OVERRIDE; diff --git a/src/vtexteditcompleter.cpp b/src/vtexteditcompleter.cpp new file mode 100644 index 00000000..89ccba91 --- /dev/null +++ b/src/vtexteditcompleter.cpp @@ -0,0 +1,321 @@ +#include "vtexteditcompleter.h" + +#include +#include +#include +#include +#include +#include + +#include "utils/vutils.h" +#include "veditor.h" +#include "vmainwindow.h" + +extern VMainWindow *g_mainWin; + +VTextEditCompleter::VTextEditCompleter(QObject *p_parent) + : QCompleter(p_parent), + m_initialized(false) +{ +} + +void VTextEditCompleter::performCompletion(const QStringList &p_words, + const QString &p_prefix, + Qt::CaseSensitivity p_cs, + bool p_reversed, + const QRect &p_rect, + VEditor *p_editor) +{ + init(); + + m_editor = p_editor; + + setWidget(m_editor->getEditor()); + + m_model->setStringList(p_words); + setCaseSensitivity(p_cs); + setCompletionPrefix(p_prefix); + + int cnt = completionCount(); + if (cnt == 0) { + finishCompletion(); + return; + } + + selectRow(p_reversed ? cnt - 1 : 0); + + if (cnt == 1 && currentCompletion() == p_prefix) { + finishCompletion(); + return; + } + + g_mainWin->setCaptainModeEnabled(false); + + m_insertedCompletion = p_prefix; + insertCurrentCompletion(); + + auto pu = popup(); + QRect rt(p_rect); + rt.setWidth(pu->sizeHintForColumn(0) + pu->verticalScrollBar()->sizeHint().width()); + complete(rt); +} + +void VTextEditCompleter::init() +{ + if (m_initialized) { + return; + } + + m_initialized = true; + + m_model = new QStringListModel(this); + setModel(m_model); + + popup()->setProperty("TextEdit", true); + popup()->setItemDelegate(new QStyledItemDelegate(this)); + + connect(this, static_cast(&QCompleter::activated), + this, [this](const QString &p_text) { + insertCompletion(p_text); + finishCompletion(); + }); +} + +void VTextEditCompleter::selectNextCompletion(bool p_reversed) +{ + QModelIndex curIndex = popup()->currentIndex(); + if (p_reversed) { + if (curIndex.isValid()) { + int row = curIndex.row(); + if (row == 0) { + setCurrentIndex(QModelIndex()); + } else { + selectRow(row - 1); + } + } else { + selectRow(completionCount() - 1); + } + } else { + if (curIndex.isValid()) { + int row = curIndex.row(); + if (!selectRow(row + 1)) { + setCurrentIndex(QModelIndex()); + } + } else { + selectRow(0); + } + } +} + +bool VTextEditCompleter::selectRow(int p_row) +{ + if (setCurrentRow(p_row)) { + setCurrentIndex(currentIndex()); + return true; + } + + return false; +} + +void VTextEditCompleter::setCurrentIndex(QModelIndex p_index, bool p_select) +{ + auto pu = popup(); + if (!pu) { + return; + } + + if (!p_select) { + pu->selectionModel()->setCurrentIndex(p_index, QItemSelectionModel::NoUpdate); + } else { + if (!p_index.isValid()) { + pu->selectionModel()->clear(); + } else { + pu->selectionModel()->setCurrentIndex(p_index, QItemSelectionModel::ClearAndSelect); + } + } + + p_index = pu->selectionModel()->currentIndex(); + if (!p_index.isValid()) { + pu->scrollToTop(); + } else { + pu->scrollTo(p_index); + } +} + +bool VTextEditCompleter::eventFilter(QObject *p_obj, QEvent *p_eve) +{ + switch (p_eve->type()) { + case QEvent::KeyPress: + { + if (p_obj != popup() || !m_editor) { + break; + } + + bool exited = false; + + QKeyEvent *ke = static_cast(p_eve); + const int key = ke->key(); + const int modifiers = ke->modifiers(); + switch (key) { + case Qt::Key_N: + V_FALLTHROUGH; + case Qt::Key_P: + if (VUtils::isControlModifierForVim(modifiers)) { + selectNextCompletion(key == Qt::Key_P); + insertCurrentCompletion(); + return true; + } + break; + + case Qt::Key_J: + V_FALLTHROUGH; + case Qt::Key_K: + if (VUtils::isControlModifierForVim(modifiers)) { + selectNextCompletion(key == Qt::Key_K); + return true; + } + break; + + case Qt::Key_BracketLeft: + if (!VUtils::isControlModifierForVim(modifiers)) { + break; + } + V_FALLTHROUGH; + case Qt::Key_Escape: + exited = true; + // Propogate this event to the editor widget for Vim mode. + if (!m_editor->getVim()) { + finishCompletion(); + return true; + } + break; + + case Qt::Key_E: + if (VUtils::isControlModifierForVim(modifiers)) { + cancelCompletion(); + return true; + } + break; + + case Qt::Key_Enter: + case Qt::Key_Return: + { + if (m_insertedCompletion != currentCompletion()) { + insertCurrentCompletion(); + finishCompletion(); + return true; + } else { + exited = true; + } + break; + } + + default: + break; + } + + int cursorPos = -1; + if (!exited) { + cursorPos = m_editor->textCursorW().position(); + } + + bool ret = QCompleter::eventFilter(p_obj, p_eve); + if (!exited) { + // Detect if cursor position changed after key press. + int pos = m_editor->textCursorW().position(); + if (pos == cursorPos - 1) { + // Deleted one char. + if (m_insertedCompletion.size() > 1) { + updatePrefix(m_editor->fetchCompletionPrefix()); + } else { + exited = true; + } + } else if (pos == cursorPos + 1) { + // Added one char. + QString prefix = m_editor->fetchCompletionPrefix(); + if (prefix.size() == m_insertedCompletion.size() + 1 + && prefix.startsWith(m_insertedCompletion)) { + updatePrefix(prefix); + } else { + exited = true; + } + } else if (pos != cursorPos) { + exited = true; + } + } + + if (exited) { + // finishCompletion() will do clean up. Must be called after QCompleter::eventFilter(). + finishCompletion(); + } + + return ret; + } + + case QEvent::Hide: + { + // Completion exited. + cleanUp(); + break; + } + + default: + break; + } + + return QCompleter::eventFilter(p_obj, p_eve); +} + +void VTextEditCompleter::insertCurrentCompletion() +{ + QString completion; + QModelIndex curIndex = popup()->currentIndex(); + if (curIndex.isValid()) { + completion = currentCompletion(); + } else { + completion = completionPrefix(); + } + + insertCompletion(completion); +} + +void VTextEditCompleter::insertCompletion(const QString &p_completion) +{ + if (m_insertedCompletion == p_completion) { + return; + } + + m_editor->insertCompletion(m_insertedCompletion, p_completion); + m_insertedCompletion = p_completion; +} + +void VTextEditCompleter::cancelCompletion() +{ + insertCompletion(completionPrefix()); + + finishCompletion(); +} + +void VTextEditCompleter::cleanUp() +{ + // Do not clean up m_editor and m_insertedCompletion, since activated() + // signal is after the HideEvent. + setWidget(NULL); + g_mainWin->setCaptainModeEnabled(true); +} + +void VTextEditCompleter::updatePrefix(const QString &p_prefix) +{ + m_insertedCompletion = p_prefix; + setCompletionPrefix(p_prefix); + + int cnt = completionCount(); + if (cnt == 0) { + finishCompletion(); + } else if (cnt == 1) { + setCurrentRow(0); + if (currentCompletion() == p_prefix) { + finishCompletion(); + } + } +} diff --git a/src/vtexteditcompleter.h b/src/vtexteditcompleter.h new file mode 100644 index 00000000..2f815f6f --- /dev/null +++ b/src/vtexteditcompleter.h @@ -0,0 +1,68 @@ +#ifndef VTEXTEDITCOMPLETER_H +#define VTEXTEDITCOMPLETER_H + +#include +#include + +class QStringListModel; +class VEditor; + +class VTextEditCompleter : public QCompleter +{ + Q_OBJECT +public: + explicit VTextEditCompleter(QObject *p_parent = nullptr); + + bool isPopupVisible() const; + + void performCompletion(const QStringList &p_words, + const QString &p_prefix, + Qt::CaseSensitivity p_cs, + bool p_reversed, + const QRect &p_rect, + VEditor *p_editor); + +protected: + bool eventFilter(QObject *p_obj, QEvent *p_eve) Q_DECL_OVERRIDE; + +private: + void init(); + + bool selectRow(int p_row); + + void setCurrentIndex(QModelIndex p_index, bool p_select = true); + + void selectNextCompletion(bool p_reversed); + + void insertCurrentCompletion(); + + void insertCompletion(const QString &p_completion); + + void finishCompletion(); + + // Revert inserted completion to prefix and finish completion. + void cancelCompletion(); + + void cleanUp(); + + void updatePrefix(const QString &p_prefix); + + bool m_initialized; + + QStringListModel *m_model; + + VEditor *m_editor; + + QString m_insertedCompletion; +}; + +inline bool VTextEditCompleter::isPopupVisible() const +{ + return popup()->isVisible(); +} + +inline void VTextEditCompleter::finishCompletion() +{ + popup()->hide(); +} +#endif // VTEXTEDITCOMPLETER_H