-
-
Notifications
You must be signed in to change notification settings - Fork 213
/
provider.ts
342 lines (298 loc) · 12.6 KB
/
provider.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
import * as vscode_lsp from 'vscode-languageclient/node';
import * as project_utils from '../project-root';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as vscode from 'vscode';
import * as os from 'node:os';
import * as status_bar from './status-bar';
import * as commands from './commands';
import * as lsp_client from './client';
import * as defs from './definitions';
import * as config from './config';
import * as state from '../state';
import * as utils from './utils';
import * as api from './api';
/**
* Can be called to shutdown the fallback lsp server if there are no longer any relevant workspaces or files
* open
*/
const shutdownFallbackClientIfNeeded = async (clients: defs.LspClientStore) => {
const roots = await project_utils.findProjectRootsWithReasons({
include_lsp_directories: true,
});
const non_project_folders = roots
.filter((root) => {
return !root.valid_project && root.workspace_root;
})
.map((root) => root.uri);
const contains_external_files = !!vscode.workspace.textDocuments.find((doc) => {
const clojure_file = doc.languageId === 'clojure' && doc.uri.scheme !== 'untitled';
const external = vscode.workspace.getWorkspaceFolder(doc.uri);
return clojure_file && external;
});
if (non_project_folders.length === 0 && !contains_external_files) {
console.log('Shutting down fallback lsp client');
void clients
.get(api.FALLBACK_CLIENT_ID)
?.client.stop()
.catch((err) => console.error('Failed to stop fallback client', err));
}
};
type CreateClientProviderParams = {
context: vscode.ExtensionContext;
testTreeHandler: defs.TestTreeHandler;
};
/**
* Creates an LSP client provider which is responsible for dynamically provisioning LSP clients for Clojure projects
* opened in the VSCode workspace(s).
*
* Whenever a Clojure document is opened this provider will search backwards (starting from the directory the
* document resides in) for the root most Clojure project in the workspace. If a valid Clojure project is found
* and no LSP client exists yet then one is provisioned. If no valid project is found then it falls back to using
* the workspace root.
*
* Any other systems that need to perform LSP operations can then request the LSP client governing a given
* document URI which will be returned if it exists.
*
* This module is also responsible for updating the VSCode status bar item with the state of the LSP client for the
* current active editor.
*/
export const createClientProvider = (params: CreateClientProviderParams) => {
const clients: defs.LspClientStore = new Map();
const status_bar_item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 0);
status_bar_item.command = 'calva.clojureLsp.manage';
const updateStatusBar = () => {
const any_starting = Array.from(clients.values()).find(
(client) => client.status === defs.LspStatus.Starting
);
if (any_starting) {
return status_bar.updateStatusBar(status_bar_item, defs.LspStatus.Starting);
}
const active_editor = vscode.window.activeTextEditor?.document;
if (!active_editor || active_editor.languageId !== 'clojure') {
// If there are multiple clients then we don't know which client to show the status for and we set it to unknown
if (clients.size !== 1) {
status_bar.updateStatusBar(status_bar_item, defs.LspStatus.Unknown);
return;
}
const client = Array.from(clients.values())[0];
status_bar.updateStatusBar(status_bar_item, client.status);
return;
}
const client = api.getActiveClientForUri(clients, active_editor.uri);
if (!client) {
status_bar.updateStatusBar(status_bar_item, defs.LspStatus.Stopped);
return;
}
status_bar.updateStatusBar(status_bar_item, client.status);
};
let lsp_server_path: Promise<string | void> | undefined = undefined;
const provisionClient = async (uri: vscode.Uri, id = uri.fsPath) => {
if (lsp_server_path === undefined) {
lsp_server_path = lsp_client.ensureLSPServer(params.context).catch((err) => {
console.error('Failed to download lsp server', err);
return;
});
}
const server_path = await lsp_server_path;
if (!server_path) {
console.error('Server path could not be resolved');
return;
}
const existing = clients.get(id);
if (existing && [defs.LspStatus.Starting, defs.LspStatus.Running].includes(existing.status)) {
return;
}
console.log(`Creating new LSP client using ${uri.path} as the project root`);
const client = lsp_client.createClient({
lsp_server_path: server_path,
id,
uri,
});
clients.set(id, client);
updateStatusBar();
client.client.onDidChangeState(async ({ newState }) => {
updateStatusBar();
if (newState === vscode_lsp.State.Running) {
const serverInfo = await api
.getServerInfo(client.client)
.catch((err) => console.error(err));
if (serverInfo) {
const calvaSaysChannel = state.outputChannel();
calvaSaysChannel.appendLine(`clojure-lsp version used: ${serverInfo['server-version']}`);
calvaSaysChannel.appendLine(`clj-kondo version used: ${serverInfo['clj-kondo-version']}`);
}
}
});
client.client.onNotification('clojure/textDocument/testTree', (tree: defs.TestTreeParams) => {
params.testTreeHandler(tree);
});
};
/**
* Provision a fallback lsp client in an OS temp directory to service any clojure files not part of any opened
* valid clojure projects.
*
* This logic has been disabled for now as it does not function correctly on Windows. This can be re-enabled
* once support for windows has been added.
*/
const provisionFallbackClient = async () => {
return;
const dir = path.join(os.tmpdir(), 'calva-clojure-lsp');
await fs.mkdir(dir, {
recursive: true,
});
return provisionClient(vscode.Uri.parse(dir), api.FALLBACK_CLIENT_ID);
};
const provisionClientForOpenedDocument = async (document: vscode.TextDocument) => {
// We exclude untitled files because clojure-lsp does not support them (yet?)
if (document.languageId !== 'clojure' || document.uri.scheme === 'untitled') {
return;
}
// Don't provision a client if the document is already governed (ignoring the fallback client)
const existing = api.getActiveClientForUri(clients, document.uri);
if (existing && existing.id !== api.FALLBACK_CLIENT_ID) {
return;
}
// If there is no appropriate project root in any of the workspaces then we provision the fallback client
const uri = await utils.findClojureProjectRootForUri(document.uri);
if (!uri) {
return provisionFallbackClient();
}
return provisionClient(uri);
};
const provisionClientInFirstWorkspaceRoot = async () => {
const folder = vscode.workspace.workspaceFolders[0];
if (!folder) {
return;
}
return provisionClient(folder.uri);
// TODO: Rather provision fallback client if not a valid clojure project:
// if (folder && (await project_utils.isValidClojureProject(folder.uri))) {
// return provisionClient(folder.uri);
// }
// return provisionFallbackClient();
};
return {
getClientForDocumentUri: (uri: vscode.Uri) => api.getClientForDocumentUri(clients, uri),
init: async () => {
status_bar_item.show();
if (vscode.window.activeTextEditor?.document.languageId === 'clojure') {
status_bar.updateStatusBar(status_bar_item, defs.LspStatus.Stopped);
} else {
status_bar.updateStatusBar(status_bar_item, defs.LspStatus.Unknown);
}
switch (config.getAutoStartBehaviour()) {
case config.AutoStartBehaviour.WorkspaceOpened: {
const roots = await project_utils.findProjectRootsWithReasons({
include_lsp_directories: true,
});
const valid_workspace_folders = roots
.filter((root) => {
return root.valid_project && root.workspace_root;
})
.map((root) => root.uri);
const invalid_workspace_folders = roots
.filter((root) => {
return !root.valid_project && root.workspace_root;
})
.map((root) => root.uri);
const distinct = project_utils.filterShortestDistinctPaths(valid_workspace_folders);
distinct.forEach((root) => {
void provisionClient(root).catch((err) => console.error(err));
});
// If the workspace contains any 'invalid' clojure projects (projects with no project files) then we
// provision the fallback lsp client
if (invalid_workspace_folders.length > 0) {
void provisionFallbackClient().catch((err) => console.error(err));
}
break;
}
case config.AutoStartBehaviour.FileOpened: {
vscode.workspace.textDocuments.forEach((document) => {
void provisionClientForOpenedDocument(document).catch((err) => console.error(err));
});
break;
}
case config.AutoStartBehaviour.FirstWorkspace: {
void provisionClientInFirstWorkspaceRoot().catch((err) => console.error(err));
break;
}
}
params.context.subscriptions.push(
/**
* We setup a listener for the active editor changing and use that to update the status bar with the
* status of the LSP server related to the currently active document.
*/
vscode.window.onDidChangeActiveTextEditor((event) => {
updateStatusBar();
}),
// Provision new LSP clients when clojure files are opened and for all already opened clojure files.
vscode.workspace.onDidOpenTextDocument((document) => {
if (config.getAutoStartBehaviour() === config.AutoStartBehaviour.FileOpened) {
void provisionClientForOpenedDocument(document).catch((err) => console.error(err));
}
}),
vscode.workspace.onDidCloseTextDocument((doc) => {
if (doc.languageId === 'clojure') {
void shutdownFallbackClientIfNeeded(clients);
}
}),
// Provision new LSP clients when workspaces folders are added, and shutdown clients when folders are removed
vscode.workspace.onDidChangeWorkspaceFolders((event) => {
event.removed.forEach((folder) => {
clients.forEach((client, dir) => {
const relative = path.relative(folder.uri.path, dir);
if ((relative && !relative.includes('../')) || dir === folder.uri.path) {
console.log(
`Shutting down the LSP client at ${dir} as it belongs to a removed workspace`
);
void client.client.stop().catch((err) => console.error(err));
}
});
});
switch (config.getAutoStartBehaviour()) {
case config.AutoStartBehaviour.WorkspaceOpened: {
return void Promise.allSettled(
event.added.map(async (folder) => {
// Don't provision an lsp client if this workspace folder is already governed by an existing folder
const existing = api.getActiveClientForUri(clients, folder.uri);
if (existing && existing.id !== api.FALLBACK_CLIENT_ID) {
return;
}
if (await project_utils.isValidClojureProject(folder.uri)) {
void provisionClient(folder.uri).catch((err) => console.error(err));
} else {
void provisionFallbackClient().catch((err) => console.error(err));
}
})
);
}
case config.AutoStartBehaviour.FirstWorkspace: {
return void provisionClientInFirstWorkspaceRoot().catch((err) => console.error(err));
}
}
void shutdownFallbackClientIfNeeded(clients);
})
);
params.context.subscriptions.push(
...commands.registerLspCommands(clients),
...commands.registerEventHandlers(),
...commands.registerVSCodeCommands({
clients,
context: params.context,
handleStartRequest: (uri) => {
return provisionClient(uri);
},
})
);
},
/**
* Remove the status bar item and shutdown all LSP clients
*/
shutdown: async () => {
status_bar_item.dispose();
await Promise.allSettled(Array.from(clients.values()).map((client) => client.client.stop()));
},
};
};
export type ClientProvider = ReturnType<typeof createClientProvider>;