Correctly handle file links in markdown and agent threads (#56024)

Closes https://github.com/zed-industries/zed/issues/54840

There are likely more places we can use this new workspace function in,
but these are the known ones to me right now

Release Notes:

- Fixed file hyperlinks in the agent panel not opening files when
clicked

Co-authored-by: saberoueslati <saberoueslati12@gmail.com>
This commit is contained in:
Lukas Wirthandsaberoueslati authored and GitHub committed 2026-06-05 11:17:44 +00:00
1 parent b7b5d0c1a0
commit 84ebc5be71
10 files changed
+292 -108

No files matched your search

+9 -1
View File
@@ -405,7 +405,15 @@ impl Render for LanguageServerPrompt {
wrap_button_visibility: markdown::WrapButtonVisibility::Hidden,
border: false,
})
.on_url_click(|link, _, cx| cx.open_url(&link)),
.on_url_click(|link, window, cx| {
if let Some(workspace) = Workspace::for_window(window, cx) {
workspace.update(cx, |workspace, cx| {
workspace.open_url_or_file(&link, None, window, cx);
});
} else {
cx.open_url(&link);
}
}),
)
.children(request.actions.iter().enumerate().map(|(ix, action)| {
let this_handle = cx.entity();
+148
View File
@@ -145,6 +145,7 @@ pub use toolbar::{
};
pub use ui;
use ui::{Window, prelude::*};
use url::Url;
use util::{
ResultExt, TryFutureExt,
paths::{PathStyle, SanitizedPath},
@@ -4672,6 +4673,108 @@ impl Workspace {
})
}
/// Opens a URL or file path, intelligently routing to the appropriate handler:
///
/// - `http://` and `https://` URLs are opened in the system's default browser
/// - `file://` URIs are opened as files in the editor
/// - Paths without a URL scheme are treated as file paths:
/// - Absolute paths are opened directly
/// - Relative paths are first resolved against `base_path` (if provided),
/// then against visible project worktrees
/// - Other URI schemes (e.g., `mailto:`, `vscode:`) are passed to the system handler
///
/// # Arguments
/// * `url_or_path` - The URL or file path to open
/// * `base_path` - Optional base directory for resolving relative paths (e.g., the
/// directory containing a markdown file). If not provided, relative paths are
/// resolved against project worktrees.
///
/// This method provides a unified way to handle links that may be either URLs
/// or file paths, such as those found in markdown documents, terminal output,
/// or LSP responses.
pub fn open_url_or_file(
&mut self,
url_or_path: &str,
base_path: Option<&Path>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut open_abs_path = |this: &mut Self, path, cx: &mut _| {
let url_or_path = url_or_path.to_owned();
let task = this.open_abs_path(
path,
OpenOptions {
visible: Some(OpenVisible::None),
..Default::default()
},
window,
cx,
);
(**cx)
.spawn(async move |cx| {
if let Err(_) = task.await {
cx.update(|cx| cx.open_url(&url_or_path));
}
})
.detach();
};
if let Ok(url) = Url::parse(url_or_path) {
match url.scheme() {
"http" | "https" => cx.open_url(url_or_path),
"file" => open_abs_path(self, PathBuf::from(url.path()), cx),
_ => cx.open_url(url_or_path),
}
return;
}
// Not a valid URL - treat as a file path
let project = self.project();
let path_style = project.read(cx).path_style(cx);
// If it's an absolute path, open it directly
if path_style.is_absolute(url_or_path) {
open_abs_path(self, PathBuf::from(url_or_path), cx);
return;
}
let path = Path::new(url_or_path);
// Try to resolve relative path against base_path first
if let Some(base) = base_path
// TODO: remotes, the exists check below hits the local FS, unsure
// if this runs on the remote or not
&& project.read(cx).is_local()
{
let resolved = path_style.join(base, path).map(PathBuf::from);
if let Some(resolved) = resolved
&& resolved.exists()
{
open_abs_path(self, resolved, cx);
return;
}
}
// Try to resolve against project worktrees
if let Some(project_path) =
project.update(cx, |project, cx| project.find_project_path(url_or_path, cx))
{
let url_or_path = url_or_path.to_owned();
let task = self.open_path(project_path, None, true, window, cx);
(**cx)
.spawn(async move |cx| {
if let Err(_) = task.await {
cx.update(|cx| cx.open_url(&url_or_path));
}
})
.detach();
return;
}
// Couldn't resolve as a file path - try opening as URL anyway
// (the OS might be able to handle it)
cx.open_url(url_or_path);
}
pub fn split_path(
&mut self,
path: impl Into<ProjectPath>,
@@ -15996,4 +16099,49 @@ mod tests {
let path = workspace.read_with(cx, |workspace, cx| workspace.most_recent_active_path(cx));
assert_eq!(path, None);
}
#[gpui::test]
async fn test_open_url_or_file_routes_urls(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root",
json!({
"file.txt": "content",
"subdir": {
"nested.txt": "nested content"
}
}),
)
.await;
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
// Test opening an HTTPS URL - should go to open_url
workspace.update_in(cx, |workspace, window, cx| {
workspace.open_url_or_file("https://example.com", None, window, cx);
});
assert_eq!(cx.opened_url(), Some("https://example.com".to_string()));
// Test opening an HTTP URL - should go to open_url
workspace.update_in(cx, |workspace, window, cx| {
workspace.open_url_or_file("http://example.com", None, window, cx);
});
assert_eq!(cx.opened_url(), Some("http://example.com".to_string()));
// Test opening other URI schemes - should go to open_url
workspace.update_in(cx, |workspace, window, cx| {
workspace.open_url_or_file("mailto:test@example.com", None, window, cx);
});
assert_eq!(cx.opened_url(), Some("mailto:test@example.com".to_string()));
// Test opening a path that doesn't exist and doesn't match project - should fallback to open_url
workspace.update_in(cx, |workspace, window, cx| {
workspace.open_url_or_file("nonexistent.txt", None, window, cx);
});
assert_eq!(cx.opened_url(), Some("nonexistent.txt".to_string()));
}
}