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(first: strings.cbegin(), last: strings.cend(), pred: [str](QStringView wildcard) { |
47 | return WildcardMatcher::exactMatch(candidate: 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(first: strings.cbegin(), last: strings.cend(), pred: [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(x&: 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(t&: 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_fullDefs.value(key: defName.toLower()); |
123 | } |
124 | |
125 | Definition Repository::definitionForFileName(const QString &fileName) const |
126 | { |
127 | return findHighestPriorityDefinitionIf(defs: d->m_defs, predicate: anyWildcardMatches(str: fileNameFromFilePath(filePath: fileName))); |
128 | } |
129 | |
130 | QList<Definition> Repository::definitionsForFileName(const QString &fileName) const |
131 | { |
132 | return findDefinitionsIf(defs: d->m_defs, predicate: anyWildcardMatches(str: fileNameFromFilePath(filePath: fileName))); |
133 | } |
134 | |
135 | Definition Repository::definitionForMimeType(const QString &mimeType) const |
136 | { |
137 | return findHighestPriorityDefinitionIf(defs: d->m_defs, predicate: anyMimeTypeEquals(mimeTypeName: mimeType)); |
138 | } |
139 | |
140 | QList<Definition> Repository::definitionsForMimeType(const QString &mimeType) const |
141 | { |
142 | return findDefinitionsIf(defs: d->m_defs, predicate: anyMimeTypeEquals(mimeTypeName: 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(first: themes.begin(), last: themes.end(), val: themeName, comp: [](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(cr: QPalette::Base); |
183 | const auto highlight = palette.color(cr: QPalette::Highlight).rgb(); |
184 | |
185 | // find themes with matching background and highlight colors |
186 | const Theme *firstMatchingTheme = nullptr; |
187 | for (const auto &theme : std::as_const(t&: d->m_themes)) { |
188 | const auto background = theme.editorColor(role: Theme::EditorColorRole::BackgroundColor); |
189 | if (background == base.rgb()) { |
190 | // find theme with a matching highlight color |
191 | auto selection = theme.editorColor(role: Theme::EditorColorRole::TextSelection); |
192 | if (selection == highlight) { |
193 | return theme; |
194 | } |
195 | if (!firstMatchingTheme) { |
196 | firstMatchingTheme = &theme; |
197 | } |
198 | } |
199 | } |
200 | if (firstMatchingTheme) { |
201 | return *firstMatchingTheme; |
202 | } |
203 | |
204 | // fallback to just use the default light or dark theme |
205 | return defaultTheme(t: (base.lightness() < 128) ? Repository::DarkTheme : 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(type: QStandardPaths::GenericDataLocation, |
227 | QStringLiteral("org.kde.syntax-highlighting/syntax" ), |
228 | options: QStandardPaths::LocateDirectory)) { |
229 | loadSyntaxFolder(repo, path: dir); |
230 | } |
231 | #endif |
232 | |
233 | // default resources are always used, this is the one location that has a index cbor file |
234 | loadSyntaxFolderFromIndex(repo, QStringLiteral(":/org.kde.syntax-highlighting/syntax" )); |
235 | |
236 | // extra resources provided by 3rdparty libraries/applications |
237 | loadSyntaxFolder(repo, QStringLiteral(":/org.kde.syntax-highlighting/syntax-addons" )); |
238 | |
239 | // user given extra paths |
240 | for (const auto &path : std::as_const(t&: m_customSearchPaths)) { |
241 | loadSyntaxFolder(repo, path: path + QStringLiteral("/syntax" )); |
242 | } |
243 | |
244 | m_sortedDefs.reserve(asize: m_defs.size()); |
245 | for (auto it = m_defs.constBegin(); it != m_defs.constEnd(); ++it) { |
246 | m_sortedDefs.push_back(t: it.value()); |
247 | } |
248 | std::sort(first: m_sortedDefs.begin(), last: m_sortedDefs.end(), comp: [](const Definition &left, const Definition &right) { |
249 | auto comparison = left.translatedSection().compare(s: right.translatedSection(), cs: Qt::CaseInsensitive); |
250 | if (comparison == 0) { |
251 | comparison = left.translatedName().compare(s: right.translatedName(), cs: Qt::CaseInsensitive); |
252 | } |
253 | return comparison < 0; |
254 | }); |
255 | |
256 | for (auto it = m_sortedDefs.constBegin(); it != m_sortedDefs.constEnd(); ++it) { |
257 | m_fullDefs.insert(key: it->name().toLower(), value: *it); |
258 | for (const auto &altName : it->alternativeNames()) { |
259 | m_fullDefs.insert(key: altName.toLower(), value: *it); |
260 | } |
261 | } |
262 | |
263 | // load themes |
264 | |
265 | // do lookup in standard paths, if not disabled |
266 | #ifndef NO_STANDARD_PATHS |
267 | for (const auto &dir : QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, |
268 | QStringLiteral("org.kde.syntax-highlighting/themes" ), |
269 | options: QStandardPaths::LocateDirectory)) { |
270 | loadThemeFolder(path: dir); |
271 | } |
272 | #endif |
273 | |
274 | // default resources are always used |
275 | loadThemeFolder(QStringLiteral(":/org.kde.syntax-highlighting/themes" )); |
276 | |
277 | // extra resources provided by 3rdparty libraries/applications |
278 | loadThemeFolder(QStringLiteral(":/org.kde.syntax-highlighting/themes-addons" )); |
279 | |
280 | // user given extra paths |
281 | for (const auto &path : std::as_const(t&: m_customSearchPaths)) { |
282 | loadThemeFolder(path: path + QStringLiteral("/themes" )); |
283 | } |
284 | } |
285 | |
286 | void RepositoryPrivate::loadSyntaxFolder(Repository *repo, const QString &path) |
287 | { |
288 | QDirIterator it(path, QStringList() << QLatin1String("*.xml" ), QDir::Files); |
289 | while (it.hasNext()) { |
290 | Definition def; |
291 | auto defData = DefinitionData::get(def); |
292 | defData->repo = repo; |
293 | if (defData->loadMetaData(definitionFileName: it.next())) { |
294 | addDefinition(def); |
295 | } |
296 | } |
297 | } |
298 | |
299 | bool RepositoryPrivate::loadSyntaxFolderFromIndex(Repository *repo, const QString &path) |
300 | { |
301 | QFile indexFile(path + QLatin1String("/index.katesyntax" )); |
302 | if (!indexFile.open(flags: QFile::ReadOnly)) { |
303 | return false; |
304 | } |
305 | |
306 | const auto indexDoc(QCborValue::fromCbor(ba: indexFile.readAll())); |
307 | const auto index = indexDoc.toMap(); |
308 | for (auto it = index.begin(); it != index.end(); ++it) { |
309 | if (!it.value().isMap()) { |
310 | continue; |
311 | } |
312 | const auto fileName = QString(path + QLatin1Char('/') + it.key().toString()); |
313 | const auto defMap = it.value().toMap(); |
314 | Definition def; |
315 | auto defData = DefinitionData::get(def); |
316 | defData->repo = repo; |
317 | if (defData->loadMetaData(fileName, obj: defMap)) { |
318 | addDefinition(def); |
319 | } |
320 | } |
321 | |
322 | return true; |
323 | } |
324 | |
325 | void RepositoryPrivate::addDefinition(const Definition &def) |
326 | { |
327 | const auto it = m_defs.constFind(key: def.name()); |
328 | if (it == m_defs.constEnd()) { |
329 | m_defs.insert(key: def.name(), value: def); |
330 | return; |
331 | } |
332 | |
333 | if (it.value().version() >= def.version()) { |
334 | return; |
335 | } |
336 | m_defs.insert(key: def.name(), value: def); |
337 | } |
338 | |
339 | void RepositoryPrivate::loadThemeFolder(const QString &path) |
340 | { |
341 | QDirIterator it(path, QStringList() << QLatin1String("*.theme" ), QDir::Files); |
342 | while (it.hasNext()) { |
343 | auto themeData = std::unique_ptr<ThemeData>(new ThemeData); |
344 | if (themeData->load(filePath: it.next())) { |
345 | addTheme(theme: Theme(themeData.release())); |
346 | } |
347 | } |
348 | } |
349 | |
350 | static int themeRevision(const Theme &theme) |
351 | { |
352 | auto data = ThemeData::get(theme); |
353 | return data->revision(); |
354 | } |
355 | |
356 | void RepositoryPrivate::addTheme(const Theme &theme) |
357 | { |
358 | const auto &constThemes = m_themes; |
359 | const auto themeName = theme.name(); |
360 | const auto constIt = lowerBoundTheme(themes: constThemes, themeName); |
361 | const auto index = constIt - constThemes.begin(); |
362 | if (constIt == constThemes.end() || (*constIt).name() != themeName) { |
363 | m_themes.insert(i: index, t: theme); |
364 | return; |
365 | } |
366 | if (themeRevision(theme: *constIt) < themeRevision(theme)) { |
367 | m_themes[index] = theme; |
368 | } |
369 | } |
370 | |
371 | int RepositoryPrivate::foldingRegionId(const QString &defName, const QString &foldName) |
372 | { |
373 | const auto it = m_foldingRegionIds.constFind(key: qMakePair(value1: defName, value2: foldName)); |
374 | if (it != m_foldingRegionIds.constEnd()) { |
375 | return it.value(); |
376 | } |
377 | Q_ASSERT(m_foldingRegionId < std::numeric_limits<int>::max()); |
378 | m_foldingRegionIds.insert(key: qMakePair(value1: defName, value2: foldName), value: ++m_foldingRegionId); |
379 | return m_foldingRegionId; |
380 | } |
381 | |
382 | int RepositoryPrivate::nextFormatId() |
383 | { |
384 | Q_ASSERT(m_formatId < std::numeric_limits<int>::max()); |
385 | return ++m_formatId; |
386 | } |
387 | |
388 | void Repository::reload() |
389 | { |
390 | Q_EMIT aboutToReload(); |
391 | |
392 | for (const auto &def : std::as_const(t&: d->m_sortedDefs)) { |
393 | DefinitionData::get(def)->clear(); |
394 | } |
395 | d->m_defs.clear(); |
396 | d->m_sortedDefs.clear(); |
397 | d->m_fullDefs.clear(); |
398 | |
399 | d->m_themes.clear(); |
400 | |
401 | d->m_foldingRegionId = 0; |
402 | d->m_foldingRegionIds.clear(); |
403 | |
404 | d->m_formatId = 0; |
405 | |
406 | d->load(repo: this); |
407 | |
408 | Q_EMIT reloaded(); |
409 | } |
410 | |
411 | void Repository::addCustomSearchPath(const QString &path) |
412 | { |
413 | d->m_customSearchPaths.append(t: path); |
414 | reload(); |
415 | } |
416 | |
417 | QList<QString> Repository::customSearchPaths() const |
418 | { |
419 | return d->m_customSearchPaths; |
420 | } |
421 | |
422 | #include "moc_repository.cpp" |
423 | |