Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve debug config in django #264

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
Loading