1
#include "zealdocsetsregistry.h"
3
#include "zealsearchquery.h"
4
#include "zealsearchresult.h"
6
#include <QCoreApplication>
9
#include <QStandardPaths>
15
DocsetsRegistry *DocsetsRegistry::m_instance;
17
DocsetsRegistry *DocsetsRegistry::instance()
24
m_instance = new DocsetsRegistry();
32
int DocsetsRegistry::count() const
34
return m_docs.count();
37
QStringList DocsetsRegistry::names() const
42
void DocsetsRegistry::remove(const QString &name)
44
m_docs[name].db.close();
48
void DocsetsRegistry::clear()
50
for (const QString &key : m_docs.keys())
54
QSqlDatabase &DocsetsRegistry::db(const QString &name)
56
Q_ASSERT(m_docs.contains(name));
57
return m_docs[name].db;
60
const QDir &DocsetsRegistry::dir(const QString &name)
62
Q_ASSERT(m_docs.contains(name));
63
return m_docs[name].dir;
66
const DocsetMetadata &DocsetsRegistry::meta(const QString &name)
68
Q_ASSERT(m_docs.contains(name));
69
return m_docs[name].metadata;
72
QIcon DocsetsRegistry::icon(const QString &docsetName) const
74
const DocsetEntry &entry = m_docs[docsetName];
75
QString bundleName = entry.info.bundleName;
76
bundleName.replace(" ", "_");
77
QString identifier = entry.info.bundleIdentifier;
78
QIcon icon(entry.dir.absoluteFilePath("favicon.ico"));
79
if (icon.availableSizes().isEmpty())
80
icon = QIcon(entry.dir.absoluteFilePath("icon.png"));
82
if (icon.availableSizes().isEmpty()) {
83
icon = QIcon(QString("icons:%1.png").arg(bundleName));
85
// Fallback to identifier and docset file name.
86
if (icon.availableSizes().isEmpty())
87
icon = QIcon(QString("icons:%1.png").arg(identifier));
88
if (icon.availableSizes().isEmpty())
89
icon = QIcon(QString("icons:%1.png").arg(docsetName));
94
DocsetType DocsetsRegistry::type(const QString &name) const
96
Q_ASSERT(m_docs.contains(name));
97
return m_docs[name].type;
100
DocsetsRegistry::DocsetsRegistry()
102
/// FIXME: Only search should be performed in a separate thread
103
auto thread = new QThread(this);
104
moveToThread(thread);
108
QList<DocsetsRegistry::DocsetEntry> DocsetsRegistry::docsets()
110
return m_docs.values();
113
void DocsetsRegistry::addDocset(const QString &path)
116
auto name = dir.dirName().replace(QStringLiteral(".docset"), QString());
120
if (QFile::exists(dir.filePath("index.sqlite"))) {
121
db = QSqlDatabase::addDatabase("QSQLITE", name);
122
db.setDatabaseName(dir.filePath("index.sqlite"));
128
QDir contentsDir(dir.filePath("Contents"));
129
entry.info.readDocset(contentsDir.absoluteFilePath("Info.plist"));
131
if (entry.info.family == "cheatsheet")
132
name = QString("%1_cheats").arg(name);
135
auto dashFile = QDir(contentsDir.filePath("Resources")).filePath("docSet.dsidx");
136
db = QSqlDatabase::addDatabase("QSQLITE", name);
137
db.setDatabaseName(dashFile);
139
auto q = db.exec("select name from sqlite_master where type='table'");
142
tables.append(q.value(0).toString());
144
if (tables.contains("searchIndex"))
154
if (m_docs.contains(name))
157
entry.prefix = entry.info.bundleName.isEmpty()
159
: entry.info.bundleName;
165
meta.read(path+"/meta.json");
166
entry.metadata = meta;
167
m_docs[name] = entry;
170
DocsetsRegistry::DocsetEntry *DocsetsRegistry::entry(const QString &name)
172
return &m_docs[name];
175
void DocsetsRegistry::runQuery(const QString &query)
178
QMetaObject::invokeMethod(this, "_runQuery", Qt::QueuedConnection, Q_ARG(QString, query),
179
Q_ARG(int, m_lastQuery));
182
void DocsetsRegistry::invalidateQueries()
187
void DocsetsRegistry::_runQuery(const QString &rawQuery, int queryNum)
189
// If some other queries pending, ignore this one.
190
if (queryNum != m_lastQuery)
193
QList<SearchResult> results;
194
SearchQuery query(rawQuery);
196
QString preparedQuery = query.sanitizedQuery();
197
bool hasDocsetFilter = query.hasDocsetFilter();
199
for (const DocsetsRegistry::DocsetEntry &docset : docsets()) {
200
// Filter out this docset as the names don't match the docset prefix
201
if (hasDocsetFilter && !query.docsetPrefixMatch(docset.prefix))
206
QList<QList<QVariant>> found;
207
bool withSubStrings = false;
208
// %.%1% for long Django docset values like django.utils.http
209
// %::%1% for long C++ docset values like std::set
210
// %/%1% for long Go docset values like archive/tar
211
QString subNames = QStringLiteral(" or %1 like '%.%2%' escape '\\'");
212
subNames += QStringLiteral(" or %1 like '%::%2%' escape '\\'");
213
subNames += QStringLiteral(" or %1 like '%/%2%' escape '\\'");
214
while (found.size() < 100) {
215
auto curQuery = preparedQuery;
216
QString notQuery; // don't return the same result twice
218
if (withSubStrings) {
219
// if less than 100 found starting with query, search all substrings
220
curQuery = "%" + preparedQuery;
221
// don't return 'starting with' results twice
222
if (docset.type == ZDASH) {
223
notQuery = QString(" and not (ztokenname like '%1%' escape '\\' %2) ").arg(preparedQuery, subNames.arg("ztokenname", preparedQuery));
225
if (docset.type == ZEAL) {
226
notQuery = QString(" and not (t.name like '%1%' escape '\\') ").arg(preparedQuery);
227
parentQuery = QString(" or t2.name like '%1%' escape '\\' ").arg(preparedQuery);
229
notQuery = QString(" and not (t.name like '%1%' escape '\\' %2) ").arg(preparedQuery, subNames.arg("t.name", preparedQuery));
234
if (docset.type == ZEAL) {
235
qstr = QString("select t.name, t2.name, t.path from things t left join things t2 on t2.id=t.parent where "
236
"(t.name like '%1%' escape '\\' %3) %2 order by length(t.name), lower(t.name) asc, t.path asc limit 100").arg(curQuery, notQuery, parentQuery);
238
} else if (docset.type == DASH) {
239
qstr = QString("select t.name, null, t.path from searchIndex t where (t.name "
240
"like '%1%' escape '\\' %3) %2 order by length(t.name), lower(t.name) asc, t.path asc limit 100").arg(curQuery, notQuery, subNames.arg("t.name", curQuery));
241
} else if (docset.type == ZDASH) {
243
qstr = QString("select ztokenname, null, zpath, zanchor from ztoken "
244
"join ztokenmetainformation on ztoken.zmetainformation = ztokenmetainformation.z_pk "
245
"join zfilepath on ztokenmetainformation.zfile = zfilepath.z_pk where (ztokenname "
246
"like '%1%' escape '\\' %3) %2 order by length(ztokenname), lower(ztokenname) asc, zpath asc, "
247
"zanchor asc limit 100").arg(curQuery, notQuery,
248
subNames.arg("ztokenname", curQuery));
250
q = db(docset.name).exec(qstr);
252
QList<QVariant> values;
253
for (int i = 0; i < cols; ++i)
254
values.append(q.value(i));
255
found.append(values);
260
withSubStrings = true; // try again searching for substrings
263
for (const auto &row : found) {
265
if (!row[1].isNull())
266
parentName = row[1].toString();
268
auto path = row[2].toString();
269
// FIXME: refactoring to use common code in ZealListModel and DocsetsRegistry
270
if (docset.type == ZDASH)
271
path += "#" + row[3].toString();
273
auto itemName = row[0].toString();
274
normalizeName(itemName, parentName, row[1].toString());
275
results.append(SearchResult(itemName, parentName, path, docset.name,
280
if (queryNum != m_lastQuery)
281
return; // some other queries pending - ignore this one
283
m_queryResults = results;
284
emit queryCompleted();
287
void DocsetsRegistry::normalizeName(QString &itemName, QString &parentName,
288
const QString &initialParent)
290
QRegExp matchMethodName("^([^\\(]+)(?:\\(.*\\))?$");
291
if (matchMethodName.indexIn(itemName) != -1)
292
itemName = matchMethodName.cap(1);
294
QString separators[] = {".", "::", "/"};
295
for (unsigned i = 0; i < sizeof separators / sizeof *separators; ++i) {
296
QString sep = separators[i];
297
if (itemName.indexOf(sep) != -1 && itemName.indexOf(sep) != 0 && initialParent.isNull()) {
298
auto splitted = itemName.split(sep);
299
itemName = splitted.at(splitted.size()-1);
300
parentName = splitted.at(splitted.size()-2);
305
const QList<SearchResult> &DocsetsRegistry::queryResults()
307
return m_queryResults;
310
QList<SearchResult> DocsetsRegistry::relatedLinks(const QString &name, const QString &path)
312
QList<SearchResult> results;
313
// Get the url without the #anchor.
315
mainUrl.setFragment(NULL);
316
QString pageUrl(mainUrl.toString());
317
DocsetEntry entry = m_docs[name];
319
// Prepare the query to look up all pages with the same url.
321
if (entry.type == DASH) {
322
query = QString("SELECT name, type, path FROM searchIndex WHERE path LIKE \"%1%%\"").arg(pageUrl);
323
} else if (entry.type == ZDASH) {
324
query = QString("SELECT ztoken.ztokenname, ztokentype.ztypename, zfilepath.zpath, ztokenmetainformation.zanchor "
326
"JOIN ztokenmetainformation ON ztoken.zmetainformation = ztokenmetainformation.z_pk "
327
"JOIN zfilepath ON ztokenmetainformation.zfile = zfilepath.z_pk "
328
"JOIN ztokentype ON ztoken.ztokentype = ztokentype.z_pk "
329
"WHERE zfilepath.zpath = \"%1\"").arg(pageUrl);
330
} else if (entry.type == ZEAL) {
331
query = QString("SELECT name type, path FROM things WHERE path LIKE \"%1%%\"").arg(pageUrl);
334
QSqlQuery result = entry.db.exec(query);
335
while (result.next()) {
336
QString sectionName = result.value(0).toString();
337
QString sectionPath = result.value(2).toString();
339
if (entry.type == ZDASH) {
340
sectionPath.append("#");
341
sectionPath.append(result.value(3).toString());
344
normalizeName(sectionName, parentName);
346
results.append(SearchResult(sectionName, QString(), sectionPath, name, QString()));
352
QString DocsetsRegistry::docsetsDir() const
354
const QScopedPointer<const QSettings> settings(new QSettings());
355
if (settings->contains("docsetsDir"))
356
return settings->value("docsetsDir").toString();
358
QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::DataLocation));
359
if (!dataDir.cd("docsets"))
360
dataDir.mkpath("docsets");
361
dataDir.cd("docsets");
362
return dataDir.absolutePath();
365
// Recursively finds and adds all docsets in a given directory.
366
void DocsetsRegistry::addDocsetsFromFolder(const QDir &folder)
368
for (const QFileInfo &subdir : folder.entryInfoList(QDir::NoDotAndDotDot | QDir::AllDirs)) {
369
if (subdir.suffix() == "docset") {
370
QMetaObject::invokeMethod(this, "addDocset", Qt::BlockingQueuedConnection,
371
Q_ARG(QString, subdir.absoluteFilePath()));
373
addDocsetsFromFolder(QDir(subdir.absoluteFilePath()));
378
void DocsetsRegistry::initialiseDocsets()
381
addDocsetsFromFolder(QDir(docsetsDir()));
382
QDir appDir(QCoreApplication::applicationDirPath());
383
if (appDir.cd("docsets"))
384
addDocsetsFromFolder(appDir);