1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "documentmanager.h"
27 
28 #include "icore.h"
29 #include "idocument.h"
30 #include "idocumentfactory.h"
31 #include "coreconstants.h"
32 
33 #include <coreplugin/actionmanager/actioncontainer.h>
34 #include <coreplugin/actionmanager/actionmanager.h>
35 #include <coreplugin/actionmanager/command.h>
36 #include <coreplugin/diffservice.h>
37 #include <coreplugin/dialogs/filepropertiesdialog.h>
38 #include <coreplugin/dialogs/readonlyfilesdialog.h>
39 #include <coreplugin/dialogs/saveitemsdialog.h>
40 #include <coreplugin/editormanager/editormanager.h>
41 #include <coreplugin/editormanager/editormanager_p.h>
42 #include <coreplugin/editormanager/editorview.h>
43 #include <coreplugin/editormanager/ieditor.h>
44 #include <coreplugin/editormanager/ieditorfactory.h>
45 #include <coreplugin/editormanager/iexternaleditor.h>
46 
47 
48 #include <extensionsystem/pluginmanager.h>
49 
50 #include <utils/algorithm.h>
51 #include <utils/fileutils.h>
52 #include <utils/globalfilechangeblocker.h>
53 #include <utils/hostosinfo.h>
54 #include <utils/mimetypes/mimedatabase.h>
55 #include <utils/optional.h>
56 #include <utils/pathchooser.h>
57 #include <utils/qtcassert.h>
58 #include <utils/reloadpromptutils.h>
59 
60 #include <QStringList>
61 #include <QDateTime>
62 #include <QDir>
63 #include <QFile>
64 #include <QFileInfo>
65 #include <QFileSystemWatcher>
66 #include <QLoggingCategory>
67 #include <QSettings>
68 #include <QTimer>
69 #include <QAction>
70 #include <QFileDialog>
71 #include <QMainWindow>
72 #include <QMenu>
73 #include <QMessageBox>
74 
75 static const bool kUseProjectsDirectoryDefault = true;
76 static Q_LOGGING_CATEGORY(log, "qtc.core.documentmanager", QtWarningMsg)
77 
78 /*!
79   \class Core::DocumentManager
80   \inheaderfile coreplugin/documentmanager.h
81   \ingroup mainclasses
82   \inmodule QtCreator
83 
84   \brief The DocumentManager class manages a set of documents.
85 
86   The DocumentManager service monitors a set of IDocument objects.
87 
88   This section uses the following terminology:
89 
90   \list
91     \li A \e file means a collection of data stored on a disk under a name
92         (that is, the usual meaning of the term \e file in computing).
93     \li A \e document holds content open in Qt Creator. If it corresponds to a
94         file, it might differ from it, because it was modified. But a document
95         might not correspond to a file at all. For example, diff viewer
96         documents or Git blame or log records are created and displayed by
97         Qt Creator upon request.
98     \li An \a editor provides a view into a document that is actually visible
99         to the user and potentially allows editing the document. Multiple
100         editors can open views into the same document.
101   \endlist
102 
103   Plugins should register documents they work with at the document management
104   service. The files the IDocument objects point to will be monitored at
105   file system level. If a file changes on disk, the status of the IDocument
106   object will be adjusted accordingly. On application exit the user will be
107   asked to save all modified documents.
108 
109   Different IDocument objects in the set can point to the same file in the
110   file system. The monitoring for an IDocument can be blocked by
111   using the \l Core::FileChangeBlocker class.
112 
113   The functions \c expectFileChange() and \c unexpectFileChange() mark a file change
114   as expected. On expected file changes all IDocument objects are notified to reload
115   themselves.
116 
117   The DocumentManager service also provides convenience functions
118   for saving documents, such as \l saveModifiedDocuments() and
119   \l saveModifiedDocumentsSilently(). They present users with a
120   dialog that lists all modified documents and asks them which
121   documents should be saved.
122 
123   The service also manages the list of recent files to be shown to the user.
124 
125   \sa addToRecentFiles(), recentFiles()
126  */
127 
128 static const char settingsGroupC[] = "RecentFiles";
129 static const char filesKeyC[] = "Files";
130 static const char editorsKeyC[] = "EditorIds";
131 
132 static const char directoryGroupC[] = "Directories";
133 static const char projectDirectoryKeyC[] = "Projects";
134 static const char useProjectDirectoryKeyC[] = "UseProjectsDirectory";
135 
136 using namespace Utils;
137 
138 namespace Core {
139 
140 static void readSettings();
141 
142 static bool saveModifiedFilesHelper(const QList<IDocument *> &documents,
143                                     const QString &message,
144                                     bool *cancelled, bool silently,
145                                     const QString &alwaysSaveMessage,
146                                     bool *alwaysSave, QList<IDocument *> *failedToSave);
147 
148 namespace Internal {
149 
150 struct FileStateItem
151 {
152     QDateTime modified;
153     QFile::Permissions permissions;
154 };
155 
156 struct FileState
157 {
158     FilePath watchedFilePath;
159     QMap<IDocument *, FileStateItem> lastUpdatedState;
160     FileStateItem expected;
161 };
162 
163 
164 class DocumentManagerPrivate : public QObject
165 {
166     Q_OBJECT
167 public:
168     DocumentManagerPrivate();
169     QFileSystemWatcher *fileWatcher();
170     QFileSystemWatcher *linkWatcher();
171 
172     void checkOnNextFocusChange();
173     void onApplicationFocusChange();
174 
175     void registerSaveAllAction();
176 
177     QMap<FilePath, FileState> m_states; // filePathKey -> FileState
178     QSet<FilePath> m_changedFiles; // watched file paths collected from file watcher notifications
179     QList<IDocument *> m_documentsWithoutWatch;
180     QMap<IDocument *, FilePaths> m_documentsWithWatch; // document -> list of filePathKeys
181     QSet<FilePath> m_expectedFileNames; // set of file paths without normalization
182 
183     QList<DocumentManager::RecentFile> m_recentFiles;
184 
185     bool m_postponeAutoReload = false;
186     bool m_blockActivated = false;
187     bool m_checkOnFocusChange = false;
188     bool m_useProjectsDirectory = kUseProjectsDirectoryDefault;
189 
190     QFileSystemWatcher *m_fileWatcher = nullptr; // Delayed creation.
191     QFileSystemWatcher *m_linkWatcher = nullptr; // Delayed creation (only UNIX/if a link is seen).
192     QString m_lastVisitedDirectory = QDir::currentPath();
193     QString m_defaultLocationForNewFiles;
194     FilePath m_projectsDirectory;
195     // When we are calling into an IDocument
196     // we don't want to receive a changed()
197     // signal
198     // That makes the code easier
199     IDocument *m_blockedIDocument = nullptr;
200 
201     QAction *m_saveAllAction;
202 };
203 
204 static DocumentManager *m_instance;
205 static DocumentManagerPrivate *d;
206 
fileWatcher()207 QFileSystemWatcher *DocumentManagerPrivate::fileWatcher()
208 {
209     if (!m_fileWatcher) {
210         m_fileWatcher= new QFileSystemWatcher(m_instance);
211         QObject::connect(m_fileWatcher, &QFileSystemWatcher::fileChanged,
212                          m_instance, &DocumentManager::changedFile);
213     }
214     return m_fileWatcher;
215 }
216 
linkWatcher()217 QFileSystemWatcher *DocumentManagerPrivate::linkWatcher()
218 {
219     if (HostOsInfo::isAnyUnixHost()) {
220         if (!m_linkWatcher) {
221             m_linkWatcher = new QFileSystemWatcher(m_instance);
222             m_linkWatcher->setObjectName(QLatin1String("_qt_autotest_force_engine_poller"));
223             QObject::connect(m_linkWatcher, &QFileSystemWatcher::fileChanged,
224                              m_instance, &DocumentManager::changedFile);
225         }
226         return m_linkWatcher;
227     }
228 
229     return fileWatcher();
230 }
231 
checkOnNextFocusChange()232 void DocumentManagerPrivate::checkOnNextFocusChange()
233 {
234     m_checkOnFocusChange = true;
235 }
236 
onApplicationFocusChange()237 void DocumentManagerPrivate::onApplicationFocusChange()
238 {
239     if (!m_checkOnFocusChange)
240         return;
241     m_checkOnFocusChange = false;
242     m_instance->checkForReload();
243 }
244 
registerSaveAllAction()245 void DocumentManagerPrivate::registerSaveAllAction()
246 {
247     ActionContainer *mfile = ActionManager::actionContainer(Constants::M_FILE);
248     Command *cmd = ActionManager::registerAction(m_saveAllAction, Constants::SAVEALL);
249     cmd->setDefaultKeySequence(QKeySequence(useMacShortcuts ? QString() : tr("Ctrl+Shift+S")));
250     mfile->addAction(cmd, Constants::G_FILE_SAVE);
251     m_saveAllAction->setEnabled(false);
252     connect(m_saveAllAction, &QAction::triggered, []() {
253         DocumentManager::saveAllModifiedDocumentsSilently();
254     });
255 }
256 
DocumentManagerPrivate()257 DocumentManagerPrivate::DocumentManagerPrivate() :
258     m_saveAllAction(new QAction(tr("Save A&ll"), this))
259 {
260     // we do not want to do too much directly in the focus change event, so queue the connection
261     connect(qApp,
262             &QApplication::focusChanged,
263             this,
264             &DocumentManagerPrivate::onApplicationFocusChange,
265             Qt::QueuedConnection);
266 }
267 
268 } // namespace Internal
269 } // namespace Core
270 
271 namespace Core {
272 
273 using namespace Internal;
274 
DocumentManager(QObject * parent)275 DocumentManager::DocumentManager(QObject *parent)
276   : QObject(parent)
277 {
278     d = new DocumentManagerPrivate;
279     m_instance = this;
280 
281     connect(Utils::GlobalFileChangeBlocker::instance(), &Utils::GlobalFileChangeBlocker::stateChanged,
282             this, [](bool blocked) {
283         d->m_postponeAutoReload = blocked;
284         if (!blocked)
285             QTimer::singleShot(500, m_instance, &DocumentManager::checkForReload);
286     });
287 
288     readSettings();
289 
290     if (d->m_useProjectsDirectory)
291         setFileDialogLastVisitedDirectory(d->m_projectsDirectory.toString());
292 }
293 
~DocumentManager()294 DocumentManager::~DocumentManager()
295 {
296     delete d;
297 }
298 
instance()299 DocumentManager *DocumentManager::instance()
300 {
301     return m_instance;
302 }
303 
304 /* Only called from addFileInfo(IDocument *). Adds the document & state to various caches/lists,
305    but does not actually add a watcher. */
addFileInfo(IDocument * document,const FilePath & filePath,const FilePath & filePathKey)306 static void addFileInfo(IDocument *document, const FilePath &filePath, const FilePath &filePathKey)
307 {
308     FileStateItem state;
309     if (!filePath.isEmpty()) {
310         qCDebug(log) << "adding document for" << filePath << "(" << filePathKey << ")";
311         state.modified = filePath.lastModified();
312         state.permissions = filePath.permissions();
313         // Add state if we don't have already
314         if (!d->m_states.contains(filePathKey)) {
315             FileState state;
316             state.watchedFilePath = filePath;
317             d->m_states.insert(filePathKey, state);
318         }
319         d->m_states[filePathKey].lastUpdatedState.insert(document, state);
320     }
321     d->m_documentsWithWatch[document].append(filePathKey); // inserts a new QStringList if not already there
322 }
323 
324 /* Adds the IDocuments' file and possibly it's final link target to both m_states
325    (if it's file name is not empty), and the m_filesWithWatch list,
326    and adds a file watcher for each if not already done.
327    (The added file names are guaranteed to be absolute and cleaned.) */
addFileInfos(const QList<IDocument * > & documents)328 static void addFileInfos(const QList<IDocument *> &documents)
329 {
330     FilePaths pathsToWatch;
331     FilePaths linkPathsToWatch;
332     for (IDocument *document : documents) {
333         const FilePath filePath = DocumentManager::filePathKey(document->filePath(),
334                                                                DocumentManager::KeepLinks);
335         const FilePath resolvedFilePath = filePath.canonicalPath();
336         const bool isLink = filePath != resolvedFilePath;
337         addFileInfo(document, filePath, filePath);
338         if (isLink) {
339             addFileInfo(document, resolvedFilePath, resolvedFilePath);
340             if (!filePath.needsDevice()) {
341                 linkPathsToWatch.append(d->m_states.value(filePath).watchedFilePath);
342                 pathsToWatch.append(d->m_states.value(resolvedFilePath).watchedFilePath);
343             }
344         } else if (!filePath.needsDevice()) {
345             pathsToWatch.append(d->m_states.value(filePath).watchedFilePath);
346         }
347     }
348     // Add or update watcher on file path
349     // This is also used to update the watcher in case of saved (==replaced) files or
350     // update link targets, even if there are multiple documents registered for it
351     if (!pathsToWatch.isEmpty()) {
352         qCDebug(log) << "adding full watch for" << pathsToWatch;
353         d->fileWatcher()->addPaths(Utils::transform(pathsToWatch, &FilePath::toString));
354     }
355     if (!linkPathsToWatch.isEmpty()) {
356         qCDebug(log) << "adding link watch for" << linkPathsToWatch;
357         d->linkWatcher()->addPaths(Utils::transform(linkPathsToWatch, &FilePath::toString));
358     }
359 }
360 
361 /*!
362     Adds a list of \a documents to the collection. If \a addWatcher is \c true
363     (the default), the documents' files are added to a file system watcher that
364     notifies the document manager about file changes.
365 */
addDocuments(const QList<IDocument * > & documents,bool addWatcher)366 void DocumentManager::addDocuments(const QList<IDocument *> &documents, bool addWatcher)
367 {
368     if (!addWatcher) {
369         // We keep those in a separate list
370 
371         foreach (IDocument *document, documents) {
372             if (document && !d->m_documentsWithoutWatch.contains(document)) {
373                 connect(document, &QObject::destroyed,
374                         m_instance, &DocumentManager::documentDestroyed);
375                 connect(document, &IDocument::filePathChanged,
376                         m_instance, &DocumentManager::filePathChanged);
377                 connect(document, &IDocument::changed, m_instance, &DocumentManager::updateSaveAll);
378                 d->m_documentsWithoutWatch.append(document);
379             }
380         }
381         return;
382     }
383 
384     const QList<IDocument *> documentsToWatch = Utils::filtered(documents, [](IDocument *document) {
385         return document && !d->m_documentsWithWatch.contains(document);
386     });
387     for (IDocument *document : documentsToWatch) {
388         connect(document, &IDocument::changed, m_instance, &DocumentManager::checkForNewFileName);
389         connect(document, &QObject::destroyed, m_instance, &DocumentManager::documentDestroyed);
390         connect(document, &IDocument::filePathChanged,
391                 m_instance, &DocumentManager::filePathChanged);
392         connect(document, &IDocument::changed, m_instance, &DocumentManager::updateSaveAll);
393     }
394     addFileInfos(documentsToWatch);
395 }
396 
397 
398 /* Removes all occurrences of the IDocument from m_filesWithWatch and m_states.
399    If that results in a file no longer being referenced by any IDocument, this
400    also removes the file watcher.
401 */
removeFileInfo(IDocument * document)402 static void removeFileInfo(IDocument *document)
403 {
404     if (!d->m_documentsWithWatch.contains(document))
405         return;
406     foreach (const FilePath &filePath, d->m_documentsWithWatch.value(document)) {
407         if (!d->m_states.contains(filePath))
408             continue;
409         qCDebug(log) << "removing document (" << filePath << ")";
410         d->m_states[filePath].lastUpdatedState.remove(document);
411         if (d->m_states.value(filePath).lastUpdatedState.isEmpty()) {
412             const FilePath &watchedFilePath = d->m_states.value(filePath).watchedFilePath;
413             if (!watchedFilePath.needsDevice()) {
414                 const QString &localFilePath = watchedFilePath.path();
415                 if (d->m_fileWatcher
416                     && d->m_fileWatcher->files().contains(localFilePath)) {
417                     qCDebug(log) << "removing watch for" << localFilePath;
418                     d->m_fileWatcher->removePath(localFilePath);
419                 }
420                 if (d->m_linkWatcher
421                     && d->m_linkWatcher->files().contains(localFilePath)) {
422                     qCDebug(log) << "removing watch for" << localFilePath;
423                     d->m_linkWatcher->removePath(localFilePath);
424                 }
425             }
426             d->m_states.remove(filePath);
427         }
428     }
429     d->m_documentsWithWatch.remove(document);
430 }
431 
432 /// Dumps the state of the file manager's map
433 /// For debugging purposes
434 /*
435 static void dump()
436 {
437     qDebug() << "======== dumping state map";
438     QMap<QString, FileState>::const_iterator it, end;
439     it = d->m_states.constBegin();
440     end = d->m_states.constEnd();
441     for (; it != end; ++it) {
442         qDebug() << it.key();
443         qDebug() << "   expected:" << it.value().expected.modified;
444 
445         QMap<IDocument *, FileStateItem>::const_iterator jt, jend;
446         jt = it.value().lastUpdatedState.constBegin();
447         jend = it.value().lastUpdatedState.constEnd();
448         for (; jt != jend; ++jt) {
449             qDebug() << "  " << jt.key()->fileName() << jt.value().modified;
450         }
451     }
452     qDebug() << "------- dumping files with watch list";
453     foreach (IDocument *key, d->m_filesWithWatch.keys()) {
454         qDebug() << key->fileName() << d->m_filesWithWatch.value(key);
455     }
456     qDebug() << "------- dumping watch list";
457     if (d->m_fileWatcher)
458         qDebug() << d->m_fileWatcher->files();
459     qDebug() << "------- dumping link watch list";
460     if (d->m_linkWatcher)
461         qDebug() << d->m_linkWatcher->files();
462 }
463 */
464 
465 /*!
466     Tells the document manager that a file has been renamed from \a from to
467     \a to on disk from within \QC.
468 
469     Needs to be called right after the actual renaming on disk (that is, before
470     the file system watcher can report the event during the next event loop run).
471 
472     \a from needs to be an absolute file path.
473     This will notify all IDocument objects pointing to that file of the rename
474     by calling \l IDocument::setFilePath(), and update the cached time and
475     permission information to avoid annoying the user with \e {the file has
476     been removed} popups.
477 */
renamedFile(const Utils::FilePath & from,const Utils::FilePath & to)478 void DocumentManager::renamedFile(const Utils::FilePath &from, const Utils::FilePath &to)
479 {
480     const Utils::FilePath &fromKey = filePathKey(from, KeepLinks);
481 
482     // gather the list of IDocuments
483     QList<IDocument *> documentsToRename;
484     for (auto it = d->m_documentsWithWatch.cbegin(), end = d->m_documentsWithWatch.cend();
485             it != end; ++it) {
486         if (it.value().contains(fromKey))
487             documentsToRename.append(it.key());
488     }
489 
490     // rename the IDocuments
491     foreach (IDocument *document, documentsToRename) {
492         d->m_blockedIDocument = document;
493         removeFileInfo(document);
494         document->setFilePath(to);
495         addFileInfos({document});
496         d->m_blockedIDocument = nullptr;
497     }
498     emit m_instance->allDocumentsRenamed(from, to);
499 }
500 
filePathChanged(const FilePath & oldName,const FilePath & newName)501 void DocumentManager::filePathChanged(const FilePath &oldName, const FilePath &newName)
502 {
503     auto doc = qobject_cast<IDocument *>(sender());
504     QTC_ASSERT(doc, return);
505     if (doc == d->m_blockedIDocument)
506         return;
507     emit m_instance->documentRenamed(doc, oldName, newName);
508 }
509 
updateSaveAll()510 void DocumentManager::updateSaveAll()
511 {
512     d->m_saveAllAction->setEnabled(!modifiedDocuments().empty());
513 }
514 
515 /*!
516     Adds \a document to the collection. If \a addWatcher is \c true
517     (the default), the document's file is added to a file system watcher
518     that notifies the document manager about file changes.
519 */
addDocument(IDocument * document,bool addWatcher)520 void DocumentManager::addDocument(IDocument *document, bool addWatcher)
521 {
522     addDocuments({document}, addWatcher);
523 }
524 
documentDestroyed(QObject * obj)525 void DocumentManager::documentDestroyed(QObject *obj)
526 {
527     auto document = static_cast<IDocument*>(obj);
528     // Check the special unwatched first:
529     if (!d->m_documentsWithoutWatch.removeOne(document))
530         removeFileInfo(document);
531 }
532 
533 /*!
534     Removes \a document from the collection.
535 
536     Returns \c true if the document had the \c addWatcher argument to
537     addDocument() set.
538 */
removeDocument(IDocument * document)539 bool DocumentManager::removeDocument(IDocument *document)
540 {
541     QTC_ASSERT(document, return false);
542 
543     bool addWatcher = false;
544     // Special casing unwatched files
545     if (!d->m_documentsWithoutWatch.removeOne(document)) {
546         addWatcher = true;
547         removeFileInfo(document);
548         disconnect(document, &IDocument::changed, m_instance, &DocumentManager::checkForNewFileName);
549     }
550     disconnect(document, &QObject::destroyed, m_instance, &DocumentManager::documentDestroyed);
551     disconnect(document, &IDocument::changed, m_instance, &DocumentManager::updateSaveAll);
552     return addWatcher;
553 }
554 
555 /* Slot reacting on IDocument::changed. We need to check if the signal was sent
556    because the document was saved under different name. */
checkForNewFileName()557 void DocumentManager::checkForNewFileName()
558 {
559     auto document = qobject_cast<IDocument *>(sender());
560     // We modified the IDocument
561     // Trust the other code to also update the m_states map
562     if (document == d->m_blockedIDocument)
563         return;
564     QTC_ASSERT(document, return);
565     QTC_ASSERT(d->m_documentsWithWatch.contains(document), return);
566 
567     // Maybe the name has changed or file has been deleted and created again ...
568     // This also updates the state to the on disk state
569     removeFileInfo(document);
570     addFileInfos({document});
571 }
572 
573 /*!
574     Returns a guaranteed cleaned absolute file path for \a filePath.
575     Resolves symlinks if \a resolveMode is ResolveLinks.
576 */
filePathKey(const Utils::FilePath & filePath,ResolveMode resolveMode)577 FilePath DocumentManager::filePathKey(const Utils::FilePath &filePath, ResolveMode resolveMode)
578 {
579     const FilePath &result = filePath.absoluteFilePath().cleanPath();
580     if (resolveMode == ResolveLinks)
581         return result.canonicalPath();
582     return result;
583 }
584 
585 /*!
586     Returns the list of IDocuments that have been modified.
587 */
modifiedDocuments()588 QList<IDocument *> DocumentManager::modifiedDocuments()
589 {
590     QList<IDocument *> modified;
591 
592     const auto docEnd = d->m_documentsWithWatch.keyEnd();
593     for (auto docIt = d->m_documentsWithWatch.keyBegin(); docIt != docEnd; ++docIt) {
594         IDocument *document = *docIt;
595         if (document->isModified())
596             modified << document;
597     }
598 
599     foreach (IDocument *document, d->m_documentsWithoutWatch) {
600         if (document->isModified())
601             modified << document;
602     }
603 
604     return modified;
605 }
606 
607 /*!
608     Treats any subsequent change to \a fileName as an expected file change.
609 
610     \sa unexpectFileChange()
611 */
expectFileChange(const Utils::FilePath & filePath)612 void DocumentManager::expectFileChange(const Utils::FilePath &filePath)
613 {
614     if (filePath.isEmpty())
615         return;
616     d->m_expectedFileNames.insert(filePath);
617 }
618 
619 /* only called from unblock and unexpect file change functions */
updateExpectedState(const FilePath & filePathKey)620 static void updateExpectedState(const FilePath &filePathKey)
621 {
622     if (filePathKey.isEmpty())
623         return;
624     if (d->m_states.contains(filePathKey)) {
625         const FilePath watched = d->m_states.value(filePathKey).watchedFilePath;
626         d->m_states[filePathKey].expected.modified = watched.lastModified();
627         d->m_states[filePathKey].expected.permissions = watched.permissions();
628     }
629 }
630 
631 /*!
632     Considers all changes to \a fileName unexpected again.
633 
634     \sa expectFileChange()
635 */
unexpectFileChange(const FilePath & filePath)636 void DocumentManager::unexpectFileChange(const FilePath &filePath)
637 {
638     // We are updating the expected time of the file
639     // And in changedFile we'll check if the modification time
640     // is the same as the saved one here
641     // If so then it's a expected change
642 
643     if (filePath.isEmpty())
644         return;
645     d->m_expectedFileNames.remove(filePath);
646     const FilePath cleanAbsFilePath = filePathKey(filePath, KeepLinks);
647     updateExpectedState(filePathKey(filePath, KeepLinks));
648     const FilePath resolvedCleanAbsFilePath = cleanAbsFilePath.canonicalPath();
649     if (cleanAbsFilePath != resolvedCleanAbsFilePath)
650         updateExpectedState(filePathKey(filePath, ResolveLinks));
651 }
652 
saveModifiedFilesHelper(const QList<IDocument * > & documents,const QString & message,bool * cancelled,bool silently,const QString & alwaysSaveMessage,bool * alwaysSave,QList<IDocument * > * failedToSave)653 static bool saveModifiedFilesHelper(const QList<IDocument *> &documents,
654                                     const QString &message, bool *cancelled, bool silently,
655                                     const QString &alwaysSaveMessage, bool *alwaysSave,
656                                     QList<IDocument *> *failedToSave)
657 {
658     if (cancelled)
659         (*cancelled) = false;
660 
661     QList<IDocument *> notSaved;
662     QHash<IDocument *, QString> modifiedDocumentsMap;
663     QList<IDocument *> modifiedDocuments;
664 
665     foreach (IDocument *document, documents) {
666         if (document && document->isModified() && !document->isTemporary()) {
667             QString name = document->filePath().toString();
668             if (name.isEmpty())
669                 name = document->fallbackSaveAsFileName();
670 
671             // There can be several IDocuments pointing to the same file
672             // Prefer one that is not readonly
673             // (even though it *should* not happen that the IDocuments are inconsistent with readonly)
674             if (!modifiedDocumentsMap.key(name, nullptr) || !document->isFileReadOnly())
675                 modifiedDocumentsMap.insert(document, name);
676         }
677     }
678     modifiedDocuments = modifiedDocumentsMap.keys();
679     if (!modifiedDocuments.isEmpty()) {
680         QList<IDocument *> documentsToSave;
681         if (silently) {
682             documentsToSave = modifiedDocuments;
683         } else {
684             SaveItemsDialog dia(ICore::dialogParent(), modifiedDocuments);
685             if (!message.isEmpty())
686                 dia.setMessage(message);
687             if (!alwaysSaveMessage.isNull())
688                 dia.setAlwaysSaveMessage(alwaysSaveMessage);
689             if (dia.exec() != QDialog::Accepted) {
690                 if (cancelled)
691                     (*cancelled) = true;
692                 if (alwaysSave)
693                     (*alwaysSave) = dia.alwaysSaveChecked();
694                 if (failedToSave)
695                     (*failedToSave) = modifiedDocuments;
696                 const QStringList filesToDiff = dia.filesToDiff();
697                 if (!filesToDiff.isEmpty()) {
698                     if (auto diffService = DiffService::instance())
699                         diffService->diffModifiedFiles(filesToDiff);
700                 }
701                 return false;
702             }
703             if (alwaysSave)
704                 *alwaysSave = dia.alwaysSaveChecked();
705             documentsToSave = dia.itemsToSave();
706         }
707         // Check for files without write permissions.
708         QList<IDocument *> roDocuments;
709         foreach (IDocument *document, documentsToSave) {
710             if (document->isFileReadOnly())
711                 roDocuments << document;
712         }
713         if (!roDocuments.isEmpty()) {
714             ReadOnlyFilesDialog roDialog(roDocuments, ICore::dialogParent());
715             roDialog.setShowFailWarning(true, DocumentManager::tr(
716                                             "Could not save the files.",
717                                             "error message"));
718             if (roDialog.exec() == ReadOnlyFilesDialog::RO_Cancel) {
719                 if (cancelled)
720                     (*cancelled) = true;
721                 if (failedToSave)
722                     (*failedToSave) = modifiedDocuments;
723                 return false;
724             }
725         }
726         foreach (IDocument *document, documentsToSave) {
727             if (!EditorManagerPrivate::saveDocument(document)) {
728                 if (cancelled)
729                     *cancelled = true;
730                 notSaved.append(document);
731             }
732         }
733     }
734     if (failedToSave)
735         (*failedToSave) = notSaved;
736     return notSaved.isEmpty();
737 }
738 
saveDocument(IDocument * document,const Utils::FilePath & filePath,bool * isReadOnly)739 bool DocumentManager::saveDocument(IDocument *document,
740                                    const Utils::FilePath &filePath,
741                                    bool *isReadOnly)
742 {
743     bool ret = true;
744     const Utils::FilePath &savePath = filePath.isEmpty() ? document->filePath() : filePath;
745     expectFileChange(savePath); // This only matters to other IDocuments which refer to this file
746     bool addWatcher = removeDocument(document); // So that our own IDocument gets no notification at all
747 
748     QString errorString;
749     if (!document->save(&errorString, filePath, false)) {
750         if (isReadOnly) {
751             QFile ofi(savePath.toString());
752             // Check whether the existing file is writable
753             if (!ofi.open(QIODevice::ReadWrite) && ofi.open(QIODevice::ReadOnly)) {
754                 *isReadOnly = true;
755                 goto out;
756             }
757             *isReadOnly = false;
758         }
759         QMessageBox::critical(ICore::dialogParent(), tr("File Error"),
760                               tr("Error while saving file: %1").arg(errorString));
761       out:
762         ret = false;
763     }
764 
765     addDocument(document, addWatcher);
766     unexpectFileChange(savePath);
767     m_instance->updateSaveAll();
768     return ret;
769 }
770 
allDocumentFactoryFiltersString(QString * allFilesFilter=nullptr)771 QString DocumentManager::allDocumentFactoryFiltersString(QString *allFilesFilter = nullptr)
772 {
773     QSet<QString> uniqueFilters;
774 
775     for (IEditorFactory *factory : IEditorFactory::allEditorFactories()) {
776         for (const QString &mt : factory->mimeTypes()) {
777             const QString filter = mimeTypeForName(mt).filterString();
778             if (!filter.isEmpty())
779                 uniqueFilters.insert(filter);
780         }
781     }
782 
783     for (IDocumentFactory *factory : IDocumentFactory::allDocumentFactories()) {
784         for (const QString &mt : factory->mimeTypes()) {
785             const QString filter = mimeTypeForName(mt).filterString();
786             if (!filter.isEmpty())
787                 uniqueFilters.insert(filter);
788         }
789     }
790 
791     QStringList filters = Utils::toList(uniqueFilters);
792     filters.sort();
793     const QString allFiles = Utils::allFilesFilterString();
794     if (allFilesFilter)
795         *allFilesFilter = allFiles;
796     filters.prepend(allFiles);
797     return filters.join(QLatin1String(";;"));
798 }
799 
getSaveFileName(const QString & title,const QString & pathIn,const QString & filter,QString * selectedFilter)800 QString DocumentManager::getSaveFileName(const QString &title, const QString &pathIn,
801                                      const QString &filter, QString *selectedFilter)
802 {
803     const QString &path = pathIn.isEmpty() ? fileDialogInitialDirectory() : pathIn;
804     QString fileName;
805     bool repeat;
806     do {
807         repeat = false;
808         fileName = QFileDialog::getSaveFileName(
809             ICore::dialogParent(), title, path, filter, selectedFilter, QFileDialog::DontConfirmOverwrite);
810         if (!fileName.isEmpty()) {
811             // If the selected filter is All Files (*) we leave the name exactly as the user
812             // specified. Otherwise the suffix must be one available in the selected filter. If
813             // the name already ends with such suffix nothing needs to be done. But if not, the
814             // first one from the filter is appended.
815             if (selectedFilter && *selectedFilter != Utils::allFilesFilterString()) {
816                 // Mime database creates filter strings like this: Anything here (*.foo *.bar)
817                 const QRegularExpression regExp(QLatin1String(".*\\s+\\((.*)\\)$"));
818                 QRegularExpressionMatchIterator matchIt = regExp.globalMatch(*selectedFilter);
819                 if (matchIt.hasNext()) {
820                     bool suffixOk = false;
821                     const QRegularExpressionMatch match = matchIt.next();
822                     QString caption = match.captured(1);
823                     caption.remove(QLatin1Char('*'));
824                     const QStringList suffixes = caption.split(QLatin1Char(' '));
825                     for (const QString &suffix : suffixes)
826                         if (fileName.endsWith(suffix)) {
827                             suffixOk = true;
828                             break;
829                         }
830                     if (!suffixOk && !suffixes.isEmpty())
831                         fileName.append(suffixes.at(0));
832                 }
833             }
834             if (QFile::exists(fileName)) {
835                 if (QMessageBox::warning(ICore::dialogParent(), tr("Overwrite?"),
836                     tr("An item named \"%1\" already exists at this location. "
837                        "Do you want to overwrite it?").arg(QDir::toNativeSeparators(fileName)),
838                     QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
839                     repeat = true;
840                 }
841             }
842         }
843     } while (repeat);
844     if (!fileName.isEmpty())
845         setFileDialogLastVisitedDirectory(QFileInfo(fileName).absolutePath());
846     return fileName;
847 }
848 
getSaveFileNameWithExtension(const QString & title,const QString & pathIn,const QString & filter)849 QString DocumentManager::getSaveFileNameWithExtension(const QString &title, const QString &pathIn,
850                                                   const QString &filter)
851 {
852     QString selected = filter;
853     return getSaveFileName(title, pathIn, filter, &selected);
854 }
855 
856 /*!
857     Asks the user for a new file name (\uicontrol {Save File As}) for \a document.
858 */
getSaveAsFileName(const IDocument * document)859 QString DocumentManager::getSaveAsFileName(const IDocument *document)
860 {
861     QTC_ASSERT(document, return QString());
862     const QString filter = allDocumentFactoryFiltersString();
863     const QString filePath = document->filePath().toString();
864     QString selectedFilter;
865     QString fileDialogPath = filePath;
866     if (!filePath.isEmpty()) {
867         selectedFilter = Utils::mimeTypeForFile(filePath).filterString();
868     } else {
869         const QString suggestedName = document->fallbackSaveAsFileName();
870         if (!suggestedName.isEmpty()) {
871             const QList<MimeType> types = Utils::mimeTypesForFileName(suggestedName);
872             if (!types.isEmpty())
873                 selectedFilter = types.first().filterString();
874         }
875         const QString defaultPath = document->fallbackSaveAsPath();
876         if (!defaultPath.isEmpty())
877             fileDialogPath = defaultPath + (suggestedName.isEmpty()
878                     ? QString()
879                     : '/' + suggestedName);
880     }
881     if (selectedFilter.isEmpty())
882         selectedFilter = Utils::mimeTypeForName(document->mimeType()).filterString();
883 
884     return getSaveFileName(tr("Save File As"),
885                            fileDialogPath,
886                            filter,
887                            &selectedFilter);
888 }
889 
890 /*!
891     Silently saves all documents and returns \c true if all modified documents
892     are saved successfully.
893 
894     This method tries to avoid showing dialogs to the user, but can do so anyway
895     (e.g. if a file is not writeable).
896 
897     If users canceled any of the dialogs they interacted with, \a canceled
898     is set. If passed to the method, \a failedToClose returns a list of
899     documents that could not be saved.
900 */
saveAllModifiedDocumentsSilently(bool * canceled,QList<IDocument * > * failedToClose)901 bool DocumentManager::saveAllModifiedDocumentsSilently(bool *canceled,
902                                                        QList<IDocument *> *failedToClose)
903 {
904     return saveModifiedDocumentsSilently(modifiedDocuments(), canceled, failedToClose);
905 }
906 
907 /*!
908     Silently saves \a documents and returns \c true if all of them were saved
909     successfully.
910 
911     This method tries to avoid showing dialogs to the user, but can do so anyway
912     (e.g. if a file is not writeable).
913 
914     If users canceled any of the dialogs they interacted with, \a canceled
915     is set. If passed to the method, \a failedToClose returns a list of
916     documents that could not be saved.
917 */
saveModifiedDocumentsSilently(const QList<IDocument * > & documents,bool * canceled,QList<IDocument * > * failedToClose)918 bool DocumentManager::saveModifiedDocumentsSilently(const QList<IDocument *> &documents,
919                                                     bool *canceled,
920                                                     QList<IDocument *> *failedToClose)
921 {
922     return saveModifiedFilesHelper(documents,
923                                    QString(),
924                                    canceled,
925                                    true,
926                                    QString(),
927                                    nullptr,
928                                    failedToClose);
929 }
930 
931 /*!
932     Silently saves \a document and returns \c true if it was saved successfully.
933 
934     This method tries to avoid showing dialogs to the user, but can do so anyway
935     (e.g. if a file is not writeable).
936 
937     If users canceled any of the dialogs they interacted with, \a canceled
938     is set. If passed to the method, \a failedToClose returns a list of
939     documents that could not be saved.
940 
941 */
saveModifiedDocumentSilently(IDocument * document,bool * canceled,QList<IDocument * > * failedToClose)942 bool DocumentManager::saveModifiedDocumentSilently(IDocument *document, bool *canceled,
943                                                    QList<IDocument *> *failedToClose)
944 {
945     return saveModifiedDocumentsSilently({document}, canceled, failedToClose);
946 }
947 
948 /*!
949     Presents a dialog with all modified documents to users and asks them which
950     of these should be saved.
951 
952     This method may show additional dialogs to the user, e.g. if a file is
953     not writeable.
954 
955     The dialog text can be set using \a message. If users canceled any
956     of the dialogs they interacted with, \a canceled is set and the
957     method returns \c false.
958 
959     The \a alwaysSaveMessage shows an additional checkbox in the dialog.
960     The state of this checkbox is written into \a alwaysSave if set.
961 
962     If passed to the method, \a failedToClose returns a list of
963     documents that could not be saved.
964 */
saveAllModifiedDocuments(const QString & message,bool * canceled,const QString & alwaysSaveMessage,bool * alwaysSave,QList<IDocument * > * failedToClose)965 bool DocumentManager::saveAllModifiedDocuments(const QString &message, bool *canceled,
966                                                const QString &alwaysSaveMessage, bool *alwaysSave,
967                                                QList<IDocument *> *failedToClose)
968 {
969     return saveModifiedDocuments(modifiedDocuments(), message, canceled,
970                                  alwaysSaveMessage, alwaysSave, failedToClose);
971 }
972 
973 /*!
974     Presents a dialog with \a documents to users and asks them which
975     of these should be saved.
976 
977     This method may show additional dialogs to the user, e.g. if a file is
978     not writeable.
979 
980     The dialog text can be set using \a message. If users canceled any
981     of the dialogs they interacted with, \a canceled is set and the
982     method returns \c false.
983 
984     The \a alwaysSaveMessage shows an additional checkbox in the dialog.
985     The state of this checkbox is written into \a alwaysSave if set.
986 
987     If passed to the method, \a failedToClose returns a list of
988     documents that could not be saved.
989 */
saveModifiedDocuments(const QList<IDocument * > & documents,const QString & message,bool * canceled,const QString & alwaysSaveMessage,bool * alwaysSave,QList<IDocument * > * failedToClose)990 bool DocumentManager::saveModifiedDocuments(const QList<IDocument *> &documents,
991                                             const QString &message, bool *canceled,
992                                             const QString &alwaysSaveMessage, bool *alwaysSave,
993                                             QList<IDocument *> *failedToClose)
994 {
995     return saveModifiedFilesHelper(documents, message, canceled, false,
996                                    alwaysSaveMessage, alwaysSave, failedToClose);
997 }
998 
999 /*!
1000     Presents a dialog with the \a document to users and asks them whether
1001     it should be saved.
1002 
1003     This method may show additional dialogs to the user, e.g. if a file is
1004     not writeable.
1005 
1006     The dialog text can be set using \a message. If users canceled any
1007     of the dialogs they interacted with, \a canceled is set and the
1008     method returns \c false.
1009 
1010     The \a alwaysSaveMessage shows an additional checkbox in the dialog.
1011     The state of this checkbox is written into \a alwaysSave if set.
1012 
1013     If passed to the method, \a failedToClose returns a list of
1014     documents that could not be saved.
1015 */
saveModifiedDocument(IDocument * document,const QString & message,bool * canceled,const QString & alwaysSaveMessage,bool * alwaysSave,QList<IDocument * > * failedToClose)1016 bool DocumentManager::saveModifiedDocument(IDocument *document, const QString &message, bool *canceled,
1017                                            const QString &alwaysSaveMessage, bool *alwaysSave,
1018                                            QList<IDocument *> *failedToClose)
1019 {
1020     return saveModifiedDocuments({document}, message, canceled,
1021                                  alwaysSaveMessage, alwaysSave, failedToClose);
1022 }
1023 
showFilePropertiesDialog(const FilePath & filePath)1024 void DocumentManager::showFilePropertiesDialog(const FilePath &filePath)
1025 {
1026     FilePropertiesDialog properties(filePath);
1027     properties.exec();
1028 }
1029 
1030 /*!
1031     Asks the user for a set of file names to be opened. The \a filters
1032     and \a selectedFilter arguments are interpreted like in
1033     QFileDialog::getOpenFileNames(). \a pathIn specifies a path to open the
1034     dialog in if that is not overridden by the user's policy.
1035 */
1036 
getOpenFileNames(const QString & filters,const QString & pathIn,QString * selectedFilter)1037 QStringList DocumentManager::getOpenFileNames(const QString &filters,
1038                                               const QString &pathIn,
1039                                               QString *selectedFilter)
1040 {
1041     const QString &path = pathIn.isEmpty() ? fileDialogInitialDirectory() : pathIn;
1042     const QStringList files = QFileDialog::getOpenFileNames(ICore::dialogParent(),
1043                                                       tr("Open File"),
1044                                                       path, filters,
1045                                                       selectedFilter);
1046     if (!files.isEmpty())
1047         setFileDialogLastVisitedDirectory(QFileInfo(files.front()).absolutePath());
1048     return files;
1049 }
1050 
changedFile(const QString & fileName)1051 void DocumentManager::changedFile(const QString &fileName)
1052 {
1053     const FilePath filePath = FilePath::fromString(fileName);
1054     const bool wasempty = d->m_changedFiles.isEmpty();
1055 
1056     if (d->m_states.contains(filePathKey(filePath, KeepLinks)))
1057         d->m_changedFiles.insert(filePath);
1058     qCDebug(log) << "file change notification for" << filePath;
1059 
1060     if (wasempty && !d->m_changedFiles.isEmpty())
1061         QTimer::singleShot(200, this, &DocumentManager::checkForReload);
1062 }
1063 
checkForReload()1064 void DocumentManager::checkForReload()
1065 {
1066     if (d->m_postponeAutoReload || d->m_changedFiles.isEmpty())
1067         return;
1068     if (QApplication::applicationState() != Qt::ApplicationActive)
1069         return;
1070     // If d->m_blockActivated is true, then it means that the event processing of either the
1071     // file modified dialog, or of loading large files, has delivered a file change event from
1072     // a watcher *and* the timer triggered. We may never end up here in a nested way, so
1073     // recheck later at the end of the checkForReload function.
1074     if (d->m_blockActivated)
1075         return;
1076     if (QApplication::activeModalWidget()) {
1077         // We do not want to prompt for modified file if we currently have some modal dialog open.
1078         // There is no really sensible way to get notified globally if a window closed,
1079         // so just check on every focus change.
1080         d->checkOnNextFocusChange();
1081         return;
1082     }
1083 
1084     d->m_blockActivated = true;
1085 
1086     IDocument::ReloadSetting defaultBehavior = EditorManager::reloadSetting();
1087     ReloadPromptAnswer previousReloadAnswer = ReloadCurrent;
1088     FileDeletedPromptAnswer previousDeletedAnswer = FileDeletedSave;
1089 
1090     QList<IDocument *> documentsToClose;
1091     QHash<IDocument*, QString> documentsToSave;
1092 
1093     // collect file information
1094     QMap<FilePath, FileStateItem> currentStates;
1095     QMap<FilePath, IDocument::ChangeType> changeTypes;
1096     QSet<IDocument *> changedIDocuments;
1097     foreach (const FilePath &filePath, d->m_changedFiles) {
1098         const FilePath fileKey = filePathKey(filePath, KeepLinks);
1099         qCDebug(log) << "handling file change for" << filePath << "(" << fileKey << ")";
1100         IDocument::ChangeType type = IDocument::TypeContents;
1101         FileStateItem state;
1102         if (!filePath.exists()) {
1103             qCDebug(log) << "file was removed";
1104             type = IDocument::TypeRemoved;
1105         } else {
1106             state.modified = filePath.lastModified();
1107             state.permissions = filePath.permissions();
1108             qCDebug(log) << "file was modified, time:" << state.modified
1109                          << "permissions: " << state.permissions;
1110         }
1111         currentStates.insert(fileKey, state);
1112         changeTypes.insert(fileKey, type);
1113         foreach (IDocument *document, d->m_states.value(fileKey).lastUpdatedState.keys())
1114             changedIDocuments.insert(document);
1115     }
1116 
1117     // clean up. do this before we may enter the main loop, otherwise we would
1118     // lose consecutive notifications.
1119     d->m_changedFiles.clear();
1120 
1121     // collect information about "expected" file names
1122     // we can't do the "resolving" already in expectFileChange, because
1123     // if the resolved names are different when unexpectFileChange is called
1124     // we would end up with never-unexpected file names
1125     QSet<FilePath> expectedFileKeys;
1126     foreach (const FilePath &filePath, d->m_expectedFileNames) {
1127         const FilePath cleanAbsFilePath = filePathKey(filePath, KeepLinks);
1128         expectedFileKeys.insert(filePathKey(filePath, KeepLinks));
1129         const FilePath resolvedCleanAbsFilePath = cleanAbsFilePath.canonicalPath();
1130         if (cleanAbsFilePath != resolvedCleanAbsFilePath)
1131             expectedFileKeys.insert(filePathKey(filePath, ResolveLinks));
1132     }
1133 
1134     // handle the IDocuments
1135     QStringList errorStrings;
1136     QStringList filesToDiff;
1137     foreach (IDocument *document, changedIDocuments) {
1138         IDocument::ChangeTrigger trigger = IDocument::TriggerInternal;
1139         optional<IDocument::ChangeType> type;
1140         bool changed = false;
1141         // find out the type & behavior from the two possible files
1142         // behavior is internal if all changes are expected (and none removed)
1143         // type is "max" of both types (remove > contents > permissions)
1144         foreach (const FilePath &fileKey, d->m_documentsWithWatch.value(document)) {
1145             // was the file reported?
1146             if (!currentStates.contains(fileKey))
1147                 continue;
1148 
1149             FileStateItem currentState = currentStates.value(fileKey);
1150             FileStateItem expectedState = d->m_states.value(fileKey).expected;
1151             FileStateItem lastState = d->m_states.value(fileKey).lastUpdatedState.value(document);
1152 
1153             // did the file actually change?
1154             if (lastState.modified == currentState.modified && lastState.permissions == currentState.permissions)
1155                 continue;
1156             changed = true;
1157 
1158             // was it only a permission change?
1159             if (lastState.modified == currentState.modified)
1160                 continue;
1161 
1162             // was the change unexpected?
1163             if ((currentState.modified != expectedState.modified || currentState.permissions != expectedState.permissions)
1164                     && !expectedFileKeys.contains(fileKey)) {
1165                 trigger = IDocument::TriggerExternal;
1166             }
1167 
1168             // find out the type
1169             IDocument::ChangeType fileChange = changeTypes.value(fileKey);
1170             if (fileChange == IDocument::TypeRemoved)
1171                 type = IDocument::TypeRemoved;
1172             else if (fileChange == IDocument::TypeContents && !type)
1173                 type = IDocument::TypeContents;
1174         }
1175 
1176         if (!changed) // probably because the change was blocked with (un)blockFileChange
1177             continue;
1178 
1179         // handle it!
1180         d->m_blockedIDocument = document;
1181 
1182         // Update file info, also handling if e.g. link target has changed.
1183         // We need to do that before the file is reloaded, because removing the watcher will
1184         // loose any pending change events. Loosing change events *before* the file is reloaded
1185         // doesn't matter, because in that case we then reload the new version of the file already
1186         // anyhow.
1187         removeFileInfo(document);
1188         addFileInfos({document});
1189 
1190         bool success = true;
1191         QString errorString;
1192         // we've got some modification
1193         // check if it's contents or permissions:
1194         if (!type) {
1195             // Only permission change
1196             document->checkPermissions();
1197             success = true;
1198             // now we know it's a content change or file was removed
1199         } else if (defaultBehavior == IDocument::ReloadUnmodified && type == IDocument::TypeContents
1200                    && !document->isModified()) {
1201             // content change, but unmodified (and settings say to reload in this case)
1202             success = document->reload(&errorString, IDocument::FlagReload, *type);
1203             // file was removed or it's a content change and the default behavior for
1204             // unmodified files didn't kick in
1205         } else if (defaultBehavior == IDocument::ReloadUnmodified && type == IDocument::TypeRemoved
1206                    && !document->isModified()) {
1207             // file removed, but unmodified files should be reloaded
1208             // so we close the file
1209             documentsToClose << document;
1210         } else if (defaultBehavior == IDocument::IgnoreAll) {
1211             // content change or removed, but settings say ignore
1212             success = document->reload(&errorString, IDocument::FlagIgnore, *type);
1213             // either the default behavior is to always ask,
1214             // or the ReloadUnmodified default behavior didn't kick in,
1215             // so do whatever the IDocument wants us to do
1216         } else {
1217             // check if IDocument wants us to ask
1218             if (document->reloadBehavior(trigger, *type) == IDocument::BehaviorSilent) {
1219                 // content change or removed, IDocument wants silent handling
1220                 if (type == IDocument::TypeRemoved)
1221                     documentsToClose << document;
1222                 else
1223                     success = document->reload(&errorString, IDocument::FlagReload, *type);
1224             // IDocument wants us to ask
1225             } else if (type == IDocument::TypeContents) {
1226                 // content change, IDocument wants to ask user
1227                 if (previousReloadAnswer == ReloadNone || previousReloadAnswer == ReloadNoneAndDiff) {
1228                     // answer already given, ignore
1229                     success = document->reload(&errorString, IDocument::FlagIgnore, IDocument::TypeContents);
1230                 } else if (previousReloadAnswer == ReloadAll) {
1231                     // answer already given, reload
1232                     success = document->reload(&errorString, IDocument::FlagReload, IDocument::TypeContents);
1233                 } else {
1234                     // Ask about content change
1235                     previousReloadAnswer = reloadPrompt(document->filePath(), document->isModified(),
1236                                                         DiffService::instance(),
1237                                                         ICore::dialogParent());
1238                     switch (previousReloadAnswer) {
1239                     case ReloadAll:
1240                     case ReloadCurrent:
1241                         success = document->reload(&errorString, IDocument::FlagReload, IDocument::TypeContents);
1242                         break;
1243                     case ReloadSkipCurrent:
1244                     case ReloadNone:
1245                     case ReloadNoneAndDiff:
1246                         success = document->reload(&errorString, IDocument::FlagIgnore, IDocument::TypeContents);
1247                         break;
1248                     case CloseCurrent:
1249                         documentsToClose << document;
1250                         break;
1251                     }
1252                 }
1253                 if (previousReloadAnswer == ReloadNoneAndDiff)
1254                     filesToDiff.append(document->filePath().toString());
1255 
1256             // IDocument wants us to ask, and it's the TypeRemoved case
1257             } else {
1258                 // Ask about removed file
1259                 bool unhandled = true;
1260                 while (unhandled) {
1261                     if (previousDeletedAnswer != FileDeletedCloseAll) {
1262                         previousDeletedAnswer =
1263                                 fileDeletedPrompt(document->filePath().toString(),
1264                                                   ICore::dialogParent());
1265                     }
1266                     switch (previousDeletedAnswer) {
1267                     case FileDeletedSave:
1268                         documentsToSave.insert(document, document->filePath().toString());
1269                         unhandled = false;
1270                         break;
1271                     case FileDeletedSaveAs:
1272                     {
1273                         const QString &saveFileName = getSaveAsFileName(document);
1274                         if (!saveFileName.isEmpty()) {
1275                             documentsToSave.insert(document, saveFileName);
1276                             unhandled = false;
1277                         }
1278                         break;
1279                     }
1280                     case FileDeletedClose:
1281                     case FileDeletedCloseAll:
1282                         documentsToClose << document;
1283                         unhandled = false;
1284                         break;
1285                     }
1286                 }
1287             }
1288         }
1289         if (!success) {
1290             if (errorString.isEmpty())
1291                 errorStrings << tr("Cannot reload %1").arg(document->filePath().toUserOutput());
1292             else
1293                 errorStrings << errorString;
1294         }
1295 
1296         d->m_blockedIDocument = nullptr;
1297     }
1298 
1299     if (!filesToDiff.isEmpty()) {
1300         if (auto diffService = DiffService::instance())
1301             diffService->diffModifiedFiles(filesToDiff);
1302     }
1303 
1304     if (!errorStrings.isEmpty())
1305         QMessageBox::critical(ICore::dialogParent(), tr("File Error"),
1306                               errorStrings.join(QLatin1Char('\n')));
1307 
1308     // handle deleted files
1309     EditorManager::closeDocuments(documentsToClose, false);
1310     for (auto it = documentsToSave.cbegin(), end = documentsToSave.cend(); it != end; ++it) {
1311         saveDocument(it.key(), Utils::FilePath::fromString(it.value()));
1312         it.key()->checkPermissions();
1313     }
1314 
1315     d->m_blockActivated = false;
1316     // re-check in case files where modified while the dialog was open
1317     QMetaObject::invokeMethod(this, &DocumentManager::checkForReload, Qt::QueuedConnection);
1318 //    dump();
1319 }
1320 
1321 /*!
1322     Adds the \a filePath to the list of recent files. Associates the file to
1323     be reopened with the editor that has the specified \a editorId, if possible.
1324     \a editorId defaults to the empty ID, which lets \QC figure out
1325     the best editor itself.
1326 */
addToRecentFiles(const Utils::FilePath & filePath,Id editorId)1327 void DocumentManager::addToRecentFiles(const Utils::FilePath &filePath, Id editorId)
1328 {
1329     if (filePath.isEmpty())
1330         return;
1331     const FilePath fileKey = filePathKey(filePath, KeepLinks);
1332     Utils::erase(d->m_recentFiles, [fileKey](const RecentFile &file) {
1333         return fileKey == filePathKey(file.first, DocumentManager::KeepLinks);
1334     });
1335     while (d->m_recentFiles.count() >= EditorManagerPrivate::maxRecentFiles())
1336         d->m_recentFiles.removeLast();
1337     d->m_recentFiles.prepend(RecentFile(filePath, editorId));
1338 }
1339 
1340 /*!
1341     Clears the list of recent files. Should only be called by
1342     the core plugin when the user chooses to clear the list.
1343 */
clearRecentFiles()1344 void DocumentManager::clearRecentFiles()
1345 {
1346     d->m_recentFiles.clear();
1347 }
1348 
1349 /*!
1350     Returns the list of recent files.
1351 */
recentFiles()1352 QList<DocumentManager::RecentFile> DocumentManager::recentFiles()
1353 {
1354     return d->m_recentFiles;
1355 }
1356 
saveSettings()1357 void DocumentManager::saveSettings()
1358 {
1359     QVariantList recentFiles;
1360     QStringList recentEditorIds;
1361     foreach (const RecentFile &file, d->m_recentFiles) {
1362         recentFiles.append(file.first.toVariant());
1363         recentEditorIds.append(file.second.toString());
1364     }
1365 
1366     QtcSettings *s = ICore::settings();
1367     s->beginGroup(settingsGroupC);
1368     s->setValueWithDefault(filesKeyC, recentFiles);
1369     s->setValueWithDefault(editorsKeyC, recentEditorIds);
1370     s->endGroup();
1371     s->beginGroup(directoryGroupC);
1372     s->setValueWithDefault(projectDirectoryKeyC,
1373                            d->m_projectsDirectory.toString(),
1374                            PathChooser::homePath());
1375     s->setValueWithDefault(useProjectDirectoryKeyC,
1376                            d->m_useProjectsDirectory,
1377                            kUseProjectsDirectoryDefault);
1378     s->endGroup();
1379 }
1380 
readSettings()1381 void readSettings()
1382 {
1383     QSettings *s = ICore::settings();
1384     d->m_recentFiles.clear();
1385     s->beginGroup(QLatin1String(settingsGroupC));
1386     const QVariantList recentFiles = s->value(QLatin1String(filesKeyC)).toList();
1387     const QStringList recentEditorIds = s->value(QLatin1String(editorsKeyC)).toStringList();
1388     s->endGroup();
1389     // clean non-existing files
1390     for (int i = 0, n = recentFiles.size(); i < n; ++i) {
1391         QString editorId;
1392         if (i < recentEditorIds.size()) // guard against old or weird settings
1393             editorId = recentEditorIds.at(i);
1394         const Utils::FilePath &filePath = FilePath::fromVariant(recentFiles.at(i));
1395         if (filePath.exists() && !filePath.isDir())
1396             d->m_recentFiles.append({filePath, Id::fromString(editorId)});
1397     }
1398 
1399     s->beginGroup(QLatin1String(directoryGroupC));
1400     const FilePath settingsProjectDir = FilePath::fromString(s->value(QLatin1String(projectDirectoryKeyC),
1401                                                 QString()).toString());
1402     if (!settingsProjectDir.isEmpty() && settingsProjectDir.isDir())
1403         d->m_projectsDirectory = settingsProjectDir;
1404     else
1405         d->m_projectsDirectory = FilePath::fromString(PathChooser::homePath());
1406     d->m_useProjectsDirectory
1407         = s->value(QLatin1String(useProjectDirectoryKeyC), kUseProjectsDirectoryDefault).toBool();
1408 
1409     s->endGroup();
1410 }
1411 
1412 /*!
1413 
1414   Returns the initial directory for a new file dialog. If there is a current
1415   document associated with a file, uses that. Or if there is a default location
1416   for new files, uses that. Otherwise, uses the last visited directory.
1417 
1418   \sa setFileDialogLastVisitedDirectory(), setDefaultLocationForNewFiles()
1419 */
1420 
fileDialogInitialDirectory()1421 QString DocumentManager::fileDialogInitialDirectory()
1422 {
1423     IDocument *doc = EditorManager::currentDocument();
1424     if (doc && !doc->isTemporary() && !doc->filePath().isEmpty())
1425         return doc->filePath().absolutePath().path();
1426     if (!d->m_defaultLocationForNewFiles.isEmpty())
1427         return d->m_defaultLocationForNewFiles;
1428     return d->m_lastVisitedDirectory;
1429 }
1430 
1431 /*!
1432 
1433   Returns the default location for new files.
1434 
1435   \sa fileDialogInitialDirectory()
1436 */
defaultLocationForNewFiles()1437 QString DocumentManager::defaultLocationForNewFiles()
1438 {
1439     return d->m_defaultLocationForNewFiles;
1440 }
1441 
1442 /*!
1443  Sets the default \a location for new files.
1444  */
setDefaultLocationForNewFiles(const QString & location)1445 void DocumentManager::setDefaultLocationForNewFiles(const QString &location)
1446 {
1447     d->m_defaultLocationForNewFiles = location;
1448 }
1449 
1450 /*!
1451 
1452   Returns the directory for projects. Defaults to HOME.
1453 
1454   \sa setProjectsDirectory(), setUseProjectsDirectory()
1455 */
1456 
projectsDirectory()1457 FilePath DocumentManager::projectsDirectory()
1458 {
1459     return d->m_projectsDirectory;
1460 }
1461 
1462 /*!
1463 
1464   Sets the \a directory for projects.
1465 
1466   \sa projectsDirectory(), useProjectsDirectory()
1467 */
1468 
setProjectsDirectory(const FilePath & directory)1469 void DocumentManager::setProjectsDirectory(const FilePath &directory)
1470 {
1471     if (d->m_projectsDirectory != directory) {
1472         d->m_projectsDirectory = directory;
1473         emit m_instance->projectsDirectoryChanged(d->m_projectsDirectory);
1474     }
1475 }
1476 
1477 /*!
1478 
1479     Returns whether the directory for projects is to be used or whether the user
1480     chose to use the current directory.
1481 
1482   \sa setProjectsDirectory(), setUseProjectsDirectory()
1483 */
1484 
useProjectsDirectory()1485 bool DocumentManager::useProjectsDirectory()
1486 {
1487     return d->m_useProjectsDirectory;
1488 }
1489 
1490 /*!
1491 
1492   Sets whether the directory for projects is to be used to
1493   \a useProjectsDirectory.
1494 
1495   \sa projectsDirectory(), useProjectsDirectory()
1496 */
1497 
setUseProjectsDirectory(bool useProjectsDirectory)1498 void DocumentManager::setUseProjectsDirectory(bool useProjectsDirectory)
1499 {
1500     d->m_useProjectsDirectory = useProjectsDirectory;
1501 }
1502 
1503 /*!
1504 
1505   Returns the last visited directory of a file dialog.
1506 
1507   \sa setFileDialogLastVisitedDirectory(), fileDialogInitialDirectory()
1508 
1509 */
1510 
fileDialogLastVisitedDirectory()1511 QString DocumentManager::fileDialogLastVisitedDirectory()
1512 {
1513     return d->m_lastVisitedDirectory;
1514 }
1515 
1516 /*!
1517 
1518   Sets the last visited \a directory of a file dialog that will be remembered
1519   for the next one.
1520 
1521   \sa fileDialogLastVisitedDirectory(), fileDialogInitialDirectory()
1522 
1523   */
1524 
setFileDialogLastVisitedDirectory(const QString & directory)1525 void DocumentManager::setFileDialogLastVisitedDirectory(const QString &directory)
1526 {
1527     d->m_lastVisitedDirectory = directory;
1528 }
1529 
notifyFilesChangedInternally(const Utils::FilePaths & filePaths)1530 void DocumentManager::notifyFilesChangedInternally(const Utils::FilePaths &filePaths)
1531 {
1532     emit m_instance->filesChangedInternally(filePaths);
1533 }
1534 
registerSaveAllAction()1535 void DocumentManager::registerSaveAllAction()
1536 {
1537     d->registerSaveAllAction();
1538 }
1539 
1540 // -------------- FileChangeBlocker
1541 
1542 /*!
1543     \class Core::FileChangeBlocker
1544     \inheaderfile coreplugin/documentmanager.h
1545     \inmodule QtCreator
1546 
1547     \brief The FileChangeBlocker class blocks all change notifications to all
1548     IDocument objects that match the given filename.
1549 
1550     Additionally, the class unblocks in the destructor. To also reload the
1551     IDocument object in the destructor, set modifiedReload() to \c true.
1552 */
1553 
FileChangeBlocker(const FilePath & filePath)1554 FileChangeBlocker::FileChangeBlocker(const FilePath &filePath)
1555     : m_filePath(filePath)
1556 {
1557     DocumentManager::expectFileChange(filePath);
1558 }
1559 
~FileChangeBlocker()1560 FileChangeBlocker::~FileChangeBlocker()
1561 {
1562     DocumentManager::unexpectFileChange(m_filePath);
1563 }
1564 
1565 } // namespace Core
1566 
1567 #include "documentmanager.moc"
1568