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 | |
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.
-rw-r--r-- | plugins/haskell/ghcmod.cpp | 311 | ||||
-rw-r--r-- | plugins/haskell/ghcmod.h | 121 | ||||
-rw-r--r-- | plugins/haskell/haskell.pro | 12 | ||||
-rw-r--r-- | plugins/haskell/haskelldocument.cpp | 51 | ||||
-rw-r--r-- | plugins/haskell/haskelldocument.h | 51 | ||||
-rw-r--r-- | plugins/haskell/haskelleditorfactory.cpp | 5 | ||||
-rw-r--r-- | plugins/haskell/haskellhoverhandler.cpp | 160 | ||||
-rw-r--r-- | plugins/haskell/haskellhoverhandler.h | 55 | ||||
-rw-r--r-- | plugins/haskell/haskellmanager.cpp | 80 | ||||
-rw-r--r-- | plugins/haskell/haskellmanager.h | 43 | ||||
-rw-r--r-- | plugins/haskell/haskelltokenizer.cpp | 14 | ||||
-rw-r--r-- | plugins/haskell/haskelltokenizer.h | 2 |
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); }; |