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