From a9dd7e9f062933336b04c07145caaf19893c0f89 Mon Sep 17 00:00:00 2001 From: danielaalves01 <56894701+danielaalves01@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:46:19 +0100 Subject: [PATCH] Fix workspace-absolute paths in markdown images (#52708) ## Context Previously, markdown images failed to load workspace-absolute paths. This updates the image resolver to identify the active workspace root directory. Paths which are workspace absolute are correctly resolved and rendered. The added test covers a successful resolution. This PR re-implements the fix that was originally proposed in my previous PR, #52178. ## Fix https://github.com/user-attachments/assets/d69644ea-06cc-4638-b4ee-ec9f3abbb1ed ## How to Review Small PR - focus on two changes in the file `crates/markdown_preview/src/markdown_preview_view.rs`: - `fn render_markdown_element()` (lines ~583-590): added the logic to determine the workspace_directory - `fn resolve_preview_image()` (lines ~714-726): added workspace_directory variable, and a verification to create the full path when a path is workspace-absolute One test was added, covering a successful resolution (`resolves_workspace_absolute_preview_images`). This test was implemented in the file `crates/markdown_preview/src/markdown_preview_view.rs`. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #46924 Release Notes: - Added workspace-absolute path detection in markdown files --- .../src/markdown_preview_view.rs | 86 ++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 0b9c63c3b16f5686afcfdafdba119ede8c37fe3f..8e289e451dada6170f7b2bd7282ef9f165d26cff 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -580,6 +580,14 @@ impl MarkdownPreviewView { .as_ref() .map(|state| state.editor.clone()); + let mut workspace_directory = None; + if let Some(workspace_entity) = self.workspace.upgrade() { + let project = workspace_entity.read(cx).project(); + if let Some(tree) = project.read(cx).worktrees(cx).next() { + workspace_directory = Some(tree.read(cx).abs_path().to_path_buf()); + } + } + let mut markdown_element = MarkdownElement::new( self.markdown.clone(), MarkdownStyle::themed(MarkdownFont::Editor, window, cx), @@ -593,7 +601,13 @@ impl MarkdownPreviewView { .show_root_block_markers() .image_resolver({ let base_directory = self.base_directory.clone(); - move |dest_url| resolve_preview_image(dest_url, base_directory.as_deref()) + move |dest_url| { + resolve_preview_image( + dest_url, + base_directory.as_deref(), + workspace_directory.as_deref(), + ) + } }) .on_url_click(move |url, window, cx| { open_preview_url(url, base_directory.clone(), &workspace, window, cx); @@ -687,7 +701,11 @@ fn resolve_preview_path(url: &str, base_directory: Option<&Path>) -> Option) -> Option { +fn resolve_preview_image( + dest_url: &str, + base_directory: Option<&Path>, + workspace_directory: Option<&Path>, +) -> Option { if dest_url.starts_with("data:") { return None; } @@ -702,6 +720,19 @@ fn resolve_preview_image(dest_url: &str, base_directory: Option<&Path>) -> Optio .map(|decoded| decoded.into_owned()) .unwrap_or_else(|_| dest_url.to_string()); + let decoded_path = Path::new(&decoded); + + if let Ok(relative_path) = decoded_path.strip_prefix("/") { + if let Some(root) = workspace_directory { + let absolute_path = root.join(relative_path); + if absolute_path.exists() { + return Some(ImageSource::Resource(Resource::Path(Arc::from( + absolute_path.as_path(), + )))); + } + } + } + let path = if Path::new(&decoded).is_absolute() { PathBuf::from(decoded) } else { @@ -778,6 +809,9 @@ impl Render for MarkdownPreviewView { #[cfg(test)] mod tests { + use crate::markdown_preview_view::ImageSource; + use crate::markdown_preview_view::Resource; + use crate::markdown_preview_view::resolve_preview_image; use anyhow::Result; use std::fs; use tempfile::TempDir; @@ -819,6 +853,54 @@ mod tests { Ok(()) } + #[test] + fn resolves_workspace_absolute_preview_images() -> Result<()> { + let temp_dir = TempDir::new()?; + let workspace_directory = temp_dir.path(); + + let base_directory = workspace_directory.join("docs"); + fs::create_dir_all(&base_directory)?; + + let image_file = workspace_directory.join("test_image.png"); + fs::write(&image_file, "mock data")?; + + let resolved_success = resolve_preview_image( + "/test_image.png", + Some(&base_directory), + Some(workspace_directory), + ); + + match resolved_success { + Some(ImageSource::Resource(Resource::Path(p))) => { + assert_eq!(p.as_ref(), image_file.as_path()); + } + _ => panic!("Expected successful resolution to be a Resource::Path"), + } + + let resolved_missing = resolve_preview_image( + "/missing_image.png", + Some(&base_directory), + Some(workspace_directory), + ); + + let expected_missing_path = if std::path::Path::new("/missing_image.png").is_absolute() { + std::path::PathBuf::from("/missing_image.png") + } else { + // join is to retain windows path prefix C:/ + #[expect(clippy::join_absolute_paths)] + base_directory.join("/missing_image.png") + }; + + match resolved_missing { + Some(ImageSource::Resource(Resource::Path(p))) => { + assert_eq!(p.as_ref(), expected_missing_path.as_path()); + } + _ => panic!("Expected missing file to fallback to a Resource::Path"), + } + + Ok(()) + } + #[test] fn does_not_treat_web_links_as_preview_paths() { assert_eq!(resolve_preview_path("https://zed.dev", None), None);