From dce877989f71e119fc94a3408e6d6435b7c9435b Mon Sep 17 00:00:00 2001
From: Kubat <mael.martin31@gmail.com>
Date: Tue, 24 Aug 2021 16:40:35 +0200
Subject: [PATCH] FakeVim: Add the FakeVim editor proxy to the script views

---
 src/Lib/Utils.hh                   |   3 +
 src/UI/ScriptViews/EditorProxy.cc  | 411 +++++++++++++++++++++++++++++
 src/UI/ScriptViews/EditorProxy.hh  |  88 ++++++
 src/UI/ScriptViews/ScriptEditor.cc |  38 +++
 src/UI/ScriptViews/ScriptEditor.hh |   6 +-
 5 files changed, 544 insertions(+), 2 deletions(-)
 create mode 100644 src/UI/ScriptViews/EditorProxy.cc
 create mode 100644 src/UI/ScriptViews/EditorProxy.hh

diff --git a/src/Lib/Utils.hh b/src/Lib/Utils.hh
index 0b087c02..3283a2c8 100644
--- a/src/Lib/Utils.hh
+++ b/src/Lib/Utils.hh
@@ -16,6 +16,9 @@
 
 #include <qglobal.h>
 
+#define VIVY_PRAGMA(x) _Pragma(#x)
+#define TODO(x)        VIVY_PRAGMA(message("\"TODO: " #x "\""))
+
 #define VIVY_WIN_EXE_SUFFIX             ".exe"
 #define VIVY_ASSERT_STRINGIFY_HELPER(x) #x
 #define VIVY_ASSERT_STRINGIFY(x)        VIVY_ASSERT_STRINGIFY_HELPER(x)
diff --git a/src/UI/ScriptViews/EditorProxy.cc b/src/UI/ScriptViews/EditorProxy.cc
new file mode 100644
index 00000000..7187cb5c
--- /dev/null
+++ b/src/UI/ScriptViews/EditorProxy.cc
@@ -0,0 +1,411 @@
+#include "EditorProxy.hh"
+#include "../FakeVim/FakeVimHandler.hh"
+#include "../FakeVim/FakeVimActions.hh"
+
+#include <QMessageBox>
+#include <QStatusBar>
+#include <QMainWindow>
+#include <QTemporaryFile>
+
+using namespace Vivy;
+
+void
+Vivy::initHandler(FakeVimHandler *handler)
+{
+    handler->handleCommand(QStringLiteral("set nopasskeys"));
+    handler->handleCommand(QStringLiteral("set nopasscontrolkey"));
+    handler->installEventFilter();
+    handler->setupWidget();
+}
+
+void
+Vivy::clearUndoRedo(QPlainTextEdit *scriptEditor)
+{
+    scriptEditor->setUndoRedoEnabled(false);
+    scriptEditor->setUndoRedoEnabled(true);
+}
+
+Vivy::EditorProxy *
+Vivy::connectSignals(FakeVimHandler *handler, QMainWindow *mainWindow, QPlainTextEdit *editor)
+{
+    EditorProxy *proxy = new EditorProxy(editor, mainWindow, handler);
+
+    handler->commandBufferChanged.connect([proxy](const QString &contents, int cursorPos,
+                                                  int /* anchorPos */,
+                                                  int /* messageLevel */) noexcept -> void {
+        proxy->changeStatusMessage(contents, cursorPos);
+    });
+    handler->extraInformationChanged.connect(
+        [proxy](const QString &text) noexcept -> void { proxy->changeExtraInformation(text); });
+    handler->statusDataChanged.connect(
+        [proxy](const QString &text) noexcept -> void { proxy->changeStatusData(text); });
+    handler->highlightMatches.connect(
+        [proxy](const QString &needle) noexcept -> void { proxy->highlightMatches(needle); });
+    handler->handleExCommandRequested.connect(
+        [proxy](bool *handled, const ExCommand &cmd) noexcept -> void {
+            proxy->handleExCommand(handled, cmd);
+        });
+    handler->requestSetBlockSelection.connect([proxy](const QTextCursor &cursor) noexcept -> void {
+        proxy->requestSetBlockSelection(cursor);
+    });
+    handler->requestDisableBlockSelection.connect(
+        [proxy]() noexcept -> void { proxy->requestDisableBlockSelection(); });
+    handler->requestHasBlockSelection.connect(
+        [proxy](bool *on) noexcept -> void { proxy->requestHasBlockSelection(on); });
+
+    handler->indentRegion.connect(
+        [proxy](int beginBlock, int endBlock, QChar typedChar) noexcept -> void {
+            proxy->indentRegion(beginBlock, endBlock, typedChar);
+        });
+    handler->checkForElectricCharacter.connect([proxy](bool *result, QChar c) noexcept -> void {
+        proxy->checkForElectricCharacter(result, c);
+    });
+
+    return proxy;
+}
+
+EditorProxy::EditorProxy(QPlainTextEdit *widg, QMainWindow *mainWin, QObject *parent) noexcept
+    : QObject(parent)
+    , widget(widg)
+    , mainWindow(mainWin)
+{
+}
+
+void
+EditorProxy::openFile(const QString &filename) noexcept
+{
+    emit handleInput(QString(QStringLiteral(":r %1<CR>")).arg(filename));
+}
+
+void
+EditorProxy::changeStatusData(const QString &info) noexcept
+{
+    statusData = info;
+    updateStatusBar();
+}
+
+void
+EditorProxy::highlightMatches(const QString &pattern) noexcept
+{
+    QTextDocument *doc = widget->document();
+    Q_ASSERT(doc);
+
+    QTextEdit::ExtraSelection selection;
+    selection.format.setBackground(Qt::yellow);
+    selection.format.setForeground(Qt::black);
+
+    // Highlight matches.
+    QRegExp re(pattern);
+    QTextCursor cur = doc->find(re);
+    searchSelection.clear();
+
+    int a = cur.position();
+    while (!cur.isNull()) {
+        if (cur.hasSelection()) {
+            selection.cursor = cur;
+            searchSelection.append(selection);
+        } else {
+            cur.movePosition(QTextCursor::NextCharacter);
+        }
+
+        cur   = doc->find(re, cur);
+        int b = cur.position();
+
+        if (a == b) {
+            cur.movePosition(QTextCursor::NextCharacter);
+            cur = doc->find(re, cur);
+            b   = cur.position();
+            if (a == b)
+                break;
+        }
+
+        a = b;
+    }
+
+    updateExtraSelections();
+}
+
+void
+EditorProxy::changeStatusMessage(const QString &contents, int cursorPos) noexcept
+{
+    statusMessage = (cursorPos == -1)
+                        ? contents
+                        : contents.left(cursorPos) + QChar(10073) + contents.mid(cursorPos);
+    updateStatusBar();
+}
+
+void
+EditorProxy::changeExtraInformation(const QString &info) noexcept
+{
+    QMessageBox::information(widget, tr("Information"), info);
+}
+
+void
+EditorProxy::updateStatusBar() noexcept
+{
+    static constexpr int msgMaxSize = 80;
+    int slack                       = msgMaxSize - statusMessage.size() - statusData.size();
+    QString msg                     = statusMessage + QString(slack, QLatin1Char(' ')) + statusData;
+    mainWindow->statusBar()->showMessage(msg);
+}
+
+void
+EditorProxy::handleExCommand(bool *handled, const ExCommand &cmd) noexcept
+{
+    if (wantSaveAndQuit(cmd))
+        emit requestSaveAndQuit(); // :wq
+
+    else if (wantSave(cmd))
+        emit requestSave(); // :w
+
+    else if (wantQuit(cmd)) {
+        if (cmd.hasBang)
+            invalidate(); // :q!
+        else
+            emit requestQuit(); // :q
+    }
+
+    else if (wantRun(cmd))
+        emit requestRun();
+
+    else {
+        *handled = false;
+        return;
+    }
+
+    *handled = true;
+}
+
+void
+EditorProxy::requestSetBlockSelection(const QTextCursor &tc) noexcept
+{
+    const QPalette pal = widget->parentWidget() != nullptr ? widget->parentWidget()->palette()
+                                                           : QApplication::palette();
+
+    blockSelection.clear();
+    clearSelection.clear();
+
+    QTextCursor cur = tc;
+
+    QTextEdit::ExtraSelection selection;
+    selection.format.setBackground(pal.color(QPalette::Base));
+    selection.format.setForeground(pal.color(QPalette::Text));
+    selection.cursor = cur;
+    clearSelection.append(selection);
+
+    selection.format.setBackground(pal.color(QPalette::Highlight));
+    selection.format.setForeground(pal.color(QPalette::HighlightedText));
+
+    const int from = cur.positionInBlock();
+    const int to   = cur.anchor() - cur.document()->findBlock(cur.anchor()).position();
+    const int min  = qMin(cur.position(), cur.anchor());
+    const int max  = qMax(cur.position(), cur.anchor());
+    for (QTextBlock block                                 = cur.document()->findBlock(min);
+         block.isValid() && block.position() < max; block = block.next()) {
+        cur.setPosition(block.position() + qMin(from, block.length()));
+        cur.setPosition(block.position() + qMin(to, block.length()), QTextCursor::KeepAnchor);
+        selection.cursor = cur;
+        blockSelection.append(selection);
+    }
+
+    disconnect(widget, &QPlainTextEdit::selectionChanged, this, &EditorProxy::updateBlockSelection);
+    widget->setTextCursor(tc);
+    connect(widget, &QPlainTextEdit::selectionChanged, this, &EditorProxy::updateBlockSelection);
+
+    QPalette pal2 = widget->palette();
+    pal2.setColor(QPalette::Highlight, Qt::transparent);
+    pal2.setColor(QPalette::HighlightedText, Qt::transparent);
+    widget->setPalette(pal2);
+
+    updateExtraSelections();
+}
+
+void
+EditorProxy::requestDisableBlockSelection() noexcept
+{
+    const QPalette pal = widget->parentWidget() != nullptr ? widget->parentWidget()->palette()
+                                                           : QApplication::palette();
+
+    blockSelection.clear();
+    clearSelection.clear();
+    widget->setPalette(pal);
+
+    disconnect(widget, &QPlainTextEdit::selectionChanged, this, &EditorProxy::updateBlockSelection);
+
+    updateExtraSelections();
+}
+
+void
+EditorProxy::updateBlockSelection() noexcept
+{
+    requestSetBlockSelection(widget->textCursor());
+}
+
+void
+EditorProxy::requestHasBlockSelection(bool *on) noexcept
+{
+    *on = !blockSelection.isEmpty();
+}
+
+void
+EditorProxy::indentRegion(int beginBlock, int endBlock, QChar typedChar) noexcept
+{
+    QTextDocument *doc = widget->document();
+    Q_ASSERT(doc);
+
+    const int indentSize =
+        static_cast<int>(FakeVim::Internal::fakeVimSettings()->shiftWidth.value());
+    QTextBlock startBlock = doc->findBlockByNumber(beginBlock);
+
+    // Record line lenghts for mark adjustments
+    QVector<int> lineLengths(endBlock - beginBlock + 1);
+    QTextBlock block = startBlock;
+
+    for (int i = beginBlock; i <= endBlock; ++i) {
+        const QString line          = block.text();
+        lineLengths[i - beginBlock] = line.length();
+
+        if (typedChar.unicode() == 0 && line.simplified().isEmpty()) {
+            // clear empty lines
+            QTextCursor cursor(block);
+            while (!cursor.atBlockEnd())
+                cursor.deleteChar();
+        }
+
+        else {
+            const QTextBlock previousBlock = block.previous();
+            const QString previousLine = previousBlock.isValid() ? previousBlock.text() : QString();
+
+            int indent = firstNonSpace(previousLine);
+            if (typedChar == '}')
+                indent = std::max(0, indent - indentSize);
+            else if (previousLine.endsWith("{"))
+                indent += indentSize;
+            const QString indentString = QString(" ").repeated(indent);
+
+            QTextCursor cursor(block);
+            cursor.beginEditBlock();
+            cursor.movePosition(QTextCursor::StartOfBlock);
+            cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor,
+                                firstNonSpace(line));
+            cursor.removeSelectedText();
+            cursor.insertText(indentString);
+            cursor.endEditBlock();
+        }
+
+        block = block.next();
+    }
+}
+
+void
+EditorProxy::checkForElectricCharacter(bool *result, QChar c) noexcept
+{
+    *result = c == '{' || c == '}';
+}
+
+int
+EditorProxy::firstNonSpace(const QString &text) noexcept
+{
+    int indent = 0;
+    while (indent < text.length() && text.at(indent) == ' ')
+        ++indent;
+    return indent;
+}
+
+void
+EditorProxy::updateExtraSelections() noexcept
+{
+    widget->setExtraSelections(clearSelection + searchSelection + blockSelection);
+}
+
+bool
+EditorProxy::wantSaveAndQuit(const ExCommand &cmd) noexcept
+{
+    return cmd.cmd == "wq";
+}
+
+bool
+EditorProxy::wantSave(const ExCommand &cmd) noexcept
+{
+    return cmd.matches("w", "write") || cmd.matches("wa", "wall");
+}
+
+bool
+EditorProxy::wantQuit(const ExCommand &cmd) noexcept
+{
+    return cmd.matches("q", "quit") || cmd.matches("qa", "qall");
+}
+
+bool
+EditorProxy::wantRun(const ExCommand &cmd) noexcept
+{
+    return cmd.matches("run", "run") || cmd.matches("make", "make");
+}
+
+bool
+EditorProxy::save(const QString &fileName) noexcept
+{
+    if (!hasChanges(fileName))
+        return true;
+
+    QTemporaryFile tmpFile;
+    if (!tmpFile.open()) {
+        qCritical() << tr("Cannot create temporary file: %1").arg(tmpFile.errorString());
+        return false;
+    }
+
+    QTextStream ts(&tmpFile);
+    ts << content();
+    ts.flush();
+
+    QFile::remove(fileName);
+    if (!QFile::copy(tmpFile.fileName(), fileName)) {
+        qCritical() << tr("Cannot write to file \"%1\"").arg(fileName);
+        return false;
+    }
+
+    return true;
+}
+
+void
+EditorProxy::cancel(const QString &fileName) noexcept
+{
+    if (hasChanges(fileName)) {
+        qCritical() << tr("File \"%1\" was changed").arg(fileName);
+    } else {
+        invalidate();
+    }
+}
+
+void
+EditorProxy::invalidate() noexcept
+{
+    TODO(Use a Vivy thing here !)
+    QApplication::quit();
+}
+
+bool
+EditorProxy::hasChanges(const QString &fileName) noexcept
+{
+    if (fileName.isEmpty() && content().isEmpty())
+        return false;
+
+    QFile f(fileName);
+    if (!f.open(QIODevice::ReadOnly))
+        return true;
+
+    QTextStream ts(&f);
+    return content() != ts.readAll();
+}
+
+QTextDocument *
+EditorProxy::document() const noexcept
+{
+    return widget->document();
+}
+
+QString
+EditorProxy::content() const noexcept
+{
+    return document()->toPlainText();
+}
diff --git a/src/UI/ScriptViews/EditorProxy.hh b/src/UI/ScriptViews/EditorProxy.hh
new file mode 100644
index 00000000..50a5b1e0
--- /dev/null
+++ b/src/UI/ScriptViews/EditorProxy.hh
@@ -0,0 +1,88 @@
+#pragma once
+
+#include <QObject>
+#include <QTextEdit>
+#include "ScriptEditor.hh"
+
+class QMainWindow;
+class QTextDocument;
+class QString;
+class QWidget;
+class QTextCursor;
+
+namespace FakeVim::Internal
+{
+class FakeVimHandler;
+struct ExCommand;
+}
+
+namespace Vivy
+{
+class EditorProxy;
+using FakeVimHandler = FakeVim::Internal::FakeVimHandler;
+using ExCommand      = FakeVim::Internal::ExCommand;
+
+class EditorProxy : public QObject {
+    Q_OBJECT
+
+public:
+    explicit EditorProxy(QPlainTextEdit *widget, QMainWindow *mw,
+                         QObject *parent = nullptr) noexcept;
+    void openFile(const QString &fileName) noexcept;
+
+    bool save(const QString &fileName) noexcept;
+    void cancel(const QString &fileName) noexcept;
+
+signals:
+    void handleInput(const QString &keys);
+    void requestSave();
+    void requestSaveAndQuit();
+    void requestQuit();
+    void requestRun();
+
+public slots:
+    void changeStatusData(const QString &info) noexcept;
+    void highlightMatches(const QString &pattern) noexcept;
+    void changeStatusMessage(const QString &contents, int cursorPos) noexcept;
+    void changeExtraInformation(const QString &info) noexcept;
+    void updateStatusBar() noexcept;
+    void handleExCommand(bool *handled, const FakeVim::Internal::ExCommand &cmd) noexcept;
+    void requestSetBlockSelection(const QTextCursor &tc) noexcept;
+    void requestDisableBlockSelection() noexcept;
+    void updateBlockSelection() noexcept;
+    void requestHasBlockSelection(bool *on) noexcept;
+    void indentRegion(int beginBlock, int endBlock, QChar typedChar) noexcept;
+    void checkForElectricCharacter(bool *result, QChar c) noexcept;
+
+private:
+    static int firstNonSpace(const QString &text) noexcept;
+
+    void updateExtraSelections() noexcept;
+    bool wantSaveAndQuit(const FakeVim::Internal::ExCommand &cmd) noexcept;
+    bool wantSave(const FakeVim::Internal::ExCommand &cmd) noexcept;
+    bool wantQuit(const FakeVim::Internal::ExCommand &cmd) noexcept;
+    bool wantRun(const FakeVim::Internal::ExCommand &cmd) noexcept;
+
+    void invalidate() noexcept;
+    bool hasChanges(const QString &fileName) noexcept;
+
+    QTextDocument *document() const noexcept;
+    QString content() const noexcept;
+
+    QPlainTextEdit *widget;
+    QMainWindow *mainWindow;
+    QString statusMessage;
+    QString statusData;
+
+    QList<QTextEdit::ExtraSelection> searchSelection;
+    QList<QTextEdit::ExtraSelection> clearSelection;
+    QList<QTextEdit::ExtraSelection> blockSelection;
+};
+
+QWidget *createEditorWidget(bool usePlainTextEdit);
+void initHandler(FakeVimHandler *handler);
+void clearUndoRedo(QPlainTextEdit *editor);
+EditorProxy *connectSignals(FakeVimHandler *handler, QMainWindow *mainWindow,
+                            QPlainTextEdit *editor);
+
+}
diff --git a/src/UI/ScriptViews/ScriptEditor.cc b/src/UI/ScriptViews/ScriptEditor.cc
index 7b8a73d9..56dff1a6 100644
--- a/src/UI/ScriptViews/ScriptEditor.cc
+++ b/src/UI/ScriptViews/ScriptEditor.cc
@@ -44,8 +44,46 @@ ScriptEditor::ScriptEditor(QWidget *parent) noexcept
     textFormat.setForeground(QBrush(Qt::white));
     mergeCurrentCharFormat(textFormat);
 
+    QPlainTextEdit::setCursorWidth(0);
+    setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
     setBackgroundVisible(true);
     updateLineNumberAreaWidth(0);
+    setObjectName(QStringLiteral("Editor"));
+    setFocus();
+}
+
+void
+ScriptEditor::paintEvent(QPaintEvent *e) noexcept
+{
+    QPlainTextEdit::paintEvent(e);
+
+    if (!cursorRect.isNull() && e->rect().intersects(cursorRect)) {
+        QRect rect = cursorRect;
+        cursorRect = QRect();
+        QPlainTextEdit::viewport()->update(rect);
+    }
+
+    // Draw text cursor.
+    QRect rect = QPlainTextEdit::cursorRect();
+    if (e->rect().intersects(rect)) {
+        QPainter painter(QPlainTextEdit::viewport());
+
+        if (QPlainTextEdit::overwriteMode()) {
+            QFontMetrics fm(QPlainTextEdit::font());
+            const int position = QPlainTextEdit::textCursor().position();
+            const QChar c      = QPlainTextEdit::document()->characterAt(position);
+            rect.setWidth(fm.horizontalAdvance(c));
+            painter.setPen(Qt::NoPen);
+            painter.setBrush(QPlainTextEdit::palette().color(QPalette::Base));
+            painter.setCompositionMode(QPainter::CompositionMode_Difference);
+        } else {
+            rect.setWidth(QPlainTextEdit::cursorWidth());
+            painter.setPen(QPlainTextEdit::palette().color(QPalette::Text));
+        }
+
+        painter.drawRect(rect);
+        cursorRect = rect;
+    }
 }
 
 void
diff --git a/src/UI/ScriptViews/ScriptEditor.hh b/src/UI/ScriptViews/ScriptEditor.hh
index 01c01769..491a427d 100644
--- a/src/UI/ScriptViews/ScriptEditor.hh
+++ b/src/UI/ScriptViews/ScriptEditor.hh
@@ -42,8 +42,9 @@ public slots:
     void updateLastLuaError(int, QString);
 
 protected:
-    void resizeEvent(QResizeEvent *event) noexcept override;
-    void keyPressEvent(QKeyEvent *e) noexcept override;
+    void resizeEvent(QResizeEvent *) noexcept override;
+    void keyPressEvent(QKeyEvent *) noexcept override;
+    void paintEvent(QPaintEvent *) noexcept override;
 
 private slots:
     void updateLineNumberAreaWidth(int newBlockCount) noexcept;
@@ -51,5 +52,6 @@ private slots:
 
 private:
     QWidget *lineNumberArea{ nullptr };
+    QRect cursorRect{};
 };
 }
-- 
GitLab