@theia/debug 1.77.0-next.6 → 1.77.0-next.63

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.
Files changed (118) hide show
  1. package/lib/browser/breakpoint/breakpoint-manager.js +1 -1
  2. package/lib/browser/breakpoint/breakpoint-manager.js.map +1 -1
  3. package/lib/browser/breakpoint/breakpoint-manager.spec.js +2 -1
  4. package/lib/browser/breakpoint/breakpoint-manager.spec.js.map +1 -1
  5. package/lib/browser/console/debug-console-contribution.d.ts +23 -1
  6. package/lib/browser/console/debug-console-contribution.d.ts.map +1 -1
  7. package/lib/browser/console/debug-console-contribution.js +47 -14
  8. package/lib/browser/console/debug-console-contribution.js.map +1 -1
  9. package/lib/browser/console/debug-console-contribution.spec.d.ts +2 -0
  10. package/lib/browser/console/debug-console-contribution.spec.d.ts.map +1 -0
  11. package/lib/browser/console/debug-console-contribution.spec.js +142 -0
  12. package/lib/browser/console/debug-console-contribution.spec.js.map +1 -0
  13. package/lib/browser/console/debug-console-items.d.ts +2 -3
  14. package/lib/browser/console/debug-console-items.d.ts.map +1 -1
  15. package/lib/browser/console/debug-console-items.js +27 -11
  16. package/lib/browser/console/debug-console-items.js.map +1 -1
  17. package/lib/browser/console/debug-console-session.d.ts +48 -3
  18. package/lib/browser/console/debug-console-session.d.ts.map +1 -1
  19. package/lib/browser/console/debug-console-session.js +92 -19
  20. package/lib/browser/console/debug-console-session.js.map +1 -1
  21. package/lib/browser/console/debug-console-session.spec.d.ts +2 -0
  22. package/lib/browser/console/debug-console-session.spec.d.ts.map +1 -0
  23. package/lib/browser/console/debug-console-session.spec.js +187 -0
  24. package/lib/browser/console/debug-console-session.spec.js.map +1 -0
  25. package/lib/browser/console/debug-copy-value.spec.d.ts +2 -0
  26. package/lib/browser/console/debug-copy-value.spec.d.ts.map +1 -0
  27. package/lib/browser/console/debug-copy-value.spec.js +123 -0
  28. package/lib/browser/console/debug-copy-value.spec.js.map +1 -0
  29. package/lib/browser/debug-configuration-manager.js +1 -1
  30. package/lib/browser/debug-configuration-manager.js.map +1 -1
  31. package/lib/browser/debug-frontend-application-contribution.d.ts +5 -0
  32. package/lib/browser/debug-frontend-application-contribution.d.ts.map +1 -1
  33. package/lib/browser/debug-frontend-application-contribution.js +13 -3
  34. package/lib/browser/debug-frontend-application-contribution.js.map +1 -1
  35. package/lib/browser/debug-package.spec.js +1 -0
  36. package/lib/browser/debug-package.spec.js.map +1 -1
  37. package/lib/browser/debug-prefix-configuration.js +1 -1
  38. package/lib/browser/debug-prefix-configuration.js.map +1 -1
  39. package/lib/browser/debug-schema-updater.js +1 -1
  40. package/lib/browser/debug-schema-updater.js.map +1 -1
  41. package/lib/browser/debug-session-configuration-label-provider.js +1 -1
  42. package/lib/browser/debug-session-configuration-label-provider.js.map +1 -1
  43. package/lib/browser/debug-session-connection.d.ts +6 -0
  44. package/lib/browser/debug-session-connection.d.ts.map +1 -1
  45. package/lib/browser/debug-session-connection.js +8 -0
  46. package/lib/browser/debug-session-connection.js.map +1 -1
  47. package/lib/browser/debug-session-manager.d.ts.map +1 -1
  48. package/lib/browser/debug-session-manager.js +6 -3
  49. package/lib/browser/debug-session-manager.js.map +1 -1
  50. package/lib/browser/debug-session-options.js +1 -1
  51. package/lib/browser/debug-session-options.js.map +1 -1
  52. package/lib/browser/debug-session.d.ts.map +1 -1
  53. package/lib/browser/debug-session.js +7 -1
  54. package/lib/browser/debug-session.js.map +1 -1
  55. package/lib/browser/debug-watch-manager.d.ts +1 -1
  56. package/lib/browser/debug-watch-manager.d.ts.map +1 -1
  57. package/lib/browser/editor/debug-breakpoint-widget.js +3 -3
  58. package/lib/browser/editor/debug-breakpoint-widget.js.map +1 -1
  59. package/lib/browser/editor/debug-editor-model.js +2 -2
  60. package/lib/browser/editor/debug-editor-model.js.map +1 -1
  61. package/lib/browser/editor/debug-editor-service.js +1 -1
  62. package/lib/browser/editor/debug-editor-service.js.map +1 -1
  63. package/lib/browser/editor/debug-exception-widget.js +1 -1
  64. package/lib/browser/editor/debug-exception-widget.js.map +1 -1
  65. package/lib/browser/editor/debug-expression-provider.js +1 -1
  66. package/lib/browser/editor/debug-expression-provider.js.map +1 -1
  67. package/lib/browser/editor/debug-hover-widget.js +1 -1
  68. package/lib/browser/editor/debug-hover-widget.js.map +1 -1
  69. package/lib/browser/editor/debug-inline-value-decorator.js +1 -1
  70. package/lib/browser/editor/debug-inline-value-decorator.js.map +1 -1
  71. package/lib/browser/model/debug-function-breakpoint.js +2 -1
  72. package/lib/browser/model/debug-function-breakpoint.js.map +1 -1
  73. package/lib/browser/model/debug-instruction-breakpoint.js +2 -1
  74. package/lib/browser/model/debug-instruction-breakpoint.js.map +1 -1
  75. package/lib/browser/model/debug-source-breakpoint.js +3 -2
  76. package/lib/browser/model/debug-source-breakpoint.js.map +1 -1
  77. package/lib/browser/model/debug-source.js +2 -1
  78. package/lib/browser/model/debug-source.js.map +1 -1
  79. package/lib/browser/model/debug-stack-frame.js +2 -1
  80. package/lib/browser/model/debug-stack-frame.js.map +1 -1
  81. package/lib/browser/view/debug-action.js +2 -1
  82. package/lib/browser/view/debug-action.js.map +1 -1
  83. package/lib/browser/view/debug-configuration-select.js +3 -2
  84. package/lib/browser/view/debug-configuration-select.js.map +1 -1
  85. package/lib/browser/view/debug-configuration-widget.js +1 -1
  86. package/lib/browser/view/debug-configuration-widget.js.map +1 -1
  87. package/lib/browser/view/debug-toolbar-widget.js +1 -1
  88. package/lib/browser/view/debug-toolbar-widget.js.map +1 -1
  89. package/lib/browser/view/debug-variables-widget.d.ts.map +1 -1
  90. package/lib/browser/view/debug-variables-widget.js +6 -0
  91. package/lib/browser/view/debug-variables-widget.js.map +1 -1
  92. package/lib/browser/view/debug-view-model.js +1 -1
  93. package/lib/browser/view/debug-view-model.js.map +1 -1
  94. package/lib/browser/view/debug-watch-expression.d.ts +1 -3
  95. package/lib/browser/view/debug-watch-expression.d.ts.map +1 -1
  96. package/lib/browser/view/debug-watch-expression.js +4 -9
  97. package/lib/browser/view/debug-watch-expression.js.map +1 -1
  98. package/lib/common/debug-preferences.d.ts +1 -0
  99. package/lib/common/debug-preferences.d.ts.map +1 -1
  100. package/lib/common/debug-preferences.js +6 -0
  101. package/lib/common/debug-preferences.js.map +1 -1
  102. package/lib/node/debug-adapter-factory.js +1 -1
  103. package/lib/node/debug-adapter-factory.js.map +1 -1
  104. package/package.json +15 -15
  105. package/src/browser/console/debug-console-contribution.spec.ts +183 -0
  106. package/src/browser/console/debug-console-contribution.tsx +53 -14
  107. package/src/browser/console/debug-console-items.tsx +25 -11
  108. package/src/browser/console/debug-console-session.spec.ts +246 -0
  109. package/src/browser/console/debug-console-session.ts +102 -19
  110. package/src/browser/console/debug-copy-value.spec.ts +144 -0
  111. package/src/browser/debug-frontend-application-contribution.ts +12 -2
  112. package/src/browser/debug-session-connection.ts +9 -0
  113. package/src/browser/debug-session-manager.ts +5 -2
  114. package/src/browser/debug-session-options.ts +1 -1
  115. package/src/browser/debug-session.tsx +6 -0
  116. package/src/browser/view/debug-variables-widget.ts +6 -0
  117. package/src/browser/view/debug-watch-expression.tsx +4 -10
  118. package/src/common/debug-preferences.ts +7 -0
@@ -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.consoleSessionManager.get(consoleParent.id);
96
- if (parentConsoleSession instanceof DebugConsoleSession) {
97
- session.on('output', event => parentConsoleSession.logOutput(parentConsoleSession.debugSession, event));
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.debugConsoleSessionFactory(session);
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.consoleSessionManager.get(topSession.id) : undefined;
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.debugSession = session;
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.debugSession.label;
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 || !this.session) {
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[]> {
@@ -166,6 +171,19 @@ export class ExpressionContainer implements CompositeConsoleItem {
166
171
  }
167
172
  }
168
173
 
174
+ protected async evaluateForClipboard(expression: string, context: DebugProtocol.EvaluateArguments['context'], fallback: string): Promise<string> {
175
+ const { session } = this;
176
+ if (!session) {
177
+ return fallback;
178
+ }
179
+ try {
180
+ const evaluationContext = session.capabilities.supportsClipboardContext ? 'clipboard' : context;
181
+ return (await session.evaluate(expression, evaluationContext)).result;
182
+ } catch {
183
+ return fallback;
184
+ }
185
+ }
186
+
169
187
  }
170
188
  export namespace ExpressionContainer {
171
189
  export interface Options {
@@ -223,7 +241,7 @@ export class DebugVariable extends ExpressionContainer {
223
241
  return <div className={this.variableClassName}>
224
242
  <span title={type || name} className='name' ref={this.setNameRef}>{name}{(value || lazy) && ': '}</span>
225
243
  {lazy && <span title={nls.localizeByDefault('Click to expand')} className={codicon('eye') + ' lazy-button'} onClick={this.handleLazyButtonClick} />}
226
- <span title={value} className='value' ref={this.setValueRef}>{value}</span>
244
+ <span title={value} className='value'>{value}</span>
227
245
  </div>;
228
246
  }
229
247
 
@@ -271,17 +289,13 @@ export class DebugVariable extends ExpressionContainer {
271
289
  }
272
290
 
273
291
  get supportCopyValue(): boolean {
274
- return !!this.valueRef && document.queryCommandSupported('copy');
292
+ return !!this.value;
275
293
  }
276
- copyValue(): void {
277
- const selection = document.getSelection();
278
- if (this.valueRef && selection) {
279
- selection.selectAllChildren(this.valueRef);
280
- document.execCommand('copy');
281
- }
294
+ async getValueToCopy(): Promise<string> {
295
+ return this.evaluateName
296
+ ? await this.evaluateForClipboard(this.evaluateName, 'variables', this.value)
297
+ : this.value;
282
298
  }
283
- protected valueRef: HTMLSpanElement | undefined;
284
- protected setValueRef = (valueRef: HTMLSpanElement | null): void => { this.valueRef = valueRef || undefined; };
285
299
 
286
300
  get supportCopyAsExpression(): boolean {
287
301
  return !!this.nameRef && document.queryCommandSupported('copy');
@@ -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
+ });
@@ -20,10 +20,12 @@ import { ConsoleSession, ConsoleItem } from '@theia/console/lib/browser/console-
20
20
  import { AnsiConsoleItem } from '@theia/console/lib/browser/ansi-console-item';
21
21
  import { DebugSession } from '../debug-session';
22
22
  import URI from '@theia/core/lib/common/uri';
23
+ import { generateUuid } from '@theia/core/lib/common/uuid';
23
24
  import { ExpressionContainer, ExpressionItem } from './debug-console-items';
24
25
  import { Severity } from '@theia/core/lib/common/severity';
25
26
  import { inject, injectable, postConstruct, named } from '@theia/core/shared/inversify';
26
27
  import { DebugSessionManager } from '../debug-session-manager';
28
+ import { DebugPreferences } from '../../common/debug-preferences';
27
29
  import * as monaco from '@theia/monaco-editor-core';
28
30
  import { LanguageSelector } from '@theia/monaco-editor-core/esm/vs/editor/common/languageSelector';
29
31
  import { Disposable, ILogger } from '@theia/core';
@@ -42,24 +44,61 @@ export class DebugConsoleSession extends ConsoleSession {
42
44
  @inject(ILogger) @named('debug:DebugConsoleSession')
43
45
  protected readonly logger: ILogger;
44
46
 
47
+ @inject(DebugPreferences)
48
+ protected readonly preferences: DebugPreferences;
49
+
50
+ /**
51
+ * The console has an identity of its own, independent of the sessions that run in it: it outlives the session it
52
+ * was created for and is reused by a later one.
53
+ */
54
+ override id = generateUuid();
55
+
45
56
  protected items: ConsoleItem[] = [];
46
57
 
47
58
  protected _terminated = false;
48
59
 
49
- protected _debugSession: DebugSession;
60
+ protected _debugSession: DebugSession | undefined;
61
+
62
+ protected _label = '';
63
+
64
+ protected _configurationName: string | undefined;
65
+
66
+ protected _workspaceFolderUri: string | undefined;
50
67
 
51
68
  // content buffer for [append](#append) method
52
69
  protected uncompletedItemContent: string | undefined;
53
70
 
54
71
  protected readonly completionKinds = new Map<DebugProtocol.CompletionItemType | undefined, monaco.languages.CompletionItemKind>();
55
72
 
56
- get debugSession(): DebugSession {
73
+ /**
74
+ * The debug session this console currently belongs to, or `undefined` once that session has terminated.
75
+ *
76
+ * The reference is dropped on termination so that the console does not keep the whole session graph alive
77
+ * while it preserves the output of that session.
78
+ */
79
+ get debugSession(): DebugSession | undefined {
57
80
  return this._debugSession;
58
81
  }
59
82
 
60
- set debugSession(value: DebugSession) {
61
- this._debugSession = value;
62
- this.id = value.id;
83
+ /**
84
+ * The label of the session this console belongs to, retained after that session has terminated.
85
+ */
86
+ get label(): string {
87
+ return this._label;
88
+ }
89
+
90
+ /**
91
+ * The name of the configuration this console was started for, used to match a restarted session to its console.
92
+ */
93
+ get configurationName(): string | undefined {
94
+ return this._configurationName;
95
+ }
96
+
97
+ /**
98
+ * The workspace folder of the configuration this console was started for, if any.
99
+ */
100
+ get workspaceFolderUri(): string | undefined {
101
+ return this._workspaceFolderUri;
63
102
  }
64
103
 
65
104
  get terminated(): boolean {
@@ -69,10 +108,41 @@ export class DebugConsoleSession extends ConsoleSession {
69
108
  markTerminated(): void {
70
109
  if (!this._terminated) {
71
110
  this._terminated = true;
111
+ // Release the session: from here on the console only holds the output it has already collected.
112
+ this._debugSession = undefined;
72
113
  this.fireDidChange();
73
114
  }
74
115
  }
75
116
 
117
+ /**
118
+ * Whether the given session can take over this console: its session must have terminated and been started from the
119
+ * same configuration, which in a multi-root workspace includes the folder the configuration comes from.
120
+ */
121
+ isReusableBy(session: DebugSession): boolean {
122
+ return this._terminated
123
+ && this._configurationName === session.configuration.name
124
+ && this._workspaceFolderUri === session.options.workspaceFolderUri;
125
+ }
126
+
127
+ /**
128
+ * Runs the given session in this console, whether it is the session the console was created for or a later one
129
+ * reusing it. Counterpart of {@link markTerminated}.
130
+ *
131
+ * The output of any previous session is dropped: it belongs to that run, and keeping it would merge two runs into
132
+ * one view with no boundary between them. The filter and severity stay, since they are the view the user has set
133
+ * up for this configuration.
134
+ */
135
+ startFor(session: DebugSession): void {
136
+ this.items = [];
137
+ this.uncompletedItemContent = undefined;
138
+ this._debugSession = session;
139
+ this._label = session.label;
140
+ this._configurationName = session.configuration.name;
141
+ this._workspaceFolderUri = session.options.workspaceFolderUri;
142
+ this._terminated = false;
143
+ this.fireDidChange();
144
+ }
145
+
76
146
  @postConstruct()
77
147
  init(): void {
78
148
  this.completionKinds.set('method', monaco.languages.CompletionItemKind.Method);
@@ -175,16 +245,17 @@ export class DebugConsoleSession extends ConsoleSession {
175
245
  }
176
246
 
177
247
  protected findCurrentSession(): DebugSession | undefined {
248
+ const debugSession = this._debugSession;
178
249
  const currentSession = this.sessionManager.currentSession;
179
- if (!currentSession) {
250
+ if (!debugSession || !currentSession) {
180
251
  return undefined;
181
252
  }
182
- if (currentSession.id === this.debugSession.id) {
253
+ if (currentSession.id === debugSession.id) {
183
254
  // perfect match
184
- return this.debugSession;
255
+ return debugSession;
185
256
  }
186
257
  const parentSession = currentSession.findConsoleParent();
187
- if (parentSession?.id === this.debugSession.id) {
258
+ if (parentSession?.id === debugSession.id) {
188
259
  // child of our session
189
260
  return currentSession;
190
261
  }
@@ -215,7 +286,7 @@ export class DebugConsoleSession extends ConsoleSession {
215
286
 
216
287
  async execute(value: string): Promise<void> {
217
288
  const expression = new ExpressionItem(value, () => this.findCurrentSession());
218
- this.items.push(expression);
289
+ this.appendItems([expression]);
219
290
  await expression.evaluate();
220
291
  this.fireDidChange();
221
292
  }
@@ -238,15 +309,28 @@ export class DebugConsoleSession extends ConsoleSession {
238
309
  this.uncompletedItemContent = value;
239
310
  }
240
311
 
241
- this.items.push(new AnsiConsoleItem(this.uncompletedItemContent, Severity.Info));
312
+ this.appendItems([new AnsiConsoleItem(this.uncompletedItemContent, Severity.Info)]);
242
313
  this.fireDidChange();
243
314
  }
244
315
 
245
316
  appendLine(value: string): void {
246
- this.items.push(new AnsiConsoleItem(value, Severity.Info));
317
+ this.appendItems([new AnsiConsoleItem(value, Severity.Info)]);
247
318
  this.fireDidChange();
248
319
  }
249
320
 
321
+ /**
322
+ * Appends items, dropping the oldest ones so that the console stays within `debug.console.maximumLines`.
323
+ */
324
+ protected appendItems(items: ConsoleItem[]): void {
325
+ for (const item of items) {
326
+ this.items.push(item);
327
+ }
328
+ const maximumLines = this.preferences['debug.console.maximumLines'];
329
+ if (maximumLines > 0 && this.items.length > maximumLines) {
330
+ this.items.splice(0, this.items.length - maximumLines);
331
+ }
332
+ }
333
+
250
334
  async logOutput(session: DebugSession, event: DebugProtocol.OutputEvent): Promise<void> {
251
335
  const body = event.body;
252
336
  const { category, variablesReference } = body;
@@ -256,14 +340,13 @@ export class DebugConsoleSession extends ConsoleSession {
256
340
  }
257
341
  const severity = category === 'stderr' ? Severity.Error : event.body.category === 'console' ? Severity.Warning : Severity.Info;
258
342
  if (variablesReference) {
259
- const items = await new ExpressionContainer({ session: () => session, variablesReference }).getElements();
260
- for (const item of items) {
261
- this.items.push(Object.assign(item, { severity }));
262
- }
343
+ // Resolve the session by id rather than capturing it, so that the items do not keep it alive once it is gone.
344
+ const { id } = session;
345
+ const sessionProvider = () => this.sessionManager.getSession(id);
346
+ const items = await new ExpressionContainer({ session: sessionProvider, variablesReference }).getElements();
347
+ this.appendItems(Array.from(items, item => Object.assign(item, { severity })));
263
348
  } else if (typeof body.output === 'string') {
264
- for (const line of body.output.split('\n')) {
265
- this.items.push(new AnsiConsoleItem(line, severity));
266
- }
349
+ this.appendItems(body.output.split('\n').map(line => new AnsiConsoleItem(line, severity)));
267
350
  }
268
351
  this.fireDidChange();
269
352
  }