-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdecorationProvider.ts
More file actions
316 lines (266 loc) · 9.65 KB
/
decorationProvider.ts
File metadata and controls
316 lines (266 loc) · 9.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
import * as vscode from 'vscode';
import { LocalizationData } from './types';
import { Logger } from './logger';
import { PowerShellExecutor } from './powershellExecutor';
import { ConfigurationManager } from './configuration';
import { POWERSHELL_LANGUAGE_ID } from './utils';
import path from 'path/win32';
import fs from 'fs';
/**
* Provides decorations for PowerShell localization variables in real-time
*/
export class LocalizationDecorationProvider {
private logger: Logger;
private powershellExecutor: PowerShellExecutor;
private localizationCache: Map<string, LocalizationData> = new Map();
private decorationType: vscode.TextEditorDecorationType;
private disposables: vscode.Disposable[] = [];
private timeout: NodeJS.Timeout | undefined;
constructor() {
this.logger = Logger.getInstance();
this.powershellExecutor = new PowerShellExecutor();
// Create decoration type for localization hints
this.decorationType = vscode.window.createTextEditorDecorationType({
after: {
margin: '0 0 0 1rem',
color: new vscode.ThemeColor('editorCodeLens.foreground'),
fontStyle: 'italic',
fontWeight: 'normal'
},
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed
});
this.logger.info('LocalizationDecorationProvider initialized');
}
/**
* Activates the decoration provider
*/
public activate(): void {
// Listen to active editor changes
this.disposables.push(
vscode.window.onDidChangeActiveTextEditor(() => {
this.triggerUpdateDecorations();
})
);
// Listen to document changes
this.disposables.push(
vscode.workspace.onDidChangeTextDocument((event) => {
if (vscode.window.activeTextEditor && event.document === vscode.window.activeTextEditor.document) {
this.triggerUpdateDecorations();
}
})
);
// Update decorations for the currently active editor
this.triggerUpdateDecorations();
}
/**
* Triggers decoration updates with debouncing
*/
private triggerUpdateDecorations(): void {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
this.updateDecorations();
}, 500); // 500ms debounce
}
/**
* Updates decorations for the active editor
*/
private async updateDecorations(): Promise<void> {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
try {
// Check if decorations are enabled
if (!ConfigurationManager.isDecorationEnabled()) {
activeEditor.setDecorations(this.decorationType, []);
return;
}
// Only process PowerShell files
if (activeEditor.document.languageId !== POWERSHELL_LANGUAGE_ID) {
return;
}
this.logger.debug(`Updating decorations for: ${activeEditor.document.uri.fsPath}`);
const localizationData = await this.getLocalizationData(activeEditor.document.uri.fsPath);
if (!localizationData || Object.keys(localizationData).length === 0) {
activeEditor.setDecorations(this.decorationType, []);
return;
}
const decorations = this.createDecorations(activeEditor.document, localizationData);
activeEditor.setDecorations(this.decorationType, decorations);
this.logger.debug(`Applied ${decorations.length} decorations`);
} catch (error) {
this.logger.error('Failed to update decorations', error as Error);
}
}
/**
* Creates decorations for localization variables in the document
*/
private createDecorations(
document: vscode.TextDocument,
localizationData: LocalizationData
): vscode.DecorationOptions[] {
const decorations: vscode.DecorationOptions[] = [];
// Get the binding variable names from localization data
const bindingVariableNames = Object.keys(localizationData);
if (bindingVariableNames.length === 0) {
return [];
}
// Create regex pattern that specifically matches the binding variables
const escapedVarNames = bindingVariableNames.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const bindingVarPattern = `\\$(${escapedVarNames.join('|')})(?:\\.([A-Za-z_][A-Za-z0-9_]*))?`;
const bindingVarRegex = new RegExp(bindingVarPattern, 'g');
for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
const textLine = document.lineAt(lineIndex);
let match: RegExpExecArray | null;
// Reset regex lastIndex for each line
bindingVarRegex.lastIndex = 0;
while ((match = bindingVarRegex.exec(textLine.text)) !== null) {
const varName = match[1];
const propName = match[2];
let value: string | null = null;
let hintText = '';
if (propName) {
// Property access: $bindingVar.key
value = this.getPropertyValue(localizationData, varName, propName);
if (value !== null) {
hintText = `"${value}"`;
}
} else {
// Plain binding variable usage: $bindingVar
const varValue = this.getVariableValue(localizationData, varName);
if (varValue !== null) {
hintText = `{${varValue}}`;
}
}
if (hintText) {
const range = new vscode.Range(
lineIndex,
match.index + match[0].length,
lineIndex,
match.index + match[0].length
);
decorations.push({
range,
renderOptions: {
after: {
contentText: ` // ${hintText}`,
color: new vscode.ThemeColor('editorCodeLens.foreground')
}
}
});
}
}
}
return decorations;
}
/**
* Gets localization data for the given file path, using cache when available
*/
private async getLocalizationData(filePath: string): Promise<LocalizationData | null> {
// Cache key should be the parent directory
const cacheKey = path.dirname(filePath);
// Check cache first
if (this.localizationCache.has(cacheKey)) {
this.logger.debug(`Using cached localization data for: ${cacheKey}`);
return this.localizationCache.get(cacheKey)!;
}
try {
// Find the module file that corresponds to this document
const modulePath = await this.findModuleForFile(filePath);
if (!modulePath) {
this.logger.warn(`No module found for file: ${filePath}`);
return null;
}
const localizationData = await this.powershellExecutor.parseLocalizationData(modulePath, ConfigurationManager.getUICulture());
// Cache the result
this.localizationCache.set(cacheKey, localizationData);
return localizationData;
} catch (error) {
this.logger.error(`Failed to get localization data for ${filePath}`, error as Error);
return null;
}
}
/**
* Finds the module file (.psm1) that corresponds to the given file
*/
private async findModuleForFile(filePath: string): Promise<string | null> {
// For now, use the first .psm1 file found in the workspace
// This could be enhanced to find the specific module that relates to the current file
const psm1Files = await vscode.workspace.findFiles('**/*.psm1');
if (psm1Files.length === 0) {
this.logger.warn(`No PowerShell module files found in workspace for: ${filePath}`);
return null;
}
// Filter to only psm1 with Import-LocalizedData calls
const filteredPsm1Files = psm1Files.filter(file => {
const content = fs.readFileSync(file.fsPath, 'utf8');
return content.includes('Import-LocalizedData');
});
// Return the first module file for now
return filteredPsm1Files[0]?.fsPath || null;
}
/**
* Gets the value of a property from localization data
*/
private getPropertyValue(localizationData: LocalizationData, varName: string, propName: string): string | null {
const variable = localizationData[varName];
if (!variable || typeof variable !== 'object' || variable === null) {
return null;
}
if (Object.prototype.hasOwnProperty.call(variable, propName)) {
return variable[propName];
}
return null;
}
/**
* Gets the value of a variable from localization data (shows all key-value pairs)
*/
private getVariableValue(localizationData: LocalizationData, varName: string): string | null {
const variable = localizationData[varName];
if (!variable || typeof variable !== 'object' || variable === null) {
return null;
}
// Show all key-value pairs for this variable as a string
const entries = Object.entries(variable);
if (entries.length === 0) {
return null;
}
// Limit the display to avoid clutter
const maxEntries = 3;
const displayEntries = entries.slice(0, maxEntries);
const result = displayEntries.map(([k, v]) => `${k}: "${v}"`).join(', ');
if (entries.length > maxEntries) {
return `${result}, ...${entries.length - maxEntries} more`;
}
return result;
}
/**
* Clears the localization cache
*/
public clearCache(): void {
this.localizationCache.clear();
this.logger.debug('Localization cache cleared');
this.triggerUpdateDecorations();
}
/**
* Clears cache for a specific file
*/
public clearCacheForFile(filePath: string): void {
this.localizationCache.delete(filePath);
this.logger.debug(`Cleared cache for file: ${filePath}`);
this.triggerUpdateDecorations();
}
/**
* Disposes of all resources
*/
public dispose(): void {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.disposables.forEach(disposable => disposable.dispose());
this.decorationType.dispose();
this.logger.debug('LocalizationDecorationProvider disposed');
}
}