Skip to content

Commit

Permalink
[debug] fix #5768: exception breakpoints support
Browse files Browse the repository at this point in the history
Signed-off-by: Anton Kosyakov <anton.kosyakov@typefox.io>
  • Loading branch information
akosyakov committed Jan 1, 2020
1 parent c889bca commit 05bd53e
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 7 deletions.
45 changes: 42 additions & 3 deletions packages/debug/src/browser/breakpoint/breakpoint-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { StorageService } from '@theia/core/lib/browser';
import { Marker } from '@theia/markers/lib/common/marker';
import { MarkerManager } from '@theia/markers/lib/browser/marker-manager';
import URI from '@theia/core/lib/common/uri';
import { SourceBreakpoint, BREAKPOINT_KIND } from './breakpoint-marker';
import { SourceBreakpoint, BREAKPOINT_KIND, ExceptionBreakpoint } from './breakpoint-marker';

export interface BreakpointsChangeEvent {
uri: URI
Expand All @@ -32,6 +32,8 @@ export interface BreakpointsChangeEvent {
@injectable()
export class BreakpointManager extends MarkerManager<SourceBreakpoint> {

static EXCEPTION_URI = new URI('debug:exception://');

protected readonly owner = 'breakpoint';

@inject(StorageService)
Expand Down Expand Up @@ -126,22 +128,58 @@ export class BreakpointManager extends MarkerManager<SourceBreakpoint> {
}
}

protected readonly exceptionBreakpoints = new Map<string, ExceptionBreakpoint>();

getExceptionBreakpoint(filter: string): ExceptionBreakpoint | undefined {
return this.exceptionBreakpoints.get(filter);
}

getExceptionBreakpoints(): IterableIterator<ExceptionBreakpoint> {
return this.exceptionBreakpoints.values();
}

setExceptionBreakpoints(exceptionBreakpoints: ExceptionBreakpoint[]): void {
const toRemove = new Set(this.exceptionBreakpoints.keys());
for (const exceptionBreakpoint of exceptionBreakpoints) {
const filter = exceptionBreakpoint.raw.filter;
toRemove.delete(filter);
this.exceptionBreakpoints.set(filter, exceptionBreakpoint);
}
for (const filter of toRemove) {
this.exceptionBreakpoints.delete(filter);
}
if (toRemove.size || exceptionBreakpoints.length) {
this.fireOnDidChangeMarkers(BreakpointManager.EXCEPTION_URI);
}
}

toggleExceptionBreakpoint(filter: string): void {
const breakpoint = this.getExceptionBreakpoint(filter);
if (breakpoint) {
breakpoint.enabled = !breakpoint.enabled;
this.fireOnDidChangeMarkers(BreakpointManager.EXCEPTION_URI);
}
}

async load(): Promise<void> {
const data = await this.storage.getData<BreakpointManager.Data>('breakpoints', {
breakpointsEnabled: true,
breakpoints: {}
breakpoints: {},
exceptionBreakpoints: []
});
this._breakpointsEnabled = data.breakpointsEnabled;
// tslint:disable-next-line:forin
for (const uri in data.breakpoints) {
this.setBreakpoints(new URI(uri), data.breakpoints[uri]);
}
this.setExceptionBreakpoints(data.exceptionBreakpoints);
}

save(): void {
const data: BreakpointManager.Data = {
breakpointsEnabled: this._breakpointsEnabled,
breakpoints: {}
breakpoints: {},
exceptionBreakpoints: [...this.exceptionBreakpoints.values()]
};
const uris = this.getUris();
for (const uri of uris) {
Expand All @@ -157,5 +195,6 @@ export namespace BreakpointManager {
breakpoints: {
[uri: string]: SourceBreakpoint[]
}
exceptionBreakpoints: ExceptionBreakpoint[]
}
}
18 changes: 17 additions & 1 deletion packages/debug/src/browser/breakpoint/breakpoint-marker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface SourceBreakpoint {
id: string;
uri: string;
enabled: boolean;
raw: DebugProtocol.SourceBreakpoint
raw: DebugProtocol.SourceBreakpoint;
}
export namespace SourceBreakpoint {
export function create(uri: URI, data: DebugProtocol.SourceBreakpoint, origin?: SourceBreakpoint): SourceBreakpoint {
Expand All @@ -49,3 +49,19 @@ export namespace BreakpointMarker {
return 'kind' in node && node.kind === BREAKPOINT_KIND;
}
}

export interface ExceptionBreakpoint {
enabled: boolean;
raw: DebugProtocol.ExceptionBreakpointsFilter;
}
export namespace ExceptionBreakpoint {
export function create(data: DebugProtocol.ExceptionBreakpointsFilter, origin?: ExceptionBreakpoint): ExceptionBreakpoint {
return {
enabled: origin ? origin.enabled : false,
raw: {
...(origin && origin.raw),
...data
}
};
}
}
21 changes: 20 additions & 1 deletion packages/debug/src/browser/debug-session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import URI from '@theia/core/lib/common/uri';
import { BreakpointManager } from './breakpoint/breakpoint-manager';
import { DebugSessionOptions, InternalDebugSessionOptions } from './debug-session-options';
import { DebugConfiguration } from '../common/debug-common';
import { SourceBreakpoint } from './breakpoint/breakpoint-marker';
import { SourceBreakpoint, ExceptionBreakpoint } from './breakpoint/breakpoint-marker';
import { FileSystem } from '@theia/filesystem/lib/common';
import { TerminalWidgetOptions, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';

Expand Down Expand Up @@ -282,6 +282,15 @@ export class DebugSession implements CompositeTreeElement {
protected initialized = false;
protected async configure(): Promise<void> {
await this.updateBreakpoints({ sourceModified: false });
if (this.capabilities.exceptionBreakpointFilters) {
const exceptionBreakpoints = [];
for (const filter of this.capabilities.exceptionBreakpointFilters) {
const origin = this.breakpoints.getExceptionBreakpoint(filter.filter);
exceptionBreakpoints.push(ExceptionBreakpoint.create(filter, origin));
}
this.breakpoints.setExceptionBreakpoints(exceptionBreakpoints);
}
await this.updateBreakpoints({ uri: BreakpointManager.EXCEPTION_URI, sourceModified: false });
if (this.capabilities.supportsConfigurationDoneRequest) {
await this.sendRequest('configurationDone', {});
}
Expand Down Expand Up @@ -543,6 +552,16 @@ export class DebugSession implements CompositeTreeElement {
return;
}
const { uri, sourceModified } = options;
if (uri && uri.toString() === BreakpointManager.EXCEPTION_URI.toString()) {
const filters = [];
for (const breakpoint of this.breakpoints.getExceptionBreakpoints()) {
if (breakpoint.enabled) {
filters.push(breakpoint.raw.filter);
}
}
await this.sendRequest('setExceptionBreakpoints', { filters });
return;
}
for (const affectedUri of this.getAffectedUris(uri)) {
const source = await this.toSource(affectedUri);
const all = this.breakpoints.findMarkers({ uri: affectedUri }).map(({ data }) =>
Expand Down
27 changes: 27 additions & 0 deletions packages/debug/src/browser/editor/debug-editor-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { SourceBreakpoint } from '../breakpoint/breakpoint-marker';
import { DebugEditor } from './debug-editor';
import { DebugHoverWidget, createDebugHoverWidgetContainer } from './debug-hover-widget';
import { DebugBreakpointWidget } from './debug-breakpoint-widget';
import { DebugExceptionWidget } from './debug-exception-widget';

export const DebugEditorModelFactory = Symbol('DebugEditorModelFactory');
export type DebugEditorModelFactory = (editor: DebugEditor) => DebugEditorModel;
Expand All @@ -37,6 +38,7 @@ export class DebugEditorModel implements Disposable {
const child = createDebugHoverWidgetContainer(parent, editor);
child.bind(DebugEditorModel).toSelf();
child.bind(DebugBreakpointWidget).toSelf();
child.bind(DebugExceptionWidget).toSelf();
return child;
}
static createModel(parent: interfaces.Container, editor: DebugEditor): DebugEditorModel {
Expand Down Expand Up @@ -77,12 +79,16 @@ export class DebugEditorModel implements Disposable {
@inject(DebugBreakpointWidget)
readonly breakpointWidget: DebugBreakpointWidget;

@inject(DebugExceptionWidget)
readonly exceptionWidget: DebugExceptionWidget;

@postConstruct()
protected init(): void {
this.uri = new URI(this.editor.getControl().getModel()!.uri.toString());
this.toDispose.pushAll([
this.hover,
this.breakpointWidget,
this.exceptionWidget,
this.editor.getControl().onMouseDown(event => this.handleMouseDown(event)),
this.editor.getControl().onMouseMove(event => this.handleMouseMove(event)),
this.editor.getControl().onMouseLeave(event => this.handleMouseLeave(event)),
Expand All @@ -99,6 +105,7 @@ export class DebugEditorModel implements Disposable {
}

protected readonly renderFrames = debounce(() => {
this.toggleExceptionWidget();
const decorations = this.createFrameDecorations();
this.frameDecorations = this.deltaDecorations(this.frameDecorations, decorations);
}, 100);
Expand Down Expand Up @@ -150,6 +157,26 @@ export class DebugEditorModel implements Disposable {
return decorations;
}

protected async toggleExceptionWidget(): Promise<void> {
const { currentFrame } = this.sessions;
if (!currentFrame ||
!currentFrame.source || currentFrame.source.uri.toString() !== this.uri.toString() ||
!currentFrame.raw.line || !currentFrame.raw.column) {
this.exceptionWidget.hide();
return;
}
const info = await currentFrame.thread.getExceptionInfo();
if (!info) {
this.exceptionWidget.hide();
return;
}
this.exceptionWidget.show({
info,
lineNumber: currentFrame.raw.line,
column: currentFrame.raw.column
});
}

render(): void {
this.renderBreakpoints();
this.renderCurrentBreakpoints();
Expand Down
82 changes: 82 additions & 0 deletions packages/debug/src/browser/editor/debug-exception-widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/********************************************************************************
* Copyright (C) 2019 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { injectable, inject, postConstruct } from 'inversify';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { MonacoEditorZoneWidget } from '@theia/monaco/lib/browser/monaco-editor-zone-widget';
import { DebugEditor } from './debug-editor';
import { DebugExceptionInfo } from '../model/debug-thread';

export interface ShowDebugExceptionParams {
info: DebugExceptionInfo
lineNumber: number
column: number
}

@injectable()
export class DebugExceptionWidget implements Disposable {

@inject(DebugEditor)
readonly editor: DebugEditor;

protected zone: MonacoEditorZoneWidget;

protected readonly toDispose = new DisposableCollection();

@postConstruct()
protected async init(): Promise<void> {
this.toDispose.push(this.zone = new MonacoEditorZoneWidget(this.editor.getControl()));
this.zone.containerNode.classList.add('theia-debug-exception-widget');
this.toDispose.push(Disposable.create(() => ReactDOM.unmountComponentAtNode(this.zone.containerNode)));
}

dispose(): void {
this.toDispose.dispose();
}

show({ info, lineNumber, column }: ShowDebugExceptionParams): void {
this.render(info);

const fontInfo = this.editor.getControl().getConfiguration().fontInfo;
this.zone.containerNode.style.fontSize = `${fontInfo.fontSize}px`;
this.zone.containerNode.style.lineHeight = `${fontInfo.lineHeight}px`;

const afterLineNumber = lineNumber;
const afterColumn = column;
const heightInLines = 0;
this.zone.show({ showFrame: true, afterLineNumber, afterColumn, heightInLines, frameWidth: 1 });
}

hide(): void {
this.zone.hide();
}

protected render(info: DebugExceptionInfo): void {
const stackTrace = info.details && info.details.stackTrace;
ReactDOM.render(<React.Fragment>
<div className='title'>{info.id ? `Exception has occurred: ${info.id}` : 'Exception has occurred.'}</div>
{info.description && <div className='description'>{info.description}</div>}
{stackTrace && <div className='stack-trace'>{stackTrace}</div>}
</React.Fragment>, this.zone.containerNode, () => {
const lineHeight = this.editor.getControl().getConfiguration().lineHeight;
const heightInLines = Math.ceil(this.zone.containerNode.offsetHeight / lineHeight);
this.zone.layout(heightInLines);
});
}

}
23 changes: 23 additions & 0 deletions packages/debug/src/browser/model/debug-thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ export class DebugThreadData {
readonly stoppedDetails: StoppedDetails | undefined;
}

export interface DebugExceptionInfo {
id?: string
description?: string
details?: DebugProtocol.ExceptionDetails
}

export class DebugThread extends DebugThreadData implements TreeElement {

protected readonly onDidChangedEmitter = new Emitter<void>();
Expand Down Expand Up @@ -93,6 +99,23 @@ export class DebugThread extends DebugThreadData implements TreeElement {
return this.session.sendRequest('pause', this.toArgs());
}

async getExceptionInfo(): Promise<DebugExceptionInfo | undefined> {
if (this.stoppedDetails && this.stoppedDetails.reason === 'exception') {
if (this.session.capabilities.supportsExceptionInfoRequest) {
const response = await this.session.sendRequest('exceptionInfo', this.toArgs());
return {
id: response.body.exceptionId,
description: response.body.description,
details: response.body.details
};
}
return {
description: this.stoppedDetails.text
};
}
return undefined;
}

get supportsTerminate(): boolean {
return !!this.session.capabilities.supportsTerminateThreadsRequest;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/debug/src/browser/style/debug-bright.useable.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@
--theia-debug-configure: url('configure.svg');
--theia-debug-repl: url('repl.svg');
--breakpoints-activate-url: url('breakpoints-activate.svg');
--theia-debug-exception-widget-border-color: #a31515;
--theia-debug-exception-widget-background-color: #f1dfde;
}
2 changes: 2 additions & 0 deletions packages/debug/src/browser/style/debug-dark.useable.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@
--theia-debug-configure: url('configure-inverse.svg');
--theia-debug-repl: url('repl-inverse.svg');
--breakpoints-activate-url: url('breakpoints-activate-inverse.svg');
--theia-debug-exception-widget-border-color: #a31515;
--theia-debug-exception-widget-background-color: #420b0d;
}
23 changes: 23 additions & 0 deletions packages/debug/src/browser/style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -362,3 +362,26 @@
color: var(--theia-statusBar-debuggingForeground);
border-top: var(--theia-border-width) solid var(--theia-statusBar-debuggingBorder);
}

/** Exception Widget */
.monaco-editor .zone-widget .zone-widget-container.theia-debug-exception-widget {
color: var(--theia-content-font-color0);
font-size: var(--theia-code-font-size);
line-height: var(--theia-code-line-height);
font-family: var(--theia-code-font-family);
border-top-color: var(--theia-debug-exception-widget-border-color);
border-bottom-color: var(--theia-debug-exception-widget-border-color);
background-color: var(--theia-debug-exception-widget-background-color);
padding: var(--theia-ui-padding) calc(var(--theia-ui-padding)*1.5);
white-space: pre-wrap;
user-select: text;
overflow: hidden;
}

.theia-debug-exception-widget .title {
font-weight: bold;
}

.theia-debug-exception-widget .stack-trace {
margin-top: 0.5em;
}
Loading

0 comments on commit 05bd53e

Please sign in to comment.