aboutsummaryrefslogtreecommitdiffstats
path: root/plugins/haskell
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 /plugins/haskell
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.
Diffstat (limited to 'plugins/haskell')
-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>