diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index dcaf8a435aabd..37343ad65c6bb 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -93,6 +93,7 @@ export const Codicon = { closeDirty: register('close-dirty', 0xea71), debugBreakpoint: register('debug-breakpoint', 0xea71), debugBreakpointDisabled: register('debug-breakpoint-disabled', 0xea71), + debugBreakpointPending: register('debug-breakpoint-pending', 0xebd9), debugHint: register('debug-hint', 0xea71), primitiveSquare: register('primitive-square', 0xea72), edit: register('edit', 0xea73), diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index bbeb7e5ecbf2a..67df7ad254b29 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -83,7 +83,8 @@ export function createBreakpointDecorations(accessor: ServicesAccessor, model: I function getBreakpointDecorationOptions(accessor: ServicesAccessor, model: ITextModel, breakpoint: IBreakpoint, state: State, breakpointsActivated: boolean, showBreakpointsInOverviewRuler: boolean, hasOtherBreakpointsOnLine: boolean): IModelDecorationOptions { const debugService = accessor.get(IDebugService); const languageService = accessor.get(ILanguageService); - const { icon, message, showAdapterUnverifiedMessage } = getBreakpointMessageAndIcon(state, breakpointsActivated, breakpoint, undefined); + const labelService = accessor.get(ILabelService); + const { icon, message, showAdapterUnverifiedMessage } = getBreakpointMessageAndIcon(state, breakpointsActivated, breakpoint, labelService, debugService.getModel()); let glyphMarginHoverMessage: MarkdownString | undefined; let unverifiedMessage: string | undefined; @@ -284,7 +285,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi if (isShiftPressed) { breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp)); - } else if (!env.isLinux && breakpoints.some(bp => !!bp.condition || !!bp.logMessage || !!bp.hitCondition)) { + } else if (!env.isLinux && breakpoints.some(bp => !!bp.condition || !!bp.logMessage || !!bp.hitCondition || !!bp.triggeredBy)) { // Show the dialog if there is a potential condition to be accidently lost. // Do not show dialog on linux due to electron issue freezing the mouse #50026 const logPoint = breakpoints.every(bp => !!bp.logMessage); @@ -454,6 +455,13 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi true, () => Promise.resolve(this.showBreakpointWidget(lineNumber, column, BreakpointWidgetContext.LOG_MESSAGE)) )); + actions.push(new Action( + 'addTriggeredBreakpoint', + nls.localize('addTriggeredBreakpoint', "Add Triggered Breakpoint.."), + undefined, + true, + () => Promise.resolve(this.showBreakpointWidget(lineNumber, column, BreakpointWidgetContext.TRIGGER_POINT)) + )); } if (this.debugService.state === State.Stopped) { @@ -522,7 +530,7 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi // Candidate decoration has a breakpoint attached when a breakpoint is already at that location and we did not yet set a decoration there // In practice this happens for the first breakpoint that was set on a line // We could have also rendered this first decoration as part of desiredBreakpointDecorations however at that moment we have no location information - const icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService).icon : icons.breakpoint.disabled; + const icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService, this.debugService.getModel()).icon : icons.breakpoint.disabled; const contextMenuActions = () => this.getContextMenuActions(candidate.breakpoint ? [candidate.breakpoint] : [], activeCodeEditor.getModel().uri, candidate.range.startLineNumber, candidate.range.startColumn); const inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, ThemeIcon.asClassName(icon), candidate.breakpoint, this.debugService, this.contextMenuService, contextMenuActions); @@ -796,6 +804,13 @@ registerThemingParticipant((theme, collector) => { color: ${debugIconBreakpointColor} !important; } `); + + collector.addRule(` + .monaco-workbench ${ThemeIcon.asCSSSelector(icons.breakpoint.pending)} { + color: ${debugIconBreakpointColor} !important; + font-size: 12px !important; + } + `); } const debugIconBreakpointDisabledColor = theme.getColor(debugIconBreakpointDisabledForeground); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 8cfe6b3730c36..205c8c80e2f5c 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -3,43 +3,46 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/breakpointWidget'; -import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { onUnexpectedError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { SelectBox, ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; import * as lifecycle from 'vs/base/common/lifecycle'; -import * as dom from 'vs/base/browser/dom'; -import { Position, IPosition } from 'vs/editor/common/core/position'; -import { ICodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IDebugService, IBreakpoint, BreakpointWidgetContext as Context, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, DEBUG_SCHEME, CONTEXT_IN_BREAKPOINT_WIDGET, IBreakpointUpdateData, IBreakpointEditorContribution, BREAKPOINT_EDITOR_CONTRIBUTION_ID } from 'vs/workbench/contrib/debug/common/debug'; -import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; -import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ServicesAccessor, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { IModelService } from 'vs/editor/common/services/model'; import { URI as uri } from 'vs/base/common/uri'; -import { CompletionList, CompletionContext, CompletionItemKind } from 'vs/editor/common/languages'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { ITextModel } from 'vs/editor/common/model'; -import { provideSuggestionItems, CompletionOptions } from 'vs/editor/contrib/suggest/browser/suggest'; +import 'vs/css!./media/breakpointWidget'; +import { IActiveCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorCommand, ServicesAccessor, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { editorForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IDecorationOptions } from 'vs/editor/common/editorCommon'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { getSimpleEditorOptions, getSimpleCodeEditorWidgetOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IDecorationOptions } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { CompletionContext, CompletionItemKind, CompletionList } from 'vs/editor/common/languages'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { ITextModel } from 'vs/editor/common/model'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IModelService } from 'vs/editor/common/services/model'; +import { CompletionOptions, provideSuggestionItems } from 'vs/editor/contrib/suggest/browser/suggest'; +import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { defaultButtonStyles, defaultSelectBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; +import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, CONTEXT_IN_BREAKPOINT_WIDGET, BreakpointWidgetContext as Context, DEBUG_SCHEME, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugService } from 'vs/workbench/contrib/debug/common/debug'; const $ = dom.$; const IPrivateBreakpointWidgetService = createDecorator('privateBreakpointWidgetService'); @@ -78,6 +81,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi private selectContainer!: HTMLElement; private inputContainer!: HTMLElement; + private selectBreakpointContainer!: HTMLElement; private input!: IActiveCodeEditor; private toDispose: lifecycle.IDisposable[]; private conditionInput = ''; @@ -86,6 +90,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi private breakpoint: IBreakpoint | undefined; private context: Context; private heightInPx: number | undefined; + private triggeredByBreakpointInput: IBreakpoint | undefined; constructor(editor: ICodeEditor, private lineNumber: number, private column: number | undefined, context: Context | undefined, @IContextViewService private readonly contextViewService: IContextViewService, @@ -98,6 +103,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi @IConfigurationService private readonly _configurationService: IConfigurationService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IKeybindingService private readonly keybindingService: IKeybindingService, + @ILabelService private readonly labelService: ILabelService, ) { super(editor, { showFrame: true, showArrow: false, frameWidth: 1, isAccessible: true }); @@ -114,6 +120,8 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.context = Context.LOG_MESSAGE; } else if (this.breakpoint && !this.breakpoint.condition && this.breakpoint.hitCondition) { this.context = Context.HIT_COUNT; + } else if (this.breakpoint && this.breakpoint.triggeredBy) { + this.context = Context.TRIGGER_POINT; } else { this.context = Context.CONDITION; } @@ -156,16 +164,18 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi } private rememberInput(): void { - const value = this.input.getModel().getValue(); - switch (this.context) { - case Context.LOG_MESSAGE: - this.logMessageInput = value; - break; - case Context.HIT_COUNT: - this.hitCountInput = value; - break; - default: - this.conditionInput = value; + if (this.context !== Context.TRIGGER_POINT) { + const value = this.input.getModel().getValue(); + switch (this.context) { + case Context.LOG_MESSAGE: + this.logMessageInput = value; + break; + case Context.HIT_COUNT: + this.hitCountInput = value; + break; + default: + this.conditionInput = value; + } } } @@ -189,17 +199,13 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi protected _fillContainer(container: HTMLElement): void { this.setCssClass('breakpoint-widget'); - const selectBox = new SelectBox([{ text: nls.localize('expression', "Expression") }, { text: nls.localize('hitCount', "Hit Count") }, { text: nls.localize('logMessage', "Log Message") }], this.context, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type') }); + const selectBox = new SelectBox([{ text: nls.localize('expression', "Expression") }, { text: nls.localize('hitCount', "Hit Count") }, { text: nls.localize('logMessage', "Log Message") }, { text: nls.localize('triggeredBy', "Wait For Breakpoint") }], this.context, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('breakpointType', 'Breakpoint Type') }); this.selectContainer = $('.breakpoint-select-container'); selectBox.render(dom.append(container, this.selectContainer)); selectBox.onDidSelect(e => { this.rememberInput(); this.context = e.index; - this.setInputMode(); - - const value = this.getInputValue(this.breakpoint); - this.input.getModel().setValue(value); - this.input.focus(); + this.updateContextInput(); }); this.inputContainer = $('.inputContainer'); @@ -210,10 +216,69 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.fitHeightToContent(); })); this.input.setPosition({ lineNumber: 1, column: this.input.getModel().getLineMaxColumn(1) }); + + this.createTriggerBreakpointInput(container); + + this.updateContextInput(); // Due to an electron bug we have to do the timeout, otherwise we do not get focus setTimeout(() => this.input.focus(), 150); } + private createTriggerBreakpointInput(container: HTMLElement) { + const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint); + + const index = breakpoints.findIndex((bp) => this.breakpoint?.triggeredBy === bp.getId()); + let select = 0; + if (index > -1) { + select = index + 1; + } + const items: ISelectOptionItem[] = [{ text: nls.localize('noTriggerByBreakpoint', 'None') }]; + breakpoints.map(bp => { text: `${this.labelService.getUriLabel(bp.uri, { relative: true })}: ${bp.lineNumber}` }) + .forEach(i => items.push(i)); + + const selectBreakpointBox = new SelectBox(items, select, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('selectBreakpoint', 'Select breakpoint') }); + selectBreakpointBox.onDidSelect(e => { + if (e.index === 0) { + this.triggeredByBreakpointInput = undefined; + } else { + this.triggeredByBreakpointInput = breakpoints[e.index - 1]; + } + }); + this.toDispose.push(selectBreakpointBox); + this.selectBreakpointContainer = $('.select-breakpoint-container'); + this.toDispose.push(dom.addDisposableListener(this.selectBreakpointContainer, dom.EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Escape)) { + this.close(false); + } + })); + + const selectionWrapper = $('.select-box-container'); + dom.append(this.selectBreakpointContainer, selectionWrapper); + selectBreakpointBox.render(selectionWrapper); + + dom.append(container, this.selectBreakpointContainer); + + const closeButton = new Button(this.selectBreakpointContainer, defaultButtonStyles); + closeButton.label = nls.localize('ok', "Ok"); + this.toDispose.push(closeButton.onDidClick(() => this.close(true))); + this.toDispose.push(closeButton); + } + + private updateContextInput() { + if (this.context === Context.TRIGGER_POINT) { + this.inputContainer.hidden = true; + this.selectBreakpointContainer.hidden = false; + } else { + this.inputContainer.hidden = false; + this.selectBreakpointContainer.hidden = true; + this.setInputMode(); + const value = this.getInputValue(this.breakpoint); + this.input.getModel().setValue(value); + this.input.focus(); + } + } + protected override _doLayout(heightInPixel: number, widthInPixel: number): void { this.heightInPx = heightInPixel; this.input.layout({ height: heightInPixel, width: widthInPixel - 113 }); @@ -321,6 +386,8 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi let condition = this.breakpoint && this.breakpoint.condition; let hitCondition = this.breakpoint && this.breakpoint.hitCondition; let logMessage = this.breakpoint && this.breakpoint.logMessage; + let triggeredBy = this.breakpoint && this.breakpoint.triggeredBy; + this.rememberInput(); if (this.conditionInput || this.context === Context.CONDITION) { @@ -332,13 +399,21 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi if (this.logMessageInput || this.context === Context.LOG_MESSAGE) { logMessage = this.logMessageInput; } + if (this.context === Context.TRIGGER_POINT) { + // currently, trigger points don't support additional conditions: + condition = undefined; + hitCondition = undefined; + logMessage = undefined; + triggeredBy = this.triggeredByBreakpointInput?.getId(); + } if (this.breakpoint) { const data = new Map(); data.set(this.breakpoint.getId(), { condition, hitCondition, - logMessage + logMessage, + triggeredBy }); this.debugService.updateBreakpoints(this.breakpoint.originalUri, data, false).then(undefined, onUnexpectedError); } else { @@ -350,7 +425,8 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi enabled: true, condition, hitCondition, - logMessage + logMessage, + triggeredBy }]); } } diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 6ad048c6170e0..11e9a4391506e 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -19,8 +19,9 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { MarkdownString } from 'vs/base/common/htmlContent'; import { KeyCode } from 'vs/base/common/keyCodes'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import * as resources from 'vs/base/common/resources'; +import { ThemeIcon } from 'vs/base/common/themables'; import { Constants } from 'vs/base/common/uint'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; @@ -32,6 +33,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -40,18 +42,16 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ThemeIcon } from 'vs/base/common/themables'; import { ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IEditorPane } from 'vs/workbench/common/editor'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import * as icons from 'vs/workbench/contrib/debug/browser/debugIcons'; import { DisassemblyView } from 'vs/workbench/contrib/debug/browser/disassemblyView'; -import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, DebuggerString, DEBUG_SCHEME, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IDataBreakpoint, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; +import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, DEBUG_SCHEME, DebuggerString, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IDataBreakpoint, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from 'vs/workbench/contrib/debug/common/debug'; import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IHoverService } from 'vs/platform/hover/browser/hover'; const $ = dom.$; @@ -539,7 +539,7 @@ class BreakpointsRenderer implements IListRenderer { return ('message' in breakpoint && breakpoint.message) ? text.concat(', ' + breakpoint.message) : text; }; + + if (debugActive && breakpoint instanceof Breakpoint && breakpoint.pending) { + return { + icon: icons.breakpoint.pending + }; + } + if (debugActive && !breakpoint.verified) { return { icon: breakpointIcon.unverified, @@ -1291,7 +1298,13 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated: }; } - if (breakpoint.logMessage || breakpoint.condition || breakpoint.hitCondition) { + // can change this when all breakpoint supports dependent breakpoint condition + let triggeringBreakpoint: IBreakpoint | undefined; + if (breakpoint instanceof Breakpoint && breakpoint.triggeredBy) { + triggeringBreakpoint = debugModel.getBreakpoints().find(bp => bp.getId() === breakpoint.triggeredBy); + } + + if (breakpoint.logMessage || breakpoint.condition || breakpoint.hitCondition || triggeringBreakpoint) { const messages: string[] = []; let icon = breakpoint.logMessage ? icons.logBreakpoint.regular : icons.conditionalBreakpoint.regular; if (!breakpoint.supported) { @@ -1308,6 +1321,9 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated: if (breakpoint.hitCondition) { messages.push(localize('hitCount', "Hit Count: {0}", breakpoint.hitCondition)); } + if (triggeringBreakpoint) { + messages.push(localize('triggeredBy', "Hit after breakpoint: {0}", `${labelService.getUriLabel(triggeringBreakpoint.uri, { relative: true })}: ${triggeringBreakpoint.lineNumber}`)); + } return { icon, diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index 415a2df008347..2540e40a1fee0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -151,6 +151,36 @@ class LogPointAction extends EditorAction { } } +class TriggerByBreakpointAction extends EditorAction { + + constructor() { + super({ + id: 'editor.debug.action.triggerByBreakpoint', + label: nls.localize('triggerByBreakpointEditorAction', "Debug: Add Triggered Breakpoint..."), + precondition: CONTEXT_DEBUGGERS_AVAILABLE, + alias: 'Debug: Triggered Breakpoint...', + menuOpts: [ + { + menuId: MenuId.MenubarNewBreakpointMenu, + title: nls.localize({ key: 'miTriggerByBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Triggered Breakpoint..."), + group: '1_breakpoints', + order: 4, + when: CONTEXT_DEBUGGERS_AVAILABLE, + } + ] + }); + } + + async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const debugService = accessor.get(IDebugService); + + const position = editor.getPosition(); + if (position && editor.hasModel() && debugService.canSetBreakpointsIn(editor.getModel())) { + editor.getContribution(BREAKPOINT_EDITOR_CONTRIBUTION_ID)?.showBreakpointWidget(position.lineNumber, position.column, BreakpointWidgetContext.TRIGGER_POINT); + } + } +} + class EditBreakpointAction extends EditorAction { constructor() { super({ @@ -596,6 +626,7 @@ registerAction2(ToggleDisassemblyViewSourceCodeAction); registerAction2(ToggleBreakpointAction); registerEditorAction(ConditionalBreakpointAction); registerEditorAction(LogPointAction); +registerEditorAction(TriggerByBreakpointAction); registerEditorAction(EditBreakpointAction); registerEditorAction(RunToCursorAction); registerEditorAction(StepIntoTargetsAction); diff --git a/src/vs/workbench/contrib/debug/browser/debugIcons.ts b/src/vs/workbench/contrib/debug/browser/debugIcons.ts index 4da189b0affea..b1a9a4a0789ce 100644 --- a/src/vs/workbench/contrib/debug/browser/debugIcons.ts +++ b/src/vs/workbench/contrib/debug/browser/debugIcons.ts @@ -18,7 +18,8 @@ export const loadedScriptsViewIcon = registerIcon('loaded-scripts-view-icon', Co export const breakpoint = { regular: registerIcon('debug-breakpoint', Codicon.debugBreakpoint, localize('debugBreakpoint', 'Icon for breakpoints.')), disabled: registerIcon('debug-breakpoint-disabled', Codicon.debugBreakpointDisabled, localize('debugBreakpointDisabled', 'Icon for disabled breakpoints.')), - unverified: registerIcon('debug-breakpoint-unverified', Codicon.debugBreakpointUnverified, localize('debugBreakpointUnverified', 'Icon for unverified breakpoints.')) + unverified: registerIcon('debug-breakpoint-unverified', Codicon.debugBreakpointUnverified, localize('debugBreakpointUnverified', 'Icon for unverified breakpoints.')), + pending: registerIcon('debug-breakpoint-pending', Codicon.debugBreakpointPending, localize('debugBreakpointPendingOnTrigger', 'Icon for breakpoints waiting on another breakpoint.')), }; export const functionBreakpoint = { regular: registerIcon('debug-breakpoint-function', Codicon.debugBreakpointFunction, localize('debugBreakpointFunction', 'Icon for function breakpoints.')), diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index a7ff2816f8a5c..b1c48f1cad502 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -40,7 +40,7 @@ import { ConfigurationManager } from 'vs/workbench/contrib/debug/browser/debugCo import { DebugMemoryFileSystemProvider } from 'vs/workbench/contrib/debug/browser/debugMemory'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { DebugTaskRunner, TaskRunResult } from 'vs/workbench/contrib/debug/browser/debugTaskRunner'; -import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_HAS_DEBUGGED, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_MODE, debuggerDisabledMessage, DEBUG_MEMORY_SCHEME, getStateLabel, IAdapterManager, IBreakpoint, IBreakpointData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, DEBUG_SCHEME } from 'vs/workbench/contrib/debug/common/debug'; +import { CALLSTACK_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_HAS_DEBUGGED, CONTEXT_DEBUG_STATE, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_UX, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_MODE, debuggerDisabledMessage, DEBUG_MEMORY_SCHEME, getStateLabel, IAdapterManager, IBreakpoint, IBreakpointData, ICompound, IConfig, IConfigurationManager, IDebugConfiguration, IDebugModel, IDebugService, IDebugSession, IDebugSessionOptions, IEnablement, IExceptionBreakpoint, IGlobalConfig, ILaunch, IStackFrame, IThread, IViewModel, REPL_VIEW_ID, State, VIEWLET_ID, DEBUG_SCHEME, IBreakpointUpdateData } from 'vs/workbench/contrib/debug/common/debug'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { Breakpoint, DataBreakpoint, DebugModel, FunctionBreakpoint, InstructionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; @@ -1004,6 +1004,7 @@ export class DebugService implements IDebugService { this.model.setEnablement(breakpoint, enable); this.debugStorage.storeBreakpoints(this.model); if (breakpoint instanceof Breakpoint) { + await this.makeTriggeredBreakpointsMatchEnablement(enable, breakpoint); await this.sendBreakpoints(breakpoint.originalUri); } else if (breakpoint instanceof FunctionBreakpoint) { await this.sendFunctionBreakpoints(); @@ -1036,7 +1037,7 @@ export class DebugService implements IDebugService { return breakpoints; } - async updateBreakpoints(uri: uri, data: Map, sendOnResourceSaved: boolean): Promise { + async updateBreakpoints(uri: uri, data: Map, sendOnResourceSaved: boolean): Promise { this.model.updateBreakpoints(data); this.debugStorage.storeBreakpoints(this.model); if (sendOnResourceSaved) { @@ -1048,15 +1049,17 @@ export class DebugService implements IDebugService { } async removeBreakpoints(id?: string): Promise { - const toRemove = this.model.getBreakpoints().filter(bp => !id || bp.getId() === id); + const breakpoints = this.model.getBreakpoints(); + const toRemove = breakpoints.filter(bp => !id || bp.getId() === id); // note: using the debugger-resolved uri for aria to reflect UI state toRemove.forEach(bp => aria.status(nls.localize('breakpointRemoved', "Removed breakpoint, line {0}, file {1}", bp.lineNumber, bp.uri.fsPath))); - const urisToClear = distinct(toRemove, bp => bp.originalUri.toString()).map(bp => bp.originalUri); + const urisToClear = new Set(toRemove.map(bp => bp.originalUri.toString())); this.model.removeBreakpoints(toRemove); + this.unlinkTriggeredBreakpoints(breakpoints, toRemove).forEach(uri => urisToClear.add(uri.toString())); this.debugStorage.storeBreakpoints(this.model); - await Promise.all(urisToClear.map(uri => this.sendBreakpoints(uri))); + await Promise.all([...urisToClear].map(uri => this.sendBreakpoints(URI.parse(uri)))); } setBreakpointsActivated(activated: boolean): Promise { @@ -1152,11 +1155,50 @@ export class DebugService implements IDebugService { } } - private async sendBreakpoints(modelUri: uri, sourceModified = false, session?: IDebugSession): Promise { + /** + * Removes the condition of triggered breakpoints that depended on + * breakpoints in `removedBreakpoints`. Returns the URIs of resources that + * had their breakpoints changed in this way. + */ + private unlinkTriggeredBreakpoints(allBreakpoints: readonly IBreakpoint[], removedBreakpoints: readonly IBreakpoint[]): uri[] { + const affectedUris: uri[] = []; + for (const removed of removedBreakpoints) { + for (const existing of allBreakpoints) { + if (!removedBreakpoints.includes(existing) && existing.triggeredBy === removed.getId()) { + this.model.updateBreakpoints(new Map([[existing.getId(), { triggeredBy: undefined }]])); + affectedUris.push(existing.originalUri); + } + } + } + + return affectedUris; + } + + private async makeTriggeredBreakpointsMatchEnablement(enable: boolean, breakpoint: Breakpoint) { + if (enable) { + /** If the breakpoint is being enabled, also ensure its triggerer is enabled */ + if (breakpoint.triggeredBy) { + const trigger = this.model.getBreakpoints().find(bp => breakpoint.triggeredBy === bp.getId()); + if (trigger && !trigger.enabled) { + await this.enableOrDisableBreakpoints(enable, trigger); + } + } + } + + + /** Makes its triggeree states match the state of this breakpoint */ + await Promise.all(this.model.getBreakpoints() + .filter(bp => bp.triggeredBy === breakpoint.getId() && bp.enabled !== enable) + .map(bp => this.enableOrDisableBreakpoints(enable, bp)) + ); + } + + public async sendBreakpoints(modelUri: uri, sourceModified = false, session?: IDebugSession): Promise { const breakpointsToSend = this.model.getBreakpoints({ originalUri: modelUri, enabledOnly: true }); await sendToOneOrAllSessions(this.model, session, async s => { if (!s.configuration.noDebug) { - await s.sendBreakpoints(modelUri, breakpointsToSend, sourceModified); + const sessionBps = breakpointsToSend.filter(bp => !bp.triggeredBy || bp.getSessionDidTrigger(s.getId())); + await s.sendBreakpoints(modelUri, sessionBps, sourceModified); } }); } diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 71966c239a3fe..847075cce33f3 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -5,7 +5,7 @@ import * as aria from 'vs/base/browser/ui/aria/aria'; import { distinct } from 'vs/base/common/arrays'; -import { Queue, RunOnceScheduler } from 'vs/base/common/async'; +import { Queue, RunOnceScheduler, raceTimeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; @@ -42,6 +42,8 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { getActiveWindow } from 'vs/base/browser/dom'; import { mainWindow } from 'vs/base/browser/window'; +const TRIGGERED_BREAKPOINT_MAX_DELAY = 1500; + export class DebugSession implements IDebugSession, IDisposable { parentSession: IDebugSession | undefined; @@ -78,6 +80,12 @@ export class DebugSession implements IDebugSession, IDisposable { private _name: string | undefined; private readonly _onDidChangeName = new Emitter(); + /** + * Promise set while enabling dependent breakpoints to block the debugger + * from continuing from a stopped state. + */ + private _waitToResume?: Promise; + constructor( private id: string, private _configuration: { resolved: IConfig; unresolved: IConfig | undefined }, @@ -636,6 +644,7 @@ export class DebugSession implements IDebugSession, IDisposable { } async restartFrame(frameId: number, threadId: number): Promise { + await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'restartFrame')); } @@ -651,6 +660,7 @@ export class DebugSession implements IDebugSession, IDisposable { } async next(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise { + await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'next')); } @@ -660,6 +670,7 @@ export class DebugSession implements IDebugSession, IDisposable { } async stepIn(threadId: number, targetId?: number, granularity?: DebugProtocol.SteppingGranularity): Promise { + await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepIn')); } @@ -669,6 +680,7 @@ export class DebugSession implements IDebugSession, IDisposable { } async stepOut(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise { + await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepOut')); } @@ -678,6 +690,7 @@ export class DebugSession implements IDebugSession, IDisposable { } async stepBack(threadId: number, granularity?: DebugProtocol.SteppingGranularity): Promise { + await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepBack')); } @@ -687,6 +700,7 @@ export class DebugSession implements IDebugSession, IDisposable { } async continue(threadId: number): Promise { + await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'continue')); } @@ -695,6 +709,7 @@ export class DebugSession implements IDebugSession, IDisposable { } async reverseContinue(threadId: number): Promise { + await this.waitForTriggeredBreakpoints(); if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'reverse continue')); } @@ -929,6 +944,17 @@ export class DebugSession implements IDebugSession, IDisposable { } } + private waitForTriggeredBreakpoints() { + if (!this._waitToResume) { + return; + } + + return raceTimeout( + this._waitToResume, + TRIGGERED_BREAKPOINT_MAX_DELAY + ); + } + private async fetchThreads(stoppedDetails?: IRawStoppedDetails): Promise { if (this.raw) { const response = await this.raw.threads(); @@ -1227,6 +1253,13 @@ export class DebugSession implements IDebugSession, IDisposable { this.passFocusScheduler.cancel(); this.stoppedDetails.push(event); + // do this very eagerly if we have hitBreakpointIds, since it may take a + // moment for breakpoints to set and we want to do our best to not miss + // anything + if (event.hitBreakpointIds) { + this._waitToResume = this.enableDependentBreakpoints(event.hitBreakpointIds); + } + this.statusQueue.run( this.fetchThreads(event).then(() => event.threadId === undefined ? this.threadIds : [event.threadId]), async (threadId, token) => { @@ -1238,6 +1271,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (focusedThreadDoesNotExist) { this.debugService.focusStackFrame(undefined, undefined); } + const thread = typeof threadId === 'number' ? this.getThread(threadId) : undefined; if (thread) { // Call fetch call stack twice, the first only return the top stack frame. @@ -1269,6 +1303,11 @@ export class DebugSession implements IDebugSession, IDisposable { }; await promises.topCallStack; + + if (!event.hitBreakpointIds) { // if hitBreakpointIds are present, this is handled earlier on + this._waitToResume = this.enableDependentBreakpoints(thread); + } + if (token.isCancellationRequested) { return; } @@ -1291,6 +1330,54 @@ export class DebugSession implements IDebugSession, IDisposable { ); } + private async enableDependentBreakpoints(hitBreakpointIdsOrThread: Thread | number[]) { + let breakpoints: IBreakpoint[]; + if (Array.isArray(hitBreakpointIdsOrThread)) { + breakpoints = this.model.getBreakpoints().filter(bp => hitBreakpointIdsOrThread.includes(bp.getIdFromAdapter(this.id)!)); + } else { + const frame = hitBreakpointIdsOrThread.getTopStackFrame(); + if (frame === undefined) { + return; + } + + if (hitBreakpointIdsOrThread.stoppedDetails && hitBreakpointIdsOrThread.stoppedDetails.reason !== 'breakpoint') { + return; + } + + breakpoints = this.getBreakpointsAtPosition(frame.source.uri, frame.range.startLineNumber, frame.range.endLineNumber, frame.range.startColumn, frame.range.endColumn); + } + + // find the current breakpoints + + // check if the current breakpoints are dependencies, and if so collect and send the dependents to DA + const urisToResend = new Set(); + this.model.getBreakpoints({ triggeredOnly: true, enabledOnly: true }).forEach(bp => { + breakpoints.forEach(cbp => { + if (bp.enabled && bp.triggeredBy === cbp.getId()) { + bp.setSessionDidTrigger(this.getId()); + urisToResend.add(bp.uri.toString()); + } + }); + }); + + const results: Promise[] = []; + urisToResend.forEach((uri) => results.push(this.debugService.sendBreakpoints(URI.parse(uri), undefined, this))); + return Promise.all(results); + } + + private getBreakpointsAtPosition(uri: URI, startLineNumber: number, endLineNumber: number, startColumn: number, endColumn: number): IBreakpoint[] { + return this.model.getBreakpoints({ uri: uri }).filter(bp => { + if (bp.lineNumber < startLineNumber || bp.lineNumber > endLineNumber) { + return false; + } + + if (bp.column && (bp.column < startColumn || bp.column > endColumn)) { + return false; + } + return true; + }); + } + private onDidExitAdapter(event?: AdapterEndEvent): void { this.initialized = true; this.model.setBreakpointSessionData(this.getId(), this.capabilities, undefined); diff --git a/src/vs/workbench/contrib/debug/browser/media/breakpointWidget.css b/src/vs/workbench/contrib/debug/browser/media/breakpointWidget.css index 89915ecb95f49..88728c85b2051 100644 --- a/src/vs/workbench/contrib/debug/browser/media/breakpointWidget.css +++ b/src/vs/workbench/contrib/debug/browser/media/breakpointWidget.css @@ -16,7 +16,7 @@ flex-shrink: 0; } -.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .breakpoint-select-container .monaco-select-box { +.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .monaco-select-box { min-width: 100px; min-height: 18px; padding: 2px 20px 2px 8px; @@ -29,3 +29,27 @@ .monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .inputContainer { flex: 1; } + +.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .select-breakpoint-container { + display: flex; + align-items: center; + flex-direction: row; + gap: 10px; +} + +.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .select-breakpoint-container .monaco-button { + padding-left: 8px; + padding-right: 8px; + line-height: 14px; +} + +.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .select-breakpoint-container .select-box-container { + display: flex; + justify-content: center; + flex-direction: column; + flex-shrink: 0; +} + +.monaco-editor .zone-widget .zone-widget-container.breakpoint-widget .select-breakpoint-container:after { + right: 14px; +} diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 11658d55c947d..308b0ac9bc0b9 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -531,6 +531,7 @@ export interface IBreakpointData { readonly condition?: string; readonly logMessage?: string; readonly hitCondition?: string; + readonly triggeredBy?: string; } export interface IBreakpointUpdateData { @@ -539,6 +540,7 @@ export interface IBreakpointUpdateData { readonly logMessage?: string; readonly lineNumber?: number; readonly column?: number; + readonly triggeredBy?: string; } export interface IBaseBreakpoint extends IEnablement { @@ -563,6 +565,15 @@ export interface IBreakpoint extends IBaseBreakpoint { readonly endColumn?: number; readonly adapterData: any; readonly sessionAgnosticData: { lineNumber: number; column: number | undefined }; + /** An ID of the breakpoint that triggers this breakpoint. */ + readonly triggeredBy?: string; + /** Pending on the trigger breakpoint, which means this breakpoint is not yet sent to DA */ + readonly pending: boolean; + + /** Marks that a session did trigger the breakpoint. */ + setSessionDidTrigger(sessionId: string): void; + /** Gets whether the `triggeredBy` condition has been met in the given sesison ID. */ + getSessionDidTrigger(sessionId: string): boolean; } export interface IFunctionBreakpoint extends IBaseBreakpoint { @@ -638,7 +649,7 @@ export interface IEvaluate { export interface IDebugModel extends ITreeElement { getSession(sessionId: string | undefined, includeInactive?: boolean): IDebugSession | undefined; getSessions(includeInactive?: boolean): IDebugSession[]; - getBreakpoints(filter?: { uri?: uri; originalUri?: uri; lineNumber?: number; column?: number; enabledOnly?: boolean }): ReadonlyArray; + getBreakpoints(filter?: { uri?: uri; originalUri?: uri; lineNumber?: number; column?: number; enabledOnly?: boolean; triggeredOnly?: boolean }): ReadonlyArray; areBreakpointsActivated(): boolean; getFunctionBreakpoints(): ReadonlyArray; getDataBreakpoints(): ReadonlyArray; @@ -1138,6 +1149,11 @@ export interface IDebugService { */ sendAllBreakpoints(session?: IDebugSession): Promise; + /** + * Sends breakpoints of the given source to the passed session. + */ + sendBreakpoints(modelUri: uri, sourceModified?: boolean, session?: IDebugSession): Promise; + /** * Adds a new watch expression and evaluates it against the debug adapter. */ @@ -1203,7 +1219,8 @@ export interface IDebugService { export const enum BreakpointWidgetContext { CONDITION = 0, HIT_COUNT = 1, - LOG_MESSAGE = 2 + LOG_MESSAGE = 2, + TRIGGER_POINT = 3 } export interface IDebugEditorContribution extends editorCommon.IEditorContribution { diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 4636296b97352..c4f5c811018da 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -4,30 +4,30 @@ *--------------------------------------------------------------------------------------------*/ import { distinct } from 'vs/base/common/arrays'; +import { findLastIdx } from 'vs/base/common/arraysFind'; import { DeferredPromise, RunOnceScheduler } from 'vs/base/common/async'; -import { decodeBase64, encodeBase64, VSBuffer } from 'vs/base/common/buffer'; +import { VSBuffer, decodeBase64, encodeBase64 } from 'vs/base/common/buffer'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { stringHash } from 'vs/base/common/hash'; import { Emitter, Event } from 'vs/base/common/event'; +import { stringHash } from 'vs/base/common/hash'; import { Disposable } from 'vs/base/common/lifecycle'; import { mixin } from 'vs/base/common/objects'; +import { autorun } from 'vs/base/common/observable'; import * as resources from 'vs/base/common/resources'; import { isString, isUndefinedOrNull } from 'vs/base/common/types'; import { URI, URI as uri } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IRange, Range } from 'vs/editor/common/core/range'; import * as nls from 'vs/nls'; +import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { IEditorPane } from 'vs/workbench/common/editor'; -import { DEBUG_MEMORY_SCHEME, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointsChangeEvent, IBreakpointUpdateData, IDataBreakpoint, IDebugModel, IDebugSession, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; -import { getUriFromSource, Source, UNKNOWN_SOURCE_LABEL } from 'vs/workbench/contrib/debug/common/debugSource'; +import { DEBUG_MEMORY_SCHEME, IBaseBreakpoint, IBreakpoint, IBreakpointData, IBreakpointUpdateData, IBreakpointsChangeEvent, IDataBreakpoint, IDebugModel, IDebugSession, IEnablement, IExceptionBreakpoint, IExceptionInfo, IExpression, IExpressionContainer, IFunctionBreakpoint, IInstructionBreakpoint, IMemoryInvalidationEvent, IMemoryRegion, IRawModelUpdate, IRawStoppedDetails, IScope, IStackFrame, IThread, ITreeElement, MemoryRange, MemoryRangeType, State } from 'vs/workbench/contrib/debug/common/debug'; +import { Source, UNKNOWN_SOURCE_LABEL, getUriFromSource } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { DisassemblyViewInput } from 'vs/workbench/contrib/debug/common/disassemblyViewInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ILogService } from 'vs/platform/log/common/log'; -import { autorun } from 'vs/base/common/observable'; -import { findLastIdx } from 'vs/base/common/arraysFind'; interface IDebugProtocolVariableWithContext extends DebugProtocol.Variable { __vscodeVariableMenuContext?: string; @@ -866,6 +866,7 @@ export abstract class BaseBreakpoint extends Enablement implements IBaseBreakpoi } export class Breakpoint extends BaseBreakpoint implements IBreakpoint { + private sessionsDidTrigger?: Set; constructor( private readonly _uri: uri, @@ -879,7 +880,8 @@ export class Breakpoint extends BaseBreakpoint implements IBreakpoint { private readonly textFileService: ITextFileService, private readonly uriIdentityService: IUriIdentityService, private readonly logService: ILogService, - id = generateUuid() + id = generateUuid(), + public triggeredBy: string | undefined = undefined ) { super(enabled, hitCondition, condition, logMessage, id); } @@ -900,6 +902,13 @@ export class Breakpoint extends BaseBreakpoint implements IBreakpoint { return true; } + get pending(): boolean { + if (this.data) { + return false; + } + return this.triggeredBy !== undefined; + } + get uri(): uri { return this.verified && this.data && this.data.source ? getUriFromSource(this.data.source, this.data.source.path, this.data.sessionId, this.uriIdentityService, this.logService) : this._uri; } @@ -965,7 +974,7 @@ export class Breakpoint extends BaseBreakpoint implements IBreakpoint { result.lineNumber = this._lineNumber; result.column = this._column; result.adapterData = this.adapterData; - + result.triggeredBy = this.triggeredBy; return result; } @@ -973,22 +982,35 @@ export class Breakpoint extends BaseBreakpoint implements IBreakpoint { return `${resources.basenameOrAuthority(this.uri)} ${this.lineNumber}`; } + public setSessionDidTrigger(sessionId: string): void { + this.sessionsDidTrigger ??= new Set(); + this.sessionsDidTrigger.add(sessionId); + } + + public getSessionDidTrigger(sessionId: string): boolean { + return !!this.sessionsDidTrigger?.has(sessionId); + } + update(data: IBreakpointUpdateData): void { - if (!isUndefinedOrNull(data.lineNumber)) { + if (data.hasOwnProperty('lineNumber') && !isUndefinedOrNull(data.lineNumber)) { this._lineNumber = data.lineNumber; } - if (!isUndefinedOrNull(data.column)) { + if (data.hasOwnProperty('column')) { this._column = data.column; } - if (!isUndefinedOrNull(data.condition)) { + if (data.hasOwnProperty('condition')) { this.condition = data.condition; } - if (!isUndefinedOrNull(data.hitCondition)) { + if (data.hasOwnProperty('hitCondition')) { this.hitCondition = data.hitCondition; } - if (!isUndefinedOrNull(data.logMessage)) { + if (data.hasOwnProperty('logMessage')) { this.logMessage = data.logMessage; } + if (data.hasOwnProperty('triggeredBy')) { + this.triggeredBy = data.triggeredBy; + this.sessionsDidTrigger = undefined; + } } } @@ -1368,7 +1390,7 @@ export class DebugModel extends Disposable implements IDebugModel { return { wholeCallStack, topCallStack: wholeCallStack }; } - getBreakpoints(filter?: { uri?: uri; originalUri?: uri; lineNumber?: number; column?: number; enabledOnly?: boolean }): IBreakpoint[] { + getBreakpoints(filter?: { uri?: uri; originalUri?: uri; lineNumber?: number; column?: number; enabledOnly?: boolean; triggeredOnly?: boolean }): IBreakpoint[] { if (filter) { const uriStr = filter.uri?.toString(); const originalUriStr = filter.originalUri?.toString(); @@ -1388,6 +1410,9 @@ export class DebugModel extends Disposable implements IDebugModel { if (filter.enabledOnly && (!this.breakpointsActivated || !bp.enabled)) { return false; } + if (filter.triggeredOnly && bp.triggeredBy === undefined) { + return false; + } return true; }); @@ -1462,7 +1487,9 @@ export class DebugModel extends Disposable implements IDebugModel { } addBreakpoints(uri: uri, rawData: IBreakpointData[], fireEvent = true): IBreakpoint[] { - const newBreakpoints = rawData.map(rawBp => new Breakpoint(uri, rawBp.lineNumber, rawBp.column, rawBp.enabled === false ? false : true, rawBp.condition, rawBp.hitCondition, rawBp.logMessage, undefined, this.textFileService, this.uriIdentityService, this.logService, rawBp.id)); + const newBreakpoints = rawData.map(rawBp => { + return new Breakpoint(uri, rawBp.lineNumber, rawBp.column, rawBp.enabled === false ? false : true, rawBp.condition, rawBp.hitCondition, rawBp.logMessage, undefined, this.textFileService, this.uriIdentityService, this.logService, rawBp.id, rawBp.triggeredBy); + }); this.breakpoints = this.breakpoints.concat(newBreakpoints); this.breakpointsActivated = true; this.sortAndDeDup(); diff --git a/src/vs/workbench/contrib/debug/common/debugStorage.ts b/src/vs/workbench/contrib/debug/common/debugStorage.ts index efcd601c246e4..dd267f9035834 100644 --- a/src/vs/workbench/contrib/debug/common/debugStorage.ts +++ b/src/vs/workbench/contrib/debug/common/debugStorage.ts @@ -3,15 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Disposable } from 'vs/base/common/lifecycle'; +import { observableValue } from 'vs/base/common/observable'; import { URI } from 'vs/base/common/uri'; -import { StorageScope, IStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ExceptionBreakpoint, Expression, Breakpoint, FunctionBreakpoint, DataBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; -import { IEvaluate, IExpression, IDebugModel } from 'vs/workbench/contrib/debug/common/debug'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { ILogService } from 'vs/platform/log/common/log'; -import { observableValue } from 'vs/base/common/observable'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IDebugModel, IEvaluate, IExpression } from 'vs/workbench/contrib/debug/common/debug'; +import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, Expression, FunctionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; const DEBUG_BREAKPOINTS_KEY = 'debug.breakpoint'; const DEBUG_FUNCTION_BREAKPOINTS_KEY = 'debug.functionbreakpoint'; @@ -66,7 +66,7 @@ export class DebugStorage extends Disposable { let result: Breakpoint[] | undefined; try { result = JSON.parse(this.storageService.get(DEBUG_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((breakpoint: any) => { - return new Breakpoint(URI.parse(breakpoint.uri.external || breakpoint.source.uri.external), breakpoint.lineNumber, breakpoint.column, breakpoint.enabled, breakpoint.condition, breakpoint.hitCondition, breakpoint.logMessage, breakpoint.adapterData, this.textFileService, this.uriIdentityService, this.logService, breakpoint.id); + return new Breakpoint(URI.parse(breakpoint.uri.external || breakpoint.source.uri.external), breakpoint.lineNumber, breakpoint.column, breakpoint.enabled, breakpoint.condition, breakpoint.hitCondition, breakpoint.logMessage, breakpoint.adapterData, this.textFileService, this.uriIdentityService, this.logService, breakpoint.id, breakpoint.triggeredBy); }); } catch (e) { } diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index 72df37df6ba8a..b655b4b6603ef 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -14,6 +14,7 @@ import { OverviewRulerLane } from 'vs/editor/common/model'; import { LanguageService } from 'vs/editor/common/services/languageService'; import { createTextModel } from 'vs/editor/test/common/testTextModel'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { ILabelService } from 'vs/platform/label/common/label'; import { NullLogService } from 'vs/platform/log/common/log'; import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { createBreakpointDecorations } from 'vs/workbench/contrib/debug/browser/breakpointEditorContribution'; @@ -23,6 +24,7 @@ import { Breakpoint, DebugModel } from 'vs/workbench/contrib/debug/common/debugM import { createTestSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; import { createMockDebugModel, mockUriIdentityService } from 'vs/workbench/contrib/debug/test/browser/mockDebugModel'; import { MockDebugService, MockDebugStorage } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { MockLabelService } from 'vs/workbench/services/label/test/common/mockLabelService'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; function addBreakpointsAndCheckEvents(model: DebugModel, uri: uri, data: IBreakpointData[]) { @@ -346,39 +348,40 @@ suite('Debug - Breakpoints', () => { { lineNumber: 500, enabled: true }, ]); const breakpoints = model.getBreakpoints(); + const ls = new MockLabelService(); - let result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[0]); + let result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[0], ls, model); assert.strictEqual(result.message, 'Condition: x > 5'); assert.strictEqual(result.icon.id, 'debug-breakpoint-conditional'); - result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[1]); + result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[1], ls, model); assert.strictEqual(result.message, 'Disabled Breakpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-disabled'); - result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[2]); + result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[2], ls, model); assert.strictEqual(result.message, 'Log Message: hello'); assert.strictEqual(result.icon.id, 'debug-breakpoint-log'); - result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[3]); + result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[3], ls, model); assert.strictEqual(result.message, 'Hit Count: 12'); assert.strictEqual(result.icon.id, 'debug-breakpoint-conditional'); - result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[4]); - assert.strictEqual(result.message, 'Breakpoint'); + result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[4], ls, model); + assert.strictEqual(result.message, ls.getUriLabel(breakpoints[4].uri)); assert.strictEqual(result.icon.id, 'debug-breakpoint'); - result = getBreakpointMessageAndIcon(State.Stopped, false, breakpoints[2]); + result = getBreakpointMessageAndIcon(State.Stopped, false, breakpoints[2], ls, model); assert.strictEqual(result.message, 'Disabled Logpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-log-disabled'); model.addDataBreakpoint('label', 'id', true, ['read'], 'read'); const dataBreakpoints = model.getDataBreakpoints(); - result = getBreakpointMessageAndIcon(State.Stopped, true, dataBreakpoints[0]); + result = getBreakpointMessageAndIcon(State.Stopped, true, dataBreakpoints[0], ls, model); assert.strictEqual(result.message, 'Data Breakpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-data'); const functionBreakpoint = model.addFunctionBreakpoint('foo', '1'); - result = getBreakpointMessageAndIcon(State.Stopped, true, functionBreakpoint); + result = getBreakpointMessageAndIcon(State.Stopped, true, functionBreakpoint, ls, model); assert.strictEqual(result.message, 'Function Breakpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-function'); @@ -389,15 +392,15 @@ suite('Debug - Breakpoints', () => { data.set(functionBreakpoint.getId(), { verified: true }); model.setBreakpointSessionData('mocksessionid', { supportsFunctionBreakpoints: false, supportsDataBreakpoints: true, supportsLogPoints: true }, data); - result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[0]); + result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[0], ls, model); assert.strictEqual(result.message, 'Unverified Breakpoint'); assert.strictEqual(result.icon.id, 'debug-breakpoint-unverified'); - result = getBreakpointMessageAndIcon(State.Stopped, true, functionBreakpoint); + result = getBreakpointMessageAndIcon(State.Stopped, true, functionBreakpoint, ls, model); assert.strictEqual(result.message, 'Function breakpoints not supported by this debug type'); assert.strictEqual(result.icon.id, 'debug-breakpoint-function-unverified'); - result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[2]); + result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[2], ls, model); assert.strictEqual(result.message, 'Log Message: hello, world'); assert.strictEqual(result.icon.id, 'debug-breakpoint-log'); }); @@ -418,7 +421,10 @@ suite('Debug - Breakpoints', () => { const breakpoints = model.getBreakpoints(); const instantiationService = new TestInstantiationService(); - instantiationService.stub(IDebugService, new MockDebugService()); + const debugService = new MockDebugService(); + debugService.getModel = () => model; + instantiationService.stub(IDebugService, debugService); + instantiationService.stub(ILabelService, new MockLabelService()); instantiationService.stub(ILanguageService, disposables.add(new LanguageService())); let decorations = instantiationService.invokeFunction(accessor => createBreakpointDecorations(accessor, textModel, breakpoints, State.Running, true, true)); assert.strictEqual(decorations.length, 3); // last breakpoint filtered out since it has a large line number diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index da4b7f0dceb6b..5dca0f132e5fa 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -61,6 +61,10 @@ export class MockDebugService implements IDebugService { throw new Error('not implemented'); } + sendBreakpoints(modelUri: uri, sourceModified?: boolean | undefined, session?: IDebugSession | undefined): Promise { + throw new Error('not implemented'); + } + addBreakpoints(uri: uri, rawBreakpoints: IBreakpointData[]): Promise { throw new Error('not implemented'); }