Indentation and (un)commenting in local layout and preamble

Daniel xracoonx at gmx.de
Sun Aug 14 07:27:40 UTC 2022


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
-------------- next part --------------
From a367b6d1e25fad1c12b7e72223ade88760d158e5 Mon Sep 17 00:00:00 2001
From: Daniel Ramoeller <d.lyx at 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)



More information about the lyx-devel mailing list