Skip to main content

markdown_editor/
marks.rs

1use gpui::{Hsla, SharedString};
2
3use gpui_kit::base::text::editor_host::{LinkMark, TextMark};
4
5/// A kind of inline mark, keyed on exactly `TextMark`'s attributes (D12).
6#[non_exhaustive]
7#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
8pub enum MarkKind {
9    Bold,
10    Italic,
11    Strikethrough,
12    Underline,
13    Code,
14    Highlight,
15    Link,
16}
17
18impl MarkKind {
19    pub(crate) fn is_set(self, mark: &TextMark) -> bool {
20        match self {
21            Self::Bold => mark.bold,
22            Self::Italic => mark.italic,
23            Self::Strikethrough => mark.strikethrough,
24            Self::Underline => mark.underline,
25            Self::Code => mark.code,
26            Self::Highlight => mark.highlight.is_some(),
27            Self::Link => mark.link.is_some(),
28        }
29    }
30
31    /// Whether typing at the trailing edge of this mark extends it (D6).
32    pub(crate) fn extends_at_boundary(self) -> bool {
33        !matches!(self, Self::Link | Self::Code)
34    }
35}
36
37/// Read-only view of the marks at a position. `TextMark` stays internal.
38#[derive(Clone, Default, PartialEq, Debug)]
39pub struct MarkSet(pub(crate) TextMark);
40
41impl MarkSet {
42    pub fn has(&self, kind: MarkKind) -> bool {
43        kind.is_set(&self.0)
44    }
45
46    pub fn highlight(&self) -> Option<Hsla> {
47        self.0.highlight
48    }
49
50    pub fn link_url(&self) -> Option<&SharedString> {
51        self.0.link.as_ref().map(|link| &link.url)
52    }
53
54    pub fn is_empty(&self) -> bool {
55        self.0 == TextMark::default()
56    }
57}
58
59/// Whether a mark covers a whole range, none of it, or part of it.
60#[derive(Clone, Copy, PartialEq, Eq, Debug)]
61pub enum MarkState {
62    Off,
63    On,
64    Mixed,
65}
66
67/// A change to apply over a range. Builder with private fields.
68#[derive(Clone, Default, PartialEq, Debug)]
69pub struct MarkChange {
70    set: Vec<MarkKind>,
71    clear: Vec<MarkKind>,
72    link: Option<LinkMark>,
73    highlight: Option<Hsla>,
74}
75
76/// The highlight colour `<mark>` gets without a colour attribute.
77pub(crate) fn default_highlight() -> Hsla {
78    gpui::rgb(0xfef08a).into()
79}
80
81impl MarkChange {
82    pub fn new() -> Self {
83        Self::default()
84    }
85
86    pub fn set(mut self, kind: MarkKind) -> Self {
87        self.clear.retain(|k| *k != kind);
88        self.set.push(kind);
89        self
90    }
91
92    pub fn clear(mut self, kind: MarkKind) -> Self {
93        self.set.retain(|k| *k != kind);
94        self.clear.push(kind);
95        self
96    }
97
98    pub fn with_link(mut self, url: impl Into<SharedString>, title: Option<SharedString>) -> Self {
99        self.link = Some(LinkMark {
100            url: url.into(),
101            identifier: None,
102            title,
103        });
104        self.set(MarkKind::Link)
105    }
106
107    pub fn with_highlight(mut self, color: Hsla) -> Self {
108        self.highlight = Some(color);
109        self.set(MarkKind::Highlight)
110    }
111
112    pub(crate) fn apply(&self, mark: &TextMark) -> TextMark {
113        let mut out = self.apply_raw(mark);
114        // Inline code holds literal text: only a link may wrap it, so other
115        // marks never apply inside code, and making text code drops them.
116        if out.code {
117            out.bold = false;
118            out.italic = false;
119            out.strikethrough = false;
120            out.underline = false;
121            out.highlight = None;
122        }
123        out
124    }
125
126    fn apply_raw(&self, mark: &TextMark) -> TextMark {
127        let mut out = mark.clone();
128        for kind in &self.clear {
129            match kind {
130                MarkKind::Bold => out.bold = false,
131                MarkKind::Italic => out.italic = false,
132                MarkKind::Strikethrough => out.strikethrough = false,
133                MarkKind::Underline => out.underline = false,
134                MarkKind::Code => out.code = false,
135                MarkKind::Highlight => out.highlight = None,
136                MarkKind::Link => out.link = None,
137            }
138        }
139        for kind in &self.set {
140            match kind {
141                MarkKind::Bold => out.bold = true,
142                MarkKind::Italic => out.italic = true,
143                MarkKind::Strikethrough => out.strikethrough = true,
144                MarkKind::Underline => out.underline = true,
145                MarkKind::Code => out.code = true,
146                MarkKind::Highlight => {
147                    out.highlight = Some(self.highlight.unwrap_or_else(default_highlight))
148                }
149                MarkKind::Link => {
150                    if let Some(link) = &self.link {
151                        out.link = Some(link.clone());
152                    }
153                }
154            }
155        }
156        out
157    }
158}