File indexing completed on 2024-12-01 03:41:19
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2000 David Smith <dsmith@algonet.se> 0004 SPDX-FileCopyrightText: 2004 Scott Wheeler <wheeler@kde.org> 0005 0006 This class was inspired by a previous KUrlCompletion by 0007 SPDX-FileContributor: Henner Zeller <zeller@think.de> 0008 0009 SPDX-License-Identifier: LGPL-2.0-or-later 0010 */ 0011 0012 #include "kurlcompletion.h" 0013 #include "../utils_p.h" 0014 #include <assert.h> 0015 #include <limits.h> 0016 #include <stdlib.h> 0017 0018 #include <QCollator> 0019 #include <QDebug> 0020 #include <QDir> 0021 #include <QDirIterator> 0022 #include <QFile> 0023 #include <QMimeDatabase> 0024 #include <QMutex> 0025 #include <QProcessEnvironment> 0026 #include <QRegularExpression> 0027 #include <QThread> 0028 #include <QUrl> 0029 #include <qplatformdefs.h> // QT_LSTAT, QT_STAT, QT_STATBUF 0030 0031 #include <KConfig> 0032 #include <KConfigGroup> 0033 #include <KSharedConfig> 0034 #include <KUser> 0035 0036 #include <kio/listjob.h> 0037 #include <kio_widgets_debug.h> 0038 #include <kioglobal_p.h> 0039 #include <kprotocolmanager.h> 0040 #include <kurlauthorized.h> 0041 0042 #include <time.h> 0043 0044 #ifdef Q_OS_WIN 0045 #include <qt_windows.h> 0046 #else 0047 #include <pwd.h> 0048 #include <sys/param.h> 0049 #endif 0050 0051 static bool expandTilde(QString &); 0052 static bool expandEnv(QString &); 0053 0054 static QString unescape(const QString &text); 0055 0056 // Permission mask for files that are executable by 0057 // user, group or other 0058 static constexpr mode_t s_modeExe = S_IXUSR | S_IXGRP | S_IXOTH; 0059 0060 // Constants for types of completion 0061 enum ComplType { CTNone = 0, CTEnv, CTUser, CTMan, CTExe, CTFile, CTUrl, CTInfo }; 0062 0063 class CompletionThread; 0064 0065 // Ensure that we don't end up with "//". 0066 static void addPathToUrl(QUrl &url, const QString &relPath) 0067 { 0068 url.setPath(Utils::concatPaths(url.path(), relPath)); 0069 } 0070 0071 static QBasicAtomicInt s_waitDuration = Q_BASIC_ATOMIC_INITIALIZER(-1); 0072 0073 static int initialWaitDuration() 0074 { 0075 if (s_waitDuration.loadRelaxed() == -1) { 0076 const QByteArray envVar = qgetenv("KURLCOMPLETION_WAIT"); 0077 if (envVar.isEmpty()) { 0078 s_waitDuration = 200; // default: 200 ms 0079 } else { 0080 s_waitDuration = envVar.toInt(); 0081 } 0082 } 0083 return s_waitDuration; 0084 } 0085 0086 // For local paths we use our custom comparer function that ignores the trailing slash character 0087 static void sortLocalPaths(QStringList &list) 0088 { 0089 QCollator c; 0090 c.setCaseSensitivity(Qt::CaseSensitive); 0091 std::sort(list.begin(), list.end(), [c](const QString &a, const QString &b) { 0092 return c.compare(a.endsWith(QStringLiteral("/")) ? a.chopped(1) : a, b.endsWith(QStringLiteral("/")) ? b.chopped(1) : b) < 0; 0093 }); 0094 } 0095 0096 /////////////////////////////////////////////////////// 0097 /////////////////////////////////////////////////////// 0098 // KUrlCompletionPrivate 0099 // 0100 class KUrlCompletionPrivate 0101 { 0102 public: 0103 explicit KUrlCompletionPrivate(KUrlCompletion *qq, KUrlCompletion::Mode m) 0104 : q(qq) 0105 , cwd(QUrl::fromLocalFile(QDir::homePath())) 0106 , mode(m) 0107 { 0108 // Read settings 0109 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("URLCompletion")); 0110 url_auto_completion = cg.readEntry("alwaysAutoComplete", true); 0111 popup_append_slash = cg.readEntry("popupAppendSlash", true); 0112 onlyLocalProto = cg.readEntry("LocalProtocolsOnly", false); 0113 0114 q->setIgnoreCase(true); 0115 } 0116 0117 ~KUrlCompletionPrivate(); 0118 0119 void slotEntries(KIO::Job *, const KIO::UDSEntryList &); 0120 void slotIOFinished(KJob *); 0121 void slotCompletionThreadDone(QThread *thread, const QStringList &matches); 0122 0123 class MyURL; 0124 bool userCompletion(const MyURL &url, QString *match); 0125 bool envCompletion(const MyURL &url, QString *match); 0126 bool exeCompletion(const MyURL &url, QString *match); 0127 bool fileCompletion(const MyURL &url, QString *match); 0128 bool urlCompletion(const MyURL &url, QString *match); 0129 0130 bool isAutoCompletion(); 0131 0132 // List the next dir in m_dirs 0133 QString listDirectories(const QStringList &, const QString &, bool only_exe = false, bool only_dir = false, bool no_hidden = false, bool stat_files = true); 0134 0135 void listUrls(const QList<QUrl> &urls, const QString &filter = QString(), bool only_exe = false, bool no_hidden = false); 0136 0137 void addMatches(const QStringList &); 0138 QString finished(); 0139 0140 void init(); 0141 0142 void setListedUrl(ComplType compl_type, const QString &dir = QString(), const QString &filter = QString(), bool no_hidden = false); 0143 0144 bool isListedUrl(ComplType compl_type, const QString &dir = QString(), const QString &filter = QString(), bool no_hidden = false); 0145 0146 KUrlCompletion *const q; 0147 QList<QUrl> list_urls; 0148 0149 bool onlyLocalProto = false; 0150 0151 // urlCompletion() in Auto/Popup mode? 0152 bool url_auto_completion = true; 0153 0154 // Append '/' to directories in Popup mode? 0155 // Doing that stat's all files and is slower 0156 bool popup_append_slash = true; 0157 0158 // Keep track of currently listed files to avoid reading them again 0159 bool last_no_hidden = false; 0160 QString last_path_listed; 0161 QString last_file_listed; 0162 QString last_prepend; 0163 ComplType last_compl_type = CTNone; 0164 0165 QUrl cwd; // "current directory" = base dir for completion 0166 0167 KUrlCompletion::Mode mode = KUrlCompletion::FileCompletion; 0168 bool replace_env = true; 0169 bool replace_home = true; 0170 bool complete_url; // if true completing a URL (i.e. 'prepend' is a URL), otherwise a path 0171 0172 KIO::ListJob *list_job = nullptr; // kio job to list directories 0173 0174 QString prepend; // text to prepend to listed items 0175 QString compl_text; // text to pass on to KCompletion 0176 0177 // Filters for files read with kio 0178 bool list_urls_only_exe; // true = only list executables 0179 bool list_urls_no_hidden; 0180 QString list_urls_filter; // filter for listed files 0181 0182 CompletionThread *userListThread = nullptr; 0183 CompletionThread *dirListThread = nullptr; 0184 0185 QStringList mimeTypeFilters; 0186 }; 0187 0188 class CompletionThread : public QThread 0189 { 0190 Q_OBJECT 0191 protected: 0192 CompletionThread(KUrlCompletionPrivate *receiver) 0193 : QThread() 0194 , m_prepend(receiver->prepend) 0195 , m_complete_url(receiver->complete_url) 0196 , m_terminationRequested(false) 0197 { 0198 } 0199 0200 public: 0201 void requestTermination() 0202 { 0203 if (!isFinished()) { 0204 qCDebug(KIO_WIDGETS) << "stopping thread" << this; 0205 } 0206 m_terminationRequested.storeRelaxed(true); 0207 wait(); 0208 } 0209 0210 QStringList matches() const 0211 { 0212 QMutexLocker locker(&m_mutex); 0213 return m_matches; 0214 } 0215 0216 Q_SIGNALS: 0217 void completionThreadDone(QThread *thread, const QStringList &matches); 0218 0219 protected: 0220 void addMatch(const QString &match) 0221 { 0222 QMutexLocker locker(&m_mutex); 0223 m_matches.append(match); 0224 } 0225 bool terminationRequested() const 0226 { 0227 return m_terminationRequested.loadRelaxed(); 0228 } 0229 void done() 0230 { 0231 if (!terminationRequested()) { 0232 qCDebug(KIO_WIDGETS) << "done, emitting signal with" << m_matches.count() << "matches"; 0233 Q_EMIT completionThreadDone(this, m_matches); 0234 } 0235 } 0236 0237 const QString m_prepend; 0238 const bool m_complete_url; // if true completing a URL (i.e. 'm_prepend' is a URL), otherwise a path 0239 0240 private: 0241 mutable QMutex m_mutex; // protects m_matches 0242 QStringList m_matches; // written by secondary thread, read by the matches() method 0243 QAtomicInt m_terminationRequested; // used as a bool 0244 }; 0245 0246 /** 0247 * A simple thread that fetches a list of tilde-completions and returns this 0248 * to the caller via the completionThreadDone signal. 0249 */ 0250 0251 class UserListThread : public CompletionThread 0252 { 0253 Q_OBJECT 0254 public: 0255 UserListThread(KUrlCompletionPrivate *receiver) 0256 : CompletionThread(receiver) 0257 { 0258 } 0259 0260 protected: 0261 void run() override 0262 { 0263 #ifndef Q_OS_ANDROID 0264 const QChar tilde = QLatin1Char('~'); 0265 0266 // we don't need to handle prepend here, right? ~user is always at pos 0 0267 assert(m_prepend.isEmpty()); 0268 #ifndef Q_OS_WIN 0269 struct passwd *pw; 0270 ::setpwent(); 0271 while ((pw = ::getpwent()) && !terminationRequested()) { 0272 addMatch(tilde + QString::fromLocal8Bit(pw->pw_name)); 0273 } 0274 ::endpwent(); 0275 #else 0276 // TODO: add KUser::allUserNames() with a std::function<bool()> shouldTerminate parameter 0277 // currently terminationRequested is ignored on Windows 0278 const QStringList allUsers = KUser::allUserNames(); 0279 for (const QString &s : allUsers) { 0280 addMatch(tilde + s); 0281 } 0282 #endif 0283 addMatch(QString(tilde)); 0284 #endif 0285 done(); 0286 } 0287 }; 0288 0289 class DirectoryListThread : public CompletionThread 0290 { 0291 Q_OBJECT 0292 public: 0293 DirectoryListThread(KUrlCompletionPrivate *receiver, 0294 const QStringList &dirList, 0295 const QString &filter, 0296 const QStringList &mimeTypeFilters, 0297 bool onlyExe, 0298 bool onlyDir, 0299 bool noHidden, 0300 bool appendSlashToDir) 0301 : CompletionThread(receiver) 0302 , m_dirList(dirList) 0303 , m_filter(filter) 0304 , m_mimeTypeFilters(mimeTypeFilters) 0305 , m_onlyExe(onlyExe) 0306 , m_onlyDir(onlyDir) 0307 , m_noHidden(noHidden) 0308 , m_appendSlashToDir(appendSlashToDir) 0309 { 0310 } 0311 0312 void run() override; 0313 0314 private: 0315 QStringList m_dirList; 0316 QString m_filter; 0317 QStringList m_mimeTypeFilters; 0318 bool m_onlyExe; 0319 bool m_onlyDir; 0320 bool m_noHidden; 0321 bool m_appendSlashToDir; 0322 }; 0323 0324 void DirectoryListThread::run() 0325 { 0326 // qDebug() << "Entered DirectoryListThread::run(), m_filter=" << m_filter << ", m_onlyExe=" << m_onlyExe << ", m_onlyDir=" << m_onlyDir << ", 0327 // m_appendSlashToDir=" << m_appendSlashToDir << ", m_dirList.size()=" << m_dirList.size(); 0328 0329 QDir::Filters iterator_filter = (m_noHidden ? QDir::Filter(0) : QDir::Hidden) | QDir::Readable | QDir::NoDotAndDotDot; 0330 if (m_onlyExe) { 0331 iterator_filter |= (QDir::Dirs | QDir::Files | QDir::Executable); 0332 } else if (m_onlyDir) { 0333 iterator_filter |= QDir::Dirs; 0334 } else { 0335 iterator_filter |= (QDir::Dirs | QDir::Files); 0336 } 0337 0338 QMimeDatabase mimeTypes; 0339 0340 for (const QString &dir : std::as_const(m_dirList)) { 0341 if (terminationRequested()) { 0342 break; 0343 } 0344 0345 // qDebug() << "Scanning directory" << dir; 0346 0347 QDirIterator current_dir_iterator(dir, iterator_filter); 0348 0349 while (current_dir_iterator.hasNext() && !terminationRequested()) { 0350 current_dir_iterator.next(); 0351 0352 QFileInfo item_info = current_dir_iterator.fileInfo(); 0353 QString item_name = item_info.fileName(); 0354 0355 // qDebug() << "Found" << file_name; 0356 0357 if (!m_filter.isEmpty() && !item_name.startsWith(m_filter)) { 0358 continue; 0359 } 0360 0361 if (!m_mimeTypeFilters.isEmpty() && !item_info.isDir()) { 0362 auto mimeType = mimeTypes.mimeTypeForFile(item_info); 0363 if (!m_mimeTypeFilters.contains(mimeType.name())) { 0364 continue; 0365 } 0366 } 0367 0368 // Add '/' to directories 0369 if (m_appendSlashToDir && item_info.isDir()) { 0370 Utils::appendSlash(item_name); 0371 } 0372 0373 if (m_complete_url) { 0374 QUrl url(m_prepend); 0375 addPathToUrl(url, item_name); 0376 addMatch(url.toDisplayString()); 0377 } else { 0378 item_name.prepend(m_prepend); 0379 addMatch(item_name); 0380 } 0381 } 0382 } 0383 0384 done(); 0385 } 0386 0387 KUrlCompletionPrivate::~KUrlCompletionPrivate() 0388 { 0389 } 0390 0391 /////////////////////////////////////////////////////// 0392 /////////////////////////////////////////////////////// 0393 // MyURL - wrapper for QUrl with some different functionality 0394 // 0395 0396 class KUrlCompletionPrivate::MyURL 0397 { 0398 public: 0399 MyURL(const QString &url, const QUrl &cwd); 0400 MyURL(const MyURL &url); 0401 ~MyURL(); 0402 0403 QUrl kurl() const 0404 { 0405 return m_kurl; 0406 } 0407 0408 bool isLocalFile() const 0409 { 0410 return m_kurl.isLocalFile(); 0411 } 0412 QString scheme() const 0413 { 0414 return m_kurl.scheme(); 0415 } 0416 // The directory with a trailing '/' 0417 QString dir() const 0418 { 0419 return m_kurl.adjusted(QUrl::RemoveFilename).path(); 0420 } 0421 QString file() const 0422 { 0423 return m_kurl.fileName(); 0424 } 0425 0426 // The initial, unparsed, url, as a string. 0427 QString url() const 0428 { 0429 return m_url; 0430 } 0431 0432 // Is the initial string a URL, or just a path (whether absolute or relative) 0433 bool isURL() const 0434 { 0435 return m_isURL; 0436 } 0437 0438 void filter(bool replace_user_dir, bool replace_env); 0439 0440 private: 0441 void init(const QString &url, const QUrl &cwd); 0442 0443 QUrl m_kurl; 0444 QString m_url; 0445 bool m_isURL; 0446 }; 0447 0448 KUrlCompletionPrivate::MyURL::MyURL(const QString &_url, const QUrl &cwd) 0449 { 0450 init(_url, cwd); 0451 } 0452 0453 KUrlCompletionPrivate::MyURL::MyURL(const MyURL &_url) 0454 : m_kurl(_url.m_kurl) 0455 { 0456 m_url = _url.m_url; 0457 m_isURL = _url.m_isURL; 0458 } 0459 0460 void KUrlCompletionPrivate::MyURL::init(const QString &_url, const QUrl &cwd) 0461 { 0462 // Save the original text 0463 m_url = _url; 0464 0465 // Non-const copy 0466 QString url_copy = _url; 0467 0468 // Special shortcuts for "man:" and "info:" 0469 if (url_copy.startsWith(QLatin1Char('#'))) { 0470 if (url_copy.length() > 1 && url_copy.at(1) == QLatin1Char('#')) { 0471 url_copy.replace(0, 2, QStringLiteral("info:")); 0472 } else { 0473 url_copy.replace(0, 1, QStringLiteral("man:")); 0474 } 0475 } 0476 0477 // Look for a protocol in 'url' 0478 const QRegularExpression protocol_regex(QStringLiteral("^(?![A-Za-z]:)[^/\\s\\\\]*:")); 0479 0480 // Assume "file:" or whatever is given by 'cwd' if there is 0481 // no protocol. (QUrl does this only for absolute paths) 0482 if (protocol_regex.match(url_copy).hasMatch()) { 0483 m_kurl = QUrl(url_copy); 0484 m_isURL = true; 0485 } else { // relative path or ~ or $something 0486 m_isURL = false; 0487 if (Utils::isAbsoluteLocalPath(url_copy) || url_copy.startsWith(QLatin1Char('~')) || url_copy.startsWith(QLatin1Char('$'))) { 0488 m_kurl = QUrl::fromLocalFile(url_copy); 0489 } else { 0490 // Relative path 0491 if (cwd.isEmpty()) { 0492 m_kurl = QUrl(url_copy); 0493 } else { 0494 m_kurl = cwd; 0495 m_kurl.setPath(Utils::concatPaths(m_kurl.path(), url_copy)); 0496 } 0497 } 0498 } 0499 } 0500 0501 KUrlCompletionPrivate::MyURL::~MyURL() 0502 { 0503 } 0504 0505 void KUrlCompletionPrivate::MyURL::filter(bool replace_user_dir, bool replace_env) 0506 { 0507 QString d = dir() + file(); 0508 if (replace_user_dir) { 0509 expandTilde(d); 0510 } 0511 if (replace_env) { 0512 expandEnv(d); 0513 } 0514 m_kurl.setPath(d); 0515 } 0516 0517 /////////////////////////////////////////////////////// 0518 /////////////////////////////////////////////////////// 0519 // KUrlCompletion 0520 // 0521 0522 KUrlCompletion::KUrlCompletion() 0523 : KUrlCompletion(FileCompletion) 0524 { 0525 } 0526 0527 KUrlCompletion::KUrlCompletion(Mode _mode) 0528 : KCompletion() 0529 , d(new KUrlCompletionPrivate(this, _mode)) 0530 { 0531 } 0532 0533 KUrlCompletion::~KUrlCompletion() 0534 { 0535 stop(); 0536 } 0537 0538 void KUrlCompletion::setDir(const QUrl &dir) 0539 { 0540 d->cwd = dir; 0541 } 0542 0543 QUrl KUrlCompletion::dir() const 0544 { 0545 return d->cwd; 0546 } 0547 0548 KUrlCompletion::Mode KUrlCompletion::mode() const 0549 { 0550 return d->mode; 0551 } 0552 0553 void KUrlCompletion::setMode(Mode _mode) 0554 { 0555 d->mode = _mode; 0556 } 0557 0558 bool KUrlCompletion::replaceEnv() const 0559 { 0560 return d->replace_env; 0561 } 0562 0563 void KUrlCompletion::setReplaceEnv(bool replace) 0564 { 0565 d->replace_env = replace; 0566 } 0567 0568 bool KUrlCompletion::replaceHome() const 0569 { 0570 return d->replace_home; 0571 } 0572 0573 void KUrlCompletion::setReplaceHome(bool replace) 0574 { 0575 d->replace_home = replace; 0576 } 0577 0578 /* 0579 * makeCompletion() 0580 * 0581 * Entry point for file name completion 0582 */ 0583 QString KUrlCompletion::makeCompletion(const QString &text) 0584 { 0585 qCDebug(KIO_WIDGETS) << text << "d->cwd=" << d->cwd; 0586 0587 KUrlCompletionPrivate::MyURL url(text, d->cwd); 0588 0589 d->compl_text = text; 0590 0591 // Set d->prepend to the original URL, with the filename [and ref/query] stripped. 0592 // This is what gets prepended to the directory-listing matches. 0593 if (url.isURL()) { 0594 QUrl directoryUrl(url.kurl()); 0595 directoryUrl.setQuery(QString()); 0596 directoryUrl.setFragment(QString()); 0597 directoryUrl.setPath(url.dir()); 0598 d->prepend = directoryUrl.toString(); 0599 } else { 0600 d->prepend = text.left(text.length() - url.file().length()); 0601 } 0602 0603 d->complete_url = url.isURL(); 0604 0605 // We use our custom sorter function if we are completing local paths 0606 setSorterFunction(!d->complete_url ? sortLocalPaths : nullptr); 0607 0608 // If we typed an exact path to a directory, we block autosuggestion 0609 // or else it would autosuggest the first child dir. 0610 setShouldAutoSuggest(d->complete_url || (url.dir() != text)); 0611 0612 QString aMatch; 0613 0614 // Environment variables 0615 // 0616 if (d->replace_env && d->envCompletion(url, &aMatch)) { 0617 return aMatch; 0618 } 0619 0620 // User directories 0621 // 0622 if (d->replace_home && d->userCompletion(url, &aMatch)) { 0623 return aMatch; 0624 } 0625 0626 // Replace user directories and variables 0627 url.filter(d->replace_home, d->replace_env); 0628 0629 // qDebug() << "Filtered: proto=" << url.scheme() 0630 // << ", dir=" << url.dir() 0631 // << ", file=" << url.file() 0632 // << ", kurl url=" << *url.kurl(); 0633 0634 if (d->mode == ExeCompletion) { 0635 // Executables 0636 // 0637 if (d->exeCompletion(url, &aMatch)) { 0638 return aMatch; 0639 } 0640 0641 // KRun can run "man:" and "info:" etc. so why not treat them 0642 // as executables... 0643 0644 if (d->urlCompletion(url, &aMatch)) { 0645 return aMatch; 0646 } 0647 } else { 0648 // Local files, directories 0649 // 0650 if (d->fileCompletion(url, &aMatch)) { 0651 return aMatch; 0652 } 0653 0654 // All other... 0655 // 0656 if (d->urlCompletion(url, &aMatch)) { 0657 return aMatch; 0658 } 0659 } 0660 0661 d->setListedUrl(CTNone); 0662 stop(); 0663 0664 return QString(); 0665 } 0666 0667 /* 0668 * finished 0669 * 0670 * Go on and call KCompletion. 0671 * Called when all matches have been added 0672 */ 0673 QString KUrlCompletionPrivate::finished() 0674 { 0675 if (last_compl_type == CTInfo) { 0676 return q->KCompletion::makeCompletion(compl_text.toLower()); 0677 } else { 0678 return q->KCompletion::makeCompletion(compl_text); 0679 } 0680 } 0681 0682 /* 0683 * isRunning 0684 * 0685 * Return true if either a KIO job or a thread is running 0686 */ 0687 bool KUrlCompletion::isRunning() const 0688 { 0689 return d->list_job || (d->dirListThread && !d->dirListThread->isFinished()) || (d->userListThread && !d->userListThread->isFinished()); 0690 } 0691 0692 /* 0693 * stop 0694 * 0695 * Stop and delete a running KIO job or the DirLister 0696 */ 0697 void KUrlCompletion::stop() 0698 { 0699 if (d->list_job) { 0700 d->list_job->kill(); 0701 d->list_job = nullptr; 0702 } 0703 0704 if (d->dirListThread) { 0705 d->dirListThread->requestTermination(); 0706 delete d->dirListThread; 0707 d->dirListThread = nullptr; 0708 } 0709 0710 if (d->userListThread) { 0711 d->userListThread->requestTermination(); 0712 delete d->userListThread; 0713 d->userListThread = nullptr; 0714 } 0715 } 0716 0717 /* 0718 * Keep track of the last listed directory 0719 */ 0720 void KUrlCompletionPrivate::setListedUrl(ComplType complType, const QString &directory, const QString &filter, bool no_hidden) 0721 { 0722 last_compl_type = complType; 0723 last_path_listed = directory; 0724 last_file_listed = filter; 0725 last_no_hidden = no_hidden; 0726 last_prepend = prepend; 0727 } 0728 0729 bool KUrlCompletionPrivate::isListedUrl(ComplType complType, const QString &directory, const QString &filter, bool no_hidden) 0730 { 0731 /* clang-format off */ 0732 return last_compl_type == complType 0733 && (last_path_listed == directory || (directory.isEmpty() && last_path_listed.isEmpty())) 0734 && (filter.startsWith(last_file_listed) || (filter.isEmpty() && last_file_listed.isEmpty())) 0735 && last_no_hidden == no_hidden 0736 && last_prepend == prepend; // e.g. relative path vs absolute 0737 /* clang-format on */ 0738 } 0739 0740 /* 0741 * isAutoCompletion 0742 * 0743 * Returns true if completion mode is Auto or Popup 0744 */ 0745 bool KUrlCompletionPrivate::isAutoCompletion() 0746 { 0747 /* clang-format off */ 0748 return q->completionMode() == KCompletion::CompletionAuto 0749 || q->completionMode() == KCompletion::CompletionPopup 0750 || q->completionMode() == KCompletion::CompletionMan 0751 || q->completionMode() == KCompletion::CompletionPopupAuto; 0752 /* clang-format on */ 0753 } 0754 ////////////////////////////////////////////////// 0755 ////////////////////////////////////////////////// 0756 // User directories 0757 // 0758 0759 bool KUrlCompletionPrivate::userCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) 0760 { 0761 if (url.scheme() != QLatin1String("file") || !url.dir().isEmpty() || !url.file().startsWith(QLatin1Char('~')) || !prepend.isEmpty()) { 0762 return false; 0763 } 0764 0765 if (!isListedUrl(CTUser)) { 0766 q->stop(); 0767 q->clear(); 0768 setListedUrl(CTUser); 0769 0770 Q_ASSERT(!userListThread); // caller called stop() 0771 userListThread = new UserListThread(this); 0772 QObject::connect(userListThread, &CompletionThread::completionThreadDone, q, [this](QThread *thread, const QStringList &matches) { 0773 slotCompletionThreadDone(thread, matches); 0774 }); 0775 userListThread->start(); 0776 0777 // If the thread finishes quickly make sure that the results 0778 // are added to the first matching case. 0779 0780 userListThread->wait(initialWaitDuration()); 0781 const QStringList l = userListThread->matches(); 0782 addMatches(l); 0783 } 0784 *pMatch = finished(); 0785 return true; 0786 } 0787 0788 ///////////////////////////////////////////////////// 0789 ///////////////////////////////////////////////////// 0790 // Environment variables 0791 // 0792 0793 bool KUrlCompletionPrivate::envCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) 0794 { 0795 if (url.file().isEmpty() || url.file().at(0) != QLatin1Char('$')) { 0796 return false; 0797 } 0798 0799 if (!isListedUrl(CTEnv)) { 0800 q->stop(); 0801 q->clear(); 0802 0803 QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); 0804 const QStringList keys = env.keys(); 0805 0806 QStringList l; 0807 l.reserve(keys.size()); 0808 for (const QString &key : keys) { 0809 l.append(prepend + QLatin1Char('$') + key); 0810 } 0811 0812 addMatches(l); 0813 } 0814 0815 setListedUrl(CTEnv); 0816 0817 *pMatch = finished(); 0818 return true; 0819 } 0820 0821 ////////////////////////////////////////////////// 0822 ////////////////////////////////////////////////// 0823 // Executables 0824 // 0825 0826 bool KUrlCompletionPrivate::exeCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) 0827 { 0828 if (!url.isLocalFile()) { 0829 return false; 0830 } 0831 0832 QString directory = unescape(url.dir()); // remove escapes 0833 0834 // Find directories to search for completions, either 0835 // 0836 // 1. complete path given in url 0837 // 2. current directory (d->cwd) 0838 // 3. $PATH 0839 // 4. no directory at all 0840 0841 QStringList dirList; 0842 0843 if (!url.file().isEmpty()) { 0844 // $PATH 0845 dirList = QString::fromLocal8Bit(qgetenv("PATH")).split(QDir::listSeparator(), Qt::SkipEmptyParts); 0846 0847 QStringList::Iterator it = dirList.begin(); 0848 0849 for (; it != dirList.end(); ++it) { 0850 it->append(QLatin1Char('/')); 0851 } 0852 } else if (Utils::isAbsoluteLocalPath(directory)) { 0853 // complete path in url 0854 dirList.append(directory); 0855 } else if (!directory.isEmpty() && !cwd.isEmpty()) { 0856 // current directory 0857 dirList.append(cwd.toLocalFile() + QLatin1Char('/') + directory); 0858 } 0859 0860 // No hidden files unless the user types "." 0861 bool no_hidden_files = url.file().isEmpty() || url.file().at(0) != QLatin1Char('.'); 0862 0863 // List files if needed 0864 // 0865 if (!isListedUrl(CTExe, directory, url.file(), no_hidden_files)) { 0866 q->stop(); 0867 q->clear(); 0868 0869 setListedUrl(CTExe, directory, url.file(), no_hidden_files); 0870 0871 *pMatch = listDirectories(dirList, url.file(), true, false, no_hidden_files); 0872 } else { 0873 *pMatch = finished(); 0874 } 0875 0876 return true; 0877 } 0878 0879 ////////////////////////////////////////////////// 0880 ////////////////////////////////////////////////// 0881 // Local files 0882 // 0883 0884 bool KUrlCompletionPrivate::fileCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) 0885 { 0886 if (!url.isLocalFile()) { 0887 return false; 0888 } 0889 0890 QString directory = unescape(url.dir()); 0891 0892 if (url.url() == QLatin1String("..")) { 0893 *pMatch = QStringLiteral(".."); 0894 return true; 0895 } 0896 0897 // qDebug() << "fileCompletion" << url << "dir=" << dir; 0898 0899 // Find directories to search for completions, either 0900 // 0901 // 1. complete path given in url 0902 // 2. current directory (d->cwd) 0903 // 3. no directory at all 0904 0905 QStringList dirList; 0906 0907 if (Utils::isAbsoluteLocalPath(directory)) { 0908 // complete path in url 0909 dirList.append(directory); 0910 } else if (!cwd.isEmpty()) { 0911 // current directory 0912 QString dirToAdd = cwd.toLocalFile(); 0913 if (!directory.isEmpty()) { 0914 Utils::appendSlash(dirToAdd); 0915 dirToAdd += directory; 0916 } 0917 dirList.append(dirToAdd); 0918 } 0919 0920 // No hidden files unless the user types "." 0921 bool no_hidden_files = !url.file().startsWith(QLatin1Char('.')); 0922 0923 // List files if needed 0924 // 0925 if (!isListedUrl(CTFile, directory, QString(), no_hidden_files)) { 0926 q->stop(); 0927 q->clear(); 0928 0929 setListedUrl(CTFile, directory, QString(), no_hidden_files); 0930 0931 // Append '/' to directories in Popup mode? 0932 bool append_slash = 0933 (popup_append_slash && (q->completionMode() == KCompletion::CompletionPopup || q->completionMode() == KCompletion::CompletionPopupAuto)); 0934 0935 bool only_dir = (mode == KUrlCompletion::DirCompletion); 0936 0937 *pMatch = listDirectories(dirList, QString(), false, only_dir, no_hidden_files, append_slash); 0938 } else { 0939 *pMatch = finished(); 0940 } 0941 0942 return true; 0943 } 0944 0945 ////////////////////////////////////////////////// 0946 ////////////////////////////////////////////////// 0947 // URLs not handled elsewhere... 0948 // 0949 0950 static bool isLocalProtocol(const QString &protocol) 0951 { 0952 return (KProtocolInfo::protocolClass(protocol) == QLatin1String(":local")); 0953 } 0954 0955 bool KUrlCompletionPrivate::urlCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch) 0956 { 0957 // qDebug() << *url.kurl(); 0958 if (onlyLocalProto && isLocalProtocol(url.scheme())) { 0959 return false; 0960 } 0961 0962 // Use d->cwd as base url in case url is not absolute 0963 QUrl url_dir = url.kurl(); 0964 if (url_dir.isRelative() && !cwd.isEmpty()) { 0965 // Create an URL with the directory to be listed 0966 url_dir = cwd.resolved(url_dir); 0967 } 0968 0969 // url is malformed 0970 if (!url_dir.isValid() || url.scheme().isEmpty()) { 0971 return false; 0972 } 0973 0974 // non local urls 0975 if (!isLocalProtocol(url.scheme())) { 0976 // url does not specify host 0977 if (url_dir.host().isEmpty()) { 0978 return false; 0979 } 0980 0981 // url does not specify a valid directory 0982 if (url_dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty()) { 0983 return false; 0984 } 0985 0986 // automatic completion is disabled 0987 if (isAutoCompletion() && !url_auto_completion) { 0988 return false; 0989 } 0990 } 0991 0992 // url handler doesn't support listing 0993 if (!KProtocolManager::supportsListing(url_dir)) { 0994 return false; 0995 } 0996 0997 // Remove escapes 0998 const QString directory = unescape(url_dir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path()); 0999 url_dir.setPath(directory); 1000 1001 // List files if needed 1002 // 1003 if (!isListedUrl(CTUrl, directory, url.file())) { 1004 q->stop(); 1005 q->clear(); 1006 1007 setListedUrl(CTUrl, directory, QString()); 1008 1009 QList<QUrl> url_list; 1010 url_list.append(url_dir); 1011 1012 listUrls(url_list, QString(), false); 1013 1014 pMatch->clear(); 1015 } else if (!q->isRunning()) { 1016 *pMatch = finished(); 1017 } else { 1018 pMatch->clear(); 1019 } 1020 1021 return true; 1022 } 1023 1024 ////////////////////////////////////////////////// 1025 ////////////////////////////////////////////////// 1026 // Directory and URL listing 1027 // 1028 1029 /* 1030 * addMatches 1031 * 1032 * Called to add matches to KCompletion 1033 */ 1034 void KUrlCompletionPrivate::addMatches(const QStringList &matchList) 1035 { 1036 q->insertItems(matchList); 1037 } 1038 1039 /* 1040 * listDirectories 1041 * 1042 * List files starting with 'filter' in the given directories, 1043 * either using DirLister or listURLs() 1044 * 1045 * In either case, addMatches() is called with the listed 1046 * files, and eventually finished() when the listing is done 1047 * 1048 * Returns the match if available, or QString() if 1049 * DirLister timed out or using kio 1050 */ 1051 QString KUrlCompletionPrivate::listDirectories(const QStringList &dirList, 1052 const QString &filter, 1053 bool only_exe, 1054 bool only_dir, 1055 bool no_hidden, 1056 bool append_slash_to_dir) 1057 { 1058 assert(!q->isRunning()); 1059 1060 if (qEnvironmentVariableIsEmpty("KURLCOMPLETION_LOCAL_KIO")) { 1061 qCDebug(KIO_WIDGETS) << "Listing directories:" << dirList << "with filter=" << filter << "using thread"; 1062 1063 // Don't use KIO 1064 1065 QStringList dirs; 1066 1067 QStringList::ConstIterator end = dirList.constEnd(); 1068 for (QStringList::ConstIterator it = dirList.constBegin(); it != end; ++it) { 1069 QUrl url = QUrl::fromLocalFile(*it); 1070 if (KUrlAuthorized::authorizeUrlAction(QStringLiteral("list"), QUrl(), url)) { 1071 dirs.append(*it); 1072 } 1073 } 1074 1075 Q_ASSERT(!dirListThread); // caller called stop() 1076 dirListThread = new DirectoryListThread(this, dirs, filter, mimeTypeFilters, only_exe, only_dir, no_hidden, append_slash_to_dir); 1077 QObject::connect(dirListThread, &CompletionThread::completionThreadDone, q, [this](QThread *thread, const QStringList &matches) { 1078 slotCompletionThreadDone(thread, matches); 1079 }); 1080 dirListThread->start(); 1081 dirListThread->wait(initialWaitDuration()); 1082 qCDebug(KIO_WIDGETS) << "Adding initial matches:" << dirListThread->matches(); 1083 addMatches(dirListThread->matches()); 1084 1085 return finished(); 1086 } 1087 1088 // Use KIO 1089 // qDebug() << "Listing (listDirectories):" << dirList << "with KIO"; 1090 1091 QList<QUrl> url_list; 1092 1093 QStringList::ConstIterator it = dirList.constBegin(); 1094 QStringList::ConstIterator end = dirList.constEnd(); 1095 1096 url_list.reserve(dirList.size()); 1097 for (; it != end; ++it) { 1098 url_list.append(QUrl(*it)); 1099 } 1100 1101 listUrls(url_list, filter, only_exe, no_hidden); 1102 // Will call addMatches() and finished() 1103 1104 return QString(); 1105 } 1106 1107 /* 1108 * listURLs 1109 * 1110 * Use KIO to list the given urls 1111 * 1112 * addMatches() is called with the listed files 1113 * finished() is called when the listing is done 1114 */ 1115 void KUrlCompletionPrivate::listUrls(const QList<QUrl> &urls, const QString &filter, bool only_exe, bool no_hidden) 1116 { 1117 assert(list_urls.isEmpty()); 1118 assert(list_job == nullptr); 1119 1120 list_urls = urls; 1121 list_urls_filter = filter; 1122 list_urls_only_exe = only_exe; 1123 list_urls_no_hidden = no_hidden; 1124 1125 // qDebug() << "Listing URLs:" << *urls[0] << ",..."; 1126 1127 // Start it off by calling slotIOFinished 1128 // 1129 // This will start a new list job as long as there 1130 // are urls in d->list_urls 1131 // 1132 slotIOFinished(nullptr); 1133 } 1134 1135 /* 1136 * slotEntries 1137 * 1138 * Receive files listed by KIO and call addMatches() 1139 */ 1140 void KUrlCompletionPrivate::slotEntries(KIO::Job *, const KIO::UDSEntryList &entries) 1141 { 1142 QStringList matchList; 1143 1144 const QString filter = list_urls_filter; 1145 const int filter_len = filter.length(); 1146 1147 // Iterate over all files 1148 for (const auto &entry : entries) { 1149 const QString udsUrl = entry.stringValue(KIO::UDSEntry::UDS_URL); 1150 1151 QString entry_name; 1152 if (!udsUrl.isEmpty()) { 1153 // qDebug() << "url:" << url; 1154 entry_name = QUrl(udsUrl).fileName(); 1155 } else { 1156 entry_name = entry.stringValue(KIO::UDSEntry::UDS_NAME); 1157 } 1158 1159 // This can happen with kdeconnect://deviceId as a completion for kdeconnect:/, 1160 // there's no fileName [and the UDS_NAME is unrelated, can't use that]. 1161 // This code doesn't support completing hostnames anyway (see addPathToUrl below). 1162 if (entry_name.isEmpty()) { 1163 continue; 1164 } 1165 1166 if (entry_name.at(0) == QLatin1Char('.') 1167 && (list_urls_no_hidden || entry_name.length() == 1 || (entry_name.length() == 2 && entry_name.at(1) == QLatin1Char('.')))) { 1168 continue; 1169 } 1170 1171 const bool isDir = entry.isDir(); 1172 1173 if (mode == KUrlCompletion::DirCompletion && !isDir) { 1174 continue; 1175 } 1176 1177 if (filter_len != 0 && QStringView(entry_name).left(filter_len) != filter) { 1178 continue; 1179 } 1180 1181 if (!mimeTypeFilters.isEmpty() && !isDir && !mimeTypeFilters.contains(entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE))) { 1182 continue; 1183 } 1184 1185 QString toAppend = entry_name; 1186 1187 if (isDir) { 1188 toAppend.append(QLatin1Char('/')); 1189 } 1190 1191 if (!list_urls_only_exe || (entry.numberValue(KIO::UDSEntry::UDS_ACCESS) & s_modeExe) // true if executable 1192 ) { 1193 if (complete_url) { 1194 QUrl url(prepend); 1195 addPathToUrl(url, toAppend); 1196 matchList.append(url.toDisplayString()); 1197 } else { 1198 matchList.append(prepend + toAppend); 1199 } 1200 } 1201 } 1202 1203 addMatches(matchList); 1204 } 1205 1206 /* 1207 * slotIOFinished 1208 * 1209 * Called when a KIO job is finished. 1210 * 1211 * Start a new list job if there are still urls in 1212 * list_urls, otherwise call finished() 1213 */ 1214 void KUrlCompletionPrivate::slotIOFinished(KJob *job) 1215 { 1216 assert(job == list_job); 1217 Q_UNUSED(job) 1218 1219 if (list_urls.isEmpty()) { 1220 list_job = nullptr; 1221 1222 finished(); // will call KCompletion::makeCompletion() 1223 1224 } else { 1225 QUrl kurl(list_urls.takeFirst()); 1226 1227 // list_urls.removeAll( kurl ); 1228 1229 // qDebug() << "Start KIO::listDir" << kurl; 1230 1231 list_job = KIO::listDir(kurl, KIO::HideProgressInfo); 1232 list_job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true")); 1233 1234 assert(list_job); 1235 1236 q->connect(list_job, &KJob::result, q, [this](KJob *job) { 1237 slotIOFinished(job); 1238 }); 1239 1240 q->connect(list_job, &KIO::ListJob::entries, q, [this](KIO::Job *job, const KIO::UDSEntryList &list) { 1241 slotEntries(job, list); 1242 }); 1243 } 1244 } 1245 1246 /////////////////////////////////////////////////// 1247 /////////////////////////////////////////////////// 1248 1249 /* 1250 * postProcessMatch, postProcessMatches 1251 * 1252 * Called by KCompletion before emitting match() and matches() 1253 * 1254 * Append '/' to directories for file completion. This is 1255 * done here to avoid stat()'ing a lot of files 1256 */ 1257 void KUrlCompletion::postProcessMatch(QString *pMatch) const 1258 { 1259 // qDebug() << *pMatch; 1260 1261 if (!pMatch->isEmpty() && pMatch->startsWith(QLatin1String("file:"))) { 1262 // Add '/' to directories in file completion mode 1263 // unless it has already been done 1264 if (d->last_compl_type == CTFile && pMatch->at(pMatch->length() - 1) != QLatin1Char('/')) { 1265 QString copy = QUrl(*pMatch).toLocalFile(); 1266 expandTilde(copy); 1267 expandEnv(copy); 1268 if (!Utils::isAbsoluteLocalPath(copy)) { 1269 copy.prepend(d->cwd.toLocalFile() + QLatin1Char('/')); 1270 } 1271 1272 // qDebug() << "stat'ing" << copy; 1273 1274 QByteArray file = QFile::encodeName(copy); 1275 1276 QT_STATBUF sbuff; 1277 if (QT_STAT(file.constData(), &sbuff) == 0) { 1278 if (Utils::isDirMask(sbuff.st_mode)) { 1279 pMatch->append(QLatin1Char('/')); 1280 } 1281 } else { 1282 // qDebug() << "Could not stat file" << copy; 1283 } 1284 } 1285 } 1286 } 1287 1288 void KUrlCompletion::postProcessMatches(QStringList * /*matches*/) const 1289 { 1290 // Maybe '/' should be added to directories here as in 1291 // postProcessMatch() but it would slow things down 1292 // when there are a lot of matches... 1293 } 1294 1295 void KUrlCompletion::postProcessMatches(KCompletionMatches * /*matches*/) const 1296 { 1297 // Maybe '/' should be added to directories here as in 1298 // postProcessMatch() but it would slow things down 1299 // when there are a lot of matches... 1300 } 1301 1302 void KUrlCompletionPrivate::slotCompletionThreadDone(QThread *thread, const QStringList &matches) 1303 { 1304 if (thread != userListThread && thread != dirListThread) { 1305 qCDebug(KIO_WIDGETS) << "got" << matches.count() << "outdated matches"; 1306 return; 1307 } 1308 1309 qCDebug(KIO_WIDGETS) << "got" << matches.count() << "matches at end of thread"; 1310 q->setItems(matches); 1311 1312 if (userListThread == thread) { 1313 thread->wait(); 1314 delete thread; 1315 userListThread = nullptr; 1316 } else if (dirListThread == thread) { 1317 thread->wait(); 1318 delete thread; 1319 dirListThread = nullptr; 1320 } 1321 finished(); // will call KCompletion::makeCompletion() 1322 } 1323 1324 // static 1325 QString KUrlCompletion::replacedPath(const QString &text, bool replaceHome, bool replaceEnv) 1326 { 1327 if (text.isEmpty()) { 1328 return text; 1329 } 1330 1331 KUrlCompletionPrivate::MyURL url(text, QUrl()); // no need to replace something of our current cwd 1332 if (!url.kurl().isLocalFile()) { 1333 return text; 1334 } 1335 1336 url.filter(replaceHome, replaceEnv); 1337 return url.dir() + url.file(); 1338 } 1339 1340 QString KUrlCompletion::replacedPath(const QString &text) const 1341 { 1342 return replacedPath(text, d->replace_home, d->replace_env); 1343 } 1344 1345 void KUrlCompletion::setMimeTypeFilters(const QStringList &mimeTypeFilters) 1346 { 1347 d->mimeTypeFilters = mimeTypeFilters; 1348 } 1349 1350 QStringList KUrlCompletion::mimeTypeFilters() const 1351 { 1352 return d->mimeTypeFilters; 1353 } 1354 1355 ///////////////////////////////////////////////////////// 1356 ///////////////////////////////////////////////////////// 1357 // Static functions 1358 1359 /* 1360 * expandEnv 1361 * 1362 * Expand environment variables in text. Escaped '$' are ignored. 1363 * Return true if expansion was made. 1364 */ 1365 static bool expandEnv(QString &text) 1366 { 1367 // Find all environment variables beginning with '$' 1368 // 1369 int pos = 0; 1370 1371 bool expanded = false; 1372 1373 while ((pos = text.indexOf(QLatin1Char('$'), pos)) != -1) { 1374 // Skip escaped '$' 1375 // 1376 if (pos > 0 && text.at(pos - 1) == QLatin1Char('\\')) { 1377 pos++; 1378 } 1379 // Variable found => expand 1380 // 1381 else { 1382 // Find the end of the variable = next '/' or ' ' 1383 // 1384 int pos2 = text.indexOf(QLatin1Char(' '), pos + 1); 1385 int pos_tmp = text.indexOf(QLatin1Char('/'), pos + 1); 1386 1387 if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) { 1388 pos2 = pos_tmp; 1389 } 1390 1391 if (pos2 == -1) { 1392 pos2 = text.length(); 1393 } 1394 1395 // Replace if the variable is terminated by '/' or ' ' 1396 // and defined 1397 // 1398 if (pos2 >= 0) { 1399 const int len = pos2 - pos; 1400 const QStringView key = QStringView(text).mid(pos + 1, len - 1); 1401 const QString value = QString::fromLocal8Bit(qgetenv(key.toLocal8Bit().constData())); 1402 1403 if (!value.isEmpty()) { 1404 expanded = true; 1405 text.replace(pos, len, value); 1406 pos = pos + value.length(); 1407 } else { 1408 pos = pos2; 1409 } 1410 } 1411 } 1412 } 1413 1414 return expanded; 1415 } 1416 1417 /* 1418 * expandTilde 1419 * 1420 * Replace "~user" with the users home directory 1421 * Return true if expansion was made. 1422 */ 1423 static bool expandTilde(QString &text) 1424 { 1425 if (text.isEmpty() || (text.at(0) != QLatin1Char('~'))) { 1426 return false; 1427 } 1428 1429 bool expanded = false; 1430 1431 // Find the end of the user name = next '/' or ' ' 1432 // 1433 int pos2 = text.indexOf(QLatin1Char(' '), 1); 1434 int pos_tmp = text.indexOf(QLatin1Char('/'), 1); 1435 1436 if (pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2)) { 1437 pos2 = pos_tmp; 1438 } 1439 1440 if (pos2 == -1) { 1441 pos2 = text.length(); 1442 } 1443 1444 // Replace ~user if the user name is terminated by '/' or ' ' 1445 // 1446 if (pos2 >= 0) { 1447 QString userName = text.mid(1, pos2 - 1); 1448 QString dir; 1449 1450 // A single ~ is replaced with $HOME 1451 // 1452 if (userName.isEmpty()) { 1453 dir = QDir::homePath(); 1454 } 1455 // ~user is replaced with the dir from passwd 1456 // 1457 else { 1458 KUser user(userName); 1459 dir = user.homeDir(); 1460 } 1461 1462 if (!dir.isEmpty()) { 1463 expanded = true; 1464 text.replace(0, pos2, dir); 1465 } 1466 } 1467 1468 return expanded; 1469 } 1470 1471 /* 1472 * unescape 1473 * 1474 * Remove escapes and return the result in a new string 1475 * 1476 */ 1477 static QString unescape(const QString &text) 1478 { 1479 QString result; 1480 result.reserve(text.size()); 1481 1482 for (const QChar ch : text) { 1483 if (ch != QLatin1Char('\\')) { 1484 result.append(ch); 1485 } 1486 } 1487 1488 return result; 1489 } 1490 1491 #include "kurlcompletion.moc" 1492 #include "moc_kurlcompletion.cpp"