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{ 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 = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = 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 { if (element instanceof Dependency) return undefined; return element.parent; } getTreeItem(element: Dependency | DependencyFile): vscode.TreeItem | Thenable { if (element.id! in this.dependenciesMap) return this.dependenciesMap[element.id!]; return element; } getChildren(element?: Dependency | DependencyFile): vscode.ProviderResult { 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 { 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(); this.dependenciesMap[stdlib.dependencyPath.toLowerCase()] = stdlib; return [stdlib].concat(deps); } private async getStdLib(): Promise { const toolchain = await activeToolchain(); const rustVersion = await getRustcVersion(os.homedir()); const stdlibPath = fspath.join(os.homedir(), '.rustup', 'toolchains', toolchain, 'lib', 'rustlib', 'src', 'rust', 'library'); const stdlib = new Dependency( "stdlib", rustVersion, stdlibPath, vscode.TreeItemCollapsibleState.Collapsed ); return stdlib; } private async getDepsInCartoTree(basePath: string): Promise { 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 };