1use std::{collections::HashSet, rc::Rc};
8
9use gpui::{App, Global, Window};
10
11use crate::{BlockKind, Doc, render::OnJump};
12
13pub type LinkHandler = fn(url: &str, &mut Window, &mut App);
15
16struct Installed(LinkHandler);
17
18impl Global for Installed {}
19
20pub(crate) fn set_link_handler(cx: &mut App, handler: LinkHandler) {
22 cx.set_global(Installed(handler));
23}
24
25pub(crate) fn open(url: &str, window: &mut Window, cx: &mut App) {
26 match cx.try_global::<Installed>().map(|installed| installed.0) {
27 Some(handler) => handler(url, window, cx),
28 None => cx.open_url(url),
29 }
30}
31
32pub fn slug(text: &str) -> String {
35 text.chars()
36 .flat_map(char::to_lowercase)
37 .filter_map(|c| match c {
38 ' ' => Some('-'),
39 '-' | '_' => Some(c),
40 c if c.is_alphanumeric() => Some(c),
41 _ => None,
42 })
43 .collect()
44}
45
46pub fn heading(doc: &Doc, fragment: &str) -> Option<usize> {
50 anchors(doc)
51 .into_iter()
52 .find_map(|(anchor, block)| (anchor == fragment).then_some(block))
53}
54
55fn anchors(doc: &Doc) -> Vec<(String, usize)> {
56 let mut taken = HashSet::new();
57 doc.blocks
58 .iter()
59 .enumerate()
60 .filter_map(|(ix, block)| match &block.kind {
61 BlockKind::Heading { text, .. } => Some((slug(&text.text), ix)),
62 _ => None,
63 })
64 .map(|(base, ix)| {
65 let anchor = std::iter::once(base.clone())
66 .chain((1..).map(|n| format!("{base}-{n}")))
67 .find(|anchor| !taken.contains(anchor))
68 .expect("an unbounded run of suffixes");
69 taken.insert(anchor.clone());
70 (anchor, ix)
71 })
72 .collect()
73}
74
75#[derive(Clone)]
78pub(crate) struct Jump {
79 to: OnJump,
80 anchors: Rc<[(String, usize)]>,
81}
82
83impl Jump {
84 pub(crate) fn new(doc: &Doc, to: OnJump) -> Self {
85 Self {
86 to,
87 anchors: anchors(doc).into(),
88 }
89 }
90}
91
92pub(crate) fn follow(url: &str, jump: Option<&Jump>, window: &mut Window, cx: &mut App) {
95 let Some(fragment) = url.strip_prefix('#') else {
96 open(url, window, cx);
97 return;
98 };
99 if let Some(jump) = jump
100 && let Some(&(_, block)) = jump.anchors.iter().find(|(anchor, _)| anchor == fragment)
101 {
102 (jump.to)(block, window, cx);
103 }
104}