Creating rust dependencies tree view
This commit is contained in:
parent
cffc402c05
commit
795a1cbe89
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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,8 +12,10 @@ import {
|
||||
LazyOutputChannel,
|
||||
log,
|
||||
RustEditor,
|
||||
} from "./util";
|
||||
import { ServerStatusParams } from "./lsp_ext";
|
||||
} 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";
|
||||
@ -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() {
|
||||
@ -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;
|
||||
|
151
editors/code/src/dependencies_provider.ts
Normal file
151
editors/code/src/dependencies_provider.ts
Normal file
@ -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 };
|
@ -28,7 +28,8 @@ export async function activate(
|
||||
"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());
|
||||
@ -144,7 +145,8 @@ function createCommands(): Record<string, CommandFactory> {
|
||||
health: "stopped",
|
||||
});
|
||||
},
|
||||
disabled: (_) => async () => {},
|
||||
disabled: (_) => async () => {
|
||||
},
|
||||
},
|
||||
|
||||
analyzerStatus: {enabled: commands.analyzerStatus},
|
||||
@ -190,5 +192,7 @@ function createCommands(): Record<string, CommandFactory> {
|
||||
showReferences: {enabled: commands.showReferences},
|
||||
triggerParameterHints: {enabled: commands.triggerParameterHints},
|
||||
openLogs: {enabled: commands.openLogs},
|
||||
openFile: {enabled: commands.openFile},
|
||||
revealDependency: {enabled: commands.revealDependency}
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user