⬆️ rust-analyzer

This commit is contained in:
Laurențiu Nicola 2023-03-20 08:31:01 +02:00
parent 544b4cfe4d
commit dbf04a5ee2
106 changed files with 2219 additions and 609 deletions

View file

@ -199,6 +199,11 @@
"title": "Reload workspace",
"category": "rust-analyzer"
},
{
"command": "rust-analyzer.addProject",
"title": "Add current file's crate to workspace",
"category": "rust-analyzer"
},
{
"command": "rust-analyzer.reload",
"title": "Restart server",
@ -428,6 +433,17 @@
"default": false,
"type": "boolean"
},
"rust-analyzer.discoverProjectCommand": {
"markdownDescription": "Sets the command that rust-analyzer uses to generate `rust-project.json` files. This command should only be used\n if a build system like Buck or Bazel is also in use. The command must accept files as arguments and return \n a rust-project.json over stdout.",
"default": null,
"type": [
"null",
"array"
],
"items": {
"type": "string"
}
},
"$generated-start": {},
"rust-analyzer.assist.emitMustUse": {
"markdownDescription": "Whether to insert #[must_use] when generating `as_` methods\nfor enum variants.",

View file

@ -6,7 +6,7 @@ import * as Is from "vscode-languageclient/lib/common/utils/is";
import { assert } from "./util";
import * as diagnostics from "./diagnostics";
import { WorkspaceEdit } from "vscode";
import { Config, substituteVSCodeVariables } from "./config";
import { Config, prepareVSCodeConfig } from "./config";
import { randomUUID } from "crypto";
export interface Env {
@ -95,7 +95,16 @@ export async function createClient(
const resp = await next(params, token);
if (resp && Array.isArray(resp)) {
return resp.map((val) => {
return substituteVSCodeVariables(val);
return prepareVSCodeConfig(val, (key, cfg) => {
// we only want to set discovered workspaces on the right key
// and if a workspace has been discovered.
if (
key === "linkedProjects" &&
config.discoveredWorkspaces.length > 0
) {
cfg[key] = config.discoveredWorkspaces;
}
});
});
} else {
return resp;

View file

@ -3,7 +3,7 @@ import * as lc from "vscode-languageclient";
import * as ra from "./lsp_ext";
import * as path from "path";
import { Ctx, Cmd, CtxInit } from "./ctx";
import { Ctx, Cmd, CtxInit, discoverWorkspace } from "./ctx";
import { applySnippetWorkspaceEdit, applySnippetTextEdits } from "./snippets";
import { spawnSync } from "child_process";
import { RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run";
@ -749,6 +749,33 @@ export function reloadWorkspace(ctx: CtxInit): Cmd {
return async () => ctx.client.sendRequest(ra.reloadWorkspace);
}
export function addProject(ctx: CtxInit): Cmd {
return async () => {
const discoverProjectCommand = ctx.config.discoverProjectCommand;
if (!discoverProjectCommand) {
return;
}
const workspaces: JsonProject[] = await Promise.all(
vscode.workspace.workspaceFolders!.map(async (folder): Promise<JsonProject> => {
const rustDocuments = vscode.workspace.textDocuments.filter(isRustDocument);
return discoverWorkspace(rustDocuments, discoverProjectCommand, {
cwd: folder.uri.fsPath,
});
})
);
ctx.addToDiscoveredWorkspaces(workspaces);
// this is a workaround to avoid needing writing the `rust-project.json` into
// a workspace-level VS Code-specific settings folder. We'd like to keep the
// `rust-project.json` entirely in-memory.
await ctx.client?.sendNotification(lc.DidChangeConfigurationNotification.type, {
settings: "",
});
};
}
async function showReferencesImpl(
client: LanguageClient | undefined,
uri: string,

View file

@ -34,6 +34,7 @@ export class Config {
constructor(ctx: vscode.ExtensionContext) {
this.globalStorageUri = ctx.globalStorageUri;
this.discoveredWorkspaces = [];
vscode.workspace.onDidChangeConfiguration(
this.onDidChangeConfiguration,
this,
@ -55,6 +56,8 @@ export class Config {
log.info("Using configuration", Object.fromEntries(cfg));
}
public discoveredWorkspaces: JsonProject[];
private async onDidChangeConfiguration(event: vscode.ConfigurationChangeEvent) {
this.refreshLogging();
@ -191,7 +194,7 @@ export class Config {
* So this getter handles this quirk by not requiring the caller to use postfix `!`
*/
private get<T>(path: string): T | undefined {
return substituteVSCodeVariables(this.cfg.get<T>(path));
return prepareVSCodeConfig(this.cfg.get<T>(path));
}
get serverPath() {
@ -214,6 +217,10 @@ export class Config {
return this.get<boolean>("trace.extension");
}
get discoverProjectCommand() {
return this.get<string[] | undefined>("discoverProjectCommand");
}
get cargoRunner() {
return this.get<string | undefined>("cargoRunner");
}
@ -280,18 +287,32 @@ export class Config {
}
}
export function substituteVSCodeVariables<T>(resp: T): T {
// the optional `cb?` parameter is meant to be used to add additional
// key/value pairs to the VS Code configuration. This needed for, e.g.,
// including a `rust-project.json` into the `linkedProjects` key as part
// of the configuration/InitializationParams _without_ causing VS Code
// configuration to be written out to workspace-level settings. This is
// undesirable behavior because rust-project.json files can be tens of
// thousands of lines of JSON, most of which is not meant for humans
// to interact with.
export function prepareVSCodeConfig<T>(
resp: T,
cb?: (key: Extract<keyof T, string>, res: { [key: string]: any }) => void
): T {
if (Is.string(resp)) {
return substituteVSCodeVariableInString(resp) as T;
} else if (resp && Is.array<any>(resp)) {
return resp.map((val) => {
return substituteVSCodeVariables(val);
return prepareVSCodeConfig(val);
}) as T;
} else if (resp && typeof resp === "object") {
const res: { [key: string]: any } = {};
for (const key in resp) {
const val = resp[key];
res[key] = substituteVSCodeVariables(val);
res[key] = prepareVSCodeConfig(val);
if (cb) {
cb(key, res);
}
}
return res as T;
}

View file

@ -2,12 +2,20 @@ import * as vscode from "vscode";
import * as lc from "vscode-languageclient/node";
import * as ra from "./lsp_ext";
import { Config, substituteVSCodeVariables } from "./config";
import { Config, prepareVSCodeConfig } from "./config";
import { createClient } from "./client";
import { isRustDocument, isRustEditor, LazyOutputChannel, log, RustEditor } from "./util";
import {
executeDiscoverProject,
isRustDocument,
isRustEditor,
LazyOutputChannel,
log,
RustEditor,
} from "./util";
import { ServerStatusParams } from "./lsp_ext";
import { PersistentState } from "./persistent_state";
import { bootstrap } from "./bootstrap";
import { ExecOptions } from "child_process";
// We only support local folders, not eg. Live Share (`vlsl:` scheme), so don't activate if
// only those are in use. We use "Empty" to represent these scenarios
@ -41,6 +49,17 @@ export function fetchWorkspace(): Workspace {
: { kind: "Workspace Folder" };
}
export async function discoverWorkspace(
files: readonly vscode.TextDocument[],
command: string[],
options: ExecOptions
): Promise<JsonProject> {
const paths = files.map((f) => `"${f.uri.fsPath}"`).join(" ");
const joinedCommand = command.join(" ");
const data = await executeDiscoverProject(`${joinedCommand} ${paths}`, options);
return JSON.parse(data) as JsonProject;
}
export type CommandFactory = {
enabled: (ctx: CtxInit) => Cmd;
disabled?: (ctx: Ctx) => Cmd;
@ -52,7 +71,7 @@ export type CtxInit = Ctx & {
export class Ctx {
readonly statusBar: vscode.StatusBarItem;
readonly config: Config;
config: Config;
readonly workspace: Workspace;
private _client: lc.LanguageClient | undefined;
@ -169,7 +188,30 @@ export class Ctx {
};
}
const initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
const discoverProjectCommand = this.config.discoverProjectCommand;
if (discoverProjectCommand) {
const workspaces: JsonProject[] = await Promise.all(
vscode.workspace.workspaceFolders!.map(async (folder): Promise<JsonProject> => {
const rustDocuments = vscode.workspace.textDocuments.filter(isRustDocument);
return discoverWorkspace(rustDocuments, discoverProjectCommand, {
cwd: folder.uri.fsPath,
});
})
);
this.addToDiscoveredWorkspaces(workspaces);
}
const initializationOptions = prepareVSCodeConfig(
rawInitializationOptions,
(key, obj) => {
// we only want to set discovered workspaces on the right key
// and if a workspace has been discovered.
if (key === "linkedProjects" && this.config.discoveredWorkspaces.length > 0) {
obj["linkedProjects"] = this.config.discoveredWorkspaces;
}
}
);
this._client = await createClient(
this.traceOutputChannel,
@ -251,6 +293,17 @@ export class Ctx {
return this._serverPath;
}
addToDiscoveredWorkspaces(workspaces: JsonProject[]) {
for (const workspace of workspaces) {
const index = this.config.discoveredWorkspaces.indexOf(workspace);
if (~index) {
this.config.discoveredWorkspaces[index] = workspace;
} else {
this.config.discoveredWorkspaces.push(workspace);
}
}
}
private updateCommands(forceDisable?: "disable") {
this.commandDisposables.forEach((disposable) => disposable.dispose());
this.commandDisposables = [];
@ -289,6 +342,7 @@ export class Ctx {
statusBar.tooltip.appendText(status.message ?? "Ready");
statusBar.color = undefined;
statusBar.backgroundColor = undefined;
statusBar.command = "rust-analyzer.stopServer";
break;
case "warning":
if (status.message) {
@ -298,6 +352,7 @@ export class Ctx {
statusBar.backgroundColor = new vscode.ThemeColor(
"statusBarItem.warningBackground"
);
statusBar.command = "rust-analyzer.openLogs";
icon = "$(warning) ";
break;
case "error":
@ -306,6 +361,7 @@ export class Ctx {
}
statusBar.color = new vscode.ThemeColor("statusBarItem.errorForeground");
statusBar.backgroundColor = new vscode.ThemeColor("statusBarItem.errorBackground");
statusBar.command = "rust-analyzer.openLogs";
icon = "$(error) ";
break;
case "stopped":
@ -315,18 +371,19 @@ export class Ctx {
);
statusBar.color = undefined;
statusBar.backgroundColor = undefined;
statusBar.command = "rust-analyzer.startServer";
statusBar.text = `$(stop-circle) rust-analyzer`;
return;
}
if (statusBar.tooltip.value) {
statusBar.tooltip.appendText("\n\n");
}
statusBar.tooltip.appendMarkdown("[Stop server](command:rust-analyzer.stopServer)");
statusBar.tooltip.appendMarkdown(
"\n\n[Reload Workspace](command:rust-analyzer.reloadWorkspace)"
);
statusBar.tooltip.appendMarkdown("\n\n[Restart server](command:rust-analyzer.startServer)");
statusBar.tooltip.appendMarkdown("\n\n[Open logs](command:rust-analyzer.openLogs)");
statusBar.tooltip.appendMarkdown("\n\n[Restart server](command:rust-analyzer.startServer)");
statusBar.tooltip.appendMarkdown("[Stop server](command:rust-analyzer.stopServer)");
if (!status.quiescent) icon = "$(sync~spin) ";
statusBar.text = `${icon}rust-analyzer`;
}

View file

@ -43,6 +43,7 @@ export const relatedTests = new lc.RequestType<lc.TextDocumentPositionParams, Te
"rust-analyzer/relatedTests"
);
export const reloadWorkspace = new lc.RequestType0<null, void>("rust-analyzer/reloadWorkspace");
export const runFlycheck = new lc.NotificationType<{
textDocument: lc.TextDocumentIdentifier | null;
}>("rust-analyzer/runFlycheck");

View file

@ -153,6 +153,7 @@ function createCommands(): Record<string, CommandFactory> {
memoryUsage: { enabled: commands.memoryUsage },
shuffleCrateGraph: { enabled: commands.shuffleCrateGraph },
reloadWorkspace: { enabled: commands.reloadWorkspace },
addProject: { enabled: commands.addProject },
matchingBrace: { enabled: commands.matchingBrace },
joinLines: { enabled: commands.joinLines },
parentModule: { enabled: commands.parentModule },

View file

@ -0,0 +1,91 @@
interface JsonProject {
/// Path to the directory with *source code* of
/// sysroot crates.
///
/// It should point to the directory where std,
/// core, and friends can be found:
///
/// https://github.com/rust-lang/rust/tree/master/library.
///
/// If provided, rust-analyzer automatically adds
/// dependencies on sysroot crates. Conversely,
/// if you omit this path, you can specify sysroot
/// dependencies yourself and, for example, have
/// several different "sysroots" in one graph of
/// crates.
sysroot_src?: string;
/// The set of crates comprising the current
/// project. Must include all transitive
/// dependencies as well as sysroot crate (libstd,
/// libcore and such).
crates: Crate[];
}
interface Crate {
/// Optional crate name used for display purposes,
/// without affecting semantics. See the `deps`
/// key for semantically-significant crate names.
display_name?: string;
/// Path to the root module of the crate.
root_module: string;
/// Edition of the crate.
edition: "2015" | "2018" | "2021";
/// Dependencies
deps: Dep[];
/// Should this crate be treated as a member of
/// current "workspace".
///
/// By default, inferred from the `root_module`
/// (members are the crates which reside inside
/// the directory opened in the editor).
///
/// Set this to `false` for things like standard
/// library and 3rd party crates to enable
/// performance optimizations (rust-analyzer
/// assumes that non-member crates don't change).
is_workspace_member?: boolean;
/// Optionally specify the (super)set of `.rs`
/// files comprising this crate.
///
/// By default, rust-analyzer assumes that only
/// files under `root_module.parent` can belong
/// to a crate. `include_dirs` are included
/// recursively, unless a subdirectory is in
/// `exclude_dirs`.
///
/// Different crates can share the same `source`.
///
/// If two crates share an `.rs` file in common,
/// they *must* have the same `source`.
/// rust-analyzer assumes that files from one
/// source can't refer to files in another source.
source?: {
include_dirs: string[];
exclude_dirs: string[];
};
/// The set of cfgs activated for a given crate, like
/// `["unix", "feature=\"foo\"", "feature=\"bar\""]`.
cfg: string[];
/// Target triple for this Crate.
///
/// Used when running `rustc --print cfg`
/// to get target-specific cfgs.
target?: string;
/// Environment variables, used for
/// the `env!` macro
env: { [key: string]: string };
/// Whether the crate is a proc-macro crate.
is_proc_macro: boolean;
/// For proc-macro crates, path to compiled
/// proc-macro (.so file).
proc_macro_dylib_path?: string;
}
interface Dep {
/// Index of a crate in the `crates` array.
crate: number;
/// Name as should appear in the (implicit)
/// `extern crate name` declaration.
name: string;
}

View file

@ -150,9 +150,11 @@ export function memoizeAsync<Ret, TThis, Param extends string>(
/** Awaitable wrapper around `child_process.exec` */
export function execute(command: string, options: ExecOptions): Promise<string> {
log.info(`running command: ${command}`);
return new Promise((resolve, reject) => {
exec(command, options, (err, stdout, stderr) => {
if (err) {
log.error(err);
reject(err);
return;
}
@ -167,6 +169,21 @@ export function execute(command: string, options: ExecOptions): Promise<string>
});
}
export function executeDiscoverProject(command: string, options: ExecOptions): Promise<string> {
log.info(`running command: ${command}`);
return new Promise((resolve, reject) => {
exec(command, options, (err, stdout, _) => {
if (err) {
log.error(err);
reject(err);
return;
}
resolve(stdout.trimEnd());
});
});
}
export class LazyOutputChannel implements vscode.OutputChannel {
constructor(name: string) {
this.name = name;