1/*
2 SPDX-FileCopyrightText: 2006-2007 Aaron Seigo <aseigo@kde.org>
3 SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "runnercontext.h"
9
10#include <cmath>
11
12#include <QPointer>
13#include <QReadWriteLock>
14#include <QRegularExpression>
15#include <QSharedData>
16#include <QUrl>
17
18#include <KConfigGroup>
19#include <KShell>
20
21#include "abstractrunner.h"
22#include "abstractrunner_p.h"
23#include "krunner_debug.h"
24#include "querymatch.h"
25#include "runnermanager.h"
26
27namespace KRunner
28{
29KRUNNER_EXPORT int __changeCountBeforeSaving = 5; // For tests
30class RunnerContextPrivate : public QSharedData
31{
32public:
33 explicit RunnerContextPrivate(RunnerManager *manager)
34 : QSharedData()
35 , m_manager(manager)
36 {
37 }
38
39 explicit RunnerContextPrivate(const RunnerContextPrivate &p)
40 : QSharedData()
41 , m_manager(p.m_manager)
42 , launchCounts(p.launchCounts)
43 {
44 }
45
46 ~RunnerContextPrivate()
47 {
48 }
49
50 void invalidate()
51 {
52 m_isValid = false;
53 }
54
55 void addMatch(const QueryMatch &match)
56 {
57 if (match.runner() && match.runner()->d->hasUniqueResults) {
58 if (uniqueIds.contains(match.id())) {
59 const QueryMatch &existentMatch = uniqueIds.value(match.id());
60 if (existentMatch.runner() && existentMatch.runner()->d->hasWeakResults) {
61 // There is an existing match with the same ID and we are allowed to replace it
62 matches.removeOne(existentMatch);
63 matches.append(match);
64 }
65 } else {
66 // There is no existing match with the same id
67 uniqueIds.insert(match.id(), match);
68 matches.append(match);
69 }
70 } else {
71 // Runner has the unique results property not set
72 matches.append(match);
73 }
74 }
75
76 void matchesChanged()
77 {
78 if (m_manager) {
79 QMetaObject::invokeMethod(m_manager, "onMatchesChanged");
80 }
81 }
82
83 QReadWriteLock lock;
84 QPointer<RunnerManager> m_manager;
85 bool m_isValid = true;
86 QList<QueryMatch> matches;
87 QHash<QString, int> launchCounts;
88 int changedLaunchCounts = 0; // We want to sync them while the app is running, but for each query it is overkill
89 QString term;
90 bool singleRunnerQueryMode = false;
91 bool shouldIgnoreCurrentMatchForHistory = false;
92 QMap<QString, QueryMatch> uniqueIds;
93 QString requestedText;
94 int requestedCursorPosition = 0;
95 qint64 queryStartTs = 0;
96};
97
98RunnerContext::RunnerContext(RunnerManager *manager)
99 : d(new RunnerContextPrivate(manager))
100{
101}
102
103// copy ctor
104RunnerContext::RunnerContext(const RunnerContext &other)
105{
106 QReadLocker locker(&other.d->lock);
107 d = other.d;
108}
109
110RunnerContext::~RunnerContext()
111{
112}
113
114RunnerContext &RunnerContext::operator=(const RunnerContext &other)
115{
116 if (this->d == other.d) {
117 return *this;
118 }
119
120 auto oldD = d; // To avoid the old ptr getting destroyed while the mutex is locked
121 QWriteLocker locker(&d->lock);
122 QReadLocker otherLocker(&other.d->lock);
123 d = other.d;
124 return *this;
125}
126
127/**
128 * Resets the search term for this object.
129 * This removes all current matches in the process and
130 * turns off single runner query mode.
131 * Copies of this object that are used by runner are invalidated
132 * and adding matches will be a noop.
133 */
134void RunnerContext::reset()
135{
136 {
137 QWriteLocker locker(&d->lock);
138 // We will detach if we are a copy of someone. But we will reset
139 // if we are the 'main' context others copied from. Resetting
140 // one RunnerContext makes all the copies obsolete.
141
142 // We need to mark the q pointer of the detached RunnerContextPrivate
143 // as dirty on detach to avoid receiving results for old queries
144 d->invalidate();
145 }
146
147 d.detach();
148 // But out detached version is valid!
149 d->m_isValid = true;
150
151 // we still have to remove all the matches, since if the
152 // ref count was 1 (e.g. only the RunnerContext is using
153 // the dptr) then we won't get a copy made
154 d->matches.clear();
155 d->term.clear();
156 d->matchesChanged();
157
158 d->uniqueIds.clear();
159 d->singleRunnerQueryMode = false;
160 d->shouldIgnoreCurrentMatchForHistory = false;
161}
162
163void RunnerContext::setQuery(const QString &term)
164{
165 if (!this->query().isEmpty()) {
166 reset();
167 }
168
169 if (term.isEmpty()) {
170 return;
171 }
172
173 d->requestedText.clear(); // Invalidate this field whenever the query changes
174 d->term = term;
175}
176
177QString RunnerContext::query() const
178{
179 // the query term should never be set after
180 // a search starts. in fact, reset() ensures this
181 // and setQuery(QString) calls reset()
182 return d->term;
183}
184
185bool RunnerContext::isValid() const
186{
187 QReadLocker locker(&d->lock);
188 return d->m_isValid;
189}
190
191bool RunnerContext::addMatches(const QList<QueryMatch> &matches)
192{
193 if (matches.isEmpty() || !isValid()) {
194 // Bail out if the query is empty or the qptr is dirty
195 return false;
196 }
197
198 {
199 QWriteLocker locker(&d->lock);
200 for (QueryMatch match : matches) {
201 // Give previously launched matches a slight boost in relevance
202 // The boost smoothly saturates to 0.5;
203 if (int count = d->launchCounts.value(match.id())) {
204 match.setRelevance(match.relevance() + 0.5 * (1 - exp(-count * 0.3)));
205 }
206 d->addMatch(match);
207 }
208 }
209 d->matchesChanged();
210
211 return true;
212}
213
214bool RunnerContext::addMatch(const QueryMatch &match)
215{
216 return addMatches({match});
217}
218
219QList<QueryMatch> RunnerContext::matches() const
220{
221 QReadLocker locker(&d->lock);
222 QList<QueryMatch> matches = d->matches;
223 return matches;
224}
225
226void RunnerContext::requestQueryStringUpdate(const QString &text, int cursorPosition) const
227{
228 d->requestedText = text;
229 d->requestedCursorPosition = cursorPosition;
230}
231
232void RunnerContext::setSingleRunnerQueryMode(bool enabled)
233{
234 d->singleRunnerQueryMode = enabled;
235}
236
237bool RunnerContext::singleRunnerQueryMode() const
238{
239 return d->singleRunnerQueryMode;
240}
241
242void RunnerContext::ignoreCurrentMatchForHistory() const
243{
244 d->shouldIgnoreCurrentMatchForHistory = true;
245}
246
247bool RunnerContext::shouldIgnoreCurrentMatchForHistory() const
248{
249 return d->shouldIgnoreCurrentMatchForHistory;
250}
251
252/**
253 * Sets the launch counts for the associated match ids
254 *
255 * If a runner adds a match to this context, the context will check if the
256 * match id has been launched before and increase the matches relevance
257 * correspondingly. In this manner, any front end can implement adaptive search
258 * by sorting items according to relevance.
259 *
260 * @param config the config group where launch data was stored
261 */
262void RunnerContext::restore(const KConfigGroup &config)
263{
264 const QStringList cfgList = config.readEntry("LaunchCounts", QStringList());
265
266 for (const QString &entry : cfgList) {
267 if (int idx = entry.indexOf(QLatin1Char(' ')); idx != -1) {
268 const int count = entry.mid(0, idx).toInt();
269 const QString id = entry.mid(idx + 1);
270 d->launchCounts[id] = count;
271 }
272 }
273}
274
275void RunnerContext::save(KConfigGroup &config)
276{
277 if (d->changedLaunchCounts < __changeCountBeforeSaving) {
278 return;
279 }
280 d->changedLaunchCounts = 0;
281 QStringList countList;
282 countList.reserve(d->launchCounts.size());
283 for (auto it = d->launchCounts.cbegin(), end = d->launchCounts.cend(); it != end; ++it) {
284 countList << QString::number(it.value()) + QLatin1Char(' ') + it.key();
285 }
286
287 config.writeEntry("LaunchCounts", countList);
288 config.sync();
289}
290
291void RunnerContext::increaseLaunchCount(const QueryMatch &match)
292{
293 ++d->launchCounts[match.id()];
294 ++d->changedLaunchCounts;
295}
296
297QString RunnerContext::requestedQueryString() const
298{
299 return d->requestedText;
300}
301int RunnerContext::requestedCursorPosition() const
302{
303 return d->requestedCursorPosition;
304}
305
306void RunnerContext::setJobStartTs(qint64 queryStartTs)
307{
308 d->queryStartTs = queryStartTs;
309}
310QString RunnerContext::runnerJobId(AbstractRunner *runner) const
311{
312 return QLatin1String("%1-%2-%3").arg(runner->id(), query(), QString::number(d->queryStartTs));
313}
314
315} // KRunner namespace
316

source code of krunner/src/runnercontext.cpp