Skip to content

Commit c493f58

Browse files
committed
cleanup
1 parent 15bbb21 commit c493f58

File tree

3 files changed

+133
-116
lines changed

3 files changed

+133
-116
lines changed

src/common/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ export const KNOWN_FILES = [
2525
];
2626

2727
export const KNOWN_TEMPLATE_ENDINGS = ['.j2', '.jinja2'];
28+
29+
export const NEW_PROJECT_TEMPLATES_FOLDER = path.join(EXTENSION_ROOT_DIR, 'src', 'features', 'creators', 'templates');

src/features/creators/creationHelpers.ts

+75-34
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ import * as fs from 'fs-extra';
33
import * as path from 'path';
44
import { EnvironmentManagers, InternalEnvironmentManager } from '../../internal.api';
55
import { CreateEnvironmentOptions } from '../../api';
6-
import { traceVerbose } from '../../common/logging';
6+
import { traceError, traceVerbose } from '../../common/logging';
77
import { showQuickPickWithButtons } from '../../common/window.apis';
88

99
/**
10-
* Prompts the user to choose whether to create a new virtual environment (venv) for a package, with a clearer return and early exit.
10+
* 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
*/
1313
export async function promptForVenv(): Promise<boolean | undefined> {
1414
const venvChoice = await showQuickPickWithButtons([{ label: 'Yes' }, { label: 'No' }], {
15-
placeHolder: 'Would you like to create a new virtual environment for this package?',
15+
placeHolder: 'Would you like to create a new virtual environment for this project?',
1616
ignoreFocusOut: true,
1717
showBackButton: true,
1818
});
@@ -57,20 +57,6 @@ export async function promptForCopilotInstructions(): Promise<boolean | undefine
5757
return copilotChoice.label === 'Yes';
5858
}
5959

60-
/**
61-
* Removes the .github Copilot instructions folder from the specified destination folder, if it exists.
62-
* @param destFolder - The absolute path to the destination folder where the .github folder may exist.
63-
* @returns {Promise<void>} Resolves when the folder is removed or if it does not exist.
64-
*/
65-
export async function removeCopilotInstructions(destFolder: string) {
66-
const copilotFolder = path.join(destFolder, '.github');
67-
if (await fs.pathExists(copilotFolder)) {
68-
await fs.remove(copilotFolder);
69-
}
70-
}
71-
72-
export async function insertCopilotInstructions() {}
73-
export async function insertLaunchJson() {}
7460

7561
/**
7662
* Quickly creates a new Python virtual environment (venv) in the specified destination folder using the available environment managers.
@@ -104,23 +90,7 @@ export async function quickCreateNewVenv(envManagers: EnvironmentManagers, destF
10490
}
10591
}
10692

107-
/**
108-
* Replaces all occurrences of a string in a single file's contents, safely handling special regex characters in the search value.
109-
* @param filePath - The absolute path to the file to update.
110-
* @param searchValue - The string to search for (will be escaped for regex).
111-
* @param replaceValue - The string to replace with.
112-
* @returns {Promise<void>} Resolves when the file has been updated.
113-
*/
114-
export async function replaceInFile(filePath: string, searchValue: string, replaceValue: string) {
115-
// Escape special regex characters in searchValue
116-
const escapedSearchValue = searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
117-
const regex = new RegExp(escapedSearchValue, 'g');
118-
let content = await fs.readFile(filePath, 'utf8');
119-
if (content.includes(searchValue)) {
120-
content = content.replace(regex, replaceValue);
121-
await fs.writeFile(filePath, content, 'utf8');
122-
}
123-
}
93+
12494

12595
/**
12696
* Recursively replaces all occurrences of a string in file and folder names, as well as file contents, within a directory tree.
@@ -153,3 +123,74 @@ export async function replaceInFilesAndNames(dir: string, searchValue: string, r
153123
}
154124
}
155125
}
126+
127+
/**
128+
* Ensures the .github/copilot-instructions.md file exists at the given root, creating or appending as needed.
129+
* @param destinationRootPath - The root directory where the .github folder should exist.
130+
* @param instructionsText - The text to write or append to the copilot-instructions.md file.
131+
*/
132+
export async function manageCopilotInstructionsFile(
133+
destinationRootPath: string,
134+
packageName: string,
135+
instructionsFilePath: string,
136+
) {
137+
const instructionsText = `\n \n` + (await fs.readFile(instructionsFilePath, 'utf-8'));
138+
const githubFolderPath = path.join(destinationRootPath, '.github');
139+
const customInstructionsPath = path.join(githubFolderPath, 'copilot-instructions.md');
140+
if (!(await fs.pathExists(githubFolderPath))) {
141+
// make the .github folder if it doesn't exist
142+
await fs.mkdir(githubFolderPath);
143+
}
144+
const customInstructions = await fs.pathExists(customInstructionsPath);
145+
if (customInstructions) {
146+
// Append to the existing file
147+
await fs.appendFile(customInstructionsPath, instructionsText.replace('<package_name>', packageName));
148+
} else {
149+
// Create the file if it doesn't exist
150+
await fs.writeFile(customInstructionsPath, instructionsText.replace('<package_name>', packageName));
151+
}
152+
}
153+
154+
/**
155+
* Appends a configuration object to the configurations array in a launch.json file.
156+
* @param launchJsonPath - The absolute path to the launch.json file.
157+
* @param projectLaunchConfig - The stringified JSON config to append.
158+
*/
159+
async function appendToJsonConfigs(launchJsonPath: string, projectLaunchConfig: string) {
160+
let content = await fs.readFile(launchJsonPath, 'utf8');
161+
const json = JSON.parse(content);
162+
// If it's a VS Code launch config, append to configurations array
163+
if (json && Array.isArray(json.configurations)) {
164+
const configObj = JSON.parse(projectLaunchConfig);
165+
json.configurations.push(configObj);
166+
await fs.writeFile(launchJsonPath, JSON.stringify(json, null, 4), 'utf8');
167+
} else {
168+
traceError('Failed to add Project Launch Config to launch.json.');
169+
return;
170+
}
171+
}
172+
173+
/**
174+
* Updates the launch.json file in the .vscode folder to include the provided project launch configuration.
175+
* @param destinationRootPath - The root directory where the .vscode folder should exist.
176+
* @param projectLaunchConfig - The stringified JSON config to append.
177+
*/
178+
export async function manageLaunchJsonFile(destinationRootPath: string, projectLaunchConfig: string) {
179+
const vscodeFolderPath = path.join(destinationRootPath, '.vscode');
180+
const launchJsonPath = path.join(vscodeFolderPath, 'launch.json');
181+
if (!(await fs.pathExists(vscodeFolderPath))) {
182+
await fs.mkdir(vscodeFolderPath);
183+
}
184+
const launchJsonExists = await fs.pathExists(launchJsonPath);
185+
if (launchJsonExists) {
186+
// Try to parse and append to existing launch.json
187+
await appendToJsonConfigs(launchJsonPath, projectLaunchConfig);
188+
} else {
189+
// Create a new launch.json with the provided config
190+
const launchJson = {
191+
version: '0.2.0',
192+
configurations: [JSON.parse(projectLaunchConfig)],
193+
};
194+
await fs.writeFile(launchJsonPath, JSON.stringify(launchJson, null, 4), 'utf8');
195+
}
196+
}

src/features/creators/newPackageProject.ts

+56-82
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import {
77
promptForCopilotInstructions,
88
isCopilotInstalled,
99
quickCreateNewVenv,
10-
removeCopilotInstructions,
1110
replaceInFilesAndNames,
12-
replaceInFile,
11+
manageCopilotInstructionsFile,
12+
manageLaunchJsonFile,
1313
} from './creationHelpers';
14-
import { EXTENSION_ROOT_DIR } from '../../common/constants';
14+
import { NEW_PROJECT_TEMPLATES_FOLDER } from '../../common/constants';
1515
import { EnvironmentManagers } from '../../internal.api';
1616
import { showInputBoxWithButtons } from '../../common/window.apis';
1717

@@ -23,13 +23,16 @@ export class NewPackageProject implements PythonProjectCreator {
2323

2424
constructor(private readonly envManagers: EnvironmentManagers) {}
2525

26-
async create(_options?: PythonProjectCreatorOptions): Promise<PythonProject | undefined> {
27-
// Prompt for package name (TODO: this doesn't make sense if the _options is already being passed in )
28-
const packageName = await showInputBoxWithButtons({
29-
prompt: 'What is the name of the package? (e.g. my_package)',
30-
ignoreFocusOut: true,
31-
showBackButton: true,
32-
});
26+
async create(options?: PythonProjectCreatorOptions): Promise<PythonProject | undefined> {
27+
// Prompt for package name if not provided
28+
let packageName = options?.name;
29+
if (!packageName) {
30+
packageName = await showInputBoxWithButtons({
31+
prompt: 'What is the name of the package? (e.g. my_package)',
32+
ignoreFocusOut: true,
33+
showBackButton: true,
34+
});
35+
}
3336
if (!packageName) {
3437
return undefined;
3538
}
@@ -55,102 +58,73 @@ export class NewPackageProject implements PythonProjectCreator {
5558
);
5659

5760
// 1. Copy template folder
58-
const templateFolder = path.join(
59-
EXTENSION_ROOT_DIR,
60-
'src',
61-
'features',
62-
'creators',
63-
'templates',
64-
'newPackageTemplate',
65-
);
66-
if (!(await fs.pathExists(templateFolder))) {
67-
window.showErrorMessage('Template folder does not exist.');
61+
const newPackageTemplateFolder = path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'newPackageTemplate');
62+
if (!(await fs.pathExists(newPackageTemplateFolder))) {
63+
window.showErrorMessage('Template folder does not exist, aborting creation.');
6864
return undefined;
69-
// might need another check or error handling here
7065
}
71-
const workspaceFolders = workspace.workspaceFolders;
72-
if (!workspaceFolders || workspaceFolders.length === 0) {
73-
window.showErrorMessage('No workspace folder is open.');
74-
return undefined;
66+
67+
// Check if the destination folder is provided, otherwise use the first workspace folder
68+
let destRoot = options?.uri?.fsPath;
69+
if (!destRoot) {
70+
const workspaceFolders = workspace.workspaceFolders;
71+
if (!workspaceFolders || workspaceFolders.length === 0) {
72+
window.showErrorMessage('No workspace folder is open or provided, aborting creation.');
73+
return undefined;
74+
}
75+
destRoot = workspaceFolders[0].uri.fsPath;
7576
}
76-
const destRoot = workspaceFolders[0].uri.fsPath; // this doesn't seem right...
77+
7778
// Check if the destination folder already exists
78-
const destFolder = path.join(destRoot, `${packageName}_project`);
79-
if (await fs.pathExists(destFolder)) {
80-
window.showErrorMessage('A project folder by that name already exists, aborting.');
79+
const projectDestinationFolder = path.join(destRoot, `${packageName}_project`);
80+
if (await fs.pathExists(projectDestinationFolder)) {
81+
window.showErrorMessage(
82+
'A project folder by that name already exists, aborting creation. Please retry with a unique package name given your workspace.',
83+
);
8184
return undefined;
8285
}
83-
await fs.copy(templateFolder, destFolder);
84-
85-
// custom instructions
86-
const instructionsTextPath = path.join(
87-
EXTENSION_ROOT_DIR,
88-
'src',
89-
'features',
90-
'creators',
91-
'templates',
92-
'copilot-instructions-text',
93-
'package-copilot-instructions.md',
94-
);
95-
const instructionsText = `\n \n` + (await fs.readFile(instructionsTextPath, 'utf-8'));
96-
97-
// check to see if .github folder exists
98-
const githubFolderPath = path.join(destRoot, '.github');
99-
const customInstructionsPath = path.join(githubFolderPath, 'copilot-instructions.md');
100-
const ghFolder = await fs.pathExists(githubFolderPath);
101-
if (ghFolder) {
102-
const customInstructions = await fs.pathExists(customInstructionsPath);
103-
if (customInstructions) {
104-
// Append to the existing file
105-
await fs.appendFile(customInstructionsPath, instructionsText);
106-
} else {
107-
// Create the file if it doesn't exist
108-
await fs.writeFile(customInstructionsPath, instructionsText);
109-
}
110-
} else {
111-
// Create the .github folder and the file
112-
await fs.mkdir(githubFolderPath);
113-
await fs.writeFile(customInstructionsPath, instructionsText);
114-
}
86+
await fs.copy(newPackageTemplateFolder, projectDestinationFolder);
11587

116-
// 2. Replace <package_name> in all files and file/folder names using helper
117-
await replaceInFilesAndNames(destFolder, 'package_name', packageName);
118-
119-
// 3. Remove Copilot instructions folder if needed
120-
if (!createCopilotInstructions) {
121-
await removeCopilotInstructions(destFolder);
122-
}
88+
// 2. Replace 'package_name' in all files and file/folder names using a helper
89+
await replaceInFilesAndNames(projectDestinationFolder, 'package_name', packageName);
12390

12491
// 4. Create virtual environment if requested
12592
if (createVenv) {
126-
await quickCreateNewVenv(this.envManagers, destFolder);
93+
await quickCreateNewVenv(this.envManagers, projectDestinationFolder);
12794
}
12895

12996
// 5. Get the Python environment for the destination folder
130-
// could be either the one created in step 4 or an existing one
131-
const pythonEnvironment = await this.envManagers.getEnvironment(Uri.parse(destFolder));
97+
// could be either the one created in an early step or an existing one
98+
const pythonEnvironment = await this.envManagers.getEnvironment(Uri.parse(projectDestinationFolder));
13299

133-
// 6. Replace <run_exec> and <activation_command> in README.md
134-
// const readmeFilePath = path.join(destFolder, 'README.md');
135100
if (!pythonEnvironment) {
136101
window.showErrorMessage('Python environment not found.');
137102
return undefined;
138103
}
139-
const execInfo = pythonEnvironment.execInfo;
140-
if (execInfo.run) {
141-
// const { executable, args = [] } = execInfo.run;
142-
// const execRunStr = [executable, ...args].join(' ');
143-
// TODO: check this as I don't think I need this anymore
144-
await replaceInFile(customInstructionsPath, '<package_name>', packageName);
104+
105+
// add custom github copilot instructions
106+
if (createCopilotInstructions) {
107+
const packageInstructionsPath = path.join(
108+
NEW_PROJECT_TEMPLATES_FOLDER,
109+
'copilot-instructions-text',
110+
'package-copilot-instructions.md',
111+
);
112+
await manageCopilotInstructionsFile(destRoot, packageName, packageInstructionsPath);
145113
}
146114

147-
// TODO: insert copilot instructions text into the copilot instructions file
148-
// TODO: insert configs into existing launch.json file
115+
// update launch.json file with config for the package
116+
const launchJsonConfig = {
117+
name: `Python Package: ${packageName}`,
118+
type: 'debugpy',
119+
request: 'launch',
120+
module: packageName,
121+
};
122+
await manageLaunchJsonFile(destRoot, JSON.stringify(launchJsonConfig));
149123

150124
// Return a PythonProject OR Uri (if no venv was created)
151125
return {
152126
name: packageName,
153-
uri: Uri.file(destFolder),
127+
uri: Uri.file(projectDestinationFolder),
154128
};
155129
}
156130
}

0 commit comments

Comments
 (0)