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 #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 Dec 14, 2021
1 parent 8bded61 commit d647f14
Show file tree
Hide file tree
Showing 12 changed files with 454 additions and 72 deletions.
142 changes: 131 additions & 11 deletions packages/debug/src/browser/debug-configuration-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { visit, parse } from 'jsonc-parser';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Emitter, Event, WaitUntilEvent } from '@theia/core/lib/common/event';
import { nls } from '@theia/core/lib/common/nls';
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import { PreferenceScope, PreferenceService, QuickPickValue, StorageService } from '@theia/core/lib/browser';
Expand All @@ -41,6 +42,14 @@ import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-mo
export interface WillProvideDebugConfiguration extends WaitUntilEvent {
}

export interface DebugSessionOptionsData {
name: string,
type: string,
request: string,
workspaceFolderUri?: string,
providerType?: string
};

@injectable()
export class DebugConfigurationManager {

Expand Down Expand Up @@ -79,10 +88,16 @@ export class DebugConfigurationManager {
return this.onWillProvideDynamicDebugConfigurationEmitter.event;
}

get onDidConfigurationProvidersChanged(): 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 @@ -119,6 +134,9 @@ export class DebugConfigurationManager {
this.updateCurrent();
}, 500);

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

async getSelectedConfiguration(): Promise<DebugSessionOptions | undefined> {
if (!this._currentOptions?.providerType) {
return Promise.resolve(this._currentOptions);
}

// Refresh a dynamic configuration from the provider,
// This allow providers to update properties before the execution e.g. program
const providerType = this._currentOptions.providerType;
const name = this._currentOptions.configuration.name;
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 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
if (this.recentDynamicOptionsTracker.push(option) > 3) {
// Remove oldest, i.e. Keeping a short number of recently used
// configuration options
this.recentDynamicOptionsTracker.shift();
}
}
}

protected dynamicOptionsMatch(one: DebugSessionOptions, other: DebugSessionOptions): boolean {
return one.providerType !== undefined
&& other.providerType !== undefined
&& one.configuration.name === other.configuration.name
&& one.providerType === other.providerType;
}

get recentDynamicOptions(): DebugSessionOptions[] {
// Most recent first
return [...this.recentDynamicOptionsTracker].reverse();
}

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;
Expand All @@ -174,11 +244,20 @@ export class DebugConfigurationManager {
this.debugConfigurationTypeKey.set(this.current && this.current.configuration.type);
this.onDidChangeEmitter.fire(undefined);
}
find(name: string, workspaceFolderUri: string | undefined): DebugSessionOptions | undefined {

find(targetConfiguration: DebugConfiguration, workspaceFolderUri?: string, providerType?: string): DebugSessionOptions | undefined {
// providerType is only applicable to dynamic debug configurations
if (providerType) {
return {
configuration: targetConfiguration,
providerType
};
}

for (const model of this.models.values()) {
if (model.workspaceFolderUri === workspaceFolderUri) {
for (const configuration of model.configurations) {
if (configuration.name === name) {
if (configuration.name === targetConfiguration.name) {
return {
configuration,
workspaceFolderUri
Expand Down Expand Up @@ -313,10 +392,17 @@ export class DebugConfigurationManager {
}

async provideDynamicDebugConfigurations(): Promise<{ type: string, configurations: DebugConfiguration[] }[]> {
await this.initialized;
await this.fireWillProvideDynamicDebugConfiguration();
return this.debug.provideDynamicDebugConfigurations!();
}

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

protected async fireWillProvideDynamicDebugConfiguration(): Promise<void> {
await WaitUntilEvent.fire(this.onWillProvideDynamicDebugConfigurationEmitter, {});
}
Expand Down Expand Up @@ -352,29 +438,63 @@ export class DebugConfigurationManager {
async load(): Promise<void> {
await this.initialized;
const data = await this.storage.getData<DebugConfigurationManager.Data>('debug.configurations', {});
this.resolveRecentDynamicOptionsFromData(data.recentDynamicOptions);

if (data.current) {
this.current = this.find(data.current.name, data.current.workspaceFolderUri);
const configuration = { name: data.current.name, type: data.current.type, request: data.current.request };
this.current = this.find(configuration, data.current.workspaceFolderUri, data.current.providerType);
}
}

protected resolveRecentDynamicOptionsFromData(options?: DebugSessionOptionsData[]): void {
if (!options || this.recentDynamicOptionsTracker.length !== 0) {
return;
}

for (const option of options) {
const configuration = {
name: option.name,
type: option.type,
request: option.request
};

this.recentDynamicOptionsTracker.push({ configuration, providerType: option.providerType });
}
}

save(): void {
const data: DebugConfigurationManager.Data = {};
const { current } = this;
const { current, recentDynamicOptionsTracker } = this;
if (current) {
data.current = {
name: current.configuration.name,
workspaceFolderUri: current.workspaceFolderUri
type: current.configuration.type,
request: current.configuration.request,
};
if (current.workspaceFolderUri) { data.current.workspaceFolderUri = current.workspaceFolderUri; }
if (current.providerType) {data.current.providerType = current.providerType; }
}
if (this.recentDynamicOptionsTracker.length > 0) {
const recentDynamicOptionsData = [];
for (const options of recentDynamicOptionsTracker) {
const optionsData: DebugSessionOptionsData = {
name: options.configuration.name,
type: options.configuration.type,
request: options.configuration.request
};
if (options.providerType) { optionsData.providerType = options.providerType; }

recentDynamicOptionsData.push(optionsData);
}
data.recentDynamicOptions = recentDynamicOptionsData;
}
this.storage.setData('debug.configurations', data);
}

}

export namespace DebugConfigurationManager {
export interface Data {
current?: {
name: string
workspaceFolderUri?: string
}
current?: DebugSessionOptionsData,
recentDynamicOptions?: DebugSessionOptionsData[]
}
}
5 changes: 3 additions & 2 deletions packages/debug/src/browser/debug-prefix-configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,18 @@ export class DebugPrefixConfiguration implements CommandContribution, CommandHan
const configurationsByType = await this.debugConfigurationManager.provideDynamicDebugConfigurations();
for (const typeConfigurations of configurationsByType) {
const dynamicConfigurations = typeConfigurations.configurations;
const providerType = typeConfigurations.type;
if (dynamicConfigurations.length > 0) {
items.push({
label: typeConfigurations.type,
label: providerType,
type: 'separator'
});
}

for (const configuration of dynamicConfigurations) {
items.push({
label: configuration.name,
execute: () => this.runConfiguration({ configuration })
execute: () => this.runConfiguration({ configuration, providerType })
});
}
}
Expand Down
24 changes: 22 additions & 2 deletions packages/debug/src/browser/debug-session-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,33 @@ import { DebugConfiguration } from '../common/debug-common';

export interface DebugSessionOptions {
configuration: DebugConfiguration
workspaceFolderUri?: string
workspaceFolderUri?: string,
providerType?: string // Applicable to dynamic configurations
}
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): [string, string, string, string, string] {
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 split as [string, string, string, string, string];
}
}
Loading

0 comments on commit d647f14

Please sign in to comment.