diff --git a/editors/code/package.json b/editors/code/package.json index f36e34b6a1b..5e2a1c69e94 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -284,6 +284,14 @@ "command": "rust-analyzer.clearFlycheck", "title": "Clear flycheck diagnostics", "category": "rust-analyzer" + }, + { + "command": "rust-analyzer.openFile", + "title": "Open File" + }, + { + "command": "rust-analyzer.revealDependency", + "title": "Reveal File" } ], "keybindings": [ @@ -1956,6 +1964,14 @@ } ] }, + "views": { + "explorer": [ + { + "id": "rustDependencies", + "name": "Rust Dependencies" + } + ] + }, "jsonValidation": [ { "fileMatch": "rust-project.json", @@ -1963,4 +1979,4 @@ } ] } -} +} \ No newline at end of file diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index 2d5272d199d..e5aa06025b2 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -8,10 +8,11 @@ import { applySnippetWorkspaceEdit, applySnippetTextEdits } from "./snippets"; import { spawnSync } from "child_process"; import { RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run"; import { AstInspector } from "./ast_inspector"; -import { isRustDocument, isCargoTomlDocument, sleep, isRustEditor } from "./util"; +import { isRustDocument, isCargoTomlDocument, sleep, isRustEditor, RustEditor } from './util'; import { startDebugSession, makeDebugConfig } from "./debug"; import { LanguageClient } from "vscode-languageclient/node"; import { LINKED_COMMANDS } from "./client"; +import { DependencyId } from './dependencies_provider'; export * from "./ast_inspector"; export * from "./run"; @@ -266,6 +267,44 @@ export function openCargoToml(ctx: CtxInit): Cmd { }; } +export function openFile(_ctx: CtxInit): Cmd { + return async (uri: vscode.Uri) => { + try { + await vscode.window.showTextDocument(uri); + } catch (err) { + await vscode.window.showErrorMessage(err.message); + } + }; +} + +export function revealDependency(ctx: CtxInit): Cmd { + return async (editor: RustEditor) => { + const rootPath = vscode.workspace.workspaceFolders![0].uri.fsPath; + const documentPath = editor.document.uri.fsPath; + if (documentPath.startsWith(rootPath)) return; + const dep = ctx.dependencies.getDependency(documentPath); + if (dep) { + await ctx.treeView.reveal(dep, { select: true, expand: true }); + } else { + let documentPath = editor.document.uri.fsPath; + const parentChain: DependencyId[] = [{ id: documentPath.toLowerCase() }]; + do { + documentPath = path.dirname(documentPath); + parentChain.push({ id: documentPath.toLowerCase() }); + } + while (!ctx.dependencies.contains(documentPath)); + parentChain.reverse(); + for (const idx in parentChain) { + await ctx.treeView.reveal(parentChain[idx], { select: true, expand: true }); + } + } + }; +} + +export async function execRevealDependency(e: RustEditor): Promise<void> { + await vscode.commands.executeCommand('rust-analyzer.revealDependency', e); +} + export function ssr(ctx: CtxInit): Cmd { return async () => { const editor = vscode.window.activeTextEditor; diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 567b9216bc1..e6829ac4b9e 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -3,8 +3,8 @@ import * as lc from "vscode-languageclient/node"; import * as ra from "./lsp_ext"; import * as path from "path"; -import { Config, prepareVSCodeConfig } from "./config"; -import { createClient } from "./client"; +import {Config, prepareVSCodeConfig} from './config'; +import {createClient} from './client'; import { executeDiscoverProject, isRustDocument, @@ -12,11 +12,13 @@ import { LazyOutputChannel, log, RustEditor, -} from "./util"; -import { ServerStatusParams } from "./lsp_ext"; -import { PersistentState } from "./persistent_state"; -import { bootstrap } from "./bootstrap"; -import { ExecOptions } from "child_process"; +} from './util'; +import {ServerStatusParams} from './lsp_ext'; +import {Dependency, DependencyFile, RustDependenciesProvider, DependencyId} from './dependencies_provider'; +import {execRevealDependency} from './commands'; +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 @@ -25,12 +27,12 @@ import { ExecOptions } from "child_process"; 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( @@ -42,12 +44,12 @@ export function fetchWorkspace(): Workspace { return folders.length === 0 ? rustDocuments.length === 0 - ? { kind: "Empty" } + ? {kind: "Empty"} : { - kind: "Detached Files", - files: rustDocuments, - } - : { kind: "Workspace Folder" }; + kind: "Detached Files", + files: rustDocuments, + } + : {kind: "Workspace Folder"}; } export async function discoverWorkspace( @@ -84,6 +86,8 @@ export class Ctx { private commandFactories: Record<string, CommandFactory>; private commandDisposables: Disposable[]; private unlinkedFiles: vscode.Uri[]; + readonly dependencies: RustDependenciesProvider; + readonly treeView: vscode.TreeView<Dependency | DependencyFile | DependencyId>; get client() { return this._client; @@ -92,7 +96,9 @@ export class Ctx { constructor( readonly extCtx: vscode.ExtensionContext, commandFactories: Record<string, CommandFactory>, - workspace: Workspace + workspace: Workspace, + dependencies: RustDependenciesProvider, + treeView: vscode.TreeView<Dependency | DependencyFile | DependencyId> ) { extCtx.subscriptions.push(this); this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); @@ -101,6 +107,8 @@ export class Ctx { this.commandDisposables = []; this.commandFactories = commandFactories; this.unlinkedFiles = []; + this.dependencies = dependencies; + this.treeView = treeView; this.state = new PersistentState(extCtx.globalState); this.config = new Config(extCtx); @@ -109,6 +117,13 @@ export class Ctx { this.setServerStatus({ health: "stopped", }); + vscode.window.onDidChangeActiveTextEditor(e => { + if (e && isRustEditor(e)) { + execRevealDependency(e).catch(reason => { + void vscode.window.showErrorMessage(`Dependency error: ${reason}`); + }); + } + }); } dispose() { @@ -174,7 +189,7 @@ export class Ctx { const newEnv = Object.assign({}, process.env, this.config.serverExtraEnv); const run: lc.Executable = { command: this._serverPath, - options: { env: newEnv }, + options: {env: newEnv}, }; const serverOptions = { run, @@ -348,6 +363,7 @@ export class Ctx { statusBar.color = undefined; statusBar.backgroundColor = undefined; statusBar.command = "rust-analyzer.stopServer"; + this.dependencies.refresh(); break; case "warning": if (status.message) { @@ -410,4 +426,5 @@ export class Ctx { export interface Disposable { dispose(): void; } + export type Cmd = (...args: any[]) => unknown; diff --git a/editors/code/src/dependencies_provider.ts b/editors/code/src/dependencies_provider.ts new file mode 100644 index 00000000000..0f2e5e5ea07 --- /dev/null +++ b/editors/code/src/dependencies_provider.ts @@ -0,0 +1,151 @@ +import * as vscode from 'vscode'; +import * as fspath from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import { activeToolchain, Cargo, Crate, getRustcVersion } from './toolchain'; + +const debugOutput = vscode.window.createOutputChannel("Debug"); + +export class RustDependenciesProvider implements vscode.TreeDataProvider<Dependency | DependencyFile>{ + cargo: Cargo; + dependenciesMap: { [id: string]: Dependency | DependencyFile }; + + constructor( + private readonly workspaceRoot: string, + ) { + this.cargo = new Cargo(this.workspaceRoot || '.', debugOutput); + this.dependenciesMap = {}; + } + + private _onDidChangeTreeData: vscode.EventEmitter<Dependency | DependencyFile | undefined | null | void> = new vscode.EventEmitter<Dependency | undefined | null | void>(); + + readonly onDidChangeTreeData: vscode.Event<Dependency | DependencyFile | undefined | null | void> = this._onDidChangeTreeData.event; + + + getDependency(filePath: string): Dependency | DependencyFile | undefined { + return this.dependenciesMap[filePath.toLowerCase()]; + } + + contains(filePath: string): boolean { + return filePath.toLowerCase() in this.dependenciesMap; + } + + refresh(): void { + this._onDidChangeTreeData.fire(); + } + + getParent?(element: Dependency | DependencyFile): vscode.ProviderResult<Dependency | DependencyFile> { + if (element instanceof Dependency) return undefined; + return element.parent; + } + + getTreeItem(element: Dependency | DependencyFile): vscode.TreeItem | Thenable<vscode.TreeItem> { + if (element.id! in this.dependenciesMap) return this.dependenciesMap[element.id!]; + return element; + } + + getChildren(element?: Dependency | DependencyFile): vscode.ProviderResult<Dependency[] | DependencyFile[]> { + return new Promise((resolve, _reject) => { + if (!this.workspaceRoot) { + void vscode.window.showInformationMessage('No dependency in empty workspace'); + return Promise.resolve([]); + } + + if (element) { + const files = fs.readdirSync(element.dependencyPath).map(fileName => { + const filePath = fspath.join(element.dependencyPath, fileName); + const collapsibleState = fs.lstatSync(filePath).isDirectory() ? + vscode.TreeItemCollapsibleState.Collapsed : + vscode.TreeItemCollapsibleState.None; + const dep = new DependencyFile( + fileName, + filePath, + element, + collapsibleState + ); + this.dependenciesMap[dep.dependencyPath.toLowerCase()] = dep; + return dep; + }); + return resolve( + files + ); + } else { + return resolve(this.getRootDependencies()); + } + }); + } + + private async getRootDependencies(): Promise<Dependency[]> { + const registryDir = fspath.join(os.homedir(), '.cargo', 'registry', 'src'); + const basePath = fspath.join(registryDir, fs.readdirSync(registryDir)[0]); + const deps = await this.getDepsInCartoTree(basePath); + const stdlib = await this.getStdLib(); + return [stdlib].concat(deps); + } + + private async getStdLib(): Promise<Dependency> { + const toolchain = await activeToolchain(); + const rustVersion = await getRustcVersion(os.homedir()); + const stdlibPath = fspath.join(os.homedir(), '.rustup', 'toolchains', toolchain, 'lib', 'rustlib', 'src', 'rust', 'library'); + return new Dependency( + "stdlib", + rustVersion, + stdlibPath, + vscode.TreeItemCollapsibleState.Collapsed + ); + } + + private async getDepsInCartoTree(basePath: string): Promise<Dependency[]> { + const crates: Crate[] = await this.cargo.crates(); + const toDep = (moduleName: string, version: string): Dependency => { + const cratePath = fspath.join(basePath, `${moduleName}-${version}`); + return new Dependency( + moduleName, + version, + cratePath, + vscode.TreeItemCollapsibleState.Collapsed + ); + }; + + const deps = crates.map(crate => { + const dep = toDep(crate.name, crate.version); + this.dependenciesMap[dep.dependencyPath.toLowerCase()] = dep; + return dep; + }); + return deps; + } +} + + +export class Dependency extends vscode.TreeItem { + constructor( + public readonly label: string, + private version: string, + readonly dependencyPath: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState + ) { + super(label, collapsibleState); + this.tooltip = `${this.label}-${this.version}`; + this.description = this.version; + this.resourceUri = vscode.Uri.file(dependencyPath); + } +} + +export class DependencyFile extends vscode.TreeItem { + + constructor( + readonly label: string, + readonly dependencyPath: string, + readonly parent: Dependency | DependencyFile, + public readonly collapsibleState: vscode.TreeItemCollapsibleState + ) { + super(vscode.Uri.file(dependencyPath), collapsibleState); + const isDir = fs.lstatSync(this.dependencyPath).isDirectory(); + this.id = this.dependencyPath.toLowerCase(); + if (!isDir) { + this.command = { command: 'rust-analyzer.openFile', title: "Open File", arguments: [vscode.Uri.file(this.dependencyPath)], }; + } + } +} + +export type DependencyId = { id: string }; \ No newline at end of file diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 7ae8fa8ca28..62b2e7a2771 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -2,10 +2,10 @@ import * as vscode from "vscode"; import * as lc from "vscode-languageclient/node"; import * as commands from "./commands"; -import { CommandFactory, Ctx, fetchWorkspace } from "./ctx"; +import {CommandFactory, Ctx, fetchWorkspace} from "./ctx"; import * as diagnostics from "./diagnostics"; -import { activateTaskProvider } from "./tasks"; -import { setContextValue } from "./util"; +import {activateTaskProvider} from "./tasks"; +import {setContextValue} from "./util"; const RUST_PROJECT_CONTEXT_NAME = "inRustProject"; @@ -24,11 +24,12 @@ 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()); @@ -118,7 +119,7 @@ function createCommands(): Record<string, CommandFactory> { return { onEnter: { enabled: commands.onEnter, - disabled: (_) => () => vscode.commands.executeCommand("default:type", { text: "\n" }), + disabled: (_) => () => vscode.commands.executeCommand("default:type", {text: "\n"}), }, restartServer: { enabled: (ctx) => async () => { @@ -144,51 +145,54 @@ 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 }, - rebuildProcMacros: { enabled: commands.rebuildProcMacros }, - addProject: { enabled: commands.addProject }, - matchingBrace: { enabled: commands.matchingBrace }, - joinLines: { enabled: commands.joinLines }, - parentModule: { enabled: commands.parentModule }, - syntaxTree: { enabled: commands.syntaxTree }, - viewHir: { enabled: commands.viewHir }, - viewMir: { enabled: commands.viewMir }, + analyzerStatus: {enabled: commands.analyzerStatus}, + memoryUsage: {enabled: commands.memoryUsage}, + shuffleCrateGraph: {enabled: commands.shuffleCrateGraph}, + reloadWorkspace: {enabled: commands.reloadWorkspace}, + rebuildProcMacros: {enabled: commands.rebuildProcMacros}, + addProject: {enabled: commands.addProject}, + matchingBrace: {enabled: commands.matchingBrace}, + joinLines: {enabled: commands.joinLines}, + parentModule: {enabled: commands.parentModule}, + syntaxTree: {enabled: commands.syntaxTree}, + viewHir: {enabled: commands.viewHir}, + viewMir: {enabled: commands.viewMir}, interpretFunction: { enabled: commands.interpretFunction }, - viewFileText: { enabled: commands.viewFileText }, - viewItemTree: { enabled: commands.viewItemTree }, - viewCrateGraph: { enabled: commands.viewCrateGraph }, - viewFullCrateGraph: { enabled: commands.viewFullCrateGraph }, - expandMacro: { enabled: commands.expandMacro }, - run: { enabled: commands.run }, - copyRunCommandLine: { enabled: commands.copyRunCommandLine }, - debug: { enabled: commands.debug }, - newDebugConfig: { enabled: commands.newDebugConfig }, - openDocs: { enabled: commands.openDocs }, - openCargoToml: { enabled: commands.openCargoToml }, - peekTests: { enabled: commands.peekTests }, - moveItemUp: { enabled: commands.moveItemUp }, - moveItemDown: { enabled: commands.moveItemDown }, - cancelFlycheck: { enabled: commands.cancelFlycheck }, - clearFlycheck: { enabled: commands.clearFlycheck }, - runFlycheck: { enabled: commands.runFlycheck }, - ssr: { enabled: commands.ssr }, - serverVersion: { enabled: commands.serverVersion }, + viewFileText: {enabled: commands.viewFileText}, + viewItemTree: {enabled: commands.viewItemTree}, + viewCrateGraph: {enabled: commands.viewCrateGraph}, + viewFullCrateGraph: {enabled: commands.viewFullCrateGraph}, + expandMacro: {enabled: commands.expandMacro}, + run: {enabled: commands.run}, + copyRunCommandLine: {enabled: commands.copyRunCommandLine}, + debug: {enabled: commands.debug}, + newDebugConfig: {enabled: commands.newDebugConfig}, + openDocs: {enabled: commands.openDocs}, + openCargoToml: {enabled: commands.openCargoToml}, + peekTests: {enabled: commands.peekTests}, + moveItemUp: {enabled: commands.moveItemUp}, + moveItemDown: {enabled: commands.moveItemDown}, + cancelFlycheck: {enabled: commands.cancelFlycheck}, + clearFlycheck: {enabled: commands.clearFlycheck}, + runFlycheck: {enabled: commands.runFlycheck}, + ssr: {enabled: commands.ssr}, + serverVersion: {enabled: commands.serverVersion}, // Internal commands which are invoked by the server. - applyActionGroup: { enabled: commands.applyActionGroup }, - applySnippetWorkspaceEdit: { enabled: commands.applySnippetWorkspaceEditCommand }, - debugSingle: { enabled: commands.debugSingle }, - gotoLocation: { enabled: commands.gotoLocation }, - linkToCommand: { enabled: commands.linkToCommand }, - resolveCodeAction: { enabled: commands.resolveCodeAction }, - runSingle: { enabled: commands.runSingle }, - showReferences: { enabled: commands.showReferences }, - triggerParameterHints: { enabled: commands.triggerParameterHints }, - openLogs: { enabled: commands.openLogs }, + applyActionGroup: {enabled: commands.applyActionGroup}, + applySnippetWorkspaceEdit: {enabled: commands.applySnippetWorkspaceEditCommand}, + debugSingle: {enabled: commands.debugSingle}, + gotoLocation: {enabled: commands.gotoLocation}, + linkToCommand: {enabled: commands.linkToCommand}, + resolveCodeAction: {enabled: commands.resolveCodeAction}, + runSingle: {enabled: commands.runSingle}, + showReferences: {enabled: commands.showReferences}, + triggerParameterHints: {enabled: commands.triggerParameterHints}, + openLogs: {enabled: commands.openLogs}, + openFile: {enabled: commands.openFile}, + revealDependency: {enabled: commands.revealDependency} }; } diff --git a/editors/code/src/toolchain.ts b/editors/code/src/toolchain.ts index 917a1d6b099..6f37451edd2 100644 --- a/editors/code/src/toolchain.ts +++ b/editors/code/src/toolchain.ts @@ -5,6 +5,15 @@ import * as readline from "readline"; import * as vscode from "vscode"; import { execute, log, memoizeAsync } from "./util"; + +const TREE_LINE_PATTERN = new RegExp(/(.+)\sv(\d+\.\d+\.\d+)(?:\s\((.+)\))?/); +const TOOLCHAIN_PATTERN = new RegExp(/(.*)\s\(.*\)/); + +export interface Crate { + name: string; + version: string; +} + interface CompilationArtifact { fileName: string; name: string; @@ -96,6 +105,43 @@ export class Cargo { return artifacts[0].fileName; } + async crates(): Promise<Crate[]> { + const pathToCargo = await cargoPath(); + return await new Promise((resolve, reject) => { + const crates: Crate[] = []; + + const cargo = cp.spawn(pathToCargo, ['tree', '--prefix', 'none'], { + stdio: ['ignore', 'pipe', 'pipe'], + cwd: this.rootFolder + }); + const rl = readline.createInterface({ input: cargo.stdout }); + rl.on('line', line => { + const match = line.match(TREE_LINE_PATTERN); + if (match) { + const name = match[1]; + const version = match[2]; + const extraInfo = match[3]; + // ignore duplicates '(*)' and path dependencies + if (this.shouldIgnore(extraInfo)) { + return; + } + crates.push({ name, version }); + } + }); + cargo.on('exit', (exitCode, _) => { + if (exitCode === 0) + resolve(crates); + else + reject(new Error(`exit code: ${exitCode}.`)); + }); + + }); + } + + private shouldIgnore(extraInfo: string): boolean { + return extraInfo !== undefined && (extraInfo === '*' || path.isAbsolute(extraInfo)); + } + private async runCargo( cargoArgs: string[], onStdoutJson: (obj: any) => void, @@ -127,6 +173,58 @@ export class Cargo { } } +export async function activeToolchain(): Promise<string> { + const pathToRustup = await rustupPath(); + return await new Promise((resolve, reject) => { + const execution = cp.spawn(pathToRustup, ['show', 'active-toolchain'], { + stdio: ['ignore', 'pipe', 'pipe'], + cwd: os.homedir() + }); + const rl = readline.createInterface({ input: execution.stdout }); + + let currToolchain: string | undefined = undefined; + rl.on('line', line => { + const match = line.match(TOOLCHAIN_PATTERN); + if (match) { + currToolchain = match[1]; + } + }); + execution.on('exit', (exitCode, _) => { + if (exitCode === 0 && currToolchain) + resolve(currToolchain); + else + reject(new Error(`exit code: ${exitCode}.`)); + }); + + }); +} + +export async function rustVersion(): Promise<string> { + const pathToRustup = await rustupPath(); + return await new Promise((resolve, reject) => { + const execution = cp.spawn(pathToRustup, ['show', 'active-toolchain'], { + stdio: ['ignore', 'pipe', 'pipe'], + cwd: os.homedir() + }); + const rl = readline.createInterface({ input: execution.stdout }); + + let currToolchain: string | undefined = undefined; + rl.on('line', line => { + const match = line.match(TOOLCHAIN_PATTERN); + if (match) { + currToolchain = match[1]; + } + }); + execution.on('exit', (exitCode, _) => { + if (exitCode === 1 && currToolchain) + resolve(currToolchain); + else + reject(new Error(`exit code: ${exitCode}.`)); + }); + + }); +} + /** Mirrors `project_model::sysroot::discover_sysroot_dir()` implementation*/ export async function getSysroot(dir: string): Promise<string> { const rustcPath = await getPathForExecutable("rustc"); @@ -145,11 +243,26 @@ export async function getRustcId(dir: string): Promise<string> { return rx.exec(data)![1]; } +export async function getRustcVersion(dir: string): Promise<string> { + const rustcPath = await getPathForExecutable("rustc"); + + // do not memoize the result because the toolchain may change between runs + const data = await execute(`${rustcPath} -V`, { cwd: dir }); + const rx = /(\d\.\d+\.\d+)/; + + return rx.exec(data)![1]; +} + /** Mirrors `toolchain::cargo()` implementation */ export function cargoPath(): Promise<string> { return getPathForExecutable("cargo"); } +/** Mirrors `toolchain::cargo()` implementation */ +export function rustupPath(): Promise<string> { + return getPathForExecutable("rustup"); +} + /** Mirrors `toolchain::get_path_for_executable()` implementation */ export const getPathForExecutable = memoizeAsync( // We apply caching to decrease file-system interactions