aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEike Ziller <git@eikeziller.de>2017-04-24 16:23:29 +0200
committerEike Ziller <git@eikeziller.de>2017-10-01 20:11:08 +0200
commitd1c0bd6491e9ff5a2d9ce31d523901640eadbb66 (patch)
treeac509c23b3936dbddef6667c1e669968eac352e4
parent5798e33d742c0f413d2d865fdb75739b4374ce98 (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.
-rw-r--r--plugins/haskell/ghcmod.cpp311
-rw-r--r--plugins/haskell/ghcmod.h121
-rw-r--r--plugins/haskell/haskell.pro12
-rw-r--r--plugins/haskell/haskelldocument.cpp51
-rw-r--r--plugins/haskell/haskelldocument.h51
-rw-r--r--plugins/haskell/haskelleditorfactory.cpp5
-rw-r--r--plugins/haskell/haskellhoverhandler.cpp160
-rw-r--r--plugins/haskell/haskellhoverhandler.h55
-rw-r--r--plugins/haskell/haskellmanager.cpp80
-rw-r--r--plugins/haskell/haskellmanager.h43
-rw-r--r--plugins/haskell/haskelltokenizer.cpp14
-rw-r--r--plugins/haskell/haskelltokenizer.h2
12 files changed, 901 insertions, 4 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
diff --git a/plugins/haskell/ghcmod.h b/plugins/haskell/ghcmod.h
new file mode 100644
index 0000000..770a0b7
--- /dev/null
+++ b/plugins/haskell/ghcmod.h
@@ -0,0 +1,121 @@
+/****************************************************************************
+**
+** 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/optional.h>
+#include <utils/synchronousprocess.h>
+
+#include <QFuture>
+#include <QMutex>
+#include <QThread>
+
+#include <memory>
+
+QT_BEGIN_NAMESPACE
+class QProcess;
+QT_END_NAMESPACE
+
+namespace Haskell {
+namespace Internal {
+
+class SymbolInfo {
+public:
+ QStringList definition;
+ QStringList additionalInfo;
+ Utils::FileName file;
+ int line = -1;
+ int col = -1;
+ QString module;
+};
+
+template <typename T> class ghcmod_deleter;
+template <> class ghcmod_deleter<QProcess>
+{
+public:
+ void operator()(QProcess *p) { Utils::SynchronousProcess::stopProcess(*p); delete p; }
+};
+using unique_ghcmod_process = std::unique_ptr<QProcess, ghcmod_deleter<QProcess>>;
+
+class GhcMod
+{
+public:
+ GhcMod(const Utils::FileName &path);
+ ~GhcMod();
+
+ Utils::FileName basePath() const;
+
+ Utils::optional<SymbolInfo> findSymbol(const Utils::FileName &filePath, const QString &symbol);
+ Utils::optional<QString> typeInfo(const Utils::FileName &filePath, int line, int col);
+
+ Utils::optional<QByteArray> runQuery(const QString &query);
+
+ Utils::optional<QByteArray> runFindSymbol(const Utils::FileName &filePath, const QString &symbol);
+ Utils::optional<QByteArray> runTypeInfo(const Utils::FileName &filePath, int line, int col);
+
+ static Utils::optional<SymbolInfo> parseFindSymbol(const Utils::optional<QByteArray> &response);
+ static Utils::optional<QString> parseTypeInfo(const Utils::optional<QByteArray> &response);
+private:
+ bool ensureStarted();
+ void shutdown();
+ void log(const QString &message);
+
+ Utils::FileName m_path;
+ unique_ghcmod_process m_process; // kills process on reset
+};
+
+class AsyncGhcMod : public QObject
+{
+ Q_OBJECT
+
+public:
+ struct Operation {
+ Operation() = default;
+ Operation(const std::function<Utils::optional<QByteArray>()> &op);
+ mutable QFutureInterface<Utils::optional<QByteArray>> fi;
+ std::function<Utils::optional<QByteArray>()> op;
+ };
+
+ AsyncGhcMod(const Utils::FileName &path);
+ ~AsyncGhcMod();
+
+ Utils::FileName basePath() const;
+
+ QFuture<Utils::optional<SymbolInfo>> findSymbol(const Utils::FileName &filePath,
+ const QString &symbol);
+ QFuture<Utils::optional<QString>> typeInfo(const Utils::FileName &filePath, int line, int col);
+
+private:
+ void reduceQueue();
+
+ QThread m_thread;
+ GhcMod m_ghcmod;
+ QVector<Operation> m_queue;
+ QMutex m_mutex;
+};
+
+} // Internal
+} // Haskell
diff --git a/plugins/haskell/haskell.pro b/plugins/haskell/haskell.pro
index aa92d3c..0fd3737 100644
--- a/plugins/haskell/haskell.pro
+++ b/plugins/haskell/haskell.pro
@@ -5,18 +5,26 @@ DEFINES += HASKELL_LIBRARY
SOURCES += \
haskellcompletionassist.cpp \
haskelleditorfactory.cpp \
+ haskellhoverhandler.cpp \
haskellplugin.cpp \
haskellhighlighter.cpp \
- haskelltokenizer.cpp
+ haskelltokenizer.cpp \
+ ghcmod.cpp \
+ haskellmanager.cpp \
+ haskelldocument.cpp
HEADERS += \
haskell_global.h \
haskellcompletionassist.h \
haskellconstants.h \
haskelleditorfactory.h \
+ haskellhoverhandler.h \
haskellplugin.h \
haskellhighlighter.h \
- haskelltokenizer.h
+ haskelltokenizer.h \
+ ghcmod.h \
+ haskellmanager.h \
+ haskelldocument.h
## uncomment to build plugin into user config directory
## <localappdata>/plugins/<ideversion>
diff --git a/plugins/haskell/haskelldocument.cpp b/plugins/haskell/haskelldocument.cpp
new file mode 100644
index 0000000..98850bc
--- /dev/null
+++ b/plugins/haskell/haskelldocument.cpp
@@ -0,0 +1,51 @@
+/****************************************************************************
+**
+** 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 "haskelldocument.h"
+
+#include "haskellconstants.h"
+#include "haskellmanager.h"
+
+using namespace TextEditor;
+using namespace Utils;
+
+namespace Haskell {
+namespace Internal {
+
+HaskellDocument::HaskellDocument()
+ : TextDocument(Constants::C_HASKELLEDITOR_ID)
+{
+ connect(this, &IDocument::filePathChanged, this, [this](const FileName &, const FileName &fn) {
+ m_ghcmod = HaskellManager::ghcModForFile(fn);
+ });
+}
+
+std::shared_ptr<AsyncGhcMod> HaskellDocument::ghcMod() const
+{
+ return m_ghcmod;
+}
+
+} // namespace Internal
+} // namespace Haskell
diff --git a/plugins/haskell/haskelldocument.h b/plugins/haskell/haskelldocument.h
new file mode 100644
index 0000000..72af5e9
--- /dev/null
+++ b/plugins/haskell/haskelldocument.h
@@ -0,0 +1,51 @@
+/****************************************************************************
+**
+** 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 <texteditor/textdocument.h>
+
+#include <memory>
+
+namespace Haskell {
+namespace Internal {
+
+class AsyncGhcMod;
+
+class HaskellDocument : public TextEditor::TextDocument
+{
+ Q_OBJECT
+
+public:
+ HaskellDocument();
+
+ std::shared_ptr<AsyncGhcMod> ghcMod() const;
+
+private:
+ std::shared_ptr<AsyncGhcMod> m_ghcmod;
+};
+
+} // namespace Internal
+} // namespace Haskell
diff --git a/plugins/haskell/haskelleditorfactory.cpp b/plugins/haskell/haskelleditorfactory.cpp
index 8119105..e1d245f 100644
--- a/plugins/haskell/haskelleditorfactory.cpp
+++ b/plugins/haskell/haskelleditorfactory.cpp
@@ -27,7 +27,9 @@
#include "haskellcompletionassist.h"
#include "haskellconstants.h"
+#include "haskelldocument.h"
#include "haskellhighlighter.h"
+#include "haskellhoverhandler.h"
#include <texteditor/textdocument.h>
#include <texteditor/texteditoractionhandler.h>
@@ -43,7 +45,8 @@ HaskellEditorFactory::HaskellEditorFactory()
setDisplayName(QCoreApplication::translate("OpenWith::Editors", "Haskell Editor"));
addMimeType("text/x-haskell");
setEditorActionHandlers(TextEditor::TextEditorActionHandler::UnCommentSelection);
- setDocumentCreator([] { return new TextEditor::TextDocument(Constants::C_HASKELLEDITOR_ID); });
+ addHoverHandler(new HaskellHoverHandler);
+ setDocumentCreator([] { return new HaskellDocument(); });
setCommentDefinition(Utils::CommentDefinition("--", "{-", "-}"));
setParenthesesMatchingEnabled(true);
setMarksVisible(true);
diff --git a/plugins/haskell/haskellhoverhandler.cpp b/plugins/haskell/haskellhoverhandler.cpp
new file mode 100644
index 0000000..5e03e32
--- /dev/null
+++ b/plugins/haskell/haskellhoverhandler.cpp
@@ -0,0 +1,160 @@
+/****************************************************************************
+**
+** 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 "haskellhoverhandler.h"
+
+#include "haskelldocument.h"
+#include "haskelltokenizer.h"
+
+#include <texteditor/textdocument.h>
+#include <texteditor/texteditor.h>
+#include <utils/qtcassert.h>
+#include <utils/runextensions.h>
+#include <utils/synchronousprocess.h>
+#include <utils/tooltip/tooltip.h>
+
+#include <QTextBlock>
+#include <QTextDocument>
+
+#include <functional>
+
+using namespace Utils;
+
+static QString toCode(const QString &s)
+{
+ if (s.isEmpty())
+ return s;
+ return "<pre>" + s.toHtmlEscaped() + "</pre>";
+}
+
+namespace Haskell {
+namespace Internal {
+
+QString symbolToHtml(const SymbolInfo &info)
+{
+ if (info.definition.isEmpty())
+ return QString();
+ QString result = "<pre>" + info.definition.join("\n") + "</pre>";
+ if (!info.file.isEmpty()) {
+ result += "<div align=\"right\"><i>-- " + info.file.toString();
+ if (info.line >= 0) {
+ result += ":" + QString::number(info.line);
+ if (info.col >= 0)
+ result += ":" + QString::number(info.col);
+ }
+ result += "</i></div>";
+ } else if (!info.module.isEmpty()) {
+ result += "<div align=\"right\"><i>-- Module \"" + info.module + "\"</i></div>";
+ }
+ if (!info.additionalInfo.isEmpty())
+ result += "<pre>" + info.additionalInfo.join("\n") + "</pre>";
+ return result;
+}
+
+void HaskellHoverHandler::identifyMatch(TextEditor::TextEditorWidget *editorWidget, int pos)
+{
+ cancel();
+ m_name.clear();
+ editorWidget->convertPosition(pos, &m_line, &m_col);
+ if (m_line < 0 || m_col < 0)
+ return;
+ QTextBlock block = editorWidget->document()->findBlockByNumber(m_line - 1);
+ if (block.text().isEmpty())
+ return;
+ m_filePath = editorWidget->textDocument()->filePath();
+ const int startState = block.previous().isValid() ? block.previous().userState() : -1;
+ const Tokens tokens = HaskellTokenizer::tokenize(block.text(), startState);
+ const Token token = tokens.tokenAtColumn(m_col);
+ if (token.isValid()
+ && (token.type == TokenType::Variable
+ || token.type == TokenType::Operator
+ || token.type == TokenType::Constructor
+ || token.type == TokenType::OperatorConstructor)) {
+ m_name = token.text.toString();
+ }
+ if (m_name.isEmpty())
+ setPriority(-1);
+ else
+ setPriority(Priority_Tooltip);
+}
+
+static void tryShowToolTip(const QPointer<QWidget> &widget, const QPoint &point,
+ QFuture<Utils::optional<QString>> typeFuture,
+ QFuture<Utils::optional<SymbolInfo>> symbolFuture)
+{
+ if (Utils::ToolTip::isVisible() && widget
+ && symbolFuture.isResultReadyAt(0) && typeFuture.isResultReadyAt(0)) {
+ const Utils::optional<QString> type = typeFuture.result();
+ const Utils::optional<SymbolInfo> info = symbolFuture.result();
+ const QString typeString = !type || type.value().isEmpty()
+ ? QString()
+ : toCode(":: " + type.value());
+ const QString infoString = info ? symbolToHtml(info.value()) : QString();
+ const QString tip = typeString + infoString;
+ Utils::ToolTip::show(point, tip, widget);
+ }
+}
+
+void HaskellHoverHandler::operateTooltip(TextEditor::TextEditorWidget *editorWidget,
+ const QPoint &point)
+{
+ cancel();
+ if (m_name.isEmpty()) {
+ Utils::ToolTip::hide();
+ return;
+ }
+ Utils::ToolTip::show(point, tr("Looking up \"%1\"").arg(m_name), editorWidget);
+
+ QPointer<QWidget> widget = editorWidget;
+ std::shared_ptr<AsyncGhcMod> ghcmod;
+ auto doc = qobject_cast<HaskellDocument *>(editorWidget->textDocument());
+ QTC_ASSERT(doc, return);
+ ghcmod = doc->ghcMod();
+ m_typeFuture = ghcmod->typeInfo(m_filePath, m_line, m_col);
+ m_symbolFuture = ghcmod->findSymbol(m_filePath, m_name);
+ Utils::onResultReady(m_typeFuture,
+ [typeFuture = m_typeFuture, symbolFuture = m_symbolFuture,
+ ghcmod, widget, point] // hold shared ghcmod pointer
+ (const Utils::optional<QString> &) {
+ tryShowToolTip(widget, point, typeFuture, symbolFuture);
+ });
+ Utils::onResultReady(m_symbolFuture,
+ [typeFuture = m_typeFuture, symbolFuture = m_symbolFuture,
+ ghcmod, widget, point] // hold shared ghcmod pointer
+ (const Utils::optional<SymbolInfo> &) {
+ tryShowToolTip(widget, point, typeFuture, symbolFuture);
+ });
+}
+
+void HaskellHoverHandler::cancel()
+{
+ if (m_symbolFuture.isRunning())
+ m_symbolFuture.cancel();
+ if (m_typeFuture.isRunning())
+ m_typeFuture.cancel();
+}
+
+} // namespace Internal
+} // namespace Haskell
diff --git a/plugins/haskell/haskellhoverhandler.h b/plugins/haskell/haskellhoverhandler.h
new file mode 100644
index 0000000..6df2da9
--- /dev/null
+++ b/plugins/haskell/haskellhoverhandler.h
@@ -0,0 +1,55 @@
+/****************************************************************************
+**
+** 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 "ghcmod.h"
+
+#include <texteditor/basehoverhandler.h>
+#include <utils/fileutils.h>
+#include <utils/optional.h>
+
+namespace Haskell {
+namespace Internal {
+
+class HaskellHoverHandler : public TextEditor::BaseHoverHandler
+{
+private:
+ void identifyMatch(TextEditor::TextEditorWidget *editorWidget, int pos) override;
+ void operateTooltip(TextEditor::TextEditorWidget *editorWidget, const QPoint &point) override;
+
+ void cancel();
+
+ Utils::FileName m_filePath;
+ int m_line = -1;
+ int m_col = -1;
+ QString m_name;
+
+ QFuture<Utils::optional<SymbolInfo>> m_symbolFuture;
+ QFuture<Utils::optional<QString>> m_typeFuture;
+};
+
+} // namespace Internal
+} // namespace Haskell
diff --git a/plugins/haskell/haskellmanager.cpp b/plugins/haskell/haskellmanager.cpp
new file mode 100644
index 0000000..275c921
--- /dev/null
+++ b/plugins/haskell/haskellmanager.cpp
@@ -0,0 +1,80 @@
+/****************************************************************************
+**
+** 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 "haskellmanager.h"
+
+#include "ghcmod.h"
+
+#include <QDir>
+#include <QFileInfo>
+
+#include <unordered_map>
+
+using namespace Utils;
+
+namespace Haskell {
+namespace Internal {
+
+class HaskellManagerPrivate
+{
+public:
+ std::unordered_map<FileName, std::weak_ptr<AsyncGhcMod>> ghcModCache;
+};
+
+Q_GLOBAL_STATIC(HaskellManagerPrivate, m_d);
+
+FileName HaskellManager::findProjectDirectory(const FileName &filePath)
+{
+ if (filePath.isEmpty())
+ return FileName();
+
+ QDir directory(filePath.toFileInfo().isDir() ? filePath.toString()
+ : filePath.parentDir().toString());
+ directory.setNameFilters({"stack.yaml", "*.cabal"});
+ directory.setFilter(QDir::Files | QDir::Readable);
+ do {
+ if (!directory.entryList().isEmpty())
+ return FileName::fromString(directory.path());
+ } while (!directory.isRoot() && directory.cdUp());
+ return FileName();
+}
+
+std::shared_ptr<AsyncGhcMod> HaskellManager::ghcModForFile(const FileName &filePath)
+{
+ const FileName projectPath = findProjectDirectory(filePath);
+ const auto cacheEntry = m_d->ghcModCache.find(projectPath);
+ if (cacheEntry != m_d->ghcModCache.cend()) {
+ if (cacheEntry->second.expired())
+ m_d->ghcModCache.erase(cacheEntry);
+ else
+ return cacheEntry->second.lock();
+ }
+ auto ghcmod = std::make_shared<AsyncGhcMod>(projectPath);
+ m_d->ghcModCache.insert(std::make_pair(projectPath, ghcmod));
+ return ghcmod;
+}
+
+} // namespace Internal
+} // namespace Haskell
diff --git a/plugins/haskell/haskellmanager.h b/plugins/haskell/haskellmanager.h
new file mode 100644
index 0000000..5fbad6c
--- /dev/null
+++ b/plugins/haskell/haskellmanager.h
@@ -0,0 +1,43 @@
+/****************************************************************************
+**
+** 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>
+
+namespace Haskell {
+namespace Internal {
+
+class AsyncGhcMod;
+
+class HaskellManager
+{
+public:
+ static Utils::FileName findProjectDirectory(const Utils::FileName &filePath);
+ static std::shared_ptr<AsyncGhcMod> ghcModForFile(const Utils::FileName &filePath);
+};
+
+} // namespace Internal
+} // namespace Haskell
diff --git a/plugins/haskell/haskelltokenizer.cpp b/plugins/haskell/haskelltokenizer.cpp
index 527e505..b59b48b 100644
--- a/plugins/haskell/haskelltokenizer.cpp
+++ b/plugins/haskell/haskelltokenizer.cpp
@@ -150,6 +150,19 @@ Tokens::Tokens(std::shared_ptr<QString> source)
{
}
+Token Tokens::tokenAtColumn(int col) const
+{
+ auto it = std::upper_bound(begin(), end(), col, [](int c, const Token &i) {
+ return c < i.startCol;
+ });
+ if (it == begin())
+ return Token();
+ --it;
+ if (it->startCol + it->length > col)
+ return *it;
+ return Token();
+}
+
static int grab(const QString &line, int begin,
const std::function<bool(const QChar&)> &test)
{
@@ -160,7 +173,6 @@ static int grab(const QString &line, int begin,
return current - begin;
};
-
static bool isIdentifierChar(const QChar &c)
{
return c.isLetterOrNumber() || c == '\'' || c == '_';
diff --git a/plugins/haskell/haskelltokenizer.h b/plugins/haskell/haskelltokenizer.h
index 46b4b00..29b10df 100644
--- a/plugins/haskell/haskelltokenizer.h
+++ b/plugins/haskell/haskelltokenizer.h
@@ -77,6 +77,8 @@ public:
Tokens(std::shared_ptr<QString> source);
+ Token tokenAtColumn(int col) const;
+
std::shared_ptr<QString> source;
int state = int(State::None);
};