Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/actions/developerActions.ts
5238 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/actions.css';
7
8
import { localize, localize2 } from '../../../nls.js';
9
import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js';
10
import { DomEmitter } from '../../../base/browser/event.js';
11
import { Color } from '../../../base/common/color.js';
12
import { Emitter, Event } from '../../../base/common/event.js';
13
import { IDisposable, toDisposable, dispose, DisposableStore, setDisposableTracker, DisposableTracker, DisposableInfo } from '../../../base/common/lifecycle.js';
14
import { getDomNodePagePosition, append, $, getActiveDocument, onDidRegisterWindow, getWindows } from '../../../base/browser/dom.js';
15
import { createCSSRule, createStyleSheet } from '../../../base/browser/domStylesheets.js';
16
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
17
import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../platform/contextkey/common/contextkey.js';
18
import { Context } from '../../../platform/contextkey/browser/contextKeyService.js';
19
import { StandardKeyboardEvent } from '../../../base/browser/keyboardEvent.js';
20
import { RunOnceScheduler } from '../../../base/common/async.js';
21
import { ILayoutService } from '../../../platform/layout/browser/layoutService.js';
22
import { Registry } from '../../../platform/registry/common/platform.js';
23
import { registerAction2, Action2, MenuRegistry } from '../../../platform/actions/common/actions.js';
24
import { IStorageService, StorageScope, StorageTarget } from '../../../platform/storage/common/storage.js';
25
import { clamp } from '../../../base/common/numbers.js';
26
import { KeyCode } from '../../../base/common/keyCodes.js';
27
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../platform/configuration/common/configurationRegistry.js';
28
import { ILogService } from '../../../platform/log/common/log.js';
29
import { IWorkingCopyService } from '../../services/workingCopy/common/workingCopyService.js';
30
import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js';
31
import { Categories } from '../../../platform/action/common/actionCommonCategories.js';
32
import { IWorkingCopyBackupService } from '../../services/workingCopy/common/workingCopyBackup.js';
33
import { ResolutionResult, ResultKind } from '../../../platform/keybinding/common/keybindingResolver.js';
34
import { IDialogService } from '../../../platform/dialogs/common/dialogs.js';
35
import { IOutputService } from '../../services/output/common/output.js';
36
import { windowLogId } from '../../services/log/common/logConstants.js';
37
import { ByteSize } from '../../../platform/files/common/files.js';
38
import { IQuickInputService, IQuickPickItem } from '../../../platform/quickinput/common/quickInput.js';
39
import { IUserDataProfileService } from '../../services/userDataProfile/common/userDataProfile.js';
40
import { IEditorService } from '../../services/editor/common/editorService.js';
41
import product from '../../../platform/product/common/product.js';
42
import { CommandsRegistry } from '../../../platform/commands/common/commands.js';
43
import { IEnvironmentService } from '../../../platform/environment/common/environment.js';
44
import { IProductService } from '../../../platform/product/common/productService.js';
45
import { IDefaultAccountService } from '../../../platform/defaultAccount/common/defaultAccount.js';
46
import { IAuthenticationService } from '../../services/authentication/common/authentication.js';
47
import { IAuthenticationAccessService } from '../../services/authentication/browser/authenticationAccessService.js';
48
import { IPolicyService } from '../../../platform/policy/common/policy.js';
49
50
class InspectContextKeysAction extends Action2 {
51
52
constructor() {
53
super({
54
id: 'workbench.action.inspectContextKeys',
55
title: localize2('inspect context keys', 'Inspect Context Keys'),
56
category: Categories.Developer,
57
f1: true
58
});
59
}
60
61
run(accessor: ServicesAccessor): void {
62
const contextKeyService = accessor.get(IContextKeyService);
63
64
const disposables = new DisposableStore();
65
66
const stylesheet = createStyleSheet(undefined, undefined, disposables);
67
createCSSRule('*', 'cursor: crosshair !important;', stylesheet);
68
69
const hoverFeedback = document.createElement('div');
70
const activeDocument = getActiveDocument();
71
activeDocument.body.appendChild(hoverFeedback);
72
disposables.add(toDisposable(() => hoverFeedback.remove()));
73
74
hoverFeedback.style.position = 'absolute';
75
hoverFeedback.style.pointerEvents = 'none';
76
hoverFeedback.style.backgroundColor = 'rgba(255, 0, 0, 0.5)';
77
hoverFeedback.style.zIndex = '1000';
78
79
const onMouseMove = disposables.add(new DomEmitter(activeDocument, 'mousemove', true));
80
disposables.add(onMouseMove.event(e => {
81
const target = e.target as HTMLElement;
82
const position = getDomNodePagePosition(target);
83
84
hoverFeedback.style.top = `${position.top}px`;
85
hoverFeedback.style.left = `${position.left}px`;
86
hoverFeedback.style.width = `${position.width}px`;
87
hoverFeedback.style.height = `${position.height}px`;
88
}));
89
90
const onMouseDown = disposables.add(new DomEmitter(activeDocument, 'mousedown', true));
91
Event.once(onMouseDown.event)(e => { e.preventDefault(); e.stopPropagation(); }, null, disposables);
92
93
const onMouseUp = disposables.add(new DomEmitter(activeDocument, 'mouseup', true));
94
Event.once(onMouseUp.event)(e => {
95
e.preventDefault();
96
e.stopPropagation();
97
98
const context = contextKeyService.getContext(e.target as HTMLElement) as Context;
99
console.log(context.collectAllValues());
100
101
dispose(disposables);
102
}, null, disposables);
103
}
104
}
105
106
interface IScreencastKeyboardOptions {
107
readonly showKeys?: boolean;
108
readonly showKeybindings?: boolean;
109
readonly showCommands?: boolean;
110
readonly showCommandGroups?: boolean;
111
readonly showSingleEditorCursorMoves?: boolean;
112
}
113
114
class ToggleScreencastModeAction extends Action2 {
115
116
static disposable: IDisposable | undefined;
117
118
constructor() {
119
super({
120
id: 'workbench.action.toggleScreencastMode',
121
title: localize2('toggle screencast mode', 'Toggle Screencast Mode'),
122
category: Categories.Developer,
123
f1: true
124
});
125
}
126
127
run(accessor: ServicesAccessor): void {
128
if (ToggleScreencastModeAction.disposable) {
129
ToggleScreencastModeAction.disposable.dispose();
130
ToggleScreencastModeAction.disposable = undefined;
131
return;
132
}
133
134
const layoutService = accessor.get(ILayoutService);
135
const configurationService = accessor.get(IConfigurationService);
136
const keybindingService = accessor.get(IKeybindingService);
137
138
const disposables = new DisposableStore();
139
140
const container = layoutService.activeContainer;
141
142
const mouseMarker = append(container, $('.screencast-mouse'));
143
disposables.add(toDisposable(() => mouseMarker.remove()));
144
145
const keyboardMarker = append(container, $('.screencast-keyboard'));
146
disposables.add(toDisposable(() => keyboardMarker.remove()));
147
148
const onMouseDown = disposables.add(new Emitter<MouseEvent>());
149
const onMouseUp = disposables.add(new Emitter<MouseEvent>());
150
const onMouseMove = disposables.add(new Emitter<MouseEvent>());
151
152
function registerContainerListeners(container: HTMLElement, windowDisposables: DisposableStore): void {
153
const listeners = new DisposableStore();
154
155
listeners.add(listeners.add(new DomEmitter(container, 'mousedown', true)).event(e => onMouseDown.fire(e)));
156
listeners.add(listeners.add(new DomEmitter(container, 'mouseup', true)).event(e => onMouseUp.fire(e)));
157
listeners.add(listeners.add(new DomEmitter(container, 'mousemove', true)).event(e => onMouseMove.fire(e)));
158
159
windowDisposables.add(listeners);
160
disposables.add(toDisposable(() => windowDisposables.delete(listeners)));
161
162
disposables.add(listeners);
163
}
164
165
for (const { window, disposables } of getWindows()) {
166
registerContainerListeners(layoutService.getContainer(window), disposables);
167
}
168
169
disposables.add(onDidRegisterWindow(({ window, disposables }) => registerContainerListeners(layoutService.getContainer(window), disposables)));
170
171
disposables.add(layoutService.onDidChangeActiveContainer(() => {
172
layoutService.activeContainer.appendChild(mouseMarker);
173
layoutService.activeContainer.appendChild(keyboardMarker);
174
}));
175
176
const updateMouseIndicatorColor = () => {
177
mouseMarker.style.borderColor = Color.fromHex(configurationService.getValue<string>('screencastMode.mouseIndicatorColor')).toString();
178
};
179
180
let mouseIndicatorSize: number;
181
const updateMouseIndicatorSize = () => {
182
mouseIndicatorSize = clamp(configurationService.getValue<number>('screencastMode.mouseIndicatorSize') || 20, 20, 100);
183
184
mouseMarker.style.height = `${mouseIndicatorSize}px`;
185
mouseMarker.style.width = `${mouseIndicatorSize}px`;
186
};
187
188
updateMouseIndicatorColor();
189
updateMouseIndicatorSize();
190
191
disposables.add(onMouseDown.event(e => {
192
mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`;
193
mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`;
194
mouseMarker.style.display = 'block';
195
mouseMarker.style.transform = `scale(${1})`;
196
mouseMarker.style.transition = 'transform 0.1s';
197
198
const mouseMoveListener = onMouseMove.event(e => {
199
mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`;
200
mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`;
201
mouseMarker.style.transform = `scale(${.8})`;
202
});
203
204
Event.once(onMouseUp.event)(() => {
205
mouseMarker.style.display = 'none';
206
mouseMoveListener.dispose();
207
});
208
}));
209
210
const updateKeyboardFontSize = () => {
211
keyboardMarker.style.fontSize = `${clamp(configurationService.getValue<number>('screencastMode.fontSize') || 56, 20, 100)}px`;
212
};
213
214
const updateKeyboardMarker = () => {
215
keyboardMarker.style.bottom = `${clamp(configurationService.getValue<number>('screencastMode.verticalOffset') || 0, 0, 90)}%`;
216
};
217
218
let keyboardMarkerTimeout!: number;
219
const updateKeyboardMarkerTimeout = () => {
220
keyboardMarkerTimeout = clamp(configurationService.getValue<number>('screencastMode.keyboardOverlayTimeout') || 800, 500, 5000);
221
};
222
223
updateKeyboardFontSize();
224
updateKeyboardMarker();
225
updateKeyboardMarkerTimeout();
226
227
disposables.add(configurationService.onDidChangeConfiguration(e => {
228
if (e.affectsConfiguration('screencastMode.verticalOffset')) {
229
updateKeyboardMarker();
230
}
231
232
if (e.affectsConfiguration('screencastMode.fontSize')) {
233
updateKeyboardFontSize();
234
}
235
236
if (e.affectsConfiguration('screencastMode.keyboardOverlayTimeout')) {
237
updateKeyboardMarkerTimeout();
238
}
239
240
if (e.affectsConfiguration('screencastMode.mouseIndicatorColor')) {
241
updateMouseIndicatorColor();
242
}
243
244
if (e.affectsConfiguration('screencastMode.mouseIndicatorSize')) {
245
updateMouseIndicatorSize();
246
}
247
}));
248
249
const onKeyDown = disposables.add(new Emitter<KeyboardEvent>());
250
const onCompositionStart = disposables.add(new Emitter<CompositionEvent>());
251
const onCompositionUpdate = disposables.add(new Emitter<CompositionEvent>());
252
const onCompositionEnd = disposables.add(new Emitter<CompositionEvent>());
253
254
function registerWindowListeners(window: Window, windowDisposables: DisposableStore): void {
255
const listeners = new DisposableStore();
256
257
listeners.add(listeners.add(new DomEmitter(window, 'keydown', true)).event(e => onKeyDown.fire(e)));
258
listeners.add(listeners.add(new DomEmitter(window, 'compositionstart', true)).event(e => onCompositionStart.fire(e)));
259
listeners.add(listeners.add(new DomEmitter(window, 'compositionupdate', true)).event(e => onCompositionUpdate.fire(e)));
260
listeners.add(listeners.add(new DomEmitter(window, 'compositionend', true)).event(e => onCompositionEnd.fire(e)));
261
262
windowDisposables.add(listeners);
263
disposables.add(toDisposable(() => windowDisposables.delete(listeners)));
264
265
disposables.add(listeners);
266
}
267
268
for (const { window, disposables } of getWindows()) {
269
registerWindowListeners(window, disposables);
270
}
271
272
disposables.add(onDidRegisterWindow(({ window, disposables }) => registerWindowListeners(window, disposables)));
273
274
let length = 0;
275
let composing: Element | undefined = undefined;
276
let imeBackSpace = false;
277
278
const clearKeyboardScheduler = disposables.add(new RunOnceScheduler(() => {
279
keyboardMarker.textContent = '';
280
composing = undefined;
281
length = 0;
282
}, keyboardMarkerTimeout));
283
284
disposables.add(onCompositionStart.event(e => {
285
imeBackSpace = true;
286
}));
287
288
disposables.add(onCompositionUpdate.event(e => {
289
if (e.data && imeBackSpace) {
290
if (length > 20) {
291
keyboardMarker.innerText = '';
292
length = 0;
293
}
294
composing = composing ?? append(keyboardMarker, $('span.key'));
295
composing.textContent = e.data;
296
} else if (imeBackSpace) {
297
keyboardMarker.innerText = '';
298
append(keyboardMarker, $('span.key', {}, `Backspace`));
299
}
300
clearKeyboardScheduler.schedule(keyboardMarkerTimeout);
301
}));
302
303
disposables.add(onCompositionEnd.event(e => {
304
composing = undefined;
305
length++;
306
}));
307
308
disposables.add(onKeyDown.event(e => {
309
if (e.key === 'Process' || /[\uac00-\ud787\u3131-\u314e\u314f-\u3163\u3041-\u3094\u30a1-\u30f4\u30fc\u3005\u3006\u3024\u4e00-\u9fa5]/u.test(e.key)) {
310
if (e.code === 'Backspace') {
311
imeBackSpace = true;
312
} else if (!e.code.includes('Key')) {
313
composing = undefined;
314
imeBackSpace = false;
315
} else {
316
imeBackSpace = true;
317
}
318
clearKeyboardScheduler.schedule(keyboardMarkerTimeout);
319
return;
320
}
321
322
if (e.isComposing) {
323
return;
324
}
325
326
const options = configurationService.getValue<IScreencastKeyboardOptions>('screencastMode.keyboardOptions');
327
const event = new StandardKeyboardEvent(e);
328
const shortcut = keybindingService.softDispatch(event, event.target);
329
330
// Hide the single arrow key pressed
331
if (shortcut.kind === ResultKind.KbFound && shortcut.commandId && !(options.showSingleEditorCursorMoves ?? true) && (
332
['cursorLeft', 'cursorRight', 'cursorUp', 'cursorDown'].includes(shortcut.commandId))
333
) {
334
return;
335
}
336
337
if (
338
event.ctrlKey || event.altKey || event.metaKey || event.shiftKey
339
|| length > 20
340
|| event.keyCode === KeyCode.Backspace || event.keyCode === KeyCode.Escape
341
|| event.keyCode === KeyCode.UpArrow || event.keyCode === KeyCode.DownArrow
342
|| event.keyCode === KeyCode.LeftArrow || event.keyCode === KeyCode.RightArrow
343
) {
344
keyboardMarker.innerText = '';
345
length = 0;
346
}
347
348
const keybinding = keybindingService.resolveKeyboardEvent(event);
349
const commandDetails = (this._isKbFound(shortcut) && shortcut.commandId) ? this.getCommandDetails(shortcut.commandId) : undefined;
350
351
let commandAndGroupLabel = commandDetails?.title;
352
let keyLabel: string | undefined | null = keybinding.getLabel();
353
354
if (commandDetails) {
355
if ((options.showCommandGroups ?? false) && commandDetails.category) {
356
commandAndGroupLabel = `${commandDetails.category}: ${commandAndGroupLabel} `;
357
}
358
359
if (this._isKbFound(shortcut) && shortcut.commandId) {
360
const keybindings = keybindingService.lookupKeybindings(shortcut.commandId)
361
.filter(k => k.getLabel()?.endsWith(keyLabel ?? ''));
362
363
if (keybindings.length > 0) {
364
keyLabel = keybindings[keybindings.length - 1].getLabel();
365
}
366
}
367
}
368
369
if ((options.showCommands ?? true) && commandAndGroupLabel) {
370
append(keyboardMarker, $('span.title', {}, `${commandAndGroupLabel} `));
371
}
372
373
if ((options.showKeys ?? true) || ((options.showKeybindings ?? true) && this._isKbFound(shortcut))) {
374
// Fix label for arrow keys
375
keyLabel = keyLabel?.replace('UpArrow', '↑')
376
?.replace('DownArrow', '↓')
377
?.replace('LeftArrow', '←')
378
?.replace('RightArrow', '→');
379
380
append(keyboardMarker, $('span.key', {}, keyLabel ?? ''));
381
}
382
383
length++;
384
clearKeyboardScheduler.schedule(keyboardMarkerTimeout);
385
}));
386
387
ToggleScreencastModeAction.disposable = disposables;
388
}
389
390
private _isKbFound(resolutionResult: ResolutionResult): resolutionResult is { kind: ResultKind.KbFound; commandId: string | null; commandArgs: unknown; isBubble: boolean } {
391
return resolutionResult.kind === ResultKind.KbFound;
392
}
393
394
private getCommandDetails(commandId: string): { title: string; category?: string } | undefined {
395
const fromMenuRegistry = MenuRegistry.getCommand(commandId);
396
397
if (fromMenuRegistry) {
398
return {
399
title: typeof fromMenuRegistry.title === 'string' ? fromMenuRegistry.title : fromMenuRegistry.title.value,
400
category: fromMenuRegistry.category ? (typeof fromMenuRegistry.category === 'string' ? fromMenuRegistry.category : fromMenuRegistry.category.value) : undefined
401
};
402
}
403
404
const fromCommandsRegistry = CommandsRegistry.getCommand(commandId);
405
406
if (fromCommandsRegistry?.metadata?.description) {
407
return { title: typeof fromCommandsRegistry.metadata.description === 'string' ? fromCommandsRegistry.metadata.description : fromCommandsRegistry.metadata.description.value };
408
}
409
410
return undefined;
411
}
412
}
413
414
class LogStorageAction extends Action2 {
415
416
constructor() {
417
super({
418
id: 'workbench.action.logStorage',
419
title: localize2({ key: 'logStorage', comment: ['A developer only action to log the contents of the storage for the current window.'] }, "Log Storage Database Contents"),
420
category: Categories.Developer,
421
f1: true
422
});
423
}
424
425
run(accessor: ServicesAccessor): void {
426
const storageService = accessor.get(IStorageService);
427
const dialogService = accessor.get(IDialogService);
428
429
storageService.log();
430
431
dialogService.info(localize('storageLogDialogMessage', "The storage database contents have been logged to the developer tools."), localize('storageLogDialogDetails', "Open developer tools from the menu and select the Console tab."));
432
}
433
}
434
435
class LogWorkingCopiesAction extends Action2 {
436
437
constructor() {
438
super({
439
id: 'workbench.action.logWorkingCopies',
440
title: localize2({ key: 'logWorkingCopies', comment: ['A developer only action to log the working copies that exist.'] }, "Log Working Copies"),
441
category: Categories.Developer,
442
f1: true
443
});
444
}
445
446
async run(accessor: ServicesAccessor): Promise<void> {
447
const workingCopyService = accessor.get(IWorkingCopyService);
448
const workingCopyBackupService = accessor.get(IWorkingCopyBackupService);
449
const logService = accessor.get(ILogService);
450
const outputService = accessor.get(IOutputService);
451
452
const backups = await workingCopyBackupService.getBackups();
453
454
const msg = [
455
``,
456
`[Working Copies]`,
457
...(workingCopyService.workingCopies.length > 0) ?
458
workingCopyService.workingCopies.map(workingCopy => `${workingCopy.isDirty() ? '● ' : ''}${workingCopy.resource.toString(true)} (typeId: ${workingCopy.typeId || '<no typeId>'})`) :
459
['<none>'],
460
``,
461
`[Backups]`,
462
...(backups.length > 0) ?
463
backups.map(backup => `${backup.resource.toString(true)} (typeId: ${backup.typeId || '<no typeId>'})`) :
464
['<none>'],
465
];
466
467
logService.info(msg.join('\n'));
468
469
outputService.showChannel(windowLogId, true);
470
}
471
}
472
473
class RemoveLargeStorageEntriesAction extends Action2 {
474
475
private static SIZE_THRESHOLD = 1024 * 16; // 16kb
476
477
constructor() {
478
super({
479
id: 'workbench.action.removeLargeStorageDatabaseEntries',
480
title: localize2('removeLargeStorageDatabaseEntries', 'Remove Large Storage Database Entries...'),
481
category: Categories.Developer,
482
f1: true
483
});
484
}
485
486
async run(accessor: ServicesAccessor): Promise<void> {
487
const storageService = accessor.get(IStorageService);
488
const quickInputService = accessor.get(IQuickInputService);
489
const userDataProfileService = accessor.get(IUserDataProfileService);
490
const dialogService = accessor.get(IDialogService);
491
const environmentService = accessor.get(IEnvironmentService);
492
493
interface IStorageItem extends IQuickPickItem {
494
readonly key: string;
495
readonly scope: StorageScope;
496
readonly target: StorageTarget;
497
readonly size: number;
498
}
499
500
const items: IStorageItem[] = [];
501
502
for (const scope of [StorageScope.APPLICATION, StorageScope.PROFILE, StorageScope.WORKSPACE]) {
503
if (scope === StorageScope.PROFILE && userDataProfileService.currentProfile.isDefault) {
504
continue; // avoid duplicates
505
}
506
507
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
508
for (const key of storageService.keys(scope, target)) {
509
const value = storageService.get(key, scope);
510
if (value && (!environmentService.isBuilt /* show all keys in dev */ || value.length > RemoveLargeStorageEntriesAction.SIZE_THRESHOLD)) {
511
items.push({
512
key,
513
scope,
514
target,
515
size: value.length,
516
label: key,
517
description: ByteSize.formatSize(value.length),
518
detail: localize('largeStorageItemDetail', "Scope: {0}, Target: {1}", scope === StorageScope.APPLICATION ? localize('global', "Global") : scope === StorageScope.PROFILE ? localize('profile', "Profile") : localize('workspace', "Workspace"), target === StorageTarget.MACHINE ? localize('machine', "Machine") : localize('user', "User")),
519
});
520
}
521
}
522
}
523
}
524
525
items.sort((itemA, itemB) => itemB.size - itemA.size);
526
527
const selectedItems = await new Promise<readonly IStorageItem[]>(resolve => {
528
const disposables = new DisposableStore();
529
530
const picker = disposables.add(quickInputService.createQuickPick<IStorageItem>());
531
picker.items = items;
532
picker.canSelectMany = true;
533
picker.ok = false;
534
picker.customButton = true;
535
picker.hideCheckAll = true;
536
picker.customLabel = localize('removeLargeStorageEntriesPickerButton', "Remove");
537
picker.placeholder = localize('removeLargeStorageEntriesPickerPlaceholder', "Select large entries to remove from storage");
538
539
if (items.length === 0) {
540
picker.description = localize('removeLargeStorageEntriesPickerDescriptionNoEntries', "There are no large storage entries to remove.");
541
}
542
543
picker.show();
544
545
disposables.add(picker.onDidCustom(() => {
546
resolve(picker.selectedItems);
547
picker.hide();
548
}));
549
550
disposables.add(picker.onDidHide(() => disposables.dispose()));
551
});
552
553
if (selectedItems.length === 0) {
554
return;
555
}
556
557
const { confirmed } = await dialogService.confirm({
558
type: 'warning',
559
message: localize('removeLargeStorageEntriesConfirmRemove', "Do you want to remove the selected storage entries from the database?"),
560
detail: localize('removeLargeStorageEntriesConfirmRemoveDetail', "{0}\n\nThis action is irreversible and may result in data loss!", selectedItems.map(item => item.label).join('\n')),
561
primaryButton: localize({ key: 'removeLargeStorageEntriesButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Remove")
562
});
563
564
if (!confirmed) {
565
return;
566
}
567
568
const scopesToOptimize = new Set<StorageScope>();
569
for (const item of selectedItems) {
570
storageService.remove(item.key, item.scope);
571
scopesToOptimize.add(item.scope);
572
}
573
574
for (const scope of scopesToOptimize) {
575
await storageService.optimize(scope);
576
}
577
}
578
}
579
580
let tracker: DisposableTracker | undefined = undefined;
581
let trackedDisposables = new Set<IDisposable>();
582
583
const DisposablesSnapshotStateContext = new RawContextKey<'started' | 'pending' | 'stopped'>('dirtyWorkingCopies', 'stopped');
584
585
class StartTrackDisposables extends Action2 {
586
587
constructor() {
588
super({
589
id: 'workbench.action.startTrackDisposables',
590
title: localize2('startTrackDisposables', 'Start Tracking Disposables'),
591
category: Categories.Developer,
592
f1: true,
593
precondition: ContextKeyExpr.and(DisposablesSnapshotStateContext.isEqualTo('pending').negate(), DisposablesSnapshotStateContext.isEqualTo('started').negate())
594
});
595
}
596
597
run(accessor: ServicesAccessor): void {
598
const disposablesSnapshotStateContext = DisposablesSnapshotStateContext.bindTo(accessor.get(IContextKeyService));
599
disposablesSnapshotStateContext.set('started');
600
601
trackedDisposables.clear();
602
603
tracker = new DisposableTracker();
604
setDisposableTracker(tracker);
605
}
606
}
607
608
class SnapshotTrackedDisposables extends Action2 {
609
610
constructor() {
611
super({
612
id: 'workbench.action.snapshotTrackedDisposables',
613
title: localize2('snapshotTrackedDisposables', 'Snapshot Tracked Disposables'),
614
category: Categories.Developer,
615
f1: true,
616
precondition: DisposablesSnapshotStateContext.isEqualTo('started')
617
});
618
}
619
620
run(accessor: ServicesAccessor): void {
621
const disposablesSnapshotStateContext = DisposablesSnapshotStateContext.bindTo(accessor.get(IContextKeyService));
622
disposablesSnapshotStateContext.set('pending');
623
624
trackedDisposables = new Set(tracker?.computeLeakingDisposables(1000)?.leaks.map(disposable => disposable.value));
625
}
626
}
627
628
class StopTrackDisposables extends Action2 {
629
630
constructor() {
631
super({
632
id: 'workbench.action.stopTrackDisposables',
633
title: localize2('stopTrackDisposables', 'Stop Tracking Disposables'),
634
category: Categories.Developer,
635
f1: true,
636
precondition: DisposablesSnapshotStateContext.isEqualTo('pending')
637
});
638
}
639
640
run(accessor: ServicesAccessor): void {
641
const editorService = accessor.get(IEditorService);
642
643
const disposablesSnapshotStateContext = DisposablesSnapshotStateContext.bindTo(accessor.get(IContextKeyService));
644
disposablesSnapshotStateContext.set('stopped');
645
646
if (tracker) {
647
const disposableLeaks = new Set<DisposableInfo>();
648
649
for (const disposable of new Set(tracker.computeLeakingDisposables(1000)?.leaks) ?? []) {
650
if (trackedDisposables.has(disposable.value)) {
651
disposableLeaks.add(disposable);
652
}
653
}
654
655
const leaks = tracker.computeLeakingDisposables(1000, Array.from(disposableLeaks));
656
if (leaks) {
657
editorService.openEditor({ resource: undefined, contents: leaks.details });
658
}
659
}
660
661
setDisposableTracker(null);
662
tracker = undefined;
663
trackedDisposables.clear();
664
}
665
}
666
667
class PolicyDiagnosticsAction extends Action2 {
668
669
constructor() {
670
super({
671
id: 'workbench.action.showPolicyDiagnostics',
672
title: localize2('policyDiagnostics', 'Policy Diagnostics'),
673
category: Categories.Developer,
674
f1: true
675
});
676
}
677
678
async run(accessor: ServicesAccessor): Promise<void> {
679
const editorService = accessor.get(IEditorService);
680
const configurationService = accessor.get(IConfigurationService);
681
const productService = accessor.get(IProductService);
682
const defaultAccountService = accessor.get(IDefaultAccountService);
683
const authenticationService = accessor.get(IAuthenticationService);
684
const authenticationAccessService = accessor.get(IAuthenticationAccessService);
685
const policyService = accessor.get(IPolicyService);
686
687
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
688
689
let content = '# VS Code Policy Diagnostics\n\n';
690
content += '*WARNING: This file may contain sensitive information.*\n\n';
691
content += '## System Information\n\n';
692
content += '| Property | Value |\n';
693
content += '|----------|-------|\n';
694
content += `| Generated | ${new Date().toISOString()} |\n`;
695
content += `| Product | ${productService.nameLong} ${productService.version} |\n`;
696
content += `| Commit | ${productService.commit || 'n/a'} |\n\n`;
697
698
// Account information
699
content += '## Account Information\n\n';
700
try {
701
const account = await defaultAccountService.getDefaultAccount();
702
const sensitiveKeys = ['sessionId', 'analytics_tracking_id'];
703
if (account) {
704
// Try to get username/display info from the authentication session
705
let username = 'Unknown';
706
let accountLabel = 'Unknown';
707
try {
708
const providerIds = authenticationService.getProviderIds();
709
for (const providerId of providerIds) {
710
const sessions = await authenticationService.getSessions(providerId);
711
const matchingSession = sessions.find(session => session.id === account.sessionId);
712
if (matchingSession) {
713
username = matchingSession.account.id;
714
accountLabel = matchingSession.account.label;
715
break;
716
}
717
}
718
} catch (error) {
719
// Fallback to just session info
720
}
721
722
content += '### Default Account Summary\n\n';
723
content += `**Account ID/Username**: ${username}\n\n`;
724
content += `**Account Label**: ${accountLabel}\n\n`;
725
726
content += '### Detailed Account Properties\n\n';
727
content += '| Property | Value |\n';
728
content += '|----------|-------|\n';
729
730
// Iterate through all properties of the account object
731
for (const [key, value] of Object.entries(account)) {
732
if (value !== undefined && value !== null) {
733
let displayValue: string;
734
735
// Mask sensitive information
736
if (sensitiveKeys.includes(key)) {
737
displayValue = '***';
738
} else if (typeof value === 'object') {
739
displayValue = JSON.stringify(value);
740
} else {
741
displayValue = String(value);
742
}
743
744
content += `| ${key} | ${displayValue} |\n`;
745
}
746
}
747
const policyData = defaultAccountService.policyData;
748
content += `| policyData | ${policyData ? JSON.stringify(policyData) : 'No Policy Data'} |\n`;
749
content += '\n';
750
} else {
751
content += '*No default account configured*\n\n';
752
}
753
} catch (error) {
754
content += `*Error retrieving account information: ${error}*\n\n`;
755
}
756
757
content += '## Policy-Controlled Settings\n\n';
758
759
const policyConfigurations = configurationRegistry.getPolicyConfigurations();
760
const configurationProperties = configurationRegistry.getConfigurationProperties();
761
const excludedProperties = configurationRegistry.getExcludedConfigurationProperties();
762
763
if (policyConfigurations.size > 0) {
764
// eslint-disable-next-line @typescript-eslint/no-explicit-any
765
const appliedPolicy: Array<{ name: string; key: string; property: any; inspection: any }> = [];
766
// eslint-disable-next-line @typescript-eslint/no-explicit-any
767
const notAppliedPolicy: Array<{ name: string; key: string; property: any; inspection: any }> = [];
768
769
for (const [policyName, settingKey] of policyConfigurations) {
770
const property = configurationProperties[settingKey] ?? excludedProperties[settingKey];
771
if (property) {
772
const inspectValue = configurationService.inspect(settingKey);
773
const settingInfo = {
774
name: policyName,
775
key: settingKey,
776
property,
777
inspection: inspectValue
778
};
779
780
if (inspectValue.policyValue !== undefined) {
781
appliedPolicy.push(settingInfo);
782
} else {
783
notAppliedPolicy.push(settingInfo);
784
}
785
}
786
}
787
788
// Try to detect where the policy came from
789
const policySourceMemo = new Map<string, string>();
790
const getPolicySource = (policyName: string): string => {
791
if (policySourceMemo.has(policyName)) {
792
return policySourceMemo.get(policyName)!;
793
}
794
try {
795
const policyServiceConstructorName = policyService.constructor.name;
796
if (policyServiceConstructorName === 'MultiplexPolicyService') {
797
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
798
const multiplexService = policyService as any;
799
if (multiplexService.policyServices) {
800
// eslint-disable-next-line @typescript-eslint/no-explicit-any
801
const componentServices = multiplexService.policyServices as ReadonlyArray<any>;
802
for (const service of componentServices) {
803
if (service.getPolicyValue && service.getPolicyValue(policyName) !== undefined) {
804
policySourceMemo.set(policyName, service.constructor.name);
805
return service.constructor.name;
806
}
807
}
808
}
809
}
810
return '';
811
} catch {
812
return 'Unknown';
813
}
814
};
815
816
content += '### Applied Policy\n\n';
817
appliedPolicy.sort((a, b) => getPolicySource(a.name).localeCompare(getPolicySource(b.name)) || a.name.localeCompare(b.name));
818
if (appliedPolicy.length > 0) {
819
content += '| Setting Key | Policy Name | Policy Source | Default Value | Current Value | Policy Value |\n';
820
content += '|-------------|-------------|---------------|---------------|---------------|-------------|\n';
821
822
for (const setting of appliedPolicy) {
823
const defaultValue = JSON.stringify(setting.property.default);
824
const currentValue = JSON.stringify(setting.inspection.value);
825
const policyValue = JSON.stringify(setting.inspection.policyValue);
826
const policySource = getPolicySource(setting.name);
827
828
content += `| ${setting.key} | ${setting.name} | ${policySource} | \`${defaultValue}\` | \`${currentValue}\` | \`${policyValue}\` |\n`;
829
}
830
content += '\n';
831
} else {
832
content += '*No settings are currently controlled by policies*\n\n';
833
}
834
835
content += '### Non-applied Policy\n\n';
836
if (notAppliedPolicy.length > 0) {
837
content += '| Setting Key | Policy Name \n';
838
content += '|-------------|-------------|\n';
839
840
for (const setting of notAppliedPolicy) {
841
842
content += `| ${setting.key} | ${setting.name}|\n`;
843
}
844
content += '\n';
845
} else {
846
content += '*All policy-controllable settings are currently being enforced*\n\n';
847
}
848
} else {
849
content += '*No policy-controlled settings found*\n\n';
850
}
851
852
// Authentication diagnostics
853
content += '## Authentication Information\n\n';
854
try {
855
const providerIds = authenticationService.getProviderIds();
856
857
if (providerIds.length > 0) {
858
content += '### Authentication Providers\n\n';
859
content += '| Provider ID | Sessions | Accounts |\n';
860
content += '|-------------|----------|----------|\n';
861
862
for (const providerId of providerIds) {
863
try {
864
const sessions = await authenticationService.getSessions(providerId);
865
const accounts = sessions.map(session => session.account);
866
const uniqueAccounts = Array.from(new Set(accounts.map(account => account.label)));
867
868
content += `| ${providerId} | ${sessions.length} | ${uniqueAccounts.join(', ') || 'None'} |\n`;
869
} catch (error) {
870
content += `| ${providerId} | Error | ${error} |\n`;
871
}
872
}
873
content += '\n';
874
875
// Detailed session information
876
content += '### Detailed Session Information\n\n';
877
for (const providerId of providerIds) {
878
try {
879
const sessions = await authenticationService.getSessions(providerId);
880
881
if (sessions.length > 0) {
882
content += `#### ${providerId}\n\n`;
883
content += '| Account | Scopes | Extensions with Access |\n';
884
content += '|---------|--------|------------------------|\n';
885
886
for (const session of sessions) {
887
const accountName = session.account.label;
888
const scopes = session.scopes.join(', ') || 'Default';
889
890
// Get extensions with access to this account
891
try {
892
const allowedExtensions = authenticationAccessService.readAllowedExtensions(providerId, accountName);
893
const extensionNames = allowedExtensions
894
.filter(ext => ext.allowed !== false)
895
.map(ext => `${ext.name}${ext.trusted ? ' (trusted)' : ''}`)
896
.join(', ') || 'None';
897
898
content += `| ${accountName} | ${scopes} | ${extensionNames} |\n`;
899
} catch (error) {
900
content += `| ${accountName} | ${scopes} | Error: ${error} |\n`;
901
}
902
}
903
content += '\n';
904
}
905
} catch (error) {
906
content += `#### ${providerId}\n*Error retrieving sessions: ${error}*\n\n`;
907
}
908
}
909
} else {
910
content += '*No authentication providers found*\n\n';
911
}
912
} catch (error) {
913
content += `*Error retrieving authentication information: ${error}*\n\n`;
914
}
915
916
await editorService.openEditor({
917
resource: undefined,
918
contents: content,
919
languageId: 'markdown',
920
options: { pinned: true, }
921
});
922
}
923
}
924
925
// --- Actions Registration
926
registerAction2(InspectContextKeysAction);
927
registerAction2(ToggleScreencastModeAction);
928
registerAction2(LogStorageAction);
929
registerAction2(LogWorkingCopiesAction);
930
registerAction2(RemoveLargeStorageEntriesAction);
931
registerAction2(PolicyDiagnosticsAction);
932
if (!product.commit) {
933
registerAction2(StartTrackDisposables);
934
registerAction2(SnapshotTrackedDisposables);
935
registerAction2(StopTrackDisposables);
936
}
937
938
// --- Configuration
939
940
// Screen Cast Mode
941
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
942
configurationRegistry.registerConfiguration({
943
id: 'screencastMode',
944
order: 9,
945
title: localize('screencastModeConfigurationTitle', "Screencast Mode"),
946
type: 'object',
947
properties: {
948
'screencastMode.verticalOffset': {
949
type: 'number',
950
default: 20,
951
minimum: 0,
952
maximum: 90,
953
description: localize('screencastMode.location.verticalPosition', "Controls the vertical offset of the screencast mode overlay from the bottom as a percentage of the workbench height.")
954
},
955
'screencastMode.fontSize': {
956
type: 'number',
957
default: 56,
958
minimum: 20,
959
maximum: 100,
960
description: localize('screencastMode.fontSize', "Controls the font size (in pixels) of the screencast mode keyboard.")
961
},
962
'screencastMode.keyboardOptions': {
963
type: 'object',
964
description: localize('screencastMode.keyboardOptions.description', "Options for customizing the keyboard overlay in screencast mode."),
965
properties: {
966
'showKeys': {
967
type: 'boolean',
968
default: true,
969
description: localize('screencastMode.keyboardOptions.showKeys', "Show raw keys.")
970
},
971
'showKeybindings': {
972
type: 'boolean',
973
default: true,
974
description: localize('screencastMode.keyboardOptions.showKeybindings', "Show keyboard shortcuts.")
975
},
976
'showCommands': {
977
type: 'boolean',
978
default: true,
979
description: localize('screencastMode.keyboardOptions.showCommands', "Show command names.")
980
},
981
'showCommandGroups': {
982
type: 'boolean',
983
default: false,
984
description: localize('screencastMode.keyboardOptions.showCommandGroups', "Show command group names, when commands are also shown.")
985
},
986
'showSingleEditorCursorMoves': {
987
type: 'boolean',
988
default: true,
989
description: localize('screencastMode.keyboardOptions.showSingleEditorCursorMoves', "Show single editor cursor move commands.")
990
}
991
},
992
default: {
993
'showKeys': true,
994
'showKeybindings': true,
995
'showCommands': true,
996
'showCommandGroups': false,
997
'showSingleEditorCursorMoves': true
998
},
999
additionalProperties: false
1000
},
1001
'screencastMode.keyboardOverlayTimeout': {
1002
type: 'number',
1003
default: 800,
1004
minimum: 500,
1005
maximum: 5000,
1006
description: localize('screencastMode.keyboardOverlayTimeout', "Controls how long (in milliseconds) the keyboard overlay is shown in screencast mode.")
1007
},
1008
'screencastMode.mouseIndicatorColor': {
1009
type: 'string',
1010
format: 'color-hex',
1011
default: '#FF0000',
1012
description: localize('screencastMode.mouseIndicatorColor', "Controls the color in hex (#RGB, #RGBA, #RRGGBB or #RRGGBBAA) of the mouse indicator in screencast mode.")
1013
},
1014
'screencastMode.mouseIndicatorSize': {
1015
type: 'number',
1016
default: 20,
1017
minimum: 20,
1018
maximum: 100,
1019
description: localize('screencastMode.mouseIndicatorSize', "Controls the size (in pixels) of the mouse indicator in screencast mode.")
1020
},
1021
}
1022
});
1023
1024