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:
Ivan Donchevskii
2019-04-01 13:37:43 +02:00
parent 07557016f8
commit 01a528c77a
2 changed files with 185 additions and 25 deletions

View File

@@ -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 &currentBlock) QChar findFirstNonWhitespaceCharacter(const QTextBlock &currentBlock)
{ {
@@ -150,10 +155,42 @@ QChar findFirstNonWhitespaceCharacter(const QTextBlock &currentBlock)
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 &currentBlock, CharacterContext characterContext(const QTextBlock &currentBlock,
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 &currentBlock,
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 &currentBlock) bool nextBlockExistsAndEmpty(const QTextBlock &currentBlock)
@@ -181,20 +221,18 @@ bool nextBlockExistsAndEmpty(const QTextBlock &currentBlock)
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;
} }

View File

@@ -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,",