// ***************************************************************************** // Copyright (C) 2018 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-only WITH Classpath-exception-2.0 // ***************************************************************************** import debounce from 'p-debounce'; import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; import { Disposable, DisposableCollection, Event, Emitter, deepClone, nls } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; import { DebugSession, DebugState } from '../debug-session'; import { DebugSessionManager } from '../debug-session-manager'; import { DebugThread } from '../model/debug-thread'; import { DebugStackFrame } from '../model/debug-stack-frame'; import { DebugSourceBreakpoint } from '../model/debug-source-breakpoint'; import { DebugWatchExpression } from './debug-watch-expression'; import { DebugWatchManager } from '../debug-watch-manager'; import { DebugFunctionBreakpoint } from '../model/debug-function-breakpoint'; import { DebugInstructionBreakpoint } from '../model/debug-instruction-breakpoint'; import { DebugSessionOptionsBase } from '../debug-session-options'; import { BreakpointManager } from '../breakpoint/breakpoint-manager'; import { DebugExceptionBreakpoint } from './debug-exception-breakpoint'; import { DebugDataBreakpoint } from '../model/debug-data-breakpoint'; import { DebugVariable } from '../console/debug-console-items'; @injectable() export class DebugViewModel implements Disposable { protected readonly onDidChangeEmitter = new Emitter(); readonly onDidChange: Event = this.onDidChangeEmitter.event; protected fireDidChange(): void { this.refreshWatchExpressions(); this.onDidChangeEmitter.fire(undefined); } protected readonly onDidChangeBreakpointsEmitter = new Emitter(); readonly onDidChangeBreakpoints: Event = this.onDidChangeBreakpointsEmitter.event; protected fireDidChangeBreakpoints(uri: URI): void { this.onDidChangeBreakpointsEmitter.fire(uri); } protected readonly onDidResolveLazyVariableEmitter = new Emitter(); readonly onDidResolveLazyVariable: Event = this.onDidResolveLazyVariableEmitter.event; protected fireDidResolveLazyVariable(variable: DebugVariable): void { this.onDidResolveLazyVariableEmitter.fire(variable); } protected readonly _watchExpressions = new Map(); protected readonly onDidChangeWatchExpressionsEmitter = new Emitter(); readonly onDidChangeWatchExpressions = this.onDidChangeWatchExpressionsEmitter.event; protected fireDidChangeWatchExpressions(): void { this.onDidChangeWatchExpressionsEmitter.fire(undefined); } protected readonly toDispose = new DisposableCollection( this.onDidChangeEmitter, this.onDidChangeBreakpointsEmitter, this.onDidResolveLazyVariableEmitter, this.onDidChangeWatchExpressionsEmitter, ); @inject(DebugSessionManager) protected readonly manager: DebugSessionManager; @inject(BreakpointManager) protected readonly breakpointManager: BreakpointManager; @inject(DebugWatchManager) protected readonly watch: DebugWatchManager; get sessions(): IterableIterator { return this.manager.sessions[Symbol.iterator](); } get sessionCount(): number { return this.manager.sessions.length; } get session(): DebugSession | undefined { return this.currentSession; } get id(): string { return this.session && this.session.id || '-1'; } get label(): string { return this.session && this.session.label || nls.localize('theia/debug/unknownSession', 'Unknown Session'); } @postConstruct() protected init(): void { this.toDispose.push(this.manager.onDidChangeActiveDebugSession(() => { this.fireDidChange(); })); this.toDispose.push(this.manager.onDidChange(current => { // Always fire change to update views, even if session is not current // This ensures threads view updates for all sessions this.fireDidChange(); })); this.toDispose.push(this.breakpointManager.onDidChangeMarkers(uri => { this.fireDidChangeBreakpoints(uri); })); this.toDispose.push(this.manager.onDidResolveLazyVariable(({ session, variable }) => { if (session === this.currentSession) { this.fireDidResolveLazyVariable(variable); } })); this.updateWatchExpressions(); this.toDispose.push(this.watch.onDidChange(() => this.updateWatchExpressions())); } dispose(): void { this.toDispose.dispose(); } get currentSession(): DebugSession | undefined { return this.manager.currentSession; } set currentSession(currentSession: DebugSession | undefined) { this.manager.currentSession = currentSession; } get state(): DebugState { const { currentSession } = this; return currentSession && currentSession.state || DebugState.Inactive; } get currentThread(): DebugThread | undefined { const { currentSession } = this; return currentSession && currentSession.currentThread; } get currentFrame(): DebugStackFrame | undefined { const { currentThread } = this; return currentThread && currentThread.currentFrame; } get breakpoints(): readonly DebugSourceBreakpoint[] { return this.breakpointManager.getBreakpoints(); } get functionBreakpoints(): readonly DebugFunctionBreakpoint[] { return this.breakpointManager.getFunctionBreakpoints(); } get instructionBreakpoints(): readonly DebugInstructionBreakpoint[] { return this.breakpointManager.getInstructionBreakpoints(); } get exceptionBreakpoints(): readonly DebugExceptionBreakpoint[] { return this.breakpointManager.getExceptionBreakpoints() .filter(candidate => this.currentSession ? candidate.isEnabledForSession(this.currentSession.id) : candidate.isPersistentlyVisible()); } get dataBreakpoints(): readonly DebugDataBreakpoint[] { return this.breakpointManager.getDataBreakpoints(); } async start(options: Partial> = {}): Promise { const { session } = this; if (!session) { return; } const optionsCopy = deepClone(session.options); const newSession = await this.manager.start(Object.assign(optionsCopy, options)); if (newSession) { this.fireDidChange(); } } async restart(): Promise { const { session } = this; if (!session) { return; } await this.manager.restartSession(session); this.fireDidChange(); } async terminate(): Promise { this.manager.terminateSession(); } get watchExpressions(): IterableIterator { return this._watchExpressions.values(); } async addWatchExpression(expression: string = ''): Promise { const watchExpression: DebugWatchExpression = new DebugWatchExpression({ id: Number.MAX_SAFE_INTEGER, expression, session: () => this.currentSession, remove: () => this.removeWatchExpression(watchExpression), onDidChange: () => { /* no-op */ }, }); await watchExpression.open(); if (!watchExpression.expression) { return undefined; } const id = this.watch.addWatchExpression(watchExpression.expression); return this._watchExpressions.get(id); } removeWatchExpressions(): void { this.watch.removeWatchExpressions(); } removeWatchExpression(expression: DebugWatchExpression): void { this.watch.removeWatchExpression(expression.id); } protected updateWatchExpressions(): void { let added = false; const toRemove = new Set(this._watchExpressions.keys()); for (const [id, expression] of this.watch.watchExpressions) { toRemove.delete(id); if (!this._watchExpressions.has(id)) { added = true; const watchExpression: DebugWatchExpression = new DebugWatchExpression({ id, expression, session: () => this.currentSession, remove: () => this.removeWatchExpression(watchExpression), onDidChange: () => this.fireDidChangeWatchExpressions() }); this._watchExpressions.set(id, watchExpression); watchExpression.evaluate(); } } for (const id of toRemove) { this._watchExpressions.delete(id); } if (added || toRemove.size) { this.fireDidChangeWatchExpressions(); } } protected refreshWatchExpressionsQueue = Promise.resolve(); protected refreshWatchExpressions = debounce(() => { this.refreshWatchExpressionsQueue = this.refreshWatchExpressionsQueue.then(async () => { try { await Promise.all(Array.from(this.watchExpressions).map(expr => expr.evaluate())); } catch (e) { console.error('Failed to refresh watch expressions: ', e); } }); }, 50); }