Indentation and (un)commenting in local layout and preamble

Daniel xracoonx at gmx.de
Sun Aug 14 18:20:20 UTC 2022


On 2022-08-14 18:38, Kornel Benko wrote:
> Applies cleanly and compiles. Rudimentary test looks good. (At least writing local
> format feels better than what we have now).
> 
> 	Kornel

Thanks for testing. I realized that I changed the tab stop size before 
setting the font which could lead to a wrong size. Very slightly 
modified patch attached. I only added

preambleTE->setTabStop(4);

and

locallayoutTE->setTabStop(4);

at the correct positions.

Daniel
-------------- next part --------------
From aef456245b29a2563204b021d58c944641be4a0e Mon Sep 17 00:00:00 2001
From: Daniel Ramoeller <d.lyx at web.de>
Date: Sun, 14 Aug 2022 20:16:02 +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     |  23 +--
 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, 336 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..7543d7126e 100644
--- a/src/frontends/qt/GuiDocument.cpp
+++ b/src/frontends/qt/GuiDocument.cpp
@@ -480,7 +480,8 @@ 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->setTabStop(4);
+	preambleTE->setSingleLine("%");
 	setFocusProxy(preambleTE);
 	connect(preambleTE, SIGNAL(textChanged()), this, SIGNAL(changed()));
 	connect(findLE, SIGNAL(textEdited(const QString &)), this, SLOT(checkFindButton()));
@@ -488,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
 }
 
 
@@ -606,20 +598,11 @@ LocalLayout::LocalLayout(QWidget * parent)
 	: UiWidget<Ui::LocalLayoutUi>(parent), current_id_(nullptr), validated_(false)
 {
 	locallayoutTE->setFont(guiApp->typewriterSystemFont());
-	locallayoutTE->setWordWrapMode(QTextOption::NoWrap);
+	locallayoutTE->setTabStop(4);
 	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