Skip to content

Commit 82015bc

Browse files
authored
Add support for file system watching with native python locator (microsoft#23852)
1 parent 9fad643 commit 82015bc

File tree

6 files changed

+308
-46
lines changed

6 files changed

+308
-46
lines changed

src/client/common/vscodeApis/workspaceApis.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,23 @@ export function onDidChangeTextDocument(handler: (e: vscode.TextDocumentChangeEv
5959
export function onDidChangeConfiguration(handler: (e: vscode.ConfigurationChangeEvent) => unknown): vscode.Disposable {
6060
return vscode.workspace.onDidChangeConfiguration(handler);
6161
}
62+
63+
export function createFileSystemWatcher(
64+
globPattern: vscode.GlobPattern,
65+
ignoreCreateEvents?: boolean,
66+
ignoreChangeEvents?: boolean,
67+
ignoreDeleteEvents?: boolean,
68+
): vscode.FileSystemWatcher {
69+
return vscode.workspace.createFileSystemWatcher(
70+
globPattern,
71+
ignoreCreateEvents,
72+
ignoreChangeEvents,
73+
ignoreDeleteEvents,
74+
);
75+
}
76+
77+
export function onDidChangeWorkspaceFolders(
78+
handler: (e: vscode.WorkspaceFoldersChangeEvent) => unknown,
79+
): vscode.Disposable {
80+
return vscode.workspace.onDidChangeWorkspaceFolders(handler);
81+
}

src/client/pythonEnvironments/base/locator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export type PythonLocatorQuery = BasicPythonLocatorQuery & {
134134
*/
135135
providerId?: string;
136136
/**
137-
* If provided, results area limited to this env.
137+
* If provided, results are limited to this env.
138138
*/
139139
envPath?: string;
140140
};
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { Disposable, Event, EventEmitter, GlobPattern, RelativePattern, Uri, WorkspaceFolder } from 'vscode';
5+
import { createFileSystemWatcher, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis';
6+
import { isWindows } from '../../../../common/platform/platformService';
7+
import { arePathsSame } from '../../../common/externalDependencies';
8+
import { FileChangeType } from '../../../../common/platform/fileSystemWatcher';
9+
10+
export interface PythonWorkspaceEnvEvent {
11+
type: FileChangeType;
12+
workspaceFolder: WorkspaceFolder;
13+
executable: string;
14+
}
15+
16+
export interface PythonGlobalEnvEvent {
17+
type: FileChangeType;
18+
uri: Uri;
19+
}
20+
21+
export interface PythonWatcher extends Disposable {
22+
watchWorkspace(wf: WorkspaceFolder): void;
23+
unwatchWorkspace(wf: WorkspaceFolder): void;
24+
onDidWorkspaceEnvChanged: Event<PythonWorkspaceEnvEvent>;
25+
26+
watchPath(uri: Uri, pattern?: string): void;
27+
unwatchPath(uri: Uri): void;
28+
onDidGlobalEnvChanged: Event<PythonGlobalEnvEvent>;
29+
}
30+
31+
/*
32+
* The pattern to search for python executables in the workspace.
33+
* project
34+
* ├── python or python.exe <--- This is what we are looking for.
35+
* ├── .conda
36+
* │ └── python or python.exe <--- This is what we are looking for.
37+
* └── .venv
38+
* │ └── Scripts or bin
39+
* │ └── python or python.exe <--- This is what we are looking for.
40+
*/
41+
const WORKSPACE_PATTERN = isWindows() ? '**/python.exe' : '**/python';
42+
43+
class PythonWatcherImpl implements PythonWatcher {
44+
private disposables: Disposable[] = [];
45+
46+
private readonly _onDidWorkspaceEnvChanged = new EventEmitter<PythonWorkspaceEnvEvent>();
47+
48+
private readonly _onDidGlobalEnvChanged = new EventEmitter<PythonGlobalEnvEvent>();
49+
50+
private readonly _disposeMap: Map<string, Disposable> = new Map<string, Disposable>();
51+
52+
constructor() {
53+
this.disposables.push(this._onDidWorkspaceEnvChanged, this._onDidGlobalEnvChanged);
54+
}
55+
56+
onDidGlobalEnvChanged: Event<PythonGlobalEnvEvent> = this._onDidGlobalEnvChanged.event;
57+
58+
onDidWorkspaceEnvChanged: Event<PythonWorkspaceEnvEvent> = this._onDidWorkspaceEnvChanged.event;
59+
60+
watchWorkspace(wf: WorkspaceFolder): void {
61+
if (this._disposeMap.has(wf.uri.fsPath)) {
62+
const disposer = this._disposeMap.get(wf.uri.fsPath);
63+
disposer?.dispose();
64+
}
65+
66+
const disposables: Disposable[] = [];
67+
const watcher = createFileSystemWatcher(new RelativePattern(wf, WORKSPACE_PATTERN));
68+
disposables.push(
69+
watcher,
70+
watcher.onDidChange((uri) => {
71+
this.fireWorkspaceEvent(FileChangeType.Changed, wf, uri);
72+
}),
73+
watcher.onDidCreate((uri) => {
74+
this.fireWorkspaceEvent(FileChangeType.Created, wf, uri);
75+
}),
76+
watcher.onDidDelete((uri) => {
77+
this.fireWorkspaceEvent(FileChangeType.Deleted, wf, uri);
78+
}),
79+
);
80+
81+
const disposable = {
82+
dispose: () => {
83+
disposables.forEach((d) => d.dispose());
84+
this._disposeMap.delete(wf.uri.fsPath);
85+
},
86+
};
87+
this._disposeMap.set(wf.uri.fsPath, disposable);
88+
}
89+
90+
unwatchWorkspace(wf: WorkspaceFolder): void {
91+
const disposable = this._disposeMap.get(wf.uri.fsPath);
92+
disposable?.dispose();
93+
}
94+
95+
private fireWorkspaceEvent(type: FileChangeType, wf: WorkspaceFolder, uri: Uri) {
96+
const uriWorkspace = getWorkspaceFolder(uri);
97+
if (uriWorkspace && arePathsSame(uriWorkspace.uri.fsPath, wf.uri.fsPath)) {
98+
this._onDidWorkspaceEnvChanged.fire({ type, workspaceFolder: wf, executable: uri.fsPath });
99+
}
100+
}
101+
102+
watchPath(uri: Uri, pattern?: string): void {
103+
if (this._disposeMap.has(uri.fsPath)) {
104+
const disposer = this._disposeMap.get(uri.fsPath);
105+
disposer?.dispose();
106+
}
107+
108+
const glob: GlobPattern = pattern ? new RelativePattern(uri, pattern) : uri.fsPath;
109+
const disposables: Disposable[] = [];
110+
const watcher = createFileSystemWatcher(glob);
111+
disposables.push(
112+
watcher,
113+
watcher.onDidChange(() => {
114+
this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Changed, uri });
115+
}),
116+
watcher.onDidCreate(() => {
117+
this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Created, uri });
118+
}),
119+
watcher.onDidDelete(() => {
120+
this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Deleted, uri });
121+
}),
122+
);
123+
124+
const disposable = {
125+
dispose: () => {
126+
disposables.forEach((d) => d.dispose());
127+
this._disposeMap.delete(uri.fsPath);
128+
},
129+
};
130+
this._disposeMap.set(uri.fsPath, disposable);
131+
}
132+
133+
unwatchPath(uri: Uri): void {
134+
const disposable = this._disposeMap.get(uri.fsPath);
135+
disposable?.dispose();
136+
}
137+
138+
dispose() {
139+
this.disposables.forEach((d) => d.dispose());
140+
this._disposeMap.forEach((d) => d.dispose());
141+
}
142+
}
143+
144+
export function createPythonWatcher(): PythonWatcher {
145+
return new PythonWatcherImpl();
146+
}

src/client/pythonEnvironments/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ export async function initialize(ext: ExtensionState): Promise<IDiscoveryAPI> {
6060

6161
if (shouldUseNativeLocator()) {
6262
const finder = getNativePythonFinder();
63+
ext.disposables.push(finder);
6364
const api = createNativeEnvironmentsApi(finder);
65+
ext.disposables.push(api);
6466
registerNewDiscoveryForIOC(
6567
// These are what get wrapped in the legacy adapter.
6668
ext.legacyIOC.serviceManager,

0 commit comments

Comments
 (0)