-
Notifications
You must be signed in to change notification settings - Fork 163
reintroduce multi project setup #638
Changes from all commits
1e2e04c
6684a08
c802552
60312e3
2a5ab78
0e1114b
6a580eb
3c957c1
da6bb20
47c66be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
@@ -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), | ||
); | ||
} | ||
|
||
|
@@ -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, | ||
) { | ||
|
@@ -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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should definitely move this logic out of There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. const workspace = workspaces.get(folderPath);
activeWorkspace = (typeof workspace === 'undefined')
? null
: workspace; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the formatter, which is build server checked, decides stuff like that. |
||
} | ||
} | ||
|
||
|
@@ -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 => { | ||
|
@@ -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(); | ||
|
@@ -156,7 +140,7 @@ function getOuterMostWorkspaceFolder(folder: WorkspaceFolder): WorkspaceFolder { | |
return folder; | ||
} | ||
|
||
function didChangeWorkspaceFolders( | ||
function whenChangingWorkspaceFolders( | ||
e: WorkspaceFoldersChangeEvent, | ||
context: ExtensionContext, | ||
) { | ||
|
@@ -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; | ||
} | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typescript has a builtin for nullable variables : let activeWorkspace?: ClientWorkspace; There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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, | ||
|
@@ -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), | ||
'(', | ||
',', | ||
|
@@ -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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}), | ||
); | ||
} | ||
|
||
|
@@ -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.', | ||
); | ||
} | ||
} | ||
|
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; | ||
} |
There was a problem hiding this comment.
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 :
I'd like your opinion on this, if it is quite some work I'd happily lend you a hand 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With this change the cargo.toml at root should also just work.
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.
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 :)