diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e823c8293f3d..aae495d0f2434 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - [core] `handleDefault`, `handleElectronDefault` method no longer called in `BrowserMainMenuFactory.registerMenu()`, `DynamicMenuWidget.buildSubMenus()` or `ElectronMainMenuFactory.fillSubmenus()`. Override the respective calling function rather than `handleDefault`. The argument to each of the three methods listed above is now `MenuNode` and not `CompositeMenuNode`, and the methods are truly recursive and called on entire menu tree. `ActionMenuNode.action` removed; access relevant field on `ActionMenuNode.command`, `.when` etc. [#11290](https://github.com/eclipse-theia/theia/pull/11290) - [core] renamed `CommonCommands.NEW_FILE` to `CommonCommands.NEW_UNTITLED_FILE` [#11429](https://github.com/eclipse-theia/theia/pull/11429) - [plugin-ext] `CodeEditorWidgetUtil` moved to `packages/plugin-ext/src/main/browser/menus/vscode-theia-menu-mappings.ts`. `MenusContributionPointHandler` extensively refactored. See PR description for details. [#11290](https://github.com/eclipse-theia/theia/pull/11290) +- [plugin] added support for `DebugProtocolBreakpoint` and `DebugProtocolSource` [#10011](https://github.com/eclipse-theia/theia/issues/10011) - Contributed on behalf of STMicroelectronics ## v1.27.0 - 6/30/2022 diff --git a/packages/debug/src/browser/debug-session.tsx b/packages/debug/src/browser/debug-session.tsx index 846a8694dcacf..86feb898b29bb 100644 --- a/packages/debug/src/browser/debug-session.tsx +++ b/packages/debug/src/browser/debug-session.tsx @@ -541,6 +541,17 @@ export class DebugSession implements CompositeTreeElement { return result; } + getBreakpoint(id: string): DebugBreakpoint | undefined { + for (const breakpoints of this._breakpoints.values()) { + const breakpoint = breakpoints.find(b => b.id === id); + if (breakpoint) { + return breakpoint; + } + + } + return undefined; + } + protected clearBreakpoints(): void { const uris = [...this._breakpoints.keys()]; this._breakpoints.clear(); diff --git a/packages/debug/src/browser/model/debug-source.ts b/packages/debug/src/browser/model/debug-source.ts index fa2010e2d522d..8a80d9f2a073a 100644 --- a/packages/debug/src/browser/model/debug-source.ts +++ b/packages/debug/src/browser/model/debug-source.ts @@ -20,6 +20,7 @@ import URI from '@theia/core/lib/common/uri'; import { DebugProtocol } from 'vscode-debugprotocol/lib/debugProtocol'; import { DebugSession } from '../debug-session'; import { URI as Uri } from '@theia/core/shared/vscode-uri'; +import { DEBUG_SCHEME, SCHEME_PATTERN } from '../../common/debug-uri-utils'; export class DebugSourceData { readonly raw: DebugProtocol.Source; @@ -58,7 +59,7 @@ export class DebugSource extends DebugSourceData { } get inMemory(): boolean { - return this.uri.scheme === DebugSource.SCHEME; + return this.uri.scheme === DEBUG_SCHEME; } get name(): string { @@ -75,16 +76,16 @@ export class DebugSource extends DebugSourceData { return this.labelProvider.getLongName(this.uri); } - static SCHEME = 'debug'; - static SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/; + static SCHEME = DEBUG_SCHEME; + static SCHEME_PATTERN = SCHEME_PATTERN; static toUri(raw: DebugProtocol.Source): URI { if (raw.sourceReference && raw.sourceReference > 0) { - return new URI().withScheme(DebugSource.SCHEME).withPath(raw.name!).withQuery(String(raw.sourceReference)); + return new URI().withScheme(DEBUG_SCHEME).withPath(raw.name!).withQuery(String(raw.sourceReference)); } if (!raw.path) { throw new Error('Unrecognized source type: ' + JSON.stringify(raw)); } - if (raw.path.match(DebugSource.SCHEME_PATTERN)) { + if (raw.path.match(SCHEME_PATTERN)) { return new URI(raw.path); } return new URI(Uri.file(raw.path)); diff --git a/packages/debug/src/common/debug-uri-utils.ts b/packages/debug/src/common/debug-uri-utils.ts new file mode 100644 index 0000000000000..107687cddb5ef --- /dev/null +++ b/packages/debug/src/common/debug-uri-utils.ts @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (C) 2022 STMicroelectronics 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 + ********************************************************************************/ + +/** + * The URI scheme for debug URIs. + */ +export const DEBUG_SCHEME = 'debug'; +/** + * The pattern for URI schemes. + */ +export const SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/; diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index d7c06e5d304c3..532184f80ec88 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1750,6 +1750,7 @@ export interface DebugMain { $startDebugging(folder: theia.WorkspaceFolder | undefined, nameOrConfiguration: string | theia.DebugConfiguration, options: theia.DebugSessionOptions): Promise; $stopDebugging(sessionId?: string): Promise; $customRequest(sessionId: string, command: string, args?: any): Promise; + $getDebugProtocolBreakpoint(sessionId: string, breakpointId: string): Promise; } export interface FileSystemExt { diff --git a/packages/plugin-ext/src/main/browser/debug/debug-main.ts b/packages/plugin-ext/src/main/browser/debug/debug-main.ts index e89edb2519ba5..e4a3c93440f4c 100644 --- a/packages/plugin-ext/src/main/browser/debug/debug-main.ts +++ b/packages/plugin-ext/src/main/browser/debug/debug-main.ts @@ -258,6 +258,15 @@ export class DebugMainImpl implements DebugMain, Disposable { } } + async $getDebugProtocolBreakpoint(sessionId: string, breakpointId: string): Promise { + const session = this.sessionManager.getSession(sessionId); + if (session) { + return session.getBreakpoint(breakpointId)?.raw; + } else { + throw new Error(`Debug session '${sessionId}' not found`); + } + } + async $removeBreakpoints(breakpoints: string[]): Promise { const { labelProvider, breakpointsManager, editorManager } = this; const session = this.sessionManager.currentSession; diff --git a/packages/plugin-ext/src/plugin/debug/debug-ext.ts b/packages/plugin-ext/src/plugin/debug/debug-ext.ts index 4c884dbffcd2a..9b952167eced4 100644 --- a/packages/plugin-ext/src/plugin/debug/debug-ext.ts +++ b/packages/plugin-ext/src/plugin/debug/debug-ext.ts @@ -23,13 +23,15 @@ import { PluginPackageDebuggersContribution } from '../../common/plugin-protocol import { RPCProtocol } from '../../common/rpc-protocol'; import { CommandRegistryImpl } from '../command-registry'; import { ConnectionImpl } from '../../common/connection'; -import { Disposable, Breakpoint as BreakpointExt, SourceBreakpoint, FunctionBreakpoint, Location, Range } from '../types-impl'; +import { DEBUG_SCHEME, SCHEME_PATTERN } from '@theia/debug/lib/common/debug-uri-utils'; +import { Disposable, Breakpoint as BreakpointExt, SourceBreakpoint, FunctionBreakpoint, Location, Range, URI as URIImpl } from '../types-impl'; import { PluginDebugAdapterSession } from './plugin-debug-adapter-session'; import { PluginDebugAdapterTracker } from './plugin-debug-adapter-tracker'; import uuid = require('uuid'); import { DebugAdapter } from '@theia/debug/lib/common/debug-model'; import { PluginDebugAdapterCreator } from './plugin-debug-adapter-creator'; import { NodeDebugAdapterCreator } from '../node/debug/plugin-node-debug-adapter-creator'; +import { DebugProtocol } from 'vscode-debugprotocol'; interface ConfigurationProviderRecord { handle: number; @@ -176,6 +178,27 @@ export class DebugExtImpl implements DebugExt { return this.proxy.$stopDebugging(session?.id); } + asDebugSourceUri(source: theia.DebugProtocolSource, session?: theia.DebugSession): theia.Uri { + return this.getDebugSourceUri(source, session?.id); + } + + private getDebugSourceUri(raw: DebugProtocol.Source, sessionId?: string): theia.Uri { + if (raw.sourceReference && raw.sourceReference > 0) { + let query = 'ref=' + String(raw.sourceReference); + if (sessionId) { + query += `&session=${sessionId}`; + } + return URIImpl.from({ scheme: DEBUG_SCHEME, path: raw.path ?? '', query }); + } + if (!raw.path) { + throw new Error('Unrecognized source type: ' + JSON.stringify(raw)); + } + if (raw.path.match(SCHEME_PATTERN)) { + return URIImpl.parse(raw.path); + } + return URIImpl.file(raw.path); + } + registerDebugAdapterDescriptorFactory(debugType: string, factory: theia.DebugAdapterDescriptorFactory): Disposable { if (this.descriptorFactories.has(debugType)) { throw new Error(`Descriptor factory for ${debugType} has been already registered`); @@ -279,13 +302,13 @@ export class DebugExtImpl implements DebugExt { this.onDidChangeBreakpointsEmitter.fire({ added: a, removed: r, changed: c }); } - protected toBreakpointExt({ functionName, location, enabled, condition, hitCondition, logMessage }: Breakpoint): BreakpointExt | undefined { + protected toBreakpointExt({ functionName, location, enabled, condition, hitCondition, logMessage, id }: Breakpoint): BreakpointExt | undefined { if (location) { const range = new Range(location.range.startLineNumber, location.range.startColumn, location.range.endLineNumber, location.range.endColumn); - return new SourceBreakpoint(new Location(URI.revive(location.uri), range), enabled, condition, hitCondition, logMessage); + return new SourceBreakpoint(new Location(URI.revive(location.uri), range), enabled, condition, hitCondition, logMessage, id); } if (functionName) { - return new FunctionBreakpoint(functionName!, enabled, condition, hitCondition, logMessage); + return new FunctionBreakpoint(functionName!, enabled, condition, hitCondition, logMessage, id); } return undefined; } @@ -305,7 +328,9 @@ export class DebugExtImpl implements DebugExt { return response.body; } return Promise.reject(new Error(response.message ?? 'custom request failed')); - } + }, + getDebugProtocolBreakpoint: async (breakpoint: Breakpoint) => + this.proxy.$getDebugProtocolBreakpoint(sessionId, breakpoint.id) }; const tracker = await this.createDebugAdapterTracker(theiaSession); diff --git a/packages/plugin-ext/src/plugin/debug/plugin-debug-adapter-session.ts b/packages/plugin-ext/src/plugin/debug/plugin-debug-adapter-session.ts index e1a498183f451..577c82937c74e 100644 --- a/packages/plugin-ext/src/plugin/debug/plugin-debug-adapter-session.ts +++ b/packages/plugin-ext/src/plugin/debug/plugin-debug-adapter-session.ts @@ -61,6 +61,10 @@ export class PluginDebugAdapterSession extends DebugAdapterSessionImpl { return this.theiaSession.customRequest(command, args); } + async getDebugProtocolBreakpoint(breakpoint: theia.Breakpoint): Promise { + return this.theiaSession.getDebugProtocolBreakpoint(breakpoint); + } + protected override onDebugAdapterError(error: Error): void { if (this.tracker.onError) { this.tracker.onError(error); diff --git a/packages/plugin-ext/src/plugin/node/debug/debug.spec.ts b/packages/plugin-ext/src/plugin/node/debug/debug.spec.ts new file mode 100644 index 0000000000000..fba237feb742a --- /dev/null +++ b/packages/plugin-ext/src/plugin/node/debug/debug.spec.ts @@ -0,0 +1,94 @@ +/******************************************************************************** + * Copyright (C) 2022 STMicroelectronics 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 { DebugSession } from '@theia/plugin'; +import * as chai from 'chai'; +import { ProxyIdentifier, RPCProtocol } from '../../../common/rpc-protocol'; + +import { DebugExtImpl } from '../../debug/debug-ext'; + +const expect = chai.expect; + +describe('Debug API', () => { + + describe('#asDebugSourceURI', () => { + + const mockRPCProtocol: RPCProtocol = { + getProxy(_proxyId: ProxyIdentifier): T { + return {} as T; + }, + set(_id: ProxyIdentifier, instance: R): R { + return instance; + }, + dispose(): void { + // Nothing + } + }; + + const debug = new DebugExtImpl(mockRPCProtocol); + + it('should use sourceReference, path and sessionId', () => { + const source = { + sourceReference: 3, + path: 'test/path' + }; + const session = { id: 'test-session' } as DebugSession; + const uri = debug.asDebugSourceUri(source, session); + expect(uri.toString(true)).to.be.equal('debug:test/path?ref=3&session=test-session'); + }); + + it('should use sourceReference', () => { + const source = { + sourceReference: 5 + }; + const uri = debug.asDebugSourceUri(source); + expect(uri.toString(true)).to.be.equal('debug:?ref=5'); + }); + + it('should use sourceReference and session', () => { + const source = { + sourceReference: 5 + }; + const session = { id: 'test-session' } as DebugSession; + const uri = debug.asDebugSourceUri(source, session); + expect(uri.toString(true)).to.be.equal('debug:?ref=5&session=test-session'); + }); + + it('should use sourceReference and path', () => { + const source = { + sourceReference: 4, + path: 'test/path' + }; + const uri = debug.asDebugSourceUri(source); + expect(uri.toString(true)).to.be.equal('debug:test/path?ref=4'); + }); + + it('should use path', () => { + const source = { + path: 'scheme:/full/path' + }; + const uri = debug.asDebugSourceUri(source); + expect(uri.toString(true)).to.be.equal('scheme:/full/path'); + }); + + it('should use file path', () => { + const source = { + path: '/full/path' + }; + const uri = debug.asDebugSourceUri(source); + expect(uri.toString(true)).to.be.equal('file:///full/path'); + }); + }); +}); diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index e9a8adcd973c9..0ecbc837047fb 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -868,6 +868,9 @@ export function createAPIFactory( }, removeBreakpoints(breakpoints: readonly theia.Breakpoint[]): void { debugExt.removeBreakpoints(breakpoints); + }, + asDebugSourceUri(source: theia.DebugProtocolSource, session?: theia.DebugSession): theia.Uri { + return debugExt.asDebugSourceUri(source, session); } }; diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index afbc66cad604c..5ababe64d05ac 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -2250,11 +2250,12 @@ export class Breakpoint { */ logMessage?: string; - protected constructor(enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { + protected constructor(enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string, id?: string) { this.enabled = enabled || false; this.condition = condition; this.hitCondition = hitCondition; this.logMessage = logMessage; + this._id = id; } private _id: string | undefined; @@ -2283,8 +2284,8 @@ export class SourceBreakpoint extends Breakpoint { /** * Create a new breakpoint for a source location. */ - constructor(location: Location, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { - super(enabled, condition, hitCondition, logMessage); + constructor(location: Location, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string, id?: string) { + super(enabled, condition, hitCondition, logMessage, id); this.location = location; } } @@ -2302,8 +2303,8 @@ export class FunctionBreakpoint extends Breakpoint { /** * Create a new function breakpoint. */ - constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { - super(enabled, condition, hitCondition, logMessage); + constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string, id?: string) { + super(enabled, condition, hitCondition, logMessage, id); this.functionName = functionName; } } diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index bbf8349baf4c0..e7151d334a047 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -9868,6 +9868,20 @@ export module '@theia/plugin' { // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage). } + /** + * A DebugProtocolBreakpoint is an opaque stand-in type for the [Breakpoint](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolBreakpoint { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint) + } + + /** + * A DebugProtocolSource is an opaque stand-in type for the [Source](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolSource { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source) + } + /** * Configuration for a debug session. */ @@ -9927,6 +9941,15 @@ export module '@theia/plugin' { * Send a custom request to the debug adapter. */ customRequest(command: string, args?: any): Thenable; + + /** + * Maps a breakpoint in the editor to the corresponding Debug Adapter Protocol (DAP) breakpoint that + * is managed by the debug adapter of the debug session. If no DAP breakpoint exists (either because + * the editor breakpoint was not yet registered or because the debug adapter is not interested in the + * breakpoint), the value undefined is returned. + * @param breakpoint a Breakpoint in the editor. + */ + getDebugProtocolBreakpoint(breakpoint: Breakpoint): PromiseLike } /** @@ -10402,6 +10425,16 @@ export module '@theia/plugin' { */ export function registerDebugAdapterDescriptorFactory(debugType: string, factory: DebugAdapterDescriptorFactory): Disposable; + /** + * Converts a "Source" descriptor object received via the Debug Adapter Protocol into a Uri that can be used to load its contents. + * If the source descriptor is based on a path, a file Uri is returned. If the source descriptor uses a reference number, a + * specific debug Uri (scheme 'debug') is constructed that requires a corresponding ContentProvider and a running debug session + * If the "Source" descriptor has insufficient information for creating the Uri, an error is thrown. + * @param source An object conforming to the Source type defined in the Debug Adapter Protocol. + * @param session An optional debug session that will be used when the source descriptor uses a reference number to load the contents from an active debug session. + */ + export function asDebugSourceUri(source: DebugProtocolSource, session?: DebugSession): Uri; + /** * Register a {@link DebugConfigurationProvider debug configuration provider} for a specific debug type. * The optional {@link DebugConfigurationProviderTriggerKind triggerKind} can be used to specify when the `provideDebugConfigurations` method of the provider is triggered.