xaizek / zograscope (License: AGPLv3 only) (since 2018-12-07)
Mainly a syntax-aware diff that also provides a number of additional tools.
<root> / tools / gdiff / CodeView.cpp (616ec6620cbbc9b4f1b0efc4b8a48b1c272030cc) (8,520B) (mode 100644) [raw]
// Copyright (C) 2018 xaizek <xaizek@posteo.net>
//
// This file is part of zograscope.
//
// zograscope is free software: you can redistribute it and/or modify
// it under the terms of version 3 of the GNU Affero General Public License as
// published by the Free Software Foundation.
//
// zograscope is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with zograscope.  If not, see <http://www.gnu.org/licenses/>.

#include "CodeView.hpp"

#include <QPainter>
#include <QScrollBar>
#include <QTextBlock>

#include <cassert>

#include <algorithm>
#include <utility>
#include <vector>

#include "utils/nums.hpp"

class CodeView::LineColumn : public QWidget
{
public:
    static constexpr int leftMargin = 2;
    static constexpr int rightMargin = 3;

public:
    LineColumn(CodeView *parent) : QWidget(parent), parent(parent) { }

private:
    virtual QSize sizeHint() const override {
        return { parent->computeLineColumnWidth(), 0 };
    }

    virtual void paintEvent(QPaintEvent *event) override {
        parent->paintLineColumn(event);
    }

    virtual void wheelEvent(QWheelEvent *e) override {
        parent->wheelEvent(e);
    }

private:
    CodeView *parent;
};

StablePos::StablePos(int line, int offset) : line(line), offset(offset)
{
}

StablePos::StablePos(QTextCursor cursor)
{
    QTextDocument *document = cursor.document();
    line = -1;
    offset = cursor.position();
    for (QTextBlock block = cursor.block(); ; block = block.previous()) {
        if (block.userState() >= 0) {
            line = block.userState();
            offset -= block.position();
            break;
        }

        if (block == document->begin()) {
            break;
        }
    }
}

QTextCursor
StablePos::toCursor(QTextDocument *document) const
{
    for (QTextBlock block = document->begin();
         block != document->end();
         block = block.next()) {
        if (block.userState() == line) {
            QTextCursor c(document);
            c.setPosition(offset + block.position());
            return c;
        }
    }
    return QTextCursor(document);
}

bool
operator<(const StablePos &a, const StablePos &b)
{
    return (a.line < b.line || (a.line == b.line && a.offset < b.offset));
}

bool
operator==(const StablePos &a, const StablePos &b)
{
    return (a.line == b.line && a.offset == b.offset);
}

CodeView::CodeView(QWidget *parent)
    : QPlainTextEdit(parent), lineColumn(new LineColumn(this))
{
    connect(this, &QPlainTextEdit::blockCountChanged, [this]() {
        setViewportMargins(computeLineColumnWidth(), 0, 0, 0);
    });
    connect(this, &QPlainTextEdit::updateRequest,
            this, &CodeView::updateLineColumn);

    connect(verticalScrollBar(), &QScrollBar::valueChanged,
            [&](int pos) { emit scrolled(pos); });
    // Qt seems to have a bug or just very weird behaviour of not emitting
    // QScrollBar::valueChanged when scrollbar is updated as a result of a key
    // press.  Below is a workaround.
    connect(this, &QPlainTextEdit::cursorPositionChanged,
            [&]() { emit scrolled(verticalScrollBar()->sliderPosition()); });
}

void
CodeView::setStopPositions(std::vector<StablePos> stopPositions)
{
    assert(std::is_sorted(stopPositions.cbegin(), stopPositions.cend()) &&
           "Positions must be sorted!");
    positions = std::move(stopPositions);
}

bool
CodeView::goToFirstStopPosition()
{
    if (positions.empty()) return false;

    // TODO: might want to preserve horizontal position in more situations.
    int horizontalPosition = horizontalScrollBar()->sliderPosition();
    setTextCursor(positions.front().toCursor(document()));
    horizontalScrollBar()->setSliderPosition(horizontalPosition);
    return true;
}

void
CodeView::updateLineColumn(const QRect &rect, int dy)
{
    if (dy) {
        lineColumn->scroll(0, dy);
    } else {
        lineColumn->update(0, rect.y(), lineColumn->width(), rect.height());
    }
}

int
CodeView::computeLineColumnWidth()
{
    return LineColumn::leftMargin
         + fontMetrics().width('0')*countWidth(blockCount())
         + LineColumn::rightMargin;
}

void
CodeView::paintLineColumn(QPaintEvent *event)
{
    QPainter painter(lineColumn);
    painter.fillRect(event->rect(), QColor(Qt::gray).light(148));
    painter.setPen(Qt::black);

    QFont normalFont = painter.font();

    QTextBlock block = firstVisibleBlock();
    int top = contentOffset().y() + blockBoundingRect(block).top();
    int bottom = top + static_cast<int>(blockBoundingRect(block).height());

    const int from = event->rect().top();
    const int to = event->rect().bottom();
    while (block.isValid() && top <= to) {
        int lineNum = block.userState();

        if (bottom >= from && block.isVisible() && lineNum >= 0) {
            if (block == textCursor().block()) {
                QFont boldFont = normalFont;
                boldFont.setWeight(QFont::Bold);
                painter.setFont(boldFont);
            }
            painter.drawText(0,
                             top,
                             lineColumn->width() - LineColumn::rightMargin,
                             fontMetrics().height(),
                             Qt::AlignRight,
                             QString::number(lineNum + 1));
            if (block == textCursor().block()) {
                painter.setFont(normalFont);
            }
        }

        block = block.next();
        top = bottom;
        bottom = top + static_cast<int>(blockBoundingRect(block).height());
    }
}

void
CodeView::resizeEvent(QResizeEvent *e)
{
    QPlainTextEdit::resizeEvent(e);

    QRect r = contentsRect();
    lineColumn->setGeometry({ r.left(), r.top(),
                              computeLineColumnWidth(), r.height() });
}

void
CodeView::keyPressEvent(QKeyEvent *e)
{
    if (e->text() == "z") {
        centerCursor();
        emit scrolled(verticalScrollBar()->sliderPosition());
        return;
    }
    if (e->text() == "j") {
        return goDown();
    }
    if (e->text() == "k") {
        return goUp();
    }
    if (e->text() == "t") {
        return makeTop();
    }
    if (e->text() == "b") {
        return makeBottom();
    }
    if (e->text() == "g") {
        return sendKey(Qt::Key_Home, Qt::ControlModifier);
    }
    if (e->text() == "G") {
        return sendKey(Qt::Key_End, Qt::ControlModifier);
    }
    if (e->key() == Qt::Key_E && e->modifiers() == Qt::ControlModifier) {
        int newPos = verticalScrollBar()->sliderPosition() + 1;
        verticalScrollBar()->setSliderPosition(newPos);
        return;
    }
    if (e->key() == Qt::Key_Y && e->modifiers() == Qt::ControlModifier) {
        int newPos = verticalScrollBar()->sliderPosition() - 1;
        verticalScrollBar()->setSliderPosition(newPos);
        return;
    }
    return QPlainTextEdit::keyPressEvent(e);
}

void
CodeView::focusInEvent(QFocusEvent *e)
{
    QPlainTextEdit::focusInEvent(e);
    emit focused();
}

void
CodeView::goDown()
{
    StablePos pos(textCursor());
    auto it = std::upper_bound(positions.cbegin(), positions.cend(), pos);
    if (it != positions.cend() && *it == pos) ++it;
    if (it == positions.cend()) return;

    setTextCursor(it->toCursor(document()));
}

void
CodeView::goUp()
{
    StablePos pos(textCursor());
    auto it = std::lower_bound(positions.cbegin(), positions.cend(), pos);
    if (it == positions.cbegin()) return;

    setTextCursor((--it)->toCursor(document()));
}

void
CodeView::makeTop()
{
    int newPos = textCursor().block().blockNumber();
    verticalScrollBar()->setSliderPosition(newPos);
    setTextCursor(textCursor());
    emit scrolled(verticalScrollBar()->sliderPosition());
}

void
CodeView::makeBottom()
{
    QTextCursor bottom = cursorForPosition(QPoint(0.0f, viewport()->height()));
    int height = bottom.block().blockNumber()
        - firstVisibleBlock().blockNumber() + 1;
    int newPos = textCursor().block().blockNumber() - height + 1;
    verticalScrollBar()->setSliderPosition(newPos);
    setTextCursor(textCursor());
    emit scrolled(verticalScrollBar()->sliderPosition());
}

void
CodeView::sendKey(int key, Qt::KeyboardModifiers modifiers)
{
    QKeyEvent ev(QEvent::KeyPress, key, modifiers);
    QPlainTextEdit::keyPressEvent(&ev);
}
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/zograscope

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/zograscope

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master