Path: blob/main/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { distinct } from '../../../../base/common/arrays.js';6import { sequence } from '../../../../base/common/async.js';7import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';8import { Emitter, Event } from '../../../../base/common/event.js';9import * as json from '../../../../base/common/json.js';10import { IJSONSchema } from '../../../../base/common/jsonSchema.js';11import { DisposableStore, IDisposable, dispose } from '../../../../base/common/lifecycle.js';12import * as resources from '../../../../base/common/resources.js';13import { ThemeIcon } from '../../../../base/common/themables.js';14import { URI as uri } from '../../../../base/common/uri.js';15import * as nls from '../../../../nls.js';16import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js';17import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';18import { IFileService } from '../../../../platform/files/common/files.js';19import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';20import { IJSONContributionRegistry, Extensions as JSONExtensions } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js';21import { ILogService } from '../../../../platform/log/common/log.js';22import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';23import { Registry } from '../../../../platform/registry/common/platform.js';24import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';25import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';26import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';27import { IEditorPane } from '../../../common/editor.js';28import { launchSchemaId } from '../../../services/configuration/common/configuration.js';29import { ACTIVE_GROUP, IEditorService } from '../../../services/editor/common/editorService.js';30import { IExtensionService } from '../../../services/extensions/common/extensions.js';31import { IHistoryService } from '../../../services/history/common/history.js';32import { IPreferencesService } from '../../../services/preferences/common/preferences.js';33import { ITextFileService } from '../../../services/textfile/common/textfiles.js';34import { CONTEXT_DEBUG_CONFIGURATION_TYPE, DebugConfigurationProviderTriggerKind, IAdapterManager, ICompound, IConfig, IConfigPresentation, IConfigurationManager, IDebugConfigurationProvider, IGlobalConfig, IGuessedDebugger, ILaunch } from '../common/debug.js';35import { launchSchema } from '../common/debugSchemas.js';36import { getVisibleAndSorted } from '../common/debugUtils.js';37import { debugConfigure } from './debugIcons.js';3839const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);40jsonRegistry.registerSchema(launchSchemaId, launchSchema);4142const DEBUG_SELECTED_CONFIG_NAME_KEY = 'debug.selectedconfigname';43const DEBUG_SELECTED_ROOT = 'debug.selectedroot';44// Debug type is only stored if a dynamic configuration is used for better restore45const DEBUG_SELECTED_TYPE = 'debug.selectedtype';46const DEBUG_RECENT_DYNAMIC_CONFIGURATIONS = 'debug.recentdynamicconfigurations';47const ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME = 'onDebugDynamicConfigurations';4849interface IDynamicPickItem { label: string; launch: ILaunch; config: IConfig }5051export class ConfigurationManager implements IConfigurationManager {52private launches!: ILaunch[];53private selectedName: string | undefined;54private selectedLaunch: ILaunch | undefined;55private getSelectedConfig: () => Promise<IConfig | undefined> = () => Promise.resolve(undefined);56private selectedType: string | undefined;57private selectedDynamic = false;58private toDispose: IDisposable[];59private readonly _onDidSelectConfigurationName = new Emitter<void>();60private configProviders: IDebugConfigurationProvider[];61private debugConfigurationTypeContext: IContextKey<string>;62private readonly _onDidChangeConfigurationProviders = new Emitter<void>();63public readonly onDidChangeConfigurationProviders = this._onDidChangeConfigurationProviders.event;6465constructor(66private readonly adapterManager: IAdapterManager,67@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,68@IConfigurationService private readonly configurationService: IConfigurationService,69@IQuickInputService private readonly quickInputService: IQuickInputService,70@IInstantiationService private readonly instantiationService: IInstantiationService,71@IStorageService private readonly storageService: IStorageService,72@IExtensionService private readonly extensionService: IExtensionService,73@IHistoryService private readonly historyService: IHistoryService,74@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,75@IContextKeyService contextKeyService: IContextKeyService,76@ILogService private readonly logService: ILogService,77) {78this.configProviders = [];79this.toDispose = [this._onDidChangeConfigurationProviders];80this.initLaunches();81this.setCompoundSchemaValues();82this.registerListeners();83const previousSelectedRoot = this.storageService.get(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE);84const previousSelectedType = this.storageService.get(DEBUG_SELECTED_TYPE, StorageScope.WORKSPACE);85const previousSelectedLaunch = this.launches.find(l => l.uri.toString() === previousSelectedRoot);86const previousSelectedName = this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE);87this.debugConfigurationTypeContext = CONTEXT_DEBUG_CONFIGURATION_TYPE.bindTo(contextKeyService);88const dynamicConfig = previousSelectedType ? { type: previousSelectedType } : undefined;89if (previousSelectedLaunch && previousSelectedLaunch.getConfigurationNames().length) {90this.selectConfiguration(previousSelectedLaunch, previousSelectedName, undefined, dynamicConfig);91} else if (this.launches.length > 0) {92this.selectConfiguration(undefined, previousSelectedName, undefined, dynamicConfig);93}94}9596registerDebugConfigurationProvider(debugConfigurationProvider: IDebugConfigurationProvider): IDisposable {97this.configProviders.push(debugConfigurationProvider);98this._onDidChangeConfigurationProviders.fire();99return {100dispose: () => {101this.unregisterDebugConfigurationProvider(debugConfigurationProvider);102this._onDidChangeConfigurationProviders.fire();103}104};105}106107unregisterDebugConfigurationProvider(debugConfigurationProvider: IDebugConfigurationProvider): void {108const ix = this.configProviders.indexOf(debugConfigurationProvider);109if (ix >= 0) {110this.configProviders.splice(ix, 1);111}112}113114/**115* if scope is not specified,a value of DebugConfigurationProvideTrigger.Initial is assumed.116*/117hasDebugConfigurationProvider(debugType: string, triggerKind?: DebugConfigurationProviderTriggerKind): boolean {118if (triggerKind === undefined) {119triggerKind = DebugConfigurationProviderTriggerKind.Initial;120}121// check if there are providers for the given type that contribute a provideDebugConfigurations method122const provider = this.configProviders.find(p => p.provideDebugConfigurations && (p.type === debugType) && (p.triggerKind === triggerKind));123return !!provider;124}125126async resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, config: IConfig, token: CancellationToken): Promise<IConfig | null | undefined> {127const resolveDebugConfigurationForType = async (type: string | undefined, config: IConfig | null | undefined) => {128if (type !== '*') {129await this.adapterManager.activateDebuggers('onDebugResolve', type);130}131132for (const p of this.configProviders) {133if (p.type === type && p.resolveDebugConfiguration && config) {134config = await p.resolveDebugConfiguration(folderUri, config, token);135}136}137138return config;139};140141let resolvedType = config.type ?? type;142let result: IConfig | null | undefined = config;143for (let seen = new Set(); result && !seen.has(resolvedType);) {144seen.add(resolvedType);145result = await resolveDebugConfigurationForType(resolvedType, result);146result = await resolveDebugConfigurationForType('*', result);147resolvedType = result?.type ?? type!;148}149150return result;151}152153async resolveDebugConfigurationWithSubstitutedVariables(folderUri: uri | undefined, type: string | undefined, config: IConfig, token: CancellationToken): Promise<IConfig | null | undefined> {154// pipe the config through the promises sequentially. Append at the end the '*' types155const providers = this.configProviders.filter(p => p.type === type && p.resolveDebugConfigurationWithSubstitutedVariables)156.concat(this.configProviders.filter(p => p.type === '*' && p.resolveDebugConfigurationWithSubstitutedVariables));157158let result: IConfig | null | undefined = config;159await sequence(providers.map(provider => async () => {160// If any provider returned undefined or null make sure to respect that and do not pass the result to more resolver161if (result) {162result = await provider.resolveDebugConfigurationWithSubstitutedVariables!(folderUri, result, token);163}164}));165166return result;167}168169async provideDebugConfigurations(folderUri: uri | undefined, type: string, token: CancellationToken): Promise<any[]> {170await this.adapterManager.activateDebuggers('onDebugInitialConfigurations');171const results = await Promise.all(this.configProviders.filter(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Initial && p.provideDebugConfigurations).map(p => p.provideDebugConfigurations!(folderUri, token)));172173return results.reduce((first, second) => first.concat(second), []);174}175176async getDynamicProviders(): Promise<{ label: string; type: string; getProvider: () => Promise<IDebugConfigurationProvider | undefined>; pick: () => Promise<{ launch: ILaunch; config: IConfig; label: string } | undefined> }[]> {177await this.extensionService.whenInstalledExtensionsRegistered();178const debugDynamicExtensionsTypes = this.extensionService.extensions.reduce((acc, e) => {179if (!e.activationEvents) {180return acc;181}182183const explicitTypes: string[] = [];184let hasGenericEvent = false;185for (const event of e.activationEvents) {186if (event === ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME) {187hasGenericEvent = true;188} else if (event.startsWith(`${ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME}:`)) {189explicitTypes.push(event.slice(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME.length + 1));190}191}192193if (explicitTypes.length) {194explicitTypes.forEach(t => acc.add(t));195} else if (hasGenericEvent) {196const debuggerType = e.contributes?.debuggers?.[0].type;197if (debuggerType) {198acc.add(debuggerType);199}200}201202return acc;203}, new Set<string>());204205for (const configProvider of this.configProviders) {206if (configProvider.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic) {207debugDynamicExtensionsTypes.add(configProvider.type);208}209}210211return [...debugDynamicExtensionsTypes].map(type => {212return {213label: this.adapterManager.getDebuggerLabel(type)!,214getProvider: async () => {215await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type);216return this.configProviders.find(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations);217},218type,219pick: async () => {220// Do a late 'onDebugDynamicConfigurationsName' activation so extensions are not activated too early #108578221await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type);222223const disposables = new DisposableStore();224const token = new CancellationTokenSource();225disposables.add(token);226const input = disposables.add(this.quickInputService.createQuickPick<IDynamicPickItem>());227input.busy = true;228input.placeholder = nls.localize('selectConfiguration', "Select Launch Configuration");229230const chosenPromise = new Promise<IDynamicPickItem | undefined>(resolve => {231disposables.add(input.onDidAccept(() => resolve(input.activeItems[0])));232disposables.add(input.onDidTriggerItemButton(async (context) => {233resolve(undefined);234const { launch, config } = context.item;235await launch.openConfigFile({ preserveFocus: false, type: config.type, suppressInitialConfigs: true });236// Only Launch have a pin trigger button237await (launch as Launch).writeConfiguration(config);238await this.selectConfiguration(launch, config.name);239this.removeRecentDynamicConfigurations(config.name, config.type);240}));241disposables.add(input.onDidHide(() => resolve(undefined)));242}).finally(() => token.cancel());243244let items: IDynamicPickItem[];245try {246// This await invokes the extension providers, which might fail due to several reasons,247// therefore we gate this logic under a try/catch to prevent leaving the Debug Tab248// selector in a borked state.249items = await this.getDynamicConfigurationsByType(type, token.token);250} catch (err) {251this.logService.error(err);252disposables.dispose();253return;254}255256input.items = items;257input.busy = false;258input.show();259const chosen = await chosenPromise;260disposables.dispose();261262return chosen;263}264};265});266}267268async getDynamicConfigurationsByType(type: string, token: CancellationToken = CancellationToken.None): Promise<IDynamicPickItem[]> {269// Do a late 'onDebugDynamicConfigurationsName' activation so extensions are not activated too early #108578270await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type);271272const picks: Promise<IDynamicPickItem[]>[] = [];273const provider = this.configProviders.find(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations);274this.getLaunches().forEach(launch => {275if (provider) {276picks.push(provider.provideDebugConfigurations!(launch.workspace?.uri, token).then(configurations => configurations.map(config => ({277label: config.name,278description: launch.name,279config,280buttons: [{281iconClass: ThemeIcon.asClassName(debugConfigure),282tooltip: nls.localize('editLaunchConfig', "Edit Debug Configuration in launch.json")283}],284launch285}))));286}287});288289return (await Promise.all(picks)).flat();290}291292getAllConfigurations(): { launch: ILaunch; name: string; presentation?: IConfigPresentation }[] {293const all: { launch: ILaunch; name: string; presentation?: IConfigPresentation }[] = [];294for (const l of this.launches) {295for (const name of l.getConfigurationNames()) {296const config = l.getConfiguration(name) || l.getCompound(name);297if (config) {298all.push({ launch: l, name, presentation: config.presentation });299}300}301}302303return getVisibleAndSorted(all);304}305306removeRecentDynamicConfigurations(name: string, type: string) {307const remaining = this.getRecentDynamicConfigurations().filter(c => c.name !== name || c.type !== type);308this.storageService.store(DEBUG_RECENT_DYNAMIC_CONFIGURATIONS, JSON.stringify(remaining), StorageScope.WORKSPACE, StorageTarget.MACHINE);309if (this.selectedConfiguration.name === name && this.selectedType === type && this.selectedDynamic) {310this.selectConfiguration(undefined, undefined);311} else {312this._onDidSelectConfigurationName.fire();313}314}315316getRecentDynamicConfigurations(): { name: string; type: string }[] {317return JSON.parse(this.storageService.get(DEBUG_RECENT_DYNAMIC_CONFIGURATIONS, StorageScope.WORKSPACE, '[]'));318}319320private registerListeners(): void {321this.toDispose.push(Event.any<IWorkspaceFoldersChangeEvent | WorkbenchState>(this.contextService.onDidChangeWorkspaceFolders, this.contextService.onDidChangeWorkbenchState)(() => {322this.initLaunches();323this.selectConfiguration(undefined);324this.setCompoundSchemaValues();325}));326this.toDispose.push(this.configurationService.onDidChangeConfiguration(async e => {327if (e.affectsConfiguration('launch')) {328// A change happen in the launch.json. If there is already a launch configuration selected, do not change the selection.329await this.selectConfiguration(undefined);330this.setCompoundSchemaValues();331}332}));333this.toDispose.push(this.adapterManager.onDidDebuggersExtPointRead(() => {334this.setCompoundSchemaValues();335}));336}337338private initLaunches(): void {339this.launches = this.contextService.getWorkspace().folders.map(folder => this.instantiationService.createInstance(Launch, this, this.adapterManager, folder));340if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {341this.launches.push(this.instantiationService.createInstance(WorkspaceLaunch, this, this.adapterManager));342}343this.launches.push(this.instantiationService.createInstance(UserLaunch, this, this.adapterManager));344345if (this.selectedLaunch && this.launches.indexOf(this.selectedLaunch) === -1) {346this.selectConfiguration(undefined);347}348}349350private setCompoundSchemaValues(): void {351const compoundConfigurationsSchema = (<IJSONSchema>launchSchema.properties!['compounds'].items).properties!['configurations'];352const launchNames = this.launches.map(l =>353l.getConfigurationNames(true)).reduce((first, second) => first.concat(second), []);354(<IJSONSchema>compoundConfigurationsSchema.items).oneOf![0].enum = launchNames;355(<IJSONSchema>compoundConfigurationsSchema.items).oneOf![1].properties!.name.enum = launchNames;356357const folderNames = this.contextService.getWorkspace().folders.map(f => f.name);358(<IJSONSchema>compoundConfigurationsSchema.items).oneOf![1].properties!.folder.enum = folderNames;359360jsonRegistry.registerSchema(launchSchemaId, launchSchema);361}362363getLaunches(): ILaunch[] {364return this.launches;365}366367getLaunch(workspaceUri: uri | undefined): ILaunch | undefined {368if (!uri.isUri(workspaceUri)) {369return undefined;370}371372return this.launches.find(l => l.workspace && this.uriIdentityService.extUri.isEqual(l.workspace.uri, workspaceUri));373}374375get selectedConfiguration(): { launch: ILaunch | undefined; name: string | undefined; getConfig: () => Promise<IConfig | undefined>; type: string | undefined } {376return {377launch: this.selectedLaunch,378name: this.selectedName,379getConfig: this.getSelectedConfig,380type: this.selectedType381};382}383384get onDidSelectConfiguration(): Event<void> {385return this._onDidSelectConfigurationName.event;386}387388getWorkspaceLaunch(): ILaunch | undefined {389if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {390return this.launches[this.launches.length - 1];391}392393return undefined;394}395396async selectConfiguration(launch: ILaunch | undefined, name?: string, config?: IConfig, dynamicConfig?: { type?: string }): Promise<void> {397if (typeof launch === 'undefined') {398const rootUri = this.historyService.getLastActiveWorkspaceRoot();399launch = this.getLaunch(rootUri);400if (!launch || launch.getConfigurationNames().length === 0) {401launch = this.launches.find(l => !!(l && l.getConfigurationNames().length)) || launch || this.launches[0];402}403}404405const previousLaunch = this.selectedLaunch;406const previousName = this.selectedName;407const previousSelectedDynamic = this.selectedDynamic;408this.selectedLaunch = launch;409410if (this.selectedLaunch) {411this.storageService.store(DEBUG_SELECTED_ROOT, this.selectedLaunch.uri.toString(), StorageScope.WORKSPACE, StorageTarget.MACHINE);412} else {413this.storageService.remove(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE);414}415416const names = launch ? launch.getConfigurationNames() : [];417this.getSelectedConfig = () => {418const selected = this.selectedName ? launch?.getConfiguration(this.selectedName) : undefined;419return Promise.resolve(selected || config);420};421422let type = config?.type;423if (name && names.indexOf(name) >= 0) {424this.setSelectedLaunchName(name);425} else if (dynamicConfig && dynamicConfig.type) {426// We could not find the previously used name and config is not passed. We should get all dynamic configurations from providers427// And potentially auto select the previously used dynamic configuration #96293428type = dynamicConfig.type;429if (!config) {430const providers = (await this.getDynamicProviders()).filter(p => p.type === type);431this.getSelectedConfig = async () => {432const activatedProviders = await Promise.all(providers.map(p => p.getProvider()));433const provider = activatedProviders.length > 0 ? activatedProviders[0] : undefined;434if (provider && launch && launch.workspace) {435const token = new CancellationTokenSource();436const dynamicConfigs = await provider.provideDebugConfigurations!(launch.workspace.uri, token.token);437const dynamicConfig = dynamicConfigs.find(c => c.name === name);438if (dynamicConfig) {439return dynamicConfig;440}441}442443return undefined;444};445}446this.setSelectedLaunchName(name);447448let recentDynamicProviders = this.getRecentDynamicConfigurations();449if (name && dynamicConfig.type) {450// We need to store the recently used dynamic configurations to be able to show them in UI #110009451recentDynamicProviders.unshift({ name, type: dynamicConfig.type });452recentDynamicProviders = distinct(recentDynamicProviders, t => `${t.name} : ${t.type}`);453this.storageService.store(DEBUG_RECENT_DYNAMIC_CONFIGURATIONS, JSON.stringify(recentDynamicProviders), StorageScope.WORKSPACE, StorageTarget.MACHINE);454}455} else if (!this.selectedName || names.indexOf(this.selectedName) === -1) {456// We could not find the configuration to select, pick the first one, or reset the selection if there is no launch configuration457const nameToSet = names.length ? names[0] : undefined;458this.setSelectedLaunchName(nameToSet);459}460461if (!config && launch && this.selectedName) {462config = launch.getConfiguration(this.selectedName);463type = config?.type;464}465466this.selectedType = dynamicConfig?.type || config?.type;467this.selectedDynamic = !!dynamicConfig;468// Only store the selected type if we are having a dynamic configuration. Otherwise restoring this configuration from storage might be misindentified as a dynamic configuration469this.storageService.store(DEBUG_SELECTED_TYPE, dynamicConfig ? this.selectedType : undefined, StorageScope.WORKSPACE, StorageTarget.MACHINE);470471if (type) {472this.debugConfigurationTypeContext.set(type);473} else {474this.debugConfigurationTypeContext.reset();475}476477if (this.selectedLaunch !== previousLaunch || this.selectedName !== previousName || previousSelectedDynamic !== this.selectedDynamic) {478this._onDidSelectConfigurationName.fire();479}480}481482private setSelectedLaunchName(selectedName: string | undefined): void {483this.selectedName = selectedName;484485if (this.selectedName) {486this.storageService.store(DEBUG_SELECTED_CONFIG_NAME_KEY, this.selectedName, StorageScope.WORKSPACE, StorageTarget.MACHINE);487} else {488this.storageService.remove(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE);489}490}491492dispose(): void {493this.toDispose = dispose(this.toDispose);494}495}496497abstract class AbstractLaunch implements ILaunch {498abstract readonly uri: uri;499abstract readonly name: string;500abstract readonly workspace: IWorkspaceFolder | undefined;501protected abstract getConfig(): IGlobalConfig | undefined;502abstract openConfigFile(options: { preserveFocus: boolean; type?: string | undefined; suppressInitialConfigs?: boolean | undefined }, token?: CancellationToken | undefined): Promise<{ editor: IEditorPane | null; created: boolean }>;503504constructor(505protected configurationManager: ConfigurationManager,506private readonly adapterManager: IAdapterManager507) { }508509getCompound(name: string): ICompound | undefined {510const config = this.getDeduplicatedConfig();511if (!config || !config.compounds) {512return undefined;513}514515return config.compounds.find(compound => compound.name === name);516}517518getConfigurationNames(ignoreCompoundsAndPresentation = false): string[] {519const config = this.getDeduplicatedConfig();520if (!config || (!Array.isArray(config.configurations) && !Array.isArray(config.compounds))) {521return [];522} else {523const configurations: (IConfig | ICompound)[] = [];524if (config.configurations) {525configurations.push(...config.configurations.filter(cfg => cfg && typeof cfg.name === 'string'));526}527528if (ignoreCompoundsAndPresentation) {529return configurations.map(c => c.name);530}531532if (config.compounds) {533configurations.push(...config.compounds.filter(compound => typeof compound.name === 'string' && compound.configurations && compound.configurations.length));534}535return getVisibleAndSorted(configurations).map(c => c.name);536}537}538539getConfiguration(name: string): IConfig | undefined {540// We need to clone the configuration in order to be able to make changes to it #42198541const config = this.getDeduplicatedConfig();542if (!config || !config.configurations) {543return undefined;544}545const configuration = config.configurations.find(config => config && config.name === name);546if (!configuration) {547return;548}549550if (this instanceof UserLaunch) {551return { ...configuration, __configurationTarget: ConfigurationTarget.USER };552} else if (this instanceof WorkspaceLaunch) {553return { ...configuration, __configurationTarget: ConfigurationTarget.WORKSPACE };554} else {555return { ...configuration, __configurationTarget: ConfigurationTarget.WORKSPACE_FOLDER };556}557}558559async getInitialConfigurationContent(folderUri?: uri, type?: string, useInitialConfigs?: boolean, token?: CancellationToken): Promise<string> {560let content = '';561const adapter: Partial<IGuessedDebugger> | undefined = type562? { debugger: this.adapterManager.getEnabledDebugger(type) }563: await this.adapterManager.guessDebugger(true);564565if (adapter?.withConfig && adapter.debugger) {566content = await adapter.debugger.getInitialConfigurationContent([adapter.withConfig.config]);567} else if (adapter?.debugger) {568const initialConfigs = useInitialConfigs ?569await this.configurationManager.provideDebugConfigurations(folderUri, adapter.debugger.type, token || CancellationToken.None) :570[];571content = await adapter.debugger.getInitialConfigurationContent(initialConfigs);572}573574return content;575}576577578get hidden(): boolean {579return false;580}581582private getDeduplicatedConfig(): IGlobalConfig | undefined {583const original = this.getConfig();584return original && {585version: original.version,586compounds: original.compounds && distinguishConfigsByName(original.compounds),587configurations: original.configurations && distinguishConfigsByName(original.configurations),588};589}590}591592function distinguishConfigsByName<T extends { name: string }>(things: readonly T[]): T[] {593const seen = new Map<string, number>();594return things.map(thing => {595const no = seen.get(thing.name) || 0;596seen.set(thing.name, no + 1);597return no === 0 ? thing : { ...thing, name: `${thing.name} (${no})` };598});599}600601class Launch extends AbstractLaunch implements ILaunch {602603constructor(604configurationManager: ConfigurationManager,605adapterManager: IAdapterManager,606public workspace: IWorkspaceFolder,607@IFileService private readonly fileService: IFileService,608@ITextFileService private readonly textFileService: ITextFileService,609@IEditorService private readonly editorService: IEditorService,610@IConfigurationService private readonly configurationService: IConfigurationService611) {612super(configurationManager, adapterManager);613}614615get uri(): uri {616return resources.joinPath(this.workspace.uri, '/.vscode/launch.json');617}618619get name(): string {620return this.workspace.name;621}622623protected getConfig(): IGlobalConfig | undefined {624return this.configurationService.inspect<IGlobalConfig>('launch', { resource: this.workspace.uri }).workspaceFolderValue;625}626627async openConfigFile({ preserveFocus, type, suppressInitialConfigs }: { preserveFocus: boolean; type?: string; suppressInitialConfigs?: boolean }, token?: CancellationToken): Promise<{ editor: IEditorPane | null; created: boolean }> {628const resource = this.uri;629let created = false;630let content = '';631try {632const fileContent = await this.fileService.readFile(resource);633content = fileContent.value.toString();634} catch {635// launch.json not found: create one by collecting launch configs from debugConfigProviders636content = await this.getInitialConfigurationContent(this.workspace.uri, type, !suppressInitialConfigs, token);637if (!content) {638// Cancelled639return { editor: null, created: false };640}641642created = true; // pin only if config file is created #8727643try {644await this.textFileService.write(resource, content);645} catch (error) {646throw new Error(nls.localize('DebugConfig.failed', "Unable to create 'launch.json' file inside the '.vscode' folder ({0}).", error.message));647}648}649650const index = content.indexOf(`"${this.configurationManager.selectedConfiguration.name}"`);651let startLineNumber = 1;652for (let i = 0; i < index; i++) {653if (content.charAt(i) === '\n') {654startLineNumber++;655}656}657const selection = startLineNumber > 1 ? { startLineNumber, startColumn: 4 } : undefined;658659const editor = await this.editorService.openEditor({660resource,661options: {662selection,663preserveFocus,664pinned: created,665revealIfVisible: true666},667}, ACTIVE_GROUP);668669return ({670editor: editor ?? null,671created672});673}674675async writeConfiguration(configuration: IConfig): Promise<void> {676// note: we don't get the deduplicated config since we don't want that to 'leak' into the file677const fullConfig: Partial<IGlobalConfig> = { ...(this.getConfig() ?? {}) };678fullConfig.configurations = [...fullConfig.configurations || [], configuration];679await this.configurationService.updateValue('launch', fullConfig, { resource: this.workspace.uri }, ConfigurationTarget.WORKSPACE_FOLDER);680}681}682683class WorkspaceLaunch extends AbstractLaunch implements ILaunch {684constructor(685configurationManager: ConfigurationManager,686adapterManager: IAdapterManager,687@IEditorService private readonly editorService: IEditorService,688@IConfigurationService private readonly configurationService: IConfigurationService,689@IWorkspaceContextService private readonly contextService: IWorkspaceContextService690) {691super(configurationManager, adapterManager);692}693694get workspace(): undefined {695return undefined;696}697698get uri(): uri {699return this.contextService.getWorkspace().configuration!;700}701702get name(): string {703return nls.localize('workspace', "workspace");704}705706protected getConfig(): IGlobalConfig | undefined {707return this.configurationService.inspect<IGlobalConfig>('launch').workspaceValue;708}709710async openConfigFile({ preserveFocus, type, useInitialConfigs }: { preserveFocus: boolean; type?: string; useInitialConfigs?: boolean }, token?: CancellationToken): Promise<{ editor: IEditorPane | null; created: boolean }> {711const launchExistInFile = !!this.getConfig();712if (!launchExistInFile) {713// Launch property in workspace config not found: create one by collecting launch configs from debugConfigProviders714const content = await this.getInitialConfigurationContent(undefined, type, useInitialConfigs, token);715if (content) {716await this.configurationService.updateValue('launch', json.parse(content), ConfigurationTarget.WORKSPACE);717} else {718return { editor: null, created: false };719}720}721722const editor = await this.editorService.openEditor({723resource: this.contextService.getWorkspace().configuration!,724options: { preserveFocus }725}, ACTIVE_GROUP);726727return ({728editor: editor ?? null,729created: false730});731}732}733734class UserLaunch extends AbstractLaunch implements ILaunch {735736constructor(737configurationManager: ConfigurationManager,738adapterManager: IAdapterManager,739@IConfigurationService private readonly configurationService: IConfigurationService,740@IPreferencesService private readonly preferencesService: IPreferencesService741) {742super(configurationManager, adapterManager);743}744745get workspace(): undefined {746return undefined;747}748749get uri(): uri {750return this.preferencesService.userSettingsResource;751}752753get name(): string {754return nls.localize('user settings', "user settings");755}756757override get hidden(): boolean {758return true;759}760761protected getConfig(): IGlobalConfig | undefined {762return this.configurationService.inspect<IGlobalConfig>('launch').userValue;763}764765async openConfigFile({ preserveFocus, type, useInitialContent }: { preserveFocus: boolean; type?: string; useInitialContent?: boolean }): Promise<{ editor: IEditorPane | null; created: boolean }> {766const editor = await this.preferencesService.openUserSettings({ jsonEditor: true, preserveFocus, revealSetting: { key: 'launch' } });767return ({768editor: editor ?? null,769created: false770});771}772}773774775