TextEditor: Snippet ranges refactoring

Parsed snippets are now reported in chunks of texts and variables. A
variable has a index that can be used to identify matching variables and
maybe a mangler that can be used to modify the variable when applying
the snippet.

This effictively moves the variable matching logic from the overlay to
the parser of the snippet, which is needed to implement the LSP snippet
parser.

Task-number: QTCREATORBUG-22406
Change-Id: I6999554c6c6d0f1887c98bf732473f01aa1f230c
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
David Schulz
2021-04-16 12:30:47 +02:00
parent 4890902abf
commit a2dadb3d0b
7 changed files with 366 additions and 342 deletions

View File

@@ -170,34 +170,10 @@ bool Snippet::isModified() const
return m_isModified;
}
struct SnippetReplacement
{
QString text;
int posDelta = 0;
};
static SnippetReplacement replacementAt(int pos, ParsedSnippet &parsedSnippet)
static QString tipPart(const ParsedSnippet::Part &part)
{
static const char kOpenBold[] = "<b>";
static const char kCloseBold[] = "</b>";
auto mangledText = [](const QString &text, const ParsedSnippet::Range &range) {
if (range.length == 0)
return QString("...");
if (NameMangler *mangler = range.mangler)
return mangler->mangle(text.mid(range.start, range.length));
return text.mid(range.start, range.length);
};
if (!parsedSnippet.ranges.isEmpty() && parsedSnippet.ranges.first().start == pos) {
ParsedSnippet::Range range = parsedSnippet.ranges.takeFirst();
return {kOpenBold + mangledText(parsedSnippet.text, range) + kCloseBold, range.length};
}
return {};
}
QString Snippet::generateTip() const
{
static const QHash<QChar, QString> replacements = {{'\n', "<br>"},
{' ', "&nbsp;"},
{'"', "&quot;"},
@@ -205,34 +181,30 @@ QString Snippet::generateTip() const
{'<', "&lt;"},
{'>', "&gt;"}};
QString text;
text.reserve(part.text.size());
for (const QChar &c : part.text)
text.append(replacements.value(c, c));
if (part.variableIndex >= 0)
text = kOpenBold + (text.isEmpty() ? QString("...") : part.text) + kCloseBold;
return text;
}
QString Snippet::generateTip() const
{
SnippetParseResult result = Snippet::parse(m_content);
if (Utils::holds_alternative<SnippetParseError>(result))
return Utils::get<SnippetParseError>(result).htmlMessage();
QTC_ASSERT(Utils::holds_alternative<ParsedSnippet>(result), return {});
auto parsedSnippet = Utils::get<ParsedSnippet>(result);
const ParsedSnippet parsedSnippet = Utils::get<ParsedSnippet>(result);
QString tip("<nobr>");
int pos = 0;
for (int end = parsedSnippet.text.count(); pos < end;) {
const SnippetReplacement &replacement = replacementAt(pos, parsedSnippet);
if (!replacement.text.isEmpty()) {
tip += replacement.text;
pos += replacement.posDelta;
} else {
const QChar &currentChar = parsedSnippet.text.at(pos);
tip += replacements.value(currentChar, currentChar);
++pos;
}
}
SnippetReplacement replacement = replacementAt(pos, parsedSnippet);
while (!replacement.text.isEmpty()) {
tip += replacement.text;
pos += replacement.posDelta;
replacement = replacementAt(pos, parsedSnippet);
}
QTC_CHECK(parsedSnippet.ranges.isEmpty());
for (const ParsedSnippet::Part &part : parsedSnippet.parts)
tip.append(tipPart(part));
return tip;
}
@@ -253,25 +225,35 @@ SnippetParseResult Snippet::parse(const QString &snippet)
return {SnippetParseError{errorMessage, {}, -1}};
const int count = preprocessedSnippet.count();
int start = -1;
NameMangler *mangler = nullptr;
result.text.reserve(count);
QMap<QString, int> variableIndexes;
bool inVar = false;
ParsedSnippet::Part currentPart;
for (int i = 0; i < count; ++i) {
QChar current = preprocessedSnippet.at(i);
QChar next = (i + 1) < count ? preprocessedSnippet.at(i + 1) : QChar();
if (current == Snippet::kVariableDelimiter) {
if (start < 0) {
// start delimiter:
start = result.text.count();
} else {
int length = result.text.count() - start;
result.ranges << ParsedSnippet::Range(start, length, mangler);
if (inVar) {
const QString variable = currentPart.text;
const int index = variableIndexes.value(currentPart.text, result.variables.size());
if (index == result.variables.size()) {
variableIndexes[variable] = index;
result.variables.append(QList<int>());
}
currentPart.variableIndex = index;
currentPart.mangler = mangler;
mangler = nullptr;
start = -1;
result.variables[index] << result.parts.size() - 1;
} else if (currentPart.text.isEmpty()) {
inVar = !inVar;
continue;
}
result.parts << currentPart;
currentPart = ParsedSnippet::Part();
inVar = !inVar;
continue;
}
@@ -281,12 +263,13 @@ SnippetParseResult Snippet::parse(const QString &snippet)
i}};
}
if (current == QLatin1Char(':') && start >= 0) {
if (next == QLatin1Char('l')) {
if (current == ':' && inVar) {
QChar next = (i + 1) < count ? preprocessedSnippet.at(i + 1) : QChar();
if (next == 'l') {
mangler = &lcMangler;
} else if (next == QLatin1Char('u')) {
} else if (next == 'u') {
mangler = &ucMangler;
} else if (next == QLatin1Char('c')) {
} else if (next == 'c') {
mangler = &tcMangler;
} else {
return SnippetParseResult{
@@ -299,20 +282,26 @@ SnippetParseResult Snippet::parse(const QString &snippet)
continue;
}
if (current == kEscapeChar && (next == kEscapeChar || next == kVariableDelimiter)) {
result.text.append(next);
++i;
continue;
if (current == kEscapeChar){
QChar next = (i + 1) < count ? preprocessedSnippet.at(i + 1) : QChar();
if (next == kEscapeChar || next == kVariableDelimiter) {
currentPart.text.append(next);
++i;
continue;
}
}
result.text.append(current);
currentPart.text.append(current);
}
if (start >= 0) {
if (inVar) {
return SnippetParseResult{
SnippetParseError{tr("Missing closing variable delimiter"), result.text, start}};
SnippetParseError{tr("Missing closing variable delimiter for:"), currentPart.text, 0}};
}
if (!currentPart.text.isEmpty())
result.parts << currentPart;
return SnippetParseResult(result);
}
@@ -323,131 +312,130 @@ SnippetParseResult Snippet::parse(const QString &snippet)
const char NOMANGLER_ID[] = "TextEditor::NoMangler";
struct SnippetPart
{
SnippetPart() = default;
explicit SnippetPart(const QString &text,
int index = -1,
const Utils::Id &manglerId = NOMANGLER_ID)
: text(text)
, variableIndex(index)
, manglerId(manglerId)
{}
QString text;
int variableIndex = -1; // if variable index is >= 0 the text is interpreted as a variable
Utils::Id manglerId;
};
Q_DECLARE_METATYPE(SnippetPart);
using Parts = QList<SnippetPart>;
void Internal::TextEditorPlugin::testSnippetParsing_data()
{
QTest::addColumn<QString>("input");
QTest::addColumn<QString>("text");
QTest::addColumn<bool>("success");
QTest::addColumn<QList<int> >("ranges_start");
QTest::addColumn<QList<int> >("ranges_length");
QTest::addColumn<QList<Utils::Id> >("ranges_mangler");
QTest::addColumn<Parts>("parts");
QTest::newRow("no input")
<< QString() << QString() << true
<< (QList<int>()) << (QList<int>()) << (QList<Utils::Id>());
QTest::newRow("empty input")
<< QString::fromLatin1("") << QString::fromLatin1("") << true
<< (QList<int>()) << (QList<int>()) << (QList<Utils::Id>());
QTest::newRow("newline only")
<< QString::fromLatin1("\n") << QString::fromLatin1("\n") << true
<< (QList<int>()) << (QList<int>()) << (QList<Utils::Id>());
QTest::newRow("no input") << QString() << true << Parts();
QTest::newRow("empty input") << QString("") << true << Parts();
QTest::newRow("newline only") << QString("\n") << true << Parts{SnippetPart("\n")};
QTest::newRow("simple identifier")
<< QString::fromLatin1("$tESt$") << QString::fromLatin1("tESt") << true
<< (QList<int>() << 0) << (QList<int>() << 4)
<< (QList<Utils::Id>() << NOMANGLER_ID);
<< QString("$tESt$") << true << Parts{SnippetPart("tESt", 0)};
QTest::newRow("simple identifier with lc")
<< QString::fromLatin1("$tESt:l$") << QString::fromLatin1("tESt") << true
<< (QList<int>() << 0) << (QList<int>() << 4)
<< (QList<Utils::Id>() << LCMANGLER_ID);
<< QString("$tESt:l$") << true << Parts{SnippetPart("tESt", 0, LCMANGLER_ID)};
QTest::newRow("simple identifier with uc")
<< QString::fromLatin1("$tESt:u$") << QString::fromLatin1("tESt") << true
<< (QList<int>() << 0) << (QList<int>() << 4)
<< (QList<Utils::Id>() << UCMANGLER_ID);
<< QString("$tESt:u$") << true << Parts{SnippetPart("tESt", 0, UCMANGLER_ID)};
QTest::newRow("simple identifier with tc")
<< QString::fromLatin1("$tESt:c$") << QString::fromLatin1("tESt") << true
<< (QList<int>() << 0) << (QList<int>() << 4)
<< (QList<Utils::Id>() << TCMANGLER_ID);
<< QString("$tESt:c$") << true << Parts{SnippetPart("tESt", 0, TCMANGLER_ID)};
QTest::newRow("escaped string")
<< QString::fromLatin1("\\\\$test\\\\$") << QString::fromLatin1("$test$") << true
<< (QList<int>()) << (QList<int>())
<< (QList<Utils::Id>());
QTest::newRow("escaped escape")
<< QString::fromLatin1("\\\\\\\\$test$\\\\\\\\") << QString::fromLatin1("\\test\\") << true
<< (QList<int>() << 1) << (QList<int>() << 4)
<< (QList<Utils::Id>() << NOMANGLER_ID);
<< QString("\\\\$test\\\\$") << true << Parts{SnippetPart("$test$")};
QTest::newRow("escaped escape") << QString("\\\\\\\\$test$\\\\\\\\") << true
<< Parts{
SnippetPart("\\"),
SnippetPart("test", 0),
SnippetPart("\\"),
};
QTest::newRow("broken escape")
<< QString::fromLatin1("\\\\$test\\\\\\\\$\\\\") << QString::fromLatin1("\\$test\\\\$\\") << false
<< (QList<int>()) << (QList<int>())
<< (QList<Utils::Id>());
<< QString::fromLatin1("\\\\$test\\\\\\\\$\\\\") << false << Parts();
QTest::newRow("Q_PROPERTY")
<< QString::fromLatin1("Q_PROPERTY($type$ $name$ READ $name$ WRITE set$name:c$ NOTIFY $name$Changed)")
<< QString::fromLatin1("Q_PROPERTY(type name READ name WRITE setname NOTIFY nameChanged)") << true
<< (QList<int>() << 11 << 16 << 26 << 40 << 52)
<< (QList<int>() << 4 << 4 << 4 << 4 << 4)
<< (QList<Utils::Id>() << NOMANGLER_ID << NOMANGLER_ID << NOMANGLER_ID << TCMANGLER_ID << NOMANGLER_ID);
QTest::newRow("Q_PROPERTY") << QString(
"Q_PROPERTY($type$ $name$ READ $name$ WRITE set$name:c$ NOTIFY $name$Changed)")
<< true
<< Parts{SnippetPart("Q_PROPERTY("),
SnippetPart("type", 0),
SnippetPart(" "),
SnippetPart("name", 1),
SnippetPart(" READ "),
SnippetPart("name", 1),
SnippetPart(" WRITE set"),
SnippetPart("name", 1, TCMANGLER_ID),
SnippetPart(" NOTIFY "),
SnippetPart("name", 1),
SnippetPart("Changed)")};
QTest::newRow("open identifier")
<< QString::fromLatin1("$test") << QString::fromLatin1("$test") << false
<< (QList<int>()) << (QList<int>())
<< (QList<Utils::Id>());
QTest::newRow("wrong mangler")
<< QString::fromLatin1("$test:X$") << QString::fromLatin1("$test:X$") << false
<< (QList<int>()) << (QList<int>())
<< (QList<Utils::Id>());
QTest::newRow("open identifier") << QString("$test") << false << Parts();
QTest::newRow("wrong mangler") << QString("$test:X$") << false << Parts();
QTest::newRow("multiline with :")
<< QString::fromLatin1("class $name$\n"
"{\n"
"public:\n"
" $name$() {}\n"
"};")
<< QString::fromLatin1("class name\n"
"{\n"
"public:\n"
" name() {}\n"
"};")
<< true
<< (QList<int>() << 6 << 25)
<< (QList<int>() << 4 << 4)
<< (QList<Utils::Id>() << NOMANGLER_ID << NOMANGLER_ID);
QTest::newRow("escape sequences")
<< QString::fromLatin1("class $name$\\n"
"{\\n"
"public\\\\:\\n"
"\\t$name$() {}\\n"
"};")
<< QString::fromLatin1("class name\n"
"{\n"
"public\\:\n"
"\tname() {}\n"
"};")
<< true
<< (QList<int>() << 6 << 23)
<< (QList<int>() << 4 << 4)
<< (QList<Utils::Id>() << NOMANGLER_ID << NOMANGLER_ID);
QTest::newRow("multiline with :") << QString("class $name$\n"
"{\n"
"public:\n"
" $name$() {}\n"
"};")
<< true
<< Parts{
SnippetPart("class "),
SnippetPart("name", 0),
SnippetPart("\n"
"{\n"
"public:\n"
" "),
SnippetPart("name", 0),
SnippetPart("() {}\n"
"};"),
};
QTest::newRow("escape sequences") << QString("class $name$\\n"
"{\\n"
"public\\\\:\\n"
"\\t$name$() {}\\n"
"};")
<< true
<< Parts{
SnippetPart("class "),
SnippetPart("name", 0),
SnippetPart("\n"
"{\n"
"public\\:\n"
"\t"),
SnippetPart("name", 0),
SnippetPart("() {}\n"
"};"),
};
}
void Internal::TextEditorPlugin::testSnippetParsing()
{
QFETCH(QString, input);
QFETCH(QString, text);
QFETCH(bool, success);
QFETCH(QList<int>, ranges_start);
QFETCH(QList<int>, ranges_length);
QFETCH(QList<Utils::Id>, ranges_mangler);
Q_ASSERT(ranges_start.count() == ranges_length.count()); // sanity check for the test data
Q_ASSERT(ranges_start.count() == ranges_mangler.count()); // sanity check for the test data
QFETCH(Parts, parts);
SnippetParseResult result = Snippet::parse(input);
QCOMPARE(Utils::holds_alternative<ParsedSnippet>(result), success);
if (!success)
return;
ParsedSnippet snippet = Utils::get<ParsedSnippet>(result);
QCOMPARE(snippet.text, text);
QCOMPARE(snippet.ranges.count(), ranges_start.count());
for (int i = 0; i < ranges_start.count(); ++i) {
QCOMPARE(snippet.ranges.at(i).start, ranges_start.at(i));
QCOMPARE(snippet.ranges.at(i).length, ranges_length.at(i));
Utils::Id id = NOMANGLER_ID;
if (snippet.ranges.at(i).mangler)
id = snippet.ranges.at(i).mangler->id();
QCOMPARE(id, ranges_mangler.at(i));
}
auto rangesCompare = [&](const ParsedSnippet::Part &actual, const SnippetPart &expected) {
QCOMPARE(actual.text, expected.text);
QCOMPARE(actual.variableIndex, expected.variableIndex);
auto manglerId = actual.mangler ? actual.mangler->id() : NOMANGLER_ID;
QCOMPARE(manglerId, expected.manglerId);
};
for (int i = 0; i < parts.count(); ++i)
rangesCompare(snippet.parts.at(i), parts.at(i));
}
#endif

View File

@@ -49,14 +49,16 @@ public:
class TEXTEDITOR_EXPORT ParsedSnippet
{
public:
QString text;
struct Range {
Range(int s, int l, NameMangler *m) : start(s), length(l), mangler(m) { }
int start;
int length;
NameMangler *mangler;
class Part {
public:
Part() = default;
explicit Part(const QString &text) : text(text) {}
QString text;
int variableIndex = -1; // if variable index is >= 0 the text is interpreted as a variable
NameMangler *mangler = nullptr;
};
QList<Range> ranges;
QList<Part> parts;
QList<QList<int>> variables;
};
class TEXTEDITOR_EXPORT SnippetParseError

View File

@@ -25,88 +25,140 @@
#include "snippetoverlay.h"
#include "snippet.h"
#include <utils/algorithm.h>
#include <utils/qtcassert.h>
namespace TextEditor {
namespace Internal {
void SnippetOverlay::clear()
{
TextEditorOverlay::clear();
m_equivalentSelections.clear();
m_manglers.clear();
m_selections.clear();
m_variables.clear();
}
void SnippetOverlay::mapEquivalentSelections()
void SnippetOverlay::addSnippetSelection(const QTextCursor &cursor,
const QColor &color,
NameMangler *mangler,
int variableIndex)
{
m_equivalentSelections.clear();
m_equivalentSelections.resize(selections().size());
QMultiMap<QString, int> all;
for (int i = 0; i < selections().size(); ++i)
all.insert(selectionText(i).toLower(), i);
const QList<QString> &uniqueKeys = all.uniqueKeys();
foreach (const QString &key, uniqueKeys) {
QList<int> indexes;
const auto cAll = all;
QMultiMap<QString, int>::const_iterator lbit = cAll.lowerBound(key);
QMultiMap<QString, int>::const_iterator ubit = cAll.upperBound(key);
while (lbit != ubit) {
indexes.append(lbit.value());
++lbit;
}
foreach (int index, indexes)
m_equivalentSelections[index] = indexes;
}
m_variables[variableIndex] << selections().size();
SnippetSelection selection;
selection.variableIndex = variableIndex;
selection.mangler = mangler;
m_selections << selection;
addOverlaySelection(cursor, color, color, TextEditorOverlay::ExpandBegin);
}
void SnippetOverlay::updateEquivalentSelections(const QTextCursor &cursor)
{
int selectionIndex = selectionIndexForCursor(cursor);
if (selectionIndex == -1)
const int &currentIndex = indexForCursor(cursor);
if (currentIndex < 0)
return;
const QString &currentText = selectionText(selectionIndex);
const QList<int> &equivalents = m_equivalentSelections.at(selectionIndex);
foreach (int i, equivalents) {
if (i == selectionIndex)
const QString &currentText = cursorForIndex(currentIndex).selectedText();
const QList<int> &equivalents = m_variables.value(m_selections[currentIndex].variableIndex);
for (int i : equivalents) {
if (i == currentIndex)
continue;
const QString &equivalentText = selectionText(i);
QTextCursor cursor = cursorForIndex(i);
const QString &equivalentText = cursor.selectedText();
if (currentText != equivalentText) {
QTextCursor selectionCursor = assembleCursorForSelection(i);
selectionCursor.joinPreviousEditBlock();
selectionCursor.removeSelectedText();
selectionCursor.insertText(currentText);
selectionCursor.endEditBlock();
cursor.joinPreviousEditBlock();
cursor.insertText(currentText);
cursor.endEditBlock();
}
}
}
void SnippetOverlay::setNameMangler(const QList<NameMangler *> &manglers)
{
m_manglers = manglers;
}
void SnippetOverlay::mangle()
{
for (int i = 0; i < m_manglers.count(); ++i) {
if (!m_manglers.at(i))
continue;
const QString current = selectionText(i);
const QString result = m_manglers.at(i)->mangle(current);
if (result != current) {
QTextCursor selectionCursor = assembleCursorForSelection(i);
selectionCursor.joinPreviousEditBlock();
selectionCursor.removeSelectedText();
selectionCursor.insertText(result);
selectionCursor.endEditBlock();
for (int i = 0; i < m_selections.size(); ++i) {
if (NameMangler *mangler = m_selections[i].mangler) {
QTextCursor cursor = cursorForIndex(i);
const QString current = cursor.selectedText();
const QString result = mangler->mangle(current);
if (result != current) {
cursor.joinPreviousEditBlock();
cursor.insertText(result);
cursor.endEditBlock();
}
}
}
}
bool SnippetOverlay::hasCursorInSelection(const QTextCursor &cursor) const
{
return indexForCursor(cursor) >= 0;
}
QTextCursor SnippetOverlay::nextSelectionCursor(const QTextCursor &cursor) const
{
const QList<OverlaySelection> selections = TextEditorOverlay::selections();
if (selections.isEmpty())
return {};
const SnippetSelection &currentSelection = selectionForCursor(cursor);
if (currentSelection.variableIndex >= 0) {
int nextVariableIndex = currentSelection.variableIndex + 1;
if (nextVariableIndex >= m_variables.size())
nextVariableIndex = 0;
for (int selectionIndex : m_variables[nextVariableIndex]) {
if (selections[selectionIndex].m_cursor_begin.position() > cursor.position())
return cursorForIndex(selectionIndex);
}
return cursorForIndex(m_variables[nextVariableIndex].first());
}
// currently not over a variable simply select the next available one
for (const OverlaySelection &candidate : selections) {
if (candidate.m_cursor_begin.position() > cursor.position())
return cursorForSelection(candidate);
}
return cursorForSelection(selections.first());
}
QTextCursor SnippetOverlay::previousSelectionCursor(const QTextCursor &cursor) const
{
const QList<OverlaySelection> selections = TextEditorOverlay::selections();
if (selections.isEmpty())
return {};
const SnippetSelection &currentSelection = selectionForCursor(cursor);
if (currentSelection.variableIndex >= 0) {
int previousVariableIndex = currentSelection.variableIndex - 1;
if (previousVariableIndex < 0)
previousVariableIndex = m_variables.size();
auto equivalents = m_variables[previousVariableIndex].toStdList();
equivalents.reverse();
for (int selectionIndex : equivalents) {
if (selections[selectionIndex].m_cursor_end.position() < cursor.position())
return cursorForIndex(selectionIndex);
}
return cursorForIndex(m_variables[previousVariableIndex].last());
}
// currently not over a variable simply select the previous available one
auto reverse = selections.toStdList();
reverse.reverse();
for (const OverlaySelection &candidate : reverse) {
if (candidate.m_cursor_end.position() < cursor.position())
return cursorForSelection(candidate);
}
return cursorForSelection(selections.last());
}
int SnippetOverlay::indexForCursor(const QTextCursor &cursor) const
{
return Utils::indexOf(selections(),
[pos = cursor.position()](const OverlaySelection &selection) {
return selection.m_cursor_begin.position() <= pos
&& selection.m_cursor_end.position() >= pos;
});
}
SnippetOverlay::SnippetSelection SnippetOverlay::selectionForCursor(const QTextCursor &cursor) const
{
return m_selections.value(indexForCursor(cursor));
}
} // namespace Internal
} // namespace TextEditor

View File

@@ -25,8 +25,11 @@
#pragma once
#include "snippet.h"
#include "texteditor/texteditoroverlay.h"
#include <QTextEdit>
namespace TextEditor {
class NameMangler;
@@ -39,14 +42,30 @@ public:
void clear() override;
void mapEquivalentSelections();
void addSnippetSelection(const QTextCursor &cursor,
const QColor &color,
NameMangler *mangler,
int variableGoup);
void updateEquivalentSelections(const QTextCursor &cursor);
void setNameMangler(const QList<NameMangler *> &manglers);
void mangle();
bool hasCursorInSelection(const QTextCursor &cursor) const;
QTextCursor nextSelectionCursor(const QTextCursor &cursor) const;
QTextCursor previousSelectionCursor(const QTextCursor &cursor) const;
private:
QVector<QList<int> > m_equivalentSelections;
QList<NameMangler *> m_manglers;
struct SnippetSelection
{
int variableIndex = -1;
NameMangler *mangler;
};
int indexForCursor(const QTextCursor &cursor) const;
SnippetSelection selectionForCursor(const QTextCursor &cursor) const;
QList<SnippetSelection> m_selections;
QMap<int, QList<int>> m_variables;
};
} // namespace Internal

View File

@@ -2699,12 +2699,33 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e)
d->m_codeAssistant.process();
}
class PositionedPart : public ParsedSnippet::Part
{
public:
explicit PositionedPart(const ParsedSnippet::Part &part) : ParsedSnippet::Part(part) {}
int start;
int end;
};
class CursorPart : public ParsedSnippet::Part
{
public:
CursorPart(const PositionedPart &part, QTextDocument *doc)
: ParsedSnippet::Part(part)
, cursor(doc)
{
cursor.setPosition(part.start);
cursor.setPosition(part.end, QTextCursor::KeepAnchor);
}
QTextCursor cursor;
};
void TextEditorWidget::insertCodeSnippet(const QTextCursor &cursor_arg, const QString &snippet)
{
SnippetParseResult result = Snippet::parse(snippet);
if (Utils::holds_alternative<SnippetParseError>(result)) {
const auto &error = Utils::get<SnippetParseError>(result);
QMessageBox::warning(this, QLatin1String("Snippet Parse Error"), error.htmlMessage());
QMessageBox::warning(this, tr("Snippet Parse Error"), error.htmlMessage());
return;
}
QTC_ASSERT(Utils::holds_alternative<ParsedSnippet>(result), return);
@@ -2715,44 +2736,47 @@ void TextEditorWidget::insertCodeSnippet(const QTextCursor &cursor_arg, const QS
cursor.removeSelectedText();
const int startCursorPosition = cursor.position();
cursor.insertText(data.text);
QList<QTextEdit::ExtraSelection> selections;
d->m_snippetOverlay->mangle();
d->m_snippetOverlay->clear();
QList<NameMangler *> manglers;
for (int i = 0; i < data.ranges.count(); ++i) {
int position = data.ranges.at(i).start + startCursorPosition;
int length = data.ranges.at(i).length;
QTextCursor tc(document());
tc.setPosition(position);
tc.setPosition(position + length, QTextCursor::KeepAnchor);
QTextEdit::ExtraSelection selection;
selection.cursor = tc;
selection.format = (length
? textDocument()->fontSettings().toTextCharFormat(C_OCCURRENCES)
: textDocument()->fontSettings().toTextCharFormat(C_OCCURRENCES_RENAME));
selections.append(selection);
manglers << data.ranges.at(i).mangler;
QList<PositionedPart> positionedParts;
for (const ParsedSnippet::Part &part : qAsConst(data.parts)) {
if (part.variableIndex >= 0) {
PositionedPart posPart(part);
posPart.start = cursor.position();
cursor.insertText(part.text);
posPart.end = cursor.position();
positionedParts << posPart;
} else {
cursor.insertText(part.text);
}
}
QList<CursorPart> cursorParts = Utils::transform(positionedParts,
[doc = document()](const PositionedPart &part) {
return CursorPart(part, doc);
});
cursor.setPosition(startCursorPosition, QTextCursor::KeepAnchor);
d->m_document->autoIndent(cursor);
cursor.endEditBlock();
setExtraSelections(TextEditorWidget::SnippetPlaceholderSelection, selections);
d->m_snippetOverlay->setNameMangler(manglers);
const QColor &occurrencesColor
= textDocument()->fontSettings().toTextCharFormat(C_OCCURRENCES).background().color();
const QColor &renameColor
= textDocument()->fontSettings().toTextCharFormat(C_OCCURRENCES_RENAME).background().color();
if (!selections.isEmpty()) {
const QTextEdit::ExtraSelection &selection = selections.first();
for (const CursorPart &part : cursorParts) {
const QColor &color = part.cursor.hasSelection() ? occurrencesColor : renameColor;
d->m_snippetOverlay->addSnippetSelection(part.cursor,
color,
part.mangler,
part.variableIndex);
}
cursor = textCursor();
if (selection.cursor.hasSelection()) {
cursor.setPosition(selection.cursor.selectionStart());
cursor.setPosition(selection.cursor.selectionEnd(), QTextCursor::KeepAnchor);
} else {
cursor.setPosition(selection.cursor.position());
}
setTextCursor(cursor);
if (!cursorParts.isEmpty()) {
setTextCursor(cursorParts.first().cursor);
d->m_snippetOverlay->setVisible(true);
}
}
@@ -3461,36 +3485,8 @@ void TextEditorWidgetPrivate::snippetTabOrBacktab(bool forward)
if (!m_snippetOverlay->isVisible() || m_snippetOverlay->isEmpty())
return;
QTextCursor cursor = q->textCursor();
OverlaySelection final;
if (forward) {
for (int i = 0; i < m_snippetOverlay->selections().count(); ++i){
const OverlaySelection &selection = m_snippetOverlay->selections().at(i);
if (selection.m_cursor_begin.position() >= cursor.position()
&& selection.m_cursor_end.position() > cursor.position()) {
final = selection;
break;
}
}
} else {
for (int i = m_snippetOverlay->selections().count()-1; i >= 0; --i){
const OverlaySelection &selection = m_snippetOverlay->selections().at(i);
if (selection.m_cursor_end.position() < cursor.position()) {
final = selection;
break;
}
}
}
if (final.m_cursor_begin.isNull())
final = forward ? m_snippetOverlay->selections().first() : m_snippetOverlay->selections().last();
if (final.m_cursor_begin.position() == final.m_cursor_end.position()) { // empty tab stop
cursor.setPosition(final.m_cursor_end.position());
} else {
cursor.setPosition(final.m_cursor_begin.position());
cursor.setPosition(final.m_cursor_end.position(), QTextCursor::KeepAnchor);
}
q->setTextCursor(cursor);
q->setTextCursor(forward ? m_snippetOverlay->nextSelectionCursor(cursor)
: m_snippetOverlay->previousSelectionCursor(cursor));
}
// Calculate global position for a tooltip considering the left extra area.
@@ -7082,17 +7078,6 @@ void TextEditorWidgetPrivate::setExtraSelections(Id kind, const QList<QTextEdit:
TextEditorOverlay::LockSize);
}
m_overlay->setVisible(!m_overlay->isEmpty());
} else if (kind == TextEditorWidget::SnippetPlaceholderSelection) {
m_snippetOverlay->mangle();
m_snippetOverlay->clear();
foreach (const QTextEdit::ExtraSelection &selection, m_extraSelections[kind]) {
m_snippetOverlay->addOverlaySelection(selection.cursor,
selection.format.background().color(),
selection.format.background().color(),
TextEditorOverlay::ExpandBegin);
}
m_snippetOverlay->mapEquivalentSelections();
m_snippetOverlay->setVisible(!m_snippetOverlay->isEmpty());
} else {
QList<QTextEdit::ExtraSelection> all;
for (auto i = m_extraSelections.constBegin(); i != m_extraSelections.constEnd(); ++i) {

View File

@@ -33,6 +33,7 @@
#include <QTextBlock>
#include <algorithm>
#include <utils/qtcassert.h>
using namespace TextEditor;
using namespace TextEditor::Internal;
@@ -416,6 +417,21 @@ void TextEditorOverlay::paint(QPainter *painter, const QRect &clip)
}
}
QTextCursor TextEditorOverlay::cursorForSelection(const OverlaySelection &selection) const
{
QTextCursor cursor = selection.m_cursor_begin;
cursor.setPosition(selection.m_cursor_begin.position());
cursor.setKeepPositionOnInsert(false);
if (!cursor.isNull())
cursor.setPosition(selection.m_cursor_end.position(), QTextCursor::KeepAnchor);
return cursor;
}
QTextCursor TextEditorOverlay::cursorForIndex(int selectionIndex) const
{
return cursorForSelection(m_selections.value(selectionIndex));
}
void TextEditorOverlay::fill(QPainter *painter, const QColor &color, const QRect &clip)
{
Q_UNUSED(clip)
@@ -443,41 +459,6 @@ void TextEditorOverlay::fill(QPainter *painter, const QColor &color, const QRect
}
}
/*! \returns true if any selection contains \a cursor, where a cursor on the
start or end of a selection is counted as contained.
*/
bool TextEditorOverlay::hasCursorInSelection(const QTextCursor &cursor) const
{
if (selectionIndexForCursor(cursor) != -1)
return true;
return false;
}
int TextEditorOverlay::selectionIndexForCursor(const QTextCursor &cursor) const
{
for (int i = 0; i < m_selections.size(); ++i) {
const OverlaySelection &selection = m_selections.at(i);
if (cursor.position() >= selection.m_cursor_begin.position()
&& cursor.position() <= selection.m_cursor_end.position())
return i;
}
return -1;
}
QString TextEditorOverlay::selectionText(int selectionIndex) const
{
return assembleCursorForSelection(selectionIndex).selectedText();
}
QTextCursor TextEditorOverlay::assembleCursorForSelection(int selectionIndex) const
{
const OverlaySelection &selection = m_selections.at(selectionIndex);
QTextCursor cursor(m_editor->document());
cursor.setPosition(selection.m_cursor_begin.position());
cursor.setPosition(selection.m_cursor_end.position(), QTextCursor::KeepAnchor);
return cursor;
}
bool TextEditorOverlay::hasFirstSelectionBeginMoved() const
{
if (m_firstSelectionOriginalBegin == -1 || m_selections.isEmpty())

View File

@@ -92,14 +92,11 @@ public:
inline int dropShadowWidth() const { return m_dropShadowWidth; }
bool hasCursorInSelection(const QTextCursor &cursor) const;
bool hasFirstSelectionBeginMoved() const;
protected:
int selectionIndexForCursor(const QTextCursor &cursor) const;
QString selectionText(int selectionIndex) const;
QTextCursor assembleCursorForSelection(int selectionIndex) const;
QTextCursor cursorForSelection(const OverlaySelection &selection) const;
QTextCursor cursorForIndex(int selectionIndex) const;
private:
QPainterPath createSelectionPath(const QTextCursor &begin, const QTextCursor &end, const QRect& clip);