Skip to content
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

Use env extension when available #24564

Merged
merged 14 commits into from
Dec 10, 2024
4 changes: 4 additions & 0 deletions src/client/common/persistentState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { cache } from './utils/decorators';
import { noop } from './utils/misc';
import { clearCacheDirectory } from '../pythonEnvironments/base/locators/common/nativePythonFinder';
import { clearCache, useEnvExtension } from '../envExt/api.internal';

let _workspaceState: Memento | undefined;
const _workspaceKeys: string[] = [];
Expand Down Expand Up @@ -134,6 +135,9 @@ export class PersistentStateFactory implements IPersistentStateFactory, IExtensi
this.cmdManager?.registerCommand(Commands.ClearStorage, async () => {
await clearWorkspaceState();
await this.cleanAllPersistentStates();
if (useEnvExtension()) {
await clearCache();
}
});
const globalKeysStorageDeprecated = this.createGlobalPersistentState(GLOBAL_PERSISTENT_KEYS_DEPRECATED, []);
const workspaceKeysStorageDeprecated = this.createWorkspacePersistentState(
Expand Down
3 changes: 2 additions & 1 deletion src/client/common/terminal/activator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { IConfigurationService, IExperimentService } from '../../types';
import { ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, TerminalActivationOptions } from '../types';
import { BaseTerminalActivator } from './base';
import { inTerminalEnvVarExperiment } from '../../experiments/helpers';
import { useEnvExtension } from '../../../envExt/api.internal';

@injectable()
export class TerminalActivator implements ITerminalActivator {
Expand Down Expand Up @@ -41,7 +42,7 @@ export class TerminalActivator implements ITerminalActivator {
const settings = this.configurationService.getSettings(options?.resource);
const activateEnvironment =
settings.terminal.activateEnvironment && !inTerminalEnvVarExperiment(this.experimentService);
if (!activateEnvironment || options?.hideFromUser) {
if (!activateEnvironment || options?.hideFromUser || useEnvExtension()) {
return false;
}

Expand Down
38 changes: 24 additions & 14 deletions src/client/common/terminal/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import {
} from './types';
import { traceVerbose } from '../../logging';
import { getConfiguration } from '../vscodeApis/workspaceApis';
import { useEnvExtension } from '../../envExt/api.internal';
import { ensureTerminalLegacy } from '../../envExt/api.legacy';
import { sleep } from '../utils/async';
import { isWindows } from '../utils/platform';

@injectable()
Expand Down Expand Up @@ -132,22 +135,29 @@ export class TerminalService implements ITerminalService, Disposable {
if (this.terminal) {
return;
}
this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal);
this.terminal = this.terminalManager.createTerminal({
name: this.options?.title || 'Python',
hideFromUser: this.options?.hideFromUser,
});
this.terminalAutoActivator.disableAutoActivation(this.terminal);

// Sometimes the terminal takes some time to start up before it can start accepting input.
await new Promise((resolve) => setTimeout(resolve, 100));
if (useEnvExtension()) {
this.terminal = await ensureTerminalLegacy(this.options?.resource, {
name: this.options?.title || 'Python',
hideFromUser: this.options?.hideFromUser,
});
} else {
this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't 100% accurate in terms of always getting the shell type right, correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not, this is the same detector as the one Python extension uses. We need the shell type API to be more accurate.

this.terminal = this.terminalManager.createTerminal({
name: this.options?.title || 'Python',
hideFromUser: this.options?.hideFromUser,
});
this.terminalAutoActivator.disableAutoActivation(this.terminal);

await sleep(100);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there reason why we wait for short duration before activateEnvironmentInTerminal

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also from the existing activation. This is to give shell initialization enough time to start.


await this.terminalActivator.activateEnvironmentInTerminal(this.terminal, {
resource: this.options?.resource,
preserveFocus,
interpreter: this.options?.interpreter,
hideFromUser: this.options?.hideFromUser,
});
await this.terminalActivator.activateEnvironmentInTerminal(this.terminal, {
resource: this.options?.resource,
preserveFocus,
interpreter: this.options?.interpreter,
hideFromUser: this.options?.hideFromUser,
});
}

if (!this.options?.hideFromUser) {
this.terminal.show(preserveFocus);
Expand Down
108 changes: 108 additions & 0 deletions src/client/envExt/api.internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { Terminal, Uri } from 'vscode';
import { getExtension } from '../common/vscodeApis/extensionsApi';
import {
GetEnvironmentScope,
PythonBackgroundRunOptions,
PythonEnvironment,
PythonEnvironmentApi,
PythonProcess,
RefreshEnvironmentsScope,
} from './types';
import { executeCommand } from '../common/vscodeApis/commandApis';

export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs';

let _useExt: boolean | undefined;
export function useEnvExtension(): boolean {
if (_useExt !== undefined) {
return _useExt;
}
_useExt = !!getExtension(ENVS_EXTENSION_ID);
return _useExt;
}

let _extApi: PythonEnvironmentApi | undefined;
export async function getEnvExtApi(): Promise<PythonEnvironmentApi> {
if (_extApi) {
return _extApi;
}
const extension = getExtension(ENVS_EXTENSION_ID);
if (!extension) {
throw new Error('Python Environments extension not found.');
}
if (extension?.isActive) {
_extApi = extension.exports as PythonEnvironmentApi;
return _extApi;
}

await extension.activate();

_extApi = extension.exports as PythonEnvironmentApi;
return _extApi;
}

export async function runInBackground(
environment: PythonEnvironment,
options: PythonBackgroundRunOptions,
): Promise<PythonProcess> {
const envExtApi = await getEnvExtApi();
return envExtApi.runInBackground(environment, options);
}

export async function getEnvironment(scope: GetEnvironmentScope): Promise<PythonEnvironment | undefined> {
const envExtApi = await getEnvExtApi();
return envExtApi.getEnvironment(scope);
}

export async function refreshEnvironments(scope: RefreshEnvironmentsScope): Promise<void> {
const envExtApi = await getEnvExtApi();
return envExtApi.refreshEnvironments(scope);
}

export async function runInTerminal(
resource: Uri | undefined,
args?: string[],
cwd?: string | Uri,
show?: boolean,
): Promise<Terminal> {
const envExtApi = await getEnvExtApi();
const env = await getEnvironment(resource);
const project = resource ? envExtApi.getPythonProject(resource) : undefined;
if (env && resource) {
return envExtApi.runInTerminal(env, {
cwd: cwd ?? project?.uri ?? process.cwd(),
args,
show,
});
}
throw new Error('Invalid arguments to run in terminal');
}

export async function runInDedicatedTerminal(
resource: Uri | undefined,
args?: string[],
cwd?: string | Uri,
show?: boolean,
): Promise<Terminal> {
const envExtApi = await getEnvExtApi();
const env = await getEnvironment(resource);
const project = resource ? envExtApi.getPythonProject(resource) : undefined;
if (env) {
return envExtApi.runInDedicatedTerminal(resource ?? 'global', env, {
cwd: cwd ?? project?.uri ?? process.cwd(),
args,
show,
});
}
throw new Error('Invalid arguments to run in dedicated terminal');
}

export async function clearCache(): Promise<void> {
const envExtApi = await getEnvExtApi();
if (envExtApi) {
await executeCommand('python-envs.clearCache');
}
}
167 changes: 167 additions & 0 deletions src/client/envExt/api.legacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { Terminal, Uri } from 'vscode';
import { getEnvExtApi, getEnvironment } from './api.internal';
import { EnvironmentType, PythonEnvironment as PythonEnvironmentLegacy } from '../pythonEnvironments/info';
import { PythonEnvironment, PythonTerminalOptions } from './types';
import { Architecture } from '../common/utils/platform';
import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion';
import { PythonEnvType } from '../pythonEnvironments/base/info';
import { traceError, traceInfo } from '../logging';
import { reportActiveInterpreterChanged } from '../environmentApi';
import { getWorkspaceFolder } from '../common/vscodeApis/workspaceApis';

function toEnvironmentType(pythonEnv: PythonEnvironment): EnvironmentType {
if (pythonEnv.envId.managerId.toLowerCase().endsWith('system')) {
return EnvironmentType.System;
}
if (pythonEnv.envId.managerId.toLowerCase().endsWith('venv')) {
return EnvironmentType.Venv;
}
if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenv')) {
return EnvironmentType.VirtualEnv;
}
if (pythonEnv.envId.managerId.toLowerCase().endsWith('conda')) {
return EnvironmentType.Conda;
}
if (pythonEnv.envId.managerId.toLowerCase().endsWith('pipenv')) {
return EnvironmentType.Pipenv;
}
if (pythonEnv.envId.managerId.toLowerCase().endsWith('poetry')) {
return EnvironmentType.Poetry;
}
if (pythonEnv.envId.managerId.toLowerCase().endsWith('pyenv')) {
return EnvironmentType.Pyenv;
}
if (pythonEnv.envId.managerId.toLowerCase().endsWith('hatch')) {
return EnvironmentType.Hatch;
}
if (pythonEnv.envId.managerId.toLowerCase().endsWith('pixi')) {
return EnvironmentType.Pixi;
}
if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenvwrapper')) {
return EnvironmentType.VirtualEnvWrapper;
}
if (pythonEnv.envId.managerId.toLowerCase().endsWith('activestate')) {
return EnvironmentType.ActiveState;
}
return EnvironmentType.Unknown;
}

function getEnvType(kind: EnvironmentType): PythonEnvType | undefined {
switch (kind) {
case EnvironmentType.Pipenv:
case EnvironmentType.VirtualEnv:
case EnvironmentType.Pyenv:
case EnvironmentType.Venv:
case EnvironmentType.Poetry:
case EnvironmentType.Hatch:
case EnvironmentType.Pixi:
case EnvironmentType.VirtualEnvWrapper:
case EnvironmentType.ActiveState:
return PythonEnvType.Virtual;

case EnvironmentType.Conda:
return PythonEnvType.Conda;

case EnvironmentType.MicrosoftStore:
case EnvironmentType.Global:
case EnvironmentType.System:
default:
return undefined;
}
}

function toLegacyType(env: PythonEnvironment): PythonEnvironmentLegacy {
const ver = parseVersion(env.version);
const envType = toEnvironmentType(env);
return {
id: env.environmentPath.fsPath,
displayName: env.displayName,
detailedDisplayName: env.name,
envType,
envPath: env.sysPrefix,
type: getEnvType(envType),
path: env.environmentPath.fsPath,
version: {
raw: env.version,
major: ver.major,
minor: ver.minor,
patch: ver.micro,
build: [],
prerelease: [],
},
sysVersion: env.version,
architecture: Architecture.x64,
sysPrefix: env.sysPrefix,
};
}

const previousEnvMap = new Map<string, PythonEnvironment | undefined>();
export async function getActiveInterpreterLegacy(resource?: Uri): Promise<PythonEnvironmentLegacy | undefined> {
const api = await getEnvExtApi();
const uri = resource ? api.getPythonProject(resource)?.uri : undefined;

const pythonEnv = await getEnvironment(resource);
const oldEnv = previousEnvMap.get(uri?.fsPath || '');
const newEnv = pythonEnv ? toLegacyType(pythonEnv) : undefined;
if (newEnv && oldEnv?.envId.id !== pythonEnv?.envId.id) {
reportActiveInterpreterChanged({
resource: getWorkspaceFolder(resource),
path: newEnv.path,
});
}
return pythonEnv ? toLegacyType(pythonEnv) : undefined;
}

export async function ensureEnvironmentContainsPythonLegacy(pythonPath: string): Promise<void> {
const api = await getEnvExtApi();
const pythonEnv = await api.resolveEnvironment(Uri.file(pythonPath));
if (!pythonEnv) {
traceError(`EnvExt: Failed to resolve environment for ${pythonPath}`);
return;
}

const envType = toEnvironmentType(pythonEnv);
if (envType === EnvironmentType.Conda) {
const packages = await api.getPackages(pythonEnv);
if (packages && packages.length > 0 && packages.some((pkg) => pkg.name.toLowerCase() === 'python')) {
return;
}
traceInfo(`EnvExt: Python not found in ${envType} environment ${pythonPath}`);
traceInfo(`EnvExt: Installing Python in ${envType} environment ${pythonPath}`);
await api.installPackages(pythonEnv, ['python']);
}
}

export async function setInterpreterLegacy(pythonPath: string, uri: Uri | undefined): Promise<void> {
const api = await getEnvExtApi();
const pythonEnv = await api.resolveEnvironment(Uri.file(pythonPath));
if (!pythonEnv) {
traceError(`EnvExt: Failed to resolve environment for ${pythonPath}`);
return;
}
await api.setEnvironment(uri, pythonEnv);
}

export async function resetInterpreterLegacy(uri: Uri | undefined): Promise<void> {
const api = await getEnvExtApi();
await api.setEnvironment(uri, undefined);
}

export async function ensureTerminalLegacy(
resource: Uri | undefined,
options?: PythonTerminalOptions,
): Promise<Terminal> {
const api = await getEnvExtApi();
const pythonEnv = await api.getEnvironment(resource);
const project = resource ? api.getPythonProject(resource) : undefined;

if (pythonEnv && project) {
const fixedOptions = options ? { ...options } : { cwd: project.uri };
const terminal = await api.createTerminal(pythonEnv, fixedOptions);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems like this is calling createTerminal from env extension API, Im trying to understand why this and the interpreter one are both called "legacy"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it is temporary solution to the old features in the Python extension. These apis can be deleted when we delete the code for old Python features.

return terminal;
}
throw new Error('Invalid arguments to create terminal');
}
Loading
Loading