Skip to main content

ui/
highlight.rs

1// highlight -- syntax highlighting for ui.code_editor: a small engine of token rules, the
2// built-in grammars (Rust, JSON, INI, TOML, a Markdown subset, JFS: the scripts of
3// apps/jfs/crates/script) and the colours of the tokens
4//
5//   Highlighter::rust() / json() / ini() / toml() / markdown() / jfs() / plain()
6//   Highlighter::for_name(s)            by name or file extension ("rs", "json", "ini", "cfg",
7//                                       "toml", "md", "jfs", a path); plain() for anything else
8//   Highlighter::new(&GRAMMAR)          an app's own grammar (a static Grammar of Rules)
9//   h.line(text, state, &mut tokens) -> state   the tokens of one line (SY_* kinds, byte
10//                                       ranges; the plain text between them is SY_TEXT) from
11//                                       the state the line before ended in; 0 = nothing open
12//   h.tokens(text) -> Vec<Vec<Token>>   every line of a text (tests)
13//   CodeTheme::light() / dark() / high_contrast(&HcColors), ui.code_theme()   the colours of
14//                                       the tokens and the editor (gutter, current line,
15//                                       selection, bracket match, find matches, diagnostics,
16//                                       breakpoints, a debugger's current line)
17//
18// A grammar is a list of rules tried in order at every position: line comments, blocks (from
19// open to close, nested or not, optionally only at a line start: Markdown fences), strings
20// (escapes, multi-line or not), strings with code inside (Interp: the ${..} of JFS, whose
21// code the same rules highlight), Rust raw strings, word lists, numbers, punctuation and
22// functions of its own (Custom: JSON keys, Rust char literals / lifetimes / attributes, INI
23// sections and keys, Markdown headings, lists, emphasis and links).  Where no rule matches,
24// an identifier is read whole: before '(' a function (calls), before '!' a macro (macros),
25// capitalised a type (caps_types).  A construct still open at a line end is the line's end
26// state: rule index + 1, the nesting depth (bits 8..15) and a Rust raw string's '#' count
27// (bits 16..31); the editor keeps a state per line and highlights only the visible lines.
28
29use alloc::vec::Vec;
30
31/// token kinds: plain text
32pub const SY_TEXT: u8 = 0;
33/// token kinds: keywords
34pub const SY_KEYWORD: u8 = 1;
35/// types (lists, capitalised names)
36pub const SY_TYPE: u8 = 2;
37/// functions, macros
38pub const SY_FUNC: u8 = 3;
39/// strings, character literals
40pub const SY_STRING: u8 = 4;
41/// numbers, dates
42pub const SY_NUMBER: u8 = 5;
43/// comments, Markdown quotations
44pub const SY_COMMENT: u8 = 6;
45/// true / false / null, enum constants
46pub const SY_CONST: u8 = 7;
47/// operators, brackets, separators
48pub const SY_PUNCT: u8 = 8;
49/// attributes, lifetimes, preprocessor lines
50pub const SY_META: u8 = 9;
51/// JSON / INI / TOML keys
52pub const SY_KEY: u8 = 10;
53/// Markdown headings, INI / TOML sections
54pub const SY_HEADING: u8 = 11;
55/// Markdown *emphasis*
56pub const SY_EMPH: u8 = 12;
57/// Markdown **strong**
58pub const SY_STRONG: u8 = 13;
59/// links, URLs
60pub const SY_LINK: u8 = 14;
61/// Markdown code spans and blocks
62pub const SY_CODE: u8 = 15;
63/// escape sequences in strings
64pub const SY_ESCAPE: u8 = 16;
65/// the number of kinds
66pub const SY_COUNT: usize = 17;
67
68/// a highlighted part of a line: bytes start .. end
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub struct Token {
71    pub start: usize,
72    pub end: usize,
73    pub kind: u8,
74}
75
76/// a rule of a grammar (see the module header)
77#[non_exhaustive]
78pub enum Rule {
79    /// from the text to the end of the line: a comment
80    LineComment(&'static str),
81    /// from open to close (nested: opens inside count; bol: open and close only at the start
82    /// of a line, after blanks -- the whole line belongs to it); may span lines
83    Block { open: &'static str, close: &'static str, kind: u8, nested: bool, bol: bool },
84    /// a string from open to close; escape: a backslash takes the next character (SY_ESCAPE);
85    /// multi: may span lines
86    Str { open: &'static str, close: &'static str, escape: bool, multi: bool, kind: u8 },
87    /// Rust raw strings: r"..", r#".."#, br".." (may span lines)
88    RawStr,
89    /// a string with code inside (interpolation), on one line: from open to close, a backslash
90    /// takes the next character (\u{..} its hex digits and brace; SY_ESCAPE); from `start`
91    /// (ending with '{') to the '}' that closes it (braces inside count) the grammar's rules
92    /// highlight code, the two delimiters SY_META
93    Interp { open: &'static str, close: &'static str, start: &'static str },
94    /// identifiers of a list
95    Words(&'static [&'static str], u8),
96    /// number literals: 12, 0x1F, 0b101, 0o17, 1_000, 1.5e-3, suffixes (u8, f32)
97    Number,
98    /// a function of its own: (line, position, at the line start after blanks) -> (bytes, kind)
99    Custom(fn(&str, usize, bool) -> Option<(usize, u8)>),
100    /// single characters
101    Punct(&'static str),
102}
103
104/// a language (see the module header)
105pub struct Grammar {
106    pub name: &'static str,
107    /// file extensions (lower case, without the dot)
108    pub exts: &'static [&'static str],
109    pub rules: &'static [Rule],
110    /// characters inside identifiers besides letters, digits and '_'
111    pub ident: &'static str,
112    /// an identifier before '(' is a function
113    pub calls: bool,
114    /// an identifier before '!' is a macro (Rust)
115    pub macros: bool,
116    /// capitalised identifiers are types
117    pub caps_types: bool,
118    /// the line comment of Ctrl+/ ("": none)
119    pub comment: &'static str,
120    /// Enter after one of these indents one level more
121    pub openers: &'static str,
122    /// one of these typed on a blank line goes one level out
123    pub closers: &'static str,
124    /// the bracket pairs the editor matches: "()[]{}"
125    pub pairs: &'static str,
126}
127
128/// the highlighter of a grammar (see the module header)
129#[derive(Clone, Copy)]
130pub struct Highlighter {
131    g: &'static Grammar,
132}
133
134fn push(out: &mut Vec<Token>, start: usize, end: usize, kind: u8) {
135    if end <= start {
136        return;
137    }
138    if let Some(t) = out.last_mut() {
139        if t.kind == kind && t.end == start {
140            t.end = end;
141            return;
142        }
143    }
144    out.push(Token { start, end, kind });
145}
146
147fn enc(rule: usize, depth: u32, extra: u32) -> u32 {
148    (rule as u32 + 1) & 0xFF | (depth.min(255) << 8) | (extra.min(0xFFFF) << 16)
149}
150
151fn utf8_len(b: u8) -> usize {
152    match b {
153        0..=0x7F => 1,
154        0xC0..=0xDF => 2,
155        0xE0..=0xEF => 3,
156        0xF0..=0xF7 => 4,
157        _ => 1,
158    }
159}
160
161impl Highlighter {
162    /// an app's own grammar
163    pub const fn new(g: &'static Grammar) -> Highlighter {
164        Highlighter { g }
165    }
166    /// plain text: no tokens
167    pub const fn plain() -> Highlighter {
168        Highlighter { g: &PLAIN }
169    }
170    /// Rust
171    pub const fn rust() -> Highlighter {
172        Highlighter { g: &RUST }
173    }
174    /// JSON (and JSONC comments)
175    pub const fn json() -> Highlighter {
176        Highlighter { g: &JSON }
177    }
178    /// INI files
179    pub const fn ini() -> Highlighter {
180        Highlighter { g: &INI }
181    }
182    /// TOML
183    pub const fn toml() -> Highlighter {
184        Highlighter { g: &TOML }
185    }
186    /// a Markdown subset
187    pub const fn markdown() -> Highlighter {
188        Highlighter { g: &MARKDOWN }
189    }
190    /// the scripting language of apps/jfs/crates/script (.jfs, JFNetworker's scripts)
191    pub const fn jfs() -> Highlighter {
192        Highlighter { g: &JFS }
193    }
194
195    /// by name or extension, also of a path ("rs", "Rust", "x/y.toml"); plain() for others
196    pub fn for_name(s: &str) -> Highlighter {
197        let s = s.trim();
198        let ext = s.rsplit(['/', '\\']).next().unwrap_or(s);
199        let ext = ext.rsplit('.').next().unwrap_or(ext).to_ascii_lowercase();
200        for g in [&RUST, &JSON, &INI, &TOML, &MARKDOWN, &JFS] {
201            if g.name.eq_ignore_ascii_case(s) || g.exts.contains(&ext.as_str()) {
202                return Highlighter { g };
203            }
204        }
205        Highlighter::plain()
206    }
207
208    /// the grammar (comment prefix, openers, closers, bracket pairs)
209    pub fn grammar(&self) -> &'static Grammar {
210        self.g
211    }
212
213    /// the grammar's name ("Rust", "JSON" ...)
214    pub fn name(&self) -> &'static str {
215        self.g.name
216    }
217
218    fn ident_char(&self, c: char) -> bool {
219        c.is_alphanumeric() || c == '_' || self.g.ident.contains(c)
220    }
221
222    // the identifier starting at i (an index past its end) or None
223    fn ident_at(&self, t: &str, i: usize) -> Option<usize> {
224        let c = t[i..].chars().next()?;
225        if !(c.is_alphabetic() || c == '_') {
226            return None;
227        }
228        let mut e = i + c.len_utf8();
229        for ch in t[e..].chars() {
230            if !self.ident_char(ch) {
231                break;
232            }
233            e += ch.len_utf8();
234        }
235        Some(e)
236    }
237
238    fn prev_ident(&self, t: &str, i: usize) -> bool {
239        t[..i].chars().next_back().is_some_and(|c| self.ident_char(c))
240    }
241
242    /// the tokens of one line from the state of the line before -> the state at its end
243    pub fn line(&self, t: &str, state: u32, out: &mut Vec<Token>) -> u32 {
244        out.clear();
245        self.line_part(t, state, 0, out, &mut |_, _| false).1.unwrap_or(0)
246    }
247
248    // (code_long.rs) the tokens of line t from `from` on -- 0, or a place outside every
249    // construct that a scan of the line from `state` passed -- appended to out; at(i, out) sees
250    // each place outside every construct from `from` on (and the tokens so far), true stops
251    // there -> (where the scan stopped, the state at the line end; None: at stopped it)
252    pub(crate) fn line_part(&self, t: &str, state: u32, from: usize, out: &mut Vec<Token>, at: &mut dyn FnMut(usize, &[Token]) -> bool) -> (usize, Option<u32>) {
253        let mut i = from;
254        let bol = t.len() - t.trim_start_matches([' ', '\t']).len();
255        if from == 0 && state != 0 {
256            let (e, st) = self.resume(t, state, bol, out);
257            if st != 0 {
258                return (t.len(), Some(st));
259            }
260            i = e;
261        }
262        self.scan(t, i, bol, false, out, at)
263    }
264
265    // the tokens from i on (code: inside an interpolation, up to the '}' that closes it; else
266    // at(i) sees each place between constructs, true stops there) -> (where it stopped, the
267    // state at the line end: 0 = nothing open; None: at stopped it)
268    fn scan(&self, t: &str, mut i: usize, bol: usize, code: bool, out: &mut Vec<Token>, at: &mut dyn FnMut(usize, &[Token]) -> bool) -> (usize, Option<u32>) {
269        let b = t.as_bytes();
270        let n = b.len();
271        let rules = self.g.rules;
272        let mut braces = 0u32;
273        'outer: while i < n {
274            if !code && at(i, out) {
275                return (i, None);
276            }
277            if code && b[i] == b'{' {
278                braces += 1;
279            } else if code && b[i] == b'}' {
280                if braces == 0 {
281                    return (i, Some(0));
282                }
283                braces -= 1;
284            }
285            let at_bol = i == bol;
286            let prev = self.prev_ident(t, i);
287            for (ri, rule) in rules.iter().enumerate() {
288                match rule {
289                    Rule::LineComment(p) => {
290                        if t[i..].starts_with(p) {
291                            push(out, i, n, SY_COMMENT);
292                            return (n, Some(0));
293                        }
294                    }
295                    Rule::Block { open, close, kind, nested, bol: only } => {
296                        if (!*only || at_bol) && t[i..].starts_with(open) && !(prev && open.starts_with(|c: char| c.is_alphanumeric())) {
297                            if *only {
298                                push(out, i, n, *kind);
299                                return (n, Some(enc(ri, 1, 0)));
300                            }
301                            let (e, depth) = scan_block(t, i + open.len(), open, close, *nested, 1);
302                            push(out, i, e, *kind);
303                            if depth > 0 {
304                                return (n, Some(enc(ri, depth, 0)));
305                            }
306                            i = e;
307                            continue 'outer;
308                        }
309                    }
310                    Rule::Str { open, close, escape, multi, kind } => {
311                        if t[i..].starts_with(open) && !(prev && open.starts_with(|c: char| c.is_alphanumeric())) {
312                            let (e, done) = scan_str(t, i, i + open.len(), close, *escape, *kind, out);
313                            if !done && *multi {
314                                return (n, Some(enc(ri, 0, 0)));
315                            }
316                            i = e;
317                            continue 'outer;
318                        }
319                    }
320                    Rule::RawStr => {
321                        if let Some((q, hashes)) = raw_open(t, i).filter(|_| !prev) {
322                            match raw_close(t, q, hashes) {
323                                Some(e) => {
324                                    push(out, i, e, SY_STRING);
325                                    i = e;
326                                    continue 'outer;
327                                }
328                                None => {
329                                    push(out, i, n, SY_STRING);
330                                    return (n, Some(enc(ri, 0, hashes as u32)));
331                                }
332                            }
333                        }
334                    }
335                    Rule::Interp { open, close, start } => {
336                        if t[i..].starts_with(open) && !(prev && open.starts_with(|c: char| c.is_alphanumeric())) {
337                            let (e, st) = self.interp(t, i, open.len(), close, start, bol, out);
338                            if st != 0 {
339                                return (n, Some(st));
340                            }
341                            i = e;
342                            continue 'outer;
343                        }
344                    }
345                    Rule::Words(list, kind) => {
346                        if !prev {
347                            if let Some(e) = self.ident_at(t, i) {
348                                if list.contains(&&t[i..e]) {
349                                    push(out, i, e, *kind);
350                                    i = e;
351                                    continue 'outer;
352                                }
353                            }
354                        }
355                    }
356                    Rule::Number => {
357                        if !prev && b[i].is_ascii_digit() {
358                            let e = number_end(t, i);
359                            push(out, i, e, SY_NUMBER);
360                            i = e;
361                            continue 'outer;
362                        }
363                    }
364                    Rule::Custom(f) => {
365                        if let Some((len, kind)) = f(t, i, at_bol) {
366                            if len > 0 {
367                                let e = (i + len).min(n);
368                                push(out, i, e, kind);
369                                i = e;
370                                while i < n && !t.is_char_boundary(i) {
371                                    i += 1;
372                                }
373                                continue 'outer;
374                            }
375                        }
376                    }
377                    Rule::Punct(chars) => {
378                        if b[i] < 0x80 && chars.as_bytes().contains(&b[i]) {
379                            push(out, i, i + 1, SY_PUNCT);
380                            i += 1;
381                            continue 'outer;
382                        }
383                    }
384                }
385            }
386            // no rule: an identifier whole, else one character
387            if !prev {
388                if let Some(e) = self.ident_at(t, i) {
389                    let g = self.g;
390                    let mut next = e;
391                    while next < n && b[next] == b' ' {
392                        next += 1;
393                    }
394                    let kind = if g.macros && b.get(e) == Some(&b'!') && b.get(e + 1) != Some(&b'=') {
395                        SY_FUNC
396                    } else if g.calls && b.get(next) == Some(&b'(') {
397                        SY_FUNC
398                    } else if g.caps_types && t[i..].chars().next().is_some_and(|c| c.is_uppercase()) {
399                        SY_TYPE
400                    } else {
401                        SY_TEXT
402                    };
403                    if kind != SY_TEXT {
404                        push(out, i, if kind == SY_FUNC && g.macros && b.get(e) == Some(&b'!') { e + 1 } else { e }, kind);
405                        if kind == SY_FUNC && g.macros && b.get(e) == Some(&b'!') {
406                            i = e + 1;
407                            continue;
408                        }
409                    }
410                    i = e;
411                    continue;
412                }
413            }
414            i += utf8_len(b[i]);
415        }
416        (n, Some(0))
417    }
418
419    // a string with interpolations from its open quote at i (open: its length) -> (its end,
420    // the state at the line end: a construct inside an interpolation stays open)
421    #[allow(clippy::too_many_arguments)]
422    fn interp(&self, t: &str, i: usize, open: usize, close: &str, start: &str, bol: usize, out: &mut Vec<Token>) -> (usize, u32) {
423        let b = t.as_bytes();
424        let mut s = i;
425        let mut j = i + open;
426        while j < b.len() {
427            if b[j] == b'\\' {
428                push(out, s, j, SY_STRING);
429                let mut e = (j + 1 + b.get(j + 1).map_or(0, |&c| utf8_len(c))).min(b.len());
430                if b.get(j + 1) == Some(&b'u') && b.get(j + 2) == Some(&b'{') {
431                    e = j + 3 + b[j + 3..].iter().take_while(|c| c.is_ascii_hexdigit()).count();
432                    if b.get(e) == Some(&b'}') {
433                        e += 1;
434                    }
435                }
436                while e < b.len() && !t.is_char_boundary(e) {
437                    e += 1;
438                }
439                push(out, j, e, SY_ESCAPE);
440                j = e;
441                s = e;
442                continue;
443            }
444            if t[j..].starts_with(close) {
445                push(out, s, j + close.len(), SY_STRING);
446                return (j + close.len(), 0);
447            }
448            if t[j..].starts_with(start) {
449                push(out, s, j, SY_STRING);
450                push(out, j, j + start.len(), SY_META);
451                let (e, st) = self.scan(t, j + start.len(), bol, true, out, &mut |_, _| false);
452                let st = st.unwrap_or(0);
453                if st != 0 || e >= b.len() {
454                    return (b.len(), st);
455                }
456                push(out, e, e + 1, SY_META);
457                j = e + 1;
458                s = j;
459                continue;
460            }
461            j += utf8_len(b[j]);
462        }
463        push(out, s, b.len(), SY_STRING);
464        (b.len(), 0)
465    }
466
467    // continue a construct open at the line start -> (where it ends, the state: 0 closed)
468    fn resume(&self, t: &str, state: u32, bol: usize, out: &mut Vec<Token>) -> (usize, u32) {
469        let n = t.len();
470        let ri = (state & 0xFF) as usize - 1;
471        let Some(rule) = self.g.rules.get(ri) else { return (0, 0) };
472        match rule {
473            Rule::Block { open, close, kind, nested, bol: only } => {
474                if *only {
475                    push(out, 0, n, *kind);
476                    return if t[bol..].starts_with(close) { (n, 0) } else { (n, state) };
477                }
478                let (e, depth) = scan_block(t, 0, open, close, *nested, (state >> 8) & 0xFF);
479                push(out, 0, e, *kind);
480                if depth > 0 { (n, enc(ri, depth, 0)) } else { (e, 0) }
481            }
482            Rule::Str { close, escape, kind, .. } => {
483                let (e, done) = scan_str(t, 0, 0, close, *escape, *kind, out);
484                if done { (e, 0) } else { (n, state) }
485            }
486            Rule::RawStr => {
487                let hashes = (state >> 16) as usize;
488                match raw_close(t, 0, hashes) {
489                    Some(e) => {
490                        push(out, 0, e, SY_STRING);
491                        (e, 0)
492                    }
493                    None => {
494                        push(out, 0, n, SY_STRING);
495                        (n, state)
496                    }
497                }
498            }
499            _ => (0, 0),
500        }
501    }
502
503    /// the tokens of every line of a text (tests)
504    pub fn tokens(&self, text: &str) -> Vec<Vec<Token>> {
505        let mut st = 0;
506        let mut out = Vec::new();
507        for l in crate::rich_doc::lines_of(text) {
508            let mut v = Vec::new();
509            st = self.line(l, st, &mut v);
510            out.push(v);
511        }
512        out
513    }
514}
515
516// from i inside a block (depth open blocks): the end and the depth left (0: closed)
517fn scan_block(t: &str, i: usize, open: &str, close: &str, nested: bool, depth: u32) -> (usize, u32) {
518    let mut d = depth;
519    let mut j = i;
520    while j < t.len() {
521        if t[j..].starts_with(close) {
522            d -= 1;
523            j += close.len();
524            if d == 0 {
525                return (j, 0);
526            }
527            continue;
528        }
529        if nested && t[j..].starts_with(open) {
530            d += 1;
531            j += open.len();
532            continue;
533        }
534        j += utf8_len(t.as_bytes()[j]);
535    }
536    (t.len(), d.max(1))
537}
538
539// a string from start (its content from i): tokens with escapes -> (end, closed)
540fn scan_str(t: &str, start: usize, i: usize, close: &str, escape: bool, kind: u8, out: &mut Vec<Token>) -> (usize, bool) {
541    let b = t.as_bytes();
542    let mut s = start;
543    let mut j = i;
544    while j < b.len() {
545        if escape && b[j] == b'\\' {
546            push(out, s, j, kind);
547            let mut e = (j + 1 + b.get(j + 1).map_or(0, |&c| utf8_len(c))).min(b.len());
548            if b.get(j + 1) == Some(&b'u') && b.get(j + 2) == Some(&b'{') {
549                e = t[j..].find('}').map_or(b.len(), |k| j + k + 1);
550            } else if b.get(j + 1) == Some(&b'x') {
551                e = (j + 4).min(b.len());
552            } else if b.get(j + 1) == Some(&b'u') && kind == SY_STRING {
553                e = (j + 6).min(b.len());
554            }
555            while e < b.len() && !t.is_char_boundary(e) {
556                e += 1;
557            }
558            push(out, j, e, SY_ESCAPE);
559            j = e;
560            s = e;
561            continue;
562        }
563        if t[j..].starts_with(close) {
564            let e = j + close.len();
565            push(out, s, e, kind);
566            return (e, true);
567        }
568        j += utf8_len(b[j]);
569    }
570    push(out, s, b.len(), kind);
571    (b.len(), false)
572}
573
574// a Rust raw string at i: (the index after its opening quote, the '#' count)
575fn raw_open(t: &str, i: usize) -> Option<(usize, usize)> {
576    let b = t.as_bytes();
577    let mut j = i;
578    if b.get(j) == Some(&b'b') || b.get(j) == Some(&b'c') {
579        j += 1;
580    }
581    if b.get(j) != Some(&b'r') {
582        return None;
583    }
584    j += 1;
585    let mut h = 0;
586    while b.get(j) == Some(&b'#') {
587        h += 1;
588        j += 1;
589    }
590    (b.get(j) == Some(&b'"')).then_some((j + 1, h))
591}
592
593// the end of a raw string's closing quote and hashes from i (None: not on this line)
594fn raw_close(t: &str, i: usize, hashes: usize) -> Option<usize> {
595    let b = t.as_bytes();
596    let mut j = i;
597    while j < b.len() {
598        if b[j] == b'"' && b[j + 1..].iter().take(hashes).filter(|&&c| c == b'#').count() == hashes && b.len() >= j + 1 + hashes {
599            return Some(j + 1 + hashes);
600        }
601        j += 1;
602    }
603    None
604}
605
606// the end of the number literal at i
607fn number_end(t: &str, i: usize) -> usize {
608    let b = t.as_bytes();
609    let mut j = i;
610    let hex = b[i] == b'0' && matches!(b.get(i + 1), Some(b'x') | Some(b'X'));
611    if hex || (b[i] == b'0' && matches!(b.get(i + 1), Some(b'b') | Some(b'o') | Some(b'B') | Some(b'O'))) {
612        j += 2;
613    }
614    while j < b.len() {
615        let c = b[j];
616        if c.is_ascii_alphanumeric() || c == b'_' {
617            // an exponent's sign
618            if !hex && (c == b'e' || c == b'E') && matches!(b.get(j + 1), Some(b'+') | Some(b'-')) && b.get(j + 2).is_some_and(|d| d.is_ascii_digit()) {
619                j += 2;
620                continue;
621            }
622            j += 1;
623        } else if c == b'.' && b.get(j + 1).is_some_and(|d| d.is_ascii_digit()) && !t[i..j].contains('.') {
624            j += 1;
625        } else {
626            break;
627        }
628    }
629    j
630}
631
632// ------------------------------------------------------------------ custom rules
633
634// Rust: 'a' / '\n' / '\u{..}' char literals, else a lifetime 'a
635fn rust_quote(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
636    let b = t.as_bytes();
637    if b[i] != b'\'' {
638        return None;
639    }
640    if b.get(i + 1) == Some(&b'\\') {
641        let e = t[i + 2..].find('\'').map_or(t.len(), |k| i + 2 + k + 1);
642        return Some((e - i, SY_STRING));
643    }
644    let c = t[i + 1..].chars().next()?;
645    let after = i + 1 + c.len_utf8();
646    if b.get(after) == Some(&b'\'') {
647        return Some((after + 1 - i, SY_STRING));
648    }
649    if c.is_alphabetic() || c == '_' {
650        let mut e = after;
651        for ch in t[after..].chars() {
652            if !(ch.is_alphanumeric() || ch == '_') {
653                break;
654            }
655            e += ch.len_utf8();
656        }
657        return Some((e - i, SY_META));
658    }
659    None
660}
661
662// Rust: #[...] / #![...] attributes (brackets counted, to the line end when unclosed)
663fn rust_attr(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
664    let r = &t[i..];
665    let start = if r.starts_with("#[") {
666        2
667    } else if r.starts_with("#![") {
668        3
669    } else {
670        return None;
671    };
672    let mut d = 1;
673    let mut in_str = false;
674    for (k, c) in r[start..].char_indices() {
675        match c {
676            '"' => in_str = !in_str,
677            '[' if !in_str => d += 1,
678            ']' if !in_str => {
679                d -= 1;
680                if d == 0 {
681                    return Some((start + k + 1, SY_META));
682                }
683            }
684            _ => {}
685        }
686    }
687    Some((r.len(), SY_META))
688}
689
690// JSON: a string; followed by ':' a key
691fn json_string(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
692    let b = t.as_bytes();
693    if b[i] != b'"' {
694        return None;
695    }
696    let mut j = i + 1;
697    while j < b.len() {
698        match b[j] {
699            b'\\' => j += 2,
700            b'"' => {
701                j += 1;
702                let rest = t[j.min(t.len())..].trim_start();
703                return Some((j - i, if rest.starts_with(':') { SY_KEY } else { SY_STRING }));
704            }
705            _ => j += 1,
706        }
707    }
708    Some((b.len() - i, SY_STRING))
709}
710
711// INI / TOML: [section] / [[table]] at a line start
712fn ini_section(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
713    if !bol || t.as_bytes()[i] != b'[' {
714        return None;
715    }
716    let e = t[i..].find(']').map_or(t.len(), |k| {
717        let mut e = i + k + 1;
718        if t[e..].starts_with(']') {
719            e += 1;
720        }
721        e
722    });
723    Some((e - i, SY_HEADING))
724}
725
726// INI / TOML: the key before '=' (INI also ':') at a line start
727fn ini_key(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
728    if !bol {
729        return None;
730    }
731    let r = &t[i..];
732    let c = r.chars().next()?;
733    if matches!(c, '[' | ';' | '#' | '=') {
734        return None;
735    }
736    let sep = r.find(['=', ':'])?;
737    let key = r[..sep].trim_end();
738    if key.is_empty() || key.contains(['"', '\'']) && !key.starts_with(['"', '\'']) {
739        return None;
740    }
741    Some((key.len(), SY_KEY))
742}
743
744// a URL: http(s)://.. to a blank or a closing bracket / quote
745fn url_len(r: &str) -> Option<usize> {
746    if !(r.starts_with("http://") || r.starts_with("https://")) {
747        return None;
748    }
749    let e = r.find(|c: char| c.is_whitespace() || matches!(c, ')' | '>' | '"' | '\'' | ']')).unwrap_or(r.len());
750    Some(e)
751}
752
753// Markdown: '#'.. heading lines
754fn md_heading(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
755    if !bol {
756        return None;
757    }
758    let r = &t[i..];
759    let h = r.bytes().take_while(|&c| c == b'#').count();
760    if h == 0 || h > 6 || !(r.len() == h || r.as_bytes()[h] == b' ') {
761        return None;
762    }
763    Some((r.len(), SY_HEADING))
764}
765
766// Markdown: '>' quotation lines
767fn md_quote(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
768    (bol && t.as_bytes()[i] == b'>').then_some((t.len() - i, SY_COMMENT))
769}
770
771// Markdown: ---, *** and ___ rules
772fn md_rule(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
773    if !bol {
774        return None;
775    }
776    let r = t[i..].trim_end();
777    let c = r.bytes().next()?;
778    if !matches!(c, b'-' | b'*' | b'_') {
779        return None;
780    }
781    let n = r.bytes().filter(|&x| x == c).count();
782    (n >= 3 && r.bytes().all(|x| x == c || x == b' ')).then_some((t.len() - i, SY_PUNCT))
783}
784
785// Markdown: list markers "- ", "* ", "+ ", "1. ", "1) "
786fn md_list(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
787    if !bol {
788        return None;
789    }
790    let b = &t.as_bytes()[i..];
791    if b.len() >= 2 && matches!(b[0], b'-' | b'*' | b'+') && b[1] == b' ' {
792        return Some((1, SY_KEYWORD));
793    }
794    let d = b.iter().take_while(|c| c.is_ascii_digit()).count();
795    if d > 0 && d < 10 && b.len() > d + 1 && matches!(b[d], b'.' | b')') && b[d + 1] == b' ' {
796        return Some((d + 1, SY_KEYWORD));
797    }
798    None
799}
800
801// Markdown: **strong** / __strong__ and *emphasis* / _emphasis_ (not inside words)
802fn md_emph(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
803    let b = t.as_bytes();
804    let c = b[i];
805    if c != b'*' && c != b'_' {
806        return None;
807    }
808    if c == b'_' && t[..i].chars().next_back().is_some_and(|p| p.is_alphanumeric()) {
809        return None;
810    }
811    let strong = b.get(i + 1) == Some(&c);
812    let delim = if strong { 2 } else { 1 };
813    let inner = i + delim;
814    if b.get(inner).is_none_or(|x| *x == b' ' || *x == c) {
815        return None;
816    }
817    let pat: &[u8] = if strong { &b[i..i + 2] } else { &b[i..i + 1] };
818    let mut j = inner + 1;
819    while j + delim <= b.len() {
820        if &b[j..j + delim] == pat && b[j - 1] != b' ' && (strong || b.get(j + 1) != Some(&c)) {
821            if c == b'_' && t[j + delim..].chars().next().is_some_and(|p| p.is_alphanumeric()) {
822                j += 1;
823                continue;
824            }
825            return Some((j + delim - i, if strong { SY_STRONG } else { SY_EMPH }));
826        }
827        j += 1;
828    }
829    None
830}
831
832// Markdown: [text](url), ![alt](url), <url>, bare URLs
833fn md_link(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
834    let r = &t[i..];
835    if let Some(e) = url_len(r) {
836        return Some((e, SY_LINK));
837    }
838    if r.starts_with('<') {
839        let e = r.find('>')?;
840        return url_len(&r[1..]).filter(|&u| u + 1 == e).map(|_| (e + 1, SY_LINK));
841    }
842    let s = if r.starts_with("![") { 1 } else if r.starts_with('[') { 0 } else { return None };
843    let close = r[s..].find("](")? + s;
844    let end = r[close + 2..].find(')')? + close + 3;
845    Some((end, SY_LINK))
846}
847
848// Markdown: \* and other backslash escapes
849fn md_escape(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
850    let b = t.as_bytes();
851    (b[i] == b'\\' && b.get(i + 1).is_some_and(|c| c.is_ascii_punctuation())).then_some((2, SY_ESCAPE))
852}
853
854// TOML: dates and times (1979-05-27, 07:32:00, 1979-05-27T07:32:00Z) as one number
855fn toml_date(t: &str, i: usize, _: bool) -> Option<(usize, u8)> {
856    let b = &t.as_bytes()[i..];
857    if b.len() < 5 || !b[0].is_ascii_digit() {
858        return None;
859    }
860    let date = b.len() >= 10 && b[4] == b'-' && b[7] == b'-';
861    let time = b.len() >= 8 && b[2] == b':' && b[5] == b':';
862    if !date && !time {
863        return None;
864    }
865    let e = b.iter().position(|c| !(c.is_ascii_alphanumeric() || matches!(c, b'-' | b':' | b'.' | b'+'))).unwrap_or(b.len());
866    Some((e, SY_NUMBER))
867}
868
869// ------------------------------------------------------------------ the grammars
870
871const RUST_KEYWORDS: &[&str] = &[
872    "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "fn", "for", "if", "impl", "in", "let", "loop",
873    "match", "mod", "move", "mut", "pub", "ref", "return", "self", "static", "struct", "super", "trait", "type", "union", "unsafe", "use", "where",
874    "while", "yield", "macro_rules", "try", "gen",
875];
876const RUST_TYPES: &[&str] = &[
877    "u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32", "f64", "bool", "char", "str", "Self",
878];
879const RUST_CONSTS: &[&str] = &["true", "false", "None", "Some", "Ok", "Err"];
880
881static RUST_RULES: [Rule; 12] = [
882    Rule::LineComment("//"),
883    Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: true, bol: false },
884    Rule::RawStr,
885    Rule::Str { open: "b\"", close: "\"", escape: true, multi: true, kind: SY_STRING },
886    Rule::Str { open: "\"", close: "\"", escape: true, multi: true, kind: SY_STRING },
887    Rule::Custom(rust_quote),
888    Rule::Custom(rust_attr),
889    Rule::Words(RUST_KEYWORDS, SY_KEYWORD),
890    Rule::Words(RUST_TYPES, SY_TYPE),
891    Rule::Words(RUST_CONSTS, SY_CONST),
892    Rule::Number,
893    Rule::Punct("+-*/%=<>!&|^~?:;,.()[]{}@#$"),
894];
895
896/// Rust
897pub static RUST: Grammar = Grammar {
898    name: "Rust",
899    exts: &["rs"],
900    rules: &RUST_RULES,
901    ident: "",
902    calls: true,
903    macros: true,
904    caps_types: true,
905    comment: "//",
906    openers: "{[(",
907    closers: "}])",
908    pairs: "()[]{}",
909};
910
911static JSON_RULES: [Rule; 6] = [
912    Rule::LineComment("//"),
913    Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: false, bol: false },
914    Rule::Custom(json_string),
915    Rule::Words(&["true", "false", "null"], SY_CONST),
916    Rule::Number,
917    Rule::Punct("{}[],:-"),
918];
919
920/// JSON (comments of JSONC too)
921pub static JSON: Grammar = Grammar {
922    name: "JSON",
923    exts: &["json", "jsonc", "geojson"],
924    rules: &JSON_RULES,
925    ident: "",
926    calls: false,
927    macros: false,
928    caps_types: false,
929    comment: "//",
930    openers: "{[",
931    closers: "}]",
932    pairs: "[]{}",
933};
934
935const INI_CONSTS: &[&str] = &["true", "false", "yes", "no", "on", "off", "TRUE", "FALSE", "Yes", "No", "On", "Off", "True", "False"];
936
937fn ini_comment(t: &str, i: usize, bol: bool) -> Option<(usize, u8)> {
938    (bol && matches!(t.as_bytes()[i], b';' | b'#')).then_some((t.len() - i, SY_COMMENT))
939}
940
941static INI_RULES: [Rule; 8] = [
942    Rule::Custom(ini_comment),
943    Rule::Custom(ini_section),
944    Rule::Custom(ini_key),
945    Rule::Str { open: "\"", close: "\"", escape: true, multi: false, kind: SY_STRING },
946    Rule::Str { open: "'", close: "'", escape: false, multi: false, kind: SY_STRING },
947    Rule::Words(INI_CONSTS, SY_CONST),
948    Rule::Number,
949    Rule::Punct("=:,[]"),
950];
951
952/// INI files (; and # comments at a line start, [sections], key = value, key: value)
953pub static INI: Grammar = Grammar {
954    name: "INI",
955    exts: &["ini", "cfg", "conf", "desktop", "properties", "inf", "reg"],
956    rules: &INI_RULES,
957    ident: "-.",
958    calls: false,
959    macros: false,
960    caps_types: false,
961    comment: ";",
962    openers: "",
963    closers: "",
964    pairs: "[]",
965};
966
967static TOML_RULES: [Rule; 12] = [
968    Rule::LineComment("#"),
969    Rule::Custom(ini_section),
970    Rule::Custom(ini_key),
971    Rule::Str { open: "\"\"\"", close: "\"\"\"", escape: true, multi: true, kind: SY_STRING },
972    Rule::Str { open: "'''", close: "'''", escape: false, multi: true, kind: SY_STRING },
973    Rule::Str { open: "\"", close: "\"", escape: true, multi: false, kind: SY_STRING },
974    Rule::Str { open: "'", close: "'", escape: false, multi: false, kind: SY_STRING },
975    Rule::Words(&["true", "false", "inf", "nan"], SY_CONST),
976    Rule::Custom(toml_date),
977    Rule::Number,
978    Rule::Punct("=,.[]{}+-"),
979    Rule::Punct(":"),
980];
981
982/// TOML
983pub static TOML: Grammar = Grammar {
984    name: "TOML",
985    exts: &["toml", "lock"],
986    rules: &TOML_RULES,
987    ident: "-",
988    calls: false,
989    macros: false,
990    caps_types: false,
991    comment: "#",
992    openers: "[{",
993    closers: "]}",
994    pairs: "[]{}",
995};
996
997static MD_RULES: [Rule; 11] = [
998    Rule::Block { open: "```", close: "```", kind: SY_CODE, nested: false, bol: true },
999    Rule::Block { open: "~~~", close: "~~~", kind: SY_CODE, nested: false, bol: true },
1000    Rule::Block { open: "<!--", close: "-->", kind: SY_COMMENT, nested: false, bol: false },
1001    Rule::Custom(md_heading),
1002    Rule::Custom(md_quote),
1003    Rule::Custom(md_rule),
1004    Rule::Custom(md_list),
1005    Rule::Custom(md_escape),
1006    Rule::Str { open: "`", close: "`", escape: false, multi: false, kind: SY_CODE },
1007    Rule::Custom(md_emph),
1008    Rule::Custom(md_link),
1009];
1010
1011/// a Markdown subset: headings, quotations, rules, lists, fenced code, code spans, emphasis,
1012/// strong, links, escapes, HTML comments
1013pub static MARKDOWN: Grammar = Grammar {
1014    name: "Markdown",
1015    exts: &["md", "markdown", "mdown"],
1016    rules: &MD_RULES,
1017    ident: "",
1018    calls: false,
1019    macros: false,
1020    caps_types: false,
1021    comment: "",
1022    openers: "",
1023    closers: "",
1024    pairs: "()[]",
1025};
1026
1027// the lexical rules of apps/jfs/crates/script/lex.rs: keywords, true / false / nil, one-line strings
1028// with escapes and ${..} (code inside, nested strings too), numbers with _, 0x, 0b, a
1029// fraction and an exponent, // and /* */ comments (not nested)
1030const JFS_KEYWORDS: &[&str] = &["let", "fn", "if", "else", "while", "for", "in", "break", "continue", "return", "try", "catch", "throw"];
1031
1032static JFS_RULES: [Rule; 7] = [
1033    Rule::LineComment("//"),
1034    Rule::Block { open: "/*", close: "*/", kind: SY_COMMENT, nested: false, bol: false },
1035    Rule::Interp { open: "\"", close: "\"", start: "${" },
1036    Rule::Words(JFS_KEYWORDS, SY_KEYWORD),
1037    Rule::Words(&["true", "false", "nil"], SY_CONST),
1038    Rule::Number,
1039    Rule::Punct("+-*/%=<>!&|?:;,.()[]{}"),
1040];
1041
1042/// the scripting language of apps/jfs/crates/script (.jfs): calls before '(' are functions, the
1043/// delimiters of ${..} in strings SY_META
1044pub static JFS: Grammar = Grammar {
1045    name: "JFS",
1046    exts: &["jfs"],
1047    rules: &JFS_RULES,
1048    ident: "",
1049    calls: true,
1050    macros: false,
1051    caps_types: false,
1052    comment: "//",
1053    openers: "{[(",
1054    closers: "}])",
1055    pairs: "()[]{}",
1056};
1057
1058static PLAIN_RULES: [Rule; 0] = [];
1059
1060/// plain text: no tokens
1061pub static PLAIN: Grammar = Grammar {
1062    name: "Text",
1063    exts: &["txt", "text", "log"],
1064    rules: &PLAIN_RULES,
1065    ident: "",
1066    calls: false,
1067    macros: false,
1068    caps_types: false,
1069    comment: "",
1070    openers: "",
1071    closers: "",
1072    pairs: "()[]{}",
1073};
1074
1075// ------------------------------------------------------------------ colours
1076
1077/// the colours of a code editor (0xAARRGGBB); bold / italic: a bit per SY_* kind
1078#[derive(Clone, Debug, PartialEq)]
1079pub struct CodeTheme {
1080    pub bg: u32,
1081    pub fg: u32,
1082    pub gutter_bg: u32,
1083    pub gutter_fg: u32,
1084    /// the current line's number
1085    pub gutter_cur: u32,
1086    /// the current line's background
1087    pub line_hl: u32,
1088    /// the selection (focused / not focused)
1089    pub sel: u32,
1090    pub sel_inactive: u32,
1091    /// the selected text (0: the tokens keep their colours)
1092    pub sel_text: u32,
1093    /// the frame around a matching bracket pair
1094    pub bracket: u32,
1095    /// the find matches and the current one
1096    pub find: u32,
1097    pub find_cur: u32,
1098    /// find matches framed instead of filled (high contrast)
1099    pub find_frame: bool,
1100    pub caret: u32,
1101    pub tokens: [u32; SY_COUNT],
1102    pub bold: u32,
1103    pub italic: u32,
1104    /// the wavy underlines and gutter markers of errors, warnings and infos (DIAG_* order)
1105    pub diag: [u32; 3],
1106    /// breakpoints in the gutter
1107    pub breakpoint: u32,
1108    /// the background of a debugger's current line (the editor's background: a frame in
1109    /// exec_mark instead) and its arrow in the gutter
1110    pub exec_line: u32,
1111    pub exec_mark: u32,
1112}
1113
1114const fn argb(rgb: u32) -> u32 {
1115    0xFF00_0000 | rgb
1116}
1117
1118impl CodeTheme {
1119    /// on a light background (the colours of GitHub's light theme)
1120    pub fn light() -> CodeTheme {
1121        let fg = argb(0x1F2328);
1122        CodeTheme {
1123            bg: argb(0xFFFFFF),
1124            fg,
1125            gutter_bg: argb(0xF6F8FA),
1126            gutter_fg: argb(0x8C959F),
1127            gutter_cur: fg,
1128            line_hl: argb(0xF1F4F8),
1129            sel: argb(0xB6D7FF),
1130            sel_inactive: argb(0xE1E6EB),
1131            sel_text: 0,
1132            bracket: argb(0x8C959F),
1133            find: argb(0xFFE58A),
1134            find_cur: argb(0xFFA94D),
1135            find_frame: false,
1136            caret: fg,
1137            tokens: [
1138                fg,
1139                argb(0xCF222E),
1140                argb(0x953800),
1141                argb(0x8250DF),
1142                argb(0x0A3069),
1143                argb(0x0550AE),
1144                argb(0x6E7781),
1145                argb(0x0550AE),
1146                argb(0x57606A),
1147                argb(0x116329),
1148                argb(0x0550AE),
1149                argb(0x0550AE),
1150                fg,
1151                fg,
1152                argb(0x0969DA),
1153                argb(0x0A3069),
1154                argb(0x116329),
1155            ],
1156            bold: 1 << SY_HEADING | 1 << SY_STRONG,
1157            italic: 1 << SY_COMMENT | 1 << SY_EMPH,
1158            diag: [argb(0xCF222E), argb(0x9A6700), argb(0x0969DA)],
1159            breakpoint: argb(0xD1242F),
1160            exec_line: argb(0xFFF1BF),
1161            exec_mark: argb(0x9A6700),
1162        }
1163    }
1164
1165    /// on a dark background (the colours of GitHub's dark theme)
1166    pub fn dark() -> CodeTheme {
1167        let fg = argb(0xE6EDF3);
1168        CodeTheme {
1169            bg: argb(0x0D1117),
1170            fg,
1171            gutter_bg: argb(0x0D1117),
1172            gutter_fg: argb(0x6E7681),
1173            gutter_cur: fg,
1174            line_hl: argb(0x161B22),
1175            sel: argb(0x264F78),
1176            sel_inactive: argb(0x30363D),
1177            sel_text: 0,
1178            bracket: argb(0x8B949E),
1179            find: argb(0x5C4A14),
1180            find_cur: argb(0x9E6A03),
1181            find_frame: false,
1182            caret: fg,
1183            tokens: [
1184                fg,
1185                argb(0xFF7B72),
1186                argb(0xFFA657),
1187                argb(0xD2A8FF),
1188                argb(0xA5D6FF),
1189                argb(0x79C0FF),
1190                argb(0x8B949E),
1191                argb(0x79C0FF),
1192                argb(0xC9D1D9),
1193                argb(0x7EE787),
1194                argb(0x79C0FF),
1195                argb(0x79C0FF),
1196                fg,
1197                fg,
1198                argb(0x58A6FF),
1199                argb(0xA5D6FF),
1200                argb(0x7EE787),
1201            ],
1202            bold: 1 << SY_HEADING | 1 << SY_STRONG,
1203            italic: 1 << SY_COMMENT | 1 << SY_EMPH,
1204            diag: [argb(0xF85149), argb(0xD29922), argb(0x58A6FF)],
1205            breakpoint: argb(0xF85149),
1206            exec_line: argb(0x3B3520),
1207            exec_mark: argb(0xE3B341),
1208        }
1209    }
1210
1211    /// the colours of a high-contrast scheme: few colours, keywords and headings bold;
1212    /// diagnostics in the text, link and highlight colours (the gutter markers' shapes tell
1213    /// them apart too)
1214    pub fn high_contrast(c: &plat::HcColors) -> CodeTheme {
1215        let (bg, fg, hi, link, dis) = (argb(c.bg), argb(c.text), argb(c.highlight), argb(c.link), argb(c.disabled));
1216        CodeTheme {
1217            bg,
1218            fg,
1219            gutter_bg: bg,
1220            gutter_fg: fg,
1221            gutter_cur: link,
1222            line_hl: bg,
1223            sel: hi,
1224            sel_inactive: dis,
1225            sel_text: argb(c.highlight_text),
1226            bracket: link,
1227            find: link,
1228            find_cur: hi,
1229            find_frame: true,
1230            caret: fg,
1231            tokens: [fg, link, link, fg, hi, hi, dis, hi, fg, link, link, link, fg, fg, link, hi, link],
1232            bold: 1 << SY_KEYWORD | 1 << SY_HEADING | 1 << SY_STRONG | 1 << SY_FUNC,
1233            italic: 1 << SY_COMMENT | 1 << SY_EMPH,
1234            diag: [fg, link, hi],
1235            breakpoint: link,
1236            exec_line: bg,
1237            exec_mark: hi,
1238        }
1239    }
1240}
1241
1242impl crate::Ui {
1243    /// the code editors' colours for the UI's theme: dark / light syntax colours on the UI's
1244    /// text box colours (accent selection, diagnostics in its critical / warning / info
1245    /// colours), or the high-contrast scheme's colours
1246    pub fn code_theme(&self) -> CodeTheme {
1247        use crate::consts::*;
1248        use crate::theme::color_mix;
1249        if self.high_contrast {
1250            let c = self.hc_sys.unwrap_or(if self.theme_dark { plat::HcColors::BLACK } else { plat::HcColors::WHITE });
1251            return CodeTheme::high_contrast(&c);
1252        }
1253        let mut t = if self.theme_dark { CodeTheme::dark() } else { CodeTheme::light() };
1254        let over = |bg: u32, c: u32| -> u32 {
1255            let a = (c >> 24) as i32;
1256            color_mix(bg | 0xFF00_0000, c | 0xFF00_0000, a + (a >> 7))
1257        };
1258        let bg = over(self.color(T_BG), self.color(T_INPUT_ACTIVE));
1259        let fg = over(bg, self.color(T_TEXT));
1260        t.bg = bg;
1261        t.fg = fg;
1262        t.tokens[SY_TEXT as usize] = fg;
1263        t.tokens[SY_EMPH as usize] = fg;
1264        t.tokens[SY_STRONG as usize] = fg;
1265        t.caret = fg;
1266        t.gutter_bg = color_mix(bg, fg, 10);
1267        t.gutter_fg = over(t.gutter_bg, self.color(T_TEXT3));
1268        t.gutter_cur = fg;
1269        t.line_hl = color_mix(bg, fg, if self.theme_dark { 16 } else { 10 });
1270        t.sel = color_mix(bg, self.color(T_ACCENT) | 0xFF00_0000, if self.theme_dark { 110 } else { 80 });
1271        t.sel_inactive = color_mix(bg, fg, if self.theme_dark { 40 } else { 30 });
1272        t.diag = [over(bg, self.color(T_CRIT)), over(bg, self.color(T_WARN)), over(bg, self.color(T_INFO))];
1273        t.breakpoint = t.diag[0];
1274        t.exec_mark = t.diag[1];
1275        t.exec_line = color_mix(bg, t.diag[1], if self.theme_dark { 48 } else { 40 });
1276        t
1277    }
1278}