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

source code of krunner/src/runnercontext.cpp