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>
21 #include <QRegularExpression>
24 using namespace Search
;
26 bool Search::isSupportedSearchScheme(const QString
&urlScheme
)
28 static const QStringList supportedSchemes
= {
29 QStringLiteral("filenamesearch"),
30 QStringLiteral("baloosearch"),
31 QStringLiteral("tags"),
34 return supportedSchemes
.contains(urlScheme
);
37 bool g_testMode
= false;
39 bool Search::isIndexingEnabledIn(QUrl directory
)
42 return true; // For unit-testing, let's pretend everything is indexed correctly.
46 const Baloo::IndexerConfig searchInfo
;
47 return searchInfo
.fileIndexingEnabled() && !directory
.isEmpty() && searchInfo
.shouldBeIndexed(directory
.toLocalFile());
54 bool Search::isContentIndexingEnabled()
57 return true; // For unit-testing, let's pretend everything is indexed correctly.
61 return !Baloo::IndexerConfig
{}.onlyBasicIndexing();
69 /** The path to be passed so Baloo searches everywhere. */
70 constexpr auto balooSearchEverywherePath
= QLatin1String("");
71 /** The path to be passed so Filenamesearch searches everywhere. */
72 constexpr auto filenamesearchEverywherePath
= QLatin1String("file:///");
75 /** Checks if a given term in the Baloo::Query::searchString() is a special search term
76 * @return: the specific search token of the term, or an empty QString() if none is found
78 QString
searchTermToken(const QString
&term
)
80 static const QLatin1String searchTokens
[]{QLatin1String("filename:"),
81 QLatin1String("modified>="),
82 QLatin1String("modified>"),
83 QLatin1String("rating>="),
84 QLatin1String("tag:"),
85 QLatin1String("tag=")};
87 for (const auto &searchToken
: searchTokens
) {
88 if (term
.startsWith(searchToken
)) {
95 QString
stripQuotes(const QString
&text
)
97 if (text
.length() >= 2 && text
.at(0) == QLatin1Char('"') && text
.back() == QLatin1Char('"')) {
98 return text
.mid(1, text
.size() - 2);
103 QStringList
splitOutsideQuotes(const QString
&text
)
105 // Match groups on 3 possible conditions:
106 // - Groups with two leading quotes must close both on them (filename:""abc xyz" tuv")
107 // - Groups enclosed in quotes
108 // - Words separated by spaces
109 static const QRegularExpression
subTermsRegExp("(\\S*?\"\"[^\"]+\"[^\"]+\"+|\\S*?\"[^\"]+\"+|(?<=\\s|^)\\S+(?=\\s|$))");
110 auto subTermsMatchIterator
= subTermsRegExp
.globalMatch(text
);
112 QStringList textParts
;
113 while (subTermsMatchIterator
.hasNext()) {
114 textParts
<< subTermsMatchIterator
.next().captured(0);
120 QString
trimChar(const QString
&text
, const QLatin1Char aChar
)
122 const int start
= text
.startsWith(aChar
) ? 1 : 0;
123 const int end
= (text
.length() > 1 && text
.endsWith(aChar
)) ? 1 : 0;
125 return text
.mid(start
, text
.length() - start
- end
);
129 Search::DolphinQuery::DolphinQuery(const QUrl
&url
, const QUrl
&backupSearchPath
)
131 if (url
.scheme() == QLatin1String("filenamesearch")) {
132 m_searchTool
= SearchTool::Filenamesearch
;
133 const QUrlQuery
query(url
);
134 const QString filenamesearchSearchPathString
= query
.queryItemValue(QStringLiteral("url"), QUrl::FullyDecoded
);
135 const QUrl filenamesearchSearchPathUrl
= QUrl::fromUserInput(filenamesearchSearchPathString
, QString(), QUrl::AssumeLocalFile
);
136 if (!filenamesearchSearchPathUrl
.isValid() || filenamesearchSearchPathString
== filenamesearchEverywherePath
) {
137 // The parsed search location is either invalid or matches a string that represents searching "everywhere".
138 m_searchLocations
= SearchLocations::Everywhere
;
139 m_searchPath
= backupSearchPath
;
141 m_searchLocations
= SearchLocations::FromHere
;
142 m_searchPath
= filenamesearchSearchPathUrl
;
144 m_searchTerm
= query
.queryItemValue(QStringLiteral("search"), QUrl::FullyDecoded
);
145 m_searchThrough
= query
.queryItemValue(QStringLiteral("checkContent")) == QLatin1String("yes") ? SearchThrough::FileContents
: SearchThrough::FileNames
;
150 if (url
.scheme() == QLatin1String("baloosearch")) {
151 m_searchTool
= SearchTool::Baloo
;
152 initializeFromBalooQuery(Baloo::Query::fromSearchUrl(url
), backupSearchPath
);
157 if (url
.scheme() == QLatin1String("tags")) {
159 m_searchTool
= SearchTool::Baloo
;
161 m_searchLocations
= SearchLocations::Everywhere
;
162 m_searchPath
= backupSearchPath
;
163 // tags can contain # symbols or slashes within the Url
164 const auto tag
= trimChar(url
.toString(QUrl::RemoveScheme
), QLatin1Char('/'));
165 if (!tag
.isEmpty()) {
166 m_requiredTags
.append(trimChar(url
.toString(QUrl::RemoveScheme
), QLatin1Char('/')));
172 switchToPreferredSearchTool();
175 QUrl
DolphinQuery::toUrl() const
177 // 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
178 // 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
179 // setter methods which caused this impossible-to-fulfill combination of conditions.
180 Q_ASSERT_X(m_searchLocations
== SearchLocations::Everywhere
|| m_searchPath
.isValid(),
181 "DolphinQuery::toUrl()",
182 "We are supposed to search in a specific location but we do not know where!");
184 Q_ASSERT_X(m_searchLocations
== SearchLocations::Everywhere
|| m_searchTool
!= SearchTool::Baloo
|| isIndexingEnabledIn(m_searchPath
),
185 "DolphinQuery::toUrl()",
186 "We are asking Baloo to search in a location which Baloo is not supposed to have indexed!");
192 /// Create Baloo search URL
193 if (m_searchTool
== SearchTool::Baloo
) {
195 if (m_fileType
!= KFileMetaData::Type::Empty
) {
196 query
.addType(KFileMetaData::TypeInfo
{m_fileType
}.name());
199 QStringList balooQueryStrings
= m_unrecognizedBalooQueryStrings
;
201 if (m_searchThrough
== SearchThrough::FileContents
) {
202 balooQueryStrings
<< m_searchTerm
;
203 } else if (!m_searchTerm
.isEmpty()) {
204 balooQueryStrings
<< QStringLiteral("filename:\"%1\"").arg(m_searchTerm
);
207 if (m_searchLocations
== SearchLocations::FromHere
) {
208 query
.setIncludeFolder(m_searchPath
.toLocalFile());
211 if (m_modifiedSinceDate
.isValid()) {
212 balooQueryStrings
<< QStringLiteral("modified>=%1").arg(m_modifiedSinceDate
.toString(Qt::ISODate
));
215 if (m_minimumRating
>= 1) {
216 balooQueryStrings
<< QStringLiteral("rating>=%1").arg(m_minimumRating
);
219 for (const auto &tag
: m_requiredTags
) {
220 if (tag
.contains(QLatin1Char(' '))) {
221 balooQueryStrings
<< QStringLiteral("tag:\"%1\"").arg(tag
);
223 balooQueryStrings
<< QStringLiteral("tag:%1").arg(tag
);
227 query
.setSearchString(balooQueryStrings
.join(QLatin1Char(' ')));
229 return query
.toSearchUrl(QUrl::toPercentEncoding(title()));
233 /// Create Filenamsearch search URL
234 url
.setScheme(QStringLiteral("filenamesearch"));
237 qUrlQuery
.addQueryItem(QStringLiteral("search"), QUrl::toPercentEncoding(m_searchTerm
));
238 if (m_searchThrough
== SearchThrough::FileContents
) {
239 qUrlQuery
.addQueryItem(QStringLiteral("checkContent"), QStringLiteral("yes"));
242 if (m_searchLocations
== SearchLocations::FromHere
&& m_searchPath
.isValid()) {
243 qUrlQuery
.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(m_searchPath
.url()));
245 // Search in root which is considered searching "everywhere".
246 qUrlQuery
.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(filenamesearchEverywherePath
));
248 qUrlQuery
.addQueryItem(QStringLiteral("title"), QUrl::toPercentEncoding(title()));
250 url
.setQuery(qUrlQuery
);
254 void DolphinQuery::setSearchLocations(SearchLocations searchLocations
)
256 m_searchLocations
= searchLocations
;
257 switchToPreferredSearchTool();
260 void DolphinQuery::setSearchPath(const QUrl
&searchPath
)
262 m_searchPath
= searchPath
;
263 switchToPreferredSearchTool();
266 void DolphinQuery::setSearchThrough(SearchThrough searchThrough
)
268 m_searchThrough
= searchThrough
;
269 switchToPreferredSearchTool();
272 void DolphinQuery::switchToPreferredSearchTool()
274 const bool isIndexingEnabledInCurrentSearchLocation
= m_searchLocations
== SearchLocations::Everywhere
|| isIndexingEnabledIn(m_searchPath
);
275 const bool searchThroughFileContentsWithoutIndexing
= m_searchThrough
== SearchThrough::FileContents
&& !isContentIndexingEnabled();
276 if (!isIndexingEnabledInCurrentSearchLocation
|| searchThroughFileContentsWithoutIndexing
) {
277 m_searchTool
= SearchTool::Filenamesearch
;
281 // The current search location allows searching with Baloo. We switch to Baloo if this is the saved user preference.
282 if (SearchSettings::searchTool() == QStringLiteral("Baloo")) {
283 m_searchTool
= SearchTool::Baloo
;
289 void DolphinQuery::initializeFromBalooQuery(const Baloo::Query
&balooQuery
, const QUrl
&backupSearchPath
)
291 const QString balooSearchPathString
= balooQuery
.includeFolder();
292 const QUrl balooSearchPathUrl
= QUrl::fromUserInput(balooSearchPathString
, QString(), QUrl::AssumeLocalFile
);
293 if (!balooSearchPathUrl
.isValid() || balooSearchPathString
== balooSearchEverywherePath
) {
294 // The parsed search location is either invalid or matches a string that represents searching "everywhere" i.e. in all indexed locations.
295 m_searchLocations
= SearchLocations::Everywhere
;
296 m_searchPath
= backupSearchPath
;
298 m_searchLocations
= SearchLocations::FromHere
;
299 m_searchPath
= balooSearchPathUrl
;
302 const QStringList types
= balooQuery
.types();
303 // We currently only allow searching for one file type at once. (Searching for more seems out of scope for Dolphin anyway IMO.)
304 m_fileType
= types
.isEmpty() ? KFileMetaData::Type::Empty
: KFileMetaData::TypeInfo::fromName(types
.first()).type();
306 /// If nothing is requested, we use the default.
307 std::optional
<SearchThrough
> requestedToSearchThrough
;
308 const QStringList subTerms
= splitOutsideQuotes(balooQuery
.searchString());
309 for (const QString
&subTerm
: subTerms
) {
310 const QString token
= searchTermToken(subTerm
);
311 const QString value
= stripQuotes(subTerm
.mid(token
.length()));
313 if (token
== QLatin1String("filename:")) {
314 // This query is meant to not search in file contents.
315 if (!value
.isEmpty()) {
316 if (m_searchTerm
.isEmpty()) { // Seems like we already received a search term for the content search. We don't overwrite it because the Dolphin
317 // UI does not support searching for differing strings in content and file name.
318 m_searchTerm
= value
;
320 if (!requestedToSearchThrough
.has_value()) { // If requested to search thorugh contents, searching file names is already implied.
321 requestedToSearchThrough
= SearchThrough::FileNames
;
325 } else if (token
.startsWith(QLatin1String("modified>="))) {
326 m_modifiedSinceDate
= QDate::fromString(value
, Qt::ISODate
);
328 } else if (token
.startsWith(QLatin1String("modified>"))) {
329 m_modifiedSinceDate
= QDate::fromString(value
, Qt::ISODate
).addDays(1);
331 } else if (token
.startsWith(QLatin1String("rating>="))) {
332 m_minimumRating
= value
.toInt();
334 } else if (token
.startsWith(QLatin1String("tag"))) {
335 m_requiredTags
.append(value
);
337 } else if (!token
.isEmpty()) {
338 m_unrecognizedBalooQueryStrings
<< token
+ value
;
340 } else if (subTerm
== QLatin1String("AND") && subTerm
!= subTerms
.at(0) && subTerm
!= subTerms
.back()) {
342 } else if (!value
.isEmpty()) {
343 // An empty token means this is just blank text, which is where the generic search term is located.
344 if (!m_searchTerm
.isEmpty()) {
345 // Multiple search terms are separated by spaces.
346 m_searchTerm
.append(QLatin1Char
{' '});
348 m_searchTerm
.append(value
);
349 requestedToSearchThrough
= SearchThrough::FileContents
;
352 if (requestedToSearchThrough
.has_value()) {
353 m_searchThrough
= requestedToSearchThrough
.value();
358 QString
DolphinQuery::title() const
360 if (m_searchLocations
== SearchLocations::FromHere
) {
361 QString prettySearchLocation
;
362 KFilePlacesModel
*placesModel
= DolphinPlacesModelSingleton::instance().placesModel();
363 QModelIndex url_index
= placesModel
->closestItem(m_searchPath
);
364 if (url_index
.isValid() && placesModel
->url(url_index
).matches(m_searchPath
, QUrl::StripTrailingSlash
)) {
365 prettySearchLocation
= placesModel
->text(url_index
);
367 prettySearchLocation
= m_searchPath
.fileName();
369 if (prettySearchLocation
.isEmpty()) {
370 if (m_searchPath
.isLocalFile()) {
371 prettySearchLocation
= KShell::tildeCollapse(m_searchPath
.adjusted(QUrl::StripTrailingSlash
).toLocalFile());
373 prettySearchLocation
= m_searchPath
.toString(QUrl::RemoveAuthority
| QUrl::StripTrailingSlash
);
377 // A great title clearly identifies a search results page among many tabs, windows, or links in the Places panel.
378 // 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
379 // reuse the search term in the title if possible.
380 if (!m_searchTerm
.isEmpty()) {
381 if (m_searchThrough
== SearchThrough::FileNames
) {
382 return i18nc("@title of a search results page. %1 is the search term a user entered, %2 is a folder name",
383 "Search results for “%1” in %2",
385 prettySearchLocation
);
387 Q_ASSERT(m_searchThrough
== SearchThrough::FileContents
);
388 return i18nc("@title of a search results page. %1 is the search term a user entered, %2 is a folder name",
389 "Files containing “%1” in %2",
391 prettySearchLocation
);
393 if (!m_requiredTags
.isEmpty()) {
394 if (m_requiredTags
.count() == 1) {
395 return i18nc("@title of a search results page. %1 is a tag e.g. 'important'. %2 is a folder name",
396 "Search items tagged “%1” in %2",
397 m_requiredTags
.constFirst(),
398 prettySearchLocation
);
400 return i18nc("@title of a search results page. %1 and %2 are tags e.g. 'important'. %3 is a folder name",
401 "Search items tagged “%1” and “%2” in %3",
402 m_requiredTags
.constFirst(),
403 m_requiredTags
.constLast(),
404 prettySearchLocation
);
406 if (m_fileType
!= KFileMetaData::Type::Empty
) {
407 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",
408 "%1 search results in %2",
409 KFileMetaData::TypeInfo
{m_fileType
}.displayName(),
410 prettySearchLocation
);
412 // Everything else failed so we use a very generic title.
413 return i18nc("@title of a search results page with items matching pre-defined conditions. %1 is a folder name",
414 "Search results in %1",
415 prettySearchLocation
);
418 Q_ASSERT(m_searchLocations
== SearchLocations::Everywhere
);
419 // A great title clearly identifies a search results page among many tabs, windows, or links in the Places panel.
420 // 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
421 // the search term in the title if possible.
422 if (!m_searchTerm
.isEmpty()) {
423 if (m_searchThrough
== SearchThrough::FileNames
) {
424 return i18nc("@title of a search results page. %1 is the search term a user entered", "Search results for “%1”", m_searchTerm
);
426 Q_ASSERT(m_searchThrough
== SearchThrough::FileContents
);
427 return i18nc("@title of a search results page. %1 is the search term a user entered", "Files containing “%1”", m_searchTerm
);
429 if (!m_requiredTags
.isEmpty()) {
430 if (m_requiredTags
.count() == 1) {
431 return i18nc("@title of a search results page. %1 is a tag e.g. 'important'", "Search items tagged “%1”", m_requiredTags
.constFirst());
433 return i18nc("@title of a search results page. %1 and %2 are tags e.g. 'important'",
434 "Search items tagged “%1” and “%2”",
435 m_requiredTags
.constFirst(),
436 m_requiredTags
.constLast());
438 if (m_fileType
!= KFileMetaData::Type::Empty
) {
439 // i18n: Results page for items of a specified type. %1 is a file type e.g. 'Audio', 'Document', 'Folder', 'Archive'. 'Presentation'.
440 // 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
441 // something along the lines of 'Search items of type “%1”'.
442 return i18nc("@title of a search. %1 is file type", "%1 search results", KFileMetaData::TypeInfo
{m_fileType
}.displayName());
444 // Everything else failed so we use a very generic title.
445 return i18nc("@title of a search results page with items matching pre-defined conditions", "Search results");
448 /** For testing Baloo in DolphinQueryTest even if it is not indexing any locations yet. */
449 void Search::setTestMode()