Editor: support completion

This commit is contained in:
Le Tan 2018-08-03 19:13:33 +08:00
parent 284cba698f
commit 10a1e9c1a8
24 changed files with 804 additions and 21 deletions

View File

@ -66,7 +66,18 @@ Zoom in/out the page through the mouse scroll.
Recover the page zoom factor to 100%. Recover the page zoom factor to 100%.
- `Ctrl+J/K` - `Ctrl+J/K`
Scroll page down/up without changing cursor. 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 #### Text Editing
- `Ctrl+B` - `Ctrl+B`

View File

@ -66,6 +66,18 @@
恢复页面大小为100%。 恢复页面大小为100%。
- `Ctrl+J/K` - `Ctrl+J/K`
向下/向上滚动页面,不会改变光标。 向下/向上滚动页面,不会改变光标。
- `Ctrl+N/P`
激活自动补全。
- `Ctrl+N/P`
浏览补全列表并插入当前补全。
- `Ctrl+J/K`
浏览补全列表。
- `Ctrl+E`
取消补全。
- `Enter`
插入补全。
- `Ctrl+[` or `Escape`
结束补全。
#### 文本编辑 #### 文本编辑
- `Ctrl+B` - `Ctrl+B`

View File

@ -9,7 +9,7 @@ mdhl_file=v_detorte.mdhl
css_file=v_detorte.css css_file=v_detorte.css
codeblock_css_file=v_detorte_codeblock.css codeblock_css_file=v_detorte_codeblock.css
mermaid_css_file=v_detorte_mermaid.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 ; 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. ; 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_fg=@inactive_fg
listview_item_selected_inactive_bg=@inactive_bg 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.
splitter_handle_bg=@border_bg splitter_handle_bg=@border_bg
splitter_handle_pressed_bg=@pressed_bg splitter_handle_pressed_bg=@pressed_bg

View File

@ -930,6 +930,46 @@ QListView::item:disabled {
} }
/* End QListView */ /* 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 */
QSplitter#MainSplitter { QSplitter#MainSplitter {
border: none; border: none;

View File

@ -7,7 +7,7 @@ mdhl_file=v_moonlight.mdhl
css_file=v_moonlight.css css_file=v_moonlight.css
codeblock_css_file=v_moonlight_codeblock.css codeblock_css_file=v_moonlight_codeblock.css
mermaid_css_file=v_moonlight_mermaid.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 ; 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. ; 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_fg=@inactive_fg
listview_item_selected_inactive_bg=@inactive_bg 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.
splitter_handle_bg=@border_bg splitter_handle_bg=@border_bg
splitter_handle_pressed_bg=@pressed_bg splitter_handle_pressed_bg=@pressed_bg

View File

@ -930,6 +930,46 @@ QListView::item:disabled {
} }
/* End QListView */ /* 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 */
QSplitter#MainSplitter { QSplitter#MainSplitter {
border: none; border: none;

View File

@ -7,7 +7,7 @@ mdhl_file=v_native.mdhl
css_file=v_native.css css_file=v_native.css
codeblock_css_file=v_native_codeblock.css codeblock_css_file=v_native_codeblock.css
mermaid_css_file=v_native_mermaid.css mermaid_css_file=v_native_mermaid.css
version=15 version=16
[phony] [phony]
; Abstract color attributes. ; Abstract color attributes.

View File

@ -510,6 +510,12 @@ QListView::item {
} }
/* End QListView */ /* End QListView */
/* QAbstractItemView for TextEdit Completer popup*/
QAbstractItemView[TextEdit="true"] {
border: 1px solid @border_bg;
}
/* End QAbstractItemView */
/* QSplitter */ /* QSplitter */
QSplitter { QSplitter {
border: none; border: none;

View File

@ -7,7 +7,7 @@ mdhl_file=v_pure.mdhl
css_file=v_pure.css css_file=v_pure.css
codeblock_css_file=v_pure_codeblock.css codeblock_css_file=v_pure_codeblock.css
mermaid_css_file=v_pure_mermaid.css mermaid_css_file=v_pure_mermaid.css
version=16 version=17
[phony] [phony]
; Abstract color attributes. ; 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_fg=@inactive_fg
listview_item_selected_inactive_bg=@inactive_bg 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.
splitter_handle_bg=@border_bg splitter_handle_bg=@border_bg
splitter_handle_pressed_bg=@pressed_bg splitter_handle_pressed_bg=@pressed_bg

View File

@ -930,6 +930,46 @@ QListView::item:disabled {
} }
/* End QListView */ /* 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 */
QSplitter#MainSplitter { QSplitter#MainSplitter {
border: none; border: none;

View File

@ -142,7 +142,8 @@ SOURCES += main.cpp\
vtagexplorer.cpp \ vtagexplorer.cpp \
pegmarkdownhighlighter.cpp \ pegmarkdownhighlighter.cpp \
pegparser.cpp \ pegparser.cpp \
peghighlighterresult.cpp peghighlighterresult.cpp \
vtexteditcompleter.cpp
HEADERS += vmainwindow.h \ HEADERS += vmainwindow.h \
vdirectorytree.h \ vdirectorytree.h \
@ -279,7 +280,8 @@ HEADERS += vmainwindow.h \
markdownhighlighterdata.h \ markdownhighlighterdata.h \
pegmarkdownhighlighter.h \ pegmarkdownhighlighter.h \
pegparser.h \ pegparser.h \
peghighlighterresult.h peghighlighterresult.h \
vtexteditcompleter.h
RESOURCES += \ RESOURCES += \
vnote.qrc \ vnote.qrc \

View File

@ -10,10 +10,13 @@
#include <QPair> #include <QPair>
#include <QSplitter> #include <QSplitter>
#include <QStack> #include <QStack>
#include <QSharedPointer>
#include "vnotebook.h" #include "vnotebook.h"
#include "veditwindow.h" #include "veditwindow.h"
#include "vnavigationmode.h" #include "vnavigationmode.h"
#include "vfilesessioninfo.h" #include "vfilesessioninfo.h"
#include "vtexteditcompleter.h"
class VFile; class VFile;
class VDirectory; class VDirectory;
@ -95,6 +98,8 @@ public:
// Distribute all the splits evenly. // Distribute all the splits evenly.
void distributeSplits(); void distributeSplits();
QSharedPointer<VTextEditCompleter> getCompleter() const;
signals: signals:
// Emit when current window's tab status updated. // Emit when current window's tab status updated.
void tabStatusUpdated(const VEditTabInfo &p_info); void tabStatusUpdated(const VEditTabInfo &p_info);
@ -244,6 +249,8 @@ private:
bool m_autoSave; bool m_autoSave;
VMathJaxPreviewHelper *m_mathPreviewHelper; VMathJaxPreviewHelper *m_mathPreviewHelper;
QSharedPointer<VTextEditCompleter> m_completer;
}; };
inline VEditWindow* VEditArea::getWindow(int windowIndex) const inline VEditWindow* VEditArea::getWindow(int windowIndex) const
@ -266,4 +273,14 @@ inline VMathJaxPreviewHelper *VEditArea::getMathJaxPreviewHelper() const
{ {
return m_mathPreviewHelper; return m_mathPreviewHelper;
} }
inline QSharedPointer<VTextEditCompleter> VEditArea::getCompleter() const
{
if (m_completer.isNull()) {
VEditArea *ea = const_cast<VEditArea *>(this);
ea->m_completer.reset(new VTextEditCompleter(ea));
}
return m_completer;
}
#endif // VEDITAREA_H #endif // VEDITAREA_H

View File

@ -15,7 +15,9 @@ extern VConfigManager *g_config;
extern VMetaWordManager *g_mwMgr; extern VMetaWordManager *g_mwMgr;
VEditor::VEditor(VFile *p_file, QWidget *p_editor) VEditor::VEditor(VFile *p_file,
QWidget *p_editor,
const QSharedPointer<VTextEditCompleter> &p_completer)
: m_editor(p_editor), : m_editor(p_editor),
m_object(new VEditorObject(this, p_editor)), m_object(new VEditorObject(this, p_editor)),
m_file(p_file), m_file(p_file),
@ -23,12 +25,16 @@ VEditor::VEditor(VFile *p_file, QWidget *p_editor)
m_document(nullptr), m_document(nullptr),
m_enableInputMethod(true), m_enableInputMethod(true),
m_timeStamp(0), m_timeStamp(0),
m_trailingSpaceSelectionTS(0) m_trailingSpaceSelectionTS(0),
m_completer(p_completer)
{ {
} }
VEditor::~VEditor() VEditor::~VEditor()
{ {
if (m_completer->widget() == m_editor) {
m_completer->setWidget(NULL);
}
} }
void VEditor::init() void VEditor::init()
@ -161,8 +167,19 @@ void VEditor::highlightOnCursorPositionChanged()
} else { } else {
// Judge whether we have trailing space at current line. // Judge whether we have trailing space at current line.
QString text = cursor.block().text(); QString text = cursor.block().text();
if (text.rbegin()->isSpace()) { if (!text.isEmpty()) {
updateTrailingSpaceHighlights(); 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. // Handle word-wrap in one block.
@ -228,14 +245,15 @@ void VEditor::filterTrailingSpace(QList<QTextEdit::ExtraSelection> &p_selects,
const QList<QTextEdit::ExtraSelection> &p_src) const QList<QTextEdit::ExtraSelection> &p_src)
{ {
QTextCursor cursor = textCursorW(); QTextCursor cursor = textCursorW();
if (!cursor.atBlockEnd()) { bool blockEnd = cursor.atBlockEnd();
p_selects.append(p_src); int blockNum = cursor.blockNumber();
return;
}
int cursorPos = cursor.position();
for (auto it = p_src.begin(); it != p_src.end(); ++it) { 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; continue;
} else { } else {
p_selects.append(*it); p_selects.append(*it);
@ -1100,3 +1118,91 @@ bool VEditor::setCursorPosition(int p_blockNumber, int p_posInBlock)
setTextCursorW(cursor); setTextCursorW(cursor);
return true; 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);
}

View File

@ -6,11 +6,13 @@
#include <QList> #include <QList>
#include <QTextEdit> #include <QTextEdit>
#include <QColor> #include <QColor>
#include <QSharedPointer>
#include "veditconfig.h" #include "veditconfig.h"
#include "vconstants.h" #include "vconstants.h"
#include "vfile.h" #include "vfile.h"
#include "vwordcountinfo.h" #include "vwordcountinfo.h"
#include "vtexteditcompleter.h"
class QWidget; class QWidget;
class VEditorObject; class VEditorObject;
@ -39,7 +41,9 @@ enum class SelectionId {
class VEditor class VEditor
{ {
public: public:
explicit VEditor(VFile *p_file, QWidget *p_editor); explicit VEditor(VFile *p_file,
QWidget *p_editor,
const QSharedPointer<VTextEditCompleter> &p_completer);
virtual ~VEditor(); virtual ~VEditor();
@ -154,6 +158,15 @@ public:
virtual bool setCursorPosition(int p_blockNumber, int p_posInBlock); 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. // Wrapper functions for QPlainTextEdit/QTextEdit.
// Ends with W to distinguish it from the original interfaces. // Ends with W to distinguish it from the original interfaces.
public: public:
@ -201,6 +214,10 @@ public:
virtual void ensureCursorVisibleW() = 0; virtual void ensureCursorVisibleW() = 0;
virtual QRect cursorRectW() = 0;
virtual QRect cursorRectW(const QTextCursor &p_cursor) = 0;
protected: protected:
void init(); void init();
@ -235,6 +252,8 @@ protected:
bool handleWheelEvent(QWheelEvent *p_event); bool handleWheelEvent(QWheelEvent *p_event);
virtual int lineNumberAreaWidth() const = 0;
QWidget *m_editor; QWidget *m_editor;
VEditorObject *m_object; VEditorObject *m_object;
@ -288,6 +307,8 @@ private:
// Highlight @p_cursor as the searched keyword under cursor. // Highlight @p_cursor as the searched keyword under cursor.
void highlightSearchedWordUnderCursor(const QTextCursor &p_cursor); void highlightSearchedWordUnderCursor(const QTextCursor &p_cursor);
QStringList generateCompletionCandidates() const;
QLabel *m_wrapLabel; QLabel *m_wrapLabel;
QTimer *m_labelTimer; QTimer *m_labelTimer;
@ -329,6 +350,8 @@ private:
TimeStamp m_trailingSpaceSelectionTS; TimeStamp m_trailingSpaceSelectionTS;
QSharedPointer<VTextEditCompleter> m_completer;
// Functions for private slots. // Functions for private slots.
private: private:
void labelTimerTimeout(); void labelTimerTimeout();

View File

@ -3343,3 +3343,8 @@ void VMainWindow::focusEditArea() const
widget->setFocus(); widget->setFocus();
} }
void VMainWindow::setCaptainModeEnabled(bool p_enabled)
{
m_captain->setCaptainModeEnabled(p_enabled);
}

View File

@ -128,6 +128,8 @@ public:
VExplorer *getExplorer() const; VExplorer *getExplorer() const;
void setCaptainModeEnabled(bool p_enabled);
signals: signals:
// Emit when editor related configurations were changed by user. // Emit when editor related configurations were changed by user.
void editorConfigUpdated(); void editorConfigUpdated();

View File

@ -431,6 +431,30 @@ bool VMdEditOperations::handleKeyPressEvent(QKeyEvent *p_event)
break; 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: default:
break; break;
} }

View File

@ -32,9 +32,10 @@ extern VConfigManager *g_config;
VMdEditor::VMdEditor(VFile *p_file, VMdEditor::VMdEditor(VFile *p_file,
VDocument *p_doc, VDocument *p_doc,
MarkdownConverterType p_type, MarkdownConverterType p_type,
const QSharedPointer<VTextEditCompleter> &p_completer,
QWidget *p_parent) QWidget *p_parent)
: VTextEdit(p_parent), : VTextEdit(p_parent),
VEditor(p_file, this), VEditor(p_file, this, p_completer),
m_pegHighlighter(NULL), m_pegHighlighter(NULL),
m_freshEdit(true), m_freshEdit(true),
m_textToHtmlDialog(NULL), 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::Text).name())
.arg(p_palette.color(QPalette::Base).name())); .arg(p_palette.color(QPalette::Base).name()));
} }
int VMdEditor::lineNumberAreaWidth() const
{
return VTextEdit::lineNumberAreaWidth();
}

View File

@ -28,6 +28,7 @@ public:
VMdEditor(VFile *p_file, VMdEditor(VFile *p_file,
VDocument *p_doc, VDocument *p_doc,
MarkdownConverterType p_type, MarkdownConverterType p_type,
const QSharedPointer<VTextEditCompleter> &p_completer,
QWidget *p_parent = nullptr); QWidget *p_parent = nullptr);
void beginEdit() Q_DECL_OVERRIDE; void beginEdit() Q_DECL_OVERRIDE;
@ -189,6 +190,16 @@ public:
ensureCursorVisible(); ensureCursorVisible();
} }
QRect cursorRectW() Q_DECL_OVERRIDE
{
return cursorRect();
}
QRect cursorRectW(const QTextCursor &p_cursor) Q_DECL_OVERRIDE
{
return cursorRect(p_cursor);
}
signals: signals:
// Signal when headers change. // Signal when headers change.
void headersChanged(const QVector<VTableOfContentItem> &p_headers); void headersChanged(const QVector<VTableOfContentItem> &p_headers);
@ -223,6 +234,8 @@ protected:
void wheelEvent(QWheelEvent *p_event) Q_DECL_OVERRIDE; void wheelEvent(QWheelEvent *p_event) Q_DECL_OVERRIDE;
int lineNumberAreaWidth() const Q_DECL_OVERRIDE;
private slots: private slots:
// Update m_headers according to elements. // Update m_headers according to elements.
void updateHeaders(const QVector<VElementRegion> &p_headerRegions); void updateHeaders(const QVector<VElementRegion> &p_headerRegions);

View File

@ -473,7 +473,11 @@ void VMdTab::setupMarkdownEditor()
{ {
Q_ASSERT(!m_editor); 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->setProperty("MainEditor", true);
m_editor->setEditTab(this); m_editor->setEditTab(this);
int delta = g_config->getEditorZoomDelta(); int delta = g_config->getEditorZoomDelta();

View File

@ -459,3 +459,8 @@ void VTextEdit::dragMoveEvent(QDragMoveEvent *p_event)
// TODO: find out the rect of current cursor to update that rect only. // TODO: find out the rect of current cursor to update that rect only.
update(); update();
} }
int VTextEdit::lineNumberAreaWidth() const
{
return m_lineNumberArea->width();
}

View File

@ -81,6 +81,8 @@ protected:
// Return the Y offset of the content via the scrollbar. // Return the Y offset of the content via the scrollbar.
int contentOffsetY() const; int contentOffsetY() const;
int lineNumberAreaWidth() const;
void updateLineNumberAreaWidth(const QFontMetrics &p_metrics); void updateLineNumberAreaWidth(const QFontMetrics &p_metrics);
void dragMoveEvent(QDragMoveEvent *p_event) Q_DECL_OVERRIDE; void dragMoveEvent(QDragMoveEvent *p_event) Q_DECL_OVERRIDE;

321
src/vtexteditcompleter.cpp Normal file
View File

@ -0,0 +1,321 @@
#include "vtexteditcompleter.h"
#include <QStringListModel>
#include <QStyledItemDelegate>
#include <QScrollBar>
#include <QDebug>
#include <QEvent>
#include <QKeyEvent>
#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<void(QCompleter::*)(const QString &)>(&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<QKeyEvent *>(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();
}
}
}

68
src/vtexteditcompleter.h Normal file
View File

@ -0,0 +1,68 @@
#ifndef VTEXTEDITCOMPLETER_H
#define VTEXTEDITCOMPLETER_H
#include <QCompleter>
#include <QAbstractItemView>
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