1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "helpindexfilter.h"
27 
28 #include "helpicons.h"
29 #include "helpmanager.h"
30 #include "topicchooser.h"
31 
32 #include <coreplugin/icore.h>
33 #include <coreplugin/helpmanager.h>
34 #include <extensionsystem/pluginmanager.h>
35 #include <utils/utilsicons.h>
36 
37 #include <QIcon>
38 
39 #ifndef HELP_NEW_FILTER_ENGINE
40 
41 #include <utils/algorithm.h>
42 
43 #include <QSqlDatabase>
44 #include <QSqlDriver>
45 #include <QSqlError>
46 #include <QSqlQuery>
47 
48 #else
49 
50 #include "localhelpmanager.h"
51 #include <QtHelp/QHelpEngine>
52 #include <QtHelp/QHelpFilterEngine>
53 #include <QtHelp/QHelpLink>
54 
55 #endif
56 
57 using namespace Core;
58 using namespace Help;
59 using namespace Help::Internal;
60 
HelpIndexFilter()61 HelpIndexFilter::HelpIndexFilter()
62 {
63     setId("HelpIndexFilter");
64     setDisplayName(tr("Help Index"));
65     setDefaultIncludedByDefault(false);
66     setDefaultShortcutString("?");
67 
68     m_icon = Utils::Icons::BOOKMARK.icon();
69     connect(Core::HelpManager::Signals::instance(), &Core::HelpManager::Signals::setupFinished,
70             this, &HelpIndexFilter::invalidateCache);
71     connect(Core::HelpManager::Signals::instance(),
72             &Core::HelpManager::Signals::documentationChanged,
73             this,
74             &HelpIndexFilter::invalidateCache);
75     connect(HelpManager::instance(), &HelpManager::collectionFileChanged,
76             this, &HelpIndexFilter::invalidateCache);
77 }
78 
79 HelpIndexFilter::~HelpIndexFilter() = default;
80 
81 #ifndef HELP_NEW_FILTER_ENGINE
82 
prepareSearch(const QString & entry)83 void HelpIndexFilter::prepareSearch(const QString &entry)
84 {
85     Q_UNUSED(entry)
86     QStringList namespaces = HelpManager::registeredNamespaces();
87     m_helpDatabases = Utils::transform(namespaces, &HelpManager::fileFromNamespace);
88 }
89 
matchesFor(QFutureInterface<LocatorFilterEntry> & future,const QString & entry)90 QList<LocatorFilterEntry> HelpIndexFilter::matchesFor(QFutureInterface<LocatorFilterEntry> &future, const QString &entry)
91 {
92     if (m_needsUpdate.exchange(false) || m_searchTermCache.size() < 2
93             || m_searchTermCache.isEmpty() || !entry.contains(m_searchTermCache)) {
94         int limit = entry.size() < 2 ? 200 : INT_MAX;
95         QSet<QString> results;
96         for (const QString &filePath : qAsConst(m_helpDatabases)) {
97             if (future.isCanceled())
98                 return QList<LocatorFilterEntry>();
99             QSet<QString> result;
100             QMetaObject::invokeMethod(this, "searchMatches", Qt::BlockingQueuedConnection,
101                                       Q_RETURN_ARG(QSet<QString>, result),
102                                       Q_ARG(QString, filePath),
103                                       Q_ARG(QString, entry),
104                                       Q_ARG(int, limit));
105             results.unite(result);
106         }
107         m_keywordCache = results;
108         m_searchTermCache = entry;
109     }
110 
111     Qt::CaseSensitivity cs = caseSensitivity(entry);
112     QList<LocatorFilterEntry> entries;
113     QStringList keywords;
114     QStringList unsortedKeywords;
115     keywords.reserve(m_keywordCache.size());
116     unsortedKeywords.reserve(m_keywordCache.size());
117     QSet<QString> allresults;
118     for (const QString &keyword : qAsConst(m_keywordCache)) {
119         if (future.isCanceled())
120             return QList<LocatorFilterEntry>();
121         if (keyword.startsWith(entry, cs)) {
122             keywords.append(keyword);
123             allresults.insert(keyword);
124         } else if (keyword.contains(entry, cs)) {
125             unsortedKeywords.append(keyword);
126             allresults.insert(keyword);
127         }
128     }
129     Utils::sort(keywords);
130     keywords << unsortedKeywords;
131     m_keywordCache = allresults;
132     m_searchTermCache = entry;
133     for (const QString &keyword : qAsConst(keywords)) {
134         const int index = keyword.indexOf(entry, 0, cs);
135         LocatorFilterEntry filterEntry(this, keyword, QVariant(), m_icon);
136         filterEntry.highlightInfo = {index, entry.length()};
137         entries.append(filterEntry);
138     }
139 
140     return entries;
141 }
142 
143 #else
144 
updateCache(QFutureInterface<LocatorFilterEntry> & future,const QStringList & cache,const QString & entry)145 bool HelpIndexFilter::updateCache(QFutureInterface<LocatorFilterEntry> &future,
146                                   const QStringList &cache, const QString &entry)
147 {
148     const Qt::CaseSensitivity cs = caseSensitivity(entry);
149     QStringList bestKeywords;
150     QStringList worseKeywords;
151     bestKeywords.reserve(cache.size());
152     worseKeywords.reserve(cache.size());
153     for (const QString &keyword : cache) {
154         if (future.isCanceled())
155             return false;
156         if (keyword.startsWith(entry, cs))
157             bestKeywords.append(keyword);
158         else if (keyword.contains(entry, cs))
159             worseKeywords.append(keyword);
160     }
161     bestKeywords << worseKeywords;
162     m_lastIndicesCache = bestKeywords;
163     m_lastEntry = entry;
164 
165     return true;
166 }
167 
matchesFor(QFutureInterface<LocatorFilterEntry> & future,const QString & entry)168 QList<LocatorFilterEntry> HelpIndexFilter::matchesFor(QFutureInterface<LocatorFilterEntry> &future, const QString &entry)
169 {
170     if (m_needsUpdate.exchange(false)) {
171         QStringList indices;
172         QMetaObject::invokeMethod(this, [this] { return allIndices(); },
173                                   Qt::BlockingQueuedConnection, &indices);
174         m_allIndicesCache = indices;
175         // force updating the cache taking the m_allIndicesCache
176         m_lastIndicesCache = QStringList();
177         m_lastEntry = QString();
178     }
179 
180     const QStringList cacheBase = m_lastEntry.isEmpty() || !entry.contains(m_lastEntry)
181             ? m_allIndicesCache : m_lastIndicesCache;
182 
183     if (!updateCache(future, cacheBase, entry))
184         return QList<LocatorFilterEntry>();
185 
186     const Qt::CaseSensitivity cs = caseSensitivity(entry);
187     QList<LocatorFilterEntry> entries;
188     for (const QString &keyword : qAsConst(m_lastIndicesCache)) {
189         const int index = keyword.indexOf(entry, 0, cs);
190         LocatorFilterEntry filterEntry(this, keyword, QVariant(), m_icon);
191         filterEntry.highlightInfo = {index, int(entry.length())};
192         entries.append(filterEntry);
193     }
194 
195     return entries;
196 }
197 
198 #endif
199 
accept(LocatorFilterEntry selection,QString * newText,int * selectionStart,int * selectionLength) const200 void HelpIndexFilter::accept(LocatorFilterEntry selection,
201                              QString *newText, int *selectionStart, int *selectionLength) const
202 {
203     Q_UNUSED(newText)
204     Q_UNUSED(selectionStart)
205     Q_UNUSED(selectionLength)
206     const QString &key = selection.displayName;
207 #ifndef HELP_NEW_FILTER_ENGINE
208     const QMultiMap<QString, QUrl> &links = HelpManager::instance()->linksForKeyword(key);
209 #else
210     QMultiMap<QString, QUrl> links;
211     const QList<QHelpLink> docs = LocalHelpManager::helpEngine().documentsForKeyword(key, QString());
212     for (const auto &doc : docs)
213         links.insert(doc.title, doc.url);
214 #endif
215     emit linksActivated(links, key);
216 }
217 
refresh(QFutureInterface<void> & future)218 void HelpIndexFilter::refresh(QFutureInterface<void> &future)
219 {
220     Q_UNUSED(future)
221     invalidateCache();
222 }
223 
224 #ifndef HELP_NEW_FILTER_ENGINE
225 
searchMatches(const QString & databaseFilePath,const QString & term,int limit)226 QSet<QString> HelpIndexFilter::searchMatches(const QString &databaseFilePath,
227                                            const QString &term, int limit)
228 {
229     static const QLatin1String sqlite("QSQLITE");
230     static const QLatin1String name("HelpManager::findKeywords");
231 
232     QSet<QString> keywords;
233 
234     { // make sure db is destroyed before removeDatabase call
235         QSqlDatabase db = QSqlDatabase::addDatabase(sqlite, name);
236         if (db.driver() && db.driver()->lastError().type() == QSqlError::NoError) {
237             db.setDatabaseName(databaseFilePath);
238             if (db.open()) {
239                 QSqlQuery query = QSqlQuery(db);
240                 query.setForwardOnly(true);
241                 query.exec(QString::fromLatin1("SELECT DISTINCT Name FROM IndexTable WHERE Name LIKE "
242                     "'%%1%' LIMIT %2").arg(term, QString::number(limit)));
243                 while (query.next()) {
244                     const QString &keyValue = query.value(0).toString();
245                     if (!keyValue.isEmpty())
246                         keywords.insert(keyValue);
247                 }
248                 db.close();
249             }
250         }
251     }
252     QSqlDatabase::removeDatabase(name);
253     return keywords;
254 }
255 
256 #else
257 
allIndices() const258 QStringList HelpIndexFilter::allIndices() const
259 {
260     LocalHelpManager::setupGuiHelpEngine();
261     return LocalHelpManager::filterEngine()->indices(QString());
262 }
263 
264 #endif
265 
invalidateCache()266 void HelpIndexFilter::invalidateCache()
267 {
268     m_needsUpdate = true;
269 }
270