Skip to content

Commit

Permalink
[Debug view] Add dynamic debug configurations
Browse files Browse the repository at this point in the history
Following the support for dynamic debug configurations via
command Debug: Select and Start debugging. PR eclipse-theia#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 <[email protected]>
Co-authored-by: Paul Marechal <[email protected]>
  • Loading branch information
alvsan09 and paul-marechal committed Jun 1, 2022
1 parent 5f887e2 commit e1e200a
Show file tree
Hide file tree
Showing 15 changed files with 516 additions and 118 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
<a name="breaking_changes_1.27.0">[Breaking Changes:](#breaking_changes_1.27.0)</a>

- [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

Expand Down
54 changes: 37 additions & 17 deletions packages/core/src/browser/widgets/select-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -64,10 +66,10 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
constructor(props: SelectComponentProps) {
super(props);
let selected = 0;
if (typeof props.value === 'number') {
selected = props.value;
} else if (typeof props.value === 'string') {
selected = Math.max(props.options.findIndex(e => 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,
Expand Down Expand Up @@ -162,8 +164,8 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
override render(): React.ReactNode {
const { options } = this.props;
let { selected } = this.state;
while (options[selected]?.separator) {
selected = (selected + 1) % this.props.options.length;
if (options[selected]?.separator) {
selected = this.nextNotSeparator('forwards');
}
const selectedItemLabel = options[selected].label ?? options[selected].value;
return <>
Expand All @@ -173,7 +175,13 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
tabIndex={0}
className="theia-select-component"
onClick={e => this.handleClickEvent(e)}
onBlur={() => this.hide()}
onBlur={
() => {
this.hide();
this.props.onBlur?.();
}
}
onFocus={() => this.props.onFocus?.()}
onKeyDown={e => this.handleKeypress(e)}
>
<div key="label" className="theia-select-component-label">{selectedItemLabel}</div>
Expand All @@ -183,33 +191,45 @@ export class SelectComponent extends React.Component<SelectComponentProps, Selec
</>;
}

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<HTMLDivElement>): 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
});
} else {
this.toggleVisibility();
this.setState({
selected: 0,
hover: 0,
selected: 0
});
}
} else if (ev.key === 'Enter') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export class DebugConsoleContribution extends AbstractViewContribution<ConsoleWi
return <SelectComponent
key="debugConsoleSeverity"
options={severityElements}
value={this.consoleSessionManager.severity || Severity.Ignore}
defaultValue={this.consoleSessionManager.severity || Severity.Ignore}
onChange={this.changeSeverity} />;
}

Expand All @@ -207,7 +207,7 @@ export class DebugConsoleContribution extends AbstractViewContribution<ConsoleWi
return <SelectComponent
key="debugConsoleSelector"
options={availableConsoles}
value={0}
defaultValue={0}
onChange={this.changeDebugConsole} />;
}

Expand Down
127 changes: 111 additions & 16 deletions packages/debug/src/browser/debug-configuration-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,16 @@ export class DebugConfigurationManager {
return this.onWillProvideDynamicDebugConfigurationEmitter.event;
}

get onDidChangeConfigurationProviders(): Event<void> {
return this.debug.onDidChangeDebugConfigurationProviders;
}

protected debugConfigurationTypeKey: ContextKey<string>;

protected initialized: Promise<void>;

protected recentDynamicOptionsTracker: DebugSessionOptions[] = [];

@postConstruct()
protected async init(): Promise<void> {
this.debugConfigurationTypeKey = this.contextKeyService.createKey<string>('debugConfigurationType', undefined);
Expand Down Expand Up @@ -126,6 +132,9 @@ export class DebugConfigurationManager {
this.updateCurrent();
}, 500);

/**
* All _non-dynamic_ debug configurations.
*/
get all(): IterableIterator<DebugSessionOptions> {
return this.getAll();
}
Expand Down Expand Up @@ -160,11 +169,61 @@ export class DebugConfigurationManager {
get current(): DebugSessionOptions | undefined {
return this._currentOptions;
}

async getSelectedConfiguration(): Promise<DebugSessionOptions | undefined> {
// 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();
Expand All @@ -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) {
Expand All @@ -194,7 +270,6 @@ export class DebugConfigurationManager {
}
}
}
return undefined;
}

async openConfiguration(): Promise<void> {
Expand Down Expand Up @@ -349,7 +424,13 @@ export class DebugConfigurationManager {
return this.debug.provideDynamicDebugConfigurations!();
}

async fetchDynamicDebugConfiguration(name: string, type: string): Promise<DebugConfiguration | undefined> {
await this.fireWillProvideDynamicDebugConfiguration();
return this.debug.fetchDynamicDebugConfiguration(name, type);
}

protected async fireWillProvideDynamicDebugConfiguration(): Promise<void> {
await this.initialized;
await WaitUntilEvent.fire(this.onWillProvideDynamicDebugConfigurationEmitter, {});
}

Expand Down Expand Up @@ -384,29 +465,43 @@ export class DebugConfigurationManager {
async load(): Promise<void> {
await this.initialized;
const data = await this.storage.getData<DebugConfigurationManager.Data>('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[]
}
}
20 changes: 6 additions & 14 deletions packages/debug/src/browser/debug-prefix-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,18 +123,18 @@ 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'
});
}

for (const configuration of dynamicConfigurations) {
items.push({
label: configuration.name,
execute: () => this.runDynamicConfiguration({ configuration })
execute: () => this.runConfiguration({ configuration, providerType })
});
}
}
Expand All @@ -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.
Expand Down
Loading

0 comments on commit e1e200a

Please sign in to comment.