Escape markdown special chars in file deletion confirmation dialog (#55697)

alkinun created

## Summary

Filenames containing md syntax (e.g. `__somefile__`, `*somefile*`) were
being rendered as markdown text in the file deletion confirmation
dialog.

Fixes #55651

## Changes

Wrapped file paths with `MarkdownEscaped` in the single and multi-file
deletion confirmation dialogs in `project_panel.rs`, so special md chars
like `_`, `*`, and `[` are escaped before being rendered.

## Testing

Created a file named `__somefile__` and tried to delete it, the name now
displays literally in the confirmation dialog instead of being rendered
as bold text:
<img width="339" height="206" alt="img"
src="https://github.com/user-attachments/assets/93e4e7d1-d5dc-45bb-9c08-2fe83c75aad2"
/>

Also added `test_delete_prompt_escapes_markdown_in_file_name` in
`project_panel_tests.rs` that verifies filenames with markdown special
characters render literally in the confirmation dialog.

Release Notes:

- Fixed file names containing markdown special characters (e.g.
`__somefile__`)
being rendered as formatted text in the file deletion confirmation
dialog.

Change summary

crates/project_panel/src/project_panel.rs       | 16 ++++++--
crates/project_panel/src/project_panel_tests.rs | 36 +++++++++++++++++++
2 files changed, 48 insertions(+), 4 deletions(-)

Detailed changes

crates/project_panel/src/project_panel.rs 🔗

@@ -66,7 +66,9 @@ use ui::{
     StickyCandidate, Tooltip, WithScrollbar, prelude::*, v_flex,
 };
 use util::{
-    ResultExt, TakeUntilExt, TryFutureExt, maybe,
+    ResultExt, TakeUntilExt, TryFutureExt,
+    markdown::MarkdownInlineCode,
+    maybe,
     paths::{PathStyle, compare_paths},
     rel_path::{RelPath, RelPathBuf},
 };
@@ -2357,7 +2359,10 @@ impl ProjectPanel {
                             ""
                         };
 
-                        format!("{message_start} {path}?{unsaved_warning}")
+                        format!(
+                            "{message_start} {}?{unsaved_warning}",
+                            MarkdownInlineCode(path)
+                        )
                     }
                     _ => {
                         const CUTOFF_POINT: usize = 10;
@@ -2365,7 +2370,7 @@ impl ProjectPanel {
                             let truncated_path_counts = file_paths.len() - CUTOFF_POINT;
                             let mut paths = file_paths
                                 .iter()
-                                .map(|(_, _, path)| path.clone())
+                                .map(|(_, _, path)| MarkdownInlineCode(path).to_string())
                                 .take(CUTOFF_POINT)
                                 .collect::<Vec<_>>();
                             paths.truncate(CUTOFF_POINT);
@@ -2376,7 +2381,10 @@ impl ProjectPanel {
                             }
                             paths
                         } else {
-                            file_paths.iter().map(|(_, _, path)| path.clone()).collect()
+                            file_paths
+                                .iter()
+                                .map(|(_, _, path)| MarkdownInlineCode(path).to_string())
+                                .collect()
                         };
                         let unsaved_warning = if dirty_buffers == 0 {
                             String::new()

crates/project_panel/src/project_panel_tests.rs 🔗

@@ -10339,3 +10339,39 @@ impl Render for TestProjectItemView {
         Empty
     }
 }
+
+#[gpui::test]
+async fn test_delete_prompt_escapes_markdown_in_file_name(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        "/root",
+        json!({
+            "__somefile__": "",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
+    let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+    let workspace = window
+        .read_with(cx, |mw, _| mw.workspace().clone())
+        .unwrap();
+    let cx = &mut VisualTestContext::from_window(window.into(), cx);
+    let panel = workspace.update_in(cx, ProjectPanel::new);
+    cx.run_until_parked();
+
+    select_path(&panel, "root/__somefile__", cx);
+    panel.update_in(cx, |panel, window, cx| {
+        panel.delete(&Delete { skip_prompt: false }, window, cx)
+    });
+    let (message, _detail) = cx
+        .pending_prompt()
+        .expect("delete should show a confirmation prompt");
+
+    assert_eq!(
+        message,
+        "Are you sure you want to permanently delete `__somefile__`?"
+    );
+}