fix tricky regression noticed by Vyacheslav Tokarev on Google Reader.
[kdelibs.git] / kio / kio / kurlcompletion.cpp
blobc8be6eac892bd6bf4ffa8addafcb9258a2a5472e
1 /* -*- indent-tabs-mode: t; tab-width: 4; c-basic-offset:4 -*-
3 This file is part of the KDE libraries
4 Copyright (C) 2000 David Smith <dsmith@algonet.se>
5 Copyright (C) 2004 Scott Wheeler <wheeler@kde.org>
7 This class was inspired by a previous KUrlCompletion by
8 Henner Zeller <zeller@think.de>
10 This library is free software; you can redistribute it and/or
11 modify it under the terms of the GNU Library General Public
12 License as published by the Free Software Foundation; either
13 version 2 of the License, or (at your option) any later version.
15 This library is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 Library General Public License for more details.
20 You should have received a copy of the GNU Library General Public License
21 along with this library; see the file COPYING.LIB. If not, write to
22 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
23 Boston, MA 02110-1301, USA.
26 #include "kurlcompletion.h"
28 #include <config.h>
30 #include <stdlib.h>
31 #include <assert.h>
32 #include <limits.h>
34 #include <QtCore/QMutableStringListIterator>
35 #include <QtCore/QRegExp>
36 #include <QtCore/QTimer>
37 #include <QtCore/QDir>
38 #include <QtCore/QDirIterator>
39 #include <QtCore/QFile>
40 #include <QtCore/QTextIStream>
41 #include <QtCore/QThread>
42 #include <QtGui/QActionEvent>
44 #include <kapplication.h>
45 #include <kauthorized.h>
46 #include <kdebug.h>
47 #include <kurl.h>
48 #include <kio/job.h>
49 #include <kprotocolmanager.h>
50 #include <kconfig.h>
51 #include <kglobal.h>
52 #include <kde_file.h>
54 #include <sys/types.h>
55 #include <dirent.h>
56 #include <unistd.h>
57 #include <sys/stat.h>
58 #include <pwd.h>
59 #include <time.h>
60 #include <sys/param.h>
61 #include <kconfiggroup.h>
63 #ifdef Q_WS_WIN
64 #include <kkernel_win.h>
65 #endif
67 static bool expandTilde(QString &);
68 static bool expandEnv(QString &);
70 static QString unescape(const QString &text);
72 // Permission mask for files that are executable by
73 // user, group or other
74 #define MODE_EXE (S_IXUSR | S_IXGRP | S_IXOTH)
76 // Constants for types of completion
77 enum ComplType {CTNone=0, CTEnv, CTUser, CTMan, CTExe, CTFile, CTUrl, CTInfo};
79 class CompletionThread;
81 ///////////////////////////////////////////////////////
82 ///////////////////////////////////////////////////////
83 // KUrlCompletionPrivate
85 class KUrlCompletionPrivate
87 public:
88 KUrlCompletionPrivate(KUrlCompletion *parent)
89 : q(parent),
90 url_auto_completion(true),
91 userListThread(0),
92 dirListThread(0)
96 ~KUrlCompletionPrivate();
98 void _k_slotEntries( KIO::Job*, const KIO::UDSEntryList& );
99 void _k_slotIOFinished( KJob* );
101 class MyURL;
102 bool userCompletion(const MyURL &url, QString *match);
103 bool envCompletion(const MyURL &url, QString *match);
104 bool exeCompletion(const MyURL &url, QString *match);
105 bool fileCompletion(const MyURL &url, QString *match);
106 bool urlCompletion(const MyURL &url, QString *match);
108 bool isAutoCompletion();
110 // List the next dir in m_dirs
111 QString listDirectories(const QStringList &,
112 const QString &,
113 bool only_exe = false,
114 bool only_dir = false,
115 bool no_hidden = false,
116 bool stat_files = true);
118 void listUrls( const QList<KUrl *> &urls,
119 const QString &filter = QString(),
120 bool only_exe = false,
121 bool no_hidden = false );
123 void addMatches( const QStringList & );
124 QString finished();
126 void init();
128 void setListedUrl(int compl_type /* enum ComplType */,
129 const QString& dir = QString(),
130 const QString& filter = QString(),
131 bool no_hidden = false );
133 bool isListedUrl( int compl_type /* enum ComplType */,
134 const QString& dir = QString(),
135 const QString& filter = QString(),
136 bool no_hidden = false );
138 KUrlCompletion *q;
139 QList<KUrl*> list_urls;
141 bool onlyLocalProto;
143 // urlCompletion() in Auto/Popup mode?
144 bool url_auto_completion;
146 // Append '/' to directories in Popup mode?
147 // Doing that stat's all files and is slower
148 bool popup_append_slash;
150 // Keep track of currently listed files to avoid reading them again
151 QString last_path_listed;
152 QString last_file_listed;
153 QString last_prepend;
154 int last_compl_type;
155 int last_no_hidden;
157 QString cwd; // "current directory" = base dir for completion
159 KUrlCompletion::Mode mode; // ExeCompletion, FileCompletion, DirCompletion
160 bool replace_env;
161 bool replace_home;
162 bool complete_url; // if true completing a URL (i.e. 'prepend' is a URL), otherwise a path
164 KIO::ListJob *list_job; // kio job to list directories
166 QString prepend; // text to prepend to listed items
167 QString compl_text; // text to pass on to KCompletion
169 // Filters for files read with kio
170 bool list_urls_only_exe; // true = only list executables
171 bool list_urls_no_hidden;
172 QString list_urls_filter; // filter for listed files
174 CompletionThread *userListThread;
175 CompletionThread *dirListThread;
179 * A custom event type that is used to return a list of completion
180 * matches from an asynchronous lookup.
183 class CompletionMatchEvent : public QEvent
185 public:
186 CompletionMatchEvent( CompletionThread *thread ) :
187 QEvent( uniqueType() ),
188 m_completionThread( thread )
191 CompletionThread *completionThread() const { return m_completionThread; }
192 static Type uniqueType() { return Type(User + 61080); }
194 private:
195 CompletionThread *m_completionThread;
198 class CompletionThread : public QThread
200 protected:
201 CompletionThread( KUrlCompletionPrivate *receiver ) :
202 QThread(),
203 m_prepend( receiver->prepend ),
204 m_complete_url( receiver->complete_url ),
205 m_receiver( receiver ),
206 m_terminationRequested( false )
209 public:
210 void requestTermination() { m_terminationRequested = true; }
211 QStringList matches() const { return m_matches; }
213 protected:
214 void addMatch( const QString &match ) { m_matches.append( match ); }
215 bool terminationRequested() const { return m_terminationRequested; }
216 void done()
218 if ( !m_terminationRequested )
219 qApp->postEvent( m_receiver->q, new CompletionMatchEvent( this ) );
220 else
221 deleteLater();
224 const QString m_prepend;
225 const bool m_complete_url; // if true completing a URL (i.e. 'm_prepend' is a URL), otherwise a path
227 private:
228 KUrlCompletionPrivate *m_receiver;
229 QStringList m_matches;
230 bool m_terminationRequested;
234 * A simple thread that fetches a list of tilde-completions and returns this
235 * to the caller via a CompletionMatchEvent.
238 class UserListThread : public CompletionThread
240 public:
241 UserListThread( KUrlCompletionPrivate *receiver ) :
242 CompletionThread( receiver )
245 protected:
246 virtual void run()
248 static const QChar tilde = '~';
250 // we don't need to handle prepend here, right? ~user is always at pos 0
251 assert(m_prepend.isEmpty());
252 struct passwd *pw;
253 while ( ( pw = ::getpwent() ) && !terminationRequested() )
254 addMatch( tilde + QString::fromLocal8Bit( pw->pw_name ) );
256 ::endpwent();
258 addMatch( QString( tilde ) );
260 done();
264 class DirectoryListThread : public CompletionThread
266 public:
267 DirectoryListThread( KUrlCompletionPrivate *receiver,
268 const QStringList &dirList,
269 const QString &filter,
270 bool onlyExe,
271 bool onlyDir,
272 bool noHidden,
273 bool appendSlashToDir ) :
274 CompletionThread( receiver ),
275 m_dirList( dirList ),
276 m_filter( filter ),
277 m_onlyExe( onlyExe ),
278 m_onlyDir( onlyDir ),
279 m_noHidden( noHidden ),
280 m_appendSlashToDir( appendSlashToDir )
283 virtual void run();
285 private:
286 QStringList m_dirList;
287 QString m_filter;
288 bool m_onlyExe;
289 bool m_onlyDir;
290 bool m_noHidden;
291 bool m_appendSlashToDir;
294 void DirectoryListThread::run()
296 // Thread safety notes:
298 // There very possibly may be thread safety issues here, but I've done a check
299 // of all of the things that would seem to be problematic. Here are a few
300 // things that I have checked to be safe here (some used indirectly):
302 // QDir::currentPath(), QDir::setCurrent(), QFile::decodeName(), QFile::encodeName()
303 // QString::fromLocal8Bit(), QString::toLocal8Bit(), QTextCodec::codecForLocale()
305 // Also see (for POSIX functions):
306 // http://www.opengroup.org/onlinepubs/009695399/functions/xsh_chap02_09.html
308 // kDebug() << "Entered DirectoryListThread::run(), m_filter=" << m_filter << ", m_onlyExe=" << m_onlyExe << ", m_onlyDir=" << m_onlyDir << ", m_appendSlashToDir=" << m_appendSlashToDir << ", m_dirList.size()=" << m_dirList.size();
310 QStringList::ConstIterator end = m_dirList.constEnd();
311 for ( QStringList::ConstIterator it = m_dirList.constBegin();
312 it != end && !terminationRequested();
313 ++it )
315 // kDebug() << "Scanning directory" << *it;
317 // A trick from KIO that helps performance by a little bit:
318 // chdir to the directory so we won't have to deal with full paths
319 // with stat()
321 QString path = QDir::currentPath();
322 QDir::setCurrent( *it );
324 QDir::Filters iterator_filter = (m_noHidden ? QDir::Filter(0) : QDir::Hidden) | QDir::Readable | QDir::NoDotAndDotDot;
326 if ( m_onlyExe )
327 iterator_filter |= (QDir::Dirs | QDir::Files | QDir::Executable);
328 else if ( m_onlyDir )
329 iterator_filter |= QDir::Dirs;
330 else
331 iterator_filter |= (QDir::Dirs | QDir::Files);
333 QDirIterator current_dir_iterator( *it, iterator_filter);
335 while (current_dir_iterator.hasNext()) {
336 current_dir_iterator.next();
338 QFileInfo file_info = current_dir_iterator.fileInfo();
339 const QString file_name = file_info.fileName();
341 //kDebug() << "Found" << file_name;
343 if ( m_filter.isEmpty() || file_name.startsWith( m_filter ) ) {
345 QString toAppend = m_complete_url ? QUrl::toPercentEncoding(file_name) : file_name;
346 // Add '/' to directories
347 if ( m_appendSlashToDir && file_info.isDir() )
348 toAppend.append( QLatin1Char( '/' ) );
350 addMatch( m_prepend + toAppend );
354 // chdir to the original directory
355 QDir::setCurrent( path );
358 done();
361 KUrlCompletionPrivate::~KUrlCompletionPrivate()
363 if ( userListThread )
364 userListThread->requestTermination();
365 if ( dirListThread )
366 dirListThread->requestTermination();
369 ///////////////////////////////////////////////////////
370 ///////////////////////////////////////////////////////
371 // MyURL - wrapper for KUrl with some different functionality
374 class KUrlCompletionPrivate::MyURL
376 public:
377 MyURL(const QString &url, const QString &cwd);
378 MyURL(const MyURL &url);
379 ~MyURL();
381 KUrl *kurl() const { return m_kurl; }
383 QString protocol() const { return m_kurl->protocol(); }
384 // The directory with a trailing '/'
385 QString dir() const { return m_kurl->directory(KUrl::AppendTrailingSlash|KUrl::ObeyTrailingSlash); }
386 QString file() const { return m_kurl->fileName(KUrl::ObeyTrailingSlash); }
388 // The initial, unparsed, url, as a string.
389 QString url() const { return m_url; }
391 // Is the initial string a URL, or just a path (whether absolute or relative)
392 bool isURL() const { return m_isURL; }
394 void filter( bool replace_user_dir, bool replace_env );
396 private:
397 void init(const QString &url, const QString &cwd);
399 KUrl *m_kurl;
400 QString m_url;
401 bool m_isURL;
404 KUrlCompletionPrivate::MyURL::MyURL(const QString &_url, const QString &cwd)
406 init(_url, cwd);
409 KUrlCompletionPrivate::MyURL::MyURL(const MyURL &_url)
411 m_kurl = new KUrl( *(_url.m_kurl) );
412 m_url = _url.m_url;
413 m_isURL = _url.m_isURL;
416 void KUrlCompletionPrivate::MyURL::init(const QString &_url, const QString &cwd)
418 // Save the original text
419 m_url = _url;
421 // Non-const copy
422 QString url_copy = _url;
424 // Special shortcuts for "man:" and "info:"
425 if ( url_copy.startsWith( QLatin1Char('#') ) ) {
426 if ( url_copy.length() > 1 && url_copy.at(1) == QLatin1Char('#') )
427 url_copy.replace( 0, 2, QLatin1String("info:") );
428 else
429 url_copy.replace( 0, 1, QLatin1String("man:") );
432 // Look for a protocol in 'url'
433 QRegExp protocol_regex = QRegExp( "^(?![A-Za-z]:)[^/\\s\\\\]*:" );
435 // Assume "file:" or whatever is given by 'cwd' if there is
436 // no protocol. (KUrl does this only for absolute paths)
437 if ( protocol_regex.indexIn( url_copy ) == 0 )
439 m_kurl = new KUrl( url_copy );
440 m_isURL = true;
442 else // relative path or ~ or $something
444 m_isURL = false;
445 if ( !QDir::isRelativePath(url_copy) ||
446 url_copy.startsWith( QLatin1Char('~') ) ||
447 url_copy.startsWith( QLatin1Char('$') ))
449 m_kurl = new KUrl;
450 m_kurl->setPath( url_copy );
452 else
454 if ( cwd.isEmpty() ) {
455 m_kurl = new KUrl( url_copy );
456 } else {
457 m_kurl = new KUrl( cwd );
458 m_kurl->addPath( url_copy );
464 KUrlCompletionPrivate::MyURL::~MyURL()
466 delete m_kurl;
469 void KUrlCompletionPrivate::MyURL::filter( bool replace_user_dir, bool replace_env )
471 QString d = dir() + file();
472 if ( replace_user_dir ) expandTilde( d );
473 if ( replace_env ) expandEnv( d );
474 m_kurl->setPath( d );
477 ///////////////////////////////////////////////////////
478 ///////////////////////////////////////////////////////
479 // KUrlCompletion
482 KUrlCompletion::KUrlCompletion() : KCompletion(),d(new KUrlCompletionPrivate(this))
484 d->init();
488 KUrlCompletion::KUrlCompletion( Mode _mode ) : KCompletion(),d(new KUrlCompletionPrivate(this))
490 d->init();
491 setMode ( _mode );
494 KUrlCompletion::~KUrlCompletion()
496 stop();
497 delete d;
501 void KUrlCompletionPrivate::init()
503 cwd = QDir::homePath();
505 replace_home = true;
506 replace_env = true;
507 last_no_hidden = false;
508 last_compl_type = 0;
509 list_job = 0L;
510 mode = KUrlCompletion::FileCompletion;
512 // Read settings
513 KConfigGroup cg( KGlobal::config(), "URLCompletion" );
515 url_auto_completion = cg.readEntry("alwaysAutoComplete", true);
516 popup_append_slash = cg.readEntry("popupAppendSlash", true);
517 onlyLocalProto = cg.readEntry("LocalProtocolsOnly", false);
519 q->setIgnoreCase(true);
522 void KUrlCompletion::setDir(const QString &_dir)
524 d->cwd = _dir;
527 QString KUrlCompletion::dir() const
529 return d->cwd;
532 KUrlCompletion::Mode KUrlCompletion::mode() const
534 return d->mode;
537 void KUrlCompletion::setMode( Mode _mode )
539 d->mode = _mode;
542 bool KUrlCompletion::replaceEnv() const
544 return d->replace_env;
547 void KUrlCompletion::setReplaceEnv( bool replace )
549 d->replace_env = replace;
552 bool KUrlCompletion::replaceHome() const
554 return d->replace_home;
557 void KUrlCompletion::setReplaceHome( bool replace )
559 d->replace_home = replace;
563 * makeCompletion()
565 * Entry point for file name completion
567 QString KUrlCompletion::makeCompletion(const QString &text)
569 //kDebug() << text << "d->cwd=" << d->cwd;
571 KUrlCompletionPrivate::MyURL url(text, d->cwd);
573 d->compl_text = text;
575 // Set d->prepend to the original URL, with the filename [and ref/query] stripped.
576 // This is what gets prepended to the directory-listing matches.
577 int toRemove = url.file().length() - url.kurl()->query().length();
578 if ( url.kurl()->hasRef() )
579 toRemove += url.kurl()->ref().length() + 1;
580 d->prepend = text.left( text.length() - toRemove );
581 d->complete_url = url.isURL();
583 QString aMatch;
585 // Environment variables
587 if ( d->replace_env && d->envCompletion( url, &aMatch ) )
588 return aMatch;
590 // User directories
592 if ( d->replace_home && d->userCompletion( url, &aMatch ) )
593 return aMatch;
595 // Replace user directories and variables
596 url.filter( d->replace_home, d->replace_env );
598 //kDebug() << "Filtered: proto=" << url.protocol()
599 // << ", dir=" << url.dir()
600 // << ", file=" << url.file()
601 // << ", kurl url=" << *url.kurl();
603 if ( d->mode == ExeCompletion ) {
604 // Executables
606 if ( d->exeCompletion( url, &aMatch ) )
607 return aMatch;
609 // KRun can run "man:" and "info:" etc. so why not treat them
610 // as executables...
612 if ( d->urlCompletion( url, &aMatch ) )
613 return aMatch;
615 else {
616 // Local files, directories
618 if ( d->fileCompletion( url, &aMatch ) )
619 return aMatch;
621 // All other...
623 if ( d->urlCompletion( url, &aMatch ) )
624 return aMatch;
627 d->setListedUrl( CTNone );
628 stop();
630 return QString();
634 * finished
636 * Go on and call KCompletion.
637 * Called when all matches have been added
639 QString KUrlCompletionPrivate::finished()
641 if ( last_compl_type == CTInfo )
642 return q->KCompletion::makeCompletion( compl_text.toLower() );
643 else
644 return q->KCompletion::makeCompletion( compl_text );
648 * isRunning
650 * Return true if either a KIO job or the DirLister
651 * is running
653 bool KUrlCompletion::isRunning() const
655 return d->list_job || (d->dirListThread && !d->dirListThread->isFinished());
659 * stop
661 * Stop and delete a running KIO job or the DirLister
663 void KUrlCompletion::stop()
665 if ( d->list_job ) {
666 d->list_job->kill();
667 d->list_job = 0L;
670 while ( !d->list_urls.isEmpty() ) {
671 delete d->list_urls.takeFirst();
674 if ( d->dirListThread ) {
675 d->dirListThread->requestTermination();
676 d->dirListThread = 0;
681 * Keep track of the last listed directory
683 void KUrlCompletionPrivate::setListedUrl( int complType,
684 const QString& directory,
685 const QString& filter,
686 bool no_hidden )
688 last_compl_type = complType;
689 last_path_listed = directory;
690 last_file_listed = filter;
691 last_no_hidden = (int)no_hidden;
692 last_prepend = prepend;
695 bool KUrlCompletionPrivate::isListedUrl( int complType,
696 const QString& directory,
697 const QString& filter,
698 bool no_hidden )
700 return last_compl_type == complType
701 && ( last_path_listed == directory
702 || (directory.isEmpty() && last_path_listed.isEmpty()) )
703 && ( filter.startsWith(last_file_listed)
704 || (filter.isEmpty() && last_file_listed.isEmpty()) )
705 && last_no_hidden == (int)no_hidden
706 && last_prepend == prepend; // e.g. relative path vs absolute
710 * isAutoCompletion
712 * Returns true if completion mode is Auto or Popup
714 bool KUrlCompletionPrivate::isAutoCompletion()
716 return q->completionMode() == KGlobalSettings::CompletionAuto
717 || q->completionMode() == KGlobalSettings::CompletionPopup
718 || q->completionMode() == KGlobalSettings::CompletionMan
719 || q->completionMode() == KGlobalSettings::CompletionPopupAuto;
721 //////////////////////////////////////////////////
722 //////////////////////////////////////////////////
723 // User directories
726 bool KUrlCompletionPrivate::userCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
728 if ( url.protocol() != QLatin1String("file")
729 || !url.dir().isEmpty()
730 || !url.file().startsWith( QLatin1Char('~') ) )
731 return false;
733 if ( !isListedUrl( CTUser ) ) {
734 q->stop();
735 q->clear();
737 if ( !userListThread ) {
738 userListThread = new UserListThread( this );
739 userListThread->start();
741 // If the thread finishes quickly make sure that the results
742 // are added to the first matching case.
744 userListThread->wait( 200 );
745 const QStringList l = userListThread->matches();
746 addMatches( l );
749 *pMatch = finished();
750 return true;
753 /////////////////////////////////////////////////////
754 /////////////////////////////////////////////////////
755 // Environment variables
758 #ifndef Q_OS_WIN
759 extern char **environ; // Array of environment variables
760 #endif
762 bool KUrlCompletionPrivate::envCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
764 if ( url.file().isEmpty() || url.file().at(0) != QLatin1Char('$') )
765 return false;
767 if ( !isListedUrl( CTEnv ) ) {
768 q->stop();
769 q->clear();
771 char **env = environ;
773 QString dollar = QLatin1String("$");
775 QStringList l;
777 while ( *env ) {
778 QString s = QString::fromLocal8Bit( *env );
780 int pos = s.indexOf(QLatin1Char('='));
782 if ( pos == -1 )
783 pos = s.length();
785 if ( pos > 0 )
786 l.append( prepend + dollar + s.left(pos) );
788 env++;
791 addMatches( l );
794 setListedUrl( CTEnv );
796 *pMatch = finished();
797 return true;
800 //////////////////////////////////////////////////
801 //////////////////////////////////////////////////
802 // Executables
805 bool KUrlCompletionPrivate::exeCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
807 if ( url.protocol() != QLatin1String("file") )
808 return false;
810 QString directory = unescape( url.dir() ); // remove escapes
812 // Find directories to search for completions, either
814 // 1. complete path given in url
815 // 2. current directory (d->cwd)
816 // 3. $PATH
817 // 4. no directory at all
819 QStringList dirList;
821 if ( !QDir::isRelativePath(directory) ) {
822 // complete path in url
823 dirList.append( directory );
825 else if ( !directory.isEmpty() && !cwd.isEmpty() ) {
826 // current directory
827 dirList.append( cwd + QLatin1Char('/') + directory );
829 else if ( !url.file().isEmpty() ) {
830 // $PATH
831 dirList = QString::fromLocal8Bit(qgetenv("PATH")).split(
832 KPATH_SEPARATOR,QString::SkipEmptyParts);
834 QStringList::Iterator it = dirList.begin();
836 for ( ; it != dirList.end(); ++it )
837 it->append(QLatin1Char('/'));
840 // No hidden files unless the user types "."
841 bool no_hidden_files = url.file().isEmpty() || url.file().at(0) != QLatin1Char('.');
843 // List files if needed
845 if ( !isListedUrl( CTExe, directory, url.file(), no_hidden_files ) )
847 q->stop();
848 q->clear();
850 setListedUrl( CTExe, directory, url.file(), no_hidden_files );
852 *pMatch = listDirectories( dirList, url.file(), true, false, no_hidden_files );
854 else if ( !q->isRunning() ) {
855 *pMatch = finished();
857 else {
858 if ( dirListThread )
859 setListedUrl( CTExe, directory, url.file(), no_hidden_files );
860 pMatch->clear();
863 return true;
866 //////////////////////////////////////////////////
867 //////////////////////////////////////////////////
868 // Local files
871 bool KUrlCompletionPrivate::fileCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
873 if ( url.protocol() != QLatin1String("file") )
874 return false;
876 QString directory = unescape( url.dir() );
878 if (url.url().length() && url.url().at(0) == QLatin1Char('.'))
880 if (url.url().length() == 1)
882 *pMatch = ( q->completionMode() == KGlobalSettings::CompletionMan )?
883 QLatin1String(".") :
884 QLatin1String("..");
885 return true;
887 else if (url.url().length() == 2 && url.url().at(1) == QLatin1Char('.'))
889 *pMatch = QLatin1String("..");
890 return true;
894 //kDebug() << "fileCompletion" << url << "dir=" << dir;
896 // Find directories to search for completions, either
898 // 1. complete path given in url
899 // 2. current directory (d->cwd)
900 // 3. no directory at all
902 QStringList dirList;
904 if ( !QDir::isRelativePath(directory) ) {
905 // complete path in url
906 dirList.append( directory );
908 else if ( !cwd.isEmpty() ) {
909 // current directory
910 QString dirToAdd = cwd;
911 if ( !directory.isEmpty() ) {
912 if ( !cwd.endsWith('/') )
913 dirToAdd.append( QLatin1Char('/') );
914 dirToAdd.append( directory );
916 dirList.append( dirToAdd );
919 // No hidden files unless the user types "."
920 bool no_hidden_files = !url.file().startsWith( QLatin1Char('.') );
922 // List files if needed
924 if ( !isListedUrl( CTFile, directory, QString(), no_hidden_files ) )
926 q->stop();
927 q->clear();
929 setListedUrl( CTFile, directory, QString(), no_hidden_files );
931 // Append '/' to directories in Popup mode?
932 bool append_slash = ( popup_append_slash
933 && (q->completionMode() == KGlobalSettings::CompletionPopup ||
934 q->completionMode() == KGlobalSettings::CompletionPopupAuto ) );
936 bool only_dir = ( mode == KUrlCompletion::DirCompletion );
938 *pMatch = listDirectories( dirList, QString(), false, only_dir, no_hidden_files,
939 append_slash );
941 else if ( !q->isRunning() ) {
942 *pMatch = finished();
944 else {
945 pMatch->clear();
948 return true;
951 //////////////////////////////////////////////////
952 //////////////////////////////////////////////////
953 // URLs not handled elsewhere...
956 bool KUrlCompletionPrivate::urlCompletion(const KUrlCompletionPrivate::MyURL &url, QString *pMatch)
958 //kDebug() << *url.kurl();
959 if (onlyLocalProto && KProtocolInfo::protocolClass(url.protocol()) != QLatin1String(":local"))
960 return false;
962 // Use d->cwd as base url in case url is not absolute
963 KUrl url_cwd( cwd );
965 // Create an URL with the directory to be listed
966 KUrl url_dir( url_cwd, url.kurl()->url() );
968 // Don't try url completion if
969 // 1. malformed url
970 // 2. protocol that doesn't have listDir()
971 // 3. there is no directory (e.g. "ftp://ftp.kd" shouldn't do anything)
972 // 4. auto or popup completion mode depending on settings
974 bool man_or_info = ( url_dir.protocol() == QLatin1String("man")
975 || url_dir.protocol() == QLatin1String("info") );
977 if ( !url_dir.isValid()
978 || !KProtocolManager::supportsListing( url_dir )
979 || ( !man_or_info
980 && ( url_dir.directory(KUrl::AppendTrailingSlash|KUrl::ObeyTrailingSlash).isEmpty()
981 || ( isAutoCompletion()
982 && !url_auto_completion ) ) ) ) {
983 return false;
986 url_dir.setFileName(QString()); // not really nesseccary, but clear the filename anyway...
988 // Remove escapes
989 QString directory = unescape( url_dir.directory(KUrl::AppendTrailingSlash|KUrl::ObeyTrailingSlash) );
991 url_dir.setPath( directory );
993 // List files if needed
995 if ( !isListedUrl( CTUrl, url_dir.prettyUrl(), url.file() ) )
997 q->stop();
998 q->clear();
1000 setListedUrl( CTUrl, url_dir.prettyUrl(), QString() );
1002 QList<KUrl*> url_list;
1003 url_list.append( new KUrl( url_dir ) );
1005 listUrls( url_list, QString(), false );
1007 pMatch->clear();
1009 else if ( !q->isRunning() ) {
1010 *pMatch = finished();
1012 else {
1013 pMatch->clear();
1016 return true;
1019 //////////////////////////////////////////////////
1020 //////////////////////////////////////////////////
1021 // Directory and URL listing
1025 * addMatches
1027 * Called to add matches to KCompletion
1029 void KUrlCompletionPrivate::addMatches( const QStringList &matchList )
1031 q->insertItems(matchList);
1035 * listDirectories
1037 * List files starting with 'filter' in the given directories,
1038 * either using DirLister or listURLs()
1040 * In either case, addMatches() is called with the listed
1041 * files, and eventually finished() when the listing is done
1043 * Returns the match if available, or QString() if
1044 * DirLister timed out or using kio
1046 QString KUrlCompletionPrivate::listDirectories(
1047 const QStringList &dirList,
1048 const QString &filter,
1049 bool only_exe,
1050 bool only_dir,
1051 bool no_hidden,
1052 bool append_slash_to_dir)
1054 assert( !q->isRunning() );
1056 if ( qgetenv("KURLCOMPLETION_LOCAL_KIO").isEmpty() ) {
1058 //kDebug() << "Listing (listDirectories):" << dirList << "filter=" << filter << "without KIO";
1060 // Don't use KIO
1062 if ( dirListThread )
1063 dirListThread->requestTermination();
1065 QStringList dirs;
1067 QStringList::ConstIterator end = dirList.constEnd();
1068 for ( QStringList::ConstIterator it = dirList.constBegin();
1069 it != end;
1070 ++it )
1072 KUrl url;
1073 url.setPath(*it);
1074 if ( KAuthorized::authorizeUrlAction( QLatin1String("list"), KUrl(), url ) )
1075 dirs.append( *it );
1078 dirListThread = new DirectoryListThread( this, dirs, filter, only_exe, only_dir,
1079 no_hidden, append_slash_to_dir );
1080 dirListThread->start();
1081 dirListThread->wait( 200 );
1082 addMatches( dirListThread->matches() );
1084 return finished();
1087 // Use KIO
1088 //kDebug() << "Listing (listDirectories):" << dirList << "with KIO";
1090 QList<KUrl*> url_list;
1092 QStringList::ConstIterator it = dirList.constBegin();
1093 QStringList::ConstIterator end = dirList.constEnd();
1095 for ( ; it != end; ++it ) {
1096 url_list.append( new KUrl( *it ) );
1099 listUrls( url_list, filter, only_exe, no_hidden );
1100 // Will call addMatches() and finished()
1102 return QString();
1106 * listURLs
1108 * Use KIO to list the given urls
1110 * addMatches() is called with the listed files
1111 * finished() is called when the listing is done
1113 void KUrlCompletionPrivate::listUrls(
1114 const QList<KUrl *> &urls,
1115 const QString &filter,
1116 bool only_exe,
1117 bool no_hidden )
1119 assert( list_urls.isEmpty() );
1120 assert( list_job == 0L );
1122 list_urls = urls;
1123 list_urls_filter = filter;
1124 list_urls_only_exe = only_exe;
1125 list_urls_no_hidden = no_hidden;
1127 //kDebug() << "Listing URLs:" << *urls[0] << ",...";
1129 // Start it off by calling _k_slotIOFinished
1131 // This will start a new list job as long as there
1132 // are urls in d->list_urls
1134 _k_slotIOFinished(0);
1138 * _k_slotEntries
1140 * Receive files listed by KIO and call addMatches()
1142 void KUrlCompletionPrivate::_k_slotEntries(KIO::Job*, const KIO::UDSEntryList& entries)
1144 QStringList matchList;
1146 KIO::UDSEntryList::ConstIterator it = entries.constBegin();
1147 const KIO::UDSEntryList::ConstIterator end = entries.constEnd();
1149 QString filter = list_urls_filter;
1151 int filter_len = filter.length();
1153 // Iterate over all files
1155 for (; it != end; ++it) {
1156 const KIO::UDSEntry& entry = *it;
1157 const QString url = entry.stringValue( KIO::UDSEntry::UDS_URL );
1159 QString entry_name;
1160 if (!url.isEmpty()) {
1161 // kDebug() << "url:" << url;
1162 entry_name = KUrl(url).fileName();
1163 } else {
1164 entry_name = entry.stringValue( KIO::UDSEntry::UDS_NAME );
1167 // kDebug() << "name:" << name;
1169 if ( (!entry_name.isEmpty() && entry_name.at(0) == QLatin1Char('.')) &&
1170 ( list_urls_no_hidden ||
1171 entry_name.length() == 1 ||
1172 ( entry_name.length() == 2 && entry_name.at(1) == QLatin1Char('.') ) ) )
1173 continue;
1175 const bool isDir = entry.isDir();
1177 if ( mode == KUrlCompletion::DirCompletion && !isDir )
1178 continue;
1180 if ( filter_len == 0 || entry_name.left(filter_len) == filter ) {
1182 QString toAppend = complete_url ? QUrl::toPercentEncoding(entry_name) : entry_name;
1184 if (isDir)
1185 toAppend.append( QLatin1Char( '/' ) );
1187 if ( !list_urls_only_exe ||
1188 (entry.numberValue( KIO::UDSEntry::UDS_ACCESS ) & MODE_EXE) // true if executable
1190 matchList.append( prepend + toAppend );
1195 addMatches( matchList );
1199 * _k_slotIOFinished
1201 * Called when a KIO job is finished.
1203 * Start a new list job if there are still urls in
1204 * list_urls, otherwise call finished()
1206 void KUrlCompletionPrivate::_k_slotIOFinished( KJob * job )
1208 assert( job == list_job );
1210 if ( list_urls.isEmpty() ) {
1212 list_job = 0L;
1214 finished(); // will call KCompletion::makeCompletion()
1217 else {
1219 KUrl *kurl = list_urls.takeFirst();
1221 // list_urls.removeAll( kurl );
1223 // kDebug() << "Start KIO::listDir" << *kurl;
1225 list_job = KIO::listDir( *kurl, KIO::HideProgressInfo );
1226 list_job->addMetaData("no-auth-prompt", "true");
1228 assert( list_job );
1230 q->connect( list_job,
1231 SIGNAL(result(KJob*)),
1232 SLOT(_k_slotIOFinished(KJob*)) );
1234 q->connect( list_job,
1235 SIGNAL( entries( KIO::Job*, const KIO::UDSEntryList&)),
1236 SLOT( _k_slotEntries( KIO::Job*, const KIO::UDSEntryList&)) );
1238 delete kurl;
1242 ///////////////////////////////////////////////////
1243 ///////////////////////////////////////////////////
1246 * postProcessMatch, postProcessMatches
1248 * Called by KCompletion before emitting match() and matches()
1250 * Append '/' to directories for file completion. This is
1251 * done here to avoid stat()'ing a lot of files
1253 void KUrlCompletion::postProcessMatch( QString *pMatch ) const
1255 // kDebug() << *pMatch;
1257 if ( !pMatch->isEmpty() ) {
1259 // Add '/' to directories in file completion mode
1260 // unless it has already been done
1261 if ( d->last_compl_type == CTFile
1262 && pMatch->at( pMatch->length()-1 ) != QLatin1Char('/') )
1264 QString copy;
1266 if ( pMatch->startsWith( QLatin1String("file:") ) )
1267 copy = KUrl(*pMatch).toLocalFile();
1268 else
1269 copy = *pMatch;
1271 expandTilde( copy );
1272 expandEnv( copy );
1273 #ifdef Q_WS_WIN
1274 DWORD dwAttr = GetFileAttributesW( (LPCWSTR) copy.utf16() );
1275 if ( dwAttr == INVALID_FILE_ATTRIBUTES ) {
1276 kDebug() << "Could not get file attribs ( "
1277 << GetLastError()
1278 << " ) for "
1279 << copy;
1280 } else
1281 if ( ( dwAttr & FILE_ATTRIBUTE_DIRECTORY ) == FILE_ATTRIBUTE_DIRECTORY )
1282 pMatch->append( QLatin1Char( '/' ) );
1283 #else
1284 if ( QDir::isRelativePath(copy) )
1285 copy.prepend( d->cwd + QLatin1Char('/') );
1287 // kDebug() << "stat'ing" << copy;
1289 KDE_struct_stat sbuff;
1291 QByteArray file = QFile::encodeName( copy );
1293 if ( KDE_stat( file.data(), &sbuff ) == 0 ) {
1294 if ( S_ISDIR ( sbuff.st_mode ) )
1295 pMatch->append( QLatin1Char( '/' ) );
1297 else {
1298 kDebug() << "Could not stat file" << copy;
1300 #endif
1305 void KUrlCompletion::postProcessMatches( QStringList * /*matches*/ ) const
1307 // Maybe '/' should be added to directories here as in
1308 // postProcessMatch() but it would slow things down
1309 // when there are a lot of matches...
1312 void KUrlCompletion::postProcessMatches( KCompletionMatches * /*matches*/ ) const
1314 // Maybe '/' should be added to directories here as in
1315 // postProcessMatch() but it would slow things down
1316 // when there are a lot of matches...
1319 void KUrlCompletion::customEvent(QEvent *e)
1321 if ( e->type() == CompletionMatchEvent::uniqueType() ) {
1323 CompletionMatchEvent *matchEvent = static_cast<CompletionMatchEvent *>( e );
1325 matchEvent->completionThread()->wait();
1327 if ( !d->isListedUrl( CTUser ) ) {
1328 stop();
1329 clear();
1330 d->addMatches( matchEvent->completionThread()->matches() );
1333 d->setListedUrl( CTUser );
1335 if ( d->userListThread == matchEvent->completionThread() )
1336 d->userListThread = 0;
1338 if ( d->dirListThread == matchEvent->completionThread() )
1339 d->dirListThread = 0;
1341 delete matchEvent->completionThread();
1345 // static
1346 QString KUrlCompletion::replacedPath( const QString& text, bool replaceHome, bool replaceEnv )
1348 if ( text.isEmpty() )
1349 return text;
1351 KUrlCompletionPrivate::MyURL url( text, QString() ); // no need to replace something of our current cwd
1352 if ( !url.kurl()->isLocalFile() )
1353 return text;
1355 url.filter( replaceHome, replaceEnv );
1356 return url.dir() + url.file();
1360 QString KUrlCompletion::replacedPath( const QString& text ) const
1362 return replacedPath( text, d->replace_home, d->replace_env );
1365 /////////////////////////////////////////////////////////
1366 /////////////////////////////////////////////////////////
1367 // Static functions
1370 * expandEnv
1372 * Expand environment variables in text. Escaped '$' are ignored.
1373 * Return true if expansion was made.
1375 static bool expandEnv( QString &text )
1377 // Find all environment variables beginning with '$'
1379 int pos = 0;
1381 bool expanded = false;
1383 while ( (pos = text.indexOf(QLatin1Char('$'), pos)) != -1 ) {
1385 // Skip escaped '$'
1387 if ( pos > 0 && text.at(pos-1) == QLatin1Char('\\') ) {
1388 pos++;
1390 // Variable found => expand
1392 else {
1393 // Find the end of the variable = next '/' or ' '
1395 int pos2 = text.indexOf( QLatin1Char(' '), pos+1 );
1396 int pos_tmp = text.indexOf( QLatin1Char('/'), pos+1 );
1398 if ( pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2) )
1399 pos2 = pos_tmp;
1401 if ( pos2 == -1 )
1402 pos2 = text.length();
1404 // Replace if the variable is terminated by '/' or ' '
1405 // and defined
1407 if ( pos2 >= 0 ) {
1408 int len = pos2 - pos;
1409 QString key = text.mid( pos+1, len-1);
1410 QString value =
1411 QString::fromLocal8Bit( qgetenv(key.toLocal8Bit()) );
1413 if ( !value.isEmpty() ) {
1414 expanded = true;
1415 text.replace( pos, len, value );
1416 pos = pos + value.length();
1418 else {
1419 pos = pos2;
1425 return expanded;
1429 * expandTilde
1431 * Replace "~user" with the users home directory
1432 * Return true if expansion was made.
1434 static bool expandTilde(QString &text)
1436 if ( text.isEmpty() || ( text.at(0) != QLatin1Char('~') ))
1437 return false;
1439 bool expanded = false;
1441 // Find the end of the user name = next '/' or ' '
1443 int pos2 = text.indexOf( QLatin1Char(' '), 1 );
1444 int pos_tmp = text.indexOf( QLatin1Char('/'), 1 );
1446 if ( pos2 == -1 || (pos_tmp != -1 && pos_tmp < pos2) )
1447 pos2 = pos_tmp;
1449 if ( pos2 == -1 )
1450 pos2 = text.length();
1452 // Replace ~user if the user name is terminated by '/' or ' '
1454 if ( pos2 >= 0 ) {
1456 QString user = text.mid( 1, pos2-1 );
1457 QString dir;
1459 // A single ~ is replaced with $HOME
1461 if ( user.isEmpty() ) {
1462 dir = QDir::homePath();
1464 // ~user is replaced with the dir from passwd
1466 else {
1467 struct passwd *pw = ::getpwnam( user.toLocal8Bit() );
1469 if ( pw )
1470 dir = QFile::decodeName( pw->pw_dir );
1472 ::endpwent();
1475 if ( !dir.isEmpty() ) {
1476 expanded = true;
1477 text.replace(0, pos2, dir);
1481 return expanded;
1485 * unescape
1487 * Remove escapes and return the result in a new string
1490 static QString unescape(const QString &text)
1492 QString result;
1494 for (int pos = 0; pos < text.length(); pos++)
1495 if ( text.at(pos) != QLatin1Char('\\') )
1496 result.insert( result.length(), text.at(pos) );
1498 return result;
1501 #include "kurlcompletion.moc"