diff --git a/autotests/syntaxrepository_test.cpp b/autotests/syntaxrepository_test.cpp index 54525a1..2d0f42a 100644 --- a/autotests/syntaxrepository_test.cpp +++ b/autotests/syntaxrepository_test.cpp @@ -1,305 +1,316 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "test-config.h" #include #include #include #include #include #include #include #include #include #include namespace KSyntaxHighlighting { class NullHighlighter : public AbstractHighlighter { public: using AbstractHighlighter::highlightLine; void applyFormat(int offset, int length, const Format &format) Q_DECL_OVERRIDE { Q_UNUSED(offset); Q_UNUSED(length); // only here to ensure we don't crash format.isDefaultTextStyle(theme()); format.textColor(theme()); } }; class RepositoryTest : public QObject { Q_OBJECT private: Repository m_repo; private Q_SLOTS: void initTestCase() { QStandardPaths::enableTestMode(true); } void testDefinitionByExtension_data() { QTest::addColumn("fileName"); QTest::addColumn("defName"); QTest::newRow("empty") << QString() << QString(); QTest::newRow("qml") << QStringLiteral("/bla/foo.qml") << QStringLiteral("QML"); QTest::newRow("glsl") << QStringLiteral("flat.frag") << QStringLiteral("GLSL"); // the following ones are defined in multiple syntax definitions QTest::newRow("c") << QStringLiteral("test.c") << QStringLiteral("C"); QTest::newRow("c++") << QStringLiteral("test.cpp") << QStringLiteral("C++"); QTest::newRow("markdown") << QStringLiteral("test.md") << QStringLiteral("Markdown"); QTest::newRow("Makefile 1") << QStringLiteral("Makefile") << QStringLiteral("Makefile"); QTest::newRow("Makefile 2") << QStringLiteral("/some/path/to/Makefile") << QStringLiteral("Makefile"); QTest::newRow("Makefile 3") << QStringLiteral("Makefile.am") << QStringLiteral("Makefile"); } void testDefinitionByExtension() { QFETCH(QString, fileName); QFETCH(QString, defName); auto def = m_repo.definitionForFileName(fileName); if (defName.isEmpty()) { QVERIFY(!def.isValid()); } else { QVERIFY(def.isValid()); QCOMPARE(def.name(), defName); } } void testLoadAll() { foreach (const auto &def, m_repo.definitions()) { QVERIFY(!def.name().isEmpty()); QVERIFY(!def.translatedName().isEmpty()); QVERIFY(!def.section().isEmpty()); QVERIFY(!def.translatedSection().isEmpty()); // indirectly trigger loading, as we can't reach that from public API // if the loading fails the highlighter will produce empty states NullHighlighter hl; State initialState; hl.setDefinition(def); const auto state = hl.highlightLine(QLatin1String("This should not crash } ] ) !"), initialState); QVERIFY(state != initialState); } } void testMetaData() { auto def = m_repo.definitionForName(QLatin1String("Alerts")); QVERIFY(def.isValid()); QVERIFY(def.extensions().isEmpty()); QVERIFY(def.mimeTypes().isEmpty()); QVERIFY(def.version() >= 1.11f); QVERIFY(def.isHidden()); QCOMPARE(def.section(), QLatin1String("Other")); QCOMPARE(def.license(), QLatin1String("MIT")); QVERIFY(def.author().contains(QLatin1String("Dominik"))); QFileInfo fi(def.filePath()); QVERIFY(fi.isAbsolute()); QVERIFY(def.filePath().endsWith(QLatin1String("alert.xml"))); def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); QCOMPARE(def.section(), QLatin1String("Sources")); QCOMPARE(def.indenter(), QLatin1String("cstyle")); QCOMPARE(def.style(), QLatin1String("C++")); QVERIFY(def.mimeTypes().contains(QLatin1String("text/x-c++hdr"))); QVERIFY(def.extensions().contains(QLatin1String("*.h"))); QCOMPARE(def.priority(), 9); def = m_repo.definitionForName(QLatin1String("Apache Configuration")); QVERIFY(def.isValid()); QVERIFY(def.extensions().contains(QLatin1String("httpd.conf"))); QVERIFY(def.extensions().contains(QLatin1String(".htaccess*"))); } void testGeneralMetaData() { auto def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); QVERIFY(!def.indentationBasedFoldingEnabled()); + // comment markers + QCOMPARE(def.singleLineCommentMarker(), QLatin1String("//")); + QCOMPARE(def.singleLineCommentPosition(), KSyntaxHighlighting::CommentPosition::StartOfLine); + const auto cppMultiLineCommentMarker = QPair(QLatin1String("/*"), QLatin1String("*/")); + QCOMPARE(def.multiLineCommentMarker(), cppMultiLineCommentMarker); + def = m_repo.definitionForName(QLatin1String("Python")); QVERIFY(def.isValid()); // indentation QVERIFY(def.indentationBasedFoldingEnabled()); QCOMPARE(def.foldingIgnoreList(), QStringList() << QLatin1String("(?:\\s+|\\s*#.*)")); // keyword lists QVERIFY(!def.keywordLists().isEmpty()); QVERIFY(def.keywordList(QLatin1String("operators")).contains(QLatin1String("and"))); QVERIFY(!def.keywordList(QLatin1String("does not exits")).contains(QLatin1String("and"))); } void testFormatData() { auto def = m_repo.definitionForName(QLatin1String("ChangeLog")); QVERIFY(def.isValid()); auto formats = def.formats(); QVERIFY(!formats.isEmpty()); // verify that the formats are sorted, such that the order matches the order of the itemDatas in the xml files. auto sortComparator = [](const KSyntaxHighlighting::Format & lhs, const KSyntaxHighlighting::Format & rhs) { return lhs.id() < rhs.id(); }; QVERIFY(std::is_sorted(formats.begin(), formats.end(), sortComparator)); // check all names are listed QStringList formatNames; foreach (const auto & format, formats) { formatNames.append(format.name()); } const QStringList expectedItemDatas = { QStringLiteral("Normal Text"), QStringLiteral("Name"), QStringLiteral("E-Mail"), QStringLiteral("Date"), QStringLiteral("Entry") }; QCOMPARE(formatNames, expectedItemDatas); } void testIncludedDefinitions() { auto def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); auto defs = def.includedDefinitions(); const QStringList expectedDefinitionNames = { QStringLiteral("ISO C++"), QStringLiteral("GCCExtensions"), QStringLiteral("Doxygen"), QStringLiteral("Alerts"), QStringLiteral("Modelines") }; QStringList definitionNames; for (auto d : defs) { QVERIFY(d.isValid()); definitionNames.push_back(d.name()); } QCOMPARE(definitionNames, expectedDefinitionNames); } void testReload() { auto def = m_repo.definitionForName(QLatin1String("QML")); QVERIFY(!m_repo.definitions().isEmpty()); QVERIFY(def.isValid()); NullHighlighter hl; hl.setDefinition(def); auto oldState = hl.highlightLine(QLatin1String("/* TODO this should not crash */"), State()); m_repo.reload(); QVERIFY(!m_repo.definitions().isEmpty()); QVERIFY(!def.isValid()); hl.highlightLine(QLatin1String("/* TODO this should not crash */"), State()); hl.highlightLine(QLatin1String("/* FIXME neither should this crash */"), oldState); QVERIFY(hl.definition().isValid()); QCOMPARE(hl.definition().name(), QLatin1String("QML")); } void testLifetime() { // common mistake with value-type like Repo API, make sure this doesn'T crash NullHighlighter hl; { Repository repo; hl.setDefinition(repo.definitionForName(QLatin1String("C++"))); hl.setTheme(repo.defaultTheme()); } hl.highlightLine(QLatin1String("/**! @todo this should not crash .*/"), State()); } void testCustomPath() { QString testInputPath = QStringLiteral(TESTSRCDIR "/input"); Repository repo; QVERIFY(repo.customSearchPaths().empty()); repo.addCustomSearchPath(testInputPath); QCOMPARE(repo.customSearchPaths().size(), 1); QCOMPARE(repo.customSearchPaths()[0], testInputPath); auto customDefinition = repo.definitionForName(QLatin1String("Test Syntax")); QVERIFY(customDefinition.isValid()); auto customTheme = repo.theme(QLatin1String("Test Theme")); QVERIFY(customTheme.isValid()); } void testInvalidDefinition() { Definition def; QVERIFY(!def.isValid()); QVERIFY(def.filePath().isEmpty()); QVERIFY(def.name().isEmpty()); QVERIFY(def.translatedName().isEmpty()); QVERIFY(def.section().isEmpty()); QVERIFY(def.translatedSection().isEmpty()); QVERIFY(def.mimeTypes().isEmpty()); QVERIFY(def.extensions().isEmpty()); QCOMPARE(def.version(), 0); QCOMPARE(def.priority(), 0); QVERIFY(!def.isHidden()); QVERIFY(def.style().isEmpty()); QVERIFY(def.indenter().isEmpty()); QVERIFY(def.author().isEmpty()); QVERIFY(def.license().isEmpty()); QVERIFY(!def.indentationBasedFoldingEnabled()); QVERIFY(def.foldingIgnoreList().isEmpty()); QVERIFY(def.keywordLists().isEmpty()); QVERIFY(def.formats().isEmpty()); QVERIFY(def.includedDefinitions().isEmpty()); + QVERIFY(def.singleLineCommentMarker().isEmpty()); + QCOMPARE(def.singleLineCommentPosition(), KSyntaxHighlighting::CommentPosition::StartOfLine); + const auto emptyPair = QPair(); + QCOMPARE(def.multiLineCommentMarker(), emptyPair); + for (QChar c : QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~")) { QVERIFY(def.isWordDelimiter(c)); QVERIFY(def.isWordWrapDelimiter(c)); } } void testDelimiters() { auto def = m_repo.definitionForName(QLatin1String("LaTeX")); QVERIFY(def.isValid()); // check that backslash '\' is removed for (QChar c : QStringLiteral("\t !%&()*+,-./:;<=>?[]^{|}~")) QVERIFY(def.isWordDelimiter(c)); QVERIFY(!def.isWordDelimiter(QLatin1Char('\\'))); // check where breaking a line is valid for (QChar c : QStringLiteral(",{}[]")) QVERIFY(def.isWordWrapDelimiter(c)); } }; } QTEST_GUILESS_MAIN(KSyntaxHighlighting::RepositoryTest) #include "syntaxrepository_test.moc" diff --git a/src/lib/definition.cpp b/src/lib/definition.cpp index 21d56b8..98b5370 100644 --- a/src/lib/definition.cpp +++ b/src/lib/definition.cpp @@ -1,641 +1,700 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "definition.h" #include "definition_p.h" #include "definitionref_p.h" #include "context_p.h" #include "format.h" #include "format_p.h" #include "repository_p.h" #include "rule_p.h" #include "ksyntaxhighlighting_logging.h" #include "ksyntaxhighlighting_version.h" #include "xml_p.h" #include #include #include #include #include #include #include #include #include #include using namespace KSyntaxHighlighting; DefinitionData::DefinitionData() : wordDelimiters(QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~")) // must be sorted! , wordWrapDelimiters(wordDelimiters) { } DefinitionData::~DefinitionData() { qDeleteAll(contexts); } DefinitionData* DefinitionData::get(const Definition &def) { return def.d.get(); } Definition::Definition() : d(new DefinitionData) { } Definition::Definition(const Definition &other) : d(other.d) { d->q = *this; } Definition::Definition(const std::shared_ptr &dd) : d(dd) { } Definition::~Definition() { } Definition& Definition::operator=(const Definition &rhs) { d = rhs.d; return *this; } bool Definition::operator==(const Definition &other) const { return d->fileName == other.d->fileName; } bool Definition::operator!=(const Definition& other) const { return d->fileName != other.d->fileName; } bool Definition::isValid() const { return d->repo && !d->fileName.isEmpty() && !d->name.isEmpty(); } QString Definition::filePath() const { return d->fileName; } QString Definition::name() const { return d->name; } QString Definition::translatedName() const { return QCoreApplication::instance()->translate("Language", d->name.toUtf8().constData()); } QString Definition::section() const { return d->section; } QString Definition::translatedSection() const { return QCoreApplication::instance()->translate("Language Section", d->section.toUtf8().constData()); } QVector Definition::mimeTypes() const { return d->mimetypes; } QVector Definition::extensions() const { return d->extensions; } int Definition::version() const { return d->version; } int Definition::priority() const { return d->priority; } bool Definition::isHidden() const { return d->hidden; } QString Definition::style() const { return d->style; } QString Definition::indenter() const { return d->indenter; } QString Definition::author() const { return d->author; } QString Definition::license() const { return d->license; } bool Definition::isWordDelimiter(QChar c) const { d->load(); return d->isWordDelimiter(c); } bool Definition::isWordWrapDelimiter(QChar c) const { d->load(); return std::binary_search(d->wordWrapDelimiters.constBegin(), d->wordWrapDelimiters.constEnd(), c); } bool Definition::indentationBasedFoldingEnabled() const { d->load(); return d->indentationBasedFolding; } QStringList Definition::foldingIgnoreList() const { d->load(); return d->foldingIgnoreList; } QStringList Definition::keywordLists() const { d->load(); return d->keywordLists.keys(); } QStringList Definition::keywordList(const QString& name) const { d->load(); return d->keywordList(name).keywords(); } QVector Definition::formats() const { d->load(); // sort formats so that the order matches the order of the itemDatas in the xml files. auto formatList = QVector::fromList(d->formats.values()); std::sort(formatList.begin(), formatList.end(), [](const KSyntaxHighlighting::Format & lhs, const KSyntaxHighlighting::Format & rhs){ return lhs.id() < rhs.id(); }); return formatList; } QVector Definition::includedDefinitions() const { d->load(); QVector definitions; QQueue queue; queue.enqueue(*this); while (!queue.isEmpty()) { const auto definition = queue.dequeue(); definitions.push_back(definition); // Iterate all context rules to find associated Definitions. This will // automatically catch other Definitions referenced with IncludeRuldes. foreach (const auto & context, definition.d->contexts) { foreach (const auto &rule, context->rules()) { if ((!definitions.contains(rule->definition())) && (!queue.contains(rule->definition()))) { queue.enqueue(rule->definition()); } } } } // remove the 1st entry, since it is this Definition definitions.pop_front(); return definitions; } +QString Definition::singleLineCommentMarker() const +{ + d->load(); + return d->singleLineCommentMarker; +} + +CommentPosition Definition::singleLineCommentPosition() const +{ + d->load(); + return d->singleLineCommentPosition; +} + +QPair Definition::multiLineCommentMarker() const +{ + d->load(); + return { d->multiLineCommentStartMarker, d->multiLineCommentEndMarker }; +} + Context* DefinitionData::initialContext() const { Q_ASSERT(!contexts.isEmpty()); return contexts.first(); } Context* DefinitionData::contextByName(const QString& name) const { foreach (auto context, contexts) { if (context->name() == name) return context; } return nullptr; } KeywordList DefinitionData::keywordList(const QString& name) const { return keywordLists.value(name); } bool DefinitionData::isWordDelimiter(QChar c) const { return std::binary_search(wordDelimiters.constBegin(), wordDelimiters.constEnd(), c); } Format DefinitionData::formatByName(const QString& name) const { const auto it = formats.constFind(name); if (it != formats.constEnd()) return it.value(); return Format(); } bool DefinitionData::isLoaded() const { return !contexts.isEmpty(); } bool DefinitionData::load() { if (fileName.isEmpty()) return false; if (isLoaded()) return true; QFile file(fileName); if (!file.open(QFile::ReadOnly)) return false; QXmlStreamReader reader(&file); while (!reader.atEnd()) { const auto token = reader.readNext(); if (token != QXmlStreamReader::StartElement) continue; if (reader.name() == QLatin1String("highlighting")) loadHighlighting(reader); else if (reader.name() == QLatin1String("general")) loadGeneral(reader); } for (auto it = keywordLists.begin(); it != keywordLists.end(); ++it) (*it).setCaseSensitivity(caseSensitive); foreach (auto context, contexts) { context->resolveContexts(); context->resolveIncludes(); } Q_ASSERT(std::is_sorted(wordDelimiters.constBegin(), wordDelimiters.constEnd())); return true; } void DefinitionData::clear() { // keep only name and repo, so we can re-lookup to make references persist over repo reloads keywordLists.clear(); qDeleteAll(contexts); contexts.clear(); formats.clear(); fileName.clear(); section.clear(); style.clear(); indenter.clear(); author.clear(); license.clear(); mimetypes.clear(); extensions.clear(); wordDelimiters = QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~"); // must be sorted! wordWrapDelimiters = wordDelimiters; caseSensitive = Qt::CaseSensitive; version = 0.0f; priority = 0; hidden = false; } bool DefinitionData::loadMetaData(const QString& definitionFileName) { fileName = definitionFileName; QFile file(definitionFileName); if (!file.open(QFile::ReadOnly)) return false; QXmlStreamReader reader(&file); while (!reader.atEnd()) { const auto token = reader.readNext(); if (token != QXmlStreamReader::StartElement) continue; if (reader.name() == QLatin1String("language")) { return loadLanguage(reader); } } return false; } bool DefinitionData::loadMetaData(const QString &file, const QJsonObject &obj) { name = obj.value(QLatin1String("name")).toString(); section = obj.value(QLatin1String("section")).toString(); version = obj.value(QLatin1String("version")).toInt(); priority = obj.value(QLatin1String("priority")).toInt(); style = obj.value(QLatin1String("style")).toString(); author = obj.value(QLatin1String("author")).toString(); license = obj.value(QLatin1String("license")).toString(); indenter = obj.value(QLatin1String("indenter")).toString(); hidden = obj.value(QLatin1String("hidden")).toBool(); fileName = file; const auto exts = obj.value(QLatin1String("extensions")).toString(); foreach (const auto &ext, exts.split(QLatin1Char(';'), QString::SkipEmptyParts)) extensions.push_back(ext); const auto mts = obj.value(QLatin1String("mimetype")).toString(); foreach (const auto &mt, mts.split(QLatin1Char(';'), QString::SkipEmptyParts)) mimetypes.push_back(mt); return true; } bool DefinitionData::loadLanguage(QXmlStreamReader &reader) { Q_ASSERT(reader.name() == QLatin1String("language")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); if (!checkKateVersion(reader.attributes().value(QStringLiteral("kateversion")))) return false; name = reader.attributes().value(QStringLiteral("name")).toString(); section = reader.attributes().value(QStringLiteral("section")).toString(); // toFloat instead of toInt for backward compatibility with old Kate files version = reader.attributes().value(QStringLiteral("version")).toFloat(); priority = reader.attributes().value(QStringLiteral("priority")).toInt(); hidden = Xml::attrToBool(reader.attributes().value(QStringLiteral("hidden"))); style = reader.attributes().value(QStringLiteral("style")).toString(); indenter = reader.attributes().value(QStringLiteral("indenter")).toString(); author = reader.attributes().value(QStringLiteral("author")).toString(); license = reader.attributes().value(QStringLiteral("license")).toString(); const auto exts = reader.attributes().value(QStringLiteral("extensions")).toString(); foreach (const auto &ext, exts.split(QLatin1Char(';'), QString::SkipEmptyParts)) extensions.push_back(ext); const auto mts = reader.attributes().value(QStringLiteral("mimetype")).toString(); foreach (const auto &mt, mts.split(QLatin1Char(';'), QString::SkipEmptyParts)) mimetypes.push_back(mt); if (reader.attributes().hasAttribute(QStringLiteral("casesensitive"))) caseSensitive = Xml::attrToBool(reader.attributes().value(QStringLiteral("casesensitive"))) ? Qt::CaseSensitive : Qt::CaseInsensitive; return true; } void DefinitionData::loadHighlighting(QXmlStreamReader& reader) { Q_ASSERT(reader.name() == QLatin1String("highlighting")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: if (reader.name() == QLatin1String("list")) { KeywordList keywords; keywords.load(reader); keywordLists.insert(keywords.name(), keywords); } else if (reader.name() == QLatin1String("contexts")) { loadContexts(reader); reader.readNext(); } else if (reader.name() == QLatin1String("itemDatas")) { loadItemData(reader); } else { reader.readNext(); } break; case QXmlStreamReader::EndElement: return; default: reader.readNext(); break; } } } void DefinitionData::loadContexts(QXmlStreamReader& reader) { Q_ASSERT(reader.name() == QLatin1String("contexts")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: if (reader.name() == QLatin1String("context")) { auto context = new Context; context->setDefinition(q); context->load(reader); contexts.push_back(context); } reader.readNext(); break; case QXmlStreamReader::EndElement: return; default: reader.readNext(); break; } } } void DefinitionData::loadItemData(QXmlStreamReader& reader) { Q_ASSERT(reader.name() == QLatin1String("itemDatas")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: if (reader.name() == QLatin1String("itemData")) { Format f; auto formatData = FormatPrivate::get(f); formatData->definition = q; formatData->load(reader); formatData->id = RepositoryPrivate::get(repo)->nextFormatId(); formats.insert(f.name(), f); reader.readNext(); } reader.readNext(); break; case QXmlStreamReader::EndElement: return; default: reader.readNext(); break; } } } void DefinitionData::loadGeneral(QXmlStreamReader& reader) { Q_ASSERT(reader.name() == QLatin1String("general")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); reader.readNext(); // reference counter to count XML child elements, to not return too early int elementRefCounter = 1; while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: ++elementRefCounter; if (reader.name() == QLatin1String("keywords")) { if (reader.attributes().hasAttribute(QStringLiteral("casesensitive"))) caseSensitive = Xml::attrToBool(reader.attributes().value(QStringLiteral("casesensitive"))) ? Qt::CaseSensitive : Qt::CaseInsensitive; // adapt sorted wordDelimiters wordDelimiters += reader.attributes().value(QStringLiteral("additionalDeliminator")); std::sort(wordDelimiters.begin(), wordDelimiters.end()); auto it = std::unique(wordDelimiters.begin(), wordDelimiters.end()); wordDelimiters.truncate(std::distance(wordDelimiters.begin(), it)); foreach (const auto c, reader.attributes().value(QLatin1String("weakDeliminator"))) wordDelimiters.remove(c); // adaptWordWrapDelimiters, and sort wordWrapDelimiters = reader.attributes().value(QStringLiteral("wordWrapDeliminator")).toString(); std::sort(wordWrapDelimiters.begin(), wordWrapDelimiters.end()); if (wordWrapDelimiters.isEmpty()) wordWrapDelimiters = wordDelimiters; } else if (reader.name() == QLatin1String("folding")) { if (reader.attributes().hasAttribute(QStringLiteral("indentationsensitive"))) indentationBasedFolding = Xml::attrToBool(reader.attributes().value(QStringLiteral("indentationsensitive"))); } else if (reader.name() == QLatin1String("emptyLines")) { loadFoldingIgnoreList(reader); + } else if (reader.name() == QLatin1String("comments")) { + loadComments(reader); } else { reader.skipCurrentElement(); } reader.readNext(); break; case QXmlStreamReader::EndElement: --elementRefCounter; if (elementRefCounter == 0) return; reader.readNext(); break; default: reader.readNext(); break; } } } +void DefinitionData::loadComments(QXmlStreamReader &reader) +{ + Q_ASSERT(reader.name() == QLatin1String("comments")); + Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); + reader.readNext(); + + // reference counter to count XML child elements, to not return too early + int elementRefCounter = 1; + + while (!reader.atEnd()) { + switch (reader.tokenType()) { + case QXmlStreamReader::StartElement: + ++elementRefCounter; + if (reader.name() == QLatin1String("comment")) { + const bool isSingleLine = reader.attributes().value(QStringLiteral("name")) == QStringLiteral("singleLine"); + if (isSingleLine) { + singleLineCommentMarker = reader.attributes().value(QStringLiteral("start")).toString(); + const bool afterWhiteSpace = reader.attributes().value(QStringLiteral("position")).toString() == QStringLiteral("afterwhitespace"); + singleLineCommentPosition = afterWhiteSpace ? CommentPosition::AfterWhitespace : CommentPosition::StartOfLine; + } else { + multiLineCommentStartMarker = reader.attributes().value(QStringLiteral("start")).toString(); + multiLineCommentEndMarker = reader.attributes().value(QStringLiteral("end")).toString(); + } + } + reader.readNext(); + break; + case QXmlStreamReader::EndElement: + --elementRefCounter; + if (elementRefCounter == 0) + return; + reader.readNext(); + break; + default: + reader.readNext(); + break; + } + } +} + void DefinitionData::loadFoldingIgnoreList(QXmlStreamReader& reader) { Q_ASSERT(reader.name() == QLatin1String("emptyLines")); Q_ASSERT(reader.tokenType() == QXmlStreamReader::StartElement); reader.readNext(); // reference counter to count XML child elements, to not return too early int elementRefCounter = 1; while (!reader.atEnd()) { switch (reader.tokenType()) { case QXmlStreamReader::StartElement: ++elementRefCounter; if (reader.name() == QLatin1String("emptyLine")) { foldingIgnoreList << reader.attributes().value(QStringLiteral("regexpr")).toString(); } reader.readNext(); break; case QXmlStreamReader::EndElement: --elementRefCounter; if (elementRefCounter == 0) return; reader.readNext(); break; default: reader.readNext(); break; } } } bool DefinitionData::checkKateVersion(const QStringRef& verStr) { const auto idx = verStr.indexOf(QLatin1Char('.')); if (idx <= 0) { qCWarning(Log) << "Skipping" << fileName << "due to having no valid kateversion attribute:" << verStr; return false; } const auto major = verStr.left(idx).toInt(); const auto minor = verStr.mid(idx + 1).toInt(); if (major > SyntaxHighlighting_VERSION_MAJOR || (major == SyntaxHighlighting_VERSION_MAJOR && minor > SyntaxHighlighting_VERSION_MINOR)) { qCWarning(Log) << "Skipping" << fileName << "due to being too new, version:" << verStr; return false; } return true; } quint16 DefinitionData::foldingRegionId(const QString &foldName) { return RepositoryPrivate::get(repo)->foldingRegionId(name, foldName); } DefinitionRef::DefinitionRef() { } DefinitionRef::DefinitionRef(const Definition &def) : d(def.d) { } DefinitionRef::~DefinitionRef() { } DefinitionRef& DefinitionRef::operator=(const Definition &def) { d = def.d; return *this; } Definition DefinitionRef::definition() const { if (!d.expired()) return Definition(d.lock()); return Definition(); } diff --git a/src/lib/definition.h b/src/lib/definition.h index e7890a5..6bf11a2 100644 --- a/src/lib/definition.h +++ b/src/lib/definition.h @@ -1,272 +1,310 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KSYNTAXHIGHLIGHTING_DEFINITION_H #define KSYNTAXHIGHLIGHTING_DEFINITION_H #include "ksyntaxhighlighting_export.h" #include +#include #include class QChar; class QString; class QStringList; template class QVector; namespace KSyntaxHighlighting { class Context; class Format; class KeywordList; class DefinitionData; +/** + * Defines the insert position when commenting code. + * @since 5.50 + * @see Definition::singleLineCommentPosition() + */ +enum class CommentPosition +{ + //! The comment marker is inserted at the beginning of a line at column 0 + StartOfLine = 0, + //! The comment marker is inserted after leading whitespaces right befire + //! the first non-whitespace character. + AfterWhitespace +}; + /** * Represents a syntax definition. * * @section def_intro Introduction to Definitions * * A Definition is the short term for a syntax highlighting definition. It * typically is defined in terms of an XML syntax highlighting file, containing * all information about a particular syntax highlighting. This includes the * highlighting of keywords, information about code folding regions, and * indentation preferences. * * @section def_info General Information * * Each Definition contains a non-translated unique name() and a section(). * In addition, for putting this information e.g. into menus, the functions * translatedName() and translatedSection() are provided. However, if isHidden() * returns @e true, the Definition should not be visible in the UI. The location * of the Definition can be obtained through filePath(), which either is the * location on disk or a path to a compiled-in Qt resource. * * The supported files of a Definition are defined by the list of extensions(), * and additionally by the list of mimeTypes(). Note, that extensions() returns * wildcards that need to be matched against the filename of the file that * requires highlighting. If multiple Definition%s match the file, then the one * with higher priority() wins. * * @see Repository * @since 5.28 */ class KSYNTAXHIGHLIGHTING_EXPORT Definition { public: /** * Default constructor, creating an empty (invalid) Definition instance. * isValid() for this instance returns @e false. * * Use the Repository instead to obtain valid instances. */ Definition(); /** * Copy constructor. * Both this definition as well as @p other share the Definition data. */ Definition(const Definition &other); /** * Destructor. */ ~Definition(); /** * Assignment operator. * Both this definition as well as @p rhs share the Definition data. */ Definition& operator=(const Definition &rhs); /** * Checks two definitions for equality. */ bool operator==(const Definition &other) const; /** * Checks two definitions for inequality. */ bool operator!=(const Definition &other) const; /** Checks whether this object refers to a valid syntax definition. */ bool isValid() const; /** Returns the full path to the definition XML file containing * the syntax definition. Note that this can be a path to QRC content. */ QString filePath() const; /** Name of the syntax. * Used for internal references, prefer translatedName() for display. */ QString name() const; /** Translated name for display. */ QString translatedName() const; /** The group this syntax definition belongs to. * For display, consider translatedSection(). */ QString section() const; /** Translated group name for display. */ QString translatedSection() const; /** Mime types associated with this syntax definition. */ QVector mimeTypes() const; /** * File extensions associated with this syntax definition. * The returned list contains wildcards. */ QVector extensions() const; /** Returns the definition version. */ int version() const; /** * Returns the definition priority. * A Definition with higher priority wins over Definitions with lower priorities. */ int priority() const; /** Returns @c true if this is an internal definition that should not be * displayed to the user. */ bool isHidden() const; /** Generalized language style, used for indentation. */ QString style() const; /** Indentation style to be used for this syntax. */ QString indenter() const; /** Name and email of the author of this syntax definition. */ QString author() const; /** License of this syntax definition. */ QString license() const; /** * Returns whether the character @p c is a word delimiter. * A delimiter defines whether a characters is a word boundary. Internally, * delimiters are used for matching keyword lists. As example, typically the * dot '.' is a word delimiter. However, if you have a keyword in a keyword * list that contains a dot, you have to add the dot to the * @e weakDeliminator attribute of the @e general section in your * highlighting definition. Similarly, sometimes additional delimiters are * required, which can be specified in @e additionalDeliminator. * * Checking whether a character is a delimiter is useful for instance if * text is selected with double click. Typically, the whole word should be * selected in this case. Similarly to the example above, the dot '.' * usually acts as word delimiter. However, using this function you can * implement text selection in such a way that keyword lists are correctly * selected. * * @note By default, the list of delimiters contains the following * characters: \\t !%&()*+,-./:;<=>?[\\]^{|}~ * * @since 5.50 * @see isWordWrapDelimiter() */ bool isWordDelimiter(QChar c) const; /** * Returns whether it is safe to break a line at before the character @c. * This is useful when wrapping a line e.g. by applying static word wrap. * * As example, consider the LaTeX code * @code * \command1\command2 * @endcode * Applying static word wrap could lead to the following code: * @code * \command1\ * command2 * @endcode * command2 without a leading backslash is invalid in LaTeX. If '\\' is set * as word wrap delimiter, isWordWrapDelimiter('\\') then returns true, * meaning that it is safe to break the line before @c. The resulting code * then would be * @code * \command1 * \command2 * @endcode * * @note By default, the word wrap delimiters are equal to the word * delimiters in isWordDelimiter(). * * @since 5.50 * @see isWordDelimiter() */ bool isWordWrapDelimiter(QChar c) const; /** * Returns whether indentation-based folding is enabled. * An example for indentation-based folding is Python. * When indentation-based folding is enabled, make sure to also check * foldingIgnoreList() for lines that should be treated as empty. * * @see foldingIgnoreList(), State::indentationBasedFoldingEnabled() */ bool indentationBasedFoldingEnabled() const; /** * If indentationBasedFoldingEnabled() returns @c true, this function returns * a list of regular expressions that represent empty lines. That is, all * lines matching entirely one of the regular expressions should be treated * as empty lines when calculating the indentation-based folding ranges. * * @note This list is only of relevance, if indentationBasedFoldingEnabled() * returns @c true. * * @see indentationBasedFoldingEnabled() */ QStringList foldingIgnoreList() const; /** * Returns the section names of keywords. * @since 5.49 * @see keywordList() */ QStringList keywordLists() const; /** * Returns the list of keywords for the keyword list @p name. * @since 5.49 * @see keywordLists() */ QStringList keywordList(const QString& name) const; /** * Returns a list of all Format items used by this definition. * The order of the Format items equals the order of the itemDatas in the xml file. * @since 5.49 */ QVector formats() const; /** * Returns a list of Definitions that are referenced with the IncludeRules rule. * The returned list does not include this Definition. In case no other * Definitions are referenced via IncludeRules, the returned list is empty. * * @since 5.49 */ QVector includedDefinitions() const; + /** + * Returns the marker that starts a single line comment. + * For instance, in C++ the single line comment marker is "//". + * @since 5.50 + * @see singleLineCommentPosition(); + */ + QString singleLineCommentMarker() const; + + /** + * Returns the insert position of the comment marker for sinle line + * comments. + * @since 5.50 + * @see singleLineCommentMarker(); + */ + CommentPosition singleLineCommentPosition() const; + + /** + * Returns the markers that start and end multiline comments. + * For instance, in XML this is defined as "". + * @since 5.50 + */ + QPair multiLineCommentMarker() const; + private: friend class DefinitionData; friend class DefinitionRef; explicit Definition(const std::shared_ptr &dd); std::shared_ptr d; }; } Q_DECLARE_TYPEINFO(KSyntaxHighlighting::Definition, Q_MOVABLE_TYPE); #endif diff --git a/src/lib/definition_p.h b/src/lib/definition_p.h index 73b10aa..98bcfde 100644 --- a/src/lib/definition_p.h +++ b/src/lib/definition_p.h @@ -1,97 +1,102 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KSYNTAXHIGHLIGHTING_DEFINITION_P_H #define KSYNTAXHIGHLIGHTING_DEFINITION_P_H #include "definitionref_p.h" +#include "definition.h" #include #include #include QT_BEGIN_NAMESPACE class QXmlStreamReader; class QJsonObject; QT_END_NAMESPACE namespace KSyntaxHighlighting { -class Definition; class Repository; class DefinitionData { public: DefinitionData(); ~DefinitionData(); static DefinitionData* get(const Definition &def); bool isLoaded() const; bool loadMetaData(const QString &definitionFileName); bool loadMetaData(const QString &fileName, const QJsonObject &obj); void clear(); bool load(); bool loadLanguage(QXmlStreamReader &reader); void loadHighlighting(QXmlStreamReader &reader); void loadContexts(QXmlStreamReader &reader); void loadItemData(QXmlStreamReader &reader); void loadGeneral(QXmlStreamReader &reader); + void loadComments(QXmlStreamReader &reader); void loadFoldingIgnoreList(QXmlStreamReader &reader); bool checkKateVersion(const QStringRef &verStr); KeywordList keywordList(const QString &name) const; bool isWordDelimiter(QChar c) const; Context* initialContext() const; Context* contextByName(const QString &name) const; Format formatByName(const QString &name) const; quint16 foldingRegionId(const QString &foldName); DefinitionRef q; Repository *repo = nullptr; QHash keywordLists; QVector contexts; QHash formats; QString wordDelimiters; QString wordWrapDelimiters; bool indentationBasedFolding = false; QStringList foldingIgnoreList; + QString singleLineCommentMarker; + CommentPosition singleLineCommentPosition = CommentPosition::StartOfLine; + QString multiLineCommentStartMarker; + QString multiLineCommentEndMarker; QString fileName; QString name; QString section; QString style; QString indenter; QString author; QString license; QVector mimetypes; QVector extensions; Qt::CaseSensitivity caseSensitive = Qt::CaseSensitive; int version = 0; int priority = 0; bool hidden = false; }; } #endif