Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 58 additions & 34 deletions src/extension/common/multiStepInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
QuickPickItem,
Event,
window,
QuickPickItemButtonEvent,
} from 'vscode';
import { createDeferred } from './utils/async';

// Borrowed from https://github.com/Microsoft/vscode-extension-samples/blob/master/quickinput-sample/src/multiStepInput.ts
// Why re-invent the wheel :)
Expand All @@ -37,7 +39,7 @@ export type InputStep<T extends any> = (input: MultiStepInput<T>, state: T) => P

type buttonCallbackType<T extends QuickPickItem> = (quickPick: QuickPick<T>) => void;

type QuickInputButtonSetup = {
export type QuickInputButtonSetup = {
/**
* Button for an action in a QuickPick.
*/
Expand All @@ -54,13 +56,12 @@ export interface IQuickPickParameters<T extends QuickPickItem, E = any> {
totalSteps?: number;
canGoBack?: boolean;
items: T[];
activeItem?: T | Promise<T>;
activeItem?: T | ((quickPick: QuickPick<T>) => Promise<T>);
placeholder: string | undefined;
customButtonSetups?: QuickInputButtonSetup[];
matchOnDescription?: boolean;
matchOnDetail?: boolean;
keepScrollPosition?: boolean;
sortByLabel?: boolean;
acceptFilterBoxTextAsSelection?: boolean;
/**
* A method called only after quickpick has been created and all handlers are registered.
Expand All @@ -70,6 +71,7 @@ export interface IQuickPickParameters<T extends QuickPickItem, E = any> {
callback: (event: E, quickPick: QuickPick<T>) => void;
event: Event<E>;
};
onDidTriggerItemButton?: (e: QuickPickItemButtonEvent<T>) => void;
}

interface InputBoxParameters {
Expand All @@ -83,7 +85,7 @@ interface InputBoxParameters {
validate(value: string): Promise<string | undefined>;
}

type MultiStepInputQuickPicResponseType<T, P> = T | (P extends { buttons: (infer I)[] } ? I : never) | undefined;
type MultiStepInputQuickPickResponseType<T, P> = T | (P extends { buttons: (infer I)[] } ? I : never) | undefined;
type MultiStepInputInputBoxResponseType<P> = string | (P extends { buttons: (infer I)[] } ? I : never) | undefined;
export interface IMultiStepInput<S> {
run(start: InputStep<S>, state: S): Promise<void>;
Expand All @@ -95,7 +97,7 @@ export interface IMultiStepInput<S> {
activeItem,
placeholder,
customButtonSetups,
}: P): Promise<MultiStepInputQuickPicResponseType<T, P>>;
}: P): Promise<MultiStepInputQuickPickResponseType<T, P>>;
showInputBox<P extends InputBoxParameters>({
title,
step,
Expand Down Expand Up @@ -131,8 +133,9 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
acceptFilterBoxTextAsSelection,
onChangeItem,
keepScrollPosition,
onDidTriggerItemButton,
initialize,
}: P): Promise<MultiStepInputQuickPicResponseType<T, P>> {
}: P): Promise<MultiStepInputQuickPickResponseType<T, P>> {
const disposables: Disposable[] = [];
const input = window.createQuickPick<T>();
input.title = title;
Expand Down Expand Up @@ -161,7 +164,13 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
initialize(input);
}
if (activeItem) {
input.activeItems = [await activeItem];
if (typeof activeItem === 'function') {
activeItem(input).then((item) => {
if (input.activeItems.length === 0) {
input.activeItems = [item];
}
});
}
} else {
input.activeItems = [];
}
Expand All @@ -170,35 +179,46 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
// so do it after initialization. This ensures quickpick starts with the active
// item in focus when this is true, instead of having scroll position at top.
input.keepScrollPosition = keepScrollPosition;
try {
return await new Promise<MultiStepInputQuickPicResponseType<T, P>>((resolve, reject) => {
disposables.push(
input.onDidTriggerButton(async (item) => {
if (item === QuickInputButtons.Back) {
reject(InputFlowAction.back);
}
if (customButtonSetups) {
for (const customButtonSetup of customButtonSetups) {
if (JSON.stringify(item) === JSON.stringify(customButtonSetup?.button)) {
await customButtonSetup?.callback(input);
}
}

const deferred = createDeferred<T>();

disposables.push(
input.onDidTriggerButton(async (item) => {
if (item === QuickInputButtons.Back) {
deferred.reject(InputFlowAction.back);
input.hide();
}
if (customButtonSetups) {
for (const customButtonSetup of customButtonSetups) {
if (JSON.stringify(item) === JSON.stringify(customButtonSetup?.button)) {
await customButtonSetup?.callback(input);
}
}),
input.onDidChangeSelection((selectedItems) => resolve(selectedItems[0])),
input.onDidHide(() => {
resolve(undefined);
}),
);
if (acceptFilterBoxTextAsSelection) {
disposables.push(
input.onDidAccept(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolve(input.value as any);
}),
);
}
}
});
}),
input.onDidChangeSelection((selectedItems) => deferred.resolve(selectedItems[0])),
input.onDidHide(() => {
if (!deferred.completed) {
deferred.resolve(undefined);
}
}),
input.onDidTriggerItemButton(async (item) => {
if (onDidTriggerItemButton) {
await onDidTriggerItemButton(item);
}
}),
);
if (acceptFilterBoxTextAsSelection) {
disposables.push(
input.onDidAccept(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
deferred.resolve(input.value as any);
}),
);
}

try {
return await deferred.promise;
} finally {
disposables.forEach((d) => d.dispose());
}
Expand Down Expand Up @@ -283,6 +303,9 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
if (err === InputFlowAction.back) {
this.steps.pop();
step = this.steps.pop();
if (step === undefined) {
throw err;
}
} else if (err === InputFlowAction.resume) {
step = this.steps.pop();
} else if (err === InputFlowAction.cancel) {
Expand All @@ -297,6 +320,7 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
}
}
}

export const IMultiStepInputFactory = Symbol('IMultiStepInputFactory');
export interface IMultiStepInputFactory {
create<S>(): IMultiStepInput<S>;
Expand Down
2 changes: 1 addition & 1 deletion src/extension/common/utils/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface Deferred<T> {
readonly rejected: boolean;
readonly completed: boolean;
resolve(value?: T | PromiseLike<T>): void;
reject(reason?: string | Error | Record<string, unknown>): void;
reject(reason?: string | Error | Record<string, unknown> | unknown): void;
}

class DeferredImpl<T> implements Deferred<T> {
Expand Down
11 changes: 8 additions & 3 deletions src/extension/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export namespace DebugConfigStrings {
label: l10n.t('Python Debugger'),
description: l10n.t('Select a Python Debugger debug configuration'),
};
export const browsePath = {
label: l10n.t('Browse Files...'),
detail: l10n.t('Browse your file system to find a Python file.'),
openButtonLabel: l10n.t('Select File'),
title: l10n.t('Select Python File'),
};
export namespace file {
export const snippet = {
name: l10n.t('Python Debugger: Current File'),
Expand Down Expand Up @@ -92,12 +98,11 @@ export namespace DebugConfigStrings {
label: l10n.t('Django'),
description: l10n.t('Launch and debug a Django web application'),
};
export const enterManagePyPath = {
export const djangoConfigPromp = {
title: l10n.t('Debug Django'),
prompt: l10n.t(
"Enter the path to manage.py ('${workspaceFolderToken}' points to the root of the current workspace folder)",
"Enter the path to manage.py or select a file from the list ('${workspaceFolderToken}' points to the root of the current workspace folder)",
),
invalid: l10n.t('Enter a valid Python file path'),
};
}
export namespace fastapi {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi
const config: Partial<DebugConfigurationArguments> = {};
const state = { config, folder, token };

// Disabled until configuration issues are addressed by VS Code. See #4007
const multiStep = this.multiStepFactory.create<DebugConfigurationState>();
await multiStep.run((input, s) => PythonDebugConfigurationService.pickDebugConfiguration(input, s), state);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
'use strict';

import * as path from 'path';
import * as fs from 'fs-extra';
import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode';
import { IDynamicDebugConfigurationService } from '../types';
import { asyncFilter } from '../../common/utilities';
import { DebuggerTypeName } from '../../constants';
import { replaceAll } from '../../common/stringUtils';
import { getDjangoPaths, getFastApiPaths, getFlaskPaths } from './utils/configuration';

const workspaceFolderToken = '${workspaceFolder}';

Expand All @@ -29,7 +28,10 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf
program: '${file}',
});

const djangoManagePath = await DynamicPythonDebugConfigurationService.getDjangoPath(folder);
const djangoManagePaths = await getDjangoPaths(folder);
const djangoManagePath = djangoManagePaths?.length
? path.relative(folder.uri.fsPath, djangoManagePaths[0].fsPath)
: null;
if (djangoManagePath) {
providers.push({
name: 'Python Debugger: Django',
Expand All @@ -41,7 +43,8 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf
});
}

const flaskPath = await DynamicPythonDebugConfigurationService.getFlaskPath(folder);
const flaskPaths = await getFlaskPaths(folder);
const flaskPath = flaskPaths?.length ? flaskPaths[0].fsPath : null;
if (flaskPath) {
providers.push({
name: 'Python Debugger: Flask',
Expand All @@ -57,7 +60,8 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf
});
}

let fastApiPath = await DynamicPythonDebugConfigurationService.getFastApiPath(folder);
const fastApiPaths = await getFastApiPaths(folder);
let fastApiPath = fastApiPaths?.length ? fastApiPaths[0].fsPath : null;
if (fastApiPath) {
fastApiPath = replaceAll(path.relative(folder.uri.fsPath, fastApiPath), path.sep, '.').replace('.py', '');
providers.push({
Expand All @@ -72,58 +76,4 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf

return providers;
}

private static async getDjangoPath(folder: WorkspaceFolder) {
const regExpression = /execute_from_command_line\(/;
const possiblePaths = await DynamicPythonDebugConfigurationService.getPossiblePaths(
folder,
['manage.py', '*/manage.py', 'app.py', '*/app.py'],
regExpression,
);
return possiblePaths.length ? path.relative(folder.uri.fsPath, possiblePaths[0]) : null;
}

private static async getFastApiPath(folder: WorkspaceFolder) {
const regExpression = /app\s*=\s*FastAPI\(/;
const fastApiPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths(
folder,
['main.py', 'app.py', '*/main.py', '*/app.py', '*/*/main.py', '*/*/app.py'],
regExpression,
);

return fastApiPaths.length ? fastApiPaths[0] : null;
}

private static async getFlaskPath(folder: WorkspaceFolder) {
const regExpression = /app(?:lication)?\s*=\s*(?:flask\.)?Flask\(|def\s+(?:create|make)_app\(/;
const flaskPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths(
folder,
['__init__.py', 'app.py', 'wsgi.py', '*/__init__.py', '*/app.py', '*/wsgi.py'],
regExpression,
);

return flaskPaths.length ? flaskPaths[0] : null;
}

private static async getPossiblePaths(
folder: WorkspaceFolder,
globPatterns: string[],
regex: RegExp,
): Promise<string[]> {
const foundPathsPromises = (await Promise.allSettled(
globPatterns.map(
async (pattern): Promise<string[]> =>
(await fs.pathExists(path.join(folder.uri.fsPath, pattern)))
? [path.join(folder.uri.fsPath, pattern)]
: [],
),
)) as { status: string; value: [] }[];
const possiblePaths: string[] = [];
foundPathsPromises.forEach((result) => possiblePaths.push(...result.value));
const finalPaths = await asyncFilter(possiblePaths, async (possiblePath) =>
regex.exec((await fs.readFile(possiblePath)).toString()),
);

return finalPaths;
}
}
Loading