Skip to content

Commit

Permalink
10011: Add DebugProtocolSource and DebugProtocolBreakpoint
Browse files Browse the repository at this point in the history
Add "DebugProtocolSource" and "DebugProtocolEndpoint" to the plug-in
API, as well as the methods using these interfaces:

- DebugSession.getDebugProtocolBreakpoint()
- debug.asDebugSourceUri()

10011: Fix asDebugSourceUri

- Rename asDebugSourceURI to getDebugSourceUri to avoid name clash
- Fix the way the debug source query is built
- Add tests

10011: Minor code improvements

- Improve test description to be consistent with other tests
- Move the SCHEME and SCHEME_PATTERN constants to debug/common for
reusability

10011: Rebase on master

- Rebase on master and resolve conflicts

Contributed on behalf of STMicroelectronics
Signed-off-by: Camille Letavernier <cletavernier@eclipsesource.com>
  • Loading branch information
CamilleLetavernier committed Jul 8, 2022
1 parent 84b317a commit ef2ce1e
Show file tree
Hide file tree
Showing 12 changed files with 227 additions and 15 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

- [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/)

## v1.28.0 - Unreleased

- [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

- [core] added better styling for active sidepanel borders [#11330](https://github.com/eclipse-theia/theia/pull/11330)
Expand Down
11 changes: 11 additions & 0 deletions packages/debug/src/browser/debug-session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 5 additions & 5 deletions packages/debug/src/browser/model/debug-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -75,16 +76,15 @@ 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 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));
Expand Down
24 changes: 24 additions & 0 deletions packages/debug/src/common/debug-uri-utils.ts
Original file line number Diff line number Diff line change
@@ -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\+\-\.]+:/;
1 change: 1 addition & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1749,6 +1749,7 @@ export interface DebugMain {
$startDebugging(folder: theia.WorkspaceFolder | undefined, nameOrConfiguration: string | theia.DebugConfiguration, options: theia.DebugSessionOptions): Promise<boolean>;
$stopDebugging(sessionId?: string): Promise<void>;
$customRequest(sessionId: string, command: string, args?: any): Promise<DebugProtocol.Response>;
$getDebugProtocolBreakpoint(sessionId: string, breakpointId: string): Promise<theia.DebugProtocolBreakpoint | undefined>;
}

export interface FileSystemExt {
Expand Down
9 changes: 9 additions & 0 deletions packages/plugin-ext/src/main/browser/debug/debug-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,15 @@ export class DebugMainImpl implements DebugMain, Disposable {
}
}

async $getDebugProtocolBreakpoint(sessionId: string, breakpointId: string): Promise<DebugProtocol.Breakpoint | undefined> {
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<void> {
const { labelProvider, breakpointsManager, editorManager } = this;
const session = this.sessionManager.currentSession;
Expand Down
38 changes: 33 additions & 5 deletions packages/plugin-ext/src/plugin/debug/debug-ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ 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';
import TheiaURI from '@theia/core/lib/common/uri';

interface ConfigurationProviderRecord {
handle: number;
Expand Down Expand Up @@ -176,6 +179,29 @@ export class DebugExtImpl implements DebugExt {
return this.proxy.$stopDebugging(session?.id);
}

asDebugSourceUri(source: theia.DebugProtocolSource, session?: theia.DebugSession): theia.Uri {
const raw = source as DebugProtocol.Source;
const uri = this.getDebugSourceUri(raw, session?.id);
return URIImpl.parse(uri.toString());
}

private getDebugSourceUri(raw: DebugProtocol.Source, sessionId?: string): TheiaURI {
if (raw.sourceReference && raw.sourceReference > 0) {
let query = 'ref=' + String(raw.sourceReference);
if (sessionId) {
query += `&session=${sessionId}`;
}
return new TheiaURI().withScheme(DEBUG_SCHEME).withPath(raw.path || '').withQuery(query);
}
if (!raw.path) {
throw new Error('Unrecognized source type: ' + JSON.stringify(raw));
}
if (raw.path.match(SCHEME_PATTERN)) {
return new TheiaURI(raw.path);
}
return new TheiaURI(URI.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`);
Expand Down Expand Up @@ -279,13 +305,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;
}
Expand All @@ -305,7 +331,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export class PluginDebugAdapterSession extends DebugAdapterSessionImpl {
return this.theiaSession.customRequest(command, args);
}

async getDebugProtocolBreakpoint(breakpoint: theia.Breakpoint): Promise<theia.DebugProtocolBreakpoint | undefined> {
return this.theiaSession.getDebugProtocolBreakpoint(breakpoint);
}

protected override onDebugAdapterError(error: Error): void {
if (this.tracker.onError) {
this.tracker.onError(error);
Expand Down
94 changes: 94 additions & 0 deletions packages/plugin-ext/src/plugin/node/debug/debug.spec.ts
Original file line number Diff line number Diff line change
@@ -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<T>(_proxyId: ProxyIdentifier<T>): T {
return {} as T;
},
set<T, R extends T>(_id: ProxyIdentifier<T>, 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');
});
});
});
3 changes: 3 additions & 0 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};

Expand Down
11 changes: 6 additions & 5 deletions packages/plugin-ext/src/plugin/types-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2249,11 +2249,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;
Expand Down Expand Up @@ -2282,8 +2283,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;
}
}
Expand All @@ -2301,8 +2302,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;
}
}
Expand Down
33 changes: 33 additions & 0 deletions packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9851,6 +9851,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.
*/
Expand Down Expand Up @@ -9910,6 +9924,15 @@ export module '@theia/plugin' {
* Send a custom request to the debug adapter.
*/
customRequest(command: string, args?: any): Thenable<any>;

/**
* 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<DebugProtocolBreakpoint | undefined>
}

/**
Expand Down Expand Up @@ -10385,6 +10408,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.
Expand Down

0 comments on commit ef2ce1e

Please sign in to comment.