agent2: Support directories in @file mentions (#36416)

Cole Miller created

Release Notes:

- N/A

Change summary

crates/acp_thread/src/mention.rs               |  66 +--
crates/agent2/src/thread.rs                    |  14 
crates/agent_ui/src/acp/completion_provider.rs |  13 
crates/agent_ui/src/acp/message_editor.rs      | 369 ++++++++++++++-----
crates/agent_ui/src/acp/thread_view.rs         |  31 
5 files changed, 325 insertions(+), 168 deletions(-)

Detailed changes

crates/acp_thread/src/mention.rs 🔗

@@ -15,7 +15,9 @@ use url::Url;
 pub enum MentionUri {
     File {
         abs_path: PathBuf,
-        is_directory: bool,
+    },
+    Directory {
+        abs_path: PathBuf,
     },
     Symbol {
         path: PathBuf,
@@ -79,14 +81,14 @@ impl MentionUri {
                         })
                     }
                 } else {
-                    let file_path =
+                    let abs_path =
                         PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
-                    let is_directory = input.ends_with("/");
 
-                    Ok(Self::File {
-                        abs_path: file_path,
-                        is_directory,
-                    })
+                    if input.ends_with("/") {
+                        Ok(Self::Directory { abs_path })
+                    } else {
+                        Ok(Self::File { abs_path })
+                    }
                 }
             }
             "zed" => {
@@ -120,7 +122,7 @@ impl MentionUri {
 
     pub fn name(&self) -> String {
         match self {
-            MentionUri::File { abs_path, .. } => abs_path
+            MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => abs_path
                 .file_name()
                 .unwrap_or_default()
                 .to_string_lossy()
@@ -138,18 +140,11 @@ impl MentionUri {
 
     pub fn icon_path(&self, cx: &mut App) -> SharedString {
         match self {
-            MentionUri::File {
-                abs_path,
-                is_directory,
-            } => {
-                if *is_directory {
-                    FileIcons::get_folder_icon(false, cx)
-                        .unwrap_or_else(|| IconName::Folder.path().into())
-                } else {
-                    FileIcons::get_icon(abs_path, cx)
-                        .unwrap_or_else(|| IconName::File.path().into())
-                }
+            MentionUri::File { abs_path } => {
+                FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into())
             }
+            MentionUri::Directory { .. } => FileIcons::get_folder_icon(false, cx)
+                .unwrap_or_else(|| IconName::Folder.path().into()),
             MentionUri::Symbol { .. } => IconName::Code.path().into(),
             MentionUri::Thread { .. } => IconName::Thread.path().into(),
             MentionUri::TextThread { .. } => IconName::Thread.path().into(),
@@ -165,13 +160,16 @@ impl MentionUri {
 
     pub fn to_uri(&self) -> Url {
         match self {
-            MentionUri::File {
-                abs_path,
-                is_directory,
-            } => {
+            MentionUri::File { abs_path } => {
+                let mut url = Url::parse("file:///").unwrap();
+                let path = abs_path.to_string_lossy();
+                url.set_path(&path);
+                url
+            }
+            MentionUri::Directory { abs_path } => {
                 let mut url = Url::parse("file:///").unwrap();
                 let mut path = abs_path.to_string_lossy().to_string();
-                if *is_directory && !path.ends_with("/") {
+                if !path.ends_with("/") {
                     path.push_str("/");
                 }
                 url.set_path(&path);
@@ -274,12 +272,8 @@ mod tests {
         let file_uri = "file:///path/to/file.rs";
         let parsed = MentionUri::parse(file_uri).unwrap();
         match &parsed {
-            MentionUri::File {
-                abs_path,
-                is_directory,
-            } => {
+            MentionUri::File { abs_path } => {
                 assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs");
-                assert!(!is_directory);
             }
             _ => panic!("Expected File variant"),
         }
@@ -291,32 +285,26 @@ mod tests {
         let file_uri = "file:///path/to/dir/";
         let parsed = MentionUri::parse(file_uri).unwrap();
         match &parsed {
-            MentionUri::File {
-                abs_path,
-                is_directory,
-            } => {
+            MentionUri::Directory { abs_path } => {
                 assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/");
-                assert!(is_directory);
             }
-            _ => panic!("Expected File variant"),
+            _ => panic!("Expected Directory variant"),
         }
         assert_eq!(parsed.to_uri().to_string(), file_uri);
     }
 
     #[test]
     fn test_to_directory_uri_with_slash() {
-        let uri = MentionUri::File {
+        let uri = MentionUri::Directory {
             abs_path: PathBuf::from("/path/to/dir/"),
-            is_directory: true,
         };
         assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
     }
 
     #[test]
     fn test_to_directory_uri_without_slash() {
-        let uri = MentionUri::File {
+        let uri = MentionUri::Directory {
             abs_path: PathBuf::from("/path/to/dir"),
-            is_directory: true,
         };
         assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
     }

crates/agent2/src/thread.rs 🔗

@@ -146,6 +146,7 @@ impl UserMessage {
             They are up-to-date and don't need to be re-read.\n\n";
 
         const OPEN_FILES_TAG: &str = "<files>";
+        const OPEN_DIRECTORIES_TAG: &str = "<directories>";
         const OPEN_SYMBOLS_TAG: &str = "<symbols>";
         const OPEN_THREADS_TAG: &str = "<threads>";
         const OPEN_FETCH_TAG: &str = "<fetched_urls>";
@@ -153,6 +154,7 @@ impl UserMessage {
             "<rules>\nThe user has specified the following rules that should be applied:\n";
 
         let mut file_context = OPEN_FILES_TAG.to_string();
+        let mut directory_context = OPEN_DIRECTORIES_TAG.to_string();
         let mut symbol_context = OPEN_SYMBOLS_TAG.to_string();
         let mut thread_context = OPEN_THREADS_TAG.to_string();
         let mut fetch_context = OPEN_FETCH_TAG.to_string();
@@ -168,7 +170,7 @@ impl UserMessage {
                 }
                 UserMessageContent::Mention { uri, content } => {
                     match uri {
-                        MentionUri::File { abs_path, .. } => {
+                        MentionUri::File { abs_path } => {
                             write!(
                                 &mut symbol_context,
                                 "\n{}",
@@ -179,6 +181,9 @@ impl UserMessage {
                             )
                             .ok();
                         }
+                        MentionUri::Directory { .. } => {
+                            write!(&mut directory_context, "\n{}\n", content).ok();
+                        }
                         MentionUri::Symbol {
                             path, line_range, ..
                         }
@@ -233,6 +238,13 @@ impl UserMessage {
                 .push(language_model::MessageContent::Text(file_context));
         }
 
+        if directory_context.len() > OPEN_DIRECTORIES_TAG.len() {
+            directory_context.push_str("</directories>\n");
+            message
+                .content
+                .push(language_model::MessageContent::Text(directory_context));
+        }
+
         if symbol_context.len() > OPEN_SYMBOLS_TAG.len() {
             symbol_context.push_str("</symbols>\n");
             message

crates/agent_ui/src/acp/completion_provider.rs 🔗

@@ -445,19 +445,20 @@ impl ContextPickerCompletionProvider {
 
         let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
 
-        let file_uri = MentionUri::File {
-            abs_path,
-            is_directory,
+        let uri = if is_directory {
+            MentionUri::Directory { abs_path }
+        } else {
+            MentionUri::File { abs_path }
         };
 
-        let crease_icon_path = file_uri.icon_path(cx);
+        let crease_icon_path = uri.icon_path(cx);
         let completion_icon_path = if is_recent {
             IconName::HistoryRerun.path().into()
         } else {
             crease_icon_path.clone()
         };
 
-        let new_text = format!("{} ", file_uri.as_link());
+        let new_text = format!("{} ", uri.as_link());
         let new_text_len = new_text.len();
         Some(Completion {
             replace_range: source_range.clone(),
@@ -472,7 +473,7 @@ impl ContextPickerCompletionProvider {
                 source_range.start,
                 new_text_len - 1,
                 message_editor,
-                file_uri,
+                uri,
             )),
         })
     }

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -6,6 +6,7 @@ use acp_thread::{MentionUri, selection_name};
 use agent::{TextThreadStore, ThreadId, ThreadStore};
 use agent_client_protocol as acp;
 use anyhow::{Context as _, Result, anyhow};
+use assistant_slash_commands::codeblock_fence_for_path;
 use collections::{HashMap, HashSet};
 use editor::{
     Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
@@ -15,7 +16,7 @@ use editor::{
 };
 use futures::{
     FutureExt as _, TryFutureExt as _,
-    future::{Shared, try_join_all},
+    future::{Shared, join_all, try_join_all},
 };
 use gpui::{
     AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, Image,
@@ -23,12 +24,12 @@ use gpui::{
 };
 use language::{Buffer, Language};
 use language_model::LanguageModelImage;
-use project::{CompletionIntent, Project};
+use project::{CompletionIntent, Project, ProjectPath, Worktree};
 use rope::Point;
 use settings::Settings;
 use std::{
     ffi::OsStr,
-    fmt::Write,
+    fmt::{Display, Write},
     ops::Range,
     path::{Path, PathBuf},
     rc::Rc,
@@ -245,6 +246,9 @@ impl MessageEditor {
             MentionUri::Fetch { url } => {
                 self.confirm_mention_for_fetch(crease_id, anchor, url, window, cx);
             }
+            MentionUri::Directory { abs_path } => {
+                self.confirm_mention_for_directory(crease_id, anchor, abs_path, window, cx);
+            }
             MentionUri::Thread { id, name } => {
                 self.confirm_mention_for_thread(crease_id, anchor, id, name, window, cx);
             }
@@ -260,6 +264,124 @@ impl MessageEditor {
         }
     }
 
+    fn confirm_mention_for_directory(
+        &mut self,
+        crease_id: CreaseId,
+        anchor: Anchor,
+        abs_path: PathBuf,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        fn collect_files_in_path(worktree: &Worktree, path: &Path) -> Vec<(Arc<Path>, PathBuf)> {
+            let mut files = Vec::new();
+
+            for entry in worktree.child_entries(path) {
+                if entry.is_dir() {
+                    files.extend(collect_files_in_path(worktree, &entry.path));
+                } else if entry.is_file() {
+                    files.push((entry.path.clone(), worktree.full_path(&entry.path)));
+                }
+            }
+
+            files
+        }
+
+        let uri = MentionUri::Directory {
+            abs_path: abs_path.clone(),
+        };
+        let Some(project_path) = self
+            .project
+            .read(cx)
+            .project_path_for_absolute_path(&abs_path, cx)
+        else {
+            return;
+        };
+        let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
+            return;
+        };
+        let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else {
+            return;
+        };
+        let project = self.project.clone();
+        let task = cx.spawn(async move |_, cx| {
+            let directory_path = entry.path.clone();
+
+            let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
+            let file_paths = worktree.read_with(cx, |worktree, _cx| {
+                collect_files_in_path(worktree, &directory_path)
+            })?;
+            let descendants_future = cx.update(|cx| {
+                join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
+                    let rel_path = worktree_path
+                        .strip_prefix(&directory_path)
+                        .log_err()
+                        .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
+
+                    let open_task = project.update(cx, |project, cx| {
+                        project.buffer_store().update(cx, |buffer_store, cx| {
+                            let project_path = ProjectPath {
+                                worktree_id,
+                                path: worktree_path,
+                            };
+                            buffer_store.open_buffer(project_path, cx)
+                        })
+                    });
+
+                    // TODO: report load errors instead of just logging
+                    let rope_task = cx.spawn(async move |cx| {
+                        let buffer = open_task.await.log_err()?;
+                        let rope = buffer
+                            .read_with(cx, |buffer, _cx| buffer.as_rope().clone())
+                            .log_err()?;
+                        Some(rope)
+                    });
+
+                    cx.background_spawn(async move {
+                        let rope = rope_task.await?;
+                        Some((rel_path, full_path, rope.to_string()))
+                    })
+                }))
+            })?;
+
+            let contents = cx
+                .background_spawn(async move {
+                    let contents = descendants_future.await.into_iter().flatten();
+                    contents.collect()
+                })
+                .await;
+            anyhow::Ok(contents)
+        });
+        let task = cx
+            .spawn(async move |_, _| {
+                task.await
+                    .map(|contents| DirectoryContents(contents).to_string())
+                    .map_err(|e| e.to_string())
+            })
+            .shared();
+
+        self.mention_set.directories.insert(abs_path, task.clone());
+
+        let editor = self.editor.clone();
+        cx.spawn_in(window, async move |this, cx| {
+            if task.await.notify_async_err(cx).is_some() {
+                this.update(cx, |this, _| {
+                    this.mention_set.insert_uri(crease_id, uri);
+                })
+                .ok();
+            } else {
+                editor
+                    .update(cx, |editor, cx| {
+                        editor.display_map.update(cx, |display_map, cx| {
+                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
+                        });
+                        editor.remove_creases([crease_id], cx);
+                    })
+                    .ok();
+            }
+        })
+        .detach();
+    }
+
     fn confirm_mention_for_fetch(
         &mut self,
         crease_id: CreaseId,
@@ -361,6 +483,104 @@ impl MessageEditor {
         }
     }
 
+    fn confirm_mention_for_thread(
+        &mut self,
+        crease_id: CreaseId,
+        anchor: Anchor,
+        id: ThreadId,
+        name: String,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let uri = MentionUri::Thread {
+            id: id.clone(),
+            name,
+        };
+        let open_task = self.thread_store.update(cx, |thread_store, cx| {
+            thread_store.open_thread(&id, window, cx)
+        });
+        let task = cx
+            .spawn(async move |_, cx| {
+                let thread = open_task.await.map_err(|e| e.to_string())?;
+                let content = thread
+                    .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text())
+                    .map_err(|e| e.to_string())?;
+                Ok(content)
+            })
+            .shared();
+
+        self.mention_set.insert_thread(id, task.clone());
+
+        let editor = self.editor.clone();
+        cx.spawn_in(window, async move |this, cx| {
+            if task.await.notify_async_err(cx).is_some() {
+                this.update(cx, |this, _| {
+                    this.mention_set.insert_uri(crease_id, uri);
+                })
+                .ok();
+            } else {
+                editor
+                    .update(cx, |editor, cx| {
+                        editor.display_map.update(cx, |display_map, cx| {
+                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
+                        });
+                        editor.remove_creases([crease_id], cx);
+                    })
+                    .ok();
+            }
+        })
+        .detach();
+    }
+
+    fn confirm_mention_for_text_thread(
+        &mut self,
+        crease_id: CreaseId,
+        anchor: Anchor,
+        path: PathBuf,
+        name: String,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let uri = MentionUri::TextThread {
+            path: path.clone(),
+            name,
+        };
+        let context = self.text_thread_store.update(cx, |text_thread_store, cx| {
+            text_thread_store.open_local_context(path.as_path().into(), cx)
+        });
+        let task = cx
+            .spawn(async move |_, cx| {
+                let context = context.await.map_err(|e| e.to_string())?;
+                let xml = context
+                    .update(cx, |context, cx| context.to_xml(cx))
+                    .map_err(|e| e.to_string())?;
+                Ok(xml)
+            })
+            .shared();
+
+        self.mention_set.insert_text_thread(path, task.clone());
+
+        let editor = self.editor.clone();
+        cx.spawn_in(window, async move |this, cx| {
+            if task.await.notify_async_err(cx).is_some() {
+                this.update(cx, |this, _| {
+                    this.mention_set.insert_uri(crease_id, uri);
+                })
+                .ok();
+            } else {
+                editor
+                    .update(cx, |editor, cx| {
+                        editor.display_map.update(cx, |display_map, cx| {
+                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
+                        });
+                        editor.remove_creases([crease_id], cx);
+                    })
+                    .ok();
+            }
+        })
+        .detach();
+    }
+
     pub fn contents(
         &self,
         window: &mut Window,
@@ -613,13 +833,8 @@ impl MessageEditor {
             if task.await.notify_async_err(cx).is_some() {
                 if let Some(abs_path) = abs_path.clone() {
                     this.update(cx, |this, _cx| {
-                        this.mention_set.insert_uri(
-                            crease_id,
-                            MentionUri::File {
-                                abs_path,
-                                is_directory: false,
-                            },
-                        );
+                        this.mention_set
+                            .insert_uri(crease_id, MentionUri::File { abs_path });
                     })
                     .ok();
                 }
@@ -637,104 +852,6 @@ impl MessageEditor {
         .detach();
     }
 
-    fn confirm_mention_for_thread(
-        &mut self,
-        crease_id: CreaseId,
-        anchor: Anchor,
-        id: ThreadId,
-        name: String,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let uri = MentionUri::Thread {
-            id: id.clone(),
-            name,
-        };
-        let open_task = self.thread_store.update(cx, |thread_store, cx| {
-            thread_store.open_thread(&id, window, cx)
-        });
-        let task = cx
-            .spawn(async move |_, cx| {
-                let thread = open_task.await.map_err(|e| e.to_string())?;
-                let content = thread
-                    .read_with(cx, |thread, _cx| thread.latest_detailed_summary_or_text())
-                    .map_err(|e| e.to_string())?;
-                Ok(content)
-            })
-            .shared();
-
-        self.mention_set.insert_thread(id, task.clone());
-
-        let editor = self.editor.clone();
-        cx.spawn_in(window, async move |this, cx| {
-            if task.await.notify_async_err(cx).is_some() {
-                this.update(cx, |this, _| {
-                    this.mention_set.insert_uri(crease_id, uri);
-                })
-                .ok();
-            } else {
-                editor
-                    .update(cx, |editor, cx| {
-                        editor.display_map.update(cx, |display_map, cx| {
-                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
-                        });
-                        editor.remove_creases([crease_id], cx);
-                    })
-                    .ok();
-            }
-        })
-        .detach();
-    }
-
-    fn confirm_mention_for_text_thread(
-        &mut self,
-        crease_id: CreaseId,
-        anchor: Anchor,
-        path: PathBuf,
-        name: String,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let uri = MentionUri::TextThread {
-            path: path.clone(),
-            name,
-        };
-        let context = self.text_thread_store.update(cx, |text_thread_store, cx| {
-            text_thread_store.open_local_context(path.as_path().into(), cx)
-        });
-        let task = cx
-            .spawn(async move |_, cx| {
-                let context = context.await.map_err(|e| e.to_string())?;
-                let xml = context
-                    .update(cx, |context, cx| context.to_xml(cx))
-                    .map_err(|e| e.to_string())?;
-                Ok(xml)
-            })
-            .shared();
-
-        self.mention_set.insert_text_thread(path, task.clone());
-
-        let editor = self.editor.clone();
-        cx.spawn_in(window, async move |this, cx| {
-            if task.await.notify_async_err(cx).is_some() {
-                this.update(cx, |this, _| {
-                    this.mention_set.insert_uri(crease_id, uri);
-                })
-                .ok();
-            } else {
-                editor
-                    .update(cx, |editor, cx| {
-                        editor.display_map.update(cx, |display_map, cx| {
-                            display_map.unfold_intersecting(vec![anchor..anchor], true, cx);
-                        });
-                        editor.remove_creases([crease_id], cx);
-                    })
-                    .ok();
-            }
-        })
-        .detach();
-    }
-
     pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
         self.editor.update(cx, |editor, cx| {
             editor.set_mode(mode);
@@ -817,6 +934,10 @@ impl MessageEditor {
                     self.mention_set
                         .add_fetch_result(url, Task::ready(Ok(text)).shared());
                 }
+                MentionUri::Directory { abs_path } => {
+                    let task = Task::ready(Ok(text)).shared();
+                    self.mention_set.directories.insert(abs_path, task);
+                }
                 MentionUri::File { .. }
                 | MentionUri::Symbol { .. }
                 | MentionUri::Rule { .. }
@@ -882,6 +1003,18 @@ impl MessageEditor {
     }
 }
 
+struct DirectoryContents(Arc<[(Arc<Path>, PathBuf, String)]>);
+
+impl Display for DirectoryContents {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        for (_relative_path, full_path, content) in self.0.iter() {
+            let fence = codeblock_fence_for_path(Some(full_path), None);
+            write!(f, "\n{fence}\n{content}\n```")?;
+        }
+        Ok(())
+    }
+}
+
 impl Focusable for MessageEditor {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
         self.editor.focus_handle(cx)
@@ -1064,6 +1197,7 @@ pub struct MentionSet {
     images: HashMap<CreaseId, Shared<Task<Result<MentionImage, String>>>>,
     thread_summaries: HashMap<ThreadId, Shared<Task<Result<SharedString, String>>>>,
     text_thread_summaries: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
+    directories: HashMap<PathBuf, Shared<Task<Result<String, String>>>>,
 }
 
 impl MentionSet {
@@ -1116,7 +1250,6 @@ impl MentionSet {
             .map(|(&crease_id, uri)| {
                 match uri {
                     MentionUri::File { abs_path, .. } => {
-                        // TODO directories
                         let uri = uri.clone();
                         let abs_path = abs_path.to_path_buf();
 
@@ -1141,6 +1274,24 @@ impl MentionSet {
                             anyhow::Ok((crease_id, Mention::Text { uri, content }))
                         })
                     }
+                    MentionUri::Directory { abs_path } => {
+                        let Some(content) = self.directories.get(abs_path).cloned() else {
+                            return Task::ready(Err(anyhow!("missing directory load task")));
+                        };
+                        let uri = uri.clone();
+                        cx.spawn(async move |_| {
+                            Ok((
+                                crease_id,
+                                Mention::Text {
+                                    uri,
+                                    content: content
+                                        .await
+                                        .map_err(|e| anyhow::anyhow!("{e}"))?
+                                        .to_string(),
+                                },
+                            ))
+                        })
+                    }
                     MentionUri::Symbol {
                         path, line_range, ..
                     }

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -2790,25 +2790,30 @@ impl AcpThreadView {
 
         if let Some(mention) = MentionUri::parse(&url).log_err() {
             workspace.update(cx, |workspace, cx| match mention {
-                MentionUri::File { abs_path, .. } => {
+                MentionUri::File { abs_path } => {
                     let project = workspace.project();
-                    let Some((path, entry)) = project.update(cx, |project, cx| {
+                    let Some(path) =
+                        project.update(cx, |project, cx| project.find_project_path(abs_path, cx))
+                    else {
+                        return;
+                    };
+
+                    workspace
+                        .open_path(path, None, true, window, cx)
+                        .detach_and_log_err(cx);
+                }
+                MentionUri::Directory { abs_path } => {
+                    let project = workspace.project();
+                    let Some(entry) = project.update(cx, |project, cx| {
                         let path = project.find_project_path(abs_path, cx)?;
-                        let entry = project.entry_for_path(&path, cx)?;
-                        Some((path, entry))
+                        project.entry_for_path(&path, cx)
                     }) else {
                         return;
                     };
 
-                    if entry.is_dir() {
-                        project.update(cx, |_, cx| {
-                            cx.emit(project::Event::RevealInProjectPanel(entry.id));
-                        });
-                    } else {
-                        workspace
-                            .open_path(path, None, true, window, cx)
-                            .detach_and_log_err(cx);
-                    }
+                    project.update(cx, |_, cx| {
+                        cx.emit(project::Event::RevealInProjectPanel(entry.id));
+                    });
                 }
                 MentionUri::Symbol {
                     path, line_range, ..