Add ability to copy a permalink to a line (#7119)
This PR adds the ability to copy the permalink to a line from within Zed. This functionality is available through the `editor: copy permalink to line` action in the command palette: <img width="589" alt="Screenshot 2024-01-30 at 7 07 46 PM" src="https://github.com/zed-industries/zed/assets/1486634/332282cb-211f-4f16-9eb1-415bcfee9b7b"> Executing this action will create a permalink to the currently selected line(s) and copy it to the clipboard. Here is an example line: ``` https://github.com/maxdeviant/auk/blob/56c80e80112744740be1969c89fdd34db4be6f64/src/lib.rs#L25 ``` Currently, both GitHub and GitLab are supported. ### Notes and known limitations - In order to determine where to permalink to, we read the URL of the `origin` remote in Git. This feature will not work if the `origin` remote is not present. - Attempting to permalink to a ref that is not pushed to the origin will result in the link 404ing. - Attempting to permalink when Git is in a dirty state may not generate the right link. - For instance, modifying a file (e.g., adding new lines) and grabbing a permalink to it will result in incorrect line numbers. Release Notes: - Added the ability to copy a permalink to a line ([#6777](https://github.com/zed-industries/zed/issues/6777)). - Available via the `editor: copy permalink to line` action in the command palette.
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use language::Point;
|
||||
use url::Url;
|
||||
|
||||
enum GitHostingProvider {
|
||||
Github,
|
||||
Gitlab,
|
||||
}
|
||||
|
||||
impl GitHostingProvider {
|
||||
fn base_url(&self) -> Url {
|
||||
let base_url = match self {
|
||||
Self::Github => "https://github.com",
|
||||
Self::Gitlab => "https://gitlab.com",
|
||||
};
|
||||
|
||||
Url::parse(&base_url).unwrap()
|
||||
}
|
||||
|
||||
/// Returns the fragment portion of the URL for the selected lines in
|
||||
/// the representation the [`GitHostingProvider`] expects.
|
||||
fn line_fragment(&self, selection: &Range<Point>) -> String {
|
||||
if selection.start.row == selection.end.row {
|
||||
let line = selection.start.row + 1;
|
||||
|
||||
match self {
|
||||
Self::Github | Self::Gitlab => format!("L{}", line),
|
||||
}
|
||||
} else {
|
||||
let start_line = selection.start.row + 1;
|
||||
let end_line = selection.end.row + 1;
|
||||
|
||||
match self {
|
||||
Self::Github => format!("L{}-L{}", start_line, end_line),
|
||||
Self::Gitlab => format!("L{}-{}", start_line, end_line),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BuildPermalinkParams<'a> {
|
||||
pub remote_url: &'a str,
|
||||
pub sha: &'a str,
|
||||
pub path: &'a str,
|
||||
pub selection: Option<Range<Point>>,
|
||||
}
|
||||
|
||||
pub fn build_permalink(params: BuildPermalinkParams) -> Result<Url> {
|
||||
let BuildPermalinkParams {
|
||||
remote_url,
|
||||
sha,
|
||||
path,
|
||||
selection,
|
||||
} = params;
|
||||
|
||||
let ParsedGitRemote {
|
||||
provider,
|
||||
owner,
|
||||
repo,
|
||||
} = parse_git_remote_url(remote_url)
|
||||
.ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
|
||||
|
||||
let path = match provider {
|
||||
GitHostingProvider::Github => format!("{owner}/{repo}/blob/{sha}/{path}"),
|
||||
GitHostingProvider::Gitlab => format!("{owner}/{repo}/-/blob/{sha}/{path}"),
|
||||
};
|
||||
let line_fragment = selection.map(|selection| provider.line_fragment(&selection));
|
||||
|
||||
let mut permalink = provider.base_url().join(&path).unwrap();
|
||||
permalink.set_fragment(line_fragment.as_deref());
|
||||
|
||||
Ok(permalink)
|
||||
}
|
||||
|
||||
struct ParsedGitRemote<'a> {
|
||||
pub provider: GitHostingProvider,
|
||||
pub owner: &'a str,
|
||||
pub repo: &'a str,
|
||||
}
|
||||
|
||||
fn parse_git_remote_url(url: &str) -> Option<ParsedGitRemote> {
|
||||
if url.starts_with("git@github.com:") || url.starts_with("https://github.com/") {
|
||||
let repo_with_owner = url
|
||||
.trim_start_matches("git@github.com:")
|
||||
.trim_start_matches("https://github.com/")
|
||||
.trim_end_matches(".git");
|
||||
|
||||
let (owner, repo) = repo_with_owner.split_once("/")?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: GitHostingProvider::Github,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
}
|
||||
|
||||
if url.starts_with("git@gitlab.com:") || url.starts_with("https://gitlab.com/") {
|
||||
let repo_with_owner = url
|
||||
.trim_start_matches("git@gitlab.com:")
|
||||
.trim_start_matches("https://gitlab.com/")
|
||||
.trim_end_matches(".git");
|
||||
|
||||
let (owner, repo) = repo_with_owner.split_once("/")?;
|
||||
|
||||
return Some(ParsedGitRemote {
|
||||
provider: GitHostingProvider::Gitlab,
|
||||
owner,
|
||||
repo,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_ssh_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@github.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_ssh_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@github.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(Point::new(6, 1)..Point::new(6, 10)),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_ssh_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@github.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(Point::new(23, 1)..Point::new(47, 10)),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-L48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_https_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://github.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_https_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://github.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(Point::new(6, 1)..Point::new(6, 10)),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_github_permalink_from_https_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://github.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(Point::new(23, 1)..Point::new(47, 10)),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://github.com/zed-industries/zed/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-L48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_ssh_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@gitlab.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_ssh_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@gitlab.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(Point::new(6, 1)..Point::new(6, 10)),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_ssh_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "git@gitlab.com:zed-industries/zed.git",
|
||||
sha: "e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7",
|
||||
path: "crates/editor/src/git/permalink.rs",
|
||||
selection: Some(Point::new(23, 1)..Point::new(47, 10)),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/e6ebe7974deb6bb6cc0e2595c8ec31f0c71084b7/crates/editor/src/git/permalink.rs#L24-48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_https_url() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://gitlab.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_https_url_single_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://gitlab.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(Point::new(6, 1)..Point::new(6, 10)),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L7";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_gitlab_permalink_from_https_url_multi_line_selection() {
|
||||
let permalink = build_permalink(BuildPermalinkParams {
|
||||
remote_url: "https://gitlab.com/zed-industries/zed.git",
|
||||
sha: "b2efec9824c45fcc90c9a7eb107a50d1772a60aa",
|
||||
path: "crates/zed/src/main.rs",
|
||||
selection: Some(Point::new(23, 1)..Point::new(47, 10)),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let expected_url = "https://gitlab.com/zed-industries/zed/-/blob/b2efec9824c45fcc90c9a7eb107a50d1772a60aa/crates/zed/src/main.rs#L24-48";
|
||||
assert_eq!(permalink.to_string(), expected_url.to_string())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user