diff options
author | Eike Ziller <git@eikeziller.de> | 2017-04-24 16:23:29 +0200 |
---|---|---|
committer | Eike Ziller <git@eikeziller.de> | 2017-10-01 20:11:08 +0200 |
commit | d1c0bd6491e9ff5a2d9ce31d523901640eadbb66 (patch) | |
tree | ac509c23b3936dbddef6667c1e669968eac352e4 /plugins/haskell/ghcmod.cpp | |
parent | 5798e33d742c0f413d2d865fdb75739b4374ce98 (diff) |
Add editor tooltips with type and symbol info
There is one ghcmod process started in a separate thread per project directory of opened files.
If there are no more files open for a project, that ghcmod thread is exited.
This doesn't take unsaved modifications into account.
Diffstat (limited to 'plugins/haskell/ghcmod.cpp')
-rw-r--r-- | plugins/haskell/ghcmod.cpp | 311 |
1 files changed, 311 insertions, 0 deletions
diff --git a/plugins/haskell/ghcmod.cpp b/plugins/haskell/ghcmod.cpp new file mode 100644 index 0000000..f51841b --- /dev/null +++ b/plugins/haskell/ghcmod.cpp @@ -0,0 +1,311 @@ +/**************************************************************************** +** +** 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 "ghcmod.h" + +#include <utils/environment.h> + +#include <QFileInfo> +#include <QFutureWatcher> +#include <QLoggingCategory> +#include <QMutexLocker> +#include <QProcess> +#include <QRegularExpression> +#include <QTime> +#include <QTimer> + +Q_LOGGING_CATEGORY(ghcModLog, "qtc.haskell.ghcmod") +Q_LOGGING_CATEGORY(asyncGhcModLog, "qtc.haskell.ghcmod.async") + +// TODO do not hardcode +const char STACK_EXE[] = "/usr/local/bin/stack"; +const int kTimeoutMS = 10 * 1000; + +using namespace Utils; + +namespace Haskell { +namespace Internal { + +GhcMod::GhcMod(const Utils::FileName &path) + : m_path(path) +{ +} + +GhcMod::~GhcMod() +{ + shutdown(); +} + +FileName GhcMod::basePath() const +{ + return m_path; +} + +static QString toUnicode(QByteArray data) +{ + // clean zero bytes which let QString think that the string ends there + data.replace('\x0', QByteArray()); + return QString::fromUtf8(data); +} + +Utils::optional<SymbolInfo> GhcMod::findSymbol(const FileName &filePath, const QString &symbol) +{ + return parseFindSymbol(runFindSymbol(filePath, symbol)); +} + +Utils::optional<QString> GhcMod::typeInfo(const FileName &filePath, int line, int col) +{ + return parseTypeInfo(runTypeInfo(filePath, line, col)); +} + +bool GhcMod::ensureStarted() +{ + if (m_process) + return true; + log("starting"); + Environment env = Environment::systemEnvironment(); + env.prependOrSetPath(QFileInfo(STACK_EXE).absolutePath()); // for ghc-mod finding stack back + m_process.reset(new QProcess); + m_process->setWorkingDirectory(m_path.toString()); + m_process->setEnvironment(env.toStringList()); + m_process->start(STACK_EXE, {"exec", "ghc-mod", "--", "legacy-interactive"}); + if (!m_process->waitForStarted(kTimeoutMS)) { + log("failed to start"); + return false; + } + log("started"); + m_process->setReadChannel(QProcess::StandardOutput); + return true; +} + +void GhcMod::shutdown() +{ + if (!m_process) + return; + log("shutting down"); + m_process->write("\n"); + m_process->closeWriteChannel(); + m_process->waitForFinished(300); + m_process.reset(); +} + +void GhcMod::log(const QString &message) +{ + qCDebug(ghcModLog) << "ghcmod for" << m_path.toString() << ":" << qPrintable(message); +} + +Utils::optional<QByteArray> GhcMod::runQuery(const QString &query) +{ + if (!ensureStarted()) + return Utils::nullopt; + log("query \"" + query + "\""); + m_process->write(query.toUtf8() + "\n"); + bool ok = false; + QByteArray response; + QTime readTime; + readTime.start(); + while (!ok && readTime.elapsed() < kTimeoutMS) { + m_process->waitForReadyRead(kTimeoutMS - readTime.elapsed() + 10); + response += m_process->read(2048); + ok = response.endsWith("OK\n") || response.endsWith("OK\r\n"); + } + if (ghcModLog().isDebugEnabled()) + qCDebug(ghcModLog) << "response" << QTextCodec::codecForLocale()->toUnicode(response); + if (!ok) { + log("failed"); + m_process.reset(); + return Utils::nullopt; + } + log("success"); + // convert to unix line endings + response.replace("\r\n", "\n"); + response.chop(3); // cut off "OK\n" + return response; +} + +Utils::optional<QByteArray> GhcMod::runFindSymbol(const FileName &filePath, const QString &symbol) +{ + return runQuery(QString("info %1 %2").arg(filePath.toString()) // TODO toNative? quoting? + .arg(symbol)); +} + +Utils::optional<QByteArray> GhcMod::runTypeInfo(const FileName &filePath, int line, int col) +{ + return runQuery(QString("type %1 %2 %3").arg(filePath.toString()) // TODO toNative? quoting? + .arg(line) + .arg(col + 1)); +} + +Utils::optional<SymbolInfo> GhcMod::parseFindSymbol(const Utils::optional<QByteArray> &response) +{ + QRegularExpression infoRegEx("^\\s*(.*?)\\s+--\\sDefined ((at (.+?)(:(\\d+):(\\d+))?)|(in ‘(.+)’.*))$"); + if (!response) + return Utils::nullopt; + SymbolInfo info; + bool hasFileOrModule = false; + const QString str = toUnicode(QByteArray(response.value()).replace('\x0', '\n')); + for (const QString &line : str.split('\n')) { + if (hasFileOrModule) { + info.additionalInfo += line; + } else { + QRegularExpressionMatch result = infoRegEx.match(line); + if (result.hasMatch()) { + hasFileOrModule = true; + info.definition += result.captured(1); + if (result.lastCapturedIndex() == 7) { // Defined at <file:line:col> + info.file = FileName::fromString(result.captured(4)); + bool ok; + int num = result.captured(6).toInt(&ok); + if (ok) + info.line = num; + num = result.captured(7).toInt(&ok); + if (ok) + info.col = num; + } else if (result.lastCapturedIndex() == 9) { // Defined in <module> + info.module = result.captured(9); + } + } else { + info.definition += line; + } + } + } + if (hasFileOrModule) + return info; + return Utils::nullopt; +} + +Utils::optional<QString> GhcMod::parseTypeInfo(const Utils::optional<QByteArray> &response) +{ + QRegularExpression typeRegEx("^\\d+\\s+\\d+\\s+\\d+\\s+\\d+\\s+\"(.*)\"$", + QRegularExpression::MultilineOption); + if (!response) + return Utils::nullopt; + QRegularExpressionMatch result = typeRegEx.match(toUnicode(response.value())); + if (result.hasMatch()) + return result.captured(1); + return Utils::nullopt; +} + +AsyncGhcMod::AsyncGhcMod(const FileName &path) + : m_ghcmod(path) +{ + qCDebug(asyncGhcModLog) << "starting thread for" << m_ghcmod.basePath().toString(); + moveToThread(&m_thread); + m_thread.start(); +} + +AsyncGhcMod::~AsyncGhcMod() +{ + qCDebug(asyncGhcModLog) << "stopping thread for" << m_ghcmod.basePath().toString(); + m_mutex.lock(); + for (Operation &op : m_queue) + op.fi.cancel(); + m_queue.clear(); + m_mutex.unlock(); + m_thread.quit(); + m_thread.wait(); +} + +FileName AsyncGhcMod::basePath() const +{ + return m_ghcmod.basePath(); +} + +template <typename Result> +QFuture<Result> createFuture(AsyncGhcMod::Operation op, + const std::function<Result(const Utils::optional<QByteArray>&)> &postOp) +{ + auto fi = new QFutureInterface<Result>; + fi->reportStarted(); + + auto opWatcher = new QFutureWatcher<Utils::optional<QByteArray>>(); + QObject::connect(opWatcher, &QFutureWatcherBase::canceled, [fi] { fi->cancel(); }); + QObject::connect(opWatcher, &QFutureWatcherBase::finished, opWatcher, &QObject::deleteLater); + QObject::connect(opWatcher, &QFutureWatcherBase::finished, [fi] { + fi->reportFinished(); + delete fi; + }); + QObject::connect(opWatcher, &QFutureWatcherBase::resultReadyAt, + [fi, opWatcher, postOp](int index) { + fi->reportResult(postOp(opWatcher->future().resultAt(index))); + }); + opWatcher->setFuture(op.fi.future()); + + auto fiWatcher = new QFutureWatcher<Result>(); + QObject::connect(fiWatcher, &QFutureWatcherBase::canceled, [op] { op.fi.cancel(); }); + QObject::connect(fiWatcher, &QFutureWatcherBase::finished, fiWatcher, &QObject::deleteLater); + fiWatcher->setFuture(fi->future()); + + return fi->future(); +} + +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(); }); + return createFuture<Utils::optional<SymbolInfo>>(op, &GhcMod::parseFindSymbol); +} + +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(); }); + return createFuture<Utils::optional<QString>>(op, &GhcMod::parseTypeInfo); +} + +void AsyncGhcMod::reduceQueue() +{ + const auto takeFirst = [this]() { + Operation op; + m_mutex.lock(); + if (!m_queue.isEmpty()) + op = m_queue.takeFirst(); + m_mutex.unlock(); + return op; + }; + + Operation op; + while ((op = takeFirst()).op) { + if (!op.fi.isCanceled()) { + Utils::optional<QByteArray> result = op.op(); + op.fi.reportResult(result); + } + op.fi.reportFinished(); + } +} + +AsyncGhcMod::Operation::Operation(const std::function<Utils::optional<QByteArray>()> &op) + : op(op) +{ + fi.reportStarted(); +} + +} // Internal +} // Haskell |