-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpatch_theme.py
More file actions
391 lines (338 loc) · 13.4 KB
/
Copy pathpatch_theme.py
File metadata and controls
391 lines (338 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
import os
with open('lib/core/theme/theme_registry_service.dart', 'r') as f:
content = f.read()
# 1. Imports
target_imports = """import 'theme_metadata.dart';
import 'theme_paths.dart';"""
new_imports = """import 'theme_metadata.dart';
import 'theme_paths.dart';
import '../extensions/local_extension_registry.dart';
import '../extensions/extension_paths.dart';
import '../extensions/models/extension_manifest.dart';
import '../extensions/models/extension_type.dart';"""
content = content.replace(target_imports, new_imports)
# 2. Add flag
target_flag = """ final List<String> _bundledThemeAssetFiles;
final _ThemeLruCache _themeCache;
int _themeParseCount = 0;"""
new_flag = """ final List<String> _bundledThemeAssetFiles;
final _ThemeLruCache _themeCache;
int _themeParseCount = 0;
bool _hasMigratedThemes = false;"""
content = content.replace(target_flag, new_flag)
# 3. loadThemeDefinitions
target_load = """ Future<List<ThemeDefinition>> loadThemeDefinitions() async {
final definitions = <ThemeDefinition>[];
await _loadBuiltinAssetDefinitions(definitions);
await _scanDirectory(
await _userThemesDirectory(),
ThemeSource.filesystem,
definitions,
);
await _scanDirectory(
await _importedThemesDirectory(),
ThemeSource.imported,
definitions,
);
final legacy = await _legacyImportedDefinition();
if (legacy != null) {
definitions.removeWhere(
(definition) =>
definition.path == legacy.path &&
definition.source != ThemeSource.legacyImported,
);
definitions.add(legacy);
}
definitions.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return List.unmodifiable(definitions);
}"""
new_load = """ Future<List<ThemeDefinition>> loadThemeDefinitions() async {
final definitions = <ThemeDefinition>[];
await _loadBuiltinAssetDefinitions(definitions);
if (!_hasMigratedThemes) {
await _migrateLegacyThemesToExtensions();
_hasMigratedThemes = true;
}
await LocalExtensionRegistry.instance.load();
for (final manifest in LocalExtensionRegistry.instance.manifests) {
if (manifest.type != ExtensionType.theme) continue;
final installPath = manifest.installPath;
final mainFile = manifest.main;
if (installPath == null || mainFile == null) continue;
final file = File(p.join(installPath, mainFile));
if (!await file.exists()) continue;
final definition = await _definitionFromFile(file, ThemeSource.filesystem);
if (definition != null) {
definitions.add(definition);
}
}
final legacy = await _legacyImportedDefinition();
if (legacy != null) {
definitions.removeWhere(
(definition) =>
definition.path == legacy.path &&
definition.source != ThemeSource.legacyImported,
);
definitions.add(legacy);
}
definitions.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return List.unmodifiable(definitions);
}
Future<void> _migrateLegacyThemesToExtensions() async {
final dirs = [
await _userThemesDirectory(),
await _importedThemesDirectory(),
];
final extensionsDir = await ExtensionPaths.ensureExtensionsDirectory();
for (final directory in dirs) {
if (!await directory.exists()) continue;
await for (final entity in directory.list(followLinks: false)) {
if (entity is! File) continue;
if (p.basename(entity.path) == ThemeImportService.storedFileName) continue;
final ext = p.extension(entity.path).toLowerCase();
if (ext != '.json' && ext != '.jsonc') continue;
try {
final definition = await _definitionFromFile(entity, ThemeSource.filesystem);
if (definition == null) continue;
final slug = ThemeImportService.slugifyThemeName(definition.name);
var finalExtDir = Directory(p.join(extensionsDir.path, slug));
var counter = 2;
while (await finalExtDir.exists()) {
finalExtDir = Directory(p.join(extensionsDir.path, '$slug-$counter'));
counter++;
}
await finalExtDir.create(recursive: true);
final themeFile = File(p.join(finalExtDir.path, 'theme.json'));
await entity.copy(themeFile.path);
final manifest = ExtensionManifest(
id: definition.id,
name: definition.name,
version: '1.0.0',
publisher: 'Unknown',
type: ExtensionType.theme,
engines: const {'querya_desktop': '*'},
main: 'theme.json',
description: 'Migrated custom theme',
);
final manifestFile = File(p.join(finalExtDir.path, 'manifest.json'));
await manifestFile.writeAsString(jsonEncode(manifest.toJson()));
await entity.delete(); // Delete old file to complete migration
} catch (_) {}
}
}
// Reload extensions since we might have added new ones
await LocalExtensionRegistry.instance.reload();
}"""
content = content.replace(target_load, new_load)
# 4. importThemeFile
target_import = """ /// Validates [sourcePath], copies into the user themes directory, and returns
/// the scanned [ThemeDefinition].
Future<ThemeDefinitionImportResult> importThemeFile(String sourcePath) async {
try {
final source = File(sourcePath);
if (!await source.exists()) {
return const ThemeDefinitionImportFailure('Theme file not found.');
}
final raw = await source.readAsString();
final hash = _contentHash(raw);
final json = _decodeRoot(raw);
if (json == null) {
return const ThemeDefinitionImportFailure('Invalid JSON.');
}
final themesDir = await _userThemesDirectory();
if (!await themesDir.exists()) {
await themesDir.create(recursive: true);
}
final schema = json['schema']?.toString();
late final String logicalId;
late final String preferredBaseName;
late String contentToWrite;
if (schema == queryaThemeSchemaV1) {
final manifest = QueryaThemeManifest.fromJsonString(raw);
logicalId = manifest.id;
preferredBaseName = ThemeImportService.safeThemeFileBase(manifest.id);
contentToWrite = raw;
} else {
final manifest = VsCodeThemeManifest.fromJsonString(raw);
if (manifest.colors.isEmpty) {
return const ThemeDefinitionImportFailure(
'Theme file has no "colors" section to import.',
);
}
final displayName = manifest.name?.trim().isNotEmpty == true
? manifest.name!.trim()
: p.basenameWithoutExtension(sourcePath);
preferredBaseName = ThemeImportService.slugifyThemeName(displayName);
logicalId = preferredBaseName;
contentToWrite = raw;
}
var resolved = await _resolveImportDestination(
themesDir: themesDir,
hash: hash,
logicalId: logicalId,
preferredBaseName: preferredBaseName,
);
if (!resolved.reused &&
schema == queryaThemeSchemaV1 &&
resolved.renamedId != null) {
contentToWrite = _rewriteCustomThemeId(raw, resolved.renamedId!);
}
if (!resolved.reused) {
await resolved.file.writeAsString(contentToWrite);
}
final definition =
await _definitionFromFile(resolved.file, ThemeSource.filesystem);
if (definition == null) {
return const ThemeDefinitionImportFailure(
'Failed to index imported theme.',
);
}
return ThemeDefinitionImportSuccess(
definition: definition,
reusedExisting: resolved.reused,
);
} on QueryaThemeManifestParseException catch (e) {
return ThemeDefinitionImportFailure(e.message);
} on VsCodeThemeParseException catch (e) {
return ThemeDefinitionImportFailure(e.message);
} on IOException catch (e) {
return ThemeDefinitionImportFailure(e.toString());
} on Object catch (e) {
return ThemeDefinitionImportFailure(e.toString());
}
}"""
new_import = """ /// Validates [sourcePath], creates an extension directory, and returns
/// the scanned [ThemeDefinition].
Future<ThemeDefinitionImportResult> importThemeFile(String sourcePath) async {
try {
final source = File(sourcePath);
if (!await source.exists()) {
return const ThemeDefinitionImportFailure('Theme file not found.');
}
final raw = await source.readAsString();
final json = _decodeRoot(raw);
if (json == null) {
return const ThemeDefinitionImportFailure('Invalid JSON.');
}
final extensionsDir = await ExtensionPaths.ensureExtensionsDirectory();
final schema = json['schema']?.toString();
late final String logicalId;
late final String preferredBaseName;
late String contentToWrite;
if (schema == queryaThemeSchemaV1) {
final manifest = QueryaThemeManifest.fromJsonString(raw);
logicalId = manifest.id;
preferredBaseName = ThemeImportService.safeThemeFileBase(manifest.id);
contentToWrite = raw;
} else {
final manifest = VsCodeThemeManifest.fromJsonString(raw);
if (manifest.colors.isEmpty) {
return const ThemeDefinitionImportFailure(
'Theme file has no "colors" section to import.',
);
}
final displayName = manifest.name?.trim().isNotEmpty == true
? manifest.name!.trim()
: p.basenameWithoutExtension(sourcePath);
preferredBaseName = ThemeImportService.slugifyThemeName(displayName);
logicalId = preferredBaseName;
contentToWrite = raw;
}
await LocalExtensionRegistry.instance.load();
bool reused = false;
String? existingInstallPath;
for (final extManifest in LocalExtensionRegistry.instance.manifests) {
if (extManifest.type == ExtensionType.theme && extManifest.id == logicalId) {
reused = true;
existingInstallPath = extManifest.installPath;
break;
}
}
File resolvedFile;
if (reused && existingInstallPath != null) {
resolvedFile = File(p.join(existingInstallPath, 'theme.json'));
} else {
var finalExtDir = Directory(p.join(extensionsDir.path, preferredBaseName));
var counter = 2;
while (await finalExtDir.exists()) {
finalExtDir = Directory(p.join(extensionsDir.path, '$preferredBaseName-$counter'));
counter++;
}
await finalExtDir.create(recursive: true);
resolvedFile = File(p.join(finalExtDir.path, 'theme.json'));
await resolvedFile.writeAsString(contentToWrite);
final newManifest = ExtensionManifest(
id: logicalId,
name: preferredBaseName,
version: '1.0.0',
publisher: 'Unknown',
type: ExtensionType.theme,
engines: const {'querya_desktop': '*'},
main: 'theme.json',
description: 'Imported custom theme',
);
final manifestFile = File(p.join(finalExtDir.path, 'manifest.json'));
await manifestFile.writeAsString(jsonEncode(newManifest.toJson()));
await LocalExtensionRegistry.instance.reload();
}
final definition = await _definitionFromFile(resolvedFile, ThemeSource.filesystem);
if (definition == null) {
return const ThemeDefinitionImportFailure('Failed to index imported theme.');
}
return ThemeDefinitionImportSuccess(
definition: definition,
reusedExisting: reused,
);
} on QueryaThemeManifestParseException catch (e) {
return ThemeDefinitionImportFailure(e.message);
} on VsCodeThemeParseException catch (e) {
return ThemeDefinitionImportFailure(e.message);
} on IOException catch (e) {
return ThemeDefinitionImportFailure(e.toString());
} on Object catch (e) {
return ThemeDefinitionImportFailure(e.toString());
}
}"""
content = content.replace(target_import, new_import)
# 5. Remove unused methods using python block extracting instead of regex
# Or just simple replace
import re
to_remove = [
(r' Future<void> _scanDirectory\(', r'^\s*\}\s*$', 3),
(r' Future<_ResolvedImportDestination> _resolveImportDestination\(', r'^\s*\}\s*$', 3),
(r' Future<String> _nextRenamedThemeId\(', r'^\s*\}\s*$', 3),
(r' Future<bool> _themeIdExists\(', r'^\s*\}\s*$', 3),
(r' Future<File> _nextAvailableThemeFile\(', r'^\s*\}\s*$', 3),
(r' String _rewriteCustomThemeId\(', r'^\s*\}\s*$', 3),
(r'class _ResolvedImportDestination \{', r'^\}\s*$', 0),
]
lines = content.split('\n')
for start_regex, end_regex, indent in to_remove:
start_idx = -1
end_idx = -1
for i, line in enumerate(lines):
if re.search(start_regex, line):
start_idx = i
break
if start_idx != -1:
# find matching closing brace
brace_count = 0
started = False
for i in range(start_idx, len(lines)):
brace_count += lines[i].count('{')
brace_count -= lines[i].count('}')
if '{' in lines[i]:
started = True
if started and brace_count == 0:
end_idx = i
break
if end_idx != -1:
lines[start_idx:end_idx+1] = []
content = '\n'.join(lines)
with open('lib/core/theme/theme_registry_service.dart', 'w') as f:
f.write(content)
print("Patch applied successfully.")