1use std::{path::Path, rc::Rc};
10
11use gpui::{AnyElement, App, Global, Window};
12
13use crate::{Doc, Form, Preview, render::OnJump};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct Link<'a> {
19 pub url: &'a str,
21 pub base: Option<&'a Path>,
23}
24
25impl<'a> Link<'a> {
26 pub fn new(url: &'a str, base: Option<&'a Path>) -> Self {
27 Self { url, base }
28 }
29
30 pub fn is_web(&self) -> bool {
32 let scheme = self.url.split_once("://").map_or("", |(scheme, _)| scheme);
33 scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
34 }
35}
36
37pub trait Links: 'static {
42 fn open(link: Link<'_>, _window: &mut Window, cx: &mut App) {
44 cx.open_url(link.url);
45 }
46
47 fn preview(_link: Link<'_>, _cx: &App) -> Option<Preview> {
51 None
52 }
53
54 fn card(
60 _link: Link<'_>,
61 _form: Form,
62 _window: &mut Window,
63 _cx: &mut App,
64 ) -> Option<AnyElement> {
65 None
66 }
67
68 fn hover(link: Link<'_>, _window: &mut Window, cx: &mut App) -> Option<AnyElement> {
71 link.is_web()
72 .then(|| crate::render::hover_bookmark(link, cx))
73 }
74}
75
76struct Unhandled;
78
79impl Links for Unhandled {}
80
81#[derive(Clone, Copy)]
82struct Installed {
83 open: fn(Link<'_>, &mut Window, &mut App),
84 preview: fn(Link<'_>, &App) -> Option<Preview>,
85 card: fn(Link<'_>, Form, &mut Window, &mut App) -> Option<AnyElement>,
86 hover: fn(Link<'_>, &mut Window, &mut App) -> Option<AnyElement>,
87}
88
89impl Global for Installed {}
90
91impl Installed {
92 fn of<L: Links>() -> Self {
93 Self {
94 open: L::open,
95 preview: L::preview,
96 card: L::card,
97 hover: L::hover,
98 }
99 }
100
101 fn get(cx: &App) -> Self {
102 cx.try_global::<Self>()
103 .copied()
104 .unwrap_or_else(Self::of::<Unhandled>)
105 }
106}
107
108pub(crate) fn set_links<L: Links>(cx: &mut App) {
109 cx.set_global(Installed::of::<L>());
110}
111
112pub(crate) fn open(link: Link<'_>, window: &mut Window, cx: &mut App) {
113 (Installed::get(cx).open)(link, window, cx)
114}
115
116pub(crate) fn preview(link: Link<'_>, cx: &App) -> Option<Preview> {
117 (Installed::get(cx).preview)(link, cx)
118}
119
120pub(crate) fn card(
121 link: Link<'_>,
122 form: Form,
123 window: &mut Window,
124 cx: &mut App,
125) -> Option<AnyElement> {
126 (Installed::get(cx).card)(link, form, window, cx)
127}
128
129pub(crate) fn hover(link: Link<'_>, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
130 (Installed::get(cx).hover)(link, window, cx)
131}
132
133pub fn heading(doc: &Doc, fragment: &str) -> Option<usize> {
137 doc.anchors()
138 .into_iter()
139 .find_map(|(anchor, block)| (anchor == fragment).then_some(block))
140}
141
142#[derive(Clone)]
145pub(crate) struct Jump {
146 to: OnJump,
147 anchors: Rc<[(String, usize)]>,
148}
149
150impl Jump {
151 pub(crate) fn new(doc: &Doc, to: OnJump) -> Self {
152 Self {
153 to,
154 anchors: doc.anchors().into(),
155 }
156 }
157}
158
159pub(crate) fn follow(link: Link<'_>, jump: Option<&Jump>, window: &mut Window, cx: &mut App) {
162 let Some(fragment) = link.url.strip_prefix('#') else {
163 open(link, window, cx);
164 return;
165 };
166 if let Some(jump) = jump
167 && let Some(&(_, block)) = jump.anchors.iter().find(|(anchor, _)| anchor == fragment)
168 {
169 (jump.to)(block, window, cx);
170 }
171}