forked from qt-creator/qt-creator
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:
@@ -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>"},
|
||||
{' ', " "},
|
||||
{'"', """},
|
||||
@@ -205,34 +181,30 @@ QString Snippet::generateTip() const
|
||||
{'<', "<"},
|
||||
{'>', ">"}};
|
||||
|
||||
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 ¤tChar = 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ¤tIndex = indexForCursor(cursor);
|
||||
if (currentIndex < 0)
|
||||
return;
|
||||
|
||||
const QString ¤tText = selectionText(selectionIndex);
|
||||
const QList<int> &equivalents = m_equivalentSelections.at(selectionIndex);
|
||||
foreach (int i, equivalents) {
|
||||
if (i == selectionIndex)
|
||||
const QString ¤tText = 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 ¤tSelection = 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 ¤tSelection = 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user