File indexing completed on 2024-05-12 15:50:05

0001 /*
0002     SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: MIT
0005 */
0006 
0007 #include "repository.h"
0008 #include "definition.h"
0009 #include "definition_p.h"
0010 #include "ksyntaxhighlighting_logging.h"
0011 #include "repository_p.h"
0012 #include "theme.h"
0013 #include "themedata_p.h"
0014 #include "wildcardmatcher.h"
0015 
0016 #include <QCborMap>
0017 #include <QCborValue>
0018 #include <QDirIterator>
0019 #include <QFile>
0020 #include <QFileInfo>
0021 #include <QPalette>
0022 #include <QString>
0023 #include <QStringView>
0024 
0025 #ifndef NO_STANDARD_PATHS
0026 #include <QStandardPaths>
0027 #endif
0028 
0029 #include <algorithm>
0030 #include <iterator>
0031 #include <limits>
0032 
0033 using namespace KSyntaxHighlighting;
0034 
0035 namespace
0036 {
0037 QString fileNameFromFilePath(const QString &filePath)
0038 {
0039     return QFileInfo{filePath}.fileName();
0040 }
0041 
0042 auto anyWildcardMatches(QStringView str)
0043 {
0044     return [str](const Definition &def) {
0045         const auto strings = def.extensions();
0046         return std::any_of(strings.cbegin(), strings.cend(), [str](QStringView wildcard) {
0047             return WildcardMatcher::exactMatch(str, wildcard);
0048         });
0049     };
0050 }
0051 
0052 auto anyMimeTypeEquals(QStringView mimeTypeName)
0053 {
0054     return [mimeTypeName](const Definition &def) {
0055         const auto strings = def.mimeTypes();
0056         return std::any_of(strings.cbegin(), strings.cend(), [mimeTypeName](QStringView name) {
0057             return mimeTypeName == name;
0058         });
0059     };
0060 }
0061 
0062 // The two function templates below take defs - a map sorted by highlighting name - to be deterministic and independent of translations.
0063 
0064 template<typename UnaryPredicate>
0065 Definition findHighestPriorityDefinitionIf(const QMap<QString, Definition> &defs, UnaryPredicate predicate)
0066 {
0067     const Definition *match = nullptr;
0068     auto matchPriority = std::numeric_limits<int>::lowest();
0069     for (const Definition &def : defs) {
0070         const auto defPriority = def.priority();
0071         if (defPriority > matchPriority && predicate(def)) {
0072             match = &def;
0073             matchPriority = defPriority;
0074         }
0075     }
0076     return match == nullptr ? Definition{} : *match;
0077 }
0078 
0079 template<typename UnaryPredicate>
0080 QVector<Definition> findDefinitionsIf(const QMap<QString, Definition> &defs, UnaryPredicate predicate)
0081 {
0082     QVector<Definition> matches;
0083     std::copy_if(defs.cbegin(), defs.cend(), std::back_inserter(matches), predicate);
0084     std::stable_sort(matches.begin(), matches.end(), [](const Definition &lhs, const Definition &rhs) {
0085         return lhs.priority() > rhs.priority();
0086     });
0087     return matches;
0088 }
0089 } // unnamed namespace
0090 
0091 static void initResource()
0092 {
0093 #ifdef HAS_SYNTAX_RESOURCE
0094     Q_INIT_RESOURCE(syntax_data);
0095 #endif
0096     Q_INIT_RESOURCE(theme_data);
0097 }
0098 
0099 RepositoryPrivate *RepositoryPrivate::get(Repository *repo)
0100 {
0101     return repo->d.get();
0102 }
0103 
0104 Repository::Repository()
0105     : d(new RepositoryPrivate)
0106 {
0107     initResource();
0108     d->load(this);
0109 }
0110 
0111 Repository::~Repository()
0112 {
0113     // reset repo so we can detect in still alive definition instances
0114     // that the repo was deleted
0115     for (const auto &def : std::as_const(d->m_sortedDefs)) {
0116         DefinitionData::get(def)->repo = nullptr;
0117     }
0118 }
0119 
0120 Definition Repository::definitionForName(const QString &defName) const
0121 {
0122     return d->m_defs.value(defName);
0123 }
0124 
0125 Definition Repository::definitionForFileName(const QString &fileName) const
0126 {
0127     return findHighestPriorityDefinitionIf(d->m_defs, anyWildcardMatches(fileNameFromFilePath(fileName)));
0128 }
0129 
0130 QVector<Definition> Repository::definitionsForFileName(const QString &fileName) const
0131 {
0132     return findDefinitionsIf(d->m_defs, anyWildcardMatches(fileNameFromFilePath(fileName)));
0133 }
0134 
0135 Definition Repository::definitionForMimeType(const QString &mimeType) const
0136 {
0137     return findHighestPriorityDefinitionIf(d->m_defs, anyMimeTypeEquals(mimeType));
0138 }
0139 
0140 QVector<Definition> Repository::definitionsForMimeType(const QString &mimeType) const
0141 {
0142     return findDefinitionsIf(d->m_defs, anyMimeTypeEquals(mimeType));
0143 }
0144 
0145 QVector<Definition> Repository::definitions() const
0146 {
0147     return d->m_sortedDefs;
0148 }
0149 
0150 QVector<Theme> Repository::themes() const
0151 {
0152     return d->m_themes;
0153 }
0154 
0155 static auto lowerBoundTheme(const QVector<KSyntaxHighlighting::Theme> &themes, QStringView themeName)
0156 {
0157     return std::lower_bound(themes.begin(), themes.end(), themeName, [](const Theme &lhs, QStringView rhs) {
0158         return lhs.name() < rhs;
0159     });
0160 }
0161 
0162 Theme Repository::theme(const QString &themeName) const
0163 {
0164     const auto &themes = d->m_themes;
0165     const auto it = lowerBoundTheme(themes, themeName);
0166     if (it != themes.end() && (*it).name() == themeName) {
0167         return *it;
0168     }
0169     return Theme();
0170 }
0171 
0172 Theme Repository::defaultTheme(Repository::DefaultTheme t) const
0173 {
0174     if (t == DarkTheme) {
0175         return theme(QStringLiteral("Breeze Dark"));
0176     }
0177     return theme(QStringLiteral("Breeze Light"));
0178 }
0179 
0180 Theme Repository::defaultTheme(Repository::DefaultTheme t)
0181 {
0182     return std::as_const(*this).defaultTheme(t);
0183 }
0184 
0185 Theme Repository::themeForPalette(const QPalette &palette) const
0186 {
0187     const auto base = palette.color(QPalette::Base);
0188 
0189     // find themes with matching background colors
0190     QVector<const KSyntaxHighlighting::Theme *> matchingThemes;
0191     for (const auto &theme : std::as_const(d->m_themes)) {
0192         const auto background = theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::BackgroundColor);
0193         if (background == base.rgb()) {
0194             matchingThemes.append(&theme);
0195         }
0196     }
0197     if (!matchingThemes.empty()) {
0198         // if there's multiple, search for one with a matching highlight color
0199         const auto highlight = palette.color(QPalette::Highlight);
0200         for (const auto *theme : std::as_const(matchingThemes)) {
0201             auto selection = theme->editorColor(KSyntaxHighlighting::Theme::EditorColorRole::TextSelection);
0202             if (selection == highlight.rgb()) {
0203                 return *theme;
0204             }
0205         }
0206         return *matchingThemes.first();
0207     }
0208 
0209     // fallback to just use the default light or dark theme
0210     return defaultTheme((base.lightness() < 128) ? KSyntaxHighlighting::Repository::DarkTheme : KSyntaxHighlighting::Repository::LightTheme);
0211 }
0212 
0213 Theme Repository::themeForPalette(const QPalette &palette)
0214 {
0215     return std::as_const(*this).themeForPalette(palette);
0216 }
0217 
0218 void RepositoryPrivate::load(Repository *repo)
0219 {
0220     // always add invalid default "None" highlighting
0221     addDefinition(Definition());
0222 
0223     // do lookup in standard paths, if not disabled
0224 #ifndef NO_STANDARD_PATHS
0225     // do lookup in installed path when has no syntax resource
0226 #ifndef HAS_SYNTAX_RESOURCE
0227     for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
0228                                                      QStringLiteral("org.kde.syntax-highlighting/syntax-bundled"),
0229                                                      QStandardPaths::LocateDirectory)) {
0230         if (!loadSyntaxFolderFromIndex(repo, dir)) {
0231             loadSyntaxFolder(repo, dir);
0232         }
0233     }
0234 #endif
0235 
0236     for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
0237                                                      QStringLiteral("org.kde.syntax-highlighting/syntax"),
0238                                                      QStandardPaths::LocateDirectory)) {
0239         loadSyntaxFolder(repo, dir);
0240     }
0241 
0242     // backward compatibility with Kate
0243     for (const auto &dir :
0244          QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("katepart5/syntax"), QStandardPaths::LocateDirectory)) {
0245         loadSyntaxFolder(repo, dir);
0246     }
0247 #endif
0248 
0249     // default resources are always used, this is the one location that has a index cbor file
0250     loadSyntaxFolderFromIndex(repo, QStringLiteral(":/org.kde.syntax-highlighting/syntax"));
0251 
0252     // extra resources provided by 3rdparty libraries/applications
0253     loadSyntaxFolder(repo, QStringLiteral(":/org.kde.syntax-highlighting/syntax-addons"));
0254 
0255     // user given extra paths
0256     for (const auto &path : std::as_const(m_customSearchPaths)) {
0257         loadSyntaxFolder(repo, path + QStringLiteral("/syntax"));
0258     }
0259 
0260     m_sortedDefs.reserve(m_defs.size());
0261     for (auto it = m_defs.constBegin(); it != m_defs.constEnd(); ++it) {
0262         m_sortedDefs.push_back(it.value());
0263     }
0264     std::sort(m_sortedDefs.begin(), m_sortedDefs.end(), [](const Definition &left, const Definition &right) {
0265         auto comparison = left.translatedSection().compare(right.translatedSection(), Qt::CaseInsensitive);
0266         if (comparison == 0) {
0267             comparison = left.translatedName().compare(right.translatedName(), Qt::CaseInsensitive);
0268         }
0269         return comparison < 0;
0270     });
0271 
0272     // load themes
0273 
0274     // do lookup in standard paths, if not disabled
0275 #ifndef NO_STANDARD_PATHS
0276     for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
0277                                                      QStringLiteral("org.kde.syntax-highlighting/themes"),
0278                                                      QStandardPaths::LocateDirectory)) {
0279         loadThemeFolder(dir);
0280     }
0281 #endif
0282 
0283     // default resources are always used
0284     loadThemeFolder(QStringLiteral(":/org.kde.syntax-highlighting/themes"));
0285 
0286     // extra resources provided by 3rdparty libraries/applications
0287     loadThemeFolder(QStringLiteral(":/org.kde.syntax-highlighting/themes-addons"));
0288 
0289     // user given extra paths
0290     for (const auto &path : std::as_const(m_customSearchPaths)) {
0291         loadThemeFolder(path + QStringLiteral("/themes"));
0292     }
0293 }
0294 
0295 void RepositoryPrivate::loadSyntaxFolder(Repository *repo, const QString &path)
0296 {
0297     QDirIterator it(path, QStringList() << QLatin1String("*.xml"), QDir::Files);
0298     while (it.hasNext()) {
0299         Definition def;
0300         auto defData = DefinitionData::get(def);
0301         defData->repo = repo;
0302         if (defData->loadMetaData(it.next())) {
0303             addDefinition(def);
0304         }
0305     }
0306 }
0307 
0308 bool RepositoryPrivate::loadSyntaxFolderFromIndex(Repository *repo, const QString &path)
0309 {
0310     QFile indexFile(path + QLatin1String("/index.katesyntax"));
0311     if (!indexFile.open(QFile::ReadOnly)) {
0312         return false;
0313     }
0314 
0315     const auto indexDoc(QCborValue::fromCbor(indexFile.readAll()));
0316     const auto index = indexDoc.toMap();
0317     for (auto it = index.begin(); it != index.end(); ++it) {
0318         if (!it.value().isMap()) {
0319             continue;
0320         }
0321         const auto fileName = QString(path + QLatin1Char('/') + it.key().toString());
0322         const auto defMap = it.value().toMap();
0323         Definition def;
0324         auto defData = DefinitionData::get(def);
0325         defData->repo = repo;
0326         if (defData->loadMetaData(fileName, defMap)) {
0327             addDefinition(def);
0328         }
0329     }
0330 
0331     return true;
0332 }
0333 
0334 void RepositoryPrivate::addDefinition(const Definition &def)
0335 {
0336     const auto it = m_defs.constFind(def.name());
0337     if (it == m_defs.constEnd()) {
0338         m_defs.insert(def.name(), def);
0339         return;
0340     }
0341 
0342     if (it.value().version() >= def.version()) {
0343         return;
0344     }
0345     m_defs.insert(def.name(), def);
0346 }
0347 
0348 void RepositoryPrivate::loadThemeFolder(const QString &path)
0349 {
0350     QDirIterator it(path, QStringList() << QLatin1String("*.theme"), QDir::Files);
0351     while (it.hasNext()) {
0352         auto themeData = std::unique_ptr<ThemeData>(new ThemeData);
0353         if (themeData->load(it.next())) {
0354             addTheme(Theme(themeData.release()));
0355         }
0356     }
0357 }
0358 
0359 static int themeRevision(const Theme &theme)
0360 {
0361     auto data = ThemeData::get(theme);
0362     return data->revision();
0363 }
0364 
0365 void RepositoryPrivate::addTheme(const Theme &theme)
0366 {
0367     const auto &constThemes = m_themes;
0368     const auto themeName = theme.name();
0369     const auto constIt = lowerBoundTheme(constThemes, themeName);
0370     const auto index = constIt - constThemes.begin();
0371     if (constIt == constThemes.end() || (*constIt).name() != themeName) {
0372         m_themes.insert(index, theme);
0373         return;
0374     }
0375     if (themeRevision(*constIt) < themeRevision(theme)) {
0376         m_themes[index] = theme;
0377     }
0378 }
0379 
0380 quint16 RepositoryPrivate::foldingRegionId(const QString &defName, const QString &foldName)
0381 {
0382     const auto it = m_foldingRegionIds.constFind(qMakePair(defName, foldName));
0383     if (it != m_foldingRegionIds.constEnd()) {
0384         return it.value();
0385     }
0386     m_foldingRegionIds.insert(qMakePair(defName, foldName), ++m_foldingRegionId);
0387     return m_foldingRegionId;
0388 }
0389 
0390 quint16 RepositoryPrivate::nextFormatId()
0391 {
0392     Q_ASSERT(m_formatId < std::numeric_limits<quint16>::max());
0393     return ++m_formatId;
0394 }
0395 
0396 void Repository::reload()
0397 {
0398     qCDebug(Log) << "Reloading syntax definitions!";
0399     for (const auto &def : std::as_const(d->m_sortedDefs)) {
0400         DefinitionData::get(def)->clear();
0401     }
0402     d->m_defs.clear();
0403     d->m_sortedDefs.clear();
0404 
0405     d->m_themes.clear();
0406 
0407     d->m_foldingRegionId = 0;
0408     d->m_foldingRegionIds.clear();
0409 
0410     d->m_formatId = 0;
0411 
0412     d->load(this);
0413 }
0414 
0415 void Repository::addCustomSearchPath(const QString &path)
0416 {
0417     d->m_customSearchPaths.append(path);
0418     reload();
0419 }
0420 
0421 QVector<QString> Repository::customSearchPaths() const
0422 {
0423     return d->m_customSearchPaths;
0424 }