forked from qt-creator/qt-creator
ClangFormat: Fix indentation when empty lines are involved
Unify continuation and new statement to get less false indentations. Handle one-statement if/else as a special case. Properly handle empty lines after - includes - preprocessor directives - beginning of the file - if/else Fixes: QTCREATORBUG-22238 Change-Id: Ic334eeca7de47d9fcb74963d2e31711838d04bde Reviewed-by: Marco Bubke <marco.bubke@qt.io>
This commit is contained in:
@@ -47,6 +47,7 @@ void adjustFormatStyleForLineBreak(clang::format::FormatStyle &style,
|
|||||||
|
|
||||||
// This is a separate pass, don't do it unless it's the full formatting.
|
// This is a separate pass, don't do it unless it's the full formatting.
|
||||||
style.FixNamespaceComments = false;
|
style.FixNamespaceComments = false;
|
||||||
|
style.AlignTrailingComments = false;
|
||||||
|
|
||||||
if (replacementsToKeep == ReplacementsToKeep::IndentAndBefore)
|
if (replacementsToKeep == ReplacementsToKeep::IndentAndBefore)
|
||||||
return;
|
return;
|
||||||
@@ -118,13 +119,11 @@ void trimRHSWhitespace(const QTextBlock &block)
|
|||||||
const int extraSpaceCount = static_cast<int>(std::distance(initialText.rbegin(), lastNonSpace));
|
const int extraSpaceCount = static_cast<int>(std::distance(initialText.rbegin(), lastNonSpace));
|
||||||
|
|
||||||
QTextCursor cursor(block);
|
QTextCursor cursor(block);
|
||||||
cursor.beginEditBlock();
|
|
||||||
cursor.movePosition(QTextCursor::Right,
|
cursor.movePosition(QTextCursor::Right,
|
||||||
QTextCursor::MoveAnchor,
|
QTextCursor::MoveAnchor,
|
||||||
initialText.size() - extraSpaceCount);
|
initialText.size() - extraSpaceCount);
|
||||||
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, extraSpaceCount);
|
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, extraSpaceCount);
|
||||||
cursor.removeSelectedText();
|
cursor.removeSelectedText();
|
||||||
cursor.endEditBlock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QTextBlock reverseFindLastEmptyBlock(QTextBlock start)
|
QTextBlock reverseFindLastEmptyBlock(QTextBlock start)
|
||||||
@@ -139,7 +138,13 @@ QTextBlock reverseFindLastEmptyBlock(QTextBlock start)
|
|||||||
return start;
|
return start;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class CharacterContext { AfterComma, LastAfterComma, NewStatement, Continuation, Unknown };
|
enum class CharacterContext {
|
||||||
|
AfterComma,
|
||||||
|
LastAfterComma,
|
||||||
|
NewStatementOrContinuation,
|
||||||
|
IfOrElseWithoutScope,
|
||||||
|
Unknown
|
||||||
|
};
|
||||||
|
|
||||||
QChar findFirstNonWhitespaceCharacter(const QTextBlock ¤tBlock)
|
QChar findFirstNonWhitespaceCharacter(const QTextBlock ¤tBlock)
|
||||||
{
|
{
|
||||||
@@ -150,10 +155,42 @@ QChar findFirstNonWhitespaceCharacter(const QTextBlock ¤tBlock)
|
|||||||
return currentPos < doc->characterCount() ? doc->characterAt(currentPos) : QChar::Null;
|
return currentPos < doc->characterCount() ? doc->characterAt(currentPos) : QChar::Null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
CharacterContext characterContext(const QTextBlock ¤tBlock,
|
CharacterContext characterContext(const QTextBlock ¤tBlock,
|
||||||
const QTextBlock &previousNonEmptyBlock)
|
const QTextBlock &previousNonEmptyBlock)
|
||||||
{
|
{
|
||||||
const QString prevLineText = previousNonEmptyBlock.text().trimmed();
|
const QString prevLineText = previousNonEmptyBlock.text().trimmed();
|
||||||
|
if (prevLineText.isEmpty())
|
||||||
|
return CharacterContext::NewStatementOrContinuation;
|
||||||
|
|
||||||
const QChar firstNonWhitespaceChar = findFirstNonWhitespaceCharacter(currentBlock);
|
const QChar firstNonWhitespaceChar = findFirstNonWhitespaceCharacter(currentBlock);
|
||||||
if (prevLineText.endsWith(',')) {
|
if (prevLineText.endsWith(',')) {
|
||||||
// We don't need to add comma in case it's the last argument.
|
// We don't need to add comma in case it's the last argument.
|
||||||
@@ -162,12 +199,15 @@ CharacterContext characterContext(const QTextBlock ¤tBlock,
|
|||||||
return CharacterContext::AfterComma;
|
return CharacterContext::AfterComma;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevLineText.endsWith(';') || prevLineText.endsWith('{') || prevLineText.endsWith('}')
|
if (prevLineText.endsWith("else"))
|
||||||
|| firstNonWhitespaceChar == QChar::Null) {
|
return CharacterContext::IfOrElseWithoutScope;
|
||||||
return CharacterContext::NewStatement;
|
if (prevLineText.endsWith(')')) {
|
||||||
|
const int pos = findMatchingOpeningParen(previousNonEmptyBlock);
|
||||||
|
if (pos >= 0 && comesDirectlyAfterIf(previousNonEmptyBlock.document(), pos))
|
||||||
|
return CharacterContext::IfOrElseWithoutScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CharacterContext::Continuation;
|
return CharacterContext::NewStatementOrContinuation;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool nextBlockExistsAndEmpty(const QTextBlock ¤tBlock)
|
bool nextBlockExistsAndEmpty(const QTextBlock ¤tBlock)
|
||||||
@@ -181,20 +221,18 @@ bool nextBlockExistsAndEmpty(const QTextBlock ¤tBlock)
|
|||||||
|
|
||||||
QByteArray dummyTextForContext(CharacterContext context, bool closingBraceBlock)
|
QByteArray dummyTextForContext(CharacterContext context, bool closingBraceBlock)
|
||||||
{
|
{
|
||||||
if (closingBraceBlock
|
if (closingBraceBlock && context == CharacterContext::NewStatementOrContinuation)
|
||||||
&& (context == CharacterContext::NewStatement
|
|
||||||
|| context == CharacterContext::Continuation)) {
|
|
||||||
return QByteArray();
|
return QByteArray();
|
||||||
}
|
|
||||||
|
|
||||||
switch (context) {
|
switch (context) {
|
||||||
case CharacterContext::AfterComma:
|
case CharacterContext::AfterComma:
|
||||||
return "a,";
|
return "a,";
|
||||||
case CharacterContext::NewStatement:
|
|
||||||
return "a;";
|
|
||||||
case CharacterContext::Continuation:
|
|
||||||
case CharacterContext::LastAfterComma:
|
case CharacterContext::LastAfterComma:
|
||||||
return "& a &";
|
return "a";
|
||||||
|
case CharacterContext::IfOrElseWithoutScope:
|
||||||
|
return ";";
|
||||||
|
case CharacterContext::NewStatementOrContinuation:
|
||||||
|
return "/**/";
|
||||||
case CharacterContext::Unknown:
|
case CharacterContext::Unknown:
|
||||||
default:
|
default:
|
||||||
QTC_ASSERT(false, return "";);
|
QTC_ASSERT(false, return "";);
|
||||||
@@ -202,7 +240,7 @@ QByteArray dummyTextForContext(CharacterContext context, bool closingBraceBlock)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add extra text in case of the empty line or the line starting with ')'.
|
// Add extra text in case of the empty line or the line starting with ')'.
|
||||||
// Track such extra pieces of text in isInsideModifiedLine().
|
// Track such extra pieces of text in isInsideDummyTextInLine().
|
||||||
int forceIndentWithExtraText(QByteArray &buffer,
|
int forceIndentWithExtraText(QByteArray &buffer,
|
||||||
CharacterContext &charContext,
|
CharacterContext &charContext,
|
||||||
const QTextBlock &block,
|
const QTextBlock &block,
|
||||||
@@ -265,7 +303,7 @@ int forceIndentWithExtraText(QByteArray &buffer,
|
|||||||
return extraLength;
|
return extraLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isInsideModifiedLine(const QString &originalLine, const QString &modifiedLine, int column)
|
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.
|
// 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
|
return originalLine.length() < modifiedLine.length() && column != modifiedLine.length() + 1
|
||||||
@@ -291,7 +329,7 @@ TextEditor::Replacements utf16Replacements(const QTextDocument *doc,
|
|||||||
const QString bufferLineText
|
const QString bufferLineText
|
||||||
= Utils::Text::utf16LineTextInUtf8Buffer(utf8Buffer,
|
= Utils::Text::utf16LineTextInUtf8Buffer(utf8Buffer,
|
||||||
static_cast<int>(replacement.getOffset()));
|
static_cast<int>(replacement.getOffset()));
|
||||||
if (isInsideModifiedLine(lineText, bufferLineText, lineColUtf16.column))
|
if (isInsideDummyTextInLine(lineText, bufferLineText, lineColUtf16.column))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
lineColUtf16.column = std::min(lineColUtf16.column, lineText.length() + 1);
|
lineColUtf16.column = std::min(lineColUtf16.column, lineText.length() + 1);
|
||||||
@@ -319,14 +357,12 @@ void applyReplacements(QTextDocument *doc, const TextEditor::Replacements &repla
|
|||||||
int fullOffsetShift = 0;
|
int fullOffsetShift = 0;
|
||||||
QTextCursor editCursor(doc);
|
QTextCursor editCursor(doc);
|
||||||
for (const TextEditor::Replacement &replacement : replacements) {
|
for (const TextEditor::Replacement &replacement : replacements) {
|
||||||
editCursor.beginEditBlock();
|
|
||||||
editCursor.setPosition(replacement.offset + fullOffsetShift);
|
editCursor.setPosition(replacement.offset + fullOffsetShift);
|
||||||
editCursor.movePosition(QTextCursor::NextCharacter,
|
editCursor.movePosition(QTextCursor::NextCharacter,
|
||||||
QTextCursor::KeepAnchor,
|
QTextCursor::KeepAnchor,
|
||||||
replacement.length);
|
replacement.length);
|
||||||
editCursor.removeSelectedText();
|
editCursor.removeSelectedText();
|
||||||
editCursor.insertText(replacement.text);
|
editCursor.insertText(replacement.text);
|
||||||
editCursor.endEditBlock();
|
|
||||||
fullOffsetShift += replacement.text.length() - replacement.length;
|
fullOffsetShift += replacement.text.length() - replacement.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -510,8 +546,11 @@ TextEditor::Replacements ClangFormatBaseIndenter::format(
|
|||||||
ranges = clang::tooling::calculateRangesAfterReplacements(clangReplacements, ranges);
|
ranges = clang::tooling::calculateRangesAfterReplacements(clangReplacements, ranges);
|
||||||
|
|
||||||
clang::format::FormattingAttemptStatus status;
|
clang::format::FormattingAttemptStatus status;
|
||||||
const clang::tooling::Replacements formatReplacements
|
const clang::tooling::Replacements formatReplacements = reformat(style,
|
||||||
= reformat(style, *changedCode, ranges, m_fileName.toString().toStdString(), &status);
|
*changedCode,
|
||||||
|
ranges,
|
||||||
|
assumedFileName,
|
||||||
|
&status);
|
||||||
clangReplacements = clangReplacements.merge(formatReplacements);
|
clangReplacements = clangReplacements.merge(formatReplacements);
|
||||||
|
|
||||||
const TextEditor::Replacements toReplace = utf16Replacements(m_doc, buffer, clangReplacements);
|
const TextEditor::Replacements toReplace = utf16Replacements(m_doc, buffer, clangReplacements);
|
||||||
@@ -533,7 +572,7 @@ TextEditor::Replacements ClangFormatBaseIndenter::indentsFor(QTextBlock startBlo
|
|||||||
|
|
||||||
startBlock = reverseFindLastEmptyBlock(startBlock);
|
startBlock = reverseFindLastEmptyBlock(startBlock);
|
||||||
const int startBlockPosition = startBlock.position();
|
const int startBlockPosition = startBlock.position();
|
||||||
if (startBlock.position() > 0) {
|
if (startBlockPosition > 0) {
|
||||||
trimRHSWhitespace(startBlock.previous());
|
trimRHSWhitespace(startBlock.previous());
|
||||||
if (cursorPositionInEditor >= 0)
|
if (cursorPositionInEditor >= 0)
|
||||||
cursorPositionInEditor += startBlock.position() - startBlockPosition;
|
cursorPositionInEditor += startBlock.position() - startBlockPosition;
|
||||||
@@ -548,9 +587,9 @@ TextEditor::Replacements ClangFormatBaseIndenter::indentsFor(QTextBlock startBlo
|
|||||||
// Format before current position only in case the cursor is inside the indented block.
|
// 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
|
// 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.
|
// the indented block - don't trigger extra formatting in this case.
|
||||||
// cursorPositionInEditor == -1 means the consition matches automatically.
|
// cursorPositionInEditor == -1 means the condition matches automatically.
|
||||||
|
|
||||||
// Format only before newline or complete statement not to break code.
|
// Format only before complete statement not to break code.
|
||||||
replacementsToKeep = ReplacementsToKeep::IndentAndBefore;
|
replacementsToKeep = ReplacementsToKeep::IndentAndBefore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -441,6 +441,31 @@ TEST_F(ClangFormat, DoNotIndentClosingBraceAfterSemicolon)
|
|||||||
"}"));
|
"}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(ClangFormat, IndentAfterIf)
|
||||||
|
{
|
||||||
|
insertLines({"if (a)",
|
||||||
|
""});
|
||||||
|
|
||||||
|
indenter.indentBlock(doc.findBlockByNumber(1), QChar::Null, TextEditor::TabSettings());
|
||||||
|
|
||||||
|
ASSERT_THAT(documentLines(), ElementsAre("if (a)",
|
||||||
|
" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ClangFormat, IndentAfterElse)
|
||||||
|
{
|
||||||
|
insertLines({"if (a)",
|
||||||
|
" foo();",
|
||||||
|
"else",
|
||||||
|
""});
|
||||||
|
indenter.indentBlock(doc.findBlockByNumber(3), QChar::Null, TextEditor::TabSettings());
|
||||||
|
|
||||||
|
ASSERT_THAT(documentLines(), ElementsAre("if (a)",
|
||||||
|
" foo();",
|
||||||
|
"else",
|
||||||
|
" "));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(ClangFormat, SameIndentAfterSecondNewLineAfterIf)
|
TEST_F(ClangFormat, SameIndentAfterSecondNewLineAfterIf)
|
||||||
{
|
{
|
||||||
insertLines({"if (a)",
|
insertLines({"if (a)",
|
||||||
@@ -504,6 +529,102 @@ TEST_F(ClangFormat, SameIndentsOnNewLinesAfterComments)
|
|||||||
""));
|
""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(ClangFormat, IndentAfterEmptyLineAfterAngledIncludeDirective)
|
||||||
|
{
|
||||||
|
insertLines({"#include <string>",
|
||||||
|
"",
|
||||||
|
"using namespace std;"});
|
||||||
|
|
||||||
|
indenter.indentBlock(doc.findBlockByNumber(2), QChar::Null, TextEditor::TabSettings());
|
||||||
|
|
||||||
|
ASSERT_THAT(documentLines(), ElementsAre("#include <string>",
|
||||||
|
"",
|
||||||
|
"using namespace std;"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ClangFormat, IndentAfterEmptyLineAfterQuotedIncludeDirective)
|
||||||
|
{
|
||||||
|
insertLines({"#include \"foo.h\"",
|
||||||
|
"",
|
||||||
|
"using namespace std;"});
|
||||||
|
|
||||||
|
indenter.indentBlock(doc.findBlockByNumber(2), QChar::Null, TextEditor::TabSettings());
|
||||||
|
|
||||||
|
ASSERT_THAT(documentLines(), ElementsAre("#include \"foo.h\"",
|
||||||
|
"",
|
||||||
|
"using namespace std;"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ClangFormat, IndentAfterLineComment)
|
||||||
|
{
|
||||||
|
insertLines({"int foo()",
|
||||||
|
"{",
|
||||||
|
" // Comment",
|
||||||
|
" ",
|
||||||
|
" if (",
|
||||||
|
"}"});
|
||||||
|
|
||||||
|
indenter.indentBlock(doc.findBlockByNumber(4), '(', TextEditor::TabSettings());
|
||||||
|
|
||||||
|
ASSERT_THAT(documentLines(), ElementsAre("int foo()",
|
||||||
|
"{",
|
||||||
|
" // Comment",
|
||||||
|
" ",
|
||||||
|
" if (",
|
||||||
|
"}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ClangFormat, IndentAfterBlockComment)
|
||||||
|
{
|
||||||
|
insertLines({"int foo()",
|
||||||
|
"{",
|
||||||
|
" bar(); /* Comment */",
|
||||||
|
" ",
|
||||||
|
" if (",
|
||||||
|
"}"});
|
||||||
|
|
||||||
|
indenter.indentBlock(doc.findBlockByNumber(4), '(', TextEditor::TabSettings());
|
||||||
|
|
||||||
|
ASSERT_THAT(documentLines(), ElementsAre("int foo()",
|
||||||
|
"{",
|
||||||
|
" bar(); /* Comment */",
|
||||||
|
" ",
|
||||||
|
" if (",
|
||||||
|
"}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ClangFormat, IndentAfterIfdef)
|
||||||
|
{
|
||||||
|
insertLines({"int foo()",
|
||||||
|
"{",
|
||||||
|
"#ifdef FOO",
|
||||||
|
"#endif",
|
||||||
|
" ",
|
||||||
|
" if (",
|
||||||
|
"}"});
|
||||||
|
|
||||||
|
indenter.indentBlock(doc.findBlockByNumber(5), '(', TextEditor::TabSettings());
|
||||||
|
|
||||||
|
ASSERT_THAT(documentLines(), ElementsAre("int foo()",
|
||||||
|
"{",
|
||||||
|
"#ifdef FOO",
|
||||||
|
"#endif",
|
||||||
|
" ",
|
||||||
|
" if (",
|
||||||
|
"}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ClangFormat, IndentAfterEmptyLineInTheFileBeginning)
|
||||||
|
{
|
||||||
|
insertLines({"",
|
||||||
|
"void foo()"});
|
||||||
|
|
||||||
|
indenter.indentBlock(doc.findBlockByNumber(1), ')', TextEditor::TabSettings());
|
||||||
|
|
||||||
|
ASSERT_THAT(documentLines(), ElementsAre("",
|
||||||
|
"void foo()"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(ClangFormat, IndentFunctionBodyButNotFormatBeforeIt)
|
TEST_F(ClangFormat, IndentFunctionBodyButNotFormatBeforeIt)
|
||||||
{
|
{
|
||||||
insertLines({"int foo(int a, int b,",
|
insertLines({"int foo(int a, int b,",
|
||||||
|
|||||||
Reference in New Issue
Block a user