Skip to content
This repository was archived by the owner on Nov 18, 2022. It is now read-only.

Commit ffe36f3

Browse files
authored
Merge pull request #638 from jannickj/allow-multiple-rls-projects
reintroduce multi project setup
2 parents 6820cbf + 47c66be commit ffe36f3

File tree

4 files changed

+133
-64
lines changed

4 files changed

+133
-64
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,11 @@
219219
"default": true,
220220
"description": "If one root workspace folder is nested in another root folder, look for the Rust config in the outermost root."
221221
},
222+
"rust-client.enableMultiProjectSetup": {
223+
"type": "boolean",
224+
"default": false,
225+
"description": "Allow multiple projects in the same folder, along with remove the constraint that the cargo.toml must be located at the root. (Experimental: might not work for certain setups)"
226+
},
222227
"rust.sysroot": {
223228
"type": [
224229
"string",

src/configuration.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ export class RLSConfiguration {
118118
return this.configuration.get<string>('rust-client.rlsPath');
119119
}
120120

121+
public get multiProjectEnabled(): boolean {
122+
return this.configuration.get<boolean>(
123+
'rust-client.enableMultiProjectSetup',
124+
false,
125+
);
126+
}
127+
121128
// Added ignoreChannel for readChannel function. Otherwise we end in an infinite loop.
122129
public rustupConfig(ignoreChannel: boolean = false): RustupConfig {
123130
return {

src/extension.ts

Lines changed: 79 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { startSpinner, stopSpinner } from './spinner';
2828
import { activateTaskProvider, Execution, runRlsCommand } from './tasks';
2929
import { withWsl } from './utils/child_process';
3030
import { uriWindowsToWsl, uriWslToWindows } from './utils/wslpath';
31+
import * as workspace_util from './workspace_util';
3132

3233
/**
3334
* Parameter type to `window/progress` request as issued by the RLS.
@@ -44,10 +45,10 @@ interface ProgressParams {
4445
export async function activate(context: ExtensionContext) {
4546
context.subscriptions.push(configureLanguage());
4647

47-
workspace.onDidOpenTextDocument(doc => didOpenTextDocument(doc, context));
48-
workspace.textDocuments.forEach(doc => didOpenTextDocument(doc, context));
48+
workspace.onDidOpenTextDocument(doc => whenOpeningTextDocument(doc, context));
49+
workspace.textDocuments.forEach(doc => whenOpeningTextDocument(doc, context));
4950
workspace.onDidChangeWorkspaceFolders(e =>
50-
didChangeWorkspaceFolders(e, context),
51+
whenChangingWorkspaceFolders(e, context),
5152
);
5253
}
5354

@@ -56,7 +57,7 @@ export async function deactivate() {
5657
}
5758

5859
// Taken from https://github.com/Microsoft/vscode-extension-samples/blob/master/lsp-multi-server-sample/client/src/extension.ts
59-
function didOpenTextDocument(
60+
function whenOpeningTextDocument(
6061
document: TextDocument,
6162
context: ExtensionContext,
6263
) {
@@ -69,23 +70,36 @@ function didOpenTextDocument(
6970
if (!folder) {
7071
return;
7172
}
72-
if (
73-
workspace
74-
.getConfiguration()
75-
.get<boolean>('rust-client.nestedMultiRootConfigInOutermost', true)
76-
) {
73+
74+
const inMultiProjectMode = workspace
75+
.getConfiguration()
76+
.get<boolean>('rust-client.enableMultiProjectSetup', false);
77+
78+
const inNestedOuterProjectMode = workspace
79+
.getConfiguration()
80+
.get<boolean>('rust-client.nestedMultiRootConfigInOutermost', true);
81+
82+
if (inMultiProjectMode) {
83+
folder = workspace_util.nearestParentWorkspace(folder, document.uri.fsPath);
84+
} else if (inNestedOuterProjectMode) {
7785
folder = getOuterMostWorkspaceFolder(folder);
7886
}
79-
// folder = getCargoTomlWorkspace(folder, document.uri.fsPath);
87+
8088
if (!folder) {
8189
stopSpinner(`RLS: Cargo.toml missing`);
8290
return;
8391
}
8492

85-
if (!workspaces.has(folder.uri)) {
93+
const folderPath = folder.uri.toString();
94+
95+
if (!workspaces.has(folderPath)) {
8696
const workspace = new ClientWorkspace(folder);
87-
workspaces.set(folder.uri, workspace);
97+
activeWorkspace = workspace;
98+
workspaces.set(folderPath, workspace);
8899
workspace.start(context);
100+
} else {
101+
const ws = workspaces.get(folderPath);
102+
activeWorkspace = typeof ws === 'undefined' ? null : ws;
89103
}
90104
}
91105

@@ -94,6 +108,7 @@ function didOpenTextDocument(
94108
let _sortedWorkspaceFolders: string[] | undefined;
95109

96110
function sortedWorkspaceFolders(): string[] {
111+
// TODO: decouple the global state such that it can be moved to workspace_util
97112
if (!_sortedWorkspaceFolders && workspace.workspaceFolders) {
98113
_sortedWorkspaceFolders = workspace.workspaceFolders
99114
.map(folder => {
@@ -110,39 +125,8 @@ function sortedWorkspaceFolders(): string[] {
110125
return _sortedWorkspaceFolders || [];
111126
}
112127

113-
// function getCargoTomlWorkspace(cur_workspace: WorkspaceFolder, file_path: string): WorkspaceFolder {
114-
// if (!cur_workspace) {
115-
// return cur_workspace;
116-
// }
117-
118-
// const workspace_root = path.parse(cur_workspace.uri.fsPath).dir;
119-
// const root_manifest = path.join(workspace_root, 'Cargo.toml');
120-
// if (fs.existsSync(root_manifest)) {
121-
// return cur_workspace;
122-
// }
123-
124-
// let current = file_path;
125-
126-
// while (true) {
127-
// const old = current;
128-
// current = path.dirname(current);
129-
// if (old == current) {
130-
// break;
131-
// }
132-
// if (workspace_root == path.parse(current).dir) {
133-
// break;
134-
// }
135-
136-
// const cargo_path = path.join(current, 'Cargo.toml');
137-
// if (fs.existsSync(cargo_path)) {
138-
// return { ...cur_workspace, uri: Uri.parse(current) };
139-
// }
140-
// }
141-
142-
// return cur_workspace;
143-
// }
144-
145128
function getOuterMostWorkspaceFolder(folder: WorkspaceFolder): WorkspaceFolder {
129+
// TODO: decouple the global state such that it can be moved to workspace_util
146130
const sorted = sortedWorkspaceFolders();
147131
for (const element of sorted) {
148132
let uri = folder.uri.toString();
@@ -156,7 +140,7 @@ function getOuterMostWorkspaceFolder(folder: WorkspaceFolder): WorkspaceFolder {
156140
return folder;
157141
}
158142

159-
function didChangeWorkspaceFolders(
143+
function whenChangingWorkspaceFolders(
160144
e: WorkspaceFoldersChangeEvent,
161145
context: ExtensionContext,
162146
) {
@@ -166,13 +150,13 @@ function didChangeWorkspaceFolders(
166150
// if not, and it is a Rust project (i.e., has a Cargo.toml), then create a new client.
167151
for (let folder of e.added) {
168152
folder = getOuterMostWorkspaceFolder(folder);
169-
if (workspaces.has(folder.uri)) {
153+
if (workspaces.has(folder.uri.toString())) {
170154
continue;
171155
}
172156
for (const f of fs.readdirSync(folder.uri.fsPath)) {
173157
if (f === 'Cargo.toml') {
174158
const workspace = new ClientWorkspace(folder);
175-
workspaces.set(folder.uri, workspace);
159+
workspaces.set(folder.uri.toString(), workspace);
176160
workspace.start(context);
177161
break;
178162
}
@@ -181,15 +165,18 @@ function didChangeWorkspaceFolders(
181165

182166
// If a workspace is removed which is a Rust workspace, kill the client.
183167
for (const folder of e.removed) {
184-
const ws = workspaces.get(folder.uri);
168+
const ws = workspaces.get(folder.uri.toString());
185169
if (ws) {
186-
workspaces.delete(folder.uri);
170+
workspaces.delete(folder.uri.toString());
187171
ws.stop();
188172
}
189173
}
190174
}
191175

192-
const workspaces: Map<Uri, ClientWorkspace> = new Map();
176+
// Don't use URI as it's unreliable the same path might not become the same URI.
177+
const workspaces: Map<string, ClientWorkspace> = new Map();
178+
let activeWorkspace: ClientWorkspace | null;
179+
let commandsRegistered: boolean = false;
193180

194181
// We run one RLS and one corresponding language client per workspace folder
195182
// (VSCode workspace, not Cargo workspace). This class contains all the per-client
@@ -209,21 +196,31 @@ class ClientWorkspace {
209196
}
210197

211198
public async start(context: ExtensionContext) {
212-
warnOnMissingCargoToml();
199+
if (!this.config.multiProjectEnabled) {
200+
warnOnMissingCargoToml();
201+
}
213202

214203
startSpinner('RLS', 'Starting');
215204

216205
const serverOptions: ServerOptions = async () => {
217206
await this.autoUpdate();
218207
return this.makeRlsProcess();
219208
};
209+
210+
const pattern = this.config.multiProjectEnabled
211+
? `${this.folder.uri.path}/**`
212+
: undefined;
213+
const collectionName = this.config.multiProjectEnabled
214+
? `rust ${this.folder.uri.toString()}`
215+
: 'rust';
220216
const clientOptions: LanguageClientOptions = {
221217
// Register the server for Rust files
218+
222219
documentSelector: [
223-
{ language: 'rust', scheme: 'file' },
224-
{ language: 'rust', scheme: 'untitled' },
220+
{ language: 'rust', scheme: 'file', pattern },
221+
{ language: 'rust', scheme: 'untitled', pattern },
225222
],
226-
diagnosticCollectionName: 'rust',
223+
diagnosticCollectionName: collectionName,
227224
synchronize: { configurationSection: 'rust' },
228225
// Controls when to focus the channel rather than when to reveal it in the drop-down list
229226
revealOutputChannelOn: this.config.revealOutputChannelOn,
@@ -259,13 +256,17 @@ class ClientWorkspace {
259256
clientOptions,
260257
);
261258

259+
const selector = this.config.multiProjectEnabled
260+
? { language: 'rust', scheme: 'file', pattern }
261+
: { language: 'rust' };
262+
262263
this.setupProgressCounter();
263-
this.registerCommands(context);
264+
this.registerCommands(context, this.config.multiProjectEnabled);
264265
this.disposables.push(activateTaskProvider(this.folder));
265266
this.disposables.push(this.lc.start());
266267
this.disposables.push(
267268
languages.registerSignatureHelpProvider(
268-
{ language: 'rust' },
269+
selector,
269270
new SignatureHelpProvider(this.lc),
270271
'(',
271272
',',
@@ -279,31 +280,45 @@ class ClientWorkspace {
279280
}
280281

281282
this.disposables.forEach(d => d.dispose());
283+
commandsRegistered = false;
282284
}
283285

284-
private registerCommands(context: ExtensionContext) {
286+
private registerCommands(
287+
context: ExtensionContext,
288+
multiProjectEnabled: boolean,
289+
) {
285290
if (!this.lc) {
286291
return;
287292
}
293+
if (multiProjectEnabled && commandsRegistered) {
294+
return;
295+
}
288296

297+
commandsRegistered = true;
289298
const rustupUpdateDisposable = commands.registerCommand(
290299
'rls.update',
291300
() => {
292-
return rustupUpdate(this.config.rustupConfig());
301+
const ws =
302+
multiProjectEnabled && activeWorkspace ? activeWorkspace : this;
303+
return rustupUpdate(ws.config.rustupConfig());
293304
},
294305
);
295306
this.disposables.push(rustupUpdateDisposable);
296307

297308
const restartServer = commands.registerCommand('rls.restart', async () => {
298-
await this.stop();
299-
return this.start(context);
309+
const ws =
310+
multiProjectEnabled && activeWorkspace ? activeWorkspace : this;
311+
await ws.stop();
312+
return ws.start(context);
300313
});
301314
this.disposables.push(restartServer);
302315

303316
this.disposables.push(
304-
commands.registerCommand('rls.run', (cmd: Execution) =>
305-
runRlsCommand(this.folder, cmd),
306-
),
317+
commands.registerCommand('rls.run', (cmd: Execution) => {
318+
const ws =
319+
multiProjectEnabled && activeWorkspace ? activeWorkspace : this;
320+
runRlsCommand(ws.folder, cmd);
321+
}),
307322
);
308323
}
309324

@@ -471,7 +486,7 @@ async function warnOnMissingCargoToml() {
471486

472487
if (files.length < 1) {
473488
window.showWarningMessage(
474-
'A Cargo.toml file must be at the root of the workspace in order to support all features',
489+
'A Cargo.toml file must be at the root of the workspace in order to support all features. Alternatively set rust-client.enableMultiProjectSetup=true in settings.',
475490
);
476491
}
477492
}

src/workspace_util.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { Uri, WorkspaceFolder } from 'vscode';
4+
5+
// searches up the folder structure until it finds a Cargo.toml
6+
export function nearestParentWorkspace(
7+
curWorkspace: WorkspaceFolder,
8+
filePath: string,
9+
): WorkspaceFolder {
10+
// check that the workspace folder already contains the "Cargo.toml"
11+
const workspaceRoot = path.parse(curWorkspace.uri.fsPath).dir;
12+
const rootManifest = path.join(workspaceRoot, 'Cargo.toml');
13+
if (fs.existsSync(rootManifest)) {
14+
return curWorkspace;
15+
}
16+
17+
// algorithm that will strip one folder at a time and check if that folder contains "Cargo.toml"
18+
let current = filePath;
19+
while (true) {
20+
const old = current;
21+
current = path.dirname(current);
22+
23+
// break in case there is a bug that could result in a busy loop
24+
if (old === current) {
25+
break;
26+
}
27+
28+
// break in case the strip folder has not changed
29+
if (workspaceRoot === path.parse(current).dir) {
30+
break;
31+
}
32+
33+
// check if "Cargo.toml" is present in the parent folder
34+
const cargoPath = path.join(current, 'Cargo.toml');
35+
if (fs.existsSync(cargoPath)) {
36+
// ghetto change the uri on Workspace folder to make vscode think it's located elsewhere
37+
return { ...curWorkspace, uri: Uri.parse(current) };
38+
}
39+
}
40+
41+
return curWorkspace;
42+
}

0 commit comments

Comments
 (0)