1use std::{collections::HashSet, path::Path, rc::Rc};
10
11use gpui::{AnyElement, App, Global, Window};
12
13use crate::{BlockKind, 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 slug(text: &str) -> String {
136 text.chars()
137 .flat_map(char::to_lowercase)
138 .filter_map(|c| match c {
139 ' ' => Some('-'),
140 '-' | '_' => Some(c),
141 c if c.is_alphanumeric() => Some(c),
142 _ => None,
143 })
144 .collect()
145}
146
147pub fn heading(doc: &Doc, fragment: &str) -> Option<usize> {
151 anchors(doc)
152 .into_iter()
153 .find_map(|(anchor, block)| (anchor == fragment).then_some(block))
154}
155
156fn anchors(doc: &Doc) -> Vec<(String, usize)> {
157 let mut taken = HashSet::new();
158 doc.blocks
159 .iter()
160 .enumerate()
161 .filter_map(|(ix, block)| match &block.kind {
162 BlockKind::Heading { text, .. } => Some((slug(&text.text), ix)),
163 _ => None,
164 })
165 .map(|(base, ix)| {
166 let anchor = std::iter::once(base.clone())
167 .chain((1..).map(|n| format!("{base}-{n}")))
168 .find(|anchor| !taken.contains(anchor))
169 .expect("an unbounded run of suffixes");
170 taken.insert(anchor.clone());
171 (anchor, ix)
172 })
173 .collect()
174}
175
176#[derive(Clone)]
179pub(crate) struct Jump {
180 to: OnJump,
181 anchors: Rc<[(String, usize)]>,
182}
183
184impl Jump {
185 pub(crate) fn new(doc: &Doc, to: OnJump) -> Self {
186 Self {
187 to,
188 anchors: anchors(doc).into(),
189 }
190 }
191}
192
193pub(crate) fn follow(link: Link<'_>, jump: Option<&Jump>, window: &mut Window, cx: &mut App) {
196 let Some(fragment) = link.url.strip_prefix('#') else {
197 open(link, window, cx);
198 return;
199 };
200 if let Some(jump) = jump
201 && let Some(&(_, block)) = jump.anchors.iter().find(|(anchor, _)| anchor == fragment)
202 {
203 (jump.to)(block, window, cx);
204 }
205}