]> cloud.milkyroute.net Git - dolphin.git/blob - src/search/dolphinquery.cpp
DolphinStatusbar: Fix background and margins for non-Breeze styles
[dolphin.git] / src / search / dolphinquery.cpp
1 /*
2 SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
3 SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "dolphinquery.h"
9
10 #include "config-dolphin.h"
11 #if HAVE_BALOO
12 #include <Baloo/IndexerConfig>
13 #include <Baloo/Query>
14 #endif
15 #include "dolphinplacesmodelsingleton.h"
16
17 #include <KFileMetaData/TypeInfo>
18 #include <KLocalizedString>
19
20 #include <QRegularExpression>
21 #include <QUrlQuery>
22
23 using namespace Search;
24
25 bool Search::isSupportedSearchScheme(const QString &urlScheme)
26 {
27 static const QStringList supportedSchemes = {
28 QStringLiteral("filenamesearch"),
29 QStringLiteral("baloosearch"),
30 QStringLiteral("tags"),
31 };
32
33 return supportedSchemes.contains(urlScheme);
34 }
35
36 bool g_testMode = false;
37
38 bool Search::isIndexingEnabledIn(QUrl directory)
39 {
40 if (g_testMode) {
41 return true; // For unit-testing, let's pretend everything is indexed correctly.
42 }
43
44 #if HAVE_BALOO
45 const Baloo::IndexerConfig searchInfo;
46 return searchInfo.fileIndexingEnabled() && !directory.isEmpty() && searchInfo.shouldBeIndexed(directory.toLocalFile());
47 #else
48 Q_UNUSED(directory)
49 return false;
50 #endif
51 }
52
53 bool Search::isContentIndexingEnabled()
54 {
55 if (g_testMode) {
56 return true; // For unit-testing, let's pretend everything is indexed correctly.
57 }
58
59 #if HAVE_BALOO
60 return !Baloo::IndexerConfig{}.onlyBasicIndexing();
61 #else
62 return false;
63 #endif
64 }
65
66 namespace
67 {
68 /** The path to be passed so Baloo searches everywhere. */
69 constexpr auto balooSearchEverywherePath = QLatin1String("");
70 /** The path to be passed so Filenamesearch searches everywhere. */
71 constexpr auto filenamesearchEverywherePath = QLatin1String("file:///");
72
73 #if HAVE_BALOO
74 /** Checks if a given term in the Baloo::Query::searchString() is a special search term
75 * @return: the specific search token of the term, or an empty QString() if none is found
76 */
77 QString searchTermToken(const QString &term)
78 {
79 static const QLatin1String searchTokens[]{QLatin1String("filename:"),
80 QLatin1String("modified>="),
81 QLatin1String("modified>"),
82 QLatin1String("rating>="),
83 QLatin1String("tag:"),
84 QLatin1String("tag=")};
85
86 for (const auto &searchToken : searchTokens) {
87 if (term.startsWith(searchToken)) {
88 return searchToken;
89 }
90 }
91 return QString();
92 }
93
94 QString stripQuotes(const QString &text)
95 {
96 if (text.length() >= 2 && text.at(0) == QLatin1Char('"') && text.back() == QLatin1Char('"')) {
97 return text.mid(1, text.size() - 2);
98 }
99 return text;
100 }
101
102 QStringList splitOutsideQuotes(const QString &text)
103 {
104 // Match groups on 3 possible conditions:
105 // - Groups with two leading quotes must close both on them (filename:""abc xyz" tuv")
106 // - Groups enclosed in quotes
107 // - Words separated by spaces
108 static const QRegularExpression subTermsRegExp("(\\S*?\"\"[^\"]+\"[^\"]+\"+|\\S*?\"[^\"]+\"+|(?<=\\s|^)\\S+(?=\\s|$))");
109 auto subTermsMatchIterator = subTermsRegExp.globalMatch(text);
110
111 QStringList textParts;
112 while (subTermsMatchIterator.hasNext()) {
113 textParts << subTermsMatchIterator.next().captured(0);
114 }
115 return textParts;
116 }
117 #endif
118
119 QString trimChar(const QString &text, const QLatin1Char aChar)
120 {
121 const int start = text.startsWith(aChar) ? 1 : 0;
122 const int end = (text.length() > 1 && text.endsWith(aChar)) ? 1 : 0;
123
124 return text.mid(start, text.length() - start - end);
125 }
126 }
127
128 Search::DolphinQuery::DolphinQuery(const QUrl &url, const QUrl &backupSearchPath)
129 {
130 if (url.scheme() == QLatin1String("filenamesearch")) {
131 m_searchTool = SearchTool::Filenamesearch;
132 const QUrlQuery query(url);
133 const QString filenamesearchSearchPathString = query.queryItemValue(QStringLiteral("url"), QUrl::FullyDecoded);
134 const QUrl filenamesearchSearchPathUrl = QUrl::fromUserInput(filenamesearchSearchPathString, QString(), QUrl::AssumeLocalFile);
135 if (!filenamesearchSearchPathUrl.isValid() || filenamesearchSearchPathString == filenamesearchEverywherePath) {
136 // The parsed search location is either invalid or matches a string that represents searching "everywhere".
137 m_searchLocations = SearchLocations::Everywhere;
138 m_searchPath = backupSearchPath;
139 } else {
140 m_searchLocations = SearchLocations::FromHere;
141 m_searchPath = filenamesearchSearchPathUrl;
142 }
143 m_searchTerm = query.queryItemValue(QStringLiteral("search"), QUrl::FullyDecoded);
144 m_searchThrough = query.queryItemValue(QStringLiteral("checkContent")) == QLatin1String("yes") ? SearchThrough::FileContents : SearchThrough::FileNames;
145 return;
146 }
147
148 #if HAVE_BALOO
149 if (url.scheme() == QLatin1String("baloosearch")) {
150 m_searchTool = SearchTool::Baloo;
151 initializeFromBalooQuery(Baloo::Query::fromSearchUrl(url), backupSearchPath);
152 return;
153 }
154 #endif
155
156 if (url.scheme() == QLatin1String("tags")) {
157 #if HAVE_BALOO
158 m_searchTool = SearchTool::Baloo;
159 #endif
160 m_searchLocations = SearchLocations::Everywhere;
161 m_searchPath = backupSearchPath;
162 // tags can contain # symbols or slashes within the Url
163 const auto tag = trimChar(url.toString(QUrl::RemoveScheme), QLatin1Char('/'));
164 if (!tag.isEmpty()) {
165 m_requiredTags.append(trimChar(url.toString(QUrl::RemoveScheme), QLatin1Char('/')));
166 }
167 return;
168 }
169
170 m_searchPath = url;
171 switchToPreferredSearchTool();
172 }
173
174 QUrl DolphinQuery::toUrl() const
175 {
176 // The following pre-conditions are sanity checks on this DolphinQuery object. If they fail, the issue is that we ever allowed the DolphinQuery to be in an
177 // inconsistent state to begin with. This should be fixed by bringing this DolphinQuery object into a reasonable state at the end of the constructors or
178 // setter methods which caused this impossible-to-fulfill combination of conditions.
179 Q_ASSERT_X(m_searchLocations == SearchLocations::Everywhere || m_searchPath.isValid(),
180 "DolphinQuery::toUrl()",
181 "We are supposed to search in a specific location but we do not know where!");
182 #if HAVE_BALOO
183 Q_ASSERT_X(m_searchLocations == SearchLocations::Everywhere || m_searchTool != SearchTool::Baloo || isIndexingEnabledIn(m_searchPath),
184 "DolphinQuery::toUrl()",
185 "We are asking Baloo to search in a location which Baloo is not supposed to have indexed!");
186 #endif
187
188 QUrl url;
189
190 #if HAVE_BALOO
191 /// Create Baloo search URL
192 if (m_searchTool == SearchTool::Baloo) {
193 Baloo::Query query;
194 if (m_fileType != KFileMetaData::Type::Empty) {
195 query.addType(KFileMetaData::TypeInfo{m_fileType}.name());
196 }
197
198 QStringList balooQueryStrings = m_unrecognizedBalooQueryStrings;
199
200 if (m_searchThrough == SearchThrough::FileContents) {
201 balooQueryStrings << m_searchTerm;
202 } else if (!m_searchTerm.isEmpty()) {
203 balooQueryStrings << QStringLiteral("filename:\"%1\"").arg(m_searchTerm);
204 }
205
206 if (m_searchLocations == SearchLocations::FromHere) {
207 query.setIncludeFolder(m_searchPath.toLocalFile());
208 }
209
210 if (m_modifiedSinceDate.isValid()) {
211 balooQueryStrings << QStringLiteral("modified>=%1").arg(m_modifiedSinceDate.toString(Qt::ISODate));
212 }
213
214 if (m_minimumRating >= 1) {
215 balooQueryStrings << QStringLiteral("rating>=%1").arg(m_minimumRating);
216 }
217
218 for (const auto &tag : m_requiredTags) {
219 if (tag.contains(QLatin1Char(' '))) {
220 balooQueryStrings << QStringLiteral("tag:\"%1\"").arg(tag);
221 } else {
222 balooQueryStrings << QStringLiteral("tag:%1").arg(tag);
223 }
224 }
225
226 query.setSearchString(balooQueryStrings.join(QLatin1Char(' ')));
227
228 return query.toSearchUrl(QUrl::toPercentEncoding(title()));
229 }
230 #endif
231
232 /// Create Filenamsearch search URL
233 url.setScheme(QStringLiteral("filenamesearch"));
234
235 QUrlQuery qUrlQuery;
236 qUrlQuery.addQueryItem(QStringLiteral("search"), QUrl::toPercentEncoding(m_searchTerm));
237 if (m_searchThrough == SearchThrough::FileContents) {
238 qUrlQuery.addQueryItem(QStringLiteral("checkContent"), QStringLiteral("yes"));
239 }
240
241 if (m_searchLocations == SearchLocations::FromHere && m_searchPath.isValid()) {
242 qUrlQuery.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(m_searchPath.url()));
243 } else {
244 // Search in root which is considered searching "everywhere".
245 qUrlQuery.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(filenamesearchEverywherePath));
246 }
247 qUrlQuery.addQueryItem(QStringLiteral("title"), QUrl::toPercentEncoding(title()));
248
249 url.setQuery(qUrlQuery);
250 return url;
251 }
252
253 void DolphinQuery::setSearchLocations(SearchLocations searchLocations)
254 {
255 m_searchLocations = searchLocations;
256 switchToPreferredSearchTool();
257 }
258
259 void DolphinQuery::setSearchPath(const QUrl &searchPath)
260 {
261 m_searchPath = searchPath;
262 switchToPreferredSearchTool();
263 }
264
265 void DolphinQuery::setSearchThrough(SearchThrough searchThrough)
266 {
267 m_searchThrough = searchThrough;
268 switchToPreferredSearchTool();
269 }
270
271 void DolphinQuery::switchToPreferredSearchTool()
272 {
273 const bool isIndexingEnabledInCurrentSearchLocation = m_searchLocations == SearchLocations::Everywhere || isIndexingEnabledIn(m_searchPath);
274 const bool searchThroughFileContentsWithoutIndexing = m_searchThrough == SearchThrough::FileContents && !isContentIndexingEnabled();
275 if (!isIndexingEnabledInCurrentSearchLocation || searchThroughFileContentsWithoutIndexing) {
276 m_searchTool = SearchTool::Filenamesearch;
277 return;
278 }
279 #if HAVE_BALOO
280 // The current search location allows searching with Baloo. We switch to Baloo if this is the saved user preference.
281 if (SearchSettings::searchTool() == QStringLiteral("Baloo")) {
282 m_searchTool = SearchTool::Baloo;
283 }
284 #endif
285 }
286
287 #if HAVE_BALOO
288 void DolphinQuery::initializeFromBalooQuery(const Baloo::Query &balooQuery, const QUrl &backupSearchPath)
289 {
290 const QString balooSearchPathString = balooQuery.includeFolder();
291 const QUrl balooSearchPathUrl = QUrl::fromUserInput(balooSearchPathString, QString(), QUrl::AssumeLocalFile);
292 if (!balooSearchPathUrl.isValid() || balooSearchPathString == balooSearchEverywherePath) {
293 // The parsed search location is either invalid or matches a string that represents searching "everywhere" i.e. in all indexed locations.
294 m_searchLocations = SearchLocations::Everywhere;
295 m_searchPath = backupSearchPath;
296 } else {
297 m_searchLocations = SearchLocations::FromHere;
298 m_searchPath = balooSearchPathUrl;
299 }
300
301 const QStringList types = balooQuery.types();
302 // We currently only allow searching for one file type at once. (Searching for more seems out of scope for Dolphin anyway IMO.)
303 m_fileType = types.isEmpty() ? KFileMetaData::Type::Empty : KFileMetaData::TypeInfo::fromName(types.first()).type();
304
305 /// If nothing is requested, we use the default.
306 std::optional<SearchThrough> requestedToSearchThrough;
307 const QStringList subTerms = splitOutsideQuotes(balooQuery.searchString());
308 for (const QString &subTerm : subTerms) {
309 const QString token = searchTermToken(subTerm);
310 const QString value = stripQuotes(subTerm.mid(token.length()));
311
312 if (token == QLatin1String("filename:")) {
313 // This query is meant to not search in file contents.
314 if (!value.isEmpty()) {
315 if (m_searchTerm.isEmpty()) { // Seems like we already received a search term for the content search. We don't overwrite it because the Dolphin
316 // UI does not support searching for differing strings in content and file name.
317 m_searchTerm = value;
318 }
319 if (!requestedToSearchThrough.has_value()) { // If requested to search thorugh contents, searching file names is already implied.
320 requestedToSearchThrough = SearchThrough::FileNames;
321 }
322 }
323 continue;
324 } else if (token.startsWith(QLatin1String("modified>="))) {
325 m_modifiedSinceDate = QDate::fromString(value, Qt::ISODate);
326 continue;
327 } else if (token.startsWith(QLatin1String("modified>"))) {
328 m_modifiedSinceDate = QDate::fromString(value, Qt::ISODate).addDays(1);
329 continue;
330 } else if (token.startsWith(QLatin1String("rating>="))) {
331 m_minimumRating = value.toInt();
332 continue;
333 } else if (token.startsWith(QLatin1String("tag"))) {
334 m_requiredTags.append(value);
335 continue;
336 } else if (!token.isEmpty()) {
337 m_unrecognizedBalooQueryStrings << token + value;
338 continue;
339 } else if (subTerm == QLatin1String("AND") && subTerm != subTerms.at(0) && subTerm != subTerms.back()) {
340 continue;
341 } else if (!value.isEmpty()) {
342 // An empty token means this is just blank text, which is where the generic search term is located.
343 if (!m_searchTerm.isEmpty()) {
344 // Multiple search terms are separated by spaces.
345 m_searchTerm.append(QLatin1Char{' '});
346 }
347 m_searchTerm.append(value);
348 requestedToSearchThrough = SearchThrough::FileContents;
349 }
350 }
351 if (requestedToSearchThrough.has_value()) {
352 m_searchThrough = requestedToSearchThrough.value();
353 }
354 }
355 #endif // HAVE_BALOO
356
357 QString DolphinQuery::title() const
358 {
359 if (m_searchLocations == SearchLocations::FromHere) {
360 QString prettySearchLocation;
361 KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel();
362 QModelIndex url_index = placesModel->closestItem(m_searchPath);
363 if (url_index.isValid() && placesModel->url(url_index).matches(m_searchPath, QUrl::StripTrailingSlash)) {
364 prettySearchLocation = placesModel->text(url_index);
365 } else {
366 prettySearchLocation = m_searchPath.fileName();
367 }
368 if (prettySearchLocation.isEmpty()) {
369 prettySearchLocation = m_searchPath.toString(QUrl::RemoveAuthority);
370 }
371
372 // A great title clearly identifies a search results page among many tabs, windows, or links in the Places panel.
373 // Using the search term is phenomenal for this because the user has typed this very specific string with a clear intention, so we definitely want to
374 // reuse the search term in the title if possible.
375 if (!m_searchTerm.isEmpty()) {
376 if (m_searchThrough == SearchThrough::FileNames) {
377 return i18nc("@title of a search results page. %1 is the search term a user entered, %2 is a folder name",
378 "Search results for “%1” in %2",
379 m_searchTerm,
380 prettySearchLocation);
381 }
382 Q_ASSERT(m_searchThrough == SearchThrough::FileContents);
383 return i18nc("@title of a search results page. %1 is the search term a user entered, %2 is a folder name",
384 "Files containing “%1” in %2",
385 m_searchTerm,
386 prettySearchLocation);
387 }
388 if (!m_requiredTags.isEmpty()) {
389 if (m_requiredTags.count() == 1) {
390 return i18nc("@title of a search results page. %1 is a tag e.g. 'important'. %2 is a folder name",
391 "Search items tagged “%1” in %2",
392 m_requiredTags.constFirst(),
393 prettySearchLocation);
394 }
395 return i18nc("@title of a search results page. %1 and %2 are tags e.g. 'important'. %3 is a folder name",
396 "Search items tagged “%1” and “%2” in %3",
397 m_requiredTags.constFirst(),
398 m_requiredTags.constLast(),
399 prettySearchLocation);
400 }
401 if (m_fileType != KFileMetaData::Type::Empty) {
402 return i18nc("@title of a search results page for items of a specified type. %1 is a file type e.g. 'Document', 'Folder'. %2 is a folder name",
403 "%1 search results in %2",
404 KFileMetaData::TypeInfo{m_fileType}.displayName(),
405 prettySearchLocation);
406 }
407 // Everything else failed so we use a very generic title.
408 return i18nc("@title of a search results page with items matching pre-defined conditions. %1 is a folder name",
409 "Search results in %1",
410 prettySearchLocation);
411 }
412
413 Q_ASSERT(m_searchLocations == SearchLocations::Everywhere);
414 // A great title clearly identifies a search results page among many tabs, windows, or links in the Places panel.
415 // Using the search term is phenomenal for this because the user has typed this very specific string with a clear intention, so we definitely want to reuse
416 // the search term in the title if possible.
417 if (!m_searchTerm.isEmpty()) {
418 if (m_searchThrough == SearchThrough::FileNames) {
419 return i18nc("@title of a search results page. %1 is the search term a user entered", "Search results for “%1”", m_searchTerm);
420 }
421 Q_ASSERT(m_searchThrough == SearchThrough::FileContents);
422 return i18nc("@title of a search results page. %1 is the search term a user entered", "Files containing “%1”", m_searchTerm);
423 }
424 if (!m_requiredTags.isEmpty()) {
425 if (m_requiredTags.count() == 1) {
426 return i18nc("@title of a search results page. %1 is a tag e.g. 'important'", "Search items tagged “%1”", m_requiredTags.constFirst());
427 }
428 return i18nc("@title of a search results page. %1 and %2 are tags e.g. 'important'",
429 "Search items tagged “%1” and “%2”",
430 m_requiredTags.constFirst(),
431 m_requiredTags.constLast());
432 }
433 if (m_fileType != KFileMetaData::Type::Empty) {
434 // i18n: Results page for items of a specified type. %1 is a file type e.g. 'Audio', 'Document', 'Folder', 'Archive'. 'Presentation'.
435 // If putting such a file type at the start does not work in your language in this context, you might want to translate this liberally with
436 // something along the lines of 'Search items of type “%1”'.
437 return i18nc("@title of a search. %1 is file type", "%1 search results", KFileMetaData::TypeInfo{m_fileType}.displayName());
438 }
439 // Everything else failed so we use a very generic title.
440 return i18nc("@title of a search results page with items matching pre-defined conditions", "Search results");
441 }
442
443 /** For testing Baloo in DolphinQueryTest even if it is not indexing any locations yet. */
444 void Search::setTestMode()
445 {
446 g_testMode = true;
447 };