forked from qt-creator/qt-creator
ClangFormat: Refactor indenter to allow ClangFormat unit-tests
We do not build texteditor files in unit-tests so some tricks were required to make ClangFormatIndenter available. First simple unit-test proofs it builds and runs. Change-Id: I81d5ea099bd27fd1c1ed8b5b7877299dcc62a67f Reviewed-by: Nikolai Kosjar <nikolai.kosjar@qt.io>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2018 The Qt Company Ltd.
|
||||
** Copyright (C) 2019 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of Qt Creator.
|
||||
@@ -24,479 +24,26 @@
|
||||
****************************************************************************/
|
||||
|
||||
#include "clangformatindenter.h"
|
||||
|
||||
#include "clangformatutils.h"
|
||||
|
||||
#include <clang/Format/Format.h>
|
||||
#include <clang/Tooling/Core/Replacement.h>
|
||||
|
||||
#include <cpptools/cppmodelmanager.h>
|
||||
#include <texteditor/textdocument.h>
|
||||
#include <texteditor/texteditor.h>
|
||||
|
||||
#include <utils/hostosinfo.h>
|
||||
#include <utils/textutils.h>
|
||||
#include <utils/qtcassert.h>
|
||||
|
||||
#include <llvm/Config/llvm-config.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QTextBlock>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
using ClangReplacement = clang::tooling::Replacement;
|
||||
using ClangReplacements = clang::tooling::Replacements;
|
||||
using QtReplacement = TextEditor::Replacement;
|
||||
using QtReplacements = TextEditor::Replacements;
|
||||
#include <texteditor/tabsettings.h>
|
||||
|
||||
using namespace clang;
|
||||
using namespace format;
|
||||
using namespace llvm;
|
||||
using namespace tooling;
|
||||
using namespace ProjectExplorer;
|
||||
using namespace TextEditor;
|
||||
|
||||
namespace ClangFormat {
|
||||
|
||||
namespace {
|
||||
ClangFormatIndenter::ClangFormatIndenter(QTextDocument *doc)
|
||||
: ClangFormatBaseIndenter(doc)
|
||||
{}
|
||||
|
||||
void adjustFormatStyleForLineBreak(format::FormatStyle &style)
|
||||
FormatStyle ClangFormatIndenter::styleForFile() const
|
||||
{
|
||||
style.DisableFormat = false;
|
||||
style.ColumnLimit = 0;
|
||||
#ifdef KEEP_LINE_BREAKS_FOR_NON_EMPTY_LINES_BACKPORTED
|
||||
style.KeepLineBreaksForNonEmptyLines = true;
|
||||
#endif
|
||||
style.MaxEmptyLinesToKeep = 2;
|
||||
style.SortIncludes = false;
|
||||
style.SortUsingDeclarations = false;
|
||||
return ClangFormat::styleForFile(m_fileName);
|
||||
}
|
||||
|
||||
StringRef clearExtraNewline(StringRef text)
|
||||
{
|
||||
while (text.startswith("\n\n"))
|
||||
text = text.drop_front();
|
||||
return text;
|
||||
}
|
||||
|
||||
ClangReplacements filteredReplacements(const ClangReplacements &replacements,
|
||||
int offset,
|
||||
int extraOffsetToAdd,
|
||||
bool onlyIndention)
|
||||
{
|
||||
ClangReplacements filtered;
|
||||
for (const ClangReplacement &replacement : replacements) {
|
||||
int replacementOffset = static_cast<int>(replacement.getOffset());
|
||||
if (onlyIndention && replacementOffset != offset - 1)
|
||||
continue;
|
||||
|
||||
if (replacementOffset + 1 >= offset)
|
||||
replacementOffset += extraOffsetToAdd;
|
||||
|
||||
StringRef text = onlyIndention ? clearExtraNewline(replacement.getReplacementText())
|
||||
: replacement.getReplacementText();
|
||||
|
||||
Error error = filtered.add(ClangReplacement(replacement.getFilePath(),
|
||||
static_cast<unsigned int>(replacementOffset),
|
||||
replacement.getLength(),
|
||||
text));
|
||||
// Throws if error is not checked.
|
||||
if (error)
|
||||
break;
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
void trimFirstNonEmptyBlock(const QTextBlock ¤tBlock)
|
||||
{
|
||||
QTextBlock prevBlock = currentBlock.previous();
|
||||
while (prevBlock.position() > 0 && prevBlock.text().trimmed().isEmpty())
|
||||
prevBlock = prevBlock.previous();
|
||||
|
||||
if (prevBlock.text().trimmed().isEmpty())
|
||||
return;
|
||||
|
||||
const QString initialText = prevBlock.text();
|
||||
if (!initialText.at(initialText.size() - 1).isSpace())
|
||||
return;
|
||||
|
||||
auto lastNonSpace = std::find_if_not(initialText.rbegin(),
|
||||
initialText.rend(),
|
||||
[](const QChar &letter) {
|
||||
return letter.isSpace();
|
||||
});
|
||||
const int extraSpaceCount = static_cast<int>(std::distance(initialText.rbegin(), lastNonSpace));
|
||||
|
||||
QTextCursor cursor(prevBlock);
|
||||
cursor.beginEditBlock();
|
||||
cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor,
|
||||
initialText.size() - extraSpaceCount);
|
||||
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, extraSpaceCount);
|
||||
cursor.removeSelectedText();
|
||||
cursor.endEditBlock();
|
||||
}
|
||||
|
||||
void trimCurrentBlock(const QTextBlock ¤tBlock)
|
||||
{
|
||||
if (currentBlock.text().trimmed().isEmpty()) {
|
||||
// Clear the block containing only spaces
|
||||
QTextCursor cursor(currentBlock);
|
||||
cursor.beginEditBlock();
|
||||
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||
cursor.removeSelectedText();
|
||||
cursor.endEditBlock();
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the total langth of previous lines with pure whitespace.
|
||||
int previousEmptyLinesLength(const QTextBlock ¤tBlock)
|
||||
{
|
||||
int length{0};
|
||||
QTextBlock prevBlock = currentBlock.previous();
|
||||
while (prevBlock.position() > 0 && prevBlock.text().trimmed().isEmpty()) {
|
||||
length += prevBlock.text().length() + 1;
|
||||
prevBlock = prevBlock.previous();
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
void modifyToIndentEmptyLines(
|
||||
QByteArray &buffer, int offset, int &length, const QTextBlock &block, bool secondTry)
|
||||
{
|
||||
const QString blockText = block.text().trimmed();
|
||||
const bool closingParenBlock = blockText.startsWith(')');
|
||||
if (blockText.isEmpty() || closingParenBlock) {
|
||||
//This extra text works for the most cases.
|
||||
QByteArray dummyText("a;");
|
||||
|
||||
// Search for previous character
|
||||
QTextBlock prevBlock = block.previous();
|
||||
bool prevBlockIsEmpty = prevBlock.position() > 0 && prevBlock.text().trimmed().isEmpty();
|
||||
while (prevBlockIsEmpty) {
|
||||
prevBlock = prevBlock.previous();
|
||||
prevBlockIsEmpty = prevBlock.position() > 0 && prevBlock.text().trimmed().isEmpty();
|
||||
}
|
||||
if (prevBlock.text().endsWith(','))
|
||||
dummyText = "int a";
|
||||
|
||||
if (closingParenBlock) {
|
||||
if (prevBlock.text().endsWith(','))
|
||||
dummyText = "int a";
|
||||
else
|
||||
dummyText = "&& a";
|
||||
}
|
||||
|
||||
length += dummyText.length();
|
||||
buffer.insert(offset, dummyText);
|
||||
}
|
||||
|
||||
if (secondTry) {
|
||||
int nextLinePos = buffer.indexOf('\n', offset);
|
||||
if (nextLinePos > 0) {
|
||||
// If first try was not successful try to put ')' in the end of the line to close possibly
|
||||
// unclosed parentheses.
|
||||
// TODO: Does it help to add different endings depending on the context?
|
||||
buffer.insert(nextLinePos, ')');
|
||||
length += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const int kMaxLinesFromCurrentBlock = 200;
|
||||
|
||||
Utils::LineColumn utf16LineColumn(const QTextBlock &block,
|
||||
int blockOffsetUtf8,
|
||||
const QByteArray &utf8Buffer,
|
||||
int utf8Offset)
|
||||
{
|
||||
// If lastIndexOf('\n') returns -1 then we are fine to add 1 and get 0 offset.
|
||||
const int lineStartUtf8Offset = utf8Buffer.lastIndexOf('\n', utf8Offset - 1) + 1;
|
||||
int line = block.blockNumber() + 1; // Init with the line corresponding the block.
|
||||
|
||||
if (utf8Offset < blockOffsetUtf8) {
|
||||
line -= static_cast<int>(std::count(utf8Buffer.begin() + lineStartUtf8Offset,
|
||||
utf8Buffer.begin() + blockOffsetUtf8,
|
||||
'\n'));
|
||||
} else {
|
||||
line += static_cast<int>(std::count(utf8Buffer.begin() + blockOffsetUtf8,
|
||||
utf8Buffer.begin() + lineStartUtf8Offset,
|
||||
'\n'));
|
||||
}
|
||||
|
||||
const QByteArray lineText = utf8Buffer.mid(lineStartUtf8Offset,
|
||||
utf8Offset - lineStartUtf8Offset);
|
||||
return Utils::LineColumn(line, QString::fromUtf8(lineText).size() + 1);
|
||||
}
|
||||
|
||||
QtReplacements utf16Replacements(const QTextBlock &block,
|
||||
int blockOffsetUtf8,
|
||||
const QByteArray &utf8Buffer,
|
||||
const ClangReplacements &replacements)
|
||||
{
|
||||
QtReplacements convertedReplacements;
|
||||
convertedReplacements.reserve(replacements.size());
|
||||
for (const ClangReplacement &replacement : replacements) {
|
||||
const Utils::LineColumn lineColUtf16 = utf16LineColumn(block,
|
||||
blockOffsetUtf8,
|
||||
utf8Buffer,
|
||||
static_cast<int>(
|
||||
replacement.getOffset()));
|
||||
if (!lineColUtf16.isValid())
|
||||
continue;
|
||||
const int utf16Offset = Utils::Text::positionInText(block.document(),
|
||||
lineColUtf16.line,
|
||||
lineColUtf16.column);
|
||||
const int utf16Length = QString::fromUtf8(
|
||||
utf8Buffer.mid(static_cast<int>(replacement.getOffset()),
|
||||
static_cast<int>(replacement.getLength())))
|
||||
.size();
|
||||
convertedReplacements.emplace_back(utf16Offset,
|
||||
utf16Length,
|
||||
QString::fromStdString(replacement.getReplacementText()));
|
||||
}
|
||||
|
||||
return convertedReplacements;
|
||||
}
|
||||
|
||||
QtReplacements replacements(const Utils::FileName &fileName,
|
||||
QByteArray buffer,
|
||||
int utf8Offset,
|
||||
int utf8Length,
|
||||
const QTextBlock &block,
|
||||
const QChar &typedChar = QChar::Null,
|
||||
bool onlyIndention = true,
|
||||
bool secondTry = false)
|
||||
{
|
||||
FormatStyle style = styleForFile(fileName);
|
||||
|
||||
int originalOffsetUtf8 = utf8Offset;
|
||||
int originalLengthUtf8 = utf8Length;
|
||||
QByteArray originalBuffer = buffer;
|
||||
|
||||
int extraOffset = 0;
|
||||
if (onlyIndention) {
|
||||
if (block.blockNumber() > kMaxLinesFromCurrentBlock) {
|
||||
extraOffset = Utils::Text::utf8NthLineOffset(
|
||||
block.document(), buffer, block.blockNumber() - kMaxLinesFromCurrentBlock);
|
||||
}
|
||||
int endOffset = Utils::Text::utf8NthLineOffset(
|
||||
block.document(), buffer, block.blockNumber() + kMaxLinesFromCurrentBlock);
|
||||
if (endOffset == -1)
|
||||
endOffset = buffer.size();
|
||||
|
||||
buffer = buffer.mid(extraOffset, endOffset - extraOffset);
|
||||
utf8Offset -= extraOffset;
|
||||
|
||||
const int emptySpaceLength = previousEmptyLinesLength(block);
|
||||
utf8Offset -= emptySpaceLength;
|
||||
buffer.remove(utf8Offset, emptySpaceLength);
|
||||
|
||||
extraOffset += emptySpaceLength;
|
||||
|
||||
adjustFormatStyleForLineBreak(style);
|
||||
if (typedChar == QChar::Null)
|
||||
modifyToIndentEmptyLines(buffer, utf8Offset, utf8Length, block, secondTry);
|
||||
}
|
||||
|
||||
std::vector<Range> ranges{{static_cast<unsigned int>(utf8Offset),
|
||||
static_cast<unsigned int>(utf8Length)}};
|
||||
FormattingAttemptStatus status;
|
||||
|
||||
ClangReplacements clangReplacements = reformat(style,
|
||||
buffer.data(),
|
||||
ranges,
|
||||
fileName.toString().toStdString(),
|
||||
&status);
|
||||
|
||||
if (!status.FormatComplete)
|
||||
QtReplacements();
|
||||
|
||||
const ClangReplacements filtered = filteredReplacements(clangReplacements,
|
||||
utf8Offset,
|
||||
extraOffset,
|
||||
onlyIndention);
|
||||
|
||||
const bool canTryAgain = onlyIndention && typedChar == QChar::Null && !secondTry;
|
||||
if (canTryAgain && filtered.empty()) {
|
||||
return replacements(fileName,
|
||||
originalBuffer,
|
||||
originalOffsetUtf8,
|
||||
originalLengthUtf8,
|
||||
block,
|
||||
typedChar,
|
||||
onlyIndention,
|
||||
true);
|
||||
}
|
||||
|
||||
return utf16Replacements(block, originalOffsetUtf8, originalBuffer, filtered);
|
||||
}
|
||||
|
||||
void applyReplacements(const QTextBlock &block, const QtReplacements &replacements)
|
||||
{
|
||||
if (replacements.empty())
|
||||
return;
|
||||
|
||||
int fullOffsetShift = 0;
|
||||
QTextCursor editCursor(block);
|
||||
for (const QtReplacement &replacement : replacements) {
|
||||
editCursor.beginEditBlock();
|
||||
editCursor.setPosition(replacement.offset + fullOffsetShift);
|
||||
editCursor.movePosition(QTextCursor::NextCharacter,
|
||||
QTextCursor::KeepAnchor,
|
||||
replacement.length);
|
||||
editCursor.removeSelectedText();
|
||||
editCursor.insertText(replacement.text);
|
||||
editCursor.endEditBlock();
|
||||
fullOffsetShift += replacement.text.length() - replacement.length;
|
||||
}
|
||||
}
|
||||
|
||||
QString selectedLines(QTextDocument *doc, const QTextBlock &startBlock, const QTextBlock &endBlock)
|
||||
{
|
||||
QString text = Utils::Text::textAt(
|
||||
QTextCursor(doc),
|
||||
startBlock.position(),
|
||||
std::max(0, endBlock.position() + endBlock.length() - startBlock.position() - 1));
|
||||
while (!text.isEmpty() && text.rbegin()->isSpace())
|
||||
text.chop(1);
|
||||
return text;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
bool ClangFormatIndenter::isElectricCharacter(const QChar &ch) const
|
||||
{
|
||||
switch (ch.toLatin1()) {
|
||||
case '{':
|
||||
case '}':
|
||||
case ':':
|
||||
case '#':
|
||||
case '<':
|
||||
case '>':
|
||||
case ';':
|
||||
case '(':
|
||||
case ')':
|
||||
case ',':
|
||||
case '.':
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ClangFormatIndenter::indent(QTextDocument *doc,
|
||||
const QTextCursor &cursor,
|
||||
const QChar &typedChar,
|
||||
const TabSettings &tabSettings,
|
||||
bool /*autoTriggered*/)
|
||||
{
|
||||
if (cursor.hasSelection()) {
|
||||
// Calling currentBlock.next() might be unsafe because we change the document.
|
||||
// Let's operate with block numbers instead.
|
||||
const int startNumber = doc->findBlock(cursor.selectionStart()).blockNumber();
|
||||
const int endNumber = doc->findBlock(cursor.selectionEnd()).blockNumber();
|
||||
for (int currentBlockNumber = startNumber; currentBlockNumber <= endNumber;
|
||||
++currentBlockNumber) {
|
||||
const QTextBlock currentBlock = doc->findBlockByNumber(currentBlockNumber);
|
||||
if (currentBlock.isValid()) {
|
||||
const int blocksAmount = doc->blockCount();
|
||||
indentBlock(doc, currentBlock, typedChar, tabSettings);
|
||||
QTC_CHECK(blocksAmount == doc->blockCount()
|
||||
&& "ClangFormat plugin indentation changed the amount of blocks.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
indentBlock(doc, cursor.block(), typedChar, tabSettings);
|
||||
}
|
||||
}
|
||||
|
||||
QtReplacements ClangFormatIndenter::format(QTextDocument *doc,
|
||||
const Utils::FileName &fileName,
|
||||
const QTextCursor &cursor,
|
||||
const TextEditor::TabSettings & /*tabSettings*/)
|
||||
{
|
||||
int utf8Offset;
|
||||
int utf8Length;
|
||||
const QByteArray buffer = doc->toPlainText().toUtf8();
|
||||
QTextBlock block = cursor.block();
|
||||
if (cursor.hasSelection()) {
|
||||
block = doc->findBlock(cursor.selectionStart());
|
||||
const QTextBlock end = doc->findBlock(cursor.selectionEnd());
|
||||
utf8Offset = Utils::Text::utf8NthLineOffset(doc, buffer, block.blockNumber() + 1);
|
||||
QTC_ASSERT(utf8Offset >= 0, return QtReplacements(););
|
||||
utf8Length = selectedLines(doc, block, end).toUtf8().size();
|
||||
|
||||
} else {
|
||||
const QTextBlock block = cursor.block();
|
||||
utf8Offset = Utils::Text::utf8NthLineOffset(doc, buffer, block.blockNumber() + 1);
|
||||
QTC_ASSERT(utf8Offset >= 0, return QtReplacements(););
|
||||
utf8Length = block.text().toUtf8().size();
|
||||
}
|
||||
|
||||
const QtReplacements toReplace
|
||||
= replacements(fileName, buffer, utf8Offset, utf8Length, block, QChar::Null, false);
|
||||
applyReplacements(block, toReplace);
|
||||
|
||||
return toReplace;
|
||||
}
|
||||
|
||||
void ClangFormatIndenter::reindent(QTextDocument *doc,
|
||||
const QTextCursor &cursor,
|
||||
const TabSettings &tabSettings)
|
||||
{
|
||||
indent(doc, cursor, QChar::Null, tabSettings);
|
||||
}
|
||||
|
||||
void ClangFormatIndenter::indentBlock(QTextDocument *doc,
|
||||
const QTextBlock &block,
|
||||
const QChar &typedChar,
|
||||
const TabSettings &tabSettings)
|
||||
{
|
||||
Q_UNUSED(tabSettings);
|
||||
|
||||
TextEditorWidget *editor = TextEditorWidget::currentTextEditorWidget();
|
||||
if (!editor)
|
||||
return;
|
||||
|
||||
const Utils::FileName fileName = editor->textDocument()->filePath();
|
||||
trimFirstNonEmptyBlock(block);
|
||||
trimCurrentBlock(block);
|
||||
const QByteArray buffer = doc->toPlainText().toUtf8();
|
||||
const int utf8Offset = Utils::Text::utf8NthLineOffset(doc, buffer, block.blockNumber() + 1);
|
||||
QTC_ASSERT(utf8Offset >= 0, return;);
|
||||
|
||||
applyReplacements(block,
|
||||
replacements(fileName, buffer, utf8Offset, 0, block, typedChar));
|
||||
}
|
||||
|
||||
int ClangFormatIndenter::indentFor(const QTextBlock &block, const TextEditor::TabSettings &)
|
||||
{
|
||||
TextEditorWidget *editor = TextEditorWidget::currentTextEditorWidget();
|
||||
if (!editor)
|
||||
return -1;
|
||||
|
||||
const Utils::FileName fileName = editor->textDocument()->filePath();
|
||||
trimFirstNonEmptyBlock(block);
|
||||
trimCurrentBlock(block);
|
||||
const QTextDocument *doc = block.document();
|
||||
const QByteArray buffer = doc->toPlainText().toUtf8();
|
||||
const int utf8Offset = Utils::Text::utf8NthLineOffset(doc, buffer, block.blockNumber() + 1);
|
||||
QTC_ASSERT(utf8Offset >= 0, return 0;);
|
||||
|
||||
const QtReplacements toReplace = replacements(fileName, buffer, utf8Offset, 0, block);
|
||||
|
||||
if (toReplace.empty())
|
||||
return -1;
|
||||
|
||||
const QtReplacement &replacement = toReplace.front();
|
||||
int afterLineBreak = replacement.text.lastIndexOf('\n');
|
||||
afterLineBreak = (afterLineBreak < 0) ? 0 : afterLineBreak + 1;
|
||||
return static_cast<int>(replacement.text.size() - afterLineBreak);
|
||||
}
|
||||
|
||||
TabSettings ClangFormatIndenter::tabSettings() const
|
||||
Utils::optional<TabSettings> ClangFormatIndenter::tabSettings() const
|
||||
{
|
||||
FormatStyle style = currentProjectStyle();
|
||||
TabSettings tabSettings;
|
||||
|
||||
Reference in New Issue
Block a user