statusbar-quick-actions 0.0.10
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/.github/FUNDING.yml +3 -0
- package/.vscodeignore +11 -0
- package/CLAUDE.md +230 -0
- package/LICENSE +21 -0
- package/README.md +529 -0
- package/assets/icon.png +0 -0
- package/bun.lock +908 -0
- package/docs/PERFORMANCE_OPTIMIZATIONS.md +240 -0
- package/docs/PRESET_AND_DYNAMIC_LABELS.md +536 -0
- package/docs/SAMPLE-CONFIGURATIONS.md +973 -0
- package/eslint.config.mjs +41 -0
- package/package.json +605 -0
- package/src/config-cli.ts +1287 -0
- package/src/configuration.ts +530 -0
- package/src/dynamic-label.ts +360 -0
- package/src/executor.ts +406 -0
- package/src/extension.ts +1754 -0
- package/src/history.ts +175 -0
- package/src/material-icons.ts +388 -0
- package/src/notifications.ts +189 -0
- package/src/output-panel.ts +403 -0
- package/src/preset-manager.ts +406 -0
- package/src/theme.ts +318 -0
- package/src/types.ts +368 -0
- package/src/utils/debounce.ts +91 -0
- package/src/visibility.ts +283 -0
- package/tsconfig.dev.json +10 -0
- package/tsconfig.json +19 -0
package/src/extension.ts
ADDED
|
@@ -0,0 +1,1754 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusBar Quick Actions Extension
|
|
3
|
+
* A comprehensive extension for customizable statusbar buttons
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as vscode from "vscode";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import {
|
|
9
|
+
StatusBarButtonConfig,
|
|
10
|
+
ExtensionConfig,
|
|
11
|
+
ButtonState,
|
|
12
|
+
ExecutionResult,
|
|
13
|
+
ExecutionOptions,
|
|
14
|
+
} from "./types";
|
|
15
|
+
import { ConfigManager } from "./configuration";
|
|
16
|
+
import { CommandExecutor } from "./executor";
|
|
17
|
+
import { ThemeManager } from "./theme";
|
|
18
|
+
import { VisibilityManager } from "./visibility";
|
|
19
|
+
import { MaterialIconManager } from "./material-icons";
|
|
20
|
+
import { OutputPanelManager } from "./output-panel";
|
|
21
|
+
import { PresetManager } from "./preset-manager";
|
|
22
|
+
import { DynamicLabelManager } from "./dynamic-label";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Main extension class
|
|
26
|
+
*/
|
|
27
|
+
export class StatusBarQuickActionsExtension {
|
|
28
|
+
private context: vscode.ExtensionContext;
|
|
29
|
+
private configManager: ConfigManager;
|
|
30
|
+
private commandExecutor: CommandExecutor;
|
|
31
|
+
private themeManager: ThemeManager;
|
|
32
|
+
private visibilityManager!: VisibilityManager;
|
|
33
|
+
private materialIconManager!: MaterialIconManager;
|
|
34
|
+
private outputPanelManager!: OutputPanelManager;
|
|
35
|
+
private presetManager!: PresetManager;
|
|
36
|
+
private dynamicLabelManager!: DynamicLabelManager;
|
|
37
|
+
private buttonStates: Map<string, ButtonState> = new Map<
|
|
38
|
+
string,
|
|
39
|
+
ButtonState
|
|
40
|
+
>();
|
|
41
|
+
private disposables: vscode.Disposable[] = [];
|
|
42
|
+
private editorChangeListener: vscode.Disposable | null = null;
|
|
43
|
+
private isActivated = false;
|
|
44
|
+
private debugMode = false;
|
|
45
|
+
|
|
46
|
+
constructor(context: vscode.ExtensionContext) {
|
|
47
|
+
this.context = context;
|
|
48
|
+
this.configManager = new ConfigManager();
|
|
49
|
+
this.commandExecutor = new CommandExecutor();
|
|
50
|
+
this.themeManager = new ThemeManager();
|
|
51
|
+
this.debugMode = vscode.workspace
|
|
52
|
+
.getConfiguration("statusbarQuickActions.settings")
|
|
53
|
+
.get<boolean>("debug", false);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Log debug messages only when debug mode is enabled
|
|
58
|
+
*/
|
|
59
|
+
private debugLog(...args: unknown[]): void {
|
|
60
|
+
if (this.debugMode) {
|
|
61
|
+
console.log("[StatusBar Quick Actions]", ...args);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Activate the extension
|
|
67
|
+
*/
|
|
68
|
+
public async activate(): Promise<void> {
|
|
69
|
+
if (this.isActivated) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Initialize critical managers in parallel
|
|
75
|
+
await this.initializeManagers();
|
|
76
|
+
|
|
77
|
+
// Register commands (synchronous, fast)
|
|
78
|
+
this.registerCommands();
|
|
79
|
+
|
|
80
|
+
// Set up configuration watching (synchronous, fast)
|
|
81
|
+
this.setupConfigurationWatching();
|
|
82
|
+
|
|
83
|
+
// Load initial configuration and create buttons
|
|
84
|
+
await this.loadConfiguration();
|
|
85
|
+
|
|
86
|
+
this.isActivated = true;
|
|
87
|
+
this.debugLog("Extension activated successfully");
|
|
88
|
+
|
|
89
|
+
// Defer non-critical operations to avoid blocking activation
|
|
90
|
+
setImmediate(() => {
|
|
91
|
+
this.showWelcomeMessageIfNeeded().catch((error) => {
|
|
92
|
+
this.debugLog("Failed to show welcome message:", error);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(
|
|
97
|
+
"Failed to activate StatusBar Quick Actions extension:",
|
|
98
|
+
error,
|
|
99
|
+
);
|
|
100
|
+
vscode.window.showErrorMessage(
|
|
101
|
+
`Failed to activate StatusBar Quick Actions: ${error}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Show welcome message on first activation (deferred)
|
|
108
|
+
*/
|
|
109
|
+
private async showWelcomeMessageIfNeeded(): Promise<void> {
|
|
110
|
+
if (!this.context.globalState.get("hasBeenActivated")) {
|
|
111
|
+
await this.showWelcomeMessage();
|
|
112
|
+
await this.context.globalState.update("hasBeenActivated", true);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Deactivate the extension
|
|
118
|
+
*/
|
|
119
|
+
public deactivate(): void {
|
|
120
|
+
if (!this.isActivated) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Dispose of all resources
|
|
125
|
+
this.disposables.forEach((disposable) => disposable.dispose());
|
|
126
|
+
this.buttonStates.clear();
|
|
127
|
+
|
|
128
|
+
// Dispose new managers
|
|
129
|
+
if (this.outputPanelManager) {
|
|
130
|
+
this.outputPanelManager.dispose();
|
|
131
|
+
}
|
|
132
|
+
if (this.visibilityManager) {
|
|
133
|
+
this.visibilityManager.dispose();
|
|
134
|
+
}
|
|
135
|
+
if (this.presetManager) {
|
|
136
|
+
this.presetManager.dispose();
|
|
137
|
+
}
|
|
138
|
+
if (this.dynamicLabelManager) {
|
|
139
|
+
this.dynamicLabelManager.dispose();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.isActivated = false;
|
|
143
|
+
this.debugLog("Extension deactivated");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Initialize all managers - optimized for parallel execution
|
|
148
|
+
*/
|
|
149
|
+
private async initializeManagers(): Promise<void> {
|
|
150
|
+
try {
|
|
151
|
+
// Initialize configuration manager first (synchronous, required by others)
|
|
152
|
+
this.configManager.initialize(this.context);
|
|
153
|
+
this.debugLog("ConfigManager initialized");
|
|
154
|
+
|
|
155
|
+
// Get configs once to avoid multiple reads
|
|
156
|
+
const outputConfig = this.configManager.getConfigValue(
|
|
157
|
+
"settings.output",
|
|
158
|
+
this.getDefaultOutputConfig(),
|
|
159
|
+
);
|
|
160
|
+
const performanceConfig = this.configManager.getConfigValue(
|
|
161
|
+
"settings.performance",
|
|
162
|
+
this.getDefaultPerformanceConfig(),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Initialize managers in parallel where possible
|
|
166
|
+
await Promise.all([
|
|
167
|
+
// Theme manager (async)
|
|
168
|
+
this.themeManager.initialize(this.context).then(() => {
|
|
169
|
+
this.debugLog("ThemeManager initialized");
|
|
170
|
+
}),
|
|
171
|
+
|
|
172
|
+
// Dynamic Label Manager (async)
|
|
173
|
+
(async () => {
|
|
174
|
+
this.dynamicLabelManager = new DynamicLabelManager();
|
|
175
|
+
await this.dynamicLabelManager.initialize();
|
|
176
|
+
this.dynamicLabelManager.onLabelRefresh = (buttonId) => {
|
|
177
|
+
this.refreshButtonLabel(buttonId);
|
|
178
|
+
};
|
|
179
|
+
this.debugLog("DynamicLabelManager initialized");
|
|
180
|
+
})(),
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
// Initialize synchronous managers (fast, no await needed)
|
|
184
|
+
this.materialIconManager = new MaterialIconManager();
|
|
185
|
+
this.outputPanelManager = new OutputPanelManager(outputConfig);
|
|
186
|
+
this.visibilityManager = new VisibilityManager(
|
|
187
|
+
performanceConfig.visibilityDebounceMs,
|
|
188
|
+
);
|
|
189
|
+
this.presetManager = new PresetManager();
|
|
190
|
+
this.presetManager.initialize(this.context);
|
|
191
|
+
|
|
192
|
+
this.debugLog("All synchronous managers initialized");
|
|
193
|
+
|
|
194
|
+
// Setup editor change listener (lightweight)
|
|
195
|
+
this.setupEditorChangeListener();
|
|
196
|
+
this.debugLog("Managers initialization complete");
|
|
197
|
+
} catch (error) {
|
|
198
|
+
const errorMessage =
|
|
199
|
+
error instanceof Error ? error.message : String(error);
|
|
200
|
+
console.error("Failed to initialize managers:", errorMessage);
|
|
201
|
+
vscode.window.showErrorMessage(
|
|
202
|
+
`StatusBar Quick Actions: Failed to initialize - ${errorMessage}`,
|
|
203
|
+
);
|
|
204
|
+
throw error; // Re-throw to prevent activation from completing
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Register extension commands
|
|
210
|
+
*/
|
|
211
|
+
private registerCommands(): void {
|
|
212
|
+
// Edit button command
|
|
213
|
+
this.disposables.push(
|
|
214
|
+
vscode.commands.registerCommand(
|
|
215
|
+
"statusbarQuickActions.editButton",
|
|
216
|
+
this.editButton.bind(this),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// View history command
|
|
221
|
+
this.disposables.push(
|
|
222
|
+
vscode.commands.registerCommand(
|
|
223
|
+
"statusbarQuickActions.viewHistory",
|
|
224
|
+
this.viewHistory.bind(this),
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Clear history command
|
|
229
|
+
this.disposables.push(
|
|
230
|
+
vscode.commands.registerCommand(
|
|
231
|
+
"statusbarQuickActions.clearHistory",
|
|
232
|
+
this.clearHistory.bind(this),
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Preset management commands
|
|
237
|
+
this.disposables.push(
|
|
238
|
+
vscode.commands.registerCommand(
|
|
239
|
+
"statusbarQuickActions.managePresets",
|
|
240
|
+
this.managePresets.bind(this),
|
|
241
|
+
),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
this.disposables.push(
|
|
245
|
+
vscode.commands.registerCommand(
|
|
246
|
+
"statusbarQuickActions.applyPreset",
|
|
247
|
+
this.applyPresetCommand.bind(this),
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
this.disposables.push(
|
|
252
|
+
vscode.commands.registerCommand(
|
|
253
|
+
"statusbarQuickActions.saveAsPreset",
|
|
254
|
+
this.saveAsPreset.bind(this),
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Register individual button commands
|
|
259
|
+
this.registerButtonCommands();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Register commands for each button
|
|
264
|
+
*/
|
|
265
|
+
private registerButtonCommands(): void {
|
|
266
|
+
const config = this.configManager.getConfig();
|
|
267
|
+
config.buttons.forEach((button) => {
|
|
268
|
+
const commandId = `statusbarQuickActions.execute_${button.id}`;
|
|
269
|
+
this.disposables.push(
|
|
270
|
+
vscode.commands.registerCommand(commandId, () =>
|
|
271
|
+
this.executeButton(button.id),
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Set up configuration change watching
|
|
279
|
+
*/
|
|
280
|
+
private setupConfigurationWatching(): void {
|
|
281
|
+
this.disposables.push(
|
|
282
|
+
this.configManager.onConfigurationChanged(async (newConfig) => {
|
|
283
|
+
// Update debug mode when configuration changes
|
|
284
|
+
this.debugMode = vscode.workspace
|
|
285
|
+
.getConfiguration("statusbarQuickActions.settings")
|
|
286
|
+
.get<boolean>("debug", false);
|
|
287
|
+
this.debugLog("Configuration changed, updating buttons");
|
|
288
|
+
await this.updateConfiguration(newConfig);
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Load configuration and create statusbar items
|
|
295
|
+
*/
|
|
296
|
+
private async loadConfiguration(): Promise<void> {
|
|
297
|
+
const config = this.configManager.getConfig();
|
|
298
|
+
await this.updateConfiguration(config);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Update configuration and recreate statusbar items
|
|
303
|
+
*/
|
|
304
|
+
private async updateConfiguration(config: ExtensionConfig): Promise<void> {
|
|
305
|
+
this.debugLog(
|
|
306
|
+
"Updating configuration with buttons:",
|
|
307
|
+
config.buttons.length,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Debug: Log each button configuration
|
|
311
|
+
if (this.debugMode) {
|
|
312
|
+
config.buttons.forEach((button, index) => {
|
|
313
|
+
this.debugLog(
|
|
314
|
+
`Button ${index}: ${button.id} - ${button.text || "no text"}`,
|
|
315
|
+
button,
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Validate configuration first
|
|
321
|
+
const validation = this.configManager.validateConfig(config);
|
|
322
|
+
if (!validation.isValid) {
|
|
323
|
+
const errorMessage = `Invalid button configuration:\n${validation.errors.join("\n")}`;
|
|
324
|
+
console.error(errorMessage);
|
|
325
|
+
vscode.window.showErrorMessage(
|
|
326
|
+
`StatusBar Quick Actions: Configuration validation failed. Check console for details.`,
|
|
327
|
+
);
|
|
328
|
+
// Show detailed error in output channel
|
|
329
|
+
const outputChannel = vscode.window.createOutputChannel(
|
|
330
|
+
"StatusBar Quick Actions - Errors",
|
|
331
|
+
);
|
|
332
|
+
outputChannel.appendLine("Configuration Validation Errors:");
|
|
333
|
+
validation.errors.forEach((error) =>
|
|
334
|
+
outputChannel.appendLine(` - ${error}`),
|
|
335
|
+
);
|
|
336
|
+
outputChannel.show(true);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Remove existing statusbar items
|
|
340
|
+
this.buttonStates.forEach((state) => {
|
|
341
|
+
state.item.dispose();
|
|
342
|
+
});
|
|
343
|
+
this.buttonStates.clear();
|
|
344
|
+
|
|
345
|
+
// Create new statusbar items (even if validation failed, try to create valid ones)
|
|
346
|
+
let createdCount = 0;
|
|
347
|
+
let failedCount = 0;
|
|
348
|
+
let disabledCount = 0;
|
|
349
|
+
|
|
350
|
+
// Create buttons in parallel for better performance
|
|
351
|
+
const buttonCreationPromises = config.buttons.map(async (buttonConfig) => {
|
|
352
|
+
if (buttonConfig.enabled === false) {
|
|
353
|
+
this.debugLog(`Button ${buttonConfig.id} is disabled, skipping`);
|
|
354
|
+
disabledCount++;
|
|
355
|
+
return { created: false, disabled: true };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const created = await this.createStatusBarItem(buttonConfig);
|
|
359
|
+
return { created, disabled: false };
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const results = await Promise.all(buttonCreationPromises);
|
|
363
|
+
results.forEach((result) => {
|
|
364
|
+
if (result.created) {
|
|
365
|
+
createdCount++;
|
|
366
|
+
} else if (!result.disabled) {
|
|
367
|
+
failedCount++;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Log summary
|
|
372
|
+
this.debugLog(
|
|
373
|
+
`Created ${createdCount} buttons, ${failedCount} failed, ${disabledCount} disabled`,
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// Show notification if no buttons were created
|
|
377
|
+
if (createdCount === 0 && config.buttons.length > 0) {
|
|
378
|
+
vscode.window.showWarningMessage(
|
|
379
|
+
`StatusBar Quick Actions: No buttons could be created. Check the output panel for errors.`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Create a statusbar item for a button configuration
|
|
386
|
+
* @returns true if button was created successfully, false otherwise
|
|
387
|
+
*/
|
|
388
|
+
private async createStatusBarItem(
|
|
389
|
+
buttonConfig: StatusBarButtonConfig,
|
|
390
|
+
): Promise<boolean> {
|
|
391
|
+
try {
|
|
392
|
+
this.debugLog(`Creating status bar item for button: ${buttonConfig.id}`);
|
|
393
|
+
|
|
394
|
+
// Validate button configuration
|
|
395
|
+
if (!buttonConfig.id) {
|
|
396
|
+
const error = `Button ${buttonConfig.id || "unknown"} missing ID`;
|
|
397
|
+
this.debugLog(error);
|
|
398
|
+
throw new Error("Button ID is required");
|
|
399
|
+
}
|
|
400
|
+
if (!buttonConfig.text && !buttonConfig.icon) {
|
|
401
|
+
const error = `Button ${buttonConfig.id} missing both text and icon`;
|
|
402
|
+
this.debugLog(error);
|
|
403
|
+
throw new Error("Either button text or icon is required");
|
|
404
|
+
}
|
|
405
|
+
if (!buttonConfig.command) {
|
|
406
|
+
const error = `Button ${buttonConfig.id} missing command`;
|
|
407
|
+
this.debugLog(error);
|
|
408
|
+
throw new Error("Button command is required");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Create statusbar item
|
|
412
|
+
const alignment =
|
|
413
|
+
buttonConfig.alignment === "left"
|
|
414
|
+
? vscode.StatusBarAlignment.Left
|
|
415
|
+
: vscode.StatusBarAlignment.Right;
|
|
416
|
+
const priority = buttonConfig.priority ?? 100;
|
|
417
|
+
|
|
418
|
+
const statusBarItem = vscode.window.createStatusBarItem(
|
|
419
|
+
alignment,
|
|
420
|
+
priority,
|
|
421
|
+
);
|
|
422
|
+
this.debugLog(
|
|
423
|
+
`Created status bar item for ${buttonConfig.id} (alignment: ${alignment}, priority: ${priority})`,
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// Set button properties
|
|
427
|
+
const displayText = this.getButtonDisplayText(buttonConfig);
|
|
428
|
+
this.debugLog(`Button ${buttonConfig.id} display text: "${displayText}"`);
|
|
429
|
+
if (!displayText || displayText.trim() === "") {
|
|
430
|
+
const error = `Button ${buttonConfig.id} has empty display text`;
|
|
431
|
+
this.debugLog(error);
|
|
432
|
+
throw new Error("Button display text cannot be empty");
|
|
433
|
+
}
|
|
434
|
+
statusBarItem.text = displayText;
|
|
435
|
+
statusBarItem.tooltip =
|
|
436
|
+
buttonConfig.tooltip || buttonConfig.text || "Quick Action";
|
|
437
|
+
statusBarItem.command = `statusbarQuickActions.execute_${buttonConfig.id}`;
|
|
438
|
+
this.debugLog(
|
|
439
|
+
`Button ${buttonConfig.id} command: ${statusBarItem.command}`,
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
// Apply theme colors
|
|
443
|
+
this.themeManager.applyThemeToStatusBarItem(statusBarItem);
|
|
444
|
+
|
|
445
|
+
// Set accessibility properties
|
|
446
|
+
statusBarItem.accessibilityInformation = {
|
|
447
|
+
label:
|
|
448
|
+
buttonConfig.tooltip ||
|
|
449
|
+
buttonConfig.text ||
|
|
450
|
+
`Button ${buttonConfig.id}`,
|
|
451
|
+
role: "button",
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// Create button state with empty history (will be loaded async)
|
|
455
|
+
const buttonState: ButtonState = {
|
|
456
|
+
item: statusBarItem,
|
|
457
|
+
config: buttonConfig,
|
|
458
|
+
isExecuting: false,
|
|
459
|
+
history: [], // Start with empty history, load async
|
|
460
|
+
accessibility: {
|
|
461
|
+
role: "button",
|
|
462
|
+
ariaLabel:
|
|
463
|
+
buttonConfig.tooltip ||
|
|
464
|
+
buttonConfig.text ||
|
|
465
|
+
`Button ${buttonConfig.id}`,
|
|
466
|
+
focusOrder: priority,
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
this.buttonStates.set(buttonConfig.id, buttonState);
|
|
471
|
+
this.disposables.push(statusBarItem);
|
|
472
|
+
|
|
473
|
+
// Show button immediately (don't block on history or dynamic labels)
|
|
474
|
+
statusBarItem.show();
|
|
475
|
+
|
|
476
|
+
// Load history and dynamic labels asynchronously (non-blocking)
|
|
477
|
+
setImmediate(() => {
|
|
478
|
+
this.loadHistoryAsync(buttonConfig.id).catch((error) => {
|
|
479
|
+
this.debugLog(
|
|
480
|
+
`Failed to load history for ${buttonConfig.id}:`,
|
|
481
|
+
error,
|
|
482
|
+
);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
if (buttonConfig.dynamicLabel) {
|
|
486
|
+
this.refreshButtonLabel(buttonConfig.id).catch((error) => {
|
|
487
|
+
this.debugLog(
|
|
488
|
+
`Failed to refresh label for ${buttonConfig.id}:`,
|
|
489
|
+
error,
|
|
490
|
+
);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
this.debugLog(`Button ${buttonConfig.id} created and shown`);
|
|
496
|
+
return true;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
const errorMessage =
|
|
499
|
+
error instanceof Error ? error.message : String(error);
|
|
500
|
+
console.error(
|
|
501
|
+
`Failed to create statusbar item for button ${buttonConfig.id}:`,
|
|
502
|
+
errorMessage,
|
|
503
|
+
);
|
|
504
|
+
vscode.window.showErrorMessage(
|
|
505
|
+
`Failed to create button "${buttonConfig.text || buttonConfig.id}": ${errorMessage}`,
|
|
506
|
+
);
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Get display text for button (text or icon)
|
|
513
|
+
*/
|
|
514
|
+
private getButtonDisplayText(buttonConfig: StatusBarButtonConfig): string {
|
|
515
|
+
if (buttonConfig.icon) {
|
|
516
|
+
// Resolve Material icons to Codicons if needed
|
|
517
|
+
const resolvedIconId = this.materialIconManager.resolveIcon(
|
|
518
|
+
buttonConfig.icon,
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
const iconPrefix =
|
|
522
|
+
buttonConfig.icon.animation === "spin"
|
|
523
|
+
? `$(${resolvedIconId}~spin)`
|
|
524
|
+
: buttonConfig.icon.animation === "pulse"
|
|
525
|
+
? `$(${resolvedIconId}~pulse)`
|
|
526
|
+
: `$(${resolvedIconId})`;
|
|
527
|
+
return iconPrefix;
|
|
528
|
+
}
|
|
529
|
+
return buttonConfig.text;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Execute a button command
|
|
534
|
+
*/
|
|
535
|
+
private async executeButton(buttonId: string): Promise<void> {
|
|
536
|
+
const buttonState = this.buttonStates.get(buttonId);
|
|
537
|
+
if (!buttonState || buttonState.isExecuting) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const config = buttonState.config;
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
// Set executing state
|
|
545
|
+
buttonState.isExecuting = true;
|
|
546
|
+
this.updateButtonState(buttonId, buttonState);
|
|
547
|
+
|
|
548
|
+
// Show progress if enabled
|
|
549
|
+
if (config.execution?.showProgress) {
|
|
550
|
+
this.showProgress(buttonId);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Prepare execution options
|
|
554
|
+
const executionOptions: ExecutionOptions = {
|
|
555
|
+
workingDirectory: config.workingDirectory,
|
|
556
|
+
environment: config.environment,
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
if (config.execution?.timeout) {
|
|
560
|
+
executionOptions.timeout = config.execution.timeout;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Add streaming support if output panel is enabled
|
|
564
|
+
const outputConfig = this.outputPanelManager.getConfig();
|
|
565
|
+
if (outputConfig.enabled) {
|
|
566
|
+
// Ensure panel exists
|
|
567
|
+
this.outputPanelManager.getOrCreatePanel(buttonId, config.text);
|
|
568
|
+
|
|
569
|
+
if (outputConfig.clearOnRun) {
|
|
570
|
+
this.outputPanelManager.clearPanel(buttonId);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
executionOptions.streaming = {
|
|
574
|
+
enabled: true,
|
|
575
|
+
onStdout: (data) => {
|
|
576
|
+
this.outputPanelManager.appendOutput(buttonId, data, "stdout");
|
|
577
|
+
},
|
|
578
|
+
onStderr: (data) => {
|
|
579
|
+
this.outputPanelManager.appendOutput(buttonId, data, "stderr");
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
this.outputPanelManager.showPanel(buttonId, true);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Execute the command
|
|
587
|
+
const result = await this.commandExecutor.execute(
|
|
588
|
+
config.command,
|
|
589
|
+
executionOptions,
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
// Update button state
|
|
593
|
+
buttonState.isExecuting = false;
|
|
594
|
+
buttonState.lastResult = result;
|
|
595
|
+
|
|
596
|
+
// Add to history if enabled
|
|
597
|
+
if (config.history?.enabled !== false) {
|
|
598
|
+
await this.addToHistory(buttonId, result);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
this.updateButtonState(buttonId, buttonState);
|
|
602
|
+
|
|
603
|
+
// Show result
|
|
604
|
+
await this.showExecutionResult(buttonId, result);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
buttonState.isExecuting = false;
|
|
607
|
+
this.updateButtonState(buttonId, buttonState);
|
|
608
|
+
|
|
609
|
+
const errorResult: ExecutionResult = {
|
|
610
|
+
code: -1,
|
|
611
|
+
stdout: "",
|
|
612
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
613
|
+
duration: 0,
|
|
614
|
+
timestamp: new Date(),
|
|
615
|
+
command: "Unknown",
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
buttonState.lastResult = errorResult;
|
|
619
|
+
this.updateButtonState(buttonId, buttonState);
|
|
620
|
+
|
|
621
|
+
await this.showExecutionError(buttonId, error);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Update button state in UI
|
|
627
|
+
*/
|
|
628
|
+
private updateButtonState(buttonId: string, buttonState: ButtonState): void {
|
|
629
|
+
const config = buttonState.config;
|
|
630
|
+
|
|
631
|
+
if (buttonState.isExecuting) {
|
|
632
|
+
// Show executing state
|
|
633
|
+
if (config.icon?.animation) {
|
|
634
|
+
buttonState.item.text =
|
|
635
|
+
config.icon.animation === "spin"
|
|
636
|
+
? "$(sync~spin)"
|
|
637
|
+
: config.icon.animation === "pulse"
|
|
638
|
+
? "$(sync~pulse)"
|
|
639
|
+
: this.getButtonDisplayText(config);
|
|
640
|
+
} else {
|
|
641
|
+
buttonState.item.text = `$(sync~spin) ${config.text}`;
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
// Show normal state
|
|
645
|
+
buttonState.item.text = this.getButtonDisplayText(config);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Update tooltip with last result if available
|
|
649
|
+
if (buttonState.lastResult) {
|
|
650
|
+
const result = buttonState.lastResult;
|
|
651
|
+
const status = result.code === 0 ? "✅" : "❌";
|
|
652
|
+
const duration = result.duration ? ` (${result.duration}ms)` : "";
|
|
653
|
+
buttonState.item.tooltip = `${config.tooltip || config.text}\n${status} Last run: ${result.timestamp.toLocaleTimeString()}${duration}`;
|
|
654
|
+
} else {
|
|
655
|
+
buttonState.item.tooltip = config.tooltip || config.text;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
buttonState.item.show();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Show progress indicator
|
|
663
|
+
*/
|
|
664
|
+
private showProgress(buttonId: string): void {
|
|
665
|
+
const buttonState = this.buttonStates.get(buttonId);
|
|
666
|
+
if (!buttonState) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Use VS Code's built-in progress API for better integration
|
|
671
|
+
vscode.window.withProgress(
|
|
672
|
+
{
|
|
673
|
+
location: vscode.ProgressLocation.Notification,
|
|
674
|
+
title: `Executing: ${buttonState.config.text}`,
|
|
675
|
+
cancellable: false,
|
|
676
|
+
},
|
|
677
|
+
async (progress) => {
|
|
678
|
+
// Simulate progress (in a real implementation, this would be updated by the command executor)
|
|
679
|
+
for (let i = 0; i <= 100; i += 10) {
|
|
680
|
+
if (!buttonState.isExecuting) {
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
progress.report({ increment: 10, message: `${i}%` });
|
|
684
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Show execution result
|
|
692
|
+
*/
|
|
693
|
+
private async showExecutionResult(
|
|
694
|
+
buttonId: string,
|
|
695
|
+
result: ExecutionResult,
|
|
696
|
+
): Promise<void> {
|
|
697
|
+
const buttonState = this.buttonStates.get(buttonId);
|
|
698
|
+
if (!buttonState) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const config = buttonState.config;
|
|
703
|
+
|
|
704
|
+
// Show success notification
|
|
705
|
+
if (result.code === 0) {
|
|
706
|
+
const message = this.getResultMessage(result);
|
|
707
|
+
vscode.window
|
|
708
|
+
.showInformationMessage(`✅ ${config.text}: ${message}`, "View Output")
|
|
709
|
+
.then((selection) => {
|
|
710
|
+
if (selection === "View Output") {
|
|
711
|
+
this.showOutput(result);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Show execution error
|
|
719
|
+
*/
|
|
720
|
+
private async showExecutionError(
|
|
721
|
+
buttonId: string,
|
|
722
|
+
error: unknown,
|
|
723
|
+
): Promise<void> {
|
|
724
|
+
const buttonState = this.buttonStates.get(buttonId);
|
|
725
|
+
if (!buttonState) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const config = buttonState.config;
|
|
730
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
731
|
+
vscode.window
|
|
732
|
+
.showErrorMessage(`❌ ${config.text}: ${errorMessage}`, "View Details")
|
|
733
|
+
.then((selection) => {
|
|
734
|
+
if (selection === "View Details") {
|
|
735
|
+
vscode.window.showErrorMessage(errorMessage, { modal: true });
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Get result message for display
|
|
742
|
+
*/
|
|
743
|
+
private getResultMessage(result: ExecutionResult): string {
|
|
744
|
+
const showTime = true;
|
|
745
|
+
const timeStr =
|
|
746
|
+
showTime && result.duration ? ` in ${result.duration}ms` : "";
|
|
747
|
+
|
|
748
|
+
if (result.stdout && result.stdout.trim()) {
|
|
749
|
+
const output = result.stdout.trim().split("\n")[0];
|
|
750
|
+
return output.length > 100
|
|
751
|
+
? `${output.substring(0, 100)}...${timeStr}`
|
|
752
|
+
: `${output}${timeStr}`;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return `Completed successfully${timeStr}`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Show command output
|
|
760
|
+
*/
|
|
761
|
+
private showOutput(result: ExecutionResult): void {
|
|
762
|
+
const output = `Command Output:\n${result.stdout}\n\nErrors:\n${result.stderr}`;
|
|
763
|
+
vscode.window.showInformationMessage(output, { modal: true });
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Edit button configuration - Main settings menu
|
|
768
|
+
*/
|
|
769
|
+
private async editButton(): Promise<void> {
|
|
770
|
+
const mainMenuItems: vscode.QuickPickItem[] = [
|
|
771
|
+
{
|
|
772
|
+
label: "$(add) Add New Button",
|
|
773
|
+
description: "Create a new status bar button",
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
label: "$(edit) Edit Existing Button",
|
|
777
|
+
description: "Modify an existing button configuration",
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
label: "$(trash) Delete Button",
|
|
781
|
+
description: "Remove a button from the status bar",
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
label: "$(copy) Duplicate Button",
|
|
785
|
+
description: "Create a copy of an existing button",
|
|
786
|
+
},
|
|
787
|
+
{
|
|
788
|
+
label: "$(sync) Toggle Button",
|
|
789
|
+
description: "Enable or disable a button",
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
label: "$(archive) Manage Presets",
|
|
793
|
+
description: "Save, load, or manage configuration presets",
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
label: "$(settings-gear) Open Full Settings",
|
|
797
|
+
description: "Open VS Code settings page",
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
label: "$(export) Export Configuration",
|
|
801
|
+
description: "Export all button configurations to a file",
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
label: "$(import) Import Configuration",
|
|
805
|
+
description: "Import button configurations from a file",
|
|
806
|
+
},
|
|
807
|
+
];
|
|
808
|
+
|
|
809
|
+
const selected = await vscode.window.showQuickPick(mainMenuItems, {
|
|
810
|
+
placeHolder: "StatusBar Quick Actions - Settings",
|
|
811
|
+
matchOnDescription: true,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
if (!selected) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
switch (selected.label) {
|
|
819
|
+
case "$(add) Add New Button":
|
|
820
|
+
await this.addNewButton();
|
|
821
|
+
break;
|
|
822
|
+
case "$(edit) Edit Existing Button":
|
|
823
|
+
await this.selectAndEditButton();
|
|
824
|
+
break;
|
|
825
|
+
case "$(trash) Delete Button":
|
|
826
|
+
await this.deleteButton();
|
|
827
|
+
break;
|
|
828
|
+
case "$(copy) Duplicate Button":
|
|
829
|
+
await this.duplicateButton();
|
|
830
|
+
break;
|
|
831
|
+
case "$(sync) Toggle Button":
|
|
832
|
+
await this.toggleButton();
|
|
833
|
+
break;
|
|
834
|
+
case "$(archive) Manage Presets":
|
|
835
|
+
await this.managePresets();
|
|
836
|
+
break;
|
|
837
|
+
case "$(settings-gear) Open Full Settings":
|
|
838
|
+
vscode.commands.executeCommand(
|
|
839
|
+
"workbench.action.openSettings",
|
|
840
|
+
"@ext:involvex.statusbar-quick-actions",
|
|
841
|
+
);
|
|
842
|
+
break;
|
|
843
|
+
case "$(export) Export Configuration":
|
|
844
|
+
await this.exportConfiguration();
|
|
845
|
+
break;
|
|
846
|
+
case "$(import) Import Configuration":
|
|
847
|
+
await this.importConfiguration();
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Add a new button interactively
|
|
854
|
+
*/
|
|
855
|
+
private async addNewButton(): Promise<void> {
|
|
856
|
+
// Get button text
|
|
857
|
+
const text = await vscode.window.showInputBox({
|
|
858
|
+
prompt: "Enter button text (supports emojis)",
|
|
859
|
+
placeHolder: "e.g., Build 🔨",
|
|
860
|
+
validateInput: (value) => (value ? null : "Button text is required"),
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
if (!text) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Get command type
|
|
868
|
+
const commandTypes: vscode.QuickPickItem[] = [
|
|
869
|
+
{ label: "npm", description: "Run npm script" },
|
|
870
|
+
{ label: "yarn", description: "Run yarn script" },
|
|
871
|
+
{ label: "pnpm", description: "Run pnpm script" },
|
|
872
|
+
{ label: "bun", description: "Run bun script" },
|
|
873
|
+
{ label: "shell", description: "Run shell command" },
|
|
874
|
+
{ label: "vscode", description: "Run VS Code command" },
|
|
875
|
+
{ label: "task", description: "Run VS Code task" },
|
|
876
|
+
{ label: "github", description: "Run GitHub CLI command" },
|
|
877
|
+
{ label: "npx", description: "Run npx command" },
|
|
878
|
+
{ label: "pnpx", description: "Run pnpx command" },
|
|
879
|
+
{ label: "bunx", description: "Run bunx command" },
|
|
880
|
+
{ label: "detect", description: "Auto-detect package manager" },
|
|
881
|
+
];
|
|
882
|
+
|
|
883
|
+
const commandType = await vscode.window.showQuickPick(commandTypes, {
|
|
884
|
+
placeHolder: "Select command type",
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
if (!commandType) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Get command/script
|
|
892
|
+
const command = await vscode.window.showInputBox({
|
|
893
|
+
prompt:
|
|
894
|
+
commandType.label === "npm" ||
|
|
895
|
+
commandType.label === "yarn" ||
|
|
896
|
+
commandType.label === "pnpm" ||
|
|
897
|
+
commandType.label === "bun" ||
|
|
898
|
+
commandType.label === "npx" ||
|
|
899
|
+
commandType.label === "pnpx" ||
|
|
900
|
+
commandType.label === "bunx" ||
|
|
901
|
+
commandType.label === "detect"
|
|
902
|
+
? "Enter script name"
|
|
903
|
+
: "Enter command",
|
|
904
|
+
placeHolder:
|
|
905
|
+
commandType.label === "npm" ? "e.g., build" : 'e.g., echo "Hello"',
|
|
906
|
+
validateInput: (value) => (value ? null : "Command is required"),
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
if (!command) {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Generate unique ID
|
|
914
|
+
const id = `button_${Date.now()}`;
|
|
915
|
+
|
|
916
|
+
// Create new button configuration
|
|
917
|
+
const newButton: StatusBarButtonConfig = {
|
|
918
|
+
id,
|
|
919
|
+
text,
|
|
920
|
+
tooltip: text,
|
|
921
|
+
command: {
|
|
922
|
+
type: commandType.label as
|
|
923
|
+
| "npm"
|
|
924
|
+
| "yarn"
|
|
925
|
+
| "pnpm"
|
|
926
|
+
| "bun"
|
|
927
|
+
| "npx"
|
|
928
|
+
| "pnpx"
|
|
929
|
+
| "bunx"
|
|
930
|
+
| "shell"
|
|
931
|
+
| "github"
|
|
932
|
+
| "vscode"
|
|
933
|
+
| "task"
|
|
934
|
+
| "detect",
|
|
935
|
+
script: [
|
|
936
|
+
"npm",
|
|
937
|
+
"yarn",
|
|
938
|
+
"pnpm",
|
|
939
|
+
"bun",
|
|
940
|
+
"bunx",
|
|
941
|
+
"npx",
|
|
942
|
+
"pnpx",
|
|
943
|
+
"detect",
|
|
944
|
+
].includes(commandType.label)
|
|
945
|
+
? command
|
|
946
|
+
: undefined,
|
|
947
|
+
command: ![
|
|
948
|
+
"npm",
|
|
949
|
+
"yarn",
|
|
950
|
+
"pnpm",
|
|
951
|
+
"bun",
|
|
952
|
+
"bunx",
|
|
953
|
+
"npx",
|
|
954
|
+
"pnpx",
|
|
955
|
+
"detect",
|
|
956
|
+
].includes(commandType.label)
|
|
957
|
+
? command
|
|
958
|
+
: undefined,
|
|
959
|
+
},
|
|
960
|
+
enabled: true,
|
|
961
|
+
alignment: "left",
|
|
962
|
+
priority: 100,
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
// Add to configuration
|
|
966
|
+
const config = this.configManager.getConfig();
|
|
967
|
+
config.buttons.push(newButton);
|
|
968
|
+
await this.configManager.setConfig("buttons", config.buttons);
|
|
969
|
+
|
|
970
|
+
vscode.window.showInformationMessage(
|
|
971
|
+
`✅ Button "${text}" added successfully!`,
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Select and edit an existing button
|
|
977
|
+
*/
|
|
978
|
+
private async selectAndEditButton(): Promise<void> {
|
|
979
|
+
const config = this.configManager.getConfig();
|
|
980
|
+
if (config.buttons.length === 0) {
|
|
981
|
+
vscode.window.showInformationMessage("No buttons configured yet.");
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const items = config.buttons.map((button) => ({
|
|
986
|
+
label: button.text,
|
|
987
|
+
description: button.command.type,
|
|
988
|
+
detail: button.tooltip,
|
|
989
|
+
button: button,
|
|
990
|
+
}));
|
|
991
|
+
|
|
992
|
+
const selected = await vscode.window.showQuickPick(items, {
|
|
993
|
+
placeHolder: "Select a button to edit",
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
if (selected) {
|
|
997
|
+
vscode.commands.executeCommand(
|
|
998
|
+
"workbench.action.openSettings",
|
|
999
|
+
`@ext:involvex.statusbar-quick-actions buttons`,
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Delete a button
|
|
1006
|
+
*/
|
|
1007
|
+
private async deleteButton(): Promise<void> {
|
|
1008
|
+
const config = this.configManager.getConfig();
|
|
1009
|
+
if (config.buttons.length === 0) {
|
|
1010
|
+
vscode.window.showInformationMessage("No buttons configured yet.");
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const items = config.buttons.map((button) => ({
|
|
1015
|
+
label: button.text,
|
|
1016
|
+
description: button.command.type,
|
|
1017
|
+
detail: button.id,
|
|
1018
|
+
}));
|
|
1019
|
+
|
|
1020
|
+
const selected = await vscode.window.showQuickPick(items, {
|
|
1021
|
+
placeHolder: "Select a button to delete",
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
if (!selected) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const confirm = await vscode.window.showWarningMessage(
|
|
1029
|
+
`Delete button "${selected.label}"?`,
|
|
1030
|
+
{ modal: true },
|
|
1031
|
+
"Yes, Delete",
|
|
1032
|
+
"No",
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
if (confirm === "Yes, Delete") {
|
|
1036
|
+
const updatedButtons = config.buttons.filter(
|
|
1037
|
+
(b) => b.id !== selected.detail,
|
|
1038
|
+
);
|
|
1039
|
+
await this.configManager.setConfig("buttons", updatedButtons);
|
|
1040
|
+
vscode.window.showInformationMessage(
|
|
1041
|
+
`✅ Button "${selected.label}" deleted`,
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Duplicate a button
|
|
1048
|
+
*/
|
|
1049
|
+
private async duplicateButton(): Promise<void> {
|
|
1050
|
+
const config = this.configManager.getConfig();
|
|
1051
|
+
if (config.buttons.length === 0) {
|
|
1052
|
+
vscode.window.showInformationMessage("No buttons configured yet.");
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const items = config.buttons.map((button) => ({
|
|
1057
|
+
label: button.text,
|
|
1058
|
+
description: button.command.type,
|
|
1059
|
+
button: button,
|
|
1060
|
+
}));
|
|
1061
|
+
|
|
1062
|
+
const selected = await vscode.window.showQuickPick(items, {
|
|
1063
|
+
placeHolder: "Select a button to duplicate",
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
if (!selected) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const newButton = {
|
|
1071
|
+
...selected.button,
|
|
1072
|
+
id: `button_${Date.now()}`,
|
|
1073
|
+
text: `${selected.button.text} (Copy)`,
|
|
1074
|
+
};
|
|
1075
|
+
|
|
1076
|
+
config.buttons.push(newButton);
|
|
1077
|
+
await this.configManager.setConfig("buttons", config.buttons);
|
|
1078
|
+
vscode.window.showInformationMessage(`✅ Button duplicated successfully!`);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Toggle button enabled state
|
|
1083
|
+
*/
|
|
1084
|
+
private async toggleButton(): Promise<void> {
|
|
1085
|
+
const config = this.configManager.getConfig();
|
|
1086
|
+
if (config.buttons.length === 0) {
|
|
1087
|
+
vscode.window.showInformationMessage("No buttons configured yet.");
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const items = config.buttons.map((button) => ({
|
|
1092
|
+
label: button.text,
|
|
1093
|
+
description: button.enabled ? "$(check) Enabled" : "$(x) Disabled",
|
|
1094
|
+
button: button,
|
|
1095
|
+
}));
|
|
1096
|
+
|
|
1097
|
+
const selected = await vscode.window.showQuickPick(items, {
|
|
1098
|
+
placeHolder: "Select a button to toggle",
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
if (!selected) {
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const buttonIndex = config.buttons.findIndex(
|
|
1106
|
+
(b) => b.id === selected.button.id,
|
|
1107
|
+
);
|
|
1108
|
+
config.buttons[buttonIndex].enabled = !config.buttons[buttonIndex].enabled;
|
|
1109
|
+
|
|
1110
|
+
await this.configManager.setConfig("buttons", config.buttons);
|
|
1111
|
+
const status = config.buttons[buttonIndex].enabled ? "enabled" : "disabled";
|
|
1112
|
+
vscode.window.showInformationMessage(
|
|
1113
|
+
`✅ Button "${selected.label}" ${status}`,
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Export configuration to file
|
|
1119
|
+
*/
|
|
1120
|
+
private async exportConfiguration(): Promise<void> {
|
|
1121
|
+
const config = this.configManager.getConfig();
|
|
1122
|
+
|
|
1123
|
+
const uri = await vscode.window.showSaveDialog({
|
|
1124
|
+
filters: { JSON: ["json"] },
|
|
1125
|
+
defaultUri: vscode.Uri.file("statusbar-quick-actions-config.json"),
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
if (uri) {
|
|
1129
|
+
fs.writeFileSync(uri.fsPath, JSON.stringify(config, null, 2));
|
|
1130
|
+
vscode.window.showInformationMessage(
|
|
1131
|
+
`✅ Configuration exported to ${uri.fsPath}`,
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Import configuration from file
|
|
1138
|
+
*/
|
|
1139
|
+
private async importConfiguration(): Promise<void> {
|
|
1140
|
+
const uri = await vscode.window.showOpenDialog({
|
|
1141
|
+
filters: { JSON: ["json"] },
|
|
1142
|
+
canSelectMany: false,
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
if (uri && uri[0]) {
|
|
1146
|
+
try {
|
|
1147
|
+
const content = fs.readFileSync(uri[0].fsPath, "utf8");
|
|
1148
|
+
const importedConfig = JSON.parse(content);
|
|
1149
|
+
|
|
1150
|
+
const merge = await vscode.window.showQuickPick(
|
|
1151
|
+
["Replace All", "Merge with Existing"],
|
|
1152
|
+
{ placeHolder: "Import mode" },
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
if (!merge) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (merge === "Replace All") {
|
|
1160
|
+
await this.configManager.setConfig(
|
|
1161
|
+
"buttons",
|
|
1162
|
+
importedConfig.buttons || [],
|
|
1163
|
+
);
|
|
1164
|
+
} else {
|
|
1165
|
+
const config = this.configManager.getConfig();
|
|
1166
|
+
const mergedButtons = [
|
|
1167
|
+
...config.buttons,
|
|
1168
|
+
...(importedConfig.buttons || []),
|
|
1169
|
+
];
|
|
1170
|
+
await this.configManager.setConfig("buttons", mergedButtons);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
vscode.window.showInformationMessage(
|
|
1174
|
+
"✅ Configuration imported successfully!",
|
|
1175
|
+
);
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
vscode.window.showErrorMessage(
|
|
1178
|
+
`Failed to import configuration: ${error}`,
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Add execution result to history
|
|
1186
|
+
*/
|
|
1187
|
+
private async addToHistory(
|
|
1188
|
+
buttonId: string,
|
|
1189
|
+
result: ExecutionResult,
|
|
1190
|
+
): Promise<void> {
|
|
1191
|
+
try {
|
|
1192
|
+
const historyKey = `history_${buttonId}`;
|
|
1193
|
+
const history: ExecutionResult[] = this.context.globalState.get(
|
|
1194
|
+
historyKey,
|
|
1195
|
+
[],
|
|
1196
|
+
);
|
|
1197
|
+
|
|
1198
|
+
// Add new result
|
|
1199
|
+
history.unshift(result);
|
|
1200
|
+
|
|
1201
|
+
// Limit history size (default 20, configurable per button)
|
|
1202
|
+
const buttonState = this.buttonStates.get(buttonId);
|
|
1203
|
+
const maxEntries = buttonState?.config.history?.maxEntries || 20;
|
|
1204
|
+
while (history.length > maxEntries) {
|
|
1205
|
+
history.pop();
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Save to global state
|
|
1209
|
+
await this.context.globalState.update(historyKey, history);
|
|
1210
|
+
|
|
1211
|
+
// Also update button state for quick access
|
|
1212
|
+
if (buttonState) {
|
|
1213
|
+
buttonState.history = history;
|
|
1214
|
+
}
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
console.error(
|
|
1217
|
+
`Failed to add execution to history for button ${buttonId}:`,
|
|
1218
|
+
error,
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Load history from global state
|
|
1225
|
+
*/
|
|
1226
|
+
private async loadHistory(buttonId: string): Promise<ExecutionResult[]> {
|
|
1227
|
+
try {
|
|
1228
|
+
const historyKey = `history_${buttonId}`;
|
|
1229
|
+
return this.context.globalState.get(historyKey, []);
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
console.error(`Failed to load history for button ${buttonId}:`, error);
|
|
1232
|
+
return [];
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Load history asynchronously and update button state
|
|
1238
|
+
*/
|
|
1239
|
+
private async loadHistoryAsync(buttonId: string): Promise<void> {
|
|
1240
|
+
const buttonState = this.buttonStates.get(buttonId);
|
|
1241
|
+
if (!buttonState) {
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const history = await this.loadHistory(buttonId);
|
|
1246
|
+
buttonState.history = history;
|
|
1247
|
+
this.debugLog(`History loaded for ${buttonId}: ${history.length} entries`);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* Get all history entries across all buttons
|
|
1252
|
+
*/
|
|
1253
|
+
private async getAllHistory(): Promise<Map<string, ExecutionResult[]>> {
|
|
1254
|
+
const allHistory = new Map<string, ExecutionResult[]>();
|
|
1255
|
+
|
|
1256
|
+
for (const [buttonId] of this.buttonStates) {
|
|
1257
|
+
const history = await this.loadHistory(buttonId);
|
|
1258
|
+
if (history.length > 0) {
|
|
1259
|
+
allHistory.set(buttonId, history);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return allHistory;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* View command history
|
|
1268
|
+
*/
|
|
1269
|
+
private async viewHistory(): Promise<void> {
|
|
1270
|
+
const allHistory = await this.getAllHistory();
|
|
1271
|
+
|
|
1272
|
+
if (allHistory.size === 0) {
|
|
1273
|
+
vscode.window.showInformationMessage("No command history available yet.");
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Create quick pick items from history
|
|
1278
|
+
const items: vscode.QuickPickItem[] = [];
|
|
1279
|
+
|
|
1280
|
+
for (const [buttonId, history] of allHistory) {
|
|
1281
|
+
const buttonState = this.buttonStates.get(buttonId);
|
|
1282
|
+
const buttonName = buttonState?.config.text || buttonId;
|
|
1283
|
+
|
|
1284
|
+
items.push({
|
|
1285
|
+
label: `$(inbox) ${buttonName}`,
|
|
1286
|
+
kind: vscode.QuickPickItemKind.Separator,
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
history.forEach((entry) => {
|
|
1290
|
+
const status = entry.code === 0 ? "$(check)" : "$(error)";
|
|
1291
|
+
const time = entry.timestamp.toLocaleString();
|
|
1292
|
+
const duration = entry.duration ? ` (${entry.duration}ms)` : "";
|
|
1293
|
+
|
|
1294
|
+
items.push({
|
|
1295
|
+
label: `${status} ${entry.command}`,
|
|
1296
|
+
description: `${time}${duration}`,
|
|
1297
|
+
detail: entry.stderr || entry.stdout?.substring(0, 100),
|
|
1298
|
+
buttons: [
|
|
1299
|
+
{
|
|
1300
|
+
iconPath: new vscode.ThemeIcon("output"),
|
|
1301
|
+
tooltip: "View Full Output",
|
|
1302
|
+
},
|
|
1303
|
+
],
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Show quick pick
|
|
1309
|
+
const selected = await vscode.window.showQuickPick(items, {
|
|
1310
|
+
placeHolder: "Command Execution History",
|
|
1311
|
+
matchOnDescription: true,
|
|
1312
|
+
matchOnDetail: true,
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
if (selected && selected.detail) {
|
|
1316
|
+
// Show detailed output in a new text document
|
|
1317
|
+
const doc = await vscode.workspace.openTextDocument({
|
|
1318
|
+
content: `Command: ${selected.label}\nTime: ${selected.description}\n\nOutput:\n${selected.detail}`,
|
|
1319
|
+
language: "text",
|
|
1320
|
+
});
|
|
1321
|
+
await vscode.window.showTextDocument(doc);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* Clear command history
|
|
1327
|
+
*/
|
|
1328
|
+
private async clearHistory(): Promise<void> {
|
|
1329
|
+
const confirm = await vscode.window.showWarningMessage(
|
|
1330
|
+
"Are you sure you want to clear all command history?",
|
|
1331
|
+
{ modal: true },
|
|
1332
|
+
"Yes, Clear History",
|
|
1333
|
+
"No",
|
|
1334
|
+
);
|
|
1335
|
+
|
|
1336
|
+
if (confirm === "Yes, Clear History") {
|
|
1337
|
+
try {
|
|
1338
|
+
// Clear history for all buttons
|
|
1339
|
+
for (const [buttonId, buttonState] of this.buttonStates) {
|
|
1340
|
+
const historyKey = `history_${buttonId}`;
|
|
1341
|
+
await this.context.globalState.update(historyKey, []);
|
|
1342
|
+
buttonState.history = [];
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
vscode.window.showInformationMessage(
|
|
1346
|
+
"✅ Command history cleared successfully",
|
|
1347
|
+
);
|
|
1348
|
+
} catch (error) {
|
|
1349
|
+
vscode.window.showErrorMessage(`Failed to clear history: ${error}`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Show welcome message on first activation
|
|
1356
|
+
*/
|
|
1357
|
+
private async showWelcomeMessage(): Promise<void> {
|
|
1358
|
+
const config = this.configManager.getConfig();
|
|
1359
|
+
|
|
1360
|
+
if (config.buttons.length === 0) {
|
|
1361
|
+
vscode.window
|
|
1362
|
+
.showInformationMessage(
|
|
1363
|
+
"👋 Welcome to StatusBar Quick Actions! Configure your first button in Settings.",
|
|
1364
|
+
"Open Settings",
|
|
1365
|
+
)
|
|
1366
|
+
.then((selection) => {
|
|
1367
|
+
if (selection === "Open Settings") {
|
|
1368
|
+
vscode.commands.executeCommand(
|
|
1369
|
+
"workbench.action.openSettings",
|
|
1370
|
+
"@ext:statusbar-quick-actions",
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Get default output panel configuration
|
|
1379
|
+
*/
|
|
1380
|
+
private getDefaultOutputConfig() {
|
|
1381
|
+
return {
|
|
1382
|
+
enabled: true,
|
|
1383
|
+
mode: "per-button" as const,
|
|
1384
|
+
format: "formatted" as const,
|
|
1385
|
+
clearOnRun: false,
|
|
1386
|
+
showTimestamps: true,
|
|
1387
|
+
preserveHistory: true,
|
|
1388
|
+
maxLines: 1000,
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Get default performance configuration
|
|
1394
|
+
*/
|
|
1395
|
+
private getDefaultPerformanceConfig() {
|
|
1396
|
+
return {
|
|
1397
|
+
visibilityDebounceMs: 300,
|
|
1398
|
+
enableVirtualization: false,
|
|
1399
|
+
cacheResults: true,
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Setup editor change listener for debounced visibility checks
|
|
1405
|
+
*/
|
|
1406
|
+
private setupEditorChangeListener(): void {
|
|
1407
|
+
this.editorChangeListener = vscode.window.onDidChangeActiveTextEditor(
|
|
1408
|
+
() => {
|
|
1409
|
+
// Debounced visibility check for all buttons
|
|
1410
|
+
this.buttonStates.forEach((buttonState, buttonId) => {
|
|
1411
|
+
if (buttonState.config.visibility) {
|
|
1412
|
+
const customDebounce = buttonState.config.visibility.debounceMs;
|
|
1413
|
+
|
|
1414
|
+
this.visibilityManager.checkVisibilityDebounced(
|
|
1415
|
+
buttonId,
|
|
1416
|
+
buttonState.config.visibility,
|
|
1417
|
+
customDebounce,
|
|
1418
|
+
(isVisible) => {
|
|
1419
|
+
// Update button visibility
|
|
1420
|
+
if (isVisible) {
|
|
1421
|
+
buttonState.item.show();
|
|
1422
|
+
} else {
|
|
1423
|
+
buttonState.item.hide();
|
|
1424
|
+
}
|
|
1425
|
+
},
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
},
|
|
1430
|
+
);
|
|
1431
|
+
|
|
1432
|
+
this.disposables.push(this.editorChangeListener);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Manage presets UI
|
|
1437
|
+
*/
|
|
1438
|
+
private async managePresets(): Promise<void> {
|
|
1439
|
+
const items: vscode.QuickPickItem[] = [
|
|
1440
|
+
{
|
|
1441
|
+
label: "$(add) Create New Preset",
|
|
1442
|
+
description: "Save current configuration as a preset",
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
label: "$(archive) Apply Preset",
|
|
1446
|
+
description: "Load a saved preset",
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
label: "$(list-unordered) View All Presets",
|
|
1450
|
+
description: "Browse and manage saved presets",
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
label: "$(export) Export Preset",
|
|
1454
|
+
description: "Export a preset to a file",
|
|
1455
|
+
},
|
|
1456
|
+
{
|
|
1457
|
+
label: "$(import) Import Preset",
|
|
1458
|
+
description: "Import a preset from a file",
|
|
1459
|
+
},
|
|
1460
|
+
];
|
|
1461
|
+
|
|
1462
|
+
const selected = await vscode.window.showQuickPick(items, {
|
|
1463
|
+
placeHolder: "Preset Management",
|
|
1464
|
+
matchOnDescription: true,
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
if (!selected) {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
switch (selected.label) {
|
|
1472
|
+
case "$(add) Create New Preset":
|
|
1473
|
+
await this.saveAsPreset();
|
|
1474
|
+
break;
|
|
1475
|
+
case "$(archive) Apply Preset":
|
|
1476
|
+
await this.applyPresetCommand();
|
|
1477
|
+
break;
|
|
1478
|
+
case "$(list-unordered) View All Presets":
|
|
1479
|
+
await this.viewAllPresets();
|
|
1480
|
+
break;
|
|
1481
|
+
case "$(export) Export Preset":
|
|
1482
|
+
await this.exportPresetCommand();
|
|
1483
|
+
break;
|
|
1484
|
+
case "$(import) Import Preset":
|
|
1485
|
+
await this.importPresetCommand();
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/**
|
|
1491
|
+
* Save current configuration as a preset
|
|
1492
|
+
*/
|
|
1493
|
+
private async saveAsPreset(): Promise<void> {
|
|
1494
|
+
const name = await vscode.window.showInputBox({
|
|
1495
|
+
prompt: "Enter preset name",
|
|
1496
|
+
placeHolder: "e.g., My Development Setup",
|
|
1497
|
+
validateInput: (value) => (value ? null : "Name is required"),
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
if (!name) {
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const description = await vscode.window.showInputBox({
|
|
1505
|
+
prompt: "Enter preset description (optional)",
|
|
1506
|
+
placeHolder: "e.g., Standard buttons for Node.js development",
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
try {
|
|
1510
|
+
const currentConfig = this.configManager.getConfig();
|
|
1511
|
+
await this.presetManager.createPresetFromConfig(
|
|
1512
|
+
name,
|
|
1513
|
+
description || "",
|
|
1514
|
+
currentConfig,
|
|
1515
|
+
);
|
|
1516
|
+
|
|
1517
|
+
vscode.window.showInformationMessage(
|
|
1518
|
+
`✅ Preset "${name}" created successfully!`,
|
|
1519
|
+
);
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
vscode.window.showErrorMessage(`Failed to create preset: ${error}`);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Apply a preset command
|
|
1527
|
+
*/
|
|
1528
|
+
private async applyPresetCommand(): Promise<void> {
|
|
1529
|
+
const presets = this.presetManager.getAllPresets();
|
|
1530
|
+
|
|
1531
|
+
if (presets.length === 0) {
|
|
1532
|
+
vscode.window.showInformationMessage("No presets available yet.");
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const items = presets.map((preset) => ({
|
|
1537
|
+
label: preset.name,
|
|
1538
|
+
description: preset.description,
|
|
1539
|
+
detail: `${preset.buttons.length} buttons · Created ${preset.metadata?.created.toLocaleDateString()}`,
|
|
1540
|
+
preset,
|
|
1541
|
+
}));
|
|
1542
|
+
|
|
1543
|
+
const selected = await vscode.window.showQuickPick(items, {
|
|
1544
|
+
placeHolder: "Select a preset to apply",
|
|
1545
|
+
matchOnDescription: true,
|
|
1546
|
+
matchOnDetail: true,
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
if (!selected) {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Ask for application mode
|
|
1554
|
+
const modeItems: vscode.QuickPickItem[] = [
|
|
1555
|
+
{
|
|
1556
|
+
label: "Replace",
|
|
1557
|
+
description: "Replace all current buttons with preset buttons",
|
|
1558
|
+
},
|
|
1559
|
+
{
|
|
1560
|
+
label: "Merge",
|
|
1561
|
+
description:
|
|
1562
|
+
"Merge preset buttons with current buttons (overwrite duplicates)",
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
label: "Append",
|
|
1566
|
+
description: "Add preset buttons to current buttons",
|
|
1567
|
+
},
|
|
1568
|
+
];
|
|
1569
|
+
|
|
1570
|
+
const modeSelected = await vscode.window.showQuickPick(modeItems, {
|
|
1571
|
+
placeHolder: "How should the preset be applied?",
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
if (!modeSelected) {
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
const mode = modeSelected.label.toLowerCase() as
|
|
1579
|
+
| "replace"
|
|
1580
|
+
| "merge"
|
|
1581
|
+
| "append";
|
|
1582
|
+
|
|
1583
|
+
// Show impact preview
|
|
1584
|
+
const impact = this.configManager.getPresetImpact(selected.preset, mode);
|
|
1585
|
+
const confirm = await vscode.window.showWarningMessage(
|
|
1586
|
+
`Apply preset "${selected.preset.name}"?\n\nImpact:\n• ${impact.added} buttons added\n• ${impact.modified} buttons modified\n• ${impact.removed} buttons removed`,
|
|
1587
|
+
{ modal: true },
|
|
1588
|
+
"Yes, Apply",
|
|
1589
|
+
"No",
|
|
1590
|
+
);
|
|
1591
|
+
|
|
1592
|
+
if (confirm !== "Yes, Apply") {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
try {
|
|
1597
|
+
await this.configManager.applyPreset(selected.preset, mode);
|
|
1598
|
+
vscode.window.showInformationMessage(
|
|
1599
|
+
`✅ Preset "${selected.preset.name}" applied successfully!`,
|
|
1600
|
+
);
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
vscode.window.showErrorMessage(`Failed to apply preset: ${error}`);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/**
|
|
1607
|
+
* View all presets
|
|
1608
|
+
*/
|
|
1609
|
+
private async viewAllPresets(): Promise<void> {
|
|
1610
|
+
const presets = this.presetManager.getAllPresets();
|
|
1611
|
+
|
|
1612
|
+
if (presets.length === 0) {
|
|
1613
|
+
vscode.window.showInformationMessage("No presets available yet.");
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
const items = presets.map((preset) => ({
|
|
1618
|
+
label: preset.name,
|
|
1619
|
+
description: `${preset.buttons.length} buttons`,
|
|
1620
|
+
detail: preset.description,
|
|
1621
|
+
buttons: [
|
|
1622
|
+
{
|
|
1623
|
+
iconPath: new vscode.ThemeIcon("edit"),
|
|
1624
|
+
tooltip: "Rename Preset",
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
iconPath: new vscode.ThemeIcon("copy"),
|
|
1628
|
+
tooltip: "Duplicate Preset",
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
iconPath: new vscode.ThemeIcon("trash"),
|
|
1632
|
+
tooltip: "Delete Preset",
|
|
1633
|
+
},
|
|
1634
|
+
],
|
|
1635
|
+
preset,
|
|
1636
|
+
}));
|
|
1637
|
+
|
|
1638
|
+
const selected = await vscode.window.showQuickPick(items, {
|
|
1639
|
+
placeHolder: "Manage Presets",
|
|
1640
|
+
matchOnDescription: true,
|
|
1641
|
+
matchOnDetail: true,
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
if (selected) {
|
|
1645
|
+
// For now, just apply the preset if selected
|
|
1646
|
+
await this.applyPresetCommand();
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* Export preset command
|
|
1652
|
+
*/
|
|
1653
|
+
private async exportPresetCommand(): Promise<void> {
|
|
1654
|
+
const presets = this.presetManager.getAllPresets();
|
|
1655
|
+
|
|
1656
|
+
if (presets.length === 0) {
|
|
1657
|
+
vscode.window.showInformationMessage("No presets available to export.");
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const items = presets.map((preset) => ({
|
|
1662
|
+
label: preset.name,
|
|
1663
|
+
description: `${preset.buttons.length} buttons`,
|
|
1664
|
+
preset,
|
|
1665
|
+
}));
|
|
1666
|
+
|
|
1667
|
+
const selected = await vscode.window.showQuickPick(items, {
|
|
1668
|
+
placeHolder: "Select a preset to export",
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
if (!selected) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
try {
|
|
1676
|
+
await this.presetManager.exportPreset(selected.preset);
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
vscode.window.showErrorMessage(`Failed to export preset: ${error}`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Import preset command
|
|
1684
|
+
*/
|
|
1685
|
+
private async importPresetCommand(): Promise<void> {
|
|
1686
|
+
try {
|
|
1687
|
+
const preset = await this.presetManager.importPreset();
|
|
1688
|
+
if (preset) {
|
|
1689
|
+
// Optionally ask if they want to apply it immediately
|
|
1690
|
+
const apply = await vscode.window.showInformationMessage(
|
|
1691
|
+
`Preset "${preset.name}" imported. Apply it now?`,
|
|
1692
|
+
"Yes",
|
|
1693
|
+
"No",
|
|
1694
|
+
);
|
|
1695
|
+
|
|
1696
|
+
if (apply === "Yes") {
|
|
1697
|
+
await this.configManager.applyPreset(preset, "replace");
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
vscode.window.showErrorMessage(`Failed to import preset: ${error}`);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* Refresh a button's dynamic label
|
|
1707
|
+
*/
|
|
1708
|
+
private async refreshButtonLabel(buttonId: string): Promise<void> {
|
|
1709
|
+
const buttonState = this.buttonStates.get(buttonId);
|
|
1710
|
+
if (!buttonState || !buttonState.config.dynamicLabel) {
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
try {
|
|
1715
|
+
const newLabel = await this.dynamicLabelManager.evaluateLabel(
|
|
1716
|
+
buttonId,
|
|
1717
|
+
buttonState.config.dynamicLabel,
|
|
1718
|
+
);
|
|
1719
|
+
|
|
1720
|
+
// Update button text with dynamic label
|
|
1721
|
+
if (buttonState.config.icon) {
|
|
1722
|
+
// If icon exists, append label after icon
|
|
1723
|
+
const iconText = this.getButtonDisplayText(buttonState.config);
|
|
1724
|
+
buttonState.item.text = `${iconText} ${newLabel}`;
|
|
1725
|
+
} else {
|
|
1726
|
+
// Replace text entirely
|
|
1727
|
+
buttonState.item.text = newLabel;
|
|
1728
|
+
}
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
console.error(`Failed to refresh label for ${buttonId}:`, error);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/**
|
|
1736
|
+
* Extension activation function
|
|
1737
|
+
*/
|
|
1738
|
+
export function activate(context: vscode.ExtensionContext): void {
|
|
1739
|
+
console.log("Activating StatusBar Quick Actions extension...");
|
|
1740
|
+
|
|
1741
|
+
const extension = new StatusBarQuickActionsExtension(context);
|
|
1742
|
+
context.subscriptions.push({
|
|
1743
|
+
dispose: () => extension.deactivate(),
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
extension.activate();
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
/**
|
|
1750
|
+
* Extension deactivation function
|
|
1751
|
+
*/
|
|
1752
|
+
export function deactivate(): void {
|
|
1753
|
+
console.log("Deactivating StatusBar Quick Actions extension...");
|
|
1754
|
+
}
|