Editor: refine find logics

This commit is contained in:
Le Tan 2018-09-10 20:26:39 +08:00
parent f3e4f370dd
commit 647807a918
14 changed files with 501 additions and 103 deletions

View File

@ -307,3 +307,8 @@ void VFindReplaceDialog::updateState(DocType p_docType, bool p_editMode)
m_replaceAvailable = p_editMode;
}
QString VFindReplaceDialog::textToFind() const
{
return m_findEdit->text();
}

View File

@ -14,11 +14,17 @@ class VFindReplaceDialog : public QWidget
Q_OBJECT
public:
explicit VFindReplaceDialog(QWidget *p_parent = 0);
uint options() const;
void setOption(FindOption p_opt, bool p_enabled);
// Update the options enabled/disabled state according to current
// edit tab.
void updateState(DocType p_docType, bool p_editMode);
QString textToFind() const;
signals:
void dialogClosed();
void findTextChanged(const QString &p_text, uint p_options);
@ -68,4 +74,8 @@ private:
QCheckBox *m_incrementalSearchCheck;
};
inline uint VFindReplaceDialog::options() const
{
return m_options;
}
#endif // VFINDREPLACEDIALOG_H

View File

@ -25,6 +25,8 @@ Open Flash Page.
Edit current note or save changes and exit edit mode.
- `Ctrl+G`
Activate Universal Entry.
- `Ctrl+8`/`Ctrl+9`
Jump to the next/previous match in last find action.
### Read Mode
- `H`/`J`/`K`/`L`

View File

@ -25,6 +25,8 @@
编辑当前笔记或保存更改并退出编辑模式。
- `Ctrl+G`
激活通用入口。
- `Ctrl+8`/`Ctrl+9`
跳转到最近一次查找的下一个/上一个匹配。
### 阅读模式
- `H`/`J`/`K`/`L`

View File

@ -391,6 +391,10 @@ Find=Ctrl+F
FindNext=F3
; Find previous occurence
FindPrevious=Shift+F3
; Jump to next match of last find
NextMatch=Ctrl+8
; Jump to previous match of last find
PreviousMatch=Ctrl+9
; Advanced find
AdvancedFind=Ctrl+Alt+F
; Recover last closed file

View File

@ -35,29 +35,7 @@ VEditArea::VEditArea(QWidget *parent)
registerCaptainTargets();
QString keySeq = g_config->getShortcutKeySequence("ActivateNextTab");
qDebug() << "set ActivateNextTab shortcut to" << keySeq;
QShortcut *activateNextTab = new QShortcut(QKeySequence(keySeq), this);
activateNextTab->setContext(Qt::ApplicationShortcut);
connect(activateNextTab, &QShortcut::activated,
this, [this]() {
VEditWindow *win = getCurrentWindow();
if (win) {
win->focusNextTab(true);
}
});
keySeq = g_config->getShortcutKeySequence("ActivatePreviousTab");
qDebug() << "set ActivatePreviousTab shortcut to" << keySeq;
QShortcut *activatePreviousTab = new QShortcut(QKeySequence(keySeq), this);
activatePreviousTab->setContext(Qt::ApplicationShortcut);
connect(activatePreviousTab, &QShortcut::activated,
this, [this]() {
VEditWindow *win = getCurrentWindow();
if (win) {
win->focusNextTab(false);
}
});
initShortcuts();
QTimer *timer = new QTimer(this);
timer->setSingleShot(false);
@ -130,6 +108,49 @@ void VEditArea::setupUI()
});
}
void VEditArea::initShortcuts()
{
QString keySeq = g_config->getShortcutKeySequence("ActivateNextTab");
qDebug() << "set ActivateNextTab shortcut to" << keySeq;
QShortcut *activateNextTab = new QShortcut(QKeySequence(keySeq), this);
activateNextTab->setContext(Qt::ApplicationShortcut);
connect(activateNextTab, &QShortcut::activated,
this, [this]() {
VEditWindow *win = getCurrentWindow();
if (win) {
win->focusNextTab(true);
}
});
keySeq = g_config->getShortcutKeySequence("ActivatePreviousTab");
qDebug() << "set ActivatePreviousTab shortcut to" << keySeq;
QShortcut *activatePreviousTab = new QShortcut(QKeySequence(keySeq), this);
activatePreviousTab->setContext(Qt::ApplicationShortcut);
connect(activatePreviousTab, &QShortcut::activated,
this, [this]() {
VEditWindow *win = getCurrentWindow();
if (win) {
win->focusNextTab(false);
}
});
keySeq = g_config->getShortcutKeySequence("NextMatch");
qDebug() << "set NextMatch shortcut to" << keySeq;
QShortcut *nextMatchSC = new QShortcut(QKeySequence(keySeq), this);
connect(nextMatchSC, &QShortcut::activated,
this, [this]() {
nextMatch(true);
});
keySeq = g_config->getShortcutKeySequence("PreviousMatch");
qDebug() << "set PreviousMatch shortcut to" << keySeq;
QShortcut *previousMatchSC = new QShortcut(QKeySequence(keySeq), this);
connect(previousMatchSC, &QShortcut::activated,
this, [this]() {
nextMatch(false);
});
}
void VEditArea::insertSplitWindow(int idx)
{
VEditWindow *win = new VEditWindow(this);
@ -1277,3 +1298,17 @@ void VEditArea::distributeSplits()
splitter->setSizes(sizes);
}
void VEditArea::nextMatch(bool p_forward)
{
VEditTab *tab = getCurrentTab();
if (!tab) {
return;
}
Q_ASSERT(m_findReplace);
tab->nextMatch(m_findReplace->textToFind(),
m_findReplace->options(),
p_forward);
}

View File

@ -179,9 +179,16 @@ private slots:
// Handle the timeout signal of file timer.
void handleFileTimerTimeout();
// Jump to next match of last find.
void nextMatch(bool p_forward);
private:
void setupUI();
void initShortcuts();
QVector<QPair<int, int> > findTabsByFile(const VFile *p_file);
int openFileInWindow(int windowIndex, VFile *p_file, OpenFileMode p_mode);
void setCurrentTab(int windowIndex, int tabIndex, bool setFocus);
void setCurrentWindow(int windowIndex, bool setFocus);

View File

@ -56,6 +56,8 @@ void VEditor::init()
const int labelSize = 64;
m_document = documentW();
QObject::connect(m_document, &QTextDocument::contentsChanged,
m_object, &VEditorObject::clearFindCache);
m_selectedWordFg = QColor(g_config->getEditorSelectedWordFg());
m_selectedWordBg = QColor(g_config->getEditorSelectedWordBg());
@ -344,7 +346,10 @@ static QTextDocument::FindFlags findOptionsToFlags(uint p_options, bool p_forwar
return findFlags;
}
QList<QTextCursor> VEditor::findTextAll(const QString &p_text, uint p_options)
QList<QTextCursor> VEditor::findTextAll(const QString &p_text,
uint p_options,
int p_start,
int p_end)
{
QList<QTextCursor> results;
if (p_text.isEmpty()) {
@ -355,35 +360,37 @@ QList<QTextCursor> VEditor::findTextAll(const QString &p_text, uint p_options)
bool caseSensitive = p_options & FindOption::CaseSensitive;
QTextDocument::FindFlags findFlags = findOptionsToFlags(p_options, true);
// Use regular expression
bool useRegExp = p_options & FindOption::RegularExpression;
QRegExp exp;
if (useRegExp) {
useRegExp = true;
exp = QRegExp(p_text,
if (p_options & FindOption::RegularExpression) {
QRegExp exp(p_text,
caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
}
int startPos = 0;
QTextCursor cursor;
while (true) {
if (useRegExp) {
cursor = m_document->find(exp, startPos, findFlags);
results = findTextAllInRange(m_document, exp, findFlags, p_start, p_end);
} else {
cursor = m_document->find(p_text, startPos, findFlags);
}
if (cursor.isNull()) {
break;
} else {
results.append(cursor);
startPos = cursor.selectionEnd();
}
results = findTextAllInRange(m_document, p_text, findFlags, p_start, p_end);
}
return results;
}
const QList<QTextCursor> &VEditor::findTextAllCached(const QString &p_text,
uint p_options,
int p_start,
int p_end)
{
if (p_text.isEmpty()) {
m_findInfo.clear();
return m_findInfo.m_result;
}
if (m_findInfo.isCached(p_text, p_options, p_start, p_end)) {
return m_findInfo.m_result;
}
QList<QTextCursor> result = findTextAll(p_text, p_options, p_start, p_end);
m_findInfo.update(p_text, p_options, p_start, p_end, result);
return m_findInfo.m_result;
}
void VEditor::highlightSelectedWord()
{
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SelectedWord];
@ -521,72 +528,146 @@ bool VEditor::peekText(const QString &p_text, uint p_options, bool p_forward)
return found;
}
// @p_cursors is in ascending order.
// If @p_forward is true, find the smallest cursor whose selection start is greater
// than @p_pos or the first cursor if wrapped.
// Otherwise, find the largest cursor whose selection start is smaller than @p_pos
// or the last cursor if wrapped.
static int selectCursor(const QList<QTextCursor> &p_cursors,
int p_pos,
bool p_forward,
bool &p_wrapped)
{
Q_ASSERT(!p_cursors.isEmpty());
p_wrapped = false;
int first = 0, last = p_cursors.size() - 1;
int lastMatch = -1;
while (first <= last) {
int mid = (first + last) / 2;
const QTextCursor &cur = p_cursors.at(mid);
if (p_forward) {
if (cur.selectionStart() < p_pos) {
first = mid + 1;
} else if (cur.selectionStart() == p_pos) {
// Next one is the right one.
if (mid < p_cursors.size() - 1) {
lastMatch = mid + 1;
} else {
lastMatch = 0;
p_wrapped = true;
}
break;
} else {
// It is a match.
if (lastMatch == -1 || mid < lastMatch) {
lastMatch = mid;
}
last = mid - 1;
}
} else {
if (cur.selectionStart() > p_pos) {
last = mid - 1;
} else if (cur.selectionStart() == p_pos) {
// Previous one is the right one.
if (mid > 0) {
lastMatch = mid - 1;
} else {
lastMatch = p_cursors.size() - 1;
p_wrapped = true;
}
break;
} else {
// It is a match.
if (lastMatch == -1 || mid > lastMatch) {
lastMatch = mid;
}
first = mid + 1;
}
}
}
if (lastMatch == -1) {
p_wrapped = true;
lastMatch = p_forward ? 0 : (p_cursors.size() - 1);
}
return lastMatch;
}
bool VEditor::findText(const QString &p_text,
uint p_options,
bool p_forward,
QTextCursor *p_cursor,
QTextCursor::MoveMode p_moveMode,
bool p_useLeftSideOfCursor)
{
return findTextInRange(p_text,
p_options,
p_forward,
p_cursor,
p_moveMode,
p_useLeftSideOfCursor);
}
bool VEditor::findTextInRange(const QString &p_text,
uint p_options,
bool p_forward,
QTextCursor *p_cursor,
QTextCursor::MoveMode p_moveMode,
bool p_useLeftSideOfCursor,
int p_start,
int p_end)
{
clearIncrementalSearchedWordHighlight();
if (p_text.isEmpty()) {
m_findInfo.clear();
clearSearchedWordHighlight();
return false;
}
QTextCursor cursor = textCursorW();
bool wrapped = false;
QTextCursor retCursor;
int matches = 0;
int start = p_cursor ? p_cursor->position() : cursor.position();
if (p_useLeftSideOfCursor) {
--start;
}
int skipPosition = start;
const QList<QTextCursor> &result = findTextAllCached(p_text, p_options, p_start, p_end);
bool found = false;
while (true) {
found = findTextHelper(p_text, p_options, p_forward, start, wrapped, retCursor);
if (found) {
Q_ASSERT(!retCursor.isNull());
if (result.isEmpty()) {
clearSearchedWordHighlight();
emit m_object->statusMessage(QObject::tr("No match found"));
} else {
// Locate to the right match and update current cursor.
QTextCursor cursor = textCursorW();
int pos = p_cursor ? p_cursor->position() : cursor.position();
if (p_useLeftSideOfCursor) {
--pos;
}
bool wrapped = false;
int idx = selectCursor(result, pos, p_forward, wrapped);
const QTextCursor &tcursor = result.at(idx);
if (wrapped) {
showWrapLabel();
}
if (p_forward && retCursor.selectionStart() == skipPosition) {
// Skip the first match.
skipPosition = -1;
start = retCursor.selectionEnd();
continue;
}
if (p_cursor) {
p_cursor->setPosition(retCursor.selectionStart(), p_moveMode);
p_cursor->setPosition(tcursor.selectionStart(), p_moveMode);
} else {
cursor.setPosition(retCursor.selectionStart(), p_moveMode);
cursor.setPosition(tcursor.selectionStart(), p_moveMode);
setTextCursorW(cursor);
}
highlightSearchedWord(p_text, p_options);
highlightSearchedWordUnderCursor(retCursor);
matches = m_extraSelections[(int)SelectionId::SearchedKeyword].size();
} else {
clearSearchedWordHighlight();
highlightSearchedWord(result);
highlightSearchedWordUnderCursor(tcursor);
emit m_object->statusMessage(QObject::tr("Match found: %2 of %3")
.arg(idx + 1)
.arg(result.size()));
}
break;
}
if (matches == 0) {
emit m_object->statusMessage(QObject::tr("Found no match"));
} else {
emit m_object->statusMessage(QObject::tr("Found %1 %2").arg(matches)
.arg(matches > 1 ? QObject::tr("matches")
: QObject::tr("match")));
}
return found;
return !result.isEmpty();
}
bool VEditor::findTextOne(const QString &p_text, uint p_options, bool p_forward)
@ -626,10 +707,14 @@ bool VEditor::findTextInRange(const QString &p_text,
int p_start,
int p_end)
{
Q_UNUSED(p_start);
Q_UNUSED(p_end);
// TODO
return findText(p_text, p_options, p_forward);
return findTextInRange(p_text,
p_options,
p_forward,
nullptr,
QTextCursor::MoveAnchor,
false,
p_start,
p_end);
}
void VEditor::highlightIncrementalSearchedWord(const QTextCursor &p_cursor)
@ -791,10 +876,10 @@ void VEditor::showWrapLabel()
m_labelTimer->start();
}
void VEditor::highlightSearchedWord(const QString &p_text, uint p_options)
void VEditor::highlightSearchedWord(const QList<QTextCursor> &p_matches)
{
QList<QTextEdit::ExtraSelection> &selects = m_extraSelections[(int)SelectionId::SearchedKeyword];
if (!g_config->getHighlightSearchedWord() || p_text.isEmpty()) {
if (!g_config->getHighlightSearchedWord() || p_matches.isEmpty()) {
if (!selects.isEmpty()) {
selects.clear();
highlightExtraSelections(true);
@ -803,10 +888,20 @@ void VEditor::highlightSearchedWord(const QString &p_text, uint p_options)
return;
}
selects.clear();
QTextCharFormat format;
format.setForeground(m_searchedWordFg);
format.setBackground(m_searchedWordBg);
highlightTextAll(p_text, p_options, SelectionId::SearchedKeyword, format);
for (int i = 0; i < p_matches.size(); ++i) {
QTextEdit::ExtraSelection select;
select.format = format;
select.cursor = p_matches[i];
selects.append(select);
}
highlightExtraSelections();
}
void VEditor::highlightSearchedWordUnderCursor(const QTextCursor &p_cursor)
@ -1297,3 +1392,83 @@ void VEditor::insertCompletion(const QString &p_prefix, const QString &p_complet
setTextCursorW(cursor);
}
QList<QTextCursor> VEditor::findTextAllInRange(const QTextDocument *p_doc,
const QString &p_text,
QTextDocument::FindFlags p_flags,
int p_start,
int p_end)
{
QList<QTextCursor> results;
if (p_text.isEmpty()) {
return results;
}
int start = p_start;
int end = p_end == -1 ? p_doc->characterCount() + 1 : p_end;
while (start < end) {
QTextCursor cursor = p_doc->find(p_text, start, p_flags);
if (cursor.isNull()) {
break;
} else {
start = cursor.selectionEnd();
if (start <= end) {
results.append(cursor);
}
}
}
return results;
}
QList<QTextCursor> VEditor::findTextAllInRange(const QTextDocument *p_doc,
const QRegExp &p_reg,
QTextDocument::FindFlags p_flags,
int p_start,
int p_end)
{
QList<QTextCursor> results;
if (!p_reg.isValid()) {
return results;
}
int start = p_start;
int end = p_end == -1 ? p_doc->characterCount() + 1 : p_end;
while (start < end) {
QTextCursor cursor = p_doc->find(p_reg, start, p_flags);
if (cursor.isNull()) {
break;
} else {
start = cursor.selectionEnd();
if (start <= end) {
results.append(cursor);
}
}
}
return results;
}
void VEditor::clearFindCache()
{
m_findInfo.clearResult();
}
void VEditor::nextMatch(bool p_forward)
{
if (m_findInfo.isNull()) {
return;
}
if (m_findInfo.m_useToken) {
// TODO
} else {
findTextInRange(m_findInfo.m_text,
m_findInfo.m_options,
p_forward,
m_findInfo.m_start,
m_findInfo.m_end);
}
}

View File

@ -13,6 +13,7 @@
#include "vfile.h"
#include "vwordcountinfo.h"
#include "vtexteditcompleter.h"
#include "vsearchconfig.h"
class QWidget;
class VEditorObject;
@ -99,6 +100,9 @@ public:
uint p_options,
const QString &p_replaceText);
// Use m_findInfo to find next match.
void nextMatch(bool p_forward = false);
// Scroll the content to make @p_block visible.
// If the @p_block is too long to hold in one page, just let it occupy the
// whole page.
@ -276,6 +280,100 @@ protected:
private:
friend class VEditorObject;
// Info about one find-in-page.
struct FindInfo
{
FindInfo()
: m_start(0),
m_end(-1),
m_useToken(false),
m_options(0),
m_cacheValid(false)
{
}
void clear()
{
m_start = 0;
m_end = -1;
m_useToken = false;
m_text.clear();
m_options = 0;
m_token.clear();
m_cacheValid = false;
m_result.clear();
}
void clearResult()
{
m_cacheValid = false;
m_result.clear();
}
bool isCached(const QString &p_text,
uint p_options,
int p_start = 0,
int p_end = -1) const
{
return m_cacheValid
&& !m_useToken
&& m_text == p_text
&& m_options == p_options
&& m_start == p_start
&& m_end == p_end;
}
void update(const QString &p_text,
uint p_options,
int p_start,
int p_end,
const QList<QTextCursor> &p_result)
{
m_start = p_start;
m_end = p_end;
m_useToken = false;
m_text = p_text;
m_options = p_options;
m_cacheValid = true;
m_result = p_result;
m_token.clear();
}
bool isNull() const
{
if (m_useToken) {
return m_token.tokenSize() == 0;
} else {
return m_text.isEmpty();
}
}
// Find in [m_start, m_end).
int m_start;
int m_end;
bool m_useToken;
// Use text and options to search.
QString m_text;
uint m_options;
// Use token to search.
VSearchToken m_token;
bool m_cacheValid;
QList<QTextCursor> m_result;
};
// Filter out the trailing space right before cursor.
void filterTrailingSpace(QList<QTextEdit::ExtraSelection> &p_selects,
const QList<QTextEdit::ExtraSelection> &p_src);
@ -293,7 +391,15 @@ private:
QList<QTextEdit::ExtraSelection> &) = NULL);
// Find all the occurences of @p_text.
QList<QTextCursor> findTextAll(const QString &p_text, uint p_options);
QList<QTextCursor> findTextAll(const QString &p_text,
uint p_options,
int p_start = 0,
int p_end = -1);
const QList<QTextCursor> &findTextAllCached(const QString &p_text,
uint p_options,
int p_start = 0,
int p_end = -1);
// Highlight @p_cursor as the incremental searched keyword.
void highlightIncrementalSearchedWord(const QTextCursor &p_cursor);
@ -311,7 +417,7 @@ private:
void showWrapLabel();
void highlightSearchedWord(const QString &p_text, uint p_options);
void highlightSearchedWord(const QList<QTextCursor> &p_matches);
// Highlight @p_cursor as the searched keyword under cursor.
void highlightSearchedWordUnderCursor(const QTextCursor &p_cursor);
@ -322,6 +428,28 @@ private:
bool findTextOne(const QString &p_text, uint p_options, bool p_forward);
// @p_end, -1 indicates the end of doc.
static QList<QTextCursor> findTextAllInRange(const QTextDocument *p_doc,
const QString &p_text,
QTextDocument::FindFlags p_flags,
int p_start = 0,
int p_end = -1);
static QList<QTextCursor> findTextAllInRange(const QTextDocument *p_doc,
const QRegExp &p_reg,
QTextDocument::FindFlags p_flags,
int p_start = 0,
int p_end = -1);
bool findTextInRange(const QString &p_text,
uint p_options,
bool p_forward,
QTextCursor *p_cursor = nullptr,
QTextCursor::MoveMode p_moveMode = QTextCursor::MoveAnchor,
bool p_useLeftSideOfCursor = false,
int p_start = 0,
int p_end = -1);
QLabel *m_wrapLabel;
QTimer *m_labelTimer;
@ -368,6 +496,8 @@ private:
// Temp files needed to be delete.
QStringList m_tempFiles;
FindInfo m_findInfo;
// Functions for private slots.
private:
void labelTimerTimeout();
@ -378,6 +508,8 @@ private:
void updateTrailingSpaceHighlights();
void doUpdateTrailingSpaceHighlights();
void clearFindCache();
};
@ -446,6 +578,11 @@ private slots:
m_editor->doUpdateTrailingSpaceHighlights();
}
void clearFindCache()
{
m_editor->clearFindCache();
}
private:
friend class VEditor;

View File

@ -66,6 +66,8 @@ public:
virtual void replaceTextAll(const QString &p_text, uint p_options,
const QString &p_replaceText) = 0;
virtual void nextMatch(const QString &p_text, uint p_options, bool p_forward) = 0;
// Return selected text.
virtual QString getSelectedText() const = 0;

View File

@ -240,6 +240,11 @@ void VHtmlTab::replaceTextAll(const QString &p_text, uint p_options,
}
}
void VHtmlTab::nextMatch(const QString &p_text, uint p_options, bool p_forward)
{
findText(p_text, p_options, false, p_forward);
}
QString VHtmlTab::getSelectedText() const
{
QTextCursor cursor = m_editor->textCursor();

View File

@ -41,6 +41,8 @@ public:
void replaceTextAll(const QString &p_text, uint p_options,
const QString &p_replaceText) Q_DECL_OVERRIDE;
void nextMatch(const QString &p_text, uint p_options, bool p_forward) Q_DECL_OVERRIDE;
QString getSelectedText() const Q_DECL_OVERRIDE;
void clearSearchedWordHighlight() Q_DECL_OVERRIDE;

View File

@ -719,6 +719,16 @@ void VMdTab::replaceTextAll(const QString &p_text, uint p_options,
}
}
void VMdTab::nextMatch(const QString &p_text, uint p_options, bool p_forward)
{
if (m_isEditMode) {
Q_ASSERT(m_editor);
m_editor->nextMatch(p_forward);
} else {
findTextInWebView(p_text, p_options, false, p_forward);
}
}
void VMdTab::findTextInWebView(const QString &p_text, uint p_options,
bool /* p_peek */, bool p_forward)
{

View File

@ -57,6 +57,8 @@ public:
void replaceTextAll(const QString &p_text, uint p_options,
const QString &p_replaceText) Q_DECL_OVERRIDE;
void nextMatch(const QString &p_text, uint p_options, bool p_forward) Q_DECL_OVERRIDE;
QString getSelectedText() const Q_DECL_OVERRIDE;
void clearSearchedWordHighlight() Q_DECL_OVERRIDE;