Path: blob/main/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.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 { isSafari } from '../../../../base/browser/browser.js';6import { BrowserFeatures } from '../../../../base/browser/canIUse.js';7import * as dom from '../../../../base/browser/dom.js';8import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';9import { Action, IAction, Separator, SubmenuAction } from '../../../../base/common/actions.js';10import { distinct } from '../../../../base/common/arrays.js';11import { RunOnceScheduler, timeout } from '../../../../base/common/async.js';12import { memoize } from '../../../../base/common/decorators.js';13import { onUnexpectedError } from '../../../../base/common/errors.js';14import { MarkdownString } from '../../../../base/common/htmlContent.js';15import { dispose, disposeIfDisposable, IDisposable } from '../../../../base/common/lifecycle.js';16import * as env from '../../../../base/common/platform.js';17import severity from '../../../../base/common/severity.js';18import { noBreakWhitespace } from '../../../../base/common/strings.js';19import { ThemeIcon } from '../../../../base/common/themables.js';20import { URI } from '../../../../base/common/uri.js';21import { generateUuid } from '../../../../base/common/uuid.js';22import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js';23import { EditorOption } from '../../../../editor/common/config/editorOptions.js';24import { IPosition } from '../../../../editor/common/core/position.js';25import { Range } from '../../../../editor/common/core/range.js';26import { ILanguageService } from '../../../../editor/common/languages/language.js';27import { GlyphMarginLane, IModelDecorationOptions, IModelDecorationOverviewRulerOptions, IModelDecorationsChangeAccessor, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from '../../../../editor/common/model.js';28import * as nls from '../../../../nls.js';29import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';30import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';31import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';32import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';33import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';34import { ILabelService } from '../../../../platform/label/common/label.js';35import { registerColor } from '../../../../platform/theme/common/colorRegistry.js';36import { registerThemingParticipant, themeColorFromId } from '../../../../platform/theme/common/themeService.js';37import { GutterActionsRegistry } from '../../codeEditor/browser/editorLineNumberMenu.js';38import { getBreakpointMessageAndIcon } from './breakpointsView.js';39import { BreakpointWidget } from './breakpointWidget.js';40import * as icons from './debugIcons.js';41import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, DebuggerString, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, IDebugService, IDebugSession, State } from '../common/debug.js';4243const $ = dom.$;4445interface IBreakpointDecoration {46decorationId: string;47breakpoint: IBreakpoint;48range: Range;49inlineWidget?: InlineBreakpointWidget;50}5152const breakpointHelperDecoration: IModelDecorationOptions = {53description: 'breakpoint-helper-decoration',54glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint),55glyphMargin: { position: GlyphMarginLane.Right },56glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint")),57stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges58};5960export function createBreakpointDecorations(accessor: ServicesAccessor, model: ITextModel, breakpoints: ReadonlyArray<IBreakpoint>, state: State, breakpointsActivated: boolean, showBreakpointsInOverviewRuler: boolean): { range: Range; options: IModelDecorationOptions }[] {61const result: { range: Range; options: IModelDecorationOptions }[] = [];62breakpoints.forEach((breakpoint) => {63if (breakpoint.lineNumber > model.getLineCount()) {64return;65}66const hasOtherBreakpointsOnLine = breakpoints.some(bp => bp !== breakpoint && bp.lineNumber === breakpoint.lineNumber);67const column = model.getLineFirstNonWhitespaceColumn(breakpoint.lineNumber);68const range = model.validateRange(69breakpoint.column ? new Range(breakpoint.lineNumber, breakpoint.column, breakpoint.lineNumber, breakpoint.column + 1)70: new Range(breakpoint.lineNumber, column, breakpoint.lineNumber, column + 1) // Decoration has to have a width #2068871);7273result.push({74options: getBreakpointDecorationOptions(accessor, model, breakpoint, state, breakpointsActivated, showBreakpointsInOverviewRuler, hasOtherBreakpointsOnLine),75range76});77});7879return result;80}8182function getBreakpointDecorationOptions(accessor: ServicesAccessor, model: ITextModel, breakpoint: IBreakpoint, state: State, breakpointsActivated: boolean, showBreakpointsInOverviewRuler: boolean, hasOtherBreakpointsOnLine: boolean): IModelDecorationOptions {83const debugService = accessor.get(IDebugService);84const languageService = accessor.get(ILanguageService);85const labelService = accessor.get(ILabelService);86const { icon, message, showAdapterUnverifiedMessage } = getBreakpointMessageAndIcon(state, breakpointsActivated, breakpoint, labelService, debugService.getModel());87let glyphMarginHoverMessage: MarkdownString | undefined;8889let unverifiedMessage: string | undefined;90if (showAdapterUnverifiedMessage) {91let langId: string | undefined;92unverifiedMessage = debugService.getModel().getSessions().map(s => {93const dbg = debugService.getAdapterManager().getDebugger(s.configuration.type);94const message = dbg?.strings?.[DebuggerString.UnverifiedBreakpoints];95if (message) {96if (!langId) {97// Lazily compute this, only if needed for some debug adapter98langId = languageService.guessLanguageIdByFilepathOrFirstLine(breakpoint.uri) ?? undefined;99}100return langId && dbg.interestedInLanguage(langId) ? message : undefined;101}102103return undefined;104})105.find(messages => !!messages);106}107108if (message) {109glyphMarginHoverMessage = new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true });110if (breakpoint.condition || breakpoint.hitCondition) {111const languageId = model.getLanguageId();112glyphMarginHoverMessage.appendCodeblock(languageId, message);113if (unverifiedMessage) {114glyphMarginHoverMessage.appendMarkdown('$(warning) ' + unverifiedMessage);115}116} else {117glyphMarginHoverMessage.appendText(message);118if (unverifiedMessage) {119glyphMarginHoverMessage.appendMarkdown('\n\n$(warning) ' + unverifiedMessage);120}121}122} else if (unverifiedMessage) {123glyphMarginHoverMessage = new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true }).appendMarkdown(unverifiedMessage);124}125126let overviewRulerDecoration: IModelDecorationOverviewRulerOptions | null = null;127if (showBreakpointsInOverviewRuler) {128overviewRulerDecoration = {129color: themeColorFromId(debugIconBreakpointForeground),130position: OverviewRulerLane.Left131};132}133134const renderInline = breakpoint.column && (hasOtherBreakpointsOnLine || breakpoint.column > model.getLineFirstNonWhitespaceColumn(breakpoint.lineNumber));135return {136description: 'breakpoint-decoration',137glyphMargin: { position: GlyphMarginLane.Right },138glyphMarginClassName: ThemeIcon.asClassName(icon),139glyphMarginHoverMessage,140stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,141before: renderInline ? {142content: noBreakWhitespace,143inlineClassName: `debug-breakpoint-placeholder`,144inlineClassNameAffectsLetterSpacing: true145} : undefined,146overviewRuler: overviewRulerDecoration,147zIndex: 9999148};149}150151type BreakpointsForLine = { lineNumber: number; positions: IPosition[] };152153async function requestBreakpointCandidateLocations(model: ITextModel, lineNumbers: number[], session: IDebugSession): Promise<BreakpointsForLine[]> {154if (!session.capabilities.supportsBreakpointLocationsRequest) {155return [];156}157158return await Promise.all(distinct(lineNumbers, l => l).map(async lineNumber => {159try {160return { lineNumber, positions: await session.breakpointsLocations(model.uri, lineNumber) };161} catch {162return { lineNumber, positions: [] };163}164}));165}166167function createCandidateDecorations(model: ITextModel, breakpointDecorations: IBreakpointDecoration[], lineBreakpoints: BreakpointsForLine[]): { range: Range; options: IModelDecorationOptions; breakpoint: IBreakpoint | undefined }[] {168const result: { range: Range; options: IModelDecorationOptions; breakpoint: IBreakpoint | undefined }[] = [];169for (const { positions, lineNumber } of lineBreakpoints) {170if (positions.length === 0) {171continue;172}173174// Do not render candidates if there is only one, since it is already covered by the line breakpoint175const firstColumn = model.getLineFirstNonWhitespaceColumn(lineNumber);176const lastColumn = model.getLineLastNonWhitespaceColumn(lineNumber);177positions.forEach(p => {178const range = new Range(p.lineNumber, p.column, p.lineNumber, p.column + 1);179if ((p.column <= firstColumn && !breakpointDecorations.some(bp => bp.range.startColumn > firstColumn && bp.range.startLineNumber === p.lineNumber)) || p.column > lastColumn) {180// Do not render candidates on the start of the line if there's no other breakpoint on the line.181return;182}183184const breakpointAtPosition = breakpointDecorations.find(bpd => bpd.range.equalsRange(range));185if (breakpointAtPosition && breakpointAtPosition.inlineWidget) {186// Space already occupied, do not render candidate.187return;188}189result.push({190range,191options: {192description: 'breakpoint-placeholder-decoration',193stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,194before: breakpointAtPosition ? undefined : {195content: noBreakWhitespace,196inlineClassName: `debug-breakpoint-placeholder`,197inlineClassNameAffectsLetterSpacing: true198},199},200breakpoint: breakpointAtPosition ? breakpointAtPosition.breakpoint : undefined201});202});203}204205return result;206}207208export class BreakpointEditorContribution implements IBreakpointEditorContribution {209210private breakpointHintDecoration: string | null = null;211private breakpointWidget: BreakpointWidget | undefined;212private breakpointWidgetVisible!: IContextKey<boolean>;213private toDispose: IDisposable[] = [];214private ignoreDecorationsChangedEvent = false;215private ignoreBreakpointsChangeEvent = false;216private breakpointDecorations: IBreakpointDecoration[] = [];217private candidateDecorations: { decorationId: string; inlineWidget: InlineBreakpointWidget }[] = [];218private setDecorationsScheduler!: RunOnceScheduler;219220constructor(221private readonly editor: ICodeEditor,222@IDebugService private readonly debugService: IDebugService,223@IContextMenuService private readonly contextMenuService: IContextMenuService,224@IInstantiationService private readonly instantiationService: IInstantiationService,225@IContextKeyService contextKeyService: IContextKeyService,226@IDialogService private readonly dialogService: IDialogService,227@IConfigurationService private readonly configurationService: IConfigurationService,228@ILabelService private readonly labelService: ILabelService229) {230this.breakpointWidgetVisible = CONTEXT_BREAKPOINT_WIDGET_VISIBLE.bindTo(contextKeyService);231this.setDecorationsScheduler = new RunOnceScheduler(() => this.setDecorations(), 30);232this.setDecorationsScheduler.schedule();233this.registerListeners();234}235236/**237* Returns context menu actions at the line number if breakpoints can be238* set. This is used by the {@link TestingDecorations} to allow breakpoint239* setting on lines where breakpoint "run" actions are present.240*/241public getContextMenuActionsAtPosition(lineNumber: number, model: ITextModel) {242if (!this.debugService.getAdapterManager().hasEnabledDebuggers()) {243return [];244}245246if (!this.debugService.canSetBreakpointsIn(model)) {247return [];248}249250const breakpoints = this.debugService.getModel().getBreakpoints({ lineNumber, uri: model.uri });251return this.getContextMenuActions(breakpoints, model.uri, lineNumber);252}253254private registerListeners(): void {255this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => {256if (!this.debugService.getAdapterManager().hasEnabledDebuggers()) {257return;258}259260const model = this.editor.getModel();261if (!e.target.position262|| !model263|| e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN264|| e.target.detail.isAfterLines265|| !this.marginFreeFromNonDebugDecorations(e.target.position.lineNumber)266// don't return early if there's a breakpoint267&& !e.target.element?.className.includes('breakpoint')268) {269return;270}271const canSetBreakpoints = this.debugService.canSetBreakpointsIn(model);272const lineNumber = e.target.position.lineNumber;273const uri = model.uri;274275if (e.event.rightButton || (env.isMacintosh && e.event.leftButton && e.event.ctrlKey)) {276// handled by editor gutter context menu277return;278} else {279const breakpoints = this.debugService.getModel().getBreakpoints({ uri, lineNumber });280281if (breakpoints.length) {282const isShiftPressed = e.event.shiftKey;283const enabled = breakpoints.some(bp => bp.enabled);284285if (isShiftPressed) {286breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp));287} else if (!env.isLinux && breakpoints.some(bp => !!bp.condition || !!bp.logMessage || !!bp.hitCondition || !!bp.triggeredBy)) {288// Show the dialog if there is a potential condition to be accidently lost.289// Do not show dialog on linux due to electron issue freezing the mouse #50026290const logPoint = breakpoints.every(bp => !!bp.logMessage);291const breakpointType = logPoint ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint");292293const disabledBreakpointDialogMessage = nls.localize(294'breakpointHasConditionDisabled',295"This {0} has a {1} that will get lost on remove. Consider enabling the {0} instead.",296breakpointType.toLowerCase(),297logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition")298);299const enabledBreakpointDialogMessage = nls.localize(300'breakpointHasConditionEnabled',301"This {0} has a {1} that will get lost on remove. Consider disabling the {0} instead.",302breakpointType.toLowerCase(),303logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition")304);305306await this.dialogService.prompt({307type: severity.Info,308message: enabled ? enabledBreakpointDialogMessage : disabledBreakpointDialogMessage,309buttons: [310{311label: nls.localize({ key: 'removeLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Remove {0}", breakpointType),312run: () => breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId()))313},314{315label: nls.localize('disableLogPoint', "{0} {1}", enabled ? nls.localize({ key: 'disable', comment: ['&& denotes a mnemonic'] }, "&&Disable") : nls.localize({ key: 'enable', comment: ['&& denotes a mnemonic'] }, "&&Enable"), breakpointType),316run: () => breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp))317}318],319cancelButton: true320});321} else {322if (!enabled) {323breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp));324} else {325breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId()));326}327}328} else if (canSetBreakpoints) {329if (e.event.middleButton) {330const action = this.configurationService.getValue<IDebugConfiguration>('debug').gutterMiddleClickAction;331if (action !== 'none') {332let context: BreakpointWidgetContext;333switch (action) {334case 'logpoint':335context = BreakpointWidgetContext.LOG_MESSAGE;336break;337case 'conditionalBreakpoint':338context = BreakpointWidgetContext.CONDITION;339break;340case 'triggeredBreakpoint':341context = BreakpointWidgetContext.TRIGGER_POINT;342}343this.showBreakpointWidget(lineNumber, undefined, context);344}345} else {346this.debugService.addBreakpoints(uri, [{ lineNumber }]);347}348}349}350}));351352if (!(BrowserFeatures.pointerEvents && isSafari)) {353/**354* We disable the hover feature for Safari on iOS as355* 1. Browser hover events are handled specially by the system (it treats first click as hover if there is `:hover` css registered). Below hover behavior will confuse users with inconsistent expeirence.356* 2. When users click on line numbers, the breakpoint hint displays immediately, however it doesn't create the breakpoint unless users click on the left gutter. On a touch screen, it's hard to click on that small area.357*/358this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => {359if (!this.debugService.getAdapterManager().hasEnabledDebuggers()) {360return;361}362363let showBreakpointHintAtLineNumber = -1;364const model = this.editor.getModel();365if (model && e.target.position && (e.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN || e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS) && this.debugService.canSetBreakpointsIn(model) &&366this.marginFreeFromNonDebugDecorations(e.target.position.lineNumber)) {367const data = e.target.detail;368if (!data.isAfterLines) {369showBreakpointHintAtLineNumber = e.target.position.lineNumber;370}371}372this.ensureBreakpointHintDecoration(showBreakpointHintAtLineNumber);373}));374this.toDispose.push(this.editor.onMouseLeave(() => {375this.ensureBreakpointHintDecoration(-1);376}));377}378379380this.toDispose.push(this.editor.onDidChangeModel(async () => {381this.closeBreakpointWidget();382await this.setDecorations();383}));384this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => {385if (!this.ignoreBreakpointsChangeEvent && !this.setDecorationsScheduler.isScheduled()) {386this.setDecorationsScheduler.schedule();387}388}));389this.toDispose.push(this.debugService.onDidChangeState(() => {390// We need to update breakpoint decorations when state changes since the top stack frame and breakpoint decoration might change391if (!this.setDecorationsScheduler.isScheduled()) {392this.setDecorationsScheduler.schedule();393}394}));395this.toDispose.push(this.editor.onDidChangeModelDecorations(() => this.onModelDecorationsChanged()));396this.toDispose.push(this.configurationService.onDidChangeConfiguration(async (e) => {397if (e.affectsConfiguration('debug.showBreakpointsInOverviewRuler') || e.affectsConfiguration('debug.showInlineBreakpointCandidates')) {398await this.setDecorations();399}400}));401}402403private getContextMenuActions(breakpoints: ReadonlyArray<IBreakpoint>, uri: URI, lineNumber: number, column?: number): IAction[] {404const actions: IAction[] = [];405406if (breakpoints.length === 1) {407const breakpointType = breakpoints[0].logMessage ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint");408actions.push(new Action('debug.removeBreakpoint', nls.localize('removeBreakpoint', "Remove {0}", breakpointType), undefined, true, async () => {409await this.debugService.removeBreakpoints(breakpoints[0].getId());410}));411actions.push(new Action(412'workbench.debug.action.editBreakpointAction',413nls.localize('editBreakpoint', "Edit {0}...", breakpointType),414undefined,415true,416() => Promise.resolve(this.showBreakpointWidget(breakpoints[0].lineNumber, breakpoints[0].column))417));418419actions.push(new Action(420`workbench.debug.viewlet.action.toggleBreakpoint`,421breakpoints[0].enabled ? nls.localize('disableBreakpoint', "Disable {0}", breakpointType) : nls.localize('enableBreakpoint', "Enable {0}", breakpointType),422undefined,423true,424() => this.debugService.enableOrDisableBreakpoints(!breakpoints[0].enabled, breakpoints[0])425));426} else if (breakpoints.length > 1) {427const sorted = breakpoints.slice().sort((first, second) => (first.column && second.column) ? first.column - second.column : 1);428actions.push(new SubmenuAction('debug.removeBreakpoints', nls.localize('removeBreakpoints', "Remove Breakpoints"), sorted.map(bp => new Action(429'removeInlineBreakpoint',430bp.column ? nls.localize('removeInlineBreakpointOnColumn', "Remove Inline Breakpoint on Column {0}", bp.column) : nls.localize('removeLineBreakpoint', "Remove Line Breakpoint"),431undefined,432true,433() => this.debugService.removeBreakpoints(bp.getId())434))));435436actions.push(new SubmenuAction('debug.editBreakpoints', nls.localize('editBreakpoints', "Edit Breakpoints"), sorted.map(bp =>437new Action('editBreakpoint',438bp.column ? nls.localize('editInlineBreakpointOnColumn', "Edit Inline Breakpoint on Column {0}", bp.column) : nls.localize('editLineBreakpoint', "Edit Line Breakpoint"),439undefined,440true,441() => Promise.resolve(this.showBreakpointWidget(bp.lineNumber, bp.column))442)443)));444445actions.push(new SubmenuAction('debug.enableDisableBreakpoints', nls.localize('enableDisableBreakpoints', "Enable/Disable Breakpoints"), sorted.map(bp => new Action(446bp.enabled ? 'disableColumnBreakpoint' : 'enableColumnBreakpoint',447bp.enabled ? (bp.column ? nls.localize('disableInlineColumnBreakpoint', "Disable Inline Breakpoint on Column {0}", bp.column) : nls.localize('disableBreakpointOnLine', "Disable Line Breakpoint"))448: (bp.column ? nls.localize('enableBreakpoints', "Enable Inline Breakpoint on Column {0}", bp.column) : nls.localize('enableBreakpointOnLine', "Enable Line Breakpoint")),449undefined,450true,451() => this.debugService.enableOrDisableBreakpoints(!bp.enabled, bp)452))));453} else {454actions.push(new Action(455'addBreakpoint',456nls.localize('addBreakpoint', "Add Breakpoint"),457undefined,458true,459() => this.debugService.addBreakpoints(uri, [{ lineNumber, column }])460));461actions.push(new Action(462'addConditionalBreakpoint',463nls.localize('addConditionalBreakpoint', "Add Conditional Breakpoint..."),464undefined,465true,466() => Promise.resolve(this.showBreakpointWidget(lineNumber, column, BreakpointWidgetContext.CONDITION))467));468actions.push(new Action(469'addLogPoint',470nls.localize('addLogPoint', "Add Logpoint..."),471undefined,472true,473() => Promise.resolve(this.showBreakpointWidget(lineNumber, column, BreakpointWidgetContext.LOG_MESSAGE))474));475actions.push(new Action(476'addTriggeredBreakpoint',477nls.localize('addTriggeredBreakpoint', "Add Triggered Breakpoint..."),478undefined,479true,480() => Promise.resolve(this.showBreakpointWidget(lineNumber, column, BreakpointWidgetContext.TRIGGER_POINT))481));482}483484if (this.debugService.state === State.Stopped) {485actions.push(new Separator());486actions.push(new Action(487'runToLine',488nls.localize('runToLine', "Run to Line"),489undefined,490true,491() => this.debugService.runTo(uri, lineNumber).catch(onUnexpectedError)492));493}494495return actions;496}497498private marginFreeFromNonDebugDecorations(line: number): boolean {499const decorations = this.editor.getLineDecorations(line);500if (decorations) {501for (const { options } of decorations) {502const clz = options.glyphMarginClassName;503if (!clz) {504continue;505}506const hasSomeActionableCodicon = !(clz.includes('codicon-') || clz.startsWith('coverage-deco-')) || clz.includes('codicon-testing-') || clz.includes('codicon-merge-') || clz.includes('codicon-arrow-') || clz.includes('codicon-loading') || clz.includes('codicon-fold') || clz.includes('codicon-gutter-lightbulb') || clz.includes('codicon-lightbulb-sparkle');507if (hasSomeActionableCodicon) {508return false;509}510}511}512513return true;514}515516private ensureBreakpointHintDecoration(showBreakpointHintAtLineNumber: number): void {517this.editor.changeDecorations((accessor) => {518if (this.breakpointHintDecoration) {519accessor.removeDecoration(this.breakpointHintDecoration);520this.breakpointHintDecoration = null;521}522if (showBreakpointHintAtLineNumber !== -1) {523this.breakpointHintDecoration = accessor.addDecoration({524startLineNumber: showBreakpointHintAtLineNumber,525startColumn: 1,526endLineNumber: showBreakpointHintAtLineNumber,527endColumn: 1528}, breakpointHelperDecoration529);530}531});532}533534private async setDecorations(): Promise<void> {535if (!this.editor.hasModel()) {536return;537}538539const setCandidateDecorations = (changeAccessor: IModelDecorationsChangeAccessor, desiredCandidatePositions: BreakpointsForLine[]) => {540const desiredCandidateDecorations = createCandidateDecorations(model, this.breakpointDecorations, desiredCandidatePositions);541const candidateDecorationIds = changeAccessor.deltaDecorations(this.candidateDecorations.map(c => c.decorationId), desiredCandidateDecorations);542this.candidateDecorations.forEach(candidate => {543candidate.inlineWidget.dispose();544});545this.candidateDecorations = candidateDecorationIds.map((decorationId, index) => {546const candidate = desiredCandidateDecorations[index];547// Candidate decoration has a breakpoint attached when a breakpoint is already at that location and we did not yet set a decoration there548// In practice this happens for the first breakpoint that was set on a line549// We could have also rendered this first decoration as part of desiredBreakpointDecorations however at that moment we have no location information550const icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService, this.debugService.getModel()).icon : icons.breakpoint.disabled;551const contextMenuActions = () => this.getContextMenuActions(candidate.breakpoint ? [candidate.breakpoint] : [], activeCodeEditor.getModel().uri, candidate.range.startLineNumber, candidate.range.startColumn);552const inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, ThemeIcon.asClassName(icon), candidate.breakpoint, this.debugService, this.contextMenuService, contextMenuActions);553554return {555decorationId,556inlineWidget557};558});559};560561const activeCodeEditor = this.editor;562const model = activeCodeEditor.getModel();563const breakpoints = this.debugService.getModel().getBreakpoints({ uri: model.uri });564const debugSettings = this.configurationService.getValue<IDebugConfiguration>('debug');565const desiredBreakpointDecorations = this.instantiationService.invokeFunction(accessor => createBreakpointDecorations(accessor, model, breakpoints, this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), debugSettings.showBreakpointsInOverviewRuler));566567// try to set breakpoint location candidates in the same changeDecorations()568// call to avoid flickering, if the DA responds reasonably quickly.569const session = this.debugService.getViewModel().focusedSession;570const desiredCandidatePositions = debugSettings.showInlineBreakpointCandidates && session ? requestBreakpointCandidateLocations(this.editor.getModel(), desiredBreakpointDecorations.map(bp => bp.range.startLineNumber), session) : Promise.resolve([]);571const desiredCandidatePositionsRaced = await Promise.race([desiredCandidatePositions, timeout(500).then(() => undefined)]);572if (desiredCandidatePositionsRaced === undefined) { // the timeout resolved first573desiredCandidatePositions.then(v => activeCodeEditor.changeDecorations(d => setCandidateDecorations(d, v)));574}575576try {577this.ignoreDecorationsChangedEvent = true;578579// Set breakpoint decorations580activeCodeEditor.changeDecorations((changeAccessor) => {581const decorationIds = changeAccessor.deltaDecorations(this.breakpointDecorations.map(bpd => bpd.decorationId), desiredBreakpointDecorations);582this.breakpointDecorations.forEach(bpd => {583bpd.inlineWidget?.dispose();584});585this.breakpointDecorations = decorationIds.map((decorationId, index) => {586let inlineWidget: InlineBreakpointWidget | undefined = undefined;587const breakpoint = breakpoints[index];588if (desiredBreakpointDecorations[index].options.before) {589const contextMenuActions = () => this.getContextMenuActions([breakpoint], activeCodeEditor.getModel().uri, breakpoint.lineNumber, breakpoint.column);590inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, desiredBreakpointDecorations[index].options.glyphMarginClassName, breakpoint, this.debugService, this.contextMenuService, contextMenuActions);591}592593return {594decorationId,595breakpoint,596range: desiredBreakpointDecorations[index].range,597inlineWidget598};599});600601if (desiredCandidatePositionsRaced) {602setCandidateDecorations(changeAccessor, desiredCandidatePositionsRaced);603}604});605} finally {606this.ignoreDecorationsChangedEvent = false;607}608609for (const d of this.breakpointDecorations) {610if (d.inlineWidget) {611this.editor.layoutContentWidget(d.inlineWidget);612}613}614}615616private async onModelDecorationsChanged(): Promise<void> {617if (this.breakpointDecorations.length === 0 || this.ignoreDecorationsChangedEvent || !this.editor.hasModel()) {618// I have no decorations619return;620}621let somethingChanged = false;622const model = this.editor.getModel();623this.breakpointDecorations.forEach(breakpointDecoration => {624if (somethingChanged) {625return;626}627const newBreakpointRange = model.getDecorationRange(breakpointDecoration.decorationId);628if (newBreakpointRange && (!breakpointDecoration.range.equalsRange(newBreakpointRange))) {629somethingChanged = true;630breakpointDecoration.range = newBreakpointRange;631}632});633if (!somethingChanged) {634// nothing to do, my decorations did not change.635return;636}637638const data = new Map<string, IBreakpointUpdateData>();639for (let i = 0, len = this.breakpointDecorations.length; i < len; i++) {640const breakpointDecoration = this.breakpointDecorations[i];641const decorationRange = model.getDecorationRange(breakpointDecoration.decorationId);642// check if the line got deleted.643if (decorationRange) {644// since we know it is collapsed, it cannot grow to multiple lines645if (breakpointDecoration.breakpoint) {646data.set(breakpointDecoration.breakpoint.getId(), {647lineNumber: decorationRange.startLineNumber,648column: breakpointDecoration.breakpoint.column ? decorationRange.startColumn : undefined,649});650}651}652}653654try {655this.ignoreBreakpointsChangeEvent = true;656await this.debugService.updateBreakpoints(model.uri, data, true);657} finally {658this.ignoreBreakpointsChangeEvent = false;659}660}661662// breakpoint widget663showBreakpointWidget(lineNumber: number, column: number | undefined, context?: BreakpointWidgetContext): void {664this.breakpointWidget?.dispose();665666this.breakpointWidget = this.instantiationService.createInstance(BreakpointWidget, this.editor, lineNumber, column, context);667this.breakpointWidget.show({ lineNumber, column: 1 });668this.breakpointWidgetVisible.set(true);669}670671closeBreakpointWidget(): void {672if (this.breakpointWidget) {673this.breakpointWidget.dispose();674this.breakpointWidget = undefined;675this.breakpointWidgetVisible.reset();676this.editor.focus();677}678}679680dispose(): void {681this.breakpointWidget?.dispose();682this.editor.removeDecorations(this.breakpointDecorations.map(bpd => bpd.decorationId));683dispose(this.toDispose);684}685}686687GutterActionsRegistry.registerGutterActionsGenerator(({ lineNumber, editor, accessor }, result) => {688const model = editor.getModel();689const debugService = accessor.get(IDebugService);690if (!model || !debugService.getAdapterManager().hasEnabledDebuggers() || !debugService.canSetBreakpointsIn(model)) {691return;692}693694const breakpointEditorContribution = editor.getContribution<IBreakpointEditorContribution>(BREAKPOINT_EDITOR_CONTRIBUTION_ID);695if (!breakpointEditorContribution) {696return;697}698699const actions = breakpointEditorContribution.getContextMenuActionsAtPosition(lineNumber, model);700701for (const action of actions) {702result.push(action, '2_debug');703}704});705706class InlineBreakpointWidget implements IContentWidget, IDisposable {707708// editor.IContentWidget.allowEditorOverflow709allowEditorOverflow = false;710suppressMouseDown = true;711712private domNode!: HTMLElement;713private range: Range | null;714private toDispose: IDisposable[] = [];715716constructor(717private readonly editor: IActiveCodeEditor,718private readonly decorationId: string,719cssClass: string | null | undefined,720private readonly breakpoint: IBreakpoint | undefined,721private readonly debugService: IDebugService,722private readonly contextMenuService: IContextMenuService,723private readonly getContextMenuActions: () => IAction[]724) {725this.range = this.editor.getModel().getDecorationRange(decorationId);726this.toDispose.push(this.editor.onDidChangeModelDecorations(() => {727const model = this.editor.getModel();728const range = model.getDecorationRange(this.decorationId);729if (this.range && !this.range.equalsRange(range)) {730this.range = range;731this.editor.layoutContentWidget(this);732this.updateSize();733}734}));735this.create(cssClass);736737this.editor.addContentWidget(this);738this.editor.layoutContentWidget(this);739}740741private create(cssClass: string | null | undefined): void {742this.domNode = $('.inline-breakpoint-widget');743if (cssClass) {744this.domNode.classList.add(...cssClass.split(' '));745}746this.toDispose.push(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, async e => {747switch (this.breakpoint?.enabled) {748case undefined:749await this.debugService.addBreakpoints(this.editor.getModel().uri, [{ lineNumber: this.range!.startLineNumber, column: this.range!.startColumn }]);750break;751case true:752await this.debugService.removeBreakpoints(this.breakpoint.getId());753break;754case false:755this.debugService.enableOrDisableBreakpoints(true, this.breakpoint);756break;757}758}));759this.toDispose.push(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, e => {760const event = new StandardMouseEvent(dom.getWindow(this.domNode), e);761const actions = this.getContextMenuActions();762this.contextMenuService.showContextMenu({763getAnchor: () => event,764getActions: () => actions,765getActionsContext: () => this.breakpoint,766onHide: () => disposeIfDisposable(actions)767});768}));769770this.updateSize();771772this.toDispose.push(this.editor.onDidChangeConfiguration(c => {773if (c.hasChanged(EditorOption.fontSize) || c.hasChanged(EditorOption.lineHeight)) {774this.updateSize();775}776}));777}778779private updateSize() {780const lineHeight = this.range ? this.editor.getLineHeightForPosition(this.range.getStartPosition()) : this.editor.getOption(EditorOption.lineHeight);781this.domNode.style.height = `${lineHeight}px`;782this.domNode.style.width = `${Math.ceil(0.8 * lineHeight)}px`;783this.domNode.style.marginLeft = `4px`;784}785786@memoize787getId(): string {788return generateUuid();789}790791getDomNode(): HTMLElement {792return this.domNode;793}794795getPosition(): IContentWidgetPosition | null {796if (!this.range) {797return null;798}799// Workaround: since the content widget can not be placed before the first column we need to force the left position800this.domNode.classList.toggle('line-start', this.range.startColumn === 1);801802return {803position: { lineNumber: this.range.startLineNumber, column: this.range.startColumn - 1 },804preference: [ContentWidgetPositionPreference.EXACT]805};806}807808dispose(): void {809this.editor.removeContentWidget(this);810dispose(this.toDispose);811}812}813814registerThemingParticipant((theme, collector) => {815const scope = '.monaco-editor .glyph-margin-widgets, .monaco-workbench .debug-breakpoints, .monaco-workbench .disassembly-view, .monaco-editor .contentWidgets';816const debugIconBreakpointColor = theme.getColor(debugIconBreakpointForeground);817if (debugIconBreakpointColor) {818collector.addRule(`${scope} {819${icons.allBreakpoints.map(b => `${ThemeIcon.asCSSSelector(b.regular)}`).join(',\n ')},820${ThemeIcon.asCSSSelector(icons.debugBreakpointUnsupported)},821${ThemeIcon.asCSSSelector(icons.debugBreakpointHint)}:not([class*='codicon-debug-breakpoint']):not([class*='codicon-debug-stackframe']),822${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after,823${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after {824color: ${debugIconBreakpointColor} !important;825}826}`);827828collector.addRule(`${scope} {829${ThemeIcon.asCSSSelector(icons.breakpoint.pending)} {830color: ${debugIconBreakpointColor} !important;831font-size: 12px !important;832}833}`);834}835836const debugIconBreakpointDisabledColor = theme.getColor(debugIconBreakpointDisabledForeground);837if (debugIconBreakpointDisabledColor) {838collector.addRule(`${scope} {839${icons.allBreakpoints.map(b => ThemeIcon.asCSSSelector(b.disabled)).join(',\n ')} {840color: ${debugIconBreakpointDisabledColor};841}842}`);843}844845const debugIconBreakpointUnverifiedColor = theme.getColor(debugIconBreakpointUnverifiedForeground);846if (debugIconBreakpointUnverifiedColor) {847collector.addRule(`${scope} {848${icons.allBreakpoints.map(b => ThemeIcon.asCSSSelector(b.unverified)).join(',\n ')} {849color: ${debugIconBreakpointUnverifiedColor};850}851}`);852}853854const debugIconBreakpointCurrentStackframeForegroundColor = theme.getColor(debugIconBreakpointCurrentStackframeForeground);855if (debugIconBreakpointCurrentStackframeForegroundColor) {856collector.addRule(`857.monaco-editor .debug-top-stack-frame-column {858color: ${debugIconBreakpointCurrentStackframeForegroundColor} !important;859}860${scope} {861${ThemeIcon.asCSSSelector(icons.debugStackframe)} {862color: ${debugIconBreakpointCurrentStackframeForegroundColor} !important;863}864}865`);866}867868const debugIconBreakpointStackframeFocusedColor = theme.getColor(debugIconBreakpointStackframeForeground);869if (debugIconBreakpointStackframeFocusedColor) {870collector.addRule(`${scope} {871${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)} {872color: ${debugIconBreakpointStackframeFocusedColor} !important;873}874}`);875}876});877878export const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', '#E51400', nls.localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.'));879const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', '#848484', nls.localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.'));880const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', '#848484', nls.localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.'));881const debugIconBreakpointCurrentStackframeForeground = registerColor('debugIcon.breakpointCurrentStackframeForeground', { dark: '#FFCC00', light: '#BE8700', hcDark: '#FFCC00', hcLight: '#BE8700' }, nls.localize('debugIcon.breakpointCurrentStackframeForeground', 'Icon color for the current breakpoint stack frame.'));882const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', '#89D185', nls.localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.'));883884885