use std::ops::Range; use iced_wgpu::core::text::highlighter; use iced_wgpu::core::{Color, Font}; use iced_wgpu::core::font::{Weight, Style as FontStyle}; use acord_core::highlight::{highlight_source, HighlightSpan}; use acord_core::doc::{classify_document, LineKind}; use crate::editor::{RESULT_PREFIX, ERROR_PREFIX}; use crate::palette; pub const EVAL_RESULT_KIND: u8 = 24; pub const EVAL_ERROR_KIND: u8 = 25; const MD_HEADING_MARKER: u8 = 26; const MD_H1: u8 = 27; const MD_H2: u8 = 28; const MD_H3: u8 = 29; const MD_BOLD: u8 = 30; const MD_ITALIC: u8 = 31; const MD_INLINE_CODE: u8 = 32; const MD_FORMAT_MARKER: u8 = 33; const MD_LINK_TEXT: u8 = 34; const MD_LINK_URL: u8 = 35; const MD_BLOCKQUOTE_MARKER: u8 = 36; const MD_BLOCKQUOTE: u8 = 37; const MD_LIST_MARKER: u8 = 38; const MD_FENCE_MARKER: u8 = 39; const MD_CODE_BLOCK: u8 = 40; const MD_HR: u8 = 41; const MD_TASK_OPEN: u8 = 42; const MD_TASK_DONE: u8 = 43; const MD_BOLD_ITALIC: u8 = 44; #[derive(Clone, PartialEq)] pub struct SyntaxSettings { pub lang: String, pub source: String, } #[derive(Clone, Copy, Debug)] pub struct SyntaxHighlight { pub kind: u8, } #[derive(Clone, Copy, Debug, PartialEq)] pub enum LineDecor { None, CodeBlock, Blockquote, HorizontalRule, FenceMarker, } pub struct SyntaxHighlighter { lang: String, spans: Vec, line_offsets: Vec, line_kinds: Vec, in_fenced_code: bool, current_line: usize, line_decors: Vec, } impl SyntaxHighlighter { fn rebuild(&mut self, source: &str) { self.spans = highlight_source(source, &self.lang); self.line_offsets.clear(); let mut offset = 0; for line in source.split('\n') { self.line_offsets.push(offset); offset += line.len() + 1; } let classified = classify_document(source); self.line_kinds = classified.into_iter().map(|cl| cl.kind).collect(); self.line_decors.clear(); let mut in_fence = false; for (i, raw_line) in source.split('\n').enumerate() { let is_md = i < self.line_kinds.len() && self.line_kinds[i] == LineKind::Markdown; if is_md { let trimmed = raw_line.trim_start(); if trimmed.starts_with("```") { in_fence = !in_fence; self.line_decors.push(LineDecor::FenceMarker); } else if in_fence { self.line_decors.push(LineDecor::CodeBlock); } else if is_horizontal_rule(trimmed) { self.line_decors.push(LineDecor::HorizontalRule); } else if trimmed.starts_with("> ") || trimmed == ">" { self.line_decors.push(LineDecor::Blockquote); } else { self.line_decors.push(LineDecor::None); } } else { if in_fence { in_fence = false; } self.line_decors.push(LineDecor::None); } } self.in_fenced_code = false; self.current_line = 0; } fn highlight_markdown(&self, line: &str) -> Vec<(Range, SyntaxHighlight)> { let trimmed = line.trim_start(); let leading = line.len() - trimmed.len(); if is_horizontal_rule(trimmed) { return vec![(0..line.len(), SyntaxHighlight { kind: MD_HR })]; } if let Some(level) = heading_level(trimmed) { let marker_end = leading + level + 1; let kind = match level { 1 => MD_H1, 2 => MD_H2, _ => MD_H3, }; let mut spans = vec![ (0..marker_end, SyntaxHighlight { kind: MD_HEADING_MARKER }), ]; if marker_end < line.len() { spans.push((marker_end..line.len(), SyntaxHighlight { kind })); } return spans; } if trimmed.starts_with("> ") || trimmed == ">" { let marker_end = leading + if trimmed.len() > 1 { 2 } else { 1 }; let mut spans = vec![ (0..marker_end, SyntaxHighlight { kind: MD_BLOCKQUOTE_MARKER }), ]; if marker_end < line.len() { let content = &line[marker_end..]; let inner = parse_inline(content, marker_end); if inner.is_empty() { spans.push((marker_end..line.len(), SyntaxHighlight { kind: MD_BLOCKQUOTE })); } else { spans.extend(inner); } } return spans; } if let Some(list_info) = list_marker_info(trimmed) { let (marker_len, marker_kind) = match list_info { ListKind::TaskOpen(n) => (n, MD_TASK_OPEN), ListKind::TaskDone(n) => (n, MD_TASK_DONE), ListKind::Plain(n) => (n, MD_LIST_MARKER), }; let marker_end = leading + marker_len; let mut spans = vec![ (0..marker_end, SyntaxHighlight { kind: marker_kind }), ]; if marker_end < line.len() { let content = &line[marker_end..]; let inner = parse_inline(content, marker_end); if inner.is_empty() { return spans; } spans.extend(inner); } return spans; } parse_inline(line, 0) } } fn heading_level(trimmed: &str) -> Option { let bytes = trimmed.as_bytes(); if bytes.is_empty() || bytes[0] != b'#' { return None; } let mut level = 0; while level < bytes.len() && bytes[level] == b'#' { level += 1; } if level > 3 { return None; } if level < bytes.len() && bytes[level] == b' ' { Some(level) } else { None } } fn is_horizontal_rule(trimmed: &str) -> bool { if trimmed.len() < 3 { return false; } let first = trimmed.as_bytes()[0]; if !matches!(first, b'-' | b'*' | b'_') { return false; } trimmed.bytes().all(|b| b == first || b == b' ') } #[derive(Clone, Copy, PartialEq)] enum ListKind { Plain(usize), TaskOpen(usize), TaskDone(usize), } fn list_marker_info(trimmed: &str) -> Option { let bytes = trimmed.as_bytes(); if bytes.is_empty() { return None; } if matches!(bytes[0], b'-' | b'*' | b'+') && bytes.get(1) == Some(&b' ') { if trimmed.starts_with("- [ ] ") { return Some(ListKind::TaskOpen(6)); } if trimmed.starts_with("- [x] ") || trimmed.starts_with("- [X] ") { return Some(ListKind::TaskDone(6)); } return Some(ListKind::Plain(2)); } let mut i = 0; while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; } if i > 0 && i < bytes.len() && matches!(bytes[i], b'.' | b')') { if bytes.get(i + 1) == Some(&b' ') { return Some(ListKind::Plain(i + 2)); } } None } fn parse_inline(text: &str, base: usize) -> Vec<(Range, SyntaxHighlight)> { let bytes = text.as_bytes(); let len = bytes.len(); let mut spans = Vec::new(); let mut i = 0; while i < len { if bytes[i] == b'\\' && i + 1 < len && is_md_punctuation(bytes[i + 1]) { i += 2; continue; } if i + 2 < len && bytes[i] == b'*' && bytes[i + 1] == b'*' && bytes[i + 2] == b'*' { if let Some(end) = find_triple_star(bytes, i + 3) { spans.push((base + i..base + i + 3, SyntaxHighlight { kind: MD_FORMAT_MARKER })); if i + 3 < end { spans.push((base + i + 3..base + end, SyntaxHighlight { kind: MD_BOLD_ITALIC })); } spans.push((base + end..base + end + 3, SyntaxHighlight { kind: MD_FORMAT_MARKER })); i = end + 3; continue; } } if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'*' { if let Some(end) = find_closing(bytes, i + 2, b'*', b'*') { spans.push((base + i..base + i + 2, SyntaxHighlight { kind: MD_FORMAT_MARKER })); if i + 2 < end { let inner = parse_inline(&text[i + 2..end], base + i + 2); if inner.is_empty() { spans.push((base + i + 2..base + end, SyntaxHighlight { kind: MD_BOLD })); } else { for (r, h) in inner { let kind = if h.kind == MD_ITALIC { MD_BOLD_ITALIC } else { h.kind }; spans.push((r, SyntaxHighlight { kind })); } } } spans.push((base + end..base + end + 2, SyntaxHighlight { kind: MD_FORMAT_MARKER })); i = end + 2; continue; } } if bytes[i] == b'*' && (i + 1 >= len || bytes[i + 1] != b'*') { if let Some(end) = find_single_closing(bytes, i + 1, b'*') { if end > i + 1 && bytes[end - 1] != b'*' { spans.push((base + i..base + i + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER })); if i + 1 < end { spans.push((base + i + 1..base + end, SyntaxHighlight { kind: MD_ITALIC })); } spans.push((base + end..base + end + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER })); i = end + 1; continue; } } } if bytes[i] == b'`' { let tick_count = count_backticks(bytes, i); if let Some(end) = find_backtick_close(bytes, i + tick_count, tick_count) { spans.push((base + i..base + i + tick_count, SyntaxHighlight { kind: MD_FORMAT_MARKER })); if i + tick_count < end { spans.push((base + i + tick_count..base + end, SyntaxHighlight { kind: MD_INLINE_CODE })); } spans.push((base + end..base + end + tick_count, SyntaxHighlight { kind: MD_FORMAT_MARKER })); i = end + tick_count; continue; } } if bytes[i] == b'[' { if let Some((text_end, url_end)) = find_link(bytes, i) { spans.push((base + i..base + i + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER })); if i + 1 < text_end { spans.push((base + i + 1..base + text_end, SyntaxHighlight { kind: MD_LINK_TEXT })); } spans.push((base + text_end..base + text_end + 2, SyntaxHighlight { kind: MD_FORMAT_MARKER })); if text_end + 2 < url_end { spans.push((base + text_end + 2..base + url_end, SyntaxHighlight { kind: MD_LINK_URL })); } spans.push((base + url_end..base + url_end + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER })); i = url_end + 1; continue; } } i += 1; } spans } fn is_md_punctuation(b: u8) -> bool { matches!(b, b'\\' | b'`' | b'*' | b'_' | b'{' | b'}' | b'[' | b']' | b'(' | b')' | b'#' | b'+' | b'-' | b'.' | b'!' | b'|') } fn find_triple_star(bytes: &[u8], start: usize) -> Option { let mut i = start; while i + 2 < bytes.len() { if bytes[i] == b'*' && bytes[i + 1] == b'*' && bytes[i + 2] == b'*' { return Some(i); } i += 1; } None } fn count_backticks(bytes: &[u8], start: usize) -> usize { let mut n = 0; while start + n < bytes.len() && bytes[start + n] == b'`' { n += 1; } n } fn find_backtick_close(bytes: &[u8], start: usize, count: usize) -> Option { if count == 0 { return None; } let mut i = start; while i + count <= bytes.len() { if count_backticks(bytes, i) == count { return Some(i); } i += 1; } None } fn find_closing(bytes: &[u8], start: usize, c1: u8, c2: u8) -> Option { let mut i = start; while i + 1 < bytes.len() { if bytes[i] == c1 && bytes[i + 1] == c2 { return Some(i); } i += 1; } None } fn find_single_closing(bytes: &[u8], start: usize, ch: u8) -> Option { let mut i = start; while i < bytes.len() { if bytes[i] == ch { return Some(i); } i += 1; } None } fn find_link(bytes: &[u8], open: usize) -> Option<(usize, usize)> { let mut i = open + 1; while i < bytes.len() { if bytes[i] == b']' { if i + 1 < bytes.len() && bytes[i + 1] == b'(' { let text_end = i; let mut j = i + 2; while j < bytes.len() { if bytes[j] == b')' { return Some((text_end, j)); } j += 1; } } return None; } if bytes[i] == b'\n' { return None; } i += 1; } None } impl highlighter::Highlighter for SyntaxHighlighter { type Settings = SyntaxSettings; type Highlight = SyntaxHighlight; type Iterator<'a> = std::vec::IntoIter<(Range, SyntaxHighlight)>; fn new(settings: &Self::Settings) -> Self { let mut h = SyntaxHighlighter { lang: settings.lang.clone(), spans: Vec::new(), line_offsets: Vec::new(), line_kinds: Vec::new(), in_fenced_code: false, current_line: 0, line_decors: Vec::new(), }; h.rebuild(&settings.source); h } fn update(&mut self, new_settings: &Self::Settings) { self.lang = new_settings.lang.clone(); self.rebuild(&new_settings.source); } fn change_line(&mut self, line: usize) { self.current_line = self.current_line.min(line); if line == 0 { self.in_fenced_code = false; } } fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { let ln = self.current_line; self.current_line += 1; let trimmed = line.trim_start(); if trimmed.starts_with(RESULT_PREFIX) { return vec![(0..line.len(), SyntaxHighlight { kind: EVAL_RESULT_KIND })].into_iter(); } if trimmed.starts_with(ERROR_PREFIX) { return vec![(0..line.len(), SyntaxHighlight { kind: EVAL_ERROR_KIND })].into_iter(); } let is_markdown = ln < self.line_kinds.len() && self.line_kinds[ln] == LineKind::Markdown; if is_markdown { if trimmed.starts_with("```") { self.in_fenced_code = !self.in_fenced_code; return vec![(0..line.len(), SyntaxHighlight { kind: MD_FENCE_MARKER })].into_iter(); } if self.in_fenced_code { return vec![(0..line.len(), SyntaxHighlight { kind: MD_CODE_BLOCK })].into_iter(); } let md_spans = self.highlight_markdown(line); if !md_spans.is_empty() { return md_spans.into_iter(); } } else if self.in_fenced_code { self.in_fenced_code = false; } if ln >= self.line_offsets.len() { return Vec::new().into_iter(); } let line_start = self.line_offsets[ln]; let line_end = if ln + 1 < self.line_offsets.len() { self.line_offsets[ln + 1] - 1 } else { line_start + line.len() }; let mut result = Vec::new(); for span in &self.spans { if span.end <= line_start || span.start >= line_end { continue; } let start = span.start.max(line_start) - line_start; let end = span.end.min(line_end) - line_start; if start < end { result.push((start..end, SyntaxHighlight { kind: span.kind })); } } result.into_iter() } fn current_line(&self) -> usize { self.current_line } } pub fn highlight_color(kind: u8) -> Color { let p = palette::current(); match kind { 0 => p.mauve, 1 => p.blue, 2 => p.teal, 3 => p.yellow, 4 => p.yellow, 5 => p.teal, 6 => p.peach, 7 => p.peach, 8 => p.green, 9 => p.peach, 10 => p.overlay0, 11 => p.text, 12 => p.red, 13 => p.flamingo, 14 => p.sky, 15 => p.overlay2, 16 => p.overlay2, 17 => p.overlay2, 18 => p.blue, 19 => p.mauve, 20 => p.yellow, 21 => p.teal, 22 => p.red, 23 => p.text, 24 => p.green, 25 => p.maroon, MD_HEADING_MARKER => p.overlay0, MD_H1 => p.rosewater, MD_H2 => p.peach, MD_H3 => p.yellow, MD_BOLD => p.peach, MD_ITALIC => p.mauve, MD_INLINE_CODE => p.green, MD_FORMAT_MARKER => p.overlay0, MD_LINK_TEXT => p.blue, MD_LINK_URL => p.overlay1, MD_BLOCKQUOTE_MARKER => p.overlay0, MD_BLOCKQUOTE => p.sky, MD_LIST_MARKER => p.sky, MD_FENCE_MARKER => p.overlay0, MD_CODE_BLOCK => p.text, MD_HR => p.overlay1, MD_TASK_OPEN => p.overlay2, MD_TASK_DONE => p.green, MD_BOLD_ITALIC => p.peach, _ => p.text, } } pub fn highlight_font(kind: u8) -> Option { match kind { MD_HEADING_MARKER => Some(Font { weight: Weight::Bold, ..Font::MONOSPACE }), MD_H1 => Some(Font { weight: Weight::Black, ..Font::DEFAULT }), MD_H2 => Some(Font { weight: Weight::Bold, ..Font::DEFAULT }), MD_H3 => Some(Font { weight: Weight::Semibold, ..Font::DEFAULT }), MD_BOLD => Some(Font { weight: Weight::Bold, ..Font::DEFAULT }), MD_ITALIC => Some(Font { style: FontStyle::Italic, ..Font::DEFAULT }), MD_BOLD_ITALIC => Some(Font { weight: Weight::Bold, style: FontStyle::Italic, ..Font::DEFAULT }), MD_INLINE_CODE => Some(Font::MONOSPACE), MD_FORMAT_MARKER => Some(Font::MONOSPACE), MD_BLOCKQUOTE => Some(Font { style: FontStyle::Italic, ..Font::DEFAULT }), MD_FENCE_MARKER => Some(Font::MONOSPACE), MD_CODE_BLOCK => Some(Font::MONOSPACE), MD_TASK_DONE => Some(Font { weight: Weight::Bold, ..Font::MONOSPACE }), _ => None, } } pub fn compute_line_decors(source: &str) -> Vec { let classified = classify_document(source); let line_kinds: Vec = classified.into_iter().map(|cl| cl.kind).collect(); let mut decors = Vec::new(); let mut in_fence = false; for (i, raw_line) in source.split('\n').enumerate() { let is_md = i < line_kinds.len() && line_kinds[i] == LineKind::Markdown; if is_md { let trimmed = raw_line.trim_start(); if trimmed.starts_with("```") { in_fence = !in_fence; decors.push(LineDecor::FenceMarker); } else if in_fence { decors.push(LineDecor::CodeBlock); } else if is_horizontal_rule(trimmed) { decors.push(LineDecor::HorizontalRule); } else if trimmed.starts_with("> ") || trimmed == ">" { decors.push(LineDecor::Blockquote); } else { decors.push(LineDecor::None); } } else { if in_fence { in_fence = false; } decors.push(LineDecor::None); } } decors }