From 233678df4307588d6c748fbf4463674fd4a58268 Mon Sep 17 00:00:00 2001 From: Eike Ziller Date: Sat, 10 Jun 2017 20:25:12 +0200 Subject: Take unsaved modifications into account For this we need to take snapshots of open and modified files within the project directory. Since we need to restart ghc-mod whenever the mapping changes, we do not remove files from the snapshot until they are actually closed, even if they stay unmodified. --- plugins/haskell/filecache.cpp | 139 ++++++++++++++++++++++++++++++++++++++++++ plugins/haskell/filecache.h | 61 ++++++++++++++++++ plugins/haskell/ghcmod.cpp | 81 ++++++++++++++++++++++-- plugins/haskell/ghcmod.h | 11 +++- plugins/haskell/haskell.pro | 6 +- 5 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 plugins/haskell/filecache.cpp create mode 100644 plugins/haskell/filecache.h diff --git a/plugins/haskell/filecache.cpp b/plugins/haskell/filecache.cpp new file mode 100644 index 0000000..0495a8b --- /dev/null +++ b/plugins/haskell/filecache.cpp @@ -0,0 +1,139 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "filecache.h" + +#include +#include +#include + +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(cacheLog, "qtc.haskell.filecache") + +using namespace Core; +using namespace TextEditor; +using namespace Utils; + +namespace Haskell { +namespace Internal { + +FileCache::FileCache(const QString &id, + const std::function()> &documentsToUpdate) + : m_tempDir(id), + m_documentsToUpdate(documentsToUpdate) +{ + qCDebug(cacheLog) << "creating cache path at" << m_tempDir.path(); +} + + +void FileCache::update() +{ + const QList documents = m_documentsToUpdate(); + for (IDocument *document : documents) { + const Utils::FileName filePath = document->filePath(); + if (m_fileMap.contains(filePath)) { + // update the existing cached file + // check revision if possible + if (auto textDocument = qobject_cast(document)) { + if (m_fileRevision.value(filePath) != textDocument->document()->revision()) + writeFile(document); + } else { + writeFile(document); + } + } else { + // save file if it is modified + if (document->isModified()) + writeFile(document); + } + } + cleanUp(documents); +} + +QHash FileCache::fileMap() const +{ + return m_fileMap; +} + +void FileCache::writeFile(IDocument *document) +{ + FileName cacheFilePath = m_fileMap.value(document->filePath()); + if (cacheFilePath.isEmpty()) { + cacheFilePath = createCacheFile(document->filePath()); + m_fileMap.insert(document->filePath(), cacheFilePath); + } + qCDebug(cacheLog) << "writing" << cacheFilePath; + if (auto baseTextDocument = qobject_cast(document)) { + QString errorMessage; + if (baseTextDocument->write(cacheFilePath.toString(), + QString::fromUtf8(baseTextDocument->contents()), + &errorMessage)) { + if (auto textDocument = qobject_cast(document)) { + m_fileRevision.insert(document->filePath(), textDocument->document()->revision()); + } else { + m_fileRevision.insert(document->filePath(), -1); + } + } else { + qCDebug(cacheLog) << "!!! writing file failed:" << errorMessage; + } + + } else { + QFile file(cacheFilePath.toString()); + if (file.open(QIODevice::WriteOnly)) { + file.write(document->contents()); + } else { + qCDebug(cacheLog) << "!!! opening file for writing failed"; + } + } +} + +void FileCache::cleanUp(const QList &documents) +{ + const QSet files = Utils::transform(documents, &IDocument::filePath); + auto it = QMutableHashIterator(m_fileMap); + while (it.hasNext()) { + it.next(); + if (!files.contains(it.key())) { + qCDebug(cacheLog) << "deleting" << it.value(); + QFile::remove(it.value().toString()); + m_fileRevision.remove(it.key()); + it.remove(); + } + } +} + +FileName FileCache::createCacheFile(const FileName &filePath) +{ + QTemporaryFile tempFile(m_tempDir.path() + "/XXXXXX-" + filePath.fileName()); + tempFile.setAutoRemove(false); + tempFile.open(); + return FileName::fromString(tempFile.fileName()); +} + +} // namespace Internal +} // namespace Haskell diff --git a/plugins/haskell/filecache.h b/plugins/haskell/filecache.h new file mode 100644 index 0000000..46c48ab --- /dev/null +++ b/plugins/haskell/filecache.h @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include +#include + +#include + +#include + +namespace Core { class IDocument; } + +namespace Haskell { +namespace Internal { + +class FileCache +{ +public: + FileCache(const QString &id, + const std::function()> &documentsToUpdate); + + void update(); + QHash fileMap() const; + +private: + void writeFile(Core::IDocument *document); + void cleanUp(const QList &documents); + Utils::FileName createCacheFile(const Utils::FileName &filePath); + + Utils::TemporaryDirectory m_tempDir; + QHash m_fileMap; + QHash m_fileRevision; + std::function()> m_documentsToUpdate; +}; + +} // namespace Internal +} // namespace Haskell diff --git a/plugins/haskell/ghcmod.cpp b/plugins/haskell/ghcmod.cpp index 4eabc1a..1d5595c 100644 --- a/plugins/haskell/ghcmod.cpp +++ b/plugins/haskell/ghcmod.cpp @@ -25,17 +25,24 @@ #include "ghcmod.h" +#include +#include +#include #include +#include #include #include #include +#include #include #include #include #include #include +#include + Q_LOGGING_CATEGORY(ghcModLog, "qtc.haskell.ghcmod") Q_LOGGING_CATEGORY(asyncGhcModLog, "qtc.haskell.ghcmod.async") @@ -65,6 +72,15 @@ FileName GhcMod::basePath() const return m_path; } +void GhcMod::setFileMap(const QHash &fileMap) +{ + if (fileMap != m_fileMap) { + log("setting new file map"); + m_fileMap = fileMap; + shutdown(); + } +} + static QString toUnicode(QByteArray data) { // clean zero bytes which let QString think that the string ends there @@ -82,6 +98,15 @@ Utils::optional GhcMod::typeInfo(const FileName &filePath, int line, in return parseTypeInfo(runTypeInfo(filePath, line, col)); } +static QStringList fileMapArgs(const QHash &map) +{ + QStringList result; + const auto end = map.cend(); + for (auto it = map.cbegin(); it != end; ++it) + result << "--map-file" << it.key().toString() + "=" + it.value().toString(); + return result; +} + bool GhcMod::ensureStarted() { m_mutex.lock(); @@ -99,7 +124,9 @@ bool GhcMod::ensureStarted() m_process.reset(new QProcess); m_process->setWorkingDirectory(m_path.toString()); m_process->setEnvironment(env.toStringList()); - m_process->start(stackExecutable.toString(), {"exec", "ghc-mod", "--", "legacy-interactive"}); + m_process->start(stackExecutable.toString(), + QStringList{"exec", "ghc-mod", "--"} + fileMapArgs(m_fileMap) + << "legacy-interactive"); if (!m_process->waitForStarted(kTimeoutMS)) { log("failed to start"); return false; @@ -223,12 +250,20 @@ void GhcMod::setStackExecutable(const FileName &filePath) m_stackExecutable = filePath; } +static QList getOpenDocuments(const FileName &path) +{ + return Utils::filtered(Core::DocumentModel::openedDocuments(), [path] (Core::IDocument *doc) { + return path.isEmpty() || doc->filePath().isChildOf(path); + }); +} + AsyncGhcMod::AsyncGhcMod(const FileName &path) - : m_ghcmod(path) + : m_ghcmod(path), + m_fileCache("haskell", std::bind(getOpenDocuments, path)) { qCDebug(asyncGhcModLog) << "starting thread for" << m_ghcmod.basePath().toString(); - moveToThread(&m_thread); m_thread.start(); + m_threadTarget.moveToThread(&m_thread); } AsyncGhcMod::~AsyncGhcMod() @@ -255,6 +290,7 @@ QFuture createFuture(AsyncGhcMod::Operation op, auto fi = new QFutureInterface; fi->reportStarted(); + // propagate inner events to outside future auto opWatcher = new QFutureWatcher>(); QObject::connect(opWatcher, &QFutureWatcherBase::canceled, [fi] { fi->cancel(); }); QObject::connect(opWatcher, &QFutureWatcherBase::finished, opWatcher, &QObject::deleteLater); @@ -268,6 +304,7 @@ QFuture createFuture(AsyncGhcMod::Operation op, }); opWatcher->setFuture(op.fi.future()); + // propagate cancel from outer future to inner future auto fiWatcher = new QFutureWatcher(); QObject::connect(fiWatcher, &QFutureWatcherBase::canceled, [op] { op.fi.cancel(); }); QObject::connect(fiWatcher, &QFutureWatcherBase::finished, fiWatcher, &QObject::deleteLater); @@ -276,27 +313,59 @@ QFuture createFuture(AsyncGhcMod::Operation op, return fi->future(); } +/*! + Asynchronously looks up the \a symbol in the context of \a filePath. + + Returns a QFuture handle for the asynchronous operation. You may not block the main event loop + while waiting for it to finish - doing so will result in a deadlock. +*/ QFuture> AsyncGhcMod::findSymbol(const FileName &filePath, const QString &symbol) { QMutexLocker lock(&m_mutex); Operation op([this, filePath, symbol] { return m_ghcmod.runFindSymbol(filePath, symbol); }); m_queue.append(op); - QTimer::singleShot(0, this, [this] { reduceQueue(); }); + QTimer::singleShot(0, &m_threadTarget, [this] { reduceQueue(); }); return createFuture>(op, &GhcMod::parseFindSymbol); } +/*! + Asynchronously looks up the type at \a line and \a col in \a filePath. + + Returns a QFuture handle for the asynchronous operation. You may not block the main event loop + while waiting for it to finish - doing so will result in a deadlock. +*/ QFuture> AsyncGhcMod::typeInfo(const FileName &filePath, int line, int col) { QMutexLocker lock(&m_mutex); Operation op([this, filePath, line, col] { return m_ghcmod.runTypeInfo(filePath, line, col); }); m_queue.append(op); - QTimer::singleShot(0, this, [this] { reduceQueue(); }); + QTimer::singleShot(0, &m_threadTarget, [this] { reduceQueue(); }); return createFuture>(op, &GhcMod::parseTypeInfo); } +/*! + Synchronously runs an update of the cache of modified files. + This must be run on the main thread. + + \internal +*/ +void AsyncGhcMod::updateCache() +{ + m_fileCache.update(); +} + +/*! + Takes operations from the queue and executes them, until the queue is empty. + This must be run within the internal thread whenever an operation is added to the queue. + Canceled operations are not executed, but removed from the queue. + Before each operation, the cache of modified files is updated on the main thread. + + \internal +*/ void AsyncGhcMod::reduceQueue() { + QTC_ASSERT(QThread::currentThread() != thread(), return); const auto takeFirst = [this]() { Operation op; m_mutex.lock(); @@ -309,6 +378,8 @@ void AsyncGhcMod::reduceQueue() Operation op; while ((op = takeFirst()).op) { if (!op.fi.isCanceled()) { + QMetaObject::invokeMethod(this, "updateCache", Qt::BlockingQueuedConnection); + m_ghcmod.setFileMap(m_fileCache.fileMap()); Utils::optional result = op.op(); op.fi.reportResult(result); } diff --git a/plugins/haskell/ghcmod.h b/plugins/haskell/ghcmod.h index 450b112..0553472 100644 --- a/plugins/haskell/ghcmod.h +++ b/plugins/haskell/ghcmod.h @@ -25,6 +25,8 @@ #pragma once +#include "filecache.h" + #include #include #include @@ -67,6 +69,7 @@ public: ~GhcMod(); Utils::FileName basePath() const; + void setFileMap(const QHash &fileMap); Utils::optional findSymbol(const Utils::FileName &filePath, const QString &symbol); Utils::optional typeInfo(const Utils::FileName &filePath, int line, int col); @@ -91,6 +94,7 @@ private: Utils::FileName m_path; unique_ghcmod_process m_process; // kills process on reset + QHash m_fileMap; }; class AsyncGhcMod : public QObject @@ -114,11 +118,16 @@ public: const QString &symbol); QFuture> typeInfo(const Utils::FileName &filePath, int line, int col); +private slots: + void updateCache(); // called through QMetaObject::invokeMethod + private: void reduceQueue(); QThread m_thread; - GhcMod m_ghcmod; + QObject m_threadTarget; // used to run methods in m_thread + GhcMod m_ghcmod; // only use in m_thread + FileCache m_fileCache; // only update through reduceQueue QVector m_queue; QMutex m_mutex; }; diff --git a/plugins/haskell/haskell.pro b/plugins/haskell/haskell.pro index 45dd9d3..8f5bbc5 100644 --- a/plugins/haskell/haskell.pro +++ b/plugins/haskell/haskell.pro @@ -12,7 +12,8 @@ SOURCES += \ ghcmod.cpp \ haskellmanager.cpp \ haskelldocument.cpp \ - optionspage.cpp + optionspage.cpp \ + filecache.cpp HEADERS += \ haskell_global.h \ @@ -26,7 +27,8 @@ HEADERS += \ ghcmod.h \ haskellmanager.h \ haskelldocument.h \ - optionspage.h + optionspage.h \ + filecache.h ## uncomment to build plugin into user config directory ## /plugins/ -- cgit v1.2.3