Indentation and (un)commenting in local layout and preamble

Daniel xracoonx at gmx.de
Wed Aug 10 02:15:30 UTC 2022


It would be nice if LyX's source editors (for Local Layout and LaTeX 
Preamble) would have proper indentation and (un)commenting support.

I know that the external editing is supported now, but I consider this 
more of a pro feature since it presupposes already having set up an 
editor (other than the standard Windows and macOS text editors) and even 
then it seems often unnecessary cumbersome to use.

In the attached Qt project, I implemented those features. It probably 
needs some more cleaning up. But it seems to work and you could already 
try it out if you like. The (un)commenting feature leans heavily on code 
from QtCreator. (I tried to improve a bit upon it, e.g. comments are 
added at the deepest common indentation as in


Begin
	% Comment
	Code
End


and it is possible to start a comment in an empty line. Both seem to me 
quite a bit of an oversight in Qt Creator.)

If there is interest, what I would at least need help with for bringing 
this over to LyX is a basic setup of the "GuiSourceEdit" class. I tried 
it but failed (linker error). I guess it should be in its own
h/.cpp file. It could already have the constructor as in the attached 
"mainwindow.h" which is code from the source text edits in 
"GuiDocument.cpp". I could then add all the other stuff when it is ready 
but, first, I wanted to make sure that there is some interest.

Daniel
-------------- next part --------------
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

#include <QKeyEvent>
#include <QTextCursor>
#include <QTextBlock>
#include <QTextDocument>
#include <QTextEdit>
#include <QDebug>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
};

class GuiSourceEdit : public QTextEdit
{
    Q_OBJECT
public:
    GuiSourceEdit(QWidget * parent) : QTextEdit(parent) {
        int const tabStop = 4;
        QFontMetrics metrics(currentFont());
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
        // horizontalAdvance() is available starting in 5.11.0
        // setTabStopDistance() is available starting in 5.10.0
        setTabStopDistance(tabStop * metrics.horizontalAdvance(' '));
#else
        setTabStopWidth(tabStop * metrics.width(' '));
#endif
    }

    void keyPressEvent(QKeyEvent *event)
    {
        QTextCursor cursor = textCursor();
        QTextDocument *doc = cursor.document();
        int pos = cursor.position();
        int anchor = cursor.anchor();
        int start = qMin(anchor, pos);
        int end = qMax(anchor, pos);
        QTextBlock startBlock = doc->findBlock(start);
        QTextBlock endBlock = doc->findBlock(end);
        if ((event->modifiers() & Qt::ControlModifier) && event->key() == Qt::Key_Slash) {
            unCommentSelection(cursor, "%");
        } else if (event->key() == Qt::Key_Tab && startBlock != endBlock) {
            indent(cursor);
        } else if (event->key() == Qt::Key_Backtab && startBlock != endBlock) {
            indent(cursor, true);
        } else {
            return QTextEdit::keyPressEvent(event);
        }
    }

    QTextCursor indent(const QTextCursor &cursorIn, bool unIndent = false) {
        QTextCursor cursor = cursorIn;
        QTextDocument *doc = cursor.document();
        cursor.beginEditBlock();

        int pos = cursor.position();
        int anchor = cursor.anchor();
        int start = qMin(anchor, pos);
        int end = qMax(anchor, pos);
        bool anchorIsStart = (anchor == start);

        QTextBlock startBlock = doc->findBlock(start);
        QTextBlock endBlock = doc->findBlock(end);
        endBlock = endBlock.next();

        if (end > start && endBlock.position() == end) {
            --end;
            endBlock = endBlock.previous();
        }

        bool hasSelection = cursor.hasSelection();
        if (unIndent) {
            for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
                if (block.text().at(0) == '\t') {
                    cursor.setPosition(block.position());
                    cursor.movePosition(QTextCursor::NextCharacter,
                                        QTextCursor::KeepAnchor, 1);
                    cursor.removeSelectedText();
                }
            }
        } else {
            for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
                cursor.setPosition(block.position());
                cursor.insertText("\t", QTextCharFormat());
            }
        }
        cursor.endEditBlock();

        cursor = cursorIn;
        if (hasSelection && !unIndent) {
            start = startBlock.position();
            int lastSelPos = anchorIsStart ? cursor.position() : cursor.anchor();
            if (anchorIsStart) {
                cursor.setPosition(start);
                cursor.setPosition(lastSelPos, QTextCursor::KeepAnchor);
            } else {
                cursor.setPosition(lastSelPos);
                cursor.setPosition(start, QTextCursor::KeepAnchor);
            }
        }
        return cursor;
    }

    // Slightly modified version of Qt Creator's unCommentSelection
    QTextCursor unCommentSelection(const QTextCursor &cursorIn, QString singleLine)
    {
        QTextCursor cursor = cursorIn;
        QTextDocument *doc = cursor.document();
        cursor.beginEditBlock();

        int pos = cursor.position();
        int anchor = cursor.anchor();
        int start = qMin(anchor, pos);
        int end = qMax(anchor, pos);
        bool anchorIsStart = (anchor == start);

        QTextBlock startBlock = doc->findBlock(start);
        QTextBlock endBlock = doc->findBlock(end);

        if (end > start && endBlock.position() == end) {
            --end;
            endBlock = endBlock.previous();
        }

        bool hasSelection = cursor.hasSelection();

        bool oneBlock = startBlock == endBlock;

        endBlock = endBlock.next();
        bool doUncomment = true;

        // Check whether uncommenting
        for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
            QString text = block.text().trimmed();
            if ((oneBlock || !text.isEmpty()) && !text.startsWith(singleLine)) {
                doUncomment = false;
                break;
            }
        }

        // Determine for minimal indentation (tabs)
        int minIndent = 1000;
        for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
            const QString text = block.text();
            int tabs = 0;
            for (QChar c : text) {
                if (c == QChar::Tabulation) {
                    tabs++;
                }
                if (!c.isSpace()) {
                    minIndent = qMin(minIndent, tabs);
                    break;
                }
            }
        }

        if (minIndent == 1000) minIndent = 0;

        const int singleLineLength = singleLine.length();
        for (QTextBlock block = startBlock; block != endBlock; block = block.next()) {
            if (doUncomment) {
                QString text = block.text();
                int i = 0;
                while (i <= text.size() - singleLineLength) {
                    if (isComment(text, i, singleLine)) {
                        // Check for whether there is a space after the comment
                        bool hasSpace = false;
                        if (text.size() > i + 1 && text.at(i + 1) == ' ')
                            hasSpace = true;
                        cursor.setPosition(block.position() + i);
                        cursor.movePosition(QTextCursor::NextCharacter,
                                            QTextCursor::KeepAnchor,
                                            singleLineLength + (hasSpace ? 1 : 0));
                        cursor.removeSelectedText();
                        break;
                    }
                    if (!text.at(i).isSpace())
                        break;
                    ++i;
                }
            } else {
                if (!block.text().trimmed().isEmpty() || oneBlock) {
                    cursor.setPosition(block.position() + minIndent);
                    // Insert comment string with space and without formatting
                    cursor.insertText(singleLine + " ", QTextCharFormat());
                }
            }
        }

        cursor.endEditBlock();

        cursor = cursorIn;
        // adjust selection when commenting out
        if (hasSelection && !doUncomment) {
            start = startBlock.position(); // move the comment into the selection
            int lastSelPos = anchorIsStart ? cursor.position() : cursor.anchor();
            if (anchorIsStart) {
                cursor.setPosition(start);
                cursor.setPosition(lastSelPos, QTextCursor::KeepAnchor);
            } else {
                cursor.setPosition(lastSelPos);
                cursor.setPosition(start, QTextCursor::KeepAnchor);
            }
        }
        return cursor;
    }

    static bool isComment(const QString &text, int index,
       const QString &commentType)
    {
        const int length = commentType.length();

        Q_ASSERT(text.length() - index >= length);

        int i = 0;
        while (i < length) {
            if (text.at(index + i) != commentType.at(i))
                return false;
            ++i;
        }
        return true;
    }
};

#endif // MAINWINDOW_H
-------------- next part --------------
A non-text attachment was scrubbed...
Name: mainwindow.ui
Type: text/xml
Size: 1886 bytes
Desc: not available
URL: <http://lists.lyx.org/pipermail/lyx-devel/attachments/20220810/0296c6a4/attachment-0001.xml>
-------------- next part --------------
#include "mainwindow.h"
#include "ui_mainwindow.h"

#include <QFont>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    ui->textEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
}

MainWindow::~MainWindow()
{
    delete ui;
}
-------------- next part --------------
#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}
-------------- next part --------------
QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
    main.cpp \
    mainwindow.cpp

HEADERS += \
    mainwindow.h

FORMS += \
    mainwindow.ui

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target


More information about the lyx-devel mailing list