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

reintroduce multi project setup #638

Merged
merged 10 commits into from
Sep 18, 2019
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@
"default": true,
"description": "If one root workspace folder is nested in another root folder, look for the Rust config in the outermost root."
},
"rust-client.enableMultiProjectSetup": {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know much about the extension, didn't have the time to clone it yet, but wouldn't it be possible to make the extension work like :

  • Normal cargo.toml at root of the project is tried
  • If it fails the extension tries to determine if it's not a monorepo

I'd like your opinion on this, if it is quite some work I'd happily lend you a hand 🙂

Copy link
Contributor Author

@jannickj jannickj Aug 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normal cargo.toml at root of the project is tried

With this change the cargo.toml at root should also just work.

If it fails the extension tries to determine if it's not a monorepo

How would you detect that? I could imagine some wtf moments where you add a folder and all of the sudden the tooling stops working. I would rather make configuration for stuff like that.

I'd like your opinion on this, if it is quite some work I'd happily lend you a hand

In general I would estimate that 99% will just always have it turned on. So mostly this is about not breaking it for the 1%.

and thx for the offer :)

"type": "boolean",
"default": false,
"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)"
},
"rust.sysroot": {
"type": [
"string",
Expand Down
7 changes: 7 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ export class RLSConfiguration {
return this.configuration.get<string>('rust-client.rlsPath');
}

public get multiProjectEnabled(): boolean {
return this.configuration.get<boolean>(
'rust-client.enableMultiProjectSetup',
false,
);
}

// Added ignoreChannel for readChannel function. Otherwise we end in an infinite loop.
public rustupConfig(ignoreChannel: boolean = false): RustupConfig {
return {
Expand Down
143 changes: 79 additions & 64 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { startSpinner, stopSpinner } from './spinner';
import { activateTaskProvider, Execution, runRlsCommand } from './tasks';
import { withWsl } from './utils/child_process';
import { uriWindowsToWsl, uriWslToWindows } from './utils/wslpath';
import * as workspace_util from './workspace_util';

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

workspace.onDidOpenTextDocument(doc => didOpenTextDocument(doc, context));
workspace.textDocuments.forEach(doc => didOpenTextDocument(doc, context));
workspace.onDidOpenTextDocument(doc => whenOpeningTextDocument(doc, context));
workspace.textDocuments.forEach(doc => whenOpeningTextDocument(doc, context));
workspace.onDidChangeWorkspaceFolders(e =>
didChangeWorkspaceFolders(e, context),
whenChangingWorkspaceFolders(e, context),
);
}

Expand All @@ -56,7 +57,7 @@ export async function deactivate() {
}

// Taken from https://github.com/Microsoft/vscode-extension-samples/blob/master/lsp-multi-server-sample/client/src/extension.ts
function didOpenTextDocument(
function whenOpeningTextDocument(
document: TextDocument,
context: ExtensionContext,
) {
Expand All @@ -69,23 +70,36 @@ function didOpenTextDocument(
if (!folder) {
return;
}
if (
workspace
.getConfiguration()
.get<boolean>('rust-client.nestedMultiRootConfigInOutermost', true)
) {

const inMultiProjectMode = workspace
.getConfiguration()
.get<boolean>('rust-client.enableMultiProjectSetup', false);

const inNestedOuterProjectMode = workspace
.getConfiguration()
.get<boolean>('rust-client.nestedMultiRootConfigInOutermost', true);

if (inMultiProjectMode) {
folder = workspace_util.nearestParentWorkspace(folder, document.uri.fsPath);
} else if (inNestedOuterProjectMode) {
folder = getOuterMostWorkspaceFolder(folder);
}
// folder = getCargoTomlWorkspace(folder, document.uri.fsPath);

if (!folder) {
stopSpinner(`RLS: Cargo.toml missing`);
return;
}

if (!workspaces.has(folder.uri)) {
const folderPath = folder.uri.toString();

if (!workspaces.has(folderPath)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should definitely move this logic out of didOpenTextDocument. Anything related to organizing and identifying instances across different folders/workspaces should be under src/workspace.ts perhaps? (name open for bikeshedding)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I'm not against refactoring some of the code out, I'm afraid that this will delay this PR even more, as it's now doing two things at once.

const workspace = new ClientWorkspace(folder);
workspaces.set(folder.uri, workspace);
activeWorkspace = workspace;
workspaces.set(folderPath, workspace);
workspace.start(context);
} else {
const ws = workspaces.get(folderPath);
activeWorkspace = typeof ws === 'undefined' ? null : ws;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const workspace = workspaces.get(folderPath);
activeWorkspace = (typeof workspace === 'undefined')
    ? null 
    : workspace;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the formatter, which is build server checked, decides stuff like that.

}
}

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

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

// function getCargoTomlWorkspace(cur_workspace: WorkspaceFolder, file_path: string): WorkspaceFolder {
// if (!cur_workspace) {
// return cur_workspace;
// }

// const workspace_root = path.parse(cur_workspace.uri.fsPath).dir;
// const root_manifest = path.join(workspace_root, 'Cargo.toml');
// if (fs.existsSync(root_manifest)) {
// return cur_workspace;
// }

// let current = file_path;

// while (true) {
// const old = current;
// current = path.dirname(current);
// if (old == current) {
// break;
// }
// if (workspace_root == path.parse(current).dir) {
// break;
// }

// const cargo_path = path.join(current, 'Cargo.toml');
// if (fs.existsSync(cargo_path)) {
// return { ...cur_workspace, uri: Uri.parse(current) };
// }
// }

// return cur_workspace;
// }

function getOuterMostWorkspaceFolder(folder: WorkspaceFolder): WorkspaceFolder {
// TODO: decouple the global state such that it can be moved to workspace_util
const sorted = sortedWorkspaceFolders();
for (const element of sorted) {
let uri = folder.uri.toString();
Expand All @@ -156,7 +140,7 @@ function getOuterMostWorkspaceFolder(folder: WorkspaceFolder): WorkspaceFolder {
return folder;
}

function didChangeWorkspaceFolders(
function whenChangingWorkspaceFolders(
e: WorkspaceFoldersChangeEvent,
context: ExtensionContext,
) {
Expand All @@ -166,13 +150,13 @@ function didChangeWorkspaceFolders(
// if not, and it is a Rust project (i.e., has a Cargo.toml), then create a new client.
for (let folder of e.added) {
folder = getOuterMostWorkspaceFolder(folder);
if (workspaces.has(folder.uri)) {
if (workspaces.has(folder.uri.toString())) {
continue;
}
for (const f of fs.readdirSync(folder.uri.fsPath)) {
if (f === 'Cargo.toml') {
const workspace = new ClientWorkspace(folder);
workspaces.set(folder.uri, workspace);
workspaces.set(folder.uri.toString(), workspace);
workspace.start(context);
break;
}
Expand All @@ -181,15 +165,18 @@ function didChangeWorkspaceFolders(

// If a workspace is removed which is a Rust workspace, kill the client.
for (const folder of e.removed) {
const ws = workspaces.get(folder.uri);
const ws = workspaces.get(folder.uri.toString());
if (ws) {
workspaces.delete(folder.uri);
workspaces.delete(folder.uri.toString());
ws.stop();
}
}
}

const workspaces: Map<Uri, ClientWorkspace> = new Map();
// Don't use URI as it's unreliable the same path might not become the same URI.
const workspaces: Map<string, ClientWorkspace> = new Map();
let activeWorkspace: ClientWorkspace | null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typescript has a builtin for nullable variables :

let activeWorkspace?: ClientWorkspace;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per above, it'd be great if we could extract this 'global' state somehow to a dedicated module, similar to any other code that manages/fetches appropriate workspace servers.

let commandsRegistered: boolean = false;

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

public async start(context: ExtensionContext) {
warnOnMissingCargoToml();
if (!this.config.multiProjectEnabled) {
warnOnMissingCargoToml();
}

startSpinner('RLS', 'Starting');

const serverOptions: ServerOptions = async () => {
await this.autoUpdate();
return this.makeRlsProcess();
};

const pattern = this.config.multiProjectEnabled
? `${this.folder.uri.path}/**`
: undefined;
const collectionName = this.config.multiProjectEnabled
? `rust ${this.folder.uri.toString()}`
: 'rust';
const clientOptions: LanguageClientOptions = {
// Register the server for Rust files

documentSelector: [
{ language: 'rust', scheme: 'file' },
{ language: 'rust', scheme: 'untitled' },
{ language: 'rust', scheme: 'file', pattern },
{ language: 'rust', scheme: 'untitled', pattern },
],
diagnosticCollectionName: 'rust',
diagnosticCollectionName: collectionName,
synchronize: { configurationSection: 'rust' },
// Controls when to focus the channel rather than when to reveal it in the drop-down list
revealOutputChannelOn: this.config.revealOutputChannelOn,
Expand Down Expand Up @@ -259,13 +256,17 @@ class ClientWorkspace {
clientOptions,
);

const selector = this.config.multiProjectEnabled
? { language: 'rust', scheme: 'file', pattern }
: { language: 'rust' };

this.setupProgressCounter();
this.registerCommands(context);
this.registerCommands(context, this.config.multiProjectEnabled);
this.disposables.push(activateTaskProvider(this.folder));
this.disposables.push(this.lc.start());
this.disposables.push(
languages.registerSignatureHelpProvider(
{ language: 'rust' },
selector,
new SignatureHelpProvider(this.lc),
'(',
',',
Expand All @@ -279,31 +280,45 @@ class ClientWorkspace {
}

this.disposables.forEach(d => d.dispose());
commandsRegistered = false;
}

private registerCommands(context: ExtensionContext) {
private registerCommands(
context: ExtensionContext,
multiProjectEnabled: boolean,
) {
if (!this.lc) {
return;
}
if (multiProjectEnabled && commandsRegistered) {
return;
}

commandsRegistered = true;
const rustupUpdateDisposable = commands.registerCommand(
'rls.update',
() => {
return rustupUpdate(this.config.rustupConfig());
const ws =
multiProjectEnabled && activeWorkspace ? activeWorkspace : this;
return rustupUpdate(ws.config.rustupConfig());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const workspace = (multiProjectEnabled && activeWorkspace) 
    ? activeWorkspace 
    : this;
return rustupUpdate(workspace.config.rustupConfig());

},
);
this.disposables.push(rustupUpdateDisposable);

const restartServer = commands.registerCommand('rls.restart', async () => {
await this.stop();
return this.start(context);
const ws =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about ws -> workspace here 🙂

multiProjectEnabled && activeWorkspace ? activeWorkspace : this;
await ws.stop();
return ws.start(context);
});
this.disposables.push(restartServer);

this.disposables.push(
commands.registerCommand('rls.run', (cmd: Execution) =>
runRlsCommand(this.folder, cmd),
),
commands.registerCommand('rls.run', (cmd: Execution) => {
const ws =

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading this took me some second to understand that ws was workspace, also personal preference but I find ternary easier to read like so :

const workspace = (multiProjectEnabled && activeWorkspace)
      ? activeWorkspace
      : this;
runRlsCommand(workspace.folder, cmd);

I don't know if it's ok with the tslint configs.

multiProjectEnabled && activeWorkspace ? activeWorkspace : this;
runRlsCommand(ws.folder, cmd);
}),
);
}

Expand Down Expand Up @@ -471,7 +486,7 @@ async function warnOnMissingCargoToml() {

if (files.length < 1) {
window.showWarningMessage(
'A Cargo.toml file must be at the root of the workspace in order to support all features',
'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.',
);
}
}
Expand Down
42 changes: 42 additions & 0 deletions src/workspace_util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as fs from 'fs';
import * as path from 'path';
import { Uri, WorkspaceFolder } from 'vscode';

// searches up the folder structure until it finds a Cargo.toml
export function nearestParentWorkspace(
curWorkspace: WorkspaceFolder,
filePath: string,
): WorkspaceFolder {
// check that the workspace folder already contains the "Cargo.toml"
const workspaceRoot = path.parse(curWorkspace.uri.fsPath).dir;
const rootManifest = path.join(workspaceRoot, 'Cargo.toml');
if (fs.existsSync(rootManifest)) {
return curWorkspace;
}

// algorithm that will strip one folder at a time and check if that folder contains "Cargo.toml"
let current = filePath;
while (true) {
const old = current;
current = path.dirname(current);

// break in case there is a bug that could result in a busy loop
if (old === current) {
break;
}

// break in case the strip folder has not changed
if (workspaceRoot === path.parse(current).dir) {
break;
}

// check if "Cargo.toml" is present in the parent folder
const cargoPath = path.join(current, 'Cargo.toml');
if (fs.existsSync(cargoPath)) {
// ghetto change the uri on Workspace folder to make vscode think it's located elsewhere
return { ...curWorkspace, uri: Uri.parse(current) };
}
}

return curWorkspace;
}