@@ -18,6 +18,8 @@ import {
1818 IDisposableRegistry ,
1919 IInstaller ,
2020 IInterpreterPathService ,
21+ IPersistentState ,
22+ IPersistentStateFactory ,
2123 Product ,
2224} from '../common/types' ;
2325import { IServiceContainer } from '../ioc/types' ;
@@ -49,6 +51,15 @@ import { getActiveInterpreterLegacy } from '../envExt/api.legacy';
4951
5052type StoredPythonEnvironment = PythonEnvironment & { store ?: boolean } ;
5153
54+ // Upper bound on how long `getActiveInterpreter` may block its caller. Resolving the active
55+ // interpreter can wait on a full environment refresh (especially with the environments
56+ // extension enabled), which would otherwise delay language server startup.
57+ const GET_ACTIVE_INTERPRETER_TIMEOUT_MS = 100 ;
58+ const ACTIVE_INTERPRETER_TIMED_OUT = Symbol ( 'activeInterpreterTimedOut' ) ;
59+
60+ // Key prefix for the persisted (cross-restart) last-known active interpreter, one entry per workspace.
61+ const LAST_KNOWN_ACTIVE_INTERPRETER_KEY_PREFIX = 'lastKnownActiveInterpreter:' ;
62+
5263@injectable ( )
5364export class InterpreterService implements Disposable , IInterpreterService {
5465 public async hasInterpreters (
@@ -100,6 +111,23 @@ export class InterpreterService implements Disposable, IInterpreterService {
100111 { path : string ; workspaceFolder : WorkspaceFolder | undefined }
101112 > ( ) ;
102113
114+ // Last successfully resolved active interpreter per workspace, served when a resolution
115+ // doesn't complete within `GET_ACTIVE_INTERPRETER_TIMEOUT_MS`.
116+ private readonly lastKnownActiveInterpreter = new Map <
117+ string ,
118+ { resource : Uri | undefined ; env : StoredPythonEnvironment | undefined }
119+ > ( ) ;
120+
121+ // In-flight active interpreter resolutions, de-duplicated per workspace.
122+ private readonly inFlightActiveInterpreter = new Map < string , Promise < StoredPythonEnvironment | undefined > > ( ) ;
123+
124+ // Persisted (cross-restart) last-known active interpreter, one persistent-state handle per workspace.
125+ // Lets a warm start serve a real interpreter within the timeout budget before discovery completes.
126+ private readonly persistedActiveInterpreter = new Map <
127+ string ,
128+ IPersistentState < StoredPythonEnvironment | undefined >
129+ > ( ) ;
130+
103131 constructor (
104132 @inject ( IServiceContainer ) private serviceContainer : IServiceContainer ,
105133 @inject ( IComponentAdapter ) private readonly pyenvs : IComponentAdapter ,
@@ -198,6 +226,13 @@ export class InterpreterService implements Disposable, IInterpreterService {
198226 } ) ,
199227 ) ;
200228 disposables . push ( this . interpreterPathService . onDidChange ( ( i ) => this . _onConfigChanged ( i . uri ) ) ) ;
229+ // Auto-selection completes in the background (it is no longer awaited before activation),
230+ // which updates the python path setting once an interpreter is chosen. Re-run the config
231+ // change handler so the newly selected interpreter is reported to listeners (e.g. Pylance).
232+ // `_onConfigChanged` is gated on the python path actually changing, and
233+ // `reportActiveInterpreterChanged` de-duplicates by path, so this is a no-op when nothing
234+ // changed. The environments-extension path reports its own changes via the legacy API.
235+ disposables . push ( this . configService . onDidChange ( ( ) => this . _onConfigChanged ( ) ) ) ;
201236 }
202237
203238 public getInterpreters ( resource ?: Uri ) : PythonEnvironment [ ] {
@@ -219,6 +254,72 @@ export class InterpreterService implements Disposable, IInterpreterService {
219254 }
220255
221256 public async getActiveInterpreter ( resource ?: Uri ) : Promise < PythonEnvironment | undefined > {
257+ const workspaceService = this . serviceContainer . get < IWorkspaceService > ( IWorkspaceService ) ;
258+ const key = workspaceService . getWorkspaceFolderIdentifier ( resource ) ;
259+
260+ // De-duplicate concurrent resolutions for the same workspace.
261+ let resolution = this . inFlightActiveInterpreter . get ( key ) ;
262+ if ( ! resolution ) {
263+ resolution = this . resolveActiveInterpreter ( resource )
264+ . then ( ( env ) => {
265+ this . lastKnownActiveInterpreter . set ( key , { resource, env } ) ;
266+ if ( env ) {
267+ // Persist the resolved interpreter so a future window/session can serve a
268+ // real value within the timeout budget before discovery/refresh completes.
269+ this . getPersistedActiveInterpreterState ( key )
270+ . updateValue ( env )
271+ . catch ( ( ex ) => traceError ( 'Failed to persist active interpreter' , ex ) ) ;
272+ }
273+ return env ;
274+ } )
275+ . catch ( ( ex ) => {
276+ traceError ( 'Failed to get active interpreter' , ex ) ;
277+ return undefined ;
278+ } )
279+ . finally ( ( ) => {
280+ this . inFlightActiveInterpreter . delete ( key ) ;
281+ } ) ;
282+ this . inFlightActiveInterpreter . set ( key , resolution ) ;
283+ }
284+
285+ // Don't block callers (notably language server startup) on a potentially slow
286+ // environment discovery/refresh. If resolution doesn't complete promptly, return the
287+ // last-known interpreter (or undefined on a cold start). The resolution promise is not
288+ // abandoned, so listeners are still notified of the real value once it becomes available:
289+ // the env-extension path reports via the environments API (see getActiveInterpreterLegacy),
290+ // and the native path reports via `_onConfigChanged` once auto-selection completes.
291+ const result = await Promise . race ( [
292+ resolution ,
293+ sleep ( GET_ACTIVE_INTERPRETER_TIMEOUT_MS ) . then ( ( ) => ACTIVE_INTERPRETER_TIMED_OUT ) ,
294+ ] ) ;
295+ if ( result !== ACTIVE_INTERPRETER_TIMED_OUT ) {
296+ return result as StoredPythonEnvironment | undefined ;
297+ }
298+ // Prefer the value resolved earlier this session; otherwise fall back to the persisted
299+ // value from a previous session so warm restarts still serve a real interpreter quickly.
300+ const cached = this . lastKnownActiveInterpreter . get ( key ) ;
301+ if ( cached ) {
302+ return cached . env ;
303+ }
304+ return this . getPersistedActiveInterpreterState ( key ) . value ;
305+ }
306+
307+ private getPersistedActiveInterpreterState (
308+ key : string ,
309+ ) : IPersistentState < StoredPythonEnvironment | undefined > {
310+ let state = this . persistedActiveInterpreter . get ( key ) ;
311+ if ( ! state ) {
312+ const factory = this . serviceContainer . get < IPersistentStateFactory > ( IPersistentStateFactory ) ;
313+ state = factory . createWorkspacePersistentState < StoredPythonEnvironment | undefined > (
314+ `${ LAST_KNOWN_ACTIVE_INTERPRETER_KEY_PREFIX } ${ key } ` ,
315+ undefined ,
316+ ) ;
317+ this . persistedActiveInterpreter . set ( key , state ) ;
318+ }
319+ return state ;
320+ }
321+
322+ private async resolveActiveInterpreter ( resource ?: Uri ) : Promise < StoredPythonEnvironment | undefined > {
222323 if ( useEnvExtension ( ) ) {
223324 return getActiveInterpreterLegacy ( resource ) ;
224325 }
0 commit comments