Utils: Add formatting information to ChangeSet::EditOp

... and make use of that in TextEditor::RefactoringFile.
This allows calling code to have fine-grained control over which parts of
a refactoring should get re-formatted, while also providing sensible
default values that are "almost always" right, so things typically work
as expected out of the box.

Change-Id: I9200c2135b7477c33bc5a61c5d410b34853e4b61
Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
Christian Kandeler
2023-11-22 16:21:56 +01:00
parent 0e04f40fa7
commit 3b7d29d2a1
9 changed files with 113 additions and 80 deletions

View File

@@ -35,7 +35,7 @@ bool ChangeSet::hasOverlap(int pos, int length) const
{
for (const EditOp &cmd : m_operationList) {
switch (cmd.type) {
switch (cmd.type()) {
case EditOp::Replace:
if (overlaps(pos, length, cmd.pos1, cmd.length1))
return true;
@@ -71,9 +71,6 @@ bool ChangeSet::hasOverlap(int pos, int length) const
if (cmd.pos2 > pos && cmd.pos2 < pos + length)
return true;
break;
case EditOp::Unset:
break;
}
}
@@ -86,6 +83,11 @@ bool ChangeSet::isEmpty() const
}
QList<ChangeSet::EditOp> ChangeSet::operationList() const
{
return const_cast<ChangeSet *>(this)->operationList();
}
QList<ChangeSet::EditOp> &ChangeSet::operationList()
{
return m_operationList;
}
@@ -103,10 +105,9 @@ bool ChangeSet::replace_helper(int pos, int length, const QString &replacement)
if (hasOverlap(pos, length))
m_error = true;
EditOp cmd(EditOp::Replace);
EditOp cmd(EditOp::Replace, replacement);
cmd.pos1 = pos;
cmd.length1 = length;
cmd.text = replacement;
m_operationList += cmd;
return !m_error;
@@ -136,9 +137,8 @@ bool ChangeSet::insert(int pos, const QString &text)
if (hasOverlap(pos, 0))
m_error = true;
EditOp cmd(EditOp::Insert);
EditOp cmd(EditOp::Insert, text);
cmd.pos1 = pos;
cmd.text = text;
m_operationList += cmd;
return !m_error;
@@ -224,79 +224,90 @@ bool ChangeSet::copy_helper(int pos, int length, int to)
void ChangeSet::doReplace(const EditOp &op, QList<EditOp> *replaceList)
{
Q_ASSERT(op.type == EditOp::Replace);
Q_ASSERT(op.type() == EditOp::Replace);
{
for (EditOp &c : *replaceList) {
if (op.pos1 <= c.pos1)
c.pos1 += op.text.size();
c.pos1 += op.text().size();
if (op.pos1 < c.pos1)
c.pos1 -= op.length1;
}
}
if (m_string) {
m_string->replace(op.pos1, op.length1, op.text);
m_string->replace(op.pos1, op.length1, op.text());
} else if (m_cursor) {
m_cursor->setPosition(op.pos1);
m_cursor->setPosition(op.pos1 + op.length1, QTextCursor::KeepAnchor);
m_cursor->insertText(op.text);
m_cursor->insertText(op.text());
}
}
void ChangeSet::convertToReplace(const EditOp &op, QList<EditOp> *replaceList)
{
EditOp replace1(EditOp::Replace);
EditOp replace2(EditOp::Replace);
switch (op.type) {
switch (op.type()) {
case EditOp::Replace:
replaceList->append(op);
break;
case EditOp::Move:
case EditOp::Move: {
EditOp replace1(EditOp::Replace);
replace1.pos1 = op.pos1;
replace1.length1 = op.length1;
if (op.hasFormat1())
replace1.setFormat1(op.format1());
replaceList->append(replace1);
EditOp replace2(EditOp::Replace, textAt(op.pos1, op.length1));
replace2.pos1 = op.pos2;
replace2.text = textAt(op.pos1, op.length1);
if (op.hasFormat2())
replace2.setFormat1(op.format2());
replaceList->append(replace2);
break;
case EditOp::Insert:
replace1.pos1 = op.pos1;
replace1.text = op.text;
replaceList->append(replace1);
}
case EditOp::Insert: {
EditOp replace(EditOp::Replace, op.text());
replace.pos1 = op.pos1;
if (op.hasFormat1())
replace.setFormat1(op.format1());
replaceList->append(replace);
break;
case EditOp::Remove:
}
case EditOp::Remove: {
EditOp replace(EditOp::Replace);
replace.pos1 = op.pos1;
replace.length1 = op.length1;
if (op.hasFormat1())
replace.setFormat1(op.format1());
replaceList->append(replace);
break;
}
case EditOp::Flip: {
EditOp replace1(EditOp::Replace, textAt(op.pos2, op.length2));
replace1.pos1 = op.pos1;
replace1.length1 = op.length1;
replaceList->append(replace1);
break;
case EditOp::Flip:
replace1.pos1 = op.pos1;
replace1.length1 = op.length1;
replace1.text = textAt(op.pos2, op.length2);
if (op.hasFormat1())
replace1.setFormat1(op.format1());
replaceList->append(replace1);
EditOp replace2(EditOp::Replace, textAt(op.pos1, op.length1));
replace2.pos1 = op.pos2;
replace2.length1 = op.length2;
replace2.text = textAt(op.pos1, op.length1);
if (op.hasFormat2())
replace2.setFormat1(op.format2());
replaceList->append(replace2);
break;
case EditOp::Copy:
replace1.pos1 = op.pos2;
replace1.text = textAt(op.pos1, op.length1);
replaceList->append(replace1);
break;
case EditOp::Unset:
}
case EditOp::Copy: {
EditOp replace(EditOp::Replace, textAt(op.pos1, op.length1));
replace.pos1 = op.pos2;
if (op.hasFormat2())
replace.setFormat1(op.format2());
replaceList->append(replace);
break;
}
}
}
bool ChangeSet::hadErrors() const
@@ -354,4 +365,32 @@ void ChangeSet::apply_helper()
m_cursor->endEditBlock();
}
ChangeSet::EditOp::EditOp(Type t, const QString &text) : m_type(t), m_text(text)
{
// The formatting default values are derived as follows:
// 1) Inserted code generally needs to be formatted, because indentation etc is
// context-dependent.
// 2) Removed code should not be formatted, as it is gone.
// 3) Copied code needs to be formatted at the insertion site as per 1).
// 4) Moved code needs to be formatted at the insertion site as per 1) and 2).
// 5) Replaced code generally does not need to be formatted, as it's typically just a name
// change. Exception: The new text contains newlines, in which case we do want to format.
// 6) Flipped may or may be formatted at either side, as per 5).
switch (t) {
case Insert:
m_format1 = true;
break;
case Remove:
break;
case Copy:
case Move:
m_format2 = true;
break;
case Replace:
m_format1 = text.contains('\n') || text.contains(QChar::ParagraphSeparator);
break;
case Flip:
break; // Default will be set at conversion to Replace.
}
}
} // end namespace Utils

View File

@@ -8,6 +8,8 @@
#include <QString>
#include <QList>
#include <optional>
QT_BEGIN_NAMESPACE
class QTextCursor;
class QTextDocument;
@@ -21,7 +23,6 @@ public:
struct EditOp {
enum Type
{
Unset,
Replace,
Move,
Insert,
@@ -30,15 +31,28 @@ public:
Copy
};
EditOp() = default;
EditOp(Type t): type(t) {}
EditOp(Type t, const QString &text = {});
Type type() const { return m_type; }
QString text() const { return m_text; }
bool hasFormat1() const { return m_format1.has_value(); }
bool format1() const { return hasFormat1() && *m_format1; }
void setFormat1(bool f) { m_format1 = f; }
bool hasFormat2() const { return m_format2.has_value(); }
bool format2() const { return hasFormat2() && *m_format2; }
void setFormat2(bool f) { m_format2 = f; }
Type type = Unset;
int pos1 = 0;
int pos2 = 0;
int length1 = 0;
int length2 = 0;
QString text;
private:
Type m_type;
QString m_text;
std::optional<bool> m_format1;
std::optional<bool> m_format2;
};
struct Range {
@@ -58,6 +72,7 @@ public:
bool isEmpty() const;
QList<EditOp> operationList() const;
QList<EditOp> &operationList();
void clear();

View File

@@ -436,15 +436,15 @@ int indentationForBlock(const Utils::ChangeSet &toReplace,
auto replacementIt
= std::find_if(ops.begin(), ops.end(), [utf8Offset](const Utils::ChangeSet::EditOp &op) {
QTC_ASSERT(op.type == Utils::ChangeSet::EditOp::Replace, return false);
QTC_ASSERT(op.type() == Utils::ChangeSet::EditOp::Replace, return false);
return op.pos1 == utf8Offset - 1;
});
if (replacementIt == ops.end())
return -1;
int afterLineBreak = replacementIt->text.lastIndexOf('\n');
int afterLineBreak = replacementIt->text().lastIndexOf('\n');
afterLineBreak = (afterLineBreak < 0) ? 0 : afterLineBreak + 1;
return static_cast<int>(replacementIt->text.size() - afterLineBreak);
return static_cast<int>(replacementIt->text().size() - afterLineBreak);
}
bool doNotIndentInContext(QTextDocument *doc, int pos)

View File

@@ -188,10 +188,10 @@ void FixitsRefactoringFile::shiftAffectedReplacements(const FilePath &filePath,
continue;
for (const auto &op : replacements) {
QTC_ASSERT(op.type == ChangeSet::EditOp::Replace, continue);
QTC_ASSERT(op.type() == ChangeSet::EditOp::Replace, continue);
if (op.pos1 > current.pos)
break;
current.pos += op.text.size() - op.length1;
current.pos += op.text().size() - op.length1;
}
}
}

View File

@@ -2230,7 +2230,7 @@ public:
QString description;
if (m_change.operationList().size() == 1) {
description = Tr::tr(
"Reformat to \"%1\"").arg(m_change.operationList().constFirst().text);
"Reformat to \"%1\"").arg(m_change.operationList().constFirst().text());
} else { // > 1
description = Tr::tr("Reformat Pointers or References");
}
@@ -2732,7 +2732,6 @@ public:
ChangeSet localChangeSet;
ChangeSet * const target = changeSet ? changeSet : &localChangeSet;
target->replace(targetPos - 1, targetPos, QLatin1String("\n {\n\n}")); // replace ';'
const ChangeSet::Range indentRange(targetPos, targetPos + 4);
if (!changeSet) {
targetFile->setChangeSet(*target);
@@ -2805,13 +2804,10 @@ public:
defText.insert(index, inlinePref);
defText += QLatin1String("\n{\n\n}");
const int targetPos = targetFile->position(loc.line(), loc.column());
const int targetPos2 = qMax(0, targetFile->position(loc.line(), 1) - 1);
ChangeSet localChangeSet;
ChangeSet * const target = changeSet ? changeSet : &localChangeSet;
const int targetPos = targetFile->position(loc.line(), loc.column());
target->insert(targetPos, loc.prefix() + defText + loc.suffix());
const ChangeSet::Range indentRange(targetPos2, targetPos);
if (!changeSet) {
targetFile->setChangeSet(*target);
@@ -6583,7 +6579,6 @@ public:
}
if (!m_fromFileChangeSet.isEmpty()) {
m_fromFile->setChangeSet(m_fromFileChangeSet);
m_fromFile->skipFormatting();
m_fromFile->apply();
}
}
@@ -7627,13 +7622,6 @@ private:
CppRefactoringChanges refactoring(snapshot());
CppRefactoringFilePtr currentFile = refactoring.cppFile(filePath());
currentFile->setChangeSet(m_changes);
// It would probably be unexpected to users if we were to re-format here, though
// an argument could also be made for doing it, especially if "format instead of indent"
// is enabled, as a difference in line length might trigger a different ClangFormat
// re-flowing.
currentFile->skipFormatting();
currentFile->apply();
}
@@ -8242,6 +8230,7 @@ private:
m_changeSet.insert(m_file->startOf(destructorName->unqualified_name), m_missingNamespace);
else
m_changeSet.insert(m_file->startOf(ast->name), m_missingNamespace);
m_changeSet.operationList().last().setFormat1(false);
}
bool needMissingNamespaces(QList<const Name *> &&fullName, int currentNameCount)
@@ -8411,10 +8400,8 @@ private:
processIncludes(refactoring, filePath());
}
for (auto &file : std::as_const(m_changes)) {
file->skipFormatting();
for (auto &file : std::as_const(m_changes))
file->apply();
}
}
/**

View File

@@ -294,7 +294,7 @@ void QuickToolBar::setProperty(const QString &propertyName, const QVariant &valu
int column;
int changeSetPos = changeSet.operationList().constLast().pos1;
int changeSetLength = changeSet.operationList().constLast().text.length();
int changeSetLength = changeSet.operationList().constLast().text().length();
QTextCursor tc = m_editorWidget->textCursor();
tc.beginEditBlock();
changeSet.apply(&tc);

View File

@@ -595,7 +595,6 @@ FilePaths BaseFileFind::replaceAll(const QString &text, const SearchResultItems
changeSet.replace(start, end, replacement);
}
file->setChangeSet(changeSet);
file->skipFormatting();
file->apply();
}

View File

@@ -288,17 +288,14 @@ bool RefactoringFile::apply()
void RefactoringFile::setupFormattingRanges(const QList<ChangeSet::EditOp> &replaceList)
{
if (!m_formattingEnabled)
return;
QTextDocument * const doc = m_editor ? m_editor->document() : m_document;
QTC_ASSERT(doc, return);
for (const ChangeSet::EditOp &op : replaceList) {
if (!op.format1())
continue;
QTextCursor cursor(doc);
switch (op.type) {
case ChangeSet::EditOp::Unset:
break;
switch (op.type()) {
case ChangeSet::EditOp::Replace:
case ChangeSet::EditOp::Insert:
case ChangeSet::EditOp::Remove:

View File

@@ -60,9 +60,6 @@ public:
bool apply();
bool create(const QString &contents, bool reindent, bool openInEditor);
// TODO: Per EditOp?
void skipFormatting() { m_formattingEnabled = false; }
protected:
// users may only get const access to RefactoringFiles created through
// this constructor, because it can't be used to apply changes
@@ -93,7 +90,6 @@ private:
bool m_activateEditor = false;
int m_editorCursorPosition = -1;
bool m_appliedOnce = false;
bool m_formattingEnabled = true;
};
class TEXTEDITOR_EXPORT RefactoringFileFactory