diff options
| author | David Barsky <me@davidbarsky.com> | 2023-03-09 15:06:26 -0500 |
|---|---|---|
| committer | David Barsky <me@davidbarsky.com> | 2023-03-13 13:30:18 -0400 |
| commit | 8af3d6367ecead0abf80e697176f697d97c25215 (patch) | |
| tree | 54a28c70aadc17ddc93991fb4344a516ae6cba29 /editors/code | |
| parent | 95497533524537b1cc7a2870ce94b0b14503be8b (diff) | |
| download | rust-8af3d6367ecead0abf80e697176f697d97c25215.tar.gz rust-8af3d6367ecead0abf80e697176f697d97c25215.zip | |
This commit add Cargo-style project discovery for Buck and Bazel users.
This feature requires the user to add a command that generates a
`rust-project.json` from a set of files. Project discovery can be invoked
in two ways:
1. At extension activation time, which includes the generated
`rust-project.json` as part of the linkedProjects argument in
InitializeParams
2. Through a new command titled "Add current file to workspace", which
makes use of a new, rust-analyzer specific LSP request that adds
the workspace without erasing any existing workspaces.
I think that the command-running functionality _could_ merit being
placed into its own extension (and expose it via extension contribution
points), if only provide build-system idiomatic progress reporting and
status handling, but I haven't (yet) made an extension that does this.
Diffstat (limited to 'editors/code')
| -rw-r--r-- | editors/code/package.json | 18 | ||||
| -rw-r--r-- | editors/code/src/commands.ts | 19 | ||||
| -rw-r--r-- | editors/code/src/config.ts | 4 | ||||
| -rw-r--r-- | editors/code/src/ctx.ts | 42 | ||||
| -rw-r--r-- | editors/code/src/lsp_ext.ts | 6 | ||||
| -rw-r--r-- | editors/code/src/main.ts | 9 | ||||
| -rw-r--r-- | editors/code/src/rust_project.ts | 91 | ||||
| -rw-r--r-- | editors/code/src/util.ts | 17 |
8 files changed, 189 insertions, 17 deletions
diff --git a/editors/code/package.json b/editors/code/package.json index a3b1a3107d0..e79ab33726d 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -200,6 +200,11 @@ "category": "rust-analyzer" }, { + "command": "rust-analyzer.addProject", + "title": "Add current file to workspace", + "category": "rust-analyzer" + }, + { "command": "rust-analyzer.reload", "title": "Restart server", "category": "rust-analyzer" @@ -447,6 +452,17 @@ "Fill missing expressions with reasonable defaults, `new` or `default` constructors." ] }, + "rust-analyzer.discoverProjectCommand": { + "markdownDescription": "Sets the command that rust-analyzer uses to generate `rust-project.json` files. This command is\n only suggested if a build system like Buck or Bazel is used. The command must accept files as arguements and return \n a rust-project.json over stdout.", + "default": null, + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, "rust-analyzer.cachePriming.enable": { "markdownDescription": "Warm up caches on project load.", "default": true, @@ -1904,4 +1920,4 @@ } ] } -} +} \ No newline at end of file diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index f4a4579a92c..beff8501dc8 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -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,23 @@ 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; + } + + let workspaces: JsonProject[] = await Promise.all(vscode.workspace.workspaceFolders!.map(async (folder): Promise<JsonProject> => { + return discoverWorkspace(vscode.workspace.textDocuments, discoverProjectCommand, { cwd: folder.uri.fsPath }); + })); + + await ctx.client.sendRequest(ra.addProject, { + project: workspaces + }); + } +} + async function showReferencesImpl( client: LanguageClient | undefined, uri: string, diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 1faa0ad9106..f62843dffa6 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -214,6 +214,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"); } diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 1708d47cee7..ba2d4e97af1 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -4,10 +4,11 @@ import * as ra from "./lsp_ext"; import { Config, substituteVSCodeVariables } 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 @@ -16,12 +17,12 @@ import { bootstrap } from "./bootstrap"; export type Workspace = | { kind: "Empty" } | { - kind: "Workspace Folder"; - } + kind: "Workspace Folder"; + } | { - kind: "Detached Files"; - files: vscode.TextDocument[]; - }; + kind: "Detached Files"; + files: vscode.TextDocument[]; + }; export function fetchWorkspace(): Workspace { const folders = (vscode.workspace.workspaceFolders || []).filter( @@ -35,12 +36,19 @@ export function fetchWorkspace(): Workspace { ? rustDocuments.length === 0 ? { kind: "Empty" } : { - kind: "Detached Files", - files: rustDocuments, - } + kind: "Detached Files", + files: rustDocuments, + } : { 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; @@ -63,6 +71,7 @@ export class Ctx { private state: PersistentState; private commandFactories: Record<string, CommandFactory>; private commandDisposables: Disposable[]; + private discoveredWorkspaces: JsonProject[] | undefined; get client() { return this._client; @@ -71,7 +80,7 @@ export class Ctx { constructor( readonly extCtx: vscode.ExtensionContext, commandFactories: Record<string, CommandFactory>, - workspace: Workspace + workspace: Workspace, ) { extCtx.subscriptions.push(this); this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); @@ -169,7 +178,18 @@ export class Ctx { }; } - const initializationOptions = substituteVSCodeVariables(rawInitializationOptions); + const discoverProjectCommand = this.config.discoverProjectCommand; + if (discoverProjectCommand) { + let workspaces: JsonProject[] = await Promise.all(vscode.workspace.workspaceFolders!.map(async (folder): Promise<JsonProject> => { + return discoverWorkspace(vscode.workspace.textDocuments, discoverProjectCommand, { cwd: folder.uri.fsPath }); + })); + + this.discoveredWorkspaces = workspaces; + } + + let initializationOptions = substituteVSCodeVariables(rawInitializationOptions); + // this appears to be load-bearing, for better or worse. + await initializationOptions.update('linkedProjects', this.discoveredWorkspaces) this._client = await createClient( this.traceOutputChannel, diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts index 400cd207d41..6c8428aa972 100644 --- a/editors/code/src/lsp_ext.ts +++ b/editors/code/src/lsp_ext.ts @@ -43,6 +43,10 @@ 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 addProject = new lc.RequestType<AddProjectParams, string, void>( + "rust-analyzer/addProject" +) + export const runFlycheck = new lc.NotificationType<{ textDocument: lc.TextDocumentIdentifier | null; }>("rust-analyzer/runFlycheck"); @@ -68,6 +72,8 @@ export const viewItemTree = new lc.RequestType<ViewItemTreeParams, string, void> export type AnalyzerStatusParams = { textDocument?: lc.TextDocumentIdentifier }; +export type AddProjectParams = { project: JsonProject[] }; + export type ExpandMacroParams = { textDocument: lc.TextDocumentIdentifier; position: lc.Position; diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 8a2412af849..323aa89ef06 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -24,11 +24,11 @@ export async function activate( vscode.window .showWarningMessage( `You have both the rust-analyzer (rust-lang.rust-analyzer) and Rust (rust-lang.rust) ` + - "plugins enabled. These are known to conflict and cause various functions of " + - "both plugins to not work correctly. You should disable one of them.", + "plugins enabled. These are known to conflict and cause various functions of " + + "both plugins to not work correctly. You should disable one of them.", "Got it" ) - .then(() => {}, console.error); + .then(() => { }, console.error); } const ctx = new Ctx(context, createCommands(), fetchWorkspace()); @@ -146,13 +146,14 @@ function createCommands(): Record<string, CommandFactory> { health: "stopped", }); }, - disabled: (_) => async () => {}, + disabled: (_) => async () => { }, }, analyzerStatus: { enabled: commands.analyzerStatus }, 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 }, diff --git a/editors/code/src/rust_project.ts b/editors/code/src/rust_project.ts new file mode 100644 index 00000000000..adf0f89c961 --- /dev/null +++ b/editors/code/src/rust_project.ts @@ -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, +} \ No newline at end of file diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index d93b9caeb16..d2ecdce5b4e 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts @@ -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; |
