1 | /* |
2 | SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org> |
3 | |
4 | SPDX-License-Identifier: MIT |
5 | */ |
6 | |
7 | #include "repository.h" |
8 | #include "definition.h" |
9 | #include "definition_p.h" |
10 | #include "ksyntaxhighlighting_logging.h" |
11 | #include "repository_p.h" |
12 | #include "theme.h" |
13 | #include "themedata_p.h" |
14 | #include "wildcardmatcher.h" |
15 | |
16 | #include <QCborMap> |
17 | #include <QCborValue> |
18 | #include <QDirIterator> |
19 | #include <QFile> |
20 | #include <QFileInfo> |
21 | #include <QPalette> |
22 | #include <QString> |
23 | #include <QStringView> |
24 | |
25 | #ifndef NO_STANDARD_PATHS |
26 | #include <QStandardPaths> |
27 | #endif |
28 | |
29 | #include <algorithm> |
30 | #include <iterator> |
31 | #include <limits> |
32 | |
33 | using namespace KSyntaxHighlighting; |
34 | |
35 | namespace |
36 | { |
37 | QString fileNameFromFilePath(const QString &filePath) |
38 | { |
39 | return QFileInfo{filePath}.fileName(); |
40 | } |
41 | |
42 | auto anyWildcardMatches(QStringView str) |
43 | { |
44 | return [str](const Definition &def) { |
45 | const auto strings = def.extensions(); |
46 | return std::any_of(strings.cbegin(), strings.cend(), [str](QStringView wildcard) { |
47 | return WildcardMatcher::exactMatch(str, wildcard); |
48 | }); |
49 | }; |
50 | } |
51 | |
52 | auto anyMimeTypeEquals(QStringView mimeTypeName) |
53 | { |
54 | return [mimeTypeName](const Definition &def) { |
55 | const auto strings = def.mimeTypes(); |
56 | return std::any_of(strings.cbegin(), strings.cend(), [mimeTypeName](QStringView name) { |
57 | return mimeTypeName == name; |
58 | }); |
59 | }; |
60 | } |
61 | |
62 | // The two function templates below take defs - a map sorted by highlighting name - to be deterministic and independent of translations. |
63 | |
64 | template<typename UnaryPredicate> |
65 | Definition findHighestPriorityDefinitionIf(const QMap<QString, Definition> &defs, UnaryPredicate predicate) |
66 | { |
67 | const Definition *match = nullptr; |
68 | auto matchPriority = std::numeric_limits<int>::lowest(); |
69 | for (const Definition &def : defs) { |
70 | const auto defPriority = def.priority(); |
71 | if (defPriority > matchPriority && predicate(def)) { |
72 | match = &def; |
73 | matchPriority = defPriority; |
74 | } |
75 | } |
76 | return match == nullptr ? Definition{} : *match; |
77 | } |
78 | |
79 | template<typename UnaryPredicate> |
80 | QList<Definition> findDefinitionsIf(const QMap<QString, Definition> &defs, UnaryPredicate predicate) |
81 | { |
82 | QList<Definition> matches; |
83 | std::copy_if(defs.cbegin(), defs.cend(), std::back_inserter(matches), predicate); |
84 | std::stable_sort(matches.begin(), matches.end(), [](const Definition &lhs, const Definition &rhs) { |
85 | return lhs.priority() > rhs.priority(); |
86 | }); |
87 | return matches; |
88 | } |
89 | } // unnamed namespace |
90 | |
91 | static void initResource() |
92 | { |
93 | #ifdef HAS_SYNTAX_RESOURCE |
94 | Q_INIT_RESOURCE(syntax_data); |
95 | #endif |
96 | Q_INIT_RESOURCE(theme_data); |
97 | } |
98 | |
99 | RepositoryPrivate *RepositoryPrivate::get(Repository *repo) |
100 | { |
101 | return repo->d.get(); |
102 | } |
103 | |
104 | Repository::Repository() |
105 | : d(new RepositoryPrivate) |
106 | { |
107 | initResource(); |
108 | d->load(repo: this); |
109 | } |
110 | |
111 | Repository::~Repository() |
112 | { |
113 | // reset repo so we can detect in still alive definition instances |
114 | // that the repo was deleted |
115 | for (const auto &def : std::as_const(d->m_sortedDefs)) { |
116 | DefinitionData::get(def)->repo = nullptr; |
117 | } |
118 | } |
119 | |
120 | Definition Repository::definitionForName(const QString &defName) const |
121 | { |
122 | return d->m_defs.value(defName); |
123 | } |
124 | |
125 | Definition Repository::definitionForFileName(const QString &fileName) const |
126 | { |
127 | return findHighestPriorityDefinitionIf(d->m_defs, anyWildcardMatches(fileNameFromFilePath(fileName))); |
128 | } |
129 | |
130 | QList<Definition> Repository::definitionsForFileName(const QString &fileName) const |
131 | { |
132 | return findDefinitionsIf(d->m_defs, anyWildcardMatches(fileNameFromFilePath(fileName))); |
133 | } |
134 | |
135 | Definition Repository::definitionForMimeType(const QString &mimeType) const |
136 | { |
137 | return findHighestPriorityDefinitionIf(d->m_defs, anyMimeTypeEquals(mimeType)); |
138 | } |
139 | |
140 | QList<Definition> Repository::definitionsForMimeType(const QString &mimeType) const |
141 | { |
142 | return findDefinitionsIf(d->m_defs, anyMimeTypeEquals(mimeType)); |
143 | } |
144 | |
145 | QList<Definition> Repository::definitions() const |
146 | { |
147 | return d->m_sortedDefs; |
148 | } |
149 | |
150 | QList<Theme> Repository::themes() const |
151 | { |
152 | return d->m_themes; |
153 | } |
154 | |
155 | static auto lowerBoundTheme(const QList<KSyntaxHighlighting::Theme> &themes, QStringView themeName) |
156 | { |
157 | return std::lower_bound(themes.begin(), themes.end(), themeName, [](const Theme &lhs, QStringView rhs) { |
158 | return lhs.name() < rhs; |
159 | }); |
160 | } |
161 | |
162 | Theme Repository::theme(const QString &themeName) const |
163 | { |
164 | const auto &themes = d->m_themes; |
165 | const auto it = lowerBoundTheme(themes, themeName); |
166 | if (it != themes.end() && (*it).name() == themeName) { |
167 | return *it; |
168 | } |
169 | return Theme(); |
170 | } |
171 | |
172 | Theme Repository::defaultTheme(Repository::DefaultTheme t) const |
173 | { |
174 | if (t == DarkTheme) { |
175 | return theme(QStringLiteral("Breeze Dark" )); |
176 | } |
177 | return theme(QStringLiteral("Breeze Light" )); |
178 | } |
179 | |
180 | Theme Repository::themeForPalette(const QPalette &palette) const |
181 | { |
182 | const auto base = palette.color(QPalette::Base); |
183 | |
184 | // find themes with matching background colors |
185 | QList<const KSyntaxHighlighting::Theme *> matchingThemes; |
186 | for (const auto &theme : std::as_const(d->m_themes)) { |
187 | const auto background = theme.editorColor(KSyntaxHighlighting::Theme::EditorColorRole::BackgroundColor); |
188 | if (background == base.rgb()) { |
189 | matchingThemes.append(&theme); |
190 | } |
191 | } |
192 | if (!matchingThemes.empty()) { |
193 | // if there's multiple, search for one with a matching highlight color |
194 | const auto highlight = palette.color(QPalette::Highlight); |
195 | for (const auto *theme : std::as_const(matchingThemes)) { |
196 | auto selection = theme->editorColor(KSyntaxHighlighting::Theme::EditorColorRole::TextSelection); |
197 | if (selection == highlight.rgb()) { |
198 | return *theme; |
199 | } |
200 | } |
201 | return *matchingThemes.first(); |
202 | } |
203 | |
204 | // fallback to just use the default light or dark theme |
205 | return defaultTheme((base.lightness() < 128) ? KSyntaxHighlighting::Repository::DarkTheme : KSyntaxHighlighting::Repository::LightTheme); |
206 | } |
207 | |
208 | void RepositoryPrivate::load(Repository *repo) |
209 | { |
210 | // always add invalid default "None" highlighting |
211 | addDefinition(def: Definition()); |
212 | |
213 | // do lookup in standard paths, if not disabled |
214 | #ifndef NO_STANDARD_PATHS |
215 | // do lookup in installed path when has no syntax resource |
216 | #ifndef HAS_SYNTAX_RESOURCE |
217 | for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, |
218 | QStringLiteral("org.kde.syntax-highlighting/syntax-bundled" ), |
219 | QStandardPaths::LocateDirectory)) { |
220 | if (!loadSyntaxFolderFromIndex(repo, dir)) { |
221 | loadSyntaxFolder(repo, dir); |
222 | } |
223 | } |
224 | #endif |
225 | |
226 | for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, |
227 | QStringLiteral("org.kde.syntax-highlighting/syntax" ), |
228 | QStandardPaths::LocateDirectory)) { |
229 | loadSyntaxFolder(repo, dir); |
230 | } |
231 | |
232 | // backward compatibility with Kate |
233 | for (const auto &dir : |
234 | QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("katepart5/syntax" ), QStandardPaths::LocateDirectory)) { |
235 | loadSyntaxFolder(repo, dir); |
236 | } |
237 | #endif |
238 | |
239 | // default resources are always used, this is the one location that has a index cbor file |
240 | loadSyntaxFolderFromIndex(repo, path: QStringLiteral(":/org.kde.syntax-highlighting/syntax" )); |
241 | |
242 | // extra resources provided by 3rdparty libraries/applications |
243 | loadSyntaxFolder(repo, path: QStringLiteral(":/org.kde.syntax-highlighting/syntax-addons" )); |
244 | |
245 | // user given extra paths |
246 | for (const auto &path : std::as_const(m_customSearchPaths)) { |
247 | loadSyntaxFolder(repo, path + QStringLiteral("/syntax" )); |
248 | } |
249 | |
250 | m_sortedDefs.reserve(m_defs.size()); |
251 | for (auto it = m_defs.constBegin(); it != m_defs.constEnd(); ++it) { |
252 | m_sortedDefs.push_back(it.value()); |
253 | } |
254 | std::sort(m_sortedDefs.begin(), m_sortedDefs.end(), [](const Definition &left, const Definition &right) { |
255 | auto comparison = left.translatedSection().compare(right.translatedSection(), Qt::CaseInsensitive); |
256 | if (comparison == 0) { |
257 | comparison = left.translatedName().compare(right.translatedName(), Qt::CaseInsensitive); |
258 | } |
259 | return comparison < 0; |
260 | }); |
261 | |
262 | // load themes |
263 | |
264 | // do lookup in standard paths, if not disabled |
265 | #ifndef NO_STANDARD_PATHS |
266 | for (const auto &dir : QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, |
267 | QStringLiteral("org.kde.syntax-highlighting/themes" ), |
268 | QStandardPaths::LocateDirectory)) { |
269 | loadThemeFolder(dir); |
270 | } |
271 | #endif |
272 | |
273 | // default resources are always used |
274 | loadThemeFolder(path: QStringLiteral(":/org.kde.syntax-highlighting/themes" )); |
275 | |
276 | // extra resources provided by 3rdparty libraries/applications |
277 | loadThemeFolder(path: QStringLiteral(":/org.kde.syntax-highlighting/themes-addons" )); |
278 | |
279 | // user given extra paths |
280 | for (const auto &path : std::as_const(m_customSearchPaths)) { |
281 | loadThemeFolder(path + QStringLiteral("/themes" )); |
282 | } |
283 | } |
284 | |
285 | void RepositoryPrivate::loadSyntaxFolder(Repository *repo, const QString &path) |
286 | { |
287 | QDirIterator it(path, QStringList() << QLatin1String("*.xml" ), QDir::Files); |
288 | while (it.hasNext()) { |
289 | Definition def; |
290 | auto defData = DefinitionData::get(def); |
291 | defData->repo = repo; |
292 | if (defData->loadMetaData(it.next())) { |
293 | addDefinition(def); |
294 | } |
295 | } |
296 | } |
297 | |
298 | bool RepositoryPrivate::loadSyntaxFolderFromIndex(Repository *repo, const QString &path) |
299 | { |
300 | QFile indexFile(path + QLatin1String("/index.katesyntax" )); |
301 | if (!indexFile.open(QFile::ReadOnly)) { |
302 | return false; |
303 | } |
304 | |
305 | const auto indexDoc(QCborValue::fromCbor(indexFile.readAll())); |
306 | const auto index = indexDoc.toMap(); |
307 | for (auto it = index.begin(); it != index.end(); ++it) { |
308 | if (!it.value().isMap()) { |
309 | continue; |
310 | } |
311 | const auto fileName = QString(path + QLatin1Char('/') + it.key().toString()); |
312 | const auto defMap = it.value().toMap(); |
313 | Definition def; |
314 | auto defData = DefinitionData::get(def); |
315 | defData->repo = repo; |
316 | if (defData->loadMetaData(fileName, defMap)) { |
317 | addDefinition(def); |
318 | } |
319 | } |
320 | |
321 | return true; |
322 | } |
323 | |
324 | void RepositoryPrivate::addDefinition(const Definition &def) |
325 | { |
326 | const auto it = m_defs.constFind(def.name()); |
327 | if (it == m_defs.constEnd()) { |
328 | m_defs.insert(def.name(), def); |
329 | return; |
330 | } |
331 | |
332 | if (it.value().version() >= def.version()) { |
333 | return; |
334 | } |
335 | m_defs.insert(def.name(), def); |
336 | } |
337 | |
338 | void RepositoryPrivate::loadThemeFolder(const QString &path) |
339 | { |
340 | QDirIterator it(path, QStringList() << QLatin1String("*.theme" ), QDir::Files); |
341 | while (it.hasNext()) { |
342 | auto themeData = std::unique_ptr<ThemeData>(new ThemeData); |
343 | if (themeData->load(filePath: it.next())) { |
344 | addTheme(theme: Theme(themeData.release())); |
345 | } |
346 | } |
347 | } |
348 | |
349 | static int themeRevision(const Theme &theme) |
350 | { |
351 | auto data = ThemeData::get(theme); |
352 | return data->revision(); |
353 | } |
354 | |
355 | void RepositoryPrivate::addTheme(const Theme &theme) |
356 | { |
357 | const auto &constThemes = m_themes; |
358 | const auto themeName = theme.name(); |
359 | const auto constIt = lowerBoundTheme(constThemes, themeName); |
360 | const auto index = constIt - constThemes.begin(); |
361 | if (constIt == constThemes.end() || (*constIt).name() != themeName) { |
362 | m_themes.insert(index, theme); |
363 | return; |
364 | } |
365 | if (themeRevision(*constIt) < themeRevision(theme)) { |
366 | m_themes[index] = theme; |
367 | } |
368 | } |
369 | |
370 | int RepositoryPrivate::foldingRegionId(const QString &defName, const QString &foldName) |
371 | { |
372 | const auto it = m_foldingRegionIds.constFind(qMakePair(defName, foldName)); |
373 | if (it != m_foldingRegionIds.constEnd()) { |
374 | return it.value(); |
375 | } |
376 | Q_ASSERT(m_foldingRegionId < std::numeric_limits<int>::max()); |
377 | m_foldingRegionIds.insert(qMakePair(defName, foldName), ++m_foldingRegionId); |
378 | return m_foldingRegionId; |
379 | } |
380 | |
381 | int RepositoryPrivate::nextFormatId() |
382 | { |
383 | Q_ASSERT(m_formatId < std::numeric_limits<int>::max()); |
384 | return ++m_formatId; |
385 | } |
386 | |
387 | void Repository::reload() |
388 | { |
389 | Q_EMIT aboutToReload(); |
390 | |
391 | for (const auto &def : std::as_const(d->m_sortedDefs)) { |
392 | DefinitionData::get(def)->clear(); |
393 | } |
394 | d->m_defs.clear(); |
395 | d->m_sortedDefs.clear(); |
396 | |
397 | d->m_themes.clear(); |
398 | |
399 | d->m_foldingRegionId = 0; |
400 | d->m_foldingRegionIds.clear(); |
401 | |
402 | d->m_formatId = 0; |
403 | |
404 | d->load(repo: this); |
405 | |
406 | Q_EMIT reloaded(); |
407 | } |
408 | |
409 | void Repository::addCustomSearchPath(const QString &path) |
410 | { |
411 | d->m_customSearchPaths.append(path); |
412 | reload(); |
413 | } |
414 | |
415 | QList<QString> Repository::customSearchPaths() const |
416 | { |
417 | return d->m_customSearchPaths; |
418 | } |
419 | |
420 | #include "moc_repository.cpp" |
421 | |