diff --git a/Cargo.lock b/Cargo.lock index 639eeab5aac9a04da906e78a90962777e8cca13e..9964dd56e574d58108ee4f0cf64052081a456a70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9592,6 +9592,7 @@ dependencies = [ "tempfile", "terminal", "text", + "toml 0.8.19", "unindent", "url", "util", diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 68fdb375f43eb4c2def13ad4c966eb43ba832d0f..249f788c82bcf90dcac1230b4a5d92d0680ae545 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -68,6 +68,7 @@ snippet.workspace = true snippet_provider.workspace = true terminal.workspace = true text.workspace = true +toml.workspace = true util.workspace = true url.workspace = true which.workspace = true diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 3abc794d041f1cc2df02fc686560bd5db950e6d3..8602c3d62936a51c4a40bf8d75b1a8295739425c 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -5,12 +5,12 @@ use crate::{ ProjectItem as _, ProjectPath, }; use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry}; -use anyhow::{anyhow, Context as _, Result}; +use anyhow::{anyhow, bail, Context as _, Result}; use client::Client; use collections::{hash_map, HashMap, HashSet}; use fs::Fs; use futures::{channel::oneshot, future::Shared, Future, FutureExt as _, StreamExt}; -use git::{blame::Blame, diff::BufferDiff}; +use git::{blame::Blame, diff::BufferDiff, repository::RepoPath}; use gpui::{ AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Subscription, Task, WeakModel, @@ -24,8 +24,16 @@ use language::{ Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation, }; use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope}; +use serde::Deserialize; use smol::channel::Receiver; -use std::{io, ops::Range, path::Path, str::FromStr as _, sync::Arc, time::Instant}; +use std::{ + io, + ops::Range, + path::{Path, PathBuf}, + str::FromStr as _, + sync::Arc, + time::Instant, +}; use text::{BufferId, LineEnding, Rope}; use util::{debug_panic, maybe, ResultExt as _, TryFutureExt}; use worktree::{File, PathChange, ProjectEntryId, UpdatedGitRepositoriesSet, Worktree, WorktreeId}; @@ -1213,11 +1221,36 @@ impl BufferStore { match file.worktree.read(cx) { Worktree::Local(worktree) => { - let Some(repo) = worktree.local_git_repo(file.path()) else { - return Task::ready(Err(anyhow!("no repository for buffer found"))); + let worktree_path = worktree.abs_path().clone(); + let Some((repo_entry, repo)) = + worktree.repository_for_path(file.path()).and_then(|entry| { + let repo = worktree.get_local_repo(&entry)?.repo().clone(); + Some((entry, repo)) + }) + else { + // If we're not in a Git repo, check whether this is a Rust source + // file in the Cargo registry (presumably opened with go-to-definition + // from a normal Rust file). If so, we can put together a permalink + // using crate metadata. + if !buffer + .language() + .is_some_and(|lang| lang.name() == "Rust".into()) + { + return Task::ready(Err(anyhow!("no permalink available"))); + } + let file_path = worktree_path.join(file.path()); + return cx.spawn(|cx| async move { + let provider_registry = + cx.update(GitHostingProviderRegistry::default_global)?; + get_permalink_in_rust_registry_src(provider_registry, file_path, selection) + .map_err(|_| anyhow!("no permalink available")) + }); }; - let path = file.path().clone(); + let path = match repo_entry.relativize(worktree, file.path()) { + Ok(RepoPath(path)) => path, + Err(e) => return Task::ready(Err(e)), + }; cx.spawn(|cx| async move { const REMOTE_NAME: &str = "origin"; @@ -1238,7 +1271,7 @@ impl BufferStore { let path = path .to_str() - .context("failed to convert buffer path to string")?; + .ok_or_else(|| anyhow!("failed to convert path to string"))?; Ok(provider.build_permalink( remote, @@ -2432,3 +2465,52 @@ fn deserialize_blame_buffer_response( remote_url: response.remote_url, }) } + +fn get_permalink_in_rust_registry_src( + provider_registry: Arc, + path: PathBuf, + selection: Range, +) -> Result { + #[derive(Deserialize)] + struct CargoVcsGit { + sha1: String, + } + + #[derive(Deserialize)] + struct CargoVcsInfo { + git: CargoVcsGit, + path_in_vcs: String, + } + + #[derive(Deserialize)] + struct CargoPackage { + repository: String, + } + + #[derive(Deserialize)] + struct CargoToml { + package: CargoPackage, + } + + let Some((dir, cargo_vcs_info_json)) = path.ancestors().skip(1).find_map(|dir| { + let json = std::fs::read_to_string(dir.join(".cargo_vcs_info.json")).ok()?; + Some((dir, json)) + }) else { + bail!("No .cargo_vcs_info.json found in parent directories") + }; + let cargo_vcs_info = serde_json::from_str::(&cargo_vcs_info_json)?; + let cargo_toml = std::fs::read_to_string(dir.join("Cargo.toml"))?; + let manifest = toml::from_str::(&cargo_toml)?; + let (provider, remote) = parse_git_remote_url(provider_registry, &manifest.package.repository) + .ok_or_else(|| anyhow!("Failed to parse package.repository field of manifest"))?; + let path = PathBuf::from(cargo_vcs_info.path_in_vcs).join(path.strip_prefix(dir).unwrap()); + let permalink = provider.build_permalink( + remote, + BuildPermalinkParams { + sha: &cargo_vcs_info.git.sha1, + path: &path.to_string_lossy(), + selection: Some(selection), + }, + ); + Ok(permalink) +} diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 13f335334ae70e978949c79b33afb987151f557f..9ee909f73eabb4937fff322bfec6157beadb3d19 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2396,7 +2396,6 @@ impl Snapshot { .map(|(path, entry)| (&path.0, entry)) } - /// Get the repository whose work directory contains the given path. pub fn repository_for_work_directory(&self, path: &Path) -> Option { self.repository_entries .get(&RepositoryWorkDirectory(path.into()))