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
This commit is contained in:
Daniel Ramoeller 2023-01-14 13:22:57 +01:00 committed by Jean-Marc Lasgouttes
parent 60d0cc2611
commit 439c099124
7 changed files with 442 additions and 22 deletions

View File

@ -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
}

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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 \

View File

@ -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>

View File

@ -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>