Skip to content

Commit 77ff0f7

Browse files
committed
add newProjectSelection
1 parent 060c440 commit 77ff0f7

File tree

6 files changed

+158
-116
lines changed

6 files changed

+158
-116
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,12 @@
209209
"category": "Python Envs",
210210
"icon": "$(terminal)"
211211
},
212+
{
213+
"command": "python-envs.createNewProjectFromTemplate",
214+
"title": "%python-envs.createNewProjectFromTemplate.title%",
215+
"category": "Python Envs",
216+
"icon": "$(play)"
217+
},
212218
{
213219
"command": "python-envs.runAsTask",
214220
"title": "%python-envs.runAsTask.title%",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"python-envs.runInTerminal.title": "Run in Terminal",
3232
"python-envs.createTerminal.title": "Create Python Terminal",
3333
"python-envs.runAsTask.title": "Run as Task",
34+
"python-envs.createNewProjectFromTemplate.title": "Create New Project from Template",
3435
"python-envs.terminal.activate.title": "Activate Environment in Current Terminal",
3536
"python-envs.terminal.deactivate.title": "Deactivate Environment in Current Terminal",
3637
"python-envs.uninstallPackage.title": "Uninstall Package"

src/common/pickers/managers.ts

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { QuickPickItem, QuickPickItemKind } from 'vscode';
1+
import { commands, QuickInputButtons, QuickPickItem, QuickPickItemKind } from 'vscode';
22
import { PythonProjectCreator } from '../../api';
33
import { InternalEnvironmentManager, InternalPackageManager } from '../../internal.api';
44
import { Common, Pickers } from '../localize';
@@ -137,7 +137,6 @@ export async function pickCreator(creators: PythonProjectCreator[]): Promise<Pyt
137137
// First level menu
138138
const autoFindCreator = creators.find((c) => c.name === 'autoProjects');
139139
const existingProjectsCreator = creators.find((c) => c.name === 'existingProjects');
140-
const otherCreators = creators.filter((c) => c.name !== 'autoProjects' && c.name !== 'existingProjects');
141140

142141
const items: QuickPickItem[] = [
143142
{
@@ -175,34 +174,47 @@ export async function pickCreator(creators: PythonProjectCreator[]): Promise<Pyt
175174
case 'Select Existing':
176175
return existingProjectsCreator;
177176
case 'Create New':
178-
// Show second level menu for other creators
179-
if (otherCreators.length === 0) {
180-
return undefined;
181-
}
182-
const newItems: (QuickPickItem & { c: PythonProjectCreator })[] = otherCreators.map((c) => ({
183-
label: c.displayName ?? c.name,
184-
description: c.description,
185-
c: c,
186-
}));
187-
const newSelected = await showQuickPickWithButtons(newItems, {
188-
placeHolder: 'Select project type for new project',
189-
ignoreFocusOut: true,
190-
showBackButton: true,
191-
});
192-
if (!newSelected) {
193-
// User cancelled the picker
194-
return undefined;
195-
}
196-
// Handle back button
197-
if ((newSelected as any)?.kind === -1 || (newSelected as any)?.back === true) {
198-
// User pressed the back button, re-show the first menu
199-
return pickCreator(creators);
200-
}
201-
202-
// Handle case where newSelected could be an array (should not happen, but for type safety)
203-
const selectedCreator = Array.isArray(newSelected) ? newSelected[0] : newSelected;
204-
return selectedCreator?.c;
177+
return newProjectSelection(creators);
205178
}
206179

207180
return undefined;
208181
}
182+
183+
export async function newProjectSelection(creators: PythonProjectCreator[]): Promise<PythonProjectCreator | undefined> {
184+
const otherCreators = creators.filter((c) => c.name !== 'autoProjects' && c.name !== 'existingProjects');
185+
186+
// Show second level menu for other creators
187+
if (otherCreators.length === 0) {
188+
return undefined;
189+
}
190+
const newItems: (QuickPickItem & { c: PythonProjectCreator })[] = otherCreators.map((c) => ({
191+
label: c.displayName ?? c.name,
192+
description: c.description,
193+
c: c,
194+
}));
195+
try {
196+
const newSelected = await showQuickPickWithButtons(newItems, {
197+
placeHolder: 'Select project type for new project',
198+
ignoreFocusOut: true,
199+
showBackButton: true,
200+
});
201+
202+
if (!newSelected) {
203+
// User cancelled the picker
204+
return undefined;
205+
}
206+
// Handle back button
207+
if ((newSelected as any)?.kind === -1 || (newSelected as any)?.back === true) {
208+
// User pressed the back button, re-show the first menu
209+
return pickCreator(creators);
210+
}
211+
212+
// Handle case where newSelected could be an array (should not happen, but for type safety)
213+
const selectedCreator = Array.isArray(newSelected) ? newSelected[0] : newSelected;
214+
return selectedCreator?.c;
215+
} catch (ex) {
216+
if (ex === QuickInputButtons.Back) {
217+
await commands.executeCommand('python-envs.addPythonProject');
218+
}
219+
}
220+
}

src/extension.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ensureCorrectVersion } from './common/extVersion';
55
import { registerTools } from './common/lm.apis';
66
import { registerLogger, traceError, traceInfo } from './common/logging';
77
import { setPersistentState } from './common/persistentState';
8+
import { newProjectSelection } from './common/pickers/managers';
89
import { StopWatch } from './common/stopWatch';
910
import { EventNames } from './common/telemetry/constants';
1011
import { sendManagerSelectionTelemetry } from './common/telemetry/helpers';
@@ -236,6 +237,12 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
236237
await terminalManager.deactivate(terminal);
237238
}
238239
}),
240+
commands.registerCommand('python-envs.createNewProjectFromTemplate', async () => {
241+
const selected = await newProjectSelection(projectCreators.getProjectCreators());
242+
if (selected) {
243+
await selected.create();
244+
}
245+
}),
239246
terminalActivation.onDidChangeTerminalActivationState(async (e) => {
240247
await setActivateMenuButtonContext(e.terminal, e.environment, e.activated);
241248
}),

src/features/creators/creationHelpers.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1-
import { window, extensions, Uri } from 'vscode';
21
import * as fs from 'fs-extra';
32
import * as path from 'path';
4-
import { EnvironmentManagers, InternalEnvironmentManager } from '../../internal.api';
3+
import { extensions, QuickInputButtons, Uri, window } from 'vscode';
54
import { CreateEnvironmentOptions } from '../../api';
65
import { traceError, traceVerbose } from '../../common/logging';
76
import { showQuickPickWithButtons } from '../../common/window.apis';
7+
import { EnvironmentManagers, InternalEnvironmentManager } from '../../internal.api';
88

99
/**
1010
* Prompts the user to choose whether to create a new virtual environment (venv) for a project, with a clearer return and early exit.
1111
* @returns {Promise<boolean | undefined>} Resolves to true if 'Yes' is selected, false if 'No', or undefined if cancelled.
1212
*/
13-
export async function promptForVenv(): Promise<boolean | undefined> {
13+
export async function promptForVenv(callback: () => void): Promise<boolean | undefined> {
14+
try {
15+
} catch (ex) {
16+
if (ex === QuickInputButtons.Back) {
17+
callback();
18+
}
19+
}
1420
const venvChoice = await showQuickPickWithButtons([{ label: 'Yes' }, { label: 'No' }], {
1521
placeHolder: 'Would you like to create a new virtual environment for this project?',
1622
ignoreFocusOut: true,
@@ -57,7 +63,6 @@ export async function promptForCopilotInstructions(): Promise<boolean | undefine
5763
return copilotChoice.label === 'Yes';
5864
}
5965

60-
6166
/**
6267
* Quickly creates a new Python virtual environment (venv) in the specified destination folder using the available environment managers.
6368
* Attempts to use the venv manager if available, otherwise falls back to any manager that supports environment creation.
@@ -90,8 +95,6 @@ export async function quickCreateNewVenv(envManagers: EnvironmentManagers, destF
9095
}
9196
}
9297

93-
94-
9598
/**
9699
* Recursively replaces all occurrences of a string in file and folder names, as well as file contents, within a directory tree.
97100
* @param dir - The root directory to start the replacement from.
Lines changed: 94 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as fs from 'fs-extra';
22
import * as path from 'path';
3-
import { MarkdownString, Uri, window, workspace } from 'vscode';
3+
import { commands, MarkdownString, QuickInputButtons, Uri, window, workspace } from 'vscode';
44
import { PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api';
55
import { NEW_PROJECT_TEMPLATES_FOLDER } from '../../common/constants';
66
import { showInputBoxWithButtons } from '../../common/window.apis';
@@ -37,101 +37,114 @@ export class NewPackageProject implements PythonProjectCreator {
3737
} else {
3838
//Prompt as quickCreate is false
3939
if (!packageName) {
40-
packageName = await showInputBoxWithButtons({
41-
prompt: 'What is the name of the package? (e.g. my_package)',
42-
ignoreFocusOut: true,
43-
showBackButton: true,
44-
});
45-
}
46-
if (!packageName) {
47-
return undefined;
40+
try {
41+
packageName = await showInputBoxWithButtons({
42+
prompt: 'What is the name of the package? (e.g. my_package)',
43+
ignoreFocusOut: true,
44+
showBackButton: true,
45+
});
46+
} catch (ex) {
47+
if (ex === QuickInputButtons.Back) {
48+
await commands.executeCommand('python-envs.createNewProjectFromTemplate');
49+
}
50+
}
51+
if (!packageName) {
52+
return undefined;
53+
}
54+
// Use helper to prompt for virtual environment creation
55+
const callback = () => {
56+
if (options) {
57+
return this.create(options);
58+
} else {
59+
return this.create({ name: packageName } as PythonProjectCreatorOptions);
60+
}
61+
};
62+
createVenv = await promptForVenv(callback);
63+
if (createVenv === undefined) {
64+
return undefined;
65+
}
66+
if (isCopilotInstalled()) {
67+
const copilotResult = await promptForCopilotInstructions();
68+
if (copilotResult === undefined) {
69+
return undefined;
70+
}
71+
createCopilotInstructions = copilotResult === true;
72+
}
4873
}
49-
// Use helper to prompt for virtual environment creation
50-
createVenv = await promptForVenv();
51-
if (createVenv === undefined) {
74+
75+
window.showInformationMessage(
76+
`Creating a new Python project: ${packageName}\nvenv: ${createVenv}\nCopilot instructions: ${createCopilotInstructions}`,
77+
);
78+
79+
// 1. Copy template folder
80+
const newPackageTemplateFolder = path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'newPackageTemplate');
81+
if (!(await fs.pathExists(newPackageTemplateFolder))) {
82+
window.showErrorMessage('Template folder does not exist, aborting creation.');
5283
return undefined;
5384
}
54-
if (isCopilotInstalled()) {
55-
const copilotResult = await promptForCopilotInstructions();
56-
if (copilotResult === undefined) {
85+
86+
// Check if the destination folder is provided, otherwise use the first workspace folder
87+
let destRoot = options?.rootUri.fsPath;
88+
if (!destRoot) {
89+
const workspaceFolders = workspace.workspaceFolders;
90+
if (!workspaceFolders || workspaceFolders.length === 0) {
91+
window.showErrorMessage('No workspace folder is open or provided, aborting creation.');
5792
return undefined;
5893
}
59-
createCopilotInstructions = copilotResult === true;
94+
destRoot = workspaceFolders[0].uri.fsPath;
6095
}
61-
}
62-
63-
window.showInformationMessage(
64-
`Creating a new Python project: ${packageName}\nvenv: ${createVenv}\nCopilot instructions: ${createCopilotInstructions}`,
65-
);
6696

67-
// 1. Copy template folder
68-
const newPackageTemplateFolder = path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'newPackageTemplate');
69-
if (!(await fs.pathExists(newPackageTemplateFolder))) {
70-
window.showErrorMessage('Template folder does not exist, aborting creation.');
71-
return undefined;
72-
}
73-
74-
// Check if the destination folder is provided, otherwise use the first workspace folder
75-
let destRoot = options?.rootUri.fsPath;
76-
if (!destRoot) {
77-
const workspaceFolders = workspace.workspaceFolders;
78-
if (!workspaceFolders || workspaceFolders.length === 0) {
79-
window.showErrorMessage('No workspace folder is open or provided, aborting creation.');
97+
// Check if the destination folder already exists
98+
const projectDestinationFolder = path.join(destRoot, `${packageName}_project`);
99+
if (await fs.pathExists(projectDestinationFolder)) {
100+
window.showErrorMessage(
101+
'A project folder by that name already exists, aborting creation. Please retry with a unique package name given your workspace.',
102+
);
80103
return undefined;
81104
}
82-
destRoot = workspaceFolders[0].uri.fsPath;
83-
}
84-
85-
// Check if the destination folder already exists
86-
const projectDestinationFolder = path.join(destRoot, `${packageName}_project`);
87-
if (await fs.pathExists(projectDestinationFolder)) {
88-
window.showErrorMessage(
89-
'A project folder by that name already exists, aborting creation. Please retry with a unique package name given your workspace.',
90-
);
91-
return undefined;
92-
}
93-
await fs.copy(newPackageTemplateFolder, projectDestinationFolder);
105+
await fs.copy(newPackageTemplateFolder, projectDestinationFolder);
94106

95-
// 2. Replace 'package_name' in all files and file/folder names using a helper
96-
await replaceInFilesAndNames(projectDestinationFolder, 'package_name', packageName);
107+
// 2. Replace 'package_name' in all files and file/folder names using a helper
108+
await replaceInFilesAndNames(projectDestinationFolder, 'package_name', packageName);
97109

98-
// 4. Create virtual environment if requested
99-
if (createVenv) {
100-
await quickCreateNewVenv(this.envManagers, projectDestinationFolder);
101-
}
110+
// 4. Create virtual environment if requested
111+
if (createVenv) {
112+
await quickCreateNewVenv(this.envManagers, projectDestinationFolder);
113+
}
102114

103-
// 5. Get the Python environment for the destination folder
104-
// could be either the one created in an early step or an existing one
105-
const pythonEnvironment = await this.envManagers.getEnvironment(Uri.parse(projectDestinationFolder));
115+
// 5. Get the Python environment for the destination folder
116+
// could be either the one created in an early step or an existing one
117+
const pythonEnvironment = await this.envManagers.getEnvironment(Uri.parse(projectDestinationFolder));
106118

107-
if (!pythonEnvironment) {
108-
window.showErrorMessage('Python environment not found.');
109-
return undefined;
110-
}
119+
if (!pythonEnvironment) {
120+
window.showErrorMessage('Python environment not found.');
121+
return undefined;
122+
}
111123

112-
// add custom github copilot instructions
113-
if (createCopilotInstructions) {
114-
const packageInstructionsPath = path.join(
115-
NEW_PROJECT_TEMPLATES_FOLDER,
116-
'copilot-instructions-text',
117-
'package-copilot-instructions.md',
118-
);
119-
await manageCopilotInstructionsFile(destRoot, packageName, packageInstructionsPath);
120-
}
124+
// add custom github copilot instructions
125+
if (createCopilotInstructions) {
126+
const packageInstructionsPath = path.join(
127+
NEW_PROJECT_TEMPLATES_FOLDER,
128+
'copilot-instructions-text',
129+
'package-copilot-instructions.md',
130+
);
131+
await manageCopilotInstructionsFile(destRoot, packageName, packageInstructionsPath);
132+
}
121133

122-
// update launch.json file with config for the package
123-
const launchJsonConfig = {
124-
name: `Python Package: ${packageName}`,
125-
type: 'debugpy',
126-
request: 'launch',
127-
module: packageName,
128-
};
129-
await manageLaunchJsonFile(destRoot, JSON.stringify(launchJsonConfig));
134+
// update launch.json file with config for the package
135+
const launchJsonConfig = {
136+
name: `Python Package: ${packageName}`,
137+
type: 'debugpy',
138+
request: 'launch',
139+
module: packageName,
140+
};
141+
await manageLaunchJsonFile(destRoot, JSON.stringify(launchJsonConfig));
130142

131-
// Return a PythonProject OR Uri (if no venv was created)
132-
return {
133-
name: packageName,
134-
uri: Uri.file(projectDestinationFolder),
135-
};
143+
// Return a PythonProject OR Uri (if no venv was created)
144+
return {
145+
name: packageName,
146+
uri: Uri.file(projectDestinationFolder),
147+
};
148+
}
136149
}
137150
}

0 commit comments

Comments
 (0)