use std::{collections::HashSet, path::Path, rc::Rc};
use gpui::{AnyElement, App, Global, Window};
use crate::{BlockKind, Doc, Form, Preview, render::OnJump};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Link<'a> {
pub url: &'a str,
pub base: Option<&'a Path>,
}
impl<'a> Link<'a> {
pub fn new(url: &'a str, base: Option<&'a Path>) -> Self {
Self { url, base }
}
pub fn is_web(&self) -> bool {
let scheme = self.url.split_once("://").map_or("", |(scheme, _)| scheme);
scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
}
}
pub trait Links: 'static {
fn open(link: Link<'_>, _window: &mut Window, cx: &mut App) {
cx.open_url(link.url);
}
fn preview(_link: Link<'_>, _cx: &App) -> Option<Preview> {
None
}
fn card(
_link: Link<'_>,
_form: Form,
_window: &mut Window,
_cx: &mut App,
) -> Option<AnyElement> {
None
}
fn hover(link: Link<'_>, _window: &mut Window, cx: &mut App) -> Option<AnyElement> {
link.is_web()
.then(|| crate::render::hover_bookmark(link, cx))
}
}
struct Unhandled;
impl Links for Unhandled {}
#[derive(Clone, Copy)]
struct Installed {
open: fn(Link<'_>, &mut Window, &mut App),
preview: fn(Link<'_>, &App) -> Option<Preview>,
card: fn(Link<'_>, Form, &mut Window, &mut App) -> Option<AnyElement>,
hover: fn(Link<'_>, &mut Window, &mut App) -> Option<AnyElement>,
}
impl Global for Installed {}
impl Installed {
fn of<L: Links>() -> Self {
Self {
open: L::open,
preview: L::preview,
card: L::card,
hover: L::hover,
}
}
fn get(cx: &App) -> Self {
cx.try_global::<Self>()
.copied()
.unwrap_or_else(Self::of::<Unhandled>)
}
}
pub(crate) fn set_links<L: Links>(cx: &mut App) {
cx.set_global(Installed::of::<L>());
}
pub(crate) fn open(link: Link<'_>, window: &mut Window, cx: &mut App) {
(Installed::get(cx).open)(link, window, cx)
}
pub(crate) fn preview(link: Link<'_>, cx: &App) -> Option<Preview> {
(Installed::get(cx).preview)(link, cx)
}
pub(crate) fn card(
link: Link<'_>,
form: Form,
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
(Installed::get(cx).card)(link, form, window, cx)
}
pub(crate) fn hover(link: Link<'_>, window: &mut Window, cx: &mut App) -> Option<AnyElement> {
(Installed::get(cx).hover)(link, window, cx)
}
pub fn slug(text: &str) -> String {
text.chars()
.flat_map(char::to_lowercase)
.filter_map(|c| match c {
' ' => Some('-'),
'-' | '_' => Some(c),
c if c.is_alphanumeric() => Some(c),
_ => None,
})
.collect()
}
pub fn heading(doc: &Doc, fragment: &str) -> Option<usize> {
anchors(doc)
.into_iter()
.find_map(|(anchor, block)| (anchor == fragment).then_some(block))
}
fn anchors(doc: &Doc) -> Vec<(String, usize)> {
let mut taken = HashSet::new();
doc.blocks
.iter()
.enumerate()
.filter_map(|(ix, block)| match &block.kind {
BlockKind::Heading { text, .. } => Some((slug(&text.text), ix)),
_ => None,
})
.map(|(base, ix)| {
let anchor = std::iter::once(base.clone())
.chain((1..).map(|n| format!("{base}-{n}")))
.find(|anchor| !taken.contains(anchor))
.expect("an unbounded run of suffixes");
taken.insert(anchor.clone());
(anchor, ix)
})
.collect()
}
#[derive(Clone)]
pub(crate) struct Jump {
to: OnJump,
anchors: Rc<[(String, usize)]>,
}
impl Jump {
pub(crate) fn new(doc: &Doc, to: OnJump) -> Self {
Self {
to,
anchors: anchors(doc).into(),
}
}
}
pub(crate) fn follow(link: Link<'_>, jump: Option<&Jump>, window: &mut Window, cx: &mut App) {
let Some(fragment) = link.url.strip_prefix('#') else {
open(link, window, cx);
return;
};
if let Some(jump) = jump
&& let Some(&(_, block)) = jump.anchors.iter().find(|(anchor, _)| anchor == fragment)
{
(jump.to)(block, window, cx);
}
}