Path: blob/main/src/vs/workbench/contrib/debug/browser/debugToolBar.ts
3296 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 dom from '../../../../base/browser/dom.js';6import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';7import { PixelRatio } from '../../../../base/browser/pixelRatio.js';8import { ActionBar, ActionsOrientation, IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js';9import { IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';10import { CodeWindow, mainWindow } from '../../../../base/browser/window.js';11import { Action, IAction, IRunEvent, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js';12import * as arrays from '../../../../base/common/arrays.js';13import { RunOnceScheduler } from '../../../../base/common/async.js';14import { Codicon } from '../../../../base/common/codicons.js';15import * as errors from '../../../../base/common/errors.js';16import { DisposableStore, markAsSingleton, MutableDisposable } from '../../../../base/common/lifecycle.js';17import { Platform, platform } from '../../../../base/common/platform.js';18import { ThemeIcon } from '../../../../base/common/themables.js';19import { URI } from '../../../../base/common/uri.js';20import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';21import { localize } from '../../../../nls.js';22import { ICommandAction, ICommandActionTitle } from '../../../../platform/action/common/action.js';23import { DropdownWithPrimaryActionViewItem, IDropdownWithPrimaryActionViewItemOptions } from '../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js';24import { createActionViewItem, getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';25import { IMenu, IMenuService, MenuId, MenuItemAction, MenuRegistry } from '../../../../platform/actions/common/actions.js';26import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';27import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';28import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';29import { INotificationService } from '../../../../platform/notification/common/notification.js';30import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';31import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';32import { widgetBorder, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js';33import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';34import { getTitleBarStyle, TitlebarStyle } from '../../../../platform/window/common/window.js';35import { IWorkbenchContribution } from '../../../common/contributions.js';36import { EditorTabsMode, IWorkbenchLayoutService, LayoutSettings, Parts } from '../../../services/layout/browser/layoutService.js';37import { CONTEXT_DEBUG_STATE, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG, CONTEXT_IN_DEBUG_MODE, CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED, IDebugConfiguration, IDebugService, State, VIEWLET_ID } from '../common/debug.js';38import { FocusSessionActionViewItem } from './debugActionViewItems.js';39import { debugToolBarBackground, debugToolBarBorder } from './debugColors.js';40import { CONTINUE_ID, CONTINUE_LABEL, DISCONNECT_AND_SUSPEND_ID, DISCONNECT_AND_SUSPEND_LABEL, DISCONNECT_ID, DISCONNECT_LABEL, FOCUS_SESSION_ID, FOCUS_SESSION_LABEL, PAUSE_ID, PAUSE_LABEL, RESTART_LABEL, RESTART_SESSION_ID, REVERSE_CONTINUE_ID, STEP_BACK_ID, STEP_INTO_ID, STEP_INTO_LABEL, STEP_OUT_ID, STEP_OUT_LABEL, STEP_OVER_ID, STEP_OVER_LABEL, STOP_ID, STOP_LABEL } from './debugCommands.js';41import * as icons from './debugIcons.js';42import './media/debugToolBar.css';4344const DEBUG_TOOLBAR_POSITION_KEY = 'debug.actionswidgetposition';45const DEBUG_TOOLBAR_Y_KEY = 'debug.actionswidgety';4647export class DebugToolBar extends Themable implements IWorkbenchContribution {4849private $el: HTMLElement;50private dragArea: HTMLElement;51private actionBar: ActionBar;52private activeActions: IAction[];53private updateScheduler: RunOnceScheduler;54private debugToolBarMenu: IMenu;5556private isVisible = false;57private isBuilt = false;5859private readonly stopActionViewItemDisposables = this._register(new DisposableStore());60/** coordinate of the debug toolbar per aux window */61private readonly auxWindowCoordinates = new WeakMap<CodeWindow, { x: number; y: number | undefined }>();6263private readonly trackPixelRatioListener = this._register(new MutableDisposable());6465constructor(66@INotificationService private readonly notificationService: INotificationService,67@ITelemetryService private readonly telemetryService: ITelemetryService,68@IDebugService private readonly debugService: IDebugService,69@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,70@IStorageService private readonly storageService: IStorageService,71@IConfigurationService private readonly configurationService: IConfigurationService,72@IThemeService themeService: IThemeService,73@IInstantiationService private readonly instantiationService: IInstantiationService,74@IMenuService menuService: IMenuService,75@IContextKeyService contextKeyService: IContextKeyService,76) {77super(themeService);7879this.$el = dom.$('div.debug-toolbar');8081// Note: changes to this setting require a restart, so no need to listen to it.82const controlsOnTitlebar = getTitleBarStyle(this.configurationService) === TitlebarStyle.CUSTOM;8384// Do not allow the widget to overflow or underflow window controls.85// Use CSS calculations to avoid having to force layout with `.clientWidth`86const controlsOnLeft = controlsOnTitlebar && platform === Platform.Mac;87const controlsOnRight = controlsOnTitlebar && (platform === Platform.Windows || platform === Platform.Linux);88this.$el.style.transform = `translate(89min(90max(${controlsOnLeft ? '60px' : '0px'}, calc(-50% + (100vw * var(--x-position)))),91calc(100vw - 100% - ${controlsOnRight ? '100px' : '0px'})92),93var(--y-position)94)`;9596this.dragArea = dom.append(this.$el, dom.$('div.drag-area' + ThemeIcon.asCSSSelector(icons.debugGripper)));9798const actionBarContainer = dom.append(this.$el, dom.$('div.action-bar-container'));99this.debugToolBarMenu = menuService.createMenu(MenuId.DebugToolBar, contextKeyService);100this._register(this.debugToolBarMenu);101102this.activeActions = [];103this.actionBar = this._register(new ActionBar(actionBarContainer, {104orientation: ActionsOrientation.HORIZONTAL,105actionViewItemProvider: (action: IAction, options: IBaseActionViewItemOptions) => {106if (action.id === FOCUS_SESSION_ID) {107return this.instantiationService.createInstance(FocusSessionActionViewItem, action, undefined);108} else if (action.id === STOP_ID || action.id === DISCONNECT_ID) {109this.stopActionViewItemDisposables.clear();110const item = this.instantiationService.invokeFunction(accessor => createDisconnectMenuItemAction(action as MenuItemAction, this.stopActionViewItemDisposables, accessor, { hoverDelegate: options.hoverDelegate }));111if (item) {112return item;113}114}115116return createActionViewItem(this.instantiationService, action, options);117}118}));119120this.updateScheduler = this._register(new RunOnceScheduler(() => {121const state = this.debugService.state;122const toolBarLocation = this.configurationService.getValue<IDebugConfiguration>('debug').toolBarLocation;123if (124state === State.Inactive ||125toolBarLocation !== 'floating' ||126this.debugService.getModel().getSessions().every(s => s.suppressDebugToolbar) ||127(state === State.Initializing && this.debugService.initializingOptions?.suppressDebugToolbar)128) {129return this.hide();130}131132const actions = getFlatActionBarActions(this.debugToolBarMenu.getActions({ shouldForwardArgs: true }));133if (!arrays.equals(actions, this.activeActions, (first, second) => first.id === second.id && first.enabled === second.enabled)) {134this.actionBar.clear();135this.actionBar.push(actions, { icon: true, label: false });136this.activeActions = actions;137}138139this.show();140}, 20));141142this.updateStyles();143this.registerListeners();144this.hide();145}146147private registerListeners(): void {148this._register(this.debugService.onDidChangeState(() => this.updateScheduler.schedule()));149this._register(this.configurationService.onDidChangeConfiguration(e => {150if (e.affectsConfiguration('debug.toolBarLocation')) {151this.updateScheduler.schedule();152}153if (e.affectsConfiguration(LayoutSettings.EDITOR_TABS_MODE) || e.affectsConfiguration(LayoutSettings.COMMAND_CENTER)) {154this._yRange = undefined;155this.setCoordinates();156}157}));158this._register(this.debugToolBarMenu.onDidChange(() => this.updateScheduler.schedule()));159this._register(this.actionBar.actionRunner.onDidRun((e: IRunEvent) => {160// check for error161if (e.error && !errors.isCancellationError(e.error)) {162this.notificationService.warn(e.error);163}164165// log in telemetry166this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: e.action.id, from: 'debugActionsWidget' });167}));168169this._register(dom.addDisposableGenericMouseUpListener(this.dragArea, (event: MouseEvent) => {170const mouseClickEvent = new StandardMouseEvent(dom.getWindow(this.dragArea), event);171if (mouseClickEvent.detail === 2) {172// double click on debug bar centers it again #8250173this.setCoordinates(0.5, this.yDefault);174this.storePosition();175}176}));177178this._register(dom.addDisposableGenericMouseDownListener(this.dragArea, (e: MouseEvent) => {179this.dragArea.classList.add('dragged');180const activeWindow = dom.getWindow(this.layoutService.activeContainer);181const originEvent = new StandardMouseEvent(activeWindow, e);182183const originX = this.computeCurrentXPercent();184const originY = this.getCurrentYPosition();185186const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(activeWindow, (e: MouseEvent) => {187const mouseMoveEvent = new StandardMouseEvent(activeWindow, e);188// Prevent default to stop editor selecting text #8524189mouseMoveEvent.preventDefault();190this.setCoordinates(191originX + (mouseMoveEvent.posx - originEvent.posx) / activeWindow.innerWidth,192originY + mouseMoveEvent.posy - originEvent.posy,193);194});195196const mouseUpListener = dom.addDisposableGenericMouseUpListener(activeWindow, (e: MouseEvent) => {197this.storePosition();198this.dragArea.classList.remove('dragged');199200mouseMoveListener.dispose();201mouseUpListener.dispose();202});203}));204205this._register(this.layoutService.onDidChangePartVisibility(() => this.setCoordinates()));206207this._register(this.layoutService.onDidChangeActiveContainer(async () => {208this._yRange = undefined;209210// note: we intentionally don't keep the activeContainer before the211// `await` clause to avoid any races due to quickly switching windows.212await this.layoutService.whenContainerStylesLoaded(dom.getWindow(this.layoutService.activeContainer));213if (this.isBuilt) {214this.doShowInActiveContainer();215this.setCoordinates();216}217}));218}219220/**221* Computes the x percent position at which the toolbar is currently displayed.222*/223private computeCurrentXPercent(): number {224const { left, width } = this.$el.getBoundingClientRect();225return (left + width / 2) / dom.getWindow(this.$el).innerWidth;226}227228/**229* Gets the x position set in the style of the toolbar. This may not be its230* actual position on screen depending on toolbar locations.231*/232private getCurrentXPercent(): number {233return Number(this.$el.style.getPropertyValue('--x-position'));234}235236/** Gets the y position set in the style of the toolbar */237private getCurrentYPosition(): number {238return parseInt(this.$el.style.getPropertyValue('--y-position'));239}240241private storePosition(): void {242const activeWindow = dom.getWindow(this.layoutService.activeContainer);243const isMainWindow = this.layoutService.activeContainer === this.layoutService.mainContainer;244245const x = this.getCurrentXPercent();246const y = this.getCurrentYPosition();247if (isMainWindow) {248this.storageService.store(DEBUG_TOOLBAR_POSITION_KEY, x, StorageScope.PROFILE, StorageTarget.MACHINE);249this.storageService.store(DEBUG_TOOLBAR_Y_KEY, y, StorageScope.PROFILE, StorageTarget.MACHINE);250} else {251this.auxWindowCoordinates.set(activeWindow, { x, y });252}253}254255override updateStyles(): void {256super.updateStyles();257258if (this.$el) {259this.$el.style.backgroundColor = this.getColor(debugToolBarBackground) || '';260261const widgetShadowColor = this.getColor(widgetShadow);262this.$el.style.boxShadow = widgetShadowColor ? `0 0 8px 2px ${widgetShadowColor}` : '';263264const contrastBorderColor = this.getColor(widgetBorder);265const borderColor = this.getColor(debugToolBarBorder);266267if (contrastBorderColor) {268this.$el.style.border = `1px solid ${contrastBorderColor}`;269} else {270this.$el.style.border = borderColor ? `solid ${borderColor}` : 'none';271this.$el.style.border = '1px 0';272}273}274}275276/** Gets the stored X position of the middle of the toolbar based on the current window width */277private getStoredXPosition() {278const currentWindow = dom.getWindow(this.layoutService.activeContainer);279const isMainWindow = currentWindow === mainWindow;280const storedPercentage = isMainWindow281? Number(this.storageService.get(DEBUG_TOOLBAR_POSITION_KEY, StorageScope.PROFILE))282: this.auxWindowCoordinates.get(currentWindow)?.x;283return storedPercentage !== undefined && !isNaN(storedPercentage) ? storedPercentage : 0.5;284}285286private getStoredYPosition() {287const currentWindow = dom.getWindow(this.layoutService.activeContainer);288const isMainWindow = currentWindow === mainWindow;289const storedY = isMainWindow290? this.storageService.getNumber(DEBUG_TOOLBAR_Y_KEY, StorageScope.PROFILE)291: this.auxWindowCoordinates.get(currentWindow)?.y;292return storedY ?? this.yDefault;293}294295private setCoordinates(x?: number, y?: number): void {296if (!this.isVisible) {297return;298}299300x ??= this.getStoredXPosition();301y ??= this.getStoredYPosition();302303const [yMin, yMax] = this.yRange;304y = Math.max(yMin, Math.min(y, yMax));305this.$el.style.setProperty('--x-position', `${x}`);306this.$el.style.setProperty('--y-position', `${y}px`);307}308309private get yDefault() {310return this.layoutService.mainContainerOffset.top;311}312313private _yRange: [number, number] | undefined;314private get yRange(): [number, number] {315if (!this._yRange) {316const isTitleBarVisible = this.layoutService.isVisible(Parts.TITLEBAR_PART, dom.getWindow(this.layoutService.activeContainer));317const yMin = isTitleBarVisible ? 0 : this.layoutService.mainContainerOffset.top;318let yMax = 0;319320if (isTitleBarVisible) {321if (this.configurationService.getValue(LayoutSettings.COMMAND_CENTER) === true) {322yMax += 35;323} else {324yMax += 28;325}326}327328if (this.configurationService.getValue(LayoutSettings.EDITOR_TABS_MODE) !== EditorTabsMode.NONE) {329yMax += 35;330}331this._yRange = [yMin, yMax];332}333return this._yRange;334}335336private show(): void {337if (this.isVisible) {338this.setCoordinates();339return;340}341if (!this.isBuilt) {342this.isBuilt = true;343this.doShowInActiveContainer();344}345346this.isVisible = true;347dom.show(this.$el);348this.setCoordinates();349}350351private doShowInActiveContainer(): void {352this.layoutService.activeContainer.appendChild(this.$el);353this.trackPixelRatioListener.value = PixelRatio.getInstance(dom.getWindow(this.$el)).onDidChange(354() => this.setCoordinates()355);356}357358private hide(): void {359this.isVisible = false;360dom.hide(this.$el);361}362363override dispose(): void {364super.dispose();365366this.$el?.remove();367}368}369370export function createDisconnectMenuItemAction(action: MenuItemAction, disposables: DisposableStore, accessor: ServicesAccessor, options: IDropdownWithPrimaryActionViewItemOptions): IActionViewItem | undefined {371const menuService = accessor.get(IMenuService);372const contextKeyService = accessor.get(IContextKeyService);373const instantiationService = accessor.get(IInstantiationService);374375const menu = menuService.getMenuActions(MenuId.DebugToolBarStop, contextKeyService, { shouldForwardArgs: true });376const secondary = getFlatActionBarActions(menu);377378if (!secondary.length) {379return undefined;380}381382const dropdownAction = disposables.add(new Action('notebook.moreRunActions', localize('notebook.moreRunActionsLabel', "More..."), 'codicon-chevron-down', true));383const item = instantiationService.createInstance(DropdownWithPrimaryActionViewItem,384action as MenuItemAction,385dropdownAction,386secondary,387'debug-stop-actions',388options);389return item;390}391392// Debug toolbar393394const debugViewTitleItems = new DisposableStore();395const registerDebugToolBarItem = (id: string, title: string | ICommandActionTitle, order: number, icon?: { light?: URI; dark?: URI } | ThemeIcon, when?: ContextKeyExpression, precondition?: ContextKeyExpression, alt?: ICommandAction) => {396MenuRegistry.appendMenuItem(MenuId.DebugToolBar, {397group: 'navigation',398when,399order,400command: {401id,402title,403icon,404precondition405},406alt407});408409// Register actions in debug viewlet when toolbar is docked410debugViewTitleItems.add(MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, {411group: 'navigation',412when: ContextKeyExpr.and(when, ContextKeyExpr.equals('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'docked')),413order,414command: {415id,416title,417icon,418precondition419}420}));421};422423markAsSingleton(MenuRegistry.onDidChangeMenu(e => {424// In case the debug toolbar is docked we need to make sure that the docked toolbar has the up to date commands registered #115945425if (e.has(MenuId.DebugToolBar)) {426debugViewTitleItems.clear();427const items = MenuRegistry.getMenuItems(MenuId.DebugToolBar);428for (const i of items) {429debugViewTitleItems.add(MenuRegistry.appendMenuItem(MenuId.ViewContainerTitle, {430...i,431when: ContextKeyExpr.and(i.when, ContextKeyExpr.equals('viewContainer', VIEWLET_ID), CONTEXT_DEBUG_STATE.notEqualsTo('inactive'), ContextKeyExpr.equals('config.debug.toolBarLocation', 'docked'))432}));433}434}435}));436437438const CONTEXT_TOOLBAR_COMMAND_CENTER = ContextKeyExpr.equals('config.debug.toolBarLocation', 'commandCenter');439440MenuRegistry.appendMenuItem(MenuId.CommandCenterCenter, {441submenu: MenuId.DebugToolBar,442title: 'Debug',443icon: Codicon.debug,444order: 1,445when: ContextKeyExpr.and(CONTEXT_IN_DEBUG_MODE, CONTEXT_TOOLBAR_COMMAND_CENTER)446});447448registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, icons.debugContinue, CONTEXT_DEBUG_STATE.isEqualTo('stopped'));449registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, icons.debugPause, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), ContextKeyExpr.and(CONTEXT_DEBUG_STATE.isEqualTo('running'), CONTEXT_FOCUSED_SESSION_IS_NO_DEBUG.toNegated()));450registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, icons.debugStop, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), undefined, { id: DISCONNECT_ID, title: DISCONNECT_LABEL, icon: icons.debugDisconnect, precondition: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), });451registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, icons.debugDisconnect, CONTEXT_FOCUSED_SESSION_IS_ATTACH, undefined, { id: STOP_ID, title: STOP_LABEL, icon: icons.debugStop, precondition: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED), });452registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, icons.debugStepOver, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped'));453registerDebugToolBarItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, icons.debugStepInto, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped'));454registerDebugToolBarItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, icons.debugStepOut, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped'));455registerDebugToolBarItem(RESTART_SESSION_ID, RESTART_LABEL, 60, icons.debugRestart);456registerDebugToolBarItem(STEP_BACK_ID, localize('stepBackDebug', "Step Back"), 50, icons.debugStepBack, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped'));457registerDebugToolBarItem(REVERSE_CONTINUE_ID, localize('reverseContinue', "Reverse"), 55, icons.debugReverseContinue, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped'));458registerDebugToolBarItem(FOCUS_SESSION_ID, FOCUS_SESSION_LABEL, 100, Codicon.listTree, ContextKeyExpr.and(CONTEXT_MULTI_SESSION_DEBUG, CONTEXT_TOOLBAR_COMMAND_CENTER.negate()));459460MenuRegistry.appendMenuItem(MenuId.DebugToolBarStop, {461group: 'navigation',462when: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED),463order: 0,464command: {465id: DISCONNECT_ID,466title: DISCONNECT_LABEL,467icon: icons.debugDisconnect468}469});470471MenuRegistry.appendMenuItem(MenuId.DebugToolBarStop, {472group: 'navigation',473when: ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED),474order: 0,475command: {476id: STOP_ID,477title: STOP_LABEL,478icon: icons.debugStop479}480});481482MenuRegistry.appendMenuItem(MenuId.DebugToolBarStop, {483group: 'navigation',484when: ContextKeyExpr.or(485ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated(), CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED, CONTEXT_TERMINATE_DEBUGGEE_SUPPORTED),486ContextKeyExpr.and(CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_SUSPEND_DEBUGGEE_SUPPORTED),487),488order: 0,489command: {490id: DISCONNECT_AND_SUSPEND_ID,491title: DISCONNECT_AND_SUSPEND_LABEL,492icon: icons.debugDisconnect493}494});495496497