Editor: add function to guess tab settings for a document

It can be manually triggered from the menu of the tab settings tool
button.

Task-number: QTCREATORBUG-25628
Change-Id: Icde79f3579a035fddef354cfdd3bf9b594f28bef
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
David Schulz
2024-11-21 16:30:51 +01:00
parent a4dcec3f6c
commit baf02224a2
3 changed files with 103 additions and 3 deletions

View File

@@ -4,6 +4,7 @@
#include "tabsettings.h" #include "tabsettings.h"
#include <QDebug> #include <QDebug>
#include <QRandomGenerator>
#include <QTextCursor> #include <QTextCursor>
#include <QTextDocument> #include <QTextDocument>
@@ -50,6 +51,98 @@ void TabSettings::fromMap(const Store &map)
map.value(paddingModeKey, m_continuationAlignBehavior).toInt(); map.value(paddingModeKey, m_continuationAlignBehavior).toInt();
} }
TabSettings TabSettings::autoDetect(const QTextDocument *document) const
{
QTC_ASSERT(document, return *this);
const int blockCount = document->blockCount();
if (blockCount < 10)
return *this;
int totalIndentations = 0;
int indentationWithTabs = 0;
QMap<int, int> indentCount;
auto checkText =
[this, &totalIndentations, &indentCount, &indentationWithTabs](const QTextBlock &block) {
if (block.length() == 0)
return;
const QTextDocument *doc = block.document();
int pos = block.position();
bool hasTabs = false;
int indentation = 0;
// iterate ove the characters in the document is faster since we do not have to allocate
// a string for each block text when we are only interested in the first few characters
QChar c = doc->characterAt(pos);
while (c.isSpace() && c != QChar::ParagraphSeparator) {
if (c == QChar::Tabulation) {
hasTabs = true;
indentation += m_tabSize;
} else {
++indentation;
}
c = doc->characterAt(++pos);
}
// only track indentations that are at least 2 columns wide
if (indentation > 1) {
if (hasTabs)
++indentationWithTabs;
++indentCount[indentation];
++totalIndentations;
}
};
if (blockCount < 200) {
// check the indentation of all blocks if the document is shorter than 200 lines
for (QTextBlock block = document->firstBlock(); block.isValid(); block = block.next())
checkText(block);
} else {
// scanning the first and last 25 lines specifically since those most probably contain
// different indentations
const int startEndDelta = 25;
for (int delta = 0; delta < startEndDelta; ++delta) {
checkText(document->findBlockByNumber(delta));
checkText(document->findBlockByNumber(blockCount - 1 - delta));
}
// scan random lines until we have 100 indentations or checked a maximum of 2000 lines
// to limit the number of checks for large documents
QRandomGenerator gen(QDateTime::currentDateTime().toMSecsSinceEpoch());
int checks = 0;
while (totalIndentations < 100) {
++checks;
if (checks > 2000)
break;
const int blockNummer = gen.bounded(startEndDelta + 1, blockCount - startEndDelta - 2);
checkText(document->findBlockByNumber(blockNummer));
}
}
// find the most common indent
int mostCommonIndent = 0;
int mostCommonIndentCount = 0;
for (auto it = indentCount.cbegin(); it != indentCount.cend(); ++it) {
if (const int count = it.value(); count > mostCommonIndentCount) {
mostCommonIndentCount = count;
mostCommonIndent = it.key();
}
}
for (auto it = indentCount.cbegin(); it != indentCount.cend(); ++it) {
// check whether the smallest indent is a fraction of the most common indent
// to filter out some false positives
if (mostCommonIndent % it.key() == 0) {
TabSettings result = *this;
result.m_indentSize = it.key();
double relativeTabCount = double(indentationWithTabs) / double(totalIndentations);
result.m_tabPolicy = relativeTabCount > 0.5 ? TabSettings::TabsOnlyTabPolicy
: TabSettings::SpacesOnlyTabPolicy;
return result;
}
}
return *this;
}
bool TabSettings::cursorIsAtBeginningOfLine(const QTextCursor &cursor) bool TabSettings::cursorIsAtBeginningOfLine(const QTextCursor &cursor)
{ {
QString text = cursor.block().text(); QString text = cursor.block().text();
@@ -80,7 +173,7 @@ int TabSettings::firstNonSpace(const QString &text)
return i; return i;
} }
QString TabSettings::indentationString(const QString &text) const QString TabSettings::indentationString(const QString &text)
{ {
return text.left(firstNonSpace(text)); return text.left(firstNonSpace(text));
} }

View File

@@ -41,6 +41,8 @@ public:
Utils::Store toMap() const; Utils::Store toMap() const;
void fromMap(const Utils::Store &map); void fromMap(const Utils::Store &map);
TabSettings autoDetect(const QTextDocument *document) const;
int lineIndentPosition(const QString &text) const; int lineIndentPosition(const QString &text) const;
int columnAt(const QString &text, int position) const; int columnAt(const QString &text, int position) const;
int columnAtCursorPosition(const QTextCursor &cursor) const; int columnAtCursorPosition(const QTextCursor &cursor) const;
@@ -48,7 +50,6 @@ public:
int columnCountForText(const QString &text, int startColumn = 0) const; int columnCountForText(const QString &text, int startColumn = 0) const;
int indentedColumn(int column, bool doIndent = true) const; int indentedColumn(int column, bool doIndent = true) const;
QString indentationString(int startColumn, int targetColumn, int padding, const QTextBlock &currentBlock = QTextBlock()) const; QString indentationString(int startColumn, int targetColumn, int padding, const QTextBlock &currentBlock = QTextBlock()) const;
QString indentationString(const QString &text) const;
int indentationColumn(const QString &text) const; int indentationColumn(const QString &text) const;
static int maximumPadding(const QString &text); static int maximumPadding(const QString &text);
@@ -62,6 +63,7 @@ public:
friend bool operator!=(const TabSettings &t1, const TabSettings &t2) { return !t1.equals(t2); } friend bool operator!=(const TabSettings &t1, const TabSettings &t2) { return !t1.equals(t2); }
static int firstNonSpace(const QString &text); static int firstNonSpace(const QString &text);
static QString indentationString(const QString &text);
static inline bool onlySpace(const QString &text) { return firstNonSpace(text) == text.length(); } static inline bool onlySpace(const QString &text) { return firstNonSpace(text) == text.length(); }
static int spacesLeftFromPosition(const QString &text, int position); static int spacesLeftFromPosition(const QString &text, int position);
static bool cursorIsAtBeginningOfLine(const QTextCursor &cursor); static bool cursorIsAtBeginningOfLine(const QTextCursor &cursor);

View File

@@ -316,7 +316,6 @@ private:
menu->addAction(ActionManager::command(Constants::AUTO_INDENT_SELECTION)->action()); menu->addAction(ActionManager::command(Constants::AUTO_INDENT_SELECTION)->action());
auto documentSettings = menu->addMenu(Tr::tr("Document Settings")); auto documentSettings = menu->addMenu(Tr::tr("Document Settings"));
auto tabSettings = documentSettings->addMenu(Tr::tr("Tab Settings"));
auto modifyTabSettings = [this](std::function<void(TabSettings &tabSettings)> modifier) { auto modifyTabSettings = [this](std::function<void(TabSettings &tabSettings)> modifier) {
return [this, modifier]() { return [this, modifier]() {
auto ts = m_doc->tabSettings(); auto ts = m_doc->tabSettings();
@@ -324,6 +323,12 @@ private:
m_doc->setTabSettings(ts); m_doc->setTabSettings(ts);
}; };
}; };
documentSettings->addAction(
Tr::tr("Auto detect"),
modifyTabSettings([doc = m_doc->document()](TabSettings &tabSettings) {
tabSettings = tabSettings.autoDetect(doc);
}));
auto tabSettings = documentSettings->addMenu(Tr::tr("Tab Settings"));
tabSettings->addAction(Tr::tr("Spaces"), modifyTabSettings([](TabSettings &tabSettings) { tabSettings->addAction(Tr::tr("Spaces"), modifyTabSettings([](TabSettings &tabSettings) {
tabSettings.m_tabPolicy = TabSettings::SpacesOnlyTabPolicy; tabSettings.m_tabPolicy = TabSettings::SpacesOnlyTabPolicy;
})); }));