1
+ import { PythonExtension } from "@vscode/python-extension" ;
1
2
import * as fs from "node:fs" ;
2
3
import * as path from "node:path" ;
3
4
import * as vscode from "vscode" ;
@@ -7,46 +8,60 @@ import {
7
8
RevealOutputChannelOn ,
8
9
} from "vscode-languageclient/node" ;
9
10
10
- const NAME = "auto-typing-final" ;
11
- const PYTHON_EXTENSION_ID = "ms-python.python" ;
11
+ const EXTENSION_NAME = "auto-typing-final" ;
12
12
const LSP_SERVER_EXECUTABLE_NAME = "auto-typing-final-lsp-server" ;
13
-
14
- let languageClient : LanguageClient | undefined ;
15
13
let outputChannel : vscode . LogOutputChannel | undefined ;
14
+ let SORTED_WORKSPACE_FOLDERS = getSortedWorkspaceFolders ( ) ;
15
+
16
+ function normalizeFolderUri ( workspaceFolder : vscode . WorkspaceFolder ) {
17
+ const uri = workspaceFolder . uri . toString ( ) ;
18
+ return uri . charAt ( uri . length - 1 ) === "/" ? uri : uri + "/" ;
19
+ }
16
20
17
- function getPythonExtension ( ) {
18
- return vscode . extensions . getExtension ( PYTHON_EXTENSION_ID ) as
19
- | vscode . Extension < {
20
- environments : {
21
- onDidChangeActiveEnvironmentPath : ( event : any ) => any ;
22
- getActiveEnvironmentPath : ( ) => { path : string } ;
23
- resolveEnvironment : ( environment : { path : string } ) => Promise <
24
- { executable : { uri ?: { fsPath : string } } } | undefined
25
- > ;
26
- } ;
27
- } >
28
- | undefined ;
21
+ function getSortedWorkspaceFolders ( ) {
22
+ return vscode . workspace . workspaceFolders
23
+ ?. map < [ string , vscode . WorkspaceFolder ] > ( ( folder ) => [
24
+ normalizeFolderUri ( folder ) ,
25
+ folder ,
26
+ ] )
27
+ . sort ( ( first , second ) => first [ 0 ] . length - second [ 0 ] . length ) ;
28
+ }
29
+ function getOuterMostWorkspaceFolder ( folder : vscode . WorkspaceFolder ) {
30
+ const folderUri = normalizeFolderUri ( folder ) ;
31
+ for ( const [ sortedFolderUri , sortedFolder ] of SORTED_WORKSPACE_FOLDERS ??
32
+ [ ] ) {
33
+ if ( folderUri . startsWith ( sortedFolderUri ) ) return sortedFolder ;
34
+ }
35
+ return folder ;
29
36
}
30
37
31
- async function findExecutable ( ) {
32
- const extension = getPythonExtension ( ) ;
33
- if ( ! extension ) {
34
- outputChannel ?. info ( `${ PYTHON_EXTENSION_ID } not installed` ) ;
38
+ async function getPythonExtension ( ) {
39
+ try {
40
+ return await PythonExtension . api ( ) ;
41
+ } catch {
42
+ outputChannel ?. info ( `python extension not installed` ) ;
35
43
return ;
36
44
}
45
+ }
46
+
47
+ async function findServerExecutable ( workspaceFolder : vscode . WorkspaceFolder ) {
48
+ const pythonExtension = await getPythonExtension ( ) ;
49
+ if ( ! pythonExtension ) return ;
37
50
38
51
const environmentPath =
39
- extension . exports . environments . getActiveEnvironmentPath ( ) ;
52
+ pythonExtension . environments . getActiveEnvironmentPath ( workspaceFolder ) ;
40
53
if ( ! environmentPath ) {
41
- outputChannel ?. info ( `no active environment` ) ;
54
+ outputChannel ?. info ( `no active environment for ${ workspaceFolder . uri } ` ) ;
42
55
return ;
43
56
}
44
57
45
58
const fsPath = (
46
- await extension . exports . environments . resolveEnvironment ( environmentPath )
59
+ await pythonExtension . environments . resolveEnvironment ( environmentPath )
47
60
) ?. executable . uri ?. fsPath ;
48
61
if ( ! fsPath ) {
49
- outputChannel ?. info ( `failed to resolve environment at ${ environmentPath } ` ) ;
62
+ outputChannel ?. info (
63
+ `failed to resolve environment for ${ workspaceFolder . uri } ` ,
64
+ ) ;
50
65
return ;
51
66
}
52
67
@@ -58,66 +73,192 @@ async function findExecutable() {
58
73
} ) ;
59
74
if ( ! fs . existsSync ( lspServerPath ) ) {
60
75
outputChannel ?. info (
61
- `failed to find ${ LSP_SERVER_EXECUTABLE_NAME } for ${ fsPath } ` ,
76
+ `failed to find ${ LSP_SERVER_EXECUTABLE_NAME } for ${ workspaceFolder . uri } ` ,
62
77
) ;
63
78
return ;
64
79
}
65
80
66
- outputChannel ?. info ( `using executable at ${ lspServerPath } ` ) ;
81
+ outputChannel ?. info (
82
+ `using executable at ${ lspServerPath } for ${ workspaceFolder . uri } ` ,
83
+ ) ;
67
84
return lspServerPath ;
68
85
}
69
86
70
- async function restartServer ( ) {
71
- if ( languageClient ) {
72
- await languageClient . stop ( ) ;
73
- outputChannel ?. info ( "stopped server" ) ;
74
- }
75
-
76
- const executable = await findExecutable ( ) ;
77
- if ( ! executable ) return ;
78
-
87
+ async function createClient (
88
+ workspaceFolder : vscode . WorkspaceFolder ,
89
+ executable : string ,
90
+ ) {
79
91
const serverOptions = {
80
92
command : executable ,
81
93
args : [ ] ,
82
94
options : { env : process . env } ,
83
95
} ;
84
96
const clientOptions : LanguageClientOptions = {
85
97
documentSelector : [
86
- { scheme : "file" , language : "python" } ,
87
- { scheme : "untitled" , language : "python" } ,
98
+ {
99
+ scheme : "file" ,
100
+ language : "python" ,
101
+ pattern : `${ workspaceFolder . uri . fsPath } /**/*` ,
102
+ } ,
88
103
] ,
89
104
outputChannel : outputChannel ,
90
105
traceOutputChannel : outputChannel ,
91
106
revealOutputChannelOn : RevealOutputChannelOn . Never ,
107
+ workspaceFolder : workspaceFolder ,
92
108
} ;
93
- languageClient = new LanguageClient ( NAME , serverOptions , clientOptions ) ;
109
+ const languageClient = new LanguageClient (
110
+ EXTENSION_NAME ,
111
+ serverOptions ,
112
+ clientOptions ,
113
+ ) ;
94
114
await languageClient . start ( ) ;
95
- outputChannel ?. info ( "started server" ) ;
115
+ outputChannel ?. info ( `started server for ${ workspaceFolder . uri } ` ) ;
116
+ return languageClient ;
117
+ }
118
+
119
+ function createClientManager ( ) {
120
+ const allClients = new Map < string , [ string , LanguageClient ] > ( ) ;
121
+ const allExecutables = new Map < string , string | null > ( ) ;
122
+
123
+ async function _stopClient ( workspaceFolder : vscode . WorkspaceFolder ) {
124
+ const folderUri = workspaceFolder . uri . toString ( ) ;
125
+ const oldEntry = allClients . get ( folderUri ) ;
126
+ if ( oldEntry ) {
127
+ const [ _ , oldClient ] = oldEntry ;
128
+ await oldClient . stop ( ) ;
129
+ allClients . delete ( folderUri ) ;
130
+ allExecutables . delete ( folderUri ) ;
131
+ outputChannel ?. info ( `stopped server for ${ folderUri } ` ) ;
132
+ }
133
+ }
134
+
135
+ async function requireClientForWorkspace (
136
+ workspaceFolder : vscode . WorkspaceFolder ,
137
+ ) {
138
+ const outerMostFolder = getOuterMostWorkspaceFolder ( workspaceFolder ) ;
139
+ const outerMostFolderUri = outerMostFolder . uri . toString ( ) ;
140
+ if ( workspaceFolder . uri . toString ( ) != outerMostFolderUri ) {
141
+ await _stopClient ( workspaceFolder ) ;
142
+ }
143
+
144
+ const cachedExecutable = allExecutables . get ( outerMostFolderUri ) ;
145
+ let newExecutable ;
146
+ if ( cachedExecutable === undefined ) {
147
+ newExecutable = await findServerExecutable ( outerMostFolder ) ;
148
+ allExecutables . set ( outerMostFolderUri , newExecutable ?? null ) ;
149
+ } else {
150
+ newExecutable = cachedExecutable ;
151
+ }
152
+
153
+ const outerMostOldEntry = allClients . get ( outerMostFolderUri ) ;
154
+ if ( outerMostOldEntry ) {
155
+ const [ oldExecutable , _ ] = outerMostOldEntry ;
156
+ if ( oldExecutable == newExecutable ) {
157
+ return ;
158
+ }
159
+ await _stopClient ( outerMostFolder ) ;
160
+ }
161
+
162
+ if ( newExecutable ) {
163
+ allClients . set ( outerMostFolderUri , [
164
+ newExecutable ,
165
+ await createClient ( outerMostFolder , newExecutable ) ,
166
+ ] ) ;
167
+ }
168
+ }
169
+ return {
170
+ requireClientForWorkspace,
171
+ async requireClientsForWorkspaces (
172
+ workspaceFolders : readonly vscode . WorkspaceFolder [ ] ,
173
+ ) {
174
+ const outerMostFolders = [
175
+ ...new Set ( workspaceFolders . map ( getOuterMostWorkspaceFolder ) ) ,
176
+ ] ;
177
+ await Promise . all (
178
+ outerMostFolders . map ( ( folder ) => requireClientForWorkspace ( folder ) ) ,
179
+ ) ;
180
+ } ,
181
+ async updateExecutableForWorkspace (
182
+ workspaceFolder : vscode . WorkspaceFolder ,
183
+ ) {
184
+ allExecutables . delete (
185
+ getOuterMostWorkspaceFolder ( workspaceFolder ) . uri . toString ( ) ,
186
+ ) ;
187
+ await requireClientForWorkspace ( workspaceFolder ) ;
188
+ } ,
189
+ async stopClientsForWorkspaces (
190
+ workspaceFolders : readonly vscode . WorkspaceFolder [ ] ,
191
+ ) {
192
+ await Promise . all (
193
+ workspaceFolders . map ( async ( folder ) => {
194
+ const outerMostFolder = getOuterMostWorkspaceFolder ( folder ) ;
195
+ if ( outerMostFolder . uri . toString ( ) === folder . uri . toString ( ) ) {
196
+ await _stopClient ( folder ) ;
197
+ }
198
+ } ) ,
199
+ ) ;
200
+ } ,
201
+ async stopAllClients ( ) {
202
+ await Promise . all (
203
+ Array . from ( allClients . values ( ) ) . map ( ( [ _ , client ] ) => client . stop ( ) ) ,
204
+ ) ;
205
+ allClients . clear ( ) ;
206
+ allExecutables . clear ( ) ;
207
+ } ,
208
+ } ;
96
209
}
97
210
98
211
export async function activate ( context : vscode . ExtensionContext ) {
99
- outputChannel = vscode . window . createOutputChannel ( NAME , { log : true } ) ;
212
+ outputChannel = vscode . window . createOutputChannel ( EXTENSION_NAME , {
213
+ log : true ,
214
+ } ) ;
215
+ const clientManager = createClientManager ( ) ;
100
216
101
- const pythonExtension = getPythonExtension ( ) ;
102
- if ( ! pythonExtension ?. isActive ) await pythonExtension ?. activate ( ) ;
217
+ function takePythonFiles ( textDocuments : readonly vscode . TextDocument [ ] ) {
218
+ return textDocuments
219
+ . map ( ( document ) => {
220
+ if (
221
+ document . languageId === "python" &&
222
+ document . uri . scheme === "file"
223
+ ) {
224
+ const workspaceFolder = vscode . workspace . getWorkspaceFolder (
225
+ document . uri ,
226
+ ) ;
227
+ if ( workspaceFolder ) return workspaceFolder ;
228
+ }
229
+ } )
230
+ . filter ( ( value ) => value !== undefined ) ;
231
+ }
103
232
104
233
context . subscriptions . push (
105
234
outputChannel ,
106
- pythonExtension ?. exports . environments . onDidChangeActiveEnvironmentPath (
107
- async ( ) => {
108
- outputChannel ?. info ( "restarting on python environment changed" ) ;
109
- await restartServer ( ) ;
235
+ ( await getPythonExtension ( ) ) ? .environments . onDidChangeActiveEnvironmentPath (
236
+ async ( { resource } ) => {
237
+ if ( ! resource ) return ;
238
+ await clientManager . updateExecutableForWorkspace ( resource ) ;
110
239
} ,
111
- ) ,
112
- vscode . commands . registerCommand ( `${ NAME } .restart` , async ( ) => {
113
- outputChannel ?. info ( `restarting on ${ NAME } .restart` ) ;
114
- await restartServer ( ) ;
240
+ ) || { dispose : ( ) => undefined } ,
241
+ vscode . commands . registerCommand ( `${ EXTENSION_NAME } .restart` , async ( ) => {
242
+ if ( ! vscode . workspace . workspaceFolders ) return ;
243
+ outputChannel ?. info ( `restarting on ${ EXTENSION_NAME } .restart` ) ;
244
+ await clientManager . stopAllClients ( ) ;
245
+ await clientManager . requireClientsForWorkspaces (
246
+ takePythonFiles ( vscode . workspace . textDocuments ) ,
247
+ ) ;
248
+ } ) ,
249
+ vscode . workspace . onDidOpenTextDocument ( async ( document ) => {
250
+ await clientManager . requireClientsForWorkspaces (
251
+ takePythonFiles ( [ document ] ) ,
252
+ ) ;
115
253
} ) ,
254
+ vscode . workspace . onDidChangeWorkspaceFolders ( async ( { removed } ) => {
255
+ SORTED_WORKSPACE_FOLDERS = getSortedWorkspaceFolders ( ) ;
256
+ await clientManager . stopClientsForWorkspaces ( removed ) ;
257
+ } ) ,
258
+ { dispose : clientManager . stopAllClients } ,
116
259
) ;
117
260
118
- await restartServer ( ) ;
119
- }
120
-
121
- export async function deactivate ( ) {
122
- await languageClient ?. stop ( ) ;
261
+ await clientManager . requireClientsForWorkspaces (
262
+ takePythonFiles ( vscode . workspace . textDocuments ) ,
263
+ ) ;
123
264
}
0 commit comments