Skip to content

Commit c6a7846

Browse files
Improve debug config in django (#264)
* move config common functions to utils * get django paths * fix error in getting paths * Add prompt to show django paths * update strings * fix tests * fix tests * fix lint errorr and text * fix lint in tests * Add default option * fix tests * fix code
1 parent 0fd6d86 commit c6a7846

File tree

13 files changed

+387
-273
lines changed

13 files changed

+387
-273
lines changed

src/extension/common/multiStepInput.ts

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
QuickPickItem,
1616
Event,
1717
window,
18+
QuickPickItemButtonEvent,
1819
} from 'vscode';
20+
import { createDeferred } from './utils/async';
1921

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

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

40-
type QuickInputButtonSetup = {
42+
export type QuickInputButtonSetup = {
4143
/**
4244
* Button for an action in a QuickPick.
4345
*/
@@ -54,13 +56,12 @@ export interface IQuickPickParameters<T extends QuickPickItem, E = any> {
5456
totalSteps?: number;
5557
canGoBack?: boolean;
5658
items: T[];
57-
activeItem?: T | Promise<T>;
59+
activeItem?: T | ((quickPick: QuickPick<T>) => Promise<T>);
5860
placeholder: string | undefined;
5961
customButtonSetups?: QuickInputButtonSetup[];
6062
matchOnDescription?: boolean;
6163
matchOnDetail?: boolean;
6264
keepScrollPosition?: boolean;
63-
sortByLabel?: boolean;
6465
acceptFilterBoxTextAsSelection?: boolean;
6566
/**
6667
* A method called only after quickpick has been created and all handlers are registered.
@@ -70,6 +71,7 @@ export interface IQuickPickParameters<T extends QuickPickItem, E = any> {
7071
callback: (event: E, quickPick: QuickPick<T>) => void;
7172
event: Event<E>;
7273
};
74+
onDidTriggerItemButton?: (e: QuickPickItemButtonEvent<T>) => void;
7375
}
7476

7577
interface InputBoxParameters {
@@ -83,7 +85,7 @@ interface InputBoxParameters {
8385
validate(value: string): Promise<string | undefined>;
8486
}
8587

86-
type MultiStepInputQuickPicResponseType<T, P> = T | (P extends { buttons: (infer I)[] } ? I : never) | undefined;
88+
type MultiStepInputQuickPickResponseType<T, P> = T | (P extends { buttons: (infer I)[] } ? I : never) | undefined;
8789
type MultiStepInputInputBoxResponseType<P> = string | (P extends { buttons: (infer I)[] } ? I : never) | undefined;
8890
export interface IMultiStepInput<S> {
8991
run(start: InputStep<S>, state: S): Promise<void>;
@@ -95,7 +97,7 @@ export interface IMultiStepInput<S> {
9597
activeItem,
9698
placeholder,
9799
customButtonSetups,
98-
}: P): Promise<MultiStepInputQuickPicResponseType<T, P>>;
100+
}: P): Promise<MultiStepInputQuickPickResponseType<T, P>>;
99101
showInputBox<P extends InputBoxParameters>({
100102
title,
101103
step,
@@ -131,8 +133,9 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
131133
acceptFilterBoxTextAsSelection,
132134
onChangeItem,
133135
keepScrollPosition,
136+
onDidTriggerItemButton,
134137
initialize,
135-
}: P): Promise<MultiStepInputQuickPicResponseType<T, P>> {
138+
}: P): Promise<MultiStepInputQuickPickResponseType<T, P>> {
136139
const disposables: Disposable[] = [];
137140
const input = window.createQuickPick<T>();
138141
input.title = title;
@@ -161,7 +164,13 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
161164
initialize(input);
162165
}
163166
if (activeItem) {
164-
input.activeItems = [await activeItem];
167+
if (typeof activeItem === 'function') {
168+
activeItem(input).then((item) => {
169+
if (input.activeItems.length === 0) {
170+
input.activeItems = [item];
171+
}
172+
});
173+
}
165174
} else {
166175
input.activeItems = [];
167176
}
@@ -170,35 +179,46 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
170179
// so do it after initialization. This ensures quickpick starts with the active
171180
// item in focus when this is true, instead of having scroll position at top.
172181
input.keepScrollPosition = keepScrollPosition;
173-
try {
174-
return await new Promise<MultiStepInputQuickPicResponseType<T, P>>((resolve, reject) => {
175-
disposables.push(
176-
input.onDidTriggerButton(async (item) => {
177-
if (item === QuickInputButtons.Back) {
178-
reject(InputFlowAction.back);
179-
}
180-
if (customButtonSetups) {
181-
for (const customButtonSetup of customButtonSetups) {
182-
if (JSON.stringify(item) === JSON.stringify(customButtonSetup?.button)) {
183-
await customButtonSetup?.callback(input);
184-
}
185-
}
182+
183+
const deferred = createDeferred<T>();
184+
185+
disposables.push(
186+
input.onDidTriggerButton(async (item) => {
187+
if (item === QuickInputButtons.Back) {
188+
deferred.reject(InputFlowAction.back);
189+
input.hide();
190+
}
191+
if (customButtonSetups) {
192+
for (const customButtonSetup of customButtonSetups) {
193+
if (JSON.stringify(item) === JSON.stringify(customButtonSetup?.button)) {
194+
await customButtonSetup?.callback(input);
186195
}
187-
}),
188-
input.onDidChangeSelection((selectedItems) => resolve(selectedItems[0])),
189-
input.onDidHide(() => {
190-
resolve(undefined);
191-
}),
192-
);
193-
if (acceptFilterBoxTextAsSelection) {
194-
disposables.push(
195-
input.onDidAccept(() => {
196-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
197-
resolve(input.value as any);
198-
}),
199-
);
196+
}
200197
}
201-
});
198+
}),
199+
input.onDidChangeSelection((selectedItems) => deferred.resolve(selectedItems[0])),
200+
input.onDidHide(() => {
201+
if (!deferred.completed) {
202+
deferred.resolve(undefined);
203+
}
204+
}),
205+
input.onDidTriggerItemButton(async (item) => {
206+
if (onDidTriggerItemButton) {
207+
await onDidTriggerItemButton(item);
208+
}
209+
}),
210+
);
211+
if (acceptFilterBoxTextAsSelection) {
212+
disposables.push(
213+
input.onDidAccept(() => {
214+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
215+
deferred.resolve(input.value as any);
216+
}),
217+
);
218+
}
219+
220+
try {
221+
return await deferred.promise;
202222
} finally {
203223
disposables.forEach((d) => d.dispose());
204224
}
@@ -283,6 +303,9 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
283303
if (err === InputFlowAction.back) {
284304
this.steps.pop();
285305
step = this.steps.pop();
306+
if (step === undefined) {
307+
throw err;
308+
}
286309
} else if (err === InputFlowAction.resume) {
287310
step = this.steps.pop();
288311
} else if (err === InputFlowAction.cancel) {
@@ -297,6 +320,7 @@ export class MultiStepInput<S> implements IMultiStepInput<S> {
297320
}
298321
}
299322
}
323+
300324
export const IMultiStepInputFactory = Symbol('IMultiStepInputFactory');
301325
export interface IMultiStepInputFactory {
302326
create<S>(): IMultiStepInput<S>;

src/extension/common/utils/async.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface Deferred<T> {
1111
readonly rejected: boolean;
1212
readonly completed: boolean;
1313
resolve(value?: T | PromiseLike<T>): void;
14-
reject(reason?: string | Error | Record<string, unknown>): void;
14+
reject(reason?: string | Error | Record<string, unknown> | unknown): void;
1515
}
1616

1717
class DeferredImpl<T> implements Deferred<T> {

src/extension/common/utils/localize.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ export namespace DebugConfigStrings {
2222
label: l10n.t('Python Debugger'),
2323
description: l10n.t('Select a Python Debugger debug configuration'),
2424
};
25+
export const browsePath = {
26+
label: l10n.t('Browse Files...'),
27+
detail: l10n.t('Browse your file system to find a Python file.'),
28+
openButtonLabel: l10n.t('Select File'),
29+
title: l10n.t('Select Python File'),
30+
};
2531
export namespace file {
2632
export const snippet = {
2733
name: l10n.t('Python Debugger: Current File'),
@@ -92,12 +98,11 @@ export namespace DebugConfigStrings {
9298
label: l10n.t('Django'),
9399
description: l10n.t('Launch and debug a Django web application'),
94100
};
95-
export const enterManagePyPath = {
101+
export const djangoConfigPromp = {
96102
title: l10n.t('Debug Django'),
97103
prompt: l10n.t(
98-
"Enter the path to manage.py ('${workspaceFolderToken}' points to the root of the current workspace folder)",
104+
"Enter the path to manage.py or select a file from the list ('${workspaceFolderToken}' points to the root of the current workspace folder)",
99105
),
100-
invalid: l10n.t('Enter a valid Python file path'),
101106
};
102107
}
103108
export namespace fastapi {

src/extension/debugger/configuration/debugConfigurationService.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ export class PythonDebugConfigurationService implements IDebugConfigurationServi
4242
const config: Partial<DebugConfigurationArguments> = {};
4343
const state = { config, folder, token };
4444

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

src/extension/debugger/configuration/dynamicdebugConfigurationService.ts

Lines changed: 9 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
'use strict';
66

77
import * as path from 'path';
8-
import * as fs from 'fs-extra';
98
import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode';
109
import { IDynamicDebugConfigurationService } from '../types';
11-
import { asyncFilter } from '../../common/utilities';
1210
import { DebuggerTypeName } from '../../constants';
1311
import { replaceAll } from '../../common/stringUtils';
12+
import { getDjangoPaths, getFastApiPaths, getFlaskPaths } from './utils/configuration';
1413

1514
const workspaceFolderToken = '${workspaceFolder}';
1615

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

32-
const djangoManagePath = await DynamicPythonDebugConfigurationService.getDjangoPath(folder);
31+
const djangoManagePaths = await getDjangoPaths(folder);
32+
const djangoManagePath = djangoManagePaths?.length
33+
? path.relative(folder.uri.fsPath, djangoManagePaths[0].fsPath)
34+
: null;
3335
if (djangoManagePath) {
3436
providers.push({
3537
name: 'Python Debugger: Django',
@@ -41,7 +43,8 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf
4143
});
4244
}
4345

44-
const flaskPath = await DynamicPythonDebugConfigurationService.getFlaskPath(folder);
46+
const flaskPaths = await getFlaskPaths(folder);
47+
const flaskPath = flaskPaths?.length ? flaskPaths[0].fsPath : null;
4548
if (flaskPath) {
4649
providers.push({
4750
name: 'Python Debugger: Flask',
@@ -57,7 +60,8 @@ export class DynamicPythonDebugConfigurationService implements IDynamicDebugConf
5760
});
5861
}
5962

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

7377
return providers;
7478
}
75-
76-
private static async getDjangoPath(folder: WorkspaceFolder) {
77-
const regExpression = /execute_from_command_line\(/;
78-
const possiblePaths = await DynamicPythonDebugConfigurationService.getPossiblePaths(
79-
folder,
80-
['manage.py', '*/manage.py', 'app.py', '*/app.py'],
81-
regExpression,
82-
);
83-
return possiblePaths.length ? path.relative(folder.uri.fsPath, possiblePaths[0]) : null;
84-
}
85-
86-
private static async getFastApiPath(folder: WorkspaceFolder) {
87-
const regExpression = /app\s*=\s*FastAPI\(/;
88-
const fastApiPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths(
89-
folder,
90-
['main.py', 'app.py', '*/main.py', '*/app.py', '*/*/main.py', '*/*/app.py'],
91-
regExpression,
92-
);
93-
94-
return fastApiPaths.length ? fastApiPaths[0] : null;
95-
}
96-
97-
private static async getFlaskPath(folder: WorkspaceFolder) {
98-
const regExpression = /app(?:lication)?\s*=\s*(?:flask\.)?Flask\(|def\s+(?:create|make)_app\(/;
99-
const flaskPaths = await DynamicPythonDebugConfigurationService.getPossiblePaths(
100-
folder,
101-
['__init__.py', 'app.py', 'wsgi.py', '*/__init__.py', '*/app.py', '*/wsgi.py'],
102-
regExpression,
103-
);
104-
105-
return flaskPaths.length ? flaskPaths[0] : null;
106-
}
107-
108-
private static async getPossiblePaths(
109-
folder: WorkspaceFolder,
110-
globPatterns: string[],
111-
regex: RegExp,
112-
): Promise<string[]> {
113-
const foundPathsPromises = (await Promise.allSettled(
114-
globPatterns.map(
115-
async (pattern): Promise<string[]> =>
116-
(await fs.pathExists(path.join(folder.uri.fsPath, pattern)))
117-
? [path.join(folder.uri.fsPath, pattern)]
118-
: [],
119-
),
120-
)) as { status: string; value: [] }[];
121-
const possiblePaths: string[] = [];
122-
foundPathsPromises.forEach((result) => possiblePaths.push(...result.value));
123-
const finalPaths = await asyncFilter(possiblePaths, async (possiblePath) =>
124-
regex.exec((await fs.readFile(possiblePath)).toString()),
125-
);
126-
127-
return finalPaths;
128-
}
12979
}

0 commit comments

Comments
 (0)