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
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visibility management for StatusBar Quick Actions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
VisibilityContext,
|
|
7
|
+
VisibilityConfig,
|
|
8
|
+
VisibilityCondition,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import * as vscode from "vscode";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import { DebouncedVisibilityChecker } from "./utils/debounce";
|
|
14
|
+
|
|
15
|
+
export class VisibilityManager {
|
|
16
|
+
private debouncedChecker: DebouncedVisibilityChecker;
|
|
17
|
+
private visibilityCache: Map<string, boolean>;
|
|
18
|
+
|
|
19
|
+
constructor(defaultDebounceMs = 300) {
|
|
20
|
+
this.debouncedChecker = new DebouncedVisibilityChecker(defaultDebounceMs);
|
|
21
|
+
this.visibilityCache = new Map();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if a button should be visible based on current context
|
|
25
|
+
*/
|
|
26
|
+
public isVisible(
|
|
27
|
+
buttonConfig: VisibilityConfig,
|
|
28
|
+
context: VisibilityContext,
|
|
29
|
+
): boolean {
|
|
30
|
+
if (!buttonConfig.conditions || buttonConfig.conditions.length === 0) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// All conditions must be met (AND logic)
|
|
35
|
+
return buttonConfig.conditions.every((condition) => {
|
|
36
|
+
const result = this.evaluateCondition(condition, context);
|
|
37
|
+
// Apply invert logic if specified
|
|
38
|
+
return condition.invert ? !result : result;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Evaluate a single visibility condition
|
|
44
|
+
*/
|
|
45
|
+
private evaluateCondition(
|
|
46
|
+
condition: VisibilityCondition,
|
|
47
|
+
context: VisibilityContext,
|
|
48
|
+
): boolean {
|
|
49
|
+
switch (condition.type) {
|
|
50
|
+
case "fileType":
|
|
51
|
+
return this.checkFileTypeCondition(condition, context);
|
|
52
|
+
case "fileExists":
|
|
53
|
+
return this.checkFileExistsCondition(condition, context);
|
|
54
|
+
case "gitStatus":
|
|
55
|
+
return this.checkGitStatusCondition(condition, context);
|
|
56
|
+
case "workspaceFolder":
|
|
57
|
+
return this.checkWorkspaceFolderCondition(condition, context);
|
|
58
|
+
default:
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get current visibility context
|
|
65
|
+
*/
|
|
66
|
+
public getCurrentContext(): VisibilityContext {
|
|
67
|
+
const activeTextEditor = vscode.window.activeTextEditor;
|
|
68
|
+
const activeFile = activeTextEditor?.document.uri;
|
|
69
|
+
const fileName = activeTextEditor?.document.fileName;
|
|
70
|
+
const extension = activeTextEditor?.document.languageId;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
activeFile,
|
|
74
|
+
activeFileName: fileName,
|
|
75
|
+
activeFileExtension: extension,
|
|
76
|
+
workspaceFolders: [...(vscode.workspace.workspaceFolders || [])],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check file type condition
|
|
82
|
+
*/
|
|
83
|
+
private checkFileTypeCondition(
|
|
84
|
+
condition: VisibilityCondition,
|
|
85
|
+
context: VisibilityContext,
|
|
86
|
+
): boolean {
|
|
87
|
+
if (!condition.patterns || !context.activeFileName) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const fileName = path.basename(context.activeFileName);
|
|
92
|
+
const fileExt = context.activeFileExtension;
|
|
93
|
+
|
|
94
|
+
return condition.patterns.some((pattern: string) => {
|
|
95
|
+
// Check if pattern is a glob pattern
|
|
96
|
+
if (pattern.includes("*") || pattern.includes("?")) {
|
|
97
|
+
return this.simpleGlobMatch(fileName, pattern);
|
|
98
|
+
}
|
|
99
|
+
// Check if pattern is an extension
|
|
100
|
+
else if (pattern.startsWith(".")) {
|
|
101
|
+
return fileName.endsWith(pattern);
|
|
102
|
+
}
|
|
103
|
+
// Check if pattern matches language ID
|
|
104
|
+
else {
|
|
105
|
+
return fileExt === pattern;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check file exists condition
|
|
112
|
+
*/
|
|
113
|
+
private checkFileExistsCondition(
|
|
114
|
+
condition: VisibilityCondition,
|
|
115
|
+
_context: VisibilityContext,
|
|
116
|
+
): boolean {
|
|
117
|
+
if (!condition.path) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
122
|
+
if (!workspaceFolder) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const fullPath = path.isAbsolute(condition.path)
|
|
127
|
+
? condition.path
|
|
128
|
+
: path.join(workspaceFolder, condition.path);
|
|
129
|
+
|
|
130
|
+
return fs.existsSync(fullPath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check git status condition
|
|
135
|
+
*/
|
|
136
|
+
private checkGitStatusCondition(
|
|
137
|
+
condition: VisibilityCondition,
|
|
138
|
+
_context: VisibilityContext,
|
|
139
|
+
): boolean {
|
|
140
|
+
if (!condition.status) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const gitExtension = vscode.extensions.getExtension("vscode.git");
|
|
145
|
+
if (!gitExtension) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const git = gitExtension.exports.getAPI(1);
|
|
150
|
+
const repo = git.repositories[0];
|
|
151
|
+
|
|
152
|
+
if (!repo) {
|
|
153
|
+
return condition.status === "repository" ? false : true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
switch (condition.status) {
|
|
157
|
+
case "repository":
|
|
158
|
+
return true;
|
|
159
|
+
case "clean":
|
|
160
|
+
return (
|
|
161
|
+
repo.state.workingTreeChanges.length === 0 &&
|
|
162
|
+
repo.state.indexChanges.length === 0
|
|
163
|
+
);
|
|
164
|
+
case "dirty":
|
|
165
|
+
return (
|
|
166
|
+
repo.state.workingTreeChanges.length > 0 ||
|
|
167
|
+
repo.state.indexChanges.length > 0
|
|
168
|
+
);
|
|
169
|
+
case "ahead":
|
|
170
|
+
return (repo.state.HEAD?.ahead || 0) > 0;
|
|
171
|
+
case "behind":
|
|
172
|
+
return (repo.state.HEAD?.behind || 0) > 0;
|
|
173
|
+
default:
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Check workspace folder condition
|
|
180
|
+
*/
|
|
181
|
+
private checkWorkspaceFolderCondition(
|
|
182
|
+
condition: VisibilityCondition,
|
|
183
|
+
context: VisibilityContext,
|
|
184
|
+
): boolean {
|
|
185
|
+
if (!condition.folders || !context.workspaceFolders) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return context.workspaceFolders.some((folder) =>
|
|
190
|
+
condition.folders?.includes(folder.name),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check visibility with debouncing
|
|
196
|
+
*/
|
|
197
|
+
public checkVisibilityDebounced(
|
|
198
|
+
buttonId: string,
|
|
199
|
+
buttonConfig: VisibilityConfig,
|
|
200
|
+
customDebounce?: number,
|
|
201
|
+
callback?: (isVisible: boolean) => void,
|
|
202
|
+
): void {
|
|
203
|
+
const checkFn = () => {
|
|
204
|
+
const context = this.getCurrentContext();
|
|
205
|
+
const isVisible = this.isVisible(buttonConfig, context);
|
|
206
|
+
this.visibilityCache.set(buttonId, isVisible);
|
|
207
|
+
if (callback) {
|
|
208
|
+
callback(isVisible);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Use per-button custom debounce if specified, otherwise use global default
|
|
213
|
+
const delay = customDebounce ?? buttonConfig.debounceMs;
|
|
214
|
+
|
|
215
|
+
const debouncedCheck = this.debouncedChecker.getDebouncedCheck(
|
|
216
|
+
buttonId,
|
|
217
|
+
checkFn,
|
|
218
|
+
delay,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
debouncedCheck();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Get cached visibility result for a button
|
|
226
|
+
*/
|
|
227
|
+
public getCachedVisibility(buttonId: string): boolean | undefined {
|
|
228
|
+
return this.visibilityCache.get(buttonId);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Simple glob pattern matching (replaces minimatch for basic patterns)
|
|
233
|
+
*/
|
|
234
|
+
private simpleGlobMatch(text: string, pattern: string): boolean {
|
|
235
|
+
// Convert glob pattern to regex
|
|
236
|
+
let regexPattern = pattern
|
|
237
|
+
.replace(/\./g, "\\.") // Escape dots
|
|
238
|
+
.replace(/\*/g, ".*") // Replace * with .*
|
|
239
|
+
.replace(/\?/g, ".") // Replace ? with .
|
|
240
|
+
.replace(/\+\//g, ".*\\/"); // Handle path separators
|
|
241
|
+
|
|
242
|
+
// Ensure full string match
|
|
243
|
+
regexPattern = `^${regexPattern}$`;
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const regex = new RegExp(regexPattern, "i"); // Case insensitive
|
|
247
|
+
return regex.test(text);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.warn(`Invalid glob pattern: ${pattern}`, error);
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Clear cached visibility for a button
|
|
256
|
+
*/
|
|
257
|
+
public clearCachedVisibility(buttonId: string): void {
|
|
258
|
+
this.visibilityCache.delete(buttonId);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Clear all cached visibility results
|
|
263
|
+
*/
|
|
264
|
+
public clearAllCachedVisibility(): void {
|
|
265
|
+
this.visibilityCache.clear();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Update visibility when editor changes
|
|
270
|
+
*/
|
|
271
|
+
public onEditorChanged(): void {
|
|
272
|
+
// This would trigger re-evaluation of button visibility
|
|
273
|
+
// Implementation would depend on how the main extension handles this
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Dispose of resources
|
|
278
|
+
*/
|
|
279
|
+
public dispose(): void {
|
|
280
|
+
this.debouncedChecker.dispose();
|
|
281
|
+
this.visibilityCache.clear();
|
|
282
|
+
}
|
|
283
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"target": "ES2024",
|
|
5
|
+
"lib": ["ES2024"],
|
|
6
|
+
"outDir": "out",
|
|
7
|
+
"sourceMap": false,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"declaration": false,
|
|
14
|
+
"declarationMap": false,
|
|
15
|
+
"inlineSourceMap": false,
|
|
16
|
+
"removeComments": true
|
|
17
|
+
},
|
|
18
|
+
"exclude": ["node_modules", ".vscode-test"]
|
|
19
|
+
}
|