diff --git a/src/plugins/texteditor/tabsettings.cpp b/src/plugins/texteditor/tabsettings.cpp index d3aa8eb40f4..54aa18f3c29 100644 --- a/src/plugins/texteditor/tabsettings.cpp +++ b/src/plugins/texteditor/tabsettings.cpp @@ -4,6 +4,7 @@ #include "tabsettings.h" #include +#include #include #include @@ -50,6 +51,98 @@ void TabSettings::fromMap(const Store &map) 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 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) { QString text = cursor.block().text(); @@ -80,7 +173,7 @@ int TabSettings::firstNonSpace(const QString &text) return i; } -QString TabSettings::indentationString(const QString &text) const +QString TabSettings::indentationString(const QString &text) { return text.left(firstNonSpace(text)); } diff --git a/src/plugins/texteditor/tabsettings.h b/src/plugins/texteditor/tabsettings.h index 0a8e4305713..4d28a3c05cd 100644 --- a/src/plugins/texteditor/tabsettings.h +++ b/src/plugins/texteditor/tabsettings.h @@ -41,6 +41,8 @@ public: Utils::Store toMap() const; void fromMap(const Utils::Store &map); + TabSettings autoDetect(const QTextDocument *document) const; + int lineIndentPosition(const QString &text) const; int columnAt(const QString &text, int position) const; int columnAtCursorPosition(const QTextCursor &cursor) const; @@ -48,7 +50,6 @@ public: int columnCountForText(const QString &text, int startColumn = 0) const; int indentedColumn(int column, bool doIndent = true) const; QString indentationString(int startColumn, int targetColumn, int padding, const QTextBlock ¤tBlock = QTextBlock()) const; - QString indentationString(const QString &text) const; int indentationColumn(const QString &text) const; static int maximumPadding(const QString &text); @@ -62,6 +63,7 @@ public: friend bool operator!=(const TabSettings &t1, const TabSettings &t2) { return !t1.equals(t2); } 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 int spacesLeftFromPosition(const QString &text, int position); static bool cursorIsAtBeginningOfLine(const QTextCursor &cursor); diff --git a/src/plugins/texteditor/texteditor.cpp b/src/plugins/texteditor/texteditor.cpp index 056ce0aab95..51fb959b996 100644 --- a/src/plugins/texteditor/texteditor.cpp +++ b/src/plugins/texteditor/texteditor.cpp @@ -316,7 +316,6 @@ private: menu->addAction(ActionManager::command(Constants::AUTO_INDENT_SELECTION)->action()); auto documentSettings = menu->addMenu(Tr::tr("Document Settings")); - auto tabSettings = documentSettings->addMenu(Tr::tr("Tab Settings")); auto modifyTabSettings = [this](std::function modifier) { return [this, modifier]() { auto ts = m_doc->tabSettings(); @@ -324,6 +323,12 @@ private: 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.m_tabPolicy = TabSettings::SpacesOnlyTabPolicy; }));