Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8e5a9a2

Browse files
author
Akos Kitta
committedNov 29, 2022
1 parent d1b2173 commit 8e5a9a2

15 files changed

+895
-2
lines changed
 

‎arduino-ide-extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@types/react-virtualized": "^9.21.21",
6161
"@types/temp": "^0.8.34",
6262
"@types/which": "^1.3.1",
63+
"@vscode/debugprotocol": "^1.51.0",
6364
"arduino-serial-plotter-webapp": "0.2.0",
6465
"async-mutex": "^0.3.0",
6566
"auth0-js": "^9.14.0",

‎arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import '../../src/browser/style/index.css';
2-
import { ContainerModule } from '@theia/core/shared/inversify';
2+
import { Container, ContainerModule } from '@theia/core/shared/inversify';
33
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
44
import { CommandContribution } from '@theia/core/lib/common/command';
55
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
@@ -331,6 +331,18 @@ import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarc
331331
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
332332
import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-contribution';
333333
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution';
334+
import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution';
335+
import { DebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
336+
import { DebugToolbar } from './theia/debug/debug-toolbar-widget';
337+
import { DebugToolBar as TheiaDebugToolbar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
338+
import { PluginMenuCommandAdapter } from './theia/plugin-ext/plugin-menu-command-adapter';
339+
import { PluginMenuCommandAdapter as TheiaPluginMenuCommandAdapter } from '@theia/plugin-ext/lib/main/browser/menus/plugin-menu-command-adapter';
340+
import { DebugSessionManager } from './theia/debug/debug-session-manager';
341+
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
342+
import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
343+
import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
344+
import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
345+
import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
334346

335347
export default new ContainerModule((bind, unbind, isBound, rebind) => {
336348
// Commands and toolbar items
@@ -960,4 +972,36 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
960972
);
961973
bind(TypeHierarchyContribution).toSelf().inSingletonScope();
962974
rebind(TheiaTypeHierarchyContribution).toService(TypeHierarchyContribution);
975+
976+
// patched the debugger for `cortex-debug@1.5.1`
977+
// https://github.com/eclipse-theia/theia/issues/11871
978+
// https://github.com/eclipse-theia/theia/issues/11879
979+
// https://github.com/eclipse-theia/theia/issues/11880
980+
// https://github.com/eclipse-theia/theia/issues/11885
981+
// https://github.com/eclipse-theia/theia/issues/11886
982+
// https://github.com/eclipse-theia/theia/issues/11916
983+
// based on: https://github.com/eclipse-theia/theia/compare/master...kittaakos:theia:%2311871
984+
bind(DefaultDebugSessionFactory).toSelf().inSingletonScope();
985+
rebind(DebugSessionFactory).toService(DefaultDebugSessionFactory);
986+
bind(DebugSessionManager).toSelf().inSingletonScope();
987+
rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
988+
bind(DebugToolbar).toSelf().inSingletonScope();
989+
rebind(TheiaDebugToolbar).toService(DebugToolbar);
990+
bind(PluginMenuCommandAdapter).toSelf().inSingletonScope();
991+
rebind(TheiaPluginMenuCommandAdapter).toService(PluginMenuCommandAdapter);
992+
bind(WidgetFactory)
993+
.toDynamicValue(({ container }) => ({
994+
id: DebugWidget.ID,
995+
createWidget: () => {
996+
const child = new Container({ defaultScope: 'Singleton' });
997+
child.parent = container;
998+
child.bind(DebugViewModel).toSelf();
999+
child.bind(DebugToolbar).toSelf(); // patched toolbar
1000+
child.bind(DebugSessionWidget).toSelf();
1001+
child.bind(DebugConfigurationWidget).toSelf();
1002+
child.bind(DebugWidget).toSelf();
1003+
return child.get(DebugWidget);
1004+
},
1005+
}))
1006+
.inSingletonScope();
9631007
});

‎arduino-ide-extension/src/browser/style/index.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,13 @@ button.theia-button.message-box-dialog-button {
176176
outline: 1px dashed var(--theia-focusBorder);
177177
outline-offset: -2px;
178178
}
179+
180+
.debug-toolbar .debug-action>div {
181+
font-family: var(--theia-ui-font-family);
182+
font-size: var(--theia-ui-font-size0);
183+
display: flex;
184+
align-items: center;
185+
align-self: center;
186+
justify-content: center;
187+
min-height: inherit;
188+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as React from '@theia/core/shared/react';
2+
import { DebugAction as TheiaDebugAction } from '@theia/debug/lib/browser/view/debug-action';
3+
import {
4+
codiconArray,
5+
DISABLED_CLASS,
6+
} from '@theia/core/lib/browser/widgets/widget';
7+
8+
// customized debug action to show the contributed command's label when there is no icon
9+
export class DebugAction extends TheiaDebugAction {
10+
override render(): React.ReactNode {
11+
const { enabled, label, iconClass } = this.props;
12+
const classNames = ['debug-action', ...codiconArray(iconClass, true)];
13+
if (enabled === false) {
14+
classNames.push(DISABLED_CLASS);
15+
}
16+
return (
17+
<span
18+
tabIndex={0}
19+
className={classNames.join(' ')}
20+
title={label}
21+
onClick={this.props.run}
22+
ref={this.setRef}
23+
>
24+
{!iconClass ||
25+
(iconClass.match(/plugin-icon-\d+/) && <div>{label}</div>)}
26+
</span>
27+
);
28+
}
29+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { injectable } from '@theia/core/shared/inversify';
2+
import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
3+
import { DefaultDebugSessionFactory as TheiaDefaultDebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
4+
import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
5+
import {
6+
DebugAdapterPath,
7+
DebugChannel,
8+
ForwardingDebugChannel,
9+
} from '@theia/debug/lib/common/debug-service';
10+
import { DebugSession } from './debug-session';
11+
12+
@injectable()
13+
export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory {
14+
override get(
15+
sessionId: string,
16+
options: DebugConfigurationSessionOptions,
17+
parentSession?: DebugSession
18+
): DebugSession {
19+
const connection = new DebugSessionConnection(
20+
sessionId,
21+
() =>
22+
new Promise<DebugChannel>((resolve) =>
23+
this.connectionProvider.openChannel(
24+
`${DebugAdapterPath}/${sessionId}`,
25+
(wsChannel) => {
26+
resolve(new ForwardingDebugChannel(wsChannel));
27+
},
28+
{ reconnecting: false }
29+
)
30+
),
31+
this.getTraceOutputChannel()
32+
);
33+
// patched debug session
34+
return new DebugSession(
35+
sessionId,
36+
options,
37+
parentSession,
38+
connection,
39+
this.terminalService,
40+
this.editorManager,
41+
this.breakpoints,
42+
this.labelProvider,
43+
this.messages,
44+
this.fileService,
45+
this.debugContributionProvider,
46+
this.workspaceService
47+
);
48+
}
49+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { ContextKey } from '@theia/core/lib/browser/context-key-service';
2+
import { injectable, postConstruct } from '@theia/core/shared/inversify';
3+
import {
4+
DebugSession,
5+
DebugState,
6+
} from '@theia/debug/lib/browser/debug-session';
7+
import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
8+
import type { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
9+
10+
function debugStateLabel(state: DebugState): string {
11+
switch (state) {
12+
case DebugState.Initializing:
13+
return 'initializing';
14+
case DebugState.Stopped:
15+
return 'stopped';
16+
case DebugState.Running:
17+
return 'running';
18+
default:
19+
return 'inactive';
20+
}
21+
}
22+
23+
@injectable()
24+
export class DebugSessionManager extends TheiaDebugSessionManager {
25+
protected debugStateKey: ContextKey<string>;
26+
27+
@postConstruct()
28+
protected override init(): void {
29+
this.debugStateKey = this.contextKeyService.createKey<string>(
30+
'debugState',
31+
debugStateLabel(this.state)
32+
);
33+
super.init();
34+
}
35+
36+
protected override fireDidChange(current: DebugSession | undefined): void {
37+
this.debugTypeKey.set(current?.configuration.type);
38+
this.inDebugModeKey.set(this.inDebugMode);
39+
this.debugStateKey.set(debugStateLabel(this.state));
40+
this.onDidChangeEmitter.fire(current);
41+
}
42+
43+
protected override async doStart(
44+
sessionId: string,
45+
options: DebugConfigurationSessionOptions
46+
): Promise<DebugSession> {
47+
const parentSession =
48+
options.configuration.parentSession &&
49+
this._sessions.get(options.configuration.parentSession.id);
50+
const contrib = this.sessionContributionRegistry.get(
51+
options.configuration.type
52+
);
53+
const sessionFactory = contrib
54+
? contrib.debugSessionFactory()
55+
: this.debugSessionFactory;
56+
const session = sessionFactory.get(sessionId, options, parentSession);
57+
this._sessions.set(sessionId, session);
58+
59+
this.debugTypeKey.set(session.configuration.type);
60+
// this.onDidCreateDebugSessionEmitter.fire(session); // defer the didCreate event after start https://github.com/eclipse-theia/theia/issues/11916
61+
62+
let state = DebugState.Inactive;
63+
session.onDidChange(() => {
64+
if (state !== session.state) {
65+
state = session.state;
66+
if (state === DebugState.Stopped) {
67+
this.onDidStopDebugSessionEmitter.fire(session);
68+
}
69+
}
70+
this.updateCurrentSession(session);
71+
});
72+
session.onDidChangeBreakpoints((uri) =>
73+
this.fireDidChangeBreakpoints({ session, uri })
74+
);
75+
session.on('terminated', async (event) => {
76+
const restart = event.body && event.body.restart;
77+
if (restart) {
78+
// postDebugTask isn't run in case of auto restart as well as preLaunchTask
79+
this.doRestart(session, !!restart);
80+
} else {
81+
await session.disconnect(false, () =>
82+
this.debug.terminateDebugSession(session.id)
83+
);
84+
await this.runTask(
85+
session.options.workspaceFolderUri,
86+
session.configuration.postDebugTask
87+
);
88+
}
89+
});
90+
91+
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
92+
session.on('exited', async (event) => {
93+
await session.disconnect(false, () =>
94+
this.debug.terminateDebugSession(session.id)
95+
);
96+
});
97+
98+
session.onDispose(() => this.cleanup(session));
99+
session
100+
.start()
101+
.then(() => {
102+
this.onDidCreateDebugSessionEmitter.fire(session); // now fire the didCreate event
103+
this.onDidStartDebugSessionEmitter.fire(session);
104+
})
105+
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
106+
.catch((e) => {
107+
session.stop(false, () => {
108+
this.debug.terminateDebugSession(session.id);
109+
});
110+
});
111+
session.onDidCustomEvent(({ event, body }) =>
112+
this.onDidReceiveDebugSessionCustomEventEmitter.fire({
113+
event,
114+
body,
115+
session,
116+
})
117+
);
118+
return session;
119+
}
120+
}
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
2+
import { Deferred } from '@theia/core/lib/common/promise-util';
3+
import { Mutable } from '@theia/core/lib/common/types';
4+
import { URI } from '@theia/core/lib/common/uri';
5+
import { DebugSession as TheiaDebugSession } from '@theia/debug/lib/browser/debug-session';
6+
import { DebugFunctionBreakpoint } from '@theia/debug/lib/browser/model/debug-function-breakpoint';
7+
import { DebugSourceBreakpoint } from '@theia/debug/lib/browser/model/debug-source-breakpoint';
8+
import {
9+
DebugThreadData,
10+
StoppedDetails,
11+
} from '@theia/debug/lib/browser/model/debug-thread';
12+
import { DebugProtocol } from '@vscode/debugprotocol';
13+
import { DebugThread } from './debug-thread';
14+
15+
export class DebugSession extends TheiaDebugSession {
16+
/**
17+
* The `send('initialize')` request resolves later than `on('initialized')` emits the event.
18+
* Hence, the `configure` would use the empty object `capabilities`.
19+
* Using the empty `capabilities` could result in missing exception breakpoint filters, as
20+
* always `capabilities.exceptionBreakpointFilters` is falsy. This deferred promise works
21+
* around this timing issue.
22+
* See: https://github.com/eclipse-theia/theia/issues/11886.
23+
*/
24+
protected didReceiveCapabilities = new Deferred();
25+
26+
protected override async initialize(): Promise<void> {
27+
const clientName = FrontendApplicationConfigProvider.get().applicationName;
28+
try {
29+
const response = await this.connection.sendRequest('initialize', {
30+
clientID: clientName.toLocaleLowerCase().replace(/ /g, '_'),
31+
clientName,
32+
adapterID: this.configuration.type,
33+
locale: 'en-US',
34+
linesStartAt1: true,
35+
columnsStartAt1: true,
36+
pathFormat: 'path',
37+
supportsVariableType: false,
38+
supportsVariablePaging: false,
39+
supportsRunInTerminalRequest: true,
40+
});
41+
this.updateCapabilities(response?.body || {});
42+
this.didReceiveCapabilities.resolve();
43+
} catch (err) {
44+
this.didReceiveCapabilities.reject(err);
45+
throw err;
46+
}
47+
}
48+
49+
protected override async configure(): Promise<void> {
50+
await this.didReceiveCapabilities.promise;
51+
return super.configure();
52+
}
53+
54+
override async stop(isRestart: boolean, callback: () => void): Promise<void> {
55+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
56+
const _this = this as any;
57+
if (!_this.isStopping) {
58+
_this.isStopping = true;
59+
if (this.configuration.lifecycleManagedByParent && this.parentSession) {
60+
await this.parentSession.stop(isRestart, callback);
61+
} else {
62+
if (this.canTerminate()) {
63+
const terminated = this.waitFor('terminated', 5000);
64+
try {
65+
await this.connection.sendRequest(
66+
'terminate',
67+
{ restart: isRestart },
68+
5000
69+
);
70+
await terminated;
71+
} catch (e) {
72+
console.error('Did not receive terminated event in time', e);
73+
}
74+
} else {
75+
const terminateDebuggee =
76+
this.initialized && this.capabilities.supportTerminateDebuggee;
77+
// Related https://github.com/microsoft/vscode/issues/165138
78+
try {
79+
await this.sendRequest(
80+
'disconnect',
81+
{ restart: isRestart, terminateDebuggee },
82+
2000
83+
);
84+
} catch (err) {
85+
if (
86+
'message' in err &&
87+
typeof err.message === 'string' &&
88+
err.message.test(err.message)
89+
) {
90+
// VS Code ignores errors when sending the `disconnect` request.
91+
// Debug adapter might not send the `disconnected` event as a response.
92+
} else {
93+
throw err;
94+
}
95+
}
96+
}
97+
callback();
98+
}
99+
}
100+
}
101+
102+
protected override async sendFunctionBreakpoints(
103+
affectedUri: URI
104+
): Promise<void> {
105+
const all = this.breakpoints
106+
.getFunctionBreakpoints()
107+
.map(
108+
(origin) =>
109+
new DebugFunctionBreakpoint(origin, this.asDebugBreakpointOptions())
110+
);
111+
const enabled = all.filter((b) => b.enabled);
112+
if (this.capabilities.supportsFunctionBreakpoints) {
113+
try {
114+
const response = await this.sendRequest('setFunctionBreakpoints', {
115+
breakpoints: enabled.map((b) => b.origin.raw),
116+
});
117+
// Apparently, `body` and `breakpoints` can be missing.
118+
// https://github.com/eclipse-theia/theia/issues/11885
119+
// https://github.com/microsoft/vscode/blob/80004351ccf0884b58359f7c8c801c91bb827d83/src/vs/workbench/contrib/debug/browser/debugSession.ts#L448-L449
120+
if (response && response.body) {
121+
response.body.breakpoints.forEach((raw, index) => {
122+
// node debug adapter returns more breakpoints sometimes
123+
if (enabled[index]) {
124+
enabled[index].update({ raw });
125+
}
126+
});
127+
}
128+
} catch (error) {
129+
// could be error or promise rejection of DebugProtocol.SetFunctionBreakpoints
130+
if (error instanceof Error) {
131+
console.error(`Error setting breakpoints: ${error.message}`);
132+
} else {
133+
// handle adapters that send failed DebugProtocol.SetFunctionBreakpoints for invalid breakpoints
134+
const genericMessage =
135+
'Function breakpoint not valid for current debug session';
136+
const message = error.message ? `${error.message}` : genericMessage;
137+
console.warn(
138+
`Could not handle function breakpoints: ${message}, disabling...`
139+
);
140+
enabled.forEach((b) =>
141+
b.update({
142+
raw: {
143+
verified: false,
144+
message,
145+
},
146+
})
147+
);
148+
}
149+
}
150+
}
151+
this.setBreakpoints(affectedUri, all);
152+
}
153+
154+
protected override async sendSourceBreakpoints(
155+
affectedUri: URI,
156+
sourceModified?: boolean
157+
): Promise<void> {
158+
const source = await this.toSource(affectedUri);
159+
const all = this.breakpoints
160+
.findMarkers({ uri: affectedUri })
161+
.map(
162+
({ data }) =>
163+
new DebugSourceBreakpoint(data, this.asDebugBreakpointOptions())
164+
);
165+
const enabled = all.filter((b) => b.enabled);
166+
try {
167+
const breakpoints = enabled.map(({ origin }) => origin.raw);
168+
const response = await this.sendRequest('setBreakpoints', {
169+
source: source.raw,
170+
sourceModified,
171+
breakpoints,
172+
lines: breakpoints.map(({ line }) => line),
173+
});
174+
response.body.breakpoints.forEach((raw, index) => {
175+
// node debug adapter returns more breakpoints sometimes
176+
if (enabled[index]) {
177+
enabled[index].update({ raw });
178+
}
179+
});
180+
} catch (error) {
181+
// could be error or promise rejection of DebugProtocol.SetBreakpointsResponse
182+
if (error instanceof Error) {
183+
console.error(`Error setting breakpoints: ${error.message}`);
184+
} else {
185+
// handle adapters that send failed DebugProtocol.SetBreakpointsResponse for invalid breakpoints
186+
const genericMessage = 'Breakpoint not valid for current debug session';
187+
const message = error.message ? `${error.message}` : genericMessage;
188+
console.warn(
189+
`Could not handle breakpoints for ${affectedUri}: ${message}, disabling...`
190+
);
191+
enabled.forEach((b) =>
192+
b.update({
193+
raw: {
194+
verified: false,
195+
message,
196+
},
197+
})
198+
);
199+
}
200+
}
201+
this.setSourceBreakpoints(affectedUri, all);
202+
}
203+
204+
protected override doUpdateThreads(
205+
threads: DebugProtocol.Thread[],
206+
stoppedDetails?: StoppedDetails
207+
): void {
208+
const existing = this._threads;
209+
this._threads = new Map();
210+
for (const raw of threads) {
211+
const id = raw.id;
212+
const thread = existing.get(id) || new DebugThread(this); // patched debug thread
213+
this._threads.set(id, thread);
214+
const data: Partial<Mutable<DebugThreadData>> = { raw };
215+
if (stoppedDetails) {
216+
if (stoppedDetails.threadId === id) {
217+
data.stoppedDetails = stoppedDetails;
218+
} else if (stoppedDetails.allThreadsStopped) {
219+
data.stoppedDetails = {
220+
// When a debug adapter notifies us that all threads are stopped,
221+
// we do not know why the others are stopped, so we should default
222+
// to something generic.
223+
reason: '',
224+
};
225+
}
226+
}
227+
thread.update(data);
228+
}
229+
this.updateCurrentThread(stoppedDetails);
230+
}
231+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { WidgetOpenerOptions } from '@theia/core/lib/browser/widget-open-handler';
2+
import { Range } from '@theia/core/shared/vscode-languageserver-types';
3+
import { DebugStackFrame as TheiaDebugStackFrame } from '@theia/debug/lib/browser/model/debug-stack-frame';
4+
import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
5+
6+
export class DebugStackFrame extends TheiaDebugStackFrame {
7+
override async open(
8+
options: WidgetOpenerOptions = {
9+
mode: 'reveal',
10+
}
11+
): Promise<EditorWidget | undefined> {
12+
if (!this.source) {
13+
return undefined;
14+
}
15+
const { line, column, endLine, endColumn, source } = this.raw;
16+
if (!source) {
17+
return undefined;
18+
}
19+
// create selection based on VS Code
20+
// https://github.com/eclipse-theia/theia/issues/11880
21+
const selection = Range.create(
22+
line,
23+
column,
24+
endLine || line,
25+
endColumn || column
26+
);
27+
this.source.open({
28+
...options,
29+
selection,
30+
});
31+
}
32+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { DebugStackFrame as TheiaDebugStackFrame } from '@theia/debug/lib/browser/model/debug-stack-frame';
2+
import { DebugThread as TheiaDebugThread } from '@theia/debug/lib/browser/model/debug-thread';
3+
import { DebugProtocol } from '@vscode/debugprotocol';
4+
import { DebugStackFrame } from './debug-stack-frame';
5+
6+
export class DebugThread extends TheiaDebugThread {
7+
protected override doUpdateFrames(
8+
frames: DebugProtocol.StackFrame[]
9+
): TheiaDebugStackFrame[] {
10+
const result = new Set<TheiaDebugStackFrame>();
11+
for (const raw of frames) {
12+
const id = raw.id;
13+
const frame =
14+
this._frames.get(id) || new DebugStackFrame(this, this.session); // patched debug stack frame
15+
this._frames.set(id, frame);
16+
frame.update({ raw });
17+
result.add(frame);
18+
}
19+
this.updateCurrentFrame();
20+
return [...result.values()];
21+
}
22+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
2+
import { CommandRegistry } from '@theia/core/lib/common/command';
3+
import {
4+
ActionMenuNode,
5+
CompositeMenuNode,
6+
MenuModelRegistry,
7+
} from '@theia/core/lib/common/menu';
8+
import { nls } from '@theia/core/lib/common/nls';
9+
import { inject, injectable } from '@theia/core/shared/inversify';
10+
import * as React from '@theia/core/shared/react';
11+
import { DebugState } from '@theia/debug/lib/browser/debug-session';
12+
import { DebugAction } from './debug-action';
13+
import { DebugToolBar as TheiaDebugToolbar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
14+
15+
@injectable()
16+
export class DebugToolbar extends TheiaDebugToolbar {
17+
@inject(CommandRegistry) private readonly commandRegistry: CommandRegistry;
18+
@inject(MenuModelRegistry)
19+
private readonly menuModelRegistry: MenuModelRegistry;
20+
@inject(ContextKeyService)
21+
private readonly contextKeyService: ContextKeyService;
22+
23+
protected override render(): React.ReactNode {
24+
const { state } = this.model;
25+
return (
26+
<React.Fragment>
27+
{this.renderContributedCommands()}
28+
{this.renderContinue()}
29+
<DebugAction
30+
enabled={state === DebugState.Stopped}
31+
run={this.stepOver}
32+
label={nls.localizeByDefault('Step Over')}
33+
iconClass="debug-step-over"
34+
ref={this.setStepRef}
35+
/>
36+
<DebugAction
37+
enabled={state === DebugState.Stopped}
38+
run={this.stepIn}
39+
label={nls.localizeByDefault('Step Into')}
40+
iconClass="debug-step-into"
41+
/>
42+
<DebugAction
43+
enabled={state === DebugState.Stopped}
44+
run={this.stepOut}
45+
label={nls.localizeByDefault('Step Out')}
46+
iconClass="debug-step-out"
47+
/>
48+
<DebugAction
49+
enabled={state !== DebugState.Inactive}
50+
run={this.restart}
51+
label={nls.localizeByDefault('Restart')}
52+
iconClass="debug-restart"
53+
/>
54+
{this.renderStart()}
55+
</React.Fragment>
56+
);
57+
}
58+
59+
private renderContributedCommands(): React.ReactNode {
60+
return this.menuModelRegistry
61+
.getMenu(TheiaDebugToolbar.MENU)
62+
.children.filter((node) => node instanceof CompositeMenuNode)
63+
.map((node) => (node as CompositeMenuNode).children)
64+
.reduce((acc, curr) => acc.concat(curr), [])
65+
.filter((node) => node instanceof ActionMenuNode)
66+
.map((node) => this.debugAction(node as ActionMenuNode));
67+
}
68+
69+
private debugAction(node: ActionMenuNode): React.ReactNode {
70+
const { label, command, when, icon: iconClass = '' } = node;
71+
const run = () => this.commandRegistry.executeCommand(command);
72+
const enabled = when ? this.contextKeyService.match(when) : true;
73+
return (
74+
enabled && (
75+
<DebugAction
76+
key={command}
77+
enabled={enabled}
78+
label={label}
79+
iconClass={iconClass}
80+
run={run}
81+
/>
82+
)
83+
);
84+
}
85+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import {
2+
Disposable,
3+
DisposableCollection,
4+
} from '@theia/core/lib/common/disposable';
5+
import { DebuggerDescription } from '@theia/debug/lib/common/debug-service';
6+
import { DebugMainImpl as TheiaDebugMainImpl } from '@theia/plugin-ext/lib/main/browser/debug/debug-main';
7+
import { PluginDebugAdapterContribution } from '@theia/plugin-ext/lib/main/browser/debug/plugin-debug-adapter-contribution';
8+
import { PluginDebugSessionFactory } from './plugin-debug-session-factory';
9+
10+
export class DebugMainImpl extends TheiaDebugMainImpl {
11+
override async $registerDebuggerContribution(
12+
description: DebuggerDescription
13+
): Promise<void> {
14+
const debugType = description.type;
15+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
16+
const _this = <any>this;
17+
const terminalOptionsExt = await _this.debugExt.$getTerminalCreationOptions(
18+
debugType
19+
);
20+
21+
if (_this.toDispose.disposed) {
22+
return;
23+
}
24+
25+
const debugSessionFactory = new PluginDebugSessionFactory(
26+
_this.terminalService,
27+
_this.editorManager,
28+
_this.breakpointsManager,
29+
_this.labelProvider,
30+
_this.messages,
31+
_this.outputChannelManager,
32+
_this.debugPreferences,
33+
async (sessionId: string) => {
34+
const connection = await _this.connectionMain.ensureConnection(
35+
sessionId
36+
);
37+
return connection;
38+
},
39+
_this.fileService,
40+
terminalOptionsExt,
41+
_this.debugContributionProvider,
42+
_this.workspaceService
43+
);
44+
45+
const toDispose = new DisposableCollection(
46+
Disposable.create(() => _this.debuggerContributions.delete(debugType))
47+
);
48+
_this.debuggerContributions.set(debugType, toDispose);
49+
toDispose.pushAll([
50+
_this.pluginDebugService.registerDebugAdapterContribution(
51+
new PluginDebugAdapterContribution(
52+
description,
53+
_this.debugExt,
54+
_this.pluginService
55+
)
56+
),
57+
_this.sessionContributionRegistrator.registerDebugSessionContribution({
58+
debugType: description.type,
59+
debugSessionFactory: () => debugSessionFactory,
60+
}),
61+
]);
62+
_this.toDispose.push(
63+
Disposable.create(() => this.$unregisterDebuggerConfiguration(debugType))
64+
);
65+
}
66+
}

‎arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import { Emitter, Event, JsonRpcProxy } from '@theia/core';
22
import { injectable, interfaces } from '@theia/core/shared/inversify';
33
import { HostedPluginServer } from '@theia/plugin-ext/lib/common/plugin-protocol';
4-
import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
4+
import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
5+
import {
6+
HostedPluginSupport as TheiaHostedPluginSupport,
7+
PluginHost,
8+
} from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
9+
import { PluginWorker } from '@theia/plugin-ext/lib/hosted/browser/plugin-worker';
10+
import { setUpPluginApi } from '@theia/plugin-ext/lib/main/browser/main-context';
11+
import { PLUGIN_RPC_CONTEXT } from '@theia/plugin-ext/lib/common/plugin-api-rpc';
12+
import { DebugMainImpl } from './debug-main';
13+
import { ConnectionImpl } from '@theia/plugin-ext/lib/common/connection';
14+
515
@injectable()
616
export class HostedPluginSupport extends TheiaHostedPluginSupport {
717
private readonly onDidLoadEmitter = new Emitter<void>();
@@ -31,4 +41,26 @@ export class HostedPluginSupport extends TheiaHostedPluginSupport {
3141
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3242
return (this as any).server;
3343
}
44+
45+
// to patch the VS Code extension based debugger
46+
// eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
47+
protected override initRpc(host: PluginHost, pluginId: string): RPCProtocol {
48+
const rpc =
49+
host === 'frontend' ? new PluginWorker().rpc : this.createServerRpc(host);
50+
setUpPluginApi(rpc, this.container);
51+
this.patchDebugMain(rpc);
52+
this.mainPluginApiProviders
53+
.getContributions()
54+
.forEach((p) => p.initialize(rpc, this.container));
55+
return rpc;
56+
}
57+
58+
private patchDebugMain(rpc: RPCProtocol): void {
59+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
60+
const connectionMain = (rpc as any).locals.get(
61+
PLUGIN_RPC_CONTEXT.CONNECTION_MAIN.id
62+
) as ConnectionImpl;
63+
const debugMain = new DebugMainImpl(rpc, connectionMain, this.container);
64+
rpc.set(PLUGIN_RPC_CONTEXT.DEBUG_MAIN, debugMain);
65+
}
3466
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { DebugSession } from '@theia/debug/lib/browser/debug-session';
2+
import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
3+
import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
4+
import { PluginDebugSessionFactory as TheiaPluginDebugSessionFactory } from '@theia/plugin-ext/lib/main/browser/debug/plugin-debug-session-factory';
5+
import { PluginDebugSession } from './plugin-debug-session';
6+
7+
export class PluginDebugSessionFactory extends TheiaPluginDebugSessionFactory {
8+
override get(
9+
sessionId: string,
10+
options: DebugConfigurationSessionOptions,
11+
parentSession?: DebugSession
12+
): DebugSession {
13+
const connection = new DebugSessionConnection(
14+
sessionId,
15+
this.connectionFactory,
16+
this.getTraceOutputChannel()
17+
);
18+
19+
return new PluginDebugSession(
20+
sessionId,
21+
options,
22+
parentSession,
23+
connection,
24+
this.terminalService,
25+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
26+
this.editorManager as any,
27+
this.breakpoints,
28+
this.labelProvider,
29+
this.messages,
30+
this.fileService,
31+
this.terminalOptionsExt,
32+
this.debugContributionProvider,
33+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
34+
this.workspaceService as any
35+
);
36+
}
37+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { ContributionProvider, MessageClient } from '@theia/core';
2+
import { LabelProvider } from '@theia/core/lib/browser';
3+
import { BreakpointManager } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager';
4+
import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution';
5+
import { DebugSession as TheiaDebugSession } from '@theia/debug/lib/browser/debug-session';
6+
import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
7+
import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
8+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
9+
import { TerminalOptionsExt } from '@theia/plugin-ext';
10+
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
11+
import {
12+
TerminalWidget,
13+
TerminalWidgetOptions,
14+
} from '@theia/terminal/lib/browser/base/terminal-widget';
15+
import { DebugSession } from '../debug/debug-session';
16+
import { EditorManager } from '../editor/editor-manager';
17+
import { WorkspaceService } from '../workspace/workspace-service';
18+
19+
// This class extends the patched debug session, and not the default debug session from Theia
20+
export class PluginDebugSession extends DebugSession {
21+
constructor(
22+
override readonly id: string,
23+
override readonly options: DebugConfigurationSessionOptions,
24+
override readonly parentSession: TheiaDebugSession | undefined,
25+
protected override readonly connection: DebugSessionConnection,
26+
protected override readonly terminalServer: TerminalService,
27+
protected override readonly editorManager: EditorManager,
28+
protected override readonly breakpoints: BreakpointManager,
29+
protected override readonly labelProvider: LabelProvider,
30+
protected override readonly messages: MessageClient,
31+
protected override readonly fileService: FileService,
32+
protected readonly terminalOptionsExt: TerminalOptionsExt | undefined,
33+
protected override readonly debugContributionProvider: ContributionProvider<DebugContribution>,
34+
protected override readonly workspaceService: WorkspaceService
35+
) {
36+
super(
37+
id,
38+
options,
39+
parentSession,
40+
connection,
41+
terminalServer,
42+
editorManager,
43+
breakpoints,
44+
labelProvider,
45+
messages,
46+
fileService,
47+
debugContributionProvider,
48+
workspaceService
49+
);
50+
}
51+
52+
protected override async doCreateTerminal(
53+
terminalWidgetOptions: TerminalWidgetOptions
54+
): Promise<TerminalWidget> {
55+
terminalWidgetOptions = Object.assign(
56+
{},
57+
terminalWidgetOptions,
58+
this.terminalOptionsExt
59+
);
60+
return super.doCreateTerminal(terminalWidgetOptions);
61+
}
62+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { MenuPath } from '@theia/core';
2+
import { TAB_BAR_TOOLBAR_CONTEXT_MENU } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
3+
import { injectable, postConstruct } from '@theia/core/shared/inversify';
4+
import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
5+
import { DebugVariablesWidget } from '@theia/debug/lib/browser/view/debug-variables-widget';
6+
import {
7+
ArgumentAdapter,
8+
PluginMenuCommandAdapter as TheiaPluginMenuCommandAdapter,
9+
} from '@theia/plugin-ext/lib/main/browser/menus/plugin-menu-command-adapter';
10+
import {
11+
codeToTheiaMappings,
12+
ContributionPoint,
13+
} from '@theia/plugin-ext/lib/main/browser/menus/vscode-theia-menu-mappings';
14+
15+
function patch(
16+
toPatch: typeof codeToTheiaMappings,
17+
key: string,
18+
value: MenuPath[]
19+
): void {
20+
const loose = toPatch as Map<string, MenuPath[]>;
21+
if (!loose.has(key)) {
22+
loose.set(key, value);
23+
}
24+
}
25+
// mappings is a const and cannot be customized with DI
26+
patch(codeToTheiaMappings, 'debug/variables/context', [
27+
DebugVariablesWidget.CONTEXT_MENU,
28+
]);
29+
patch(codeToTheiaMappings, 'debug/toolBar', [DebugToolBar.MENU]);
30+
31+
@injectable()
32+
export class PluginMenuCommandAdapter extends TheiaPluginMenuCommandAdapter {
33+
@postConstruct()
34+
protected override init(): void {
35+
const toCommentArgs: ArgumentAdapter = (...args) =>
36+
this.toCommentArgs(...args);
37+
const firstArgOnly: ArgumentAdapter = (...args) => [args[0]];
38+
const noArgs: ArgumentAdapter = () => [];
39+
const toScmArgs: ArgumentAdapter = (...args) => this.toScmArgs(...args);
40+
const selectedResource = () => this.getSelectedResources();
41+
const widgetURI: ArgumentAdapter = (widget) =>
42+
this.codeEditorUtil.is(widget)
43+
? [this.codeEditorUtil.getResourceUri(widget)]
44+
: [];
45+
(<Array<[ContributionPoint, ArgumentAdapter | undefined]>>[
46+
['comments/comment/context', toCommentArgs],
47+
['comments/comment/title', toCommentArgs],
48+
['comments/commentThread/context', toCommentArgs],
49+
['debug/callstack/context', firstArgOnly],
50+
['debug/variables/context', firstArgOnly],
51+
['debug/toolBar', noArgs],
52+
['editor/context', selectedResource],
53+
['editor/title', widgetURI],
54+
['editor/title/context', selectedResource],
55+
['explorer/context', selectedResource],
56+
['scm/resourceFolder/context', toScmArgs],
57+
['scm/resourceGroup/context', toScmArgs],
58+
['scm/resourceState/context', toScmArgs],
59+
['scm/title', () => this.toScmArg(this.scmService.selectedRepository)],
60+
['timeline/item/context', (...args) => this.toTimelineArgs(...args)],
61+
['view/item/context', (...args) => this.toTreeArgs(...args)],
62+
['view/title', noArgs],
63+
]).forEach(([contributionPoint, adapter]) => {
64+
if (adapter) {
65+
const paths = codeToTheiaMappings.get(contributionPoint);
66+
if (paths) {
67+
paths.forEach((path) => this.addArgumentAdapter(path, adapter));
68+
}
69+
}
70+
});
71+
this.addArgumentAdapter(TAB_BAR_TOOLBAR_CONTEXT_MENU, widgetURI);
72+
}
73+
}

0 commit comments

Comments
 (0)
Please sign in to comment.