File indexing completed on 2024-03-24 03:59:37
0001 /* 0002 SPDX-FileCopyrightText: 2006-2007 Aaron Seigo <aseigo@kde.org> 0003 SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "runnercontext.h" 0009 0010 #include <cmath> 0011 0012 #include <QPointer> 0013 #include <QReadWriteLock> 0014 #include <QRegularExpression> 0015 #include <QSharedData> 0016 #include <QUrl> 0017 0018 #include <KConfigGroup> 0019 #include <KShell> 0020 0021 #include "abstractrunner.h" 0022 #include "abstractrunner_p.h" 0023 #include "querymatch.h" 0024 #include "runnermanager.h" 0025 0026 namespace KRunner 0027 { 0028 KRUNNER_EXPORT int __changeCountBeforeSaving = 5; // For tests 0029 class RunnerContextPrivate : public QSharedData 0030 { 0031 public: 0032 explicit RunnerContextPrivate(RunnerManager *manager) 0033 : QSharedData() 0034 , m_manager(manager) 0035 { 0036 } 0037 0038 explicit RunnerContextPrivate(const RunnerContextPrivate &p) 0039 : QSharedData() 0040 , m_manager(p.m_manager) 0041 , launchCounts(p.launchCounts) 0042 { 0043 } 0044 0045 ~RunnerContextPrivate() 0046 { 0047 } 0048 0049 void invalidate() 0050 { 0051 m_isValid = false; 0052 } 0053 0054 void addMatch(const QueryMatch &match) 0055 { 0056 if (match.runner() && match.runner()->d->hasUniqueResults) { 0057 if (uniqueIds.contains(match.id())) { 0058 const QueryMatch &existentMatch = uniqueIds.value(match.id()); 0059 if (existentMatch.runner() && existentMatch.runner()->d->hasWeakResults) { 0060 // There is an existing match with the same ID and we are allowed to replace it 0061 matches.removeOne(existentMatch); 0062 matches.append(match); 0063 } 0064 } else { 0065 // There is no existing match with the same id 0066 uniqueIds.insert(match.id(), match); 0067 matches.append(match); 0068 } 0069 } else { 0070 // Runner has the unique results property not set 0071 matches.append(match); 0072 } 0073 } 0074 0075 void matchesChanged() 0076 { 0077 if (m_manager) { 0078 QMetaObject::invokeMethod(m_manager, "onMatchesChanged"); 0079 } 0080 } 0081 0082 QReadWriteLock lock; 0083 QPointer<RunnerManager> m_manager; 0084 bool m_isValid = true; 0085 QList<QueryMatch> matches; 0086 QHash<QString, int> launchCounts; 0087 int changedLaunchCounts = 0; // We want to sync them while the app is running, but for each query it is overkill 0088 QString term; 0089 bool singleRunnerQueryMode = false; 0090 bool shouldIgnoreCurrentMatchForHistory = false; 0091 QMap<QString, QueryMatch> uniqueIds; 0092 QString requestedText; 0093 int requestedCursorPosition = 0; 0094 qint64 queryStartTs = 0; 0095 }; 0096 0097 RunnerContext::RunnerContext(RunnerManager *manager) 0098 : d(new RunnerContextPrivate(manager)) 0099 { 0100 } 0101 0102 // copy ctor 0103 RunnerContext::RunnerContext(const RunnerContext &other) 0104 { 0105 QReadLocker locker(&other.d->lock); 0106 d = other.d; 0107 } 0108 0109 RunnerContext::~RunnerContext() 0110 { 0111 } 0112 0113 RunnerContext &RunnerContext::operator=(const RunnerContext &other) 0114 { 0115 if (this->d == other.d) { 0116 return *this; 0117 } 0118 0119 auto oldD = d; // To avoid the old ptr getting destroyed while the mutex is locked 0120 QWriteLocker locker(&d->lock); 0121 QReadLocker otherLocker(&other.d->lock); 0122 d = other.d; 0123 return *this; 0124 } 0125 0126 /** 0127 * Resets the search term for this object. 0128 * This removes all current matches in the process and 0129 * turns off single runner query mode. 0130 * Copies of this object that are used by runner are invalidated 0131 * and adding matches will be a noop. 0132 */ 0133 void RunnerContext::reset() 0134 { 0135 { 0136 QWriteLocker locker(&d->lock); 0137 // We will detach if we are a copy of someone. But we will reset 0138 // if we are the 'main' context others copied from. Resetting 0139 // one RunnerContext makes all the copies obsolete. 0140 0141 // We need to mark the q pointer of the detached RunnerContextPrivate 0142 // as dirty on detach to avoid receiving results for old queries 0143 d->invalidate(); 0144 } 0145 0146 d.detach(); 0147 // But out detached version is valid! 0148 d->m_isValid = true; 0149 0150 // we still have to remove all the matches, since if the 0151 // ref count was 1 (e.g. only the RunnerContext is using 0152 // the dptr) then we won't get a copy made 0153 d->matches.clear(); 0154 d->term.clear(); 0155 d->matchesChanged(); 0156 0157 d->uniqueIds.clear(); 0158 d->singleRunnerQueryMode = false; 0159 d->shouldIgnoreCurrentMatchForHistory = false; 0160 } 0161 0162 void RunnerContext::setQuery(const QString &term) 0163 { 0164 if (!this->query().isEmpty()) { 0165 reset(); 0166 } 0167 0168 if (term.isEmpty()) { 0169 return; 0170 } 0171 0172 d->requestedText.clear(); // Invalidate this field whenever the query changes 0173 d->term = term; 0174 } 0175 0176 QString RunnerContext::query() const 0177 { 0178 // the query term should never be set after 0179 // a search starts. in fact, reset() ensures this 0180 // and setQuery(QString) calls reset() 0181 return d->term; 0182 } 0183 0184 bool RunnerContext::isValid() const 0185 { 0186 QReadLocker locker(&d->lock); 0187 return d->m_isValid; 0188 } 0189 0190 bool RunnerContext::addMatches(const QList<QueryMatch> &matches) 0191 { 0192 if (matches.isEmpty() || !isValid()) { 0193 // Bail out if the query is empty or the qptr is dirty 0194 return false; 0195 } 0196 0197 { 0198 QWriteLocker locker(&d->lock); 0199 for (QueryMatch match : matches) { 0200 // Give previously launched matches a slight boost in relevance 0201 // The boost smoothly saturates to 0.5; 0202 if (int count = d->launchCounts.value(match.id())) { 0203 match.setRelevance(match.relevance() + 0.5 * (1 - exp(-count * 0.3))); 0204 } 0205 d->addMatch(match); 0206 } 0207 } 0208 d->matchesChanged(); 0209 0210 return true; 0211 } 0212 0213 bool RunnerContext::addMatch(const QueryMatch &match) 0214 { 0215 return addMatches({match}); 0216 } 0217 0218 QList<QueryMatch> RunnerContext::matches() const 0219 { 0220 QReadLocker locker(&d->lock); 0221 QList<QueryMatch> matches = d->matches; 0222 return matches; 0223 } 0224 0225 void RunnerContext::requestQueryStringUpdate(const QString &text, int cursorPosition) const 0226 { 0227 d->requestedText = text; 0228 d->requestedCursorPosition = cursorPosition; 0229 } 0230 0231 void RunnerContext::setSingleRunnerQueryMode(bool enabled) 0232 { 0233 d->singleRunnerQueryMode = enabled; 0234 } 0235 0236 bool RunnerContext::singleRunnerQueryMode() const 0237 { 0238 return d->singleRunnerQueryMode; 0239 } 0240 0241 void RunnerContext::ignoreCurrentMatchForHistory() const 0242 { 0243 d->shouldIgnoreCurrentMatchForHistory = true; 0244 } 0245 0246 bool RunnerContext::shouldIgnoreCurrentMatchForHistory() const 0247 { 0248 return d->shouldIgnoreCurrentMatchForHistory; 0249 } 0250 0251 /** 0252 * Sets the launch counts for the associated match ids 0253 * 0254 * If a runner adds a match to this context, the context will check if the 0255 * match id has been launched before and increase the matches relevance 0256 * correspondingly. In this manner, any front end can implement adaptive search 0257 * by sorting items according to relevance. 0258 * 0259 * @param config the config group where launch data was stored 0260 */ 0261 void RunnerContext::restore(const KConfigGroup &config) 0262 { 0263 const QStringList cfgList = config.readEntry("LaunchCounts", QStringList()); 0264 0265 for (const QString &entry : cfgList) { 0266 if (int idx = entry.indexOf(QLatin1Char(' ')); idx != -1) { 0267 const int count = entry.mid(0, idx).toInt(); 0268 const QString id = entry.mid(idx + 1); 0269 d->launchCounts[id] = count; 0270 } 0271 } 0272 } 0273 0274 void RunnerContext::save(KConfigGroup &config) 0275 { 0276 if (d->changedLaunchCounts < __changeCountBeforeSaving) { 0277 return; 0278 } 0279 d->changedLaunchCounts = 0; 0280 QStringList countList; 0281 countList.reserve(d->launchCounts.size()); 0282 for (auto it = d->launchCounts.cbegin(), end = d->launchCounts.cend(); it != end; ++it) { 0283 countList << QString::number(it.value()) + QLatin1Char(' ') + it.key(); 0284 } 0285 0286 config.writeEntry("LaunchCounts", countList); 0287 config.sync(); 0288 } 0289 0290 void RunnerContext::increaseLaunchCount(const QueryMatch &match) 0291 { 0292 ++d->launchCounts[match.id()]; 0293 ++d->changedLaunchCounts; 0294 } 0295 0296 QString RunnerContext::requestedQueryString() const 0297 { 0298 return d->requestedText; 0299 } 0300 int RunnerContext::requestedCursorPosition() const 0301 { 0302 return d->requestedCursorPosition; 0303 } 0304 0305 void RunnerContext::setJobStartTs(qint64 queryStartTs) 0306 { 0307 d->queryStartTs = queryStartTs; 0308 } 0309 QString RunnerContext::runnerJobId(AbstractRunner *runner) const 0310 { 0311 return QLatin1String("%1-%2-%3").arg(runner->id(), query(), QString::number(d->queryStartTs)); 0312 } 0313 0314 } // KRunner namespace