WIP

Mikayla Maki and Nathan created

co-authored-by: Nathan <nathan@zed.dev>

Change summary

crates/acp/src/thread_view.rs   |  8 ++
crates/markdown/src/markdown.rs | 95 +++++++++++++++++++++++++++++++++-
crates/project/src/direnv.rs    |  2 
3 files changed, 97 insertions(+), 8 deletions(-)

Detailed changes

crates/acp/src/thread_view.rs 🔗

@@ -1,5 +1,6 @@
-use std::path::Path;
+use std::path::{Path, PathBuf};
 use std::rc::Rc;
+use std::sync::Arc;
 use std::time::Duration;
 
 use agentic_coding_protocol::{self as acp, ToolCallConfirmation};
@@ -32,6 +33,7 @@ pub struct AcpThreadView {
     message_editor: Entity<Editor>,
     list_state: ListState,
     send_task: Option<Task<Result<()>>>,
+    root: Arc<Path>,
 }
 
 enum ThreadState {
@@ -47,6 +49,7 @@ enum ThreadState {
 
 impl AcpThreadView {
     pub fn new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+        // todo!(): This should probably be contextual, like the terminal
         let Some(root_dir) = project
             .read(cx)
             .visible_worktrees(cx)
@@ -62,7 +65,7 @@ impl AcpThreadView {
         let child = util::command::new_smol_command("node")
             .arg(cli_path)
             .arg("--acp")
-            .current_dir(root_dir)
+            .current_dir(&root_dir)
             .stdin(std::process::Stdio::piped())
             .stdout(std::process::Stdio::piped())
             .stderr(std::process::Stdio::inherit())
@@ -146,6 +149,7 @@ impl AcpThreadView {
             message_editor,
             send_task: None,
             list_state: list_state,
+            root: root_dir,
         }
     }
 

crates/markdown/src/markdown.rs 🔗

@@ -199,6 +199,9 @@ impl Markdown {
         self.pending_parse.is_some()
     }
 
+    // Chunks: `Mark|down.md` You need to reparse every back tick, everytime
+    // `[foo.rs](foo.rs)` [`foo.rs`](foo.rs)  `ba|r.rs`
+
     pub fn source(&self) -> &str {
         &self.source
     }
@@ -439,11 +442,25 @@ impl ParsedMarkdown {
     }
 }
 
+// pub trait TextClickHandler {
+//     fn pattern(&self) ->
+//     fn hovered(&mut self, text: &str) -> bool;
+//     fn clicked(&mut self, text: &str);
+// }
+// const WORD_REGEX: &str =
+//     r#"[\$\+\w.\[\]:/\\@\-~()]+(?:\((?:\d+|\d+,\d+)\))|[\$\+\w.\[\]:/\\@\-~()]+"#;
+
+pub struct UrlHandler {
+    pub on_hover: Box<dyn Fn(&str, &mut Window, &mut App) -> bool>,
+    pub on_click: Box<dyn Fn(&str, &mut Window, &mut App)>,
+}
+
 pub struct MarkdownElement {
     markdown: Entity<Markdown>,
     style: MarkdownStyle,
     code_block_renderer: CodeBlockRenderer,
-    on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
+    on_link_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
+    url_handler: Option<UrlHandler>,
 }
 
 impl MarkdownElement {
@@ -456,7 +473,8 @@ impl MarkdownElement {
                 copy_button_on_hover: false,
                 border: false,
             },
-            on_url_click: None,
+            on_link_click: None,
+            url_handler: None,
         }
     }
 
@@ -490,7 +508,12 @@ impl MarkdownElement {
         mut self,
         handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
     ) -> Self {
-        self.on_url_click = Some(Box::new(handler));
+        self.on_link_click = Some(Box::new(handler));
+        self
+    }
+
+    pub fn handle_urls(mut self, handler: UrlHandler) -> Self {
+        self.url_handler = Some(handler);
         self
     }
 
@@ -580,7 +603,7 @@ impl MarkdownElement {
             window.set_cursor_style(CursorStyle::IBeam, hitbox);
         }
 
-        let on_open_url = self.on_url_click.take();
+        let on_open_url = self.on_link_click.take();
 
         self.on_mouse_event(window, cx, {
             let rendered_text = rendered_text.clone();
@@ -591,6 +614,8 @@ impl MarkdownElement {
                         if let Some(link) = rendered_text.link_for_position(event.position) {
                             markdown.pressed_link = Some(link.clone());
                         } else {
+                            // if
+
                             let source_index =
                                 match rendered_text.source_index_for_position(event.position) {
                                     Ok(ix) | Err(ix) => ix,
@@ -1798,8 +1823,10 @@ impl RenderedText {
 
 #[cfg(test)]
 mod tests {
+    use std::cell::RefCell;
+
     use super::*;
-    use gpui::{TestAppContext, size};
+    use gpui::{Modifiers, MouseButton, TestAppContext, size};
 
     #[gpui::test]
     fn test_mappings(cx: &mut TestAppContext) {
@@ -1881,6 +1908,64 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    fn test_url_handling(cx: &mut TestAppContext) {
+        let markdown = r#"hello `world`
+        Check out `https://zed.dev` for a great editor!
+        Also available locally: crates/ README.md,
+        "#;
+
+        struct TestWindow;
+
+        impl Render for TestWindow {
+            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
+                div()
+            }
+        }
+
+        let (_, cx) = cx.add_window_view(|_, _| TestWindow);
+        let markdown = cx.new(|cx| Markdown::new(markdown.to_string().into(), None, None, cx));
+        cx.run_until_parked();
+
+        let paths_hovered = Rc::new(RefCell::new(Vec::new()));
+        let paths_clicked = Rc::new(RefCell::new(Vec::new()));
+
+        let handler = {
+            let paths_hovered = paths_hovered.clone();
+            let paths_clicked = paths_clicked.clone();
+
+            UrlHandler {
+                on_hover: Box::new(move |path, _window, _app| {
+                    paths_hovered.borrow_mut().push(path.to_string());
+                    true
+                }),
+                on_click: Box::new(move |path, _window, _app| {
+                    paths_clicked.borrow_mut().push(path.to_string());
+                }),
+            }
+        };
+
+        let (rendered, _) = cx.draw(
+            Default::default(),
+            size(px(600.0), px(600.0)),
+            |_window, _cx| {
+                MarkdownElement::new(markdown, MarkdownStyle::default()).handle_urls(handler)
+            },
+        );
+
+        cx.simulate_mouse_move(
+            point(px(0.0), px(0.0)),
+            MouseButton::Left,
+            Modifiers::default(),
+        );
+
+        assert_eq!(paths_hovered.borrow().len(), 1)
+    }
+
+    // To have a markdown document with paths and links in it
+    // We want to run a function
+    // and we want to get those paths and links out?
+
     #[track_caller]
     fn assert_mappings(rendered: &RenderedText, expected: Vec<Vec<(usize, usize)>>) {
         assert_eq!(rendered.lines.len(), expected.len(), "line count mismatch");

crates/project/src/direnv.rs 🔗

@@ -65,7 +65,7 @@ pub async fn load_direnv_environment(
     let output = String::from_utf8_lossy(&direnv_output.stdout);
     if output.is_empty() {
         // direnv outputs nothing when it has no changes to apply to environment variables
-        return Ok(HashMap::new());
+        return Ok(HashMap::default());
     }
 
     match serde_json::from_str(&output) {