Skip to content

Commit 3de501a

Browse files
Support condition and hit count on data breakpoints (fix microsoft#188721) (microsoft#195710)
* Support setting of condition and hit count on data breakpoints (fix microsoft#188721) * Add info and inline buttons to data breakpoints * Fix tests --------- Co-authored-by: Rob Lourens <[email protected]>
1 parent 47ca547 commit 3de501a

File tree

7 files changed

+201
-18
lines changed

7 files changed

+201
-18
lines changed

src/vs/workbench/contrib/debug/browser/breakpointsView.ts

Lines changed: 152 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export function getExpandedBodySize(model: IDebugModel, sessionId: string | unde
7272
type BreakpointItem = IBreakpoint | IFunctionBreakpoint | IDataBreakpoint | IExceptionBreakpoint | IInstructionBreakpoint;
7373

7474
interface InputBoxData {
75-
breakpoint: IFunctionBreakpoint | IExceptionBreakpoint;
75+
breakpoint: IFunctionBreakpoint | IExceptionBreakpoint | IDataBreakpoint;
7676
type: 'condition' | 'hitCount' | 'name';
7777
}
7878

@@ -136,8 +136,9 @@ export class BreakpointsView extends ViewPane {
136136
new ExceptionBreakpointsRenderer(this.menu, this.breakpointSupportsCondition, this.breakpointItemType, this.debugService),
137137
new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService),
138138
this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition, this.breakpointItemType),
139-
this.instantiationService.createInstance(DataBreakpointsRenderer),
140139
new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService),
140+
this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointSupportsCondition, this.breakpointItemType),
141+
new DataBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService),
141142
this.instantiationService.createInstance(InstructionBreakpointsRenderer),
142143
], {
143144
identityProvider: { getId: (element: IEnablement) => element.getId() },
@@ -403,6 +404,11 @@ class BreakpointsDelegate implements IListVirtualDelegate<BreakpointItem> {
403404
return ExceptionBreakpointsRenderer.ID;
404405
}
405406
if (element instanceof DataBreakpoint) {
407+
const inputBoxBreakpoint = this.view.inputBoxData?.breakpoint;
408+
if (inputBoxBreakpoint && inputBoxBreakpoint.getId() === element.getId()) {
409+
return DataBreakpointInputRenderer.ID;
410+
}
411+
406412
return DataBreakpointsRenderer.ID;
407413
}
408414
if (element instanceof InstructionBreakpoint) {
@@ -441,6 +447,7 @@ interface IFunctionBreakpointTemplateData extends IBaseBreakpointWithIconTemplat
441447

442448
interface IDataBreakpointTemplateData extends IBaseBreakpointWithIconTemplateData {
443449
accessType: HTMLElement;
450+
condition: HTMLElement;
444451
}
445452

446453
interface IInstructionBreakpointTemplateData extends IBaseBreakpointWithIconTemplateData {
@@ -457,6 +464,16 @@ interface IFunctionBreakpointInputTemplateData {
457464
updating?: boolean;
458465
}
459466

467+
interface IDataBreakpointInputTemplateData {
468+
inputBox: InputBox;
469+
checkbox: HTMLInputElement;
470+
icon: HTMLElement;
471+
breakpoint: IDataBreakpoint;
472+
toDispose: IDisposable[];
473+
type: 'hitCount' | 'condition' | 'name';
474+
updating?: boolean;
475+
}
476+
460477
interface IExceptionBreakpointInputTemplateData {
461478
inputBox: InputBox;
462479
checkbox: HTMLInputElement;
@@ -657,7 +674,7 @@ class FunctionBreakpointsRenderer implements IListRenderer<FunctionBreakpoint, I
657674
data.checkbox.checked = functionBreakpoint.enabled;
658675
data.breakpoint.title = message ? message : '';
659676
if (functionBreakpoint.condition && functionBreakpoint.hitCondition) {
660-
data.condition.textContent = localize('expressionAndHitCount', "Expression: {0} | Hit Count: {1}", functionBreakpoint.condition, functionBreakpoint.hitCondition);
677+
data.condition.textContent = localize('expressionAndHitCount', "Condition: {0} | Hit Count: {1}", functionBreakpoint.condition, functionBreakpoint.hitCondition);
661678
} else {
662679
data.condition.textContent = functionBreakpoint.condition || functionBreakpoint.hitCondition || '';
663680
}
@@ -686,6 +703,9 @@ class FunctionBreakpointsRenderer implements IListRenderer<FunctionBreakpoint, I
686703
class DataBreakpointsRenderer implements IListRenderer<DataBreakpoint, IDataBreakpointTemplateData> {
687704

688705
constructor(
706+
private menu: IMenu,
707+
private breakpointSupportsCondition: IContextKey<boolean>,
708+
private breakpointItemType: IContextKey<string | undefined>,
689709
@IDebugService private readonly debugService: IDebugService,
690710
@ILabelService private readonly labelService: ILabelService
691711
) {
@@ -714,6 +734,10 @@ class DataBreakpointsRenderer implements IListRenderer<DataBreakpoint, IDataBrea
714734

715735
data.name = dom.append(data.breakpoint, $('span.name'));
716736
data.accessType = dom.append(data.breakpoint, $('span.access-type'));
737+
data.condition = dom.append(data.breakpoint, $('span.condition'));
738+
739+
data.actionBar = new ActionBar(data.breakpoint);
740+
data.toDispose.push(data.actionBar);
717741

718742
return data;
719743
}
@@ -727,7 +751,7 @@ class DataBreakpointsRenderer implements IListRenderer<DataBreakpoint, IDataBrea
727751
data.checkbox.checked = dataBreakpoint.enabled;
728752
data.breakpoint.title = message ? message : '';
729753

730-
// Mark function breakpoints as disabled if deactivated or if debug type does not support them #9099
754+
// Mark data breakpoints as disabled if deactivated or if debug type does not support them
731755
const session = this.debugService.getViewModel().focusedSession;
732756
data.breakpoint.classList.toggle('disabled', (session && !session.capabilities.supportsDataBreakpoints) || !this.debugService.getModel().areBreakpointsActivated());
733757
if (session && !session.capabilities.supportsDataBreakpoints) {
@@ -739,6 +763,19 @@ class DataBreakpointsRenderer implements IListRenderer<DataBreakpoint, IDataBrea
739763
} else {
740764
data.accessType.textContent = '';
741765
}
766+
if (dataBreakpoint.condition && dataBreakpoint.hitCondition) {
767+
data.condition.textContent = localize('expressionAndHitCount', "Condition: {0} | Hit Count: {1}", dataBreakpoint.condition, dataBreakpoint.hitCondition);
768+
} else {
769+
data.condition.textContent = dataBreakpoint.condition || dataBreakpoint.hitCondition || '';
770+
}
771+
772+
const primary: IAction[] = [];
773+
this.breakpointSupportsCondition.set(!session || !!session.capabilities.supportsConditionalBreakpoints);
774+
this.breakpointItemType.set('dataBreakpoint');
775+
createAndFillInActionBarActions(this.menu, { arg: dataBreakpoint, shouldForwardArgs: true }, { primary, secondary: [] }, 'inline');
776+
data.actionBar.clear();
777+
data.actionBar.push(primary, { icon: true, label: false });
778+
breakpointIdToActionBarDomeNode.set(dataBreakpoint.getId(), data.actionBar.domNode);
742779
}
743780

744781
disposeTemplate(templateData: IBaseBreakpointWithIconTemplateData): void {
@@ -922,6 +959,113 @@ class FunctionBreakpointInputRenderer implements IListRenderer<IFunctionBreakpoi
922959
}
923960
}
924961

962+
class DataBreakpointInputRenderer implements IListRenderer<IDataBreakpoint, IDataBreakpointInputTemplateData> {
963+
964+
constructor(
965+
private view: BreakpointsView,
966+
private debugService: IDebugService,
967+
private contextViewService: IContextViewService,
968+
private labelService: ILabelService
969+
) { }
970+
971+
static readonly ID = 'databreakpointinput';
972+
973+
get templateId() {
974+
return DataBreakpointInputRenderer.ID;
975+
}
976+
977+
renderTemplate(container: HTMLElement): IDataBreakpointInputTemplateData {
978+
const template: IDataBreakpointInputTemplateData = Object.create(null);
979+
const toDispose: IDisposable[] = [];
980+
981+
const breakpoint = dom.append(container, $('.breakpoint'));
982+
template.icon = $('.icon');
983+
template.checkbox = createCheckbox(toDispose);
984+
985+
dom.append(breakpoint, template.icon);
986+
dom.append(breakpoint, template.checkbox);
987+
this.view.breakpointInputFocused.set(true);
988+
const inputBoxContainer = dom.append(breakpoint, $('.inputBoxContainer'));
989+
990+
991+
const inputBox = new InputBox(inputBoxContainer, this.contextViewService, { inputBoxStyles: defaultInputBoxStyles });
992+
993+
const wrapUp = (success: boolean) => {
994+
template.updating = true;
995+
try {
996+
this.view.breakpointInputFocused.set(false);
997+
const id = template.breakpoint.getId();
998+
999+
if (success) {
1000+
if (template.type === 'condition') {
1001+
this.debugService.updateDataBreakpoint(id, { condition: inputBox.value });
1002+
}
1003+
if (template.type === 'hitCount') {
1004+
this.debugService.updateDataBreakpoint(id, { hitCondition: inputBox.value });
1005+
}
1006+
} else {
1007+
this.view.renderInputBox(undefined);
1008+
}
1009+
} finally {
1010+
template.updating = false;
1011+
}
1012+
};
1013+
1014+
toDispose.push(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => {
1015+
const isEscape = e.equals(KeyCode.Escape);
1016+
const isEnter = e.equals(KeyCode.Enter);
1017+
if (isEscape || isEnter) {
1018+
e.preventDefault();
1019+
e.stopPropagation();
1020+
wrapUp(isEnter);
1021+
}
1022+
}));
1023+
toDispose.push(dom.addDisposableListener(inputBox.inputElement, 'blur', () => {
1024+
if (!template.updating) {
1025+
wrapUp(!!inputBox.value);
1026+
}
1027+
}));
1028+
1029+
template.inputBox = inputBox;
1030+
template.toDispose = toDispose;
1031+
return template;
1032+
}
1033+
1034+
renderElement(dataBreakpoint: DataBreakpoint, _index: number, data: IDataBreakpointInputTemplateData): void {
1035+
data.breakpoint = dataBreakpoint;
1036+
data.type = this.view.inputBoxData?.type || 'condition'; // If there is no type set take the 'condition' as the default
1037+
const { icon, message } = getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), dataBreakpoint, this.labelService);
1038+
1039+
data.icon.className = ThemeIcon.asClassName(icon);
1040+
data.icon.title = message ? message : '';
1041+
data.checkbox.checked = dataBreakpoint.enabled;
1042+
data.checkbox.disabled = true;
1043+
data.inputBox.value = '';
1044+
let placeholder = '';
1045+
let ariaLabel = '';
1046+
if (data.type === 'condition') {
1047+
data.inputBox.value = dataBreakpoint.condition || '';
1048+
placeholder = localize('dataBreakpointExpressionPlaceholder', "Break when expression evaluates to true");
1049+
ariaLabel = localize('dataBreakPointExpresionAriaLabel', "Type expression. Data breakpoint will break when expression evaluates to true");
1050+
} else if (data.type === 'hitCount') {
1051+
data.inputBox.value = dataBreakpoint.hitCondition || '';
1052+
placeholder = localize('dataBreakpointHitCountPlaceholder', "Break when hit count is met");
1053+
ariaLabel = localize('dataBreakPointHitCountAriaLabel', "Type hit count. Data breakpoint will break when hit count is met.");
1054+
}
1055+
data.inputBox.setAriaLabel(ariaLabel);
1056+
data.inputBox.setPlaceHolder(placeholder);
1057+
1058+
setTimeout(() => {
1059+
data.inputBox.focus();
1060+
data.inputBox.select();
1061+
}, 0);
1062+
}
1063+
1064+
disposeTemplate(templateData: IDataBreakpointInputTemplateData): void {
1065+
dispose(templateData.toDispose);
1066+
}
1067+
}
1068+
9251069
class ExceptionBreakpointInputRenderer implements IListRenderer<IExceptionBreakpoint, IExceptionBreakpointInputTemplateData> {
9261070

9271071
constructor(
@@ -1109,7 +1253,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated:
11091253
const messages: string[] = [];
11101254
messages.push(breakpoint.message || localize('functionBreakpoint', "Function Breakpoint"));
11111255
if (breakpoint.condition) {
1112-
messages.push(localize('expression', "Expression condition: {0}", breakpoint.condition));
1256+
messages.push(localize('expression', "Condition: {0}", breakpoint.condition));
11131257
}
11141258
if (breakpoint.hitCondition) {
11151259
messages.push(localize('hitCount', "Hit Count: {0}", breakpoint.hitCondition));
@@ -1161,7 +1305,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated:
11611305
messages.push(localize('logMessage', "Log Message: {0}", breakpoint.logMessage));
11621306
}
11631307
if (breakpoint.condition) {
1164-
messages.push(localize('expression', "Expression condition: {0}", breakpoint.condition));
1308+
messages.push(localize('expression', "Condition: {0}", breakpoint.condition));
11651309
}
11661310
if (breakpoint.hitCondition) {
11671311
messages.push(localize('hitCount', "Hit Count: {0}", breakpoint.hitCondition));
@@ -1410,7 +1554,7 @@ registerAction2(class extends ViewAction<BreakpointsView> {
14101554
});
14111555
}
14121556

1413-
async runInView(accessor: ServicesAccessor, view: BreakpointsView, breakpoint: ExceptionBreakpoint | Breakpoint | FunctionBreakpoint): Promise<void> {
1557+
async runInView(accessor: ServicesAccessor, view: BreakpointsView, breakpoint: ExceptionBreakpoint | Breakpoint | FunctionBreakpoint | DataBreakpoint): Promise<void> {
14141558
const debugService = accessor.get(IDebugService);
14151559
const editorService = accessor.get(IEditorService);
14161560
if (breakpoint instanceof Breakpoint) {
@@ -1472,7 +1616,7 @@ registerAction2(class extends ViewAction<BreakpointsView> {
14721616
id: MenuId.DebugBreakpointsContext,
14731617
group: 'navigation',
14741618
order: 20,
1475-
when: CONTEXT_BREAKPOINT_ITEM_TYPE.isEqualTo('functionBreakpoint')
1619+
when: ContextKeyExpr.or(CONTEXT_BREAKPOINT_ITEM_TYPE.isEqualTo('functionBreakpoint'), CONTEXT_BREAKPOINT_ITEM_TYPE.isEqualTo('dataBreakpoint'))
14761620
}]
14771621
});
14781622
}

src/vs/workbench/contrib/debug/browser/debugService.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,12 @@ export class DebugService implements IDebugService {
10431043
this.debugStorage.storeBreakpoints(this.model);
10441044
}
10451045

1046+
async updateDataBreakpoint(id: string, update: { hitCondition?: string; condition?: string }): Promise<void> {
1047+
this.model.updateDataBreakpoint(id, update);
1048+
this.debugStorage.storeBreakpoints(this.model);
1049+
await this.sendDataBreakpoints();
1050+
}
1051+
10461052
async removeDataBreakpoints(id?: string): Promise<void> {
10471053
this.model.removeDataBreakpoints(id);
10481054
this.debugStorage.storeBreakpoints(this.model);

src/vs/workbench/contrib/debug/browser/media/debugViewlet.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,12 @@
312312
justify-content: center;
313313
}
314314

315-
.debug-pane .debug-breakpoints .breakpoint > .access-type,
315+
.debug-pane .debug-breakpoints .breakpoint > .access-type {
316+
opacity: 0.7;
317+
margin-left: 0.9em;
318+
text-overflow: ellipsis;
319+
overflow: hidden;
320+
}
316321
.debug-pane .debug-breakpoints .breakpoint > .file-path,
317322
.debug-pane .debug-breakpoints .breakpoint > .condition {
318323
opacity: 0.7;

src/vs/workbench/contrib/debug/common/debug.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,12 @@ export interface IDebugService {
10921092
*/
10931093
addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType): Promise<void>;
10941094

1095+
/**
1096+
* Updates an already existing data breakpoint.
1097+
* Notifies debug adapter of breakpoint changes.
1098+
*/
1099+
updateDataBreakpoint(id: string, update: { hitCondition?: string; condition?: string }): Promise<void>;
1100+
10951101
/**
10961102
* Removes all data breakpoints. If id is passed only removes the data breakpoint with the passed id.
10971103
* Notifies debug adapter of breakpoint changes.

src/vs/workbench/contrib/debug/common/debugModel.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,12 +1661,25 @@ export class DebugModel extends Disposable implements IDebugModel {
16611661
this._onDidChangeBreakpoints.fire({ removed, sessionOnly: false });
16621662
}
16631663

1664-
addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType): void {
1665-
const newDataBreakpoint = new DataBreakpoint(label, dataId, canPersist, true, undefined, undefined, undefined, accessTypes, accessType);
1664+
addDataBreakpoint(label: string, dataId: string, canPersist: boolean, accessTypes: DebugProtocol.DataBreakpointAccessType[] | undefined, accessType: DebugProtocol.DataBreakpointAccessType, id?: string): void {
1665+
const newDataBreakpoint = new DataBreakpoint(label, dataId, canPersist, true, undefined, undefined, undefined, accessTypes, accessType, id);
16661666
this.dataBreakpoints.push(newDataBreakpoint);
16671667
this._onDidChangeBreakpoints.fire({ added: [newDataBreakpoint], sessionOnly: false });
16681668
}
16691669

1670+
updateDataBreakpoint(id: string, update: { hitCondition?: string; condition?: string }): void {
1671+
const dataBreakpoint = this.dataBreakpoints.find(fbp => fbp.getId() === id);
1672+
if (dataBreakpoint) {
1673+
if (typeof update.condition === 'string') {
1674+
dataBreakpoint.condition = update.condition;
1675+
}
1676+
if (typeof update.hitCondition === 'string') {
1677+
dataBreakpoint.hitCondition = update.hitCondition;
1678+
}
1679+
this._onDidChangeBreakpoints.fire({ changed: [dataBreakpoint], sessionOnly: false });
1680+
}
1681+
}
1682+
16701683
removeDataBreakpoints(id?: string): void {
16711684
let removed: DataBreakpoint[];
16721685
if (id) {

src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -315,25 +315,29 @@ suite('Debug - Breakpoints', () => {
315315
let eventCount = 0;
316316
disposables.add(model.onDidChangeBreakpoints(() => eventCount++));
317317

318-
model.addDataBreakpoint('label', 'id', true, ['read'], 'read');
319-
model.addDataBreakpoint('second', 'secondId', false, ['readWrite'], 'readWrite');
318+
model.addDataBreakpoint('label', 'id', true, ['read'], 'read', '1');
319+
model.addDataBreakpoint('second', 'secondId', false, ['readWrite'], 'readWrite', '2');
320+
model.updateDataBreakpoint('1', { condition: 'aCondition' });
321+
model.updateDataBreakpoint('2', { hitCondition: '10' });
320322
const dataBreakpoints = model.getDataBreakpoints();
321323
assert.strictEqual(dataBreakpoints[0].canPersist, true);
322324
assert.strictEqual(dataBreakpoints[0].dataId, 'id');
323325
assert.strictEqual(dataBreakpoints[0].accessType, 'read');
326+
assert.strictEqual(dataBreakpoints[0].condition, 'aCondition');
324327
assert.strictEqual(dataBreakpoints[1].canPersist, false);
325328
assert.strictEqual(dataBreakpoints[1].description, 'second');
326329
assert.strictEqual(dataBreakpoints[1].accessType, 'readWrite');
330+
assert.strictEqual(dataBreakpoints[1].hitCondition, '10');
327331

328-
assert.strictEqual(eventCount, 2);
332+
assert.strictEqual(eventCount, 4);
329333

330334
model.removeDataBreakpoints(dataBreakpoints[0].getId());
331-
assert.strictEqual(eventCount, 3);
335+
assert.strictEqual(eventCount, 5);
332336
assert.strictEqual(model.getDataBreakpoints().length, 1);
333337

334338
model.removeDataBreakpoints();
335339
assert.strictEqual(model.getDataBreakpoints().length, 0);
336-
assert.strictEqual(eventCount, 4);
340+
assert.strictEqual(eventCount, 6);
337341
});
338342

339343
test('message and class name', () => {
@@ -348,7 +352,7 @@ suite('Debug - Breakpoints', () => {
348352
const breakpoints = model.getBreakpoints();
349353

350354
let result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[0]);
351-
assert.strictEqual(result.message, 'Expression condition: x > 5');
355+
assert.strictEqual(result.message, 'Condition: x > 5');
352356
assert.strictEqual(result.icon.id, 'debug-breakpoint-conditional');
353357

354358
result = getBreakpointMessageAndIcon(State.Stopped, true, breakpoints[1]);
@@ -428,7 +432,7 @@ suite('Debug - Breakpoints', () => {
428432
assert.strictEqual(decorations[0].options.beforeContentClassName, undefined);
429433
assert.strictEqual(decorations[1].options.before?.inlineClassName, `debug-breakpoint-placeholder`);
430434
assert.strictEqual(decorations[0].options.overviewRuler?.position, OverviewRulerLane.Left);
431-
const expected = new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true }).appendCodeblock(languageId, 'Expression condition: x > 5');
435+
const expected = new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true }).appendCodeblock(languageId, 'Condition: x > 5');
432436
assert.deepStrictEqual(decorations[0].options.glyphMarginHoverMessage, expected);
433437

434438
decorations = instantiationService.invokeFunction(accessor => createBreakpointDecorations(accessor, textModel, breakpoints, State.Running, true, false));

src/vs/workbench/contrib/debug/test/common/mockDebug.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ export class MockDebugService implements IDebugService {
112112
addDataBreakpoint(label: string, dataId: string, canPersist: boolean): Promise<void> {
113113
throw new Error('Method not implemented.');
114114
}
115+
116+
updateDataBreakpoint(id: string, update: { hitCondition?: string; condition?: string }): Promise<void> {
117+
throw new Error('not implemented');
118+
}
119+
115120
removeDataBreakpoints(id?: string | undefined): Promise<void> {
116121
throw new Error('Method not implemented.');
117122
}

0 commit comments

Comments
 (0)