-
Notifications
You must be signed in to change notification settings - Fork 13
New package Flow #324
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
Merged
eleanorjboyd
merged 22 commits into
microsoft:create-project-branch
from
eleanorjboyd:new-package
May 19, 2025
Merged
New package Flow #324
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
ffba068
Adding menu items (#310)
eleanorjboyd 6d6e25d
new package support for new project
eleanorjboyd f31d3cf
update and add TODO notes
eleanorjboyd 98341dc
updates
eleanorjboyd 470db8b
create venv support
eleanorjboyd 7238114
minor
eleanorjboyd c7cbd4d
minor edits
eleanorjboyd 8e4680f
edits first pass
eleanorjboyd 7449d5e
updates based on feedback
eleanorjboyd 432d7f5
updates
eleanorjboyd 98e80c2
cleanup
eleanorjboyd 4856c1d
remove UV creator
eleanorjboyd 060c440
support quick create
eleanorjboyd 77ff0f7
add newProjectSelection
eleanorjboyd 7b2f915
handle callback correctly for back button
eleanorjboyd 3f43c8a
touchups
eleanorjboyd 9f35db4
Adding menu items (#310)
eleanorjboyd f027893
use l10n
eleanorjboyd 1c04a7a
add project manager add to create function
eleanorjboyd bd4cde8
Merge branch 'create-project-branch' into new-package
eleanorjboyd 9080a9b
fix errors from merging
eleanorjboyd d0e7390
updates to pyproject.toml
eleanorjboyd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
import * as fs from 'fs-extra'; | ||
import * as path from 'path'; | ||
import { extensions, l10n, QuickInputButtons, Uri, window } from 'vscode'; | ||
import { CreateEnvironmentOptions } from '../../api'; | ||
import { traceError, traceVerbose } from '../../common/logging'; | ||
import { showQuickPickWithButtons } from '../../common/window.apis'; | ||
import { EnvironmentManagers, InternalEnvironmentManager } from '../../internal.api'; | ||
|
||
/** | ||
* Prompts the user to choose whether to create a new virtual environment (venv) for a project, with a clearer return and early exit. | ||
* @returns {Promise<boolean | undefined>} Resolves to true if 'Yes' is selected, false if 'No', or undefined if cancelled. | ||
*/ | ||
export async function promptForVenv(callback: () => void): Promise<boolean | undefined> { | ||
try { | ||
const venvChoice = await showQuickPickWithButtons([{ label: l10n.t('Yes') }, { label: l10n.t('No') }], { | ||
placeHolder: l10n.t('Would you like to create a new virtual environment for this project?'), | ||
ignoreFocusOut: true, | ||
showBackButton: true, | ||
}); | ||
if (!venvChoice) { | ||
return undefined; | ||
} | ||
if (Array.isArray(venvChoice)) { | ||
// Should not happen for single selection, but handle just in case | ||
return venvChoice.some((item) => item.label === 'Yes'); | ||
} | ||
return venvChoice.label === 'Yes'; | ||
} catch (ex) { | ||
if (ex === QuickInputButtons.Back) { | ||
callback(); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Checks if the GitHub Copilot extension is installed in the current VS Code environment. | ||
* @returns {boolean} True if Copilot is installed, false otherwise. | ||
*/ | ||
export function isCopilotInstalled(): boolean { | ||
return !!extensions.getExtension('GitHub.copilot'); | ||
} | ||
|
||
/** | ||
* Prompts the user to choose whether to create a Copilot instructions file, only if Copilot is installed. | ||
* @returns {Promise<boolean | undefined>} Resolves to true if 'Yes' is selected, false if 'No', or undefined if cancelled or Copilot is not installed. | ||
*/ | ||
export async function promptForCopilotInstructions(): Promise<boolean | undefined> { | ||
if (!isCopilotInstalled()) { | ||
return undefined; | ||
} | ||
const copilotChoice = await showQuickPickWithButtons([{ label: 'Yes' }, { label: 'No' }], { | ||
placeHolder: 'Would you like to create a Copilot instructions file?', | ||
eleanorjboyd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
ignoreFocusOut: true, | ||
showBackButton: true, | ||
}); | ||
if (!copilotChoice) { | ||
return undefined; | ||
} | ||
if (Array.isArray(copilotChoice)) { | ||
// Should not happen for single selection, but handle just in case | ||
return copilotChoice.some((item) => item.label === 'Yes'); | ||
} | ||
return copilotChoice.label === 'Yes'; | ||
} | ||
|
||
/** | ||
* Quickly creates a new Python virtual environment (venv) in the specified destination folder using the available environment managers. | ||
* Attempts to use the venv manager if available, otherwise falls back to any manager that supports environment creation. | ||
* @param envManagers - The collection of available environment managers. | ||
* @param destFolder - The absolute path to the destination folder where the environment should be created. | ||
* @returns {Promise<void>} Resolves when the environment is created or an error is shown. | ||
*/ | ||
export async function quickCreateNewVenv(envManagers: EnvironmentManagers, destFolder: string) { | ||
eleanorjboyd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// get the environment manager for venv, should always exist | ||
const envManager: InternalEnvironmentManager | undefined = envManagers.managers.find( | ||
(m) => m.id === 'ms-python.python:venv', | ||
); | ||
const destinationUri = Uri.parse(destFolder); | ||
if (envManager?.supportsQuickCreate) { | ||
// with quickCreate enabled, user will not be prompted when creating the environment | ||
const options: CreateEnvironmentOptions = { quickCreate: false }; | ||
if (envManager.supportsQuickCreate) { | ||
options.quickCreate = true; | ||
} | ||
const pyEnv = await envManager.create(destinationUri, options); | ||
// TODO: do I need to update to say this is the env for the file? Like set it? | ||
if (!pyEnv) { | ||
// comes back as undefined if this doesn't work | ||
window.showErrorMessage(`Failed to create virtual environment, please create it manually.`); | ||
} else { | ||
traceVerbose(`Created venv at: ${pyEnv?.environmentPath}`); | ||
} | ||
} else { | ||
window.showErrorMessage(`Failed to quick create virtual environment, please create it manually.`); | ||
} | ||
} | ||
|
||
/** | ||
* Recursively replaces all occurrences of a string in file and folder names, as well as file contents, within a directory tree. | ||
* @param dir - The root directory to start the replacement from. | ||
* @param searchValue - The string to search for in names and contents. | ||
* @param replaceValue - The string to replace with. | ||
* @returns {Promise<void>} Resolves when all replacements are complete. | ||
*/ | ||
export async function replaceInFilesAndNames(dir: string, searchValue: string, replaceValue: string) { | ||
eleanorjboyd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const entries = await fs.readdir(dir, { withFileTypes: true }); | ||
for (const entry of entries) { | ||
let entryName = entry.name; | ||
let fullPath = path.join(dir, entryName); | ||
let newFullPath = fullPath; | ||
// If the file or folder name contains searchValue, rename it | ||
if (entryName.includes(searchValue)) { | ||
const newName = entryName.replace(new RegExp(searchValue, 'g'), replaceValue); | ||
newFullPath = path.join(dir, newName); | ||
await fs.rename(fullPath, newFullPath); | ||
entryName = newName; | ||
} | ||
if (entry.isDirectory()) { | ||
await replaceInFilesAndNames(newFullPath, searchValue, replaceValue); | ||
} else { | ||
let content = await fs.readFile(newFullPath, 'utf8'); | ||
if (content.includes(searchValue)) { | ||
content = content.replace(new RegExp(searchValue, 'g'), replaceValue); | ||
await fs.writeFile(newFullPath, content, 'utf8'); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Ensures the .github/copilot-instructions.md file exists at the given root, creating or appending as needed. | ||
* @param destinationRootPath - The root directory where the .github folder should exist. | ||
* @param instructionsText - The text to write or append to the copilot-instructions.md file. | ||
*/ | ||
export async function manageCopilotInstructionsFile( | ||
destinationRootPath: string, | ||
packageName: string, | ||
instructionsFilePath: string, | ||
) { | ||
const instructionsText = `\n \n` + (await fs.readFile(instructionsFilePath, 'utf-8')); | ||
const githubFolderPath = path.join(destinationRootPath, '.github'); | ||
const customInstructionsPath = path.join(githubFolderPath, 'copilot-instructions.md'); | ||
if (!(await fs.pathExists(githubFolderPath))) { | ||
// make the .github folder if it doesn't exist | ||
await fs.mkdir(githubFolderPath); | ||
} | ||
const customInstructions = await fs.pathExists(customInstructionsPath); | ||
if (customInstructions) { | ||
// Append to the existing file | ||
await fs.appendFile(customInstructionsPath, instructionsText.replace(/<package_name>/g, packageName)); | ||
} else { | ||
// Create the file if it doesn't exist | ||
await fs.writeFile(customInstructionsPath, instructionsText.replace(/<package_name>/g, packageName)); | ||
} | ||
} | ||
|
||
/** | ||
* Appends a configuration object to the configurations array in a launch.json file. | ||
* @param launchJsonPath - The absolute path to the launch.json file. | ||
* @param projectLaunchConfig - The stringified JSON config to append. | ||
*/ | ||
async function appendToJsonConfigs(launchJsonPath: string, projectLaunchConfig: string) { | ||
let content = await fs.readFile(launchJsonPath, 'utf8'); | ||
const json = JSON.parse(content); | ||
// If it's a VS Code launch config, append to configurations array | ||
if (json && Array.isArray(json.configurations)) { | ||
const configObj = JSON.parse(projectLaunchConfig); | ||
json.configurations.push(configObj); | ||
await fs.writeFile(launchJsonPath, JSON.stringify(json, null, 4), 'utf8'); | ||
} else { | ||
traceError('Failed to add Project Launch Config to launch.json.'); | ||
return; | ||
} | ||
} | ||
|
||
/** | ||
* Updates the launch.json file in the .vscode folder to include the provided project launch configuration. | ||
* @param destinationRootPath - The root directory where the .vscode folder should exist. | ||
* @param projectLaunchConfig - The stringified JSON config to append. | ||
*/ | ||
export async function manageLaunchJsonFile(destinationRootPath: string, projectLaunchConfig: string) { | ||
const vscodeFolderPath = path.join(destinationRootPath, '.vscode'); | ||
const launchJsonPath = path.join(vscodeFolderPath, 'launch.json'); | ||
if (!(await fs.pathExists(vscodeFolderPath))) { | ||
await fs.mkdir(vscodeFolderPath); | ||
} | ||
const launchJsonExists = await fs.pathExists(launchJsonPath); | ||
if (launchJsonExists) { | ||
// Try to parse and append to existing launch.json | ||
await appendToJsonConfigs(launchJsonPath, projectLaunchConfig); | ||
} else { | ||
// Create a new launch.json with the provided config | ||
const launchJson = { | ||
version: '0.2.0', | ||
configurations: [JSON.parse(projectLaunchConfig)], | ||
}; | ||
await fs.writeFile(launchJsonPath, JSON.stringify(launchJson, null, 4), 'utf8'); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have localized versions of this in
localize.ts
. You can address this later.