Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions crates/rust-analyzer/src/bin/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::{fmt::Write, path::PathBuf};
pub(crate) struct Args {
pub(crate) verbosity: Verbosity,
pub(crate) command: Command,
pub(crate) roots: Option<Vec<String>>,
}

pub(crate) enum Command {
Expand Down Expand Up @@ -47,12 +48,25 @@ pub(crate) enum Command {
}

impl Args {
fn get_roots(mut matches: Arguments) -> Result<Option<Vec<String>>> {
if matches.contains("--roots") {
Copy link
Contributor

Choose a reason for hiding this comment

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

Adding a command line option, given that this is the first command line option, massively changes the interface of the program. It's much better to stick to existing conventions, which, in this case, are the initializationOptions (not sure about the naming) field of the initialize requst

Copy link
Author

Choose a reason for hiding this comment

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

massively changes the interface of the program

As I said I couldn't find a way to send it via the initialization options in vscode. It also shouldn't be a massive change to the interface since it's a completely optional parameter but I can see how it feels intrusive.

Ok(Some(matches.free()?))
} else {
matches.finish().or_else(handle_extra_flags)?;
Ok(None)
}
}

pub(crate) fn parse() -> Result<Result<Args, HelpPrinted>> {
let mut matches = Arguments::from_env();

if matches.contains("--version") {
matches.finish().or_else(handle_extra_flags)?;
return Ok(Ok(Args { verbosity: Verbosity::Normal, command: Command::Version }));
return Ok(Ok(Args {
verbosity: Verbosity::Normal,
command: Command::Version,
roots: None,
}));
}

let verbosity = match (
Expand All @@ -71,8 +85,11 @@ impl Args {
let subcommand = match matches.subcommand()? {
Some(it) => it,
None => {
matches.finish().or_else(handle_extra_flags)?;
return Ok(Ok(Args { verbosity, command: Command::RunServer }));
return Ok(Ok(Args {
verbosity,
command: Command::RunServer,
roots: Self::get_roots(matches)?,
}));
}
};
let command = match subcommand.as_str() {
Expand Down Expand Up @@ -269,7 +286,7 @@ SUBCOMMANDS:
return Ok(Err(HelpPrinted));
}
};
Ok(Ok(Args { verbosity, command }))
Ok(Ok(Args { verbosity, command, roots: None }))
}
}

Expand Down
25 changes: 16 additions & 9 deletions crates/rust-analyzer/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ fn main() -> Result<()> {
cli::diagnostics(path.as_ref(), load_output_dirs, all)?
}

args::Command::RunServer => run_server()?,
args::Command::RunServer => run_server(args.roots)?,
args::Command::Version => println!("rust-analyzer {}", env!("REV")),
}
Ok(())
Expand All @@ -56,7 +56,7 @@ fn setup_logging() -> Result<()> {
Ok(())
}

fn run_server() -> Result<()> {
fn run_server(roots: Option<Vec<String>>) -> Result<()> {
log::info!("lifecycle: server started");

let (connection, io_threads) = Connection::stdio();
Expand All @@ -73,13 +73,20 @@ fn run_server() -> Result<()> {
let cwd = std::env::current_dir()?;
let root = initialize_params.root_uri.and_then(|it| it.to_file_path().ok()).unwrap_or(cwd);

let workspace_roots = initialize_params
.workspace_folders
.map(|workspaces| {
workspaces.into_iter().filter_map(|it| it.uri.to_file_path().ok()).collect::<Vec<_>>()
})
.filter(|workspaces| !workspaces.is_empty())
.unwrap_or_else(|| vec![root]);
let workspace_roots = if let Some(roots) = roots {
roots.into_iter().map(Into::into).collect::<Vec<_>>()
} else {
initialize_params
.workspace_folders
.map(|workspaces| {
workspaces
.into_iter()
.filter_map(|it| it.uri.to_file_path().ok())
.collect::<Vec<_>>()
})
.filter(|workspaces| !workspaces.is_empty())
.unwrap_or_else(|| vec![root])
};

let config = {
let mut config = Config::default();
Expand Down
5 changes: 5 additions & 0 deletions editors/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@
"default": true,
"description": "Whether to ask for permission before downloading any files from the Internet"
},
"rust-analyzer.server.autoRestartOnNew": {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"rust-analyzer.server.autoRestartOnNew": {
"rust-analyzer.server.autoRestartOnNewProjectFound": {

"type": "boolean",
"default": false,
"description": "automatically restarts rust-analyzer server when a new project is discovered"
},
"rust-analyzer.serverPath": {
"type": [
"null",
Expand Down
3 changes: 2 additions & 1 deletion editors/code/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import * as vscode from 'vscode';
import { CallHierarchyFeature } from 'vscode-languageclient/lib/callHierarchy.proposed';
import { SemanticTokensFeature, DocumentSemanticsTokensSignature } from 'vscode-languageclient/lib/semanticTokens.proposed';

export async function createClient(serverPath: string, cwd: string): Promise<lc.LanguageClient> {
export async function createClient(serverPath: string, cwd: string, projectFolders: string[]): Promise<lc.LanguageClient> {
// '.' Is the fallback if no folder is open
// TODO?: Workspace folders support Uri's (eg: file://test.txt).
// It might be a good idea to test if the uri points to a file.

const run: lc.Executable = {
command: serverPath,
args: ["--roots"].concat(projectFolders),
options: { cwd },
};
const serverOptions: lc.ServerOptions = {
Expand Down
2 changes: 2 additions & 0 deletions editors/code/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ export class Config {
get askBeforeDownload() { return this.get<boolean>("updates.askBeforeDownload"); }
get traceExtension() { return this.get<boolean>("trace.extension"); }

get autoRestartOnNew() { return this.get<boolean>("server.autoRestartOnNew"); }


get inlayHints() {
return {
Expand Down
3 changes: 2 additions & 1 deletion editors/code/src/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ export class Ctx {
extCtx: vscode.ExtensionContext,
serverPath: string,
cwd: string,
projectFolders: string[],
): Promise<Ctx> {
const client = await createClient(serverPath, cwd);
const client = await createClient(serverPath, cwd, projectFolders);
const res = new Ctx(config, extCtx, client, serverPath);
res.pushCleanup(client.start());
await client.onReady();
Expand Down
87 changes: 70 additions & 17 deletions editors/code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,76 @@ import { activateInlayHints } from './inlay_hints';
import { activateStatusDisplay } from './status_display';
import { Ctx } from './ctx';
import { Config, NIGHTLY_TAG } from './config';
import { log, assert } from './util';
import { log, assert, nearestParentWithCargoToml, createWorkspaceWithNewLocation } from './util';
import { PersistentState } from './persistent_state';
import { fetchRelease, download } from './net';
import { spawnSync } from 'child_process';
import { activateTaskProvider } from './tasks';

let ctx: Ctx | undefined;
const foundProjects: Set<string> = new Set();
Copy link
Contributor

Choose a reason for hiding this comment

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

As a general comment, global singletons should not be added without very good reason.

Copy link
Author

Choose a reason for hiding this comment

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

100% agree,
I only used it here because it's not meant be shared outside main, and it didn't fit in Ctx since this is meant to live after a restart.

let config: Config | undefined = undefined;

async function locateRustProjects(root: vscode.Uri) {
const cargoRoots = await Promise.all(vscode.workspace.textDocuments.map((doc) => nearestParentWithCargoToml(root, doc.uri)));
for (const cargoRoot of cargoRoots) {
if (cargoRoot != null) {
foundProjects.add(cargoRoot.fsPath);
}
}
}

export async function activate(context: vscode.ExtensionContext) {

config = new Config(context);

const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (workspaceFolder !== undefined) {
await locateRustProjects(workspaceFolder.uri);
}

vscode.workspace.onDidOpenTextDocument(async (doc) => {
const wf = vscode.workspace.getWorkspaceFolder(doc.uri);
if (wf) {
const cargoRoot = await nearestParentWithCargoToml(wf.uri, doc.uri);
if (cargoRoot != null) {
const isMissing = !foundProjects.has(cargoRoot.fsPath);

foundProjects.add(cargoRoot.fsPath);
if (isMissing) {
if (config?.autoRestartOnNew) {
Copy link
Contributor

Choose a reason for hiding this comment

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

My experience in IntelliJ tells me that auto restart doesn't worth the trouble. The problem is that we should only restart in quiescent state, and only the user knows when this state is reached.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ie, setting a status "needs restart" is absolutely necessary, and a big missing piece in rust-analyzer at the moment, but it should be the user who decides to click the restart button.

Copy link
Author

Choose a reason for hiding this comment

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

I added it after testing it a bit, since it got very old very quickly having to restart manually all the time, which is also why it's turned off by default.

restart(context);
} else {
vscode.window.showInformationMessage(
`Found a new project at ${cargoRoot.fsPath}.` +
" Manually run: \"Rust Analyzer: Restart server\"" +
" or set rust-analyzer.server.autoRestartOnNew=true"
);
}
}
}
}
});

startRA(context);

}

export async function restart(context: vscode.ExtensionContext) {
void vscode.window.showInformationMessage('Reloading rust-analyzer...');
await deactivate();
while (context.subscriptions.length > 0) {
try {
context.subscriptions.pop()!.dispose();
} catch (err) {
log.error("Dispose error:", err);
}
}
await startRA(context).catch(log.error);
}

export async function startRA(context: vscode.ExtensionContext) {

// Register a "dumb" onEnter command for the case where server fails to
// start.
//
Expand All @@ -31,13 +92,15 @@ export async function activate(context: vscode.ExtensionContext) {
// "rust-analyzer is not available"
// ),
// )

const defaultOnEnter = vscode.commands.registerCommand(
'rust-analyzer.onEnter',
() => vscode.commands.executeCommand('default:type', { text: '\n' }),
);
context.subscriptions.push(defaultOnEnter);

const config = new Config(context);
config = new Config(context);

const state = new PersistentState(context.globalState);
const serverPath = await bootstrap(config, state);

Expand All @@ -52,23 +115,11 @@ export async function activate(context: vscode.ExtensionContext) {
// registers its `onDidChangeDocument` handler before us.
//
// This a horribly, horribly wrong way to deal with this problem.
ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath);

ctx = await Ctx.create(config, context, serverPath, workspaceFolder.uri.fsPath, Array.from(foundProjects));
// Commands which invokes manually via command palette, shortcut, etc.

// Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895
ctx.registerCommand('reload', _ => async () => {
void vscode.window.showInformationMessage('Reloading rust-analyzer...');
await deactivate();
while (context.subscriptions.length > 0) {
try {
context.subscriptions.pop()!.dispose();
} catch (err) {
log.error("Dispose error:", err);
}
}
await activate(context).catch(log.error);
});
ctx.registerCommand('reload', _ => restart);

ctx.registerCommand('analyzerStatus', commands.analyzerStatus);
ctx.registerCommand('collectGarbage', commands.collectGarbage);
Expand All @@ -92,7 +143,9 @@ export async function activate(context: vscode.ExtensionContext) {
ctx.registerCommand('applySourceChange', commands.applySourceChange);
ctx.registerCommand('selectAndApplySourceChange', commands.selectAndApplySourceChange);

ctx.pushCleanup(activateTaskProvider(workspaceFolder));
for (const project of foundProjects) {
ctx.pushCleanup(activateTaskProvider(createWorkspaceWithNewLocation(workspaceFolder, vscode.Uri.file(project))));
}

activateStatusDisplay(ctx);

Expand Down
2 changes: 1 addition & 1 deletion editors/code/src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function getStandardCargoTasks(target: vscode.WorkspaceFolder): vscode.Task[] {
`cargo ${command}`,
'rust',
// What to do when this command is executed.
new vscode.ShellExecution('cargo', [command]),
new vscode.ShellExecution('cargo', [command], { cwd: target.uri.fsPath }),
// Problem matchers.
['$rustc'],
);
Expand Down
50 changes: 50 additions & 0 deletions editors/code/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as lc from "vscode-languageclient";
import * as vscode from "vscode";
import * as path from 'path';
import * as fs from 'fs';
import * as util from 'util';
import { strict as nativeAssert } from "assert";

export function assert(condition: boolean, explanation: string): asserts condition {
Expand All @@ -11,6 +14,7 @@ export function assert(condition: boolean, explanation: string): asserts conditi
}
}


export const log = new class {
private enabled = true;

Expand Down Expand Up @@ -82,3 +86,49 @@ export function isRustDocument(document: vscode.TextDocument): document is RustD
export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor {
return isRustDocument(editor.document);
}

export function createWorkspaceWithNewLocation(workspace: vscode.WorkspaceFolder, newLoc: vscode.Uri) {
return {
...workspace,
name: path.basename(newLoc.fsPath),
uri: newLoc,
};
}

// searches up the folder structure until it finds a Cargo.toml
export async function nearestParentWithCargoToml(
Copy link
Contributor

Choose a reason for hiding this comment

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

We already have this implemented in rust (find_cargo_toml in ra_project_model), and I think we should strive for keeping as much as possible in rust, as it is sharable between the editors then.

Copy link
Author

@jannickj jannickj Apr 16, 2020

Choose a reason for hiding this comment

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

@matklad

Took a look. I must say this feels like a pretty big change to how everything works, which departs a lot from what is already implemented/what I envision, so it was a bit hard to review for me :)

Not sure what it takes away from what is already implemented / what visions it's breaking?
Is it that nothing can be done on the client side?

The big piece is that we should strive to keep almost everything in Rust. I think the only thing here which genuinely belongs to the editor is didChangeWorkspaceFolders event, for everything else, I don't see a fundamental reason why it can't be done at the editor's side.

That's a good point, I saw this as a very minor change to improve QoL for vscode, but I can see how it might be better to do 100% server side for many other reasons. It took me a couple of hours to code up (mostly reading vscode issues). If that's what is ruining this pr for you I could redo it in the server, however there it will actually affect all users of it (but maybe that is what we want?).

still don't fully understand what specific use-cases this covers, but here's my ideas about what we can do to make handing multi-projects better

There are many use-cases

  1. You have a large project where rust is in a deep subfolder
  2. You open a random file from another project (outside your workspace)
  3. You have many projects in one folder which depend on each other (ala crates)

workspaceRootUri: vscode.Uri,
fileLoc: vscode.Uri,
): Promise<vscode.Uri | null> {
const fileExists: (path: fs.PathLike) => Promise<boolean> = util.promisify(fs.exists);
// check that the workspace folder already contains the "Cargo.toml"
const workspaceRoot = workspaceRootUri.fsPath;
// algorithm that will strip one folder at a time and check if that folder contains "Cargo.toml"
let current = fileLoc.fsPath;
if (fileLoc.fsPath.substring(0, workspaceRoot.length) !== workspaceRoot) {
return null;
}
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 reached the workspace root
if (workspaceRoot === current) {
break;
}

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

return null;
}