Skip to content

Commit 9892393

Browse files
authored
Spawn server for each workspace & fix error on failed settings initialization (#35)
1 parent aa7e980 commit 9892393

File tree

6 files changed

+228
-78
lines changed

6 files changed

+228
-78
lines changed

.vscode/settings.json

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
{
2-
"[json]": {
3-
"editor.defaultFormatter": "biomejs.biome"
4-
},
5-
"[javascript]": {
6-
"editor.defaultFormatter": "biomejs.biome"
7-
},
82
"[typescript]": {
9-
"editor.defaultFormatter": "biomejs.biome"
3+
"editor.defaultFormatter": "biomejs.biome",
4+
"editor.codeActionsOnSave": {
5+
"source.organizeImports": "never"
6+
}
107
},
118
"auto-typing-final.import-style": "final"
129
}

.vscode/tasks.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44
{
55
"type": "npm",
66
"script": "watch",
7-
"problemMatcher": [
8-
"$ts-webpack-watch",
9-
"$tslint-webpack-watch"
10-
],
7+
"problemMatcher": ["$ts-webpack-watch", "$tslint-webpack-watch"],
118
"isBackground": true,
129
"presentation": {
1310
"reveal": "never"

auto_typing_final/lsp.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,24 +75,25 @@ def make_text_edit(edit: Edit) -> lsp.TextEdit:
7575
)
7676

7777

78-
@dataclass(init=False)
78+
@dataclass
7979
class Service:
8080
ls_name: str
8181
ignored_paths: list[Path]
8282
import_config: ImportConfig
8383

84-
def __init__(self, ls_name: str, settings: Any) -> None: # noqa: ANN401
85-
self.ls_name = ls_name
86-
87-
executable_path: Final = Path(sys.executable)
88-
if executable_path.parent.name == "bin":
89-
self.ignored_paths = [executable_path.parent.parent]
90-
84+
@staticmethod
85+
def try_from_settings(ls_name: str, settings: Any) -> "Service | None": # noqa: ANN401
9186
try:
9287
validated_settings: Final = cattrs.structure(settings, FullClientSettings)
9388
except cattrs.BaseValidationError:
94-
return
95-
self.import_config = IMPORT_STYLES_TO_IMPORT_CONFIGS[validated_settings["auto-typing-final"]["import-style"]]
89+
return None
90+
91+
executable_path: Final = Path(sys.executable)
92+
return Service(
93+
ls_name=ls_name,
94+
ignored_paths=[executable_path.parent.parent] if executable_path.parent.name == "bin" else [],
95+
import_config=IMPORT_STYLES_TO_IMPORT_CONFIGS[validated_settings["auto-typing-final"]["import-style"]],
96+
)
9697

9798
def make_diagnostics(self, source: str) -> list[lsp.Diagnostic]:
9899
replacement_result: Final = make_replacements(
@@ -173,7 +174,9 @@ async def initialized(ls: CustomLanguageServer, _: lsp.InitializedParams) -> Non
173174

174175
@LSP_SERVER.feature(lsp.WORKSPACE_DID_CHANGE_CONFIGURATION)
175176
def workspace_did_change_configuration(ls: CustomLanguageServer, params: lsp.DidChangeConfigurationParams) -> None:
176-
ls.service = Service(ls_name=ls.name, settings=params.settings)
177+
ls.service = Service.try_from_settings(ls_name=ls.name, settings=params.settings) or ls.service
178+
if not ls.service:
179+
return
177180
for text_document in ls.workspace.text_documents.values():
178181
ls.publish_diagnostics(text_document.uri, diagnostics=ls.service.make_diagnostics(text_document.source))
179182

extension.ts

Lines changed: 196 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { PythonExtension } from "@vscode/python-extension";
12
import * as fs from "node:fs";
23
import * as path from "node:path";
34
import * as vscode from "vscode";
@@ -7,46 +8,60 @@ import {
78
RevealOutputChannelOn,
89
} from "vscode-languageclient/node";
910

10-
const NAME = "auto-typing-final";
11-
const PYTHON_EXTENSION_ID = "ms-python.python";
11+
const EXTENSION_NAME = "auto-typing-final";
1212
const LSP_SERVER_EXECUTABLE_NAME = "auto-typing-final-lsp-server";
13-
14-
let languageClient: LanguageClient | undefined;
1513
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+
}
1620

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;
2936
}
3037

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`);
3543
return;
3644
}
45+
}
46+
47+
async function findServerExecutable(workspaceFolder: vscode.WorkspaceFolder) {
48+
const pythonExtension = await getPythonExtension();
49+
if (!pythonExtension) return;
3750

3851
const environmentPath =
39-
extension.exports.environments.getActiveEnvironmentPath();
52+
pythonExtension.environments.getActiveEnvironmentPath(workspaceFolder);
4053
if (!environmentPath) {
41-
outputChannel?.info(`no active environment`);
54+
outputChannel?.info(`no active environment for ${workspaceFolder.uri}`);
4255
return;
4356
}
4457

4558
const fsPath = (
46-
await extension.exports.environments.resolveEnvironment(environmentPath)
59+
await pythonExtension.environments.resolveEnvironment(environmentPath)
4760
)?.executable.uri?.fsPath;
4861
if (!fsPath) {
49-
outputChannel?.info(`failed to resolve environment at ${environmentPath}`);
62+
outputChannel?.info(
63+
`failed to resolve environment for ${workspaceFolder.uri}`,
64+
);
5065
return;
5166
}
5267

@@ -58,66 +73,192 @@ async function findExecutable() {
5873
});
5974
if (!fs.existsSync(lspServerPath)) {
6075
outputChannel?.info(
61-
`failed to find ${LSP_SERVER_EXECUTABLE_NAME} for ${fsPath}`,
76+
`failed to find ${LSP_SERVER_EXECUTABLE_NAME} for ${workspaceFolder.uri}`,
6277
);
6378
return;
6479
}
6580

66-
outputChannel?.info(`using executable at ${lspServerPath}`);
81+
outputChannel?.info(
82+
`using executable at ${lspServerPath} for ${workspaceFolder.uri}`,
83+
);
6784
return lspServerPath;
6885
}
6986

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+
) {
7991
const serverOptions = {
8092
command: executable,
8193
args: [],
8294
options: { env: process.env },
8395
};
8496
const clientOptions: LanguageClientOptions = {
8597
documentSelector: [
86-
{ scheme: "file", language: "python" },
87-
{ scheme: "untitled", language: "python" },
98+
{
99+
scheme: "file",
100+
language: "python",
101+
pattern: `${workspaceFolder.uri.fsPath}/**/*`,
102+
},
88103
],
89104
outputChannel: outputChannel,
90105
traceOutputChannel: outputChannel,
91106
revealOutputChannelOn: RevealOutputChannelOn.Never,
107+
workspaceFolder: workspaceFolder,
92108
};
93-
languageClient = new LanguageClient(NAME, serverOptions, clientOptions);
109+
const languageClient = new LanguageClient(
110+
EXTENSION_NAME,
111+
serverOptions,
112+
clientOptions,
113+
);
94114
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+
};
96209
}
97210

98211
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();
100216

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+
}
103232

104233
context.subscriptions.push(
105234
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);
110239
},
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+
);
115253
}),
254+
vscode.workspace.onDidChangeWorkspaceFolders(async ({ removed }) => {
255+
SORTED_WORKSPACE_FOLDERS = getSortedWorkspaceFolders();
256+
await clientManager.stopClientsForWorkspaces(removed);
257+
}),
258+
{ dispose: clientManager.stopAllClients },
116259
);
117260

118-
await restartServer();
119-
}
120-
121-
export async function deactivate() {
122-
await languageClient?.stop();
261+
await clientManager.requireClientsForWorkspaces(
262+
takePythonFiles(vscode.workspace.textDocuments),
263+
);
123264
}

0 commit comments

Comments
 (0)