Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.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 { distinct } from '../../../../base/common/arrays.js';
7
import { sequence } from '../../../../base/common/async.js';
8
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
9
import { Emitter, Event } from '../../../../base/common/event.js';
10
import * as json from '../../../../base/common/json.js';
11
import { IJSONSchema } from '../../../../base/common/jsonSchema.js';
12
import { DisposableStore, IDisposable, dispose } from '../../../../base/common/lifecycle.js';
13
import * as resources from '../../../../base/common/resources.js';
14
import { ThemeIcon } from '../../../../base/common/themables.js';
15
import { URI as uri } from '../../../../base/common/uri.js';
16
import * as nls from '../../../../nls.js';
17
import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
18
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
19
import { IFileService } from '../../../../platform/files/common/files.js';
20
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
21
import { IJSONContributionRegistry, Extensions as JSONExtensions } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';
22
import { ILogService } from '../../../../platform/log/common/log.js';
23
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
24
import { Registry } from '../../../../platform/registry/common/platform.js';
25
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
26
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
27
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
28
import { IEditorPane } from '../../../common/editor.js';
29
import { launchSchemaId } from '../../../services/configuration/common/configuration.js';
30
import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';
31
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
32
import { IHistoryService } from '../../../services/history/common/history.js';
33
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
34
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
35
import { CONTEXT_DEBUG_CONFIGURATION_TYPE, DebugConfigurationProviderTriggerKind, IAdapterManager, ICompound, IConfig, IConfigPresentation, IConfigurationManager, IDebugConfigurationProvider, IGlobalConfig, IGuessedDebugger, ILaunch } from '../common/debug.js';
36
import { launchSchema } from '../common/debugSchemas.js';
37
import { getVisibleAndSorted } from '../common/debugUtils.js';
38
import { debugConfigure } from './debugIcons.js';
39
40
const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
41
jsonRegistry.registerSchema(launchSchemaId, launchSchema);
42
43
const DEBUG_SELECTED_CONFIG_NAME_KEY = 'debug.selectedconfigname';
44
const DEBUG_SELECTED_ROOT = 'debug.selectedroot';
45
// Debug type is only stored if a dynamic configuration is used for better restore
46
const DEBUG_SELECTED_TYPE = 'debug.selectedtype';
47
const DEBUG_RECENT_DYNAMIC_CONFIGURATIONS = 'debug.recentdynamicconfigurations';
48
const ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME = 'onDebugDynamicConfigurations';
49
50
interface IDynamicPickItem { label: string; launch: ILaunch; config: IConfig }
51
52
export class ConfigurationManager implements IConfigurationManager {
53
private launches!: ILaunch[];
54
private selectedName: string | undefined;
55
private selectedLaunch: ILaunch | undefined;
56
private getSelectedConfig: () => Promise<IConfig | undefined> = () => Promise.resolve(undefined);
57
private selectedType: string | undefined;
58
private selectedDynamic = false;
59
private toDispose: IDisposable[];
60
private readonly _onDidSelectConfigurationName = new Emitter<void>();
61
private configProviders: IDebugConfigurationProvider[];
62
private debugConfigurationTypeContext: IContextKey<string>;
63
private readonly _onDidChangeConfigurationProviders = new Emitter<void>();
64
public readonly onDidChangeConfigurationProviders = this._onDidChangeConfigurationProviders.event;
65
66
constructor(
67
private readonly adapterManager: IAdapterManager,
68
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
69
@IConfigurationService private readonly configurationService: IConfigurationService,
70
@IQuickInputService private readonly quickInputService: IQuickInputService,
71
@IInstantiationService private readonly instantiationService: IInstantiationService,
72
@IStorageService private readonly storageService: IStorageService,
73
@IExtensionService private readonly extensionService: IExtensionService,
74
@IHistoryService private readonly historyService: IHistoryService,
75
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
76
@IContextKeyService contextKeyService: IContextKeyService,
77
@ILogService private readonly logService: ILogService,
78
) {
79
this.configProviders = [];
80
this.toDispose = [this._onDidChangeConfigurationProviders];
81
this.initLaunches();
82
this.setCompoundSchemaValues();
83
this.registerListeners();
84
const previousSelectedRoot = this.storageService.get(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE);
85
const previousSelectedType = this.storageService.get(DEBUG_SELECTED_TYPE, StorageScope.WORKSPACE);
86
const previousSelectedLaunch = this.launches.find(l => l.uri.toString() === previousSelectedRoot);
87
const previousSelectedName = this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE);
88
this.debugConfigurationTypeContext = CONTEXT_DEBUG_CONFIGURATION_TYPE.bindTo(contextKeyService);
89
const dynamicConfig = previousSelectedType ? { type: previousSelectedType } : undefined;
90
if (previousSelectedLaunch && previousSelectedLaunch.getConfigurationNames().length) {
91
this.selectConfiguration(previousSelectedLaunch, previousSelectedName, undefined, dynamicConfig);
92
} else if (this.launches.length > 0) {
93
this.selectConfiguration(undefined, previousSelectedName, undefined, dynamicConfig);
94
}
95
}
96
97
registerDebugConfigurationProvider(debugConfigurationProvider: IDebugConfigurationProvider): IDisposable {
98
this.configProviders.push(debugConfigurationProvider);
99
this._onDidChangeConfigurationProviders.fire();
100
return {
101
dispose: () => {
102
this.unregisterDebugConfigurationProvider(debugConfigurationProvider);
103
this._onDidChangeConfigurationProviders.fire();
104
}
105
};
106
}
107
108
unregisterDebugConfigurationProvider(debugConfigurationProvider: IDebugConfigurationProvider): void {
109
const ix = this.configProviders.indexOf(debugConfigurationProvider);
110
if (ix >= 0) {
111
this.configProviders.splice(ix, 1);
112
}
113
}
114
115
/**
116
* if scope is not specified,a value of DebugConfigurationProvideTrigger.Initial is assumed.
117
*/
118
hasDebugConfigurationProvider(debugType: string, triggerKind?: DebugConfigurationProviderTriggerKind): boolean {
119
if (triggerKind === undefined) {
120
triggerKind = DebugConfigurationProviderTriggerKind.Initial;
121
}
122
// check if there are providers for the given type that contribute a provideDebugConfigurations method
123
const provider = this.configProviders.find(p => p.provideDebugConfigurations && (p.type === debugType) && (p.triggerKind === triggerKind));
124
return !!provider;
125
}
126
127
async resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, config: IConfig, token: CancellationToken): Promise<IConfig | null | undefined> {
128
const resolveDebugConfigurationForType = async (type: string | undefined, config: IConfig | null | undefined) => {
129
if (type !== '*') {
130
await this.adapterManager.activateDebuggers('onDebugResolve', type);
131
}
132
133
for (const p of this.configProviders) {
134
if (p.type === type && p.resolveDebugConfiguration && config) {
135
config = await p.resolveDebugConfiguration(folderUri, config, token);
136
}
137
}
138
139
return config;
140
};
141
142
let resolvedType = config.type ?? type;
143
let result: IConfig | null | undefined = config;
144
for (let seen = new Set(); result && !seen.has(resolvedType);) {
145
seen.add(resolvedType);
146
result = await resolveDebugConfigurationForType(resolvedType, result);
147
result = await resolveDebugConfigurationForType('*', result);
148
resolvedType = result?.type ?? type!;
149
}
150
151
return result;
152
}
153
154
async resolveDebugConfigurationWithSubstitutedVariables(folderUri: uri | undefined, type: string | undefined, config: IConfig, token: CancellationToken): Promise<IConfig | null | undefined> {
155
// pipe the config through the promises sequentially. Append at the end the '*' types
156
const providers = this.configProviders.filter(p => p.type === type && p.resolveDebugConfigurationWithSubstitutedVariables)
157
.concat(this.configProviders.filter(p => p.type === '*' && p.resolveDebugConfigurationWithSubstitutedVariables));
158
159
let result: IConfig | null | undefined = config;
160
await sequence(providers.map(provider => async () => {
161
// If any provider returned undefined or null make sure to respect that and do not pass the result to more resolver
162
if (result) {
163
result = await provider.resolveDebugConfigurationWithSubstitutedVariables!(folderUri, result, token);
164
}
165
}));
166
167
return result;
168
}
169
170
async provideDebugConfigurations(folderUri: uri | undefined, type: string, token: CancellationToken): Promise<any[]> {
171
await this.adapterManager.activateDebuggers('onDebugInitialConfigurations');
172
const results = await Promise.all(this.configProviders.filter(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Initial && p.provideDebugConfigurations).map(p => p.provideDebugConfigurations!(folderUri, token)));
173
174
return results.reduce((first, second) => first.concat(second), []);
175
}
176
177
async getDynamicProviders(): Promise<{ label: string; type: string; getProvider: () => Promise<IDebugConfigurationProvider | undefined>; pick: () => Promise<{ launch: ILaunch; config: IConfig; label: string } | undefined> }[]> {
178
await this.extensionService.whenInstalledExtensionsRegistered();
179
const debugDynamicExtensionsTypes = this.extensionService.extensions.reduce((acc, e) => {
180
if (!e.activationEvents) {
181
return acc;
182
}
183
184
const explicitTypes: string[] = [];
185
let hasGenericEvent = false;
186
for (const event of e.activationEvents) {
187
if (event === ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME) {
188
hasGenericEvent = true;
189
} else if (event.startsWith(`${ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME}:`)) {
190
explicitTypes.push(event.slice(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME.length + 1));
191
}
192
}
193
194
if (explicitTypes.length) {
195
explicitTypes.forEach(t => acc.add(t));
196
} else if (hasGenericEvent) {
197
const debuggerType = e.contributes?.debuggers?.[0].type;
198
if (debuggerType) {
199
acc.add(debuggerType);
200
}
201
}
202
203
return acc;
204
}, new Set<string>());
205
206
for (const configProvider of this.configProviders) {
207
if (configProvider.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic) {
208
debugDynamicExtensionsTypes.add(configProvider.type);
209
}
210
}
211
212
return [...debugDynamicExtensionsTypes].map(type => {
213
return {
214
label: this.adapterManager.getDebuggerLabel(type)!,
215
getProvider: async () => {
216
await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type);
217
return this.configProviders.find(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations);
218
},
219
type,
220
pick: async () => {
221
// Do a late 'onDebugDynamicConfigurationsName' activation so extensions are not activated too early #108578
222
await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type);
223
224
const disposables = new DisposableStore();
225
const token = new CancellationTokenSource();
226
disposables.add(token);
227
const input = disposables.add(this.quickInputService.createQuickPick<IDynamicPickItem>());
228
input.busy = true;
229
input.placeholder = nls.localize('selectConfiguration', "Select Launch Configuration");
230
231
const chosenPromise = new Promise<IDynamicPickItem | undefined>(resolve => {
232
disposables.add(input.onDidAccept(() => resolve(input.activeItems[0])));
233
disposables.add(input.onDidTriggerItemButton(async (context) => {
234
resolve(undefined);
235
const { launch, config } = context.item;
236
await launch.openConfigFile({ preserveFocus: false, type: config.type, suppressInitialConfigs: true });
237
// Only Launch have a pin trigger button
238
await (launch as Launch).writeConfiguration(config);
239
await this.selectConfiguration(launch, config.name);
240
this.removeRecentDynamicConfigurations(config.name, config.type);
241
}));
242
disposables.add(input.onDidHide(() => resolve(undefined)));
243
}).finally(() => token.cancel());
244
245
let items: IDynamicPickItem[];
246
try {
247
// This await invokes the extension providers, which might fail due to several reasons,
248
// therefore we gate this logic under a try/catch to prevent leaving the Debug Tab
249
// selector in a borked state.
250
items = await this.getDynamicConfigurationsByType(type, token.token);
251
} catch (err) {
252
this.logService.error(err);
253
disposables.dispose();
254
return;
255
}
256
257
input.items = items;
258
input.busy = false;
259
input.show();
260
const chosen = await chosenPromise;
261
disposables.dispose();
262
263
return chosen;
264
}
265
};
266
});
267
}
268
269
async getDynamicConfigurationsByType(type: string, token: CancellationToken = CancellationToken.None): Promise<IDynamicPickItem[]> {
270
// Do a late 'onDebugDynamicConfigurationsName' activation so extensions are not activated too early #108578
271
await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type);
272
273
const picks: Promise<IDynamicPickItem[]>[] = [];
274
const provider = this.configProviders.find(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations);
275
this.getLaunches().forEach(launch => {
276
if (provider) {
277
picks.push(provider.provideDebugConfigurations!(launch.workspace?.uri, token).then(configurations => configurations.map(config => ({
278
label: config.name,
279
description: launch.name,
280
config,
281
buttons: [{
282
iconClass: ThemeIcon.asClassName(debugConfigure),
283
tooltip: nls.localize('editLaunchConfig', "Edit Debug Configuration in launch.json")
284
}],
285
launch
286
}))));
287
}
288
});
289
290
return (await Promise.all(picks)).flat();
291
}
292
293
getAllConfigurations(): { launch: ILaunch; name: string; presentation?: IConfigPresentation }[] {
294
const all: { launch: ILaunch; name: string; presentation?: IConfigPresentation }[] = [];
295
for (const l of this.launches) {
296
for (const name of l.getConfigurationNames()) {
297
const config = l.getConfiguration(name) || l.getCompound(name);
298
if (config) {
299
all.push({ launch: l, name, presentation: config.presentation });
300
}
301
}
302
}
303
304
return getVisibleAndSorted(all);
305
}
306
307
removeRecentDynamicConfigurations(name: string, type: string) {
308
const remaining = this.getRecentDynamicConfigurations().filter(c => c.name !== name || c.type !== type);
309
this.storageService.store(DEBUG_RECENT_DYNAMIC_CONFIGURATIONS, JSON.stringify(remaining), StorageScope.WORKSPACE, StorageTarget.MACHINE);
310
if (this.selectedConfiguration.name === name && this.selectedType === type && this.selectedDynamic) {
311
this.selectConfiguration(undefined, undefined);
312
} else {
313
this._onDidSelectConfigurationName.fire();
314
}
315
}
316
317
getRecentDynamicConfigurations(): { name: string; type: string }[] {
318
return JSON.parse(this.storageService.get(DEBUG_RECENT_DYNAMIC_CONFIGURATIONS, StorageScope.WORKSPACE, '[]'));
319
}
320
321
private registerListeners(): void {
322
this.toDispose.push(Event.any<IWorkspaceFoldersChangeEvent | WorkbenchState>(this.contextService.onDidChangeWorkspaceFolders, this.contextService.onDidChangeWorkbenchState)(() => {
323
this.initLaunches();
324
this.selectConfiguration(undefined);
325
this.setCompoundSchemaValues();
326
}));
327
this.toDispose.push(this.configurationService.onDidChangeConfiguration(async e => {
328
if (e.affectsConfiguration('launch')) {
329
// A change happen in the launch.json. If there is already a launch configuration selected, do not change the selection.
330
await this.selectConfiguration(undefined);
331
this.setCompoundSchemaValues();
332
}
333
}));
334
this.toDispose.push(this.adapterManager.onDidDebuggersExtPointRead(() => {
335
this.setCompoundSchemaValues();
336
}));
337
}
338
339
private initLaunches(): void {
340
this.launches = this.contextService.getWorkspace().folders.map(folder => this.instantiationService.createInstance(Launch, this, this.adapterManager, folder));
341
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
342
this.launches.push(this.instantiationService.createInstance(WorkspaceLaunch, this, this.adapterManager));
343
}
344
this.launches.push(this.instantiationService.createInstance(UserLaunch, this, this.adapterManager));
345
346
if (this.selectedLaunch && this.launches.indexOf(this.selectedLaunch) === -1) {
347
this.selectConfiguration(undefined);
348
}
349
}
350
351
private setCompoundSchemaValues(): void {
352
const compoundConfigurationsSchema = (<IJSONSchema>launchSchema.properties!['compounds'].items).properties!['configurations'];
353
const launchNames = this.launches.map(l =>
354
l.getConfigurationNames(true)).reduce((first, second) => first.concat(second), []);
355
(<IJSONSchema>compoundConfigurationsSchema.items).oneOf![0].enum = launchNames;
356
(<IJSONSchema>compoundConfigurationsSchema.items).oneOf![1].properties!.name.enum = launchNames;
357
358
const folderNames = this.contextService.getWorkspace().folders.map(f => f.name);
359
(<IJSONSchema>compoundConfigurationsSchema.items).oneOf![1].properties!.folder.enum = folderNames;
360
361
jsonRegistry.registerSchema(launchSchemaId, launchSchema);
362
}
363
364
getLaunches(): ILaunch[] {
365
return this.launches;
366
}
367
368
getLaunch(workspaceUri: uri | undefined): ILaunch | undefined {
369
if (!uri.isUri(workspaceUri)) {
370
return undefined;
371
}
372
373
return this.launches.find(l => l.workspace && this.uriIdentityService.extUri.isEqual(l.workspace.uri, workspaceUri));
374
}
375
376
get selectedConfiguration(): { launch: ILaunch | undefined; name: string | undefined; getConfig: () => Promise<IConfig | undefined>; type: string | undefined } {
377
return {
378
launch: this.selectedLaunch,
379
name: this.selectedName,
380
getConfig: this.getSelectedConfig,
381
type: this.selectedType
382
};
383
}
384
385
get onDidSelectConfiguration(): Event<void> {
386
return this._onDidSelectConfigurationName.event;
387
}
388
389
getWorkspaceLaunch(): ILaunch | undefined {
390
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
391
return this.launches[this.launches.length - 1];
392
}
393
394
return undefined;
395
}
396
397
async selectConfiguration(launch: ILaunch | undefined, name?: string, config?: IConfig, dynamicConfig?: { type?: string }): Promise<void> {
398
if (typeof launch === 'undefined') {
399
const rootUri = this.historyService.getLastActiveWorkspaceRoot();
400
launch = this.getLaunch(rootUri);
401
if (!launch || launch.getConfigurationNames().length === 0) {
402
launch = this.launches.find(l => !!(l && l.getConfigurationNames().length)) || launch || this.launches[0];
403
}
404
}
405
406
const previousLaunch = this.selectedLaunch;
407
const previousName = this.selectedName;
408
const previousSelectedDynamic = this.selectedDynamic;
409
this.selectedLaunch = launch;
410
411
if (this.selectedLaunch) {
412
this.storageService.store(DEBUG_SELECTED_ROOT, this.selectedLaunch.uri.toString(), StorageScope.WORKSPACE, StorageTarget.MACHINE);
413
} else {
414
this.storageService.remove(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE);
415
}
416
417
const names = launch ? launch.getConfigurationNames() : [];
418
this.getSelectedConfig = () => {
419
const selected = this.selectedName ? launch?.getConfiguration(this.selectedName) : undefined;
420
return Promise.resolve(selected || config);
421
};
422
423
let type = config?.type;
424
if (name && names.indexOf(name) >= 0) {
425
this.setSelectedLaunchName(name);
426
} else if (dynamicConfig && dynamicConfig.type) {
427
// We could not find the previously used name and config is not passed. We should get all dynamic configurations from providers
428
// And potentially auto select the previously used dynamic configuration #96293
429
type = dynamicConfig.type;
430
if (!config) {
431
const providers = (await this.getDynamicProviders()).filter(p => p.type === type);
432
this.getSelectedConfig = async () => {
433
const activatedProviders = await Promise.all(providers.map(p => p.getProvider()));
434
const provider = activatedProviders.length > 0 ? activatedProviders[0] : undefined;
435
if (provider && launch && launch.workspace) {
436
const token = new CancellationTokenSource();
437
const dynamicConfigs = await provider.provideDebugConfigurations!(launch.workspace.uri, token.token);
438
const dynamicConfig = dynamicConfigs.find(c => c.name === name);
439
if (dynamicConfig) {
440
return dynamicConfig;
441
}
442
}
443
444
return undefined;
445
};
446
}
447
this.setSelectedLaunchName(name);
448
449
let recentDynamicProviders = this.getRecentDynamicConfigurations();
450
if (name && dynamicConfig.type) {
451
// We need to store the recently used dynamic configurations to be able to show them in UI #110009
452
recentDynamicProviders.unshift({ name, type: dynamicConfig.type });
453
recentDynamicProviders = distinct(recentDynamicProviders, t => `${t.name} : ${t.type}`);
454
this.storageService.store(DEBUG_RECENT_DYNAMIC_CONFIGURATIONS, JSON.stringify(recentDynamicProviders), StorageScope.WORKSPACE, StorageTarget.MACHINE);
455
}
456
} else if (!this.selectedName || names.indexOf(this.selectedName) === -1) {
457
// We could not find the configuration to select, pick the first one, or reset the selection if there is no launch configuration
458
const nameToSet = names.length ? names[0] : undefined;
459
this.setSelectedLaunchName(nameToSet);
460
}
461
462
if (!config && launch && this.selectedName) {
463
config = launch.getConfiguration(this.selectedName);
464
type = config?.type;
465
}
466
467
this.selectedType = dynamicConfig?.type || config?.type;
468
this.selectedDynamic = !!dynamicConfig;
469
// Only store the selected type if we are having a dynamic configuration. Otherwise restoring this configuration from storage might be misindentified as a dynamic configuration
470
this.storageService.store(DEBUG_SELECTED_TYPE, dynamicConfig ? this.selectedType : undefined, StorageScope.WORKSPACE, StorageTarget.MACHINE);
471
472
if (type) {
473
this.debugConfigurationTypeContext.set(type);
474
} else {
475
this.debugConfigurationTypeContext.reset();
476
}
477
478
if (this.selectedLaunch !== previousLaunch || this.selectedName !== previousName || previousSelectedDynamic !== this.selectedDynamic) {
479
this._onDidSelectConfigurationName.fire();
480
}
481
}
482
483
private setSelectedLaunchName(selectedName: string | undefined): void {
484
this.selectedName = selectedName;
485
486
if (this.selectedName) {
487
this.storageService.store(DEBUG_SELECTED_CONFIG_NAME_KEY, this.selectedName, StorageScope.WORKSPACE, StorageTarget.MACHINE);
488
} else {
489
this.storageService.remove(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE);
490
}
491
}
492
493
dispose(): void {
494
this.toDispose = dispose(this.toDispose);
495
}
496
}
497
498
abstract class AbstractLaunch implements ILaunch {
499
abstract readonly uri: uri;
500
abstract readonly name: string;
501
abstract readonly workspace: IWorkspaceFolder | undefined;
502
protected abstract getConfig(): IGlobalConfig | undefined;
503
abstract openConfigFile(options: { preserveFocus: boolean; type?: string | undefined; suppressInitialConfigs?: boolean | undefined }, token?: CancellationToken | undefined): Promise<{ editor: IEditorPane | null; created: boolean }>;
504
505
constructor(
506
protected configurationManager: ConfigurationManager,
507
private readonly adapterManager: IAdapterManager
508
) { }
509
510
getCompound(name: string): ICompound | undefined {
511
const config = this.getDeduplicatedConfig();
512
if (!config || !config.compounds) {
513
return undefined;
514
}
515
516
return config.compounds.find(compound => compound.name === name);
517
}
518
519
getConfigurationNames(ignoreCompoundsAndPresentation = false): string[] {
520
const config = this.getDeduplicatedConfig();
521
if (!config || (!Array.isArray(config.configurations) && !Array.isArray(config.compounds))) {
522
return [];
523
} else {
524
const configurations: (IConfig | ICompound)[] = [];
525
if (config.configurations) {
526
configurations.push(...config.configurations.filter(cfg => cfg && typeof cfg.name === 'string'));
527
}
528
529
if (ignoreCompoundsAndPresentation) {
530
return configurations.map(c => c.name);
531
}
532
533
if (config.compounds) {
534
configurations.push(...config.compounds.filter(compound => typeof compound.name === 'string' && compound.configurations && compound.configurations.length));
535
}
536
return getVisibleAndSorted(configurations).map(c => c.name);
537
}
538
}
539
540
getConfiguration(name: string): IConfig | undefined {
541
// We need to clone the configuration in order to be able to make changes to it #42198
542
const config = this.getDeduplicatedConfig();
543
if (!config || !config.configurations) {
544
return undefined;
545
}
546
const configuration = config.configurations.find(config => config && config.name === name);
547
if (!configuration) {
548
return;
549
}
550
551
if (this instanceof UserLaunch) {
552
return { ...configuration, __configurationTarget: ConfigurationTarget.USER };
553
} else if (this instanceof WorkspaceLaunch) {
554
return { ...configuration, __configurationTarget: ConfigurationTarget.WORKSPACE };
555
} else {
556
return { ...configuration, __configurationTarget: ConfigurationTarget.WORKSPACE_FOLDER };
557
}
558
}
559
560
async getInitialConfigurationContent(folderUri?: uri, type?: string, useInitialConfigs?: boolean, token?: CancellationToken): Promise<string> {
561
let content = '';
562
const adapter: Partial<IGuessedDebugger> | undefined = type
563
? { debugger: this.adapterManager.getEnabledDebugger(type) }
564
: await this.adapterManager.guessDebugger(true);
565
566
if (adapter?.withConfig && adapter.debugger) {
567
content = await adapter.debugger.getInitialConfigurationContent([adapter.withConfig.config]);
568
} else if (adapter?.debugger) {
569
const initialConfigs = useInitialConfigs ?
570
await this.configurationManager.provideDebugConfigurations(folderUri, adapter.debugger.type, token || CancellationToken.None) :
571
[];
572
content = await adapter.debugger.getInitialConfigurationContent(initialConfigs);
573
}
574
575
return content;
576
}
577
578
579
get hidden(): boolean {
580
return false;
581
}
582
583
private getDeduplicatedConfig(): IGlobalConfig | undefined {
584
const original = this.getConfig();
585
return original && {
586
version: original.version,
587
compounds: original.compounds && distinguishConfigsByName(original.compounds),
588
configurations: original.configurations && distinguishConfigsByName(original.configurations),
589
};
590
}
591
}
592
593
function distinguishConfigsByName<T extends { name: string }>(things: readonly T[]): T[] {
594
const seen = new Map<string, number>();
595
return things.map(thing => {
596
const no = seen.get(thing.name) || 0;
597
seen.set(thing.name, no + 1);
598
return no === 0 ? thing : { ...thing, name: `${thing.name} (${no})` };
599
});
600
}
601
602
class Launch extends AbstractLaunch implements ILaunch {
603
604
constructor(
605
configurationManager: ConfigurationManager,
606
adapterManager: IAdapterManager,
607
public workspace: IWorkspaceFolder,
608
@IFileService private readonly fileService: IFileService,
609
@ITextFileService private readonly textFileService: ITextFileService,
610
@IEditorService private readonly editorService: IEditorService,
611
@IConfigurationService private readonly configurationService: IConfigurationService
612
) {
613
super(configurationManager, adapterManager);
614
}
615
616
get uri(): uri {
617
return resources.joinPath(this.workspace.uri, '/.vscode/launch.json');
618
}
619
620
get name(): string {
621
return this.workspace.name;
622
}
623
624
protected getConfig(): IGlobalConfig | undefined {
625
return this.configurationService.inspect<IGlobalConfig>('launch', { resource: this.workspace.uri }).workspaceFolderValue;
626
}
627
628
async openConfigFile({ preserveFocus, type, suppressInitialConfigs }: { preserveFocus: boolean; type?: string; suppressInitialConfigs?: boolean }, token?: CancellationToken): Promise<{ editor: IEditorPane | null; created: boolean }> {
629
const resource = this.uri;
630
let created = false;
631
let content = '';
632
try {
633
const fileContent = await this.fileService.readFile(resource);
634
content = fileContent.value.toString();
635
} catch {
636
// launch.json not found: create one by collecting launch configs from debugConfigProviders
637
content = await this.getInitialConfigurationContent(this.workspace.uri, type, !suppressInitialConfigs, token);
638
if (!content) {
639
// Cancelled
640
return { editor: null, created: false };
641
}
642
643
created = true; // pin only if config file is created #8727
644
try {
645
await this.textFileService.write(resource, content);
646
} catch (error) {
647
throw new Error(nls.localize('DebugConfig.failed', "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", error.message));
648
}
649
}
650
651
const index = content.indexOf(`"${this.configurationManager.selectedConfiguration.name}"`);
652
let startLineNumber = 1;
653
for (let i = 0; i < index; i++) {
654
if (content.charAt(i) === '\n') {
655
startLineNumber++;
656
}
657
}
658
const selection = startLineNumber > 1 ? { startLineNumber, startColumn: 4 } : undefined;
659
660
const editor = await this.editorService.openEditor({
661
resource,
662
options: {
663
selection,
664
preserveFocus,
665
pinned: created,
666
revealIfVisible: true
667
},
668
}, ACTIVE_GROUP);
669
670
return ({
671
editor: editor ?? null,
672
created
673
});
674
}
675
676
async writeConfiguration(configuration: IConfig): Promise<void> {
677
// note: we don't get the deduplicated config since we don't want that to 'leak' into the file
678
const fullConfig: Partial<IGlobalConfig> = { ...(this.getConfig() ?? {}) };
679
fullConfig.configurations = [...fullConfig.configurations || [], configuration];
680
await this.configurationService.updateValue('launch', fullConfig, { resource: this.workspace.uri }, ConfigurationTarget.WORKSPACE_FOLDER);
681
}
682
}
683
684
class WorkspaceLaunch extends AbstractLaunch implements ILaunch {
685
constructor(
686
configurationManager: ConfigurationManager,
687
adapterManager: IAdapterManager,
688
@IEditorService private readonly editorService: IEditorService,
689
@IConfigurationService private readonly configurationService: IConfigurationService,
690
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
691
) {
692
super(configurationManager, adapterManager);
693
}
694
695
get workspace(): undefined {
696
return undefined;
697
}
698
699
get uri(): uri {
700
return this.contextService.getWorkspace().configuration!;
701
}
702
703
get name(): string {
704
return nls.localize('workspace', "workspace");
705
}
706
707
protected getConfig(): IGlobalConfig | undefined {
708
return this.configurationService.inspect<IGlobalConfig>('launch').workspaceValue;
709
}
710
711
async openConfigFile({ preserveFocus, type, useInitialConfigs }: { preserveFocus: boolean; type?: string; useInitialConfigs?: boolean }, token?: CancellationToken): Promise<{ editor: IEditorPane | null; created: boolean }> {
712
const launchExistInFile = !!this.getConfig();
713
if (!launchExistInFile) {
714
// Launch property in workspace config not found: create one by collecting launch configs from debugConfigProviders
715
const content = await this.getInitialConfigurationContent(undefined, type, useInitialConfigs, token);
716
if (content) {
717
await this.configurationService.updateValue('launch', json.parse(content), ConfigurationTarget.WORKSPACE);
718
} else {
719
return { editor: null, created: false };
720
}
721
}
722
723
const editor = await this.editorService.openEditor({
724
resource: this.contextService.getWorkspace().configuration!,
725
options: { preserveFocus }
726
}, ACTIVE_GROUP);
727
728
return ({
729
editor: editor ?? null,
730
created: false
731
});
732
}
733
}
734
735
class UserLaunch extends AbstractLaunch implements ILaunch {
736
737
constructor(
738
configurationManager: ConfigurationManager,
739
adapterManager: IAdapterManager,
740
@IConfigurationService private readonly configurationService: IConfigurationService,
741
@IPreferencesService private readonly preferencesService: IPreferencesService
742
) {
743
super(configurationManager, adapterManager);
744
}
745
746
get workspace(): undefined {
747
return undefined;
748
}
749
750
get uri(): uri {
751
return this.preferencesService.userSettingsResource;
752
}
753
754
get name(): string {
755
return nls.localize('user settings', "user settings");
756
}
757
758
override get hidden(): boolean {
759
return true;
760
}
761
762
protected getConfig(): IGlobalConfig | undefined {
763
return this.configurationService.inspect<IGlobalConfig>('launch').userValue;
764
}
765
766
async openConfigFile({ preserveFocus, type, useInitialContent }: { preserveFocus: boolean; type?: string; useInitialContent?: boolean }): Promise<{ editor: IEditorPane | null; created: boolean }> {
767
const editor = await this.preferencesService.openUserSettings({ jsonEditor: true, preserveFocus, revealSetting: { key: 'launch' } });
768
return ({
769
editor: editor ?? null,
770
created: false
771
});
772
}
773
}
774
775