Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts
5239 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { isSafari } from '../../../../base/browser/browser.js';
7
import { BrowserFeatures } from '../../../../base/browser/canIUse.js';
8
import * as dom from '../../../../base/browser/dom.js';
9
import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
10
import { IAction, Separator, SubmenuAction, toAction } from '../../../../base/common/actions.js';
11
import { distinct } from '../../../../base/common/arrays.js';
12
import { RunOnceScheduler, timeout } from '../../../../base/common/async.js';
13
import { memoize } from '../../../../base/common/decorators.js';
14
import { onUnexpectedError } from '../../../../base/common/errors.js';
15
import { MarkdownString } from '../../../../base/common/htmlContent.js';
16
import { dispose, disposeIfDisposable, IDisposable } from '../../../../base/common/lifecycle.js';
17
import * as env from '../../../../base/common/platform.js';
18
import severity from '../../../../base/common/severity.js';
19
import { noBreakWhitespace } from '../../../../base/common/strings.js';
20
import { ThemeIcon } from '../../../../base/common/themables.js';
21
import { URI } from '../../../../base/common/uri.js';
22
import { generateUuid } from '../../../../base/common/uuid.js';
23
import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent, MouseTargetType } from '../../../../editor/browser/editorBrowser.js';
24
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
25
import { IPosition } from '../../../../editor/common/core/position.js';
26
import { Range } from '../../../../editor/common/core/range.js';
27
import { ILanguageService } from '../../../../editor/common/languages/language.js';
28
import { GlyphMarginLane, IModelDecorationOptions, IModelDecorationOverviewRulerOptions, IModelDecorationsChangeAccessor, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from '../../../../editor/common/model.js';
29
import * as nls from '../../../../nls.js';
30
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
31
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
32
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
33
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
34
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
35
import { ILabelService } from '../../../../platform/label/common/label.js';
36
import { registerColor } from '../../../../platform/theme/common/colorRegistry.js';
37
import { registerThemingParticipant, themeColorFromId } from '../../../../platform/theme/common/themeService.js';
38
import { GutterActionsRegistry } from '../../codeEditor/browser/editorLineNumberMenu.js';
39
import { getBreakpointMessageAndIcon } from './breakpointsView.js';
40
import { BreakpointWidget } from './breakpointWidget.js';
41
import * as icons from './debugIcons.js';
42
import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, DebuggerString, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDebugConfiguration, IDebugService, IDebugSession, State } from '../common/debug.js';
43
44
const $ = dom.$;
45
46
interface IBreakpointDecoration {
47
decorationId: string;
48
breakpoint: IBreakpoint;
49
range: Range;
50
inlineWidget?: InlineBreakpointWidget;
51
}
52
53
const breakpointHelperDecoration: IModelDecorationOptions = {
54
description: 'breakpoint-helper-decoration',
55
glyphMarginClassName: ThemeIcon.asClassName(icons.debugBreakpointHint),
56
glyphMargin: { position: GlyphMarginLane.Right },
57
glyphMarginHoverMessage: new MarkdownString().appendText(nls.localize('breakpointHelper', "Click to add a breakpoint")),
58
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
59
};
60
61
export function createBreakpointDecorations(accessor: ServicesAccessor, model: ITextModel, breakpoints: ReadonlyArray<IBreakpoint>, state: State, breakpointsActivated: boolean, showBreakpointsInOverviewRuler: boolean): { range: Range; options: IModelDecorationOptions }[] {
62
const result: { range: Range; options: IModelDecorationOptions }[] = [];
63
breakpoints.forEach((breakpoint) => {
64
if (breakpoint.lineNumber > model.getLineCount()) {
65
return;
66
}
67
const hasOtherBreakpointsOnLine = breakpoints.some(bp => bp !== breakpoint && bp.lineNumber === breakpoint.lineNumber);
68
const column = model.getLineFirstNonWhitespaceColumn(breakpoint.lineNumber);
69
const range = model.validateRange(
70
breakpoint.column ? new Range(breakpoint.lineNumber, breakpoint.column, breakpoint.lineNumber, breakpoint.column + 1)
71
: new Range(breakpoint.lineNumber, column, breakpoint.lineNumber, column + 1) // Decoration has to have a width #20688
72
);
73
74
result.push({
75
options: getBreakpointDecorationOptions(accessor, model, breakpoint, state, breakpointsActivated, showBreakpointsInOverviewRuler, hasOtherBreakpointsOnLine),
76
range
77
});
78
});
79
80
return result;
81
}
82
83
function getBreakpointDecorationOptions(accessor: ServicesAccessor, model: ITextModel, breakpoint: IBreakpoint, state: State, breakpointsActivated: boolean, showBreakpointsInOverviewRuler: boolean, hasOtherBreakpointsOnLine: boolean): IModelDecorationOptions {
84
const debugService = accessor.get(IDebugService);
85
const languageService = accessor.get(ILanguageService);
86
const labelService = accessor.get(ILabelService);
87
const { icon, message, showAdapterUnverifiedMessage } = getBreakpointMessageAndIcon(state, breakpointsActivated, breakpoint, labelService, debugService.getModel());
88
let glyphMarginHoverMessage: MarkdownString | undefined;
89
90
let unverifiedMessage: string | undefined;
91
if (showAdapterUnverifiedMessage) {
92
let langId: string | undefined;
93
unverifiedMessage = debugService.getModel().getSessions().map(s => {
94
const dbg = debugService.getAdapterManager().getDebugger(s.configuration.type);
95
const message = dbg?.strings?.[DebuggerString.UnverifiedBreakpoints];
96
if (message) {
97
if (!langId) {
98
// Lazily compute this, only if needed for some debug adapter
99
langId = languageService.guessLanguageIdByFilepathOrFirstLine(breakpoint.uri) ?? undefined;
100
}
101
return langId && dbg.interestedInLanguage(langId) ? message : undefined;
102
}
103
104
return undefined;
105
})
106
.find(messages => !!messages);
107
}
108
109
if (message) {
110
glyphMarginHoverMessage = new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true });
111
if (breakpoint.condition || breakpoint.hitCondition) {
112
const languageId = model.getLanguageId();
113
glyphMarginHoverMessage.appendCodeblock(languageId, message);
114
if (unverifiedMessage) {
115
glyphMarginHoverMessage.appendMarkdown('$(warning) ' + unverifiedMessage);
116
}
117
} else {
118
glyphMarginHoverMessage.appendText(message);
119
if (unverifiedMessage) {
120
glyphMarginHoverMessage.appendMarkdown('\n\n$(warning) ' + unverifiedMessage);
121
}
122
}
123
} else if (unverifiedMessage) {
124
glyphMarginHoverMessage = new MarkdownString(undefined, { isTrusted: true, supportThemeIcons: true }).appendMarkdown(unverifiedMessage);
125
}
126
127
let overviewRulerDecoration: IModelDecorationOverviewRulerOptions | null = null;
128
if (showBreakpointsInOverviewRuler) {
129
overviewRulerDecoration = {
130
color: themeColorFromId(debugIconBreakpointForeground),
131
position: OverviewRulerLane.Left
132
};
133
}
134
135
const renderInline = breakpoint.column && (hasOtherBreakpointsOnLine || breakpoint.column > model.getLineFirstNonWhitespaceColumn(breakpoint.lineNumber));
136
return {
137
description: 'breakpoint-decoration',
138
glyphMargin: { position: GlyphMarginLane.Right },
139
glyphMarginClassName: ThemeIcon.asClassName(icon),
140
glyphMarginHoverMessage,
141
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
142
before: renderInline ? {
143
content: noBreakWhitespace,
144
inlineClassName: `debug-breakpoint-placeholder`,
145
inlineClassNameAffectsLetterSpacing: true
146
} : undefined,
147
overviewRuler: overviewRulerDecoration,
148
zIndex: 9999
149
};
150
}
151
152
type BreakpointsForLine = { lineNumber: number; positions: IPosition[] };
153
154
async function requestBreakpointCandidateLocations(model: ITextModel, lineNumbers: number[], session: IDebugSession): Promise<BreakpointsForLine[]> {
155
if (!session.capabilities.supportsBreakpointLocationsRequest) {
156
return [];
157
}
158
159
return await Promise.all(distinct(lineNumbers, l => l).map(async lineNumber => {
160
try {
161
return { lineNumber, positions: await session.breakpointsLocations(model.uri, lineNumber) };
162
} catch {
163
return { lineNumber, positions: [] };
164
}
165
}));
166
}
167
168
function createCandidateDecorations(model: ITextModel, breakpointDecorations: IBreakpointDecoration[], lineBreakpoints: BreakpointsForLine[]): { range: Range; options: IModelDecorationOptions; breakpoint: IBreakpoint | undefined }[] {
169
const result: { range: Range; options: IModelDecorationOptions; breakpoint: IBreakpoint | undefined }[] = [];
170
for (const { positions, lineNumber } of lineBreakpoints) {
171
if (positions.length === 0) {
172
continue;
173
}
174
175
// Do not render candidates if there is only one, since it is already covered by the line breakpoint
176
const firstColumn = model.getLineFirstNonWhitespaceColumn(lineNumber);
177
const lastColumn = model.getLineLastNonWhitespaceColumn(lineNumber);
178
positions.forEach(p => {
179
const range = new Range(p.lineNumber, p.column, p.lineNumber, p.column + 1);
180
if ((p.column <= firstColumn && !breakpointDecorations.some(bp => bp.range.startColumn > firstColumn && bp.range.startLineNumber === p.lineNumber)) || p.column > lastColumn) {
181
// Do not render candidates on the start of the line if there's no other breakpoint on the line.
182
return;
183
}
184
185
const breakpointAtPosition = breakpointDecorations.find(bpd => bpd.range.equalsRange(range));
186
if (breakpointAtPosition && breakpointAtPosition.inlineWidget) {
187
// Space already occupied, do not render candidate.
188
return;
189
}
190
result.push({
191
range,
192
options: {
193
description: 'breakpoint-placeholder-decoration',
194
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
195
before: breakpointAtPosition ? undefined : {
196
content: noBreakWhitespace,
197
inlineClassName: `debug-breakpoint-placeholder`,
198
inlineClassNameAffectsLetterSpacing: true
199
},
200
},
201
breakpoint: breakpointAtPosition ? breakpointAtPosition.breakpoint : undefined
202
});
203
});
204
}
205
206
return result;
207
}
208
209
export class BreakpointEditorContribution implements IBreakpointEditorContribution {
210
211
private breakpointHintDecoration: string | null = null;
212
private breakpointWidget: BreakpointWidget | undefined;
213
private breakpointWidgetVisible!: IContextKey<boolean>;
214
private toDispose: IDisposable[] = [];
215
private ignoreDecorationsChangedEvent = false;
216
private ignoreBreakpointsChangeEvent = false;
217
private breakpointDecorations: IBreakpointDecoration[] = [];
218
private candidateDecorations: { decorationId: string; inlineWidget: InlineBreakpointWidget }[] = [];
219
private setDecorationsScheduler!: RunOnceScheduler;
220
221
constructor(
222
private readonly editor: ICodeEditor,
223
@IDebugService private readonly debugService: IDebugService,
224
@IContextMenuService private readonly contextMenuService: IContextMenuService,
225
@IInstantiationService private readonly instantiationService: IInstantiationService,
226
@IContextKeyService contextKeyService: IContextKeyService,
227
@IDialogService private readonly dialogService: IDialogService,
228
@IConfigurationService private readonly configurationService: IConfigurationService,
229
@ILabelService private readonly labelService: ILabelService
230
) {
231
this.breakpointWidgetVisible = CONTEXT_BREAKPOINT_WIDGET_VISIBLE.bindTo(contextKeyService);
232
this.setDecorationsScheduler = new RunOnceScheduler(() => this.setDecorations(), 30);
233
this.setDecorationsScheduler.schedule();
234
this.registerListeners();
235
}
236
237
/**
238
* Returns context menu actions at the line number if breakpoints can be
239
* set. This is used by the {@link TestingDecorations} to allow breakpoint
240
* setting on lines where breakpoint "run" actions are present.
241
*/
242
public getContextMenuActionsAtPosition(lineNumber: number, model: ITextModel) {
243
if (!this.debugService.getAdapterManager().hasEnabledDebuggers()) {
244
return [];
245
}
246
247
if (!this.debugService.canSetBreakpointsIn(model)) {
248
return [];
249
}
250
251
const breakpoints = this.debugService.getModel().getBreakpoints({ lineNumber, uri: model.uri });
252
return this.getContextMenuActions(breakpoints, model.uri, lineNumber);
253
}
254
255
private registerListeners(): void {
256
this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => {
257
if (!this.debugService.getAdapterManager().hasEnabledDebuggers()) {
258
return;
259
}
260
261
const model = this.editor.getModel();
262
if (!e.target.position
263
|| !model
264
|| e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN
265
|| e.target.detail.isAfterLines
266
|| !this.marginFreeFromNonDebugDecorations(e.target.position.lineNumber)
267
// don't return early if there's a breakpoint
268
&& !e.target.element?.className.includes('breakpoint')
269
) {
270
return;
271
}
272
const canSetBreakpoints = this.debugService.canSetBreakpointsIn(model);
273
const lineNumber = e.target.position.lineNumber;
274
const uri = model.uri;
275
276
if (e.event.rightButton || (env.isMacintosh && e.event.leftButton && e.event.ctrlKey)) {
277
// handled by editor gutter context menu
278
return;
279
} else {
280
const breakpoints = this.debugService.getModel().getBreakpoints({ uri, lineNumber });
281
282
if (breakpoints.length) {
283
const isShiftPressed = e.event.shiftKey;
284
const enabled = breakpoints.some(bp => bp.enabled);
285
286
if (isShiftPressed) {
287
breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp));
288
} else if (!env.isLinux && breakpoints.some(bp => !!bp.condition || !!bp.logMessage || !!bp.hitCondition || !!bp.triggeredBy)) {
289
// Show the dialog if there is a potential condition to be accidently lost.
290
// Do not show dialog on linux due to electron issue freezing the mouse #50026
291
const logPoint = breakpoints.every(bp => !!bp.logMessage);
292
const breakpointType = logPoint ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint");
293
294
const disabledBreakpointDialogMessage = nls.localize(
295
'breakpointHasConditionDisabled',
296
"This {0} has a {1} that will get lost on remove. Consider enabling the {0} instead.",
297
breakpointType.toLowerCase(),
298
logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition")
299
);
300
const enabledBreakpointDialogMessage = nls.localize(
301
'breakpointHasConditionEnabled',
302
"This {0} has a {1} that will get lost on remove. Consider disabling the {0} instead.",
303
breakpointType.toLowerCase(),
304
logPoint ? nls.localize('message', "message") : nls.localize('condition', "condition")
305
);
306
307
await this.dialogService.prompt({
308
type: severity.Info,
309
message: enabled ? enabledBreakpointDialogMessage : disabledBreakpointDialogMessage,
310
buttons: [
311
{
312
label: nls.localize({ key: 'removeLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Remove {0}", breakpointType),
313
run: () => breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId()))
314
},
315
{
316
label: 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),
317
run: () => breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp))
318
}
319
],
320
cancelButton: true
321
});
322
} else {
323
if (!enabled) {
324
breakpoints.forEach(bp => this.debugService.enableOrDisableBreakpoints(!enabled, bp));
325
} else {
326
breakpoints.forEach(bp => this.debugService.removeBreakpoints(bp.getId()));
327
}
328
}
329
} else if (canSetBreakpoints) {
330
if (e.event.middleButton) {
331
const action = this.configurationService.getValue<IDebugConfiguration>('debug').gutterMiddleClickAction;
332
if (action !== 'none') {
333
let context: BreakpointWidgetContext;
334
switch (action) {
335
case 'logpoint':
336
context = BreakpointWidgetContext.LOG_MESSAGE;
337
break;
338
case 'conditionalBreakpoint':
339
context = BreakpointWidgetContext.CONDITION;
340
break;
341
case 'triggeredBreakpoint':
342
context = BreakpointWidgetContext.TRIGGER_POINT;
343
}
344
this.showBreakpointWidget(lineNumber, undefined, context);
345
}
346
} else {
347
this.debugService.addBreakpoints(uri, [{ lineNumber }]);
348
}
349
}
350
}
351
}));
352
353
if (!(BrowserFeatures.pointerEvents && isSafari)) {
354
/**
355
* We disable the hover feature for Safari on iOS as
356
* 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.
357
* 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.
358
*/
359
this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => {
360
if (!this.debugService.getAdapterManager().hasEnabledDebuggers()) {
361
return;
362
}
363
364
let showBreakpointHintAtLineNumber = -1;
365
const model = this.editor.getModel();
366
if (model && e.target.position && (e.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN || e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS) && this.debugService.canSetBreakpointsIn(model) &&
367
this.marginFreeFromNonDebugDecorations(e.target.position.lineNumber)) {
368
const data = e.target.detail;
369
if (!data.isAfterLines) {
370
showBreakpointHintAtLineNumber = e.target.position.lineNumber;
371
}
372
}
373
this.ensureBreakpointHintDecoration(showBreakpointHintAtLineNumber);
374
}));
375
this.toDispose.push(this.editor.onMouseLeave(() => {
376
this.ensureBreakpointHintDecoration(-1);
377
}));
378
}
379
380
381
this.toDispose.push(this.editor.onDidChangeModel(async () => {
382
this.closeBreakpointWidget();
383
await this.setDecorations();
384
}));
385
this.toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(() => {
386
if (!this.ignoreBreakpointsChangeEvent && !this.setDecorationsScheduler.isScheduled()) {
387
this.setDecorationsScheduler.schedule();
388
}
389
}));
390
this.toDispose.push(this.debugService.onDidChangeState(() => {
391
// We need to update breakpoint decorations when state changes since the top stack frame and breakpoint decoration might change
392
if (!this.setDecorationsScheduler.isScheduled()) {
393
this.setDecorationsScheduler.schedule();
394
}
395
}));
396
this.toDispose.push(this.editor.onDidChangeModelDecorations(() => this.onModelDecorationsChanged()));
397
this.toDispose.push(this.configurationService.onDidChangeConfiguration(async (e) => {
398
if (e.affectsConfiguration('debug.showBreakpointsInOverviewRuler') || e.affectsConfiguration('debug.showInlineBreakpointCandidates')) {
399
await this.setDecorations();
400
}
401
}));
402
}
403
404
private getContextMenuActions(breakpoints: ReadonlyArray<IBreakpoint>, uri: URI, lineNumber: number, column?: number): IAction[] {
405
const actions: IAction[] = [];
406
407
if (breakpoints.length === 1) {
408
const breakpointType = breakpoints[0].logMessage ? nls.localize('logPoint', "Logpoint") : nls.localize('breakpoint', "Breakpoint");
409
actions.push(toAction({
410
id: 'debug.removeBreakpoint', label: nls.localize('removeBreakpoint', "Remove {0}", breakpointType), enabled: true, run: async () => {
411
await this.debugService.removeBreakpoints(breakpoints[0].getId());
412
}
413
}));
414
actions.push(toAction({
415
id: 'workbench.debug.action.editBreakpointAction',
416
label: nls.localize('editBreakpoint', "Edit {0}...", breakpointType),
417
enabled: true,
418
run: () => Promise.resolve(this.showBreakpointWidget(breakpoints[0].lineNumber, breakpoints[0].column))
419
})); actions.push(toAction({
420
id: `workbench.debug.viewlet.action.toggleBreakpoint`,
421
label: breakpoints[0].enabled ? nls.localize('disableBreakpoint', "Disable {0}", breakpointType) : nls.localize('enableBreakpoint', "Enable {0}", breakpointType),
422
enabled: true,
423
run: () => this.debugService.enableOrDisableBreakpoints(!breakpoints[0].enabled, breakpoints[0])
424
}));
425
} else if (breakpoints.length > 1) {
426
const sorted = breakpoints.slice().sort((first, second) => (first.column && second.column) ? first.column - second.column : 1);
427
actions.push(new SubmenuAction('debug.removeBreakpoints', nls.localize('removeBreakpoints', "Remove Breakpoints"), sorted.map(bp => toAction({
428
id: 'removeInlineBreakpoint',
429
label: bp.column ? nls.localize('removeInlineBreakpointOnColumn', "Remove Inline Breakpoint on Column {0}", bp.column) : nls.localize('removeLineBreakpoint', "Remove Line Breakpoint"),
430
enabled: true,
431
run: () => this.debugService.removeBreakpoints(bp.getId())
432
})))); actions.push(new SubmenuAction('debug.editBreakpoints', nls.localize('editBreakpoints', "Edit Breakpoints"), sorted.map(bp =>
433
toAction({
434
id: 'editBreakpoint',
435
label: bp.column ? nls.localize('editInlineBreakpointOnColumn', "Edit Inline Breakpoint on Column {0}", bp.column) : nls.localize('editLineBreakpoint', "Edit Line Breakpoint"),
436
enabled: true,
437
run: () => Promise.resolve(this.showBreakpointWidget(bp.lineNumber, bp.column))
438
})
439
))); actions.push(new SubmenuAction('debug.enableDisableBreakpoints', nls.localize('enableDisableBreakpoints', "Enable/Disable Breakpoints"), sorted.map(bp => toAction({
440
id: bp.enabled ? 'disableColumnBreakpoint' : 'enableColumnBreakpoint',
441
label: bp.enabled ? (bp.column ? nls.localize('disableInlineColumnBreakpoint', "Disable Inline Breakpoint on Column {0}", bp.column) : nls.localize('disableBreakpointOnLine', "Disable Line Breakpoint"))
442
: (bp.column ? nls.localize('enableBreakpoints', "Enable Inline Breakpoint on Column {0}", bp.column) : nls.localize('enableBreakpointOnLine', "Enable Line Breakpoint")),
443
enabled: true,
444
run: () => this.debugService.enableOrDisableBreakpoints(!bp.enabled, bp)
445
}))));
446
} else {
447
actions.push(toAction({
448
id: 'addBreakpoint',
449
label: nls.localize('addBreakpoint', "Add Breakpoint"),
450
enabled: true,
451
run: () => this.debugService.addBreakpoints(uri, [{ lineNumber, column }])
452
}));
453
actions.push(toAction({
454
id: 'addConditionalBreakpoint',
455
label: nls.localize('addConditionalBreakpoint', "Add Conditional Breakpoint..."),
456
enabled: true,
457
run: () => Promise.resolve(this.showBreakpointWidget(lineNumber, column, BreakpointWidgetContext.CONDITION))
458
}));
459
actions.push(toAction({
460
id: 'addLogPoint',
461
label: nls.localize('addLogPoint', "Add Logpoint..."),
462
enabled: true,
463
run: () => Promise.resolve(this.showBreakpointWidget(lineNumber, column, BreakpointWidgetContext.LOG_MESSAGE))
464
}));
465
actions.push(toAction({
466
id: 'addTriggeredBreakpoint',
467
label: nls.localize('addTriggeredBreakpoint', "Add Triggered Breakpoint..."),
468
enabled: true,
469
run: () => Promise.resolve(this.showBreakpointWidget(lineNumber, column, BreakpointWidgetContext.TRIGGER_POINT))
470
}));
471
}
472
473
if (this.debugService.state === State.Stopped) {
474
actions.push(new Separator());
475
actions.push(toAction({
476
id: 'runToLine',
477
label: nls.localize('runToLine', "Run to Line"),
478
enabled: true,
479
run: () => this.debugService.runTo(uri, lineNumber).catch(onUnexpectedError)
480
}));
481
} return actions;
482
}
483
484
private marginFreeFromNonDebugDecorations(line: number): boolean {
485
const decorations = this.editor.getLineDecorations(line);
486
if (decorations) {
487
for (const { options } of decorations) {
488
const clz = options.glyphMarginClassName;
489
if (!clz) {
490
continue;
491
}
492
const 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');
493
if (hasSomeActionableCodicon) {
494
return false;
495
}
496
}
497
}
498
499
return true;
500
}
501
502
private ensureBreakpointHintDecoration(showBreakpointHintAtLineNumber: number): void {
503
this.editor.changeDecorations((accessor) => {
504
if (this.breakpointHintDecoration) {
505
accessor.removeDecoration(this.breakpointHintDecoration);
506
this.breakpointHintDecoration = null;
507
}
508
if (showBreakpointHintAtLineNumber !== -1) {
509
this.breakpointHintDecoration = accessor.addDecoration({
510
startLineNumber: showBreakpointHintAtLineNumber,
511
startColumn: 1,
512
endLineNumber: showBreakpointHintAtLineNumber,
513
endColumn: 1
514
}, breakpointHelperDecoration
515
);
516
}
517
});
518
}
519
520
private async setDecorations(): Promise<void> {
521
if (!this.editor.hasModel()) {
522
return;
523
}
524
525
const setCandidateDecorations = (changeAccessor: IModelDecorationsChangeAccessor, desiredCandidatePositions: BreakpointsForLine[]) => {
526
const desiredCandidateDecorations = createCandidateDecorations(model, this.breakpointDecorations, desiredCandidatePositions);
527
const candidateDecorationIds = changeAccessor.deltaDecorations(this.candidateDecorations.map(c => c.decorationId), desiredCandidateDecorations);
528
this.candidateDecorations.forEach(candidate => {
529
candidate.inlineWidget.dispose();
530
});
531
this.candidateDecorations = candidateDecorationIds.map((decorationId, index) => {
532
const candidate = desiredCandidateDecorations[index];
533
// Candidate decoration has a breakpoint attached when a breakpoint is already at that location and we did not yet set a decoration there
534
// In practice this happens for the first breakpoint that was set on a line
535
// We could have also rendered this first decoration as part of desiredBreakpointDecorations however at that moment we have no location information
536
const icon = candidate.breakpoint ? getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), candidate.breakpoint, this.labelService, this.debugService.getModel()).icon : icons.breakpoint.disabled;
537
const contextMenuActions = () => this.getContextMenuActions(candidate.breakpoint ? [candidate.breakpoint] : [], activeCodeEditor.getModel().uri, candidate.range.startLineNumber, candidate.range.startColumn);
538
const inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, ThemeIcon.asClassName(icon), candidate.breakpoint, this.debugService, this.contextMenuService, contextMenuActions);
539
540
return {
541
decorationId,
542
inlineWidget
543
};
544
});
545
};
546
547
const activeCodeEditor = this.editor;
548
const model = activeCodeEditor.getModel();
549
const breakpoints = this.debugService.getModel().getBreakpoints({ uri: model.uri });
550
const debugSettings = this.configurationService.getValue<IDebugConfiguration>('debug');
551
const desiredBreakpointDecorations = this.instantiationService.invokeFunction(accessor => createBreakpointDecorations(accessor, model, breakpoints, this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), debugSettings.showBreakpointsInOverviewRuler));
552
553
// try to set breakpoint location candidates in the same changeDecorations()
554
// call to avoid flickering, if the DA responds reasonably quickly.
555
const session = this.debugService.getViewModel().focusedSession;
556
const desiredCandidatePositions = debugSettings.showInlineBreakpointCandidates && session ? requestBreakpointCandidateLocations(this.editor.getModel(), desiredBreakpointDecorations.map(bp => bp.range.startLineNumber), session) : Promise.resolve([]);
557
const desiredCandidatePositionsRaced = await Promise.race([desiredCandidatePositions, timeout(500).then(() => undefined)]);
558
if (desiredCandidatePositionsRaced === undefined) { // the timeout resolved first
559
desiredCandidatePositions.then(v => activeCodeEditor.changeDecorations(d => setCandidateDecorations(d, v)));
560
}
561
562
try {
563
this.ignoreDecorationsChangedEvent = true;
564
565
// Set breakpoint decorations
566
activeCodeEditor.changeDecorations((changeAccessor) => {
567
const decorationIds = changeAccessor.deltaDecorations(this.breakpointDecorations.map(bpd => bpd.decorationId), desiredBreakpointDecorations);
568
this.breakpointDecorations.forEach(bpd => {
569
bpd.inlineWidget?.dispose();
570
});
571
this.breakpointDecorations = decorationIds.map((decorationId, index) => {
572
let inlineWidget: InlineBreakpointWidget | undefined = undefined;
573
const breakpoint = breakpoints[index];
574
if (desiredBreakpointDecorations[index].options.before) {
575
const contextMenuActions = () => this.getContextMenuActions([breakpoint], activeCodeEditor.getModel().uri, breakpoint.lineNumber, breakpoint.column);
576
inlineWidget = new InlineBreakpointWidget(activeCodeEditor, decorationId, desiredBreakpointDecorations[index].options.glyphMarginClassName, breakpoint, this.debugService, this.contextMenuService, contextMenuActions);
577
}
578
579
return {
580
decorationId,
581
breakpoint,
582
range: desiredBreakpointDecorations[index].range,
583
inlineWidget
584
};
585
});
586
587
if (desiredCandidatePositionsRaced) {
588
setCandidateDecorations(changeAccessor, desiredCandidatePositionsRaced);
589
}
590
});
591
} finally {
592
this.ignoreDecorationsChangedEvent = false;
593
}
594
595
for (const d of this.breakpointDecorations) {
596
if (d.inlineWidget) {
597
this.editor.layoutContentWidget(d.inlineWidget);
598
}
599
}
600
}
601
602
private async onModelDecorationsChanged(): Promise<void> {
603
if (this.breakpointDecorations.length === 0 || this.ignoreDecorationsChangedEvent || !this.editor.hasModel()) {
604
// I have no decorations
605
return;
606
}
607
let somethingChanged = false;
608
const model = this.editor.getModel();
609
this.breakpointDecorations.forEach(breakpointDecoration => {
610
if (somethingChanged) {
611
return;
612
}
613
const newBreakpointRange = model.getDecorationRange(breakpointDecoration.decorationId);
614
if (newBreakpointRange && (!breakpointDecoration.range.equalsRange(newBreakpointRange))) {
615
somethingChanged = true;
616
breakpointDecoration.range = newBreakpointRange;
617
}
618
});
619
if (!somethingChanged) {
620
// nothing to do, my decorations did not change.
621
return;
622
}
623
624
const data = new Map<string, IBreakpointUpdateData>();
625
for (let i = 0, len = this.breakpointDecorations.length; i < len; i++) {
626
const breakpointDecoration = this.breakpointDecorations[i];
627
const decorationRange = model.getDecorationRange(breakpointDecoration.decorationId);
628
// check if the line got deleted.
629
if (decorationRange) {
630
// since we know it is collapsed, it cannot grow to multiple lines
631
if (breakpointDecoration.breakpoint) {
632
data.set(breakpointDecoration.breakpoint.getId(), {
633
lineNumber: decorationRange.startLineNumber,
634
column: breakpointDecoration.breakpoint.column ? decorationRange.startColumn : undefined,
635
});
636
}
637
}
638
}
639
640
try {
641
this.ignoreBreakpointsChangeEvent = true;
642
await this.debugService.updateBreakpoints(model.uri, data, true);
643
} finally {
644
this.ignoreBreakpointsChangeEvent = false;
645
}
646
}
647
648
// breakpoint widget
649
showBreakpointWidget(lineNumber: number, column: number | undefined, context?: BreakpointWidgetContext): void {
650
this.breakpointWidget?.dispose();
651
652
this.breakpointWidget = this.instantiationService.createInstance(BreakpointWidget, this.editor, lineNumber, column, context);
653
this.breakpointWidget.show({ lineNumber, column: 1 });
654
this.breakpointWidgetVisible.set(true);
655
}
656
657
closeBreakpointWidget(): void {
658
if (this.breakpointWidget) {
659
this.breakpointWidget.dispose();
660
this.breakpointWidget = undefined;
661
this.breakpointWidgetVisible.reset();
662
this.editor.focus();
663
}
664
}
665
666
dispose(): void {
667
this.breakpointWidget?.dispose();
668
this.setDecorationsScheduler.dispose();
669
this.editor.removeDecorations(this.breakpointDecorations.map(bpd => bpd.decorationId));
670
dispose(this.toDispose);
671
}
672
}
673
674
GutterActionsRegistry.registerGutterActionsGenerator(({ lineNumber, editor, accessor }, result) => {
675
const model = editor.getModel();
676
const debugService = accessor.get(IDebugService);
677
if (!model || !debugService.getAdapterManager().hasEnabledDebuggers() || !debugService.canSetBreakpointsIn(model)) {
678
return;
679
}
680
681
const breakpointEditorContribution = editor.getContribution<IBreakpointEditorContribution>(BREAKPOINT_EDITOR_CONTRIBUTION_ID);
682
if (!breakpointEditorContribution) {
683
return;
684
}
685
686
const actions = breakpointEditorContribution.getContextMenuActionsAtPosition(lineNumber, model);
687
688
for (const action of actions) {
689
result.push(action, '2_debug');
690
}
691
});
692
693
class InlineBreakpointWidget implements IContentWidget, IDisposable {
694
695
// editor.IContentWidget.allowEditorOverflow
696
allowEditorOverflow = false;
697
suppressMouseDown = true;
698
699
private domNode!: HTMLElement;
700
private range: Range | null;
701
private toDispose: IDisposable[] = [];
702
703
constructor(
704
private readonly editor: IActiveCodeEditor,
705
private readonly decorationId: string,
706
cssClass: string | null | undefined,
707
private readonly breakpoint: IBreakpoint | undefined,
708
private readonly debugService: IDebugService,
709
private readonly contextMenuService: IContextMenuService,
710
private readonly getContextMenuActions: () => IAction[]
711
) {
712
this.range = this.editor.getModel().getDecorationRange(decorationId);
713
this.toDispose.push(this.editor.onDidChangeModelDecorations(() => {
714
const model = this.editor.getModel();
715
const range = model.getDecorationRange(this.decorationId);
716
if (this.range && !this.range.equalsRange(range)) {
717
this.range = range;
718
this.editor.layoutContentWidget(this);
719
this.updateSize();
720
}
721
}));
722
this.create(cssClass);
723
724
this.editor.addContentWidget(this);
725
this.editor.layoutContentWidget(this);
726
}
727
728
private create(cssClass: string | null | undefined): void {
729
this.domNode = $('.inline-breakpoint-widget');
730
if (cssClass) {
731
this.domNode.classList.add(...cssClass.split(' '));
732
}
733
this.toDispose.push(dom.addDisposableListener(this.domNode, dom.EventType.CLICK, async e => {
734
switch (this.breakpoint?.enabled) {
735
case undefined:
736
await this.debugService.addBreakpoints(this.editor.getModel().uri, [{ lineNumber: this.range!.startLineNumber, column: this.range!.startColumn }]);
737
break;
738
case true:
739
await this.debugService.removeBreakpoints(this.breakpoint.getId());
740
break;
741
case false:
742
this.debugService.enableOrDisableBreakpoints(true, this.breakpoint);
743
break;
744
}
745
}));
746
this.toDispose.push(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, e => {
747
const event = new StandardMouseEvent(dom.getWindow(this.domNode), e);
748
const actions = this.getContextMenuActions();
749
this.contextMenuService.showContextMenu({
750
getAnchor: () => event,
751
getActions: () => actions,
752
getActionsContext: () => this.breakpoint,
753
onHide: () => disposeIfDisposable(actions)
754
});
755
}));
756
757
this.updateSize();
758
759
this.toDispose.push(this.editor.onDidChangeConfiguration(c => {
760
if (c.hasChanged(EditorOption.fontSize) || c.hasChanged(EditorOption.lineHeight)) {
761
this.updateSize();
762
}
763
}));
764
}
765
766
private updateSize() {
767
const lineHeight = this.range ? this.editor.getLineHeightForPosition(this.range.getStartPosition()) : this.editor.getOption(EditorOption.lineHeight);
768
this.domNode.style.height = `${lineHeight}px`;
769
this.domNode.style.width = `${Math.ceil(0.8 * lineHeight)}px`;
770
this.domNode.style.marginLeft = `4px`;
771
}
772
773
@memoize
774
getId(): string {
775
return generateUuid();
776
}
777
778
getDomNode(): HTMLElement {
779
return this.domNode;
780
}
781
782
getPosition(): IContentWidgetPosition | null {
783
if (!this.range) {
784
return null;
785
}
786
// Workaround: since the content widget can not be placed before the first column we need to force the left position
787
this.domNode.classList.toggle('line-start', this.range.startColumn === 1);
788
789
return {
790
position: { lineNumber: this.range.startLineNumber, column: this.range.startColumn - 1 },
791
preference: [ContentWidgetPositionPreference.EXACT]
792
};
793
}
794
795
dispose(): void {
796
this.editor.removeContentWidget(this);
797
dispose(this.toDispose);
798
}
799
}
800
801
registerThemingParticipant((theme, collector) => {
802
const scope = '.monaco-editor .glyph-margin-widgets, .monaco-workbench .debug-breakpoints, .monaco-workbench .disassembly-view, .monaco-editor .contentWidgets';
803
const debugIconBreakpointColor = theme.getColor(debugIconBreakpointForeground);
804
if (debugIconBreakpointColor) {
805
collector.addRule(`${scope} {
806
${icons.allBreakpoints.map(b => `${ThemeIcon.asCSSSelector(b.regular)}`).join(',\n ')},
807
${ThemeIcon.asCSSSelector(icons.debugBreakpointUnsupported)},
808
${ThemeIcon.asCSSSelector(icons.debugBreakpointHint)}:not([class*='codicon-debug-breakpoint']):not([class*='codicon-debug-stackframe']),
809
${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)}::after,
810
${ThemeIcon.asCSSSelector(icons.breakpoint.regular)}${ThemeIcon.asCSSSelector(icons.debugStackframe)}::after {
811
color: ${debugIconBreakpointColor} !important;
812
}
813
}`);
814
815
collector.addRule(`${scope} {
816
${ThemeIcon.asCSSSelector(icons.breakpoint.pending)} {
817
color: ${debugIconBreakpointColor} !important;
818
font-size: 12px !important;
819
}
820
}`);
821
}
822
823
const debugIconBreakpointDisabledColor = theme.getColor(debugIconBreakpointDisabledForeground);
824
if (debugIconBreakpointDisabledColor) {
825
collector.addRule(`${scope} {
826
${icons.allBreakpoints.map(b => ThemeIcon.asCSSSelector(b.disabled)).join(',\n ')} {
827
color: ${debugIconBreakpointDisabledColor};
828
}
829
}`);
830
}
831
832
const debugIconBreakpointUnverifiedColor = theme.getColor(debugIconBreakpointUnverifiedForeground);
833
if (debugIconBreakpointUnverifiedColor) {
834
collector.addRule(`${scope} {
835
${icons.allBreakpoints.map(b => ThemeIcon.asCSSSelector(b.unverified)).join(',\n ')} {
836
color: ${debugIconBreakpointUnverifiedColor};
837
}
838
}`);
839
}
840
841
const debugIconBreakpointCurrentStackframeForegroundColor = theme.getColor(debugIconBreakpointCurrentStackframeForeground);
842
if (debugIconBreakpointCurrentStackframeForegroundColor) {
843
collector.addRule(`
844
.monaco-editor .debug-top-stack-frame-column {
845
color: ${debugIconBreakpointCurrentStackframeForegroundColor} !important;
846
}
847
${scope} {
848
${ThemeIcon.asCSSSelector(icons.debugStackframe)} {
849
color: ${debugIconBreakpointCurrentStackframeForegroundColor} !important;
850
}
851
}
852
`);
853
}
854
855
const debugIconBreakpointStackframeFocusedColor = theme.getColor(debugIconBreakpointStackframeForeground);
856
if (debugIconBreakpointStackframeFocusedColor) {
857
collector.addRule(`${scope} {
858
${ThemeIcon.asCSSSelector(icons.debugStackframeFocused)} {
859
color: ${debugIconBreakpointStackframeFocusedColor} !important;
860
}
861
}`);
862
}
863
});
864
865
export const debugIconBreakpointForeground = registerColor('debugIcon.breakpointForeground', '#E51400', nls.localize('debugIcon.breakpointForeground', 'Icon color for breakpoints.'));
866
const debugIconBreakpointDisabledForeground = registerColor('debugIcon.breakpointDisabledForeground', '#848484', nls.localize('debugIcon.breakpointDisabledForeground', 'Icon color for disabled breakpoints.'));
867
const debugIconBreakpointUnverifiedForeground = registerColor('debugIcon.breakpointUnverifiedForeground', '#848484', nls.localize('debugIcon.breakpointUnverifiedForeground', 'Icon color for unverified breakpoints.'));
868
const debugIconBreakpointCurrentStackframeForeground = registerColor('debugIcon.breakpointCurrentStackframeForeground', { dark: '#FFCC00', light: '#BE8700', hcDark: '#FFCC00', hcLight: '#BE8700' }, nls.localize('debugIcon.breakpointCurrentStackframeForeground', 'Icon color for the current breakpoint stack frame.'));
869
const debugIconBreakpointStackframeForeground = registerColor('debugIcon.breakpointStackframeForeground', '#89D185', nls.localize('debugIcon.breakpointStackframeForeground', 'Icon color for all breakpoint stack frames.'));
870
871