diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 0f2291ac19ec844c9c80c1862d38f454d42de878..3b21464790d5e2b6d0282e6fb49623ee144dce09 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -247,4 +247,84 @@ declare module 'vscode' { */ provideCodeActions2?(document: TextDocument, range: Range, context: CodeActionContext, token: CancellationToken): ProviderResult<(Command | CodeAction)[]>; } + + export namespace debug { + + /** + * List of breakpoints. + * + * @readonly + */ + export let breakpoints: Breakpoint[]; + + /** + * An event that is emitted when a breakpoint is added, removed, or changed. + */ + export const onDidChangeBreakpoints: Event; + } + + /** + * An event describing a change to the set of [breakpoints](#debug.Breakpoint). + */ + export interface BreakpointsChangeEvent { + /** + * Added breakpoints. + */ + readonly added: Breakpoint[]; + + /** + * Removed breakpoints. + */ + readonly removed: Breakpoint[]; + + /** + * Changed breakpoints. + */ + readonly changed: Breakpoint[]; + } + + export interface Breakpoint { + /** + * Type of breakpoint. + */ + readonly type: 'source' | 'function'; + /** + * Is breakpoint enabled. + */ + readonly enabled: boolean; + /** + * An optional expression for conditional breakpoints. + */ + readonly condition?: string; + /** + * An optional expression that controls how many hits of the breakpoint are ignored. + */ + readonly hitCondition?: string; + } + + export interface SourceBreakpoint extends Breakpoint { + /** + * Breakpoint type 'source'. + */ + readonly type: 'source'; + /** + * The source to which this breakpoint is attached. + */ + readonly source: Uri; + /** + * The line and character position of the breakpoint. + */ + readonly location: Position; + } + + export interface FunctionBreakpoint extends Breakpoint { + /** + * Breakpoint type 'function'. + */ + readonly type: 'function'; + /** + * The name of the function to which this breakpoint is attached. + */ + readonly functionName: string; + } } diff --git a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts index 3556feea70b81dfee3a4eea322200d88c471c499..b2847ca666e6abc1d10f9b7cb6111c9c4240a879 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDebugService.ts @@ -6,10 +6,10 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import uri from 'vs/base/common/uri'; -import { IDebugService, IConfig, IDebugConfigurationProvider } from 'vs/workbench/parts/debug/common/debug'; +import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint } from 'vs/workbench/parts/debug/common/debug'; import { TPromise } from 'vs/base/common/winjs.base'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, IExtHostContext } from '../node/extHost.protocol'; +import { ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext, IExtHostContext, IBreakpointsDelta, ISourceBreakpointData, IFunctionBreakpointData } from '../node/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import severity from 'vs/base/common/severity'; @@ -18,6 +18,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { private _proxy: ExtHostDebugServiceShape; private _toDispose: IDisposable[]; + private _breakpointEventsActive: boolean; constructor( extHostContext: IExtHostContext, @@ -35,6 +36,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { this._proxy.$acceptDebugSessionActiveChanged(undefined); } })); + this._toDispose.push(debugService.onDidCustomEvent(event => { if (event && event.sessionId) { const process = this.debugService.getModel().getProcesses().filter(p => p.getId() === event.sessionId).pop(); @@ -49,6 +51,75 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape { this._toDispose = dispose(this._toDispose); } + public $startBreakpointEvents(): TPromise { + + if (!this._breakpointEventsActive) { + this._breakpointEventsActive = true; + + // set up a handler to send more + this._toDispose.push(this.debugService.getModel().onDidChangeBreakpoints(e => { + if (e) { + const delta: IBreakpointsDelta = {}; + if (e.added) { + delta.added = this.toWire(e.added); + } + if (e.removed) { + delta.removed = e.removed.map(x => x.getId()); + } + if (e.changed) { + delta.changed = this.toWire(e.changed); + } + + if (delta.added || delta.removed || delta.changed) { + this._proxy.$acceptBreakpointsDelta(delta); + } + } + })); + + // send all breakpoints + const bps = this.debugService.getModel().getBreakpoints(); + const fbps = this.debugService.getModel().getFunctionBreakpoints(); + if (bps.length > 0 || fbps.length > 0) { + this._proxy.$acceptBreakpointsDelta({ + added: this.toWire(bps).concat(this.toWire(fbps)) + }); + } + } + + return TPromise.wrap(undefined); + } + + private toWire(bps: (IBreakpoint | IFunctionBreakpoint)[]): (ISourceBreakpointData | IFunctionBreakpointData)[] { + + return bps.map(bp => { + if ('name' in bp) { + const fbp = bp; + return { + type: 'function', + id: bp.getId(), + enabled: bp.enabled, + functionName: fbp.name, + hitCondition: bp.hitCondition, + /* condition: bp.condition */ + }; + } else { + const sbp = bp; + return { + type: 'source', + id: bp.getId(), + enabled: bp.enabled, + condition: sbp.condition, + hitCondition: bp.hitCondition, + sourceUriStr: sbp.uri.toString(), + location: { + line: sbp.lineNumber, + character: sbp.column + } + }; + } + }); + } + public $registerDebugConfigurationProvider(debugType: string, hasProvide: boolean, hasResolve: boolean, handle: number): TPromise { const provider = { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 22201ab41be5c1d4639f45553d6eab0925b79499..ca3986a30e20a63aabf39899e6948e2d7c35dda8 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -515,8 +515,8 @@ export function createApiFactory( get activeDebugConsole() { return extHostDebugService.activeDebugConsole; }, - startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration) { - return extHostDebugService.startDebugging(folder, nameOrConfig); + get breakpoints() { + return extHostDebugService.breakpoints; }, onDidStartDebugSession(listener, thisArg?, disposables?) { return extHostDebugService.onDidStartDebugSession(listener, thisArg, disposables); @@ -530,6 +530,12 @@ export function createApiFactory( onDidReceiveDebugSessionCustomEvent(listener, thisArg?, disposables?) { return extHostDebugService.onDidReceiveDebugSessionCustomEvent(listener, thisArg, disposables); }, + onDidChangeBreakpoints: proposedApiFunction(extension, (listener, thisArgs?, disposables?) => { + return extHostDebugService.onDidChangeBreakpoints(listener, thisArgs, disposables); + }), + startDebugging(folder: vscode.WorkspaceFolder | undefined, nameOrConfig: string | vscode.DebugConfiguration) { + return extHostDebugService.startDebugging(folder, nameOrConfig); + }, registerDebugConfigurationProvider(debugType: string, provider: vscode.DebugConfigurationProvider) { return extHostDebugService.registerDebugConfigurationProvider(debugType, provider); } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 23849bca3b1687a4c5ec44c0dd8f37806a48853a..1159a392db9ff260a87e7267e2640973396379e3 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -419,6 +419,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { $startDebugging(folder: URI | undefined, nameOrConfig: string | vscode.DebugConfiguration): TPromise; $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): TPromise; $appendDebugConsole(value: string): TPromise; + $startBreakpointEvents(): TPromise; } export interface MainThreadWindowShape extends IDisposable { @@ -623,6 +624,31 @@ export interface ExtHostTaskShape { $provideTasks(handle: number): TPromise; } +export interface IBreakpointData { + type: 'source' | 'function'; + id: string; + enabled: boolean; + condition?: string; + hitCondition?: string; +} + +export interface ISourceBreakpointData extends IBreakpointData { + type: 'source'; + sourceUriStr: string; + location: vscode.Position; +} + +export interface IFunctionBreakpointData extends IBreakpointData { + type: 'function'; + functionName: string; +} + +export interface IBreakpointsDelta { + added?: (ISourceBreakpointData | IFunctionBreakpointData)[]; + removed?: string[]; + changed?: (ISourceBreakpointData | IFunctionBreakpointData)[]; +} + export interface ExtHostDebugServiceShape { $resolveDebugConfiguration(handle: number, folder: URI | undefined, debugConfiguration: any): TPromise; $provideDebugConfigurations(handle: number, folder: URI | undefined): TPromise; @@ -630,6 +656,7 @@ export interface ExtHostDebugServiceShape { $acceptDebugSessionTerminated(id: DebugSessionUUID, type: string, name: string): void; $acceptDebugSessionActiveChanged(id: DebugSessionUUID | undefined, type?: string, name?: string): void; $acceptDebugSessionCustomEvent(id: DebugSessionUUID, type: string, name: string, event: any): void; + $acceptBreakpointsDelta(delat: IBreakpointsDelta): void; } diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 4e9bd4c99b439fb21335d95921767ec2de91fa5c..552669070ec4811ed8fcd10547f33f0421acd59b 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -7,7 +7,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import Event, { Emitter } from 'vs/base/common/event'; import { asWinJsPromise } from 'vs/base/common/async'; -import { MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, IMainContext } from 'vs/workbench/api/node/extHost.protocol'; +import { MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, IMainContext, IBreakpointsDelta, ISourceBreakpointData, IFunctionBreakpointData } from 'vs/workbench/api/node/extHost.protocol'; import { ExtHostWorkspace } from 'vs/workbench/api/node/extHostWorkspace'; import * as vscode from 'vscode'; @@ -43,6 +43,11 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { private _activeDebugConsole: ExtHostDebugConsole; get activeDebugConsole(): ExtHostDebugConsole { return this._activeDebugConsole; } + private _breakpoints: Map; + private _breakpointEventsActive: boolean; + + private _onDidChangeBreakpoints: Emitter; + constructor(mainContext: IMainContext, workspace: ExtHostWorkspace) { @@ -58,7 +63,84 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { this._debugServiceProxy = mainContext.get(MainContext.MainThreadDebugService); + this._onDidChangeBreakpoints = new Emitter({ + onFirstListenerAdd: () => { + this.startBreakpoints(); + } + }); + this._activeDebugConsole = new ExtHostDebugConsole(this._debugServiceProxy); + + this._breakpoints = new Map(); + this._breakpointEventsActive = false; + } + + private startBreakpoints() { + if (!this._breakpointEventsActive) { + this._breakpointEventsActive = true; + this._debugServiceProxy.$startBreakpointEvents(); + } + } + + get onDidChangeBreakpoints(): Event { + return this._onDidChangeBreakpoints.event; + } + + get breakpoints(): vscode.Breakpoint[] { + + this.startBreakpoints(); + + const result: vscode.Breakpoint[] = []; + this._breakpoints.forEach(bp => result.push(bp)); + return result; + } + + public $acceptBreakpointsDelta(delta: IBreakpointsDelta): void { + + let a: vscode.Breakpoint[] = []; + let r: vscode.Breakpoint[] = []; + let c: vscode.Breakpoint[] = []; + + if (delta.added) { + a = delta.added.map(bpd => { + const id = bpd.id; + this._breakpoints.set(id, this.fromWire(bpd)); + return bpd; + }); + } + + if (delta.removed) { + r = delta.removed.map(id => { + const bp = this._breakpoints.get(id); + if (bp) { + this._breakpoints.delete(id); + } + return bp; + }); + } + + if (delta.changed) { + c = delta.changed.map(bpd => { + const id = bpd.id; + this._breakpoints.set(id, this.fromWire(bpd)); + return bpd; + }); + } + + this._onDidChangeBreakpoints.fire(Object.freeze({ + added: Object.freeze(a || []), + removed: Object.freeze(r || []), + changed: Object.freeze(c || []) + })); + } + + private fromWire(bp: ISourceBreakpointData | IFunctionBreakpointData): vscode.Breakpoint { + delete bp.id; + if (bp.type === 'source') { + (bp).source = URI.parse(bp.sourceUriStr); + delete bp.sourceUriStr; + } + return bp; } public registerDebugConfigurationProvider(type: string, provider: vscode.DebugConfigurationProvider): vscode.Disposable { @@ -87,7 +169,6 @@ export class ExtHostDebugService implements ExtHostDebugServiceShape { return asWinJsPromise(token => handler.provideDebugConfigurations(this.getFolder(folderUri), token)); } - public $resolveDebugConfiguration(handle: number, folderUri: URI | undefined, debugConfiguration: vscode.DebugConfiguration): TPromise { let handler = this._handlers.get(handle); if (!handler) { diff --git a/src/vs/workbench/parts/debug/common/debug.ts b/src/vs/workbench/parts/debug/common/debug.ts index 9c8991d8dcad2d113f026bf3d68146978cf5bf67..912b7131c86a1f1ab9b447dc26b715d5c2067f4d 100644 --- a/src/vs/workbench/parts/debug/common/debug.ts +++ b/src/vs/workbench/parts/debug/common/debug.ts @@ -299,12 +299,21 @@ export interface IModel extends ITreeElement { getWatchExpressions(): IExpression[]; getReplElements(): IReplElement[]; - onDidChangeBreakpoints: Event; + onDidChangeBreakpoints: Event; onDidChangeCallStack: Event; onDidChangeWatchExpressions: Event; onDidChangeReplElements: Event; } +/** + * An event describing a change to the set of [breakpoints](#debug.Breakpoint). + */ +export interface IBreakpointsChangeEvent { + added?: (IBreakpoint | IFunctionBreakpoint)[]; + removed?: (IBreakpoint | IFunctionBreakpoint)[]; + changed?: (IBreakpoint | IFunctionBreakpoint)[]; +} + // Debug enums export enum State { diff --git a/src/vs/workbench/parts/debug/common/debugModel.ts b/src/vs/workbench/parts/debug/common/debugModel.ts index d072bf6d488a519e0ab2aee0bdc72fe5a24f62d9..25bd10054af4affd36963e4545c361fb9fe9eb79 100644 --- a/src/vs/workbench/parts/debug/common/debugModel.ts +++ b/src/vs/workbench/parts/debug/common/debugModel.ts @@ -20,7 +20,7 @@ import { ISuggestion } from 'vs/editor/common/modes'; import { Position } from 'vs/editor/common/core/position'; import { ITreeElement, IExpression, IExpressionContainer, IProcess, IStackFrame, IExceptionBreakpoint, IBreakpoint, IFunctionBreakpoint, IModel, IReplElementSource, - IConfig, ISession, IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IRawBreakpoint, IExceptionInfo, IReplElement, ProcessState + IConfig, ISession, IThread, IRawModelUpdate, IScope, IRawStoppedDetails, IEnablement, IRawBreakpoint, IExceptionInfo, IReplElement, ProcessState, IBreakpointsChangeEvent } from 'vs/workbench/parts/debug/common/debug'; import { Source } from 'vs/workbench/parts/debug/common/debugSource'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -739,7 +739,7 @@ export class Model implements IModel { private toDispose: lifecycle.IDisposable[]; private replElements: IReplElement[]; private schedulers = new Map(); - private _onDidChangeBreakpoints: Emitter; + private _onDidChangeBreakpoints: Emitter; private _onDidChangeCallStack: Emitter; private _onDidChangeWatchExpressions: Emitter; private _onDidChangeREPLElements: Emitter; @@ -754,7 +754,7 @@ export class Model implements IModel { this.processes = []; this.replElements = []; this.toDispose = []; - this._onDidChangeBreakpoints = new Emitter(); + this._onDidChangeBreakpoints = new Emitter(); this._onDidChangeCallStack = new Emitter(); this._onDidChangeWatchExpressions = new Emitter(); this._onDidChangeREPLElements = new Emitter(); @@ -780,7 +780,7 @@ export class Model implements IModel { this._onDidChangeCallStack.fire(); } - public get onDidChangeBreakpoints(): Event { + public get onDidChangeBreakpoints(): Event { return this._onDidChangeBreakpoints.event; } @@ -868,8 +868,9 @@ export class Model implements IModel { this.breakpoints = this.breakpoints.concat(newBreakpoints); this.breakpointsActivated = true; this.sortAndDeDup(); + if (fireEvent) { - this._onDidChangeBreakpoints.fire(); + this._onDidChangeBreakpoints.fire({ added: newBreakpoints }); } return newBreakpoints; @@ -877,10 +878,11 @@ export class Model implements IModel { public removeBreakpoints(toRemove: IBreakpoint[]): void { this.breakpoints = this.breakpoints.filter(bp => !toRemove.some(toRemove => toRemove.getId() === bp.getId())); - this._onDidChangeBreakpoints.fire(); + this._onDidChangeBreakpoints.fire({ removed: toRemove }); } public updateBreakpoints(data: { [id: string]: DebugProtocol.Breakpoint }): void { + const updated: IBreakpoint[] = []; this.breakpoints.forEach(bp => { const bpData = data[bp.getId()]; if (bpData) { @@ -892,10 +894,11 @@ export class Model implements IModel { bp.idFromAdapter = bpData.id; bp.message = bpData.message; bp.adapterData = bpData.source ? bpData.source.adapterData : bp.adapterData; + updated.push(bp); } }); this.sortAndDeDup(); - this._onDidChangeBreakpoints.fire(); + this._onDidChangeBreakpoints.fire({ changed: updated }); } private sortAndDeDup(): void { @@ -913,36 +916,56 @@ export class Model implements IModel { } public setEnablement(element: IEnablement, enable: boolean): void { + + const changed: (IBreakpoint | IFunctionBreakpoint)[] = []; + if (element.enabled !== enable && (element instanceof Breakpoint || element instanceof FunctionBreakpoint)) { + changed.push(element); + } + element.enabled = enable; if (element instanceof Breakpoint && !element.enabled) { const breakpoint = element; breakpoint.verified = false; } - this._onDidChangeBreakpoints.fire(); + this._onDidChangeBreakpoints.fire({ changed: changed }); } public enableOrDisableAllBreakpoints(enable: boolean): void { + + const changed: (IBreakpoint | IFunctionBreakpoint)[] = []; + this.breakpoints.forEach(bp => { + if (bp.enabled !== enable) { + changed.push(bp); + } bp.enabled = enable; if (!enable) { bp.verified = false; } }); - this.functionBreakpoints.forEach(fbp => fbp.enabled = enable); + this.functionBreakpoints.forEach(fbp => { + if (fbp.enabled !== enable) { + changed.push(fbp); + } + fbp.enabled = enable; + }); - this._onDidChangeBreakpoints.fire(); + this._onDidChangeBreakpoints.fire({ changed: changed }); } public addFunctionBreakpoint(functionName: string): FunctionBreakpoint { const newFunctionBreakpoint = new FunctionBreakpoint(functionName, true, null); this.functionBreakpoints.push(newFunctionBreakpoint); - this._onDidChangeBreakpoints.fire(); + this._onDidChangeBreakpoints.fire({ added: [newFunctionBreakpoint] }); return newFunctionBreakpoint; } public updateFunctionBreakpoints(data: { [id: string]: { name?: string, verified?: boolean; id?: number; hitCondition?: string } }): void { + + const changed: IFunctionBreakpoint[] = []; + this.functionBreakpoints.forEach(fbp => { const fbpData = data[fbp.getId()]; if (fbpData) { @@ -950,15 +973,25 @@ export class Model implements IModel { fbp.verified = fbpData.verified; fbp.idFromAdapter = fbpData.id; fbp.hitCondition = fbpData.hitCondition; + + changed.push(fbp); } }); - this._onDidChangeBreakpoints.fire(); + this._onDidChangeBreakpoints.fire({ changed: changed }); } public removeFunctionBreakpoints(id?: string): void { - this.functionBreakpoints = id ? this.functionBreakpoints.filter(fbp => fbp.getId() !== id) : []; - this._onDidChangeBreakpoints.fire(); + + let removed: IFunctionBreakpoint[]; + if (id) { + removed = this.functionBreakpoints.filter(fbp => fbp.getId() === id); + this.functionBreakpoints = this.functionBreakpoints.filter(fbp => fbp.getId() !== id); + } else { + removed = this.functionBreakpoints; + this.functionBreakpoints = []; + } + this._onDidChangeBreakpoints.fire({ removed: removed }); } public getReplElements(): IReplElement[] {