2 SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
3 SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
5 SPDX-License-Identifier: GPL-2.0-or-later
8 #include "dolphinquery.h"
10 #include "config-dolphin.h"
12 #include <Baloo/IndexerConfig>
13 #include <Baloo/Query>
15 #include "dolphinplacesmodelsingleton.h"
17 #include <KFileMetaData/TypeInfo>
18 #include <KLocalizedString>
20 #include <QRegularExpression>
23 using namespace Search
;
25 bool Search::isSupportedSearchScheme(const QString
&urlScheme
)
27 static const QStringList supportedSchemes
= {
28 QStringLiteral("filenamesearch"),
29 QStringLiteral("baloosearch"),
30 QStringLiteral("tags"),
33 return supportedSchemes
.contains(urlScheme
);
36 bool g_testMode
= false;
38 bool Search::isIndexingEnabledIn(QUrl directory
)
41 return true; // For unit-testing, let's pretend everything is indexed correctly.
45 const Baloo::IndexerConfig searchInfo
;
46 return searchInfo
.fileIndexingEnabled() && !directory
.isEmpty() && searchInfo
.shouldBeIndexed(directory
.toLocalFile());
53 bool Search::isContentIndexingEnabled()
56 return true; // For unit-testing, let's pretend everything is indexed correctly.
60 return !Baloo::IndexerConfig
{}.onlyBasicIndexing();
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:///");
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
77 QString
searchTermToken(const QString
&term
)
79 static const QLatin1String searchTokens
[]{QLatin1String("filename:"),
80 QLatin1String("modified>="),
81 QLatin1String("modified>"),
82 QLatin1String("rating>="),
83 QLatin1String("tag:"),
84 QLatin1String("tag=")};
86 for (const auto &searchToken
: searchTokens
) {
87 if (term
.startsWith(searchToken
)) {
94 QString
stripQuotes(const QString
&text
)
96 if (text
.length() >= 2 && text
.at(0) == QLatin1Char('"') && text
.back() == QLatin1Char('"')) {
97 return text
.mid(1, text
.size() - 2);
102 QStringList
splitOutsideQuotes(const QString
&text
)
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
);
111 QStringList textParts
;
112 while (subTermsMatchIterator
.hasNext()) {
113 textParts
<< subTermsMatchIterator
.next().captured(0);
119 QString
trimChar(const QString
&text
, const QLatin1Char aChar
)
121 const int start
= text
.startsWith(aChar
) ? 1 : 0;
122 const int end
= (text
.length() > 1 && text
.endsWith(aChar
)) ? 1 : 0;
124 return text
.mid(start
, text
.length() - start
- end
);
128 Search::DolphinQuery::DolphinQuery(const QUrl
&url
, const QUrl
&backupSearchPath
)
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
;
140 m_searchLocations
= SearchLocations::FromHere
;
141 m_searchPath
= filenamesearchSearchPathUrl
;
143 m_searchTerm
= query
.queryItemValue(QStringLiteral("search"), QUrl::FullyDecoded
);
144 m_searchThrough
= query
.queryItemValue(QStringLiteral("checkContent")) == QLatin1String("yes") ? SearchThrough::FileContents
: SearchThrough::FileNames
;
149 if (url
.scheme() == QLatin1String("baloosearch")) {
150 m_searchTool
= SearchTool::Baloo
;
151 initializeFromBalooQuery(Baloo::Query::fromSearchUrl(url
), backupSearchPath
);
156 if (url
.scheme() == QLatin1String("tags")) {
158 m_searchTool
= SearchTool::Baloo
;
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('/')));
171 switchToPreferredSearchTool();
174 QUrl
DolphinQuery::toUrl() const
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!");
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!");
191 /// Create Baloo search URL
192 if (m_searchTool
== SearchTool::Baloo
) {
194 if (m_fileType
!= KFileMetaData::Type::Empty
) {
195 query
.addType(KFileMetaData::TypeInfo
{m_fileType
}.name());
198 QStringList balooQueryStrings
= m_unrecognizedBalooQueryStrings
;
200 if (m_searchThrough
== SearchThrough::FileContents
) {
201 balooQueryStrings
<< m_searchTerm
;
202 } else if (!m_searchTerm
.isEmpty()) {
203 balooQueryStrings
<< QStringLiteral("filename:\"%1\"").arg(m_searchTerm
);
206 if (m_searchLocations
== SearchLocations::FromHere
) {
207 query
.setIncludeFolder(m_searchPath
.toLocalFile());
210 if (m_modifiedSinceDate
.isValid()) {
211 balooQueryStrings
<< QStringLiteral("modified>=%1").arg(m_modifiedSinceDate
.toString(Qt::ISODate
));
214 if (m_minimumRating
>= 1) {
215 balooQueryStrings
<< QStringLiteral("rating>=%1").arg(m_minimumRating
);
218 for (const auto &tag
: m_requiredTags
) {
219 if (tag
.contains(QLatin1Char(' '))) {
220 balooQueryStrings
<< QStringLiteral("tag:\"%1\"").arg(tag
);
222 balooQueryStrings
<< QStringLiteral("tag:%1").arg(tag
);
226 query
.setSearchString(balooQueryStrings
.join(QLatin1Char(' ')));
228 return query
.toSearchUrl(QUrl::toPercentEncoding(title()));
232 /// Create Filenamsearch search URL
233 url
.setScheme(QStringLiteral("filenamesearch"));
236 qUrlQuery
.addQueryItem(QStringLiteral("search"), QUrl::toPercentEncoding(m_searchTerm
));
237 if (m_searchThrough
== SearchThrough::FileContents
) {
238 qUrlQuery
.addQueryItem(QStringLiteral("checkContent"), QStringLiteral("yes"));
241 if (m_searchLocations
== SearchLocations::FromHere
&& m_searchPath
.isValid()) {
242 qUrlQuery
.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(m_searchPath
.url()));
244 // Search in root which is considered searching "everywhere".
245 qUrlQuery
.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(filenamesearchEverywherePath
));
247 qUrlQuery
.addQueryItem(QStringLiteral("title"), QUrl::toPercentEncoding(title()));
249 url
.setQuery(qUrlQuery
);
253 void DolphinQuery::setSearchLocations(SearchLocations searchLocations
)
255 m_searchLocations
= searchLocations
;
256 switchToPreferredSearchTool();
259 void DolphinQuery::setSearchPath(const QUrl
&searchPath
)
261 m_searchPath
= searchPath
;
262 switchToPreferredSearchTool();
265 void DolphinQuery::setSearchThrough(SearchThrough searchThrough
)
267 m_searchThrough
= searchThrough
;
268 switchToPreferredSearchTool();
271 void DolphinQuery::switchToPreferredSearchTool()
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
;
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
;
288 void DolphinQuery::initializeFromBalooQuery(const Baloo::Query
&balooQuery
, const QUrl
&backupSearchPath
)
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
;
297 m_searchLocations
= SearchLocations::FromHere
;
298 m_searchPath
= balooSearchPathUrl
;
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();
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()));
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
;
319 if (!requestedToSearchThrough
.has_value()) { // If requested to search thorugh contents, searching file names is already implied.
320 requestedToSearchThrough
= SearchThrough::FileNames
;
324 } else if (token
.startsWith(QLatin1String("modified>="))) {
325 m_modifiedSinceDate
= QDate::fromString(value
, Qt::ISODate
);
327 } else if (token
.startsWith(QLatin1String("modified>"))) {
328 m_modifiedSinceDate
= QDate::fromString(value
, Qt::ISODate
).addDays(1);
330 } else if (token
.startsWith(QLatin1String("rating>="))) {
331 m_minimumRating
= value
.toInt();
333 } else if (token
.startsWith(QLatin1String("tag"))) {
334 m_requiredTags
.append(value
);
336 } else if (!token
.isEmpty()) {
337 m_unrecognizedBalooQueryStrings
<< token
+ value
;
339 } else if (subTerm
== QLatin1String("AND") && subTerm
!= subTerms
.at(0) && subTerm
!= subTerms
.back()) {
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
{' '});
347 m_searchTerm
.append(value
);
348 requestedToSearchThrough
= SearchThrough::FileContents
;
351 if (requestedToSearchThrough
.has_value()) {
352 m_searchThrough
= requestedToSearchThrough
.value();
357 QString
DolphinQuery::title() const
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
);
366 prettySearchLocation
= m_searchPath
.fileName();
368 if (prettySearchLocation
.isEmpty()) {
369 prettySearchLocation
= m_searchPath
.toString(QUrl::RemoveAuthority
);
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",
380 prettySearchLocation
);
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",
386 prettySearchLocation
);
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
);
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
);
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
);
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
);
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
);
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
);
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());
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());
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());
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");
443 /** For testing Baloo in DolphinQueryTest even if it is not indexing any locations yet. */
444 void Search::setTestMode()