|
5 | 5 | user opens VS Code |
6 | 6 | python environments extension begins activation |
7 | 7 |
|
8 | | -SYNC (`activate` in extension.ts): |
9 | | -1. create core objects: ProjectManager, EnvironmentManagers, ManagerReady |
10 | | -2. `setPythonApi()` — API object created, deferred resolved (API is now available to consumers) |
11 | | -3. create views (EnvManagerView, ProjectView), status bar, terminal manager |
12 | | -4. register all commands |
13 | | -5. activate() returns — extension is "active" from VS Code's perspective |
| 8 | +**SYNC (`activate` in extension.ts):** |
| 9 | +1. create StatusBar, ProjectManager, EnvVarManager, EnvironmentManagers, ManagerReady |
| 10 | +2. create TerminalActivation, shell providers, TerminalManager |
| 11 | +3. create ProjectCreators |
| 12 | +4. `setPythonApi()` — API object created, deferred resolved (API is now available to consumers) |
| 13 | +5. create views (EnvManagerView, ProjectView) |
| 14 | +6. register all commands |
| 15 | +7. activate() returns — extension is "active" from VS Code's perspective |
14 | 16 |
|
15 | 17 | 📊 TELEMETRY: EXTENSION.ACTIVATION_DURATION { duration } |
16 | 18 |
|
17 | | -ASYNC (setImmediate callback, still in extension.ts): |
| 19 | +**ASYNC (setImmediate callback, still in extension.ts):** |
18 | 20 | 1. spawn PET process (`createNativePythonFinder`) |
19 | 21 | 1. sets up a JSON-RPC connection to it over stdin/stdout |
20 | 22 | 2. register all built-in managers in parallel (Promise.all): |
21 | | - - for each manager (system, conda, pyenv, pipenv, poetry): |
22 | | - 1. check if tool exists (e.g. `getConda(nativeFinder)` asks PET for the conda binary) |
23 | | - 2. if tool not found → log, return early (manager not registered) |
24 | | - 3. if tool found → create manager, call `api.registerEnvironmentManager(manager)` |
25 | | - - this adds it to the `EnvironmentManagers` map |
26 | | - - fires `onDidChangeEnvironmentManager` → `ManagerReady` deferred resolves for this manager |
27 | | -3. all registrations complete (Promise.all resolves) |
28 | | - |
29 | | ---- gate point: `applyInitialEnvironmentSelection` --- |
30 | | - 📊 TELEMETRY: ENV_SELECTION.STARTED { duration (activation→here), registeredManagerCount, registeredManagerIds, workspaceFolderCount } |
31 | | - |
32 | | -1. for each workspace folder + global scope (no workspace case), run `resolvePriorityChainCore` to find manager: |
33 | | - - P1: pythonProjects[] setting → specific manager for this project |
34 | | - - P2: user-configured defaultEnvManager setting |
35 | | - - P3: user-configured python.defaultInterpreterPath → nativeFinder.resolve(path) |
36 | | - - P4: auto-discovery → try venv manager (local .venv), fall back to system python |
37 | | - - for workspace scope: ask venv manager if there's a local env (.venv/venv in the folder) |
38 | | - - if found → use venv manager with that env |
39 | | - - if not found → fall back to system python manager |
40 | | - - for global scope: use system python manager directly |
41 | | - |
42 | | -2. get the environment from the winning priority level: |
43 | | - |
44 | | - --- fork point: `result.environment ?? await result.manager.get(folder.uri)` --- |
45 | | - left side truthy = envPreResolved | left side undefined = managerDiscovery |
46 | | - |
47 | | - envPreResolved — P3 won (interpreter → manager): |
48 | | - `resolvePriorityChainCore` calls `tryResolveInterpreterPath()`: |
49 | | - 1. `nativeFinder.resolve(path)` — single PET call, resolves just this one binary |
50 | | - 2. find which manager owns the resolved env (by managerId) |
51 | | - 3. return { manager, environment } — BOTH are known |
52 | | - → result.environment is set → the `??` short-circuits |
53 | | - → no `manager.get()` called, no `initialize()`, no full discovery |
54 | | - |
55 | | - managerDiscovery — P1, P2, or P4 won (manager → interpreter): |
56 | | - `resolvePriorityChainCore` returns { manager, environment: undefined } |
57 | | - → result.environment is undefined → falls through to `await result.manager.get(scope)` |
58 | | - `manager.get(scope)` (e.g. `CondaEnvManager.get()`): |
59 | | - 4. `initialize()` — lazy, once-only per manager (guarded by deferred) |
60 | | - a. `nativeFinder.refresh(hardRefresh=false)`: |
61 | | - → `handleSoftRefresh()` checks in-memory cache (Map) for key 'all' (bc one big scan, shared cache, all managers benefit) |
62 | | - - on reload: cache is empty (Map was destroyed) → cache miss |
63 | | - - falls through to `handleHardRefresh()` |
64 | | - → `handleHardRefresh()`: |
65 | | - - adds request to WorkerPool queue (concurrency 1, so serialized) |
66 | | - - when its turn comes, calls `doRefresh()`: |
67 | | - 1. `configure()` — JSON-RPC to PET with search paths, conda/poetry/pipenv paths, cache dir |
68 | | - 2. `refresh` — JSON-RPC to PET, PET scans filesystem |
69 | | - - PET may use its own on-disk cache (cacheDirectory) to speed this up |
70 | | - - PET streams back results as 'environment' and 'manager' notifications |
71 | | - - envs missing version/prefix get an inline resolve() call |
72 | | - 3. returns NativeInfo[] (all envs of all types) |
73 | | - - result stored in in-memory cache under key 'all' |
74 | | - → subsequent managers calling nativeFinder.refresh(false) get cache hit → instant |
75 | | - b. filter results to this manager's env type (e.g. conda filters to kind=conda) |
76 | | - c. convert NativeEnvInfo → PythonEnvironment objects → populate collection |
77 | | - d. `loadEnvMap()` — reads persisted env path from workspace state |
78 | | - → matches path against freshly discovered collection via `findEnvironmentByPath()` |
79 | | - → populates `fsPathToEnv` map |
80 | | - 5. look up scope in `fsPathToEnv` → return the matched env |
81 | | - |
82 | | - 📊 TELEMETRY: ENV_SELECTION.RESULT (per scope) { duration (priority chain + manager.get), scope, prioritySource, managerId, path, hasPersistedSelection } |
83 | | - |
84 | | -3. env is cached in memory (no settings.json write) |
85 | | -4. Python extension / status bar can now get the selected env via `api.getEnvironment(scope)` |
86 | | - |
87 | | - 📊 TELEMETRY: EXTENSION.MANAGER_REGISTRATION_DURATION { duration (activation→here), result, failureStage?, errorType? } |
| 23 | + - system: create SysPythonManager + VenvManager + PipPackageManager, register immediately (✅ NO PET call, sets up file watcher) |
| 24 | + - conda: `getConda(nativeFinder)` checks settings → cache → persistent state → PATH |
| 25 | + - pyenv & pipenv & poetry: create PyEnvManager, register immediately |
| 26 | + - ✅ NO PET call — always registers unconditionally (lazy discovery) |
| 27 | + - shellStartupVars: initialize |
| 28 | + - all managers fire `onDidChangeEnvironmentManager` → ManagerReady resolves |
| 29 | +3. all registrations complete (Promise.all resolves) — fast, typically milliseconds |
| 30 | + |
| 31 | + |
| 32 | +**--- gate point: `applyInitialEnvironmentSelection` ---** |
| 33 | + |
| 34 | + 📊 TELEMETRY: ENV_SELECTION.STARTED { duration, registeredManagerCount, registeredManagerIds, workspaceFolderCount } |
| 35 | + |
| 36 | +**Step 1 — pick a manager** (`resolvePriorityChainCore`, per workspace folder + global): |
| 37 | + |
| 38 | +| Priority | Source | Returns | |
| 39 | +|----------|--------|---------| |
| 40 | +| P1 | `pythonProjects[]` setting | manager only | |
| 41 | +| P2 | `defaultEnvManager` setting | manager only | |
| 42 | +| P3 | `python.defaultInterpreterPath` → `nativeFinder.resolve(path)` | manager **+ environment** | |
| 43 | +| P4 | auto-discovery: venv → system python fallback | manager only | |
| 44 | + |
| 45 | +**Step 2 — get the environment** (`result.environment ?? await result.manager.get(scope)`): |
| 46 | + |
| 47 | +- **If P3 won:** environment is already resolved → done, no `get()` call needed. |
| 48 | +- **Otherwise:** calls `manager.get(scope)`, which has two internal paths: |
| 49 | + |
| 50 | + **Fast path** (`tryFastPathGet` in `fastPath.ts`) — entered when `_initialized` hasn't completed and scope is a `Uri`: |
| 51 | + 1. Synchronously create `_initialized` deferred + kick off `startBackgroundInit()` (fire-and-forget full PET discovery) |
| 52 | + 2. Read persisted env path from workspace state |
| 53 | + 3. If persisted path exists → `resolve(path)` → return immediately (background init continues in parallel) |
| 54 | + 4. If no persisted path or resolve fails → fall through to slow path |
| 55 | + - *On background init failure:* clears `_initialized` so next `get()` retries |
| 56 | + |
| 57 | + **Slow path** — fast path skipped or failed: |
| 58 | + 1. `initialize()` — lazy, once-only (guarded by `_initialized` deferred, concurrent callers await it) |
| 59 | + - `nativeFinder.refresh(false)` → PET scan (cached across managers after first call) |
| 60 | + - Filter results to this manager's type → populate `collection` |
| 61 | + - `loadEnvMap()` → match persisted paths against discovered envs |
| 62 | + 2. Look up scope in `fsPathToEnv` → return matched env |
| 63 | + |
| 64 | + 📊 TELEMETRY: ENV_SELECTION.RESULT (per scope) { duration, scope, prioritySource, managerId, path, hasPersistedSelection } |
| 65 | + |
| 66 | +**Step 3 — done:** |
| 67 | +- env cached in memory (no settings.json write) |
| 68 | +- available via `api.getEnvironment(scope)` |
| 69 | + |
| 70 | + 📊 TELEMETRY: EXTENSION.MANAGER_REGISTRATION_DURATION { duration, result, failureStage?, errorType? } |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +### Other entry points to `initialize()` |
| 75 | + |
| 76 | +All three trigger `initialize()` lazily (once-only, guarded by `_initialized` deferred). After the first call completes, subsequent calls are no-ops. |
| 77 | + |
| 78 | +**`manager.get(scope)`** — environment selection (Step 2 above): |
| 79 | +- Called during `applyInitialEnvironmentSelection` or when settings change triggers re-selection |
| 80 | +- Fast path may resolve immediately; slow path awaits `initialize()` |
| 81 | + |
| 82 | +**`manager.getEnvironments(scope)`** — sidebar / listing: |
| 83 | +- Called when user expands a manager node in the Python environments panel |
| 84 | +- Also called by any API consumer requesting the full environment list |
| 85 | +- If PET cache populated from earlier `get()` → instant hit; otherwise warm PET call |
| 86 | + |
| 87 | +**`manager.resolve(context)`** — path resolution: |
| 88 | +- Called when resolving a specific Python binary path to check if it belongs to this manager |
| 89 | +- Used by `tryResolveInterpreterPath()` in the priority chain (P3) and by external API consumers |
| 90 | +- Awaits `initialize()`, then delegates to manager-specific resolve (e.g., `resolvePipenvPath`) |
| 91 | + |
| 92 | +--- |
88 | 93 |
|
89 | 94 | POST-INIT: |
90 | 95 | 1. register terminal package watcher |
91 | 96 | 2. register settings change listener (`registerInterpreterSettingsChangeListener`) — re-runs priority chain if settings change |
92 | 97 | 3. initialize terminal manager |
93 | | -4. send telemetry (manager selection, project structure, discovery summary) |
| 98 | +4. send telemetry (manager selection, project structure, discovery summary) |
0 commit comments