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 | |
26 | namespace KRunner |
27 | { |
28 | KRUNNER_EXPORT int __changeCountBeforeSaving = 5; // For tests |
29 | class RunnerContextPrivate : public QSharedData |
30 | { |
31 | public: |
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 | |
97 | RunnerContext::RunnerContext(RunnerManager *manager) |
98 | : d(new RunnerContextPrivate(manager)) |
99 | { |
100 | } |
101 | |
102 | // copy ctor |
103 | RunnerContext::RunnerContext(const RunnerContext &other) |
104 | { |
105 | QReadLocker locker(&other.d->lock); |
106 | d = other.d; |
107 | } |
108 | |
109 | RunnerContext::~RunnerContext() |
110 | { |
111 | } |
112 | |
113 | RunnerContext &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 | */ |
133 | void 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 | |
162 | void 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 | |
176 | QString 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 | |
184 | bool RunnerContext::isValid() const |
185 | { |
186 | QReadLocker locker(&d->lock); |
187 | return d->m_isValid; |
188 | } |
189 | |
190 | bool 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 | |
213 | bool RunnerContext::addMatch(const QueryMatch &match) |
214 | { |
215 | return addMatches(matches: {match}); |
216 | } |
217 | |
218 | QList<QueryMatch> RunnerContext::matches() const |
219 | { |
220 | QReadLocker locker(&d->lock); |
221 | QList<QueryMatch> matches = d->matches; |
222 | return matches; |
223 | } |
224 | |
225 | void RunnerContext::requestQueryStringUpdate(const QString &text, int cursorPosition) const |
226 | { |
227 | d->requestedText = text; |
228 | d->requestedCursorPosition = cursorPosition; |
229 | } |
230 | |
231 | void RunnerContext::setSingleRunnerQueryMode(bool enabled) |
232 | { |
233 | d->singleRunnerQueryMode = enabled; |
234 | } |
235 | |
236 | bool RunnerContext::singleRunnerQueryMode() const |
237 | { |
238 | return d->singleRunnerQueryMode; |
239 | } |
240 | |
241 | void RunnerContext::ignoreCurrentMatchForHistory() const |
242 | { |
243 | d->shouldIgnoreCurrentMatchForHistory = true; |
244 | } |
245 | |
246 | bool 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 | */ |
261 | void 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 | |
274 | void 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 | |
290 | void RunnerContext::increaseLaunchCount(const QueryMatch &match) |
291 | { |
292 | ++d->launchCounts[match.id()]; |
293 | ++d->changedLaunchCounts; |
294 | } |
295 | |
296 | QString RunnerContext::requestedQueryString() const |
297 | { |
298 | return d->requestedText; |
299 | } |
300 | int RunnerContext::requestedCursorPosition() const |
301 | { |
302 | return d->requestedCursorPosition; |
303 | } |
304 | |
305 | void RunnerContext::setJobStartTs(qint64 queryStartTs) |
306 | { |
307 | d->queryStartTs = queryStartTs; |
308 | } |
309 | QString 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 | |