-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Creating rust dependencies tree explorer #11557
Conversation
700af65
to
01b0fd8
Compare
Hey guys! I don't want to be a annoying or anything like that, but as this is my first PR here, do i need to do something else for the PR to be reviewed? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know my way around the client code too much, and I don't think any of the active people do either but overall this seems good to me.
Though I have two questions, how often does this invoke cargo (as in when) and we should ideally not have this code show error popups I think, if this code errors but the rest of r-a works fine I wouldn't personally want to bother users with it. So ideally errors should only be logged, and preferably in a way that is not too spammy.
editors/code/src/ctx.ts
Outdated
vscode.window.onDidChangeActiveTextEditor(e => { | ||
if (e && isRustEditor(e)) { | ||
execRevealDependency(e).catch(reason => { | ||
void vscode.window.showErrorMessage(`Dependency error: ${reason}`); | ||
}); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just asking for my own understanding as I am not familiar with VSCodes api, but this is executed whenever the user switches the active editor to a rust file right? Why do we have to do this every time again, and secondly, if we do this on every change and an error does occur, the way this is written looks to me like we will spam the user with error messages whenever they switch their editor tab which sounds really unpleasant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Veykril Thanks for the review, i'll try to answer your questions:
1- It calls cargo tree is invoked every time the editor opens and after that for every "ok" that it receives from the server: The code is this:
switch (status.health) {
case "ok":
this.statusBar.color = undefined;
this.dependencies.refresh(); // it tells the editor that something changed in the server and it should update the dependency tree
break;
case "warning":
this.statusBar.tooltip += "\nClick to reload.";
....
2- I agree that we should not show the popup errors. I can change that, what are the best practices to logging in the editor?
3- This is executed every time, so the dependency tree can "follow" the user. So every time the user changes the active editor, the dependency tree "follows" the user and reveals the file in it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server has some (limited?) support for detecting when the dependencies (Cargo.lock or rust-project.json) have actually changed, it would be nice to hook into that instead of running cargo tree every time the active editor changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lnicola it does not run cargo tree every time the editor changes, it runs when it receives a "status change" notification from the server. It checks that the status is "ok" and tries to reload the dependencies.
But as pointed in the other comments ill try to look into the dependency graph feature.
Instead of using |
@bjorn3 Can you please send me a reference or any example on how i can use the dependency graph? And is this blocking, can we make this change afterwards? Thanks for the comments |
editors/code/src/commands.ts
Outdated
|
||
export function revealDependency(ctx: Ctx): Cmd { | ||
return async (editor: RustEditor) => { | ||
const rootPath = vscode.workspace.workspaceFolders![0].uri.fsPath; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this work for Code workspaces? We've historically had a lot of trouble with them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too familiar with VSCode API, i'll look into that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this won't really work for workspace folders—it'll only get this data for the first workspace, but you wouldn't be able to see dependencies from other workspaces.
return [stdlib].concat(deps); | ||
} | ||
|
||
private async getStdLib(): Promise<Dependency> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This duplicates the logic in the server. I think we should do as much as possible there instead of in the client code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, can you please point me out on where can i find this logic in the server? ty
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's https://github.com/rust-analyzer/rust-analyzer/blob/master/crates/ide/src/view_crate_graph.rs for "View crate graph" and https://github.com/rust-analyzer/rust-analyzer/blob/master/crates/project_model/src/workspace.rs#L140 takes care of loading a workspace. The sysroot is detected by Sysroot::load
via Sysroot::discover
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This duplicates the logic in the server. I think we should do as much as possible there instead of in the client code.
I'm new here, but I disagree. The server-side implementation takes some of the burden off of the developers of non-VSCode clients.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nemethf are you arguing for implementing it on the server, or client-side? I said I think it should be in the server, but maybe it wasn't very clear with all those prepositions and stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I as well think it should be on the server side. I'm sorry if I misunderstood something or if I wasn't clear. Thanks.
editors/code/src/ctx.ts
Outdated
vscode.window.onDidChangeActiveTextEditor(e => { | ||
if (e && isRustEditor(e)) { | ||
execRevealDependency(e).catch(reason => { | ||
void vscode.window.showErrorMessage(`Dependency error: ${reason}`); | ||
}); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server has some (limited?) support for detecting when the dependencies (Cargo.lock or rust-project.json) have actually changed, it would be nice to hook into that instead of running cargo tree every time the active editor changes.
We will probably need an lsp extension to provide the dependency graph from the language server to the extension. @Veykril maybe you can explain how to do this? |
There is a "view dependency graph" command which could serve as inspiration. |
Ye, the server notifying the client here will have to be done as an lsp extension, a good inspiration for that would probably be the ServerStatus notification, as well as the view dependency graph as noted earlier. |
☔ The latest upstream changes (presumably #12294) made this pull request unmergeable. Please resolve the merge conflicts. |
01b0fd8
to
c6d3337
Compare
Went ahead and rebased this, this still needs to use the server for querying, I added a small commit implementing the scaffolding part for the lsp extension for now. |
☔ The latest upstream changes (presumably #12919) made this pull request unmergeable. Please resolve the merge conflicts. |
@Veykril sorry for the long delay, I got swamped at work. I implemented the server query, feel free to review the code again.. |
crates/ide/src/fetch_crates.rs
Outdated
fn crate_path(db: &RootDatabase, data: &ide_db::base_db::CrateData, crate_name: &str) -> String { | ||
let source_root_id = db.file_source_root(data.root_file_id); | ||
let source_root = db.source_root(source_root_id); | ||
let source_root_path = source_root.path_for_file(&data.root_file_id); | ||
match source_root_path.cloned() { | ||
Some(mut root_path) => { | ||
let mut crate_path = "".to_string(); | ||
while let Some(vfs_path) = root_path.parent() { | ||
match vfs_path.name_and_extension() { | ||
Some((name, _)) => { | ||
if name.starts_with(crate_name) { | ||
crate_path = vfs_path.to_string(); | ||
break; | ||
} | ||
} | ||
None => break, | ||
} | ||
root_path = vfs_path; | ||
} | ||
crate_path | ||
} | ||
None => "".to_owned(), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel that this function is SUPER hacky, but I couldn't figure out another way around it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that your instinct that this is a bit hacky is correct, but I'm struggling to fully understand this: are you trying to convert a URI to a path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidbarsky this function tries to find a crate root dir in the filesystem, through the CrateGraph I can get the root file of a crate, for example, tokio's root file is /home/<user>/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.27.0/src/lib.rs
. But I don't need the root file I need the root dir of a crate, so I have to convert this file path to /home/<user>/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.27.0
.
If there is a cleaner way of doing it, I'll gladly change the implementation.
crates: { | ||
name: string; | ||
version: string; | ||
path: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't it be more general (and more in line with the language server protocol) to use textDocument: TextDocumentIdentifier
here instead of the current path: string
?
And as a side note: why this is called a graph/tree, when this is just a list? It is not possible to construct a graph from the response. Ie.: visualize the the dependencies of a dependency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nemethf you're right about the naming, I changed from "graph" to "list". The tree is because the crates are displayed in a "ViewTree".
About the TextDocumentIdentifier
, I tried using it, but it just made the code more complicated, because to reveal the items in the viewTree I need to work with the filesystem path, and the TextDocumentIdentifier
inputs the "scheme" in the path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About the TextDocumentIdentifier , I tried using it, but it just made the code more complicated, because to reveal the items in the viewTree I need to work with the filesystem path, and the TextDocumentIdentifier inputs the "scheme" in the path.
Looking over vscode.TreeItem
, it seems like it requires you to operate in terms of URIs anyways, so I think it might clean up some of the code here if everything here operated in terms of URIs—the scheme can be dropped accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tree is because the crates are displayed in a "ViewTree".
Thank you. I have no objections against it. However, it is a bit strange that a client-side implementation detail determines an abstract name in protocol specification. I can imagine some clients using this extension for something else, for example, to provide a "jump to dependency" functionality that implements an incremental search on the dependencies without showing them beforehand.
Also this fetchDependencyGraph reminds me of the rust-analyzer/viewCrateGraph LSP extension. In the long run, it might make sense to generalize both of them to send the same details. fetchDependencyGraph could list the dependencies of a dependency, and viewCrateGraph could be extended to have a uri/path for each dependency.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A TextDocumentIdentifier
would indeed make more sense if possible
editors/code/src/commands.ts
Outdated
|
||
export function revealDependency(ctx: Ctx): Cmd { | ||
return async (editor: RustEditor) => { | ||
const rootPath = vscode.workspace.workspaceFolders![0].uri.fsPath; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this won't really work for workspace folders—it'll only get this data for the first workspace, but you wouldn't be able to see dependencies from other workspaces.
editors/code/src/commands.ts
Outdated
return async (editor: RustEditor) => { | ||
const rootPath = vscode.workspace.workspaceFolders![0].uri.fsPath; | ||
const documentPath = editor.document.uri.fsPath; | ||
if (documentPath.startsWith(rootPath)) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not entirely sure why you'd want to do this. There's already non-workspace filtering done on the server; this just adds an additional check.
(This also breaks for monorepos where users open their editor at the monorepo root. Non-workspace crates are often stored in third-party/
but share the workspace root.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The purpose of the function revealDependency is to make the TreeView "reveal" the opened file in the editor.
Gotcha, thanks for explaining—I've built the branch locally and usage made a bit more sense.
But if I where to open a file that belongs to the local project, I dont want to trigger any "revealing"
That makes sense and works for most default Cargo-based builds. However, using if (documentPath.startsWith(rootPath)) return;
in situations where cargo vendor
has been run or when a user opens their VS Code instance at the root of the monorepo (where a third-party/
folder exists and contains vendored Rust crates), this check will prevent this tree-based explorer from ever opening and it seems unfortunate that those users won't benefit from this feature1. I don't have merge permissions on rust-analyzer, but I'd recommend removing this check, as fetch_crates
in crates/ide/src/fetch_crates.rs
already performs a similar check. I need to look over this PR a bit more, but if a user asks to reveal the dependency for a crate that is part of their editable workspace, then I would consider showing it. Alternatively, it might make sense to add another LSP extension to query the server itself to check whether a file is editable/part of the current editable workspace or not.
Footnotes
-
Personally, I'd love to build atop of this UI in order to allow users to incrementally add crates to their (editable) rust-analyzer workspace—it's a pretty common workflow in monorepos. ↩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@davidbarsky thanks for the reply, I just pushed a commit improving some pain points and hopefully giving it proper support to multiple workspace folders.
Could you point me at any github repos that make use of the features that you mentioned? I would love to test this cases because I suspect that they won't be a problem, because if they're already part of the project tree, the main treeView of vscode will already display those dependencies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you point me at any github repos that make use of the features that you mentioned?
@bruno-ortiz I can't think of any project off-hand, but it would be possible to take, e.g., the rust-analyzer repository and run cargo vendor
and you'll see a subset of the behavior I'm mentioning.
I would love to test this cases because I suspect that they won't be a problem, because if they're already part of the project tree, the main treeView of vscode will already display those dependencies.
At the risk of being that person, but a smaller repository won't really demonstrate this issue—it only really becomes apparent once a monorepo has tens (or hundreds!) of millions of lines of code. While said dependencies are technically part of the primary explorer view, it's often infeasible to see them at a glance because of the sheer number of folders present in the default file explorer.
My mental model of this feature—at least in monorepos—is that it is a subset of the monorepo (think of it as a materialized view...) that corresponds to the portions of monorepo most relevant to the code being worked on at the moment.
The reason that I'm so adamant about requesting this is that I didn't really get the need/desire for it until I tried it. While it is technically possible to navigate through the primary explorer window to dependencies, it ends up taking closer to 30-60 seconds per dependency rather than something instantaneous.
crates: { | ||
name: string; | ||
version: string; | ||
path: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About the TextDocumentIdentifier , I tried using it, but it just made the code more complicated, because to reveal the items in the viewTree I need to work with the filesystem path, and the TextDocumentIdentifier inputs the "scheme" in the path.
Looking over vscode.TreeItem
, it seems like it requires you to operate in terms of URIs anyways, so I think it might clean up some of the code here if everything here operated in terms of URIs—the scheme can be dropped accordingly.
editors/code/src/commands.ts
Outdated
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 }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you mind adding a comment explaining why this bit is necessary? is it to show a dependency of a dependency?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add it.
Its because the TreeView is Lazy, so at first it only has the root dependencies: Example:
- core
- alloc
- std
if I want to reveal alloc/src/str.rs, I have to:
- reveal every children of alloc
- core
- alloc
|-beches
|-src
|- ... - std
- reveal every children of src:
- core
- alloc
|-beches
|-src
| - lib.rs
| - str.rs <------- FOUND IT!
| - ...
|- ... - std
So basically, i have to calculate the parents of the file that I want to reveal until i find the root crate path.
crates/ide/src/fetch_crates.rs
Outdated
fn crate_path(db: &RootDatabase, data: &ide_db::base_db::CrateData, crate_name: &str) -> String { | ||
let source_root_id = db.file_source_root(data.root_file_id); | ||
let source_root = db.source_root(source_root_id); | ||
let source_root_path = source_root.path_for_file(&data.root_file_id); | ||
match source_root_path.cloned() { | ||
Some(mut root_path) => { | ||
let mut crate_path = "".to_string(); | ||
while let Some(vfs_path) = root_path.parent() { | ||
match vfs_path.name_and_extension() { | ||
Some((name, _)) => { | ||
if name.starts_with(crate_name) { | ||
crate_path = vfs_path.to_string(); | ||
break; | ||
} | ||
} | ||
None => break, | ||
} | ||
root_path = vfs_path; | ||
} | ||
crate_path | ||
} | ||
None => "".to_owned(), | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that your instinct that this is a bit hacky is correct, but I'm struggling to fully understand this: are you trying to convert a URI to a path?
|
4b7914d
to
ecfe7c0
Compare
@bors r+ |
☀️ Test successful - checks-actions |
Hello!
I tried to implement a tree view that shows the dependencies of a project.
It allows to see all dependencies to the project and it uses
cargo tree
for it. Also it allows to click and open the files, the viewtree tries its best to follow the openned file in the editor.Here is an example:
Any feedback is welcome since i have basically no professional experience with TS.