@fresh-editor/fresh-editor 0.1.69 → 0.1.71
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 +61 -0
- package/package.json +1 -1
- package/plugins/audit_mode.i18n.json +441 -63
- package/plugins/audit_mode.ts +231 -234
- package/plugins/buffer_modified.i18n.json +34 -4
- package/plugins/calculator.i18n.json +49 -7
- package/plugins/clangd_support.i18n.json +119 -17
- package/plugins/color_highlighter.i18n.json +86 -20
- package/plugins/config-schema.json +3 -5
- package/plugins/csharp_support.i18n.json +43 -7
- package/plugins/diagnostics_panel.i18n.json +127 -19
- package/plugins/find_references.i18n.json +167 -41
- package/plugins/git_blame.i18n.json +262 -34
- package/plugins/git_find_file.i18n.json +182 -38
- package/plugins/git_grep.i18n.json +95 -17
- package/plugins/git_gutter.i18n.json +54 -12
- package/plugins/git_log.i18n.json +256 -34
- package/plugins/lib/fresh.d.ts +94 -0
- package/plugins/live_grep.i18n.json +104 -26
- package/plugins/markdown_compose.i18n.json +123 -21
- package/plugins/merge_conflict.i18n.json +440 -62
- package/plugins/path_complete.i18n.json +44 -8
- package/plugins/search_replace.i18n.json +239 -53
- package/plugins/test_i18n.i18n.json +50 -0
- package/plugins/theme_editor.i18n.json +2560 -519
- package/plugins/theme_editor.ts +1007 -469
- package/plugins/todo_highlighter.i18n.json +108 -24
- package/plugins/vi_mode.i18n.json +825 -111
- package/plugins/welcome.i18n.json +127 -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,6 +482,37 @@ 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
518
|
* Uses XDG_CONFIG_HOME if set, otherwise falls back to $HOME/.config
|
|
@@ -494,16 +542,17 @@ function getUserThemesDir(): string {
|
|
|
494
542
|
*/
|
|
495
543
|
function buildVisibleFields(): ThemeField[] {
|
|
496
544
|
const fields: ThemeField[] = [];
|
|
545
|
+
const themeSections = getThemeSections();
|
|
497
546
|
|
|
498
|
-
for (const section of
|
|
547
|
+
for (const section of themeSections) {
|
|
499
548
|
const expanded = state.expandedSections.has(section.name);
|
|
500
549
|
|
|
501
|
-
// Section header
|
|
550
|
+
// Section header - displayName and description are already translated in getThemeSections()
|
|
502
551
|
fields.push({
|
|
503
552
|
def: {
|
|
504
553
|
key: section.name,
|
|
505
|
-
displayName:
|
|
506
|
-
description:
|
|
554
|
+
displayName: section.displayName,
|
|
555
|
+
description: section.description,
|
|
507
556
|
section: section.name,
|
|
508
557
|
},
|
|
509
558
|
value: [0, 0, 0], // Placeholder
|
|
@@ -519,12 +568,9 @@ function buildVisibleFields(): ThemeField[] {
|
|
|
519
568
|
const path = `${section.name}.${fieldDef.key}`;
|
|
520
569
|
const value = getNestedValue(state.themeData, path) as ColorValue || [128, 128, 128];
|
|
521
570
|
|
|
571
|
+
// fieldDef displayName and description are already translated in getThemeSections()
|
|
522
572
|
fields.push({
|
|
523
|
-
def:
|
|
524
|
-
...fieldDef,
|
|
525
|
-
displayName: editor.t(`field.${fieldDef.key}`),
|
|
526
|
-
description: editor.t(`field.${fieldDef.key}_desc`),
|
|
527
|
-
},
|
|
573
|
+
def: fieldDef,
|
|
528
574
|
value,
|
|
529
575
|
path,
|
|
530
576
|
depth: 1,
|
|
@@ -566,6 +612,21 @@ function buildDisplayEntries(): TextPropertyEntry[] {
|
|
|
566
612
|
});
|
|
567
613
|
}
|
|
568
614
|
|
|
615
|
+
// Key hints at the top (moved from footer)
|
|
616
|
+
entries.push({
|
|
617
|
+
text: editor.t("panel.nav_hint") + "\n",
|
|
618
|
+
properties: { type: "footer" },
|
|
619
|
+
});
|
|
620
|
+
entries.push({
|
|
621
|
+
text: editor.t("panel.action_hint", SHORTCUTS) + "\n",
|
|
622
|
+
properties: { type: "footer" },
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
entries.push({
|
|
626
|
+
text: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
|
|
627
|
+
properties: { type: "separator" },
|
|
628
|
+
});
|
|
629
|
+
|
|
569
630
|
entries.push({
|
|
570
631
|
text: "\n",
|
|
571
632
|
properties: { type: "blank" },
|
|
@@ -603,11 +664,11 @@ function buildDisplayEntries(): TextPropertyEntry[] {
|
|
|
603
664
|
properties: { type: "description", path: field.path },
|
|
604
665
|
});
|
|
605
666
|
|
|
606
|
-
// Color field with swatch
|
|
667
|
+
// Color field with swatch characters (X for fg preview, space for bg preview)
|
|
607
668
|
const colorStr = formatColorValue(field.value);
|
|
608
669
|
|
|
609
670
|
entries.push({
|
|
610
|
-
text: `${indent} ${field.def.displayName}: ${colorStr}\n`,
|
|
671
|
+
text: `${indent} ${field.def.displayName}: X ${colorStr}\n`,
|
|
611
672
|
properties: {
|
|
612
673
|
type: "field",
|
|
613
674
|
path: field.path,
|
|
@@ -623,25 +684,12 @@ function buildDisplayEntries(): TextPropertyEntry[] {
|
|
|
623
684
|
});
|
|
624
685
|
}
|
|
625
686
|
|
|
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
687
|
return entries;
|
|
641
688
|
}
|
|
642
689
|
|
|
643
690
|
/**
|
|
644
|
-
* Helper to add a colored overlay
|
|
691
|
+
* Helper to add a colored overlay (foreground color)
|
|
692
|
+
* addOverlay signature: (bufferId, namespace, start, end, r, g, b, underline, bold, italic, bg_r, bg_g, bg_b, extend_to_line_end)
|
|
645
693
|
*/
|
|
646
694
|
function addColorOverlay(
|
|
647
695
|
bufferId: number,
|
|
@@ -650,7 +698,20 @@ function addColorOverlay(
|
|
|
650
698
|
color: RGB,
|
|
651
699
|
bold: boolean = false
|
|
652
700
|
): void {
|
|
653
|
-
editor.addOverlay(bufferId, "theme", start, end, color[0], color[1], color[2], false, bold, false);
|
|
701
|
+
editor.addOverlay(bufferId, "theme", start, end, color[0], color[1], color[2], false, bold, false, -1, -1, -1, false);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Helper to add a background highlight overlay
|
|
706
|
+
* addOverlay signature: (bufferId, namespace, start, end, r, g, b, underline, bold, italic, bg_r, bg_g, bg_b, extend_to_line_end)
|
|
707
|
+
*/
|
|
708
|
+
function addBackgroundHighlight(
|
|
709
|
+
bufferId: number,
|
|
710
|
+
start: number,
|
|
711
|
+
end: number,
|
|
712
|
+
bgColor: RGB
|
|
713
|
+
): void {
|
|
714
|
+
editor.addOverlay(bufferId, "theme-selection", start, end, -1, -1, -1, false, false, false, bgColor[0], bgColor[1], bgColor[2], true);
|
|
654
715
|
}
|
|
655
716
|
|
|
656
717
|
/**
|
|
@@ -669,70 +730,6 @@ function isSpecialColor(value: ColorValue): boolean {
|
|
|
669
730
|
return typeof value === "string" && SPECIAL_COLORS.includes(value);
|
|
670
731
|
}
|
|
671
732
|
|
|
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
733
|
/**
|
|
737
734
|
* Apply syntax highlighting
|
|
738
735
|
*/
|
|
@@ -741,15 +738,26 @@ function applyHighlighting(): void {
|
|
|
741
738
|
|
|
742
739
|
const bufferId = state.bufferId;
|
|
743
740
|
editor.clearNamespace(bufferId, "theme");
|
|
741
|
+
editor.clearNamespace(bufferId, "theme-selection");
|
|
744
742
|
|
|
745
743
|
const entries = buildDisplayEntries();
|
|
746
744
|
let byteOffset = 0;
|
|
747
745
|
|
|
746
|
+
// Get current field at cursor to highlight it
|
|
747
|
+
const currentField = getFieldAtCursor();
|
|
748
|
+
const currentFieldPath = currentField?.path;
|
|
749
|
+
|
|
748
750
|
for (const entry of entries) {
|
|
749
751
|
const text = entry.text;
|
|
750
752
|
const textLen = getUtf8ByteLength(text);
|
|
751
753
|
const props = entry.properties as Record<string, unknown>;
|
|
752
754
|
const entryType = props.type as string;
|
|
755
|
+
const entryPath = props.path as string | undefined;
|
|
756
|
+
|
|
757
|
+
// Add selection highlight for current field/section
|
|
758
|
+
if (currentFieldPath && entryPath === currentFieldPath && (entryType === "field" || entryType === "section")) {
|
|
759
|
+
addBackgroundHighlight(bufferId, byteOffset, byteOffset + textLen, colors.selectionBg);
|
|
760
|
+
}
|
|
753
761
|
|
|
754
762
|
if (entryType === "title") {
|
|
755
763
|
addColorOverlay(bufferId, byteOffset, byteOffset + textLen, colors.sectionHeader, true);
|
|
@@ -766,8 +774,26 @@ function applyHighlighting(): void {
|
|
|
766
774
|
const nameEnd = byteOffset + getUtf8ByteLength(text.substring(0, colonPos));
|
|
767
775
|
addColorOverlay(bufferId, byteOffset, nameEnd, colors.fieldName);
|
|
768
776
|
|
|
769
|
-
//
|
|
770
|
-
|
|
777
|
+
// Color the swatch characters with the field's actual color
|
|
778
|
+
// Text format: "FieldName: X #RRGGBB" (X=fg, space=bg)
|
|
779
|
+
const colorValue = props.colorValue as ColorValue;
|
|
780
|
+
const rgb = parseColorToRgb(colorValue);
|
|
781
|
+
if (rgb) {
|
|
782
|
+
// "X" is at colon + 2 (": " = 2 bytes), and is 1 byte
|
|
783
|
+
const swatchFgStart = nameEnd + getUtf8ByteLength(": ");
|
|
784
|
+
const swatchFgEnd = swatchFgStart + 1; // "X" is 1 byte
|
|
785
|
+
addColorOverlay(bufferId, swatchFgStart, swatchFgEnd, rgb);
|
|
786
|
+
|
|
787
|
+
// First space after "X" is the bg swatch, 1 byte
|
|
788
|
+
const swatchBgStart = swatchFgEnd;
|
|
789
|
+
const swatchBgEnd = swatchBgStart + 1;
|
|
790
|
+
// Use background color for the space
|
|
791
|
+
editor.addOverlay(bufferId, "theme", swatchBgStart, swatchBgEnd, -1, -1, -1, false, false, false, rgb[0], rgb[1], rgb[2], false);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Value (hex code) - custom color (green)
|
|
795
|
+
// Format: ": X #RRGGBB" - value starts after "X " (X + 2 spaces)
|
|
796
|
+
const valueStart = nameEnd + getUtf8ByteLength(": X ");
|
|
771
797
|
addColorOverlay(bufferId, valueStart, byteOffset + textLen, colors.customValue);
|
|
772
798
|
}
|
|
773
799
|
} else if (entryType === "separator" || entryType === "footer") {
|
|
@@ -776,20 +802,25 @@ function applyHighlighting(): void {
|
|
|
776
802
|
|
|
777
803
|
byteOffset += textLen;
|
|
778
804
|
}
|
|
779
|
-
|
|
780
|
-
// Add color swatches
|
|
781
|
-
addColorSwatches();
|
|
782
805
|
}
|
|
783
806
|
|
|
784
807
|
/**
|
|
785
|
-
* Update display
|
|
808
|
+
* Update display (preserves cursor position)
|
|
786
809
|
*/
|
|
787
810
|
function updateDisplay(): void {
|
|
788
811
|
if (state.bufferId === null) return;
|
|
789
812
|
|
|
813
|
+
// Save current field path before updating
|
|
814
|
+
const currentPath = getCurrentFieldPath();
|
|
815
|
+
|
|
790
816
|
const entries = buildDisplayEntries();
|
|
791
817
|
editor.setVirtualBufferContent(state.bufferId, entries);
|
|
792
818
|
applyHighlighting();
|
|
819
|
+
|
|
820
|
+
// Restore cursor to the same field if possible
|
|
821
|
+
if (currentPath) {
|
|
822
|
+
moveCursorToField(currentPath);
|
|
823
|
+
}
|
|
793
824
|
}
|
|
794
825
|
|
|
795
826
|
// =============================================================================
|
|
@@ -814,66 +845,87 @@ function getFieldAtCursor(): ThemeField | null {
|
|
|
814
845
|
}
|
|
815
846
|
|
|
816
847
|
/**
|
|
817
|
-
*
|
|
848
|
+
* Get field by path
|
|
818
849
|
*/
|
|
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);
|
|
850
|
+
function getFieldByPath(path: string): ThemeField | null {
|
|
851
|
+
return state.visibleFields.find(f => f.path === path) || null;
|
|
852
|
+
}
|
|
824
853
|
|
|
825
|
-
|
|
854
|
+
/**
|
|
855
|
+
* Build color suggestions for a field
|
|
856
|
+
*/
|
|
857
|
+
function buildColorSuggestions(field: ThemeField): PromptSuggestion[] {
|
|
858
|
+
const currentValue = formatColorValue(field.value);
|
|
826
859
|
const suggestions: PromptSuggestion[] = [
|
|
827
|
-
{
|
|
828
|
-
text: currentValue,
|
|
829
|
-
description: editor.t("suggestion.current"),
|
|
830
|
-
value: currentValue,
|
|
831
|
-
},
|
|
860
|
+
{ text: currentValue, description: editor.t("suggestion.current"), value: currentValue },
|
|
832
861
|
];
|
|
833
862
|
|
|
834
|
-
// Add special colors
|
|
863
|
+
// Add special colors (Default/Reset for terminal transparency)
|
|
835
864
|
for (const name of SPECIAL_COLORS) {
|
|
836
|
-
suggestions.push({
|
|
837
|
-
text: name,
|
|
838
|
-
description: editor.t("suggestion.terminal_native"),
|
|
839
|
-
value: name,
|
|
840
|
-
});
|
|
865
|
+
suggestions.push({ text: name, description: editor.t("suggestion.terminal_native"), value: name });
|
|
841
866
|
}
|
|
842
867
|
|
|
843
|
-
// Add named colors
|
|
868
|
+
// Add named colors with hex format
|
|
844
869
|
for (const name of NAMED_COLOR_LIST) {
|
|
845
870
|
const rgb = NAMED_COLORS[name];
|
|
846
871
|
const hexValue = rgbToHex(rgb[0], rgb[1], rgb[2]);
|
|
847
|
-
suggestions.push({
|
|
848
|
-
text: name,
|
|
849
|
-
description: hexValue,
|
|
850
|
-
value: name,
|
|
851
|
-
});
|
|
872
|
+
suggestions.push({ text: name, description: hexValue, value: name });
|
|
852
873
|
}
|
|
853
874
|
|
|
854
|
-
|
|
875
|
+
return suggestions;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Start color editing prompt
|
|
880
|
+
*/
|
|
881
|
+
function editColorField(field: ThemeField): void {
|
|
882
|
+
const currentValue = formatColorValue(field.value);
|
|
883
|
+
editor.startPromptWithInitial(
|
|
884
|
+
editor.t("prompt.color_input", { field: field.def.displayName }),
|
|
885
|
+
`theme-color-${field.path}`,
|
|
886
|
+
currentValue
|
|
887
|
+
);
|
|
888
|
+
editor.setPromptSuggestions(buildColorSuggestions(field));
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
interface ParseColorResult {
|
|
892
|
+
value?: ColorValue;
|
|
893
|
+
error?: string;
|
|
855
894
|
}
|
|
856
895
|
|
|
857
896
|
/**
|
|
858
|
-
* Parse color input from user
|
|
897
|
+
* Parse color input from user with detailed error messages
|
|
859
898
|
*/
|
|
860
|
-
function parseColorInput(input: string):
|
|
899
|
+
function parseColorInput(input: string): ParseColorResult {
|
|
861
900
|
input = input.trim();
|
|
862
901
|
|
|
902
|
+
if (!input) {
|
|
903
|
+
return { error: "empty" };
|
|
904
|
+
}
|
|
905
|
+
|
|
863
906
|
// Check for special colors (Default/Reset - use terminal's native color)
|
|
864
907
|
if (SPECIAL_COLORS.includes(input)) {
|
|
865
|
-
return input;
|
|
908
|
+
return { value: input };
|
|
866
909
|
}
|
|
867
910
|
|
|
868
911
|
// Check for named color
|
|
869
912
|
if (input in NAMED_COLORS) {
|
|
870
|
-
return input;
|
|
913
|
+
return { value: input };
|
|
871
914
|
}
|
|
872
915
|
|
|
873
916
|
// Try to parse as hex color #RRGGBB
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
917
|
+
if (input.startsWith("#")) {
|
|
918
|
+
const hex = input.slice(1);
|
|
919
|
+
if (hex.length !== 6) {
|
|
920
|
+
return { error: "hex_length" };
|
|
921
|
+
}
|
|
922
|
+
if (!/^[0-9A-Fa-f]{6}$/.test(hex)) {
|
|
923
|
+
return { error: "hex_invalid" };
|
|
924
|
+
}
|
|
925
|
+
const hexResult = hexToRgb(input);
|
|
926
|
+
if (hexResult) {
|
|
927
|
+
return { value: hexResult };
|
|
928
|
+
}
|
|
877
929
|
}
|
|
878
930
|
|
|
879
931
|
// Try to parse as RGB array [r, g, b]
|
|
@@ -882,19 +934,46 @@ function parseColorInput(input: string): ColorValue | null {
|
|
|
882
934
|
const r = parseInt(rgbMatch[1], 10);
|
|
883
935
|
const g = parseInt(rgbMatch[2], 10);
|
|
884
936
|
const b = parseInt(rgbMatch[3], 10);
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
return [r, g, b];
|
|
937
|
+
if (r > 255 || g > 255 || b > 255) {
|
|
938
|
+
return { error: "rgb_range" };
|
|
888
939
|
}
|
|
940
|
+
return { value: [r, g, b] };
|
|
889
941
|
}
|
|
890
942
|
|
|
891
|
-
|
|
943
|
+
// Unknown format
|
|
944
|
+
return { error: "unknown" };
|
|
892
945
|
}
|
|
893
946
|
|
|
894
947
|
// =============================================================================
|
|
895
948
|
// Prompt Handlers
|
|
896
949
|
// =============================================================================
|
|
897
950
|
|
|
951
|
+
/**
|
|
952
|
+
* Find best matching color name for partial input
|
|
953
|
+
*/
|
|
954
|
+
function findMatchingColor(input: string): string | null {
|
|
955
|
+
const lower = input.toLowerCase();
|
|
956
|
+
// First try exact match
|
|
957
|
+
for (const name of Object.keys(NAMED_COLORS)) {
|
|
958
|
+
if (name.toLowerCase() === lower) return name;
|
|
959
|
+
}
|
|
960
|
+
for (const name of SPECIAL_COLORS) {
|
|
961
|
+
if (name.toLowerCase() === lower) return name;
|
|
962
|
+
}
|
|
963
|
+
// Then try prefix match
|
|
964
|
+
for (const name of Object.keys(NAMED_COLORS)) {
|
|
965
|
+
if (name.toLowerCase().startsWith(lower)) return name;
|
|
966
|
+
}
|
|
967
|
+
for (const name of SPECIAL_COLORS) {
|
|
968
|
+
if (name.toLowerCase().startsWith(lower)) return name;
|
|
969
|
+
}
|
|
970
|
+
// Then try contains match
|
|
971
|
+
for (const name of Object.keys(NAMED_COLORS)) {
|
|
972
|
+
if (name.toLowerCase().includes(lower)) return name;
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
|
|
898
977
|
/**
|
|
899
978
|
* Handle color prompt confirmation
|
|
900
979
|
*/
|
|
@@ -906,65 +985,112 @@ globalThis.onThemeColorPromptConfirmed = function(args: {
|
|
|
906
985
|
if (!args.prompt_type.startsWith("theme-color-")) return true;
|
|
907
986
|
|
|
908
987
|
const path = args.prompt_type.replace("theme-color-", "");
|
|
909
|
-
const
|
|
988
|
+
const field = getFieldByPath(path);
|
|
989
|
+
if (!field) return true;
|
|
910
990
|
|
|
911
|
-
|
|
912
|
-
|
|
991
|
+
const result = parseColorInput(args.input);
|
|
992
|
+
|
|
993
|
+
if (result.value !== undefined) {
|
|
994
|
+
// Valid color - apply it
|
|
995
|
+
setNestedValue(state.themeData, path, result.value);
|
|
913
996
|
state.hasChanges = !deepEqual(state.themeData, state.originalThemeData);
|
|
914
|
-
|
|
997
|
+
|
|
998
|
+
const entries = buildDisplayEntries();
|
|
999
|
+
if (state.bufferId !== null) {
|
|
1000
|
+
editor.setVirtualBufferContent(state.bufferId, entries);
|
|
1001
|
+
applyHighlighting();
|
|
1002
|
+
}
|
|
1003
|
+
moveCursorToField(path);
|
|
915
1004
|
editor.setStatus(editor.t("status.updated", { path }));
|
|
916
1005
|
} else {
|
|
917
|
-
|
|
1006
|
+
// Invalid input - try to find a matching color name
|
|
1007
|
+
const matchedColor = findMatchingColor(args.input);
|
|
1008
|
+
if (matchedColor) {
|
|
1009
|
+
// Found a match - reopen prompt with the matched value
|
|
1010
|
+
editor.startPromptWithInitial(
|
|
1011
|
+
editor.t("prompt.color_input", { field: field.def.displayName }),
|
|
1012
|
+
`theme-color-${path}`,
|
|
1013
|
+
matchedColor
|
|
1014
|
+
);
|
|
1015
|
+
// Rebuild suggestions
|
|
1016
|
+
const suggestions: PromptSuggestion[] = buildColorSuggestions(field);
|
|
1017
|
+
editor.setPromptSuggestions(suggestions);
|
|
1018
|
+
editor.setStatus(editor.t("status.autocompleted", { value: matchedColor }));
|
|
1019
|
+
} else {
|
|
1020
|
+
// No match found - reopen prompt with original input
|
|
1021
|
+
editor.startPromptWithInitial(
|
|
1022
|
+
editor.t("prompt.color_input", { field: field.def.displayName }),
|
|
1023
|
+
`theme-color-${path}`,
|
|
1024
|
+
args.input
|
|
1025
|
+
);
|
|
1026
|
+
const suggestions: PromptSuggestion[] = buildColorSuggestions(field);
|
|
1027
|
+
editor.setPromptSuggestions(suggestions);
|
|
1028
|
+
|
|
1029
|
+
const errorKey = `error.color_${result.error}`;
|
|
1030
|
+
editor.setStatus(editor.t(errorKey, { input: args.input }));
|
|
1031
|
+
}
|
|
918
1032
|
}
|
|
919
1033
|
|
|
920
1034
|
return true;
|
|
921
1035
|
};
|
|
922
1036
|
|
|
923
1037
|
/**
|
|
924
|
-
* Handle theme
|
|
1038
|
+
* Handle open theme prompt (both builtin and user themes)
|
|
925
1039
|
*/
|
|
926
|
-
globalThis.
|
|
1040
|
+
globalThis.onThemeOpenPromptConfirmed = async function(args: {
|
|
927
1041
|
prompt_type: string;
|
|
928
1042
|
selected_index: number | null;
|
|
929
1043
|
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
|
-
}
|
|
1044
|
+
}): Promise<boolean> {
|
|
1045
|
+
if (args.prompt_type !== "theme-open") return true;
|
|
941
1046
|
|
|
942
|
-
|
|
943
|
-
};
|
|
1047
|
+
const value = args.input.trim();
|
|
944
1048
|
|
|
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;
|
|
1049
|
+
// Parse the value to determine if it's user or builtin
|
|
1050
|
+
let isBuiltin = false;
|
|
1051
|
+
let themeName = value;
|
|
954
1052
|
|
|
955
|
-
|
|
956
|
-
|
|
1053
|
+
if (value.startsWith("user:")) {
|
|
1054
|
+
themeName = value.slice(5);
|
|
1055
|
+
isBuiltin = false;
|
|
1056
|
+
} else if (value.startsWith("builtin:")) {
|
|
1057
|
+
themeName = value.slice(8);
|
|
1058
|
+
isBuiltin = true;
|
|
1059
|
+
} else {
|
|
1060
|
+
// Fallback: check if it's a builtin theme
|
|
1061
|
+
isBuiltin = state.builtinThemes.includes(value);
|
|
1062
|
+
}
|
|
957
1063
|
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1064
|
+
if (isBuiltin) {
|
|
1065
|
+
// Load builtin theme
|
|
1066
|
+
const themeData = await loadThemeFile(themeName);
|
|
1067
|
+
if (themeData) {
|
|
1068
|
+
state.themeData = deepClone(themeData);
|
|
1069
|
+
state.originalThemeData = deepClone(themeData);
|
|
1070
|
+
state.themeName = themeName;
|
|
1071
|
+
state.themePath = null; // No user path for builtin
|
|
1072
|
+
state.isBuiltin = true;
|
|
1073
|
+
state.hasChanges = false;
|
|
1074
|
+
updateDisplay();
|
|
1075
|
+
editor.setStatus(editor.t("status.opened_builtin", { name: themeName }));
|
|
1076
|
+
} else {
|
|
1077
|
+
editor.setStatus(editor.t("status.load_failed", { name: themeName }));
|
|
1078
|
+
}
|
|
966
1079
|
} else {
|
|
967
|
-
|
|
1080
|
+
// Load user theme
|
|
1081
|
+
const result = await loadUserThemeFile(themeName);
|
|
1082
|
+
if (result) {
|
|
1083
|
+
state.themeData = deepClone(result.data);
|
|
1084
|
+
state.originalThemeData = deepClone(result.data);
|
|
1085
|
+
state.themeName = themeName;
|
|
1086
|
+
state.themePath = result.path;
|
|
1087
|
+
state.isBuiltin = false;
|
|
1088
|
+
state.hasChanges = false;
|
|
1089
|
+
updateDisplay();
|
|
1090
|
+
editor.setStatus(editor.t("status.loaded", { name: themeName }));
|
|
1091
|
+
} else {
|
|
1092
|
+
editor.setStatus(editor.t("status.load_failed", { name: themeName }));
|
|
1093
|
+
}
|
|
968
1094
|
}
|
|
969
1095
|
|
|
970
1096
|
return true;
|
|
@@ -982,47 +1108,131 @@ globalThis.onThemeSaveAsPromptConfirmed = async function(args: {
|
|
|
982
1108
|
|
|
983
1109
|
const name = args.input.trim();
|
|
984
1110
|
if (name) {
|
|
1111
|
+
// Check if theme already exists
|
|
1112
|
+
const userThemesDir = getUserThemesDir();
|
|
1113
|
+
const targetPath = editor.pathJoin(userThemesDir, `${name}.json`);
|
|
1114
|
+
|
|
1115
|
+
if (editor.fileExists(targetPath)) {
|
|
1116
|
+
// Store pending save name for overwrite confirmation
|
|
1117
|
+
state.pendingSaveName = name;
|
|
1118
|
+
editor.startPrompt(editor.t("prompt.overwrite_confirm", { name }), "theme-overwrite-confirm");
|
|
1119
|
+
const suggestions: PromptSuggestion[] = [
|
|
1120
|
+
{ text: editor.t("prompt.overwrite_yes"), description: "", value: "overwrite" },
|
|
1121
|
+
{ text: editor.t("prompt.overwrite_no"), description: "", value: "cancel" },
|
|
1122
|
+
];
|
|
1123
|
+
editor.setPromptSuggestions(suggestions);
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
985
1127
|
state.themeName = name;
|
|
986
1128
|
state.themeData.name = name;
|
|
987
|
-
|
|
1129
|
+
const restorePath = state.savedCursorPath;
|
|
1130
|
+
state.savedCursorPath = null;
|
|
1131
|
+
await saveTheme(name, restorePath);
|
|
1132
|
+
} else {
|
|
1133
|
+
state.savedCursorPath = null;
|
|
988
1134
|
}
|
|
989
1135
|
|
|
990
1136
|
return true;
|
|
991
1137
|
};
|
|
992
1138
|
|
|
993
1139
|
/**
|
|
994
|
-
* Handle
|
|
1140
|
+
* Handle prompt cancellation
|
|
1141
|
+
*/
|
|
1142
|
+
globalThis.onThemePromptCancelled = function(args: { prompt_type: string }): boolean {
|
|
1143
|
+
if (!args.prompt_type.startsWith("theme-")) return true;
|
|
1144
|
+
|
|
1145
|
+
// Clear saved cursor path on cancellation
|
|
1146
|
+
state.savedCursorPath = null;
|
|
1147
|
+
state.pendingSaveName = null;
|
|
1148
|
+
|
|
1149
|
+
editor.setStatus(editor.t("status.cancelled"));
|
|
1150
|
+
return true;
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Handle initial theme selection prompt (when opening editor)
|
|
995
1155
|
*/
|
|
996
|
-
globalThis.
|
|
1156
|
+
globalThis.onThemeSelectInitialPromptConfirmed = async function(args: {
|
|
997
1157
|
prompt_type: string;
|
|
998
1158
|
selected_index: number | null;
|
|
999
1159
|
input: string;
|
|
1000
1160
|
}): Promise<boolean> {
|
|
1001
|
-
if (args.prompt_type !== "theme-
|
|
1161
|
+
if (args.prompt_type !== "theme-select-initial") return true;
|
|
1002
1162
|
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1163
|
+
const value = args.input.trim();
|
|
1164
|
+
|
|
1165
|
+
// Parse the value to determine if it's user or builtin
|
|
1166
|
+
let isBuiltin = false;
|
|
1167
|
+
let themeName = value;
|
|
1168
|
+
|
|
1169
|
+
if (value.startsWith("user:")) {
|
|
1170
|
+
themeName = value.slice(5);
|
|
1171
|
+
isBuiltin = false;
|
|
1172
|
+
} else if (value.startsWith("builtin:")) {
|
|
1173
|
+
themeName = value.slice(8);
|
|
1174
|
+
isBuiltin = true;
|
|
1175
|
+
} else {
|
|
1176
|
+
// Fallback: check if it's a builtin theme
|
|
1177
|
+
isBuiltin = state.builtinThemes.includes(value);
|
|
1006
1178
|
}
|
|
1007
1179
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1180
|
+
editor.setStatus(editor.t("status.loading"));
|
|
1181
|
+
|
|
1182
|
+
if (isBuiltin) {
|
|
1183
|
+
// Load builtin theme
|
|
1184
|
+
const themeData = await loadThemeFile(themeName);
|
|
1185
|
+
if (themeData) {
|
|
1186
|
+
state.themeData = deepClone(themeData);
|
|
1187
|
+
state.originalThemeData = deepClone(themeData);
|
|
1188
|
+
state.themeName = themeName;
|
|
1189
|
+
state.themePath = null; // No user path for builtin
|
|
1190
|
+
state.isBuiltin = true;
|
|
1191
|
+
state.hasChanges = false;
|
|
1192
|
+
} else {
|
|
1193
|
+
// Fallback to default theme if load failed
|
|
1194
|
+
state.themeData = createDefaultTheme();
|
|
1195
|
+
state.originalThemeData = deepClone(state.themeData);
|
|
1196
|
+
state.themeName = themeName;
|
|
1197
|
+
state.themePath = null;
|
|
1198
|
+
state.isBuiltin = true;
|
|
1199
|
+
state.hasChanges = false;
|
|
1200
|
+
}
|
|
1201
|
+
} else {
|
|
1202
|
+
// Load user theme
|
|
1203
|
+
const result = await loadUserThemeFile(themeName);
|
|
1204
|
+
if (result) {
|
|
1205
|
+
state.themeData = deepClone(result.data);
|
|
1206
|
+
state.originalThemeData = deepClone(result.data);
|
|
1207
|
+
state.themeName = themeName;
|
|
1208
|
+
state.themePath = result.path;
|
|
1209
|
+
state.isBuiltin = false;
|
|
1210
|
+
state.hasChanges = false;
|
|
1211
|
+
} else {
|
|
1212
|
+
// Fallback to default theme if load failed
|
|
1213
|
+
state.themeData = createDefaultTheme();
|
|
1214
|
+
state.originalThemeData = deepClone(state.themeData);
|
|
1215
|
+
state.themeName = themeName;
|
|
1216
|
+
state.themePath = null;
|
|
1217
|
+
state.isBuiltin = false;
|
|
1218
|
+
state.hasChanges = false;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Now open the editor with loaded theme
|
|
1223
|
+
await doOpenThemeEditor();
|
|
1010
1224
|
|
|
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
1225
|
return true;
|
|
1018
1226
|
};
|
|
1019
1227
|
|
|
1020
1228
|
// Register prompt handlers
|
|
1229
|
+
editor.on("prompt_confirmed", "onThemeSelectInitialPromptConfirmed");
|
|
1021
1230
|
editor.on("prompt_confirmed", "onThemeColorPromptConfirmed");
|
|
1022
|
-
editor.on("prompt_confirmed", "
|
|
1023
|
-
editor.on("prompt_confirmed", "onThemeCopyPromptConfirmed");
|
|
1231
|
+
editor.on("prompt_confirmed", "onThemeOpenPromptConfirmed");
|
|
1024
1232
|
editor.on("prompt_confirmed", "onThemeSaveAsPromptConfirmed");
|
|
1025
|
-
editor.on("prompt_confirmed", "
|
|
1233
|
+
editor.on("prompt_confirmed", "onThemeDiscardPromptConfirmed");
|
|
1234
|
+
editor.on("prompt_confirmed", "onThemeOverwritePromptConfirmed");
|
|
1235
|
+
editor.on("prompt_confirmed", "onThemeDeletePromptConfirmed");
|
|
1026
1236
|
editor.on("prompt_cancelled", "onThemePromptCancelled");
|
|
1027
1237
|
|
|
1028
1238
|
// =============================================================================
|
|
@@ -1031,8 +1241,10 @@ editor.on("prompt_cancelled", "onThemePromptCancelled");
|
|
|
1031
1241
|
|
|
1032
1242
|
/**
|
|
1033
1243
|
* Save theme to file
|
|
1244
|
+
* @param name - Theme name to save as
|
|
1245
|
+
* @param restorePath - Optional field path to restore cursor to after save
|
|
1034
1246
|
*/
|
|
1035
|
-
async function saveTheme(name?: string): Promise<boolean> {
|
|
1247
|
+
async function saveTheme(name?: string, restorePath?: string | null): Promise<boolean> {
|
|
1036
1248
|
const themeName = name || state.themeName;
|
|
1037
1249
|
const userThemesDir = getUserThemesDir();
|
|
1038
1250
|
|
|
@@ -1056,11 +1268,25 @@ async function saveTheme(name?: string): Promise<boolean> {
|
|
|
1056
1268
|
|
|
1057
1269
|
state.themePath = themePath;
|
|
1058
1270
|
state.themeName = themeName;
|
|
1271
|
+
state.isBuiltin = false; // After saving, it's now a user theme
|
|
1059
1272
|
state.originalThemeData = deepClone(state.themeData);
|
|
1060
1273
|
state.hasChanges = false;
|
|
1061
|
-
updateDisplay();
|
|
1062
1274
|
|
|
1063
|
-
|
|
1275
|
+
// Update display
|
|
1276
|
+
const entries = buildDisplayEntries();
|
|
1277
|
+
if (state.bufferId !== null) {
|
|
1278
|
+
editor.setVirtualBufferContent(state.bufferId, entries);
|
|
1279
|
+
applyHighlighting();
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// Restore cursor position if provided
|
|
1283
|
+
if (restorePath) {
|
|
1284
|
+
moveCursorToField(restorePath);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Automatically apply the saved theme
|
|
1288
|
+
editor.applyTheme(themeName);
|
|
1289
|
+
editor.setStatus(editor.t("status.saved_and_applied", { name: themeName }));
|
|
1064
1290
|
return true;
|
|
1065
1291
|
} catch (e) {
|
|
1066
1292
|
editor.setStatus(editor.t("status.save_failed", { error: String(e) }));
|
|
@@ -1069,20 +1295,7 @@ async function saveTheme(name?: string): Promise<boolean> {
|
|
|
1069
1295
|
}
|
|
1070
1296
|
|
|
1071
1297
|
/**
|
|
1072
|
-
*
|
|
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
|
-
/**
|
|
1085
|
-
* Create a default/empty theme
|
|
1298
|
+
* Create a default/empty theme
|
|
1086
1299
|
*/
|
|
1087
1300
|
function createDefaultTheme(): Record<string, unknown> {
|
|
1088
1301
|
return {
|
|
@@ -1174,12 +1387,228 @@ globalThis.onThemeEditorCursorMoved = function(data: {
|
|
|
1174
1387
|
|
|
1175
1388
|
editor.on("cursor_moved", "onThemeEditorCursorMoved");
|
|
1176
1389
|
|
|
1390
|
+
/**
|
|
1391
|
+
* Handle buffer_closed event to reset state when buffer is closed by any means
|
|
1392
|
+
*/
|
|
1393
|
+
globalThis.onThemeEditorBufferClosed = function(data: {
|
|
1394
|
+
buffer_id: number;
|
|
1395
|
+
}): void {
|
|
1396
|
+
if (state.bufferId !== null && data.buffer_id === state.bufferId) {
|
|
1397
|
+
// Reset state when our buffer is closed
|
|
1398
|
+
state.isOpen = false;
|
|
1399
|
+
state.bufferId = null;
|
|
1400
|
+
state.splitId = null;
|
|
1401
|
+
state.themeData = {};
|
|
1402
|
+
state.originalThemeData = {};
|
|
1403
|
+
state.hasChanges = false;
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1407
|
+
editor.on("buffer_closed", "onThemeEditorBufferClosed");
|
|
1408
|
+
|
|
1409
|
+
// =============================================================================
|
|
1410
|
+
// Smart Navigation - Skip Non-Selectable Lines
|
|
1411
|
+
// =============================================================================
|
|
1412
|
+
|
|
1413
|
+
interface SelectableEntry {
|
|
1414
|
+
byteOffset: number;
|
|
1415
|
+
valueByteOffset: number; // Position at the value (after "field: ")
|
|
1416
|
+
index: number;
|
|
1417
|
+
isSection: boolean;
|
|
1418
|
+
path: string;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* Get byte offsets for all selectable entries (fields and sections)
|
|
1423
|
+
*/
|
|
1424
|
+
function getSelectableEntries(): SelectableEntry[] {
|
|
1425
|
+
const entries = buildDisplayEntries();
|
|
1426
|
+
const selectableEntries: SelectableEntry[] = [];
|
|
1427
|
+
let byteOffset = 0;
|
|
1428
|
+
|
|
1429
|
+
for (const entry of entries) {
|
|
1430
|
+
const props = entry.properties as Record<string, unknown>;
|
|
1431
|
+
const entryType = props.type as string;
|
|
1432
|
+
const path = (props.path as string) || "";
|
|
1433
|
+
|
|
1434
|
+
// Only fields and sections are selectable (they have index property)
|
|
1435
|
+
if ((entryType === "field" || entryType === "section") && typeof props.index === "number") {
|
|
1436
|
+
// For fields, calculate position at the color value (after "FieldName: X ")
|
|
1437
|
+
let valueByteOffset = byteOffset;
|
|
1438
|
+
if (entryType === "field") {
|
|
1439
|
+
const colonIdx = entry.text.indexOf(":");
|
|
1440
|
+
if (colonIdx >= 0) {
|
|
1441
|
+
// Position at the hex value, after ": X " (colon + space + X + 2 spaces = 5 chars)
|
|
1442
|
+
valueByteOffset = byteOffset + getUtf8ByteLength(entry.text.substring(0, colonIdx + 5));
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
selectableEntries.push({
|
|
1447
|
+
byteOffset,
|
|
1448
|
+
valueByteOffset,
|
|
1449
|
+
index: props.index as number,
|
|
1450
|
+
isSection: entryType === "section",
|
|
1451
|
+
path,
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
byteOffset += getUtf8ByteLength(entry.text);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
return selectableEntries;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* Get the current selectable entry index based on cursor position
|
|
1463
|
+
*/
|
|
1464
|
+
function getCurrentSelectableIndex(): number {
|
|
1465
|
+
if (state.bufferId === null) return -1;
|
|
1466
|
+
|
|
1467
|
+
const props = editor.getTextPropertiesAtCursor(state.bufferId);
|
|
1468
|
+
if (props.length > 0 && typeof props[0].index === "number") {
|
|
1469
|
+
return props[0].index as number;
|
|
1470
|
+
}
|
|
1471
|
+
return -1;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Get the current field path at cursor
|
|
1476
|
+
*/
|
|
1477
|
+
function getCurrentFieldPath(): string | null {
|
|
1478
|
+
if (state.bufferId === null) return null;
|
|
1479
|
+
|
|
1480
|
+
const props = editor.getTextPropertiesAtCursor(state.bufferId);
|
|
1481
|
+
if (props.length > 0 && typeof props[0].path === "string") {
|
|
1482
|
+
return props[0].path as string;
|
|
1483
|
+
}
|
|
1484
|
+
return null;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Move cursor to a field by path (positions at value for fields)
|
|
1489
|
+
*/
|
|
1490
|
+
function moveCursorToField(path: string): void {
|
|
1491
|
+
if (state.bufferId === null) return;
|
|
1492
|
+
|
|
1493
|
+
const selectableEntries = getSelectableEntries();
|
|
1494
|
+
for (const entry of selectableEntries) {
|
|
1495
|
+
if (entry.path === path) {
|
|
1496
|
+
// Use valueByteOffset for fields, byteOffset for sections
|
|
1497
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1498
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* Navigate to the next selectable field/section
|
|
1506
|
+
*/
|
|
1507
|
+
globalThis.theme_editor_nav_down = function(): void {
|
|
1508
|
+
if (state.bufferId === null) return;
|
|
1509
|
+
|
|
1510
|
+
const selectableEntries = getSelectableEntries();
|
|
1511
|
+
const currentIndex = getCurrentSelectableIndex();
|
|
1512
|
+
|
|
1513
|
+
// Find next selectable entry after current
|
|
1514
|
+
for (const entry of selectableEntries) {
|
|
1515
|
+
if (entry.index > currentIndex) {
|
|
1516
|
+
// Use valueByteOffset for fields, byteOffset for sections
|
|
1517
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1518
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// Already at last selectable, stay there
|
|
1524
|
+
editor.setStatus(editor.t("status.at_last_field"));
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* Navigate to the previous selectable field/section
|
|
1529
|
+
*/
|
|
1530
|
+
globalThis.theme_editor_nav_up = function(): void {
|
|
1531
|
+
if (state.bufferId === null) return;
|
|
1532
|
+
|
|
1533
|
+
const selectableEntries = getSelectableEntries();
|
|
1534
|
+
const currentIndex = getCurrentSelectableIndex();
|
|
1535
|
+
|
|
1536
|
+
// Find previous selectable entry before current
|
|
1537
|
+
for (let i = selectableEntries.length - 1; i >= 0; i--) {
|
|
1538
|
+
const entry = selectableEntries[i];
|
|
1539
|
+
if (entry.index < currentIndex) {
|
|
1540
|
+
// Use valueByteOffset for fields, byteOffset for sections
|
|
1541
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1542
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Already at first selectable, stay there
|
|
1548
|
+
editor.setStatus(editor.t("status.at_first_field"));
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Navigate to next element (Tab) - includes both fields and sections
|
|
1553
|
+
*/
|
|
1554
|
+
globalThis.theme_editor_nav_next_section = function(): void {
|
|
1555
|
+
if (state.bufferId === null) return;
|
|
1556
|
+
|
|
1557
|
+
const selectableEntries = getSelectableEntries();
|
|
1558
|
+
const currentIndex = getCurrentSelectableIndex();
|
|
1559
|
+
|
|
1560
|
+
// Find next selectable entry after current
|
|
1561
|
+
for (const entry of selectableEntries) {
|
|
1562
|
+
if (entry.index > currentIndex) {
|
|
1563
|
+
// Use valueByteOffset for fields, byteOffset for sections
|
|
1564
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1565
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
// Wrap to first entry
|
|
1571
|
+
if (selectableEntries.length > 0) {
|
|
1572
|
+
const entry = selectableEntries[0];
|
|
1573
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1574
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Navigate to previous element (Shift+Tab) - includes both fields and sections
|
|
1580
|
+
*/
|
|
1581
|
+
globalThis.theme_editor_nav_prev_section = function(): void {
|
|
1582
|
+
if (state.bufferId === null) return;
|
|
1583
|
+
|
|
1584
|
+
const selectableEntries = getSelectableEntries();
|
|
1585
|
+
const currentIndex = getCurrentSelectableIndex();
|
|
1586
|
+
|
|
1587
|
+
// Find previous selectable entry before current
|
|
1588
|
+
for (let i = selectableEntries.length - 1; i >= 0; i--) {
|
|
1589
|
+
const entry = selectableEntries[i];
|
|
1590
|
+
if (entry.index < currentIndex) {
|
|
1591
|
+
// Use valueByteOffset for fields, byteOffset for sections
|
|
1592
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1593
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// Wrap to last entry
|
|
1599
|
+
if (selectableEntries.length > 0) {
|
|
1600
|
+
const entry = selectableEntries[selectableEntries.length - 1];
|
|
1601
|
+
const targetOffset = entry.isSection ? entry.byteOffset : entry.valueByteOffset;
|
|
1602
|
+
editor.setBufferCursor(state.bufferId, targetOffset);
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
|
|
1177
1606
|
// =============================================================================
|
|
1178
1607
|
// Public Commands
|
|
1179
1608
|
// =============================================================================
|
|
1180
1609
|
|
|
1181
1610
|
/**
|
|
1182
|
-
* Open the theme editor
|
|
1611
|
+
* Open the theme editor - prompts user to select theme first
|
|
1183
1612
|
*/
|
|
1184
1613
|
globalThis.open_theme_editor = async function(): Promise<void> {
|
|
1185
1614
|
if (state.isOpen) {
|
|
@@ -1187,8 +1616,6 @@ globalThis.open_theme_editor = async function(): Promise<void> {
|
|
|
1187
1616
|
return;
|
|
1188
1617
|
}
|
|
1189
1618
|
|
|
1190
|
-
editor.setStatus(editor.t("status.loading"));
|
|
1191
|
-
|
|
1192
1619
|
// Save context
|
|
1193
1620
|
state.sourceSplitId = editor.getActiveSplitId();
|
|
1194
1621
|
state.sourceBufferId = editor.getActiveBufferId();
|
|
@@ -1196,13 +1623,52 @@ globalThis.open_theme_editor = async function(): Promise<void> {
|
|
|
1196
1623
|
// Load available themes
|
|
1197
1624
|
state.builtinThemes = await loadBuiltinThemes();
|
|
1198
1625
|
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1626
|
+
// Get current theme name from config
|
|
1627
|
+
const config = editor.getConfig() as Record<string, unknown>;
|
|
1628
|
+
const currentThemeName = (config?.theme as string) || "dark";
|
|
1629
|
+
|
|
1630
|
+
// Prompt user to select which theme to edit
|
|
1631
|
+
editor.startPrompt(editor.t("prompt.select_theme_to_edit"), "theme-select-initial");
|
|
1632
|
+
|
|
1633
|
+
const suggestions: PromptSuggestion[] = [];
|
|
1634
|
+
|
|
1635
|
+
// Add user themes first
|
|
1636
|
+
const userThemes = listUserThemes();
|
|
1637
|
+
for (const name of userThemes) {
|
|
1638
|
+
const isCurrent = name === currentThemeName;
|
|
1639
|
+
suggestions.push({
|
|
1640
|
+
text: name,
|
|
1641
|
+
description: isCurrent ? editor.t("suggestion.user_theme_current") : editor.t("suggestion.user_theme"),
|
|
1642
|
+
value: `user:${name}`,
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// Add built-in themes
|
|
1647
|
+
for (const name of state.builtinThemes) {
|
|
1648
|
+
const isCurrent = name === currentThemeName;
|
|
1649
|
+
suggestions.push({
|
|
1650
|
+
text: name,
|
|
1651
|
+
description: isCurrent ? editor.t("suggestion.builtin_theme_current") : editor.t("suggestion.builtin_theme"),
|
|
1652
|
+
value: `builtin:${name}`,
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// Sort suggestions to put current theme first
|
|
1657
|
+
suggestions.sort((a, b) => {
|
|
1658
|
+
const aIsCurrent = a.description.includes("current");
|
|
1659
|
+
const bIsCurrent = b.description.includes("current");
|
|
1660
|
+
if (aIsCurrent && !bIsCurrent) return -1;
|
|
1661
|
+
if (!aIsCurrent && bIsCurrent) return 1;
|
|
1662
|
+
return 0;
|
|
1663
|
+
});
|
|
1205
1664
|
|
|
1665
|
+
editor.setPromptSuggestions(suggestions);
|
|
1666
|
+
};
|
|
1667
|
+
|
|
1668
|
+
/**
|
|
1669
|
+
* Actually open the theme editor with loaded theme data
|
|
1670
|
+
*/
|
|
1671
|
+
async function doOpenThemeEditor(): Promise<void> {
|
|
1206
1672
|
// Build initial entries
|
|
1207
1673
|
const entries = buildDisplayEntries();
|
|
1208
1674
|
|
|
@@ -1222,14 +1688,12 @@ globalThis.open_theme_editor = async function(): Promise<void> {
|
|
|
1222
1688
|
state.bufferId = bufferId;
|
|
1223
1689
|
state.splitId = null;
|
|
1224
1690
|
|
|
1225
|
-
editor.setContext("theme-editor", true);
|
|
1226
|
-
|
|
1227
1691
|
applyHighlighting();
|
|
1228
1692
|
editor.setStatus(editor.t("status.ready"));
|
|
1229
1693
|
} else {
|
|
1230
1694
|
editor.setStatus(editor.t("status.open_failed"));
|
|
1231
1695
|
}
|
|
1232
|
-
}
|
|
1696
|
+
}
|
|
1233
1697
|
|
|
1234
1698
|
/**
|
|
1235
1699
|
* Close the theme editor
|
|
@@ -1238,11 +1702,23 @@ globalThis.theme_editor_close = function(): void {
|
|
|
1238
1702
|
if (!state.isOpen) return;
|
|
1239
1703
|
|
|
1240
1704
|
if (state.hasChanges) {
|
|
1241
|
-
|
|
1705
|
+
// Show confirmation prompt before closing with unsaved changes
|
|
1706
|
+
editor.startPrompt(editor.t("prompt.discard_confirm"), "theme-discard-confirm");
|
|
1707
|
+
const suggestions: PromptSuggestion[] = [
|
|
1708
|
+
{ text: editor.t("prompt.discard_yes"), description: "", value: "discard" },
|
|
1709
|
+
{ text: editor.t("prompt.discard_no"), description: "", value: "keep" },
|
|
1710
|
+
];
|
|
1711
|
+
editor.setPromptSuggestions(suggestions);
|
|
1712
|
+
return;
|
|
1242
1713
|
}
|
|
1243
1714
|
|
|
1244
|
-
|
|
1715
|
+
doCloseEditor();
|
|
1716
|
+
};
|
|
1245
1717
|
|
|
1718
|
+
/**
|
|
1719
|
+
* Actually close the editor (called after confirmation or when no changes)
|
|
1720
|
+
*/
|
|
1721
|
+
function doCloseEditor(): void {
|
|
1246
1722
|
// Close the buffer (this will switch to another buffer in the same split)
|
|
1247
1723
|
if (state.bufferId !== null) {
|
|
1248
1724
|
editor.closeBuffer(state.bufferId);
|
|
@@ -1257,6 +1733,27 @@ globalThis.theme_editor_close = function(): void {
|
|
|
1257
1733
|
state.hasChanges = false;
|
|
1258
1734
|
|
|
1259
1735
|
editor.setStatus(editor.t("status.closed"));
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
/**
|
|
1739
|
+
* Handle discard confirmation prompt
|
|
1740
|
+
*/
|
|
1741
|
+
globalThis.onThemeDiscardPromptConfirmed = function(args: {
|
|
1742
|
+
prompt_type: string;
|
|
1743
|
+
selected_index: number | null;
|
|
1744
|
+
input: string;
|
|
1745
|
+
}): boolean {
|
|
1746
|
+
if (args.prompt_type !== "theme-discard-confirm") return true;
|
|
1747
|
+
|
|
1748
|
+
const response = args.input.trim().toLowerCase();
|
|
1749
|
+
if (response === "discard" || args.selected_index === 0) {
|
|
1750
|
+
editor.setStatus(editor.t("status.unsaved_discarded"));
|
|
1751
|
+
doCloseEditor();
|
|
1752
|
+
} else {
|
|
1753
|
+
editor.setStatus(editor.t("status.cancelled"));
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
return false;
|
|
1260
1757
|
};
|
|
1261
1758
|
|
|
1262
1759
|
/**
|
|
@@ -1297,49 +1794,116 @@ globalThis.theme_editor_toggle_section = function(): void {
|
|
|
1297
1794
|
};
|
|
1298
1795
|
|
|
1299
1796
|
/**
|
|
1300
|
-
*
|
|
1797
|
+
* Open a theme (builtin or user) for editing
|
|
1301
1798
|
*/
|
|
1302
|
-
globalThis.
|
|
1303
|
-
editor.startPrompt(editor.t("prompt.
|
|
1799
|
+
globalThis.theme_editor_open = function(): void {
|
|
1800
|
+
editor.startPrompt(editor.t("prompt.open_theme"), "theme-open");
|
|
1304
1801
|
|
|
1305
|
-
const suggestions: PromptSuggestion[] =
|
|
1306
|
-
text: name,
|
|
1307
|
-
description: editor.t("suggestion.builtin_theme"),
|
|
1308
|
-
value: name,
|
|
1309
|
-
}));
|
|
1802
|
+
const suggestions: PromptSuggestion[] = [];
|
|
1310
1803
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1804
|
+
// Add user themes first
|
|
1805
|
+
const userThemes = listUserThemes();
|
|
1806
|
+
for (const name of userThemes) {
|
|
1807
|
+
suggestions.push({
|
|
1808
|
+
text: name,
|
|
1809
|
+
description: editor.t("suggestion.user_theme"),
|
|
1810
|
+
value: `user:${name}`,
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1313
1813
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1814
|
+
// Add built-in themes
|
|
1815
|
+
for (const name of state.builtinThemes) {
|
|
1816
|
+
suggestions.push({
|
|
1817
|
+
text: name,
|
|
1818
|
+
description: editor.t("suggestion.builtin_theme"),
|
|
1819
|
+
value: `builtin:${name}`,
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1319
1822
|
|
|
1320
|
-
editor.setPromptSuggestions(
|
|
1321
|
-
text: state.themeName,
|
|
1322
|
-
description: editor.t("suggestion.current"),
|
|
1323
|
-
value: state.themeName,
|
|
1324
|
-
}]);
|
|
1823
|
+
editor.setPromptSuggestions(suggestions);
|
|
1325
1824
|
};
|
|
1326
1825
|
|
|
1327
1826
|
/**
|
|
1328
1827
|
* Save theme
|
|
1329
1828
|
*/
|
|
1330
1829
|
globalThis.theme_editor_save = async function(): Promise<void> {
|
|
1331
|
-
|
|
1830
|
+
// Save cursor path for restoration after save
|
|
1831
|
+
state.savedCursorPath = getCurrentFieldPath();
|
|
1832
|
+
|
|
1833
|
+
// Built-in themes require Save As
|
|
1834
|
+
if (state.isBuiltin) {
|
|
1835
|
+
editor.setStatus(editor.t("status.builtin_requires_save_as"));
|
|
1836
|
+
theme_editor_save_as();
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// If theme has never been saved (no path), trigger "Save As" instead
|
|
1841
|
+
if (!state.themePath) {
|
|
1842
|
+
theme_editor_save_as();
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
if (!state.hasChanges) {
|
|
1332
1847
|
editor.setStatus(editor.t("status.no_changes"));
|
|
1333
1848
|
return;
|
|
1334
1849
|
}
|
|
1335
1850
|
|
|
1336
|
-
|
|
1851
|
+
// Check for name collision if name has changed since last save
|
|
1852
|
+
const userThemesDir = getUserThemesDir();
|
|
1853
|
+
const targetPath = editor.pathJoin(userThemesDir, `${state.themeName}.json`);
|
|
1854
|
+
|
|
1855
|
+
if (state.themePath !== targetPath && editor.fileExists(targetPath)) {
|
|
1856
|
+
// File exists with this name - ask for confirmation
|
|
1857
|
+
editor.startPrompt(editor.t("prompt.overwrite_confirm", { name: state.themeName }), "theme-overwrite-confirm");
|
|
1858
|
+
const suggestions: PromptSuggestion[] = [
|
|
1859
|
+
{ text: editor.t("prompt.overwrite_yes"), description: "", value: "overwrite" },
|
|
1860
|
+
{ text: editor.t("prompt.overwrite_no"), description: "", value: "cancel" },
|
|
1861
|
+
];
|
|
1862
|
+
editor.setPromptSuggestions(suggestions);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
await saveTheme(undefined, state.savedCursorPath);
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Handle overwrite confirmation prompt
|
|
1871
|
+
*/
|
|
1872
|
+
globalThis.onThemeOverwritePromptConfirmed = async function(args: {
|
|
1873
|
+
prompt_type: string;
|
|
1874
|
+
selected_index: number | null;
|
|
1875
|
+
input: string;
|
|
1876
|
+
}): Promise<boolean> {
|
|
1877
|
+
if (args.prompt_type !== "theme-overwrite-confirm") return true;
|
|
1878
|
+
|
|
1879
|
+
const response = args.input.trim().toLowerCase();
|
|
1880
|
+
if (response === "overwrite" || args.selected_index === 0) {
|
|
1881
|
+
// Use pending name if set (from Save As), otherwise use current name
|
|
1882
|
+
const nameToSave = state.pendingSaveName || state.themeName;
|
|
1883
|
+
state.themeName = nameToSave;
|
|
1884
|
+
state.themeData.name = nameToSave;
|
|
1885
|
+
state.pendingSaveName = null;
|
|
1886
|
+
const restorePath = state.savedCursorPath;
|
|
1887
|
+
state.savedCursorPath = null;
|
|
1888
|
+
await saveTheme(nameToSave, restorePath);
|
|
1889
|
+
} else {
|
|
1890
|
+
state.pendingSaveName = null;
|
|
1891
|
+
state.savedCursorPath = null;
|
|
1892
|
+
editor.setStatus(editor.t("status.cancelled"));
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
return false;
|
|
1337
1896
|
};
|
|
1338
1897
|
|
|
1339
1898
|
/**
|
|
1340
1899
|
* Save theme as (new name)
|
|
1341
1900
|
*/
|
|
1342
1901
|
globalThis.theme_editor_save_as = function(): void {
|
|
1902
|
+
// Save cursor path for restoration after save (if not already saved by theme_editor_save)
|
|
1903
|
+
if (!state.savedCursorPath) {
|
|
1904
|
+
state.savedCursorPath = getCurrentFieldPath();
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1343
1907
|
editor.startPrompt(editor.t("prompt.save_as"), "theme-save-as");
|
|
1344
1908
|
|
|
1345
1909
|
editor.setPromptSuggestions([{
|
|
@@ -1349,34 +1913,6 @@ globalThis.theme_editor_save_as = function(): void {
|
|
|
1349
1913
|
}]);
|
|
1350
1914
|
};
|
|
1351
1915
|
|
|
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
1916
|
/**
|
|
1381
1917
|
* Reload theme
|
|
1382
1918
|
*/
|
|
@@ -1407,11 +1943,68 @@ globalThis.theme_editor_show_help = function(): void {
|
|
|
1407
1943
|
editor.setStatus(editor.t("status.help"));
|
|
1408
1944
|
};
|
|
1409
1945
|
|
|
1946
|
+
/**
|
|
1947
|
+
* Delete the current user theme
|
|
1948
|
+
*/
|
|
1949
|
+
globalThis.theme_editor_delete = function(): void {
|
|
1950
|
+
// Can only delete saved user themes
|
|
1951
|
+
if (!state.themePath) {
|
|
1952
|
+
editor.setStatus(editor.t("status.cannot_delete_unsaved"));
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// Show confirmation dialog
|
|
1957
|
+
editor.startPrompt(editor.t("prompt.delete_confirm", { name: state.themeName }), "theme-delete-confirm");
|
|
1958
|
+
const suggestions: PromptSuggestion[] = [
|
|
1959
|
+
{ text: editor.t("prompt.delete_yes"), description: "", value: "delete" },
|
|
1960
|
+
{ text: editor.t("prompt.delete_no"), description: "", value: "cancel" },
|
|
1961
|
+
];
|
|
1962
|
+
editor.setPromptSuggestions(suggestions);
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* Handle delete confirmation prompt
|
|
1967
|
+
*/
|
|
1968
|
+
globalThis.onThemeDeletePromptConfirmed = async function(args: {
|
|
1969
|
+
prompt_type: string;
|
|
1970
|
+
selected_index: number | null;
|
|
1971
|
+
input: string;
|
|
1972
|
+
}): Promise<boolean> {
|
|
1973
|
+
if (args.prompt_type !== "theme-delete-confirm") return true;
|
|
1974
|
+
|
|
1975
|
+
const value = args.input.trim();
|
|
1976
|
+
if (value === "delete" || value === editor.t("prompt.delete_yes")) {
|
|
1977
|
+
if (state.themePath) {
|
|
1978
|
+
try {
|
|
1979
|
+
// Delete the theme file
|
|
1980
|
+
await editor.deleteFile(state.themePath);
|
|
1981
|
+
const deletedName = state.themeName;
|
|
1982
|
+
|
|
1983
|
+
// Reset to default theme
|
|
1984
|
+
state.themeData = createDefaultTheme();
|
|
1985
|
+
state.originalThemeData = deepClone(state.themeData);
|
|
1986
|
+
state.themeName = "custom";
|
|
1987
|
+
state.themePath = null;
|
|
1988
|
+
state.hasChanges = false;
|
|
1989
|
+
updateDisplay();
|
|
1990
|
+
|
|
1991
|
+
editor.setStatus(editor.t("status.deleted", { name: deletedName }));
|
|
1992
|
+
} catch (e) {
|
|
1993
|
+
editor.setStatus(editor.t("status.delete_failed", { error: String(e) }));
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
} else {
|
|
1997
|
+
editor.setStatus(editor.t("status.cancelled"));
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
return true;
|
|
2001
|
+
};
|
|
2002
|
+
|
|
1410
2003
|
// =============================================================================
|
|
1411
2004
|
// Command Registration
|
|
1412
2005
|
// =============================================================================
|
|
1413
2006
|
|
|
1414
|
-
// Main command to open theme editor
|
|
2007
|
+
// Main command to open theme editor (always available)
|
|
1415
2008
|
editor.registerCommand(
|
|
1416
2009
|
"%cmd.edit_theme",
|
|
1417
2010
|
"%cmd.edit_theme_desc",
|
|
@@ -1419,76 +2012,21 @@ editor.registerCommand(
|
|
|
1419
2012
|
"normal"
|
|
1420
2013
|
);
|
|
1421
2014
|
|
|
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
|
-
);
|
|
2015
|
+
// Buffer-scoped commands - only visible when a buffer with mode "theme-editor" is focused
|
|
2016
|
+
// The core automatically checks the focused buffer's mode against command contexts
|
|
2017
|
+
editor.registerCommand("%cmd.close_editor", "%cmd.close_editor_desc", "theme_editor_close", "theme-editor");
|
|
2018
|
+
editor.registerCommand("%cmd.edit_color", "%cmd.edit_color_desc", "theme_editor_edit_color", "theme-editor");
|
|
2019
|
+
editor.registerCommand("%cmd.toggle_section", "%cmd.toggle_section_desc", "theme_editor_toggle_section", "theme-editor");
|
|
2020
|
+
editor.registerCommand("%cmd.open_theme", "%cmd.open_theme_desc", "theme_editor_open", "theme-editor");
|
|
2021
|
+
editor.registerCommand("%cmd.save", "%cmd.save_desc", "theme_editor_save", "theme-editor");
|
|
2022
|
+
editor.registerCommand("%cmd.save_as", "%cmd.save_as_desc", "theme_editor_save_as", "theme-editor");
|
|
2023
|
+
editor.registerCommand("%cmd.reload", "%cmd.reload_desc", "theme_editor_reload", "theme-editor");
|
|
2024
|
+
editor.registerCommand("%cmd.show_help", "%cmd.show_help_desc", "theme_editor_show_help", "theme-editor");
|
|
2025
|
+
editor.registerCommand("%cmd.delete_theme", "%cmd.delete_theme_desc", "theme_editor_delete", "theme-editor");
|
|
2026
|
+
editor.registerCommand("%cmd.nav_up", "%cmd.nav_up_desc", "theme_editor_nav_up", "theme-editor");
|
|
2027
|
+
editor.registerCommand("%cmd.nav_down", "%cmd.nav_down_desc", "theme_editor_nav_down", "theme-editor");
|
|
2028
|
+
editor.registerCommand("%cmd.nav_next", "%cmd.nav_next_desc", "theme_editor_nav_next_section", "theme-editor");
|
|
2029
|
+
editor.registerCommand("%cmd.nav_prev", "%cmd.nav_prev_desc", "theme_editor_nav_prev_section", "theme-editor");
|
|
1492
2030
|
|
|
1493
2031
|
// =============================================================================
|
|
1494
2032
|
// Plugin Initialization
|