Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts
5227 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 * as nls from '../../../../nls.js';
7
import { IAction } from '../../../../base/common/actions.js';
8
import { KeyCode } from '../../../../base/common/keyCodes.js';
9
import * as dom from '../../../../base/browser/dom.js';
10
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
11
import { SelectBox, ISelectOptionItem, SeparatorSelectOption } from '../../../../base/browser/ui/selectBox/selectBox.js';
12
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
13
import { ICommandService } from '../../../../platform/commands/common/commands.js';
14
import { IDebugService, IDebugSession, IDebugConfiguration, IConfig, ILaunch, State } from '../common/debug.js';
15
import { ThemeIcon } from '../../../../base/common/themables.js';
16
import { selectBorder, selectBackground, asCssVariable } from '../../../../platform/theme/common/colorRegistry.js';
17
import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
18
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
19
import { IDisposable, dispose } from '../../../../base/common/lifecycle.js';
20
import { ADD_CONFIGURATION_ID } from './debugCommands.js';
21
import { BaseActionViewItem, IBaseActionViewItemOptions, SelectActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
22
import { debugStart } from './debugIcons.js';
23
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
24
import { defaultSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
25
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
26
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
27
import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';
28
import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js';
29
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
30
import { hasNativeContextMenu } from '../../../../platform/window/common/window.js';
31
import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js';
32
33
const $ = dom.$;
34
35
export class StartDebugActionViewItem extends BaseActionViewItem {
36
37
private container!: HTMLElement;
38
private start!: HTMLElement;
39
private selectBox: SelectBox;
40
private debugOptions: { label: string; handler: (() => Promise<boolean>) }[] = [];
41
private toDispose: IDisposable[];
42
private selected = 0;
43
private providers: { label: string; type: string; pick: () => Promise<{ launch: ILaunch; config: IConfig } | undefined> }[] = [];
44
45
constructor(
46
private context: unknown,
47
action: IAction,
48
options: IBaseActionViewItemOptions,
49
@IDebugService private readonly debugService: IDebugService,
50
@IConfigurationService private readonly configurationService: IConfigurationService,
51
@ICommandService private readonly commandService: ICommandService,
52
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
53
@IContextViewService contextViewService: IContextViewService,
54
@IKeybindingService private readonly keybindingService: IKeybindingService,
55
@IHoverService private readonly hoverService: IHoverService,
56
@IContextKeyService private readonly contextKeyService: IContextKeyService
57
) {
58
super(context, action, options);
59
this.toDispose = [];
60
this.selectBox = new SelectBox([], -1, contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('debugLaunchConfigurations', 'Debug Launch Configurations'), useCustomDrawn: !hasNativeContextMenu(this.configurationService) });
61
this.selectBox.setFocusable(false);
62
this.toDispose.push(this.selectBox);
63
64
this.registerListeners();
65
}
66
67
private registerListeners(): void {
68
this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => {
69
if (e.affectsConfiguration('launch')) {
70
this.updateOptions();
71
}
72
}));
73
this.toDispose.push(this.debugService.getConfigurationManager().onDidSelectConfiguration(() => {
74
this.updateOptions();
75
}));
76
}
77
78
override render(container: HTMLElement): void {
79
this.container = container;
80
container.classList.add('start-debug-action-item');
81
this.start = dom.append(container, $(ThemeIcon.asCSSSelector(debugStart)));
82
const title = this.keybindingService.appendKeybinding(this.action.label, this.action.id);
83
this.toDispose.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.start, title));
84
this.start.setAttribute('role', 'button');
85
this._setAriaLabel(title);
86
87
this._register(Gesture.addTarget(this.start));
88
for (const event of [dom.EventType.CLICK, TouchEventType.Tap]) {
89
this.toDispose.push(dom.addDisposableListener(this.start, event, () => {
90
this.start.blur();
91
if (this.debugService.state !== State.Initializing) {
92
this.actionRunner.run(this.action, this.context);
93
}
94
}));
95
}
96
97
this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => {
98
if (this.action.enabled && e.button === 0) {
99
this.start.classList.add('active');
100
}
101
}));
102
this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_UP, () => {
103
this.start.classList.remove('active');
104
}));
105
this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_OUT, () => {
106
this.start.classList.remove('active');
107
}));
108
109
this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
110
const event = new StandardKeyboardEvent(e);
111
if (event.equals(KeyCode.RightArrow)) {
112
this.start.tabIndex = -1;
113
this.selectBox.focus();
114
event.stopPropagation();
115
}
116
}));
117
this.toDispose.push(this.selectBox.onDidSelect(async e => {
118
const target = this.debugOptions[e.index];
119
const shouldBeSelected = target.handler ? await target.handler() : false;
120
if (shouldBeSelected) {
121
this.selected = e.index;
122
} else {
123
// Some select options should not remain selected https://github.com/microsoft/vscode/issues/31526
124
this.selectBox.select(this.selected);
125
}
126
}));
127
128
const selectBoxContainer = $('.configuration');
129
this.selectBox.render(dom.append(container, selectBoxContainer));
130
this.toDispose.push(dom.addDisposableListener(selectBoxContainer, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
131
const event = new StandardKeyboardEvent(e);
132
if (event.equals(KeyCode.LeftArrow)) {
133
this.selectBox.setFocusable(false);
134
this.start.tabIndex = 0;
135
this.start.focus();
136
event.stopPropagation();
137
event.preventDefault();
138
}
139
}));
140
this.container.style.border = `1px solid ${asCssVariable(selectBorder)}`;
141
selectBoxContainer.style.borderLeft = `1px solid ${asCssVariable(selectBorder)}`;
142
this.container.style.backgroundColor = asCssVariable(selectBackground);
143
144
const configManager = this.debugService.getConfigurationManager();
145
const updateDynamicConfigs = () => configManager.getDynamicProviders().then(providers => {
146
if (providers.length !== this.providers.length) {
147
this.providers = providers;
148
this.updateOptions();
149
}
150
});
151
152
this.toDispose.push(configManager.onDidChangeConfigurationProviders(updateDynamicConfigs));
153
updateDynamicConfigs();
154
this.updateOptions();
155
}
156
157
override setActionContext(context: any): void {
158
this.context = context;
159
}
160
161
override isEnabled(): boolean {
162
return true;
163
}
164
165
override focus(fromRight?: boolean): void {
166
if (fromRight) {
167
this.selectBox.focus();
168
} else {
169
this.start.tabIndex = 0;
170
this.start.focus();
171
}
172
}
173
174
override blur(): void {
175
this.start.tabIndex = -1;
176
this.selectBox.blur();
177
this.container.blur();
178
}
179
180
override setFocusable(focusable: boolean): void {
181
if (focusable) {
182
this.start.tabIndex = 0;
183
} else {
184
this.start.tabIndex = -1;
185
this.selectBox.setFocusable(false);
186
}
187
}
188
189
override dispose(): void {
190
this.toDispose = dispose(this.toDispose);
191
super.dispose();
192
}
193
194
private updateOptions(): void {
195
this.selected = 0;
196
this.debugOptions = [];
197
const manager = this.debugService.getConfigurationManager();
198
const inWorkspace = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;
199
let lastGroup: string | undefined;
200
const disabledIdxs: number[] = [];
201
manager.getAllConfigurations().forEach(({ launch, name, presentation }) => {
202
if (lastGroup !== presentation?.group) {
203
lastGroup = presentation?.group;
204
if (this.debugOptions.length) {
205
this.debugOptions.push({ label: SeparatorSelectOption.text, handler: () => Promise.resolve(false) });
206
disabledIdxs.push(this.debugOptions.length - 1);
207
}
208
}
209
if (name === manager.selectedConfiguration.name && launch === manager.selectedConfiguration.launch) {
210
this.selected = this.debugOptions.length;
211
}
212
213
const label = inWorkspace ? `${name} (${launch.name})` : name;
214
this.debugOptions.push({
215
label, handler: async () => {
216
await manager.selectConfiguration(launch, name);
217
return true;
218
}
219
});
220
});
221
222
// Only take 3 elements from the recent dynamic configurations to not clutter the dropdown
223
manager.getRecentDynamicConfigurations().slice(0, 3).forEach(({ name, type }) => {
224
if (type === manager.selectedConfiguration.type && manager.selectedConfiguration.name === name) {
225
this.selected = this.debugOptions.length;
226
}
227
this.debugOptions.push({
228
label: name,
229
handler: async () => {
230
await manager.selectConfiguration(undefined, name, undefined, { type });
231
return true;
232
}
233
});
234
});
235
236
if (this.debugOptions.length === 0) {
237
this.debugOptions.push({ label: nls.localize('noConfigurations', "No Configurations"), handler: async () => false });
238
}
239
240
this.debugOptions.push({ label: SeparatorSelectOption.text, handler: () => Promise.resolve(false) });
241
disabledIdxs.push(this.debugOptions.length - 1);
242
243
this.providers.forEach(p => {
244
245
this.debugOptions.push({
246
label: `${p.label}...`,
247
handler: async () => {
248
const picked = await p.pick();
249
if (picked) {
250
await manager.selectConfiguration(picked.launch, picked.config.name, picked.config, { type: p.type });
251
return true;
252
}
253
return false;
254
}
255
});
256
});
257
258
manager.getLaunches().filter(l => !l.hidden).forEach(l => {
259
const label = inWorkspace ? nls.localize("addConfigTo", "Add Config ({0})...", l.name) : nls.localize('addConfiguration', "Add Configuration...");
260
this.debugOptions.push({
261
label, handler: async () => {
262
await this.commandService.executeCommand(ADD_CONFIGURATION_ID, l.uri.toString());
263
return false;
264
}
265
});
266
});
267
268
this.selectBox.setOptions(this.debugOptions.map((data, index): ISelectOptionItem => ({ text: data.label, isDisabled: disabledIdxs.indexOf(index) !== -1 })), this.selected);
269
}
270
271
private _setAriaLabel(title: string): void {
272
let ariaLabel = title;
273
let keybinding: string | undefined;
274
const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Debug);
275
if (verbose) {
276
keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp, this.contextKeyService)?.getLabel() ?? undefined;
277
}
278
if (keybinding) {
279
ariaLabel = nls.localize('commentLabelWithKeybinding', "{0}, use ({1}) for accessibility help", ariaLabel, keybinding);
280
} else {
281
ariaLabel = nls.localize('commentLabelWithKeybindingNoKeybinding', "{0}, run the command Open Accessibility Help which is currently not triggerable via keybinding.", ariaLabel);
282
}
283
this.start.ariaLabel = ariaLabel;
284
}
285
}
286
287
export class FocusSessionActionViewItem extends SelectActionViewItem<IDebugSession> {
288
constructor(
289
action: IAction,
290
session: IDebugSession | undefined,
291
@IDebugService protected readonly debugService: IDebugService,
292
@IContextViewService contextViewService: IContextViewService,
293
@IConfigurationService private readonly configurationService: IConfigurationService
294
) {
295
super(null, action, [], -1, contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('debugSession', 'Debug Session'), useCustomDrawn: !hasNativeContextMenu(configurationService) });
296
297
this._register(this.debugService.getViewModel().onDidFocusSession(() => {
298
const session = this.getSelectedSession();
299
if (session) {
300
const index = this.getSessions().indexOf(session);
301
this.select(index);
302
}
303
}));
304
305
this._register(this.debugService.onDidNewSession(session => {
306
const sessionListeners: IDisposable[] = [];
307
sessionListeners.push(session.onDidChangeName(() => this.update()));
308
sessionListeners.push(session.onDidEndAdapter(() => dispose(sessionListeners)));
309
this.update();
310
}));
311
// Apply the same pattern to existing sessions - track listeners for cleanup
312
this.getSessions().forEach(session => {
313
const sessionListeners: IDisposable[] = [];
314
sessionListeners.push(session.onDidChangeName(() => this.update()));
315
sessionListeners.push(session.onDidEndAdapter(() => dispose(sessionListeners)));
316
});
317
this._register(this.debugService.onDidEndSession(() => this.update()));
318
319
const selectedSession = session ? this.mapFocusedSessionToSelected(session) : undefined;
320
this.update(selectedSession);
321
}
322
323
protected override getActionContext(_: string, index: number): IDebugSession {
324
return this.getSessions()[index];
325
}
326
327
private update(session?: IDebugSession) {
328
if (!session) {
329
session = this.getSelectedSession();
330
}
331
const sessions = this.getSessions();
332
const names = sessions.map(s => {
333
const label = s.getLabel();
334
if (s.parentSession) {
335
// Indent child sessions so they look like children
336
return `\u00A0\u00A0${label}`;
337
}
338
339
return label;
340
});
341
this.setOptions(names.map((data): ISelectOptionItem => ({ text: data })), session ? sessions.indexOf(session) : undefined);
342
}
343
344
private getSelectedSession(): IDebugSession | undefined {
345
const session = this.debugService.getViewModel().focusedSession;
346
return session ? this.mapFocusedSessionToSelected(session) : undefined;
347
}
348
349
protected getSessions(): ReadonlyArray<IDebugSession> {
350
const showSubSessions = this.configurationService.getValue<IDebugConfiguration>('debug').showSubSessionsInToolBar;
351
const sessions = this.debugService.getModel().getSessions();
352
353
return showSubSessions ? sessions : sessions.filter(s => !s.parentSession);
354
}
355
356
protected mapFocusedSessionToSelected(focusedSession: IDebugSession): IDebugSession {
357
const showSubSessions = this.configurationService.getValue<IDebugConfiguration>('debug').showSubSessionsInToolBar;
358
while (focusedSession.parentSession && !showSubSessions) {
359
focusedSession = focusedSession.parentSession;
360
}
361
return focusedSession;
362
}
363
}
364
365