File indexing completed on 2024-04-28 07:45:39

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