Skip to content

Commit 384606d

Browse files
committed
feat: support multi-root workspaces in single-server mode
- Fix bug where multiServer setting was read incorrectly - Fix bug where checkClient caused deadlocks on running servers - Enable multi-root support when r.lsp.multiServer is disabled - Set r.lsp.multiServer default to false - Refactor LanguageService to simplify client management - Update settings description to recommend languageserver >= 0.3.18
1 parent 79b70d9 commit 384606d

2 files changed

Lines changed: 54 additions & 55 deletions

File tree

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,8 +1389,8 @@
13891389
},
13901390
"r.lsp.multiServer": {
13911391
"type": "boolean",
1392-
"default": true,
1393-
"markdownDescription": "Use multiple language servers for [multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces). If disabled, only one language server will be used to handle all requests from all workspaces and files."
1392+
"default": false,
1393+
"markdownDescription": "Use multiple language servers for [multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces). If disabled, only one language server will be used to handle all requests from all workspaces and files. Disabling this is recommended if you have the R `languageserver` package version >= 0.3.18 installed."
13941394
},
13951395
"r.rmarkdown.codeLensCommands": {
13961396
"type": "array",

‎src/languageService.ts‎

Lines changed: 52 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,15 @@ import { extensionContext } from './extension';
99
import { CommonOptions } from 'child_process';
1010

1111
export class LanguageService implements Disposable {
12-
private client: LanguageClient | undefined;
1312
private readonly clients: Map<string, LanguageClient> = new Map();
1413
private readonly initSet: Set<string> = new Set();
1514
private readonly config: WorkspaceConfiguration;
1615
private readonly outputChannel: OutputChannel;
1716

1817
constructor() {
1918
this.outputChannel = window.createOutputChannel('R Language Server');
20-
this.client = undefined;
2119
this.config = workspace.getConfiguration('r');
22-
void this.startLanguageService(this);
20+
void this.startLanguageService();
2321
}
2422

2523
dispose(): Thenable<void> {
@@ -53,24 +51,24 @@ export class LanguageService implements Disposable {
5351
return childProcess;
5452
}
5553

56-
private async createClient(config: WorkspaceConfiguration, selector: DocumentFilter[],
54+
private async createClient(selector: DocumentFilter[],
5755
cwd: string, workspaceFolder: WorkspaceFolder | undefined, outputChannel: OutputChannel): Promise<LanguageClient> {
5856

5957
let client: LanguageClient;
6058

61-
const debug = config.get<boolean>('lsp.debug');
62-
const useRenvLibPath = config.get<boolean>('useRenvLibPath') ?? false;
59+
const debug = this.config.get<boolean>('lsp.debug');
60+
const useRenvLibPath = this.config.get<boolean>('useRenvLibPath') ?? false;
6361
const rPath = await getRpath() || ''; // TODO: Abort gracefully
6462
if (debug) {
6563
console.log(`R path: ${rPath}`);
6664
}
67-
const use_stdio = config.get<boolean>('lsp.use_stdio');
65+
const use_stdio = this.config.get<boolean>('lsp.use_stdio');
6866
const env = Object.create(process.env) as NodeJS.ProcessEnv;
6967
env.VSCR_LSP_DEBUG = debug ? 'TRUE' : 'FALSE';
7068
env.VSCR_LIB_PATHS = getRLibPaths();
7169
env.VSCR_USE_RENV_LIB_PATH = useRenvLibPath ? 'TRUE' : 'FALSE';
7270

73-
const lang = config.get<string>('lsp.lang');
71+
const lang = this.config.get<string>('lsp.lang');
7472
if (lang !== '') {
7573
env.LANG = lang;
7674
} else if (env.LANG === undefined) {
@@ -84,7 +82,7 @@ export class LanguageService implements Disposable {
8482

8583
const rScriptPath = extensionContext.asAbsolutePath('R/languageServer.R');
8684
const options = { cwd: cwd, env: env };
87-
const args = (config.get<string[]>('lsp.args')?.map(substituteVariables) ?? []).concat(
85+
const args = (this.config.get<string[]>('lsp.args')?.map(substituteVariables) ?? []).concat(
8886
'--silent',
8987
'--no-echo',
9088
'--no-save',
@@ -167,9 +165,12 @@ export class LanguageService implements Disposable {
167165
if (this.initSet.has(name)) {
168166
return true;
169167
}
170-
this.initSet.add(name);
171168
const client = this.clients.get(name);
172-
return (!!client) && client.needsStop();
169+
if (client && client.needsStop()) {
170+
return true;
171+
}
172+
this.initSet.add(name);
173+
return false;
173174
}
174175

175176
private getKey(uri: Uri): string {
@@ -183,8 +184,8 @@ export class LanguageService implements Disposable {
183184
}
184185
}
185186

186-
private startMultiLanguageService(self: LanguageService): void {
187-
async function didOpenTextDocument(document: TextDocument) {
187+
private startMultiLanguageService(): void {
188+
const didOpenTextDocument = async (document: TextDocument) => {
188189
if (document.uri.scheme !== 'file' && document.uri.scheme !== 'untitled' && document.uri.scheme !== 'vscode-notebook-cell') {
189190
return;
190191
}
@@ -197,73 +198,73 @@ export class LanguageService implements Disposable {
197198

198199
// Each notebook uses a server started from parent folder
199200
if (document.uri.scheme === 'vscode-notebook-cell') {
200-
const key = self.getKey(document.uri);
201-
if (!self.checkClient(key)) {
201+
const key = this.getKey(document.uri);
202+
if (!this.checkClient(key)) {
202203
console.log(`Start language server for ${document.uri.toString(true)}`);
203204
const documentSelector: DocumentFilter[] = [
204205
{ scheme: 'vscode-notebook-cell', language: 'r', pattern: `${document.uri.fsPath}` },
205206
];
206-
const client = await self.createClient(self.config, documentSelector,
207-
dirname(document.uri.fsPath), folder, self.outputChannel);
208-
self.clients.set(key, client);
209-
self.initSet.delete(key);
207+
const client = await this.createClient(documentSelector,
208+
dirname(document.uri.fsPath), folder, this.outputChannel);
209+
this.clients.set(key, client);
210+
this.initSet.delete(key);
210211
}
211212
return;
212213
}
213214

214215
if (folder) {
215216

216217
// Each workspace uses a server started from the workspace folder
217-
const key = self.getKey(folder.uri);
218-
if (!self.checkClient(key)) {
218+
const key = this.getKey(folder.uri);
219+
if (!this.checkClient(key)) {
219220
console.log(`Start language server for ${document.uri.toString(true)}`);
220221
const pattern = `${folder.uri.fsPath}/**/*`;
221222
const documentSelector: DocumentFilter[] = [
222223
{ scheme: 'file', language: 'r', pattern: pattern },
223224
{ scheme: 'file', language: 'rmd', pattern: pattern },
224225
];
225-
const client = await self.createClient(self.config, documentSelector, folder.uri.fsPath, folder, self.outputChannel);
226-
self.clients.set(key, client);
227-
self.initSet.delete(key);
226+
const client = await this.createClient(documentSelector, folder.uri.fsPath, folder, this.outputChannel);
227+
this.clients.set(key, client);
228+
this.initSet.delete(key);
228229
}
229230

230231
} else {
231232

232233
// All untitled documents share a server started from home folder
233234
if (document.uri.scheme === 'untitled') {
234-
const key = self.getKey(document.uri);
235-
if (!self.checkClient(key)) {
235+
const key = this.getKey(document.uri);
236+
if (!this.checkClient(key)) {
236237
console.log(`Start language server for ${document.uri.toString(true)}`);
237238
const documentSelector: DocumentFilter[] = [
238239
{ scheme: 'untitled', language: 'r' },
239240
{ scheme: 'untitled', language: 'rmd' },
240241
];
241-
const client = await self.createClient(self.config, documentSelector, os.homedir(), undefined, self.outputChannel);
242-
self.clients.set(key, client);
243-
self.initSet.delete(key);
242+
const client = await this.createClient(documentSelector, os.homedir(), undefined, this.outputChannel);
243+
this.clients.set(key, client);
244+
this.initSet.delete(key);
244245
}
245246
return;
246247
}
247248

248249
// Each file outside workspace uses a server started from parent folder
249250
if (document.uri.scheme === 'file') {
250-
const key = self.getKey(document.uri);
251-
if (!self.checkClient(key)) {
251+
const key = this.getKey(document.uri);
252+
if (!this.checkClient(key)) {
252253
console.log(`Start language server for ${document.uri.toString(true)}`);
253254
const documentSelector: DocumentFilter[] = [
254255
{ scheme: 'file', pattern: document.uri.fsPath },
255256
];
256-
const client = await self.createClient(self.config, documentSelector,
257-
dirname(document.uri.fsPath), undefined, self.outputChannel);
258-
self.clients.set(key, client);
259-
self.initSet.delete(key);
257+
const client = await this.createClient(documentSelector,
258+
dirname(document.uri.fsPath), undefined, this.outputChannel);
259+
this.clients.set(key, client);
260+
this.initSet.delete(key);
260261
}
261262
return;
262263
}
263264
}
264-
}
265+
};
265266

266-
function didCloseTextDocument(document: TextDocument): void {
267+
const didCloseTextDocument = (document: TextDocument): void => {
267268
if (document.uri.scheme === 'untitled') {
268269
const result = workspace.textDocuments.find((doc) => doc.uri.scheme === 'untitled');
269270
if (result) {
@@ -282,34 +283,34 @@ export class LanguageService implements Disposable {
282283
}
283284

284285
// Stop the language server when single file outside workspace is closed, or the above cases.
285-
const key = self.getKey(document.uri);
286-
const client = self.clients.get(key);
286+
const key = this.getKey(document.uri);
287+
const client = this.clients.get(key);
287288
if (client) {
288-
self.clients.delete(key);
289-
self.initSet.delete(key);
289+
this.clients.delete(key);
290+
this.initSet.delete(key);
290291
void client.stop();
291292
}
292-
}
293+
};
293294

294295
workspace.onDidOpenTextDocument(didOpenTextDocument);
295296
workspace.onDidCloseTextDocument(didCloseTextDocument);
296297
workspace.textDocuments.forEach((doc) => void didOpenTextDocument(doc));
297298
workspace.onDidChangeWorkspaceFolders((event) => {
298299
for (const folder of event.removed) {
299-
const key = self.getKey(folder.uri);
300-
const client = self.clients.get(key);
300+
const key = this.getKey(folder.uri);
301+
const client = this.clients.get(key);
301302
if (client) {
302-
self.clients.delete(key);
303-
self.initSet.delete(key);
303+
this.clients.delete(key);
304+
this.initSet.delete(key);
304305
void client.stop();
305306
}
306307
}
307308
});
308309
}
309310

310-
private async startLanguageService(self: LanguageService): Promise<void> {
311-
if (self.config.get<boolean>('r.lsp.multiServer')) {
312-
return this.startMultiLanguageService(self);
311+
private async startLanguageService(): Promise<void> {
312+
if (this.config.get<boolean>('lsp.multiServer')) {
313+
this.startMultiLanguageService();
313314
} else {
314315
const documentSelector: DocumentFilter[] = [
315316
{ language: 'r' },
@@ -318,15 +319,13 @@ export class LanguageService implements Disposable {
318319

319320
const workspaceFolder = workspace.workspaceFolders?.[0];
320321
const cwd = workspaceFolder ? workspaceFolder.uri.fsPath : os.homedir();
321-
self.client = await self.createClient(self.config, documentSelector, cwd, workspaceFolder, self.outputChannel);
322+
const client = await this.createClient(documentSelector, cwd, undefined, this.outputChannel);
323+
this.clients.set('global', client);
322324
}
323325
}
324326

325327
private stopLanguageService(): Thenable<void> {
326328
const promises: Thenable<void>[] = [];
327-
if (this.client) {
328-
promises.push(this.client.stop());
329-
}
330329
for (const client of this.clients.values()) {
331330
promises.push(client.stop());
332331
}

0 commit comments

Comments
 (0)