// Copyright (C) 2019 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "clangformatbaseindenter.h" #include "clangformatutils.h" #include "llvmfilesystem.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace TextEditor; using namespace Utils; using namespace std::chrono; using namespace std::chrono_literals; namespace ClangFormat { Q_LOGGING_CATEGORY(clangIndenterLog, "qtc.dbg.clangformat", QtWarningMsg) enum class ReplacementsToKeep { OnlyIndent, IndentAndBefore, All }; static Internal::LlvmFileSystemAdapter llvmFileSystemAdapter = {}; static void adjustFormatStyleForLineBreak(clang::format::FormatStyle &style, ReplacementsToKeep replacementsToKeep) { style.MaxEmptyLinesToKeep = 100; #if LLVM_VERSION_MAJOR > 20 style.SortIncludes = {.Enabled = false}; #else style.SortIncludes = clang::format::FormatStyle::SI_Never; #endif #if LLVM_VERSION_MAJOR >= 16 style.SortUsingDeclarations = clang::format::FormatStyle::SUD_Never; #else style.SortUsingDeclarations = false; #endif // This is a separate pass, don't do it unless it's the full formatting. style.FixNamespaceComments = false; #if LLVM_VERSION_MAJOR >= 16 style.AlignTrailingComments = {clang::format::FormatStyle::TCAS_Never, 0}; #else style.AlignTrailingComments = false; #endif if (replacementsToKeep == ReplacementsToKeep::IndentAndBefore) return; style.ColumnLimit = 0; } static llvm::StringRef clearExtraNewline(llvm::StringRef text) { #if LLVM_VERSION_MAJOR >= 16 while (text.starts_with("\n\n")) #else while (text.startswith("\n\n")) #endif text = text.drop_front(); return text; } static clang::tooling::Replacements filteredReplacements(const QByteArray &buffer, const clang::tooling::Replacements &replacements, int utf8Offset, int utf8Length, ReplacementsToKeep replacementsToKeep) { clang::tooling::Replacements filtered; for (const clang::tooling::Replacement &replacement : replacements) { int replacementOffset = static_cast(replacement.getOffset()); // Skip everything after. if (replacementOffset >= utf8Offset + utf8Length) return filtered; const bool isNotIndentOrInRange = replacementOffset < utf8Offset - 1 || buffer.at(replacementOffset) != '\n'; if (isNotIndentOrInRange && replacementsToKeep == ReplacementsToKeep::OnlyIndent) continue; llvm::StringRef text = replacementsToKeep == ReplacementsToKeep::OnlyIndent ? clearExtraNewline(replacement.getReplacementText()) : replacement.getReplacementText(); if (replacementsToKeep == ReplacementsToKeep::OnlyIndent && int(text.count('\n')) != buffer.mid(replacementOffset, replacement.getLength()).count('\n')) { continue; } llvm::Error error = filtered.add( clang::tooling::Replacement(replacement.getFilePath(), static_cast(replacementOffset), replacement.getLength(), text)); // Throws if error is not checked. if (error) { error = llvm::handleErrors(std::move(error), [](const llvm::ErrorInfoBase &) -> llvm::Error { return llvm::Error::success(); }); QTC_CHECK(!error && "Error must be a \"success\" at this point"); break; } } return filtered; } void trimRHSWhitespace(const QTextBlock &block) { const QString initialText = block.text(); if (!initialText.rbegin()->isSpace()) return; auto lastNonSpace = std::find_if_not(initialText.rbegin(), initialText.rend(), [](const QChar &letter) { return letter.isSpace(); }); const int extraSpaceCount = static_cast(std::distance(initialText.rbegin(), lastNonSpace)); QTextCursor cursor(block); cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, initialText.size() - extraSpaceCount); cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, extraSpaceCount); cursor.removeSelectedText(); } static QTextBlock reverseFindLastEmptyBlock(QTextBlock start) { if (start.position() > 0) { start = start.previous(); while (start.position() > 0 && start.text().trimmed().isEmpty()) start = start.previous(); if (!start.text().trimmed().isEmpty()) start = start.next(); } return start; } static QTextBlock reverseFindLastBlockWithSymbol(QTextBlock start, QChar ch) { if (start.position() > 0) { start = start.previous(); while (start.position() > 0 && !start.text().contains(ch)) start = start.previous(); } return start; } enum class CharacterContext { AfterComma, LastAfterComma, NewStatementOrContinuation, IfOrElseWithoutScope, BracketAfterFunctionCall, Unknown }; static QChar findFirstNonWhitespaceCharacter(const QTextBlock ¤tBlock) { const QTextDocument *doc = currentBlock.document(); int currentPos = currentBlock.position(); while (currentPos < doc->characterCount() && doc->characterAt(currentPos).isSpace()) ++currentPos; return currentPos < doc->characterCount() ? doc->characterAt(currentPos) : QChar::Null; } static int findMatchingOpeningParen(const QTextBlock &blockEndingWithClosingParen) { const QTextDocument *doc = blockEndingWithClosingParen.document(); int currentPos = blockEndingWithClosingParen.position() + blockEndingWithClosingParen.text().lastIndexOf(')'); int parenBalance = 1; while (currentPos > 0 && parenBalance > 0) { --currentPos; if (doc->characterAt(currentPos) == ')') ++parenBalance; if (doc->characterAt(currentPos) == '(') --parenBalance; } if (parenBalance == 0) return currentPos; return -1; } static bool comesDirectlyAfterIf(const QTextDocument *doc, int pos) { --pos; while (pos > 0 && doc->characterAt(pos).isSpace()) --pos; return pos > 0 && doc->characterAt(pos) == 'f' && doc->characterAt(pos - 1) == 'i'; } static bool startsWithKeyWord(const QString &keyWord, const QString &text) { if (text.size() <= keyWord.size()) return false; const QChar chAfter = text.at(keyWord.size()); return text.startsWith(keyWord) && !chAfter.isDigit() && !chAfter.isLetter() && chAfter != '_'; } static bool startsWithKeyWords(const QString &text) { return startsWithKeyWord("if", text) || startsWithKeyWord("while", text) || startsWithKeyWord("for", text); } static CharacterContext characterContext(const QTextBlock ¤tBlock) { QTextBlock previousNonEmptyBlock = reverseFindLastEmptyBlock(currentBlock); if (previousNonEmptyBlock.position() > 0) previousNonEmptyBlock = previousNonEmptyBlock.previous(); const QString prevLineText = previousNonEmptyBlock.text().trimmed(); if (prevLineText.isEmpty()) return CharacterContext::NewStatementOrContinuation; const QString currentBlockText = currentBlock.text().trimmed(); if ((currentBlockText.isEmpty() || currentBlockText.endsWith(")")) && prevLineText.endsWith("{") && !startsWithKeyWords(currentBlockText)) return CharacterContext::BracketAfterFunctionCall; const QChar firstNonWhitespaceChar = findFirstNonWhitespaceCharacter(currentBlock); if (prevLineText.endsWith(',')) { if (firstNonWhitespaceChar == '}') { if (reverseFindLastBlockWithSymbol(currentBlock, '{').text().trimmed().last(1) == '{') return CharacterContext::NewStatementOrContinuation; return CharacterContext::LastAfterComma; } if (firstNonWhitespaceChar == ')') { if (reverseFindLastBlockWithSymbol(currentBlock, '(').text().trimmed().last(1) == '(') return CharacterContext::NewStatementOrContinuation; return CharacterContext::LastAfterComma; } return CharacterContext::AfterComma; } if (prevLineText.endsWith("else")) return CharacterContext::IfOrElseWithoutScope; if (prevLineText.endsWith(')')) { const int pos = findMatchingOpeningParen(previousNonEmptyBlock); if (pos >= 0 && comesDirectlyAfterIf(previousNonEmptyBlock.document(), pos)) return CharacterContext::IfOrElseWithoutScope; } return CharacterContext::NewStatementOrContinuation; } static bool nextBlockExistsAndEmpty(const QTextBlock ¤tBlock) { QTextBlock nextBlock = currentBlock.next(); if (!nextBlock.isValid() || nextBlock.position() == currentBlock.position()) return false; return nextBlock.text().trimmed().isEmpty(); } static QByteArray dummyTextForContext(CharacterContext context, bool closingBraceBlock) { if (closingBraceBlock && context == CharacterContext::NewStatementOrContinuation) return QByteArray(); switch (context) { case CharacterContext::AfterComma: return "a,"; case CharacterContext::LastAfterComma: return "a"; case CharacterContext::IfOrElseWithoutScope: return ";"; case CharacterContext::BracketAfterFunctionCall: return ";"; case CharacterContext::NewStatementOrContinuation: return "/*//*/"; case CharacterContext::Unknown: default: QTC_ASSERT(false, return "";); } } // Add extra text in case of the empty line or the line starting with ')'. // Track such extra pieces of text in isInsideDummyTextInLine(). static int forceIndentWithExtraText(QByteArray &buffer, CharacterContext &charContext, const QTextBlock &block, bool secondTry) { if (!block.isValid()) return 0; auto tmpcharContext = characterContext(block); if (charContext == CharacterContext::LastAfterComma && tmpcharContext == CharacterContext::LastAfterComma) { charContext = CharacterContext::AfterComma; } else { charContext = tmpcharContext; } const QString blockText = block.text(); int firstNonWhitespace = Utils::indexOf(blockText, [](const QChar &ch) { return !ch.isSpace(); }); int utf8Offset = Text::utf8NthLineOffset(block.document(), buffer, block.blockNumber() + 1); int utf8EndOfLineOffset = utf8Offset + blockText.length(); if (firstNonWhitespace >= 0) utf8Offset += firstNonWhitespace; else utf8Offset += blockText.length(); const bool closingParenBlock = firstNonWhitespace >= 0 && blockText.at(firstNonWhitespace) == ')'; const bool closingBraceBlock = firstNonWhitespace >= 0 && blockText.at(firstNonWhitespace) == '}'; int extraLength = 0; QByteArray dummyText; if (firstNonWhitespace < 0 && charContext != CharacterContext::Unknown && nextBlockExistsAndEmpty(block)) { // If the next line is also empty it's safer to use a comment line. dummyText = "//"; } else if ( firstNonWhitespace < 0 || closingParenBlock || closingBraceBlock || charContext == CharacterContext::BracketAfterFunctionCall) { dummyText = dummyTextForContext(charContext, closingBraceBlock); } // A comment at the end of the line appears to prevent clang-format from removing line breaks. if (dummyText == "/*//*/" || dummyText.isEmpty()) { if (block.previous().isValid()) { const int prevEndOffset = Text::utf8NthLineOffset(block.document(), buffer, block.blockNumber()) + block.previous().text().toUtf8().length(); buffer.insert(prevEndOffset, " //"); extraLength += 3; } } if (charContext == CharacterContext::BracketAfterFunctionCall) { buffer.insert(utf8EndOfLineOffset + extraLength, dummyText); extraLength += dummyText.length(); return extraLength; } buffer.insert(utf8Offset + extraLength, dummyText); extraLength += dummyText.length(); if (secondTry) { int nextLinePos = buffer.indexOf('\n', utf8Offset); if (nextLinePos < 0) nextLinePos = buffer.size() - 1; if (nextLinePos > 0) { // If first try was not successful try to put ')' in the end of the line to close possibly // unclosed parenthesis. // TODO: Does it help to add different endings depending on the context? buffer.insert(nextLinePos, ')'); extraLength += 1; } } return extraLength; } static bool isInsideDummyTextInLine(const QString &originalLine, const QString &modifiedLine, int column) { // Detect the cases when we have inserted extra text into the line to get the indentation. return originalLine.length() < modifiedLine.length() && column != modifiedLine.length() + 1 && (column > originalLine.length() || originalLine.trimmed().isEmpty() || !modifiedLine.startsWith(originalLine)); } static Text::Position utf16LineColumn(const QByteArray &utf8Buffer, int utf8Offset) { Text::Position position; position.line = static_cast(std::count(utf8Buffer.begin(), utf8Buffer.begin() + utf8Offset, '\n')) + 1; const int startOfLineOffset = utf8Offset ? (utf8Buffer.lastIndexOf('\n', utf8Offset - 1) + 1) : 0; position.column = QString::fromUtf8(utf8Buffer.mid(startOfLineOffset, utf8Offset - startOfLineOffset)).length(); return position; } static ChangeSet convertReplacements(const QTextDocument *doc, const QByteArray &utf8Buffer, const clang::tooling::Replacements &replacements) { ChangeSet convertedReplacements; for (const clang::tooling::Replacement &replacement : replacements) { Text::Position lineColUtf16 = utf16LineColumn( utf8Buffer, static_cast(replacement.getOffset())); if (!lineColUtf16.isValid()) continue; const QString lineText = doc->findBlockByNumber(lineColUtf16.line - 1).text(); const QString bufferLineText = Text::utf16LineTextInUtf8Buffer(utf8Buffer, static_cast(replacement.getOffset())); if (isInsideDummyTextInLine(lineText, bufferLineText, lineColUtf16.column + 1)) continue; lineColUtf16.column = std::min(lineColUtf16.column, int(lineText.length())); int utf16Offset = Text::positionInText(doc, lineColUtf16.line, lineColUtf16.column); int utf16Length = QString::fromUtf8( utf8Buffer.mid(static_cast(replacement.getOffset()), static_cast(replacement.getLength()))) .size(); QString replacementText = QString::fromStdString(replacement.getReplacementText().str()); replacementText.replace("\r", ""); auto sameCharAt = [&](int replacementOffset) { if (utf16Length == 0 || replacementText.size() <= replacementOffset || replacementOffset < 0) { return false; } const QChar docChar = doc->characterAt(utf16Offset + replacementOffset); const QChar replacementChar = replacementText.at(replacementOffset); return docChar == replacementChar || (docChar == QChar::ParagraphSeparator && replacementChar == '\n'); }; // remove identical prefix from replacement text while (sameCharAt(0)) { ++utf16Offset; --utf16Length; if (!replacementText.isEmpty()) replacementText.remove(0, 1); } // remove identical suffix from replacement text while (sameCharAt(utf16Length - 1)) { --utf16Length; replacementText.chop(1); } if (!replacementText.isEmpty() || utf16Length > 0) convertedReplacements.replace(utf16Offset, utf16Offset + utf16Length, replacementText); } return convertedReplacements; } static QString selectedLines(QTextDocument *doc, const QTextBlock &startBlock, const QTextBlock &endBlock) { return Text::textAt( doc, startBlock.position(), std::max(0, endBlock.position() + endBlock.length() - startBlock.position() - 1)); } static int indentationForBlock(const ChangeSet &toReplace, const QByteArray &buffer, const QTextBlock ¤tBlock) { const int utf8Offset = Text::utf8NthLineOffset(currentBlock.document(), buffer, currentBlock.blockNumber() + 1); auto ops = toReplace.operationList(); auto replacementIt = std::find_if(ops.begin(), ops.end(), [utf8Offset](const ChangeSet::EditOp &op) { QTC_ASSERT(op.type() == ChangeSet::EditOp::Replace, return false); return op.pos1 == utf8Offset - 1; }); if (replacementIt == ops.end()) return -1; int afterLineBreak = replacementIt->text().lastIndexOf('\n'); afterLineBreak = (afterLineBreak < 0) ? 0 : afterLineBreak + 1; return static_cast(replacementIt->text().size() - afterLineBreak); } static bool doNotIndentInContext(QTextDocument *doc, int pos) { const QChar character = doc->characterAt(pos); const QTextBlock currentBlock = doc->findBlock(pos); const QString text = currentBlock.text().left(pos - currentBlock.position()); // NOTE: check if "<<" and ">>" always work correctly. switch (character.toLatin1()) { default: break; case ':': // Do not indent when it's the first ':' and it's not the 'case' line. if (text.contains(QLatin1String("case")) || text.contains(QLatin1String("default")) || text.contains(QLatin1String("public")) || text.contains(QLatin1String("private")) || text.contains(QLatin1String("protected")) || text.contains(QLatin1String("signals")) || text.contains(QLatin1String("Q_SIGNALS"))) { return false; } if (pos > 0 && doc->characterAt(pos - 1) != ':') return true; break; } return false; } static int formattingRangeStart(const QTextBlock ¤tBlock, const QByteArray &buffer, int documentRevision) { QTextBlock prevBlock = currentBlock.previous(); while ((prevBlock.position() > 0 || prevBlock.length() > 0) && prevBlock.revision() != documentRevision) { // Find the first block with not matching revision. prevBlock = prevBlock.previous(); } if (prevBlock.revision() == documentRevision) prevBlock = prevBlock.next(); return Text::utf8NthLineOffset(prevBlock.document(), buffer, prevBlock.blockNumber() + 1); } class ClangFormatBaseIndenterPrivate final { public: ClangFormatBaseIndenterPrivate(ClangFormatBaseIndenter *parent, QTextDocument *doc, FilePath *fileName) : q(parent), m_doc(doc), m_fileName(fileName) {} void indent(const QTextCursor &cursor, const QChar &typedChar, int cursorPositionInEditor); void indentBlocks(const QTextBlock &startBlock, const QTextBlock &endBlock, const QChar &typedChar, int cursorPositionInEditor); ChangeSet indentsFor(QTextBlock startBlock, const QTextBlock &endBlock, const QChar &typedChar, int cursorPositionInEditor, bool trimTrailingWhitespace = true); ChangeSet replacements(QByteArray buffer, const QTextBlock &startBlock, const QTextBlock &endBlock, int cursorPositionInEditor, ReplacementsToKeep replacementsToKeep, const QChar &typedChar = QChar::Null, bool secondTry = false) const; const clang::format::FormatStyle &styleForFile() const; ClangFormatBaseIndenter *q; QTextDocument *m_doc; FilePath *m_fileName; struct CachedStyle { clang::format::FormatStyle style = clang::format::getNoStyle(); QDateTime expirationTime; void setCache(clang::format::FormatStyle newStyle, milliseconds timeout) { style = newStyle; expirationTime = QDateTime::currentDateTime().addMSecs(timeout.count()); } }; mutable CachedStyle m_cachedStyle; clang::format::FormatStyle customSettingsStyle(const FilePath &fileName) const; ICodeStylePreferences *m_overriddenPreferences = nullptr; clang::format::FormatStyle m_overriddenStyle = clang::format::getNoStyle(); }; ClangFormatBaseIndenter::ClangFormatBaseIndenter(QTextDocument *doc) : Indenter(doc), d(new ClangFormatBaseIndenterPrivate(this, doc, &m_fileName)) {} ClangFormatBaseIndenter::~ClangFormatBaseIndenter() { delete d; } static void printBuffer(QString str) { for (const auto &line : str.split("\n")) { qCDebug(clangIndenterLog) << line; } } static void printDebugInfo( const QByteArray &buffer, clang::tooling::Replacements replacements, const QString &additionalInfo) { if (!clangIndenterLog().isInfoEnabled()) return; QString str = QString::fromStdString(buffer.data()); if (replacements.empty()) { std::string code = buffer.data(); llvm::Expected code_new = clang::tooling::applyAllReplacements(code, replacements); if (!code_new) return; str = QString::fromStdString(code_new.get()); } qCDebug(clangIndenterLog) << additionalInfo << str; printBuffer(str); } ChangeSet ClangFormatBaseIndenterPrivate::replacements(QByteArray buffer, const QTextBlock &startBlock, const QTextBlock &endBlock, int cursorPositionInEditor, ReplacementsToKeep replacementsToKeep, const QChar &typedChar, bool secondTry) const { QTC_ASSERT(replacementsToKeep != ReplacementsToKeep::All, return ChangeSet()); QTC_ASSERT(!m_fileName->isEmpty(), return {}); QByteArray originalBuffer = buffer; int utf8Offset = Text::utf8NthLineOffset(m_doc, buffer, startBlock.blockNumber() + 1); QTC_ASSERT(utf8Offset >= 0, return ChangeSet();); int utf8Length = selectedLines(m_doc, startBlock, endBlock).toUtf8().size(); int rangeStart = 0; if (replacementsToKeep == ReplacementsToKeep::IndentAndBefore) rangeStart = formattingRangeStart(startBlock, buffer, q->lastSaveRevision()); clang::format::FormatStyle style = styleForFile(); adjustFormatStyleForLineBreak(style, replacementsToKeep); if (replacementsToKeep == ReplacementsToKeep::OnlyIndent) { CharacterContext currentCharContext = CharacterContext::Unknown; // Iterate backwards to reuse the same dummy text for all empty lines. for (int index = endBlock.blockNumber(); index >= startBlock.blockNumber(); --index) { utf8Length += forceIndentWithExtraText(buffer, currentCharContext, m_doc->findBlockByNumber(index), secondTry); } } printDebugInfo(buffer, {}, "before"); if (replacementsToKeep != ReplacementsToKeep::IndentAndBefore || utf8Offset < rangeStart) rangeStart = utf8Offset; unsigned int rangeLength = static_cast(utf8Offset + utf8Length - rangeStart); std::vector ranges{{static_cast(rangeStart), rangeLength}}; clang::format::FormattingAttemptStatus status; clang::tooling::Replacements clangReplacements = clang::format::reformat( style, buffer.data(), ranges, m_fileName->toFSPathString().toStdString(), &status); printDebugInfo(buffer, clangReplacements, "after"); clang::tooling::Replacements filtered; if (status.FormatComplete) { filtered = filteredReplacements(buffer, clangReplacements, utf8Offset, utf8Length, replacementsToKeep); } printDebugInfo(buffer, filtered, "filtered"); const bool canTryAgain = replacementsToKeep == ReplacementsToKeep::OnlyIndent && typedChar == QChar::Null && !secondTry; if (canTryAgain && filtered.empty()) { return replacements(originalBuffer, startBlock, endBlock, cursorPositionInEditor, replacementsToKeep, typedChar, true); } return convertReplacements(m_doc, buffer, filtered); } EditOperations ClangFormatBaseIndenter::format(const RangesInLines &rangesInLines, FormattingMode mode) { Q_UNUSED(mode) QTC_ASSERT(!m_fileName.isEmpty(), return {}); if (rangesInLines.empty()) return {}; const QByteArray buffer = m_doc->toPlainText().toUtf8(); std::vector ranges; ranges.reserve(rangesInLines.size()); for (auto &range : rangesInLines) { const int utf8StartOffset = Text::utf8NthLineOffset(m_doc, buffer, range.startLine); int utf8RangeLength = m_doc->findBlockByNumber(range.endLine - 1).text().toUtf8().size(); if (range.endLine > range.startLine) { utf8RangeLength += Text::utf8NthLineOffset(m_doc, buffer, range.endLine) - utf8StartOffset; } ranges.emplace_back(static_cast(utf8StartOffset), static_cast(utf8RangeLength)); } clang::format::FormatStyle style = styleForFile(); const std::string assumedFileName = m_fileName.toFSPathString().toStdString(); clang::tooling::Replacements clangReplacements = clang::format::sortIncludes(style, buffer.data(), ranges, assumedFileName); auto changedCode = clang::tooling::applyAllReplacements(buffer.data(), clangReplacements); QTC_ASSERT(changedCode, { qCDebug(clangIndenterLog) << QString::fromStdString(llvm::toString(changedCode.takeError())); return {}; }); ranges = clang::tooling::calculateRangesAfterReplacements(clangReplacements, ranges); clang::format::FormattingAttemptStatus status; const clang::tooling::Replacements formatReplacements = clang::format::reformat(style, *changedCode, ranges, assumedFileName, &status); clangReplacements = clangReplacements.merge(formatReplacements); ChangeSet changeSet = convertReplacements(m_doc, buffer, clangReplacements); const EditOperations editOperations = changeSet.operationList(); changeSet.apply(m_doc); return editOperations; } ChangeSet ClangFormatBaseIndenterPrivate::indentsFor(QTextBlock startBlock, const QTextBlock &endBlock, const QChar &typedChar, int cursorPositionInEditor, bool trimTrailingWhitespace) { if (typedChar != QChar::Null && cursorPositionInEditor > 0 && m_doc->characterAt(cursorPositionInEditor - 1) == typedChar && doNotIndentInContext(m_doc, cursorPositionInEditor - 1)) { return ChangeSet(); } startBlock = reverseFindLastEmptyBlock(startBlock); const int startBlockPosition = startBlock.position(); if (trimTrailingWhitespace && startBlockPosition > 0) { trimRHSWhitespace(startBlock.previous()); if (cursorPositionInEditor >= 0) cursorPositionInEditor += startBlock.position() - startBlockPosition; } const QByteArray buffer = m_doc->toPlainText().toUtf8(); ReplacementsToKeep replacementsToKeep = ReplacementsToKeep::OnlyIndent; if (q->formatWhileTyping() && (cursorPositionInEditor == -1 || cursorPositionInEditor >= startBlockPosition) && (typedChar == ';' || typedChar == '}')) { // Format before current position only in case the cursor is inside the indented block. // So if cursor position is less then the block position then the current line is before // the indented block - don't trigger extra formatting in this case. // cursorPositionInEditor == -1 means the condition matches automatically. // Format only before complete statement not to break code. replacementsToKeep = ReplacementsToKeep::IndentAndBefore; } return replacements(buffer, startBlock, endBlock, cursorPositionInEditor, replacementsToKeep, typedChar); } void ClangFormatBaseIndenterPrivate::indentBlocks(const QTextBlock &startBlock, const QTextBlock &endBlock, const QChar &typedChar, int cursorPositionInEditor) { ChangeSet changeset = indentsFor(startBlock, endBlock, typedChar, cursorPositionInEditor); changeset.apply(m_doc); } void ClangFormatBaseIndenterPrivate::indent(const QTextCursor &cursor, const QChar &typedChar, int cursorPositionInEditor) { const QString blockText = cursor.block().text().trimmed(); if (cursor.hasSelection()) { indentBlocks(m_doc->findBlock(cursor.selectionStart()), m_doc->findBlock(cursor.selectionEnd()), typedChar, cursorPositionInEditor); } else if ( typedChar == QChar::Null || blockText.startsWith(typedChar) || blockText.endsWith(typedChar) || blockText.isEmpty()) { indentBlocks(cursor.block(), cursor.block(), typedChar, cursorPositionInEditor); } } void ClangFormatBaseIndenter::indent(const QTextCursor &cursor, const QChar &typedChar, const TabSettings & /*tabSettings*/, int cursorPositionInEditor) { d->indent(cursor, typedChar, cursorPositionInEditor); } void ClangFormatBaseIndenter::reindent(const QTextCursor &cursor, const TabSettings & /*tabSettings*/, int cursorPositionInEditor) { d->indent(cursor, QChar::Null, cursorPositionInEditor); } void ClangFormatBaseIndenter::indentBlock(const QTextBlock &block, const QChar &typedChar, const TabSettings & /*tabSettings*/, int cursorPositionInEditor) { d->indentBlocks(block, block, typedChar, cursorPositionInEditor); } int ClangFormatBaseIndenter::indentFor(const QTextBlock &block, const TabSettings & /*tabSettings*/, int cursorPositionInEditor) { ChangeSet toReplace = d->indentsFor(block, block, QChar::Null, cursorPositionInEditor, false); if (toReplace.isEmpty()) return -1; const QByteArray buffer = m_doc->toPlainText().toUtf8(); return indentationForBlock(toReplace, buffer, block); } IndentationForBlock ClangFormatBaseIndenter::indentationForBlocks( const QList &blocks, const TabSettings & /*tabSettings*/, int cursorPositionInEditor) { IndentationForBlock ret; if (blocks.isEmpty()) return ret; ChangeSet toReplace = d->indentsFor(blocks.front(), blocks.back(), QChar::Null, cursorPositionInEditor); const QByteArray buffer = m_doc->toPlainText().toUtf8(); for (const QTextBlock &block : blocks) ret.insert(block.blockNumber(), indentationForBlock(toReplace, buffer, block)); return ret; } bool ClangFormatBaseIndenter::isElectricCharacter(const QChar &ch) const { switch (ch.toLatin1()) { case '{': case '}': case ':': case '#': case '<': case '>': case ';': case '(': case ')': return true; } return false; } std::optional ClangFormat::ClangFormatBaseIndenter::margin() const { return styleForFile().ColumnLimit; } void ClangFormatBaseIndenter::autoIndent(const QTextCursor &cursor, const TabSettings & /*tabSettings*/, int cursorPositionInEditor) { if (formatCodeInsteadOfIndent()) { QTextBlock start; QTextBlock end; if (cursor.hasSelection()) { start = m_doc->findBlock(cursor.selectionStart()); end = m_doc->findBlock(cursor.selectionEnd()); } else { start = end = cursor.block(); } format({{start.blockNumber() + 1, end.blockNumber() + 1}}); } else { d->indent(cursor, QChar::Null, cursorPositionInEditor); } } clang::format::FormatStyle ClangFormatBaseIndenterPrivate::customSettingsStyle( const FilePath &fileName) const { const ProjectExplorer::Project *projectForFile = ProjectExplorer::ProjectManager::projectForFile(fileName); const ICodeStylePreferences *preferences = !getProjectUseGlobalSettings(projectForFile) && projectForFile ? projectForFile->editorConfiguration()->codeStyle("Cpp")->currentPreferences() : TextEditorSettings::codeStyle("Cpp")->currentPreferences(); if (m_overriddenPreferences) preferences = m_overriddenPreferences->currentPreferences(); FilePath filePath = filePathToCurrentSettings(preferences); if (!filePath.exists()) return currentQtStyle(preferences); clang::format::FormatStyle currentSettingsStyle; const Utils::Result<> result = parseConfigurationFile(filePath, currentSettingsStyle); if (!result) { qCWarning(clangIndenterLog) << QString{"Failed to parse config %1. Falling back to the Qt style."}.arg( filePath.toUserOutput()) << result.error(); return currentQtStyle(preferences); }; return currentSettingsStyle; } static milliseconds getCacheTimeout() { bool ok = false; const int envCacheTimeout = qEnvironmentVariableIntValue("CLANG_FORMAT_CACHE_TIMEOUT", &ok); return ok ? milliseconds(envCacheTimeout) : 1s; } const clang::format::FormatStyle &ClangFormatBaseIndenter::styleForFile() const { return d->styleForFile(); } const llvm::Expected getStyleFromProjectFolder( const Utils::FilePath *fileName) { #if LLVM_VERSION_MAJOR >= 19 static QString s_cachedError; llvm::SourceMgr::DiagHandlerTy diagHandler = [](const llvm::SMDiagnostic &diag, void *) { QString errorMessage = QString::fromStdString(diag.getMessage().str()) + " " + QString::number(diag.getLineNo()) + ":" + QString::number(diag.getColumnNo()); if (s_cachedError == errorMessage) return; s_cachedError = errorMessage; Core::MessageManager::writeSilently("ClangFormat file error: " + errorMessage); }; return clang::format::getStyle( "file", fileName->toFSPathString().toStdString(), "none", "", &llvmFileSystemAdapter, true, diagHandler); #else return clang::format::getStyle( "file", fileName->toFSPathString().toStdString(), "none", "", &llvmFileSystemAdapter, true); #endif } const clang::format::FormatStyle &ClangFormatBaseIndenterPrivate::styleForFile() const { static const milliseconds cacheTimeout = getCacheTimeout(); if (!(m_overriddenStyle == clang::format::getNoStyle())) return m_overriddenStyle; QDateTime time = QDateTime::currentDateTime(); if (m_cachedStyle.expirationTime > time && !(m_cachedStyle.style == clang::format::getNoStyle())) return m_cachedStyle.style; if (getCurrentCustomSettings(*m_fileName)) { clang::format::FormatStyle style = customSettingsStyle(*m_fileName); addQtcStatementMacros(style); m_cachedStyle.setCache(style, cacheTimeout); return m_cachedStyle.style; } llvm::Expected styleFromProjectFolder = getStyleFromProjectFolder( m_fileName); if (styleFromProjectFolder && !(*styleFromProjectFolder == clang::format::getNoStyle())) { addQtcStatementMacros(*styleFromProjectFolder); m_cachedStyle.setCache(*styleFromProjectFolder, cacheTimeout); return m_cachedStyle.style; } handleAllErrors(styleFromProjectFolder.takeError(), [](const llvm::ErrorInfoBase &) { // do nothing }); m_cachedStyle.setCache(qtcStyle(), 0ms); return m_cachedStyle.style; } void ClangFormatBaseIndenter::setOverriddenPreferences(ICodeStylePreferences *preferences) { d->m_overriddenPreferences = preferences; } void ClangFormatBaseIndenter::setOverriddenStyle(const clang::format::FormatStyle &style) { d->m_overriddenStyle = style; } bool ClangFormatBaseIndenter::respectsTabSettings() const { return false; } } // namespace ClangFormat