Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for breakpoint dependencies #166202

Merged
merged 17 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/vs/base/common/codicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
168 changes: 122 additions & 46 deletions src/vs/workbench/contrib/debug/browser/breakpointWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IPrivateBreakpointWidgetService>('privateBreakpointWidgetService');
Expand Down Expand Up @@ -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 = '';
Expand All @@ -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,
Expand All @@ -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 });

Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
}
}

Expand All @@ -189,17 +199,13 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi

protected _fillContainer(container: HTMLElement): void {
this.setCssClass('breakpoint-widget');
const selectBox = new SelectBox(<ISelectOptionItem[]>[{ 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(<ISelectOptionItem[]>[{ 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');
Expand All @@ -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 => <ISelectOptionItem>{ 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 });
Expand Down Expand Up @@ -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) {
Expand All @@ -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<string, IBreakpointUpdateData>();
data.set(this.breakpoint.getId(), {
condition,
hitCondition,
logMessage
logMessage,
triggeredBy
});
this.debugService.updateBreakpoints(this.breakpoint.originalUri, data, false).then(undefined, onUnexpectedError);
} else {
Expand All @@ -350,7 +425,8 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi
enabled: true,
condition,
hitCondition,
logMessage
logMessage,
triggeredBy
}]);
}
}
Expand Down
Loading
Loading