Browse Source

Search: highlight matched items in opened files

Le Tan 4 years ago
parent
commit
ed8cd503b5

+ 1 - 1
libs/vtextedit

@@ -1 +1 @@
-Subproject commit 392c0e218f5981b4fc6566512103b6149f714931
+Subproject commit 59e53f2cdc2f5a4b9dc44cbf28c483cca07559ba

+ 1 - 1
src/core/configmgr.cpp

@@ -24,7 +24,7 @@
 using namespace vnotex;
 
 #ifndef QT_NO_DEBUG
-    // #define VX_DEBUG_WEB
+    #define VX_DEBUG_WEB
 #endif
 
 const QString ConfigMgr::c_orgName = "VNote";

+ 6 - 0
src/core/fileopenparameters.h

@@ -1,11 +1,14 @@
 #ifndef FILEOPENPARAMETERS_H
 #define FILEOPENPARAMETERS_H
 
+#include <QSharedPointer>
+
 #include "global.h"
 
 namespace vnotex
 {
     class Node;
+    class SearchToken;
 
     struct FileOpenParameters
     {
@@ -32,6 +35,9 @@ namespace vnotex
 
         // Whether always open a new window for file.
         bool m_alwaysNewWindow = false;
+
+        // If not empty, use this token to do a search text highlight.
+        QSharedPointer<SearchToken> m_searchToken;
     };
 }
 

+ 2 - 2
src/data/extra/web/js/markdownviewer.js

@@ -39,8 +39,8 @@ new QWebChannel(qt.webChannelTransport,
             window.vnotex.crossCopy(p_id, p_timeStamp, p_target, p_baseUrl, p_html);
         });
 
-        adapter.findTextRequested.connect(function(p_text, p_options) {
-            window.vnotex.findText(p_text, p_options);
+        adapter.findTextRequested.connect(function(p_texts, p_options, p_currentMatchLine) {
+            window.vnotex.findText(p_texts, p_options, p_currentMatchLine);
         });
 
         adapter.contentRequested.connect(function() {

+ 124 - 23
src/data/extra/web/js/markjs.js

@@ -7,6 +7,7 @@ class MarkJs {
         this.markjs = null;
         this.cache = null;
         this.matchedNodes = null;
+        this.currentMatchedNodes = null;
 
         this.adapter.on('basicMarkdownRendered', () => {
             this.clearCache();
@@ -19,59 +20,102 @@ class MarkJs {
     //     wholeWordOnly,
     //     regularExpression
     // }
-    findText(p_text, p_options) {
+    findText(p_texts, p_options, p_currentMatchLine) {
         if (!this.markjs) {
             this.markjs = new Mark(this.container);
         }
 
-        if (!p_text) {
+        if (!p_texts || p_texts.length == 0) {
             // Clear the cache and highlight.
             this.clearCache();
             return;
         }
 
-        if (this.findInCache(p_text, p_options)) {
+        if (this.findInCache(p_texts, p_options, p_currentMatchLine)) {
             return;
         }
 
         // A new find.
         this.clearCache();
 
-        let callbackFunc = function(markjs, text, options) {
+        let callbackFunc = function(markjs, texts, options, currentMatchLine) {
             let _markjs = markjs;
-            let _text = text;
+            let _texts = texts;
             let _options = options;
-            return function(totalMatches) {
-                if (!_markjs.matchedNodes) {
+            let _currentMatchLine = currentMatchLine;
+            return function() {
+                if (_markjs.matchedNodes === null) {
                     _markjs.matchedNodes = _markjs.container.getElementsByClassName(_markjs.className);
+                    _markjs.currentMatchedNodes = _markjs.container.getElementsByClassName(_markjs.currentMatchClassName);
                 }
 
                 // Update cache.
                 _markjs.cache = {
-                    text: _text,
+                    texts: _texts,
                     options: _options,
                     currentIdx: -1
                 }
 
-                _markjs.updateCurrentMatch(_text, !_options.findBackward);
+                _markjs.updateCurrentMatch(_texts, !_options.findBackward, _currentMatchLine);
             };
         }
+
+        if (p_options.regularExpression) {
+            this.findByOneRegExp({
+                'texts': p_texts,
+                'options': p_options,
+                'textIdx': 0,
+                'lastCallback': callbackFunc(this, p_texts, p_options, p_currentMatchLine)
+            });
+        } else {
+            let opt = this.createMarkjsOptions(p_options);
+            opt.done = callbackFunc(this, p_texts, p_options, p_currentMatchLine);
+            this.markjs.mark(p_texts, opt);
+        }
+    }
+
+    createMarkjsOptions(p_options) {
         let opt = {
             'element': 'span',
             'className': this.className,
             'caseSensitive': p_options.caseSensitive,
             'accuracy': p_options.wholeWordOnly ? 'exactly' : 'partially',
-            'done': callbackFunc(this, p_text, p_options),
             // Ignore SVG, or SVG will be corrupted.
             'exclude': ['svg *']
         }
+        return opt;
+    }
 
-        if (p_options.regularExpression) {
-            // TODO: may need transformation from QRegularExpression to RegExp.
-            this.markjs.markRegExp(new RegExp(p_text), opt);
+    // @p_paras: {
+    //     texts,
+    //     options,
+    //     textIdx,
+    //     lastCallback
+    // }
+    findByOneRegExp(p_paras) {
+        console.log('findByOneRegExp', p_paras.texts.length, p_paras.textIdx);
+
+        if (p_paras.textIdx >= p_paras.texts.length) {
+            return;
+        }
+
+        let opt = this.createMarkjsOptions(p_paras.options);
+        if (p_paras.textIdx == p_paras.texts.length - 1) {
+            opt.done = p_paras.lastCallback;
         } else {
-            this.markjs.mark(p_text, opt);
+            let callbackFunc = function(markjs, paras) {
+                let _markjs = markjs;
+                let _paras = paras;
+                return function() {
+                    _paras.textIdx += 1;
+                    _markjs.findByOneRegExp(_paras);
+                };
+            };
+            opt.done = callbackFunc(this, p_paras);
         }
+
+        // TODO: may need transformation from QRegularExpression to RegExp.
+        this.markjs.markRegExp(new RegExp(p_paras.texts[p_paras.textIdx]), opt);
     }
 
     clearCache() {
@@ -83,33 +127,62 @@ class MarkJs {
         this.markjs.unmark();
     }
 
-    findInCache(p_text, p_options) {
+    findInCache(p_texts, p_options, p_currentMatchLine) {
         if (!this.cache) {
             return false;
         }
 
-        if (this.cache.text === p_text
-            && this.cache.options.caseSensitive == p_options.caseSensitive
+        if (p_texts.length != this.cache.texts.length) {
+            return false;
+        }
+
+        for (let i = 0; i < p_texts.length; ++i) {
+            if (!(p_texts[i] === this.cache.texts[i])) {
+                return false;
+            }
+        }
+
+        if (this.cache.options.caseSensitive == p_options.caseSensitive
             && this.cache.options.wholeWordOnly == p_options.wholeWordOnly
             && this.cache.options.regularExpression == p_options.regularExpression) {
             // Matched. Move current match forward or backward.
-            this.updateCurrentMatch(p_text, !p_options.findBackward);
+            this.updateCurrentMatch(p_texts, !p_options.findBackward, p_currentMatchLine);
             return true;
         }
 
         return false;
     }
 
-    updateCurrentMatch(p_text, p_forward) {
+    updateCurrentMatch(p_texts, p_forward, p_currentMatchLine) {
         let matches = this.matchedNodes.length;
         if (matches == 0) {
-            this.adapter.showFindResult(p_text, 0, 0);
+            this.adapter.showFindResult(p_texts, 0, 0);
             return;
         }
-        if (this.cache.currentIdx >= 0) {
+
+        if (this.currentMatchedNodes.length > 0) {
+            console.assert(this.currentMatchedNodes.length == 1);
+            if (this.cache.currentIdx >= matches
+                || this.cache.currentIdx < 0
+                || this.matchedNodes[this.cache.currentIdx] != this.currentMatchedNodes[0]) {
+                // Need to update current index.
+                // The mismatch may comes from the rendering of graphs which may change the matches.
+                for (let i = 0; i < matches; ++i) {
+                    if (this.matchedNodes[i] === this.currentMatchedNodes[0]) {
+                        this.cache.currentIdx = i;
+                        break;
+                    }
+                }
+            }
+
             this.matchedNodes[this.cache.currentIdx].classList.remove(this.currentMatchClassName);
+        } else {
+            this.cache.currentIdx = -1;
         }
-        if (p_forward) {
+
+        if (p_currentMatchLine > -1) {
+            this.cache.currentIdx = this.binarySearchCurrentIndexForLineNumber(p_currentMatchLine);
+        } else if (p_forward) {
             this.cache.currentIdx += 1;
             if (this.cache.currentIdx >= matches) {
                 this.cache.currentIdx = 0;
@@ -120,11 +193,39 @@ class MarkJs {
                 this.cache.currentIdx = matches - 1;
             }
         }
+
         let node = this.matchedNodes[this.cache.currentIdx];
         node.classList.add(this.currentMatchClassName);
         if (!Utils.isVisible(node)) {
             node.scrollIntoView();
         }
-        this.adapter.showFindResult(p_text, matches, this.cache.currentIdx);
+        this.adapter.showFindResult(p_texts, matches, this.cache.currentIdx);
+    }
+
+    binarySearchCurrentIndexForLineNumber(p_lineNumber) {
+        let viewY = this.adapter.nodeLineMapper.getViewYOfLine(p_lineNumber);
+        if (viewY === null) {
+            return 0;
+        }
+
+        let left = 0;
+        let right = this.matchedNodes.length - 1;
+        let lastIdx = -1;
+        while (left <= right) {
+            let mid = Math.floor((left + right) / 2);
+            let y = this.matchedNodes[mid].getBoundingClientRect().top;
+            if (y >= viewY) {
+                lastIdx = mid;
+                right = mid - 1;
+            } else {
+                left = mid + 1;
+            }
+        }
+
+        if (lastIdx != -1) {
+            return lastIdx;
+        } else {
+            return 0;
+        }
     }
 }

+ 16 - 0
src/data/extra/web/js/nodelinemapper.js

@@ -65,6 +65,22 @@ class NodeLineMapper {
         this.adapter.setHeadings(headings);
     }
 
+    getViewYOfLine(p_lineNumber) {
+        if (p_lineNumber == 0) {
+            return null;
+        }
+
+        this.fetchAllNodesWithLineNumber();
+
+        // Binary search the last node with line number not larger than @p_lineNumber.
+        let targetNode = this.binarySearchNodeForLineNumber(this.nodesWithSourceLine, p_lineNumber);
+        if (targetNode) {
+            return targetNode.getBoundingClientRect().top;
+        } else {
+            return null;
+        }
+    }
+
     scrollToLine(p_lineNumber) {
         if (p_lineNumber == 0) {
             this.scrollToY(0, false, true);

+ 4 - 4
src/data/extra/web/js/vnotex.js

@@ -280,12 +280,12 @@ class VNoteX extends EventEmitter {
         window.vxMarkdownAdapter.setCrossCopyResult(p_id, p_timeStamp, p_html);
     }
 
-    findText(p_text, p_options) {
-        this.searcher.findText(p_text, p_options);
+    findText(p_texts, p_options, p_currentMatchLine) {
+        this.searcher.findText(p_texts, p_options, p_currentMatchLine);
     }
 
-    showFindResult(p_text, p_totalMatches, p_currentMatchIndex) {
-        window.vxMarkdownAdapter.setFindText(p_text, p_totalMatches, p_currentMatchIndex);
+    showFindResult(p_texts, p_totalMatches, p_currentMatchIndex) {
+        window.vxMarkdownAdapter.setFindText(p_texts, p_totalMatches, p_currentMatchIndex);
     }
 
     saveContent() {

+ 5 - 0
src/search/searcher.cpp

@@ -513,3 +513,8 @@ void Searcher::createSearchEngine()
 
     m_engine.reset(new FileSearchEngine());
 }
+
+const SearchToken &Searcher::getToken() const
+{
+    return m_token;
+}

+ 2 - 0
src/search/searcher.h

@@ -34,6 +34,8 @@ namespace vnotex
 
         SearchState search(const QSharedPointer<SearchOption> &p_option, const QVector<Notebook *> &p_notebooks);
 
+        const SearchToken &getToken() const;
+
     signals:
         void progressUpdated(int p_val, int p_maximum);
 

+ 18 - 0
src/search/searchtoken.cpp

@@ -271,3 +271,21 @@ QString SearchToken::getHelpText()
     // Skip the first line containing the application name.
     return text.mid(text.indexOf('\n') + 1);
 }
+
+QPair<QStringList, FindOptions> SearchToken::toPatterns() const
+{
+    QPair<QStringList, FindOptions> ret;
+
+    ret.second = m_caseSensitivity == Qt::CaseSensitive ? FindOption::CaseSensitive : FindOption::FindNone;
+    if (m_type == Type::RegularExpression) {
+        ret.second |= FindOption::RegularExpression;
+
+        for (const auto &reg : m_regularExpressions) {
+            ret.first << reg.pattern();
+        }
+    } else {
+        ret.first = m_keywords;
+    }
+
+    return ret;
+}

+ 2 - 0
src/search/searchtoken.h

@@ -54,6 +54,8 @@ namespace vnotex
 
         void endBatchMode();
 
+        QPair<QStringList, FindOptions> toPatterns() const;
+
         // Compile tokens from keyword.
         // Support some magic switchs in the keyword which will suppress the given options.
         static bool compile(const QString &p_keyword, FindOptions p_options, SearchToken &p_token);

+ 16 - 2
src/utils/widgetutils.cpp

@@ -23,6 +23,7 @@
 #include <QDebug>
 #include <QLineEdit>
 #include <QLayout>
+#include <QPushButton>
 
 #include <core/global.h>
 
@@ -198,8 +199,7 @@ void WidgetUtils::addActionShortcut(QAction *p_action,
     p_action->setText(QString("%1\t%2").arg(p_action->text(), kseq.toString(QKeySequence::NativeText)));
 }
 
-void WidgetUtils::addActionShortcutText(QAction *p_action,
-                                        const QString &p_shortcut)
+void WidgetUtils::addActionShortcutText(QAction *p_action, const QString &p_shortcut)
 {
     if (p_shortcut.isEmpty()) {
         return;
@@ -213,6 +213,20 @@ void WidgetUtils::addActionShortcutText(QAction *p_action,
     p_action->setText(QString("%1\t%2").arg(p_action->text(), kseq.toString(QKeySequence::NativeText)));
 }
 
+void WidgetUtils::addButtonShortcutText(QPushButton *p_button, const QString &p_shortcut)
+{
+    if (p_shortcut.isEmpty()) {
+        return;
+    }
+
+    QKeySequence kseq(p_shortcut);
+    if (kseq.isEmpty()) {
+        return;
+    }
+
+    p_button->setText(QString("%1 (%2)").arg(p_button->text(), kseq.toString(QKeySequence::NativeText)));
+}
+
 void WidgetUtils::updateSize(QWidget *p_widget)
 {
     p_widget->adjustSize();

+ 4 - 2
src/utils/widgetutils.h

@@ -19,6 +19,7 @@ class QMenu;
 class QShortcut;
 class QLineEdit;
 class QLayout;
+class QPushButton;
 
 namespace vnotex
 {
@@ -60,8 +61,9 @@ namespace vnotex
                                       Qt::ShortcutContext p_context = Qt::WindowShortcut);
 
         // Just add a shortcut text hint to the action.
-        static void addActionShortcutText(QAction *p_action,
-                                          const QString &p_shortcut);
+        static void addActionShortcutText(QAction *p_action, const QString &p_shortcut);
+
+        static void addButtonShortcutText(QPushButton *p_button, const QString &p_shortcut);
 
         static QShortcut *createShortcut(const QString &p_shortcut,
                                          QWidget *p_widget,

+ 8 - 7
src/widgets/dialogs/exportdialog.cpp

@@ -781,12 +781,13 @@ QWidget *ExportDialog::getCustomAdvancedSettings()
         }
 
         {
-            auto usage = tr("%1: List of input files.\n"
-                            "%2: List of paths to search for images and other resources.\n"
-                            "%3: Path of rendering CSS style sheet.\n"
-                            "%4: Path of syntax highlighting CSS style sheet.\n"
-                            "%5: Path of output file.\n");
-            layout->addRow(tr("Command usage:"), new QLabel(usage, widget));
+            auto usage = tr("Command:\n"
+                            "\t%1: List of input files.\n"
+                            "\t%2: List of paths to search for images and other resources.\n"
+                            "\t%3: Path of rendering CSS style sheet.\n"
+                            "\t%4: Path of syntax highlighting CSS style sheet.\n"
+                            "\t%5: Path of output file.\n");
+            layout->addRow(new QLabel(usage, widget));
         }
 
         {
@@ -798,7 +799,7 @@ QWidget *ExportDialog::getCustomAdvancedSettings()
 #endif
             m_commandTextEdit->setMaximumHeight(m_commandTextEdit->minimumSizeHint().height());
             m_commandTextEdit->setEnabled(false);
-            layout->addRow(tr("Command:"), m_commandTextEdit);
+            layout->addRow(m_commandTextEdit);
         }
 
         connect(m_customExportComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),

+ 27 - 26
src/widgets/editors/markdownvieweradapter.cpp

@@ -6,17 +6,10 @@
 #include "../outlineprovider.h"
 #include "plantumlhelper.h"
 #include "graphvizhelper.h"
+#include <utils/utils.h>
 
 using namespace vnotex;
 
-MarkdownViewerAdapter::MarkdownData::MarkdownData(const QString &p_text,
-                                                  int p_lineNumber,
-                                                  const QString &p_anchor)
-    : m_text(p_text),
-      m_position(p_lineNumber, p_anchor)
-{
-}
-
 MarkdownViewerAdapter::Position::Position(int p_lineNumber, const QString &p_anchor)
     : m_lineNumber(p_lineNumber),
       m_anchor(p_anchor)
@@ -92,7 +85,10 @@ void MarkdownViewerAdapter::setText(int p_revision,
         emit textUpdated(p_text);
         scrollToPosition(Position(p_lineNumber, ""));
     } else {
-        m_pendingData.reset(new MarkdownData(p_text, p_lineNumber, ""));
+        m_pendingActions.append([this, p_text, p_lineNumber]() {
+            emit textUpdated(p_text);
+            scrollToPosition(Position(p_lineNumber, ""));
+        });
     }
 }
 
@@ -102,7 +98,9 @@ void MarkdownViewerAdapter::setText(const QString &p_text)
     if (m_viewerReady) {
         emit textUpdated(p_text);
     } else {
-        m_pendingData.reset(new MarkdownData(p_text, -1, ""));
+        m_pendingActions.append([this, p_text]() {
+            emit textUpdated(p_text);
+        });
     }
 }
 
@@ -114,15 +112,12 @@ void MarkdownViewerAdapter::setReady(bool p_ready)
 
     m_viewerReady = p_ready;
     if (m_viewerReady) {
-        if (m_pendingData) {
-            emit textUpdated(m_pendingData->m_text);
-            scrollToPosition(m_pendingData->m_position);
-            m_pendingData.reset();
+        for (auto &act : m_pendingActions) {
+            act();
         }
-
+        m_pendingActions.clear();
         emit viewerReady();
     }
-
 }
 
 void MarkdownViewerAdapter::scrollToLine(int p_lineNumber)
@@ -132,11 +127,9 @@ void MarkdownViewerAdapter::scrollToLine(int p_lineNumber)
     }
 
     if (!m_viewerReady) {
-        if (m_pendingData) {
-            m_pendingData->m_position = Position(p_lineNumber, QString());
-        } else {
-            qWarning() << "Markdown viewer is not ready";
-        }
+        m_pendingActions.append([this, p_lineNumber]() {
+            scrollToPosition(Position(p_lineNumber, ""));
+        });
         return;
     }
 
@@ -321,7 +314,7 @@ void MarkdownViewerAdapter::setCrossCopyResult(quint64 p_id, quint64 p_timeStamp
     emit crossCopyReady(p_id, p_timeStamp, p_html);
 }
 
-void MarkdownViewerAdapter::findText(const QString &p_text, FindOptions p_options)
+void MarkdownViewerAdapter::findText(const QStringList &p_texts, FindOptions p_options, int p_currentMatchLine)
 {
     FindOption opts;
     if (p_options & vnotex::FindOption::FindBackward) {
@@ -337,12 +330,20 @@ void MarkdownViewerAdapter::findText(const QString &p_text, FindOptions p_option
         opts.m_regularExpression = true;
     }
 
-    emit findTextRequested(p_text, opts.toJson());
+    if (m_viewerReady) {
+        emit findTextRequested(p_texts, opts.toJson(), p_currentMatchLine);
+    } else {
+        m_pendingActions.append([this, p_texts, opts, p_currentMatchLine]() {
+            // FIXME: highlights will be clear once the page is ready. Add a delay here.
+            Utils::sleepWait(1000);
+            emit findTextRequested(p_texts, opts.toJson(), p_currentMatchLine);
+        });
+    }
 }
 
-void MarkdownViewerAdapter::setFindText(const QString &p_text, int p_totalMatches, int p_currentMatchIndex)
+void MarkdownViewerAdapter::setFindText(const QStringList &p_texts, int p_totalMatches, int p_currentMatchIndex)
 {
-    emit findTextReady(p_text, p_totalMatches, p_currentMatchIndex);
+    emit findTextReady(p_texts, p_totalMatches, p_currentMatchIndex);
 }
 
 void MarkdownViewerAdapter::setWorkFinished()
@@ -367,7 +368,7 @@ void MarkdownViewerAdapter::reset()
 {
     m_revision = 0;
     m_viewerReady = false;
-    m_pendingData.reset();
+    m_pendingActions.clear();
     m_topLineNumber = -1;
     m_headings.clear();
     m_currentHeadingIndex = -1;

+ 6 - 19
src/widgets/editors/markdownvieweradapter.h

@@ -29,19 +29,6 @@ namespace vnotex
             QString m_anchor;
         };
 
-        struct MarkdownData
-        {
-            MarkdownData() = default;
-
-            MarkdownData(const QString &p_text,
-                         int p_lineNumber,
-                         const QString &p_anchor);
-
-            QString m_text;
-
-            Position m_position;
-        };
-
         struct PreviewData
         {
             PreviewData() = default;
@@ -119,7 +106,7 @@ namespace vnotex
 
         QString getCrossCopyTargetDisplayName(const QString &p_target) const;
 
-        void findText(const QString &p_text, FindOptions p_options);
+        void findText(const QStringList &p_texts, FindOptions p_options, int p_currentMatchLine = -1);
 
         void saveContent();
 
@@ -168,7 +155,7 @@ namespace vnotex
 
         void setCrossCopyResult(quint64 p_id, quint64 p_timeStamp, const QString &p_html);
 
-        void setFindText(const QString &p_text, int p_totalMatches, int p_currentMatchIndex);
+        void setFindText(const QStringList &p_texts, int p_totalMatches, int p_currentMatchIndex);
 
         void setSavedContent(const QString &p_headContent, const QString &p_styleContent, const QString &p_content, const QString &p_bodyClassList);
 
@@ -210,7 +197,7 @@ namespace vnotex
                                 const QString &p_baseUrl,
                                 const QString &p_html);
 
-        void findTextRequested(const QString &p_text, const QJsonObject &p_options);
+        void findTextRequested(const QStringList &p_texts, const QJsonObject &p_options, int p_currentMatchLine);
 
         // Request to get the whole HTML content.
         void contentRequested();
@@ -243,7 +230,7 @@ namespace vnotex
 
         void crossCopyReady(quint64 p_id, quint64 p_timeStamp, const QString &p_html);
 
-        void findTextReady(const QString &p_text, int p_totalMatches, int p_currentMatchIndex);
+        void findTextReady(const QStringList &p_texts, int p_totalMatches, int p_currentMatchIndex);
 
         void contentReady(const QString &p_headContent,
                           const QString &p_styleContent,
@@ -260,8 +247,8 @@ namespace vnotex
         // Whether web side viewer is ready to handle text update.
         bool m_viewerReady = false;
 
-        // Pending Markdown data for the viewer once it is ready.
-        QScopedPointer<MarkdownData> m_pendingData;
+        // Pending actions for the viewer once it is ready.
+        QVector<std::function<void()>> m_pendingActions;
 
         // Source line number of the top element node at web side.
         int m_topLineNumber = -1;

+ 8 - 2
src/widgets/findandreplacewidget.cpp

@@ -14,11 +14,13 @@
 #include "lineedit.h"
 #include "widgetsfactory.h"
 #include <utils/iconutils.h>
+#include <utils/widgetutils.h>
 #include <core/thememgr.h>
 #include <core/vnotex.h>
 #include "propertydefs.h"
-#include "configmgr.h"
-#include "widgetconfig.h"
+#include <core/configmgr.h>
+#include <core/editorconfig.h>
+#include <widgetconfig.h>
 
 using namespace vnotex;
 
@@ -80,12 +82,16 @@ void FindAndReplaceWidget::setupUI()
 
         setFocusProxy(m_findLineEdit);
 
+        const auto &editorConfig = ConfigMgr::getInst().getEditorConfig();
+
         auto findNextBtn = new QPushButton(tr("Find &Next"), this);
+        WidgetUtils::addButtonShortcutText(findNextBtn, editorConfig.getShortcut(EditorConfig::FindNext));
         findNextBtn->setDefault(true);
         connect(findNextBtn, &QPushButton::clicked,
                 this, &FindAndReplaceWidget::findNext);
 
         auto findPrevBtn = new QPushButton(tr("Find &Previous"), this);
+        WidgetUtils::addButtonShortcutText(findPrevBtn, editorConfig.getShortcut(EditorConfig::FindPrevious));
         connect(findPrevBtn, &QPushButton::clicked,
                 this, &FindAndReplaceWidget::findPrevious);
 

+ 4 - 4
src/widgets/historypanel.cpp

@@ -159,7 +159,7 @@ void HistoryPanel::updateHistoryList()
 
     if (itemIdx >= 0) {
         // Older.
-        auto sepItem = ListWidget::createSeparatorItem(tr(">>> Older"));
+        auto sepItem = ListWidget::createSeparatorItem(tr("Older"));
         m_historyList->addItem(sepItem);
 
         for (; itemIdx >= 0; --itemIdx) {
@@ -191,11 +191,11 @@ void HistoryPanel::updateSeparators()
     auto curDateTime = QDateTime::currentDateTime();
     curDateTime.setTime(QTime());
 
-    m_separators[0].m_text = tr(">>> Today");
+    m_separators[0].m_text = tr("Today");
     m_separators[0].m_dateUtc = curDateTime.toUTC();
-    m_separators[1].m_text = tr(">>> Yesterday");
+    m_separators[1].m_text = tr("Yesterday");
     m_separators[1].m_dateUtc = curDateTime.addDays(-1).toUTC();
-    m_separators[2].m_text = tr(">>> Last 7 Days");
+    m_separators[2].m_text = tr("Last 7 Days");
     m_separators[2].m_dateUtc = curDateTime.addDays(-7).toUTC();
 }
 

+ 24 - 7
src/widgets/markdownviewwindow.cpp

@@ -457,8 +457,8 @@ void MarkdownViewWindow::setupViewer()
                 }
             });
     connect(adapter, &MarkdownViewerAdapter::findTextReady,
-            this, [this](const QString &p_text, int p_totalMatches, int p_currentMatchIndex) {
-                this->showFindResult(p_text, p_totalMatches, p_currentMatchIndex);
+            this, [this](const QStringList &p_texts, int p_totalMatches, int p_currentMatchIndex) {
+                this->showFindResult(p_texts, p_totalMatches, p_currentMatchIndex);
             });
 }
 
@@ -944,19 +944,19 @@ void MarkdownViewWindow::handleFindTextChanged(const QString &p_text, FindOption
 {
     if (isReadMode()) {
         if (p_options & FindOption::IncrementalSearch) {
-            adapter()->findText(p_text, p_options);
+            adapter()->findText(QStringList(p_text), p_options);
         }
     } else {
         TextViewWindowHelper::handleFindTextChanged(this, p_text, p_options);
     }
 }
 
-void MarkdownViewWindow::handleFindNext(const QString &p_text, FindOptions p_options)
+void MarkdownViewWindow::handleFindNext(const QStringList &p_texts, FindOptions p_options)
 {
     if (isReadMode()) {
-        adapter()->findText(p_text, p_options);
+        adapter()->findText(p_texts, p_options);
     } else {
-        TextViewWindowHelper::handleFindNext(this, p_text, p_options);
+        TextViewWindowHelper::handleFindNext(this, p_texts, p_options);
     }
 }
 
@@ -983,7 +983,7 @@ void MarkdownViewWindow::handleFindAndReplaceWidgetClosed()
     if (m_editor) {
         TextViewWindowHelper::handleFindAndReplaceWidgetClosed(this);
     } else {
-        adapter()->findText("", FindOption::FindNone);
+        adapter()->findText(QStringList(), FindOption::FindNone);
     }
 }
 
@@ -1013,6 +1013,10 @@ void MarkdownViewWindow::handleFileOpenParameters(const QSharedPointer<FileOpenP
         }
 
         scrollToLine(p_paras->m_lineNumber);
+
+        if (p_paras->m_searchToken) {
+            findTextBySearchToken(p_paras->m_searchToken, p_paras->m_lineNumber);
+        }
     }
 }
 
@@ -1031,6 +1035,19 @@ void MarkdownViewWindow::scrollToLine(int p_lineNumber)
     }
 }
 
+void MarkdownViewWindow::findTextBySearchToken(const QSharedPointer<SearchToken> &p_token, int p_currentMatchLine)
+{
+    if (isReadMode()) {
+        Q_ASSERT(m_viewer);
+        const auto patterns = p_token->toPatterns();
+        updateLastFindInfo(patterns.first, patterns.second);
+        adapter()->findText(patterns.first, patterns.second, p_currentMatchLine);
+    } else {
+        Q_ASSERT(m_editor);
+        TextViewWindowHelper::findTextBySearchToken(this, p_token, p_currentMatchLine);
+    }
+}
+
 bool MarkdownViewWindow::isReadMode() const
 {
     return m_mode == ViewWindowMode::Read;

+ 4 - 1
src/widgets/markdownviewwindow.h

@@ -24,6 +24,7 @@ namespace vnotex
     class MarkdownEditorConfig;
     class EditorConfig;
     class ImageHost;
+    class SearchToken;
 
     class MarkdownViewWindow : public ViewWindow
     {
@@ -65,7 +66,7 @@ namespace vnotex
 
         void handleFindTextChanged(const QString &p_text, FindOptions p_options) Q_DECL_OVERRIDE;
 
-        void handleFindNext(const QString &p_text, FindOptions p_options) Q_DECL_OVERRIDE;
+        void handleFindNext(const QStringList &p_texts, FindOptions p_options) Q_DECL_OVERRIDE;
 
         void handleReplace(const QString &p_text, FindOptions p_options, const QString &p_replaceText) Q_DECL_OVERRIDE;
 
@@ -146,6 +147,8 @@ namespace vnotex
 
         void scrollToLine(int p_lineNumber);
 
+        void findTextBySearchToken(const QSharedPointer<SearchToken> &p_token, int p_currentMatchLine);
+
         bool isReadMode() const;
 
         void updatePreviewHelperFromConfig(const MarkdownEditorConfig &p_config);

+ 10 - 1
src/widgets/searchpanel.cpp

@@ -554,6 +554,9 @@ void SearchPanel::prepareLocationList()
     }
 
     m_locationList->clear();
+
+    m_searchTokenOfSession.clear();
+
     m_locationList->startSession([this](const Location &p_location) {
                 handleLocationActivated(p_location);
             });
@@ -561,9 +564,15 @@ void SearchPanel::prepareLocationList()
 
 void SearchPanel::handleLocationActivated(const Location &p_location)
 {
-    qDebug() << "location activated" << p_location;
+    Q_ASSERT(m_searcher);
+
+    if (!m_searchTokenOfSession) {
+        m_searchTokenOfSession = QSharedPointer<SearchToken>::create(m_searcher->getToken());
+    }
+
     // TODO: decode the path of location and handle different types of destination.
     auto paras = QSharedPointer<FileOpenParameters>::create();
     paras->m_lineNumber = p_location.m_lineNumber;
+    paras->m_searchToken = m_searchTokenOfSession;
     emit VNoteX::getInst().openFileRequested(p_location.m_path, paras);
 }

+ 3 - 0
src/widgets/searchpanel.h

@@ -26,6 +26,7 @@ namespace vnotex
     class Notebook;
     class LocationList;
     struct Location;
+    class SearchToken;
 
     class ISearchInfoProvider
     {
@@ -145,6 +146,8 @@ namespace vnotex
         Searcher *m_searcher = nullptr;
 
         LocationList *m_locationList = nullptr;
+
+        QSharedPointer<SearchToken> m_searchTokenOfSession;
     };
 }
 

+ 6 - 2
src/widgets/textviewwindow.cpp

@@ -218,9 +218,9 @@ void TextViewWindow::handleFindTextChanged(const QString &p_text, FindOptions p_
     TextViewWindowHelper::handleFindTextChanged(this, p_text, p_options);
 }
 
-void TextViewWindow::handleFindNext(const QString &p_text, FindOptions p_options)
+void TextViewWindow::handleFindNext(const QStringList &p_texts, FindOptions p_options)
 {
-    TextViewWindowHelper::handleFindNext(this, p_text, p_options);
+    TextViewWindowHelper::handleFindNext(this, p_texts, p_options);
 }
 
 void TextViewWindow::handleReplace(const QString &p_text, FindOptions p_options, const QString &p_replaceText)
@@ -261,6 +261,10 @@ void TextViewWindow::handleFileOpenParameters(const QSharedPointer<FileOpenParam
     if (p_paras->m_lineNumber > -1) {
         m_editor->scrollToLine(p_paras->m_lineNumber, true);
     }
+
+    if (p_paras->m_searchToken) {
+        TextViewWindowHelper::findTextBySearchToken(this, p_paras->m_searchToken, p_paras->m_lineNumber);
+    }
 }
 
 ViewWindowSession TextViewWindow::saveSession() const

+ 1 - 1
src/widgets/textviewwindow.h

@@ -45,7 +45,7 @@ namespace vnotex
 
         void handleFindTextChanged(const QString &p_text, FindOptions p_options) Q_DECL_OVERRIDE;
 
-        void handleFindNext(const QString &p_text, FindOptions p_options) Q_DECL_OVERRIDE;
+        void handleFindNext(const QStringList &p_texts, FindOptions p_options) Q_DECL_OVERRIDE;
 
         void handleReplace(const QString &p_text, FindOptions p_options, const QString &p_replaceText) Q_DECL_OVERRIDE;
 

+ 14 - 3
src/widgets/textviewwindowhelper.h

@@ -5,12 +5,14 @@
 #include <QTextCursor>
 #include <QRegularExpression>
 #include <QTextBlock>
+#include <QSharedPointer>
 
 #include <vtextedit/texteditorconfig.h>
 #include <core/texteditorconfig.h>
 #include <core/configmgr.h>
 #include <utils/widgetutils.h>
 #include <snippet/snippetmgr.h>
+#include <search/searchtoken.h>
 
 #include "quickselector.h"
 
@@ -169,10 +171,10 @@ namespace vnotex
         }
 
         template <typename _ViewWindow>
-        static void handleFindNext(_ViewWindow *p_win, const QString &p_text, FindOptions p_options)
+        static void handleFindNext(_ViewWindow *p_win, const QStringList &p_texts, FindOptions p_options)
         {
-            const auto result = p_win->m_editor->findText(p_text, toEditorFindFlags(p_options));
-            p_win->showFindResult(p_text, result.m_totalMatches, result.m_currentMatchIndex);
+            const auto result = p_win->m_editor->findText(p_texts, toEditorFindFlags(p_options));
+            p_win->showFindResult(p_texts, result.m_totalMatches, result.m_currentMatchIndex);
         }
 
         template <typename _ViewWindow>
@@ -293,6 +295,15 @@ namespace vnotex
             }
             return textEdit->mapToGlobal(localPos);
         }
+
+        template <typename _ViewWindow>
+        static void findTextBySearchToken(_ViewWindow *p_win, const QSharedPointer<SearchToken> &p_token, int p_currentMatchLine)
+        {
+            const auto patterns = p_token->toPatterns();
+            p_win->updateLastFindInfo(patterns.first, patterns.second);
+            const auto result = p_win->m_editor->findText(patterns.first, toEditorFindFlags(patterns.second), 0, -1, p_currentMatchLine);
+            p_win->showFindResult(patterns.first, result.m_totalMatches, result.m_currentMatchIndex);
+        }
     };
 }
 

+ 21 - 12
src/widgets/viewwindow.cpp

@@ -1007,9 +1007,9 @@ void ViewWindow::handleFindTextChanged(const QString &p_text, FindOptions p_opti
     Q_UNUSED(p_options);
 }
 
-void ViewWindow::handleFindNext(const QString &p_text, FindOptions p_options)
+void ViewWindow::handleFindNext(const QStringList &p_texts, FindOptions p_options)
 {
-    Q_UNUSED(p_text);
+    Q_UNUSED(p_texts);
     Q_UNUSED(p_options);
 }
 
@@ -1039,46 +1039,49 @@ void ViewWindow::findNextOnLastFind(bool p_forward)
 {
     // Check if need to update the find info.
     if (m_findAndReplace && m_findAndReplace->isVisible()) {
-        m_findInfo.m_text = m_findAndReplace->getFindText();
+        m_findInfo.m_texts = QStringList(m_findAndReplace->getFindText());
         m_findInfo.m_options = m_findAndReplace->getOptions();
     }
 
-    if (m_findInfo.m_text.isEmpty()) {
+    if (m_findInfo.m_texts.isEmpty()) {
         return;
     }
 
     if (p_forward) {
-        handleFindNext(m_findInfo.m_text, m_findInfo.m_options & ~FindOption::FindBackward);
+        handleFindNext(m_findInfo.m_texts, m_findInfo.m_options & ~FindOption::FindBackward);
     } else {
-        handleFindNext(m_findInfo.m_text, m_findInfo.m_options | FindOption::FindBackward);
+        handleFindNext(m_findInfo.m_texts, m_findInfo.m_options | FindOption::FindBackward);
     }
 }
 
 void ViewWindow::findNext(const QString &p_text, FindOptions p_options)
 {
-    m_findInfo.m_text = p_text;
+    const QStringList texts(p_text);
+
+    m_findInfo.m_texts = texts;
     m_findInfo.m_options = p_options;
-    handleFindNext(p_text, p_options);
+    handleFindNext(texts, p_options);
 }
 
 void ViewWindow::replace(const QString &p_text, FindOptions p_options, const QString &p_replaceText)
 {
-    m_findInfo.m_text = p_text;
+    m_findInfo.m_texts = QStringList(p_text);
     m_findInfo.m_options = p_options;
     handleReplace(p_text, p_options, p_replaceText);
 }
 
 void ViewWindow::replaceAll(const QString &p_text, FindOptions p_options, const QString &p_replaceText)
 {
-    m_findInfo.m_text = p_text;
+    m_findInfo.m_texts = QStringList(p_text);
     m_findInfo.m_options = p_options;
     handleReplaceAll(p_text, p_options, p_replaceText);
 }
 
-void ViewWindow::showFindResult(const QString &p_text, int p_totalMatches, int p_currentMatchIndex)
+void ViewWindow::showFindResult(const QStringList &p_texts, int p_totalMatches, int p_currentMatchIndex)
 {
     if (p_totalMatches == 0) {
-        showMessage(tr("Pattern not found: %1").arg(p_text));
+        showMessage(tr("Pattern not found: %1%2").arg(p_texts.isEmpty() ? QString() : p_texts[0],
+                                                      p_texts.size() > 1 ? tr(" [+]"): QString()));
     } else {
         showMessage(tr("Match found: %1/%2").arg(p_currentMatchIndex + 1).arg(p_totalMatches));
     }
@@ -1215,3 +1218,9 @@ void ViewWindow::updateImageHostMenu()
 
     handleImageHostChanged(curHost ? curHost->getName() : nullptr);
 }
+
+void ViewWindow::updateLastFindInfo(const QStringList &p_texts, FindOptions p_options)
+{
+    m_findInfo.m_texts = p_texts;
+    m_findInfo.m_options = p_options;
+}

+ 5 - 3
src/widgets/viewwindow.h

@@ -164,7 +164,7 @@ namespace vnotex
 
         virtual void handleFindTextChanged(const QString &p_text, FindOptions p_options);
 
-        virtual void handleFindNext(const QString &p_text, FindOptions p_options);
+        virtual void handleFindNext(const QStringList &p_texts, FindOptions p_options);
 
         virtual void handleReplace(const QString &p_text, FindOptions p_options, const QString &p_replaceText);
 
@@ -229,7 +229,7 @@ namespace vnotex
         bool findAndReplaceWidgetVisible() const;
 
         // @p_currentMatchIndex: 0-based.
-        void showFindResult(const QString &p_text, int p_totalMatches, int p_currentMatchIndex);
+        void showFindResult(const QStringList &p_texts, int p_totalMatches, int p_currentMatchIndex);
 
         void showReplaceResult(const QString &p_text, int p_totalReplaces);
 
@@ -240,6 +240,8 @@ namespace vnotex
         // Show message in status widget if exists. Otherwise, show it in the mainwindow's status widget.
         void showMessage(const QString p_msg);
 
+        void updateLastFindInfo(const QStringList &p_texts, FindOptions p_options);
+
         virtual QPoint getFloatingWidgetPosition();
 
         static QToolBar *createToolBar(QWidget *p_parent = nullptr);
@@ -259,7 +261,7 @@ namespace vnotex
     private:
         struct FindInfo
         {
-            QString m_text;
+            QStringList m_texts;
             FindOptions m_options;
         };