// ***************************************************************************** // 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-only WITH Classpath-exception-2.0 // ***************************************************************************** import * as React from '@theia/core/shared/react'; import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol'; import URI from '@theia/core/lib/common/uri'; import { CommandService } from '@theia/core/lib/common'; import { LabelProvider, DISABLED_CLASS, OpenerService, TreeWidget } from '@theia/core/lib/browser'; import { TreeElement } from '@theia/core/lib/browser/source-tree'; import { SelectableTreeNode } from '@theia/core/lib/browser/tree/tree-selection'; import { BaseBreakpoint } from '../breakpoint/breakpoint-marker'; import { BreakpointManager } from '../breakpoint/breakpoint-manager'; import { nls } from '@theia/core'; export interface DebugBreakpointOptions { readonly labelProvider: LabelProvider; readonly breakpoints: BreakpointManager; readonly openerService: OpenerService; readonly commandService: CommandService; } export class DebugBreakpointDecoration { readonly className: string; readonly message: string[]; } export type BPCapabilities = Required>; export interface BPSessionData extends BPCapabilities, DebugProtocol.Breakpoint { sessionId: string; } export abstract class DebugBreakpoint implements TreeElement { readonly labelProvider: LabelProvider; readonly breakpoints: BreakpointManager; readonly openerService: OpenerService; readonly commandService: CommandService; protected _raw?: BPSessionData; protected readonly sessionData = new Map(); protected treeWidget?: TreeWidget; constructor( readonly uri: URI, options: DebugBreakpointOptions ) { this.labelProvider = options.labelProvider; this.breakpoints = options.breakpoints; this.openerService = options.openerService; this.commandService = options.commandService; } abstract get origin(): T; get raw(): BPSessionData | undefined { return this._raw; } update(sessionId: string, data?: Omit): boolean { if (!data) { if (!this.sessionData.has(sessionId)) { return false; } this.sessionData.delete(sessionId); } else { const toSet = { ...data, sessionId }; this.sessionData.set(sessionId, toSet); } const verifiedLocations = new Map(); this.sessionData.forEach(bp => bp.verified && verifiedLocations.set(`${bp.line}:${bp.column}:${bp.instructionReference}`, bp)); if (verifiedLocations.size === 1) { // Exactly one verified location across all sessions — use it. this._raw = verifiedLocations.values().next().value; } else if (verifiedLocations.size === 0) { // No session has verified; pick the first session's data (if any) so // that capability flags and unverified messages are still available. this._raw = this.sessionData.values().next().value; } else { // Multiple sessions verified at different locations. Following // VSCode, we set _raw to undefined so that the breakpoint falls // back to its user-set position and shows as verified (the // default when no resolved data exists). Callers that need a // specific session's view can use getDebugProtocolBreakpoint(). this._raw = undefined; } return true; } getIdForSession(sessionId: string): number | undefined { return this.sessionData.get(sessionId)?.id; } /** Copied from https://github.com/microsoft/vscode/blob/8934b59d4aa696b6f51ac9bf2eeae8bbac5dac03/src/vs/workbench/contrib/debug/common/debugModel.ts#L953-L971 */ getDebugProtocolBreakpoint( sessionId: string, ): DebugProtocol.Breakpoint | undefined { const data = this.sessionData.get(sessionId); if (data) { const bp: DebugProtocol.Breakpoint = { id: data.id, verified: data.verified, message: data.message, source: data.source, line: data.line, column: data.column, endLine: data.endLine, endColumn: data.endColumn, instructionReference: data.instructionReference, offset: data.offset, }; return bp; } return undefined; } get id(): string { return this.origin.id; } get enabled(): boolean { return this.breakpoints.breakpointsEnabled && this.origin.enabled; } /** * True when at least one session has sent data for this breakpoint * (regardless of whether it was verified). */ get installed(): boolean { return this.sessionData.size > 0; } /** * When resolved session data exists, reflects the adapter's answer. * Otherwise returns `true`: either no session has weighed in yet (we * haven't been told otherwise) or multiple sessions verified at * different locations (`_raw` was cleared to fall back to the user-set * position). Matches VSCode's default-true semantics. */ get verified(): boolean { return this._raw ? this._raw.verified : true; } get message(): string { return this.raw && this.raw.message || ''; } abstract setEnabled(enabled: boolean): void; abstract remove(): void; protected readonly setBreakpointEnabled = (event: React.ChangeEvent) => { this.setEnabled(event.target.checked); }; render(host: TreeWidget): React.ReactNode { this.treeWidget = host; const classNames = ['theia-source-breakpoint']; if (!this.isEnabled()) { classNames.push(DISABLED_CLASS); } const decoration = this.getDecoration(); return
{this.doRender()}
; } protected isEnabled(): boolean { return this.breakpoints.breakpointsEnabled && this.verified; } protected abstract doRender(): React.ReactNode; getDecoration(): DebugBreakpointDecoration { if (!this.enabled) { return this.getDisabledBreakpointDecoration(); } if (this.installed && !this.verified) { return this.getUnverifiedBreakpointDecoration(); } return this.doGetDecoration(); } protected getUnverifiedBreakpointDecoration(): DebugBreakpointDecoration { const decoration = this.getBreakpointDecoration(); return { className: decoration.className + '-unverified', message: [this.message || nls.localize('theia/debug/unverifiedBreakpoint', 'Unverified {0}', decoration.message[0])] }; } protected getDisabledBreakpointDecoration(message?: string): DebugBreakpointDecoration { const decoration = this.getBreakpointDecoration(); return { className: decoration.className + '-disabled', message: [message || nls.localize('theia/debug/disabledBreakpoint', 'Disabled {0}', decoration.message[0])] }; } protected doGetDecoration(messages: string[] = []): DebugBreakpointDecoration { if (this.message) { if (messages.length) { messages[messages.length - 1].concat(', ' + this.message); } else { messages.push(this.message); } } return this.getBreakpointDecoration(messages); } protected abstract getBreakpointDecoration(message?: string[]): DebugBreakpointDecoration; protected async selectInTree(): Promise { if (this.treeWidget?.model && SelectableTreeNode.is(this)) { this.treeWidget.model.selectNode(this); } } }