[LyX/master] Extended comment and indentation for source code
Jean-Marc Lasgouttes
lasgouttes at lyx.org
Tue Jan 31 22:09:52 UTC 2023
commit 439c09912482489e79399fd9e10fa2b7f717f565
Author: Daniel Ramoeller <d.lyx at 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>
More information about the lyx-cvs
mailing list