From e1e200a7bfb9c7b9b87bf63dd8e101e734281222 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez-Leon Date: Mon, 4 Apr 2022 10:32:14 -0400 Subject: [PATCH] [Debug view] Add dynamic debug configurations Following the support for dynamic debug configurations via command Debug: Select and Start debugging. PR #10134 This change extends the functionality further to bring provided dynamic debug configurations to the list of available configurations under the debug view. The user can select these dynamic configurations by first selecting the debug type and a quick pick input box will follow to allow the selection from the available configurations of the selected type. Once a dynamic configuration has been selected it will be added to a short list (limited to 3) of recently used dynamic debug configurations, these entries are available to the user, rendered with the name and suffixed with the debug type in parenthesis. This will facilitate subsequent selection / execution. This change additionally preserves and restores the list of recently selected dynamic debug configurations so they are presented in subsequent sessions. These configurations are refreshed from the providers when: - Configuration providers are registered or unregistered - Focus is gained or lost by the configuration selection box Refreshing these configurations intends to render valid dynamic configurations to the current context. e.g. - Honoring an extension 'when' clause to a file with a specific extension opened in the active editor. However there are situations where the context for execution of a dynamic configuration may no longer be valid e.g. - The configuration is restored from a previous session and the currently selected file is not supported. - The switch of context may not have involved a refresh of dynamic debug configurations. (e.g. switching active editors, etc.) Considering the above, execution of dynamic configurations triggers the fetch of dynamic configurations for the selected provider type, if the configuration is no longer provided, the user will be notified of a missing configuration or not applicable to the current context. Signed-off-by: Alvaro Sanchez-Leon Co-authored-by: Paul Marechal --- CHANGELOG.md | 8 + .../src/browser/widgets/select-component.tsx | 54 ++-- .../console/debug-console-contribution.tsx | 4 +- .../browser/debug-configuration-manager.ts | 127 ++++++++-- .../src/browser/debug-prefix-configuration.ts | 20 +- .../src/browser/debug-session-options.ts | 33 ++- .../editor/debug-breakpoint-widget.tsx | 2 +- .../view/debug-configuration-select.tsx | 234 ++++++++++++++++++ .../view/debug-configuration-widget.tsx | 79 ++---- .../debug/src/common/debug-configuration.ts | 3 - packages/debug/src/common/debug-service.ts | 13 +- packages/debug/src/node/debug-service-impl.ts | 14 +- .../browser/output-toolbar-contribution.tsx | 6 +- .../browser/debug/plugin-debug-service.ts | 35 ++- .../components/preference-select-input.ts | 2 +- 15 files changed, 516 insertions(+), 118 deletions(-) create mode 100644 packages/debug/src/browser/view/debug-configuration-select.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 9594d17639237..7f2a8eb7e0367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ [Breaking Changes:](#breaking_changes_1.27.0) - [plugin-dev] moved and renamed interface from: `@theia/debug/lib/browser/debug-contribution/DebugPluginConfiguration` to: `plugin-dev/src/common/PluginDebugConfiguration` [#11224](https://github.com/eclipse-theia/theia/pull/11224) +- [debug, plugin-ext] [Debug view] Add dynamic debug configurations [#10212](https://github.com/eclipse-theia/theia/pull/10212) + - Changed signature of `DebugConfigurationManager.find` to receive a target DebugConfiguration instead of a configuration's name. + NOTE: The original signature is still available but no longer used inside the framework and therefore marked as `deprecated` + - Multiple methods related to the selection of Debug configuration options were relocated from `debug-configuration-widget.tsx` to the new file `debug-configuration-select.tsx`. + - Removed optional interface property `DebugConfiguration.dynamic`. + - Added the following method to the interface `DebugService`: `fetchDynamicDebugConfiguration` as well as the property `onDidChangedDebugConfigurationProviders`. + - Removed method `DebugPrefixConfiguration#runDynamicConfiguration` + - [core] The interface `SelectComponentProps` was updated to rename a property from `value` to `defaultValue` ## v1.26.0 - 5/26/2022 diff --git a/packages/core/src/browser/widgets/select-component.tsx b/packages/core/src/browser/widgets/select-component.tsx index 14fa649af6c3d..9075bbde425aa 100644 --- a/packages/core/src/browser/widgets/select-component.tsx +++ b/packages/core/src/browser/widgets/select-component.tsx @@ -34,8 +34,10 @@ export interface SelectOption { export interface SelectComponentProps { options: SelectOption[] - value?: string | number - onChange?: (option: SelectOption, index: number) => void + defaultValue?: string | number + onChange?: (option: SelectOption, index: number) => void, + onBlur?: () => void, + onFocus?: () => void } export interface SelectComponentDropdownDimensions { @@ -64,10 +66,10 @@ export class SelectComponent extends React.Component e.value === props.value), 0); + if (typeof props.defaultValue === 'number') { + selected = props.defaultValue; + } else if (typeof props.defaultValue === 'string') { + selected = Math.max(props.options.findIndex(e => e.value === props.defaultValue), 0); } this.state = { selected, @@ -162,8 +164,8 @@ export class SelectComponent extends React.Component @@ -173,7 +175,13 @@ export class SelectComponent extends React.Component this.handleClickEvent(e)} - onBlur={() => this.hide()} + onBlur={ + () => { + this.hide(); + this.props.onBlur?.(); + } + } + onFocus={() => this.props.onFocus?.()} onKeyDown={e => this.handleKeypress(e)} >
{selectedItemLabel}
@@ -183,24 +191,36 @@ export class SelectComponent extends React.Component; } + protected nextNotSeparator(direction: 'forwards' | 'backwards'): number { + const { options } = this.props; + const step = direction === 'forwards' ? 1 : -1; + const length = this.props.options.length; + let selected = this.state.selected; + let count = 0; + do { + selected = (selected + step) % length; + if (selected < 0) { + selected = length - 1; + } + count++; + } + while (options[selected]?.separator && count < length); + return selected; + } + protected handleKeypress(ev: React.KeyboardEvent): void { if (!this.fieldRef.current) { return; } if (ev.key === 'ArrowUp') { - let selected = this.state.selected; - if (selected <= 0) { - selected = this.props.options.length - 1; - } else { - selected--; - } + const selected = this.nextNotSeparator('backwards'); this.setState({ selected, hover: selected }); } else if (ev.key === 'ArrowDown') { if (this.state.dimensions) { - const selected = (this.state.selected + 1) % this.props.options.length; + const selected = this.nextNotSeparator('forwards'); this.setState({ selected, hover: selected @@ -208,8 +228,8 @@ export class SelectComponent extends React.Component; } @@ -207,7 +207,7 @@ export class DebugConsoleContribution extends AbstractViewContribution; } diff --git a/packages/debug/src/browser/debug-configuration-manager.ts b/packages/debug/src/browser/debug-configuration-manager.ts index fa2e013aa2990..2c804fe55546d 100644 --- a/packages/debug/src/browser/debug-configuration-manager.ts +++ b/packages/debug/src/browser/debug-configuration-manager.ts @@ -86,10 +86,16 @@ export class DebugConfigurationManager { return this.onWillProvideDynamicDebugConfigurationEmitter.event; } + get onDidChangeConfigurationProviders(): Event { + return this.debug.onDidChangeDebugConfigurationProviders; + } + protected debugConfigurationTypeKey: ContextKey; protected initialized: Promise; + protected recentDynamicOptionsTracker: DebugSessionOptions[] = []; + @postConstruct() protected async init(): Promise { this.debugConfigurationTypeKey = this.contextKeyService.createKey('debugConfigurationType', undefined); @@ -126,6 +132,9 @@ export class DebugConfigurationManager { this.updateCurrent(); }, 500); + /** + * All _non-dynamic_ debug configurations. + */ get all(): IterableIterator { return this.getAll(); } @@ -160,11 +169,61 @@ export class DebugConfigurationManager { get current(): DebugSessionOptions | undefined { return this._currentOptions; } + + async getSelectedConfiguration(): Promise { + // providerType applies to dynamic configurations only + if (!this._currentOptions?.providerType) { + return this._currentOptions; + } + + // Refresh a dynamic configuration from the provider. + // This allow providers to update properties before the execution e.g. program + const { providerType, configuration: { name } } = this._currentOptions; + const configuration = await this.fetchDynamicDebugConfiguration(name, providerType); + + if (!configuration) { + const message = nls.localize( + 'theia/debug/missingConfiguration', + "Dynamic configuration '{0}:{1}' is missing or not applicable", providerType, name); + throw new Error(message); + } + + return { configuration, providerType }; + } + set current(option: DebugSessionOptions | undefined) { this.updateCurrent(option); + this.updateRecentlyUsedDynamicConfigurationOptions(option); + } + + protected updateRecentlyUsedDynamicConfigurationOptions(option: DebugSessionOptions | undefined): void { + if (option?.providerType) { // if it's a dynamic configuration option + // Removing an item already present in the list + const index = this.recentDynamicOptionsTracker.findIndex(item => this.dynamicOptionsMatch(item, option)); + if (index > -1) { + this.recentDynamicOptionsTracker.splice(index, 1); + } + // Adding new item, most recent at the top of the list + const recentMax = 3; + if (this.recentDynamicOptionsTracker.unshift(option) > recentMax) { + // Keep the latest 3 dynamic configuration options to not clutter the dropdown. + this.recentDynamicOptionsTracker.splice(recentMax); + } + } } + + protected dynamicOptionsMatch(one: DebugSessionOptions, other: DebugSessionOptions): boolean { + return one.providerType !== undefined + && one.configuration.name === other.configuration.name + && one.providerType === other.providerType; + } + + get recentDynamicOptions(): readonly DebugSessionOptions[] { + return this.recentDynamicOptionsTracker; + } + protected updateCurrent(options: DebugSessionOptions | undefined = this._currentOptions): void { - this._currentOptions = options && !options.configuration.dynamic ? this.find(options.configuration.name, options.workspaceFolderUri) : options; + this._currentOptions = options && this.find(options.configuration, options.workspaceFolderUri, options.providerType); if (!this._currentOptions) { const model = this.getModel(); @@ -181,7 +240,24 @@ export class DebugConfigurationManager { this.debugConfigurationTypeKey.set(this.current && this.current.configuration.type); this.onDidChangeEmitter.fire(undefined); } - find(name: string, workspaceFolderUri: string | undefined): DebugSessionOptions | undefined { + + /** + * @deprecated since v1.27.0 + */ + find(name: string, workspaceFolderUri: string): DebugSessionOptions | undefined; + /** + * Find / Resolve DebugSessionOptions from a given target debug configuration + */ + find(targetConfiguration: DebugConfiguration, workspaceFolderUri?: string, providerType?: string): DebugSessionOptions | undefined; + find(nameOrTargetConfiguration: string | DebugConfiguration, workspaceFolderUri?: string, providerType?: string): DebugSessionOptions | undefined { + // providerType is only applicable to dynamic debug configurations + if (typeof nameOrTargetConfiguration === 'object' && providerType) { + return { + configuration: nameOrTargetConfiguration, + providerType + }; + } + const name = typeof nameOrTargetConfiguration === 'string' ? nameOrTargetConfiguration : nameOrTargetConfiguration.name; for (const model of this.models.values()) { if (model.workspaceFolderUri === workspaceFolderUri) { for (const configuration of model.configurations) { @@ -194,7 +270,6 @@ export class DebugConfigurationManager { } } } - return undefined; } async openConfiguration(): Promise { @@ -349,7 +424,13 @@ export class DebugConfigurationManager { return this.debug.provideDynamicDebugConfigurations!(); } + async fetchDynamicDebugConfiguration(name: string, type: string): Promise { + await this.fireWillProvideDynamicDebugConfiguration(); + return this.debug.fetchDynamicDebugConfiguration(name, type); + } + protected async fireWillProvideDynamicDebugConfiguration(): Promise { + await this.initialized; await WaitUntilEvent.fire(this.onWillProvideDynamicDebugConfigurationEmitter, {}); } @@ -384,29 +465,43 @@ export class DebugConfigurationManager { async load(): Promise { await this.initialized; const data = await this.storage.getData('debug.configurations', {}); - if (data.current) { - this.current = this.find(data.current.name, data.current.workspaceFolderUri); + this.resolveRecentDynamicOptionsFromData(data.recentDynamicOptions); + + // Between versions v1.26 and v1.27, the expected format of the data changed so that old stored data + // may not contain the configuration key. + if (data.current && 'configuration' in data.current) { + this.current = this.find(data.current.configuration, data.current.workspaceFolderUri, data.current.providerType); + } + } + + protected resolveRecentDynamicOptionsFromData(options?: DebugSessionOptions[]): void { + if (!options || this.recentDynamicOptionsTracker.length !== 0) { + return; } + + this.recentDynamicOptionsTracker = options; } save(): void { const data: DebugConfigurationManager.Data = {}; - const { current } = this; + const { current, recentDynamicOptionsTracker } = this; if (current) { - data.current = { - name: current.configuration.name, - workspaceFolderUri: current.workspaceFolderUri - }; + data.current = current; } - this.storage.setData('debug.configurations', data); - } + if (this.recentDynamicOptionsTracker.length > 0) { + data.recentDynamicOptions = recentDynamicOptionsTracker; + } + + if (Object.keys(data).length > 0) { + this.storage.setData('debug.configurations', data); + } + } } + export namespace DebugConfigurationManager { export interface Data { - current?: { - name: string - workspaceFolderUri?: string - } + current?: DebugSessionOptions, + recentDynamicOptions?: DebugSessionOptions[] } } diff --git a/packages/debug/src/browser/debug-prefix-configuration.ts b/packages/debug/src/browser/debug-prefix-configuration.ts index e467a41d29283..5ead072ff287a 100644 --- a/packages/debug/src/browser/debug-prefix-configuration.ts +++ b/packages/debug/src/browser/debug-prefix-configuration.ts @@ -123,10 +123,10 @@ export class DebugPrefixConfiguration implements CommandContribution, CommandHan // Resolve dynamic configurations from providers const record = await this.debugConfigurationManager.provideDynamicDebugConfigurations(); - for (const [type, dynamicConfigurations] of Object.entries(record)) { + for (const [providerType, dynamicConfigurations] of Object.entries(record)) { if (dynamicConfigurations.length > 0) { items.push({ - label: type, + label: providerType, type: 'separator' }); } @@ -134,7 +134,7 @@ export class DebugPrefixConfiguration implements CommandContribution, CommandHan for (const configuration of dynamicConfigurations) { items.push({ label: configuration.name, - execute: () => this.runDynamicConfiguration({ configuration }) + execute: () => this.runConfiguration({ configuration, providerType }) }); } } @@ -145,21 +145,13 @@ export class DebugPrefixConfiguration implements CommandContribution, CommandHan /** * Set the current debug configuration, and execute debug start command. * - * @param configuration the `DebugSessionOptions`. + * @param configurationOptions the `DebugSessionOptions`. */ - protected runConfiguration(configuration: DebugSessionOptions): void { - this.debugConfigurationManager.current = { ...configuration }; + protected runConfiguration(configurationOptions: DebugSessionOptions): void { + this.debugConfigurationManager.current = configurationOptions; this.commandRegistry.executeCommand(DebugCommands.START.id); } - /** - * Execute the debug start command without affecting the current debug configuration - * @param configuration the `DebugSessionOptions`. - */ - protected runDynamicConfiguration(configuration: DebugSessionOptions): void { - this.commandRegistry.executeCommand(DebugCommands.START.id, configuration); - } - /** * Handle the visibility of the debug status bar. * @param event the preference change event. diff --git a/packages/debug/src/browser/debug-session-options.ts b/packages/debug/src/browser/debug-session-options.ts index 8f87502eebd48..108762e0c0fd1 100644 --- a/packages/debug/src/browser/debug-session-options.ts +++ b/packages/debug/src/browser/debug-session-options.ts @@ -16,15 +16,42 @@ import { DebugConfiguration } from '../common/debug-common'; -export interface DebugSessionOptions { +export interface DebugSessionOptionsBase { + workspaceFolderUri?: string, + providerType?: string // Applicable to dynamic configurations +} + +export interface DebugSessionOptions extends DebugSessionOptionsBase { configuration: DebugConfiguration - workspaceFolderUri?: string } + +export interface DebugSessionOptionsData extends DebugSessionOptionsBase, DebugConfiguration { +} + export interface InternalDebugSessionOptions extends DebugSessionOptions { id: number } export namespace InternalDebugSessionOptions { + + const SEPARATOR = '__CONF__'; + export function is(options: DebugSessionOptions): options is InternalDebugSessionOptions { - return ('id' in options); + return 'id' in options; + } + + export function toValue(debugSessionOptions: DebugSessionOptions): string { + return debugSessionOptions.configuration.name + SEPARATOR + + debugSessionOptions.configuration.type + SEPARATOR + + debugSessionOptions.configuration.request + SEPARATOR + + debugSessionOptions.workspaceFolderUri + SEPARATOR + + debugSessionOptions.providerType; + } + + export function parseValue(value: string): DebugSessionOptionsData { + const split = value.split(SEPARATOR); + if (split.length !== 5) { + throw new Error('Unexpected argument, the argument is expected to have been generated by the \'toValue\' function'); + } + return {name: split[0], type: split[1], request: split[2], workspaceFolderUri: split[3], providerType: split[4]}; } } diff --git a/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx b/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx index 580f058384afd..2fd7d85e28bf4 100644 --- a/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx +++ b/packages/debug/src/browser/editor/debug-breakpoint-widget.tsx @@ -214,7 +214,7 @@ export class DebugBreakpointWidget implements Disposable { this._input.getControl().setValue(this._values[this.context] || ''); } ReactDOM.render( { + protected static readonly SEPARATOR = '──────────'; + protected static readonly PICK = '__PICK__'; + protected static readonly NO_CONFIGURATION = '__NO_CONF__'; + protected static readonly ADD_CONFIGURATION = '__ADD_CONF__'; + + private readonly selectRef = React.createRef(); + private manager: DebugConfigurationManager; + private quickInputService: QuickInputService; + + constructor(props: DebugConfigurationSelectProps) { + super(props); + this.manager = props.manager; + this.quickInputService = props.quickInputService; + this.state = { + providerTypes: [], + currentValue: undefined + }; + this.manager.onDidChangeConfigurationProviders(() => { + this.refreshDebugConfigurations(); + }); + } + + override componentDidMount(): void { + this.refreshDebugConfigurations(); + } + + override render(): React.ReactNode { + return this.setCurrentConfiguration(option)} + onFocus={() => this.refreshDebugConfigurations()} + onBlur={() => this.refreshDebugConfigurations()} + ref={this.selectRef} + />; + } + + protected get currentValue(): string { + const { current } = this.manager; + return current ? InternalDebugSessionOptions.toValue(current) : DebugConfigurationSelect.NO_CONFIGURATION; + } + + protected readonly setCurrentConfiguration = (option: SelectOption) => { + const value = option.value; + if (!value) { + return false; + } else if (value === DebugConfigurationSelect.ADD_CONFIGURATION) { + this.manager.addConfiguration(); + } else if (value.startsWith(DebugConfigurationSelect.PICK)) { + const providerType = this.parsePickValue(value); + this.selectDynamicConfigFromQuickPick(providerType); + } else { + const { name, type, request, workspaceFolderUri, providerType } = InternalDebugSessionOptions.parseValue(value); + this.manager.current = this.manager.find( + { name, type, request }, + workspaceFolderUri, + providerType === 'undefined' ? undefined : providerType + ); + } + }; + + protected toPickValue(providerType: string): string { + return DebugConfigurationSelect.PICK + providerType; + } + + protected parsePickValue(value: string): string { + return value.slice(DebugConfigurationSelect.PICK.length); + } + + protected async resolveDynamicConfigurationPicks(providerType: string): Promise { + const configurationsOfProviderType = + (await this.manager.provideDynamicDebugConfigurations())[providerType]; + + if (!configurationsOfProviderType) { + return []; + } + + return configurationsOfProviderType.map(configuration => ({ + label: configuration.name, + configurationType: configuration.type, + request: configuration.request, + providerType + })); + } + + protected async selectDynamicConfigFromQuickPick(providerType: string): Promise { + const picks: DynamicPickItem[] = await this.resolveDynamicConfigurationPicks(providerType); + + if (picks.length === 0) { + return; + } + + const selected: DynamicPickItem | undefined = await this.quickInputService.showQuickPick( + picks, + { + placeholder: nls.localizeByDefault('Select Launch Configuration') + } + ); + + if (!selected) { + return; + } + + const selectedConfiguration = { + name: selected.label, + type: selected.configurationType, + request: selected.request + }; + this.manager.current = this.manager.find(selectedConfiguration, undefined, selected.providerType); + this.refreshDebugConfigurations(); + } + + protected refreshDebugConfigurations = async () => { + const configsPerType = await this.manager.provideDynamicDebugConfigurations(); + const providerTypes = []; + for (const [ type, configurations ] of Object.entries(configsPerType)) { + if (configurations.length > 0) { + providerTypes.push(type); + } + } + this.selectRef.current!.value = this.currentValue; + this.setState({ providerTypes, currentValue: this.currentValue }); + }; + + protected renderOptions(): SelectOption[] { + const options: SelectOption[] = []; + + // Add non dynamic debug configurations + for (const config of this.manager.all) { + const value = InternalDebugSessionOptions.toValue(config); + options.push({ + value, + label: this.toName(config, this.props.isMultiRoot) + }); + } + + // Add recently used dynamic debug configurations + const { recentDynamicOptions } = this.manager; + if (recentDynamicOptions.length > 0) { + if (options.length > 0) { + options.push({ + separator: true + }); + } + for (const dynamicOption of recentDynamicOptions) { + const value = InternalDebugSessionOptions.toValue(dynamicOption); + options.push({ + value, + label: this.toName(dynamicOption, this.props.isMultiRoot) + ' (' + dynamicOption.providerType + ')' + }); + } + } + + // Placing a 'No Configuration' entry enables proper functioning of the 'onChange' event, by + // having an entry to switch from (E.g. a case where only one dynamic configuration type is available) + if (options.length === 0) { + const value = DebugConfigurationSelect.NO_CONFIGURATION; + options.push({ + value, + label: nls.localizeByDefault('No Configurations') + }); + } + + // Add dynamic configuration types for quick pick selection + const types = this.state.providerTypes; + if (types.length > 0) { + options.push({ + separator: true + }); + for (const type of types) { + const value = this.toPickValue(type); + options.push({ + value, + label: type + '...' + }); + } + } + + options.push({ + separator: true + }); + options.push({ + value: DebugConfigurationSelect.ADD_CONFIGURATION, + label: nls.localizeByDefault('Add Configuration...') + }); + + return options; + } + + protected toName({ configuration, workspaceFolderUri }: DebugSessionOptions, multiRoot: boolean): string { + if (!workspaceFolderUri || !multiRoot) { + return configuration.name; + } + return `${configuration.name} (${new URI(workspaceFolderUri).path.base})`; + } +} diff --git a/packages/debug/src/browser/view/debug-configuration-widget.tsx b/packages/debug/src/browser/view/debug-configuration-widget.tsx index ed13be82c4d5e..71132dfb5f77e 100644 --- a/packages/debug/src/browser/view/debug-configuration-widget.tsx +++ b/packages/debug/src/browser/view/debug-configuration-widget.tsx @@ -14,20 +14,18 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 // ***************************************************************************** +import { ReactWidget, QuickInputService } from '@theia/core/lib/browser'; +import { CommandRegistry, Disposable, MessageService } from '@theia/core/lib/common'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; -import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; -import { CommandRegistry, Disposable } from '@theia/core/lib/common'; -import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component'; -import URI from '@theia/core/lib/common/uri'; -import { ReactWidget } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser'; import { DebugConsoleContribution } from '../console/debug-console-contribution'; import { DebugConfigurationManager } from '../debug-configuration-manager'; +import { DebugCommands } from '../debug-frontend-application-contribution'; import { DebugSessionManager } from '../debug-session-manager'; import { DebugAction } from './debug-action'; +import { DebugConfigurationSelect } from './debug-configuration-select'; import { DebugViewModel } from './debug-view-model'; -import { DebugSessionOptions } from '../debug-session-options'; -import { DebugCommands } from '../debug-frontend-application-contribution'; import { nls } from '@theia/core/lib/common/nls'; @injectable() @@ -48,9 +46,15 @@ export class DebugConfigurationWidget extends ReactWidget { @inject(DebugConsoleContribution) protected readonly debugConsole: DebugConsoleContribution; + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; + @inject(MessageService) + protected readonly messageService: MessageService; + @postConstruct() protected init(): void { this.addClass('debug-toolbar'); @@ -74,68 +78,33 @@ export class DebugConfigurationWidget extends ReactWidget { this.stepRef.focus(); return true; } + protected stepRef: DebugAction | undefined; protected setStepRef = (stepRef: DebugAction | null) => this.stepRef = stepRef || undefined; render(): React.ReactNode { - const { options } = this; return - this.setCurrentConfiguration(option)} /> + ; } - protected get currentValue(): string { - const { current } = this.manager; - return current ? this.toValue(current) : '__NO_CONF__'; - } - protected get options(): SelectOption[] { - const items: SelectOption[] = Array.from(this.manager.all).map(option => ({ - value: this.toValue(option), - label: this.toName(option) - })); - if (items.length === 0) { - items.push({ - value: '__NO_CONF__', - label: nls.localizeByDefault('No Configurations') - }); - } - items.push({ - separator: true - }); - items.push({ - value: '__ADD_CONF__', - label: nls.localizeByDefault('Add Configuration...') - }); - return items; - } - protected toValue({ configuration, workspaceFolderUri }: DebugSessionOptions): string { - if (!workspaceFolderUri) { - return configuration.name; - } - return configuration.name + '__CONF__' + workspaceFolderUri; - } - protected toName({ configuration, workspaceFolderUri }: DebugSessionOptions): string { - if (!workspaceFolderUri || !this.workspaceService.isMultiRootWorkspaceOpened) { - return configuration.name; - } - return configuration.name + ' (' + new URI(workspaceFolderUri).path.base + ')'; - } - protected readonly setCurrentConfiguration = (option: SelectOption) => { - const value = option.value!; - if (value === '__ADD_CONF__') { - this.manager.addConfiguration(); - } else { - const [name, workspaceFolderUri] = value.split('__CONF__'); - this.manager.current = this.manager.find(name, workspaceFolderUri); + protected readonly start = async () => { + let configuration; + try { + configuration = await this.manager.getSelectedConfiguration(); + } catch (e) { + this.messageService.error(e.message); + return; } - }; - protected readonly start = () => { - const configuration = this.manager.current; this.commandRegistry.executeCommand(DebugCommands.START.id, configuration); }; diff --git a/packages/debug/src/common/debug-configuration.ts b/packages/debug/src/common/debug-configuration.ts index 6007565367ba0..a6b4e33a02029 100644 --- a/packages/debug/src/common/debug-configuration.ts +++ b/packages/debug/src/common/debug-configuration.ts @@ -74,9 +74,6 @@ export interface DebugConfiguration { /** Task to run after debug session ends */ postDebugTask?: string | TaskIdentifier; - - /** Indicates if it's a dynamic debug configuration */ - dynamic?: boolean; } export namespace DebugConfiguration { export function is(arg: DebugConfiguration | any): arg is DebugConfiguration { diff --git a/packages/debug/src/common/debug-service.ts b/packages/debug/src/common/debug-service.ts index 67b70d90387a9..d9d86a9364ff1 100644 --- a/packages/debug/src/common/debug-service.ts +++ b/packages/debug/src/common/debug-service.ts @@ -16,7 +16,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Disposable } from '@theia/core'; +import { Disposable, Event } from '@theia/core'; import { ApplicationError } from '@theia/core/lib/common/application-error'; import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; import { CommandIdVariables } from '@theia/variable-resolver/lib/common/variable-types'; @@ -82,6 +82,11 @@ export interface DebugService extends Disposable { */ provideDynamicDebugConfigurations?(): Promise>; + /** + * Provides a dynamic debug configuration matching the name and the provider debug type + */ + fetchDynamicDebugConfiguration(name: string, type: string): Promise; + /** * Resolves a [debug configuration](#DebugConfiguration) by filling in missing values * or by adding/changing/removing attributes before variable substitution. @@ -116,6 +121,12 @@ export interface DebugService extends Disposable { * Stop a running session for the given session id. */ terminateDebugSession(sessionId: string): Promise; + + /** + * Event handle to indicate when one or more dynamic debug configuration providers + * have been registered or unregistered. + */ + onDidChangeDebugConfigurationProviders: Event; } /** diff --git a/packages/debug/src/node/debug-service-impl.ts b/packages/debug/src/node/debug-service-impl.ts index ef5290e93f1fe..ae7c8d51f585a 100644 --- a/packages/debug/src/node/debug-service-impl.ts +++ b/packages/debug/src/node/debug-service-impl.ts @@ -21,6 +21,7 @@ import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-sch import { CommandIdVariables } from '@theia/variable-resolver/lib/common/variable-types'; import { DebugAdapterSessionManager } from './debug-adapter-session-manager'; import { DebugAdapterContributionRegistry } from './debug-adapter-contribution-registry'; +import { Event } from '@theia/core'; /** * DebugService implementation. @@ -34,6 +35,10 @@ export class DebugServiceImpl implements DebugService { @inject(DebugAdapterContributionRegistry) protected readonly registry: DebugAdapterContributionRegistry; + get onDidChangeDebugConfigurationProviders(): Event { + return Event.None; + } + dispose(): void { this.terminateDebugSession(); } @@ -62,6 +67,14 @@ export class DebugServiceImpl implements DebugService { async provideDebugConfigurations(debugType: string, workspaceFolderUri?: string): Promise { return this.registry.provideDebugConfigurations(debugType, workspaceFolderUri); } + async provideDynamicDebugConfigurations(): Promise> { + // TODO: Support dynamic debug configurations through Theia extensions? + return {}; + } + fetchDynamicDebugConfiguration(name: string, type: string): Promise { + // TODO: Support dynamic debug configurations through Theia extensions? + return Promise.resolve(undefined); + } async resolveDebugConfiguration(config: DebugConfiguration, workspaceFolderUri?: string): Promise { return this.registry.resolveDebugConfiguration(config, workspaceFolderUri); } @@ -103,5 +116,4 @@ export class DebugServiceImpl implements DebugService { await debugSession.stop(); } } - } diff --git a/packages/output/src/browser/output-toolbar-contribution.tsx b/packages/output/src/browser/output-toolbar-contribution.tsx index 92810ca181486..c9bbd0ff77c77 100644 --- a/packages/output/src/browser/output-toolbar-contribution.tsx +++ b/packages/output/src/browser/output-toolbar-contribution.tsx @@ -97,7 +97,11 @@ export class OutputToolbarContribution implements TabBarToolbarContribution { }); } return
- this.changeChannel(option)} /> + this.changeChannel(option)} + />
; } diff --git a/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts index 5482f3c245089..b82466b09fa83 100644 --- a/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts +++ b/packages/plugin-ext/src/main/browser/debug/plugin-debug-service.ts @@ -15,6 +15,8 @@ // ***************************************************************************** import { DebuggerDescription, DebugPath, DebugService } from '@theia/debug/lib/common/debug-service'; +import debounce = require('@theia/core/shared/lodash.debounce'); +import { Emitter, Event } from '@theia/core'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration'; import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema'; @@ -40,6 +42,11 @@ export class PluginDebugService implements DebugService { protected readonly configurationProviders = new Map(); protected readonly toDispose = new DisposableCollection(); + protected readonly onDidChangeDebugConfigurationProvidersEmitter = new Emitter(); + get onDidChangeDebugConfigurationProviders(): Event { + return this.onDidChangeDebugConfigurationProvidersEmitter.event; + } + // maps session and contribution protected readonly sessionId2contrib = new Map(); protected delegated: DebugService; @@ -79,14 +86,21 @@ export class PluginDebugService implements DebugService { this.contributors.delete(debugType); } + // debouncing to send a single notification for multiple registrations at initialization time + fireOnDidConfigurationProvidersChanged = debounce(() => { + this.onDidChangeDebugConfigurationProvidersEmitter.fire(); + }, 100); + registerDebugConfigurationProvider(provider: PluginDebugConfigurationProvider): Disposable { const handle = provider.handle; this.configurationProviders.set(handle, provider); + this.fireOnDidConfigurationProvidersChanged(); return Disposable.create(() => this.unregisterDebugConfigurationProvider(handle)); } unregisterDebugConfigurationProvider(handle: number): void { this.configurationProviders.delete(handle); + this.fireOnDidConfigurationProvidersChanged(); } async debugTypes(): Promise { @@ -123,6 +137,24 @@ export class PluginDebugService implements DebugService { return results; } + async fetchDynamicDebugConfiguration(name: string, providerType: string): Promise { + const pluginProviders = + Array.from(this.configurationProviders.values()).filter(p => ( + p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic && + p.type === providerType && + p.provideDebugConfigurations + )); + + for (const provider of pluginProviders) { + const configurations = await provider.provideDebugConfigurations(undefined); + for (const configuration of configurations) { + if (configuration.name === name) { + return configuration; + } + } + } + } + async provideDynamicDebugConfigurations(): Promise> { const pluginProviders = Array.from(this.configurationProviders.values()).filter(p => ( @@ -134,9 +166,6 @@ export class PluginDebugService implements DebugService { await Promise.all(pluginProviders.map(async provider => { const configurations = await provider.provideDebugConfigurations(undefined); - for (const configuration of configurations) { - configuration.dynamic = true; - } let configurationsPerType = configurationsRecord[provider.type]; configurationsPerType = configurationsPerType ? configurationsPerType.concat(configurations) : configurations; diff --git a/packages/preferences/src/browser/views/components/preference-select-input.ts b/packages/preferences/src/browser/views/components/preference-select-input.ts index ad15176790f7a..02bf2e607e6ac 100644 --- a/packages/preferences/src/browser/views/components/preference-select-input.ts +++ b/packages/preferences/src/browser/views/components/preference-select-input.ts @@ -65,7 +65,7 @@ export class PreferenceSelectInputRenderer extends PreferenceLeafNodeRenderer this.handleUserInteraction(index), ref: this.selectComponent });