diff options
Diffstat (limited to 'src/plugins/texteditor')
34 files changed, 798 insertions, 546 deletions
diff --git a/src/plugins/texteditor/CMakeLists.txt b/src/plugins/texteditor/CMakeLists.txt index 93d773ddb5..caaa4846b7 100644 --- a/src/plugins/texteditor/CMakeLists.txt +++ b/src/plugins/texteditor/CMakeLists.txt @@ -81,6 +81,8 @@ add_qtc_plugin(TextEditor snippets/snippet.cpp snippets/snippet.h snippets/snippetassistcollector.cpp snippets/snippetassistcollector.h snippets/snippeteditor.cpp snippets/snippeteditor.h + snippets/snippetoverlay.cpp snippets/snippetoverlay.h + snippets/snippetparser.cpp snippets/snippetparser.h snippets/snippetprovider.cpp snippets/snippetprovider.h snippets/snippetscollection.cpp snippets/snippetscollection.h snippets/snippetssettings.cpp snippets/snippetssettings.h diff --git a/src/plugins/texteditor/codeassist/assistproposalitem.cpp b/src/plugins/texteditor/codeassist/assistproposalitem.cpp index cf9cbe6e38..6542c1580a 100644 --- a/src/plugins/texteditor/codeassist/assistproposalitem.cpp +++ b/src/plugins/texteditor/codeassist/assistproposalitem.cpp @@ -25,8 +25,9 @@ #include "assistproposalitem.h" -#include <texteditor/texteditor.h> #include <texteditor/quickfix.h> +#include <texteditor/snippets/snippet.h> +#include <texteditor/texteditor.h> #include <QTextCursor> @@ -147,7 +148,7 @@ void AssistProposalItem::applyContextualContent(TextDocumentManipulatorInterface void AssistProposalItem::applySnippet(TextDocumentManipulatorInterface &manipulator, int basePosition) const { - manipulator.insertCodeSnippet(basePosition, data().toString()); + manipulator.insertCodeSnippet(basePosition, data().toString(), &Snippet::parse); } void AssistProposalItem::applyQuickFix(TextDocumentManipulatorInterface &manipulator, int basePosition) const diff --git a/src/plugins/texteditor/codeassist/textdocumentmanipulator.cpp b/src/plugins/texteditor/codeassist/textdocumentmanipulator.cpp index 0eee5793db..268d9494a0 100644 --- a/src/plugins/texteditor/codeassist/textdocumentmanipulator.cpp +++ b/src/plugins/texteditor/codeassist/textdocumentmanipulator.cpp @@ -85,11 +85,13 @@ bool TextDocumentManipulator::replace(int position, int length, const QString &t return textWillBeReplaced; } -void TextDocumentManipulator::insertCodeSnippet(int position, const QString &text) +void TextDocumentManipulator::insertCodeSnippet(int position, + const QString &text, + const SnippetParser &parse) { auto cursor = m_textEditorWidget->textCursor(); cursor.setPosition(position, QTextCursor::KeepAnchor); - m_textEditorWidget->insertCodeSnippet(cursor, text); + m_textEditorWidget->insertCodeSnippet(cursor, text, parse); } void TextDocumentManipulator::paste() diff --git a/src/plugins/texteditor/codeassist/textdocumentmanipulator.h b/src/plugins/texteditor/codeassist/textdocumentmanipulator.h index 46aa99f29d..23c7186d60 100644 --- a/src/plugins/texteditor/codeassist/textdocumentmanipulator.h +++ b/src/plugins/texteditor/codeassist/textdocumentmanipulator.h @@ -45,7 +45,7 @@ public: void setCursorPosition(int position) final; void setAutoCompleteSkipPosition(int position) final; bool replace(int position, int length, const QString &text) final; - void insertCodeSnippet(int position, const QString &text) final; + void insertCodeSnippet(int position, const QString &text, const SnippetParser &parse) final; void paste() final; void encourageApply() final; void autoIndent(int position, int length) override; diff --git a/src/plugins/texteditor/codeassist/textdocumentmanipulatorinterface.h b/src/plugins/texteditor/codeassist/textdocumentmanipulatorinterface.h index ae803260c7..88404713ef 100644 --- a/src/plugins/texteditor/codeassist/textdocumentmanipulatorinterface.h +++ b/src/plugins/texteditor/codeassist/textdocumentmanipulatorinterface.h @@ -25,6 +25,7 @@ #pragma once +#include <texteditor/snippets/snippetparser.h> #include <texteditor/texteditor_global.h> QT_BEGIN_NAMESPACE @@ -49,7 +50,9 @@ public: virtual void setCursorPosition(int position) = 0; virtual void setAutoCompleteSkipPosition(int position) = 0; virtual bool replace(int position, int length, const QString &text) = 0; - virtual void insertCodeSnippet(int position, const QString &text) = 0; + virtual void insertCodeSnippet(int position, + const QString &text, + const SnippetParser &parse) = 0; virtual void paste() = 0; virtual void encourageApply() = 0; virtual void autoIndent(int position, int length) = 0; diff --git a/src/plugins/texteditor/codestylepool.cpp b/src/plugins/texteditor/codestylepool.cpp index 55377eceae..e92592b348 100644 --- a/src/plugins/texteditor/codestylepool.cpp +++ b/src/plugins/texteditor/codestylepool.cpp @@ -94,9 +94,7 @@ QByteArray CodeStylePoolPrivate::generateUniqueId(const QByteArray &id) const static QString customCodeStylesPath() { - QString path = Core::ICore::userResourcePath(); - path.append(QLatin1String("/codestyles/")); - return path; + return Core::ICore::userResourcePath("codestyles").toString(); } CodeStylePool::CodeStylePool(ICodeStylePreferencesFactory *factory, QObject *parent) diff --git a/src/plugins/texteditor/colorscheme.cpp b/src/plugins/texteditor/colorscheme.cpp index d032f3e37b..69d8349734 100644 --- a/src/plugins/texteditor/colorscheme.cpp +++ b/src/plugins/texteditor/colorscheme.cpp @@ -238,7 +238,7 @@ void ColorScheme::clear() bool ColorScheme::save(const QString &fileName, QWidget *parent) const { - Utils::FileSaver saver(fileName); + Utils::FileSaver saver(Utils::FilePath::fromString(fileName)); if (!saver.hasError()) { QXmlStreamWriter w(saver.file()); w.setAutoFormatting(true); diff --git a/src/plugins/texteditor/fontsettings.cpp b/src/plugins/texteditor/fontsettings.cpp index d74a13b2cc..e35ed5c3b2 100644 --- a/src/plugins/texteditor/fontsettings.cpp +++ b/src/plugins/texteditor/fontsettings.cpp @@ -504,20 +504,19 @@ int FontSettings::defaultFontSize() */ QString FontSettings::defaultSchemeFileName(const QString &fileName) { - QString defaultScheme = Core::ICore::resourcePath(); - defaultScheme += QLatin1String("/styles/"); + Utils::FilePath defaultScheme = Core::ICore::resourcePath("styles"); - if (!fileName.isEmpty() && QFile::exists(defaultScheme + fileName)) { - defaultScheme += fileName; + if (!fileName.isEmpty() && (defaultScheme / fileName).exists()) { + defaultScheme = defaultScheme / fileName; } else { const QString themeScheme = Utils::creatorTheme()->defaultTextEditorColorScheme(); - if (!themeScheme.isEmpty() && QFile::exists(defaultScheme + themeScheme)) - defaultScheme += themeScheme; + if (!themeScheme.isEmpty() && (defaultScheme / themeScheme).exists()) + defaultScheme = defaultScheme / themeScheme; else - defaultScheme += QLatin1String("default.xml"); + defaultScheme = defaultScheme / "default.xml"; } - return defaultScheme; + return defaultScheme.toString(); } } // namespace TextEditor diff --git a/src/plugins/texteditor/fontsettingspage.cpp b/src/plugins/texteditor/fontsettingspage.cpp index 42c14549c4..2487e31fff 100644 --- a/src/plugins/texteditor/fontsettingspage.cpp +++ b/src/plugins/texteditor/fontsettingspage.cpp @@ -191,9 +191,7 @@ public: static QString customStylesPath() { - QString path = Core::ICore::userResourcePath(); - path.append(QLatin1String("/styles/")); - return path; + return Core::ICore::userResourcePath("styles").toString(); } static QString createColorSchemeFileName(const QString &pattern) @@ -558,8 +556,7 @@ void FontSettingsPageWidget::refreshColorSchemeList() { QList<ColorSchemeEntry> colorSchemes; - QString resourcePath = Core::ICore::resourcePath(); - QDir styleDir(resourcePath + QLatin1String("/styles")); + QDir styleDir(Core::ICore::resourcePath("styles").toDir()); styleDir.setNameFilters(QStringList() << QLatin1String("*.xml")); styleDir.setFilter(QDir::Files); diff --git a/src/plugins/texteditor/formattexteditor.cpp b/src/plugins/texteditor/formattexteditor.cpp index 54655de390..57b30d39f2 100644 --- a/src/plugins/texteditor/formattexteditor.cpp +++ b/src/plugins/texteditor/formattexteditor.cpp @@ -32,11 +32,11 @@ #include <coreplugin/messagemanager.h> #include <utils/differ.h> +#include <utils/qtcassert.h> +#include <utils/qtcprocess.h> #include <utils/runextensions.h> -#include <utils/synchronousprocess.h> #include <utils/temporarydirectory.h> #include <utils/textutils.h> -#include <utils/qtcassert.h> #include <QFileInfo> #include <QFutureWatcher> @@ -81,30 +81,31 @@ static FormatTask format(FormatTask task) if (!sourceFile.finalize()) { task.error = QString(QT_TRANSLATE_NOOP("TextEditor", "Cannot create temporary file \"%1\": %2.")) - .arg(sourceFile.fileName(), sourceFile.errorString()); + .arg(sourceFile.filePath().toUserOutput(), sourceFile.errorString()); return task; } // Format temporary file QStringList options = task.command.options(); - options.replaceInStrings(QLatin1String("%file"), sourceFile.fileName()); - Utils::SynchronousProcess process; + options.replaceInStrings(QLatin1String("%file"), sourceFile.filePath().toString()); + SynchronousProcess process; process.setTimeoutS(5); - Utils::SynchronousProcessResponse response = process.runBlocking({executable, options}); - if (response.result != Utils::SynchronousProcessResponse::Finished) { + process.setCommand({executable, options}); + process.runBlocking(); + if (process.result() != QtcProcess::Finished) { task.error = QString(QT_TRANSLATE_NOOP("TextEditor", "Failed to format: %1.")) - .arg(response.exitMessage(executable, 5)); + .arg(process.exitMessage()); return task; } - const QString output = response.stdErr(); + const QString output = process.stdErr(); if (!output.isEmpty()) task.error = executable + QLatin1String(": ") + output; // Read text back Utils::FileReader reader; - if (!reader.fetch(sourceFile.fileName(), QIODevice::Text)) { + if (!reader.fetch(sourceFile.filePath(), QIODevice::Text)) { task.error = QString(QT_TRANSLATE_NOOP("TextEditor", "Cannot read file \"%1\": %2.")) - .arg(sourceFile.fileName(), reader.errorString()); + .arg(sourceFile.filePath().toUserOutput(), reader.errorString()); return task; } task.formattedData = QString::fromUtf8(reader.data()); diff --git a/src/plugins/texteditor/highlighter.cpp b/src/plugins/texteditor/highlighter.cpp index 51e1a307f1..2a940b3f73 100644 --- a/src/plugins/texteditor/highlighter.cpp +++ b/src/plugins/texteditor/highlighter.cpp @@ -59,7 +59,7 @@ KSyntaxHighlighting::Repository *highlightRepository() if (!repository) { repository = new KSyntaxHighlighting::Repository(); repository->addCustomSearchPath(TextEditorSettings::highlighterSettings().definitionFilesPath()); - QDir dir(Core::ICore::resourcePath() + QLatin1String("/generic-highlighter/syntax")); + QDir dir(Core::ICore::resourcePath("generic-highlighter/syntax").toDir()); if (dir.exists() && dir.cdUp()) repository->addCustomSearchPath(dir.path()); } diff --git a/src/plugins/texteditor/highlightersettings.cpp b/src/plugins/texteditor/highlightersettings.cpp index bb224f6345..f954be4996 100644 --- a/src/plugins/texteditor/highlightersettings.cpp +++ b/src/plugins/texteditor/highlightersettings.cpp @@ -31,8 +31,8 @@ #include <utils/fileutils.h> #include <utils/hostosinfo.h> +#include <utils/qtcprocess.h> #include <utils/stringutils.h> -#include <utils/synchronousprocess.h> #include <QSettings> #include <QLatin1String> @@ -74,10 +74,10 @@ QString findFallbackDefinitionsLocation() for (auto &program : programs) { Utils::SynchronousProcess process; process.setTimeoutS(5); - Utils::SynchronousProcessResponse response - = process.runBlocking({program, {"--prefix"}}); - if (response.result == Utils::SynchronousProcessResponse::Finished) { - QString output = response.stdOut(); + process.setCommand({program, {"--prefix"}}); + process.runBlocking(); + if (process.result() == Utils::QtcProcess::Finished) { + QString output = process.stdOut(); output.remove(QLatin1Char('\n')); for (auto &kateSyntaxPath : kateSyntaxPaths) { dir.setPath(output + kateSyntaxPath); @@ -88,7 +88,7 @@ QString findFallbackDefinitionsLocation() } } - dir.setPath(Core::ICore::resourcePath() + QLatin1String("/generic-highlighter")); + dir.setPath(Core::ICore::resourcePath("generic-highlighter").toString()); if (dir.exists() && !dir.entryInfoList().isEmpty()) return dir.path(); @@ -165,8 +165,7 @@ void HighlighterSettings::assignDefaultIgnoredPatterns() void HighlighterSettings::assignDefaultDefinitionsPath() { - const QString &path = - Core::ICore::userResourcePath() + QLatin1String("/generic-highlighter"); + const QString path = Core::ICore::userResourcePath("generic-highlighter").toString(); if (QFile::exists(path) || QDir().mkpath(path)) m_definitionFilesPath = path; } diff --git a/src/plugins/texteditor/linenumberfilter.cpp b/src/plugins/texteditor/linenumberfilter.cpp index dc1f6298c2..7695bbd39d 100644 --- a/src/plugins/texteditor/linenumberfilter.cpp +++ b/src/plugins/texteditor/linenumberfilter.cpp @@ -48,6 +48,7 @@ LineNumberFilter::LineNumberFilter(QObject *parent) { setId("Line in current document"); setDisplayName(tr("Line in Current Document")); + setDescription(tr("Jumps to the given line in the current document.")); setPriority(High); setDefaultShortcutString("l"); setDefaultIncludedByDefault(true); diff --git a/src/plugins/texteditor/outlinefactory.cpp b/src/plugins/texteditor/outlinefactory.cpp index 7c7e644b18..421479ab7a 100644 --- a/src/plugins/texteditor/outlinefactory.cpp +++ b/src/plugins/texteditor/outlinefactory.cpp @@ -110,27 +110,17 @@ OutlineWidgetStack::OutlineWidgetStack(OutlineFactory *factory) : updateCurrentEditor(); } -OutlineWidgetStack::~OutlineWidgetStack() = default; - -QToolButton *OutlineWidgetStack::toggleSyncButton() -{ - return m_toggleSync; -} - -QToolButton *OutlineWidgetStack::filterButton() +QList<QToolButton *> OutlineWidgetStack::toolButtons() { - return m_filterButton; + return {m_filterButton, m_toggleSort, m_toggleSync}; } -QToolButton *OutlineWidgetStack::sortButton() -{ - return m_toggleSort; -} +OutlineWidgetStack::~OutlineWidgetStack() = default; void OutlineWidgetStack::saveSettings(QSettings *settings, int position) { const QString baseKey = QStringLiteral("Outline.%1.").arg(position); - settings->setValue(baseKey + QLatin1String("SyncWithEditor"), toggleSyncButton()->isChecked()); + settings->setValue(baseKey + QLatin1String("SyncWithEditor"), m_toggleSync->isChecked()); for (auto iter = m_widgetSettings.constBegin(); iter != m_widgetSettings.constEnd(); ++iter) settings->setValue(baseKey + iter.key(), iter.value()); } @@ -154,7 +144,7 @@ void OutlineWidgetStack::restoreSettings(QSettings *settings, int position) m_widgetSettings.insert(key, settings->value(longKey)); } - toggleSyncButton()->setChecked(syncWithEditor); + m_toggleSync->setChecked(syncWithEditor); if (auto outlineWidget = qobject_cast<IOutlineWidget*>(currentWidget())) outlineWidget->restoreSettings(m_widgetSettings); } @@ -241,13 +231,8 @@ OutlineFactory::OutlineFactory() Core::NavigationView OutlineFactory::createWidget() { - Core::NavigationView n; auto placeHolder = new OutlineWidgetStack(this); - n.widget = placeHolder; - n.dockToolBarWidgets.append(placeHolder->filterButton()); - n.dockToolBarWidgets.append(placeHolder->sortButton()); - n.dockToolBarWidgets.append(placeHolder->toggleSyncButton()); - return n; + return {placeHolder, placeHolder->toolButtons()}; } void OutlineFactory::saveSettings(Utils::QtcSettings *settings, int position, QWidget *widget) diff --git a/src/plugins/texteditor/outlinefactory.h b/src/plugins/texteditor/outlinefactory.h index 0f72bcf6e5..d231b700de 100644 --- a/src/plugins/texteditor/outlinefactory.h +++ b/src/plugins/texteditor/outlinefactory.h @@ -44,9 +44,7 @@ public: OutlineWidgetStack(OutlineFactory *factory); ~OutlineWidgetStack() override; - QToolButton *toggleSyncButton(); - QToolButton *filterButton(); - QToolButton *sortButton(); + QList<QToolButton *> toolButtons(); void saveSettings(QSettings *settings, int position); void restoreSettings(QSettings *settings, int position); diff --git a/src/plugins/texteditor/refactoringchanges.cpp b/src/plugins/texteditor/refactoringchanges.cpp index 9732972b01..b936066b1a 100644 --- a/src/plugins/texteditor/refactoringchanges.cpp +++ b/src/plugins/texteditor/refactoringchanges.cpp @@ -98,7 +98,7 @@ bool RefactoringChanges::createFile(const QString &fileName, const QString &cont TextFileFormat format; format.codec = EditorManager::defaultTextCodec(); QString error; - bool saveOk = format.writeFile(fileName, document->toPlainText(), &error); + bool saveOk = format.writeFile(Utils::FilePath::fromString(fileName), document->toPlainText(), &error); delete document; if (!saveOk) return false; @@ -198,10 +198,12 @@ QTextDocument *RefactoringFile::mutableDocument() const if (!m_fileName.isEmpty()) { QString error; QTextCodec *defaultCodec = EditorManager::defaultTextCodec(); - TextFileFormat::ReadResult result = TextFileFormat::readFile( - m_fileName, defaultCodec, - &fileContents, &m_textFileFormat, - &error); + TextFileFormat::ReadResult result = TextFileFormat::readFile(FilePath::fromString( + m_fileName), + defaultCodec, + &fileContents, + &m_textFileFormat, + &error); if (result != TextFileFormat::ReadSuccess) { qWarning() << "Could not read " << m_fileName << ". Error: " << error; m_textFileFormat.codec = nullptr; @@ -372,7 +374,9 @@ bool RefactoringFile::apply() QString error; // suppress "file has changed" warnings if the file is open in a read-only editor Core::FileChangeBlocker block(m_fileName); - if (!m_textFileFormat.writeFile(m_fileName, doc->toPlainText(), &error)) { + if (!m_textFileFormat.writeFile(FilePath::fromString(m_fileName), + doc->toPlainText(), + &error)) { qWarning() << "Could not apply changes to" << m_fileName << ". Error: " << error; result = false; } diff --git a/src/plugins/texteditor/snippets/snippet.cpp b/src/plugins/texteditor/snippets/snippet.cpp index 986655ba4e..38b481b16f 100644 --- a/src/plugins/texteditor/snippets/snippet.cpp +++ b/src/plugins/texteditor/snippets/snippet.cpp @@ -154,34 +154,10 @@ bool Snippet::isModified() const return m_isModified; } -struct SnippetReplacement -{ - QString text; - int posDelta = 0; -}; - -static SnippetReplacement replacementAt(int pos, Snippet::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 Snippet::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) { - Snippet::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>"}, {' ', " "}, {'"', """}, @@ -189,116 +165,128 @@ QString Snippet::generateTip() const {'<', "<"}, {'>', ">"}}; - ParsedSnippet parsedSnippet = Snippet::parse(m_content); + QString text; + text.reserve(part.text.size()); - 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); - } + 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 {}); + const ParsedSnippet parsedSnippet = Utils::get<ParsedSnippet>(result); - QTC_CHECK(parsedSnippet.ranges.isEmpty()); + QString tip("<nobr>"); + for (const ParsedSnippet::Part &part : parsedSnippet.parts) + tip.append(tipPart(part)); return tip; } -Snippet::ParsedSnippet Snippet::parse(const QString &snippet) +SnippetParseResult Snippet::parse(const QString &snippet) { static UppercaseMangler ucMangler; static LowercaseMangler lcMangler; static TitlecaseMangler tcMangler; - Snippet::ParsedSnippet result; + ParsedSnippet result; QString errorMessage; QString preprocessedSnippet = Utils::TemplateEngine::processText(Utils::globalMacroExpander(), snippet, &errorMessage); - result.success = errorMessage.isEmpty(); - if (!result.success) { - result.text = snippet; - result.errorMessage = errorMessage; - return result; - } + if (!errorMessage.isEmpty()) + return {SnippetParseError{errorMessage, {}, -1}}; const int count = preprocessedSnippet.count(); - bool success = true; - 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; } if (mangler) { - success = false; - break; + return SnippetParseResult{SnippetParseError{tr("Expected delimiter after mangler id"), + preprocessedSnippet, + 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 { - success = false; - break; + return SnippetParseResult{ + SnippetParseError{tr("Expected mangler id 'l'(lowercase), 'u'(uppercase), " + "or 'c'(titlecase) after colon"), + preprocessedSnippet, + i}}; } ++i; 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) - success = false; - - result.success = success; - - if (!success) { - result.ranges.clear(); - result.text = preprocessedSnippet; + if (inVar) { + return SnippetParseResult{ + SnippetParseError{tr("Missing closing variable delimiter for:"), currentPart.text, 0}}; } - return result; + if (!currentPart.text.isEmpty()) + result.parts << currentPart; + + return SnippetParseResult(result); } #ifdef WITH_TESTS @@ -308,129 +296,130 @@ Snippet::ParsedSnippet 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::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::addColumn<Parts>("parts"); + + 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>()); - - 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("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("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); - + << QString::fromLatin1("\\\\$test\\\\\\\\$\\\\") << false << Parts(); + + 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("$test") << false << Parts(); + QTest::newRow("wrong mangler") << QString("$test:X$") << false << Parts(); + + 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 - - Snippet::ParsedSnippet result = Snippet::parse(input); - - QCOMPARE(result.text, text); - QCOMPARE(result.success, success); - QCOMPARE(result.ranges.count(), ranges_start.count()); - for (int i = 0; i < ranges_start.count(); ++i) { - QCOMPARE(result.ranges.at(i).start, ranges_start.at(i)); - QCOMPARE(result.ranges.at(i).length, ranges_length.at(i)); - Utils::Id id = NOMANGLER_ID; - if (result.ranges.at(i).mangler) - id = result.ranges.at(i).mangler->id(); - QCOMPARE(id, ranges_mangler.at(i)); - } + QFETCH(Parts, parts); + + SnippetParseResult result = Snippet::parse(input); + QCOMPARE(Utils::holds_alternative<ParsedSnippet>(result), success); + if (!success) + return; + + ParsedSnippet snippet = Utils::get<ParsedSnippet>(result); + + 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 - diff --git a/src/plugins/texteditor/snippets/snippet.h b/src/plugins/texteditor/snippets/snippet.h index 4689f6af52..a75b3a297e 100644 --- a/src/plugins/texteditor/snippets/snippet.h +++ b/src/plugins/texteditor/snippets/snippet.h @@ -27,25 +27,18 @@ #include <texteditor/texteditor_global.h> -#include <utils/id.h> +#include "snippetparser.h" #include <QChar> +#include <QCoreApplication> #include <QList> #include <QString> namespace TextEditor { -class TEXTEDITOR_EXPORT NameMangler -{ -public: - virtual ~NameMangler(); - - virtual Utils::Id id() const = 0; - virtual QString mangle(const QString &unmangled) const = 0; -}; - class TEXTEDITOR_EXPORT Snippet { + Q_DECLARE_TR_FUNCTIONS(Snippet) public: explicit Snippet(const QString &groupId = QString(), const QString &id = QString()); ~Snippet(); @@ -76,21 +69,7 @@ public: static const QChar kVariableDelimiter; static const QChar kEscapeChar; - class ParsedSnippet { - public: - QString text; - QString errorMessage; - bool success; - struct Range { - Range(int s, int l, NameMangler *m) : start(s), length(l), mangler(m) { } - int start; - int length; - NameMangler *mangler; - }; - QList<Range> ranges; - }; - - static ParsedSnippet parse(const QString &snippet); + static SnippetParseResult parse(const QString &snippet); private: bool m_isRemoved = false; diff --git a/src/plugins/texteditor/snippets/snippetoverlay.cpp b/src/plugins/texteditor/snippets/snippetoverlay.cpp new file mode 100644 index 0000000000..b773acfee1 --- /dev/null +++ b/src/plugins/texteditor/snippets/snippetoverlay.cpp @@ -0,0 +1,181 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "snippetoverlay.h" + +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + +namespace TextEditor { +namespace Internal { + +void SnippetOverlay::clear() +{ + TextEditorOverlay::clear(); + m_selections.clear(); + m_variables.clear(); +} + +void SnippetOverlay::addSnippetSelection(const QTextCursor &cursor, + const QColor &color, + NameMangler *mangler, + int variableIndex) +{ + m_variables[variableIndex] << selections().size(); + SnippetSelection selection; + selection.variableIndex = variableIndex; + selection.mangler = mangler; + m_selections << selection; + addOverlaySelection(cursor, color, color, TextEditorOverlay::ExpandBegin); +} + +void SnippetOverlay::setFinalSelection(const QTextCursor &cursor, const QColor &color) +{ + m_finalSelectionIndex = selections().size(); + addOverlaySelection(cursor, color, color, TextEditorOverlay::ExpandBegin); +} + +void SnippetOverlay::updateEquivalentSelections(const QTextCursor &cursor) +{ + const int ¤tIndex = indexForCursor(cursor); + if (currentIndex < 0) + return; + 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; + QTextCursor cursor = cursorForIndex(i); + const QString &equivalentText = cursor.selectedText(); + if (currentText != equivalentText) { + cursor.joinPreviousEditBlock(); + cursor.insertText(currentText); + cursor.endEditBlock(); + } + } +} + +void SnippetOverlay::mangle() +{ + 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::firstSelectionCursor() const +{ + const QList<OverlaySelection> selections = TextEditorOverlay::selections(); + return selections.isEmpty() ? QTextCursor() : cursorForSelection(selections.first()); +} + +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()) { + if (m_finalSelectionIndex >= 0) + return cursorForIndex(m_finalSelectionIndex); + 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() - 1; + + const QList<int> &equivalents = m_variables[previousVariableIndex]; + for (int i = equivalents.size() - 1; i >= 0; --i) { + if (selections.at(equivalents.at(i)).m_cursor_end.position() < cursor.position()) + return cursorForIndex(equivalents.at(i)); + } + return cursorForIndex(m_variables[previousVariableIndex].last()); + } + // currently not over a variable simply select the previous available one + for (int i = selections.size() - 1; i >= 0; --i) { + if (selections.at(i).m_cursor_end.position() < cursor.position()) + return cursorForIndex(i); + } + return cursorForSelection(selections.last()); +} + +bool SnippetOverlay::isFinalSelection(const QTextCursor &cursor) const +{ + return m_finalSelectionIndex >= 0 ? cursor == cursorForIndex(m_finalSelectionIndex) : false; +} + +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 diff --git a/src/plugins/texteditor/snippets/snippetoverlay.h b/src/plugins/texteditor/snippets/snippetoverlay.h new file mode 100644 index 0000000000..9bf80524d6 --- /dev/null +++ b/src/plugins/texteditor/snippets/snippetoverlay.h @@ -0,0 +1,78 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "snippet.h" +#include "texteditor/texteditoroverlay.h" + +#include <QTextEdit> + +namespace TextEditor { +class NameMangler; + +namespace Internal { + +class SnippetOverlay : public TextEditorOverlay +{ +public: + using TextEditorOverlay::TextEditorOverlay; + + void clear() override; + + void addSnippetSelection(const QTextCursor &cursor, + const QColor &color, + NameMangler *mangler, + int variableGoup); + void setFinalSelection(const QTextCursor &cursor, const QColor &color); + + void updateEquivalentSelections(const QTextCursor &cursor); + void mangle(); + + bool hasCursorInSelection(const QTextCursor &cursor) const; + + QTextCursor firstSelectionCursor() const; + QTextCursor nextSelectionCursor(const QTextCursor &cursor) const; + QTextCursor previousSelectionCursor(const QTextCursor &cursor) const; + bool isFinalSelection(const QTextCursor &cursor) const; + +private: + struct SnippetSelection + { + int variableIndex = -1; + NameMangler *mangler; + }; + + int indexForCursor(const QTextCursor &cursor) const; + SnippetSelection selectionForCursor(const QTextCursor &cursor) const; + + QList<SnippetSelection> m_selections; + int m_finalSelectionIndex = -1; + QMap<int, QList<int>> m_variables; +}; + +} // namespace Internal +} // namespace TextEditor + diff --git a/src/plugins/texteditor/snippets/snippetparser.cpp b/src/plugins/texteditor/snippets/snippetparser.cpp new file mode 100644 index 0000000000..366489120d --- /dev/null +++ b/src/plugins/texteditor/snippets/snippetparser.cpp @@ -0,0 +1,46 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "snippetparser.h" + +namespace TextEditor { + +QString SnippetParseError::htmlMessage() const +{ + QString message = errorMessage; + if (pos < 0 || pos > 50) + return message; + QString detail = text.left(50); + if (detail != text) + detail.append("..."); + detail.replace(QChar::Space, " "); + message.append("<br><code>" + detail + "<br>"); + for (int i = 0; i < pos; ++i) + message.append(" "); + message.append("^</code>"); + return message; +} + +} // namespace TextEditor diff --git a/src/plugins/texteditor/snippets/snippetparser.h b/src/plugins/texteditor/snippets/snippetparser.h new file mode 100644 index 0000000000..67d0560d02 --- /dev/null +++ b/src/plugins/texteditor/snippets/snippetparser.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <texteditor/texteditor_global.h> + +#include <utils/id.h> +#include <utils/variant.h> + +namespace TextEditor { + +class TEXTEDITOR_EXPORT NameMangler +{ +public: + virtual ~NameMangler(); + + virtual Utils::Id id() const = 0; + virtual QString mangle(const QString &unmangled) const = 0; +}; + +class TEXTEDITOR_EXPORT ParsedSnippet +{ +public: + 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; + bool finalPart = false; + }; + QList<Part> parts; + QList<QList<int>> variables; +}; + +class TEXTEDITOR_EXPORT SnippetParseError +{ +public: + QString errorMessage; + QString text; + int pos; + + QString htmlMessage() const; +}; + +using SnippetParseResult = Utils::variant<ParsedSnippet, SnippetParseError>; +using SnippetParser = std::function<SnippetParseResult (const QString &)>; + +} // namespace TextEditor diff --git a/src/plugins/texteditor/snippets/snippetscollection.cpp b/src/plugins/texteditor/snippets/snippetscollection.cpp index afb6c28eda..e814607a12 100644 --- a/src/plugins/texteditor/snippets/snippetscollection.cpp +++ b/src/plugins/texteditor/snippets/snippetscollection.cpp @@ -94,11 +94,11 @@ SnippetsCollection *SnippetsCollection::instance() } // SnippetsCollection -SnippetsCollection::SnippetsCollection() : - m_userSnippetsPath(Core::ICore::userResourcePath() + QLatin1String("/snippets/")), - m_userSnippetsFile(QLatin1String("snippets.xml")) +SnippetsCollection::SnippetsCollection() + : m_userSnippetsPath(Core::ICore::userResourcePath().pathAppended("snippets/").toString()) + , m_userSnippetsFile(QLatin1String("snippets.xml")) { - QDir dir(Core::ICore::resourcePath() + QLatin1String("/snippets/")); + QDir dir = Core::ICore::resourcePath("snippets").toDir(); dir.setNameFilters(QStringList(QLatin1String("*.xml"))); foreach (const QFileInfo &fi, dir.entryInfoList()) m_builtInSnippetsFiles.append(fi.absoluteFilePath()); @@ -306,7 +306,7 @@ bool SnippetsCollection::synchronize(QString *errorString) QDir::toNativeSeparators(m_userSnippetsPath)); return false; } - Utils::FileSaver saver(m_userSnippetsPath + m_userSnippetsFile); + Utils::FileSaver saver(Utils::FilePath::fromString(m_userSnippetsPath + m_userSnippetsFile)); if (!saver.hasError()) { using GroupIndexByIdConstIt = QHash<QString, int>::ConstIterator; diff --git a/src/plugins/texteditor/textdocument.cpp b/src/plugins/texteditor/textdocument.cpp index eb6ae5b0dd..2a52c42575 100644 --- a/src/plugins/texteditor/textdocument.cpp +++ b/src/plugins/texteditor/textdocument.cpp @@ -600,7 +600,7 @@ SyntaxHighlighter *TextDocument::syntaxHighlighter() const * If \a autoSave is true, the cursor will be restored and some signals suppressed * and we do not clean up the text file (cleanWhitespace(), ensureFinalNewLine()). */ -bool TextDocument::save(QString *errorString, const QString &saveFileName, bool autoSave) +bool TextDocument::save(QString *errorString, const FilePath &filePath, bool autoSave) { QTextCursor cursor(&d->m_document); @@ -638,11 +638,9 @@ bool TextDocument::save(QString *errorString, const QString &saveFileName, bool if (d->m_storageSettings.m_addFinalNewLine) ensureFinalNewLine(cursor); cursor.endEditBlock(); - } + } - QString fName = filePath().toString(); - if (!saveFileName.isEmpty()) - fName = saveFileName; + const Utils::FilePath &savePath = filePath.isEmpty() ? this->filePath() : filePath; // check if UTF8-BOM has to be added or removed Utils::TextFileFormat saveFormat = format(); @@ -659,7 +657,7 @@ bool TextDocument::save(QString *errorString, const QString &saveFileName, bool } } - const bool ok = write(fName, saveFormat, d->m_document.toPlainText(), errorString); + const bool ok = write(savePath, saveFormat, d->m_document.toPlainText(), errorString); // restore text cursor and scroll bar positions if (autoSave && undos < d->m_document.availableUndoSteps()) { @@ -681,9 +679,8 @@ bool TextDocument::save(QString *errorString, const QString &saveFileName, bool return true; // inform about the new filename - const QFileInfo fi(fName); d->m_document.setModified(false); // also triggers update of the block revisions - setFilePath(Utils::FilePath::fromUserInput(fi.absoluteFilePath())); + setFilePath(savePath.absoluteFilePath()); emit changed(); return true; } @@ -715,33 +712,35 @@ bool TextDocument::isModified() const return d->m_document.isModified(); } -Core::IDocument::OpenResult TextDocument::open(QString *errorString, const QString &fileName, - const QString &realFileName) +Core::IDocument::OpenResult TextDocument::open(QString *errorString, + const Utils::FilePath &filePath, + const Utils::FilePath &realFilePath) { - emit aboutToOpen(fileName, realFileName); - OpenResult success = openImpl(errorString, fileName, realFileName, /*reload =*/ false); + emit aboutToOpen(filePath, realFilePath); + OpenResult success = openImpl(errorString, filePath, realFilePath, /*reload =*/ false); if (success == OpenResult::Success) { - setMimeType(Utils::mimeTypeForFile(fileName).name()); + setMimeType(Utils::mimeTypeForFile(filePath.toString()).name()); emit openFinishedSuccessfully(); } return success; } -Core::IDocument::OpenResult TextDocument::openImpl(QString *errorString, const QString &fileName, - const QString &realFileName, bool reload) +Core::IDocument::OpenResult TextDocument::openImpl(QString *errorString, + const Utils::FilePath &filePath, + const Utils::FilePath &realFilePath, + bool reload) { QStringList content; ReadResult readResult = Utils::TextFileFormat::ReadIOError; - if (!fileName.isEmpty()) { - const QFileInfo fi(fileName); - readResult = read(realFileName, &content, errorString); + if (!filePath.isEmpty()) { + readResult = read(realFilePath, &content, errorString); const int chunks = content.size(); // Don't call setUndoRedoEnabled(true) when reload is true and filenames are different, // since it will reset the undo's clear index - if (!reload || fileName == realFileName) + if (!reload || filePath == realFilePath) d->m_document.setUndoRedoEnabled(reload); QTextCursor c(&d->m_document); @@ -775,7 +774,7 @@ Core::IDocument::OpenResult TextDocument::openImpl(QString *errorString, const Q // Don't call setUndoRedoEnabled(true) when reload is true and filenames are different, // since it will reset the undo's clear index - if (!reload || fileName == realFileName) + if (!reload || filePath == realFilePath) d->m_document.setUndoRedoEnabled(true); auto documentLayout = @@ -783,8 +782,8 @@ Core::IDocument::OpenResult TextDocument::openImpl(QString *errorString, const Q QTC_ASSERT(documentLayout, return OpenResult::CannotHandle); documentLayout->lastSaveRevision = d->m_autoSaveRevision = d->m_document.revision(); d->updateRevisions(); - d->m_document.setModified(fileName != realFileName); - setFilePath(Utils::FilePath::fromUserInput(fi.absoluteFilePath())); + d->m_document.setModified(filePath != realFilePath); + setFilePath(filePath); } if (readResult == Utils::TextFileFormat::ReadIOError) return OpenResult::ReadError; @@ -800,10 +799,10 @@ bool TextDocument::reload(QString *errorString, QTextCodec *codec) bool TextDocument::reload(QString *errorString) { - return reload(errorString, filePath().toString()); + return reload(errorString, filePath()); } -bool TextDocument::reload(QString *errorString, const QString &realFileName) +bool TextDocument::reload(QString *errorString, const FilePath &realFilePath) { emit aboutToReload(); auto documentLayout = @@ -812,8 +811,8 @@ bool TextDocument::reload(QString *errorString, const QString &realFileName) if (documentLayout) marks = documentLayout->documentClosing(); // removes text marks non-permanently - const QString &file = filePath().toString(); - bool success = openImpl(errorString, file, realFileName, /*reload =*/ true) == OpenResult::Success; + bool success = openImpl(errorString, filePath(), realFilePath, /*reload =*/true) + == OpenResult::Success; if (documentLayout) documentLayout->documentReloaded(marks, this); // re-adds text marks diff --git a/src/plugins/texteditor/textdocument.h b/src/plugins/texteditor/textdocument.h index f67b55fa6e..98e6546045 100644 --- a/src/plugins/texteditor/textdocument.h +++ b/src/plugins/texteditor/textdocument.h @@ -112,7 +112,7 @@ public: void removeMarkFromMarksCache(TextMark *mark); // IDocument implementation. - bool save(QString *errorString, const QString &fileName, bool autoSave) override; + bool save(QString *errorString, const Utils::FilePath &filePath, bool autoSave) override; QByteArray contents() const override; bool setContents(const QByteArray &contents) override; bool shouldAutoSave() const override; @@ -127,10 +127,10 @@ public: void setFallbackSaveAsPath(const QString &fallbackSaveAsPath); void setFallbackSaveAsFileName(const QString &fallbackSaveAsFileName); - OpenResult open(QString *errorString, const QString &fileName, - const QString &realFileName) override; + OpenResult open(QString *errorString, const Utils::FilePath &filePath, + const Utils::FilePath &realFilePath) override; virtual bool reload(QString *errorString); - bool reload(QString *errorString, const QString &realFileName); + bool reload(QString *errorString, const Utils::FilePath &realFilePath); bool setPlainText(const QString &text); QTextDocument *document() const; @@ -156,7 +156,7 @@ public: const std::function<Utils::FilePath()> &filePath); signals: - void aboutToOpen(const QString &fileName, const QString &realFileName); + void aboutToOpen(const Utils::FilePath &filePath, const Utils::FilePath &realFilePath); void openFinishedSuccessfully(); void contentsChangedWithPosition(int position, int charsRemoved, int charsAdded); void tabSettingsChanged(); @@ -167,7 +167,9 @@ protected: virtual void applyFontSettings(); private: - OpenResult openImpl(QString *errorString, const QString &fileName, const QString &realFileName, + OpenResult openImpl(QString *errorString, + const Utils::FilePath &filePath, + const Utils::FilePath &realFileName, bool reload); void cleanWhitespace(QTextCursor &cursor, bool inEntireDocument, bool cleanIndentation); void ensureFinalNewLine(QTextCursor &cursor); diff --git a/src/plugins/texteditor/texteditor.cpp b/src/plugins/texteditor/texteditor.cpp index e9f64c5b3f..033dd2a58a 100644 --- a/src/plugins/texteditor/texteditor.cpp +++ b/src/plugins/texteditor/texteditor.cpp @@ -42,6 +42,7 @@ #include "icodestylepreferences.h" #include "refactoroverlay.h" #include "snippets/snippet.h" +#include "snippets/snippetoverlay.h" #include "storagesettings.h" #include "syntaxhighlighter.h" #include "tabsettings.h" @@ -665,7 +666,7 @@ public: int extraAreaPreviousMarkTooltipRequestedLine = -1; TextEditorOverlay *m_overlay = nullptr; - TextEditorOverlay *m_snippetOverlay = nullptr; + SnippetOverlay *m_snippetOverlay = nullptr; TextEditorOverlay *m_searchResultOverlay = nullptr; bool snippetCheckCursor(const QTextCursor &cursor); void snippetTabOrBacktab(bool forward); @@ -1004,7 +1005,7 @@ void TextEditorWidgetPrivate::ctor(const QSharedPointer<TextDocument> &doc) q->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); m_overlay = new TextEditorOverlay(q); - m_snippetOverlay = new TextEditorOverlay(q); + m_snippetOverlay = new SnippetOverlay(q); m_searchResultOverlay = new TextEditorOverlay(q); m_refactorOverlay = new RefactorOverlay(q); @@ -1214,11 +1215,7 @@ void TextEditorWidgetPrivate::print(QPrinter *printer) QAbstractTextDocumentLayout *layout = doc->documentLayout(); layout->setPaintDevice(p.device()); -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - int dpiy = p.device()->logicalDpiY(); -#else int dpiy = qRound(QGuiApplication::primaryScreen()->logicalDotsPerInchY()); -#endif int margin = int((2/2.54)*dpiy); // 2 cm margins QTextFrameFormat fmt = doc->rootFrame()->frameFormat(); @@ -1467,10 +1464,10 @@ TextDocument *TextEditorWidget::textDocument() const return d->m_document.data(); } -void TextEditorWidget::aboutToOpen(const QString &fileName, const QString &realFileName) +void TextEditorWidget::aboutToOpen(const Utils::FilePath &filePath, const Utils::FilePath &realFilePath) { - Q_UNUSED(fileName) - Q_UNUSED(realFileName) + Q_UNUSED(filePath) + Q_UNUSED(realFilePath) } void TextEditorWidget::openFinishedSuccessfully() @@ -2702,61 +2699,97 @@ void TextEditorWidget::keyPressEvent(QKeyEvent *e) d->m_codeAssistant.process(); } -void TextEditorWidget::insertCodeSnippet(const QTextCursor &cursor_arg, const QString &snippet) +class PositionedPart : public ParsedSnippet::Part +{ +public: + explicit PositionedPart(const ParsedSnippet::Part &part) : ParsedSnippet::Part(part) {} + int start; + int end; +}; + +class CursorPart : public ParsedSnippet::Part { - Snippet::ParsedSnippet data = Snippet::parse(snippet); +public: + CursorPart(const PositionedPart &part, QTextDocument *doc) + : ParsedSnippet::Part(part) + , cursor(doc) + { + cursor.setPosition(part.start); + cursor.setPosition(part.end, QTextCursor::KeepAnchor); + } + QTextCursor cursor; +}; - if (!data.success) { - QString message = QString::fromLatin1("Cannot parse snippet \"%1\".").arg(snippet); - if (!data.errorMessage.isEmpty()) - message += QLatin1String("\nParse error: ") + data.errorMessage; - QMessageBox::warning(this, QLatin1String("Snippet Parse Error"), message); +void TextEditorWidget::insertCodeSnippet(const QTextCursor &cursor_arg, + const QString &snippet, + const SnippetParser &parse) +{ + SnippetParseResult result = parse(snippet); + if (Utils::holds_alternative<SnippetParseError>(result)) { + const auto &error = Utils::get<SnippetParseError>(result); + QMessageBox::warning(this, tr("Snippet Parse Error"), error.htmlMessage()); return; } + QTC_ASSERT(Utils::holds_alternative<ParsedSnippet>(result), return); + ParsedSnippet data = Utils::get<ParsedSnippet>(result); QTextCursor cursor = cursor_arg; cursor.beginEditBlock(); cursor.removeSelectedText(); const int startCursorPosition = cursor.position(); - cursor.insertText(data.text); - QList<QTextEdit::ExtraSelection> selections; - - 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; + d->m_snippetOverlay->mangle(); + d->m_snippetOverlay->clear(); + + 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(); - - cursor = textCursor(); - if (selection.cursor.hasSelection()) { - cursor.setPosition(selection.cursor.selectionStart()); - cursor.setPosition(selection.cursor.selectionEnd(), QTextCursor::KeepAnchor); + for (const CursorPart &part : cursorParts) { + const QColor &color = part.cursor.hasSelection() ? occurrencesColor : renameColor; + if (part.finalPart) { + d->m_snippetOverlay->setFinalSelection(part.cursor, color); } else { - cursor.setPosition(selection.cursor.position()); + d->m_snippetOverlay->addSnippetSelection(part.cursor, + color, + part.mangler, + part.variableIndex); } + } + + cursor = d->m_snippetOverlay->firstSelectionCursor(); + if (!cursor.isNull()) { setTextCursor(cursor); + if (d->m_snippetOverlay->isFinalSelection(cursor)) { + d->m_snippetOverlay->mangle(); + d->m_snippetOverlay->clear(); + d->m_snippetOverlay->setVisible(false); + } else { + d->m_snippetOverlay->setVisible(true); + } } } @@ -3464,37 +3497,14 @@ 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); - } + QTextCursor cursor = forward ? m_snippetOverlay->nextSelectionCursor(q->textCursor()) + : m_snippetOverlay->previousSelectionCursor(q->textCursor()); q->setTextCursor(cursor); + if (m_snippetOverlay->isFinalSelection(cursor)) { + m_snippetOverlay->setVisible(false); + m_snippetOverlay->mangle(); + m_snippetOverlay->clear(); + } } // Calculate global position for a tooltip considering the left extra area. @@ -6250,7 +6260,7 @@ bool TextEditorWidget::openLink(const Utils::Link &link, bool inNextSplit) if (!link.hasValidTarget()) return false; - if (!inNextSplit && textDocument()->filePath().toString() == link.targetFileName) { + if (!inNextSplit && textDocument()->filePath() == link.targetFilePath) { EditorManager::addCurrentPositionToNavigationHistory(); gotoLine(link.targetLine, link.targetColumn, true, true); setFocus(); @@ -6260,8 +6270,7 @@ bool TextEditorWidget::openLink(const Utils::Link &link, bool inNextSplit) if (inNextSplit) flags |= EditorManager::OpenInOtherSplit; - return EditorManager::openEditorAt(link.targetFileName, link.targetLine, link.targetColumn, - Id(), flags); + return EditorManager::openEditorAt(link, Id(), flags); } bool TextEditorWidgetPrivate::isMouseNavigationEvent(QMouseEvent *e) const @@ -7086,17 +7095,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) { @@ -8687,7 +8685,7 @@ void TextEditorLinkLabel::mouseMoveEvent(QMouseEvent *event) return; auto data = new DropMimeData; - data->addFile(m_link.targetFileName, m_link.targetLine, m_link.targetColumn); + data->addFile(m_link.targetFilePath.toString(), m_link.targetLine, m_link.targetColumn); auto drag = new QDrag(this); drag->setMimeData(data); drag->exec(Qt::CopyAction); @@ -8699,7 +8697,9 @@ void TextEditorLinkLabel::mouseReleaseEvent(QMouseEvent *event) if (!m_link.hasValidTarget()) return; - EditorManager::openEditorAt(m_link.targetFileName, m_link.targetLine, m_link.targetColumn); + EditorManager::openEditorAt(m_link.targetFilePath.toString(), + m_link.targetLine, + m_link.targetColumn); } // diff --git a/src/plugins/texteditor/texteditor.h b/src/plugins/texteditor/texteditor.h index b48c7b3ee3..ceab8fbb53 100644 --- a/src/plugins/texteditor/texteditor.h +++ b/src/plugins/texteditor/texteditor.h @@ -26,10 +26,12 @@ #pragma once #include "texteditor_global.h" + #include "blockrange.h" #include "codeassist/assistenums.h" #include "indenter.h" #include "refactoroverlay.h" +#include "snippets/snippetparser.h" #include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/ieditor.h> @@ -186,7 +188,7 @@ public: TextDocument *textDocument() const; QSharedPointer<TextDocument> textDocumentPtr() const; - virtual void aboutToOpen(const QString &fileName, const QString &realFileName); + virtual void aboutToOpen(const Utils::FilePath &filePath, const Utils::FilePath &realFilePath); virtual void openFinishedSuccessfully(); // IEditor QByteArray saveState() const; @@ -265,7 +267,9 @@ public: void setReadOnly(bool b); - void insertCodeSnippet(const QTextCursor &cursor, const QString &snippet); + void insertCodeSnippet(const QTextCursor &cursor, + const QString &snippet, + const SnippetParser &parse); void setBlockSelection(bool on); void setBlockSelection(int positionBlock, int positionColumn, int anchhorBlock, diff --git a/src/plugins/texteditor/texteditor.pro b/src/plugins/texteditor/texteditor.pro index bbf536459e..c22ed59c59 100644 --- a/src/plugins/texteditor/texteditor.pro +++ b/src/plugins/texteditor/texteditor.pro @@ -97,7 +97,9 @@ SOURCES += texteditorplugin.cpp \ commentssettings.cpp \ marginsettings.cpp \ formattexteditor.cpp \ - command.cpp + command.cpp \ + snippets/snippetoverlay.cpp \ + snippets/snippetparser.cpp HEADERS += texteditorplugin.h \ plaintexteditorfactory.h \ @@ -192,7 +194,9 @@ HEADERS += texteditorplugin.h \ formattexteditor.h \ command.h \ indenter.h \ - formatter.h + formatter.h \ + snippets/snippetoverlay.h \ + snippets/snippetparser.h FORMS += \ displaysettingspage.ui \ diff --git a/src/plugins/texteditor/texteditor.qbs b/src/plugins/texteditor/texteditor.qbs index 5b36e0c76c..896c9d515e 100644 --- a/src/plugins/texteditor/texteditor.qbs +++ b/src/plugins/texteditor/texteditor.qbs @@ -205,8 +205,6 @@ Project { name: "Snippets" prefix: "snippets/" files: [ - "snippetprovider.cpp", - "snippetprovider.h", "reuse.h", "snippet.cpp", "snippet.h", @@ -214,6 +212,12 @@ Project { "snippetassistcollector.h", "snippeteditor.cpp", "snippeteditor.h", + "snippetoverlay.cpp", + "snippetoverlay.h", + "snippetparser.cpp", + "snippetparser.h", + "snippetprovider.cpp", + "snippetprovider.h", "snippetscollection.cpp", "snippetscollection.h", "snippetssettings.cpp", diff --git a/src/plugins/texteditor/texteditorconstants.cpp b/src/plugins/texteditor/texteditorconstants.cpp index 3902c13665..6063b1c5e8 100644 --- a/src/plugins/texteditor/texteditorconstants.cpp +++ b/src/plugins/texteditor/texteditorconstants.cpp @@ -55,6 +55,7 @@ const char *nameForStyle(TextStyle style) case C_STRING: return "String"; case C_TYPE: return "Type"; case C_LOCAL: return "Local"; + case C_PARAMETER: return "Parameter"; case C_GLOBAL: return "Global"; case C_FIELD: return "Field"; // TODO: Rename "Static" to "Enumeration" in next major update, diff --git a/src/plugins/texteditor/texteditorconstants.h b/src/plugins/texteditor/texteditorconstants.h index 22b602074d..abab979b61 100644 --- a/src/plugins/texteditor/texteditorconstants.h +++ b/src/plugins/texteditor/texteditorconstants.h @@ -55,6 +55,7 @@ enum TextStyle : quint8 { C_STRING, C_TYPE, C_LOCAL, + C_PARAMETER, C_GLOBAL, C_FIELD, C_ENUMERATION, diff --git a/src/plugins/texteditor/texteditoroverlay.cpp b/src/plugins/texteditor/texteditoroverlay.cpp index 863db175c9..4fb7de00a2 100644 --- a/src/plugins/texteditor/texteditoroverlay.cpp +++ b/src/plugins/texteditor/texteditoroverlay.cpp @@ -25,7 +25,6 @@ #include "texteditoroverlay.h" #include "texteditor.h" -#include "snippets/snippet.h" #include <QDebug> #include <QMap> @@ -34,6 +33,7 @@ #include <QTextBlock> #include <algorithm> +#include <utils/qtcassert.h> using namespace TextEditor; using namespace TextEditor::Internal; @@ -72,8 +72,6 @@ void TextEditorOverlay::clear() return; m_selections.clear(); m_firstSelectionOriginalBegin = -1; - m_equivalentSelections.clear(); - m_manglers.clear(); update(); } @@ -419,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) @@ -446,111 +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; -} - -void TextEditorOverlay::mapEquivalentSelections() -{ - m_equivalentSelections.clear(); - m_equivalentSelections.resize(m_selections.size()); - - QMultiMap<QString, int> all; - for (int i = 0; i < m_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; - } -} - -void TextEditorOverlay::updateEquivalentSelections(const QTextCursor &cursor) -{ - int selectionIndex = selectionIndexForCursor(cursor); - if (selectionIndex == -1) - return; - - const QString ¤tText = selectionText(selectionIndex); - const QList<int> &equivalents = m_equivalentSelections.at(selectionIndex); - foreach (int i, equivalents) { - if (i == selectionIndex) - continue; - const QString &equivalentText = selectionText(i); - if (currentText != equivalentText) { - QTextCursor selectionCursor = assembleCursorForSelection(i); - selectionCursor.joinPreviousEditBlock(); - selectionCursor.removeSelectedText(); - selectionCursor.insertText(currentText); - selectionCursor.endEditBlock(); - } - } -} - -void TextEditorOverlay::setNameMangler(const QList<NameMangler *> &manglers) -{ - m_manglers = manglers; -} - -void TextEditorOverlay::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(); - } - } -} - bool TextEditorOverlay::hasFirstSelectionBeginMoved() const { if (m_firstSelectionOriginalBegin == -1 || m_selections.isEmpty()) diff --git a/src/plugins/texteditor/texteditoroverlay.h b/src/plugins/texteditor/texteditoroverlay.h index 374bc0ea1e..d87a0fe7aa 100644 --- a/src/plugins/texteditor/texteditoroverlay.h +++ b/src/plugins/texteditor/texteditoroverlay.h @@ -35,7 +35,6 @@ QT_FORWARD_DECLARE_CLASS(QWidget) QT_FORWARD_DECLARE_CLASS(QPainterPath) namespace TextEditor { -class NameMangler; class TextEditorWidget; namespace Internal { @@ -74,7 +73,7 @@ public: void setAlpha(bool enabled) { m_alpha = enabled; } - void clear(); + virtual void clear(); enum OverlaySelectionFlags { LockSize = 1, @@ -93,22 +92,16 @@ public: inline int dropShadowWidth() const { return m_dropShadowWidth; } - bool hasCursorInSelection(const QTextCursor &cursor) const; - - void mapEquivalentSelections(); - void updateEquivalentSelections(const QTextCursor &cursor); - void setNameMangler(const QList<NameMangler *> &manglers); - void mangle(); - bool hasFirstSelectionBeginMoved() const; +protected: + QTextCursor cursorForSelection(const OverlaySelection &selection) const; + QTextCursor cursorForIndex(int selectionIndex) const; + private: QPainterPath createSelectionPath(const QTextCursor &begin, const QTextCursor &end, const QRect& clip); void paintSelection(QPainter *painter, const OverlaySelection &selection); void fillSelection(QPainter *painter, const OverlaySelection &selection, const QColor &color); - int selectionIndexForCursor(const QTextCursor &cursor) const; - QString selectionText(int selectionIndex) const; - QTextCursor assembleCursorForSelection(int selectionIndex) const; bool m_visible; bool m_alpha; @@ -118,8 +111,6 @@ private: TextEditorWidget *m_editor; QWidget *m_viewport; QList<OverlaySelection> m_selections; - QVector<QList<int> > m_equivalentSelections; - QList<NameMangler *> m_manglers; }; } // namespace Internal diff --git a/src/plugins/texteditor/texteditorsettings.cpp b/src/plugins/texteditor/texteditorsettings.cpp index 2c72606dea..724e3efedc 100644 --- a/src/plugins/texteditor/texteditorsettings.cpp +++ b/src/plugins/texteditor/texteditorsettings.cpp @@ -162,6 +162,8 @@ FormatDescriptions TextEditorSettingsPrivate::initialFormats() Qt::darkMagenta); formatDescr.emplace_back(C_LOCAL, tr("Local"), tr("Local variables."), QColor(9, 46, 100)); + formatDescr.emplace_back(C_PARAMETER, tr("Parameter"), + tr("Function or method parameters."), QColor(9, 46, 100)); formatDescr.emplace_back(C_FIELD, tr("Field"), tr("Class' data members."), Qt::darkRed); formatDescr.emplace_back(C_GLOBAL, tr("Global"), |