On 2022-08-10 04:15, Daniel wrote:
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


Managed to get the files working. Attached is the patch to extend LyX's source code editing facilities. (I would have posted it on trac but the login is not working currently.)

Daniel
From a367b6d1e25fad1c12b7e72223ade88760d158e5 Mon Sep 17 00:00:00 2001
From: Daniel Ramoeller <d....@web.de>
Date: Sun, 14 Aug 2022 09:22:13 +0200
Subject: [PATCH] Extended comment and indentation for source code

- automatically inherit indentation from previous block
- (un)indent blocks
- (un)comment blocks
---
 src/frontends/qt/GuiDocument.cpp     |  21 +--
 src/frontends/qt/GuiSourceEdit.cpp   | 258 +++++++++++++++++++++++++++
 src/frontends/qt/GuiSourceEdit.h     |  57 ++++++
 src/frontends/qt/Makefile.am         |   2 +
 src/frontends/qt/ui/LocalLayoutUi.ui |   9 +-
 src/frontends/qt/ui/PreambleUi.ui    |   9 +-
 6 files changed, 334 insertions(+), 22 deletions(-)
 create mode 100644 src/frontends/qt/GuiSourceEdit.cpp
 create mode 100644 src/frontends/qt/GuiSourceEdit.h

diff --git a/src/frontends/qt/GuiDocument.cpp b/src/frontends/qt/GuiDocument.cpp
index b3f31302dd..fb78757299 100644
--- a/src/frontends/qt/GuiDocument.cpp
+++ b/src/frontends/qt/GuiDocument.cpp
@@ -480,7 +480,7 @@ PreambleModule::PreambleModule(QWidget * parent)
        // @ is letter in the LyX user preamble
        (void) new LaTeXHighlighter(preambleTE->document(), true);
        preambleTE->setFont(guiApp->typewriterSystemFont());
-       preambleTE->setWordWrapMode(QTextOption::NoWrap);
+       preambleTE->setSingleLine("%");
        setFocusProxy(preambleTE);
        connect(preambleTE, SIGNAL(textChanged()), this, SIGNAL(changed()));
        connect(findLE, SIGNAL(textEdited(const QString &)), this, 
SLOT(checkFindButton()));
@@ -488,15 +488,6 @@ PreambleModule::PreambleModule(QWidget * parent)
        connect(editPB, SIGNAL(clicked()), this, SLOT(editExternal()));
        connect(findLE, SIGNAL(returnPressed()), this, SLOT(findText()));
        checkFindButton();
-       int const tabStop = 4;
-       QFontMetrics metrics(preambleTE->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
-       preambleTE->setTabStopDistance(tabStop * metrics.horizontalAdvance(' 
'));
-#else
-       preambleTE->setTabStopWidth(tabStop * metrics.width(' '));
-#endif
 }
 
 
@@ -606,20 +597,10 @@ LocalLayout::LocalLayout(QWidget * parent)
        : UiWidget<Ui::LocalLayoutUi>(parent), current_id_(nullptr), 
validated_(false)
 {
        locallayoutTE->setFont(guiApp->typewriterSystemFont());
-       locallayoutTE->setWordWrapMode(QTextOption::NoWrap);
        connect(locallayoutTE, SIGNAL(textChanged()), this, 
SLOT(textChanged()));
        connect(validatePB, SIGNAL(clicked()), this, SLOT(validatePressed()));
        connect(convertPB, SIGNAL(clicked()), this, SLOT(convertPressed()));
        connect(editPB, SIGNAL(clicked()), this, SLOT(editExternal()));
-       int const tabStop = 4;
-       QFontMetrics metrics(locallayoutTE->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
-       locallayoutTE->setTabStopDistance(tabStop * metrics.horizontalAdvance(' 
'));
-#else
-       locallayoutTE->setTabStopWidth(tabStop * metrics.width(' '));
-#endif
 }
 
 
diff --git a/src/frontends/qt/GuiSourceEdit.cpp 
b/src/frontends/qt/GuiSourceEdit.cpp
new file mode 100644
index 0000000000..d011a8aad6
--- /dev/null
+++ b/src/frontends/qt/GuiSourceEdit.cpp
@@ -0,0 +1,258 @@
+// -*- C++ -*-
+/**
+ * \file GuiSourceEdit.h
+ * This file is part of LyX, the document processor.
+ * Licence details can be found in the file COPYING.
+ *
+ * Full author contact details are available in file CREDITS.
+ */
+
+#include "GuiSourceEdit.h"
+
+namespace lyx {
+namespace frontend {
+
+GuiSourceEdit::GuiSourceEdit(QWidget * parent) : QTextEdit(parent)
+{
+       setWordWrapMode(QTextOption::NoWrap);
+       setTabStop(tabStop_);
+}
+
+void GuiSourceEdit::setTabStop(int spaces) {
+       tabStop_ = spaces;
+       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
+}
+
+GuiSourceEdit::BlockRangeInfo GuiSourceEdit::getBlockRangeInfo(
+               QTextCursor const & cursorIn) const {
+       QTextCursor cursor = cursorIn;
+       QTextDocument * doc = cursor.document();
+
+       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);
+
+       return { pos, anchor, start, end, anchorIsStart, startBlock, endBlock };
+}
+
+void GuiSourceEdit::keyPressEvent(QKeyEvent * event)
+{
+
+       QTextCursor cursor = textCursor();
+       BlockRangeInfo blockRangeInfo = getBlockRangeInfo(cursor);
+       // (un)commenting via shift + slash
+       if ((event->modifiers() & Qt::ControlModifier) && event->key() == 
Qt::Key_Slash)
+               unCommentSelection(cursor);
+       // multi-line indentation via tab key
+       else if (event->key() == Qt::Key_Tab &&
+                        blockRangeInfo.startBlock != blockRangeInfo.endBlock)
+               unIndent(cursor);
+       // unindent via backtab (typically shift+tab)
+       else if (event->key() == Qt::Key_Backtab)
+               unIndent(cursor, true);
+       // inherit indentation from previous line via return key
+       else if (event->key() == Qt::Key_Return) {
+               QTextEdit::keyPressEvent(event);
+               inheritIndent(cursor);
+       } else
+               return QTextEdit::keyPressEvent(event);
+}
+
+QTextCursor GuiSourceEdit::inheritIndent(QTextCursor const & cursorIn) {
+       QTextCursor cursor = cursorIn;
+       BlockRangeInfo blockRangeInfo = getBlockRangeInfo(cursor);
+       cursor.beginEditBlock();
+
+       QTextBlock previousBlock = blockRangeInfo.startBlock.previous();
+
+       const QString text = previousBlock.text();
+       for (QChar c : text) {
+               if (c == QChar::Tabulation)
+                       cursor.insertText("\t", QTextCharFormat());
+               else
+                       break;
+       }
+
+       cursor.endEditBlock();
+       return cursor;
+}
+
+QTextCursor GuiSourceEdit::unIndent(QTextCursor const & cursorIn, bool 
unIndent) {
+       QTextCursor cursor = cursorIn;
+       BlockRangeInfo blockRangeInfo = getBlockRangeInfo(cursor);
+
+       cursor.beginEditBlock();
+       blockRangeInfo.endBlock = blockRangeInfo.endBlock.next();
+
+       if (blockRangeInfo.end > blockRangeInfo.start &&
+                       blockRangeInfo.endBlock.position() == 
blockRangeInfo.end) {
+               --blockRangeInfo.end;
+               blockRangeInfo.endBlock = blockRangeInfo.endBlock.previous();
+       }
+
+       bool hasSelection = cursor.hasSelection();
+       if (unIndent) {
+               for (QTextBlock block = blockRangeInfo.startBlock;
+                        block != blockRangeInfo.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 = blockRangeInfo.startBlock;
+                        block != blockRangeInfo.endBlock; block = 
block.next()) {
+                       cursor.setPosition(block.position());
+                       cursor.insertText("\t", QTextCharFormat());
+               }
+       }
+       cursor.endEditBlock();
+
+       cursor = cursorIn;
+       if (hasSelection && !unIndent) {
+               blockRangeInfo.start = blockRangeInfo.startBlock.position();
+               int lastSelPos = blockRangeInfo.anchorIsStart ? 
cursor.position()
+                                                             : cursor.anchor();
+               if (blockRangeInfo.anchorIsStart) {
+                       cursor.setPosition(blockRangeInfo.start);
+                       cursor.setPosition(lastSelPos, QTextCursor::KeepAnchor);
+               } else {
+                       cursor.setPosition(lastSelPos);
+                       cursor.setPosition(blockRangeInfo.start, 
QTextCursor::KeepAnchor);
+               }
+       }
+       return cursor;
+}
+
+// Slightly modified version of Qt Creator's unCommentSelection
+QTextCursor GuiSourceEdit::unCommentSelection(QTextCursor const & cursorIn)
+{
+       QTextCursor cursor = cursorIn;
+       BlockRangeInfo blockRangeInfo = getBlockRangeInfo(cursor);
+       cursor.beginEditBlock();
+
+       if (blockRangeInfo.end > blockRangeInfo.start &&
+                       blockRangeInfo.endBlock.position() == 
blockRangeInfo.end) {
+               --blockRangeInfo.end;
+               blockRangeInfo.endBlock = blockRangeInfo.endBlock.previous();
+       }
+
+       bool hasSelection = cursor.hasSelection();
+
+       bool oneBlock = blockRangeInfo.startBlock == blockRangeInfo.endBlock;
+
+       blockRangeInfo.endBlock = blockRangeInfo.endBlock.next();
+       bool doUncomment = true;
+
+       // Check whether uncommenting
+       for (QTextBlock block = blockRangeInfo.startBlock;
+                block != blockRangeInfo.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 = blockRangeInfo.startBlock;
+                block != blockRangeInfo.endBlock; block = block.next()) {
+               const QString text = block.text();
+               int tabs = 0;
+               for (QChar c : text) {
+                       if (c == QChar::Tabulation)
+                               tabs++;
+                       if (!c.isSpace())
+                               break;
+               }
+               minIndent = qMin(minIndent, tabs);
+       }
+
+       if (minIndent == 1000) minIndent = 0;
+
+       const int singleLine_Length = singleLine_.length();
+       for (QTextBlock block = blockRangeInfo.startBlock;
+                block != blockRangeInfo.endBlock; block = block.next()) {
+               if (doUncomment) {
+                       QString text = block.text();
+                       int i = 0;
+                       while (i <= text.size() - singleLine_Length) {
+                               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,
+                                                           singleLine_Length + 
(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) {
+               blockRangeInfo.start = blockRangeInfo.startBlock.position(); // 
move the comment into the selection
+               int lastSelPos = blockRangeInfo.anchorIsStart ? 
cursor.position()
+                                                             : cursor.anchor();
+               if (blockRangeInfo.anchorIsStart) {
+                       cursor.setPosition(blockRangeInfo.start);
+                       cursor.setPosition(lastSelPos, QTextCursor::KeepAnchor);
+               } else {
+                       cursor.setPosition(lastSelPos);
+                       cursor.setPosition(blockRangeInfo.start, 
QTextCursor::KeepAnchor);
+               }
+       }
+       return cursor;
+}
+
+bool GuiSourceEdit::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;
+}
+
+}
+}
+
+#include "moc_GuiSourceEdit.cpp"
diff --git a/src/frontends/qt/GuiSourceEdit.h b/src/frontends/qt/GuiSourceEdit.h
new file mode 100644
index 0000000000..c88c80c626
--- /dev/null
+++ b/src/frontends/qt/GuiSourceEdit.h
@@ -0,0 +1,57 @@
+// -*- C++ -*-
+/**
+ * \file GuiClickableLabel.h
+ * This file is part of LyX, the document processor.
+ * Licence details can be found in the file COPYING.
+ *
+ * Full author contact details are available in file CREDITS.
+ */
+
+#ifndef GUISOURCEEDIT_H
+#define GUISOURCEEDIT_H
+
+#include <QTextBlock>
+#include <QTextEdit>
+
+namespace lyx {
+namespace frontend {
+
+class GuiSourceEdit : public QTextEdit
+{
+       Q_OBJECT
+public:
+       GuiSourceEdit(QWidget * parent);
+       void setTabStop(int spaces);
+       int tabStop() { return tabStop_; };
+       void setSingleLine(QString const & singleLine) { singleLine_ = 
singleLine; };
+       QString singleLine() { return singleLine_; };
+
+private:
+       struct BlockRangeInfo {
+               int pos;
+               int anchor;
+               int start;
+               int end;
+               bool anchorIsStart;
+               QTextBlock startBlock;
+               QTextBlock endBlock;
+       };
+
+       BlockRangeInfo getBlockRangeInfo(QTextCursor const & cursorIn) const;
+       void keyPressEvent(QKeyEvent * event);
+       // inherit indentation from previous block
+       QTextCursor inheritIndent(QTextCursor const & cursorIn);
+       // (un)indent blocks
+       QTextCursor unIndent(QTextCursor const & cursorIn, bool unIndent = 
false);
+       // (un)comment blocks
+       QTextCursor unCommentSelection(QTextCursor const & cursorIn);
+       bool isComment(const QString & text, int index, const QString & 
commentType);
+
+       int tabStop_ = 4;
+       QString singleLine_ = "#";
+};
+
+}
+}
+
+#endif // GUISOURCEEDIT_H
diff --git a/src/frontends/qt/Makefile.am b/src/frontends/qt/Makefile.am
index 9ca258d9d3..486b28fd9b 100644
--- a/src/frontends/qt/Makefile.am
+++ b/src/frontends/qt/Makefile.am
@@ -118,6 +118,7 @@ SOURCEFILES = \
        GuiSendto.cpp \
        GuiSetBorder.cpp \
        GuiShowFile.cpp \
+       GuiSourceEdit.cpp \
        GuiSpellchecker.cpp \
        GuiSymbols.cpp \
        GuiTabular.cpp \
@@ -232,6 +233,7 @@ MOCHEADER = \
        GuiSendto.h \
        GuiSetBorder.h \
        GuiShowFile.h \
+       GuiSourceEdit.h \
        GuiSpellchecker.h \
        GuiSymbols.h \
        GuiTabularCreate.h \
diff --git a/src/frontends/qt/ui/LocalLayoutUi.ui 
b/src/frontends/qt/ui/LocalLayoutUi.ui
index fc753b7c15..bcc71e14e5 100644
--- a/src/frontends/qt/ui/LocalLayoutUi.ui
+++ b/src/frontends/qt/ui/LocalLayoutUi.ui
@@ -15,7 +15,7 @@
   </property>
   <layout class="QGridLayout" name="gridLayout_2">
    <item row="0" column="0">
-    <widget class="QTextEdit" name="locallayoutTE">
+    <widget class="lyx::frontend::GuiSourceEdit" name="locallayoutTE">
      <property name="toolTip">
       <string>Document-specific layout information</string>
      </property>
@@ -105,6 +105,13 @@
    </item>
   </layout>
  </widget>
+ <customwidgets>
+  <customwidget>
+   <class>lyx::frontend::GuiSourceEdit</class>
+   <extends>QTextEdit</extends>
+   <header>GuiSourceEdit.h</header>
+  </customwidget>
+ </customwidgets>
  <includes>
   <include location="local">qt_i18n.h</include>
  </includes>
diff --git a/src/frontends/qt/ui/PreambleUi.ui 
b/src/frontends/qt/ui/PreambleUi.ui
index 8a7015c6dd..43084ee315 100644
--- a/src/frontends/qt/ui/PreambleUi.ui
+++ b/src/frontends/qt/ui/PreambleUi.ui
@@ -50,7 +50,7 @@
     </widget>
    </item>
    <item row="0" column="0" colspan="3">
-    <widget class="QTextEdit" name="preambleTE">
+    <widget class="lyx::frontend::GuiSourceEdit" name="preambleTE">
      <property name="acceptRichText">
       <bool>false</bool>
      </property>
@@ -58,6 +58,13 @@
    </item>
   </layout>
  </widget>
+ <customwidgets>
+  <customwidget>
+   <class>lyx::frontend::GuiSourceEdit</class>
+   <extends>QTextEdit</extends>
+   <header>GuiSourceEdit.h</header>
+  </customwidget>
+ </customwidgets>
  <includes>
   <include location="local">qt_i18n.h</include>
  </includes>
-- 
2.24.3 (Apple Git-128)

-- 
lyx-devel mailing list
lyx-devel@lists.lyx.org
http://lists.lyx.org/mailman/listinfo/lyx-devel

Reply via email to