@fresh-editor/fresh-editor 0.1.70 → 0.1.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/package.json +1 -1
- package/plugins/audit_mode.i18n.json +504 -63
- package/plugins/audit_mode.ts +231 -234
- package/plugins/buffer_modified.i18n.json +39 -4
- package/plugins/calculator.i18n.json +56 -7
- package/plugins/clangd_support.i18n.json +136 -17
- package/plugins/color_highlighter.i18n.json +97 -20
- package/plugins/config-schema.json +8 -1
- package/plugins/csharp_support.i18n.json +49 -7
- package/plugins/diagnostics_panel.i18n.json +145 -19
- package/plugins/find_references.i18n.json +188 -41
- package/plugins/git_blame.i18n.json +300 -34
- package/plugins/git_find_file.i18n.json +206 -38
- package/plugins/git_grep.i18n.json +108 -17
- package/plugins/git_gutter.i18n.json +61 -12
- package/plugins/git_log.i18n.json +291 -34
- package/plugins/lib/fresh.d.ts +108 -0
- package/plugins/live_grep.i18n.json +117 -26
- package/plugins/markdown_compose.i18n.json +140 -21
- package/plugins/merge_conflict.i18n.json +503 -62
- package/plugins/path_complete.i18n.json +50 -8
- package/plugins/search_replace.i18n.json +270 -53
- package/plugins/test_i18n.i18n.json +55 -0
- package/plugins/theme_editor.i18n.json +2844 -515
- package/plugins/theme_editor.ts +1013 -469
- package/plugins/todo_highlighter.i18n.json +122 -24
- package/plugins/vi_mode.i18n.json +944 -111
- package/plugins/welcome.i18n.json +145 -19
package/plugins/theme_editor.ts
CHANGED
|
@@ -89,122 +89,108 @@ interface ThemeField {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// =============================================================================
|
|
92
|
-
// Theme
|
|
92
|
+
// Theme Schema (loaded dynamically from Rust)
|
|
93
93
|
// =============================================================================
|
|
94
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
|
-
displayName: "Syntax Highlighting",
|
|
195
|
-
description: "Code syntax highlighting colors",
|
|
196
|
-
fields: [
|
|
197
|
-
{ key: "keyword", displayName: "Keyword", description: "Language keywords (if, for, fn, etc.)", section: "syntax" },
|
|
198
|
-
{ key: "string", displayName: "String", description: "String literals", section: "syntax" },
|
|
199
|
-
{ key: "comment", displayName: "Comment", description: "Code comments", section: "syntax" },
|
|
200
|
-
{ key: "function", displayName: "Function", description: "Function names", section: "syntax" },
|
|
201
|
-
{ key: "type", displayName: "Type", description: "Type names", section: "syntax" },
|
|
202
|
-
{ key: "variable", displayName: "Variable", description: "Variable names", section: "syntax" },
|
|
203
|
-
{ key: "constant", displayName: "Constant", description: "Constants and literals", section: "syntax" },
|
|
204
|
-
{ key: "operator", displayName: "Operator", description: "Operators (+, -, =, etc.)", section: "syntax" },
|
|
205
|
-
],
|
|
206
|
-
},
|
|
207
|
-
];
|
|
95
|
+
/**
|
|
96
|
+
* Cached theme sections loaded from the API.
|
|
97
|
+
* This is populated on first use and reflects the actual theme structure from Rust.
|
|
98
|
+
*/
|
|
99
|
+
let cachedThemeSections: ThemeSection[] | null = null;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Load theme sections from the Rust API.
|
|
103
|
+
* Parses the raw JSON Schema and resolves $ref references.
|
|
104
|
+
* Uses i18n keys for localized display names.
|
|
105
|
+
*/
|
|
106
|
+
function loadThemeSections(): ThemeSection[] {
|
|
107
|
+
if (cachedThemeSections !== null) {
|
|
108
|
+
return cachedThemeSections;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const schema = editor.getThemeSchema();
|
|
112
|
+
const defs = schema.$defs || {};
|
|
113
|
+
|
|
114
|
+
// Helper to resolve $ref and get the referenced schema
|
|
115
|
+
const resolveRef = (refStr: string): Record<string, unknown> | null => {
|
|
116
|
+
// $ref format: "#/$defs/TypeName"
|
|
117
|
+
const prefix = "#/$defs/";
|
|
118
|
+
if (refStr.startsWith(prefix)) {
|
|
119
|
+
const typeName = refStr.slice(prefix.length);
|
|
120
|
+
return defs[typeName] as Record<string, unknown> || null;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const sections: ThemeSection[] = [];
|
|
126
|
+
const properties = schema.properties || {};
|
|
127
|
+
|
|
128
|
+
// Section ordering
|
|
129
|
+
const sectionOrder = ["editor", "ui", "search", "diagnostic", "syntax"];
|
|
130
|
+
|
|
131
|
+
for (const [sectionName, sectionSchema] of Object.entries(properties)) {
|
|
132
|
+
// Skip "name" field - it's not a color section
|
|
133
|
+
if (sectionName === "name") continue;
|
|
134
|
+
|
|
135
|
+
const sectionObj = sectionSchema as Record<string, unknown>;
|
|
136
|
+
const sectionDesc = (sectionObj.description as string) || "";
|
|
137
|
+
|
|
138
|
+
// Resolve $ref to get the actual type definition
|
|
139
|
+
const refStr = sectionObj.$ref as string | undefined;
|
|
140
|
+
const resolvedSchema = refStr ? resolveRef(refStr) : sectionObj;
|
|
141
|
+
if (!resolvedSchema) continue;
|
|
142
|
+
|
|
143
|
+
const sectionProps = resolvedSchema.properties as Record<string, unknown> || {};
|
|
144
|
+
const fields: ThemeFieldDef[] = [];
|
|
145
|
+
|
|
146
|
+
for (const [fieldName, fieldSchema] of Object.entries(sectionProps)) {
|
|
147
|
+
const fieldObj = fieldSchema as Record<string, unknown>;
|
|
148
|
+
const fieldDesc = (fieldObj.description as string) || "";
|
|
149
|
+
|
|
150
|
+
// Generate i18n keys from field names
|
|
151
|
+
const i18nName = `field.${fieldName}`;
|
|
152
|
+
const i18nDesc = `field.${fieldName}_desc`;
|
|
153
|
+
|
|
154
|
+
fields.push({
|
|
155
|
+
key: fieldName,
|
|
156
|
+
displayName: editor.t(i18nName) || fieldDesc || fieldName,
|
|
157
|
+
description: editor.t(i18nDesc) || fieldDesc,
|
|
158
|
+
section: sectionName,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Sort fields alphabetically (use simple comparison to avoid ICU issues in Deno)
|
|
163
|
+
fields.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
164
|
+
|
|
165
|
+
// Generate i18n keys for section
|
|
166
|
+
const sectionI18nName = `section.${sectionName}`;
|
|
167
|
+
const sectionI18nDesc = `section.${sectionName}_desc`;
|
|
168
|
+
|
|
169
|
+
sections.push({
|
|
170
|
+
name: sectionName,
|
|
171
|
+
displayName: editor.t(sectionI18nName) || sectionDesc || sectionName,
|
|
172
|
+
description: editor.t(sectionI18nDesc) || sectionDesc,
|
|
173
|
+
fields,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Sort sections in logical order
|
|
178
|
+
sections.sort((a, b) => {
|
|
179
|
+
const aIdx = sectionOrder.indexOf(a.name);
|
|
180
|
+
const bIdx = sectionOrder.indexOf(b.name);
|
|
181
|
+
return (aIdx === -1 ? 99 : aIdx) - (bIdx === -1 ? 99 : bIdx);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
cachedThemeSections = sections;
|
|
185
|
+
return cachedThemeSections;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get theme sections (loads from API if not cached)
|
|
190
|
+
*/
|
|
191
|
+
function getThemeSections(): ThemeSection[] {
|
|
192
|
+
return loadThemeSections();
|
|
193
|
+
}
|
|
208
194
|
|
|
209
195
|
// =============================================================================
|
|
210
196
|
// State Management
|
|
@@ -234,6 +220,12 @@ interface ThemeEditorState {
|
|
|
234
220
|
hasChanges: boolean;
|
|
235
221
|
/** Available built-in themes */
|
|
236
222
|
builtinThemes: string[];
|
|
223
|
+
/** Pending save name for overwrite confirmation */
|
|
224
|
+
pendingSaveName: string | null;
|
|
225
|
+
/** Whether current theme is a built-in (requires Save As) */
|
|
226
|
+
isBuiltin: boolean;
|
|
227
|
+
/** Saved cursor field path (for restoring after prompts) */
|
|
228
|
+
savedCursorPath: string | null;
|
|
237
229
|
}
|
|
238
230
|
|
|
239
231
|
const state: ThemeEditorState = {
|
|
@@ -251,6 +243,9 @@ const state: ThemeEditorState = {
|
|
|
251
243
|
selectedIndex: 0,
|
|
252
244
|
hasChanges: false,
|
|
253
245
|
builtinThemes: [],
|
|
246
|
+
pendingSaveName: null,
|
|
247
|
+
isBuiltin: false,
|
|
248
|
+
savedCursorPath: null,
|
|
254
249
|
};
|
|
255
250
|
|
|
256
251
|
// =============================================================================
|
|
@@ -266,10 +261,26 @@ const colors = {
|
|
|
266
261
|
modified: [255, 255, 100] as RGB, // Yellow
|
|
267
262
|
footer: [100, 100, 100] as RGB, // Gray
|
|
268
263
|
colorBlock: [200, 200, 200] as RGB, // Light gray for color swatch outline
|
|
264
|
+
selectionBg: [50, 50, 80] as RGB, // Dark blue-gray for selected field
|
|
269
265
|
};
|
|
270
266
|
|
|
271
|
-
//
|
|
272
|
-
|
|
267
|
+
// =============================================================================
|
|
268
|
+
// Keyboard Shortcuts (defined once, used in mode and i18n)
|
|
269
|
+
// =============================================================================
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Keyboard shortcuts for the theme editor.
|
|
273
|
+
* These are defined once and used both in the mode definition and in the UI hints.
|
|
274
|
+
*/
|
|
275
|
+
const SHORTCUTS = {
|
|
276
|
+
open: "C-o",
|
|
277
|
+
save: "C-s",
|
|
278
|
+
save_as: "C-S-s",
|
|
279
|
+
delete: "C-d",
|
|
280
|
+
reload: "C-r",
|
|
281
|
+
close: "C-q",
|
|
282
|
+
help: "F1",
|
|
283
|
+
};
|
|
273
284
|
|
|
274
285
|
// =============================================================================
|
|
275
286
|
// Mode Definition
|
|
@@ -279,18 +290,24 @@ editor.defineMode(
|
|
|
279
290
|
"theme-editor",
|
|
280
291
|
"normal",
|
|
281
292
|
[
|
|
293
|
+
// Navigation (standard keys that don't conflict with typing)
|
|
282
294
|
["Return", "theme_editor_edit_color"],
|
|
283
295
|
["Space", "theme_editor_edit_color"],
|
|
284
|
-
["Tab", "
|
|
285
|
-
["
|
|
286
|
-
["
|
|
287
|
-
["
|
|
288
|
-
["S", "theme_editor_save_as"],
|
|
289
|
-
["d", "theme_editor_set_as_default"],
|
|
290
|
-
["q", "theme_editor_close"],
|
|
296
|
+
["Tab", "theme_editor_nav_next_section"],
|
|
297
|
+
["S-Tab", "theme_editor_nav_prev_section"],
|
|
298
|
+
["Up", "theme_editor_nav_up"],
|
|
299
|
+
["Down", "theme_editor_nav_down"],
|
|
291
300
|
["Escape", "theme_editor_close"],
|
|
292
|
-
[
|
|
293
|
-
|
|
301
|
+
[SHORTCUTS.help, "theme_editor_show_help"],
|
|
302
|
+
|
|
303
|
+
// Ctrl+ shortcuts (match common editor conventions)
|
|
304
|
+
[SHORTCUTS.open, "theme_editor_open"],
|
|
305
|
+
[SHORTCUTS.save, "theme_editor_save"],
|
|
306
|
+
[SHORTCUTS.save_as, "theme_editor_save_as"],
|
|
307
|
+
[SHORTCUTS.delete, "theme_editor_delete"],
|
|
308
|
+
[SHORTCUTS.reload, "theme_editor_reload"],
|
|
309
|
+
[SHORTCUTS.close, "theme_editor_close"],
|
|
310
|
+
["C-h", "theme_editor_show_help"], // Alternative help key
|
|
294
311
|
],
|
|
295
312
|
true // read-only
|
|
296
313
|
);
|
|
@@ -450,7 +467,7 @@ async function loadBuiltinThemes(): Promise<string[]> {
|
|
|
450
467
|
}
|
|
451
468
|
|
|
452
469
|
/**
|
|
453
|
-
* Load a theme file
|
|
470
|
+
* Load a theme file from built-in themes directory
|
|
454
471
|
*/
|
|
455
472
|
async function loadThemeFile(name: string): Promise<Record<string, unknown> | null> {
|
|
456
473
|
const themesDir = findThemesDir();
|
|
@@ -465,11 +482,48 @@ async function loadThemeFile(name: string): Promise<Record<string, unknown> | nu
|
|
|
465
482
|
}
|
|
466
483
|
}
|
|
467
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Load a user theme file
|
|
487
|
+
*/
|
|
488
|
+
async function loadUserThemeFile(name: string): Promise<{ data: Record<string, unknown>; path: string } | null> {
|
|
489
|
+
const userThemesDir = getUserThemesDir();
|
|
490
|
+
const themePath = editor.pathJoin(userThemesDir, `${name}.json`);
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
const content = await editor.readFile(themePath);
|
|
494
|
+
return { data: JSON.parse(content), path: themePath };
|
|
495
|
+
} catch {
|
|
496
|
+
editor.debug(`Failed to load user theme: ${name}`);
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* List available user themes
|
|
503
|
+
*/
|
|
504
|
+
function listUserThemes(): string[] {
|
|
505
|
+
const userThemesDir = getUserThemesDir();
|
|
506
|
+
try {
|
|
507
|
+
const entries = editor.readDir(userThemesDir);
|
|
508
|
+
return entries
|
|
509
|
+
.filter(e => e.is_file && e.name.endsWith(".json"))
|
|
510
|
+
.map(e => e.name.replace(".json", ""));
|
|
511
|
+
} catch {
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
468
516
|
/**
|
|
469
517
|
* Get user themes directory
|
|
470
|
-
* Uses
|
|
518
|
+
* Uses the API to get the correct path
|
|
471
519
|
*/
|
|
472
520
|
function getUserThemesDir(): string {
|
|
521
|
+
// Use the API if available (new method)
|
|
522
|
+
if (typeof editor.getThemesDir === "function") {
|
|
523
|
+
return editor.getThemesDir();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Fallback for older versions (deprecated)
|
|
473
527
|
// Check XDG_CONFIG_HOME first (standard on Linux)
|
|
474
528
|
const xdgConfig = editor.getEnv("XDG_CONFIG_HOME");
|
|
475
529
|
if (xdgConfig) {
|
|
@@ -494,16 +548,17 @@ function getUserThemesDir(): string {
|
|
|
494
548
|
*/
|
|
495
549
|
function buildVisibleFields(): ThemeField[] {
|
|
496
550
|
const fields: ThemeField[] = [];
|
|
551
|
+
const themeSections = getThemeSections();
|
|
497
552
|
|
|
498
|
-
for (const section of
|
|
553
|
+
for (const section of themeSections) {
|
|
499
554
|
const expanded = state.expandedSections.has(section.name);
|
|
500
555
|
|
|
501
|
-
// Section header
|
|
556
|
+
// Section header - displayName and description are already translated in getThemeSections()
|
|
502
557
|
fields.push({
|
|
503
558
|
def: {
|
|
504
559
|
key: section.name,
|
|
505
|
-
displayName:
|
|
506
|
-
description:
|
|
560
|
+
displayName: section.displayName,
|
|
561
|
+
description: section.description,
|
|
507
562
|
section: section.name,
|
|
508
563
|
},
|
|
509
564
|
value: [0, 0, 0], // Placeholder
|
|
@@ -519,12 +574,9 @@ function buildVisibleFields(): ThemeField[] {
|
|
|
519
574
|
const path = `${section.name}.${fieldDef.key}`;
|
|
520
575
|
const value = getNestedValue(state.themeData, path) as ColorValue || [128, 128, 128];
|
|
521
576
|
|
|
577
|
+
// fieldDef displayName and description are already translated in getThemeSections()
|
|
522
578
|
fields.push({
|
|
523
|
-
def:
|
|
524
|
-
...fieldDef,
|
|
525
|
-
displayName: editor.t(`field.${fieldDef.key}`),
|
|
526
|
-
description: editor.t(`field.${fieldDef.key}_desc`),
|
|
527
|
-
},
|
|
579
|
+
def: fieldDef,
|
|
528
580
|
value,
|
|
529
581
|
path,
|
|
530
582
|
depth: 1,
|
|
@@ -566,6 +618,21 @@ function buildDisplayEntries(): TextPropertyEntry[] {
|
|
|
566
618
|
});
|
|
567
619
|
}
|
|
568
620
|
|
|
621
|
+
// Key hints at the top (moved from footer)
|
|
622
|
+
entries.push({
|
|
623
|
+
text: editor.t("panel.nav_hint") + "\n",
|
|
624
|
+
properties: { type: "footer" },
|
|
625
|
+
});
|
|
626
|
+
entries.push({
|
|
627
|
+
text: editor.t("panel.action_hint", SHORTCUTS) + "\n",
|
|
628
|
+
properties: { type: "footer" },
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
entries.push({
|
|
632
|
+
text: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
|
|
633
|
+
properties: { type: "separator" },
|
|
634
|
+
});
|
|
635
|
+
|
|
569
636
|
entries.push({
|
|
570
637
|
text: "\n",
|
|
571
638
|
properties: { type: "blank" },
|
|
@@ -603,11 +670,11 @@ function buildDisplayEntries(): TextPropertyEntry[] {
|
|
|
603
670
|
properties: { type: "description", path: field.path },
|
|
604
671
|
});
|
|
605
672
|
|
|
606
|
-
// Color field with swatch
|
|
673
|
+
// Color field with swatch characters (X for fg preview, space for bg preview)
|
|
607
674
|
const colorStr = formatColorValue(field.value);
|
|
608
675
|
|
|
609
676
|
entries.push({
|
|
610
|
-
text: `${indent} ${field.def.displayName}: ${colorStr}\n`,
|
|
677
|
+
text: `${indent} ${field.def.displayName}: X ${colorStr}\n`,
|
|
611
678
|
properties: {
|
|
612
679
|
type: "field",
|
|
613
680
|
path: field.path,
|
|
@@ -623,25 +690,12 @@ function buildDisplayEntries(): TextPropertyEntry[] {
|
|
|
623
690
|
});
|
|
624
691
|
}
|
|
625
692
|
|
|
626
|
-
// Footer
|
|
627
|
-
entries.push({
|
|
628
|
-
text: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
|
|
629
|
-
properties: { type: "separator" },
|
|
630
|
-
});
|
|
631
|
-
entries.push({
|
|
632
|
-
text: editor.t("panel.nav_hint") + "\n",
|
|
633
|
-
properties: { type: "footer" },
|
|
634
|
-
});
|
|
635
|
-
entries.push({
|
|
636
|
-
text: editor.t("panel.action_hint") + "\n",
|
|
637
|
-
properties: { type: "footer" },
|
|
638
|
-
});
|
|
639
|
-
|
|
640
693
|
return entries;
|
|
641
694
|
}
|
|
642
695
|
|
|
643
696
|
/**
|
|
644
|
-
* Helper to add a colored overlay
|
|
697
|
+
* Helper to add a colored overlay (foreground color)
|
|
698
|
+
* addOverlay signature: (bufferId, namespace, start, end, r, g, b, underline, bold, italic, bg_r, bg_g, bg_b, extend_to_line_end)
|
|
645
699
|
*/
|
|
646
700
|
function addColorOverlay(
|
|
647
701
|
bufferId: number,
|
|
@@ -650,7 +704,20 @@ function addColorOverlay(
|
|
|
650
704
|
color: RGB,
|
|
651
705
|
bold: boolean = false
|
|
652
706
|
): void {
|
|
653
|
-
editor.addOverlay(bufferId, "theme", start, end, color[0], color[1], color[2], false, bold, false);
|
|
707
|
+
editor.addOverlay(bufferId, "theme", start, end, color[0], color[1], color[2], false, bold, false, -1, -1, -1, false);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Helper to add a background highlight overlay
|
|
712
|
+
* addOverlay signature: (bufferId, namespace, start, end, r, g, b, underline, bold, italic, bg_r, bg_g, bg_b, extend_to_line_end)
|
|
713
|
+
*/
|
|
714
|
+
function addBackgroundHighlight(
|
|
715
|
+
bufferId: number,
|
|
716
|
+
start: number,
|
|
717
|
+
end: number,
|
|
718
|
+
bgColor: RGB
|
|
719
|
+
): void {
|
|
720
|
+
editor.addOverlay(bufferId, "theme-selection", start, end, -1, -1, -1, false, false, false, bgColor[0], bgColor[1], bgColor[2], true);
|
|
654
721
|
}
|
|
655
722
|
|
|
656
723
|
/**
|
|
@@ -669,70 +736,6 @@ function isSpecialColor(value: ColorValue): boolean {
|
|
|
669
736
|
return typeof value === "string" && SPECIAL_COLORS.includes(value);
|
|
670
737
|
}
|
|
671
738
|
|
|
672
|
-
/**
|
|
673
|
-
* Add color swatches using virtual text
|
|
674
|
-
*/
|
|
675
|
-
function addColorSwatches(): void {
|
|
676
|
-
if (state.bufferId === null) return;
|
|
677
|
-
|
|
678
|
-
// Clear existing swatches
|
|
679
|
-
editor.removeVirtualTextsByPrefix(state.bufferId, "theme-swatch-");
|
|
680
|
-
|
|
681
|
-
const entries = buildDisplayEntries();
|
|
682
|
-
let byteOffset = 0;
|
|
683
|
-
|
|
684
|
-
for (const entry of entries) {
|
|
685
|
-
const props = entry.properties as Record<string, unknown>;
|
|
686
|
-
|
|
687
|
-
if (props.type === "field" && props.colorValue) {
|
|
688
|
-
const colorValue = props.colorValue as ColorValue;
|
|
689
|
-
const path = props.path as string;
|
|
690
|
-
|
|
691
|
-
// Find position after the field name colon
|
|
692
|
-
const colonIdx = entry.text.indexOf(":");
|
|
693
|
-
if (colonIdx >= 0) {
|
|
694
|
-
const swatchPos = byteOffset + getUtf8ByteLength(entry.text.substring(0, colonIdx + 2));
|
|
695
|
-
const swatchId = `theme-swatch-${path}`;
|
|
696
|
-
|
|
697
|
-
if (isSpecialColor(colorValue)) {
|
|
698
|
-
// For Default/Reset, show a placeholder indicator
|
|
699
|
-
editor.addVirtualText(
|
|
700
|
-
state.bufferId,
|
|
701
|
-
swatchId,
|
|
702
|
-
swatchPos,
|
|
703
|
-
"∅ ", // Empty set symbol to indicate "use default"
|
|
704
|
-
150, // Gray color for the indicator
|
|
705
|
-
150,
|
|
706
|
-
150,
|
|
707
|
-
true,
|
|
708
|
-
false
|
|
709
|
-
);
|
|
710
|
-
} else {
|
|
711
|
-
const rgb = parseColorToRgb(colorValue);
|
|
712
|
-
if (rgb) {
|
|
713
|
-
const useBg = isBackgroundColorField(path);
|
|
714
|
-
|
|
715
|
-
// Add swatch with a trailing space included in the text
|
|
716
|
-
editor.addVirtualText(
|
|
717
|
-
state.bufferId,
|
|
718
|
-
swatchId,
|
|
719
|
-
swatchPos,
|
|
720
|
-
useBg ? " " : COLOR_BLOCK + " ", // Include trailing space in swatch text
|
|
721
|
-
rgb[0],
|
|
722
|
-
rgb[1],
|
|
723
|
-
rgb[2],
|
|
724
|
-
true,
|
|
725
|
-
useBg // use as background color
|
|
726
|
-
);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
byteOffset += getUtf8ByteLength(entry.text);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
739
|
/**
|
|
737
740
|
* Apply syntax highlighting
|
|
738
741
|
*/
|
|
@@ -741,15 +744,26 @@ function applyHighlighting(): void {
|
|
|
741
744
|
|
|
742
745
|
const bufferId = state.bufferId;
|
|
743
746
|
editor.clearNamespace(bufferId, "theme");
|
|
747
|
+
editor.clearNamespace(bufferId, "theme-selection");
|
|
744
748
|
|
|
745
749
|
const entries = buildDisplayEntries();
|
|
746
750
|
let byteOffset = 0;
|
|
747
751
|
|
|
752
|
+
// Get current field at cursor to highlight it
|
|
753
|
+
const currentField = getFieldAtCursor();
|
|
754
|
+
const currentFieldPath = currentField?.path;
|
|
755
|
+
|
|
748
756
|
for (const entry of entries) {
|
|
749
757
|
const text = entry.text;
|
|
750
758
|
const textLen = getUtf8ByteLength(text);
|
|
751
759
|
const props = entry.properties as Record<string, unknown>;
|
|
752
760
|
const entryType = props.type as string;
|
|
761
|
+
const entryPath = props.path as string | undefined;
|
|
762
|
+
|
|
763
|
+
// Add selection highlight for current field/section
|
|
764
|
+
if (currentFieldPath && entryPath === currentFieldPath && (entryType === "field" || entryType === "section")) {
|
|
765
|
+
addBackgroundHighlight(bufferId, byteOffset, byteOffset + textLen, colors.selectionBg);
|
|
766
|
+
}
|
|
753
767
|
|
|
754
768
|
if (entryType === "title") {
|
|
755
769
|
addColorOverlay(bufferId, byteOffset, byteOffset + textLen, colors.sectionHeader, true);
|
|
@@ -766,8 +780,26 @@ function applyHighlighting(): void {
|
|
|
766
780
|
const nameEnd = byteOffset + getUtf8ByteLength(text.substring(0, colonPos));
|
|
767
781
|
addColorOverlay(bufferId, byteOffset, nameEnd, colors.fieldName);
|
|
768
782
|
|
|
769
|
-
//
|
|
770
|
-
|
|
783
|
+
// Color the swatch characters with the field's actual color
|
|
784
|
+
// Text format: "FieldName: X #RRGGBB" (X=fg, space=bg)
|
|
785
|
+
const colorValue = props.colorValue as ColorValue;
|
|
786
|
+
const rgb = parseColorToRgb(colorValue);
|
|
787
|
+
if (rgb) {
|
|
788
|
+
// "X" is at colon + 2 (": " = 2 bytes), and is 1 byte
|
|
789
|
+
const swatchFgStart = nameEnd + getUtf8ByteLength(": ");
|
|
790
|
+
const swatchFgEnd = swatchFgStart + 1; // "X" is 1 byte
|
|
791
|
+
addColorOverlay(bufferId, swatchFgStart, swatchFgEnd, rgb);
|
|
792
|
+
|
|
793
|
+
// First space after "X" is the bg swatch, 1 byte
|
|
794
|
+
const swatchBgStart = swatchFgEnd;
|
|
795
|
+
const swatchBgEnd = swatchBgStart + 1;
|
|
796
|
+
// Use background color for the space
|
|
797
|
+
editor.addOverlay(bufferId, "theme", swatchBgStart, swatchBgEnd, -1, -1, -1, false, false, false, rgb[0], rgb[1], rgb[2], false);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Value (hex code) - custom color (green)
|
|
801
|
+
// Format: ": X #RRGGBB" - value starts after "X " (X + 2 spaces)
|
|
802
|
+
const valueStart = nameEnd + getUtf8ByteLength(": X ");
|
|
771
803
|
addColorOverlay(bufferId, valueStart, byteOffset + textLen, colors.customValue);
|
|
772
804
|
}
|
|
773
805
|
} else if (entryType === "separator" || entryType === "footer") {
|
|
@@ -776,20 +808,25 @@ function applyHighlighting(): void {
|
|
|
776
808
|
|
|
777
809
|
byteOffset += textLen;
|
|
778
810
|
}
|
|
779
|
-
|
|
780
|
-
// Add color swatches
|
|
781
|
-
addColorSwatches();
|
|
782
811
|
}
|
|
783
812
|
|
|
784
813
|
/**
|
|
785
|
-
* Update display
|
|
814
|
+
* Update display (preserves cursor position)
|
|
786
815
|
*/
|
|
787
816
|
function updateDisplay(): void {
|
|
788
817
|
if (state.bufferId === null) return;
|
|
789
818
|
|
|
819
|
+
// Save current field path before updating
|
|
820
|
+
const currentPath = getCurrentFieldPath();
|
|
821
|
+
|
|
790
822
|
const entries = buildDisplayEntries();
|
|
791
823
|
editor.setVirtualBufferContent(state.bufferId, entries);
|
|
792
824
|
applyHighlighting();
|
|
825
|
+
|
|
826
|
+
// Restore cursor to the same field if possible
|
|
827
|
+
if (currentPath) {
|
|
828
|
+
moveCursorToField(currentPath);
|
|
829
|
+
}
|
|
793
830
|
}
|
|
794
831
|
|
|
795
832
|
// =============================================================================
|
|
@@ -814,66 +851,87 @@ function getFieldAtCursor(): ThemeField | null {
|
|
|
814
851
|
}
|
|
815
852
|
|
|
816
853
|
/**
|
|
817
|
-
*
|
|
854
|
+
* Get field by path
|
|
818
855
|
*/
|
|
819
|
-
function
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
// Use startPromptWithInitial to pre-fill with current value
|
|
823
|
-
editor.startPromptWithInitial(editor.t("prompt.color_input", { field: field.def.displayName }), `theme-color-${field.path}`, currentValue);
|
|
856
|
+
function getFieldByPath(path: string): ThemeField | null {
|
|
857
|
+
return state.visibleFields.find(f => f.path === path) || null;
|
|
858
|
+
}
|
|
824
859
|
|
|
825
|
-
|
|
860
|
+
/**
|
|
861
|
+
* Build color suggestions for a field
|
|
862
|
+
*/
|
|
863
|
+
function buildColorSuggestions(field: ThemeField): PromptSuggestion[] {
|
|
864
|
+
const currentValue = formatColorValue(field.value);
|
|
826
865
|
const suggestions: PromptSuggestion[] = [
|
|
827
|
-
{
|
|
828
|
-
text: currentValue,
|
|
829
|
-
description: editor.t("suggestion.current"),
|
|
830
|
-
value: currentValue,
|
|
831
|
-
},
|
|
866
|
+
{ text: currentValue, description: editor.t("suggestion.current"), value: currentValue },
|
|
832
867
|
];
|
|
833
868
|
|
|
834
|
-
// Add special colors
|
|
869
|
+
// Add special colors (Default/Reset for terminal transparency)
|
|
835
870
|
for (const name of SPECIAL_COLORS) {
|
|
836
|
-
suggestions.push({
|
|
837
|
-
text: name,
|
|
838
|
-
description: editor.t("suggestion.terminal_native"),
|
|
839
|
-
value: name,
|
|
840
|
-
});
|
|
871
|
+
suggestions.push({ text: name, description: editor.t("suggestion.terminal_native"), value: name });
|
|
841
872
|
}
|
|
842
873
|
|
|
843
|
-
// Add named colors
|
|
874
|
+
// Add named colors with hex format
|
|
844
875
|
for (const name of NAMED_COLOR_LIST) {
|
|
845
876
|
const rgb = NAMED_COLORS[name];
|
|
846
877
|
const hexValue = rgbToHex(rgb[0], rgb[1], rgb[2]);
|
|
847
|
-
suggestions.push({
|
|
848
|
-
text: name,
|
|
849
|
-
description: hexValue,
|
|
850
|
-
value: name,
|
|
851
|
-
});
|
|
878
|
+
suggestions.push({ text: name, description: hexValue, value: name });
|
|
852
879
|
}
|
|
853
880
|
|
|
854
|
-
|
|
881
|
+
return suggestions;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Start color editing prompt
|
|
886
|
+
*/
|
|
887
|
+
function editColorField(field: ThemeField): void {
|
|
888
|
+
const currentValue = formatColorValue(field.value);
|
|
889
|
+
editor.startPromptWithInitial(
|
|
890
|
+
editor.t("prompt.color_input", { field: field.def.displayName }),
|
|
891
|
+
`theme-color-${field.path}`,
|
|
892
|
+
currentValue
|
|
893
|
+
);
|
|
894
|
+
editor.setPromptSuggestions(buildColorSuggestions(field));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
interface ParseColorResult {
|
|
898
|
+
value?: ColorValue;
|
|
899
|
+
error?: string;
|
|
855
900
|
}
|
|
856
901
|
|
|
857
902
|
/**
|
|
858
|
-
* Parse color input from user
|
|
903
|
+
* Parse color input from user with detailed error messages
|
|
859
904
|
*/
|
|
860
|
-
function parseColorInput(input: string):
|
|
905
|
+
function parseColorInput(input: string): ParseColorResult {
|
|
861
906
|
input = input.trim();
|
|
862
907
|
|
|
908
|
+
if (!input) {
|
|
909
|
+
return { error: "empty" };
|
|
910
|
+
}
|
|
911
|
+
|
|
863
912
|
// Check for special colors (Default/Reset - use terminal's native color)
|
|
864
913
|
if (SPECIAL_COLORS.includes(input)) {
|
|
865
|
-
return input;
|
|
914
|
+
return { value: input };
|
|
866
915
|
}
|
|
867
916
|
|
|
868
917
|
// Check for named color
|
|
869
918
|
if (input in NAMED_COLORS) {
|
|
870
|
-
return input;
|
|
919
|
+
return { value: input };
|
|
871
920
|
}
|
|
872
921
|
|
|
873
922
|
// Try to parse as hex color #RRGGBB
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
923
|
+
if (input.startsWith("#")) {
|
|
924
|
+
const hex = input.slice(1);
|
|
925
|
+
if (hex.length !== 6) {
|
|
926
|
+
return { error: "hex_length" };
|
|
927
|
+
}
|
|
928
|
+
if (!/^[0-9A-Fa-f]{6}$/.test(hex)) {
|
|
929
|
+
return { error: "hex_invalid" };
|
|
930
|
+
}
|
|
931
|
+
const hexResult = hexToRgb(input);
|
|
932
|
+
if (hexResult) {
|
|
933
|
+
return { value: hexResult };
|
|
934
|
+
}
|
|
877
935
|
}
|
|
878
936
|
|
|
879
937
|
// Try to parse as RGB array [r, g, b]
|
|
@@ -882,19 +940,46 @@ function parseColorInput(input: string): ColorValue | null {
|
|
|
882
940
|
const r = parseInt(rgbMatch[1], 10);
|
|
883
941
|
const g = parseInt(rgbMatch[2], 10);
|
|
884
942
|
const b = parseInt(rgbMatch[3], 10);
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
return [r, g, b];
|
|
943
|
+
if (r > 255 || g > 255 || b > 255) {
|
|
944
|
+
return { error: "rgb_range" };
|
|
888
945
|
}
|
|
946
|
+
return { value: [r, g, b] };
|
|
889
947
|
}
|
|
890
948
|
|
|
891
|
-
|
|
949
|
+
// Unknown format
|
|
950
|
+
return { error: "unknown" };
|
|
892
951
|
}
|
|
893
952
|
|
|
894
953
|
// =============================================================================
|
|
895
954
|
// Prompt Handlers
|
|
896
955
|
// =============================================================================
|
|
897
956
|
|
|
957
|
+
/**
|
|
958
|
+
* Find best matching color name for partial input
|
|
959
|
+
*/
|
|
960
|
+
function findMatchingColor(input: string): string | null {
|
|
961
|
+
const lower = input.toLowerCase();
|
|
962
|
+
// First try exact match
|
|
963
|
+
for (const name of Object.keys(NAMED_COLORS)) {
|
|
964
|
+
if (name.toLowerCase() === lower) return name;
|
|
965
|
+
}
|
|
966
|
+
for (const name of SPECIAL_COLORS) {
|
|
967
|
+
if (name.toLowerCase() === lower) return name;
|
|
968
|
+
}
|
|
969
|
+
// Then try prefix match
|
|
970
|
+
for (const name of Object.keys(NAMED_COLORS)) {
|
|
971
|
+
if (name.toLowerCase().startsWith(lower)) return name;
|
|
972
|
+
}
|
|
973
|
+
for (const name of SPECIAL_COLORS) {
|
|
974
|
+
if (name.toLowerCase().startsWith(lower)) return name;
|
|
975
|
+
}
|
|
976
|
+
// Then try contains match
|
|
977
|
+
for (const name of Object.keys(NAMED_COLORS)) {
|
|
978
|
+
if (name.toLowerCase().includes(lower)) return name;
|
|
979
|
+
}
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
|
|
898
983
|
/**
|
|
899
984
|
* Handle color prompt confirmation
|
|
900
985
|
*/
|
|
@@ -906,65 +991,112 @@ globalThis.onThemeColorPromptConfirmed = function(args: {
|
|
|
906
991
|
if (!args.prompt_type.startsWith("theme-color-")) return true;
|
|
907
992
|
|
|
908
993
|
const path = args.prompt_type.replace("theme-color-", "");
|
|
909
|
-
const
|
|
994
|
+
const field = getFieldByPath(path);
|
|
995
|
+
if (!field) return true;
|
|
910
996
|
|
|
911
|
-
|
|
912
|
-
|
|
997
|
+
const result = parseColorInput(args.input);
|
|
998
|
+
|
|
999
|
+
if (result.value !== undefined) {
|
|
1000
|
+
// Valid color - apply it
|
|
1001
|
+
setNestedValue(state.themeData, path, result.value);
|
|
913
1002
|
state.hasChanges = !deepEqual(state.themeData, state.originalThemeData);
|
|
914
|
-
|
|
1003
|
+
|
|
1004
|
+
const entries = buildDisplayEntries();
|
|
1005
|
+
if (state.bufferId !== null) {
|
|
1006
|
+
editor.setVirtualBufferContent(state.bufferId, entries);
|
|
1007
|
+
applyHighlighting();
|
|
1008
|
+
}
|
|
1009
|
+
moveCursorToField(path);
|
|
915
1010
|
editor.setStatus(editor.t("status.updated", { path }));
|
|
916
1011
|
} else {
|
|
917
|
-
|
|
1012
|
+
// Invalid input - try to find a matching color name
|
|
1013
|
+
const matchedColor = findMatchingColor(args.input);
|
|
1014
|
+
if (matchedColor) {
|
|
1015
|
+
// Found a match - reopen prompt with the matched value
|
|
1016
|
+
editor.startPromptWithInitial(
|
|
1017
|
+
editor.t("prompt.color_input", { field: field.def.displayName }),
|
|
1018
|
+
`theme-color-${path}`,
|
|
1019
|
+
matchedColor
|
|
1020
|
+
);
|
|
1021
|
+
// Rebuild suggestions
|
|
1022
|
+
const suggestions: PromptSuggestion[] = buildColorSuggestions(field);
|
|
1023
|
+
editor.setPromptSuggestions(suggestions);
|
|
1024
|
+
editor.setStatus(editor.t("status.autocompleted", { value: matchedColor }));
|
|
1025
|
+
} else {
|
|
1026
|
+
// No match found - reopen prompt with original input
|
|
1027
|
+
editor.startPromptWithInitial(
|
|
1028
|
+
editor.t("prompt.color_input", { field: field.def.displayName }),
|
|
1029
|
+
`theme-color-${path}`,
|
|
1030
|
+
args.input
|
|
1031
|
+
);
|
|
1032
|
+
const suggestions: PromptSuggestion[] = buildColorSuggestions(field);
|
|
1033
|
+
editor.setPromptSuggestions(suggestions);
|
|
1034
|
+
|
|
1035
|
+
const errorKey = `error.color_${result.error}`;
|
|
1036
|
+
editor.setStatus(editor.t(errorKey, { input: args.input }));
|
|
1037
|
+
}
|
|
918
1038
|
}
|
|
919
1039
|
|
|
920
1040
|
return true;
|
|
921
1041
|
};
|
|
922
1042
|
|
|
923
1043
|
/**
|
|
924
|
-
* Handle theme
|
|
1044
|
+
* Handle open theme prompt (both builtin and user themes)
|
|
925
1045
|
*/
|
|
926
|
-
globalThis.
|
|
1046
|
+
globalThis.onThemeOpenPromptConfirmed = async function(args: {
|
|
927
1047
|
prompt_type: string;
|
|
928
1048
|
selected_index: number | null;
|
|
929
1049
|
input: string;
|
|
930
|
-
}): boolean {
|
|
931
|
-
if (args.prompt_type !== "theme-
|
|
932
|
-
|
|
933
|
-
const name = args.input.trim();
|
|
934
|
-
if (name) {
|
|
935
|
-
state.themeName = name;
|
|
936
|
-
state.themeData.name = name;
|
|
937
|
-
state.hasChanges = true;
|
|
938
|
-
updateDisplay();
|
|
939
|
-
editor.setStatus(editor.t("status.name_set", { name }));
|
|
940
|
-
}
|
|
1050
|
+
}): Promise<boolean> {
|
|
1051
|
+
if (args.prompt_type !== "theme-open") return true;
|
|
941
1052
|
|
|
942
|
-
|
|
943
|
-
};
|
|
1053
|
+
const value = args.input.trim();
|
|
944
1054
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
globalThis.onThemeCopyPromptConfirmed = async function(args: {
|
|
949
|
-
prompt_type: string;
|
|
950
|
-
selected_index: number | null;
|
|
951
|
-
input: string;
|
|
952
|
-
}): Promise<boolean> {
|
|
953
|
-
if (args.prompt_type !== "theme-copy-builtin") return true;
|
|
1055
|
+
// Parse the value to determine if it's user or builtin
|
|
1056
|
+
let isBuiltin = false;
|
|
1057
|
+
let themeName = value;
|
|
954
1058
|
|
|
955
|
-
|
|
956
|
-
|
|
1059
|
+
if (value.startsWith("user:")) {
|
|
1060
|
+
themeName = value.slice(5);
|
|
1061
|
+
isBuiltin = false;
|
|
1062
|
+
} else if (value.startsWith("builtin:")) {
|
|
1063
|
+
themeName = value.slice(8);
|
|
1064
|
+
isBuiltin = true;
|
|
1065
|
+
} else {
|
|
1066
|
+
// Fallback: check if it's a builtin theme
|
|
1067
|
+
isBuiltin = state.builtinThemes.includes(value);
|
|
1068
|
+
}
|
|
957
1069
|
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1070
|
+
if (isBuiltin) {
|
|
1071
|
+
// Load builtin theme
|
|
1072
|
+
const themeData = await loadThemeFile(themeName);
|
|
1073
|
+
if (themeData) {
|
|
1074
|
+
state.themeData = deepClone(themeData);
|
|
1075
|
+
state.originalThemeData = deepClone(themeData);
|
|
1076
|
+
state.themeName = themeName;
|
|
1077
|
+
state.themePath = null; // No user path for builtin
|
|
1078
|
+
state.isBuiltin = true;
|
|
1079
|
+
state.hasChanges = false;
|
|
1080
|
+
updateDisplay();
|
|
1081
|
+
editor.setStatus(editor.t("status.opened_builtin", { name: themeName }));
|
|
1082
|
+
} else {
|
|
1083
|
+
editor.setStatus(editor.t("status.load_failed", { name: themeName }));
|
|
1084
|
+
}
|
|
966
1085
|
} else {
|
|
967
|
-
|
|
1086
|
+
// Load user theme
|
|
1087
|
+
const result = await loadUserThemeFile(themeName);
|
|
1088
|
+
if (result) {
|
|
1089
|
+
state.themeData = deepClone(result.data);
|
|
1090
|
+
state.originalThemeData = deepClone(result.data);
|
|
1091
|
+
state.themeName = themeName;
|
|
1092
|
+
state.themePath = result.path;
|
|
1093
|
+
state.isBuiltin = false;
|
|
1094
|
+
state.hasChanges = false;
|
|
1095
|
+
updateDisplay();
|
|
1096
|
+
editor.setStatus(editor.t("status.loaded", { name: themeName }));
|
|
1097
|
+
} else {
|
|
1098
|
+
editor.setStatus(editor.t("status.load_failed", { name: themeName }));
|
|
1099
|
+
}
|
|
968
1100
|
}
|
|
969
1101
|
|
|
970
1102
|
return true;
|
|
@@ -982,47 +1114,131 @@ globalThis.onThemeSaveAsPromptConfirmed = async function(args: {
|
|
|
982
1114
|
|
|
983
1115
|
const name = args.input.trim();
|
|
984
1116
|
if (name) {
|
|
1117
|
+
// Check if theme already exists
|
|
1118
|
+
const userThemesDir = getUserThemesDir();
|
|
1119
|
+
const targetPath = editor.pathJoin(userThemesDir, `${name}.json`);
|
|
1120
|
+
|
|
1121
|
+
if (editor.fileExists(targetPath)) {
|
|
1122
|
+
// Store pending save name for overwrite confirmation
|
|
1123
|
+
state.pendingSaveName = name;
|
|
1124
|
+
editor.startPrompt(editor.t("prompt.overwrite_confirm", { name }), "theme-overwrite-confirm");
|
|
1125
|
+
const suggestions: PromptSuggestion[] = [
|
|
1126
|
+
{ text: editor.t("prompt.overwrite_yes"), description: "", value: "overwrite" },
|
|
1127
|
+
{ text: editor.t("prompt.overwrite_no"), description: "", value: "cancel" },
|
|
1128
|
+
];
|
|
1129
|
+
editor.setPromptSuggestions(suggestions);
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
985
1133
|
state.themeName = name;
|
|
986
1134
|
state.themeData.name = name;
|
|
987
|
-
|
|
1135
|
+
const restorePath = state.savedCursorPath;
|
|
1136
|
+
state.savedCursorPath = null;
|
|
1137
|
+
await saveTheme(name, restorePath);
|
|
1138
|
+
} else {
|
|
1139
|
+
state.savedCursorPath = null;
|
|
988
1140
|
}
|
|
989
1141
|
|
|
990
1142
|
return true;
|
|
991
1143
|
};
|
|
992
1144
|
|
|
993
1145
|
/**
|
|
994
|
-
* Handle
|
|
1146
|
+
* Handle prompt cancellation
|
|
1147
|
+
*/
|
|
1148
|
+
globalThis.onThemePromptCancelled = function(args: { prompt_type: string }): boolean {
|
|
1149
|
+
if (!args.prompt_type.startsWith("theme-")) return true;
|
|
1150
|
+
|
|
1151
|
+
// Clear saved cursor path on cancellation
|
|
1152
|
+
state.savedCursorPath = null;
|
|
1153
|
+
state.pendingSaveName = null;
|
|
1154
|
+
|
|
1155
|
+
editor.setStatus(editor.t("status.cancelled"));
|
|
1156
|
+
return true;
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Handle initial theme selection prompt (when opening editor)
|
|
995
1161
|
*/
|
|
996
|
-
globalThis.
|
|
1162
|
+
globalThis.onThemeSelectInitialPromptConfirmed = async function(args: {
|
|
997
1163
|
prompt_type: string;
|
|
998
1164
|
selected_index: number | null;
|
|
999
1165
|
input: string;
|
|
1000
1166
|
}): Promise<boolean> {
|
|
1001
|
-
if (args.prompt_type !== "theme-
|
|
1167
|
+
if (args.prompt_type !== "theme-select-initial") return true;
|
|
1168
|
+
|
|
1169
|
+
const value = args.input.trim();
|
|
1170
|
+
|
|
1171
|
+
// Parse the value to determine if it's user or builtin
|
|
1172
|
+
let isBuiltin = false;
|
|
1173
|
+
let themeName = value;
|
|
1002
1174
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1175
|
+
if (value.startsWith("user:")) {
|
|
1176
|
+
themeName = value.slice(5);
|
|
1177
|
+
isBuiltin = false;
|
|
1178
|
+
} else if (value.startsWith("builtin:")) {
|
|
1179
|
+
themeName = value.slice(8);
|
|
1180
|
+
isBuiltin = true;
|
|
1181
|
+
} else {
|
|
1182
|
+
// Fallback: check if it's a builtin theme
|
|
1183
|
+
isBuiltin = state.builtinThemes.includes(value);
|
|
1006
1184
|
}
|
|
1007
1185
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1186
|
+
editor.setStatus(editor.t("status.loading"));
|
|
1187
|
+
|
|
1188
|
+
if (isBuiltin) {
|
|
1189
|
+
// Load builtin theme
|
|
1190
|
+
const themeData = await loadThemeFile(themeName);
|
|
1191
|
+
if (themeData) {
|
|
1192
|
+
state.themeData = deepClone(themeData);
|
|
1193
|
+
state.originalThemeData = deepClone(themeData);
|
|
1194
|
+
state.themeName = themeName;
|
|
1195
|
+
state.themePath = null; // No user path for builtin
|
|
1196
|
+
state.isBuiltin = true;
|
|
1197
|
+
state.hasChanges = false;
|
|
1198
|
+
} else {
|
|
1199
|
+
// Fallback to default theme if load failed
|
|
1200
|
+
state.themeData = createDefaultTheme();
|
|
1201
|
+
state.originalThemeData = deepClone(state.themeData);
|
|
1202
|
+
state.themeName = themeName;
|
|
1203
|
+
state.themePath = null;
|
|
1204
|
+
state.isBuiltin = true;
|
|
1205
|
+
state.hasChanges = false;
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
// Load user theme
|
|
1209
|
+
const result = await loadUserThemeFile(themeName);
|
|
1210
|
+
if (result) {
|
|
1211
|
+
state.themeData = deepClone(result.data);
|
|
1212
|
+
state.originalThemeData = deepClone(result.data);
|
|
1213
|
+
state.themeName = themeName;
|
|
1214
|
+
state.themePath = result.path;
|
|
1215
|
+
state.isBuiltin = false;
|
|
1216
|
+
state.hasChanges = false;
|
|
1217
|
+
} else {
|
|
1218
|
+
// Fallback to default theme if load failed
|
|
1219
|
+
state.themeData = createDefaultTheme();
|
|
1220
|
+
state.originalThemeData = deepClone(state.themeData);
|
|
1221
|
+
state.themeName = themeName;
|
|
1222
|
+
state.themePath = null;
|
|
1223
|
+
state.isBuiltin = false;
|
|
1224
|
+
state.hasChanges = false;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Now open the editor with loaded theme
|
|
1229
|
+
await doOpenThemeEditor();
|
|
1010
1230
|
|
|
1011
|
-
/**
|
|
1012
|
-
* Handle prompt cancellation
|
|
1013
|
-
*/
|
|
1014
|
-
globalThis.onThemePromptCancelled = function(args: { prompt_type: string }): boolean {
|
|
1015
|
-
if (!args.prompt_type.startsWith("theme-")) return true;
|
|
1016
|
-
editor.setStatus(editor.t("status.cancelled"));
|
|
1017
1231
|
return true;
|
|
1018
1232
|
};
|
|
1019
1233
|
|
|
1020
1234
|
// Register prompt handlers
|
|
1235
|
+
editor.on("prompt_confirmed", "onThemeSelectInitialPromptConfirmed");
|
|
1021
1236
|
editor.on("prompt_confirmed", "onThemeColorPromptConfirmed");
|
|
1022
|
-
editor.on("prompt_confirmed", "
|
|
1023
|
-
editor.on("prompt_confirmed", "onThemeCopyPromptConfirmed");
|
|
1237
|
+
editor.on("prompt_confirmed", "onThemeOpenPromptConfirmed");
|
|
1024
1238
|
editor.on("prompt_confirmed", "onThemeSaveAsPromptConfirmed");
|
|
1025
|
-
editor.on("prompt_confirmed", "
|
|
1239
|
+
editor.on("prompt_confirmed", "onThemeDiscardPromptConfirmed");
|
|
1240
|
+
editor.on("prompt_confirmed", "onThemeOverwritePromptConfirmed");
|
|
1241
|
+
editor.on("prompt_confirmed", "onThemeDeletePromptConfirmed");
|
|
1026
1242
|
editor.on("prompt_cancelled", "onThemePromptCancelled");
|
|
1027
1243
|
|
|
1028
1244
|
// =============================================================================
|
|
@@ -1031,8 +1247,10 @@ editor.on("prompt_cancelled", "onThemePromptCancelled");
|
|
|
1031
1247
|
|
|
1032
1248
|
/**
|
|
1033
1249
|
* Save theme to file
|
|
1250
|
+
* @param name - Theme name to save as
|
|
1251
|
+
* @param restorePath - Optional field path to restore cursor to after save
|
|
1034
1252
|
*/
|
|
1035
|
-
async function saveTheme(name?: string): Promise<boolean> {
|
|
1253
|
+
async function saveTheme(name?: string, restorePath?: string | null): Promise<boolean> {
|
|
1036
1254
|
const themeName = name || state.themeName;
|
|
1037
1255
|
const userThemesDir = getUserThemesDir();
|
|
1038
1256
|
|
|
@@ -1056,11 +1274,25 @@ async function saveTheme(name?: string): Promise<boolean> {
|
|
|
1056
1274
|
|
|
1057
1275
|
state.themePath = themePath;
|
|
1058
1276
|
state.themeName = themeName;
|
|
1277
|
+
state.isBuiltin = false; // After saving, it's now a user theme
|
|
1059
1278
|
state.originalThemeData = deepClone(state.themeData);
|
|
1060
1279
|
state.hasChanges = false;
|
|
1061
|
-
updateDisplay();
|
|
1062
1280
|
|
|
1063
|
-
|
|
1281
|
+
// Update display
|
|
1282
|
+
const entries = buildDisplayEntries();
|
|
1283
|
+
if (state.bufferId !== null) {
|
|
1284
|
+
editor.setVirtualBufferContent(state.bufferId, entries);
|
|
1285
|
+
applyHighlighting();
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Restore cursor position if provided
|
|
1289
|
+
if (restorePath) {
|
|
1290
|
+
moveCursorToField(restorePath);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Automatically apply the saved theme
|
|
1294
|
+
editor.applyTheme(themeName);
|
|
1295
|
+
editor.setStatus(editor.t("status.saved_and_applied", { name: themeName }));
|
|
1064
1296
|
return true;
|
|
1065
1297
|
} catch (e) {
|
|
1066
1298
|
editor.setStatus(editor.t("status.save_failed", { error: String(e) }));
|
|
@@ -1068,19 +1300,6 @@ async function saveTheme(name?: string): Promise<boolean> {
|
|
|
1068
1300
|
}
|
|
1069
1301
|
}
|
|
1070
1302
|
|
|
1071
|
-
/**
|
|
1072
|
-
* Set a theme as the default in config and apply it immediately
|
|
1073
|
-
*/
|
|
1074
|
-
async function setThemeAsDefault(themeName: string): Promise<void> {
|
|
1075
|
-
try {
|
|
1076
|
-
// Use the editor API to apply and persist the theme
|
|
1077
|
-
editor.applyTheme(themeName);
|
|
1078
|
-
editor.setStatus(editor.t("status.default_set", { name: themeName }));
|
|
1079
|
-
} catch (e) {
|
|
1080
|
-
editor.setStatus(editor.t("status.apply_failed", { error: String(e) }));
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
1303
|
/**
|
|
1085
1304
|
* Create a default/empty theme
|
|
1086
1305
|
*/
|
|
@@ -1174,12 +1393,228 @@ globalThis.onThemeEditorCursorMoved = function(data: {
|
|
|
1174
1393
|
|
|
1175
1394
|
editor.on("cursor_moved", "onThemeEditorCursorMoved");
|
|
1176
1395
|
|
|
1396
|
+
/**
|
|
1397
|
+
* Handle buffer_closed event to reset state when buffer is closed by any means
|
|
1398
|
+
*/
|
|
1399
|
+
globalThis.onThemeEditorBufferClosed = function(data: {
|
|
1400
|
+
buffer_id: number;
|
|
1401
|
+
}): void {
|
|
1402
|
+
if (state.bufferId !== null && data.buffer_id === state.bufferId) {
|
|
1403
|
+
// Reset state when our buffer is closed
|
|
1404
|
+
state.isOpen = false;
|
|
1405
|
+
state.bufferId = null;
|
|
1406
|
+
state.splitId = null;
|
|
1407
|
+
state.themeData = {};
|
|
1408
|
+
state.originalThemeData = {};
|
|
1409
|
+
state.hasChanges = false;
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
editor.on("buffer_closed", "onThemeEditorBufferClosed");
|
|
1414
|
+
|
|
1415
|
+
// =============================================================================
|
|
1416
|
+
// Smart Navigation - Skip Non-Selectable Lines
|
|
1417
|
+
// =============================================================================
|
|
1418
|
+
|
|
1419
|
+
interface SelectableEntry {
|
|
1420
|
+
byteOffset: number;
|
|
1421
|
+
valueByteOffset: number; // Position at the value (after "field: ")
|
|
1422
|
+
index: number;
|
|
1423
|
+
isSection: boolean;
|
|
1424
|
+
path: string;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Get byte offsets for all selectable entries (fields and sections)
|
|
1429
|
+
*/
|
|
1430
|
+
function getSelectableEntries(): SelectableEntry[] {
|
|
1431
|
+
const entries = buildDisplayEntries();
|
|
1432
|
+
const selectableEntries: SelectableEntry[] = [];
|
|
1433
|
+
let byteOffset = 0;
|
|
1434
|
+
|
|
1435
|
+
for (const entry of entries) {
|
|
1436
|
+
const props = entry.properties as Record<string, unknown>;
|
|
1437
|
+
const entryType = props.type as string;
|
|
1438
|
+
const path = (props.path as string) || "";
|
|
1439
|
+
|
|
1440
|
+
// Only fields and sections are selectable (they have index property)
|
|
1441
|
+
if ((entryType === "field" || entryType === "section") && typeof props.index === "number") {
|
|
1442
|
+
// For fields, calculate position at the color value (after "FieldName: X ")
|
|
1443
|
+
let valueByteOffset = byteOffset;
|
|
1444
|
+
if (entryType === "field") {
|
|
1445
|
+
const colonIdx = entry.text.indexOf(":");
|
|
1446
|
+
if (colonIdx >= 0) {
|
|
1447
|
+
// Position at the hex value, after ": X " (colon + space + X + 2 spaces = 5 chars)
|
|
1448
|
+
valueByteOffset = byteOffset + getUtf8ByteLength(entry.text.substring(0, colonIdx + 5));
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
selectableEntries.push({
|
|
1453
|
+
byteOffset,
|
|
1454
|
+
valueByteOffset,
|
|
1455
|
+
index: props.index as number,
|
|
1456
|
+
isSection: entryType === "section",
|
|
1457
|
+
path,
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
byteOffset += getUtf8ByteLength(entry.text);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
return selectableEntries;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Get the current selectable entry index based on cursor position
|
|
1469
|
+
*/
|
|
1470
|
+
function getCurrentSelectableIndex(): number {
|
|
1471
|
+
if (state.bufferId === null) return -1;
|
|
1472
|
+
|
|
1473
|
+
const props = editor.getTextPropertiesAtCursor(state.bufferId);
|
|
1474
|
+
if (props.length > 0 && typeof props[0].index === "number") {
|
|
1475
|
+
return props[0].index as number;
|
|
1476
|
+
}
|
|
1477
|
+
return -1;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* Get the current field path at cursor
|
|
1482
|
+
*/
|
|
1483
|
+
function getCurrentFieldPath(): string | null {
|
|
1484
|
+
if (state.bufferId === null) return null;
|
|
1485
|
+
|
|
1486
|
+
const props = editor.getTextPropertiesAtCursor(state.bufferId);
|
|
1487
|
+
if (props.length > 0 && typeof props[0].path === "string") {
|
|
1488
|
+
return props[0].path as string;
|
|
1489
|
+
}
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Move cursor to a field by path (positions at value for fields)
|
|
1495
|
+
*/
|
|
1496
|
+
function moveCursorToField(path: string): void {
|
|
1497
|
+
if (state.bufferId === null) return;
|
|
1498
|
+
|
|
1499
|
+
const selectableEntries = getSelectableEntries();
|
|
1500
|
+
for (const entry of selectableEntries) {
|
|
1501
|
+
if (entry.path === path) {
|
|
1502
|
+
// Use valueByteOffset for fields, byteOffset for sections
|
|
1503
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1504
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Navigate to the next selectable field/section
|
|
1512
|
+
*/
|
|
1513
|
+
globalThis.theme_editor_nav_down = function(): void {
|
|
1514
|
+
if (state.bufferId === null) return;
|
|
1515
|
+
|
|
1516
|
+
const selectableEntries = getSelectableEntries();
|
|
1517
|
+
const currentIndex = getCurrentSelectableIndex();
|
|
1518
|
+
|
|
1519
|
+
// Find next selectable entry after current
|
|
1520
|
+
for (const entry of selectableEntries) {
|
|
1521
|
+
if (entry.index > currentIndex) {
|
|
1522
|
+
// Use valueByteOffset for fields, byteOffset for sections
|
|
1523
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1524
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// Already at last selectable, stay there
|
|
1530
|
+
editor.setStatus(editor.t("status.at_last_field"));
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Navigate to the previous selectable field/section
|
|
1535
|
+
*/
|
|
1536
|
+
globalThis.theme_editor_nav_up = function(): void {
|
|
1537
|
+
if (state.bufferId === null) return;
|
|
1538
|
+
|
|
1539
|
+
const selectableEntries = getSelectableEntries();
|
|
1540
|
+
const currentIndex = getCurrentSelectableIndex();
|
|
1541
|
+
|
|
1542
|
+
// Find previous selectable entry before current
|
|
1543
|
+
for (let i = selectableEntries.length - 1; i >= 0; i--) {
|
|
1544
|
+
const entry = selectableEntries[i];
|
|
1545
|
+
if (entry.index < currentIndex) {
|
|
1546
|
+
// Use valueByteOffset for fields, byteOffset for sections
|
|
1547
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1548
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Already at first selectable, stay there
|
|
1554
|
+
editor.setStatus(editor.t("status.at_first_field"));
|
|
1555
|
+
};
|
|
1556
|
+
|
|
1557
|
+
/**
|
|
1558
|
+
* Navigate to next element (Tab) - includes both fields and sections
|
|
1559
|
+
*/
|
|
1560
|
+
globalThis.theme_editor_nav_next_section = function(): void {
|
|
1561
|
+
if (state.bufferId === null) return;
|
|
1562
|
+
|
|
1563
|
+
const selectableEntries = getSelectableEntries();
|
|
1564
|
+
const currentIndex = getCurrentSelectableIndex();
|
|
1565
|
+
|
|
1566
|
+
// Find next selectable entry after current
|
|
1567
|
+
for (const entry of selectableEntries) {
|
|
1568
|
+
if (entry.index > currentIndex) {
|
|
1569
|
+
// Use valueByteOffset for fields, byteOffset for sections
|
|
1570
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1571
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// Wrap to first entry
|
|
1577
|
+
if (selectableEntries.length > 0) {
|
|
1578
|
+
const entry = selectableEntries[0];
|
|
1579
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1580
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* Navigate to previous element (Shift+Tab) - includes both fields and sections
|
|
1586
|
+
*/
|
|
1587
|
+
globalThis.theme_editor_nav_prev_section = function(): void {
|
|
1588
|
+
if (state.bufferId === null) return;
|
|
1589
|
+
|
|
1590
|
+
const selectableEntries = getSelectableEntries();
|
|
1591
|
+
const currentIndex = getCurrentSelectableIndex();
|
|
1592
|
+
|
|
1593
|
+
// Find previous selectable entry before current
|
|
1594
|
+
for (let i = selectableEntries.length - 1; i >= 0; i--) {
|
|
1595
|
+
const entry = selectableEntries[i];
|
|
1596
|
+
if (entry.index < currentIndex) {
|
|
1597
|
+
// Use valueByteOffset for fields, byteOffset for sections
|
|
1598
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1599
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Wrap to last entry
|
|
1605
|
+
if (selectableEntries.length > 0) {
|
|
1606
|
+
const entry = selectableEntries[selectableEntries.length - 1];
|
|
1607
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1608
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
|
|
1177
1612
|
// =============================================================================
|
|
1178
1613
|
// Public Commands
|
|
1179
1614
|
// =============================================================================
|
|
1180
1615
|
|
|
1181
1616
|
/**
|
|
1182
|
-
* Open the theme editor
|
|
1617
|
+
* Open the theme editor - prompts user to select theme first
|
|
1183
1618
|
*/
|
|
1184
1619
|
globalThis.open_theme_editor = async function(): Promise<void> {
|
|
1185
1620
|
if (state.isOpen) {
|
|
@@ -1187,8 +1622,6 @@ globalThis.open_theme_editor = async function(): Promise<void> {
|
|
|
1187
1622
|
return;
|
|
1188
1623
|
}
|
|
1189
1624
|
|
|
1190
|
-
editor.setStatus(editor.t("status.loading"));
|
|
1191
|
-
|
|
1192
1625
|
// Save context
|
|
1193
1626
|
state.sourceSplitId = editor.getActiveSplitId();
|
|
1194
1627
|
state.sourceBufferId = editor.getActiveBufferId();
|
|
@@ -1196,13 +1629,52 @@ globalThis.open_theme_editor = async function(): Promise<void> {
|
|
|
1196
1629
|
// Load available themes
|
|
1197
1630
|
state.builtinThemes = await loadBuiltinThemes();
|
|
1198
1631
|
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1632
|
+
// Get current theme name from config
|
|
1633
|
+
const config = editor.getConfig() as Record<string, unknown>;
|
|
1634
|
+
const currentThemeName = (config?.theme as string) || "dark";
|
|
1635
|
+
|
|
1636
|
+
// Prompt user to select which theme to edit
|
|
1637
|
+
editor.startPrompt(editor.t("prompt.select_theme_to_edit"), "theme-select-initial");
|
|
1638
|
+
|
|
1639
|
+
const suggestions: PromptSuggestion[] = [];
|
|
1640
|
+
|
|
1641
|
+
// Add user themes first
|
|
1642
|
+
const userThemes = listUserThemes();
|
|
1643
|
+
for (const name of userThemes) {
|
|
1644
|
+
const isCurrent = name === currentThemeName;
|
|
1645
|
+
suggestions.push({
|
|
1646
|
+
text: name,
|
|
1647
|
+
description: isCurrent ? editor.t("suggestion.user_theme_current") : editor.t("suggestion.user_theme"),
|
|
1648
|
+
value: `user:${name}`,
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Add built-in themes
|
|
1653
|
+
for (const name of state.builtinThemes) {
|
|
1654
|
+
const isCurrent = name === currentThemeName;
|
|
1655
|
+
suggestions.push({
|
|
1656
|
+
text: name,
|
|
1657
|
+
description: isCurrent ? editor.t("suggestion.builtin_theme_current") : editor.t("suggestion.builtin_theme"),
|
|
1658
|
+
value: `builtin:${name}`,
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Sort suggestions to put current theme first
|
|
1663
|
+
suggestions.sort((a, b) => {
|
|
1664
|
+
const aIsCurrent = a.description.includes("current");
|
|
1665
|
+
const bIsCurrent = b.description.includes("current");
|
|
1666
|
+
if (aIsCurrent && !bIsCurrent) return -1;
|
|
1667
|
+
if (!aIsCurrent && bIsCurrent) return 1;
|
|
1668
|
+
return 0;
|
|
1669
|
+
});
|
|
1205
1670
|
|
|
1671
|
+
editor.setPromptSuggestions(suggestions);
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* Actually open the theme editor with loaded theme data
|
|
1676
|
+
*/
|
|
1677
|
+
async function doOpenThemeEditor(): Promise<void> {
|
|
1206
1678
|
// Build initial entries
|
|
1207
1679
|
const entries = buildDisplayEntries();
|
|
1208
1680
|
|
|
@@ -1222,14 +1694,12 @@ globalThis.open_theme_editor = async function(): Promise<void> {
|
|
|
1222
1694
|
state.bufferId = bufferId;
|
|
1223
1695
|
state.splitId = null;
|
|
1224
1696
|
|
|
1225
|
-
editor.setContext("theme-editor", true);
|
|
1226
|
-
|
|
1227
1697
|
applyHighlighting();
|
|
1228
1698
|
editor.setStatus(editor.t("status.ready"));
|
|
1229
1699
|
} else {
|
|
1230
1700
|
editor.setStatus(editor.t("status.open_failed"));
|
|
1231
1701
|
}
|
|
1232
|
-
}
|
|
1702
|
+
}
|
|
1233
1703
|
|
|
1234
1704
|
/**
|
|
1235
1705
|
* Close the theme editor
|
|
@@ -1238,11 +1708,23 @@ globalThis.theme_editor_close = function(): void {
|
|
|
1238
1708
|
if (!state.isOpen) return;
|
|
1239
1709
|
|
|
1240
1710
|
if (state.hasChanges) {
|
|
1241
|
-
|
|
1711
|
+
// Show confirmation prompt before closing with unsaved changes
|
|
1712
|
+
editor.startPrompt(editor.t("prompt.discard_confirm"), "theme-discard-confirm");
|
|
1713
|
+
const suggestions: PromptSuggestion[] = [
|
|
1714
|
+
{ text: editor.t("prompt.discard_yes"), description: "", value: "discard" },
|
|
1715
|
+
{ text: editor.t("prompt.discard_no"), description: "", value: "keep" },
|
|
1716
|
+
];
|
|
1717
|
+
editor.setPromptSuggestions(suggestions);
|
|
1718
|
+
return;
|
|
1242
1719
|
}
|
|
1243
1720
|
|
|
1244
|
-
|
|
1721
|
+
doCloseEditor();
|
|
1722
|
+
};
|
|
1245
1723
|
|
|
1724
|
+
/**
|
|
1725
|
+
* Actually close the editor (called after confirmation or when no changes)
|
|
1726
|
+
*/
|
|
1727
|
+
function doCloseEditor(): void {
|
|
1246
1728
|
// Close the buffer (this will switch to another buffer in the same split)
|
|
1247
1729
|
if (state.bufferId !== null) {
|
|
1248
1730
|
editor.closeBuffer(state.bufferId);
|
|
@@ -1257,6 +1739,27 @@ globalThis.theme_editor_close = function(): void {
|
|
|
1257
1739
|
state.hasChanges = false;
|
|
1258
1740
|
|
|
1259
1741
|
editor.setStatus(editor.t("status.closed"));
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Handle discard confirmation prompt
|
|
1746
|
+
*/
|
|
1747
|
+
globalThis.onThemeDiscardPromptConfirmed = function(args: {
|
|
1748
|
+
prompt_type: string;
|
|
1749
|
+
selected_index: number | null;
|
|
1750
|
+
input: string;
|
|
1751
|
+
}): boolean {
|
|
1752
|
+
if (args.prompt_type !== "theme-discard-confirm") return true;
|
|
1753
|
+
|
|
1754
|
+
const response = args.input.trim().toLowerCase();
|
|
1755
|
+
if (response === "discard" || args.selected_index === 0) {
|
|
1756
|
+
editor.setStatus(editor.t("status.unsaved_discarded"));
|
|
1757
|
+
doCloseEditor();
|
|
1758
|
+
} else {
|
|
1759
|
+
editor.setStatus(editor.t("status.cancelled"));
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
return false;
|
|
1260
1763
|
};
|
|
1261
1764
|
|
|
1262
1765
|
/**
|
|
@@ -1297,49 +1800,116 @@ globalThis.theme_editor_toggle_section = function(): void {
|
|
|
1297
1800
|
};
|
|
1298
1801
|
|
|
1299
1802
|
/**
|
|
1300
|
-
*
|
|
1803
|
+
* Open a theme (builtin or user) for editing
|
|
1301
1804
|
*/
|
|
1302
|
-
globalThis.
|
|
1303
|
-
editor.startPrompt(editor.t("prompt.
|
|
1805
|
+
globalThis.theme_editor_open = function(): void {
|
|
1806
|
+
editor.startPrompt(editor.t("prompt.open_theme"), "theme-open");
|
|
1304
1807
|
|
|
1305
|
-
const suggestions: PromptSuggestion[] =
|
|
1306
|
-
text: name,
|
|
1307
|
-
description: editor.t("suggestion.builtin_theme"),
|
|
1308
|
-
value: name,
|
|
1309
|
-
}));
|
|
1808
|
+
const suggestions: PromptSuggestion[] = [];
|
|
1310
1809
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1810
|
+
// Add user themes first
|
|
1811
|
+
const userThemes = listUserThemes();
|
|
1812
|
+
for (const name of userThemes) {
|
|
1813
|
+
suggestions.push({
|
|
1814
|
+
text: name,
|
|
1815
|
+
description: editor.t("suggestion.user_theme"),
|
|
1816
|
+
value: `user:${name}`,
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1313
1819
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1820
|
+
// Add built-in themes
|
|
1821
|
+
for (const name of state.builtinThemes) {
|
|
1822
|
+
suggestions.push({
|
|
1823
|
+
text: name,
|
|
1824
|
+
description: editor.t("suggestion.builtin_theme"),
|
|
1825
|
+
value: `builtin:${name}`,
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1319
1828
|
|
|
1320
|
-
editor.setPromptSuggestions(
|
|
1321
|
-
text: state.themeName,
|
|
1322
|
-
description: editor.t("suggestion.current"),
|
|
1323
|
-
value: state.themeName,
|
|
1324
|
-
}]);
|
|
1829
|
+
editor.setPromptSuggestions(suggestions);
|
|
1325
1830
|
};
|
|
1326
1831
|
|
|
1327
1832
|
/**
|
|
1328
1833
|
* Save theme
|
|
1329
1834
|
*/
|
|
1330
1835
|
globalThis.theme_editor_save = async function(): Promise<void> {
|
|
1331
|
-
|
|
1836
|
+
// Save cursor path for restoration after save
|
|
1837
|
+
state.savedCursorPath = getCurrentFieldPath();
|
|
1838
|
+
|
|
1839
|
+
// Built-in themes require Save As
|
|
1840
|
+
if (state.isBuiltin) {
|
|
1841
|
+
editor.setStatus(editor.t("status.builtin_requires_save_as"));
|
|
1842
|
+
theme_editor_save_as();
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// If theme has never been saved (no path), trigger "Save As" instead
|
|
1847
|
+
if (!state.themePath) {
|
|
1848
|
+
theme_editor_save_as();
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
if (!state.hasChanges) {
|
|
1332
1853
|
editor.setStatus(editor.t("status.no_changes"));
|
|
1333
1854
|
return;
|
|
1334
1855
|
}
|
|
1335
1856
|
|
|
1336
|
-
|
|
1857
|
+
// Check for name collision if name has changed since last save
|
|
1858
|
+
const userThemesDir = getUserThemesDir();
|
|
1859
|
+
const targetPath = editor.pathJoin(userThemesDir, `${state.themeName}.json`);
|
|
1860
|
+
|
|
1861
|
+
if (state.themePath !== targetPath && editor.fileExists(targetPath)) {
|
|
1862
|
+
// File exists with this name - ask for confirmation
|
|
1863
|
+
editor.startPrompt(editor.t("prompt.overwrite_confirm", { name: state.themeName }), "theme-overwrite-confirm");
|
|
1864
|
+
const suggestions: PromptSuggestion[] = [
|
|
1865
|
+
{ text: editor.t("prompt.overwrite_yes"), description: "", value: "overwrite" },
|
|
1866
|
+
{ text: editor.t("prompt.overwrite_no"), description: "", value: "cancel" },
|
|
1867
|
+
];
|
|
1868
|
+
editor.setPromptSuggestions(suggestions);
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
await saveTheme(undefined, state.savedCursorPath);
|
|
1873
|
+
};
|
|
1874
|
+
|
|
1875
|
+
/**
|
|
1876
|
+
* Handle overwrite confirmation prompt
|
|
1877
|
+
*/
|
|
1878
|
+
globalThis.onThemeOverwritePromptConfirmed = async function(args: {
|
|
1879
|
+
prompt_type: string;
|
|
1880
|
+
selected_index: number | null;
|
|
1881
|
+
input: string;
|
|
1882
|
+
}): Promise<boolean> {
|
|
1883
|
+
if (args.prompt_type !== "theme-overwrite-confirm") return true;
|
|
1884
|
+
|
|
1885
|
+
const response = args.input.trim().toLowerCase();
|
|
1886
|
+
if (response === "overwrite" || args.selected_index === 0) {
|
|
1887
|
+
// Use pending name if set (from Save As), otherwise use current name
|
|
1888
|
+
const nameToSave = state.pendingSaveName || state.themeName;
|
|
1889
|
+
state.themeName = nameToSave;
|
|
1890
|
+
state.themeData.name = nameToSave;
|
|
1891
|
+
state.pendingSaveName = null;
|
|
1892
|
+
const restorePath = state.savedCursorPath;
|
|
1893
|
+
state.savedCursorPath = null;
|
|
1894
|
+
await saveTheme(nameToSave, restorePath);
|
|
1895
|
+
} else {
|
|
1896
|
+
state.pendingSaveName = null;
|
|
1897
|
+
state.savedCursorPath = null;
|
|
1898
|
+
editor.setStatus(editor.t("status.cancelled"));
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
return false;
|
|
1337
1902
|
};
|
|
1338
1903
|
|
|
1339
1904
|
/**
|
|
1340
1905
|
* Save theme as (new name)
|
|
1341
1906
|
*/
|
|
1342
1907
|
globalThis.theme_editor_save_as = function(): void {
|
|
1908
|
+
// Save cursor path for restoration after save (if not already saved by theme_editor_save)
|
|
1909
|
+
if (!state.savedCursorPath) {
|
|
1910
|
+
state.savedCursorPath = getCurrentFieldPath();
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1343
1913
|
editor.startPrompt(editor.t("prompt.save_as"), "theme-save-as");
|
|
1344
1914
|
|
|
1345
1915
|
editor.setPromptSuggestions([{
|
|
@@ -1349,34 +1919,6 @@ globalThis.theme_editor_save_as = function(): void {
|
|
|
1349
1919
|
}]);
|
|
1350
1920
|
};
|
|
1351
1921
|
|
|
1352
|
-
/**
|
|
1353
|
-
* Set current theme as default
|
|
1354
|
-
*/
|
|
1355
|
-
globalThis.theme_editor_set_as_default = function(): void {
|
|
1356
|
-
editor.startPrompt(editor.t("prompt.set_default"), "theme-set-default");
|
|
1357
|
-
|
|
1358
|
-
// Suggest current theme and all builtins
|
|
1359
|
-
const suggestions: PromptSuggestion[] = [];
|
|
1360
|
-
|
|
1361
|
-
if (state.themeName && state.themePath) {
|
|
1362
|
-
suggestions.push({
|
|
1363
|
-
text: state.themeName,
|
|
1364
|
-
description: editor.t("suggestion.current"),
|
|
1365
|
-
value: state.themeName,
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
for (const name of state.builtinThemes) {
|
|
1370
|
-
suggestions.push({
|
|
1371
|
-
text: name,
|
|
1372
|
-
description: editor.t("suggestion.builtin"),
|
|
1373
|
-
value: name,
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
editor.setPromptSuggestions(suggestions);
|
|
1378
|
-
};
|
|
1379
|
-
|
|
1380
1922
|
/**
|
|
1381
1923
|
* Reload theme
|
|
1382
1924
|
*/
|
|
@@ -1407,11 +1949,68 @@ globalThis.theme_editor_show_help = function(): void {
|
|
|
1407
1949
|
editor.setStatus(editor.t("status.help"));
|
|
1408
1950
|
};
|
|
1409
1951
|
|
|
1952
|
+
/**
|
|
1953
|
+
* Delete the current user theme
|
|
1954
|
+
*/
|
|
1955
|
+
globalThis.theme_editor_delete = function(): void {
|
|
1956
|
+
// Can only delete saved user themes
|
|
1957
|
+
if (!state.themePath) {
|
|
1958
|
+
editor.setStatus(editor.t("status.cannot_delete_unsaved"));
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// Show confirmation dialog
|
|
1963
|
+
editor.startPrompt(editor.t("prompt.delete_confirm", { name: state.themeName }), "theme-delete-confirm");
|
|
1964
|
+
const suggestions: PromptSuggestion[] = [
|
|
1965
|
+
{ text: editor.t("prompt.delete_yes"), description: "", value: "delete" },
|
|
1966
|
+
{ text: editor.t("prompt.delete_no"), description: "", value: "cancel" },
|
|
1967
|
+
];
|
|
1968
|
+
editor.setPromptSuggestions(suggestions);
|
|
1969
|
+
};
|
|
1970
|
+
|
|
1971
|
+
/**
|
|
1972
|
+
* Handle delete confirmation prompt
|
|
1973
|
+
*/
|
|
1974
|
+
globalThis.onThemeDeletePromptConfirmed = async function(args: {
|
|
1975
|
+
prompt_type: string;
|
|
1976
|
+
selected_index: number | null;
|
|
1977
|
+
input: string;
|
|
1978
|
+
}): Promise<boolean> {
|
|
1979
|
+
if (args.prompt_type !== "theme-delete-confirm") return true;
|
|
1980
|
+
|
|
1981
|
+
const value = args.input.trim();
|
|
1982
|
+
if (value === "delete" || value === editor.t("prompt.delete_yes")) {
|
|
1983
|
+
if (state.themePath) {
|
|
1984
|
+
try {
|
|
1985
|
+
// Delete the theme file
|
|
1986
|
+
await editor.deleteFile(state.themePath);
|
|
1987
|
+
const deletedName = state.themeName;
|
|
1988
|
+
|
|
1989
|
+
// Reset to default theme
|
|
1990
|
+
state.themeData = createDefaultTheme();
|
|
1991
|
+
state.originalThemeData = deepClone(state.themeData);
|
|
1992
|
+
state.themeName = "custom";
|
|
1993
|
+
state.themePath = null;
|
|
1994
|
+
state.hasChanges = false;
|
|
1995
|
+
updateDisplay();
|
|
1996
|
+
|
|
1997
|
+
editor.setStatus(editor.t("status.deleted", { name: deletedName }));
|
|
1998
|
+
} catch (e) {
|
|
1999
|
+
editor.setStatus(editor.t("status.delete_failed", { error: String(e) }));
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
} else {
|
|
2003
|
+
editor.setStatus(editor.t("status.cancelled"));
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
return true;
|
|
2007
|
+
};
|
|
2008
|
+
|
|
1410
2009
|
// =============================================================================
|
|
1411
2010
|
// Command Registration
|
|
1412
2011
|
// =============================================================================
|
|
1413
2012
|
|
|
1414
|
-
// Main command to open theme editor
|
|
2013
|
+
// Main command to open theme editor (always available)
|
|
1415
2014
|
editor.registerCommand(
|
|
1416
2015
|
"%cmd.edit_theme",
|
|
1417
2016
|
"%cmd.edit_theme_desc",
|
|
@@ -1419,76 +2018,21 @@ editor.registerCommand(
|
|
|
1419
2018
|
"normal"
|
|
1420
2019
|
);
|
|
1421
2020
|
|
|
1422
|
-
//
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
);
|
|
1429
|
-
|
|
1430
|
-
editor.registerCommand(
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
);
|
|
1436
|
-
|
|
1437
|
-
editor.registerCommand(
|
|
1438
|
-
"%cmd.toggle_section",
|
|
1439
|
-
"%cmd.toggle_section_desc",
|
|
1440
|
-
"theme_editor_toggle_section",
|
|
1441
|
-
"normal,theme-editor"
|
|
1442
|
-
);
|
|
1443
|
-
|
|
1444
|
-
editor.registerCommand(
|
|
1445
|
-
"%cmd.copy_builtin",
|
|
1446
|
-
"%cmd.copy_builtin_desc",
|
|
1447
|
-
"theme_editor_copy_from_builtin",
|
|
1448
|
-
"normal,theme-editor"
|
|
1449
|
-
);
|
|
1450
|
-
|
|
1451
|
-
editor.registerCommand(
|
|
1452
|
-
"%cmd.set_name",
|
|
1453
|
-
"%cmd.set_name_desc",
|
|
1454
|
-
"theme_editor_set_name",
|
|
1455
|
-
"normal,theme-editor"
|
|
1456
|
-
);
|
|
1457
|
-
|
|
1458
|
-
editor.registerCommand(
|
|
1459
|
-
"%cmd.save",
|
|
1460
|
-
"%cmd.save_desc",
|
|
1461
|
-
"theme_editor_save",
|
|
1462
|
-
"normal,theme-editor"
|
|
1463
|
-
);
|
|
1464
|
-
|
|
1465
|
-
editor.registerCommand(
|
|
1466
|
-
"%cmd.save_as",
|
|
1467
|
-
"%cmd.save_as_desc",
|
|
1468
|
-
"theme_editor_save_as",
|
|
1469
|
-
"normal,theme-editor"
|
|
1470
|
-
);
|
|
1471
|
-
|
|
1472
|
-
editor.registerCommand(
|
|
1473
|
-
"%cmd.set_default",
|
|
1474
|
-
"%cmd.set_default_desc",
|
|
1475
|
-
"theme_editor_set_as_default",
|
|
1476
|
-
"normal,theme-editor"
|
|
1477
|
-
);
|
|
1478
|
-
|
|
1479
|
-
editor.registerCommand(
|
|
1480
|
-
"%cmd.reload",
|
|
1481
|
-
"%cmd.reload_desc",
|
|
1482
|
-
"theme_editor_reload",
|
|
1483
|
-
"normal,theme-editor"
|
|
1484
|
-
);
|
|
1485
|
-
|
|
1486
|
-
editor.registerCommand(
|
|
1487
|
-
"%cmd.show_help",
|
|
1488
|
-
"%cmd.show_help_desc",
|
|
1489
|
-
"theme_editor_show_help",
|
|
1490
|
-
"normal,theme-editor"
|
|
1491
|
-
);
|
|
2021
|
+
// Buffer-scoped commands - only visible when a buffer with mode "theme-editor" is focused
|
|
2022
|
+
// The core automatically checks the focused buffer's mode against command contexts
|
|
2023
|
+
editor.registerCommand("%cmd.close_editor", "%cmd.close_editor_desc", "theme_editor_close", "theme-editor");
|
|
2024
|
+
editor.registerCommand("%cmd.edit_color", "%cmd.edit_color_desc", "theme_editor_edit_color", "theme-editor");
|
|
2025
|
+
editor.registerCommand("%cmd.toggle_section", "%cmd.toggle_section_desc", "theme_editor_toggle_section", "theme-editor");
|
|
2026
|
+
editor.registerCommand("%cmd.open_theme", "%cmd.open_theme_desc", "theme_editor_open", "theme-editor");
|
|
2027
|
+
editor.registerCommand("%cmd.save", "%cmd.save_desc", "theme_editor_save", "theme-editor");
|
|
2028
|
+
editor.registerCommand("%cmd.save_as", "%cmd.save_as_desc", "theme_editor_save_as", "theme-editor");
|
|
2029
|
+
editor.registerCommand("%cmd.reload", "%cmd.reload_desc", "theme_editor_reload", "theme-editor");
|
|
2030
|
+
editor.registerCommand("%cmd.show_help", "%cmd.show_help_desc", "theme_editor_show_help", "theme-editor");
|
|
2031
|
+
editor.registerCommand("%cmd.delete_theme", "%cmd.delete_theme_desc", "theme_editor_delete", "theme-editor");
|
|
2032
|
+
editor.registerCommand("%cmd.nav_up", "%cmd.nav_up_desc", "theme_editor_nav_up", "theme-editor");
|
|
2033
|
+
editor.registerCommand("%cmd.nav_down", "%cmd.nav_down_desc", "theme_editor_nav_down", "theme-editor");
|
|
2034
|
+
editor.registerCommand("%cmd.nav_next", "%cmd.nav_next_desc", "theme_editor_nav_next_section", "theme-editor");
|
|
2035
|
+
editor.registerCommand("%cmd.nav_prev", "%cmd.nav_prev_desc", "theme_editor_nav_prev_section", "theme-editor");
|
|
1492
2036
|
|
|
1493
2037
|
// =============================================================================
|
|
1494
2038
|
// Plugin Initialization
|