Skip to content

Commit 5726565

Browse files
authored
feat: add extension API for debug terminal contributions (#2136)
This should help the Bun folks work nicely with the js-debug terminal: oven-sh/bun#15223
1 parent c57467f commit 5726565

File tree

8 files changed

+128
-10
lines changed

8 files changed

+128
-10
lines changed

CDP_SHARE.md renamed to EXTENSION_AUTHORS.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,39 @@
1-
# CDP Sharing Mechanism
1+
# Extensibility
2+
3+
js-debug has a few ways other extensions can 'plug into' js-debug and provide additional extensibility.
4+
5+
## Extension API
6+
7+
js-debug provides an extension API you can use to do certain things. Please refer to [the typings](https://github.com/microsoft/vscode-js-debug/blob/main/src/typings/vscode-js-debug.d.ts) for capabilities.
8+
9+
To use this, you would:
10+
11+
1. Add a step in your build process to download the typings from https://github.com/microsoft/vscode-js-debug/blob/main/src/typings/vscode-js-debug.d.ts to somewhere in your source tree.
12+
2. Access the API like so:
13+
14+
```js
15+
const jsDebugExt = vscode.extensions.getExtension('ms-vscode.js-debug-nightly') || vscode.extensions.getExtension('ms-vscode.js-debug');
16+
await jsDebugExt.activate()
17+
const jsDebug: import('@vscode/js-debug').IExports = jsDebugExt.exports;
18+
```
19+
20+
## CDP Sharing Mechanism
221

322
This file documents the CDP sharing mechanism in js-debug. It can be useful for advanced extensions and plugins. The original feature request can be found in [#892](https://github.com/microsoft/vscode-js-debug/issues/893).
423

5-
## Requesting a CDP Connection
24+
### Requesting a CDP Connection
625

726
js-debug can be asked to share its CDP connection by running the `extension.js-debug.requestCDPProxy` command with the debug session ID you wish to connect to. js-debug will respond with an object containing a WebSocket server address in the form `{ host: string, port: string }`. You can see a sample extension that requests this information [here](https://github.com/connor4312/cdp-proxy-requestor/blob/main/extension.js).
827

928
Note that the server will always be running in the workspace. If you have a UI extension, you may need to forward the port. We also recommend using `permessage-deflate` on the WebSocket for better performance over remote connections.
1029

11-
## Protocol
30+
### Protocol
1231

1332
The protocol spoken over the WebSocket is, unsurprisingly, CDP. Over the websocket, the `sessionId` will never be used and will always be ignored. This is because a single js-debug debug session corresponds to exactly one CDP session. Other targets--like iframes, workers, and subprocesses--are represented as separate debug sessions which you can connect to separately.
1433

1534
Additionally, by default, you will not receive any CDP events on the socket. This is because the underlying CDP connection is shared between js-debug and consumers of the mechanism, and we want to avoid doing extra work to send events you don't care about. To listen to events, you can use the JsDebug domain:
1635

17-
### JsDebug domain
36+
#### JsDebug domain
1837

1938
js-debug exposes a `JsDebug` CDP domain for meta-communication. For example, you would call the method `JsDebug.subscribe` to subscribe to evetns.
2039

src/extension.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright (C) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------*/
44

5+
import { IExports } from '@vscode/js-debug';
56
import * as l10n from '@vscode/l10n';
67
import { tmpdir } from 'os';
78
import * as vscode from 'vscode';
@@ -23,14 +24,15 @@ import { registerCustomBreakpointsUI } from './ui/customBreakpointsUI';
2324
import { debugNpmScript } from './ui/debugNpmScript';
2425
import { DebugSessionTracker } from './ui/debugSessionTracker';
2526
import { registerDebugTerminalUI } from './ui/debugTerminalUI';
27+
import { ExtensionApiFactory } from './ui/extensionApi';
2628
import { attachProcess, pickProcess } from './ui/processPicker';
2729
import { registerProfilingCommand } from './ui/profiling';
2830
import { registerRequestCDPProxy } from './ui/requestCDPProxy';
2931
import { registerRevealPage } from './ui/revealPage';
3032
import { toggleSkippingFile } from './ui/toggleSkippingFile';
3133
import { VSCodeSessionManager } from './ui/vsCodeSessionManager';
3234

33-
export function activate(context: vscode.ExtensionContext) {
35+
export function activate(context: vscode.ExtensionContext): IExports {
3436
if (vscode.l10n.bundle) {
3537
l10n.config({ contents: vscode.l10n.bundle });
3638
}
@@ -101,6 +103,8 @@ export function activate(context: vscode.ExtensionContext) {
101103
registerRevealPage(context, debugSessionTracker);
102104
registerRequestCDPProxy(context, debugSessionTracker);
103105
services.getAll<IExtensionContribution>(IExtensionContribution).forEach(c => c.register(context));
106+
107+
return services.get(ExtensionApiFactory).create();
104108
}
105109

106110
export function deactivate() {

src/ioc-extras.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,8 @@ export interface IExtensionContribution {
134134
}
135135

136136
export const IExtensionContribution = Symbol('IExtensionContribution');
137+
138+
/**
139+
* Binding to a Set<IDebugTerminalOptionsProvider>.
140+
*/
141+
export const IDebugTerminalOptionsProviders = Symbol('IDebugTerminalOptionsProviders');

src/targets/node/terminalNodeLauncher.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright (C) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------*/
44

5+
import { IDebugTerminalOptionsProvider } from '@vscode/js-debug';
56
import { randomBytes } from 'crypto';
67
import { inject, injectable, optional } from 'inversify';
78
import { tmpdir } from 'os';
@@ -16,7 +17,7 @@ import { ITerminalLinkProvider } from '../../common/terminalLinkProvider';
1617
import { AnyLaunchConfiguration, ITerminalLaunchConfiguration } from '../../configuration';
1718
import { ErrorCodes } from '../../dap/errors';
1819
import { ProtocolError } from '../../dap/protocolError';
19-
import { FS, FsPromises } from '../../ioc-extras';
20+
import { FS, FsPromises, IDebugTerminalOptionsProviders } from '../../ioc-extras';
2021
import { ISourcePathResolverFactory } from '../sourcePathResolverFactory';
2122
import { IStopMetadata, ITarget } from '../targets';
2223
import { hideDebugInfoFromConsole, INodeBinaryProvider, NodeBinary } from './nodeBinaryProvider';
@@ -78,6 +79,9 @@ export class TerminalNodeLauncher extends NodeLauncherBase<ITerminalLaunchConfig
7879
@inject(FS) private readonly fs: FsPromises,
7980
@inject(ISourcePathResolverFactory) pathResolverFactory: ISourcePathResolverFactory,
8081
@inject(IPortLeaseTracker) portLeaseTracker: IPortLeaseTracker,
82+
@inject(IDebugTerminalOptionsProviders) private readonly optionsProviders: ReadonlySet<
83+
IDebugTerminalOptionsProvider
84+
>,
8185
@optional()
8286
@inject(ITerminalLinkProvider)
8387
private readonly terminalLinkProvider: ITerminalLinkProvider | undefined,
@@ -134,13 +138,22 @@ export class TerminalNodeLauncher extends NodeLauncherBase<ITerminalLaunchConfig
134138
fileCallback: this.callbackFile,
135139
});
136140

137-
const terminal = await this.createTerminal({
141+
let options: vscode.TerminalOptions = {
138142
name: runData.params.name,
139143
cwd: runData.params.cwd,
140144
iconPath: new vscode.ThemeIcon('debug'),
141145
env: hideDebugInfoFromConsole(binary, env).defined(),
142146
isTransient: true,
143-
});
147+
};
148+
149+
for (const provider of this.optionsProviders) {
150+
const result = await provider.provideTerminalOptions(options);
151+
if (result) {
152+
options = result;
153+
}
154+
}
155+
156+
const terminal = await this.createTerminal(options);
144157
this.terminalLinkProvider?.enableHandlingInTerminal(terminal);
145158
this.terminalCreatedEmitter.fire(terminal);
146159

src/typings/vscode-js-debug.d.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
declare module '@vscode/js-debug' {
6+
import * as vscode from 'vscode';
7+
8+
/** @see {IExports.registerDebugTerminalOptionsProvider} */
9+
export interface IDebugTerminalOptionsProvider {
10+
/**
11+
* Called when the user creates a JavaScript Debug Terminal. It's called
12+
* with the options js-debug wants to use to create the terminal. It should
13+
* modify and return the options to use in the terminal.
14+
*
15+
* In order to avoid conflicting with existing logic, participants should
16+
* try to modify options in a additive way. For example prefer appending
17+
* to rather than reading and overwriting `options.env.PATH`.
18+
*/
19+
provideTerminalOptions(options: vscode.TerminalOptions): vscode.ProviderResult<vscode.TerminalOptions>;
20+
}
21+
22+
/**
23+
* Defines the exports of the `js-debug` extension. Once you have this typings
24+
* file, these can be acquired in your extension using the following code:
25+
*
26+
* ```
27+
* const jsDebugExt = vscode.extensions.getExtension('ms-vscode.js-debug-nightly')
28+
* || vscode.extensions.getExtension('ms-vscode.js-debug');
29+
* await jsDebugExt.activate()
30+
* const jsDebug: import('@vscode/js-debug').IExports = jsDebug.exports;
31+
* ```
32+
*/
33+
export interface IExports {
34+
/**
35+
* Registers a participant used when the user creates a JavaScript Debug Terminal.
36+
*/
37+
registerDebugTerminalOptionsProvider(provider: IDebugTerminalOptionsProvider): vscode.Disposable;
38+
}
39+
}

src/ui/debugTerminalUI.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
terminalBaseDefaults,
2525
} from '../configuration';
2626
import { createPendingDapApi } from '../dap/pending-api';
27-
import { FS } from '../ioc-extras';
27+
import { FS, IDebugTerminalOptionsProviders } from '../ioc-extras';
2828
import { DelegateLauncherFactory } from '../targets/delegate/delegateLauncherFactory';
2929
import { NodeBinaryProvider } from '../targets/node/nodeBinaryProvider';
3030
import { noPackageJsonProvider } from '../targets/node/packageJsonProvider';
@@ -216,6 +216,7 @@ export function registerDebugTerminalUI(
216216
services.get(FS),
217217
services.get(NodeOnlyPathResolverFactory),
218218
services.get(IPortLeaseTracker),
219+
services.get(IDebugTerminalOptionsProviders),
219220
services.get(ITerminalLinkProvider),
220221
);
221222

@@ -306,6 +307,7 @@ export function registerDebugTerminalUI(
306307
services.get(FS),
307308
services.get(NodeOnlyPathResolverFactory),
308309
services.get(IPortLeaseTracker),
310+
services.get(IDebugTerminalOptionsProviders),
309311
services.get(ITerminalLinkProvider),
310312
);
311313

src/ui/extensionApi.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import type { IDebugTerminalOptionsProvider, IExports } from '@vscode/js-debug';
6+
import { inject, injectable } from 'inversify';
7+
import { IDebugTerminalOptionsProviders } from '../ioc-extras';
8+
9+
@injectable()
10+
export class ExtensionApiFactory {
11+
constructor(
12+
@inject(IDebugTerminalOptionsProviders) private readonly debugTerminalOptionsProviders: Set<
13+
IDebugTerminalOptionsProvider
14+
>,
15+
) {}
16+
17+
public create(): IExports {
18+
return {
19+
registerDebugTerminalOptionsProvider: provider => {
20+
this.debugTerminalOptionsProviders.add(provider);
21+
return { dispose: () => this.debugTerminalOptionsProviders.delete(provider) };
22+
},
23+
};
24+
}
25+
}

src/ui/ui-ioc.extensionOnly.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { Container } from 'inversify';
66
import { IDwarfModuleProvider } from '../adapter/dwarf/dwarfModuleProvider';
77
import { IRequestOptionsProvider } from '../adapter/resourceProvider/requestOptionsProvider';
88
import { ITerminalLinkProvider } from '../common/terminalLinkProvider';
9-
import { IExtensionContribution, trackDispose, VSCodeApi } from '../ioc-extras';
9+
import {
10+
IDebugTerminalOptionsProviders,
11+
IExtensionContribution,
12+
trackDispose,
13+
VSCodeApi,
14+
} from '../ioc-extras';
1015
import { TerminalNodeLauncher } from '../targets/node/terminalNodeLauncher';
1116
import { ILauncher } from '../targets/targets';
1217
import { IExperimentationService } from '../telemetry/experimentationService';
@@ -25,6 +30,7 @@ import { DisableSourceMapUI } from './disableSourceMapUI';
2530
import { DwarfModuleProvider } from './dwarfModuleProviderImpl';
2631
import { EdgeDevToolOpener } from './edgeDevToolOpener';
2732
import { ExcludedCallersUI } from './excludedCallersUI';
33+
import { ExtensionApiFactory } from './extensionApi';
2834
import { LaunchJsonCompletions } from './launchJsonCompletions';
2935
import { ILinkedBreakpointLocation } from './linkedBreakpointLocation';
3036
import { LinkedBreakpointLocationUI } from './linkedBreakpointLocationUI';
@@ -95,6 +101,11 @@ export const registerUiComponents = (container: Container) => {
95101
.bind(ITerminationConditionFactory)
96102
.to(BreakpointTerminationConditionFactory)
97103
.inSingletonScope();
104+
105+
container.bind(IDebugTerminalOptionsProviders)
106+
.toConstantValue(new Set());
107+
108+
container.bind(ExtensionApiFactory).toSelf().inSingletonScope();
98109
};
99110

100111
export const registerTopLevelSessionComponents = (container: Container) => {

0 commit comments

Comments
 (0)