From e7f784ca7393bddb60cbb111d3bdb7cd0e6d122e Mon Sep 17 00:00:00 2001 From: "Junker, Gregory" Date: Thu, 16 Jan 2020 14:18:01 -0800 Subject: [PATCH] Support configurable trailing-whitespace cleanup Allow the user to configure how trailing whitespace is handled. In some file types, for example, Markdown, trailing whitespace is semantically important. This change allows the user to select, via delimited list of wildcard filename patterns, which files to ignore for trailing whitespace cleanup. Task-number: QTCREATORBUG-13358 Change-Id: Ie6814d8c178bed8e3de78e6d359b9940d2ba0ead Reviewed-by: David Schulz --- .../texteditor/behaviorsettingswidget.cpp | 11 ++ .../texteditor/behaviorsettingswidget.ui | 147 ++++++++++++++---- src/plugins/texteditor/storagesettings.cpp | 52 ++++++- src/plugins/texteditor/storagesettings.h | 5 + src/plugins/texteditor/textdocument.cpp | 26 +++- src/plugins/texteditor/textdocument.h | 2 +- 6 files changed, 205 insertions(+), 38 deletions(-) diff --git a/src/plugins/texteditor/behaviorsettingswidget.cpp b/src/plugins/texteditor/behaviorsettingswidget.cpp index f5dc751eb6a..fe7e983fade 100644 --- a/src/plugins/texteditor/behaviorsettingswidget.cpp +++ b/src/plugins/texteditor/behaviorsettingswidget.cpp @@ -101,6 +101,8 @@ BehaviorSettingsWidget::BehaviorSettingsWidget(QWidget *parent) this, &BehaviorSettingsWidget::slotStorageSettingsChanged); connect(d->m_ui.cleanIndentation, &QAbstractButton::clicked, this, &BehaviorSettingsWidget::slotStorageSettingsChanged); + connect(d->m_ui.skipTrailingWhitespace, &QAbstractButton::clicked, + this, &BehaviorSettingsWidget::slotStorageSettingsChanged); connect(d->m_ui.mouseHiding, &QAbstractButton::clicked, this, &BehaviorSettingsWidget::slotBehaviorSettingsChanged); connect(d->m_ui.mouseNavigation, &QAbstractButton::clicked, @@ -190,6 +192,9 @@ void BehaviorSettingsWidget::setAssignedStorageSettings(const StorageSettings &s d->m_ui.inEntireDocument->setChecked(storageSettings.m_inEntireDocument); d->m_ui.cleanIndentation->setChecked(storageSettings.m_cleanIndentation); d->m_ui.addFinalNewLine->setChecked(storageSettings.m_addFinalNewLine); + d->m_ui.skipTrailingWhitespace->setChecked(storageSettings.m_skipTrailingWhitespace); + d->m_ui.ignoreFileTypes->setText(storageSettings.m_ignoreFileTypes); + d->m_ui.ignoreFileTypes->setEnabled(d->m_ui.skipTrailingWhitespace->isChecked()); } void BehaviorSettingsWidget::assignedStorageSettings(StorageSettings *storageSettings) const @@ -198,6 +203,8 @@ void BehaviorSettingsWidget::assignedStorageSettings(StorageSettings *storageSet storageSettings->m_inEntireDocument = d->m_ui.inEntireDocument->isChecked(); storageSettings->m_cleanIndentation = d->m_ui.cleanIndentation->isChecked(); storageSettings->m_addFinalNewLine = d->m_ui.addFinalNewLine->isChecked(); + storageSettings->m_skipTrailingWhitespace = d->m_ui.skipTrailingWhitespace->isChecked(); + storageSettings->m_ignoreFileTypes = d->m_ui.ignoreFileTypes->text(); } void BehaviorSettingsWidget::updateConstrainTooltipsBoxTooltip() const @@ -273,6 +280,10 @@ void BehaviorSettingsWidget::slotStorageSettingsChanged() { StorageSettings settings; assignedStorageSettings(&settings); + + bool ignoreFileTypesEnabled = d->m_ui.cleanWhitespace->isChecked() && d->m_ui.skipTrailingWhitespace->isChecked(); + d->m_ui.ignoreFileTypes->setEnabled(ignoreFileTypesEnabled); + emit storageSettingsChanged(settings); } diff --git a/src/plugins/texteditor/behaviorsettingswidget.ui b/src/plugins/texteditor/behaviorsettingswidget.ui index c36bb9ae47b..d6d9fed5c60 100644 --- a/src/plugins/texteditor/behaviorsettingswidget.ui +++ b/src/plugins/texteditor/behaviorsettingswidget.ui @@ -7,7 +7,7 @@ 0 0 801 - 547 + 693 @@ -169,13 +169,72 @@ Specifies how backspace interacts with indentation. Cleanups Upon Saving - - + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + false + + + For the file patterns listed, do not trim trailing whitespace. + + + Skip clean whitespace for file types: + + + false + + + false + + + + + + + false + + + false + + + List of wildcard-aware file patterns, separated by commas or semicolons. + + + + + + + + + + + + + true + - Removes trailing whitespace upon saving. + Always writes a newline character at the end of the file. - &Clean whitespace + &Ensure newline at end of file @@ -195,6 +254,29 @@ Specifies how backspace interacts with indentation. + + + + Removes trailing whitespace upon saving. + + + &Clean whitespace + + + + + + + false + + + Corrects leading whitespace according to tab settings. + + + Clean indentation + + + @@ -214,29 +296,6 @@ Specifies how backspace interacts with indentation. - - - - false - - - Corrects leading whitespace according to tab settings. - - - Clean indentation - - - - - - - Always writes a newline character at the end of the file. - - - &Ensure newline at end of file - - - @@ -521,5 +580,37 @@ Specifies how backspace interacts with indentation. + + cleanWhitespace + toggled(bool) + skipTrailingWhitespace + setEnabled(bool) + + + 530 + 48 + + + 548 + 144 + + + + + cleanWhitespace + toggled(bool) + ignoreFileTypes + setEnabled(bool) + + + 530 + 48 + + + 556 + 177 + + + diff --git a/src/plugins/texteditor/storagesettings.cpp b/src/plugins/texteditor/storagesettings.cpp index a1c4645b0cf..2053ffa440c 100644 --- a/src/plugins/texteditor/storagesettings.cpp +++ b/src/plugins/texteditor/storagesettings.cpp @@ -27,6 +27,7 @@ #include +#include #include #include @@ -36,13 +37,18 @@ static const char cleanWhitespaceKey[] = "cleanWhitespace"; static const char inEntireDocumentKey[] = "inEntireDocument"; static const char addFinalNewLineKey[] = "addFinalNewLine"; static const char cleanIndentationKey[] = "cleanIndentation"; +static const char skipTrailingWhitespaceKey[] = "skipTrailingWhitespace"; +static const char ignoreFileTypesKey[] = "ignoreFileTypes"; static const char groupPostfix[] = "StorageSettings"; +static const char defaultTrailingWhitespaceBlacklist[] = "*.md, *.MD, Makefile"; StorageSettings::StorageSettings() - : m_cleanWhitespace(true), + : m_ignoreFileTypes(defaultTrailingWhitespaceBlacklist), + m_cleanWhitespace(true), m_inEntireDocument(false), m_addFinalNewLine(true), - m_cleanIndentation(true) + m_cleanIndentation(true), + m_skipTrailingWhitespace(true) { } @@ -63,6 +69,8 @@ void StorageSettings::toMap(const QString &prefix, QVariantMap *map) const map->insert(prefix + QLatin1String(inEntireDocumentKey), m_inEntireDocument); map->insert(prefix + QLatin1String(addFinalNewLineKey), m_addFinalNewLine); map->insert(prefix + QLatin1String(cleanIndentationKey), m_cleanIndentation); + map->insert(prefix + QLatin1String(skipTrailingWhitespaceKey), m_skipTrailingWhitespace); + map->insert(prefix + QLatin1String(ignoreFileTypesKey), m_ignoreFileTypes.toLatin1().data()); } void StorageSettings::fromMap(const QString &prefix, const QVariantMap &map) @@ -75,6 +83,42 @@ void StorageSettings::fromMap(const QString &prefix, const QVariantMap &map) map.value(prefix + QLatin1String(addFinalNewLineKey), m_addFinalNewLine).toBool(); m_cleanIndentation = map.value(prefix + QLatin1String(cleanIndentationKey), m_cleanIndentation).toBool(); + m_skipTrailingWhitespace = + map.value(prefix + QLatin1String(skipTrailingWhitespaceKey), m_skipTrailingWhitespace).toBool(); + m_ignoreFileTypes = + map.value(prefix + QLatin1String(ignoreFileTypesKey), m_ignoreFileTypes).toString(); +} + +bool StorageSettings::removeTrailingWhitespace(const QString &fileName) const +{ + // if the user has elected not to trim trailing whitespace altogether, then + // early out here + if (!m_skipTrailingWhitespace) { + return true; + } + + const QString ignoreFileTypesRegExp(R"(\s*((?>\*\.)?[\w\d\.\*]+)[,;]?\s*)"); + + // use the ignore-files regex to extract the specified file patterns + QRegularExpression re(ignoreFileTypesRegExp); + QRegularExpressionMatchIterator iter = re.globalMatch(m_ignoreFileTypes); + + while (iter.hasNext()) { + QRegularExpressionMatch match = iter.next(); + QString pattern = match.captured(1); + + QString wildcardRegExp = QRegularExpression::wildcardToRegularExpression(pattern); + QRegularExpression patternRegExp(wildcardRegExp); + QRegularExpressionMatch patternMatch = patternRegExp.match(fileName); + if (patternMatch.hasMatch()) { + // if the filename has a pattern we want to ignore, then we need to return + // false ("don't remove trailing whitespace") + return false; + } + } + + // the supplied pattern does not match, so we want to remove trailing whitespace + return true; } bool StorageSettings::equals(const StorageSettings &ts) const @@ -82,7 +126,9 @@ bool StorageSettings::equals(const StorageSettings &ts) const return m_addFinalNewLine == ts.m_addFinalNewLine && m_cleanWhitespace == ts.m_cleanWhitespace && m_inEntireDocument == ts.m_inEntireDocument - && m_cleanIndentation == ts.m_cleanIndentation; + && m_cleanIndentation == ts.m_cleanIndentation + && m_skipTrailingWhitespace == ts.m_skipTrailingWhitespace + && m_ignoreFileTypes == ts.m_ignoreFileTypes; } } // namespace TextEditor diff --git a/src/plugins/texteditor/storagesettings.h b/src/plugins/texteditor/storagesettings.h index ffe7823f0fd..cd9c176b597 100644 --- a/src/plugins/texteditor/storagesettings.h +++ b/src/plugins/texteditor/storagesettings.h @@ -46,12 +46,17 @@ public: void toMap(const QString &prefix, QVariantMap *map) const; void fromMap(const QString &prefix, const QVariantMap &map); + // calculated based on boolean setting plus file type blacklist examination + bool removeTrailingWhitespace(const QString &filePattern) const; + bool equals(const StorageSettings &ts) const; + QString m_ignoreFileTypes; bool m_cleanWhitespace; bool m_inEntireDocument; bool m_addFinalNewLine; bool m_cleanIndentation; + bool m_skipTrailingWhitespace; }; inline bool operator==(const StorageSettings &t1, const StorageSettings &t2) { return t1.equals(t2); } diff --git a/src/plugins/texteditor/textdocument.cpp b/src/plugins/texteditor/textdocument.cpp index 319f77a3978..0f3ef7a05f5 100644 --- a/src/plugins/texteditor/textdocument.cpp +++ b/src/plugins/texteditor/textdocument.cpp @@ -622,7 +622,7 @@ bool TextDocument::save(QString *errorString, const QString &saveFileName, bool cursor.movePosition(QTextCursor::Start); if (d->m_storageSettings.m_cleanWhitespace) - cleanWhitespace(cursor, d->m_storageSettings.m_cleanIndentation, d->m_storageSettings.m_inEntireDocument); + cleanWhitespace(cursor, d->m_storageSettings); if (d->m_storageSettings.m_addFinalNewLine) ensureFinalNewLine(cursor); cursor.endEditBlock(); @@ -883,14 +883,22 @@ void TextDocument::cleanWhitespace(const QTextCursor &cursor) QTextCursor copyCursor = cursor; copyCursor.setVisualNavigation(false); copyCursor.beginEditBlock(); - cleanWhitespace(copyCursor, true, true); + + cleanWhitespace(copyCursor, d->m_storageSettings); + if (!hasSelection) ensureFinalNewLine(copyCursor); + copyCursor.endEditBlock(); } -void TextDocument::cleanWhitespace(QTextCursor &cursor, bool cleanIndentation, bool inEntireDocument) +void TextDocument::cleanWhitespace(QTextCursor &cursor, const StorageSettings &storageSettings) { + if (!d->m_storageSettings.m_cleanWhitespace) + return; + + const QString fileName(filePath().fileName()); + auto documentLayout = qobject_cast(d->m_document.documentLayout()); Q_ASSERT(cursor.visualNavigation() == false); @@ -901,7 +909,7 @@ void TextDocument::cleanWhitespace(QTextCursor &cursor, bool cleanIndentation, b QVector blocks; while (block.isValid() && block != end) { - if (inEntireDocument || block.revision() != documentLayout->lastSaveRevision) + if (storageSettings.m_inEntireDocument || block.revision() != documentLayout->lastSaveRevision) blocks.append(block); block = block.next(); } @@ -914,9 +922,12 @@ void TextDocument::cleanWhitespace(QTextCursor &cursor, bool cleanIndentation, b foreach (block, blocks) { QString blockText = block.text(); - currentTabSettings.removeTrailingWhitespace(cursor, block); + + if (storageSettings.removeTrailingWhitespace(fileName)) + currentTabSettings.removeTrailingWhitespace(cursor, block); + const int indent = indentations[block.blockNumber()]; - if (cleanIndentation && !currentTabSettings.isIndentationClean(block, indent)) { + if (storageSettings.m_cleanIndentation && !currentTabSettings.isIndentationClean(block, indent)) { cursor.setPosition(block.position()); int firstNonSpace = currentTabSettings.firstNonSpace(blockText); if (firstNonSpace == blockText.length()) { @@ -934,6 +945,9 @@ void TextDocument::cleanWhitespace(QTextCursor &cursor, bool cleanIndentation, b void TextDocument::ensureFinalNewLine(QTextCursor& cursor) { + if (!d->m_storageSettings.m_addFinalNewLine) + return; + cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); bool emptyFile = !cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); diff --git a/src/plugins/texteditor/textdocument.h b/src/plugins/texteditor/textdocument.h index 447edd0e508..35e6b334eff 100644 --- a/src/plugins/texteditor/textdocument.h +++ b/src/plugins/texteditor/textdocument.h @@ -169,7 +169,7 @@ protected: private: OpenResult openImpl(QString *errorString, const QString &fileName, const QString &realFileName, bool reload); - void cleanWhitespace(QTextCursor &cursor, bool cleanIndentation, bool inEntireDocument); + void cleanWhitespace(QTextCursor &cursor, const StorageSettings &storageSettings); void ensureFinalNewLine(QTextCursor &cursor); void modificationChanged(bool modified); void updateLayout() const;