diff --git a/.vscode/launch.json b/.vscode/launch.json
index 06f959e85..f892c3fc5 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -21,7 +21,8 @@
"--plugins=local-dir:../plugins",
"--hosted-plugin-inspect=9339",
"--content-trace",
- "--open-devtools"
+ "--open-devtools",
+ "--no-ping-timeout",
],
"env": {
"NODE_ENV": "development"
@@ -56,7 +57,8 @@
"--remote-debugging-port=9222",
"--no-app-auto-install",
"--plugins=local-dir:../plugins",
- "--hosted-plugin-inspect=9339"
+ "--hosted-plugin-inspect=9339",
+ "--no-ping-timeout",
],
"env": {
"NODE_ENV": "development"
diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json
index 6822ec4cf..7d75f7217 100644
--- a/arduino-ide-extension/package.json
+++ b/arduino-ide-extension/package.json
@@ -60,6 +60,7 @@
"@types/react-virtualized": "^9.21.21",
"@types/temp": "^0.8.34",
"@types/which": "^1.3.1",
+ "@vscode/debugprotocol": "^1.51.0",
"arduino-serial-plotter-webapp": "0.2.0",
"async-mutex": "^0.3.0",
"auth0-js": "^9.14.0",
diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
index ba13b1b11..22ea313f2 100644
--- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
+++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
@@ -1,5 +1,5 @@
import '../../src/browser/style/index.css';
-import { ContainerModule } from '@theia/core/shared/inversify';
+import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
import { CommandContribution } from '@theia/core/lib/common/command';
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
@@ -331,6 +331,18 @@ import { TypeHierarchyServiceProvider } from './theia/typehierarchy/type-hierarc
import { TypeHierarchyServiceProvider as TheiaTypeHierarchyServiceProvider } from '@theia/typehierarchy/lib/browser/typehierarchy-service';
import { TypeHierarchyContribution } from './theia/typehierarchy/type-hierarchy-contribution';
import { TypeHierarchyContribution as TheiaTypeHierarchyContribution } from '@theia/typehierarchy/lib/browser/typehierarchy-contribution';
+import { DefaultDebugSessionFactory } from './theia/debug/debug-session-contribution';
+import { DebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
+import { DebugToolbar } from './theia/debug/debug-toolbar-widget';
+import { DebugToolBar as TheiaDebugToolbar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
+import { PluginMenuCommandAdapter } from './theia/plugin-ext/plugin-menu-command-adapter';
+import { PluginMenuCommandAdapter as TheiaPluginMenuCommandAdapter } from '@theia/plugin-ext/lib/main/browser/menus/plugin-menu-command-adapter';
+import { DebugSessionManager } from './theia/debug/debug-session-manager';
+import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
+import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
+import { DebugViewModel } from '@theia/debug/lib/browser/view/debug-view-model';
+import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-widget';
+import { DebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
// Commands and toolbar items
@@ -960,4 +972,36 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
);
bind(TypeHierarchyContribution).toSelf().inSingletonScope();
rebind(TheiaTypeHierarchyContribution).toService(TypeHierarchyContribution);
+
+ // patched the debugger for `cortex-debug@1.5.1`
+ // https://github.com/eclipse-theia/theia/issues/11871
+ // https://github.com/eclipse-theia/theia/issues/11879
+ // https://github.com/eclipse-theia/theia/issues/11880
+ // https://github.com/eclipse-theia/theia/issues/11885
+ // https://github.com/eclipse-theia/theia/issues/11886
+ // https://github.com/eclipse-theia/theia/issues/11916
+ // based on: https://github.com/eclipse-theia/theia/compare/master...kittaakos:theia:%2311871
+ bind(DefaultDebugSessionFactory).toSelf().inSingletonScope();
+ rebind(DebugSessionFactory).toService(DefaultDebugSessionFactory);
+ bind(DebugSessionManager).toSelf().inSingletonScope();
+ rebind(TheiaDebugSessionManager).toService(DebugSessionManager);
+ bind(DebugToolbar).toSelf().inSingletonScope();
+ rebind(TheiaDebugToolbar).toService(DebugToolbar);
+ bind(PluginMenuCommandAdapter).toSelf().inSingletonScope();
+ rebind(TheiaPluginMenuCommandAdapter).toService(PluginMenuCommandAdapter);
+ bind(WidgetFactory)
+ .toDynamicValue(({ container }) => ({
+ id: DebugWidget.ID,
+ createWidget: () => {
+ const child = new Container({ defaultScope: 'Singleton' });
+ child.parent = container;
+ child.bind(DebugViewModel).toSelf();
+ child.bind(DebugToolbar).toSelf(); // patched toolbar
+ child.bind(DebugSessionWidget).toSelf();
+ child.bind(DebugConfigurationWidget).toSelf();
+ child.bind(DebugWidget).toSelf();
+ return child.get(DebugWidget);
+ },
+ }))
+ .inSingletonScope();
});
diff --git a/arduino-ide-extension/src/browser/style/index.css b/arduino-ide-extension/src/browser/style/index.css
index 638403697..6aa967304 100644
--- a/arduino-ide-extension/src/browser/style/index.css
+++ b/arduino-ide-extension/src/browser/style/index.css
@@ -176,3 +176,13 @@ button.theia-button.message-box-dialog-button {
outline: 1px dashed var(--theia-focusBorder);
outline-offset: -2px;
}
+
+.debug-toolbar .debug-action>div {
+ font-family: var(--theia-ui-font-family);
+ font-size: var(--theia-ui-font-size0);
+ display: flex;
+ align-items: center;
+ align-self: center;
+ justify-content: center;
+ min-height: inherit;
+}
diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-action.tsx b/arduino-ide-extension/src/browser/theia/debug/debug-action.tsx
new file mode 100644
index 000000000..c0f691b49
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/debug/debug-action.tsx
@@ -0,0 +1,29 @@
+import * as React from '@theia/core/shared/react';
+import { DebugAction as TheiaDebugAction } from '@theia/debug/lib/browser/view/debug-action';
+import {
+ codiconArray,
+ DISABLED_CLASS,
+} from '@theia/core/lib/browser/widgets/widget';
+
+// customized debug action to show the contributed command's label when there is no icon
+export class DebugAction extends TheiaDebugAction {
+ override render(): React.ReactNode {
+ const { enabled, label, iconClass } = this.props;
+ const classNames = ['debug-action', ...codiconArray(iconClass, true)];
+ if (enabled === false) {
+ classNames.push(DISABLED_CLASS);
+ }
+ return (
+
+ {!iconClass ||
+ (iconClass.match(/plugin-icon-\d+/) && {label}
)}
+
+ );
+ }
+}
diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-session-contribution.ts b/arduino-ide-extension/src/browser/theia/debug/debug-session-contribution.ts
new file mode 100644
index 000000000..d0ba503ef
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/debug/debug-session-contribution.ts
@@ -0,0 +1,49 @@
+import { injectable } from '@theia/core/shared/inversify';
+import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
+import { DefaultDebugSessionFactory as TheiaDefaultDebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
+import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
+import {
+ DebugAdapterPath,
+ DebugChannel,
+ ForwardingDebugChannel,
+} from '@theia/debug/lib/common/debug-service';
+import { DebugSession } from './debug-session';
+
+@injectable()
+export class DefaultDebugSessionFactory extends TheiaDefaultDebugSessionFactory {
+ override get(
+ sessionId: string,
+ options: DebugConfigurationSessionOptions,
+ parentSession?: DebugSession
+ ): DebugSession {
+ const connection = new DebugSessionConnection(
+ sessionId,
+ () =>
+ new Promise((resolve) =>
+ this.connectionProvider.openChannel(
+ `${DebugAdapterPath}/${sessionId}`,
+ (wsChannel) => {
+ resolve(new ForwardingDebugChannel(wsChannel));
+ },
+ { reconnecting: false }
+ )
+ ),
+ this.getTraceOutputChannel()
+ );
+ // patched debug session
+ return new DebugSession(
+ sessionId,
+ options,
+ parentSession,
+ connection,
+ this.terminalService,
+ this.editorManager,
+ this.breakpoints,
+ this.labelProvider,
+ this.messages,
+ this.fileService,
+ this.debugContributionProvider,
+ this.workspaceService
+ );
+ }
+}
diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-session-manager.ts b/arduino-ide-extension/src/browser/theia/debug/debug-session-manager.ts
new file mode 100644
index 000000000..f641a6535
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/debug/debug-session-manager.ts
@@ -0,0 +1,120 @@
+import type { ContextKey } from '@theia/core/lib/browser/context-key-service';
+import { injectable, postConstruct } from '@theia/core/shared/inversify';
+import {
+ DebugSession,
+ DebugState,
+} from '@theia/debug/lib/browser/debug-session';
+import { DebugSessionManager as TheiaDebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
+import type { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
+
+function debugStateLabel(state: DebugState): string {
+ switch (state) {
+ case DebugState.Initializing:
+ return 'initializing';
+ case DebugState.Stopped:
+ return 'stopped';
+ case DebugState.Running:
+ return 'running';
+ default:
+ return 'inactive';
+ }
+}
+
+@injectable()
+export class DebugSessionManager extends TheiaDebugSessionManager {
+ protected debugStateKey: ContextKey;
+
+ @postConstruct()
+ protected override init(): void {
+ this.debugStateKey = this.contextKeyService.createKey(
+ 'debugState',
+ debugStateLabel(this.state)
+ );
+ super.init();
+ }
+
+ protected override fireDidChange(current: DebugSession | undefined): void {
+ this.debugTypeKey.set(current?.configuration.type);
+ this.inDebugModeKey.set(this.inDebugMode);
+ this.debugStateKey.set(debugStateLabel(this.state));
+ this.onDidChangeEmitter.fire(current);
+ }
+
+ protected override async doStart(
+ sessionId: string,
+ options: DebugConfigurationSessionOptions
+ ): Promise {
+ const parentSession =
+ options.configuration.parentSession &&
+ this._sessions.get(options.configuration.parentSession.id);
+ const contrib = this.sessionContributionRegistry.get(
+ options.configuration.type
+ );
+ const sessionFactory = contrib
+ ? contrib.debugSessionFactory()
+ : this.debugSessionFactory;
+ const session = sessionFactory.get(sessionId, options, parentSession);
+ this._sessions.set(sessionId, session);
+
+ this.debugTypeKey.set(session.configuration.type);
+ // this.onDidCreateDebugSessionEmitter.fire(session); // defer the didCreate event after start https://github.com/eclipse-theia/theia/issues/11916
+
+ let state = DebugState.Inactive;
+ session.onDidChange(() => {
+ if (state !== session.state) {
+ state = session.state;
+ if (state === DebugState.Stopped) {
+ this.onDidStopDebugSessionEmitter.fire(session);
+ }
+ }
+ this.updateCurrentSession(session);
+ });
+ session.onDidChangeBreakpoints((uri) =>
+ this.fireDidChangeBreakpoints({ session, uri })
+ );
+ session.on('terminated', async (event) => {
+ const restart = event.body && event.body.restart;
+ if (restart) {
+ // postDebugTask isn't run in case of auto restart as well as preLaunchTask
+ this.doRestart(session, !!restart);
+ } else {
+ await session.disconnect(false, () =>
+ this.debug.terminateDebugSession(session.id)
+ );
+ await this.runTask(
+ session.options.workspaceFolderUri,
+ session.configuration.postDebugTask
+ );
+ }
+ });
+
+ // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
+ session.on('exited', async (event) => {
+ await session.disconnect(false, () =>
+ this.debug.terminateDebugSession(session.id)
+ );
+ });
+
+ session.onDispose(() => this.cleanup(session));
+ session
+ .start()
+ .then(() => {
+ this.onDidCreateDebugSessionEmitter.fire(session); // now fire the didCreate event
+ this.onDidStartDebugSessionEmitter.fire(session);
+ })
+ // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
+ .catch((e) => {
+ session.stop(false, () => {
+ this.debug.terminateDebugSession(session.id);
+ });
+ });
+ session.onDidCustomEvent(({ event, body }) =>
+ this.onDidReceiveDebugSessionCustomEventEmitter.fire({
+ event,
+ body,
+ session,
+ })
+ );
+ return session;
+ }
+}
diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-session.ts b/arduino-ide-extension/src/browser/theia/debug/debug-session.ts
new file mode 100644
index 000000000..7db51c2ac
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/debug/debug-session.ts
@@ -0,0 +1,231 @@
+import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
+import { Deferred } from '@theia/core/lib/common/promise-util';
+import { Mutable } from '@theia/core/lib/common/types';
+import { URI } from '@theia/core/lib/common/uri';
+import { DebugSession as TheiaDebugSession } from '@theia/debug/lib/browser/debug-session';
+import { DebugFunctionBreakpoint } from '@theia/debug/lib/browser/model/debug-function-breakpoint';
+import { DebugSourceBreakpoint } from '@theia/debug/lib/browser/model/debug-source-breakpoint';
+import {
+ DebugThreadData,
+ StoppedDetails,
+} from '@theia/debug/lib/browser/model/debug-thread';
+import { DebugProtocol } from '@vscode/debugprotocol';
+import { DebugThread } from './debug-thread';
+
+export class DebugSession extends TheiaDebugSession {
+ /**
+ * The `send('initialize')` request resolves later than `on('initialized')` emits the event.
+ * Hence, the `configure` would use the empty object `capabilities`.
+ * Using the empty `capabilities` could result in missing exception breakpoint filters, as
+ * always `capabilities.exceptionBreakpointFilters` is falsy. This deferred promise works
+ * around this timing issue.
+ * See: https://github.com/eclipse-theia/theia/issues/11886.
+ */
+ protected didReceiveCapabilities = new Deferred();
+
+ protected override async initialize(): Promise {
+ const clientName = FrontendApplicationConfigProvider.get().applicationName;
+ try {
+ const response = await this.connection.sendRequest('initialize', {
+ clientID: clientName.toLocaleLowerCase().replace(/ /g, '_'),
+ clientName,
+ adapterID: this.configuration.type,
+ locale: 'en-US',
+ linesStartAt1: true,
+ columnsStartAt1: true,
+ pathFormat: 'path',
+ supportsVariableType: false,
+ supportsVariablePaging: false,
+ supportsRunInTerminalRequest: true,
+ });
+ this.updateCapabilities(response?.body || {});
+ this.didReceiveCapabilities.resolve();
+ } catch (err) {
+ this.didReceiveCapabilities.reject(err);
+ throw err;
+ }
+ }
+
+ protected override async configure(): Promise {
+ await this.didReceiveCapabilities.promise;
+ return super.configure();
+ }
+
+ override async stop(isRestart: boolean, callback: () => void): Promise {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const _this = this as any;
+ if (!_this.isStopping) {
+ _this.isStopping = true;
+ if (this.configuration.lifecycleManagedByParent && this.parentSession) {
+ await this.parentSession.stop(isRestart, callback);
+ } else {
+ if (this.canTerminate()) {
+ const terminated = this.waitFor('terminated', 5000);
+ try {
+ await this.connection.sendRequest(
+ 'terminate',
+ { restart: isRestart },
+ 5000
+ );
+ await terminated;
+ } catch (e) {
+ console.error('Did not receive terminated event in time', e);
+ }
+ } else {
+ const terminateDebuggee =
+ this.initialized && this.capabilities.supportTerminateDebuggee;
+ // Related https://github.com/microsoft/vscode/issues/165138
+ try {
+ await this.sendRequest(
+ 'disconnect',
+ { restart: isRestart, terminateDebuggee },
+ 2000
+ );
+ } catch (err) {
+ if (
+ 'message' in err &&
+ typeof err.message === 'string' &&
+ err.message.test(err.message)
+ ) {
+ // VS Code ignores errors when sending the `disconnect` request.
+ // Debug adapter might not send the `disconnected` event as a response.
+ } else {
+ throw err;
+ }
+ }
+ }
+ callback();
+ }
+ }
+ }
+
+ protected override async sendFunctionBreakpoints(
+ affectedUri: URI
+ ): Promise {
+ const all = this.breakpoints
+ .getFunctionBreakpoints()
+ .map(
+ (origin) =>
+ new DebugFunctionBreakpoint(origin, this.asDebugBreakpointOptions())
+ );
+ const enabled = all.filter((b) => b.enabled);
+ if (this.capabilities.supportsFunctionBreakpoints) {
+ try {
+ const response = await this.sendRequest('setFunctionBreakpoints', {
+ breakpoints: enabled.map((b) => b.origin.raw),
+ });
+ // Apparently, `body` and `breakpoints` can be missing.
+ // https://github.com/eclipse-theia/theia/issues/11885
+ // https://github.com/microsoft/vscode/blob/80004351ccf0884b58359f7c8c801c91bb827d83/src/vs/workbench/contrib/debug/browser/debugSession.ts#L448-L449
+ if (response && response.body) {
+ response.body.breakpoints.forEach((raw, index) => {
+ // node debug adapter returns more breakpoints sometimes
+ if (enabled[index]) {
+ enabled[index].update({ raw });
+ }
+ });
+ }
+ } catch (error) {
+ // could be error or promise rejection of DebugProtocol.SetFunctionBreakpoints
+ if (error instanceof Error) {
+ console.error(`Error setting breakpoints: ${error.message}`);
+ } else {
+ // handle adapters that send failed DebugProtocol.SetFunctionBreakpoints for invalid breakpoints
+ const genericMessage =
+ 'Function breakpoint not valid for current debug session';
+ const message = error.message ? `${error.message}` : genericMessage;
+ console.warn(
+ `Could not handle function breakpoints: ${message}, disabling...`
+ );
+ enabled.forEach((b) =>
+ b.update({
+ raw: {
+ verified: false,
+ message,
+ },
+ })
+ );
+ }
+ }
+ }
+ this.setBreakpoints(affectedUri, all);
+ }
+
+ protected override async sendSourceBreakpoints(
+ affectedUri: URI,
+ sourceModified?: boolean
+ ): Promise {
+ const source = await this.toSource(affectedUri);
+ const all = this.breakpoints
+ .findMarkers({ uri: affectedUri })
+ .map(
+ ({ data }) =>
+ new DebugSourceBreakpoint(data, this.asDebugBreakpointOptions())
+ );
+ const enabled = all.filter((b) => b.enabled);
+ try {
+ const breakpoints = enabled.map(({ origin }) => origin.raw);
+ const response = await this.sendRequest('setBreakpoints', {
+ source: source.raw,
+ sourceModified,
+ breakpoints,
+ lines: breakpoints.map(({ line }) => line),
+ });
+ response.body.breakpoints.forEach((raw, index) => {
+ // node debug adapter returns more breakpoints sometimes
+ if (enabled[index]) {
+ enabled[index].update({ raw });
+ }
+ });
+ } catch (error) {
+ // could be error or promise rejection of DebugProtocol.SetBreakpointsResponse
+ if (error instanceof Error) {
+ console.error(`Error setting breakpoints: ${error.message}`);
+ } else {
+ // handle adapters that send failed DebugProtocol.SetBreakpointsResponse for invalid breakpoints
+ const genericMessage = 'Breakpoint not valid for current debug session';
+ const message = error.message ? `${error.message}` : genericMessage;
+ console.warn(
+ `Could not handle breakpoints for ${affectedUri}: ${message}, disabling...`
+ );
+ enabled.forEach((b) =>
+ b.update({
+ raw: {
+ verified: false,
+ message,
+ },
+ })
+ );
+ }
+ }
+ this.setSourceBreakpoints(affectedUri, all);
+ }
+
+ protected override doUpdateThreads(
+ threads: DebugProtocol.Thread[],
+ stoppedDetails?: StoppedDetails
+ ): void {
+ const existing = this._threads;
+ this._threads = new Map();
+ for (const raw of threads) {
+ const id = raw.id;
+ const thread = existing.get(id) || new DebugThread(this); // patched debug thread
+ this._threads.set(id, thread);
+ const data: Partial> = { raw };
+ if (stoppedDetails) {
+ if (stoppedDetails.threadId === id) {
+ data.stoppedDetails = stoppedDetails;
+ } else if (stoppedDetails.allThreadsStopped) {
+ data.stoppedDetails = {
+ // When a debug adapter notifies us that all threads are stopped,
+ // we do not know why the others are stopped, so we should default
+ // to something generic.
+ reason: '',
+ };
+ }
+ }
+ thread.update(data);
+ }
+ this.updateCurrentThread(stoppedDetails);
+ }
+}
diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-stack-frame.ts b/arduino-ide-extension/src/browser/theia/debug/debug-stack-frame.ts
new file mode 100644
index 000000000..c52e238ae
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/debug/debug-stack-frame.ts
@@ -0,0 +1,32 @@
+import { WidgetOpenerOptions } from '@theia/core/lib/browser/widget-open-handler';
+import { Range } from '@theia/core/shared/vscode-languageserver-types';
+import { DebugStackFrame as TheiaDebugStackFrame } from '@theia/debug/lib/browser/model/debug-stack-frame';
+import { EditorWidget } from '@theia/editor/lib/browser/editor-widget';
+
+export class DebugStackFrame extends TheiaDebugStackFrame {
+ override async open(
+ options: WidgetOpenerOptions = {
+ mode: 'reveal',
+ }
+ ): Promise {
+ if (!this.source) {
+ return undefined;
+ }
+ const { line, column, endLine, endColumn, source } = this.raw;
+ if (!source) {
+ return undefined;
+ }
+ // create selection based on VS Code
+ // https://github.com/eclipse-theia/theia/issues/11880
+ const selection = Range.create(
+ line,
+ column,
+ endLine || line,
+ endColumn || column
+ );
+ this.source.open({
+ ...options,
+ selection,
+ });
+ }
+}
diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-thread.ts b/arduino-ide-extension/src/browser/theia/debug/debug-thread.ts
new file mode 100644
index 000000000..bb0d3313c
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/debug/debug-thread.ts
@@ -0,0 +1,22 @@
+import { DebugStackFrame as TheiaDebugStackFrame } from '@theia/debug/lib/browser/model/debug-stack-frame';
+import { DebugThread as TheiaDebugThread } from '@theia/debug/lib/browser/model/debug-thread';
+import { DebugProtocol } from '@vscode/debugprotocol';
+import { DebugStackFrame } from './debug-stack-frame';
+
+export class DebugThread extends TheiaDebugThread {
+ protected override doUpdateFrames(
+ frames: DebugProtocol.StackFrame[]
+ ): TheiaDebugStackFrame[] {
+ const result = new Set();
+ for (const raw of frames) {
+ const id = raw.id;
+ const frame =
+ this._frames.get(id) || new DebugStackFrame(this, this.session); // patched debug stack frame
+ this._frames.set(id, frame);
+ frame.update({ raw });
+ result.add(frame);
+ }
+ this.updateCurrentFrame();
+ return [...result.values()];
+ }
+}
diff --git a/arduino-ide-extension/src/browser/theia/debug/debug-toolbar-widget.tsx b/arduino-ide-extension/src/browser/theia/debug/debug-toolbar-widget.tsx
new file mode 100644
index 000000000..bc6e135e8
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/debug/debug-toolbar-widget.tsx
@@ -0,0 +1,85 @@
+import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
+import { CommandRegistry } from '@theia/core/lib/common/command';
+import {
+ ActionMenuNode,
+ CompositeMenuNode,
+ MenuModelRegistry,
+} from '@theia/core/lib/common/menu';
+import { nls } from '@theia/core/lib/common/nls';
+import { inject, injectable } from '@theia/core/shared/inversify';
+import * as React from '@theia/core/shared/react';
+import { DebugState } from '@theia/debug/lib/browser/debug-session';
+import { DebugAction } from './debug-action';
+import { DebugToolBar as TheiaDebugToolbar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
+
+@injectable()
+export class DebugToolbar extends TheiaDebugToolbar {
+ @inject(CommandRegistry) private readonly commandRegistry: CommandRegistry;
+ @inject(MenuModelRegistry)
+ private readonly menuModelRegistry: MenuModelRegistry;
+ @inject(ContextKeyService)
+ private readonly contextKeyService: ContextKeyService;
+
+ protected override render(): React.ReactNode {
+ const { state } = this.model;
+ return (
+
+ {this.renderContributedCommands()}
+ {this.renderContinue()}
+
+
+
+
+ {this.renderStart()}
+
+ );
+ }
+
+ private renderContributedCommands(): React.ReactNode {
+ return this.menuModelRegistry
+ .getMenu(TheiaDebugToolbar.MENU)
+ .children.filter((node) => node instanceof CompositeMenuNode)
+ .map((node) => (node as CompositeMenuNode).children)
+ .reduce((acc, curr) => acc.concat(curr), [])
+ .filter((node) => node instanceof ActionMenuNode)
+ .map((node) => this.debugAction(node as ActionMenuNode));
+ }
+
+ private debugAction(node: ActionMenuNode): React.ReactNode {
+ const { label, command, when, icon: iconClass = '' } = node;
+ const run = () => this.commandRegistry.executeCommand(command);
+ const enabled = when ? this.contextKeyService.match(when) : true;
+ return (
+ enabled && (
+
+ )
+ );
+ }
+}
diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/debug-main.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/debug-main.ts
new file mode 100644
index 000000000..0845d6196
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/plugin-ext/debug-main.ts
@@ -0,0 +1,66 @@
+import {
+ Disposable,
+ DisposableCollection,
+} from '@theia/core/lib/common/disposable';
+import { DebuggerDescription } from '@theia/debug/lib/common/debug-service';
+import { DebugMainImpl as TheiaDebugMainImpl } from '@theia/plugin-ext/lib/main/browser/debug/debug-main';
+import { PluginDebugAdapterContribution } from '@theia/plugin-ext/lib/main/browser/debug/plugin-debug-adapter-contribution';
+import { PluginDebugSessionFactory } from './plugin-debug-session-factory';
+
+export class DebugMainImpl extends TheiaDebugMainImpl {
+ override async $registerDebuggerContribution(
+ description: DebuggerDescription
+ ): Promise {
+ const debugType = description.type;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const _this = this;
+ const terminalOptionsExt = await _this.debugExt.$getTerminalCreationOptions(
+ debugType
+ );
+
+ if (_this.toDispose.disposed) {
+ return;
+ }
+
+ const debugSessionFactory = new PluginDebugSessionFactory(
+ _this.terminalService,
+ _this.editorManager,
+ _this.breakpointsManager,
+ _this.labelProvider,
+ _this.messages,
+ _this.outputChannelManager,
+ _this.debugPreferences,
+ async (sessionId: string) => {
+ const connection = await _this.connectionMain.ensureConnection(
+ sessionId
+ );
+ return connection;
+ },
+ _this.fileService,
+ terminalOptionsExt,
+ _this.debugContributionProvider,
+ _this.workspaceService
+ );
+
+ const toDispose = new DisposableCollection(
+ Disposable.create(() => _this.debuggerContributions.delete(debugType))
+ );
+ _this.debuggerContributions.set(debugType, toDispose);
+ toDispose.pushAll([
+ _this.pluginDebugService.registerDebugAdapterContribution(
+ new PluginDebugAdapterContribution(
+ description,
+ _this.debugExt,
+ _this.pluginService
+ )
+ ),
+ _this.sessionContributionRegistrator.registerDebugSessionContribution({
+ debugType: description.type,
+ debugSessionFactory: () => debugSessionFactory,
+ }),
+ ]);
+ _this.toDispose.push(
+ Disposable.create(() => this.$unregisterDebuggerConfiguration(debugType))
+ );
+ }
+}
diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts
index 4ef4b1f55..666a6eedc 100644
--- a/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts
+++ b/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts
@@ -1,7 +1,17 @@
import { Emitter, Event, JsonRpcProxy } from '@theia/core';
import { injectable, interfaces } from '@theia/core/shared/inversify';
import { HostedPluginServer } from '@theia/plugin-ext/lib/common/plugin-protocol';
-import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
+import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
+import {
+ HostedPluginSupport as TheiaHostedPluginSupport,
+ PluginHost,
+} from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin';
+import { PluginWorker } from '@theia/plugin-ext/lib/hosted/browser/plugin-worker';
+import { setUpPluginApi } from '@theia/plugin-ext/lib/main/browser/main-context';
+import { PLUGIN_RPC_CONTEXT } from '@theia/plugin-ext/lib/common/plugin-api-rpc';
+import { DebugMainImpl } from './debug-main';
+import { ConnectionImpl } from '@theia/plugin-ext/lib/common/connection';
+
@injectable()
export class HostedPluginSupport extends TheiaHostedPluginSupport {
private readonly onDidLoadEmitter = new Emitter();
@@ -31,4 +41,26 @@ export class HostedPluginSupport extends TheiaHostedPluginSupport {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (this as any).server;
}
+
+ // to patch the VS Code extension based debugger
+ // eslint-disable-next-line unused-imports/no-unused-vars, @typescript-eslint/no-unused-vars
+ protected override initRpc(host: PluginHost, pluginId: string): RPCProtocol {
+ const rpc =
+ host === 'frontend' ? new PluginWorker().rpc : this.createServerRpc(host);
+ setUpPluginApi(rpc, this.container);
+ this.patchDebugMain(rpc);
+ this.mainPluginApiProviders
+ .getContributions()
+ .forEach((p) => p.initialize(rpc, this.container));
+ return rpc;
+ }
+
+ private patchDebugMain(rpc: RPCProtocol): void {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const connectionMain = (rpc as any).locals.get(
+ PLUGIN_RPC_CONTEXT.CONNECTION_MAIN.id
+ ) as ConnectionImpl;
+ const debugMain = new DebugMainImpl(rpc, connectionMain, this.container);
+ rpc.set(PLUGIN_RPC_CONTEXT.DEBUG_MAIN, debugMain);
+ }
}
diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session-factory.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session-factory.ts
new file mode 100644
index 000000000..a84275e1a
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session-factory.ts
@@ -0,0 +1,37 @@
+import { DebugSession } from '@theia/debug/lib/browser/debug-session';
+import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
+import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
+import { PluginDebugSessionFactory as TheiaPluginDebugSessionFactory } from '@theia/plugin-ext/lib/main/browser/debug/plugin-debug-session-factory';
+import { PluginDebugSession } from './plugin-debug-session';
+
+export class PluginDebugSessionFactory extends TheiaPluginDebugSessionFactory {
+ override get(
+ sessionId: string,
+ options: DebugConfigurationSessionOptions,
+ parentSession?: DebugSession
+ ): DebugSession {
+ const connection = new DebugSessionConnection(
+ sessionId,
+ this.connectionFactory,
+ this.getTraceOutputChannel()
+ );
+
+ return new PluginDebugSession(
+ sessionId,
+ options,
+ parentSession,
+ connection,
+ this.terminalService,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ this.editorManager as any,
+ this.breakpoints,
+ this.labelProvider,
+ this.messages,
+ this.fileService,
+ this.terminalOptionsExt,
+ this.debugContributionProvider,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ this.workspaceService as any
+ );
+ }
+}
diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session.ts
new file mode 100644
index 000000000..28b1de7ef
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-debug-session.ts
@@ -0,0 +1,62 @@
+import { ContributionProvider, MessageClient } from '@theia/core';
+import { LabelProvider } from '@theia/core/lib/browser';
+import { BreakpointManager } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager';
+import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution';
+import { DebugSession as TheiaDebugSession } from '@theia/debug/lib/browser/debug-session';
+import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
+import { DebugConfigurationSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
+import { FileService } from '@theia/filesystem/lib/browser/file-service';
+import { TerminalOptionsExt } from '@theia/plugin-ext';
+import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
+import {
+ TerminalWidget,
+ TerminalWidgetOptions,
+} from '@theia/terminal/lib/browser/base/terminal-widget';
+import { DebugSession } from '../debug/debug-session';
+import { EditorManager } from '../editor/editor-manager';
+import { WorkspaceService } from '../workspace/workspace-service';
+
+// This class extends the patched debug session, and not the default debug session from Theia
+export class PluginDebugSession extends DebugSession {
+ constructor(
+ override readonly id: string,
+ override readonly options: DebugConfigurationSessionOptions,
+ override readonly parentSession: TheiaDebugSession | undefined,
+ protected override readonly connection: DebugSessionConnection,
+ protected override readonly terminalServer: TerminalService,
+ protected override readonly editorManager: EditorManager,
+ protected override readonly breakpoints: BreakpointManager,
+ protected override readonly labelProvider: LabelProvider,
+ protected override readonly messages: MessageClient,
+ protected override readonly fileService: FileService,
+ protected readonly terminalOptionsExt: TerminalOptionsExt | undefined,
+ protected override readonly debugContributionProvider: ContributionProvider,
+ protected override readonly workspaceService: WorkspaceService
+ ) {
+ super(
+ id,
+ options,
+ parentSession,
+ connection,
+ terminalServer,
+ editorManager,
+ breakpoints,
+ labelProvider,
+ messages,
+ fileService,
+ debugContributionProvider,
+ workspaceService
+ );
+ }
+
+ protected override async doCreateTerminal(
+ terminalWidgetOptions: TerminalWidgetOptions
+ ): Promise {
+ terminalWidgetOptions = Object.assign(
+ {},
+ terminalWidgetOptions,
+ this.terminalOptionsExt
+ );
+ return super.doCreateTerminal(terminalWidgetOptions);
+ }
+}
diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-menu-command-adapter.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-menu-command-adapter.ts
new file mode 100644
index 000000000..156bbb2cb
--- /dev/null
+++ b/arduino-ide-extension/src/browser/theia/plugin-ext/plugin-menu-command-adapter.ts
@@ -0,0 +1,73 @@
+import { MenuPath } from '@theia/core';
+import { TAB_BAR_TOOLBAR_CONTEXT_MENU } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
+import { injectable, postConstruct } from '@theia/core/shared/inversify';
+import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
+import { DebugVariablesWidget } from '@theia/debug/lib/browser/view/debug-variables-widget';
+import {
+ ArgumentAdapter,
+ PluginMenuCommandAdapter as TheiaPluginMenuCommandAdapter,
+} from '@theia/plugin-ext/lib/main/browser/menus/plugin-menu-command-adapter';
+import {
+ codeToTheiaMappings,
+ ContributionPoint,
+} from '@theia/plugin-ext/lib/main/browser/menus/vscode-theia-menu-mappings';
+
+function patch(
+ toPatch: typeof codeToTheiaMappings,
+ key: string,
+ value: MenuPath[]
+): void {
+ const loose = toPatch as Map;
+ if (!loose.has(key)) {
+ loose.set(key, value);
+ }
+}
+// mappings is a const and cannot be customized with DI
+patch(codeToTheiaMappings, 'debug/variables/context', [
+ DebugVariablesWidget.CONTEXT_MENU,
+]);
+patch(codeToTheiaMappings, 'debug/toolBar', [DebugToolBar.MENU]);
+
+@injectable()
+export class PluginMenuCommandAdapter extends TheiaPluginMenuCommandAdapter {
+ @postConstruct()
+ protected override init(): void {
+ const toCommentArgs: ArgumentAdapter = (...args) =>
+ this.toCommentArgs(...args);
+ const firstArgOnly: ArgumentAdapter = (...args) => [args[0]];
+ const noArgs: ArgumentAdapter = () => [];
+ const toScmArgs: ArgumentAdapter = (...args) => this.toScmArgs(...args);
+ const selectedResource = () => this.getSelectedResources();
+ const widgetURI: ArgumentAdapter = (widget) =>
+ this.codeEditorUtil.is(widget)
+ ? [this.codeEditorUtil.getResourceUri(widget)]
+ : [];
+ (>[
+ ['comments/comment/context', toCommentArgs],
+ ['comments/comment/title', toCommentArgs],
+ ['comments/commentThread/context', toCommentArgs],
+ ['debug/callstack/context', firstArgOnly],
+ ['debug/variables/context', firstArgOnly],
+ ['debug/toolBar', noArgs],
+ ['editor/context', selectedResource],
+ ['editor/title', widgetURI],
+ ['editor/title/context', selectedResource],
+ ['explorer/context', selectedResource],
+ ['scm/resourceFolder/context', toScmArgs],
+ ['scm/resourceGroup/context', toScmArgs],
+ ['scm/resourceState/context', toScmArgs],
+ ['scm/title', () => this.toScmArg(this.scmService.selectedRepository)],
+ ['timeline/item/context', (...args) => this.toTimelineArgs(...args)],
+ ['view/item/context', (...args) => this.toTreeArgs(...args)],
+ ['view/title', noArgs],
+ ]).forEach(([contributionPoint, adapter]) => {
+ if (adapter) {
+ const paths = codeToTheiaMappings.get(contributionPoint);
+ if (paths) {
+ paths.forEach((path) => this.addArgumentAdapter(path, adapter));
+ }
+ }
+ });
+ this.addArgumentAdapter(TAB_BAR_TOOLBAR_CONTEXT_MENU, widgetURI);
+ }
+}
diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts
index 9d3ad3553..3c968ae23 100644
--- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts
+++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts
@@ -109,6 +109,10 @@ import {
} from '../common/protocol/survey-service';
import { IsTempSketch } from './is-temp-sketch';
import { rebindNsfwFileSystemWatcher } from './theia/filesystem/nsfw-watcher/nsfw-bindings';
+import { MessagingContribution } from './theia/core/messaging-contribution';
+import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service';
+import { HostedPluginReader } from './theia/plugin-ext/plugin-reader';
+import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(BackendApplication).toSelf().inSingletonScope();
@@ -379,6 +383,15 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
.inSingletonScope();
bind(IsTempSketch).toSelf().inSingletonScope();
+ rebind(MessagingService.Identifier)
+ .to(MessagingContribution)
+ .inSingletonScope();
+
+ // Removed undesired contributions from VS Code extensions
+ // Such as the RTOS view from the `cortex-debug` extension
+ // https://github.com/arduino/arduino-ide/pull/1706#pullrequestreview-1195595080
+ bind(HostedPluginReader).toSelf().inSingletonScope();
+ rebind(TheiaHostedPluginReader).toService(HostedPluginReader);
});
function bindChildLogger(bind: interfaces.Bind, name: string): void {
diff --git a/arduino-ide-extension/src/node/theia/core/messaging-contribution.ts b/arduino-ide-extension/src/node/theia/core/messaging-contribution.ts
new file mode 100644
index 000000000..7c03c7697
--- /dev/null
+++ b/arduino-ide-extension/src/node/theia/core/messaging-contribution.ts
@@ -0,0 +1,11 @@
+import { MessagingContribution as TheiaMessagingContribution } from '@theia/core/lib/node/messaging/messaging-contribution';
+import { injectable } from '@theia/core/shared/inversify';
+@injectable()
+export class MessagingContribution extends TheiaMessagingContribution {
+ // https://github.com/eclipse-theia/theia/discussions/11543
+ protected override checkAliveTimeout = process.argv.includes(
+ '--no-ping-timeout'
+ )
+ ? 24 * 60 * 60 * 1_000 // one day
+ : 30_000;
+}
diff --git a/arduino-ide-extension/src/node/theia/plugin-ext/plugin-reader.ts b/arduino-ide-extension/src/node/theia/plugin-ext/plugin-reader.ts
new file mode 100644
index 000000000..d68f34c9b
--- /dev/null
+++ b/arduino-ide-extension/src/node/theia/plugin-ext/plugin-reader.ts
@@ -0,0 +1,80 @@
+import { injectable } from '@theia/core/shared/inversify';
+import type {
+ PluginContribution,
+ PluginPackage,
+} from '@theia/plugin-ext/lib/common/plugin-protocol';
+import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader';
+
+@injectable()
+export class HostedPluginReader extends TheiaHostedPluginReader {
+ override readContribution(
+ plugin: PluginPackage
+ ): PluginContribution | undefined {
+ const scanner = this.scanner.getScanner(plugin);
+ const contributions = scanner.getContribution(plugin);
+ return this.filterContribution(plugin.name, contributions);
+ }
+ private filterContribution(
+ pluginName: string,
+ contributions: PluginContribution | undefined
+ ): PluginContribution | undefined {
+ if (!contributions) {
+ return contributions;
+ }
+ const filter = pluginFilters.get(pluginName);
+ return filter ? filter(contributions) : contributions;
+ }
+}
+
+type PluginContributionFilter = (
+ contribution: PluginContribution
+) => PluginContribution | undefined;
+const cortexDebugFilter: PluginContributionFilter = (
+ contribution: PluginContribution
+) => {
+ if (contribution.viewsContainers) {
+ for (const location of Object.keys(contribution.viewsContainers)) {
+ const viewContainers = contribution.viewsContainers[location];
+ for (let i = 0; i < viewContainers.length; i++) {
+ const viewContainer = viewContainers[i];
+ if (
+ viewContainer.id === 'cortex-debug' &&
+ viewContainer.title === 'RTOS'
+ ) {
+ viewContainers.splice(i, 1);
+ }
+ }
+ }
+ }
+ if (contribution.views) {
+ for (const location of Object.keys(contribution.views)) {
+ if (location === 'cortex-debug') {
+ const views = contribution.views[location];
+ for (let i = 0; i < views.length; i++) {
+ const view = views[i];
+ if (view.id === 'cortex-debug.rtos') {
+ views.splice(i, 1);
+ }
+ }
+ }
+ }
+ }
+ if (contribution.menus) {
+ for (const location of Object.keys(contribution.menus)) {
+ if (location === 'commandPalette') {
+ const menus = contribution.menus[location];
+ for (let i = 0; i < menus.length; i++) {
+ const menu = menus[i];
+ if (menu.command === 'cortex-debug.rtos.toggleRTOSPanel') {
+ menu.when = 'false';
+ }
+ }
+ }
+ }
+ }
+ return contribution;
+};
+
+const pluginFilters = new Map([
+ ['cortex-debug', cortexDebugFilter],
+]);
diff --git a/package.json b/package.json
index d9adf9b60..e0e268391 100644
--- a/package.json
+++ b/package.json
@@ -79,7 +79,7 @@
"vscode-arduino-tools": "https://downloads.arduino.cc/vscode-arduino-tools/vscode-arduino-tools-0.0.2-beta.5.vsix",
"vscode-builtin-json": "https://open-vsx.org/api/vscode/json/1.46.1/file/vscode.json-1.46.1.vsix",
"vscode-builtin-json-language-features": "https://open-vsx.org/api/vscode/json-language-features/1.46.1/file/vscode.json-language-features-1.46.1.vsix",
- "cortex-debug": "https://open-vsx.org/api/marus25/cortex-debug/0.3.10/file/marus25.cortex-debug-0.3.10.vsix",
+ "cortex-debug": "https://downloads.arduino.cc/marus25.cortex-debug/marus25.cortex-debug-1.5.1.vsix",
"vscode-language-pack-bg": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-bg/1.48.3/file/MS-CEINTL.vscode-language-pack-bg-1.48.3.vsix",
"vscode-language-pack-cs": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-cs/1.53.2/file/MS-CEINTL.vscode-language-pack-cs-1.53.2.vsix",
"vscode-language-pack-de": "https://open-vsx.org/api/MS-CEINTL/vscode-language-pack-de/1.53.2/file/MS-CEINTL.vscode-language-pack-de-1.53.2.vsix",