diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml new file mode 100644 index 00000000..0f01dde3 --- /dev/null +++ b/.github/workflows/pr-labels.yml @@ -0,0 +1,24 @@ +name: 'PR labels' +on: + pull_request: + types: + - 'opened' + - 'reopened' + - 'labeled' + - 'unlabeled' + - 'synchronize' + +jobs: + add-pr-label: + name: 'Ensure Required Labels' + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: 'PR impact specified' + uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5.5.0 + with: + mode: exactly + count: 1 + labels: 'bug, debt, feature-request, no-changelog' diff --git a/.vscode/settings.json b/.vscode/settings.json index 1a8790c9..5558607c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,5 +26,6 @@ }, "prettier.tabWidth": 4, "python-envs.defaultEnvManager": "ms-python.python:venv", - "python-envs.pythonProjects": [] + "python-envs.pythonProjects": [], + "git.branchRandomName.enable": true } diff --git a/package-lock.json b/package-lock.json index 35d8b20f..c4dc2735 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vscode-python-envs", - "version": "1.7.0", + "version": "1.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-python-envs", - "version": "1.7.0", + "version": "1.8.0", "dependencies": { "@iarna/toml": "^2.2.5", "@vscode/extension-telemetry": "^0.9.7", diff --git a/package.json b/package.json index e207b20b..fa21dafb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "vscode-python-envs", "displayName": "Python Environments", "description": "Provides a unified python environment experience", - "version": "1.7.0", + "version": "1.8.0", "publisher": "ms-python", "preview": true, "engines": { @@ -497,6 +497,18 @@ "when": "explorerViewletVisible && resourceExtname == .py" } ], + "editor/title": [ + { + "command": "python-envs.terminal.activate", + "group": "navigation", + "when": "resourceScheme == vscode-terminal && config.python-envs.terminal.showActivateButton && pythonTerminalActivation && !pythonTerminalActivated" + }, + { + "command": "python-envs.terminal.deactivate", + "group": "navigation", + "when": "resourceScheme == vscode-terminal && config.python-envs.terminal.showActivateButton && pythonTerminalActivation && pythonTerminalActivated" + } + ], "editor/title/run": [ { "command": "python-envs.runAsTask", diff --git a/src/api.ts b/src/api.ts index f889f10c..c5c81865 100644 --- a/src/api.ts +++ b/src/api.ts @@ -379,7 +379,7 @@ export interface EnvironmentManager { quickCreateConfig?(): QuickCreateConfig | undefined; /** - * Creates a new Python environment within the specified scope. Create should support adding a .gitignore file if it creates a folder within the workspace. + * Creates a new Python environment within the specified scope. Create should support adding a .gitignore file if it creates a folder within the workspace. If a manager does not support environment creation, do not implement this method; the UI disables "create" options when `this.manager.create === undefined`. * @param scope - The scope within which to create the environment. * @param options - Optional parameters for creating the Python environment. * @returns A promise that resolves to the created Python environment, or undefined if creation failed. diff --git a/src/extension.ts b/src/extension.ts index e7372df8..60381ed5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,7 +1,8 @@ -import { commands, ExtensionContext, extensions, LogOutputChannel, Terminal, Uri, window, workspace } from 'vscode'; +import { commands, ExtensionContext, LogOutputChannel, Terminal, Uri, window } from 'vscode'; +import { version as extensionVersion } from '../package.json'; import { PythonEnvironment, PythonEnvironmentApi, PythonProjectCreator } from './api'; import { ensureCorrectVersion } from './common/extVersion'; -import { registerLogger, traceError, traceInfo, traceWarn } from './common/logging'; +import { registerLogger, traceError, traceInfo, traceVerbose, traceWarn } from './common/logging'; import { clearPersistentState, setPersistentState } from './common/persistentState'; import { newProjectSelection } from './common/pickers/managers'; import { StopWatch } from './common/stopWatch'; @@ -9,7 +10,6 @@ import { EventNames } from './common/telemetry/constants'; import { sendManagerSelectionTelemetry } from './common/telemetry/helpers'; import { sendTelemetryEvent } from './common/telemetry/sender'; import { createDeferred } from './common/utils/deferred'; -import { normalizePath } from './common/utils/pathUtils'; import { isWindows } from './common/utils/platformUtils'; import { activeTerminal, @@ -61,19 +61,23 @@ import { cleanupStartupScripts } from './features/terminal/shellStartupSetupHand import { TerminalActivationImpl } from './features/terminal/terminalActivationState'; import { TerminalEnvVarInjector } from './features/terminal/terminalEnvVarInjector'; import { TerminalManager, TerminalManagerImpl } from './features/terminal/terminalManager'; -import { getAutoActivationType, getEnvironmentForTerminal } from './features/terminal/utils'; +import { getEnvironmentForTerminal } from './features/terminal/utils'; import { EnvManagerView } from './features/views/envManagersView'; import { ProjectView } from './features/views/projectView'; import { PythonStatusBarImpl } from './features/views/pythonStatusBar'; import { updateViewsAndStatus } from './features/views/revealHandler'; import { ProjectItem } from './features/views/treeViewItems'; +import { + collectEnvironmentInfo, + getEnvManagerAndPackageManagerConfigLevels, + resolveDefaultInterpreter, +} from './helpers'; import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './internal.api'; import { registerSystemPythonFeatures } from './managers/builtin/main'; import { SysPythonManager } from './managers/builtin/sysPythonManager'; import { createNativePythonFinder, getNativePythonToolsPath, - NativeEnvInfo, NativePythonFinder, } from './managers/common/nativePythonFinder'; import { IDisposable } from './managers/common/types'; @@ -82,89 +86,6 @@ import { registerPipenvFeatures } from './managers/pipenv/main'; import { registerPoetryFeatures } from './managers/poetry/main'; import { registerPyenvFeatures } from './managers/pyenv/main'; -/** - * Collects relevant Python environment information for issue reporting - */ -async function collectEnvironmentInfo( - context: ExtensionContext, - envManagers: EnvironmentManagers, - projectManager: PythonProjectManager, -): Promise { - const info: string[] = []; - - try { - // Extension version - const extensionVersion = context.extension?.packageJSON?.version || 'unknown'; - info.push(`Extension Version: ${extensionVersion}`); - - // Python extension version - const pythonExtension = extensions.getExtension('ms-python.python'); - const pythonVersion = pythonExtension?.packageJSON?.version || 'not installed'; - info.push(`Python Extension Version: ${pythonVersion}`); - - // Environment managers - const managers = envManagers.managers; - info.push(`\nRegistered Environment Managers (${managers.length}):`); - managers.forEach((manager) => { - info.push(` - ${manager.id} (${manager.displayName})`); - }); - - // Available environments - const allEnvironments: PythonEnvironment[] = []; - for (const manager of managers) { - try { - const envs = await manager.getEnvironments('all'); - allEnvironments.push(...envs); - } catch (err) { - info.push(` Error getting environments from ${manager.id}: ${err}`); - } - } - - info.push(`\nTotal Available Environments: ${allEnvironments.length}`); - if (allEnvironments.length > 0) { - info.push('Environment Details:'); - allEnvironments.slice(0, 10).forEach((env, index) => { - info.push(` ${index + 1}. ${env.displayName} (${env.version}) - ${env.displayPath}`); - }); - if (allEnvironments.length > 10) { - info.push(` ... and ${allEnvironments.length - 10} more environments`); - } - } - - // Python projects - const projects = projectManager.getProjects(); - info.push(`\nPython Projects (${projects.length}):`); - for (let index = 0; index < projects.length; index++) { - const project = projects[index]; - info.push(` ${index + 1}. ${project.uri.fsPath}`); - try { - const env = await envManagers.getEnvironment(project.uri); - if (env) { - info.push(` Environment: ${env.displayName}`); - } - } catch (err) { - info.push(` Error getting environment: ${err}`); - } - } - - // Current settings (non-sensitive) - const config = workspace.getConfiguration('python-envs'); - const pyConfig = workspace.getConfiguration('python'); - info.push('\nExtension Settings:'); - info.push(` Default Environment Manager: ${config.get('defaultEnvManager')}`); - info.push(` Default Package Manager: ${config.get('defaultPackageManager')}`); - const pyenvAct = config.get('terminal.autoActivationType', undefined); - const pythonAct = pyConfig.get('terminal.activateEnvironment', undefined); - info.push( - `Auto-activation is "${getAutoActivationType()}". Activation based on first 'py-env.terminal.autoActivationType' setting which is '${pyenvAct}' and 'python.terminal.activateEnvironment' if the first is undefined which is '${pythonAct}'.\n`, - ); - } catch (err) { - info.push(`\nError collecting environment information: ${err}`); - } - - return info.join('\n'); -} - export async function activate(context: ExtensionContext): Promise { const useEnvironmentsExtension = getConfiguration('python').get('useEnvironmentsExtension', true); traceInfo(`Experiment Status: useEnvironmentsExtension setting set to ${useEnvironmentsExtension}`); @@ -183,6 +104,13 @@ export async function activate(context: ExtensionContext): Promise { ); } -/** - * Sets the default Python interpreter for the workspace if the user has not explicitly set 'defaultEnvManager' or it is set to venv. - * @param nativeFinder - used to resolve interpreter paths. - * @param envManagers - contains all registered managers. - * @param api - The PythonEnvironmentApi for environment resolution and setting. - */ -async function resolveDefaultInterpreter( - nativeFinder: NativePythonFinder, - envManagers: EnvironmentManagers, - api: PythonEnvironmentApi, -) { - const defaultInterpreterPath = getConfiguration('python').get('defaultInterpreterPath'); - - if (defaultInterpreterPath) { - const config = getConfiguration('python-envs'); - const inspect = config.inspect('defaultEnvManager'); - const userDefinedDefaultManager = - inspect?.workspaceFolderValue !== undefined || - inspect?.workspaceValue !== undefined || - inspect?.globalValue !== undefined; - if (!userDefinedDefaultManager) { - try { - const resolved: NativeEnvInfo = await nativeFinder.resolve(defaultInterpreterPath); - if (resolved && resolved.executable) { - if (normalizePath(resolved.executable) === normalizePath(defaultInterpreterPath)) { - // no action required, the path is already correct - return; - } - const resolvedEnv = await api.resolveEnvironment(Uri.file(resolved.executable)); - traceInfo(`[resolveDefaultInterpreter] API resolved environment: ${JSON.stringify(resolvedEnv)}`); - - let findEnvManager = envManagers.managers.find((m) => m.id === resolvedEnv?.envId.managerId); - if (!findEnvManager) { - findEnvManager = envManagers.managers.find((m) => m.id === 'ms-python.python:system'); - } - if (resolvedEnv) { - const newEnv: PythonEnvironment = { - envId: { - id: resolvedEnv?.envId.id, - managerId: resolvedEnv?.envId.managerId ?? '', - }, - name: 'defaultInterpreterPath: ' + (resolved.version ?? ''), - displayName: 'defaultInterpreterPath: ' + (resolved.version ?? ''), - version: resolved.version ?? '', - displayPath: defaultInterpreterPath ?? '', - environmentPath: defaultInterpreterPath ? Uri.file(defaultInterpreterPath) : Uri.file(''), - sysPrefix: resolved.arch ?? '', - execInfo: { - run: { - executable: defaultInterpreterPath ?? '', - }, - }, - }; - if (workspace.workspaceFolders?.[0] && findEnvManager) { - traceInfo( - `[resolveDefaultInterpreter] Setting environment for workspace: ${workspace.workspaceFolders[0].uri.fsPath}`, - ); - await api.setEnvironment(workspace.workspaceFolders[0].uri, newEnv); - } - } - } else { - traceWarn( - `[resolveDefaultInterpreter] NativeFinder did not resolve an executable for path: ${defaultInterpreterPath}`, - ); - } - } catch (err) { - traceError(`[resolveDefaultInterpreter] Error resolving default interpreter: ${err}`); - } - } - } -} - export async function deactivate(context: ExtensionContext) { await disposeAll(context.subscriptions); context.subscriptions.length = 0; // Clear subscriptions to prevent memory leaks diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 00000000..baff94e8 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,208 @@ +import { ExtensionContext, extensions, Uri, workspace } from 'vscode'; +import { PythonEnvironment, PythonEnvironmentApi } from './api'; +import { traceError, traceInfo, traceWarn } from './common/logging'; +import { normalizePath } from './common/utils/pathUtils'; +import { getConfiguration } from './common/workspace.apis'; +import { getAutoActivationType } from './features/terminal/utils'; +import { EnvironmentManagers, PythonProjectManager } from './internal.api'; +import { NativeEnvInfo, NativePythonFinder } from './managers/common/nativePythonFinder'; + +/** + * Collects relevant Python environment information for issue reporting + */ +export async function collectEnvironmentInfo( + context: ExtensionContext, + envManagers: EnvironmentManagers, + projectManager: PythonProjectManager, +): Promise { + const info: string[] = []; + + try { + // Extension version + const extensionVersion = context.extension?.packageJSON?.version || 'unknown'; + info.push(`Extension Version: ${extensionVersion}`); + + // Python extension version + const pythonExtension = extensions.getExtension('ms-python.python'); + const pythonVersion = pythonExtension?.packageJSON?.version || 'not installed'; + info.push(`Python Extension Version: ${pythonVersion}`); + + // Environment managers + const managers = envManagers.managers; + info.push(`\nRegistered Environment Managers (${managers.length}):`); + managers.forEach((manager) => { + info.push(` - ${manager.id} (${manager.displayName})`); + }); + + // Available environments + const allEnvironments: PythonEnvironment[] = []; + for (const manager of managers) { + try { + const envs = await manager.getEnvironments('all'); + allEnvironments.push(...envs); + } catch (err) { + info.push(` Error getting environments from ${manager.id}: ${err}`); + } + } + + info.push(`\nTotal Available Environments: ${allEnvironments.length}`); + if (allEnvironments.length > 0) { + info.push('Environment Details:'); + allEnvironments.slice(0, 10).forEach((env, index) => { + info.push(` ${index + 1}. ${env.displayName} (${env.version}) - ${env.displayPath}`); + }); + if (allEnvironments.length > 10) { + info.push(` ... and ${allEnvironments.length - 10} more environments`); + } + } + + // Python projects + const projects = projectManager.getProjects(); + info.push(`\nPython Projects (${projects.length}):`); + for (let index = 0; index < projects.length; index++) { + const project = projects[index]; + info.push(` ${index + 1}. ${project.uri.fsPath}`); + try { + const env = await envManagers.getEnvironment(project.uri); + if (env) { + info.push(` Environment: ${env.displayName}`); + } + } catch (err) { + info.push(` Error getting environment: ${err}`); + } + } + + // Current settings (non-sensitive) + const config = workspace.getConfiguration('python-envs'); + const pyConfig = workspace.getConfiguration('python'); + info.push('\nExtension Settings:'); + info.push(` Default Environment Manager: ${config.get('defaultEnvManager')}`); + info.push(` Default Package Manager: ${config.get('defaultPackageManager')}`); + const pyenvAct = config.get('terminal.autoActivationType', undefined); + const pythonAct = pyConfig.get('terminal.activateEnvironment', undefined); + info.push( + `Auto-activation is "${getAutoActivationType()}". Activation based on first 'py-env.terminal.autoActivationType' setting which is '${pyenvAct}' and 'python.terminal.activateEnvironment' if the first is undefined which is '${pythonAct}'.\n`, + ); + } catch (err) { + info.push(`\nError collecting environment information: ${err}`); + } + + return info.join('\n'); +} + +/** + * Logs the values of defaultPackageManager and defaultEnvManager at all configuration levels (workspace folder, workspace, user/global, default). + */ +export function getEnvManagerAndPackageManagerConfigLevels() { + const config = getConfiguration('python-envs'); + const envManagerInspect = config.inspect('defaultEnvManager'); + const pkgManagerInspect = config.inspect('defaultPackageManager'); + + return { + section: 'Python Envs Configuration Levels', + defaultEnvManager: { + workspaceFolderValue: envManagerInspect?.workspaceFolderValue ?? 'undefined', + workspaceValue: envManagerInspect?.workspaceValue ?? 'undefined', + globalValue: envManagerInspect?.globalValue ?? 'undefined', + defaultValue: envManagerInspect?.defaultValue ?? 'undefined', + }, + defaultPackageManager: { + workspaceFolderValue: pkgManagerInspect?.workspaceFolderValue ?? 'undefined', + workspaceValue: pkgManagerInspect?.workspaceValue ?? 'undefined', + globalValue: pkgManagerInspect?.globalValue ?? 'undefined', + defaultValue: pkgManagerInspect?.defaultValue ?? 'undefined', + }, + }; +} + +/** + * Returns the user-configured value for a configuration key if set at any level (workspace folder, workspace, or global), + * otherwise returns undefined. + */ +export function getUserConfiguredSetting(section: string, key: string): T | undefined { + const config = getConfiguration(section); + const inspect = config.inspect(key); + if (!inspect) { + return undefined; + } + if (inspect.workspaceFolderValue !== undefined) { + return inspect.workspaceFolderValue; + } + if (inspect.workspaceValue !== undefined) { + return inspect.workspaceValue; + } + if (inspect.globalValue !== undefined) { + return inspect.globalValue; + } + return undefined; +} + +/** + * Sets the default Python interpreter for the workspace if the user has not explicitly set 'defaultEnvManager'. + * @param nativeFinder - used to resolve interpreter paths. + * @param envManagers - contains all registered managers. + * @param api - The PythonEnvironmentApi for environment resolution and setting. + */ +export async function resolveDefaultInterpreter( + nativeFinder: NativePythonFinder, + envManagers: EnvironmentManagers, + api: PythonEnvironmentApi, +) { + const userSetdefaultInterpreter = getUserConfiguredSetting('python', 'defaultInterpreterPath'); + const userSetDefaultManager = getUserConfiguredSetting('python-envs', 'defaultEnvManager'); + traceInfo( + `[resolveDefaultInterpreter] User configured defaultInterpreterPath: ${userSetdefaultInterpreter} and defaultEnvManager: ${userSetDefaultManager}`, + ); + + // Only proceed if the user has explicitly set defaultInterpreterPath but nothing is saved for defaultEnvManager + if (userSetdefaultInterpreter && !userSetDefaultManager) { + try { + const resolved: NativeEnvInfo = await nativeFinder.resolve(userSetdefaultInterpreter); + if (resolved && resolved.executable) { + if (normalizePath(resolved.executable) === normalizePath(userSetdefaultInterpreter)) { + // no action required, the path is already correct + return; + } + const resolvedEnv = await api.resolveEnvironment(Uri.file(resolved.executable)); + traceInfo(`[resolveDefaultInterpreter] API resolved environment: ${JSON.stringify(resolvedEnv)}`); + + let findEnvManager = envManagers.managers.find((m) => m.id === resolvedEnv?.envId.managerId); + if (!findEnvManager) { + findEnvManager = envManagers.managers.find((m) => m.id === 'ms-python.python:system'); + } + const randomString = Math.random().toString(36).substring(2, 15); + if (resolvedEnv) { + const newEnv: PythonEnvironment = { + envId: { + id: `${userSetdefaultInterpreter}_${randomString}`, + managerId: resolvedEnv?.envId.managerId ?? '', + }, + name: 'defaultInterpreterPath: ' + (resolved.version ?? ''), + displayName: 'defaultInterpreterPath: ' + (resolved.version ?? ''), + version: resolved.version ?? '', + displayPath: userSetdefaultInterpreter ?? '', + environmentPath: userSetdefaultInterpreter ? Uri.file(userSetdefaultInterpreter) : Uri.file(''), + sysPrefix: resolved.arch ?? '', + execInfo: { + run: { + executable: userSetdefaultInterpreter ?? '', + }, + }, + }; + if (workspace.workspaceFolders?.[0] && findEnvManager) { + traceInfo( + `[resolveDefaultInterpreter] Setting environment for workspace: ${workspace.workspaceFolders[0].uri.fsPath}`, + ); + await api.setEnvironment(workspace.workspaceFolders[0].uri, newEnv); + } + } + } else { + traceWarn( + `[resolveDefaultInterpreter] NativeFinder did not resolve an executable for path: ${userSetdefaultInterpreter}`, + ); + } + } catch (err) { + traceError(`[resolveDefaultInterpreter] Error resolving default interpreter: ${err}`); + } + } +} diff --git a/src/managers/common/nativePythonFinder.ts b/src/managers/common/nativePythonFinder.ts index 7f6fbd84..4a1306af 100644 --- a/src/managers/common/nativePythonFinder.ts +++ b/src/managers/common/nativePythonFinder.ts @@ -202,7 +202,7 @@ class NativePythonFinderImpl implements NativePythonFinder { } private start(): rpc.MessageConnection { - this.outputChannel.info(`Starting Python Locator ${this.toolPath} server`); + this.outputChannel.info(`[pet] Starting Python Locator ${this.toolPath} server`); // jsonrpc package cannot handle messages coming through too quickly. // Lets handle the messages and close the stream only when @@ -213,7 +213,7 @@ class NativePythonFinderImpl implements NativePythonFinder { try { const proc = ch.spawn(this.toolPath, ['server'], { env: process.env }); proc.stdout.pipe(readable, { end: false }); - proc.stderr.on('data', (data) => this.outputChannel.error(data.toString())); + proc.stderr.on('data', (data) => this.outputChannel.error(`[pet] ${data.toString()}`)); writable.pipe(proc.stdin, { end: false }); disposables.push({ @@ -223,12 +223,12 @@ class NativePythonFinderImpl implements NativePythonFinder { proc.kill(); } } catch (ex) { - this.outputChannel.error('Error disposing finder', ex); + this.outputChannel.error('[pet] Error disposing finder', ex); } }, }); } catch (ex) { - this.outputChannel.error(`Error starting Python Finder ${this.toolPath} server`, ex); + this.outputChannel.error(`[pet] Error starting Python Finder ${this.toolPath} server`, ex); } const connection = rpc.createMessageConnection( new rpc.StreamMessageReader(readable), @@ -241,27 +241,28 @@ class NativePythonFinderImpl implements NativePythonFinder { writable.end(); }), connection.onError((ex) => { - this.outputChannel.error('Connection Error:', ex); + this.outputChannel.error('[pet] Connection Error:', ex); }), connection.onNotification('log', (data: NativeLog) => { + const msg = `[pet] ${data.message}`; switch (data.level) { case 'info': - this.outputChannel.info(data.message); + this.outputChannel.info(msg); break; case 'warning': - this.outputChannel.warn(data.message); + this.outputChannel.warn(msg); break; case 'error': - this.outputChannel.error(data.message); + this.outputChannel.error(msg); break; case 'debug': - this.outputChannel.debug(data.message); + this.outputChannel.debug(msg); break; default: - this.outputChannel.trace(data.message); + this.outputChannel.trace(msg); } }), - connection.onNotification('telemetry', (data) => this.outputChannel.info(`Telemetry: `, data)), + connection.onNotification('telemetry', (data) => this.outputChannel.info('[pet] Telemetry: ', data)), connection.onClose(() => { disposables.forEach((d) => d.dispose()); }), @@ -288,7 +289,9 @@ class NativePythonFinderImpl implements NativePythonFinder { executable: data.executable, }) .then((environment: NativeEnvInfo) => { - this.outputChannel.info(`Resolved ${environment.executable}`); + this.outputChannel.info( + `Resolved environment during PET refresh: ${environment.executable}`, + ); nativeInfo.push(environment); }) .catch((ex) => @@ -307,7 +310,7 @@ class NativePythonFinderImpl implements NativePythonFinder { await this.connection.sendRequest<{ duration: number }>('refresh', refreshOptions); await Promise.all(unresolved); } catch (ex) { - this.outputChannel.error('Error refreshing', ex); + this.outputChannel.error('[pet] Error refreshing', ex); throw ex; } finally { disposables.forEach((d) => d.dispose()); @@ -333,13 +336,15 @@ class NativePythonFinderImpl implements NativePythonFinder { }; // No need to send a configuration request, is there are no changes. if (JSON.stringify(options) === JSON.stringify(this.lastConfiguration || {})) { + this.outputChannel.debug('[pet] configure: No changes detected, skipping configuration update.'); return; } + this.outputChannel.info('[pet] configure: Sending configuration update:', JSON.stringify(options)); try { this.lastConfiguration = options; await this.connection.sendRequest('configure', options); } catch (ex) { - this.outputChannel.error('Configuration error', ex); + this.outputChannel.error('[pet] configure: Configuration error', ex); } } } diff --git a/src/managers/conda/condaUtils.ts b/src/managers/conda/condaUtils.ts index c6cf3672..418fb62e 100644 --- a/src/managers/conda/condaUtils.ts +++ b/src/managers/conda/condaUtils.ts @@ -31,6 +31,7 @@ import { Common, CondaStrings, PackageManagement, Pickers } from '../../common/l import { traceInfo, traceVerbose } from '../../common/logging'; import { getWorkspacePersistentState } from '../../common/persistentState'; import { pickProject } from '../../common/pickers/projects'; +import { StopWatch } from '../../common/stopWatch'; import { createDeferred } from '../../common/utils/deferred'; import { untildify } from '../../common/utils/pathUtils'; import { isWindows } from '../../common/utils/platformUtils'; @@ -56,7 +57,6 @@ import { Installable } from '../common/types'; import { shortVersion, sortEnvironments } from '../common/utils'; import { CondaEnvManager } from './condaEnvManager'; import { getCondaHookPs1Path, getLocalActivationScript } from './condaSourcingUtils'; -import { StopWatch } from '../../common/stopWatch'; export const CONDA_PATH_KEY = `${ENVS_EXTENSION_ID}:conda:CONDA_PATH`; export const CONDA_PREFIXES_KEY = `${ENVS_EXTENSION_ID}:conda:CONDA_PREFIXES`; @@ -694,12 +694,14 @@ export async function refreshCondaEnvs( .filter((e) => e.kind === NativePythonEnvironmentKind.conda); const collection: PythonEnvironment[] = []; - envs.forEach(async (e) => { - const environment = await nativeToPythonEnv(e, api, manager, log, condaPath, condaPrefixes); - if (environment) { - collection.push(environment); - } - }); + await Promise.all( + envs.map(async (e) => { + const environment = await nativeToPythonEnv(e, api, manager, log, condaPath, condaPrefixes); + if (environment) { + collection.push(environment); + } + }), + ); return sortEnvironments(collection); } @@ -742,7 +744,6 @@ function trimVersionToMajorMinor(version: string): string { const match = version.match(/^(\d+\.\d+\.\d+)/); return match ? match[1] : version; } - export async function pickPythonVersion( api: PythonEnvironmentApi, token?: CancellationToken, @@ -755,11 +756,25 @@ export async function pickPythonVersion( .filter(Boolean) .map((v) => trimVersionToMajorMinor(v)), // cut to 3 digits ), - ) - .sort() - .reverse(); - if (!versions) { - versions = ['3.11', '3.10', '3.9', '3.12', '3.13']; + ); + + // Sort versions by major version (descending), ignoring minor/patch for simplicity + const parseMajorMinor = (v: string) => { + const m = v.match(/^(\d+)(?:\.(\d+))?/); + return { major: m ? Number(m[1]) : 0, minor: m && m[2] ? Number(m[2]) : 0 }; + }; + + versions = versions.sort((a, b) => { + const pa = parseMajorMinor(a); + const pb = parseMajorMinor(b); + if (pa.major !== pb.major) { + return pb.major - pa.major; + } // desc by major + return pb.minor - pa.minor; // desc by minor + }); + + if (!versions || versions.length === 0) { + versions = ['3.13', '3.12', '3.11', '3.10', '3.9']; } const items: QuickPickItem[] = versions.map((v) => ({ label: v === RECOMMENDED_CONDA_PYTHON ? `$(star-full) Python` : 'Python',