commit 439c09912482489e79399fd9e10fa2b7f717f565
Author: Daniel Ramoeller <d....@web.de>
Date:   Sat Jan 14 13:22:57 2023 +0100

    Extended comment and indentation for source code
    
    - automatically inherit indentation from previous block
    - (un)indent blocks
    - (un)comment blocks
    - add feature to show tabs and spaces
---
 src/frontends/qt/GuiDocument.cpp      |   21 +--
 src/frontends/qt/GuiSourceEdit.cpp    |  311 +++++++++++++++++++++++++++++++++
 src/frontends/qt/GuiSourceEdit.h      |   94 ++++++++++
 src/frontends/qt/LaTeXHighlighter.cpp |   18 ++
 src/frontends/qt/Makefile.am          |    2 +
 src/frontends/qt/ui/LocalLayoutUi.ui  |    9 +-
 src/frontends/qt/ui/PreambleUi.ui     |    9 +-
 7 files changed, 442 insertions(+), 22 deletions(-)

diff --git a/src/frontends/qt/GuiDocument.cpp b/src/frontends/qt/GuiDocument.cpp
index 60f9f84..7b4cedb 100644
--- a/src/frontends/qt/GuiDocument.cpp
+++ b/src/frontends/qt/GuiDocument.cpp
@@ -479,7 +479,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->setCommentMarker("%");
        setFocusProxy(preambleTE);
        // Install event filter on find line edit to capture return/enter key
        findLE->installEventFilter(this);
@@ -489,15 +489,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
 }
 
 
@@ -627,20 +618,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 0000000..9b008a5
--- /dev/null
+++ b/src/frontends/qt/GuiSourceEdit.cpp
@@ -0,0 +1,311 @@
+// -*- C++ -*-
+/**
+ * \file GuiSourceEdit.cpp
+ * 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 <config.h>
+
+#include "GuiSourceEdit.h"
+
+#include "qt_helpers.h"
+
+#include <QMenu>
+#include <QRegularExpression>
+
+namespace lyx {
+namespace frontend {
+
+GuiSourceEdit::GuiSourceEdit(QWidget *parent) : QTextEdit(parent)
+{
+       setWordWrapMode(QTextOption::NoWrap);
+       // Set the default tab stop
+       setTabStop(tabStop_);
+       setContextMenuPolicy(Qt::CustomContextMenu);
+       connect(this, SIGNAL(customContextMenuRequested(QPoint)),
+               this, SLOT(showMenu(QPoint)));
+}
+
+void GuiSourceEdit::showMenu(const QPoint& pos)
+{
+       // Move the cursor to the click position unless clicked within selection
+       QTextCursor cursor = textCursor();
+       int const textPos = cursorForPosition(pos).position();
+       bool const textPosInSel = textCursor().selectionStart() <= textPos &&
+                                 textPos <= textCursor().selectionEnd();
+       if (!textCursor().hasSelection() || !textPosInSel) {
+               cursor.setPosition(textPos);
+               setTextCursor(cursor);
+       }
+       // The standard menu
+       QMenu * menu = QTextEdit::createStandardContextMenu();
+       QAction * firstAction = menu->actions().at(0);
+       // Insert toggle comment entry at the top
+       QKeySequence keySeq = QKeySequence(Qt::ControlModifier | Qt::Key_Slash);
+       QAction * toggleComment = new QAction(qt_("Toggle Comment") + "\t" +
+               keySeq.toString(QKeySequence::NativeText), menu);
+       connect(toggleComment, SIGNAL(triggered()), this, 
SLOT(toggleComment()));
+       menu->insertAction(firstAction, toggleComment);
+       // Insert toggle spaces and tabs entry at the top
+       QAction * showTabsAndSpaces = new QAction(qt_("Show Tabs and Spaces"), 
menu);
+       showTabsAndSpaces->setCheckable(true);
+       auto currentFlags = document()->defaultTextOption().flags();
+       showTabsAndSpaces->setChecked(currentFlags &
+                                     QTextOption::ShowTabsAndSpaces);
+       connect(showTabsAndSpaces, SIGNAL(triggered()), this,
+               SLOT(toggleShowTabsAndSpaces()));
+       menu->insertAction(firstAction, showTabsAndSpaces);
+       // Add separator to default entries
+       menu->insertSeparator(firstAction);
+       menu->exec(mapToGlobal(pos));
+}
+
+void GuiSourceEdit::setCommentMarker(QString marker)
+{
+       commentMarker_ = marker;
+}
+
+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
+}
+
+void GuiSourceEdit::setFont(const QFont & font)
+{
+       QTextEdit::setFont(font);
+       // Re-calculate tabstop width based on new font
+       setTabStop(tabStop_);
+}
+
+QTextBlock GuiSourceEdit::blockAtSelPos(Position position) const
+{
+       QTextCursor cursor = textCursor();
+       QTextDocument * doc = cursor.document();
+       int pos = position == START ? qMin(cursor.anchor(), cursor.position()) :
+                                     qMax(cursor.anchor(), cursor.position());
+       return doc->findBlock(pos);
+}
+
+QTextCursor GuiSourceEdit::cursorAt(int position) const {
+       // Create text cursor
+       QTextCursor cursor = textCursor();
+       // Move the cursor to position
+       cursor.setPosition(position);
+       return cursor;
+}
+
+void GuiSourceEdit::removeMarker(int positionStart, QString marker,
+                                 bool addedSpace)
+{
+       // Create text cursor
+       QTextCursor cursor = cursorAt(positionStart);
+       QTextBlock const block = cursor.block();
+       QString blockText = block.text();
+       int const index = blockText.indexOf(marker);
+       int length = marker.length();
+       if (index != -1) {
+               // Check for extra space after marker
+               if (addedSpace && blockText.remove(0, index + 
length).startsWith(" "))
+                       ++length;
+               cursor.movePosition(QTextCursor::NextCharacter, 
QTextCursor::MoveAnchor,
+                                   index);
+               cursor.movePosition(QTextCursor::NextCharacter, 
QTextCursor::KeepAnchor,
+                                   length);
+               cursor.deleteChar();
+       }
+}
+
+void GuiSourceEdit::insertMarkerAtIndentation(int positionStart, QString 
marker,
+                                              bool addedSpace, int indentation)
+{
+       // Create text cursor
+       QTextCursor cursor = cursorAt(positionStart);
+       // Move the cursor to the given indentation
+       int i = 0;
+       while (i < indentation) {
+               if (toPlainText().at(cursor.position()) == '\t')
+                       i += tabStop_;
+               else
+                       ++i;
+               cursor.movePosition(QTextCursor::NextCharacter, 
QTextCursor::MoveAnchor);
+       }
+       // Add the comment marker
+       cursor.insertText(marker + (addedSpace ? " " : ""), QTextCharFormat());
+}
+
+void GuiSourceEdit::modifyMarkerInSel(QString marker, Modification 
modification,
+                                      bool allEmpty, bool addedSpace)
+{
+       QTextBlock const startBlock = blockAtSelPos(START);
+       QTextBlock const endBlock = blockAtSelPos(END);
+       QTextBlock const endBlockNext = endBlock.next();
+       // Create text cursor
+       QTextCursor cursor = textCursor();
+       cursor.beginEditBlock();
+       for (QTextBlock block = startBlock; block != endBlockNext;
+            block = block.next()) {
+               if (modification == REMOVE)
+                       removeMarker(block.position(), marker, addedSpace);
+               // Disregard white space blocks
+               else if (allEmpty || !block.text().trimmed().isEmpty()) {
+                       insertMarkerAtIndentation(block.position(), marker, 
addedSpace,
+                                                 selMinIndentation(allEmpty));
+               }
+       }
+       cursor.endEditBlock();
+}
+
+int GuiSourceEdit::getIndentation(QString text) const
+{
+       int tabs = 0;
+       for (QChar c : text) {
+               if (c == QChar::Tabulation)
+                       ++tabs;
+               if (!c.isSpace())
+                       break;
+       }
+       return tabs;
+}
+
+QString GuiSourceEdit::getIndentationString(QString text) const
+{
+       // Regex to capture indentation string (i.e. spaces and tab stops)
+       QRegularExpression static re("^( |\t)+");
+       return re.match(text).captured(0);
+}
+
+void GuiSourceEdit::newLineWithInheritedIndentation()
+{
+       QTextCursor cursor = textCursor();
+       cursor.beginEditBlock();
+       // Start new line
+       cursor.insertText("\n", QTextCharFormat());
+       // Insert as many tabstops as on the previous block
+       QTextBlock const previousBlock = blockAtSelPos(START).previous();
+       QString const indentation = getIndentationString(previousBlock.text());
+       cursor.insertText(indentation);
+       cursor.endEditBlock();
+}
+
+int GuiSourceEdit::getLengthInSpaces(QString const & text) const
+{
+       // Replace tab stops by spaces and return length
+       return QString(text).replace("\t", QString(" ").repeated(tabStop_))
+                       .length();
+}
+
+int GuiSourceEdit::selMinIndentation(bool allEmpty) const
+{
+       QTextBlock const startBlock = blockAtSelPos(START);
+       QTextBlock const endBlock = blockAtSelPos(END);
+       QTextBlock const endBlockNext = endBlock.next();
+       QString minIndentationString = getIndentationString(startBlock.text());
+       for (QTextBlock block = startBlock; block != endBlockNext;
+            block = block.next()) {
+               QString text = block.text();
+               if (allEmpty || !text.trimmed().isEmpty()) {
+                       QString curIndentationString = 
getIndentationString(text);
+                       // Chop off indentation until they have the same length
+                       while (!minIndentationString.isEmpty()) {
+                               int minLen = 
getLengthInSpaces(minIndentationString);
+                               int curLen = 
getLengthInSpaces(curIndentationString);
+                               if (minLen == curLen)
+                                       break;
+                               else if (minLen > curLen)
+                                       minIndentationString.chop(1);
+                               else
+                                       curIndentationString.chop(1);
+                       }
+               }
+       }
+       return getLengthInSpaces(minIndentationString);
+}
+
+bool GuiSourceEdit::selBlocksStartWith(QString marker) const
+{
+       QTextBlock const startBlock = blockAtSelPos(START);
+       QTextBlock const endBlock = blockAtSelPos(END);
+       QTextBlock const endBlockNext = endBlock.next();
+       for (QTextBlock block = startBlock; block != endBlockNext;
+            block = block.next()) {
+               QString const blockText = block.text();
+               QString trimmedText = blockText.trimmed();
+               // Disregard white space blocks
+               if (trimmedText.isEmpty())
+                       continue;
+               else if (!trimmedText.startsWith(marker))
+                       return false;
+       }
+       return true;
+}
+
+bool GuiSourceEdit::selBlocksWhiteSpace() const
+{
+       QTextBlock const startBlock = blockAtSelPos(START);
+       QTextBlock const endBlockNext = blockAtSelPos(END).next();
+       for (QTextBlock block = startBlock; block != endBlockNext;
+            block = block.next()) {
+               QString const blockText = block.text();
+               QString const trimmedText = blockText.trimmed();
+               // Disregard white space blocks
+               if (trimmedText.isEmpty())
+                       continue;
+               else
+                       return false;
+       }
+       return true;
+}
+
+void GuiSourceEdit::toggleComment()
+{
+       bool const allEmpty = selBlocksWhiteSpace();
+       modifyMarkerInSel(commentMarker_,
+                         !allEmpty && selBlocksStartWith(commentMarker_) ?
+                             REMOVE : INSERT,
+                         allEmpty, addedSpaceAfterComment_);
+}
+
+void GuiSourceEdit::toggleShowTabsAndSpaces()
+{
+       QTextOption option = document()->defaultTextOption();
+       auto currentFlags = document()->defaultTextOption().flags();
+       if (currentFlags & QTextOption::ShowTabsAndSpaces)
+               currentFlags &= ~QTextOption::ShowTabsAndSpaces;
+       else
+               currentFlags |= QTextOption::ShowTabsAndSpaces;
+       option.setFlags(currentFlags);
+       document()->setDefaultTextOption(option);
+}
+
+void GuiSourceEdit::keyPressEvent(QKeyEvent *event)
+{
+       if (event->modifiers() == Qt::ControlModifier &&
+           event->key() == Qt::Key_Slash)
+               toggleComment();
+       else if (event->key() == Qt::Key_Tab &&
+                blockAtSelPos(START) != blockAtSelPos(END))
+               modifyMarkerInSel("\t", INSERT, selBlocksWhiteSpace(), false);
+       else if (event->key() == Qt::Key_Backtab)
+               modifyMarkerInSel("\t", REMOVE);
+       else if (event->key() == Qt::Key_Return)
+               newLineWithInheritedIndentation();
+       else
+               // Call base class for other events
+               QTextEdit::keyPressEvent(event);
+}
+
+} // namespace frontend
+} // namespace lyx
+
+#include "moc_GuiSourceEdit.cpp"
diff --git a/src/frontends/qt/GuiSourceEdit.h b/src/frontends/qt/GuiSourceEdit.h
new file mode 100644
index 0000000..4a8bc1c
--- /dev/null
+++ b/src/frontends/qt/GuiSourceEdit.h
@@ -0,0 +1,94 @@
+// -*- 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.
+ */
+
+#ifndef GUISOURCEEDIT_H
+#define GUISOURCEEDIT_H
+
+#include <QTextEdit>
+#include <QKeyEvent>
+#include <QTextCursor>
+#include <QTextBlock>
+
+namespace lyx {
+namespace frontend {
+
+class GuiSourceEdit : public QTextEdit
+{
+       Q_OBJECT
+
+public:
+       explicit GuiSourceEdit(QWidget *parent = nullptr);
+       // Set and get tab stop in number of spaces
+       void setTabStop(int spaces);
+       int tabStop() const {return tabStop_; };
+       // Set and get line marker, e.g. "//"
+       void setCommentMarker(QString marker);
+       QString commentMarker() const { return commentMarker_; };
+       // Set and get whether a space is added after the line marker
+       void setAddedSpaceAfterComment(int spaces);
+       bool addedSpaceAfterComment() const { return addedSpaceAfterComment_; };
+       // Set font and update tab stop
+       void setFont(const QFont & font);
+
+private Q_SLOTS:
+       void showMenu(const QPoint& pos);
+       void toggleComment();
+       void toggleShowTabsAndSpaces();
+
+protected:
+       void keyPressEvent(QKeyEvent *event) override;
+
+private:
+       enum Modification { INSERT, REMOVE };
+       enum Position { START, END };
+
+       // Get block at selection start/end
+       QTextBlock blockAtSelPos(Position position) const;
+       // Whether all blocks start with marker
+       bool selBlocksStartWith(QString marker) const;
+       // Wehther all blocks are white space
+       bool selBlocksWhiteSpace() const;
+       // Get length of string substituting tabs for spaces
+       int getLengthInSpaces(QString const & text) const;
+       // Minimum tab indentation the paragraphs selected start with
+       int selMinIndentation(bool allEmpty) const;
+       // Number of tabs as indentation
+       int getIndentation(QString text) const;
+       // String of white space
+       QString getIndentationString(QString text) const;
+       // Copy of textCursor() [at position]
+       QTextCursor cursorAt(int position) const;
+
+       // From positionStart remove marker from the line
+       void removeMarker(int positionStart, QString marker,
+                         bool addedSpace = false);
+       // From positionStart insert marker at indentation
+       void insertMarkerAtIndentation(int positionStart, QString marker,
+                                      bool addedSpace = false,
+                                      int indentation = 0);
+       // Modify (insert/remove) marker
+       void modifyMarkerInSel(QString marker,
+                              Modification modification = INSERT,
+                              bool allEmpty = false, bool addedSpace = false);
+       // Create a new line at cursor same indentation
+       void newLineWithInheritedIndentation();
+
+       // The comment marker
+       QString commentMarker_ = "#";
+       // The tab stop in spaces
+       int tabStop_ = 4;
+       // Whether a space gets added after the comment marker
+       bool addedSpaceAfterComment_ = true;
+};
+
+
+} // namespace frontend
+} // namespace lyx
+
+#endif // GUISOURCEEDIT_H
diff --git a/src/frontends/qt/LaTeXHighlighter.cpp 
b/src/frontends/qt/LaTeXHighlighter.cpp
index 80e7a09..51d5867 100644
--- a/src/frontends/qt/LaTeXHighlighter.cpp
+++ b/src/frontends/qt/LaTeXHighlighter.cpp
@@ -123,6 +123,14 @@ void LaTeXHighlighter::highlightBlock(QString const & text)
                setFormat(index, length, keywordFormat);
                index = exprKeyword.indexIn(text, index + length);
        }
+       // White space
+       QRegExp exprWhiteSpace("\\s");
+       index = exprWhiteSpace.indexIn(text);
+       while (index >= 0) {
+               int length = exprWhiteSpace.matchedLength();
+               setFormat(index, length, commentFormat);
+               index = exprWhiteSpace.indexIn(text, index + length);
+       }
        // %comment
        // Treat a line as a comment starting at a percent sign
        // * that is the first character in a line
@@ -237,6 +245,16 @@ void LaTeXHighlighter::highlightBlock(QString const & text)
                match = exprKeyword.match(text, index + length);
                index = match.capturedStart(0);
        }
+       // White space
+       QRegularExpression exprWhiteSpace("\\s");
+       match = exprWhiteSpace.match(text);
+       index = match.capturedStart(0);
+       while (index >= 0) {
+               int length = match.capturedLength(0);
+               setFormat(index, length, commentFormat);
+               match = exprWhiteSpace.match(text, index + length);
+               index = match.capturedStart(0);
+       }
        // %comment
        // Treat a line as a comment starting at a percent sign
        // * that is the first character in a line
diff --git a/src/frontends/qt/Makefile.am b/src/frontends/qt/Makefile.am
index 9ca258d..486b28f 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 1a3e178..c2377b1 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 eb27e91..dece982 100644
--- a/src/frontends/qt/ui/PreambleUi.ui
+++ b/src/frontends/qt/ui/PreambleUi.ui
@@ -54,7 +54,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>
@@ -62,6 +62,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>
-- 
lyx-cvs mailing list
lyx-cvs@lists.lyx.org
http://lists.lyx.org/mailman/listinfo/lyx-cvs

Reply via email to