Skip to content

Commit

Permalink
Add condition editing UI to breakpoint filters
Browse files Browse the repository at this point in the history
fixes #111227
  • Loading branch information
isidorn committed Nov 27, 2020
1 parent 6795d76 commit cd9be28
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 39 deletions.
143 changes: 126 additions & 17 deletions src/vs/workbench/contrib/debug/browser/breakpointsView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export class BreakpointsView extends ViewPane {
this.list = <WorkbenchList<BreakpointItem>>this.instantiationService.createInstance(WorkbenchList, 'Breakpoints', container, delegate, [
this.instantiationService.createInstance(BreakpointsRenderer),
new ExceptionBreakpointsRenderer(this.debugService),
new ExceptionBreakpointInputRenderer(this.debugService, this.contextViewService, this.themeService),
this.instantiationService.createInstance(FunctionBreakpointsRenderer),
this.instantiationService.createInstance(DataBreakpointsRenderer),
new FunctionBreakpointInputRenderer(this.debugService, this.contextViewService, this.themeService, this.labelService)
Expand Down Expand Up @@ -136,9 +137,9 @@ export class BreakpointsView extends ViewPane {
if (element instanceof Breakpoint) {
openBreakpointSource(element, e.sideBySide, e.editorOptions.preserveFocus || false, e.editorOptions.pinned || !e.editorOptions.preserveFocus, this.debugService, this.editorService);
}
if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && element instanceof FunctionBreakpoint && element !== this.debugService.getViewModel().getSelectedFunctionBreakpoint()) {
if (e.browserEvent instanceof MouseEvent && e.browserEvent.detail === 2 && element instanceof FunctionBreakpoint && element !== this.debugService.getViewModel().getSelectedBreakpoint()) {
// double click
this.debugService.getViewModel().setSelectedFunctionBreakpoint(element);
this.debugService.getViewModel().setSelectedBreakpoint(element);
this.onBreakpointsChange();
}
}));
Expand Down Expand Up @@ -201,12 +202,19 @@ export class BreakpointsView extends ViewPane {
}
}
} else {
this.debugService.getViewModel().setSelectedFunctionBreakpoint(element);
this.debugService.getViewModel().setSelectedBreakpoint(element);
this.onBreakpointsChange();
}
}));
actions.push(new Separator());
}
if (element instanceof ExceptionBreakpoint && element.supportsCondition) {
actions.push(new Action('workbench.action.debug.editExceptionBreakpointCondition', nls.localize('editCondition', "Edit Condition..."), '', true, async () => {
this.debugService.getViewModel().setSelectedBreakpoint(element);
this.onBreakpointsChange();
}));
actions.push(new Separator());
}

actions.push(new RemoveBreakpointAction(RemoveBreakpointAction.ID, nls.localize('removeBreakpoint', "Remove {0}", breakpointType), this.debugService));

Expand Down Expand Up @@ -286,14 +294,18 @@ class BreakpointsDelegate implements IListVirtualDelegate<BreakpointItem> {
return BreakpointsRenderer.ID;
}
if (element instanceof FunctionBreakpoint) {
const selected = this.debugService.getViewModel().getSelectedFunctionBreakpoint();
const selected = this.debugService.getViewModel().getSelectedBreakpoint();
if (!element.name || (selected && selected.getId() === element.getId())) {
return FunctionBreakpointInputRenderer.ID;
}

return FunctionBreakpointsRenderer.ID;
}
if (element instanceof ExceptionBreakpoint) {
const selected = this.debugService.getViewModel().getSelectedBreakpoint();
if (selected && selected.getId() === element.getId()) {
return ExceptionBreakpointInputRenderer.ID;
}
return ExceptionBreakpointsRenderer.ID;
}
if (element instanceof DataBreakpoint) {
Expand Down Expand Up @@ -321,7 +333,11 @@ interface IBreakpointTemplateData extends IBaseBreakpointWithIconTemplateData {
filePath: HTMLElement;
}

interface IInputTemplateData {
interface IExceptionBreakpointTemplateData extends IBaseBreakpointTemplateData {
condition: HTMLElement;
}

interface IFunctionBreakpointInputTemplateData {
inputBox: InputBox;
checkbox: HTMLInputElement;
icon: HTMLElement;
Expand All @@ -330,6 +346,14 @@ interface IInputTemplateData {
toDispose: IDisposable[];
}

interface IExceptionBreakpointInputTemplateData {
inputBox: InputBox;
checkbox: HTMLInputElement;
breakpoint: IExceptionBreakpoint;
reactedOnEvent: boolean;
toDispose: IDisposable[];
}

class BreakpointsRenderer implements IListRenderer<IBreakpoint, IBreakpointTemplateData> {

constructor(
Expand Down Expand Up @@ -395,7 +419,7 @@ class BreakpointsRenderer implements IListRenderer<IBreakpoint, IBreakpointTempl
}
}

class ExceptionBreakpointsRenderer implements IListRenderer<IExceptionBreakpoint, IBaseBreakpointTemplateData> {
class ExceptionBreakpointsRenderer implements IListRenderer<IExceptionBreakpoint, IExceptionBreakpointTemplateData> {

constructor(
private debugService: IDebugService
Expand All @@ -409,8 +433,8 @@ class ExceptionBreakpointsRenderer implements IListRenderer<IExceptionBreakpoint
return ExceptionBreakpointsRenderer.ID;
}

renderTemplate(container: HTMLElement): IBaseBreakpointTemplateData {
const data: IBreakpointTemplateData = Object.create(null);
renderTemplate(container: HTMLElement): IExceptionBreakpointTemplateData {
const data: IExceptionBreakpointTemplateData = Object.create(null);
data.breakpoint = dom.append(container, $('.breakpoint'));

data.checkbox = createCheckbox();
Expand All @@ -422,19 +446,22 @@ class ExceptionBreakpointsRenderer implements IListRenderer<IExceptionBreakpoint
dom.append(data.breakpoint, data.checkbox);

data.name = dom.append(data.breakpoint, $('span.name'));
data.condition = dom.append(data.breakpoint, $('span.condition'));
data.breakpoint.classList.add('exception');

return data;
}

renderElement(exceptionBreakpoint: IExceptionBreakpoint, index: number, data: IBaseBreakpointTemplateData): void {
renderElement(exceptionBreakpoint: IExceptionBreakpoint, index: number, data: IExceptionBreakpointTemplateData): void {
data.context = exceptionBreakpoint;
data.name.textContent = exceptionBreakpoint.label || `${exceptionBreakpoint.filter} exceptions`;
data.breakpoint.title = data.name.textContent;
data.checkbox.checked = exceptionBreakpoint.enabled;
data.condition.textContent = exceptionBreakpoint.condition || '';
data.condition.title = nls.localize('expressionCondition', "Expression condition: {0}", exceptionBreakpoint.condition);
}

disposeTemplate(templateData: IBaseBreakpointTemplateData): void {
disposeTemplate(templateData: IExceptionBreakpointTemplateData): void {
dispose(templateData.toDispose);
}
}
Expand Down Expand Up @@ -551,7 +578,7 @@ class DataBreakpointsRenderer implements IListRenderer<DataBreakpoint, IBaseBrea
}
}

class FunctionBreakpointInputRenderer implements IListRenderer<IFunctionBreakpoint, IInputTemplateData> {
class FunctionBreakpointInputRenderer implements IListRenderer<IFunctionBreakpoint, IFunctionBreakpointInputTemplateData> {

constructor(
private debugService: IDebugService,
Expand All @@ -568,8 +595,8 @@ class FunctionBreakpointInputRenderer implements IListRenderer<IFunctionBreakpoi
return FunctionBreakpointInputRenderer.ID;
}

renderTemplate(container: HTMLElement): IInputTemplateData {
const template: IInputTemplateData = Object.create(null);
renderTemplate(container: HTMLElement): IFunctionBreakpointInputTemplateData {
const template: IFunctionBreakpointInputTemplateData = Object.create(null);

const breakpoint = dom.append(container, $('.breakpoint'));
template.icon = $('.icon');
Expand All @@ -588,7 +615,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer<IFunctionBreakpoi
const wrapUp = (renamed: boolean) => {
if (!template.reactedOnEvent) {
template.reactedOnEvent = true;
this.debugService.getViewModel().setSelectedFunctionBreakpoint(undefined);
this.debugService.getViewModel().setSelectedBreakpoint(undefined);
if (inputBox.value && (renamed || template.breakpoint.name)) {
this.debugService.renameFunctionBreakpoint(template.breakpoint.getId(), renamed ? inputBox.value : template.breakpoint.name);
} else {
Expand Down Expand Up @@ -620,7 +647,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer<IFunctionBreakpoi
return template;
}

renderElement(functionBreakpoint: FunctionBreakpoint, _index: number, data: IInputTemplateData): void {
renderElement(functionBreakpoint: FunctionBreakpoint, _index: number, data: IFunctionBreakpointInputTemplateData): void {
data.breakpoint = functionBreakpoint;
data.reactedOnEvent = false;
const { icon, message } = getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), functionBreakpoint, this.labelService);
Expand All @@ -636,7 +663,89 @@ class FunctionBreakpointInputRenderer implements IListRenderer<IFunctionBreakpoi
}, 0);
}

disposeTemplate(templateData: IInputTemplateData): void {
disposeTemplate(templateData: IFunctionBreakpointInputTemplateData): void {
dispose(templateData.toDispose);
}
}

class ExceptionBreakpointInputRenderer implements IListRenderer<IExceptionBreakpoint, IExceptionBreakpointInputTemplateData> {

constructor(
private debugService: IDebugService,
private contextViewService: IContextViewService,
private themeService: IThemeService
) {
// noop
}

static readonly ID = 'exceptionbreakpointinput';

get templateId() {
return ExceptionBreakpointInputRenderer.ID;
}

renderTemplate(container: HTMLElement): IExceptionBreakpointInputTemplateData {
const template: IExceptionBreakpointInputTemplateData = Object.create(null);

const breakpoint = dom.append(container, $('.breakpoint'));
breakpoint.classList.add('exception');
template.checkbox = createCheckbox();

dom.append(breakpoint, template.checkbox);
const inputBoxContainer = dom.append(breakpoint, $('.inputBoxContainer'));
const inputBox = new InputBox(inputBoxContainer, this.contextViewService, {
placeholder: nls.localize('exceptionBreakpointPlaceholder', "Break when expression evaluates to true"),
ariaLabel: nls.localize('exceptionBreakpointAriaLabel', "Type exception breakpoint condition")
});
const styler = attachInputBoxStyler(inputBox, this.themeService);
const toDispose: IDisposable[] = [inputBox, styler];

const wrapUp = (success: boolean) => {
if (!template.reactedOnEvent) {
template.reactedOnEvent = true;
this.debugService.getViewModel().setSelectedBreakpoint(undefined);
let newCondition = template.breakpoint.condition;
if (success) {
newCondition = inputBox.value !== '' ? inputBox.value : undefined;
}
this.debugService.setExceptionBreakpointCondition(template.breakpoint, newCondition);
}
};

toDispose.push(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => {
const isEscape = e.equals(KeyCode.Escape);
const isEnter = e.equals(KeyCode.Enter);
if (isEscape || isEnter) {
e.preventDefault();
e.stopPropagation();
wrapUp(isEnter);
}
}));
toDispose.push(dom.addDisposableListener(inputBox.inputElement, 'blur', () => {
// Need to react with a timeout on the blur event due to possible concurent splices #56443
setTimeout(() => {
wrapUp(true);
});
}));

template.inputBox = inputBox;
template.toDispose = toDispose;
return template;
}

renderElement(exceptionBreakpoint: ExceptionBreakpoint, _index: number, data: IExceptionBreakpointInputTemplateData): void {
data.breakpoint = exceptionBreakpoint;
data.reactedOnEvent = false;
data.checkbox.checked = exceptionBreakpoint.enabled;
data.checkbox.disabled = true;
data.inputBox.value = exceptionBreakpoint.condition || '';
setTimeout(() => {
data.inputBox.focus();
data.inputBox.select();
}, 0);
}

disposeTemplate(templateData: IExceptionBreakpointInputTemplateData): void {
dispose(templateData.toDispose);
}
}
Expand Down Expand Up @@ -763,7 +872,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated:
messages.push(nls.localize('logMessage', "Log Message: {0}", breakpoint.logMessage));
}
if (breakpoint.condition) {
messages.push(nls.localize('expression', "Expression: {0}", breakpoint.condition));
messages.push(nls.localize('expression', "Expression condition: {0}", breakpoint.condition));
}
if (breakpoint.hitCondition) {
messages.push(nls.localize('hitCount', "Hit Count: {0}", breakpoint.hitCondition));
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/debug/browser/debugActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export class AddFunctionBreakpointAction extends AbstractDebugAction {
}

protected isEnabled(_: State): boolean {
return !this.debugService.getViewModel().getSelectedFunctionBreakpoint()
return !this.debugService.getViewModel().getSelectedBreakpoint()
&& this.debugService.getModel().getFunctionBreakpoints().every(fbp => !!fbp.name);
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/vs/workbench/contrib/debug/browser/debugService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { IAction, Action } from 'vs/base/common/actions';
import { deepClone, equals } from 'vs/base/common/objects';
import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IDebugModel, IEnablement, IBreakpoint, IBreakpointData, ICompound, IStackFrame, getStateLabel, IDebugSessionOptions, CONTEXT_DEBUG_UX, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, IGlobalConfig, CALLSTACK_VIEW_ID, IAdapterManager } from 'vs/workbench/contrib/debug/common/debug';
import { IDebugService, State, IDebugSession, CONTEXT_DEBUG_TYPE, CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_MODE, IThread, IDebugConfiguration, VIEWLET_ID, IConfig, ILaunch, IViewModel, IConfigurationManager, IDebugModel, IEnablement, IBreakpoint, IBreakpointData, ICompound, IStackFrame, getStateLabel, IDebugSessionOptions, CONTEXT_DEBUG_UX, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, IGlobalConfig, CALLSTACK_VIEW_ID, IAdapterManager, IExceptionBreakpoint } from 'vs/workbench/contrib/debug/common/debug';
import { getExtensionHostDebugSession } from 'vs/workbench/contrib/debug/common/debugUtils';
import { isErrorWithActions } from 'vs/base/common/errorsWithActions';
import { RunOnceScheduler } from 'vs/base/common/async';
Expand Down Expand Up @@ -918,7 +918,7 @@ export class DebugService implements IDebugService {

addFunctionBreakpoint(name?: string, id?: string): void {
const newFunctionBreakpoint = this.model.addFunctionBreakpoint(name || '', id);
this.viewModel.setSelectedFunctionBreakpoint(newFunctionBreakpoint);
this.viewModel.setSelectedBreakpoint(newFunctionBreakpoint);
}

async renameFunctionBreakpoint(id: string, newFunctionName: string): Promise<void> {
Expand Down Expand Up @@ -946,6 +946,12 @@ export class DebugService implements IDebugService {
await this.sendDataBreakpoints();
}

async setExceptionBreakpointCondition(exceptionBreakpoint: IExceptionBreakpoint, condition: string | undefined): Promise<void> {
this.model.setExceptionBreakpointCondition(exceptionBreakpoint, condition);
this.debugStorage.storeBreakpoints(this.model);
await this.sendExceptionBreakpoints();
}

async sendAllBreakpoints(session?: IDebugSession): Promise<any> {
await Promise.all(distinct(this.model.getBreakpoints(), bp => bp.uri.toString()).map(bp => this.sendBreakpoints(bp.uri, false, session)));
await this.sendFunctionBreakpoints(session);
Expand Down
13 changes: 12 additions & 1 deletion src/vs/workbench/contrib/debug/browser/debugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,18 @@ export class DebugSession implements IDebugSession {
}

if (this.raw.readyForBreakpoints) {
await this.raw.setExceptionBreakpoints({ filters: exbpts.map(exb => exb.filter) });
const args: DebugProtocol.SetExceptionBreakpointsArguments = this.capabilities.supportsExceptionFilterOptions ? {
filters: [],
filterOptions: exbpts.map(exb => {
if (exb.condition) {
return { filterId: exb.filter, condition: exb.condition };
}

return { filterId: exb.filter };
})
} : { filters: exbpts.map(exb => exb.filter) };

await this.raw.setExceptionBreakpoints(args);
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/contrib/debug/browser/media/debugViewlet.css
Original file line number Diff line number Diff line change
Expand Up @@ -318,10 +318,10 @@
justify-content: center;
}

.debug-pane .debug-breakpoints .breakpoint > .file-path {
.debug-pane .debug-breakpoints .breakpoint > .file-path,
.debug-pane .debug-breakpoints .breakpoint.exception > .condition {
opacity: 0.7;
font-size: 0.9em;
margin-left: 0.8em;
margin-left: 0.9em;
flex: 1;
text-overflow: ellipsis;
overflow: hidden;
Expand Down
7 changes: 5 additions & 2 deletions src/vs/workbench/contrib/debug/common/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ export interface IFunctionBreakpoint extends IBaseBreakpoint {
export interface IExceptionBreakpoint extends IEnablement {
readonly filter: string;
readonly label: string;
readonly condition: string | undefined;
}

export interface IDataBreakpoint extends IBaseBreakpoint {
Expand Down Expand Up @@ -436,9 +437,9 @@ export interface IViewModel extends ITreeElement {
readonly focusedStackFrame: IStackFrame | undefined;

getSelectedExpression(): IExpression | undefined;
getSelectedFunctionBreakpoint(): IFunctionBreakpoint | undefined;
getSelectedBreakpoint(): IFunctionBreakpoint | IExceptionBreakpoint | undefined;
setSelectedExpression(expression: IExpression | undefined): void;
setSelectedFunctionBreakpoint(functionBreakpoint: IFunctionBreakpoint | undefined): void;
setSelectedBreakpoint(functionBreakpoint: IFunctionBreakpoint | IExceptionBreakpoint | undefined): void;
updateViews(): void;

isMultiSessionView(): boolean;
Expand Down Expand Up @@ -865,6 +866,8 @@ export interface IDebugService {
*/
removeDataBreakpoints(id?: string): Promise<void>;

setExceptionBreakpointCondition(breakpoint: IExceptionBreakpoint, condition: string | undefined): Promise<void>;

/**
* Sends all breakpoints to the passed session.
* If session is not passed, sends all breakpoints to each session.
Expand Down

0 comments on commit cd9be28

Please sign in to comment.