Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { RunOnceScheduler } from '../../../../base/common/async.js';
7
import { Emitter, Event } from '../../../../base/common/event.js';
8
import { IJSONSchema, IJSONSchemaMap } from '../../../../base/common/jsonSchema.js';
9
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
10
import Severity from '../../../../base/common/severity.js';
11
import * as strings from '../../../../base/common/strings.js';
12
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
13
import { IEditorModel } from '../../../../editor/common/editorCommon.js';
14
import { ILanguageService } from '../../../../editor/common/languages/language.js';
15
import { ITextModel } from '../../../../editor/common/model.js';
16
import * as nls from '../../../../nls.js';
17
import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js';
18
import { ICommandService } from '../../../../platform/commands/common/commands.js';
19
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
20
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
21
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
22
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
23
import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';
24
import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';
25
import { Registry } from '../../../../platform/registry/common/platform.js';
26
import { IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
27
import { Breakpoints } from '../common/breakpoints.js';
28
import { CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_EXTENSION_AVAILABLE, IAdapterDescriptor, IAdapterManager, IConfig, IConfigurationManager, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugAdapterFactory, IDebugConfiguration, IDebugSession, IGuessedDebugger, INTERNAL_CONSOLE_OPTIONS_SCHEMA } from '../common/debug.js';
29
import { Debugger } from '../common/debugger.js';
30
import { breakpointsExtPoint, debuggersExtPoint, launchSchema, presentationSchema } from '../common/debugSchemas.js';
31
import { TaskDefinitionRegistry } from '../../tasks/common/taskDefinitionRegistry.js';
32
import { ITaskService } from '../../tasks/common/taskService.js';
33
import { launchSchemaId } from '../../../services/configuration/common/configuration.js';
34
import { IEditorService } from '../../../services/editor/common/editorService.js';
35
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
36
import { ILifecycleService, LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js';
37
38
const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
39
40
export interface IAdapterManagerDelegate {
41
onDidNewSession: Event<IDebugSession>;
42
configurationManager(): IConfigurationManager;
43
}
44
45
export class AdapterManager extends Disposable implements IAdapterManager {
46
47
private debuggers: Debugger[];
48
private adapterDescriptorFactories: IDebugAdapterDescriptorFactory[];
49
private debugAdapterFactories = new Map<string, IDebugAdapterFactory>();
50
private debuggersAvailable!: IContextKey<boolean>;
51
private debugExtensionsAvailable!: IContextKey<boolean>;
52
private readonly _onDidRegisterDebugger = new Emitter<void>();
53
private readonly _onDidDebuggersExtPointRead = new Emitter<void>();
54
private breakpointContributions: Breakpoints[] = [];
55
private debuggerWhenKeys = new Set<string>();
56
private taskLabels: string[] = [];
57
58
/** Extensions that were already active before any debugger activation events */
59
private earlyActivatedExtensions: Set<string> | undefined;
60
61
private usedDebugTypes = new Set<string>();
62
63
constructor(
64
private readonly delegate: IAdapterManagerDelegate,
65
@IEditorService private readonly editorService: IEditorService,
66
@IConfigurationService private readonly configurationService: IConfigurationService,
67
@IQuickInputService private readonly quickInputService: IQuickInputService,
68
@IInstantiationService private readonly instantiationService: IInstantiationService,
69
@ICommandService private readonly commandService: ICommandService,
70
@IExtensionService private readonly extensionService: IExtensionService,
71
@IContextKeyService private readonly contextKeyService: IContextKeyService,
72
@ILanguageService private readonly languageService: ILanguageService,
73
@IDialogService private readonly dialogService: IDialogService,
74
@ILifecycleService private readonly lifecycleService: ILifecycleService,
75
@ITaskService private readonly tasksService: ITaskService,
76
@IMenuService private readonly menuService: IMenuService,
77
) {
78
super();
79
this.adapterDescriptorFactories = [];
80
this.debuggers = [];
81
this.registerListeners();
82
this.contextKeyService.bufferChangeEvents(() => {
83
this.debuggersAvailable = CONTEXT_DEBUGGERS_AVAILABLE.bindTo(contextKeyService);
84
this.debugExtensionsAvailable = CONTEXT_DEBUG_EXTENSION_AVAILABLE.bindTo(contextKeyService);
85
});
86
this._register(this.contextKeyService.onDidChangeContext(e => {
87
if (e.affectsSome(this.debuggerWhenKeys)) {
88
this.debuggersAvailable.set(this.hasEnabledDebuggers());
89
this.updateDebugAdapterSchema();
90
}
91
}));
92
this._register(this.onDidDebuggersExtPointRead(() => {
93
this.debugExtensionsAvailable.set(this.debuggers.length > 0);
94
}));
95
96
// generous debounce since this will end up calling `resolveTask` internally
97
const updateTaskScheduler = this._register(new RunOnceScheduler(() => this.updateTaskLabels(), 5000));
98
99
this._register(Event.any(tasksService.onDidChangeTaskConfig, tasksService.onDidChangeTaskProviders)(() => {
100
updateTaskScheduler.cancel();
101
updateTaskScheduler.schedule();
102
}));
103
this.lifecycleService.when(LifecyclePhase.Eventually)
104
.then(() => this.debugExtensionsAvailable.set(this.debuggers.length > 0)); // If no extensions with a debugger contribution are loaded
105
106
this._register(delegate.onDidNewSession(s => {
107
this.usedDebugTypes.add(s.configuration.type);
108
}));
109
110
updateTaskScheduler.schedule();
111
}
112
113
private registerListeners(): void {
114
debuggersExtPoint.setHandler((extensions, delta) => {
115
delta.added.forEach(added => {
116
added.value.forEach(rawAdapter => {
117
if (!rawAdapter.type || (typeof rawAdapter.type !== 'string')) {
118
added.collector.error(nls.localize('debugNoType', "Debugger 'type' can not be omitted and must be of type 'string'."));
119
}
120
121
if (rawAdapter.type !== '*') {
122
const existing = this.getDebugger(rawAdapter.type);
123
if (existing) {
124
existing.merge(rawAdapter, added.description);
125
} else {
126
const dbg = this.instantiationService.createInstance(Debugger, this, rawAdapter, added.description);
127
dbg.when?.keys().forEach(key => this.debuggerWhenKeys.add(key));
128
this.debuggers.push(dbg);
129
}
130
}
131
});
132
});
133
134
// take care of all wildcard contributions
135
extensions.forEach(extension => {
136
extension.value.forEach(rawAdapter => {
137
if (rawAdapter.type === '*') {
138
this.debuggers.forEach(dbg => dbg.merge(rawAdapter, extension.description));
139
}
140
});
141
});
142
143
delta.removed.forEach(removed => {
144
const removedTypes = removed.value.map(rawAdapter => rawAdapter.type);
145
this.debuggers = this.debuggers.filter(d => removedTypes.indexOf(d.type) === -1);
146
});
147
148
this.updateDebugAdapterSchema();
149
this._onDidDebuggersExtPointRead.fire();
150
});
151
152
breakpointsExtPoint.setHandler(extensions => {
153
this.breakpointContributions = extensions.flatMap(ext => ext.value.map(breakpoint => this.instantiationService.createInstance(Breakpoints, breakpoint)));
154
});
155
}
156
157
private updateTaskLabels() {
158
this.tasksService.getKnownTasks().then(tasks => {
159
this.taskLabels = tasks.map(task => task._label);
160
this.updateDebugAdapterSchema();
161
});
162
}
163
164
private updateDebugAdapterSchema() {
165
// update the schema to include all attributes, snippets and types from extensions.
166
const items = (<IJSONSchema>launchSchema.properties!['configurations'].items);
167
const taskSchema = TaskDefinitionRegistry.getJsonSchema();
168
const definitions: IJSONSchemaMap = {
169
'common': {
170
properties: {
171
'name': {
172
type: 'string',
173
description: nls.localize('debugName', "Name of configuration; appears in the launch configuration dropdown menu."),
174
default: 'Launch'
175
},
176
'debugServer': {
177
type: 'number',
178
description: nls.localize('debugServer', "For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode"),
179
default: 4711
180
},
181
'preLaunchTask': {
182
anyOf: [taskSchema, {
183
type: ['string']
184
}],
185
default: '',
186
defaultSnippets: [{ body: { task: '', type: '' } }],
187
description: nls.localize('debugPrelaunchTask', "Task to run before debug session starts."),
188
examples: this.taskLabels,
189
},
190
'postDebugTask': {
191
anyOf: [taskSchema, {
192
type: ['string'],
193
}],
194
default: '',
195
defaultSnippets: [{ body: { task: '', type: '' } }],
196
description: nls.localize('debugPostDebugTask', "Task to run after debug session ends."),
197
examples: this.taskLabels,
198
},
199
'presentation': presentationSchema,
200
'internalConsoleOptions': INTERNAL_CONSOLE_OPTIONS_SCHEMA,
201
'suppressMultipleSessionWarning': {
202
type: 'boolean',
203
description: nls.localize('suppressMultipleSessionWarning', "Disable the warning when trying to start the same debug configuration more than once."),
204
default: true
205
}
206
}
207
}
208
};
209
launchSchema.definitions = definitions;
210
items.oneOf = [];
211
items.defaultSnippets = [];
212
this.debuggers.forEach(adapter => {
213
const schemaAttributes = adapter.getSchemaAttributes(definitions);
214
if (schemaAttributes && items.oneOf) {
215
items.oneOf.push(...schemaAttributes);
216
}
217
const configurationSnippets = adapter.configurationSnippets;
218
if (configurationSnippets && items.defaultSnippets) {
219
items.defaultSnippets.push(...configurationSnippets);
220
}
221
});
222
jsonRegistry.registerSchema(launchSchemaId, launchSchema);
223
}
224
225
registerDebugAdapterFactory(debugTypes: string[], debugAdapterLauncher: IDebugAdapterFactory): IDisposable {
226
debugTypes.forEach(debugType => this.debugAdapterFactories.set(debugType, debugAdapterLauncher));
227
this.debuggersAvailable.set(this.hasEnabledDebuggers());
228
this._onDidRegisterDebugger.fire();
229
230
return {
231
dispose: () => {
232
debugTypes.forEach(debugType => this.debugAdapterFactories.delete(debugType));
233
}
234
};
235
}
236
237
hasEnabledDebuggers(): boolean {
238
for (const [type] of this.debugAdapterFactories) {
239
const dbg = this.getDebugger(type);
240
if (dbg && dbg.enabled) {
241
return true;
242
}
243
}
244
245
return false;
246
}
247
248
createDebugAdapter(session: IDebugSession): IDebugAdapter | undefined {
249
const factory = this.debugAdapterFactories.get(session.configuration.type);
250
if (factory) {
251
return factory.createDebugAdapter(session);
252
}
253
return undefined;
254
}
255
256
substituteVariables(debugType: string, folder: IWorkspaceFolder | undefined, config: IConfig): Promise<IConfig> {
257
const factory = this.debugAdapterFactories.get(debugType);
258
if (factory) {
259
return factory.substituteVariables(folder, config);
260
}
261
return Promise.resolve(config);
262
}
263
264
runInTerminal(debugType: string, args: DebugProtocol.RunInTerminalRequestArguments, sessionId: string): Promise<number | undefined> {
265
const factory = this.debugAdapterFactories.get(debugType);
266
if (factory) {
267
return factory.runInTerminal(args, sessionId);
268
}
269
return Promise.resolve(void 0);
270
}
271
272
registerDebugAdapterDescriptorFactory(debugAdapterProvider: IDebugAdapterDescriptorFactory): IDisposable {
273
this.adapterDescriptorFactories.push(debugAdapterProvider);
274
return {
275
dispose: () => {
276
this.unregisterDebugAdapterDescriptorFactory(debugAdapterProvider);
277
}
278
};
279
}
280
281
unregisterDebugAdapterDescriptorFactory(debugAdapterProvider: IDebugAdapterDescriptorFactory): void {
282
const ix = this.adapterDescriptorFactories.indexOf(debugAdapterProvider);
283
if (ix >= 0) {
284
this.adapterDescriptorFactories.splice(ix, 1);
285
}
286
}
287
288
getDebugAdapterDescriptor(session: IDebugSession): Promise<IAdapterDescriptor | undefined> {
289
const config = session.configuration;
290
const providers = this.adapterDescriptorFactories.filter(p => p.type === config.type && p.createDebugAdapterDescriptor);
291
if (providers.length === 1) {
292
return providers[0].createDebugAdapterDescriptor(session);
293
} else {
294
// TODO@AW handle n > 1 case
295
}
296
return Promise.resolve(undefined);
297
}
298
299
getDebuggerLabel(type: string): string | undefined {
300
const dbgr = this.getDebugger(type);
301
if (dbgr) {
302
return dbgr.label;
303
}
304
305
return undefined;
306
}
307
308
get onDidRegisterDebugger(): Event<void> {
309
return this._onDidRegisterDebugger.event;
310
}
311
312
get onDidDebuggersExtPointRead(): Event<void> {
313
return this._onDidDebuggersExtPointRead.event;
314
}
315
316
canSetBreakpointsIn(model: ITextModel): boolean {
317
const languageId = model.getLanguageId();
318
if (!languageId || languageId === 'jsonc' || languageId === 'log') {
319
// do not allow breakpoints in our settings files and output
320
return false;
321
}
322
if (this.configurationService.getValue<IDebugConfiguration>('debug').allowBreakpointsEverywhere) {
323
return true;
324
}
325
326
return this.breakpointContributions.some(breakpoints => breakpoints.language === languageId && breakpoints.enabled);
327
}
328
329
getDebugger(type: string): Debugger | undefined {
330
return this.debuggers.find(dbg => strings.equalsIgnoreCase(dbg.type, type));
331
}
332
333
getEnabledDebugger(type: string): Debugger | undefined {
334
const adapter = this.getDebugger(type);
335
return adapter && adapter.enabled ? adapter : undefined;
336
}
337
338
someDebuggerInterestedInLanguage(languageId: string): boolean {
339
return !!this.debuggers
340
.filter(d => d.enabled)
341
.find(a => a.interestedInLanguage(languageId));
342
}
343
344
async guessDebugger(gettingConfigurations: boolean): Promise<IGuessedDebugger | undefined> {
345
const activeTextEditorControl = this.editorService.activeTextEditorControl;
346
let candidates: Debugger[] = [];
347
let languageLabel: string | null = null;
348
let model: IEditorModel | null = null;
349
if (isCodeEditor(activeTextEditorControl)) {
350
model = activeTextEditorControl.getModel();
351
const language = model ? model.getLanguageId() : undefined;
352
if (language) {
353
languageLabel = this.languageService.getLanguageName(language);
354
}
355
const adapters = this.debuggers
356
.filter(a => a.enabled)
357
.filter(a => language && a.interestedInLanguage(language));
358
if (adapters.length === 1) {
359
return { debugger: adapters[0] };
360
}
361
if (adapters.length > 1) {
362
candidates = adapters;
363
}
364
}
365
366
// We want to get the debuggers that have configuration providers in the case we are fetching configurations
367
// Or if a breakpoint can be set in the current file (good hint that an extension can handle it)
368
if ((!languageLabel || gettingConfigurations || (model && this.canSetBreakpointsIn(model))) && candidates.length === 0) {
369
await this.activateDebuggers('onDebugInitialConfigurations');
370
371
candidates = this.debuggers
372
.filter(a => a.enabled)
373
.filter(dbg => dbg.hasInitialConfiguration() || dbg.hasDynamicConfigurationProviders() || dbg.hasConfigurationProvider());
374
}
375
376
if (candidates.length === 0 && languageLabel) {
377
if (languageLabel.indexOf(' ') >= 0) {
378
languageLabel = `'${languageLabel}'`;
379
}
380
const { confirmed } = await this.dialogService.confirm({
381
type: Severity.Warning,
382
message: nls.localize('CouldNotFindLanguage', "You don't have an extension for debugging {0}. Should we find a {0} extension in the Marketplace?", languageLabel),
383
primaryButton: nls.localize({ key: 'findExtension', comment: ['&& denotes a mnemonic'] }, "&&Find {0} extension", languageLabel)
384
});
385
if (confirmed) {
386
await this.commandService.executeCommand('debug.installAdditionalDebuggers', languageLabel);
387
}
388
return undefined;
389
}
390
391
this.initExtensionActivationsIfNeeded();
392
393
candidates.sort((first, second) => first.label.localeCompare(second.label));
394
candidates = candidates.filter(a => !a.isHiddenFromDropdown);
395
396
const suggestedCandidates: Debugger[] = [];
397
const otherCandidates: Debugger[] = [];
398
candidates.forEach(d => {
399
const descriptor = d.getMainExtensionDescriptor();
400
if (descriptor.id && !!this.earlyActivatedExtensions?.has(descriptor.id)) {
401
// Was activated early
402
suggestedCandidates.push(d);
403
} else if (this.usedDebugTypes.has(d.type)) {
404
// Was used already
405
suggestedCandidates.push(d);
406
} else {
407
otherCandidates.push(d);
408
}
409
});
410
411
const picks: ({ label: string; pick?: () => IGuessedDebugger | Promise<IGuessedDebugger | undefined>; type?: string } | MenuItemAction)[] = [];
412
const dynamic = await this.delegate.configurationManager().getDynamicProviders();
413
if (suggestedCandidates.length > 0) {
414
picks.push(
415
{ type: 'separator', label: nls.localize('suggestedDebuggers', "Suggested") },
416
...suggestedCandidates.map(c => ({ label: c.label, pick: () => ({ debugger: c }) })));
417
}
418
419
if (otherCandidates.length > 0) {
420
if (picks.length > 0) {
421
picks.push({ type: 'separator', label: '' });
422
}
423
424
picks.push(...otherCandidates.map(c => ({ label: c.label, pick: () => ({ debugger: c }) })));
425
}
426
427
if (dynamic.length) {
428
if (picks.length) {
429
picks.push({ type: 'separator', label: '' });
430
}
431
432
for (const d of dynamic) {
433
picks.push({
434
label: nls.localize('moreOptionsForDebugType', "More {0} options...", d.label),
435
pick: async (): Promise<IGuessedDebugger | undefined> => {
436
const cfg = await d.pick();
437
if (!cfg) { return undefined; }
438
return cfg && { debugger: this.getDebugger(d.type)!, withConfig: cfg };
439
},
440
});
441
}
442
}
443
444
picks.push(
445
{ type: 'separator', label: '' },
446
{ label: languageLabel ? nls.localize('installLanguage', "Install an extension for {0}...", languageLabel) : nls.localize('installExt', "Install extension...") }
447
);
448
449
const contributed = this.menuService.getMenuActions(MenuId.DebugCreateConfiguration, this.contextKeyService);
450
for (const [, action] of contributed) {
451
for (const item of action) {
452
picks.push(item);
453
}
454
}
455
456
const placeHolder = nls.localize('selectDebug', "Select debugger");
457
return this.quickInputService.pick<{ label: string; debugger?: Debugger } | IQuickPickItem>(picks, { activeItem: picks[0], placeHolder }).then(async picked => {
458
if (picked && 'pick' in picked && typeof picked.pick === 'function') {
459
return await picked.pick();
460
}
461
462
if (picked instanceof MenuItemAction) {
463
picked.run();
464
return;
465
}
466
467
if (picked) {
468
this.commandService.executeCommand('debug.installAdditionalDebuggers', languageLabel);
469
}
470
471
return undefined;
472
});
473
}
474
475
private initExtensionActivationsIfNeeded(): void {
476
if (!this.earlyActivatedExtensions) {
477
this.earlyActivatedExtensions = new Set<string>();
478
479
const status = this.extensionService.getExtensionsStatus();
480
for (const id in status) {
481
if (!!status[id].activationTimes) {
482
this.earlyActivatedExtensions.add(id);
483
}
484
}
485
}
486
}
487
488
async activateDebuggers(activationEvent: string, debugType?: string): Promise<void> {
489
this.initExtensionActivationsIfNeeded();
490
491
const promises: Promise<any>[] = [
492
this.extensionService.activateByEvent(activationEvent),
493
this.extensionService.activateByEvent('onDebug')
494
];
495
if (debugType) {
496
promises.push(this.extensionService.activateByEvent(`${activationEvent}:${debugType}`));
497
}
498
await Promise.all(promises);
499
}
500
}
501
502