Vectrum
Sign In
1066 lines · 39.6 KB · Rust
//! The rich text composer: Markdown styled live as you type.
//!
//! The text stays plain Markdown (that is what gets sent); only its styling
//! changes. Headings and `**strong**` turn bold, `*emphasis*` italic, code
//! sits on a faint well, quotes and fenced blocks are muted, list markers,
//! links and a leading `/command` or `$skill` are tinted, and the syntax
//! characters themselves fade back.
//!
//! gpui-base only paints highlighter runs in its code-editor layout, so the
//! rich composer is an [`EditorState`] dressed as the plain composer: no
//! gutter, folding, indent guides, auto-closing or smart indent, soft wrap on,
//! and [`MarkdownInput`] grows it from two to twelve rows the way
//! `TextareaState::auto_grow` does. [`MarkdownHighlighter`] supplies the runs.
//!
//! ```ignore
//! let composer = cx.new(|cx| {
//!     markdown_input::composer_state(EditorState::new(window, cx), rich)
//!         .submit_on_enter(true)
//!         .placeholder("Ask for changes")
//! });
//! // render:  MarkdownInput::new(&composer)
//! // toggle:  markdown_input::enable(&composer, on, cx)
//! ```

use std::{cell::Cell, ops::Range, rc::Rc};

use gpui_kit::component::input::{
    self, Editor, EditorState, FoldRange, HighlightStyleResolver, InputEdit, InputHighlighter,
    InputHighlighterFactory, Rope, RopeExt as _,
};
use gpui_kit::component::native_menu::NativeMenu;
use gpui_kit::*;

use crate::ui::metal::{self, Palette};

/// What a byte of Markdown is, as bit flags; a byte can be several at once
/// (bold inside a heading, a delimiter of inline code).
mod flag {
    pub const HEADING: u16 = 1 << 0;
    pub const STRONG: u16 = 1 << 1;
    pub const EMPHASIS: u16 = 1 << 2;
    /// Inline code, delimiters included.
    pub const CODE: u16 = 1 << 3;
    /// A line inside a fenced code block.
    pub const CODE_BLOCK: u16 = 1 << 4;
    /// The opening or closing line of a fence.
    pub const FENCE: u16 = 1 << 5;
    /// Text inside a block quote.
    pub const QUOTE: u16 = 1 << 6;
    /// A list bullet, number or task box.
    pub const LIST_MARKER: u16 = 1 << 7;
    pub const LINK_TEXT: u16 = 1 << 8;
    pub const LINK_URL: u16 = 1 << 9;
    /// A leading `/command` or `$skill`.
    pub const COMMAND: u16 = 1 << 10;
    /// Markdown syntax: `#`, `**`, `` ` ``, `>`, brackets, escapes.
    pub const SYNTAX: u16 = 1 << 11;
}

/// A styled run of the document: a byte range and its [`flag`]s.
type Span = (Range<usize>, u16);

thread_local! {
    /// The theme the composer paints with. [`InputHighlighter::styles`]
    /// gets no `App`, so [`MarkdownInput`] records the palette here on every
    /// render, ahead of the editor's paint in the same frame. One theme is
    /// active app-wide, so one cell serves every composer.
    static PALETTE: Cell<Option<&'static Palette>> = const { Cell::new(None) };
}

fn active_palette() -> &'static Palette {
    PALETTE.with(Cell::get).unwrap_or(&metal::BONE)
}

/// Turns Markdown into highlight runs for an [`EditorState`].
///
/// Every edit rescans the whole document. A hand-written line scanner gets
/// through a few hundred lines in microseconds, and a full rescan keeps fenced
/// blocks right however an edit opens or closes them.
#[derive(Default)]
pub struct MarkdownHighlighter {
    /// Styled runs over the whole document, ordered and non-overlapping.
    /// Unstyled text has no entry.
    spans: Vec<Span>,
}

impl MarkdownHighlighter {
    pub fn new() -> Self {
        Self::default()
    }

    fn rescan(&mut self, text: &str) {
        self.spans = scan(text);
    }

    /// The highlight runs for `range`, styled with `palette`.
    fn styles_with(&self, range: &Range<usize>, palette: &Palette) -> Vec<(Range<usize>, HighlightStyle)> {
        let mut runs = Vec::new();
        let mut at = range.start;
        let first = self.spans.partition_point(|(span, _)| span.end <= range.start);
        for (span, flags) in &self.spans[first..] {
            if span.start >= range.end {
                break;
            }
            let start = span.start.max(range.start);
            let end = span.end.min(range.end);
            if start > at {
                runs.push((at..start, HighlightStyle::default()));
            }
            runs.push((start..end, style_for(*flags, palette)));
            at = end;
        }
        if at < range.end {
            runs.push((at..range.end, HighlightStyle::default()));
        }
        runs
    }
}

impl InputHighlighter for MarkdownHighlighter {
    fn language(&self) -> SharedString {
        "markdown".into()
    }

    fn update(
        &mut self,
        _edit: Option<InputEdit>,
        text: &Rope,
        _folding: bool,
        _window: &mut Window,
        cx: &mut Context<EditorState>,
    ) {
        PALETTE.with(|cell| cell.set(Some(metal::palette(cx))));
        self.rescan(&text.to_string());
    }

    fn update_batch(
        &mut self,
        edits: &[(InputEdit, Rope)],
        _folding: bool,
        _window: &mut Window,
        _cx: &mut Context<EditorState>,
    ) {
        // Only the text after the last edit matters to a full rescan.
        if let Some((_, text)) = edits.last() {
            self.rescan(&text.to_string());
        }
    }

    fn styles(
        &self,
        range: &Range<usize>,
        _resolver: &dyn HighlightStyleResolver,
    ) -> Vec<(Range<usize>, HighlightStyle)> {
        // The resolver is gpui-component's syntax theme, which the `Editor`
        // element re-installs on every render, so the colors come from the
        // MetalUI palette instead.
        self.styles_with(range, active_palette())
    }

    fn fold_ranges(&self, _text: &Rope) -> Vec<FoldRange> {
        Vec::new()
    }
}

/// The highlighter factory for an [`EditorState`]: Markdown when `on`, plain
/// text otherwise. Installing a factory (even the plain one) also keeps
/// gpui-component's tree-sitter factory out.
pub fn highlighter_factory(on: bool) -> InputHighlighterFactory {
    if on {
        Rc::new(|_| Some(Box::new(MarkdownHighlighter::new()) as Box<dyn InputHighlighter>))
    } else {
        Rc::new(|_| None)
    }
}

/// Dresses a fresh [`EditorState`] as the composer: no line numbers, gutter,
/// folding or indent guides, no bracket pairing or smart indent, no search
/// panel or scrolling past the end, soft wrap on, and Markdown styling when
/// `rich`. Chain `submit_on_enter` and `placeholder` after it as for a
/// `TextareaState`, and render it with [`MarkdownInput`].
pub fn composer_state(state: EditorState, rich: bool) -> EditorState {
    let mut state = state
        .line_number(false)
        .folding(false)
        .indent_guides(false)
        .auto_close(false)
        .smart_indent(false)
        .soft_wrap(true)
        .searchable(false)
        .scroll_beyond_last_line(Some(0));
    state.ensure_highlighter_factory(highlighter_factory(rich));
    state
}

/// Turns Markdown styling on or off, live.
pub fn enable(state: &Entity<EditorState>, on: bool, cx: &mut App) {
    state.update(cx, |state, cx| state.set_highlighter_factory(highlighter_factory(on), cx));
}

/// Vertical padding the input puts around its text: gpui-component's
/// `Size::Medium.input_py()`, the same as the plain composer's.
const PADDING_Y: f32 = 8.;
/// How much further right an editor starts its text than a textarea: it pads
/// 6px instead of 10px, plus a 6px gap where the line numbers would end.
const EDITOR_INDENT: f32 = 2.;

/// Renders a [`composer_state`] editor as the plain composer would render its
/// `Textarea`: borderless, in the UI font at the textarea's size and leading,
/// growing with its content between `rows` (default 2 to 12).
#[derive(IntoElement)]
pub struct MarkdownInput {
    state: Entity<EditorState>,
    min_rows: usize,
    max_rows: usize,
}

impl MarkdownInput {
    pub fn new(state: &Entity<EditorState>) -> Self {
        Self { state: state.clone(), min_rows: 2, max_rows: 12 }
    }

    /// Grow with the content from `min` through `max` rows.
    pub fn rows(mut self, min: usize, max: usize) -> Self {
        self.min_rows = min.max(1);
        self.max_rows = max.max(self.min_rows);
        self
    }
}

impl RenderOnce for MarkdownInput {
    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
        PALETTE.with(|cell| cell.set(Some(metal::palette(cx))));

        let (min, max) = (self.min_rows, self.max_rows);
        let state = self.state.read(cx);
        let line_height = state.line_height().unwrap_or_else(|| rems(1.25).to_pixels(window.rem_size()));
        let rows = content_rows(state).unwrap_or(min).clamp(min, max);

        // The rows above come from the last layout. Once this frame has laid
        // the text out, measure again and redraw if the content grew or shrank.
        let entity = self.state.clone();
        window.on_next_frame(move |_, cx| {
            let measured = content_rows(entity.read(cx)).map(|now| now.clamp(min, max));
            if measured.is_some_and(|now| now != rows) {
                cx.notify(entity.entity_id());
            }
        });

        let menu_state = self.state.clone();
        Editor::new(&self.state)
            .appearance(false)
            .h(line_height * rows as f32 + px(PADDING_Y * 2.))
            .context_menu(move |menu, window, cx| {
                // The builder runs while the editor is mid-update, where
                // reading its state panics, so the real menu waits a tick
                // and the empty one returned here shows nothing.
                let state = menu_state.clone();
                window.defer(cx, move |window, cx| {
                    let position = window.mouse_position();
                    context_menu(&state, NativeMenu::new(), cx).show(position, window, cx);
                });
                menu
            })
            .font_family(metal::SANS)
            .text_size(rems(0.875))
            .line_height(rems(1.25))
            .ml(px(-EDITOR_INDENT))
    }
}

/// The plain text menu. The editor's own adds code navigation entries.
fn context_menu(state: &Entity<EditorState>, menu: NativeMenu, cx: &App) -> NativeMenu {
    let state = state.read(cx);
    let editable = state.is_editable();
    let selected = !state.selected_range().is_empty();
    menu.menu_with_disabled("Cut", !(editable && selected), Box::new(input::Cut))
        .menu_with_disabled("Copy", !selected, Box::new(input::Copy))
        .menu_with_disabled("Paste", !editable, Box::new(input::Paste))
        .separator()
        .menu("Select All", Box::new(input::SelectAll))
}

/// How many rows the editor's text takes up, soft wraps included, from its
/// last layout. `None` before the first layout.
///
/// The laid-out lines are measured exactly; lines scrolled out of view count
/// one row each, which only ever undercounts, so growing converges in a frame
/// or two.
fn content_rows(state: &EditorState) -> Option<usize> {
    let line_height = state.line_height()?;
    let text = state.text();
    let lines = text.lines_len().max(1);
    let visible = state.visible_row_range()?;
    let visible = visible.start.min(lines)..visible.end.min(lines);
    if visible.is_empty() {
        return None;
    }
    let start = text.line_start_offset(visible.start);
    let end = text.line_end_offset(visible.end - 1);
    let top = state.range_to_bounds(&(start..start))?.top();
    let bottom = state.range_to_bounds(&(end..end))?.bottom();
    let measured = ((bottom - top) / line_height).round().max(1.) as usize;
    Some(measured + lines - visible.len())
}

fn style_for(flags: u16, p: &Palette) -> HighlightStyle {
    let mut style = HighlightStyle::default();
    if flags & (flag::HEADING | flag::STRONG) != 0 {
        style.font_weight = Some(FontWeight::BOLD);
    }
    if flags & flag::EMPHASIS != 0 {
        // Geist ships no italic face, and GPUI on Linux does not slant one,
        // so emphasis also gets a softer ink to stay visible.
        style.font_style = Some(FontStyle::Italic);
        style.color = Some(p.ink2);
    }
    if flags & flag::QUOTE != 0 {
        style.color = Some(p.ink2);
    }
    if flags & flag::LIST_MARKER != 0 {
        style.color = Some(p.amber_ink);
    }
    if flags & flag::COMMAND != 0 {
        style.color = Some(p.green_ink);
        style.font_weight = Some(FontWeight::MEDIUM);
    }
    if flags & flag::LINK_TEXT != 0 {
        style.color = Some(p.blue_ink);
        style.underline =
            Some(UnderlineStyle { thickness: px(1.), color: Some(p.blue_ink.opacity(0.5)), wavy: false });
    }
    if flags & flag::LINK_URL != 0 {
        style.color = Some(p.ink3);
    }
    if flags & flag::CODE != 0 {
        style.color = Some(p.ink2);
        style.background_color = Some(p.well_top);
    }
    if flags & flag::SYNTAX != 0 {
        style.color = Some(p.ink3);
    }
    if flags & flag::CODE_BLOCK != 0 {
        style.color = Some(p.ink2);
        style.font_weight = None;
        style.font_style = None;
    }
    if flags & flag::FENCE != 0 {
        style.color = Some(p.ink3);
    }
    style
}

// ---------------------------------------------------------------------------
// The scanner
// ---------------------------------------------------------------------------

/// An open code fence: its character and length.
#[derive(Clone, Copy)]
struct Fence {
    ch: u8,
    len: usize,
}

/// Scans `text` into styled runs: ordered, non-overlapping, on UTF-8 char
/// boundaries, unstyled text left out.
fn scan(text: &str) -> Vec<Span> {
    let mut spans = Vec::new();
    let mut fence = None;
    let mut leading = true;
    let mut flags = Vec::new();
    let mut literal = Vec::new();
    let mut offset = 0;
    for raw in text.split('\n') {
        let line = raw.strip_suffix('\r').unwrap_or(raw);
        flags.clear();
        flags.resize(line.len(), 0u16);
        literal.clear();
        literal.resize(line.len(), false);
        // The message is sent trimmed, so its first non-blank line leads.
        let leads = leading && !line.trim().is_empty();
        scan_line(line, &mut fence, leads, &mut flags, &mut literal);
        leading &= !leads;
        push_runs(&mut spans, offset, &flags);
        offset += raw.len() + 1;
    }
    spans
}

/// Appends the runs of equal, non-zero flags in one line.
fn push_runs(spans: &mut Vec<Span>, offset: usize, flags: &[u16]) {
    let mut start = 0;
    while start < flags.len() {
        let value = flags[start];
        let mut end = start + 1;
        while end < flags.len() && flags[end] == value {
            end += 1;
        }
        if value != 0 {
            spans.push((offset + start..offset + end, value));
        }
        start = end;
    }
}

fn mark(flags: &mut [u16], range: Range<usize>, value: u16) {
    for slot in &mut flags[range] {
        *slot |= value;
    }
}

fn indent_of(bytes: &[u8], from: usize) -> usize {
    bytes[from..].iter().take_while(|&&b| b == b' ' || b == b'\t').count()
}

fn scan_line(line: &str, fence: &mut Option<Fence>, leads: bool, flags: &mut [u16], literal: &mut [bool]) {
    let bytes = line.as_bytes();
    let len = bytes.len();

    if let Some(open) = *fence {
        if closes_fence(bytes, open) {
            *fence = None;
            mark(flags, 0..len, flag::FENCE);
        } else {
            mark(flags, 0..len, flag::CODE_BLOCK);
        }
        return;
    }
    if let Some(open) = opens_fence(bytes) {
        *fence = Some(open);
        mark(flags, 0..len, flag::FENCE);
        return;
    }

    let mut pos = 0;
    if leads && let Some(end) = leading_command(bytes) {
        let start = indent_of(bytes, 0);
        mark(flags, start..end, flag::COMMAND);
        inline(line, end..len, flags, literal);
        return;
    }

    // Block quotes, nested or not: `>` markers, each with one optional space.
    loop {
        let at = pos + indent_of(bytes, pos);
        if at - pos <= 3 && bytes.get(at) == Some(&b'>') {
            mark(flags, at..len, flag::QUOTE);
            mark(flags, at..at + 1, flag::SYNTAX);
            pos = at + 1;
            if bytes.get(pos) == Some(&b' ') {
                pos += 1;
            }
        } else {
            break;
        }
    }

    if is_thematic_break(&bytes[pos..]) {
        mark(flags, pos..len, flag::SYNTAX);
        return;
    }

    // ATX heading: up to three spaces, one to six `#`, then a space or the end.
    let at = pos + indent_of(bytes, pos);
    let hashes = bytes[at..].iter().take_while(|&&b| b == b'#').count();
    if at - pos <= 3
        && (1..=6).contains(&hashes)
        && matches!(bytes.get(at + hashes), None | Some(b' ' | b'\t'))
    {
        mark(flags, at..len, flag::HEADING);
        mark(flags, at..at + hashes, flag::SYNTAX);
        // An optional closing run of `#` after a space.
        let trimmed = line.trim_end().len();
        let closing = bytes[..trimmed].iter().rev().take_while(|&&b| b == b'#').count();
        let mut content_end = trimmed;
        if closing > 0 && trimmed - closing > at + hashes && matches!(bytes[trimmed - closing - 1], b' ' | b'\t') {
            content_end = trimmed - closing;
            mark(flags, content_end..trimmed, flag::SYNTAX);
        }
        inline(line, (at + hashes).min(content_end)..content_end, flags, literal);
        return;
    }

    // List item: a bullet or a number, then a space or the end, then
    // optionally a task box.
    let mut content = pos;
    if let Some(marker_end) = list_marker(bytes, at) {
        mark(flags, at..marker_end, flag::LIST_MARKER);
        content = marker_end;
        let box_at = marker_end + indent_of(bytes, marker_end);
        if box_at > marker_end
            && let [b'[', b' ' | b'x' | b'X', b']', ..] = &bytes[box_at..]
            && matches!(bytes.get(box_at + 3), None | Some(b' ' | b'\t'))
        {
            mark(flags, box_at..box_at + 3, flag::LIST_MARKER);
            content = box_at + 3;
        }
    }
    inline(line, content..len, flags, literal);
}

/// An opening fence: three or more backticks or tildes, at any indent (fences
/// inside list items are indented). A backtick fence's info string can't hold
/// a backtick, or it's inline code.
fn opens_fence(bytes: &[u8]) -> Option<Fence> {
    let at = indent_of(bytes, 0);
    let ch = *bytes.get(at)?;
    if ch != b'`' && ch != b'~' {
        return None;
    }
    let len = bytes[at..].iter().take_while(|&&b| b == ch).count();
    if len < 3 || (ch == b'`' && bytes[at + len..].contains(&b'`')) {
        return None;
    }
    Some(Fence { ch, len })
}

/// A closing fence: at least as long as the opening one, nothing after it.
fn closes_fence(bytes: &[u8], open: Fence) -> bool {
    let at = indent_of(bytes, 0);
    let len = bytes[at..].iter().take_while(|&&b| b == open.ch).count();
    len >= open.len && bytes[at + len..].iter().all(|b| b.is_ascii_whitespace())
}

/// `---`, `***` or `___`, optionally spaced out.
fn is_thematic_break(bytes: &[u8]) -> bool {
    let mut ch = None;
    let mut count = 0;
    for &b in bytes {
        match b {
            b' ' | b'\t' => {}
            b'-' | b'*' | b'_' if ch.is_none_or(|c| c == b) => {
                ch = Some(b);
                count += 1;
            }
            _ => return false,
        }
    }
    count >= 3
}

/// The end of a list marker starting at `at`: `-`, `*` or `+`, or up to nine
/// digits and `.` or `)`, followed by a space or the end of the line.
fn list_marker(bytes: &[u8], at: usize) -> Option<usize> {
    let end = match *bytes.get(at)? {
        b'-' | b'*' | b'+' => at + 1,
        b'0'..=b'9' => {
            let digits = bytes[at..].iter().take_while(|b| b.is_ascii_digit()).count();
            if digits > 9 || !matches!(bytes.get(at + digits), Some(b'.' | b')')) {
                return None;
            }
            at + digits + 1
        }
        _ => return None,
    };
    matches!(bytes.get(end), None | Some(b' ' | b'\t')).then_some(end)
}

/// The end of a `/command` or `$skill` at the start of the line.
fn leading_command(bytes: &[u8]) -> Option<usize> {
    let at = indent_of(bytes, 0);
    let sigil = *bytes.get(at)?;
    if sigil != b'/' && sigil != b'$' {
        return None;
    }
    let name = bytes[at + 1..]
        .iter()
        .take_while(|&&b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b':' | b'.'))
        .count();
    let first = *bytes.get(at + 1)?;
    let starts_well = if sigil == b'$' { first.is_ascii_alphabetic() } else { first.is_ascii_alphanumeric() };
    let end = at + 1 + name;
    (name > 0 && starts_well && matches!(bytes.get(end), None | Some(b' ' | b'\t'))).then_some(end)
}

/// Marks inline Markdown in `range` of `line`: escapes, code spans, links,
/// bare URLs and emphasis, in that order of precedence. `literal` collects
/// bytes that later passes must not read as syntax.
fn inline(line: &str, range: Range<usize>, flags: &mut [u16], literal: &mut [bool]) {
    let bytes = line.as_bytes();
    let Range { start, end } = range;

    // Escapes and code spans, left to right as CommonMark reads them.
    let mut i = start;
    while i < end {
        match bytes[i] {
            b'\\' if i + 1 < end && bytes[i + 1].is_ascii_punctuation() => {
                mark(flags, i..i + 1, flag::SYNTAX);
                literal[i] = true;
                literal[i + 1] = true;
                i += 2;
            }
            b'`' => {
                let run = run_len(bytes, i, end, b'`');
                if let Some(close) = find_backtick_run(bytes, i + run, end, run) {
                    mark(flags, i..close + run, flag::CODE);
                    mark(flags, i..i + run, flag::SYNTAX);
                    mark(flags, close..close + run, flag::SYNTAX);
                    literal[i..close + run].fill(true);
                    i = close + run;
                } else {
                    literal[i..i + run].fill(true);
                    i += run;
                }
            }
            _ => i += 1,
        }
    }

    links(line, start..end, flags, literal);
    bare_urls(line, start..end, flags, literal);
    emphasis(line, start..end, flags, literal);
}

fn run_len(bytes: &[u8], at: usize, end: usize, ch: u8) -> usize {
    bytes[at..end].iter().take_while(|&&b| b == ch).count()
}

/// The start of the next run of exactly `len` backticks in `from..end`.
fn find_backtick_run(bytes: &[u8], from: usize, end: usize, len: usize) -> Option<usize> {
    let mut i = from;
    while i < end {
        if bytes[i] == b'`' {
            let run = run_len(bytes, i, end, b'`');
            if run == len {
                return Some(i);
            }
            i += run;
        } else {
            i += 1;
        }
    }
    None
}

/// `[text](url)` and `![alt](url)`.
fn links(line: &str, range: Range<usize>, flags: &mut [u16], literal: &mut [bool]) {
    let bytes = line.as_bytes();
    let Range { start, end } = range;
    let mut i = start;
    while i < end {
        if bytes[i] != b'[' || literal[i] {
            i += 1;
            continue;
        }
        let Some(close) = matching(bytes, literal, i, end, b'[', b']') else {
            i += 1;
            continue;
        };
        let Some(paren_close) = (bytes.get(close + 1) == Some(&b'('))
            .then(|| matching(bytes, literal, close + 1, end, b'(', b')'))
            .flatten()
        else {
            i += 1;
            continue;
        };
        let open = if i > start && bytes[i - 1] == b'!' && !literal[i - 1] { i - 1 } else { i };
        mark(flags, i + 1..close, flag::LINK_TEXT);
        mark(flags, close + 2..paren_close, flag::LINK_URL);
        mark(flags, open..i + 1, flag::SYNTAX);
        mark(flags, close..close + 2, flag::SYNTAX);
        mark(flags, paren_close..paren_close + 1, flag::SYNTAX);
        for at in [i, close, close + 1, paren_close] {
            literal[at] = true;
        }
        literal[open] = true;
        literal[close + 2..paren_close].fill(true);
        i = paren_close + 1;
    }
}

/// The index of the bracket closing the one at `open`, skipping literal bytes.
fn matching(bytes: &[u8], literal: &[bool], open: usize, end: usize, left: u8, right: u8) -> Option<usize> {
    let mut depth = 0usize;
    for i in open..end {
        if literal[i] && i != open {
            continue;
        }
        if bytes[i] == left {
            depth += 1;
        } else if bytes[i] == right {
            depth -= 1;
            if depth == 0 {
                return Some(i);
            }
        }
    }
    None
}

/// `http://` and `https://` URLs written out in the text.
fn bare_urls(line: &str, range: Range<usize>, flags: &mut [u16], literal: &mut [bool]) {
    let bytes = line.as_bytes();
    let mut search = range.start;
    while let Some(found) = line[search..range.end].find("http") {
        let at = search + found;
        let rest = &line[at..range.end];
        let scheme = if rest.starts_with("https://") {
            8
        } else if rest.starts_with("http://") {
            7
        } else {
            search = at + 4;
            continue;
        };
        let word_start = at == range.start || matches!(bytes[at - 1], b' ' | b'\t' | b'(' | b'<' | b'"' | b'\'');
        if !word_start || literal[at] {
            search = at + scheme;
            continue;
        }
        let mut end = at + rest.find(|c: char| c.is_whitespace() || c == '<' || c == '>').unwrap_or(rest.len());
        while end > at + scheme && matches!(bytes[end - 1], b'.' | b',' | b':' | b';' | b'!' | b'?' | b')' | b'\'' | b'"') {
            end -= 1;
        }
        if end > at + scheme {
            mark(flags, at..end, flag::LINK_TEXT);
            literal[at..end].fill(true);
        }
        search = end.max(at + scheme);
    }
}

/// A run of `*` or `_` that may open or close emphasis.
struct Delimiter {
    ch: u8,
    /// Where the run's unused delimiters start.
    start: usize,
    /// How many are left unused.
    len: usize,
    original_len: usize,
    can_open: bool,
    can_close: bool,
}

fn is_punctuation(c: char) -> bool {
    c.is_ascii_punctuation() || (!c.is_alphanumeric() && !c.is_whitespace() && !c.is_control())
}

/// `*emphasis*`, `_emphasis_`, `**strong**` and `__strong__`, matched with
/// CommonMark's delimiter-run rules (flanking, intraword `_`, rule of three).
fn emphasis(line: &str, range: Range<usize>, flags: &mut [u16], literal: &[bool]) {
    let bytes = line.as_bytes();
    let mut runs: Vec<Delimiter> = Vec::new();
    let mut i = range.start;
    while i < range.end {
        let ch = bytes[i];
        if (ch != b'*' && ch != b'_') || literal[i] {
            i += 1;
            continue;
        }
        let mut len = 1;
        while i + len < range.end && bytes[i + len] == ch && !literal[i + len] {
            len += 1;
        }
        let before = line[..i].chars().next_back().unwrap_or(' ');
        let after = line[i + len..].chars().next().unwrap_or(' ');
        let left = !after.is_whitespace()
            && (!is_punctuation(after) || before.is_whitespace() || is_punctuation(before));
        let right = !before.is_whitespace()
            && (!is_punctuation(before) || after.is_whitespace() || is_punctuation(after));
        let (can_open, can_close) = if ch == b'*' {
            (left, right)
        } else {
            (left && (!right || is_punctuation(before)), right && (!left || is_punctuation(after)))
        };
        if can_open || can_close {
            runs.push(Delimiter { ch, start: i, len, original_len: len, can_open, can_close });
        }
        i += len;
    }

    for closer in 0..runs.len() {
        if !runs[closer].can_close {
            continue;
        }
        while runs[closer].len > 0 {
            let opener = (0..closer).rev().find(|&o| {
                let (o, c) = (&runs[o], &runs[closer]);
                let odd_match = (o.can_close || c.can_open)
                    && (o.original_len + c.original_len) % 3 == 0
                    && !(o.original_len % 3 == 0 && c.original_len % 3 == 0);
                o.ch == c.ch && o.can_open && o.len > 0 && !odd_match
            });
            let Some(opener) = opener else { break };
            let used = if runs[opener].len >= 2 && runs[closer].len >= 2 { 2 } else { 1 };
            let open_end = runs[opener].start + runs[opener].len;
            let close_start = runs[closer].start;
            mark(flags, open_end - used..open_end, flag::SYNTAX);
            mark(flags, close_start..close_start + used, flag::SYNTAX);
            mark(flags, open_end..close_start, if used == 2 { flag::STRONG } else { flag::EMPHASIS });
            runs[opener].len -= used;
            runs[closer].start += used;
            runs[closer].len -= used;
            for between in &mut runs[opener + 1..closer] {
                between.len = 0;
            }
        }
    }
}

#[cfg(test)]
mod tests {
    // Not `super::*`: GPUI's glob exports its own `test` attribute.
    use std::ops::Range;

    use gpui_kit::{FontWeight, HighlightStyle};

    use super::{MarkdownHighlighter, flag, metal, scan};

    /// The flags of the bytes covered by `needle`'s first occurrence, which
    /// must all agree.
    fn flags_of(text: &str, needle: &str) -> u16 {
        let at = text.find(needle).unwrap_or_else(|| panic!("{needle:?} not in {text:?}"));
        flags_at(text, at..at + needle.len())
    }

    fn flags_at(text: &str, range: Range<usize>) -> u16 {
        let spans = scan(text);
        let value = |offset: usize| {
            spans.iter().find(|(span, _)| span.contains(&offset)).map_or(0, |(_, flags)| *flags)
        };
        let first = value(range.start);
        for offset in range.clone() {
            assert_eq!(value(offset), first, "mixed flags in {:?} of {text:?}", &text[range.clone()]);
        }
        first
    }

    fn assert_well_formed(text: &str) {
        let spans = scan(text);
        let mut last = 0;
        for (span, flags) in &spans {
            assert!(span.start >= last && span.start < span.end, "{spans:?}");
            assert!(*flags != 0);
            assert!(text.is_char_boundary(span.start) && text.is_char_boundary(span.end), "{span:?}");
            assert!(span.end <= text.len());
            last = span.end;
        }
    }

    /// `styles` must cover any requested range exactly, in order.
    fn assert_covers(text: &str, range: Range<usize>) {
        let mut highlighter = MarkdownHighlighter::new();
        highlighter.rescan(text);
        let runs = highlighter.styles_with(&range, &metal::BONE);
        let mut at = range.start;
        for (run, _) in &runs {
            assert_eq!(run.start, at, "gap or overlap in {runs:?}");
            assert!(run.end > run.start, "empty run in {runs:?}");
            assert!(text.is_char_boundary(run.start) && text.is_char_boundary(run.end));
            at = run.end;
        }
        assert_eq!(at, range.end, "runs end early: {runs:?}");
    }

    #[test]
    fn headings() {
        let text = "# Title\n###### Six\n####### seven\n#hashtag\n## Closed ##";
        assert_eq!(flags_of(text, "#"), flag::HEADING | flag::SYNTAX);
        assert_eq!(flags_of(text, "Title"), flag::HEADING);
        assert_eq!(flags_of(text, "Six"), flag::HEADING);
        assert_eq!(flags_of(text, "seven"), 0);
        assert_eq!(flags_of(text, "#hashtag"), 0);
        assert_eq!(flags_of(text, "Closed"), flag::HEADING);
        let closing = text.rfind("##").unwrap();
        assert_eq!(flags_at(text, closing..closing + 2), flag::HEADING | flag::SYNTAX);
        assert_eq!(flags_of("# **Bold** head", "Bold"), flag::HEADING | flag::STRONG);
    }

    #[test]
    fn emphasis_and_strong() {
        let text = "a *one* b **two** c _three_ d __four__ e ***five***";
        assert_eq!(flags_of(text, "one"), flag::EMPHASIS);
        assert_eq!(flags_of(text, "two"), flag::STRONG);
        assert_eq!(flags_of(text, "three"), flag::EMPHASIS);
        assert_eq!(flags_of(text, "four"), flag::STRONG);
        assert_eq!(flags_of(text, "five"), flag::STRONG | flag::EMPHASIS);
        assert_eq!(flags_of(text, "**"), flag::SYNTAX);
        assert_eq!(flags_of(text, " b "), 0);
    }

    #[test]
    fn emphasis_that_is_not() {
        // Intraword underscores, spaced-out stars and unclosed runs stay text.
        for text in ["snake_case_name", "2 * 3 * 4", "a **b", "* not a list*either", "x__y__z"] {
            let spans = scan(text);
            assert!(spans.iter().all(|(_, flags)| flags & (flag::EMPHASIS | flag::STRONG) == 0), "{text:?}: {spans:?}");
        }
        // Stars inside a word still do.
        assert_eq!(flags_of("un*frigging*believable", "frigging"), flag::EMPHASIS);
    }

    #[test]
    fn nested_emphasis() {
        let text = "**bold *both* bold**";
        assert_eq!(flags_of(text, "both"), flag::STRONG | flag::EMPHASIS);
        assert_eq!(flags_of(text, "bold "), flag::STRONG);
    }

    #[test]
    fn inline_code() {
        let text = "run `cargo *test*` now and ``a ` b``";
        assert_eq!(flags_of(text, "cargo *test*"), flag::CODE);
        assert_eq!(flags_of(text, "`"), flag::CODE | flag::SYNTAX);
        assert_eq!(flags_of(text, "a ` b"), flag::CODE);
        assert_eq!(flags_of(text, " now "), 0);
        // An unmatched backtick is just a backtick.
        assert_eq!(scan("a ` b"), vec![]);
    }

    #[test]
    fn escapes() {
        let text = r"\*not emphasis\* and \`not code\`";
        assert_eq!(flags_of(text, "not emphasis"), 0);
        assert_eq!(flags_of(text, "not code"), 0);
        assert_eq!(flags_at(text, 0..1), flag::SYNTAX);
    }

    /// The flags of line `n`, which must agree across it.
    fn line_flags(text: &str, n: usize) -> u16 {
        let start: usize = text.split('\n').take(n).map(|line| line.len() + 1).sum();
        let line = text.split('\n').nth(n).unwrap();
        flags_at(text, start..start + line.len())
    }

    #[test]
    fn fences_span_lines() {
        let text = "before\n```rust\nlet x = *y*;\n# not a heading\n```\nafter *em*";
        assert_eq!(line_flags(text, 0), 0);
        assert_eq!(line_flags(text, 1), flag::FENCE);
        assert_eq!(line_flags(text, 2), flag::CODE_BLOCK);
        assert_eq!(line_flags(text, 3), flag::CODE_BLOCK);
        assert_eq!(line_flags(text, 4), flag::FENCE);
        assert_eq!(flags_of(text, "em"), flag::EMPHASIS);
    }

    #[test]
    fn fences_need_a_matching_close() {
        // A shorter run or another character doesn't close; an unclosed fence
        // runs to the end, as it does on GitHub.
        let text = "~~~~\n~~~\n```\nstill code\n~~~~~\nout";
        assert_eq!(line_flags(text, 0), flag::FENCE);
        assert_eq!(line_flags(text, 1), flag::CODE_BLOCK);
        assert_eq!(line_flags(text, 2), flag::CODE_BLOCK);
        assert_eq!(line_flags(text, 3), flag::CODE_BLOCK);
        assert_eq!(line_flags(text, 4), flag::FENCE);
        assert_eq!(line_flags(text, 5), 0);
        assert_eq!(line_flags("```\nopen\nto the end", 2), flag::CODE_BLOCK);
        // Backticks in the info string make it inline code, not a fence.
        assert_eq!(flags_of("```a``` b", "b"), 0);
        assert_eq!(flags_of("```a``` b", "a"), flag::CODE);
        // Indented fences count, as inside list items.
        assert_eq!(flags_of("1. step\n   ```\n   code\n   ```\nout", "code"), flag::CODE_BLOCK);
        assert_eq!(flags_of("1. step\n   ```\n   code\n   ```\nout", "out"), 0);
    }

    #[test]
    fn quotes_lists_and_rules() {
        let text = "> quoted *em*";
        assert_eq!(flags_of(text, ">"), flag::QUOTE | flag::SYNTAX);
        assert_eq!(flags_of(text, "quoted"), flag::QUOTE);
        assert_eq!(flags_of(text, "em"), flag::QUOTE | flag::EMPHASIS);
        for (text, marker) in
            [("- item", "-"), ("* star", "*"), ("+ plus", "+"), ("12. twelve", "12."), ("3) three", "3)"), ("  - nested", "-")]
        {
            assert_eq!(flags_of(text, marker), flag::LIST_MARKER, "{text:?}");
            assert_eq!(flags_at(text, text.len() - 3..text.len()), 0, "{text:?}");
        }
        assert_eq!(flags_of("- [x] done", "[x]"), flag::LIST_MARKER);
        assert_eq!(flags_of("- [x] done", "done"), 0);
        assert_eq!(flags_of("---", "---"), flag::SYNTAX);
        assert_eq!(flags_of("* * *", "* * *"), flag::SYNTAX);
        assert_eq!(flags_of("-not", "-not"), 0);
        assert_eq!(flags_of("1.5 times", "1.5"), 0);
        assert_eq!(flags_of("> - nested", "-"), flag::QUOTE | flag::LIST_MARKER);
    }

    #[test]
    fn links_and_urls() {
        let text = "see [the docs](https://x.dev/a_b_c) and ![img](p.png) or https://t3.chat/x.";
        assert_eq!(flags_of(text, "the docs"), flag::LINK_TEXT);
        assert_eq!(flags_of(text, "https://x.dev/a_b_c"), flag::LINK_URL);
        assert_eq!(flags_of(text, "]("), flag::SYNTAX);
        assert_eq!(flags_of(text, "![") , flag::SYNTAX);
        assert_eq!(flags_of(text, "https://t3.chat/x"), flag::LINK_TEXT);
        assert_eq!(flags_at(text, text.len() - 1..text.len()), 0);
        assert_eq!(flags_of("[not a link] (x)", "not a link"), 0);
    }

    #[test]
    fn leading_commands() {
        assert_eq!(flags_of("/review this *now*", "/review"), flag::COMMAND);
        assert_eq!(flags_of("/review this *now*", "now"), flag::EMPHASIS);
        assert_eq!(flags_of("\n  $deploy-prod please", "$deploy-prod"), flag::COMMAND);
        assert_eq!(flags_of("/plugin:cmd", "/plugin:cmd"), flag::COMMAND);
        // Not leading, a path, or a price: plain text.
        assert_eq!(flags_of("hi\n/review", "/review"), 0);
        assert_eq!(flags_of("/home/joni is mine", "/home"), 0);
        assert_eq!(flags_of("$5 please", "$5"), 0);
    }

    #[test]
    fn unicode_text() {
        let text = "# Grüße 👋\n**fett** und *kursiv* — `naïve`\n> ünïcödé [lïnk](ü)\n日本語の*強調*です";
        assert_well_formed(text);
        assert_eq!(flags_of(text, "Grüße 👋"), flag::HEADING);
        assert_eq!(flags_of(text, "fett"), flag::STRONG);
        assert_eq!(flags_of(text, "kursiv"), flag::EMPHASIS);
        assert_eq!(flags_of(text, "naïve"), flag::CODE);
        assert_eq!(flags_of(text, "lïnk"), flag::QUOTE | flag::LINK_TEXT);
        assert_eq!(flags_of(text, "強調"), flag::EMPHASIS);
        for end in (0..=text.len()).filter(|&i| text.is_char_boundary(i)) {
            assert_covers(text, 0..end);
            assert_covers(text, end..text.len());
        }
    }

    #[test]
    fn styles_cover_every_line_range() {
        let text = "# H\n\n- a **b** `c`\r\n```\nx\n```\n> q [l](u)\n__u__ *e* ~ end";
        assert_well_formed(text);
        let mut starts = vec![0];
        starts.extend(text.match_indices('\n').map(|(i, _)| i + 1));
        starts.push(text.len());
        for (i, &start) in starts.iter().enumerate() {
            for &end in &starts[i..] {
                assert_covers(text, start..end);
            }
        }
        assert_covers("", 0..0);
        assert!(MarkdownHighlighter::new().styles_with(&(0..0), &metal::BONE).is_empty());
    }

    #[test]
    fn crlf_lines() {
        let text = "**a**\r\n# b\r\n";
        assert_well_formed(text);
        assert_eq!(flags_of(text, "a"), flag::STRONG);
        assert_eq!(flags_of(text, "b"), flag::HEADING);
        assert_eq!(flags_of(text, "\r"), 0);
    }

    #[test]
    fn styles_follow_flags() {
        let mut highlighter = MarkdownHighlighter::new();
        let text = "**b** `c`";
        highlighter.rescan(text);
        let runs = highlighter.styles_with(&(0..text.len()), &metal::GRAPHITE);
        let style_at = |offset: usize| runs.iter().find(|(run, _)| run.contains(&offset)).unwrap().1;
        assert_eq!(style_at(2).font_weight, Some(FontWeight::BOLD));
        assert_eq!(style_at(0).color, Some(metal::GRAPHITE.ink3));
        assert_eq!(style_at(7).background_color, Some(metal::GRAPHITE.well_top));
        assert_eq!(style_at(5), HighlightStyle::default());
    }

    #[test]
    fn a_few_hundred_lines_scan_fast() {
        let block = "# Heading\nSome *text* with **bold**, `code` and [a link](https://x.y).\n- item one\n```\nfn main() {}\n```\n> quote\n";
        let text = block.repeat(100);
        let started = std::time::Instant::now();
        for _ in 0..10 {
            assert!(!scan(&text).is_empty());
        }
        assert!(started.elapsed() < std::time::Duration::from_millis(200), "{:?}", started.elapsed());
    }
}