Skip to content

Commit

Permalink
debug: cleanup welcome view actions (#234446)
Browse files Browse the repository at this point in the history
Some consolidation especially now that copilot is also going to be
having a welcome view contribution:

- Show dynamic configurations in "Run and Debug" as "More X options..."
- Remove the separate action for "Show all automatic debug configurations"
- Make the "create a launch.json file" start adding a configuration as
  well (positioning the cursor and triggering completion)
- Make "Run and Debug"'s memoization be able to remember dynamic configs

![](https://memes.peet.io/img/24-11-6171dc57-fd60-4165-bcb6-d156bb0517cc.png)

fyi @roblourens
  • Loading branch information
connor4312 authored Nov 22, 2024
1 parent b35d9d1 commit 7fe5b95
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 126 deletions.
66 changes: 45 additions & 21 deletions src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickin
import { Registry } from '../../../../platform/registry/common/platform.js';
import { IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';
import { Breakpoints } from '../common/breakpoints.js';
import { CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUG_EXTENSION_AVAILABLE, IAdapterDescriptor, IAdapterManager, IConfig, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugAdapterFactory, IDebugConfiguration, IDebugSession, INTERNAL_CONSOLE_OPTIONS_SCHEMA } from '../common/debug.js';
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';
import { Debugger } from '../common/debugger.js';
import { breakpointsExtPoint, debuggersExtPoint, launchSchema, presentationSchema } from '../common/debugSchemas.js';
import { TaskDefinitionRegistry } from '../../tasks/common/taskDefinitionRegistry.js';
Expand All @@ -39,6 +39,7 @@ const jsonRegistry = Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONC

export interface IAdapterManagerDelegate {
onDidNewSession: Event<IDebugSession>;
configurationManager(): IConfigurationManager;
}

export class AdapterManager extends Disposable implements IAdapterManager {
Expand All @@ -60,7 +61,7 @@ export class AdapterManager extends Disposable implements IAdapterManager {
private usedDebugTypes = new Set<string>();

constructor(
delegate: IAdapterManagerDelegate,
private readonly delegate: IAdapterManagerDelegate,
@IEditorService private readonly editorService: IEditorService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
Expand Down Expand Up @@ -340,7 +341,7 @@ export class AdapterManager extends Disposable implements IAdapterManager {
.find(a => a.interestedInLanguage(languageId));
}

async guessDebugger(gettingConfigurations: boolean): Promise<Debugger | undefined> {
async guessDebugger(gettingConfigurations: boolean): Promise<IGuessedDebugger | undefined> {
const activeTextEditorControl = this.editorService.activeTextEditorControl;
let candidates: Debugger[] = [];
let languageLabel: string | null = null;
Expand All @@ -355,7 +356,7 @@ export class AdapterManager extends Disposable implements IAdapterManager {
.filter(a => a.enabled)
.filter(a => language && a.interestedInLanguage(language));
if (adapters.length === 1) {
return adapters[0];
return { debugger: adapters[0] };
}
if (adapters.length > 1) {
candidates = adapters;
Expand Down Expand Up @@ -407,45 +408,68 @@ export class AdapterManager extends Disposable implements IAdapterManager {
}
});

const picks: ({ label: string; debugger?: Debugger; type?: string } | MenuItemAction)[] = [];
const picks: ({ label: string; pick?: () => IGuessedDebugger | Promise<IGuessedDebugger | undefined>; type?: string } | MenuItemAction)[] = [];
const dynamic = await this.delegate.configurationManager().getDynamicProviders();
if (suggestedCandidates.length > 0) {
picks.push(
{ type: 'separator', label: nls.localize('suggestedDebuggers', "Suggested") },
...suggestedCandidates.map(c => ({ label: c.label, debugger: c })));
...suggestedCandidates.map(c => ({ label: c.label, pick: () => ({ debugger: c }) })));
}

if (otherCandidates.length > 0) {
if (picks.length > 0) {
picks.push({ type: 'separator', label: '' });
}

picks.push(...otherCandidates.map(c => ({ label: c.label, debugger: c })));
picks.push(...otherCandidates.map(c => ({ label: c.label, pick: () => ({ debugger: c }) })));
}

if (dynamic.length) {
if (picks.length) {
picks.push({ type: 'separator', label: '' });
}

for (const d of dynamic) {
picks.push({
label: nls.localize('moreOptionsForDebugType', "More {0} options...", d.label),
pick: async (): Promise<IGuessedDebugger | undefined> => {
const cfg = await d.pick();
if (!cfg) { return undefined; }
return cfg && { debugger: this.getDebugger(d.type)!, withConfig: cfg };
},
});
}
}

picks.push(
{ type: 'separator', label: '' },
{ label: languageLabel ? nls.localize('installLanguage', "Install an extension for {0}...", languageLabel) : nls.localize('installExt', "Install extension...") });
{ label: languageLabel ? nls.localize('installLanguage', "Install an extension for {0}...", languageLabel) : nls.localize('installExt', "Install extension...") }
);

const contributed = this.menuService.getMenuActions(MenuId.DebugCreateConfiguration, this.contextKeyService);
for (const [, action] of contributed) {
for (const item of action) {
picks.push(item);
}
}

const placeHolder = nls.localize('selectDebug', "Select debugger");
return this.quickInputService.pick<{ label: string; debugger?: Debugger } | IQuickPickItem>(picks, { activeItem: picks[0], placeHolder })
.then(async picked => {
if (picked && 'debugger' in picked && picked.debugger) {
return picked.debugger;
} else if (picked instanceof MenuItemAction) {
picked.run();
return;
}
if (picked) {
this.commandService.executeCommand('debug.installAdditionalDebuggers', languageLabel);
}
return undefined;
});
return this.quickInputService.pick<{ label: string; debugger?: Debugger } | IQuickPickItem>(picks, { activeItem: picks[0], placeHolder }).then(async picked => {
if (picked && 'pick' in picked && typeof picked.pick === 'function') {
return await picked.pick();
}

if (picked instanceof MenuItemAction) {
picked.run();
return;
}

if (picked) {
this.commandService.executeCommand('debug.installAdditionalDebuggers', languageLabel);
}

return undefined;
});
}

private initExtensionActivationsIfNeeded(): void {
Expand Down
89 changes: 47 additions & 42 deletions src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri
import { IWorkspaceContextService, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
import { IEditorPane } from '../../../common/editor.js';
import { debugConfigure } from './debugIcons.js';
import { CONTEXT_DEBUG_CONFIGURATION_TYPE, DebugConfigurationProviderTriggerKind, IAdapterManager, ICompound, IConfig, IConfigPresentation, IConfigurationManager, IDebugConfigurationProvider, IGlobalConfig, ILaunch } from '../common/debug.js';
import { CONTEXT_DEBUG_CONFIGURATION_TYPE, DebugConfigurationProviderTriggerKind, IAdapterManager, ICompound, IConfig, IConfigPresentation, IConfigurationManager, IDebugConfigurationProvider, IGlobalConfig, IGuessedDebugger, ILaunch } from '../common/debug.js';
import { launchSchema } from '../common/debugSchemas.js';
import { getVisibleAndSorted } from '../common/debugUtils.js';
import { launchSchemaId } from '../../../services/configuration/common/configuration.js';
Expand All @@ -46,6 +46,7 @@ const DEBUG_SELECTED_ROOT = 'debug.selectedroot';
// Debug type is only stored if a dynamic configuration is used for better restore
const DEBUG_SELECTED_TYPE = 'debug.selectedtype';
const DEBUG_RECENT_DYNAMIC_CONFIGURATIONS = 'debug.recentdynamicconfigurations';
const ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME = 'onDebugDynamicConfigurations';

interface IDynamicPickItem { label: string; launch: ILaunch; config: IConfig }

Expand Down Expand Up @@ -174,9 +175,8 @@ export class ConfigurationManager implements IConfigurationManager {
return results.reduce((first, second) => first.concat(second), []);
}

async getDynamicProviders(): Promise<{ label: string; type: string; getProvider: () => Promise<IDebugConfigurationProvider | undefined>; pick: () => Promise<{ launch: ILaunch; config: IConfig } | undefined> }[]> {
async getDynamicProviders(): Promise<{ label: string; type: string; getProvider: () => Promise<IDebugConfigurationProvider | undefined>; pick: () => Promise<{ launch: ILaunch; config: IConfig; label: string } | undefined> }[]> {
await this.extensionService.whenInstalledExtensionsRegistered();
const onDebugDynamicConfigurationsName = 'onDebugDynamicConfigurations';
const debugDynamicExtensionsTypes = this.extensionService.extensions.reduce((acc, e) => {
if (!e.activationEvents) {
return acc;
Expand All @@ -185,10 +185,10 @@ export class ConfigurationManager implements IConfigurationManager {
const explicitTypes: string[] = [];
let hasGenericEvent = false;
for (const event of e.activationEvents) {
if (event === onDebugDynamicConfigurationsName) {
if (event === ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME) {
hasGenericEvent = true;
} else if (event.startsWith(`${onDebugDynamicConfigurationsName}:`)) {
explicitTypes.push(event.slice(onDebugDynamicConfigurationsName.length + 1));
} else if (event.startsWith(`${ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME}:`)) {
explicitTypes.push(event.slice(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME.length + 1));
}
}

Expand All @@ -214,33 +214,17 @@ export class ConfigurationManager implements IConfigurationManager {
return {
label: this.adapterManager.getDebuggerLabel(type)!,
getProvider: async () => {
await this.adapterManager.activateDebuggers(onDebugDynamicConfigurationsName, type);
await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type);
return this.configProviders.find(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations);
},
type,
pick: async () => {
// Do a late 'onDebugDynamicConfigurationsName' activation so extensions are not activated too early #108578
await this.adapterManager.activateDebuggers(onDebugDynamicConfigurationsName, type);

const token = new CancellationTokenSource();
const picks: Promise<IDynamicPickItem[]>[] = [];
const provider = this.configProviders.find(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations);
this.getLaunches().forEach(launch => {
if (provider) {
picks.push(provider.provideDebugConfigurations!(launch.workspace?.uri, token.token).then(configurations => configurations.map(config => ({
label: config.name,
description: launch.name,
config,
buttons: [{
iconClass: ThemeIcon.asClassName(debugConfigure),
tooltip: nls.localize('editLaunchConfig', "Edit Debug Configuration in launch.json")
}],
launch
}))));
}
});
await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type);

const disposables = new DisposableStore();
const token = new CancellationTokenSource();
disposables.add(token);
const input = disposables.add(this.quickInputService.createQuickPick<IDynamicPickItem>());
input.busy = true;
input.placeholder = nls.localize('selectConfiguration', "Select Launch Configuration");
Expand All @@ -257,41 +241,56 @@ export class ConfigurationManager implements IConfigurationManager {
this.removeRecentDynamicConfigurations(config.name, config.type);
}));
disposables.add(input.onDidHide(() => resolve(undefined)));
});
}).finally(() => token.cancel());

let nestedPicks: IDynamicPickItem[][];
let items: IDynamicPickItem[];
try {
// This await invokes the extension providers, which might fail due to several reasons,
// therefore we gate this logic under a try/catch to prevent leaving the Debug Tab
// selector in a borked state.
nestedPicks = await Promise.all(picks);
items = await this.getDynamicConfigurationsByType(type, token.token);
} catch (err) {
this.logService.error(err);
disposables.dispose();
return;
}

const items = nestedPicks.flat();

input.items = items;
input.busy = false;
input.show();
const chosen = await chosenPromise;

disposables.dispose();

if (!chosen) {
// User canceled quick input we should notify the provider to cancel computing configurations
token.cancel();
return;
}

return chosen;
}
};
});
}

async getDynamicConfigurationsByType(type: string, token: CancellationToken = CancellationToken.None): Promise<IDynamicPickItem[]> {
// Do a late 'onDebugDynamicConfigurationsName' activation so extensions are not activated too early #108578
await this.adapterManager.activateDebuggers(ON_DEBUG_DYNAMIC_CONFIGURATIONS_NAME, type);

const picks: Promise<IDynamicPickItem[]>[] = [];
const provider = this.configProviders.find(p => p.type === type && p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && p.provideDebugConfigurations);
this.getLaunches().forEach(launch => {
if (provider) {
picks.push(provider.provideDebugConfigurations!(launch.workspace?.uri, token).then(configurations => configurations.map(config => ({
label: config.name,
description: launch.name,
config,
buttons: [{
iconClass: ThemeIcon.asClassName(debugConfigure),
tooltip: nls.localize('editLaunchConfig', "Edit Debug Configuration in launch.json")
}],
launch
}))));
}
});

return (await Promise.all(picks)).flat();
}

getAllConfigurations(): { launch: ILaunch; name: string; presentation?: IConfigPresentation }[] {
const all: { launch: ILaunch; name: string; presentation?: IConfigPresentation }[] = [];
for (const l of this.launches) {
Expand Down Expand Up @@ -560,13 +559,19 @@ abstract class AbstractLaunch implements ILaunch {

async getInitialConfigurationContent(folderUri?: uri, type?: string, useInitialConfigs?: boolean, token?: CancellationToken): Promise<string> {
let content = '';
const adapter = type ? this.adapterManager.getEnabledDebugger(type) : await this.adapterManager.guessDebugger(true);
if (adapter) {
const adapter: Partial<IGuessedDebugger> | undefined = type
? { debugger: this.adapterManager.getEnabledDebugger(type) }
: await this.adapterManager.guessDebugger(true);

if (adapter?.withConfig && adapter.debugger) {
content = await adapter.debugger.getInitialConfigurationContent([adapter.withConfig.config]);
} else if (adapter?.debugger) {
const initialConfigs = useInitialConfigs ?
await this.configurationManager.provideDebugConfigurations(folderUri, adapter.type, token || CancellationToken.None) :
await this.configurationManager.provideDebugConfigurations(folderUri, adapter.debugger.type, token || CancellationToken.None) :
[];
content = await adapter.getInitialConfigurationContent(initialConfigs);
content = await adapter.debugger.getInitialConfigurationContent(initialConfigs);
}

return content;
}

Expand Down
Loading

0 comments on commit 7fe5b95

Please sign in to comment.