Skip to content

Commit

Permalink
Use env extension when available (#24564)
Browse files Browse the repository at this point in the history
  • Loading branch information
karthiknadig authored Dec 10, 2024
1 parent 63c3780 commit e789348
Show file tree
Hide file tree
Showing 36 changed files with 2,272 additions and 42 deletions.
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);
this.terminal = this.terminalManager.createTerminal({
name: this.options?.title || 'Python',
hideFromUser: this.options?.hideFromUser,
});
this.terminalAutoActivator.disableAutoActivation(this.terminal);

await sleep(100);

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);
return terminal;
}
throw new Error('Invalid arguments to create terminal');
}
Loading

0 comments on commit e789348

Please sign in to comment.