@theia/debug 1.77.0-next.4 → 1.77.0-next.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/console/debug-console-contribution.d.ts +23 -1
- package/lib/browser/console/debug-console-contribution.d.ts.map +1 -1
- package/lib/browser/console/debug-console-contribution.js +47 -14
- package/lib/browser/console/debug-console-contribution.js.map +1 -1
- package/lib/browser/console/debug-console-contribution.spec.d.ts +2 -0
- package/lib/browser/console/debug-console-contribution.spec.d.ts.map +1 -0
- package/lib/browser/console/debug-console-contribution.spec.js +142 -0
- package/lib/browser/console/debug-console-contribution.spec.js.map +1 -0
- package/lib/browser/console/debug-console-items.d.ts.map +1 -1
- package/lib/browser/console/debug-console-items.js +6 -1
- package/lib/browser/console/debug-console-items.js.map +1 -1
- package/lib/browser/console/debug-console-session.d.ts +48 -3
- package/lib/browser/console/debug-console-session.d.ts.map +1 -1
- package/lib/browser/console/debug-console-session.js +90 -17
- package/lib/browser/console/debug-console-session.js.map +1 -1
- package/lib/browser/console/debug-console-session.spec.d.ts +2 -0
- package/lib/browser/console/debug-console-session.spec.d.ts.map +1 -0
- package/lib/browser/console/debug-console-session.spec.js +187 -0
- package/lib/browser/console/debug-console-session.spec.js.map +1 -0
- package/lib/browser/debug-session-connection.d.ts +6 -0
- package/lib/browser/debug-session-connection.d.ts.map +1 -1
- package/lib/browser/debug-session-connection.js +8 -0
- package/lib/browser/debug-session-connection.js.map +1 -1
- package/lib/browser/debug-session-manager.d.ts.map +1 -1
- package/lib/browser/debug-session-manager.js +5 -2
- package/lib/browser/debug-session-manager.js.map +1 -1
- package/lib/browser/debug-session.d.ts.map +1 -1
- package/lib/browser/debug-session.js +6 -0
- package/lib/browser/debug-session.js.map +1 -1
- package/lib/browser/view/debug-variables-widget.d.ts.map +1 -1
- package/lib/browser/view/debug-variables-widget.js +6 -0
- package/lib/browser/view/debug-variables-widget.js.map +1 -1
- package/lib/common/debug-preferences.d.ts +1 -0
- package/lib/common/debug-preferences.d.ts.map +1 -1
- package/lib/common/debug-preferences.js +6 -0
- package/lib/common/debug-preferences.js.map +1 -1
- package/package.json +15 -15
- package/src/browser/console/debug-console-contribution.spec.ts +183 -0
- package/src/browser/console/debug-console-contribution.tsx +53 -14
- package/src/browser/console/debug-console-items.tsx +6 -1
- package/src/browser/console/debug-console-session.spec.ts +246 -0
- package/src/browser/console/debug-console-session.ts +102 -19
- package/src/browser/debug-session-connection.ts +9 -0
- package/src/browser/debug-session-manager.ts +5 -2
- package/src/browser/debug-session.tsx +6 -0
- package/src/browser/view/debug-variables-widget.ts +6 -0
- package/src/common/debug-preferences.ts +7 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
|
|
19
|
+
let disableJSDOM = enableJSDOM();
|
|
20
|
+
|
|
21
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
22
|
+
FrontendApplicationConfigProvider.set({});
|
|
23
|
+
|
|
24
|
+
import 'reflect-metadata';
|
|
25
|
+
|
|
26
|
+
import { expect } from 'chai';
|
|
27
|
+
import { Emitter } from '@theia/core/lib/common/event';
|
|
28
|
+
import { ConsoleSessionManager } from '@theia/console/lib/browser/console-session-manager';
|
|
29
|
+
import { DebugConsoleContribution } from './debug-console-contribution';
|
|
30
|
+
import { DebugConsoleSession } from './debug-console-session';
|
|
31
|
+
import { DebugSession } from '../debug-session';
|
|
32
|
+
import { DidChangeActiveDebugSession } from '../debug-session-manager';
|
|
33
|
+
|
|
34
|
+
disableJSDOM();
|
|
35
|
+
|
|
36
|
+
class TestDebugSessionManager {
|
|
37
|
+
|
|
38
|
+
readonly onDidCreateDebugSessionEmitter = new Emitter<DebugSession>();
|
|
39
|
+
readonly onDidCreateDebugSession = this.onDidCreateDebugSessionEmitter.event;
|
|
40
|
+
readonly onDidDestroyDebugSessionEmitter = new Emitter<DebugSession>();
|
|
41
|
+
readonly onDidDestroyDebugSession = this.onDidDestroyDebugSessionEmitter.event;
|
|
42
|
+
readonly onDidChangeActiveDebugSession = new Emitter<DidChangeActiveDebugSession>().event;
|
|
43
|
+
|
|
44
|
+
protected readonly running = new Map<string, DebugSession>();
|
|
45
|
+
|
|
46
|
+
currentSession: DebugSession | undefined;
|
|
47
|
+
|
|
48
|
+
get sessions(): DebugSession[] {
|
|
49
|
+
return Array.from(this.running.values());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getSession(id: string): DebugSession | undefined {
|
|
53
|
+
return this.running.get(id);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
start(session: DebugSession): void {
|
|
57
|
+
this.running.set(session.id, session);
|
|
58
|
+
this.currentSession = session;
|
|
59
|
+
this.onDidCreateDebugSessionEmitter.fire(session);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Mirrors `DebugSessionManager.cleanup`, which drops the session before announcing its destruction. */
|
|
63
|
+
destroy(session: DebugSession): void {
|
|
64
|
+
this.running.delete(session.id);
|
|
65
|
+
if (this.currentSession?.id === session.id) {
|
|
66
|
+
this.currentSession = this.sessions[0];
|
|
67
|
+
}
|
|
68
|
+
this.onDidDestroyDebugSessionEmitter.fire(session);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('DebugConsoleContribution', () => {
|
|
74
|
+
|
|
75
|
+
before(() => disableJSDOM = enableJSDOM());
|
|
76
|
+
after(() => disableJSDOM());
|
|
77
|
+
|
|
78
|
+
let debugSessionManager: TestDebugSessionManager;
|
|
79
|
+
let consoleSessionManager: ConsoleSessionManager;
|
|
80
|
+
let contribution: DebugConsoleContribution;
|
|
81
|
+
|
|
82
|
+
/** A session whose console is merged into that of `consoleParent`, if given. */
|
|
83
|
+
const createDebugSession = (id: string, consoleParent?: DebugSession): DebugSession => ({
|
|
84
|
+
id,
|
|
85
|
+
label: id,
|
|
86
|
+
configuration: { name: id, type: 'node', request: 'launch' },
|
|
87
|
+
options: {},
|
|
88
|
+
parentSession: consoleParent,
|
|
89
|
+
findConsoleParent: () => consoleParent,
|
|
90
|
+
on: () => { }
|
|
91
|
+
} as unknown as DebugSession);
|
|
92
|
+
|
|
93
|
+
const consoleOf = (session: DebugSession): DebugConsoleSession =>
|
|
94
|
+
consoleSessionManager.all.find((candidate): candidate is DebugConsoleSession =>
|
|
95
|
+
candidate instanceof DebugConsoleSession && candidate.debugSession?.id === session.id)!;
|
|
96
|
+
|
|
97
|
+
/** The session that console input is evaluated in. */
|
|
98
|
+
const evaluationSessionOf = (consoleSession: DebugConsoleSession): DebugSession | undefined =>
|
|
99
|
+
(consoleSession as unknown as { findCurrentSession(): DebugSession | undefined }).findCurrentSession();
|
|
100
|
+
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
debugSessionManager = new TestDebugSessionManager();
|
|
103
|
+
consoleSessionManager = new ConsoleSessionManager();
|
|
104
|
+
contribution = new DebugConsoleContribution();
|
|
105
|
+
// The class uses property injection; `init()` only subscribes to the managers.
|
|
106
|
+
Object.assign(contribution as unknown as Record<string, unknown>, {
|
|
107
|
+
consoleSessionManager,
|
|
108
|
+
debugSessionManager,
|
|
109
|
+
resources: { add: () => { } },
|
|
110
|
+
debugConsoleSessionFactory: (session: DebugSession) => {
|
|
111
|
+
const consoleSession = new DebugConsoleSession();
|
|
112
|
+
Object.assign(consoleSession as unknown as Record<string, unknown>, { sessionManager: debugSessionManager });
|
|
113
|
+
consoleSession.startFor(session);
|
|
114
|
+
return consoleSession;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
(contribution as unknown as { init(): void }).init();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(() => contribution.dispose());
|
|
121
|
+
|
|
122
|
+
it('terminates the console when its session ends', () => {
|
|
123
|
+
const session = createDebugSession('parent');
|
|
124
|
+
debugSessionManager.start(session);
|
|
125
|
+
const consoleSession = consoleOf(session);
|
|
126
|
+
|
|
127
|
+
debugSessionManager.destroy(session);
|
|
128
|
+
|
|
129
|
+
expect(consoleSession.terminated).to.be.true;
|
|
130
|
+
expect(consoleSession.debugSession).to.be.undefined;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('keeps the console of a session running while a child merged into it outlives that session', () => {
|
|
134
|
+
const parent = createDebugSession('parent');
|
|
135
|
+
debugSessionManager.start(parent);
|
|
136
|
+
const consoleSession = consoleOf(parent);
|
|
137
|
+
const child = createDebugSession('child', parent);
|
|
138
|
+
debugSessionManager.start(child);
|
|
139
|
+
|
|
140
|
+
debugSessionManager.destroy(parent);
|
|
141
|
+
|
|
142
|
+
expect(consoleSession.terminated).to.be.false;
|
|
143
|
+
expect(evaluationSessionOf(consoleSession)).to.equal(child);
|
|
144
|
+
|
|
145
|
+
debugSessionManager.destroy(child);
|
|
146
|
+
|
|
147
|
+
expect(consoleSession.terminated).to.be.true;
|
|
148
|
+
expect(consoleSession.debugSession).to.be.undefined;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('keeps the console of a session running when a child merged into it ends first', () => {
|
|
152
|
+
const parent = createDebugSession('parent');
|
|
153
|
+
debugSessionManager.start(parent);
|
|
154
|
+
const consoleSession = consoleOf(parent);
|
|
155
|
+
const child = createDebugSession('child', parent);
|
|
156
|
+
debugSessionManager.start(child);
|
|
157
|
+
|
|
158
|
+
debugSessionManager.destroy(child);
|
|
159
|
+
|
|
160
|
+
expect(consoleSession.terminated).to.be.false;
|
|
161
|
+
expect(consoleSession.debugSession).to.equal(parent);
|
|
162
|
+
|
|
163
|
+
debugSessionManager.destroy(parent);
|
|
164
|
+
|
|
165
|
+
expect(consoleSession.terminated).to.be.true;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('does not let a restarted configuration take over a console still used by a child', () => {
|
|
169
|
+
const parent = createDebugSession('parent');
|
|
170
|
+
debugSessionManager.start(parent);
|
|
171
|
+
const consoleSession = consoleOf(parent);
|
|
172
|
+
debugSessionManager.start(createDebugSession('child', parent));
|
|
173
|
+
debugSessionManager.destroy(parent);
|
|
174
|
+
|
|
175
|
+
const restarted = createDebugSession('parent-restarted');
|
|
176
|
+
restarted.configuration.name = parent.configuration.name;
|
|
177
|
+
debugSessionManager.start(restarted);
|
|
178
|
+
|
|
179
|
+
expect(consoleSession.debugSession).to.equal(parent);
|
|
180
|
+
expect(consoleOf(restarted)).to.not.equal(consoleSession);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
});
|
|
@@ -92,23 +92,18 @@ export class DebugConsoleContribution extends AbstractViewContribution<ConsoleWi
|
|
|
92
92
|
this.debugSessionManager.onDidCreateDebugSession(session => {
|
|
93
93
|
const consoleParent = session.findConsoleParent();
|
|
94
94
|
if (consoleParent) {
|
|
95
|
-
const parentConsoleSession = this.
|
|
96
|
-
if (parentConsoleSession
|
|
97
|
-
session
|
|
95
|
+
const parentConsoleSession = this.findConsoleSession(consoleParent);
|
|
96
|
+
if (parentConsoleSession) {
|
|
97
|
+
// Log against the session the output came from, so that variable references resolve on its adapter.
|
|
98
|
+
session.on('output', event => parentConsoleSession.logOutput(session, event));
|
|
98
99
|
}
|
|
99
100
|
} else {
|
|
100
|
-
const consoleSession = this.
|
|
101
|
-
this.consoleSessionManager.add(consoleSession);
|
|
101
|
+
const consoleSession = this.getOrCreateConsoleSession(session);
|
|
102
102
|
session.on('output', event => consoleSession.logOutput(session, event));
|
|
103
103
|
}
|
|
104
104
|
}),
|
|
105
105
|
this.debugSessionManager.onDidChangeActiveDebugSession(event => this.handleActiveDebugSessionChanged(event)),
|
|
106
|
-
this.debugSessionManager.onDidDestroyDebugSession(session =>
|
|
107
|
-
const consoleSession = this.consoleSessionManager.get(session.id);
|
|
108
|
-
if (consoleSession instanceof DebugConsoleSession) {
|
|
109
|
-
consoleSession.markTerminated();
|
|
110
|
-
}
|
|
111
|
-
}),
|
|
106
|
+
this.debugSessionManager.onDidDestroyDebugSession(session => this.handleDebugSessionDestroyed(session)),
|
|
112
107
|
this.consoleSessionManager.onDidChangeSelectedSession(() => {
|
|
113
108
|
const session = this.consoleSessionManager.selectedSession;
|
|
114
109
|
if (session && this.filterInputRef) {
|
|
@@ -121,12 +116,56 @@ export class DebugConsoleContribution extends AbstractViewContribution<ConsoleWi
|
|
|
121
116
|
]);
|
|
122
117
|
}
|
|
123
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Returns the console of the given session, if it still belongs to one.
|
|
121
|
+
*
|
|
122
|
+
* A console keeps the id it was created with even when it is reused by a restarted session, so it cannot be
|
|
123
|
+
* looked up by the session id.
|
|
124
|
+
*/
|
|
125
|
+
protected findConsoleSession(debugSession: DebugSession): DebugConsoleSession | undefined {
|
|
126
|
+
return this.consoleSessionManager.all.find((candidate): candidate is DebugConsoleSession =>
|
|
127
|
+
candidate instanceof DebugConsoleSession && candidate.debugSession?.id === debugSession.id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Returns the console for a newly created session.
|
|
132
|
+
*
|
|
133
|
+
* A session reuses the console of a terminated session started from the same configuration, so that restarting
|
|
134
|
+
* does not leave a stopped console behind on every run.
|
|
135
|
+
*/
|
|
136
|
+
protected getOrCreateConsoleSession(debugSession: DebugSession): DebugConsoleSession {
|
|
137
|
+
const reusable = this.consoleSessionManager.all.find((candidate): candidate is DebugConsoleSession =>
|
|
138
|
+
candidate instanceof DebugConsoleSession && candidate.isReusableBy(debugSession));
|
|
139
|
+
if (reusable) {
|
|
140
|
+
reusable.startFor(debugSession);
|
|
141
|
+
return reusable;
|
|
142
|
+
}
|
|
143
|
+
const consoleSession = this.debugConsoleSessionFactory(debugSession);
|
|
144
|
+
this.consoleSessionManager.add(consoleSession);
|
|
145
|
+
return consoleSession;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Terminates the console of the given session once no session runs in it any more.
|
|
150
|
+
*
|
|
151
|
+
* A child session merged into its parent's console may outlive the parent if its lifecycle is not managed by the
|
|
152
|
+
* parent, and it still needs the console to evaluate expressions while it runs.
|
|
153
|
+
*/
|
|
154
|
+
protected handleDebugSessionDestroyed(debugSession: DebugSession): void {
|
|
155
|
+
const consoleOwner = debugSession.findConsoleParent() ?? debugSession;
|
|
156
|
+
const consoleSession = this.findConsoleSession(consoleOwner);
|
|
157
|
+
if (consoleSession && !this.debugSessionManager.sessions.some(candidate =>
|
|
158
|
+
candidate.id === consoleOwner.id || candidate.findConsoleParent()?.id === consoleOwner.id)) {
|
|
159
|
+
consoleSession.markTerminated();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
124
163
|
protected handleActiveDebugSessionChanged(event: DidChangeActiveDebugSession): void {
|
|
125
164
|
if (!event.current) {
|
|
126
165
|
return;
|
|
127
166
|
} else {
|
|
128
167
|
const topSession = event.current.findConsoleParent() || event.current;
|
|
129
|
-
const consoleSession = topSession ? this.
|
|
168
|
+
const consoleSession = topSession ? this.findConsoleSession(topSession) : undefined;
|
|
130
169
|
this.consoleSessionManager.selectedSession = consoleSession;
|
|
131
170
|
const consoleSelector = document.getElementById('debugConsoleSelector');
|
|
132
171
|
if (consoleSession && consoleSelector instanceof HTMLSelectElement) {
|
|
@@ -217,7 +256,7 @@ export class DebugConsoleContribution extends AbstractViewContribution<ConsoleWi
|
|
|
217
256
|
bind(DebugConsoleSession).toSelf().inRequestScope();
|
|
218
257
|
bind(DebugConsoleSessionFactory).toFactory(context => (session: DebugSession) => {
|
|
219
258
|
const consoleSession = context.container.get(DebugConsoleSession);
|
|
220
|
-
consoleSession.
|
|
259
|
+
consoleSession.startFor(session);
|
|
221
260
|
return consoleSession;
|
|
222
261
|
});
|
|
223
262
|
bind(ConsoleSessionManager).toSelf().inSingletonScope();
|
|
@@ -271,7 +310,7 @@ export class DebugConsoleContribution extends AbstractViewContribution<ConsoleWi
|
|
|
271
310
|
});
|
|
272
311
|
|
|
273
312
|
sortedSessions.forEach(session => {
|
|
274
|
-
let label = session.
|
|
313
|
+
let label = session.label;
|
|
275
314
|
if (session.terminated) {
|
|
276
315
|
label = `${label} (${nls.localizeByDefault('Stopped')})`;
|
|
277
316
|
}
|
|
@@ -92,12 +92,17 @@ export class ExpressionContainer implements CompositeConsoleItem {
|
|
|
92
92
|
|
|
93
93
|
protected elements: Promise<ExpressionContainer[]> | undefined;
|
|
94
94
|
async getElements(): Promise<IterableIterator<ExpressionContainer>> {
|
|
95
|
-
if (!this.hasElements
|
|
95
|
+
if (!this.hasElements) {
|
|
96
96
|
return [][Symbol.iterator]();
|
|
97
97
|
}
|
|
98
98
|
if (!this.elements) {
|
|
99
|
+
if (!this.session) {
|
|
100
|
+
// Nothing was resolved while the session ran, and there is no session left to resolve it with.
|
|
101
|
+
return [][Symbol.iterator]();
|
|
102
|
+
}
|
|
99
103
|
this.elements = this.doResolve();
|
|
100
104
|
}
|
|
105
|
+
// What was resolved while the session ran stays available after it has ended, as it does in VS Code.
|
|
101
106
|
return (await this.elements)[Symbol.iterator]();
|
|
102
107
|
}
|
|
103
108
|
protected async doResolve(): Promise<ExpressionContainer[]> {
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
|
|
19
|
+
let disableJSDOM = enableJSDOM();
|
|
20
|
+
|
|
21
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
22
|
+
FrontendApplicationConfigProvider.set({});
|
|
23
|
+
|
|
24
|
+
import 'reflect-metadata';
|
|
25
|
+
|
|
26
|
+
import { expect } from 'chai';
|
|
27
|
+
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
|
|
28
|
+
import { ExpressionContainer } from './debug-console-items';
|
|
29
|
+
import { DebugConsoleSession } from './debug-console-session';
|
|
30
|
+
import { DebugSession } from '../debug-session';
|
|
31
|
+
|
|
32
|
+
disableJSDOM();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The variables the fake adapter answers with, so that an output event with a variables reference resolves to an
|
|
36
|
+
* expandable item, as it does when a program logs an object rather than a string.
|
|
37
|
+
*/
|
|
38
|
+
const VARIABLES: Record<number, DebugProtocol.Variable[]> = {
|
|
39
|
+
1: [{ name: 'logged', value: 'Object', variablesReference: 2 }],
|
|
40
|
+
2: [{ name: 'answer', value: '42', variablesReference: 0 }]
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
class TestDebugSessionManager {
|
|
44
|
+
|
|
45
|
+
protected readonly sessions = new Map<string, DebugSession>();
|
|
46
|
+
|
|
47
|
+
currentSession: DebugSession | undefined;
|
|
48
|
+
|
|
49
|
+
getSession(id: string): DebugSession | undefined {
|
|
50
|
+
return this.sessions.get(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
add(session: DebugSession): void {
|
|
54
|
+
this.sessions.set(session.id, session);
|
|
55
|
+
this.currentSession = session;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Mirrors `DebugSessionManager.cleanup`, which drops the session before announcing its destruction. */
|
|
59
|
+
remove(id: string): void {
|
|
60
|
+
this.sessions.delete(id);
|
|
61
|
+
if (this.currentSession?.id === id) {
|
|
62
|
+
this.currentSession = undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('DebugConsoleSession', () => {
|
|
69
|
+
|
|
70
|
+
before(() => disableJSDOM = enableJSDOM());
|
|
71
|
+
after(() => disableJSDOM());
|
|
72
|
+
|
|
73
|
+
let manager: TestDebugSessionManager;
|
|
74
|
+
let consoleSession: DebugConsoleSession;
|
|
75
|
+
let maximumLines: number;
|
|
76
|
+
|
|
77
|
+
const createDebugSession = (id: string, configurationName: string, workspaceFolderUri?: string): DebugSession => ({
|
|
78
|
+
id,
|
|
79
|
+
label: configurationName,
|
|
80
|
+
configuration: { name: configurationName, type: 'node', request: 'launch' },
|
|
81
|
+
options: { workspaceFolderUri },
|
|
82
|
+
autoExpandLazyVariables: false,
|
|
83
|
+
sendRequest: async (_command: string, args: { variablesReference: number }) => ({
|
|
84
|
+
body: { variables: VARIABLES[args.variablesReference] ?? [] }
|
|
85
|
+
})
|
|
86
|
+
} as unknown as DebugSession);
|
|
87
|
+
|
|
88
|
+
const outputEvent = (body: Partial<DebugProtocol.OutputEvent['body']>): DebugProtocol.OutputEvent =>
|
|
89
|
+
({ event: 'output', body: { category: 'stdout', ...body } } as DebugProtocol.OutputEvent);
|
|
90
|
+
|
|
91
|
+
const contentsOf = (session: DebugConsoleSession): string[] =>
|
|
92
|
+
Array.from(session.getElements(), item => (item as { content?: string }).content ?? '');
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
manager = new TestDebugSessionManager();
|
|
96
|
+
maximumLines = 10000;
|
|
97
|
+
consoleSession = new DebugConsoleSession();
|
|
98
|
+
// The class uses property injection, and `init()` only registers a Monaco completion provider.
|
|
99
|
+
Object.assign(consoleSession as unknown as Record<string, unknown>, {
|
|
100
|
+
sessionManager: manager,
|
|
101
|
+
logger: { debug: () => { } },
|
|
102
|
+
preferences: { get 'debug.console.maximumLines'(): number { return maximumLines; } }
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('when the session terminates', () => {
|
|
107
|
+
|
|
108
|
+
it('releases the session but keeps its output', async () => {
|
|
109
|
+
const session = createDebugSession('session-1', 'Launch Program');
|
|
110
|
+
manager.add(session);
|
|
111
|
+
consoleSession.startFor(session);
|
|
112
|
+
await consoleSession.logOutput(session, outputEvent({ output: 'hello' }));
|
|
113
|
+
|
|
114
|
+
expect(consoleSession.debugSession).to.equal(session);
|
|
115
|
+
|
|
116
|
+
manager.remove(session.id);
|
|
117
|
+
consoleSession.markTerminated();
|
|
118
|
+
|
|
119
|
+
expect(consoleSession.terminated).to.be.true;
|
|
120
|
+
expect(consoleSession.debugSession).to.be.undefined;
|
|
121
|
+
expect(contentsOf(consoleSession)).to.deep.equal(['hello']);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('keeps the label and the configuration of the released session', () => {
|
|
125
|
+
const session = createDebugSession('session-1', 'Launch Program');
|
|
126
|
+
manager.add(session);
|
|
127
|
+
consoleSession.startFor(session);
|
|
128
|
+
|
|
129
|
+
manager.remove(session.id);
|
|
130
|
+
consoleSession.markTerminated();
|
|
131
|
+
|
|
132
|
+
expect(consoleSession.label).to.equal('Launch Program');
|
|
133
|
+
expect(consoleSession.configurationName).to.equal('Launch Program');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('keeps what was expanded while the session ran', async () => {
|
|
137
|
+
const session = createDebugSession('session-1', 'Launch Program');
|
|
138
|
+
manager.add(session);
|
|
139
|
+
consoleSession.startFor(session);
|
|
140
|
+
// An output event carrying a variables reference, as sent for a logged object, yields expandable items
|
|
141
|
+
// that resolve their children through the session.
|
|
142
|
+
await consoleSession.logOutput(session, outputEvent({ variablesReference: 1 }));
|
|
143
|
+
|
|
144
|
+
const [logged] = Array.from(consoleSession.getElements()) as ExpressionContainer[];
|
|
145
|
+
expect(Array.from(await logged.getElements())).to.have.lengthOf(1, 'expandable while the session runs');
|
|
146
|
+
|
|
147
|
+
manager.remove(session.id);
|
|
148
|
+
consoleSession.markTerminated();
|
|
149
|
+
|
|
150
|
+
expect(Array.from(consoleSession.getElements())).to.have.lengthOf(1, 'output is preserved');
|
|
151
|
+
expect(Array.from(await logged.getElements())).to.have.lengthOf(1, 'expanded children are preserved');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('does not retain the session through the items it logged', async () => {
|
|
155
|
+
const session = createDebugSession('session-1', 'Launch Program');
|
|
156
|
+
manager.add(session);
|
|
157
|
+
consoleSession.startFor(session);
|
|
158
|
+
await consoleSession.logOutput(session, outputEvent({ variablesReference: 1 }));
|
|
159
|
+
|
|
160
|
+
const [logged] = Array.from(consoleSession.getElements()) as ExpressionContainer[];
|
|
161
|
+
|
|
162
|
+
manager.remove(session.id);
|
|
163
|
+
consoleSession.markTerminated();
|
|
164
|
+
|
|
165
|
+
// Nothing was expanded before the session ended, so there is nothing cached to fall back on. The item
|
|
166
|
+
// resolves its session by id through the manager, which has dropped it, rather than holding on to it,
|
|
167
|
+
// so it has nothing left to ask.
|
|
168
|
+
expect(Array.from(await logged.getElements())).to.be.empty;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('when a session reuses the console', () => {
|
|
174
|
+
|
|
175
|
+
it('drops the output of the previous run and runs the new session', async () => {
|
|
176
|
+
const first = createDebugSession('session-1', 'Launch Program');
|
|
177
|
+
manager.add(first);
|
|
178
|
+
consoleSession.startFor(first);
|
|
179
|
+
await consoleSession.logOutput(first, outputEvent({ output: 'first run' }));
|
|
180
|
+
manager.remove(first.id);
|
|
181
|
+
consoleSession.markTerminated();
|
|
182
|
+
|
|
183
|
+
const second = createDebugSession('session-2', 'Launch Program');
|
|
184
|
+
manager.add(second);
|
|
185
|
+
consoleSession.startFor(second);
|
|
186
|
+
|
|
187
|
+
expect(consoleSession.terminated).to.be.false;
|
|
188
|
+
expect(consoleSession.debugSession).to.equal(second);
|
|
189
|
+
expect(contentsOf(consoleSession)).to.be.empty;
|
|
190
|
+
|
|
191
|
+
await consoleSession.logOutput(second, outputEvent({ output: 'second run' }));
|
|
192
|
+
expect(contentsOf(consoleSession)).to.deep.equal(['second run']);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('is reusable only by a session of the same configuration once terminated', () => {
|
|
196
|
+
consoleSession.startFor(createDebugSession('session-1', 'Launch Program'));
|
|
197
|
+
expect(consoleSession.isReusableBy(createDebugSession('session-2', 'Launch Program'))).to.be.false;
|
|
198
|
+
|
|
199
|
+
consoleSession.markTerminated();
|
|
200
|
+
|
|
201
|
+
expect(consoleSession.isReusableBy(createDebugSession('session-2', 'Launch Program'))).to.be.true;
|
|
202
|
+
expect(consoleSession.isReusableBy(createDebugSession('session-2', 'Attach'))).to.be.false;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('is not reusable by a configuration of the same name from another workspace folder', () => {
|
|
206
|
+
consoleSession.startFor(createDebugSession('session-1', 'Launch Program', 'file:///a'));
|
|
207
|
+
consoleSession.markTerminated();
|
|
208
|
+
|
|
209
|
+
expect(consoleSession.isReusableBy(createDebugSession('session-2', 'Launch Program', 'file:///b'))).to.be.false;
|
|
210
|
+
expect(consoleSession.isReusableBy(createDebugSession('session-2', 'Launch Program'))).to.be.false;
|
|
211
|
+
expect(consoleSession.isReusableBy(createDebugSession('session-2', 'Launch Program', 'file:///a'))).to.be.true;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('keeps the id it was created with, so that it is not the id of either session', () => {
|
|
215
|
+
const first = createDebugSession('session-1', 'Launch Program');
|
|
216
|
+
consoleSession.startFor(first);
|
|
217
|
+
const id = consoleSession.id;
|
|
218
|
+
|
|
219
|
+
consoleSession.markTerminated();
|
|
220
|
+
consoleSession.startFor(createDebugSession('session-2', 'Launch Program'));
|
|
221
|
+
|
|
222
|
+
expect(consoleSession.id).to.equal(id);
|
|
223
|
+
expect(consoleSession.id).to.not.equal('session-1');
|
|
224
|
+
expect(consoleSession.id).to.not.equal('session-2');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('output limit', () => {
|
|
230
|
+
|
|
231
|
+
it('drops the oldest lines beyond `debug.console.maximumLines`', async () => {
|
|
232
|
+
maximumLines = 3;
|
|
233
|
+
const session = createDebugSession('session-1', 'Launch Program');
|
|
234
|
+
manager.add(session);
|
|
235
|
+
consoleSession.startFor(session);
|
|
236
|
+
|
|
237
|
+
await consoleSession.logOutput(session, outputEvent({ output: 'one\ntwo\nthree\nfour' }));
|
|
238
|
+
expect(contentsOf(consoleSession)).to.deep.equal(['two', 'three', 'four']);
|
|
239
|
+
|
|
240
|
+
consoleSession.appendLine('five');
|
|
241
|
+
expect(contentsOf(consoleSession)).to.deep.equal(['three', 'four', 'five']);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
});
|