Fix link opening (#44910)

- **Fix editor::OpenUrl on zed links**
- **Fix cmd-clicking links too**

Closes #44293
Closes #43833

Release Notes:

- The `editor::OpenUrl` action now works for links to https://zed.dev
- Clicking on a link to a Zed channel or channel-note within the editor
no-longer redirects you via the web.

---------

Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
This commit is contained in:
Conrad Irwin
2025-12-15 20:53:50 -07:00
committed by GitHub
parent c7d248329b
commit 829b1b5661
3 changed files with 68 additions and 40 deletions

View File

@@ -1730,23 +1730,59 @@ impl ProtoClient for Client {
/// prefix for the zed:// url scheme
pub const ZED_URL_SCHEME: &str = "zed";
/// A parsed Zed link that can be handled internally by the application.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ZedLink {
/// Join a channel: `zed.dev/channel/channel-name-123` or `zed://channel/channel-name-123`
Channel { channel_id: u64 },
/// Open channel notes: `zed.dev/channel/channel-name-123/notes` or with heading `notes#heading`
ChannelNotes {
channel_id: u64,
heading: Option<String>,
},
}
/// Parses the given link into a Zed link.
///
/// Returns a [`Some`] containing the unprefixed link if the link is a Zed link.
/// Returns [`None`] otherwise.
pub fn parse_zed_link<'a>(link: &'a str, cx: &App) -> Option<&'a str> {
/// Returns a [`Some`] containing the parsed link if the link is a recognized Zed link
/// that should be handled internally by the application.
/// Returns [`None`] for links that should be opened in the browser.
pub fn parse_zed_link(link: &str, cx: &App) -> Option<ZedLink> {
let server_url = &ClientSettings::get_global(cx).server_url;
if let Some(stripped) = link
let path = link
.strip_prefix(server_url)
.and_then(|result| result.strip_prefix('/'))
{
return Some(stripped);
.or_else(|| {
link.strip_prefix(ZED_URL_SCHEME)
.and_then(|result| result.strip_prefix("://"))
})?;
let mut parts = path.split('/');
if parts.next() != Some("channel") {
return None;
}
if let Some(stripped) = link
.strip_prefix(ZED_URL_SCHEME)
.and_then(|result| result.strip_prefix("://"))
{
return Some(stripped);
let slug = parts.next()?;
let id_str = slug.split('-').next_back()?;
let channel_id = id_str.parse::<u64>().ok()?;
let Some(next) = parts.next() else {
return Some(ZedLink::Channel { channel_id });
};
if let Some(heading) = next.strip_prefix("notes#") {
return Some(ZedLink::ChannelNotes {
channel_id,
heading: Some(heading.to_string()),
});
}
if next == "notes" {
return Some(ZedLink::ChannelNotes {
channel_id,
heading: None,
});
}
None

View File

@@ -17467,7 +17467,14 @@ impl Editor {
// If there is one url or file, open it directly
match first_url_or_file {
Some(Either::Left(url)) => {
cx.update(|_, cx| cx.open_url(&url))?;
cx.update(|window, cx| {
if parse_zed_link(&url, cx).is_some() {
window
.dispatch_action(Box::new(zed_actions::OpenZedUrl { url }), cx);
} else {
cx.open_url(&url);
}
})?;
Ok(Navigated::Yes)
}
Some(Either::Right(path)) => {

View File

@@ -3,7 +3,7 @@ use crate::restorable_workspace_locations;
use anyhow::{Context as _, Result, anyhow};
use cli::{CliRequest, CliResponse, ipc::IpcSender};
use cli::{IpcHandshake, ipc};
use client::parse_zed_link;
use client::{ZedLink, parse_zed_link};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
@@ -112,8 +112,18 @@ impl OpenRequest {
});
} else if url.starts_with("ssh://") {
this.parse_ssh_file_path(&url, cx)?
} else if let Some(request_path) = parse_zed_link(&url, cx) {
this.parse_request_path(request_path).log_err();
} else if let Some(zed_link) = parse_zed_link(&url, cx) {
match zed_link {
ZedLink::Channel { channel_id } => {
this.join_channel = Some(channel_id);
}
ZedLink::ChannelNotes {
channel_id,
heading,
} => {
this.open_channel_notes.push((channel_id, heading));
}
}
} else {
log::error!("unhandled url: {}", url);
}
@@ -157,31 +167,6 @@ impl OpenRequest {
self.parse_file_path(url.path());
Ok(())
}
fn parse_request_path(&mut self, request_path: &str) -> Result<()> {
let mut parts = request_path.split('/');
if parts.next() == Some("channel")
&& let Some(slug) = parts.next()
&& let Some(id_str) = slug.split('-').next_back()
&& let Ok(channel_id) = id_str.parse::<u64>()
{
let Some(next) = parts.next() else {
self.join_channel = Some(channel_id);
return Ok(());
};
if let Some(heading) = next.strip_prefix("notes#") {
self.open_channel_notes
.push((channel_id, Some(heading.to_string())));
return Ok(());
}
if next == "notes" {
self.open_channel_notes.push((channel_id, None));
return Ok(());
}
}
anyhow::bail!("invalid zed url: {request_path}")
}
}
#[derive(Clone)]