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