Skip to content

Commit 795a1cb

Browse files
committed
Creating rust dependencies tree view
1 parent cffc402 commit 795a1cb

File tree

6 files changed

+410
-70
lines changed

6 files changed

+410
-70
lines changed

editors/code/package.json

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,14 @@
284284
"command": "rust-analyzer.clearFlycheck",
285285
"title": "Clear flycheck diagnostics",
286286
"category": "rust-analyzer"
287+
},
288+
{
289+
"command": "rust-analyzer.openFile",
290+
"title": "Open File"
291+
},
292+
{
293+
"command": "rust-analyzer.revealDependency",
294+
"title": "Reveal File"
287295
}
288296
],
289297
"keybindings": [
@@ -1956,11 +1964,19 @@
19561964
}
19571965
]
19581966
},
1967+
"views": {
1968+
"explorer": [
1969+
{
1970+
"id": "rustDependencies",
1971+
"name": "Rust Dependencies"
1972+
}
1973+
]
1974+
},
19591975
"jsonValidation": [
19601976
{
19611977
"fileMatch": "rust-project.json",
19621978
"url": "https://json.schemastore.org/rust-project.json"
19631979
}
19641980
]
19651981
}
1966-
}
1982+
}

editors/code/src/commands.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import { applySnippetWorkspaceEdit, applySnippetTextEdits } from "./snippets";
88
import { spawnSync } from "child_process";
99
import { RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run";
1010
import { AstInspector } from "./ast_inspector";
11-
import { isRustDocument, isCargoTomlDocument, sleep, isRustEditor } from "./util";
11+
import { isRustDocument, isCargoTomlDocument, sleep, isRustEditor, RustEditor } from './util';
1212
import { startDebugSession, makeDebugConfig } from "./debug";
1313
import { LanguageClient } from "vscode-languageclient/node";
1414
import { LINKED_COMMANDS } from "./client";
15+
import { DependencyId } from './dependencies_provider';
1516

1617
export * from "./ast_inspector";
1718
export * from "./run";
@@ -266,6 +267,44 @@ export function openCargoToml(ctx: CtxInit): Cmd {
266267
};
267268
}
268269

270+
export function openFile(_ctx: CtxInit): Cmd {
271+
return async (uri: vscode.Uri) => {
272+
try {
273+
await vscode.window.showTextDocument(uri);
274+
} catch (err) {
275+
await vscode.window.showErrorMessage(err.message);
276+
}
277+
};
278+
}
279+
280+
export function revealDependency(ctx: CtxInit): Cmd {
281+
return async (editor: RustEditor) => {
282+
const rootPath = vscode.workspace.workspaceFolders![0].uri.fsPath;
283+
const documentPath = editor.document.uri.fsPath;
284+
if (documentPath.startsWith(rootPath)) return;
285+
const dep = ctx.dependencies.getDependency(documentPath);
286+
if (dep) {
287+
await ctx.treeView.reveal(dep, { select: true, expand: true });
288+
} else {
289+
let documentPath = editor.document.uri.fsPath;
290+
const parentChain: DependencyId[] = [{ id: documentPath.toLowerCase() }];
291+
do {
292+
documentPath = path.dirname(documentPath);
293+
parentChain.push({ id: documentPath.toLowerCase() });
294+
}
295+
while (!ctx.dependencies.contains(documentPath));
296+
parentChain.reverse();
297+
for (const idx in parentChain) {
298+
await ctx.treeView.reveal(parentChain[idx], { select: true, expand: true });
299+
}
300+
}
301+
};
302+
}
303+
304+
export async function execRevealDependency(e: RustEditor): Promise<void> {
305+
await vscode.commands.executeCommand('rust-analyzer.revealDependency', e);
306+
}
307+
269308
export function ssr(ctx: CtxInit): Cmd {
270309
return async () => {
271310
const editor = vscode.window.activeTextEditor;

editors/code/src/ctx.ts

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,22 @@ import * as lc from "vscode-languageclient/node";
33
import * as ra from "./lsp_ext";
44
import * as path from "path";
55

6-
import { Config, prepareVSCodeConfig } from "./config";
7-
import { createClient } from "./client";
6+
import {Config, prepareVSCodeConfig} from './config';
7+
import {createClient} from './client';
88
import {
99
executeDiscoverProject,
1010
isRustDocument,
1111
isRustEditor,
1212
LazyOutputChannel,
1313
log,
1414
RustEditor,
15-
} from "./util";
16-
import { ServerStatusParams } from "./lsp_ext";
17-
import { PersistentState } from "./persistent_state";
18-
import { bootstrap } from "./bootstrap";
19-
import { ExecOptions } from "child_process";
15+
} from './util';
16+
import {ServerStatusParams} from './lsp_ext';
17+
import {Dependency, DependencyFile, RustDependenciesProvider, DependencyId} from './dependencies_provider';
18+
import {execRevealDependency} from './commands';
19+
import {PersistentState} from "./persistent_state";
20+
import {bootstrap} from "./bootstrap";
21+
import {ExecOptions} from "child_process";
2022

2123
// We only support local folders, not eg. Live Share (`vlsl:` scheme), so don't activate if
2224
// only those are in use. We use "Empty" to represent these scenarios
@@ -25,12 +27,12 @@ import { ExecOptions } from "child_process";
2527
export type Workspace =
2628
| { kind: "Empty" }
2729
| {
28-
kind: "Workspace Folder";
29-
}
30+
kind: "Workspace Folder";
31+
}
3032
| {
31-
kind: "Detached Files";
32-
files: vscode.TextDocument[];
33-
};
33+
kind: "Detached Files";
34+
files: vscode.TextDocument[];
35+
};
3436

3537
export function fetchWorkspace(): Workspace {
3638
const folders = (vscode.workspace.workspaceFolders || []).filter(
@@ -42,12 +44,12 @@ export function fetchWorkspace(): Workspace {
4244

4345
return folders.length === 0
4446
? rustDocuments.length === 0
45-
? { kind: "Empty" }
47+
? {kind: "Empty"}
4648
: {
47-
kind: "Detached Files",
48-
files: rustDocuments,
49-
}
50-
: { kind: "Workspace Folder" };
49+
kind: "Detached Files",
50+
files: rustDocuments,
51+
}
52+
: {kind: "Workspace Folder"};
5153
}
5254

5355
export async function discoverWorkspace(
@@ -84,6 +86,8 @@ export class Ctx {
8486
private commandFactories: Record<string, CommandFactory>;
8587
private commandDisposables: Disposable[];
8688
private unlinkedFiles: vscode.Uri[];
89+
readonly dependencies: RustDependenciesProvider;
90+
readonly treeView: vscode.TreeView<Dependency | DependencyFile | DependencyId>;
8791

8892
get client() {
8993
return this._client;
@@ -92,7 +96,9 @@ export class Ctx {
9296
constructor(
9397
readonly extCtx: vscode.ExtensionContext,
9498
commandFactories: Record<string, CommandFactory>,
95-
workspace: Workspace
99+
workspace: Workspace,
100+
dependencies: RustDependenciesProvider,
101+
treeView: vscode.TreeView<Dependency | DependencyFile | DependencyId>
96102
) {
97103
extCtx.subscriptions.push(this);
98104
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
@@ -101,6 +107,8 @@ export class Ctx {
101107
this.commandDisposables = [];
102108
this.commandFactories = commandFactories;
103109
this.unlinkedFiles = [];
110+
this.dependencies = dependencies;
111+
this.treeView = treeView;
104112

105113
this.state = new PersistentState(extCtx.globalState);
106114
this.config = new Config(extCtx);
@@ -109,6 +117,13 @@ export class Ctx {
109117
this.setServerStatus({
110118
health: "stopped",
111119
});
120+
vscode.window.onDidChangeActiveTextEditor(e => {
121+
if (e && isRustEditor(e)) {
122+
execRevealDependency(e).catch(reason => {
123+
void vscode.window.showErrorMessage(`Dependency error: ${reason}`);
124+
});
125+
}
126+
});
112127
}
113128

114129
dispose() {
@@ -174,7 +189,7 @@ export class Ctx {
174189
const newEnv = Object.assign({}, process.env, this.config.serverExtraEnv);
175190
const run: lc.Executable = {
176191
command: this._serverPath,
177-
options: { env: newEnv },
192+
options: {env: newEnv},
178193
};
179194
const serverOptions = {
180195
run,
@@ -348,6 +363,7 @@ export class Ctx {
348363
statusBar.color = undefined;
349364
statusBar.backgroundColor = undefined;
350365
statusBar.command = "rust-analyzer.stopServer";
366+
this.dependencies.refresh();
351367
break;
352368
case "warning":
353369
if (status.message) {
@@ -410,4 +426,5 @@ export class Ctx {
410426
export interface Disposable {
411427
dispose(): void;
412428
}
429+
413430
export type Cmd = (...args: any[]) => unknown;
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import * as vscode from 'vscode';
2+
import * as fspath from 'path';
3+
import * as fs from 'fs';
4+
import * as os from 'os';
5+
import { activeToolchain, Cargo, Crate, getRustcVersion } from './toolchain';
6+
7+
const debugOutput = vscode.window.createOutputChannel("Debug");
8+
9+
export class RustDependenciesProvider implements vscode.TreeDataProvider<Dependency | DependencyFile>{
10+
cargo: Cargo;
11+
dependenciesMap: { [id: string]: Dependency | DependencyFile };
12+
13+
constructor(
14+
private readonly workspaceRoot: string,
15+
) {
16+
this.cargo = new Cargo(this.workspaceRoot || '.', debugOutput);
17+
this.dependenciesMap = {};
18+
}
19+
20+
private _onDidChangeTreeData: vscode.EventEmitter<Dependency | DependencyFile | undefined | null | void> = new vscode.EventEmitter<Dependency | undefined | null | void>();
21+
22+
readonly onDidChangeTreeData: vscode.Event<Dependency | DependencyFile | undefined | null | void> = this._onDidChangeTreeData.event;
23+
24+
25+
getDependency(filePath: string): Dependency | DependencyFile | undefined {
26+
return this.dependenciesMap[filePath.toLowerCase()];
27+
}
28+
29+
contains(filePath: string): boolean {
30+
return filePath.toLowerCase() in this.dependenciesMap;
31+
}
32+
33+
refresh(): void {
34+
this._onDidChangeTreeData.fire();
35+
}
36+
37+
getParent?(element: Dependency | DependencyFile): vscode.ProviderResult<Dependency | DependencyFile> {
38+
if (element instanceof Dependency) return undefined;
39+
return element.parent;
40+
}
41+
42+
getTreeItem(element: Dependency | DependencyFile): vscode.TreeItem | Thenable<vscode.TreeItem> {
43+
if (element.id! in this.dependenciesMap) return this.dependenciesMap[element.id!];
44+
return element;
45+
}
46+
47+
getChildren(element?: Dependency | DependencyFile): vscode.ProviderResult<Dependency[] | DependencyFile[]> {
48+
return new Promise((resolve, _reject) => {
49+
if (!this.workspaceRoot) {
50+
void vscode.window.showInformationMessage('No dependency in empty workspace');
51+
return Promise.resolve([]);
52+
}
53+
54+
if (element) {
55+
const files = fs.readdirSync(element.dependencyPath).map(fileName => {
56+
const filePath = fspath.join(element.dependencyPath, fileName);
57+
const collapsibleState = fs.lstatSync(filePath).isDirectory() ?
58+
vscode.TreeItemCollapsibleState.Collapsed :
59+
vscode.TreeItemCollapsibleState.None;
60+
const dep = new DependencyFile(
61+
fileName,
62+
filePath,
63+
element,
64+
collapsibleState
65+
);
66+
this.dependenciesMap[dep.dependencyPath.toLowerCase()] = dep;
67+
return dep;
68+
});
69+
return resolve(
70+
files
71+
);
72+
} else {
73+
return resolve(this.getRootDependencies());
74+
}
75+
});
76+
}
77+
78+
private async getRootDependencies(): Promise<Dependency[]> {
79+
const registryDir = fspath.join(os.homedir(), '.cargo', 'registry', 'src');
80+
const basePath = fspath.join(registryDir, fs.readdirSync(registryDir)[0]);
81+
const deps = await this.getDepsInCartoTree(basePath);
82+
const stdlib = await this.getStdLib();
83+
return [stdlib].concat(deps);
84+
}
85+
86+
private async getStdLib(): Promise<Dependency> {
87+
const toolchain = await activeToolchain();
88+
const rustVersion = await getRustcVersion(os.homedir());
89+
const stdlibPath = fspath.join(os.homedir(), '.rustup', 'toolchains', toolchain, 'lib', 'rustlib', 'src', 'rust', 'library');
90+
return new Dependency(
91+
"stdlib",
92+
rustVersion,
93+
stdlibPath,
94+
vscode.TreeItemCollapsibleState.Collapsed
95+
);
96+
}
97+
98+
private async getDepsInCartoTree(basePath: string): Promise<Dependency[]> {
99+
const crates: Crate[] = await this.cargo.crates();
100+
const toDep = (moduleName: string, version: string): Dependency => {
101+
const cratePath = fspath.join(basePath, `${moduleName}-${version}`);
102+
return new Dependency(
103+
moduleName,
104+
version,
105+
cratePath,
106+
vscode.TreeItemCollapsibleState.Collapsed
107+
);
108+
};
109+
110+
const deps = crates.map(crate => {
111+
const dep = toDep(crate.name, crate.version);
112+
this.dependenciesMap[dep.dependencyPath.toLowerCase()] = dep;
113+
return dep;
114+
});
115+
return deps;
116+
}
117+
}
118+
119+
120+
export class Dependency extends vscode.TreeItem {
121+
constructor(
122+
public readonly label: string,
123+
private version: string,
124+
readonly dependencyPath: string,
125+
public readonly collapsibleState: vscode.TreeItemCollapsibleState
126+
) {
127+
super(label, collapsibleState);
128+
this.tooltip = `${this.label}-${this.version}`;
129+
this.description = this.version;
130+
this.resourceUri = vscode.Uri.file(dependencyPath);
131+
}
132+
}
133+
134+
export class DependencyFile extends vscode.TreeItem {
135+
136+
constructor(
137+
readonly label: string,
138+
readonly dependencyPath: string,
139+
readonly parent: Dependency | DependencyFile,
140+
public readonly collapsibleState: vscode.TreeItemCollapsibleState
141+
) {
142+
super(vscode.Uri.file(dependencyPath), collapsibleState);
143+
const isDir = fs.lstatSync(this.dependencyPath).isDirectory();
144+
this.id = this.dependencyPath.toLowerCase();
145+
if (!isDir) {
146+
this.command = { command: 'rust-analyzer.openFile', title: "Open File", arguments: [vscode.Uri.file(this.dependencyPath)], };
147+
}
148+
}
149+
}
150+
151+
export type DependencyId = { id: string };

0 commit comments

Comments
 (0)