@@ -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<Path
}
}
-fn resolve_preview_image(dest_url: &str, base_directory: Option<&Path>) -> Option<ImageSource> {
+fn resolve_preview_image(
+ dest_url: &str,
+ base_directory: Option<&Path>,
+ workspace_directory: Option<&Path>,
+) -> Option<ImageSource> {
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);