Path: blob/main/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts
5227 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as nls from '../../../../nls.js';6import { IAction } from '../../../../base/common/actions.js';7import { KeyCode } from '../../../../base/common/keyCodes.js';8import * as dom from '../../../../base/browser/dom.js';9import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';10import { SelectBox, ISelectOptionItem, SeparatorSelectOption } from '../../../../base/browser/ui/selectBox/selectBox.js';11import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';12import { ICommandService } from '../../../../platform/commands/common/commands.js';13import { IDebugService, IDebugSession, IDebugConfiguration, IConfig, ILaunch, State } from '../common/debug.js';14import { ThemeIcon } from '../../../../base/common/themables.js';15import { selectBorder, selectBackground, asCssVariable } from '../../../../platform/theme/common/colorRegistry.js';16import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';17import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';18import { IDisposable, dispose } from '../../../../base/common/lifecycle.js';19import { ADD_CONFIGURATION_ID } from './debugCommands.js';20import { BaseActionViewItem, IBaseActionViewItemOptions, SelectActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';21import { debugStart } from './debugIcons.js';22import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';23import { defaultSelectBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';24import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';25import { IHoverService } from '../../../../platform/hover/browser/hover.js';26import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';27import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js';28import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';29import { hasNativeContextMenu } from '../../../../platform/window/common/window.js';30import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js';3132const $ = dom.$;3334export class StartDebugActionViewItem extends BaseActionViewItem {3536private container!: HTMLElement;37private start!: HTMLElement;38private selectBox: SelectBox;39private debugOptions: { label: string; handler: (() => Promise<boolean>) }[] = [];40private toDispose: IDisposable[];41private selected = 0;42private providers: { label: string; type: string; pick: () => Promise<{ launch: ILaunch; config: IConfig } | undefined> }[] = [];4344constructor(45private context: unknown,46action: IAction,47options: IBaseActionViewItemOptions,48@IDebugService private readonly debugService: IDebugService,49@IConfigurationService private readonly configurationService: IConfigurationService,50@ICommandService private readonly commandService: ICommandService,51@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,52@IContextViewService contextViewService: IContextViewService,53@IKeybindingService private readonly keybindingService: IKeybindingService,54@IHoverService private readonly hoverService: IHoverService,55@IContextKeyService private readonly contextKeyService: IContextKeyService56) {57super(context, action, options);58this.toDispose = [];59this.selectBox = new SelectBox([], -1, contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('debugLaunchConfigurations', 'Debug Launch Configurations'), useCustomDrawn: !hasNativeContextMenu(this.configurationService) });60this.selectBox.setFocusable(false);61this.toDispose.push(this.selectBox);6263this.registerListeners();64}6566private registerListeners(): void {67this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => {68if (e.affectsConfiguration('launch')) {69this.updateOptions();70}71}));72this.toDispose.push(this.debugService.getConfigurationManager().onDidSelectConfiguration(() => {73this.updateOptions();74}));75}7677override render(container: HTMLElement): void {78this.container = container;79container.classList.add('start-debug-action-item');80this.start = dom.append(container, $(ThemeIcon.asCSSSelector(debugStart)));81const title = this.keybindingService.appendKeybinding(this.action.label, this.action.id);82this.toDispose.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.start, title));83this.start.setAttribute('role', 'button');84this._setAriaLabel(title);8586this._register(Gesture.addTarget(this.start));87for (const event of [dom.EventType.CLICK, TouchEventType.Tap]) {88this.toDispose.push(dom.addDisposableListener(this.start, event, () => {89this.start.blur();90if (this.debugService.state !== State.Initializing) {91this.actionRunner.run(this.action, this.context);92}93}));94}9596this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => {97if (this.action.enabled && e.button === 0) {98this.start.classList.add('active');99}100}));101this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_UP, () => {102this.start.classList.remove('active');103}));104this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.MOUSE_OUT, () => {105this.start.classList.remove('active');106}));107108this.toDispose.push(dom.addDisposableListener(this.start, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {109const event = new StandardKeyboardEvent(e);110if (event.equals(KeyCode.RightArrow)) {111this.start.tabIndex = -1;112this.selectBox.focus();113event.stopPropagation();114}115}));116this.toDispose.push(this.selectBox.onDidSelect(async e => {117const target = this.debugOptions[e.index];118const shouldBeSelected = target.handler ? await target.handler() : false;119if (shouldBeSelected) {120this.selected = e.index;121} else {122// Some select options should not remain selected https://github.com/microsoft/vscode/issues/31526123this.selectBox.select(this.selected);124}125}));126127const selectBoxContainer = $('.configuration');128this.selectBox.render(dom.append(container, selectBoxContainer));129this.toDispose.push(dom.addDisposableListener(selectBoxContainer, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {130const event = new StandardKeyboardEvent(e);131if (event.equals(KeyCode.LeftArrow)) {132this.selectBox.setFocusable(false);133this.start.tabIndex = 0;134this.start.focus();135event.stopPropagation();136event.preventDefault();137}138}));139this.container.style.border = `1px solid ${asCssVariable(selectBorder)}`;140selectBoxContainer.style.borderLeft = `1px solid ${asCssVariable(selectBorder)}`;141this.container.style.backgroundColor = asCssVariable(selectBackground);142143const configManager = this.debugService.getConfigurationManager();144const updateDynamicConfigs = () => configManager.getDynamicProviders().then(providers => {145if (providers.length !== this.providers.length) {146this.providers = providers;147this.updateOptions();148}149});150151this.toDispose.push(configManager.onDidChangeConfigurationProviders(updateDynamicConfigs));152updateDynamicConfigs();153this.updateOptions();154}155156override setActionContext(context: any): void {157this.context = context;158}159160override isEnabled(): boolean {161return true;162}163164override focus(fromRight?: boolean): void {165if (fromRight) {166this.selectBox.focus();167} else {168this.start.tabIndex = 0;169this.start.focus();170}171}172173override blur(): void {174this.start.tabIndex = -1;175this.selectBox.blur();176this.container.blur();177}178179override setFocusable(focusable: boolean): void {180if (focusable) {181this.start.tabIndex = 0;182} else {183this.start.tabIndex = -1;184this.selectBox.setFocusable(false);185}186}187188override dispose(): void {189this.toDispose = dispose(this.toDispose);190super.dispose();191}192193private updateOptions(): void {194this.selected = 0;195this.debugOptions = [];196const manager = this.debugService.getConfigurationManager();197const inWorkspace = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;198let lastGroup: string | undefined;199const disabledIdxs: number[] = [];200manager.getAllConfigurations().forEach(({ launch, name, presentation }) => {201if (lastGroup !== presentation?.group) {202lastGroup = presentation?.group;203if (this.debugOptions.length) {204this.debugOptions.push({ label: SeparatorSelectOption.text, handler: () => Promise.resolve(false) });205disabledIdxs.push(this.debugOptions.length - 1);206}207}208if (name === manager.selectedConfiguration.name && launch === manager.selectedConfiguration.launch) {209this.selected = this.debugOptions.length;210}211212const label = inWorkspace ? `${name} (${launch.name})` : name;213this.debugOptions.push({214label, handler: async () => {215await manager.selectConfiguration(launch, name);216return true;217}218});219});220221// Only take 3 elements from the recent dynamic configurations to not clutter the dropdown222manager.getRecentDynamicConfigurations().slice(0, 3).forEach(({ name, type }) => {223if (type === manager.selectedConfiguration.type && manager.selectedConfiguration.name === name) {224this.selected = this.debugOptions.length;225}226this.debugOptions.push({227label: name,228handler: async () => {229await manager.selectConfiguration(undefined, name, undefined, { type });230return true;231}232});233});234235if (this.debugOptions.length === 0) {236this.debugOptions.push({ label: nls.localize('noConfigurations', "No Configurations"), handler: async () => false });237}238239this.debugOptions.push({ label: SeparatorSelectOption.text, handler: () => Promise.resolve(false) });240disabledIdxs.push(this.debugOptions.length - 1);241242this.providers.forEach(p => {243244this.debugOptions.push({245label: `${p.label}...`,246handler: async () => {247const picked = await p.pick();248if (picked) {249await manager.selectConfiguration(picked.launch, picked.config.name, picked.config, { type: p.type });250return true;251}252return false;253}254});255});256257manager.getLaunches().filter(l => !l.hidden).forEach(l => {258const label = inWorkspace ? nls.localize("addConfigTo", "Add Config ({0})...", l.name) : nls.localize('addConfiguration', "Add Configuration...");259this.debugOptions.push({260label, handler: async () => {261await this.commandService.executeCommand(ADD_CONFIGURATION_ID, l.uri.toString());262return false;263}264});265});266267this.selectBox.setOptions(this.debugOptions.map((data, index): ISelectOptionItem => ({ text: data.label, isDisabled: disabledIdxs.indexOf(index) !== -1 })), this.selected);268}269270private _setAriaLabel(title: string): void {271let ariaLabel = title;272let keybinding: string | undefined;273const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Debug);274if (verbose) {275keybinding = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp, this.contextKeyService)?.getLabel() ?? undefined;276}277if (keybinding) {278ariaLabel = nls.localize('commentLabelWithKeybinding', "{0}, use ({1}) for accessibility help", ariaLabel, keybinding);279} else {280ariaLabel = nls.localize('commentLabelWithKeybindingNoKeybinding', "{0}, run the command Open Accessibility Help which is currently not triggerable via keybinding.", ariaLabel);281}282this.start.ariaLabel = ariaLabel;283}284}285286export class FocusSessionActionViewItem extends SelectActionViewItem<IDebugSession> {287constructor(288action: IAction,289session: IDebugSession | undefined,290@IDebugService protected readonly debugService: IDebugService,291@IContextViewService contextViewService: IContextViewService,292@IConfigurationService private readonly configurationService: IConfigurationService293) {294super(null, action, [], -1, contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('debugSession', 'Debug Session'), useCustomDrawn: !hasNativeContextMenu(configurationService) });295296this._register(this.debugService.getViewModel().onDidFocusSession(() => {297const session = this.getSelectedSession();298if (session) {299const index = this.getSessions().indexOf(session);300this.select(index);301}302}));303304this._register(this.debugService.onDidNewSession(session => {305const sessionListeners: IDisposable[] = [];306sessionListeners.push(session.onDidChangeName(() => this.update()));307sessionListeners.push(session.onDidEndAdapter(() => dispose(sessionListeners)));308this.update();309}));310// Apply the same pattern to existing sessions - track listeners for cleanup311this.getSessions().forEach(session => {312const sessionListeners: IDisposable[] = [];313sessionListeners.push(session.onDidChangeName(() => this.update()));314sessionListeners.push(session.onDidEndAdapter(() => dispose(sessionListeners)));315});316this._register(this.debugService.onDidEndSession(() => this.update()));317318const selectedSession = session ? this.mapFocusedSessionToSelected(session) : undefined;319this.update(selectedSession);320}321322protected override getActionContext(_: string, index: number): IDebugSession {323return this.getSessions()[index];324}325326private update(session?: IDebugSession) {327if (!session) {328session = this.getSelectedSession();329}330const sessions = this.getSessions();331const names = sessions.map(s => {332const label = s.getLabel();333if (s.parentSession) {334// Indent child sessions so they look like children335return `\u00A0\u00A0${label}`;336}337338return label;339});340this.setOptions(names.map((data): ISelectOptionItem => ({ text: data })), session ? sessions.indexOf(session) : undefined);341}342343private getSelectedSession(): IDebugSession | undefined {344const session = this.debugService.getViewModel().focusedSession;345return session ? this.mapFocusedSessionToSelected(session) : undefined;346}347348protected getSessions(): ReadonlyArray<IDebugSession> {349const showSubSessions = this.configurationService.getValue<IDebugConfiguration>('debug').showSubSessionsInToolBar;350const sessions = this.debugService.getModel().getSessions();351352return showSubSessions ? sessions : sessions.filter(s => !s.parentSession);353}354355protected mapFocusedSessionToSelected(focusedSession: IDebugSession): IDebugSession {356const showSubSessions = this.configurationService.getValue<IDebugConfiguration>('debug').showSubSessionsInToolBar;357while (focusedSession.parentSession && !showSubSessions) {358focusedSession = focusedSession.parentSession;359}360return focusedSession;361}362}363364365