diff --git a/src/client/envExt/api.internal.ts b/src/client/envExt/api.internal.ts index a47193d3cd95..7d511eac49ea 100644 --- a/src/client/envExt/api.internal.ts +++ b/src/client/envExt/api.internal.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Terminal, Uri } from 'vscode'; +import { EventEmitter, Terminal, Uri, Disposable, ConfigurationTarget } from 'vscode'; import { getExtension } from '../common/vscodeApis/extensionsApi'; import { GetEnvironmentScope, @@ -10,8 +10,10 @@ import { PythonEnvironmentApi, PythonProcess, RefreshEnvironmentsScope, + DidChangeEnvironmentEventArgs, } from './types'; import { executeCommand } from '../common/vscodeApis/commandApis'; +import { IInterpreterPathService } from '../common/types'; export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; @@ -24,6 +26,17 @@ export function useEnvExtension(): boolean { return _useExt; } +const onDidChangeEnvironmentEnvExtEmitter: EventEmitter = new EventEmitter< + DidChangeEnvironmentEventArgs +>(); +export function onDidChangeEnvironmentEnvExt( + listener: (e: DidChangeEnvironmentEventArgs) => unknown, + thisArgs?: unknown, + disposables?: Disposable[], +): Disposable { + return onDidChangeEnvironmentEnvExtEmitter.event(listener, thisArgs, disposables); +} + let _extApi: PythonEnvironmentApi | undefined; export async function getEnvExtApi(): Promise { if (_extApi) { @@ -33,14 +46,15 @@ export async function getEnvExtApi(): Promise { if (!extension) { throw new Error('Python Environments extension not found.'); } - if (extension?.isActive) { - _extApi = extension.exports as PythonEnvironmentApi; - return _extApi; + if (!extension?.isActive) { + await extension.activate(); } - await extension.activate(); - _extApi = extension.exports as PythonEnvironmentApi; + _extApi.onDidChangeEnvironment((e) => { + onDidChangeEnvironmentEnvExtEmitter.fire(e); + }); + return _extApi; } @@ -106,3 +120,32 @@ export async function clearCache(): Promise { await executeCommand('python-envs.clearCache'); } } + +export function registerEnvExtFeatures( + disposables: Disposable[], + interpreterPathService: IInterpreterPathService, +): void { + if (useEnvExtension()) { + disposables.push( + onDidChangeEnvironmentEnvExt(async (e: DidChangeEnvironmentEventArgs) => { + const previousPath = interpreterPathService.get(e.uri); + + if (previousPath !== e.new?.environmentPath.fsPath) { + if (e.uri) { + await interpreterPathService.update( + e.uri, + ConfigurationTarget.WorkspaceFolder, + e.new?.environmentPath.fsPath, + ); + } else { + await interpreterPathService.update( + undefined, + ConfigurationTarget.Global, + e.new?.environmentPath.fsPath, + ); + } + } + }), + ); + } +} diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts index 558938d7d0b7..ecd8eef21845 100644 --- a/src/client/environmentApi.ts +++ b/src/client/environmentApi.ts @@ -11,7 +11,7 @@ import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironment import { getEnvPath } from './pythonEnvironments/base/info/env'; import { IDiscoveryAPI, ProgressReportStage } from './pythonEnvironments/base/locator'; import { IPythonExecutionFactory } from './common/process/types'; -import { traceError, traceVerbose } from './logging'; +import { traceError, traceInfo, traceVerbose } from './logging'; import { isParentPath, normCasePath } from './common/platform/fs-paths'; import { sendTelemetryEvent } from './telemetry'; import { EventName } from './telemetry/constants'; @@ -42,7 +42,13 @@ type ActiveEnvironmentChangeEvent = { }; const onDidActiveInterpreterChangedEvent = new EventEmitter(); +const previousEnvMap = new Map(); export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { + const oldPath = previousEnvMap.get(e.resource?.uri.fsPath ?? ''); + if (oldPath === e.path) { + return; + } + previousEnvMap.set(e.resource?.uri.fsPath ?? '', e.path); onDidActiveInterpreterChangedEvent.fire({ id: getEnvID(e.path), path: e.path, resource: e.resource }); reportActiveInterpreterChangedDeprecated({ path: e.path, resource: e.resource?.uri }); } @@ -172,6 +178,13 @@ export function buildEnvironmentApi( } disposables.push( + onDidActiveInterpreterChangedEvent.event((e) => { + let scope = 'global'; + if (e.resource) { + scope = e.resource instanceof Uri ? e.resource.fsPath : e.resource.uri.fsPath; + } + traceInfo(`Active interpreter [${scope}]: `, e.path); + }), discoveryApi.onProgress((e) => { if (e.stage === ProgressReportStage.discoveryFinished) { knownCache = initKnownCache(); diff --git a/src/client/extensionActivation.ts b/src/client/extensionActivation.ts index 4a1acca62da5..362fcf8468ad 100644 --- a/src/client/extensionActivation.ts +++ b/src/client/extensionActivation.ts @@ -56,6 +56,7 @@ import { registerTriggerForTerminalREPL } from './terminals/codeExecution/termin import { registerPythonStartup } from './terminals/pythonStartup'; import { registerPixiFeatures } from './pythonEnvironments/common/environmentManagers/pixi'; import { registerCustomTerminalLinkProvider } from './terminals/pythonStartupLinkProvider'; +import { registerEnvExtFeatures } from './envExt/api.internal'; export async function activateComponents( // `ext` is passed to any extra activation funcs. @@ -101,6 +102,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components): const interpreterService: IInterpreterService = ext.legacyIOC.serviceContainer.get( IInterpreterService, ); + registerEnvExtFeatures(ext.disposables, interpreterPathService); const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); registerPixiFeatures(ext.disposables); registerAllCreateEnvironmentFeatures( diff --git a/src/test/common/persistentState.unit.test.ts b/src/test/common/persistentState.unit.test.ts index 9af28e2f5860..a77ee571559e 100644 --- a/src/test/common/persistentState.unit.test.ts +++ b/src/test/common/persistentState.unit.test.ts @@ -5,6 +5,7 @@ import { assert, expect } from 'chai'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import { Memento } from 'vscode'; import { ICommandManager } from '../../client/common/application/types'; import { Commands } from '../../client/common/constants'; @@ -17,17 +18,25 @@ import { import { IDisposable } from '../../client/common/types'; import { sleep } from '../core'; import { MockMemento } from '../mocks/mementos'; +import * as apiInt from '../../client/envExt/api.internal'; suite('Persistent State', () => { let cmdManager: TypeMoq.IMock; let persistentStateFactory: PersistentStateFactory; let workspaceMemento: Memento; let globalMemento: Memento; + let useEnvExtensionStub: sinon.SinonStub; setup(() => { cmdManager = TypeMoq.Mock.ofType(); workspaceMemento = new MockMemento(); globalMemento = new MockMemento(); persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento, cmdManager.object); + + useEnvExtensionStub = sinon.stub(apiInt, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + }); + teardown(() => { + sinon.restore(); }); test('Global states created are restored on invoking clean storage command', async () => {