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:
1 parent
b7b5d0c1a0
commit
84ebc5be71
10 files changed
+292
-108
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user