Allow file links in markdown & filter links a bit aggressively

Julia created

Change summary

crates/editor/src/editor.rs               | 38 +++++++++++++++++-----
crates/editor/src/element.rs              | 10 ++++-
crates/editor/src/hover_popover.rs        |  8 +++-
crates/language/src/markdown.rs           | 41 +++++++++++++++++++-----
crates/terminal_view/src/terminal_view.rs |  7 ++++
5 files changed, 81 insertions(+), 23 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -122,6 +122,7 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 pub fn render_parsed_markdown<Tag: 'static>(
     parsed: &language::ParsedMarkdown,
     editor_style: &EditorStyle,
+    workspace: Option<WeakViewHandle<Workspace>>,
     cx: &mut ViewContext<Editor>,
 ) -> Text {
     enum RenderedMarkdown {}
@@ -147,15 +148,22 @@ pub fn render_parsed_markdown<Tag: 'static>(
             region_id += 1;
             let region = parsed.regions[ix].clone();
 
-            if let Some(url) = region.link_url {
+            if let Some(link) = region.link {
                 cx.scene().push_cursor_region(CursorRegion {
                     bounds,
                     style: CursorStyle::PointingHand,
                 });
                 cx.scene().push_mouse_region(
                     MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds)
-                        .on_down::<Editor, _>(MouseButton::Left, move |_, _, cx| {
-                            cx.platform().open_url(&url)
+                        .on_down::<Editor, _>(MouseButton::Left, move |_, _, cx| match &link {
+                            markdown::Link::Web { url } => cx.platform().open_url(url),
+                            markdown::Link::Path { path } => {
+                                if let Some(workspace) = &workspace {
+                                    _ = workspace.update(cx, |workspace, cx| {
+                                        workspace.open_abs_path(path.clone(), false, cx).detach();
+                                    });
+                                }
+                            }
                         }),
                 );
             }
@@ -916,10 +924,11 @@ impl ContextMenu {
         &self,
         cursor_position: DisplayPoint,
         style: EditorStyle,
+        workspace: Option<WeakViewHandle<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> (DisplayPoint, AnyElement<Editor>) {
         match self {
-            ContextMenu::Completions(menu) => (cursor_position, menu.render(style, cx)),
+            ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)),
             ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
         }
     }
@@ -1105,7 +1114,12 @@ impl CompletionsMenu {
         !self.matches.is_empty()
     }
 
-    fn render(&self, style: EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
+    fn render(
+        &self,
+        style: EditorStyle,
+        workspace: Option<WeakViewHandle<Workspace>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> AnyElement<Editor> {
         enum CompletionTag {}
 
         let widest_completion_ix = self
@@ -1278,7 +1292,7 @@ impl CompletionsMenu {
                         Flex::column()
                             .scrollable::<MultiLineDocumentation>(0, None, cx)
                             .with_child(render_parsed_markdown::<MultiLineDocumentation>(
-                                parsed, &style, cx,
+                                parsed, &style, workspace, cx,
                             ))
                             .contained()
                             .with_style(style.autocomplete.alongside_docs_container)
@@ -3140,6 +3154,7 @@ impl Editor {
             false
         });
     }
+
     fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
         let offset = position.to_offset(buffer);
         let (word_range, kind) = buffer.surrounding_word(offset);
@@ -4215,9 +4230,14 @@ impl Editor {
         style: EditorStyle,
         cx: &mut ViewContext<Editor>,
     ) -> Option<(DisplayPoint, AnyElement<Editor>)> {
-        self.context_menu
-            .as_ref()
-            .map(|menu| menu.render(cursor_position, style, cx))
+        self.context_menu.as_ref().map(|menu| {
+            menu.render(
+                cursor_position,
+                style,
+                self.workspace.as_ref().map(|(w, _)| w.clone()),
+                cx,
+            )
+        })
     }
 
     fn show_context_menu(&mut self, menu: ContextMenu, cx: &mut ViewContext<Self>) {

crates/editor/src/element.rs 🔗

@@ -2439,9 +2439,13 @@ impl Element<Editor> for EditorElement {
         }
 
         let visible_rows = start_row..start_row + line_layouts.len() as u32;
-        let mut hover = editor
-            .hover_state
-            .render(&snapshot, &style, visible_rows, cx);
+        let mut hover = editor.hover_state.render(
+            &snapshot,
+            &style,
+            visible_rows,
+            editor.workspace.as_ref().map(|(w, _)| w.clone()),
+            cx,
+        );
         let mode = editor.mode;
 
         let mut fold_indicators = editor.render_fold_indicators(

crates/editor/src/hover_popover.rs 🔗

@@ -9,7 +9,7 @@ use gpui::{
     actions,
     elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
     platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, Element, ModelHandle, Task, ViewContext,
+    AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle,
 };
 use language::{
     markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown,
@@ -17,6 +17,7 @@ use language::{
 use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
 use std::{ops::Range, sync::Arc, time::Duration};
 use util::TryFutureExt;
+use workspace::Workspace;
 
 pub const HOVER_DELAY_MILLIS: u64 = 350;
 pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
@@ -422,6 +423,7 @@ impl HoverState {
         snapshot: &EditorSnapshot,
         style: &EditorStyle,
         visible_rows: Range<u32>,
+        workspace: Option<WeakViewHandle<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
         // If there is a diagnostic, position the popovers based on that.
@@ -451,7 +453,7 @@ impl HoverState {
             elements.push(diagnostic_popover.render(style, cx));
         }
         if let Some(info_popover) = self.info_popover.as_mut() {
-            elements.push(info_popover.render(style, cx));
+            elements.push(info_popover.render(style, workspace, cx));
         }
 
         Some((point, elements))
@@ -470,6 +472,7 @@ impl InfoPopover {
     pub fn render(
         &mut self,
         style: &EditorStyle,
+        workspace: Option<WeakViewHandle<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> AnyElement<Editor> {
         MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
@@ -478,6 +481,7 @@ impl InfoPopover {
                 .with_child(crate::render_parsed_markdown::<HoverBlock>(
                     &self.parsed_content,
                     style,
+                    workspace,
                     cx,
                 ))
                 .contained()

crates/language/src/markdown.rs 🔗

@@ -1,5 +1,5 @@
-use std::ops::Range;
 use std::sync::Arc;
+use std::{ops::Range, path::PathBuf};
 
 use crate::{HighlightId, Language, LanguageRegistry};
 use gpui::fonts::{self, HighlightStyle, Weight};
@@ -58,7 +58,28 @@ pub struct MarkdownHighlightStyle {
 #[derive(Debug, Clone)]
 pub struct ParsedRegion {
     pub code: bool,
-    pub link_url: Option<String>,
+    pub link: Option<Link>,
+}
+
+#[derive(Debug, Clone)]
+pub enum Link {
+    Web { url: String },
+    Path { path: PathBuf },
+}
+
+impl Link {
+    fn identify(text: String) -> Option<Link> {
+        if text.starts_with("http") {
+            return Some(Link::Web { url: text });
+        }
+
+        let path = PathBuf::from(text);
+        if path.is_absolute() {
+            return Some(Link::Path { path });
+        }
+
+        None
+    }
 }
 
 pub async fn parse_markdown(
@@ -115,17 +136,20 @@ pub async fn parse_markdown_block(
                     text.push_str(t.as_ref());
 
                     let mut style = MarkdownHighlightStyle::default();
+
                     if bold_depth > 0 {
                         style.weight = Weight::BOLD;
                     }
+
                     if italic_depth > 0 {
                         style.italic = true;
                     }
-                    if let Some(link_url) = link_url.clone() {
+
+                    if let Some(link) = link_url.clone().and_then(|u| Link::identify(u)) {
                         region_ranges.push(prev_len..text.len());
                         regions.push(ParsedRegion {
-                            link_url: Some(link_url),
                             code: false,
+                            link: Some(link),
                         });
                         style.underline = true;
                     }
@@ -151,7 +175,9 @@ pub async fn parse_markdown_block(
             Event::Code(t) => {
                 text.push_str(t.as_ref());
                 region_ranges.push(prev_len..text.len());
-                if link_url.is_some() {
+
+                let link = link_url.clone().and_then(|u| Link::identify(u));
+                if link.is_some() {
                     highlights.push((
                         prev_len..text.len(),
                         MarkdownHighlight::Style(MarkdownHighlightStyle {
@@ -160,10 +186,7 @@ pub async fn parse_markdown_block(
                         }),
                     ));
                 }
-                regions.push(ParsedRegion {
-                    code: true,
-                    link_url: link_url.clone(),
-                });
+                regions.push(ParsedRegion { code: true, link });
             }
 
             Event::Start(tag) => match tag {

crates/terminal_view/src/terminal_view.rs 🔗

@@ -150,11 +150,14 @@ impl TerminalView {
                 cx.notify();
                 cx.emit(Event::Wakeup);
             }
+
             Event::Bell => {
                 this.has_bell = true;
                 cx.emit(Event::Wakeup);
             }
+
             Event::BlinkChanged => this.blinking_on = !this.blinking_on,
+
             Event::TitleChanged => {
                 if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info {
                     let cwd = foreground_info.cwd.clone();
@@ -171,6 +174,7 @@ impl TerminalView {
                         .detach();
                 }
             }
+
             Event::NewNavigationTarget(maybe_navigation_target) => {
                 this.can_navigate_to_selected_word = match maybe_navigation_target {
                     Some(MaybeNavigationTarget::Url(_)) => true,
@@ -180,8 +184,10 @@ impl TerminalView {
                     None => false,
                 }
             }
+
             Event::Open(maybe_navigation_target) => match maybe_navigation_target {
                 MaybeNavigationTarget::Url(url) => cx.platform().open_url(url),
+
                 MaybeNavigationTarget::PathLike(maybe_path) => {
                     if !this.can_navigate_to_selected_word {
                         return;
@@ -246,6 +252,7 @@ impl TerminalView {
                     }
                 }
             },
+
             _ => cx.emit(event.clone()),
         })
         .detach();