aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEike Ziller <git@eikeziller.de>2017-06-10 20:25:12 +0200
committerEike Ziller <git@eikeziller.de>2017-10-01 20:11:08 +0200
commit233678df4307588d6c748fbf4463674fd4a58268 (patch)
tree78efd750c8166aa6b6f7d23f4507c0f032161cba
parent224815da1c03c2ad82900d1354590b8f678c672a (diff)
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.
-rw-r--r--plugins/haskell/filecache.cpp139
-rw-r--r--plugins/haskell/filecache.h61
-rw-r--r--plugins/haskell/ghcmod.cpp81
-rw-r--r--plugins/haskell/ghcmod.h11
-rw-r--r--plugins/haskell/haskell.pro6
5 files changed, 290 insertions, 8 deletions
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 <coreplugin/idocument.h>
+#include <texteditor/textdocument.h>
+#include <utils/algorithm.h>
+
+#include <QFile>
+#include <QLoggingCategory>
+#include <QTemporaryFile>
+#include <QTextDocument>
+
+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<QList<Core::IDocument *>()> &documentsToUpdate)
+ : m_tempDir(id),
+ m_documentsToUpdate(documentsToUpdate)
+{
+ qCDebug(cacheLog) << "creating cache path at" << m_tempDir.path();
+}
+
+
+void FileCache::update()
+{
+ const QList<IDocument *> 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<TextDocument *>(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<FileName, FileName> 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<BaseTextDocument *>(document)) {
+ QString errorMessage;
+ if (baseTextDocument->write(cacheFilePath.toString(),
+ QString::fromUtf8(baseTextDocument->contents()),
+ &errorMessage)) {
+ if (auto textDocument = qobject_cast<TextDocument *>(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<IDocument *> &documents)
+{
+ const QSet<FileName> files = Utils::transform<QSet>(documents, &IDocument::filePath);
+ auto it = QMutableHashIterator<FileName, FileName>(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 <utils/fileutils.h>
+#include <utils/temporarydirectory.h>
+
+#include <QHash>
+
+#include <functional>
+
+namespace Core { class IDocument; }
+
+namespace Haskell {
+namespace Internal {
+
+class FileCache
+{
+public:
+ FileCache(const QString &id,
+ const std::function<QList<Core::IDocument *>()> &documentsToUpdate);
+
+ void update();
+ QHash<Utils::FileName, Utils::FileName> fileMap() const;
+
+private:
+ void writeFile(Core::IDocument *document);
+ void cleanUp(const QList<Core::IDocument *> &documents);
+ Utils::FileName createCacheFile(const Utils::FileName &filePath);
+
+ Utils::TemporaryDirectory m_tempDir;
+ QHash<Utils::FileName, Utils::FileName> m_fileMap;
+ QHash<Utils::FileName, int> m_fileRevision;
+ std::function<QList<Core::IDocument *>()> 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 <coreplugin/editormanager/documentmodel.h>
+#include <coreplugin/idocument.h>
+#include <utils/algorithm.h>
#include <utils/environment.h>
+#include <utils/qtcassert.h>
#include <QFileInfo>
#include <QFutureWatcher>
#include <QLoggingCategory>
+#include <QMetaObject>
#include <QMutexLocker>
#include <QProcess>
#include <QRegularExpression>
#include <QTime>
#include <QTimer>
+#include <functional>
+
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<FileName, FileName> &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<QString> GhcMod::typeInfo(const FileName &filePath, int line, in
return parseTypeInfo(runTypeInfo(filePath, line, col));
}
+static QStringList fileMapArgs(const QHash<FileName, FileName> &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<Core::IDocument *> 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<Result> createFuture(AsyncGhcMod::Operation op,
auto fi = new QFutureInterface<Result>;
fi->reportStarted();
+ // propagate inner events to outside future
auto opWatcher = new QFutureWatcher<Utils::optional<QByteArray>>();
QObject::connect(opWatcher, &QFutureWatcherBase::canceled, [fi] { fi->cancel(); });
QObject::connect(opWatcher, &QFutureWatcherBase::finished, opWatcher, &QObject::deleteLater);
@@ -268,6 +304,7 @@ QFuture<Result> createFuture(AsyncGhcMod::Operation op,
});
opWatcher->setFuture(op.fi.future());
+ // propagate cancel from outer future to inner future
auto fiWatcher = new QFutureWatcher<Result>();
QObject::connect(fiWatcher, &QFutureWatcherBase::canceled, [op] { op.fi.cancel(); });
QObject::connect(fiWatcher, &QFutureWatcherBase::finished, fiWatcher, &QObject::deleteLater);
@@ -276,27 +313,59 @@ QFuture<Result> 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<Utils::optional<SymbolInfo>> 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<Utils::optional<SymbolInfo>>(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<Utils::optional<QString>> 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<Utils::optional<QString>>(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<QByteArray> 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 <utils/fileutils.h>
#include <utils/optional.h>
#include <utils/synchronousprocess.h>
@@ -67,6 +69,7 @@ public:
~GhcMod();
Utils::FileName basePath() const;
+ void setFileMap(const QHash<Utils::FileName, Utils::FileName> &fileMap);
Utils::optional<SymbolInfo> findSymbol(const Utils::FileName &filePath, const QString &symbol);
Utils::optional<QString> 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<Utils::FileName, Utils::FileName> m_fileMap;
};
class AsyncGhcMod : public QObject
@@ -114,11 +118,16 @@ public:
const QString &symbol);
QFuture<Utils::optional<QString>> 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<Operation> 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
## <localappdata>/plugins/<ideversion>