Merge branch 'features-01f34a-rc2' into features-01f34a
# Conflicts: # viewport/src/syntax.rs
This commit is contained in:
commit
908a69ec88
|
|
@ -445,7 +445,7 @@ impl EditorState {
|
||||||
settings,
|
settings,
|
||||||
|highlight, _theme| Format {
|
|highlight, _theme| Format {
|
||||||
color: Some(syntax::highlight_color(highlight.kind)),
|
color: Some(syntax::highlight_color(highlight.kind)),
|
||||||
font: None,
|
font: syntax::highlight_font(highlight.kind),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.into();
|
.into();
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,33 @@
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
use iced_wgpu::core::text::highlighter;
|
use iced_wgpu::core::text::highlighter;
|
||||||
use iced_wgpu::core::Color;
|
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::highlight::{highlight_source, HighlightSpan};
|
||||||
|
use acord_core::doc::{classify_document, LineKind};
|
||||||
use crate::editor::{RESULT_PREFIX, ERROR_PREFIX};
|
use crate::editor::{RESULT_PREFIX, ERROR_PREFIX};
|
||||||
use crate::palette;
|
use crate::palette;
|
||||||
|
|
||||||
pub const EVAL_RESULT_KIND: u8 = 24;
|
pub const EVAL_RESULT_KIND: u8 = 24;
|
||||||
pub const EVAL_ERROR_KIND: u8 = 25;
|
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;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct SyntaxSettings {
|
pub struct SyntaxSettings {
|
||||||
pub lang: String,
|
pub lang: String,
|
||||||
|
|
@ -24,6 +43,8 @@ pub struct SyntaxHighlighter {
|
||||||
lang: String,
|
lang: String,
|
||||||
spans: Vec<HighlightSpan>,
|
spans: Vec<HighlightSpan>,
|
||||||
line_offsets: Vec<usize>,
|
line_offsets: Vec<usize>,
|
||||||
|
line_kinds: Vec<LineKind>,
|
||||||
|
in_fenced_code: bool,
|
||||||
current_line: usize,
|
current_line: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,8 +57,221 @@ impl SyntaxHighlighter {
|
||||||
self.line_offsets.push(offset);
|
self.line_offsets.push(offset);
|
||||||
offset += line.len() + 1;
|
offset += line.len() + 1;
|
||||||
}
|
}
|
||||||
|
let classified = classify_document(source);
|
||||||
|
self.line_kinds = classified.into_iter().map(|cl| cl.kind).collect();
|
||||||
|
self.in_fenced_code = false;
|
||||||
self.current_line = 0;
|
self.current_line = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn highlight_markdown(&self, line: &str) -> Vec<(Range<usize>, 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(marker_len) = list_marker_len(trimmed) {
|
||||||
|
let marker_end = leading + marker_len;
|
||||||
|
let mut spans = vec![
|
||||||
|
(0..marker_end, SyntaxHighlight { kind: MD_LIST_MARKER }),
|
||||||
|
];
|
||||||
|
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<usize> {
|
||||||
|
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' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_marker_len(trimmed: &str) -> Option<usize> {
|
||||||
|
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("- [ ] ") || trimmed.starts_with("- [x] ") || trimmed.starts_with("- [X] ") {
|
||||||
|
return Some(6);
|
||||||
|
}
|
||||||
|
return Some(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(i + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_inline(text: &str, base: usize) -> Vec<(Range<usize>, SyntaxHighlight)> {
|
||||||
|
let bytes = text.as_bytes();
|
||||||
|
let len = bytes.len();
|
||||||
|
let mut spans = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
while i < len {
|
||||||
|
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 {
|
||||||
|
spans.push((base + i + 2..base + end, SyntaxHighlight { kind: MD_BOLD }));
|
||||||
|
}
|
||||||
|
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'*') {
|
||||||
|
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'`' {
|
||||||
|
if let Some(end) = find_single_closing(bytes, i + 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_INLINE_CODE }));
|
||||||
|
}
|
||||||
|
spans.push((base + end..base + end + 1, SyntaxHighlight { kind: MD_FORMAT_MARKER }));
|
||||||
|
i = end + 1;
|
||||||
|
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 find_closing(bytes: &[u8], start: usize, c1: u8, c2: u8) -> Option<usize> {
|
||||||
|
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<usize> {
|
||||||
|
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 {
|
impl highlighter::Highlighter for SyntaxHighlighter {
|
||||||
|
|
@ -50,6 +284,8 @@ impl highlighter::Highlighter for SyntaxHighlighter {
|
||||||
lang: settings.lang.clone(),
|
lang: settings.lang.clone(),
|
||||||
spans: Vec::new(),
|
spans: Vec::new(),
|
||||||
line_offsets: Vec::new(),
|
line_offsets: Vec::new(),
|
||||||
|
line_kinds: Vec::new(),
|
||||||
|
in_fenced_code: false,
|
||||||
current_line: 0,
|
current_line: 0,
|
||||||
};
|
};
|
||||||
h.rebuild(&settings.source);
|
h.rebuild(&settings.source);
|
||||||
|
|
@ -63,18 +299,42 @@ impl highlighter::Highlighter for SyntaxHighlighter {
|
||||||
|
|
||||||
fn change_line(&mut self, line: usize) {
|
fn change_line(&mut self, line: usize) {
|
||||||
self.current_line = self.current_line.min(line);
|
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<'_> {
|
fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> {
|
||||||
let ln = self.current_line;
|
let ln = self.current_line;
|
||||||
self.current_line += 1;
|
self.current_line += 1;
|
||||||
|
|
||||||
let trimmed = _line.trim_start();
|
let trimmed = line.trim_start();
|
||||||
if trimmed.starts_with(RESULT_PREFIX) {
|
if trimmed.starts_with(RESULT_PREFIX) {
|
||||||
return vec![(0.._line.len(), SyntaxHighlight { kind: EVAL_RESULT_KIND })].into_iter();
|
return vec![(0..line.len(), SyntaxHighlight { kind: EVAL_RESULT_KIND })].into_iter();
|
||||||
}
|
}
|
||||||
if trimmed.starts_with(ERROR_PREFIX) {
|
if trimmed.starts_with(ERROR_PREFIX) {
|
||||||
return vec![(0.._line.len(), SyntaxHighlight { kind: EVAL_ERROR_KIND })].into_iter();
|
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() {
|
if ln >= self.line_offsets.len() {
|
||||||
|
|
@ -85,7 +345,7 @@ impl highlighter::Highlighter for SyntaxHighlighter {
|
||||||
let line_end = if ln + 1 < self.line_offsets.len() {
|
let line_end = if ln + 1 < self.line_offsets.len() {
|
||||||
self.line_offsets[ln + 1] - 1
|
self.line_offsets[ln + 1] - 1
|
||||||
} else {
|
} else {
|
||||||
line_start + _line.len()
|
line_start + line.len()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
@ -136,6 +396,37 @@ pub fn highlight_color(kind: u8) -> Color {
|
||||||
23 => p.text,
|
23 => p.text,
|
||||||
24 => p.green,
|
24 => p.green,
|
||||||
25 => p.maroon,
|
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,
|
||||||
_ => p.text,
|
_ => p.text,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn highlight_font(kind: u8) -> Option<Font> {
|
||||||
|
match kind {
|
||||||
|
MD_HEADING_MARKER => Some(Font { weight: Weight::Bold, ..Font::MONOSPACE }),
|
||||||
|
MD_H1 | MD_H2 | MD_H3 => Some(Font { weight: Weight::Bold, ..Font::DEFAULT }),
|
||||||
|
MD_BOLD => Some(Font { weight: Weight::Bold, ..Font::DEFAULT }),
|
||||||
|
MD_ITALIC => Some(Font { 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),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue