Skip to content

Commit e789348

Browse files
authored
Use env extension when available (#24564)
1 parent 63c3780 commit e789348

36 files changed

+2272
-42
lines changed

src/client/common/persistentState.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import { cache } from './utils/decorators';
2121
import { noop } from './utils/misc';
2222
import { clearCacheDirectory } from '../pythonEnvironments/base/locators/common/nativePythonFinder';
23+
import { clearCache, useEnvExtension } from '../envExt/api.internal';
2324

2425
let _workspaceState: Memento | undefined;
2526
const _workspaceKeys: string[] = [];
@@ -134,6 +135,9 @@ export class PersistentStateFactory implements IPersistentStateFactory, IExtensi
134135
this.cmdManager?.registerCommand(Commands.ClearStorage, async () => {
135136
await clearWorkspaceState();
136137
await this.cleanAllPersistentStates();
138+
if (useEnvExtension()) {
139+
await clearCache();
140+
}
137141
});
138142
const globalKeysStorageDeprecated = this.createGlobalPersistentState(GLOBAL_PERSISTENT_KEYS_DEPRECATED, []);
139143
const workspaceKeysStorageDeprecated = this.createWorkspacePersistentState(

src/client/common/terminal/activator/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { IConfigurationService, IExperimentService } from '../../types';
99
import { ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, TerminalActivationOptions } from '../types';
1010
import { BaseTerminalActivator } from './base';
1111
import { inTerminalEnvVarExperiment } from '../../experiments/helpers';
12+
import { useEnvExtension } from '../../../envExt/api.internal';
1213

1314
@injectable()
1415
export class TerminalActivator implements ITerminalActivator {
@@ -41,7 +42,7 @@ export class TerminalActivator implements ITerminalActivator {
4142
const settings = this.configurationService.getSettings(options?.resource);
4243
const activateEnvironment =
4344
settings.terminal.activateEnvironment && !inTerminalEnvVarExperiment(this.experimentService);
44-
if (!activateEnvironment || options?.hideFromUser) {
45+
if (!activateEnvironment || options?.hideFromUser || useEnvExtension()) {
4546
return false;
4647
}
4748

src/client/common/terminal/service.ts

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import {
2121
} from './types';
2222
import { traceVerbose } from '../../logging';
2323
import { getConfiguration } from '../vscodeApis/workspaceApis';
24+
import { useEnvExtension } from '../../envExt/api.internal';
25+
import { ensureTerminalLegacy } from '../../envExt/api.legacy';
26+
import { sleep } from '../utils/async';
2427
import { isWindows } from '../utils/platform';
2528

2629
@injectable()
@@ -132,22 +135,29 @@ export class TerminalService implements ITerminalService, Disposable {
132135
if (this.terminal) {
133136
return;
134137
}
135-
this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal);
136-
this.terminal = this.terminalManager.createTerminal({
137-
name: this.options?.title || 'Python',
138-
hideFromUser: this.options?.hideFromUser,
139-
});
140-
this.terminalAutoActivator.disableAutoActivation(this.terminal);
141138

142-
// Sometimes the terminal takes some time to start up before it can start accepting input.
143-
await new Promise((resolve) => setTimeout(resolve, 100));
139+
if (useEnvExtension()) {
140+
this.terminal = await ensureTerminalLegacy(this.options?.resource, {
141+
name: this.options?.title || 'Python',
142+
hideFromUser: this.options?.hideFromUser,
143+
});
144+
} else {
145+
this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal);
146+
this.terminal = this.terminalManager.createTerminal({
147+
name: this.options?.title || 'Python',
148+
hideFromUser: this.options?.hideFromUser,
149+
});
150+
this.terminalAutoActivator.disableAutoActivation(this.terminal);
151+
152+
await sleep(100);
144153

145-
await this.terminalActivator.activateEnvironmentInTerminal(this.terminal, {
146-
resource: this.options?.resource,
147-
preserveFocus,
148-
interpreter: this.options?.interpreter,
149-
hideFromUser: this.options?.hideFromUser,
150-
});
154+
await this.terminalActivator.activateEnvironmentInTerminal(this.terminal, {
155+
resource: this.options?.resource,
156+
preserveFocus,
157+
interpreter: this.options?.interpreter,
158+
hideFromUser: this.options?.hideFromUser,
159+
});
160+
}
151161

152162
if (!this.options?.hideFromUser) {
153163
this.terminal.show(preserveFocus);

src/client/envExt/api.internal.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { Terminal, Uri } from 'vscode';
5+
import { getExtension } from '../common/vscodeApis/extensionsApi';
6+
import {
7+
GetEnvironmentScope,
8+
PythonBackgroundRunOptions,
9+
PythonEnvironment,
10+
PythonEnvironmentApi,
11+
PythonProcess,
12+
RefreshEnvironmentsScope,
13+
} from './types';
14+
import { executeCommand } from '../common/vscodeApis/commandApis';
15+
16+
export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs';
17+
18+
let _useExt: boolean | undefined;
19+
export function useEnvExtension(): boolean {
20+
if (_useExt !== undefined) {
21+
return _useExt;
22+
}
23+
_useExt = !!getExtension(ENVS_EXTENSION_ID);
24+
return _useExt;
25+
}
26+
27+
let _extApi: PythonEnvironmentApi | undefined;
28+
export async function getEnvExtApi(): Promise<PythonEnvironmentApi> {
29+
if (_extApi) {
30+
return _extApi;
31+
}
32+
const extension = getExtension(ENVS_EXTENSION_ID);
33+
if (!extension) {
34+
throw new Error('Python Environments extension not found.');
35+
}
36+
if (extension?.isActive) {
37+
_extApi = extension.exports as PythonEnvironmentApi;
38+
return _extApi;
39+
}
40+
41+
await extension.activate();
42+
43+
_extApi = extension.exports as PythonEnvironmentApi;
44+
return _extApi;
45+
}
46+
47+
export async function runInBackground(
48+
environment: PythonEnvironment,
49+
options: PythonBackgroundRunOptions,
50+
): Promise<PythonProcess> {
51+
const envExtApi = await getEnvExtApi();
52+
return envExtApi.runInBackground(environment, options);
53+
}
54+
55+
export async function getEnvironment(scope: GetEnvironmentScope): Promise<PythonEnvironment | undefined> {
56+
const envExtApi = await getEnvExtApi();
57+
return envExtApi.getEnvironment(scope);
58+
}
59+
60+
export async function refreshEnvironments(scope: RefreshEnvironmentsScope): Promise<void> {
61+
const envExtApi = await getEnvExtApi();
62+
return envExtApi.refreshEnvironments(scope);
63+
}
64+
65+
export async function runInTerminal(
66+
resource: Uri | undefined,
67+
args?: string[],
68+
cwd?: string | Uri,
69+
show?: boolean,
70+
): Promise<Terminal> {
71+
const envExtApi = await getEnvExtApi();
72+
const env = await getEnvironment(resource);
73+
const project = resource ? envExtApi.getPythonProject(resource) : undefined;
74+
if (env && resource) {
75+
return envExtApi.runInTerminal(env, {
76+
cwd: cwd ?? project?.uri ?? process.cwd(),
77+
args,
78+
show,
79+
});
80+
}
81+
throw new Error('Invalid arguments to run in terminal');
82+
}
83+
84+
export async function runInDedicatedTerminal(
85+
resource: Uri | undefined,
86+
args?: string[],
87+
cwd?: string | Uri,
88+
show?: boolean,
89+
): Promise<Terminal> {
90+
const envExtApi = await getEnvExtApi();
91+
const env = await getEnvironment(resource);
92+
const project = resource ? envExtApi.getPythonProject(resource) : undefined;
93+
if (env) {
94+
return envExtApi.runInDedicatedTerminal(resource ?? 'global', env, {
95+
cwd: cwd ?? project?.uri ?? process.cwd(),
96+
args,
97+
show,
98+
});
99+
}
100+
throw new Error('Invalid arguments to run in dedicated terminal');
101+
}
102+
103+
export async function clearCache(): Promise<void> {
104+
const envExtApi = await getEnvExtApi();
105+
if (envExtApi) {
106+
await executeCommand('python-envs.clearCache');
107+
}
108+
}

src/client/envExt/api.legacy.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { Terminal, Uri } from 'vscode';
5+
import { getEnvExtApi, getEnvironment } from './api.internal';
6+
import { EnvironmentType, PythonEnvironment as PythonEnvironmentLegacy } from '../pythonEnvironments/info';
7+
import { PythonEnvironment, PythonTerminalOptions } from './types';
8+
import { Architecture } from '../common/utils/platform';
9+
import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion';
10+
import { PythonEnvType } from '../pythonEnvironments/base/info';
11+
import { traceError, traceInfo } from '../logging';
12+
import { reportActiveInterpreterChanged } from '../environmentApi';
13+
import { getWorkspaceFolder } from '../common/vscodeApis/workspaceApis';
14+
15+
function toEnvironmentType(pythonEnv: PythonEnvironment): EnvironmentType {
16+
if (pythonEnv.envId.managerId.toLowerCase().endsWith('system')) {
17+
return EnvironmentType.System;
18+
}
19+
if (pythonEnv.envId.managerId.toLowerCase().endsWith('venv')) {
20+
return EnvironmentType.Venv;
21+
}
22+
if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenv')) {
23+
return EnvironmentType.VirtualEnv;
24+
}
25+
if (pythonEnv.envId.managerId.toLowerCase().endsWith('conda')) {
26+
return EnvironmentType.Conda;
27+
}
28+
if (pythonEnv.envId.managerId.toLowerCase().endsWith('pipenv')) {
29+
return EnvironmentType.Pipenv;
30+
}
31+
if (pythonEnv.envId.managerId.toLowerCase().endsWith('poetry')) {
32+
return EnvironmentType.Poetry;
33+
}
34+
if (pythonEnv.envId.managerId.toLowerCase().endsWith('pyenv')) {
35+
return EnvironmentType.Pyenv;
36+
}
37+
if (pythonEnv.envId.managerId.toLowerCase().endsWith('hatch')) {
38+
return EnvironmentType.Hatch;
39+
}
40+
if (pythonEnv.envId.managerId.toLowerCase().endsWith('pixi')) {
41+
return EnvironmentType.Pixi;
42+
}
43+
if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenvwrapper')) {
44+
return EnvironmentType.VirtualEnvWrapper;
45+
}
46+
if (pythonEnv.envId.managerId.toLowerCase().endsWith('activestate')) {
47+
return EnvironmentType.ActiveState;
48+
}
49+
return EnvironmentType.Unknown;
50+
}
51+
52+
function getEnvType(kind: EnvironmentType): PythonEnvType | undefined {
53+
switch (kind) {
54+
case EnvironmentType.Pipenv:
55+
case EnvironmentType.VirtualEnv:
56+
case EnvironmentType.Pyenv:
57+
case EnvironmentType.Venv:
58+
case EnvironmentType.Poetry:
59+
case EnvironmentType.Hatch:
60+
case EnvironmentType.Pixi:
61+
case EnvironmentType.VirtualEnvWrapper:
62+
case EnvironmentType.ActiveState:
63+
return PythonEnvType.Virtual;
64+
65+
case EnvironmentType.Conda:
66+
return PythonEnvType.Conda;
67+
68+
case EnvironmentType.MicrosoftStore:
69+
case EnvironmentType.Global:
70+
case EnvironmentType.System:
71+
default:
72+
return undefined;
73+
}
74+
}
75+
76+
function toLegacyType(env: PythonEnvironment): PythonEnvironmentLegacy {
77+
const ver = parseVersion(env.version);
78+
const envType = toEnvironmentType(env);
79+
return {
80+
id: env.environmentPath.fsPath,
81+
displayName: env.displayName,
82+
detailedDisplayName: env.name,
83+
envType,
84+
envPath: env.sysPrefix,
85+
type: getEnvType(envType),
86+
path: env.environmentPath.fsPath,
87+
version: {
88+
raw: env.version,
89+
major: ver.major,
90+
minor: ver.minor,
91+
patch: ver.micro,
92+
build: [],
93+
prerelease: [],
94+
},
95+
sysVersion: env.version,
96+
architecture: Architecture.x64,
97+
sysPrefix: env.sysPrefix,
98+
};
99+
}
100+
101+
const previousEnvMap = new Map<string, PythonEnvironment | undefined>();
102+
export async function getActiveInterpreterLegacy(resource?: Uri): Promise<PythonEnvironmentLegacy | undefined> {
103+
const api = await getEnvExtApi();
104+
const uri = resource ? api.getPythonProject(resource)?.uri : undefined;
105+
106+
const pythonEnv = await getEnvironment(resource);
107+
const oldEnv = previousEnvMap.get(uri?.fsPath || '');
108+
const newEnv = pythonEnv ? toLegacyType(pythonEnv) : undefined;
109+
if (newEnv && oldEnv?.envId.id !== pythonEnv?.envId.id) {
110+
reportActiveInterpreterChanged({
111+
resource: getWorkspaceFolder(resource),
112+
path: newEnv.path,
113+
});
114+
}
115+
return pythonEnv ? toLegacyType(pythonEnv) : undefined;
116+
}
117+
118+
export async function ensureEnvironmentContainsPythonLegacy(pythonPath: string): Promise<void> {
119+
const api = await getEnvExtApi();
120+
const pythonEnv = await api.resolveEnvironment(Uri.file(pythonPath));
121+
if (!pythonEnv) {
122+
traceError(`EnvExt: Failed to resolve environment for ${pythonPath}`);
123+
return;
124+
}
125+
126+
const envType = toEnvironmentType(pythonEnv);
127+
if (envType === EnvironmentType.Conda) {
128+
const packages = await api.getPackages(pythonEnv);
129+
if (packages && packages.length > 0 && packages.some((pkg) => pkg.name.toLowerCase() === 'python')) {
130+
return;
131+
}
132+
traceInfo(`EnvExt: Python not found in ${envType} environment ${pythonPath}`);
133+
traceInfo(`EnvExt: Installing Python in ${envType} environment ${pythonPath}`);
134+
await api.installPackages(pythonEnv, ['python']);
135+
}
136+
}
137+
138+
export async function setInterpreterLegacy(pythonPath: string, uri: Uri | undefined): Promise<void> {
139+
const api = await getEnvExtApi();
140+
const pythonEnv = await api.resolveEnvironment(Uri.file(pythonPath));
141+
if (!pythonEnv) {
142+
traceError(`EnvExt: Failed to resolve environment for ${pythonPath}`);
143+
return;
144+
}
145+
await api.setEnvironment(uri, pythonEnv);
146+
}
147+
148+
export async function resetInterpreterLegacy(uri: Uri | undefined): Promise<void> {
149+
const api = await getEnvExtApi();
150+
await api.setEnvironment(uri, undefined);
151+
}
152+
153+
export async function ensureTerminalLegacy(
154+
resource: Uri | undefined,
155+
options?: PythonTerminalOptions,
156+
): Promise<Terminal> {
157+
const api = await getEnvExtApi();
158+
const pythonEnv = await api.getEnvironment(resource);
159+
const project = resource ? api.getPythonProject(resource) : undefined;
160+
161+
if (pythonEnv && project) {
162+
const fixedOptions = options ? { ...options } : { cwd: project.uri };
163+
const terminal = await api.createTerminal(pythonEnv, fixedOptions);
164+
return terminal;
165+
}
166+
throw new Error('Invalid arguments to create terminal');
167+
}

0 commit comments

Comments
 (0)