Skip to content

Commit 5c7d618

Browse files
author
Akos Kitta
committed
fix: various debug fixes and VS Code compatibility enhancements
- feat: aded support for `debug/toolbar` and `debug/variables/context`, - feat: added support for `debugState` when context (eclipse-theia#11871), - feat: can customize debug session timeout, and error handling (eclipse-theia#11879), - fix: the `debugType` context that is not updated, - fix: `configure` must happen after receiving capabilities (eclipse-theia#11886), - fix: added missing conext menu in the _Variables_ view, - fix: handle `setFunctionBreakboints` response with no `body` (eclipse-theia#11885), - fix: `DebugExt` fires `didStart` event on `didCreate` (eclipse-theia#11916) Closes eclipse-theia#11871 Closes eclipse-theia#11879 Closes eclipse-theia#11885 Closes eclipse-theia#11886 Closes eclipse-theia#11916 Signed-off-by: Akos Kitta <[email protected]> s Signed-off-by: Akos Kitta <[email protected]>
1 parent 1446bca commit 5c7d618

14 files changed

+149
-48
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
- [core] returns of many methods of `MenuModelRegistry` changed from `CompositeMenuNode` to `MutableCompoundMenuNode`. To mutate a menu, use the `updateOptions` method or add a check for `instanceof CompositeMenuNode`, which will be true in most cases.
1414
- [plugin-ext] refactored the plugin RPC API - now also reuses the msgpackR based RPC protocol that is better suited for handling binary data and enables message tunneling [#11228](https://github.com/eclipse-theia/theia/pull/11261). All plugin protocol types now use `UInt8Array` as type for message parameters instead of `string` - Contributed on behalf of STMicroelectronics.
15+
- [plugin-ext] `DebugExtImpl#sessionDidCreate` has been replaced with `DebugExtImpl#sessionDidStart` to avoid prematurely firing a `didStart` event on `didCreate` [#11916](https://github.com/eclipse-theia/theia/issues/11916)
1516

1617
## v1.32.0 - 11/24/2022
1718

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@
113113
"vscode.typescript-language-features": "https://open-vsx.org/api/vscode/typescript-language-features/1.62.3/file/vscode.typescript-language-features-1.62.3.vsix",
114114
"EditorConfig.EditorConfig": "https://open-vsx.org/api/EditorConfig/EditorConfig/0.14.4/file/EditorConfig.EditorConfig-0.14.4.vsix",
115115
"dbaeumer.vscode-eslint": "https://open-vsx.org/api/dbaeumer/vscode-eslint/2.1.1/file/dbaeumer.vscode-eslint-2.1.1.vsix",
116-
"ms-vscode.references-view": "https://open-vsx.org/api/ms-vscode/references-view/0.0.89/file/ms-vscode.references-view-0.0.89.vsix"
116+
"ms-vscode.references-view": "https://open-vsx.org/api/ms-vscode/references-view/0.0.89/file/ms-vscode.references-view-0.0.89.vsix",
117+
"vscode.mock-debug": "https://github.com/kittaakos/vscode-mock-debug/raw/theia/mock-debug-0.51.0.vsix"
117118
},
118119
"theiaPluginsExcludeIds": [
119120
"ms-vscode.js-debug-companion",

packages/debug/src/browser/debug-session-connection.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export class DebugSessionConnection implements Disposable {
215215
};
216216

217217
this.pendingRequests.set(request.seq, result);
218-
if (timeout) {
218+
if (typeof timeout === 'number') {
219219
const handle = setTimeout(() => {
220220
const pendingRequest = this.pendingRequests.get(request.seq);
221221
if (pendingRequest) {

packages/debug/src/browser/debug-session-manager.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { DebugConfiguration } from '../common/debug-common';
2727
import { DebugError, DebugService } from '../common/debug-service';
2828
import { BreakpointManager } from './breakpoint/breakpoint-manager';
2929
import { DebugConfigurationManager } from './debug-configuration-manager';
30-
import { DebugSession, DebugState } from './debug-session';
30+
import { DebugSession, DebugState, debugStateLabel } from './debug-session';
3131
import { DebugSessionContributionRegistry, DebugSessionFactory } from './debug-session-contribution';
3232
import { DebugCompoundRoot, DebugCompoundSessionOptions, DebugConfigurationSessionOptions, DebugSessionOptions, InternalDebugSessionOptions } from './debug-session-options';
3333
import { DebugStackFrame } from './model/debug-stack-frame';
@@ -106,7 +106,9 @@ export class DebugSessionManager {
106106
protected readonly onDidChangeEmitter = new Emitter<DebugSession | undefined>();
107107
readonly onDidChange: Event<DebugSession | undefined> = this.onDidChangeEmitter.event;
108108
protected fireDidChange(current: DebugSession | undefined): void {
109+
this.debugTypeKey.set(current?.configuration.type);
109110
this.inDebugModeKey.set(this.inDebugMode);
111+
this.debugStateKey.set(debugStateLabel(this.state));
110112
this.onDidChangeEmitter.fire(current);
111113
}
112114

@@ -154,11 +156,13 @@ export class DebugSessionManager {
154156

155157
protected debugTypeKey: ContextKey<string>;
156158
protected inDebugModeKey: ContextKey<boolean>;
159+
protected debugStateKey: ContextKey<string>;
157160

158161
@postConstruct()
159162
protected init(): void {
160163
this.debugTypeKey = this.contextKeyService.createKey<string>('debugType', undefined);
161164
this.inDebugModeKey = this.contextKeyService.createKey<boolean>('inDebugMode', this.inDebugMode);
165+
this.debugStateKey = this.contextKeyService.createKey<string>('debugState', debugStateLabel(this.state));
162166
this.breakpoints.onDidChangeMarkers(uri => this.fireDidChangeBreakpoints({ uri }));
163167
this.labelProvider.onDidChange(event => {
164168
for (const uriString of this.breakpoints.getUris()) {

packages/debug/src/browser/debug-session.tsx

+77-26
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,22 @@ import { DebugContribution } from './debug-contribution';
4343
import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util';
4444
import { WorkspaceService } from '@theia/workspace/lib/browser';
4545
import { DebugInstructionBreakpoint } from './model/debug-instruction-breakpoint';
46+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
4647

4748
export enum DebugState {
4849
Inactive,
4950
Initializing,
5051
Running,
5152
Stopped
5253
}
54+
export function debugStateLabel(state: DebugState): string {
55+
switch (state) {
56+
case DebugState.Initializing: return 'initializing';
57+
case DebugState.Stopped: return 'stopped';
58+
case DebugState.Running: return 'running';
59+
default: return 'inactive';
60+
}
61+
}
5362

5463
// FIXME: make injectable to allow easily inject services
5564
export class DebugSession implements CompositeTreeElement {
@@ -74,7 +83,11 @@ export class DebugSession implements CompositeTreeElement {
7483
protected readonly childSessions = new Map<string, DebugSession>();
7584
protected readonly toDispose = new DisposableCollection();
7685

77-
private isStopping: boolean = false;
86+
protected isStopping: boolean = false;
87+
/**
88+
* Number of millis after a `stop` request times out.
89+
*/
90+
protected stopTimeout = 5_000;
7891

7992
constructor(
8093
readonly id: string,
@@ -274,19 +287,26 @@ export class DebugSession implements CompositeTreeElement {
274287
}
275288

276289
protected async initialize(): Promise<void> {
277-
const response = await this.connection.sendRequest('initialize', {
278-
clientID: 'Theia',
279-
clientName: 'Theia IDE',
280-
adapterID: this.configuration.type,
281-
locale: 'en-US',
282-
linesStartAt1: true,
283-
columnsStartAt1: true,
284-
pathFormat: 'path',
285-
supportsVariableType: false,
286-
supportsVariablePaging: false,
287-
supportsRunInTerminalRequest: true
288-
});
289-
this.updateCapabilities(response?.body || {});
290+
const clientName = FrontendApplicationConfigProvider.get().applicationName;
291+
try {
292+
const response = await this.connection.sendRequest('initialize', {
293+
clientID: clientName.toLocaleLowerCase().replace(/\s+/g, '_'),
294+
clientName,
295+
adapterID: this.configuration.type,
296+
locale: 'en-US',
297+
linesStartAt1: true,
298+
columnsStartAt1: true,
299+
pathFormat: 'path',
300+
supportsVariableType: false,
301+
supportsVariablePaging: false,
302+
supportsRunInTerminalRequest: true
303+
});
304+
this.updateCapabilities(response?.body || {});
305+
this.didReceiveCapabilities.resolve();
306+
} catch (err) {
307+
this.didReceiveCapabilities.reject(err);
308+
throw err;
309+
}
290310
}
291311

292312
protected async launchOrAttach(): Promise<void> {
@@ -304,8 +324,17 @@ export class DebugSession implements CompositeTreeElement {
304324
}
305325
}
306326

327+
/**
328+
* The `send('initialize')` request could resolve later than `on('initialized')` emits the event.
329+
* Hence, the `configure` would use the empty object `capabilities`.
330+
* Using the empty `capabilities` could result in missing exception breakpoint filters, as
331+
* always `capabilities.exceptionBreakpointFilters` is falsy. This deferred promise works
332+
* around this timing issue. https://github.com/eclipse-theia/theia/issues/11886
333+
*/
334+
protected didReceiveCapabilities = new Deferred<void>();
307335
protected initialized = false;
308336
protected async configure(): Promise<void> {
337+
await this.didReceiveCapabilities.promise;
309338
if (this.capabilities.exceptionBreakpointFilters) {
310339
const exceptionBreakpoints = [];
311340
for (const filter of this.capabilities.exceptionBreakpointFilters) {
@@ -340,24 +369,39 @@ export class DebugSession implements CompositeTreeElement {
340369
if (!this.isStopping) {
341370
this.isStopping = true;
342371
if (this.canTerminate()) {
343-
const terminated = this.waitFor('terminated', 5000);
372+
const terminated = this.waitFor('terminated', this.stopTimeout);
344373
try {
345-
await this.connection.sendRequest('terminate', { restart: isRestart }, 5000);
374+
await this.connection.sendRequest('terminate', { restart: isRestart }, this.stopTimeout);
346375
await terminated;
347376
} catch (e) {
348-
console.error('Did not receive terminated event in time', e);
377+
this.handleTerminateError(e);
349378
}
350379
} else {
380+
const terminateDebuggee = this.initialized && this.capabilities.supportTerminateDebuggee;
351381
try {
352-
await this.sendRequest('disconnect', { restart: isRestart }, 5000);
382+
await this.sendRequest('disconnect', { restart: isRestart, terminateDebuggee }, this.stopTimeout);
353383
} catch (e) {
354-
console.error('Error on disconnect', e);
384+
this.handleDisconnectError(e);
355385
}
356386
}
357387
callback();
358388
}
359389
}
360390

391+
/**
392+
* Invoked when sending the `terminate` request to the debugger is rejected or timed out.
393+
*/
394+
protected handleTerminateError(err: unknown): void {
395+
console.error('Did not receive terminated event in time', err);
396+
}
397+
398+
/**
399+
* Invoked when sending the `disconnect` request to the debugger is rejected or timed out.
400+
*/
401+
protected handleDisconnectError(err: unknown): void {
402+
console.error('Error on disconnect', err);
403+
}
404+
361405
async disconnect(isRestart: boolean, callback: () => void): Promise<void> {
362406
if (!this.isStopping) {
363407
this.isStopping = true;
@@ -665,12 +709,17 @@ export class DebugSession implements CompositeTreeElement {
665709
const response = await this.sendRequest('setFunctionBreakpoints', {
666710
breakpoints: enabled.map(b => b.origin.raw)
667711
});
668-
response.body.breakpoints.forEach((raw, index) => {
669-
// node debug adapter returns more breakpoints sometimes
670-
if (enabled[index]) {
671-
enabled[index].update({ raw });
672-
}
673-
});
712+
// Apparently, `body` and `breakpoints` can be missing.
713+
// https://github.com/eclipse-theia/theia/issues/11885
714+
// https://github.com/microsoft/vscode/blob/80004351ccf0884b58359f7c8c801c91bb827d83/src/vs/workbench/contrib/debug/browser/debugSession.ts#L448-L449
715+
if (response && response.body) {
716+
response.body.breakpoints.forEach((raw, index) => {
717+
// node debug adapter returns more breakpoints sometimes
718+
if (enabled[index]) {
719+
enabled[index].update({ raw });
720+
}
721+
});
722+
}
674723
} catch (error) {
675724
// could be error or promise rejection of DebugProtocol.SetFunctionBreakpoints
676725
if (error instanceof Error) {
@@ -699,10 +748,12 @@ export class DebugSession implements CompositeTreeElement {
699748
);
700749
const enabled = all.filter(b => b.enabled);
701750
try {
751+
const breakpoints = enabled.map(({ origin }) => origin.raw);
702752
const response = await this.sendRequest('setBreakpoints', {
703753
source: source.raw,
704754
sourceModified,
705-
breakpoints: enabled.map(({ origin }) => origin.raw)
755+
breakpoints,
756+
lines: breakpoints.map(({ line }) => line)
706757
});
707758
response.body.breakpoints.forEach((raw, index) => {
708759
// node debug adapter returns more breakpoints sometimes

packages/debug/src/browser/model/debug-stack-frame.tsx

+5-11
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@
2222

2323
import * as React from '@theia/core/shared/react';
2424
import { WidgetOpenerOptions, DISABLED_CLASS } from '@theia/core/lib/browser';
25-
import { EditorWidget, Range, Position } from '@theia/editor/lib/browser';
25+
import { EditorWidget, Range } from '@theia/editor/lib/browser';
2626
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
2727
import { TreeElement } from '@theia/core/lib/browser/source-tree';
2828
import { DebugScope } from '../console/debug-console-items';
2929
import { DebugSource } from './debug-source';
30-
import { RecursivePartial } from '@theia/core';
3130
import { DebugSession } from '../debug-session';
3231
import { DebugThread } from './debug-thread';
3332
import * as monaco from '@theia/monaco-editor-core';
@@ -70,16 +69,11 @@ export class DebugStackFrame extends DebugStackFrameData implements TreeElement
7069
if (!this.source) {
7170
return undefined;
7271
}
73-
const { line, column, endLine, endColumn } = this.raw;
74-
const selection: RecursivePartial<Range> = {
75-
start: Position.create(line - 1, column - 1)
76-
};
77-
if (typeof endLine === 'number') {
78-
selection.end = {
79-
line: endLine - 1,
80-
character: typeof endColumn === 'number' ? endColumn - 1 : undefined
81-
};
72+
const { line, column, endLine, endColumn, source } = this.raw;
73+
if (!source) {
74+
return undefined;
8275
}
76+
const selection = Range.create(line, column, endLine || line, endColumn || column);
8377
this.source.open({
8478
...options,
8579
selection

packages/debug/src/browser/style/index.css

+11
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,17 @@
146146
opacity: 1;
147147
}
148148

149+
.debug-toolbar .debug-action>div {
150+
font-family: var(--theia-ui-font-family);
151+
font-size: var(--theia-ui-font-size0);
152+
display: flex;
153+
align-items: center;
154+
align-self: center;
155+
justify-content: center;
156+
text-align: center;
157+
min-height: inherit;
158+
}
159+
149160
/** Console */
150161

151162
#debug-console .theia-console-info {

packages/debug/src/browser/view/debug-action.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,20 @@ export class DebugAction extends React.Component<DebugAction.Props> {
2121

2222
override render(): React.ReactNode {
2323
const { enabled, label, iconClass } = this.props;
24-
const classNames = ['debug-action', ...codiconArray(iconClass, true)];
24+
const classNames = ['debug-action'];
25+
if (iconClass) {
26+
classNames.push(...codiconArray(iconClass, true));
27+
}
2528
if (enabled === false) {
2629
classNames.push(DISABLED_CLASS);
2730
}
2831
return <span tabIndex={0}
2932
className={classNames.join(' ')}
3033
title={label}
3134
onClick={this.props.run}
32-
ref={this.setRef} />;
35+
ref={this.setRef} >
36+
{!iconClass && <div>{label}</div>}
37+
</span>;
3338
}
3439

3540
focus(): void {

packages/debug/src/browser/view/debug-toolbar-widget.tsx

+30-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
import * as React from '@theia/core/shared/react';
1818
import { inject, postConstruct, injectable } from '@theia/core/shared/inversify';
19-
import { Disposable, DisposableCollection, MenuPath } from '@theia/core';
19+
import { ActionMenuNode, CommandRegistry, CompositeMenuNode, Disposable, DisposableCollection, MenuModelRegistry, MenuPath } from '@theia/core';
20+
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
2021
import { ReactWidget } from '@theia/core/lib/browser/widgets';
2122
import { DebugViewModel } from './debug-view-model';
2223
import { DebugState } from '../debug-session';
@@ -28,8 +29,10 @@ export class DebugToolBar extends ReactWidget {
2829

2930
static readonly MENU: MenuPath = ['debug-toolbar-menu'];
3031

31-
@inject(DebugViewModel)
32-
protected readonly model: DebugViewModel;
32+
@inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry;
33+
@inject(MenuModelRegistry) protected readonly menuModelRegistry: MenuModelRegistry;
34+
@inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService;
35+
@inject(DebugViewModel) protected readonly model: DebugViewModel;
3336

3437
protected readonly onRender = new DisposableCollection();
3538

@@ -65,6 +68,7 @@ export class DebugToolBar extends ReactWidget {
6568
protected render(): React.ReactNode {
6669
const { state } = this.model;
6770
return <React.Fragment>
71+
{this.renderContributedCommands()}
6872
{this.renderContinue()}
6973
<DebugAction enabled={state === DebugState.Stopped} run={this.stepOver} label={nls.localizeByDefault('Step Over')}
7074
iconClass='debug-step-over' ref={this.setStepRef} />
@@ -77,6 +81,29 @@ export class DebugToolBar extends ReactWidget {
7781
{this.renderStart()}
7882
</React.Fragment>;
7983
}
84+
85+
protected renderContributedCommands(): React.ReactNode {
86+
return this.menuModelRegistry
87+
.getMenu(DebugToolBar.MENU)
88+
.children.filter(node => node instanceof CompositeMenuNode)
89+
.map(node => (node as CompositeMenuNode).children)
90+
.reduce((acc, curr) => acc.concat(curr), [])
91+
.filter(node => node instanceof ActionMenuNode)
92+
.map(node => this.debugAction(node as ActionMenuNode));
93+
}
94+
95+
protected debugAction(node: ActionMenuNode): React.ReactNode {
96+
const { label, command, when, icon: iconClass = '' } = node;
97+
const run = () => this.commandRegistry.executeCommand(command);
98+
const enabled = when ? this.contextKeyService.match(when) : true;
99+
return enabled && <DebugAction
100+
key={command}
101+
enabled={enabled}
102+
label={label}
103+
iconClass={iconClass}
104+
run={run} />;
105+
}
106+
80107
protected renderStart(): React.ReactNode {
81108
const { state } = this.model;
82109
if (state === DebugState.Inactive && this.model.sessionCount === 1) {

packages/plugin-ext/src/common/plugin-api-rpc.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1799,7 +1799,7 @@ export interface DebugConfigurationProviderDescriptor {
17991799
export interface DebugExt {
18001800
$onSessionCustomEvent(sessionId: string, event: string, body?: any): void;
18011801
$breakpointsDidChange(added: Breakpoint[], removed: string[], changed: Breakpoint[]): void;
1802-
$sessionDidCreate(sessionId: string): void;
1802+
$sessionDidStart(sessionId: string): void;
18031803
$sessionDidDestroy(sessionId: string): void;
18041804
$sessionDidChange(sessionId: string | undefined): void;
18051805
$provideDebugConfigurationsByHandle(handle: number, workspaceFolder: string | undefined): Promise<theia.DebugConfiguration[]>;

packages/plugin-ext/src/main/browser/debug/debug-main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export class DebugMainImpl implements DebugMain, Disposable {
113113
this.toDispose.pushAll([
114114
this.breakpointsManager.onDidChangeBreakpoints(fireDidChangeBreakpoints),
115115
this.breakpointsManager.onDidChangeFunctionBreakpoints(fireDidChangeBreakpoints),
116-
this.sessionManager.onDidCreateDebugSession(debugSession => this.debugExt.$sessionDidCreate(debugSession.id)),
116+
this.sessionManager.onDidStartDebugSession(debugSession => this.debugExt.$sessionDidStart(debugSession.id)),
117117
this.sessionManager.onDidDestroyDebugSession(debugSession => this.debugExt.$sessionDidDestroy(debugSession.id)),
118118
this.sessionManager.onDidChangeActiveDebugSession(event => this.debugExt.$sessionDidChange(event.current && event.current.id)),
119119
this.sessionManager.onDidReceiveDebugSessionCustomEvent(event => this.debugExt.$onSessionCustomEvent(event.session.id, event.event, event.body))

packages/plugin-ext/src/main/browser/menus/plugin-menu-command-adapter.ts

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export class PluginMenuCommandAdapter implements MenuCommandAdapter {
8989
['comments/comment/title', toCommentArgs],
9090
['comments/commentThread/context', toCommentArgs],
9191
['debug/callstack/context', firstArgOnly],
92+
['debug/variables/context', firstArgOnly],
9293
['debug/toolBar', noArgs],
9394
['editor/context', selectedResource],
9495
['editor/title', widgetURI],

0 commit comments

Comments
 (0)