Skip to main content

ui/
code_edit.rs

1// code_edit -- ui.code_editor: a code editor over a CodeDoc (code_doc.rs) with syntax
2// highlighting (highlight.rs)
3//
4//   ui.code_editor(id, &mut doc, &hl, CE_*) -> EV_*   the editor, filling the place it gets
5//        (natural 560 x 320 DIP; next_size(-1, -1) fills): line numbers, the monospace font
6//        of mono_font_set (else the system's), tabs to tab_width, the current line, the
7//        bracket pair at the caret, a find / replace bar, scroll bars.  Only the visible lines
8//        are laid out and highlighted (the editor keeps each line's start state and follows
9//        the document's change journal), so a file of 100 000 lines scrolls as fast as a
10//        short one; of a long line only the part in view (code_long.rs keeps the columns and
11//        tokens of long lines and works them again near an edit only), so typing in a line of
12//        1 MB is as fast as in a short one.  EV_ACT: the text changed; EV_FOCUS while
13//        focused; EV_COMMIT: the focus left.  Ids id .. id + 15 (id + 1 / + 2 the scroll bars,
14//        + 3 .. 13 the find bar).
15//        CE_READONLY: select, copy and find only; CE_NO_GUTTER: no line numbers;
16//        CE_NO_FRAME: no box; CE_NO_FIND: no find bar; CE_NO_LINE: no current-line highlight;
17//        CE_MARKS: the gutter keeps its column of diagnostic markers while there are none;
18//        CE_BREAKPOINTS: a column for breakpoints at the gutter's left (also there while the
19//        document has breakpoints or an execution line); CE_FOLDS: the folding column stays
20//        while the document has no folding ranges
21//   ui.code_editor_with(id, &mut doc, &hl, Some(&mut assist), CE_*)   the same with a
22//        language's services (code_assist.rs: completion popup, hover tooltips, signature
23//        help); ids id + 14 (the completion list) and id + 15 are the editor's too
24//   ui.code_events(id) -> Vec<CodeEv>   what the user asked for in the editor's last pass:
25//        Definition(pos) (Ctrl+click, F12), Rename(pos) (F2), Format (Shift+Alt+F),
26//        Breakpoint(line) (a click in the breakpoints' column); the app answers (a jump,
27//        CodeDoc::apply_edits / replace_text as one undo step, set_breakpoints);
28//        ui.code_popup_item(id) the completion item the list has chosen (None: closed)
29//   ui.code_find(id, query, replace)   open the find bar (replace: with the replace row) with
30//        a query; the editor selects the first match from the caret on
31//   ui.code_reveal(id, line)   scroll line (from 0) into view in the editor's next pass, in the
32//        middle when it was not in view, the folds hiding it opened -- without the keyboard
33//        focus, the caret or the selection (a debugger's execution line)
34//   ui.code_status(id) -> CodeStatus   line and column of the caret (from 1), the matches
35//        and the current one, the lines tokenized in the last frame, the document's
36//        diagnostics per severity and the severity of the one at the caret, the completion
37//        list's items and choice, whether signature help is open, the lines folded away
38//   ui.code_diag(id) -> Option<Diag>   the (most severe) diagnostic at the caret, its message
39//   ui.code_theme_set(Some(theme))     the colours of every code editor (None: code_theme())
40//   ui.code_font_size(pt)              the size of the code editors' font (default 10 pt)
41//
42// Diagnostics (CodeDoc::set_diagnostics; they follow the edits, code_doc.rs) are wavy
43// underlines below their text in the colour of their severity (infos below warnings below
44// errors; a point or the end of a line: one cell) and a marker in the gutter's first column on
45// the line where one starts (the most severe: a filled circle with a cross, a triangle, an
46// "i" -- shapes, not only colours, also in high contrast).  The column is there while the
47// document has diagnostics (CE_MARKS: always, so the text does not move).  The pointer over an
48// underline or a marker shows the messages as a tooltip ("Fehler: ...", most severe first);
49// the ones at the caret are in code_status / code_diag and a K_TEXT record at the caret's line
50// for screen readers (after the editor's record: the app's post-modifiers stay the editor's).
51// Only the diagnostics of the visible lines are looked at (binary search by position), an
52// underline is one command however long it is.
53//
54// Keys (besides those of ed_core.rs): Tab / Shift+Tab indent / unindent (several lines: the
55// block), Enter a line break with the indentation (one level more after an opener), a closer
56// typed on a blank line goes one level out, Backspace in leading blanks back to the previous
57// tab stop, Home toggles between the first non-blank and column 0, Ctrl+C / Ctrl+X without a
58// selection the whole line, Ctrl+/ line comments, Alt+Up / Alt+Down move the lines, Ctrl+Up /
59// Ctrl+Down scroll, Ctrl+F find, Ctrl+H replace, F3 / Shift+F3 (Enter / Shift+Enter in the
60// find box) the next / previous match, F8 / Shift+F8 the caret to the start of the next /
61// previous diagnostic (round the end; without diagnostics F8 stays the window's key for the
62// splitters), Esc closes the find bar (else drops the focus),
63// Ctrl+Tab (read-only: Tab) moves the focus on.  Mouse: click, drag, Shift+click, double
64// click a word, a click or drag in the gutter selects lines, the wheel (Shift: sideways).
65// Accessibility: a K_TEXTBOX record with the text (beyond 64 KB: the visible lines) and the
66// caret / anchor as byte offsets into it, ime_pos for IME and magnifiers, the edit state of
67// the text boxes while focused (ed_hold).
68//
69// Language services (code_editor_with; code_assist.rs has the popups): Ctrl+Space opens the
70// completion list (typing a word or a trigger character opens it too), Ctrl+Shift+Space
71// signature help, the pointer resting on a word shows the assist's hover text (after the
72// diagnostics there), F12 / Ctrl+click, F2 and Shift+Alt+F are CodeEv.  The gutter from the
73// left: breakpoints (CE_BREAKPOINTS: a filled circle, a ring when disabled, a bar across it
74// with a condition; the execution line an arrow there and a marked background), diagnostic
75// markers, line numbers, folding (a chevron on the first line of each of the document's
76// folding ranges: a click folds / unfolds; Ctrl+Shift+[ / ] at the caret's line).  Folded
77// lines are left out of the view (rows count only the visible lines), the caret skips them
78// (Up / Down / Left / Right / PgUp / PgDn), find steps over their matches, a "⋯" box after a
79// folded line unfolds it on a click, and an edit inside them or the caret landing there (an
80// app's set_selection, undo) unfolds them.  Without folds a row is a line, so 100 000 lines
81// stay as fast as before.
82
83use alloc::collections::BTreeMap;
84use alloc::format;
85use alloc::rc::Rc;
86use alloc::string::String;
87use alloc::vec::Vec;
88
89use gfx::{CAP_ROUND, JOIN_ROUND};
90use text::segment;
91use text::FS_SYN_ITALIC;
92
93use crate::cmd::{Draw, DrawRef, Gfx};
94use crate::code_assist::*;
95use crate::code_doc::*;
96use crate::code_long::{Longs, COL_LONG};
97use crate::consts::*;
98use crate::ed_core::*;
99use crate::highlight::*;
100use crate::i18n::tr;
101use crate::keys::*;
102use crate::{Sp, Ui};
103
104/// code_editor flags: select, copy and find only
105pub const CE_READONLY: u32 = 1;
106/// no line numbers
107pub const CE_NO_GUTTER: u32 = 2;
108/// no text box frame
109pub const CE_NO_FRAME: u32 = 4;
110/// no find bar
111pub const CE_NO_FIND: u32 = 8;
112/// no highlight of the current line
113pub const CE_NO_LINE: u32 = 16;
114/// the gutter keeps its column of diagnostic markers while the document has none
115pub const CE_MARKS: u32 = 32;
116/// the gutter's column of breakpoints (a click there: CodeEv::Breakpoint)
117pub const CE_BREAKPOINTS: u32 = 64;
118/// the gutter keeps its folding column while the document has no folding ranges
119pub const CE_FOLDS: u32 = 128;
120
121const NAT_W: i32 = 560;
122const NAT_H: i32 = 320;
123const LABEL_MAX: usize = 64 * 1024; // beyond: the record holds the visible lines
124const BRACKET_LINES: usize = 3000; // how far bracket matching looks
125const UNKNOWN: u32 = u32::MAX;
126
127/// what code_status reports
128#[derive(Clone, Copy, Debug, Default, PartialEq)]
129pub struct CodeStatus {
130    /// the caret's line and column (from 1; columns with tabs expanded)
131    pub line: usize,
132    pub col: usize,
133    /// the find bar's matches (0: none or closed) and the current one (from 1; 0: none)
134    pub matches: usize,
135    pub current: usize,
136    /// the lines tokenized for highlighting states in the editor's last frame (the visible
137    /// lines are tokenized again to draw them, not counted)
138    pub hl_lines: usize,
139    /// the number of lines
140    pub lines: usize,
141    /// the document's diagnostics: errors, warnings, infos (DIAG_* order)
142    pub diags: [usize; 3],
143    /// the severity (DIAG_*) of the most severe diagnostic at the caret (ui.code_diag: the
144    /// diagnostic with its message)
145    pub diag: Option<u8>,
146    /// the items the completion list shows (0: closed) and the chosen one (from 0)
147    pub popup: usize,
148    pub popup_sel: usize,
149    /// signature help is open
150    pub sig: bool,
151    /// the lines folded away
152    pub hidden: usize,
153}
154
155// the highlighting states: states[i] = the state line i starts in
156#[derive(Default)]
157struct Hl {
158    grammar: usize,
159    uid: u32,
160    rev: u64,
161    states: Vec<u32>,
162    // states[..=valid] are right; states up to `known` were right before the last changes
163    // and from `dirty` on (past the changed lines) a state that agrees again proves the rest
164    valid: usize,
165    known: usize,
166    dirty: usize,
167    // lines tokenized this frame
168    count: usize,
169}
170
171#[derive(Default)]
172struct Find {
173    open: bool,
174    replace: bool,
175    query: String,
176    rep: String,
177    case: i32,
178    word: i32,
179    matches: Vec<(TextPos, TextPos)>,
180    // the query, options, revision and document the matches belong to
181    key: (String, u8, u64, u32),
182    current: Option<usize>,
183    // the caret when the search began (incremental search starts there)
184    from: TextPos,
185}
186
187impl Find {
188    fn opts(&self) -> u8 {
189        (if self.case != 0 { FIND_CASE } else { 0 }) | (if self.word != 0 { FIND_WORD } else { 0 })
190    }
191}
192
193#[derive(Default)]
194struct View {
195    seen: u32,
196    sx: i32,
197    sy: i32,
198    t0: u32,
199    prefx: i32,
200    // 0 none, 1 characters, 2 words, 3 lines (gutter)
201    drag: u8,
202    word: (TextPos, TextPos),
203    held: Option<(usize, usize)>,
204    // the record's text: (document, revision, first line, last line, text)
205    label: (u32, u64, usize, usize, String),
206    last_sel: TextSel,
207    last_rev: u64,
208    hl: Hl,
209    find: Find,
210    // the widest line and all lines (bytes) by document and revision
211    max_cols: (u32, u64, usize, usize),
212    bracket: Option<(u32, u64, TextPos, Option<(TextPos, TextPos)>)>,
213    status: CodeStatus,
214    // the lines tokenized for states by the last pass that had something to do
215    hl_last: usize,
216    // the most severe diagnostic at the caret
217    caret_diag: Option<Diag>,
218    // columns and tokens of long lines
219    long: Longs,
220    // the language services' popups and hover text, the requests of the last pass
221    pop: Pop,
222    sig: Sig,
223    hov: Hov,
224    events: Vec<CodeEv>,
225    // the caret into view in this pass (a popup opened by a key)
226    show_caret: bool,
227}
228
229/// shared state (Ui::m.code)
230#[derive(Default)]
231pub(crate) struct S {
232    views: BTreeMap<u32, View>,
233    theme: Option<CodeTheme>,
234    size_pt: f32,
235    // a find request: id, query, replace row
236    find_req: Option<(u32, String, bool)>,
237    // code_reveal: the line each editor shows in its next pass
238    reveal_req: BTreeMap<u32, usize>,
239    // the drawings of wavy underlines: colour, size key (the same object every frame)
240    waves: Vec<(u32, u32, DrawRef)>,
241}
242
243// a wavy underline across its rectangle, drawn at render time (one command however long)
244struct Wave {
245    color: u32,
246    // half a wave and its height (px), the line's width (24.8)
247    hp: i32,
248    amp: i32,
249    width: i32,
250}
251
252impl Draw for Wave {
253    fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
254        let pad = (self.width >> 9) + 1;
255        let (x0, x1, top) = (r[0] + pad, r[0] + r[2] - pad, r[1] + pad);
256        // only the waves near the clip (a dirty part), in the phase of the whole line
257        let (c0, c1) = (g.dc.clip[0] - g.dc.ox, g.dc.clip[2] - g.dc.ox);
258        let k0 = ((c0 - x0) / self.hp - 2).max(0);
259        let y = |k: i32| ((top + (k & 1) * self.amp) << 8) + 128;
260        let sh = g.shapes();
261        sh.path.begin(sh.dc);
262        let mut k = k0;
263        let mut x = x0 + k * self.hp;
264        sh.path.move_fx(x << 8, y(k));
265        while x < x1 && x <= c1 + self.hp * 2 {
266            x = (x + self.hp).min(x1);
267            k += 1;
268            sh.path.line_fx(x << 8, y(k));
269        }
270        sh.dc.set_color(self.color);
271        sh.path.stroke(sh.ras, sh.dc, sh.sf, self.width, CAP_ROUND << 4 | JOIN_ROUND);
272    }
273}
274
275// the font and cells of the editors
276#[derive(Clone, Copy)]
277struct Metrics {
278    font: u32,
279    italic: u32,
280    size: i32,
281    cw: i32,
282    lh: i32,
283    base: i32,
284    per_cell: bool,
285}
286
287// the caret moves over lines of cells
288struct CView<'a> {
289    doc: &'a CodeDoc,
290    cw: i32,
291    lh: i32,
292    tw: usize,
293    long: &'a Longs,
294}
295
296impl EdView for CView<'_> {
297    fn lines(&self) -> usize {
298        self.doc.line_count()
299    }
300    fn text(&self, line: usize) -> &str {
301        self.doc.line(line)
302    }
303    fn rows(&self, _: usize) -> usize {
304        1
305    }
306    fn row_range(&self, line: usize, _: usize) -> (usize, usize) {
307        (0, self.doc.line(line).len())
308    }
309    fn x_at(&self, line: usize, _: usize, byte: usize) -> i32 {
310        self.long.col(self.doc, line, byte, self.tw) as i32 * self.cw
311    }
312    fn hit_x(&self, line: usize, _: usize, x: i32) -> usize {
313        let col = ((x.max(0) + self.cw / 2) / self.cw.max(1)) as usize;
314        self.long.byte(self.doc, line, col, self.tw)
315    }
316    // (rows: the lines that are not folded away)
317    fn row_y(&self, line: usize, _: usize) -> (i32, i32) {
318        (self.doc.row_of_line(line) as i32 * self.lh, self.lh)
319    }
320    fn row_at(&self, y: i32) -> (usize, usize) {
321        (self.doc.line_of_row((y.max(0) / self.lh.max(1)) as usize), 0)
322    }
323}
324
325// an identifier or a run of the same other characters around byte (double click)
326fn code_word(t: &str, byte: usize) -> (usize, usize) {
327    let b = byte.min(t.len());
328    let at = t[b..].chars().next().or_else(|| t[..b].chars().next_back());
329    match at {
330        Some(c) if is_word(c) => {
331            let a = t[..b].char_indices().rev().take_while(|(_, c)| is_word(*c)).last().map_or(b, |(i, _)| i);
332            let e = t[b..].char_indices().find(|(_, c)| !is_word(*c)).map_or(t.len(), |(i, _)| b + i);
333            (a, e)
334        }
335        _ => word_range(t, b),
336    }
337}
338
339impl Ui {
340    /// the colours of every code editor (None: code_theme(), following the UI's theme)
341    pub fn code_theme_set(&mut self, theme: Option<CodeTheme>) {
342        self.m.code.theme = theme;
343        self.repass();
344    }
345
346    /// the size of the code editors' font in points (0: 10 pt)
347    pub fn code_font_size(&mut self, pt: f32) {
348        self.m.code.size_pt = pt;
349        self.repass();
350    }
351
352    /// open the find bar of code editor id with a query (replace: with the replace row); the
353    /// editor selects the first match from its caret on
354    pub fn code_find(&mut self, id: u32, query: &str, replace: bool) {
355        self.m.code.find_req = Some((id, String::from(query), replace));
356        self.repass();
357    }
358
359    /// scroll code editor id so that line (from 0) is in view in its next pass: in the middle
360    /// of the view when it was not in it (no scroll when it was), the folded ranges that hide it
361    /// unfolded.  The keyboard focus, the caret and the selection stay as they are: a debugger
362    /// shows the execution line while the page or its toolbar keeps the keyboard
363    /// (CodeDoc::set_exec_line marks it).  A later request for the same editor replaces it.
364    pub fn code_reveal(&mut self, id: u32, line: usize) {
365        self.m.code.reveal_req.insert(id, line);
366        self.repass();
367    }
368
369    /// the caret, the find matches, the lines tokenized and the diagnostics in code editor
370    /// id's last frame
371    pub fn code_status(&self, id: u32) -> CodeStatus {
372        self.m.code.views.get(&id).map_or(CodeStatus::default(), |v| v.status)
373    }
374
375    /// the most severe diagnostic at the caret of code editor id in its last frame (its
376    /// message for a status bar)
377    pub fn code_diag(&self, id: u32) -> Option<Diag> {
378        self.m.code.views.get(&id).and_then(|v| v.caret_diag.clone())
379    }
380
381    /// what the user asked of code editor id in its last pass (go to definition, rename,
382    /// format, a breakpoint's click)
383    pub fn code_events(&self, id: u32) -> Vec<CodeEv> {
384        self.m.code.views.get(&id).map_or(Vec::new(), |v| v.events.clone())
385    }
386
387    /// the item the completion list of code editor id has chosen (None: closed)
388    pub fn code_popup_item(&self, id: u32) -> Option<CompItem> {
389        let v = self.m.code.views.get(&id)?;
390        let p = &v.pop;
391        p.shown.get(p.sel).and_then(|&i| p.items.get(i)).filter(|_| p.open).cloned()
392    }
393
394    // the monospace font: the cell a whole number of pixels wide (as the consoles)
395    fn ce_metrics(&mut self) -> Metrics {
396        let pt = if self.m.code.size_pt > 0.0 { self.m.code.size_pt } else { 10.0 };
397        let (font, _) = self.mono_font(pt);
398        let dpi = self.wp.rt.dpi as f32;
399        let scale = self.text_scale as f32 / 100.0;
400        let mut size = ((pt.clamp(4.0, 96.0) * 4.0 / 3.0 * dpi / 96.0 * scale * 256.0) as i32).max(256);
401        let prev = self.text.get_font();
402        self.text.set_font(font, size);
403        let per_cell = self.text.measure("i") != self.text.measure("M");
404        let cw = if per_cell {
405            self.text.width("M").max(self.text.width("W")).max(1)
406        } else {
407            let adv = self.text.measure("0000000000");
408            let cw = ((adv + 1280) / 2560).max(1);
409            if adv > 0 {
410                size = (size as i64 * (cw as i64 * 2560) / adv as i64) as i32;
411                self.text.set_font(font, size);
412            }
413            cw
414        };
415        let (asc, desc) = (self.text.ascent(), self.text.descent());
416        let lh = (self.text.line_h().max(asc + desc) + self.dp(3)).max(1);
417        let base = ((lh - asc - desc) >> 1) + asc;
418        self.text.set_font(prev.0, prev.1);
419        let it = self.text.style(font, FS_SYN_ITALIC);
420        Metrics { font, italic: if it >= 0 { it as u32 } else { font }, size, cw, lh, base, per_cell }
421    }
422
423    // follow the document's changes in the highlighting states
424    fn ce_hl_sync(hl: &mut Hl, doc: &CodeDoc, h: &Highlighter) {
425        let g = h.grammar() as *const Grammar as usize;
426        let n = doc.line_count();
427        let changes = if hl.grammar == g && hl.uid == doc.uid() && !hl.states.is_empty() { doc.changes_since(hl.rev).map(|c| c.to_vec()) } else { None };
428        match changes {
429            Some(cs) => {
430                for c in cs {
431                    if hl.dirty <= hl.valid {
432                        hl.dirty = 0; // the earlier changes are highlighted again
433                    }
434                    // the start states of the lines after `first` that were replaced
435                    let lo = (c.first + 1).min(hl.states.len());
436                    let hi = (c.first + c.old).min(hl.states.len()).max(lo);
437                    hl.states.splice(lo..hi, core::iter::repeat_n(UNKNOWN, c.new.saturating_sub(1)));
438                    let d = c.new as isize - c.old as isize;
439                    let shift = |x: usize| -> usize {
440                        if x >= c.first + c.old {
441                            (x as isize + d) as usize
442                        } else {
443                            x.min(c.first)
444                        }
445                    };
446                    hl.known = shift(hl.known);
447                    hl.dirty = if hl.dirty > c.first { shift(hl.dirty).max(c.first + c.new) } else { c.first + c.new };
448                    hl.valid = hl.valid.min(c.first);
449                }
450            }
451            None => {
452                hl.states.clear();
453                hl.states.push(0);
454                hl.valid = 0;
455                hl.known = 0;
456                hl.dirty = 0;
457                hl.grammar = g;
458                hl.uid = doc.uid();
459            }
460        }
461        hl.states.resize(n + 1, UNKNOWN);
462        if hl.states[0] == UNKNOWN {
463            hl.states[0] = 0;
464        }
465        hl.known = hl.known.min(n);
466        hl.valid = hl.valid.min(n);
467        hl.rev = doc.revision();
468    }
469
470    // the start states right up to line `upto` (long lines from `keep` on keep their tokens)
471    fn ce_hl_upto(hl: &mut Hl, doc: &CodeDoc, h: &Highlighter, upto: usize, toks: &mut Vec<Token>, long: &Longs, keep: usize) {
472        let upto = upto.min(doc.line_count());
473        let tw = doc.tab_width.clamp(1, 16) as usize;
474        while hl.valid < upto {
475            let k = hl.valid;
476            let t = doc.line(k);
477            let st = if t.len() >= COL_LONG && k >= keep { long.tokens(doc, k, tw, h, hl.states[k]).0 } else { h.line(t, hl.states[k], toks) };
478            hl.count += 1;
479            let next = k + 1;
480            if next >= hl.dirty && next <= hl.known && hl.states[next] == st {
481                // the old states agree again: the rest up to `known` holds
482                hl.valid = hl.known;
483                hl.dirty = 0;
484                continue;
485            }
486            hl.states[next] = st;
487            hl.valid = next;
488            if hl.valid > hl.known {
489                hl.known = hl.valid;
490            }
491        }
492    }
493
494    // the bracket at the caret (before or after it) and its partner, outside strings and
495    // comments
496    fn ce_bracket(v: &mut View, doc: &CodeDoc, h: &Highlighter) -> Option<(TextPos, TextPos)> {
497        let caret = doc.selection().caret;
498        if let Some((uid, rev, at, r)) = v.bracket {
499            if uid == doc.uid() && rev == doc.revision() && at == caret {
500                return r;
501            }
502        }
503        let pairs: Vec<(u8, u8)> = h.grammar().pairs.as_bytes().chunks(2).filter(|c| c.len() == 2).map(|c| (c[0], c[1])).collect();
504        let tw = doc.tab_width.clamp(1, 16) as usize;
505        let long = &v.long;
506        let mut toks = Vec::new();
507        // the tokens of the line the scan is on, the one looked at last (the scan goes one way)
508        let mut cur: Option<(usize, Rc<Vec<Token>>, usize)> = None;
509        let mut code_at = |hl: &mut Hl, l: usize, b: usize| -> bool {
510            if cur.as_ref().is_none_or(|c| c.0 != l) {
511                Ui::ce_hl_upto(hl, doc, h, l, &mut toks, long, usize::MAX);
512                let t = doc.line(l);
513                let tk = if t.len() >= COL_LONG {
514                    long.tokens(doc, l, tw, h, hl.states[l]).1
515                } else {
516                    h.line(t, hl.states[l], &mut toks);
517                    Rc::new(toks.clone())
518                };
519                cur = Some((l, tk, 0));
520            }
521            let Some((_, tk, last)) = &mut cur else { return true };
522            // the first token that ends after b
523            let mut k = (*last).min(tk.len());
524            while k < tk.len() && tk[k].end <= b {
525                k += 1;
526            }
527            while k > 0 && tk[k - 1].end > b {
528                k -= 1;
529            }
530            *last = k;
531            !tk.get(k).is_some_and(|t| t.start <= b && matches!(t.kind, SY_STRING | SY_COMMENT | SY_CODE | SY_ESCAPE))
532        };
533        let line = doc.line(caret.line).as_bytes();
534        let mut found = None;
535        for at in [caret.byte, caret.byte.wrapping_sub(1)] {
536            let Some(&c) = line.get(at) else { continue };
537            let Some(&(o, cl)) = pairs.iter().find(|p| p.0 == c || p.1 == c) else { continue };
538            if !code_at(&mut v.hl, caret.line, at) {
539                continue;
540            }
541            let fwd = c == o;
542            let (want, other) = if fwd { (cl, o) } else { (o, cl) };
543            let mut depth = 0;
544            let mut l = caret.line;
545            let mut b = at as isize;
546            let mut steps = 0;
547            'scan: loop {
548                let t = doc.line(l).as_bytes();
549                loop {
550                    // the next of the two brackets
551                    let is = |x: &u8| *x == want || *x == other;
552                    let next = if fwd {
553                        t.get((b + 1) as usize..).and_then(|r| r.iter().position(is)).map(|k| b + 1 + k as isize)
554                    } else {
555                        t[..b.max(0) as usize].iter().rposition(is).map(|k| k as isize)
556                    };
557                    let Some(nb) = next else { break };
558                    b = nb;
559                    let x = t[b as usize];
560                    if !code_at(&mut v.hl, l, b as usize) {
561                        continue;
562                    }
563                    if x == other {
564                        depth += 1;
565                    } else if depth == 0 {
566                        found = Some((TextPos::new(caret.line, at), TextPos::new(l, b as usize)));
567                        break 'scan;
568                    } else {
569                        depth -= 1;
570                    }
571                }
572                steps += 1;
573                if steps > BRACKET_LINES {
574                    break;
575                }
576                if fwd {
577                    l += 1;
578                    if l >= doc.line_count() {
579                        break;
580                    }
581                    b = -1;
582                } else {
583                    if l == 0 {
584                        break;
585                    }
586                    l -= 1;
587                    b = doc.line(l).len() as isize;
588                }
589            }
590            if found.is_some() {
591                break;
592            }
593        }
594        v.bracket = Some((doc.uid(), doc.revision(), caret, found));
595        found
596    }
597
598    // the find matches for the current query (kept up with the change journal)
599    fn ce_find_update(f: &mut Find, doc: &CodeDoc) {
600        let key = (f.query.clone(), f.opts(), doc.revision(), doc.uid());
601        if f.key == key {
602            return;
603        }
604        let same_query = f.key.0 == key.0 && f.key.1 == key.1 && f.key.3 == key.3;
605        let changes = if same_query { doc.changes_since(f.key.2).map(|c| c.to_vec()) } else { None };
606        match changes {
607            Some(cs) => {
608                for c in cs {
609                    let lo = f.matches.partition_point(|m| m.0.line < c.first);
610                    let hi = f.matches.partition_point(|m| m.0.line < c.first + c.old);
611                    let d = c.new as isize - c.old as isize;
612                    for m in f.matches[hi..].iter_mut() {
613                        m.0.line = (m.0.line as isize + d) as usize;
614                        m.1.line = (m.1.line as isize + d) as usize;
615                    }
616                    let fresh = doc_find_range(doc, &f.query, f.opts(), c.first, c.new);
617                    f.matches.splice(lo..hi, fresh);
618                }
619                f.matches.truncate(FIND_MAX);
620            }
621            None => f.matches = doc.find_all(&f.query, f.opts()),
622        }
623        f.key = key;
624    }
625
626    /// ui_code_editor(id, doc, hl, CE_*) -> EV_* (see the module header)
627    #[cfg_attr(jfgui_dev, track_caller)]
628    pub fn code_editor(&mut self, id: u32, doc: &mut CodeDoc, h: &Highlighter, flags: u32) -> u32 {
629        let _src = self.src_scope();
630        self.code_editor_with(id, doc, h, None, flags)
631    }
632
633    /// code_editor with a language's services (completion, hover, signature help; see the
634    /// module header and code_assist.rs)
635    #[cfg_attr(jfgui_dev, track_caller)]
636    pub fn code_editor_with(&mut self, id: u32, doc: &mut CodeDoc, h: &Highlighter, mut assist: Option<&mut dyn CodeAssist>, flags: u32) -> u32 {
637        let _src = self.src_scope();
638        let ro = flags & CE_READONLY != 0;
639        let stflags = self.take_flags();
640        let r = self.place(self.dp(NAT_W), self.dp(NAT_H));
641        let hair = self.hair();
642        let st = self.style;
643        let frame = flags & CE_NO_FRAME == 0;
644        let m = self.ce_metrics();
645        let theme = self.m.code.theme.clone().unwrap_or_else(|| self.code_theme());
646        let mut v = self.m.code.views.remove(&id).unwrap_or_default();
647        v.seen = self.frame;
648        v.hl.count = 0;
649        v.events.clear();
650        let view0 = (v.sx, v.sy);
651        let rev0 = doc.revision();
652        let sel0 = doc.selection();
653        let tw = doc.tab_width.clamp(1, 16) as usize;
654        // a find request
655        if let Some((rid, q, rep)) = self.m.code.find_req.take() {
656            if rid == id && flags & CE_NO_FIND == 0 {
657                v.find.open = true;
658                v.find.replace = rep;
659                v.find.query = q;
660                v.find.from = doc.selection().start();
661                v.find.key.2 = u64::MAX;
662                self.focus(id.wrapping_add(3));
663            } else {
664                self.m.code.find_req = Some((rid, q, rep));
665            }
666        }
667        // a line to show (code_reveal): out of its folds before the rows are counted
668        let reveal = self.m.code.reveal_req.remove(&id).map(|l| l.min(doc.line_count().saturating_sub(1)));
669        if let Some(l) = reveal {
670            doc.reveal(l);
671        }
672        // ---- geometry
673        let sbw = st.scrollbar_thickness(self);
674        let n = doc.line_count();
675        let digits = format!("{}", n).len().max(2) as i32;
676        // the gutter: breakpoints, the markers' column (while there are diagnostics), the line
677        // numbers, folding
678        let bpw = if flags & CE_BREAKPOINTS != 0 || !doc.breakpoints().is_empty() || doc.exec_line().is_some() { m.lh } else { 0 };
679        let mk = if flags & CE_MARKS != 0 || doc.diagnostic_count() > 0 { m.lh } else { 0 };
680        let nums = flags & CE_NO_GUTTER == 0;
681        let numw = if nums { digits * m.cw + self.dp(18) } else { 0 };
682        let fw = if flags & CE_FOLDS != 0 || doc.fold_count() > 0 { (m.lh * 3 / 4).max(self.dp(12)) } else { 0 };
683        let gw = bpw + mk + numw + fw;
684        let bar_h = if v.find.open { self.dp(if v.find.replace { 76 } else { 40 }) } else { 0 };
685        let inner = [r[0] + hair, r[1] + hair, (r[2] - hair * 2).max(1), (r[3] - hair * 2).max(1)];
686        let ty = inner[1] + bar_h;
687        let tx = inner[0] + gw + self.dp(6);
688        if v.max_cols.0 != doc.uid() || v.max_cols.1 != doc.revision() || v.max_cols.2 == 0 {
689            v.max_cols = line_sizes(doc);
690        }
691        let content_w = (v.max_cols.2 as i32 + 2).saturating_mul(m.cw);
692        let tw_px0 = (inner[0] + inner[2] - sbw - tx).max(m.cw);
693        let hbar = content_w > tw_px0;
694        let th = (inner[1] + inner[3] - ty - if hbar { sbw } else { 0 }).max(m.lh);
695        let tw_px = tw_px0;
696        let total_h = doc.visible_lines() as i32 * m.lh;
697        let page = th;
698        // ---- behave
699        let mut hb = HB_FOCUS | HB_TEXT | CUR_IBEAM << HB_CURSOR_SHIFT;
700        if stflags & ST_DISABLED != 0 {
701            hb |= HB_DISABLED;
702        }
703        if doc.diagnostic_count() > 0 || assist.is_some() {
704            hb |= HB_MOVES; // (the tooltip follows the pointer from one diagnostic or word to the next)
705        }
706        self.wp.ctx.last_rect = [inner[0], ty, inner[2], inner[1] + inner[3] - ty];
707        let hit0 = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
708        let ev = self.behave(id, hb);
709        let disabled = self.wp.ctx.last_state == SS_DISABLED as u32;
710        let focused = !disabled && self.ed_focused(id);
711        let mut out = ev & (EV_HOVER | EV_HELD | EV_DOUBLE | EV_RCLICK | EV_DRAG);
712        if focused {
713            if let (Some(sel), Some(held)) = (self.textbox_selection(id), v.held) {
714                if sel != held {
715                    // (a screen reader's selection, in the record's text)
716                    let base = doc.flat(TextPos::new(v.label.2, 0));
717                    let (a, c) = (doc.unflat(sel.0 + base), doc.unflat(sel.1 + base));
718                    doc.set_selection(TextSel::new(a, c));
719                }
720            }
721            if let Some(t) = self.a11y_text(id) {
722                if !ro {
723                    let e = doc.end();
724                    doc.replace(TextPos::default(), e, &t);
725                }
726            }
727        } else if v.held.is_some() {
728            v.held = None;
729            v.drag = 0;
730            self.ed_drop(id);
731            out |= EV_COMMIT;
732        }
733        if !focused {
734            v.pop.close();
735            v.sig.open = false;
736        }
737        // ---- pointer
738        let (mx, my) = (self.rtl_map(self.wp.ctx.mx, inner[0], inner[2]), self.wp.ctx.my);
739        let (cx, cy) = (mx - tx + v.sx, my - ty + v.sy);
740        let in_gutter = mx < tx && gw > 0;
741        let c = &self.wp.ctx;
742        let press = c.pressed & (1 << BTN_LEFT) != 0 && ev & EV_HOVER != 0 && c.active == id;
743        if press && !disabled {
744            let view = CView { doc: &*doc, cw: m.cw, lh: m.lh, tw, long: &v.long };
745            let p = clamp_pos(&view, TextPos::new(view.row_at(cy).0, view.hit_x(view.row_at(cy).0, 0, cx)));
746            let row_line = view.row_at(cy).0;
747            v.prefx = -1;
748            v.t0 = self.now_ms;
749            v.pop.close();
750            v.sig.open = false;
751            // the folded marker after a line's end: unfold
752            let fold_box = doc.fold_at(row_line).is_some_and(|f| f.1) && {
753                let end = v.long.col(doc, row_line, doc.line(row_line).len(), tw) as i32 * m.cw;
754                cx >= end + m.cw / 2 && cx < end + m.cw * 4
755            };
756            if in_gutter && bpw > 0 && mx < inner[0] + bpw {
757                // the breakpoints' column: the app toggles
758                v.events.push(CodeEv::Breakpoint(row_line));
759                v.drag = 0;
760            } else if in_gutter && fw > 0 && mx >= inner[0] + gw - fw && doc.fold_at(row_line).is_some() {
761                doc.toggle_fold(row_line);
762                v.drag = 0;
763                self.repass();
764            } else if fold_box && !in_gutter {
765                doc.set_folded(row_line, false);
766                v.drag = 0;
767                self.repass();
768            } else if in_gutter {
769                let l = view.row_at(cy).0;
770                let e = if l + 1 < n { TextPos::new(l + 1, 0) } else { TextPos::new(l, doc.line(l).len()) };
771                v.word = (TextPos::new(l, 0), e);
772                if self.mods & MOD_SHIFT != 0 {
773                    let a = doc.selection().anchor;
774                    doc.set_selection(TextSel::new(a, e));
775                } else {
776                    doc.set_selection(TextSel::new(v.word.0, e));
777                }
778                v.drag = 3;
779            } else if ev & EV_DOUBLE != 0 {
780                let (a, b) = code_word(doc.line(p.line), p.byte);
781                v.word = (TextPos::new(p.line, a), TextPos::new(p.line, b));
782                doc.set_selection(TextSel::new(v.word.0, v.word.1));
783                v.drag = 2;
784            } else if self.mods & MOD_SHIFT != 0 {
785                let a = doc.selection().anchor;
786                doc.set_selection(TextSel::new(a, p));
787                v.drag = 1;
788            } else {
789                doc.set_selection(TextSel::at(p));
790                v.drag = 1;
791                if self.mods & MOD_CTRL != 0 {
792                    // Ctrl+click: the definition of the word there
793                    v.events.push(CodeEv::Definition(p));
794                    v.drag = 0;
795                }
796            }
797        } else if ev & EV_HELD != 0 && v.drag != 0 {
798            if my < ty {
799                v.sy -= m.lh;
800            } else if my >= ty + th {
801                v.sy += m.lh;
802            }
803            if mx >= tx + tw_px {
804                v.sx += m.cw * 2;
805            } else if mx < tx && v.drag != 3 {
806                v.sx -= m.cw * 2;
807            }
808            v.sy = v.sy.clamp(0, (total_h - th).max(0));
809            v.sx = v.sx.clamp(0, (content_w - tw_px).max(0));
810            let (cx, cy) = (mx - tx + v.sx, my - ty + v.sy);
811            let view = CView { doc: &*doc, cw: m.cw, lh: m.lh, tw, long: &v.long };
812            let l = view.row_at(cy).0;
813            let p = TextPos::new(l, view.hit_x(l, 0, cx));
814            match v.drag {
815                3 => {
816                    let e = if l + 1 < n { TextPos::new(l + 1, 0) } else { TextPos::new(l, doc.line(l).len()) };
817                    if TextPos::new(l, 0) < v.word.0 {
818                        doc.set_selection(TextSel::new(v.word.1, TextPos::new(l, 0)));
819                    } else {
820                        doc.set_selection(TextSel::new(v.word.0, e));
821                    }
822                }
823                2 => {
824                    let (a, b) = code_word(doc.line(l), p.byte);
825                    let (wa, wb) = (TextPos::new(l, a), TextPos::new(l, b));
826                    if wa < v.word.0 {
827                        doc.set_selection(TextSel::new(v.word.1, wa));
828                    } else {
829                        doc.set_selection(TextSel::new(v.word.0, wb.max(v.word.1)));
830                    }
831                }
832                _ => {
833                    let a = doc.selection().anchor;
834                    doc.set_selection(TextSel::new(a, p));
835                }
836            }
837        } else if ev & EV_HELD == 0 {
838            v.drag = 0;
839        }
840        // ---- keys and typed text
841        if focused {
842            loop {
843                let k0 = self.ed_key_first();
844                if k0 == 0 {
845                    break;
846                }
847                let k = self.rtl_text_key(k0);
848                if Ui::ce_assist_key(doc, &mut v, assist.as_deref_mut(), k, ro, page / m.lh.max(1)) || self.ce_key(id, doc, &mut v, h, k, flags, &m) {
849                    self.ed_key_take();
850                    v.t0 = self.now_ms;
851                    self.repass();
852                    continue;
853                }
854                let act = {
855                    let view = CView { doc: &*doc, cw: m.cw, lh: m.lh, tw, long: &v.long };
856                    ed_key(&view, doc.selection(), &mut v.prefx, k, page, false)
857                };
858                let Some(act) = act else { break };
859                self.ed_key_take();
860                v.t0 = self.now_ms;
861                match act {
862                    EdAct::Sel(s) => {
863                        // (the caret steps over folded lines)
864                        let c0 = doc.selection().caret;
865                        let s = if doc.hidden().is_empty() || !matches!(k & 0xFFFF, VK_UP | VK_DOWN | VK_LEFT | VK_RIGHT | VK_PRIOR | VK_NEXT) {
866                            s
867                        } else {
868                            let view = CView { doc: &*doc, cw: m.cw, lh: m.lh, tw, long: &v.long };
869                            let c = skip_hidden(&view, c0, s.caret, k & 0xFFFF, v.prefx);
870                            if s.anchor == s.caret { TextSel::at(c) } else { TextSel::new(s.anchor, c) }
871                        };
872                        doc.set_selection(s);
873                    }
874                    EdAct::Del(a, b, _) if !ro => {
875                        let back = k & 0xFFFF == VK_BACK;
876                        let empty = doc.selection().is_empty();
877                        let (a, b) = if back && empty && !doc.use_tabs { soft_back(doc, a, b) } else { (a, b) };
878                        doc.delete_kind(a, b, if !empty { CK_NONE } else if back { CK_BACK } else { CK_DEL });
879                    }
880                    EdAct::Enter if !ro => {
881                        doc.newline(h.grammar().openers);
882                    }
883                    EdAct::Copy => self.ce_copy(doc, false, ro),
884                    EdAct::Cut => self.ce_copy(doc, !ro, ro),
885                    EdAct::Paste if !ro => {
886                        if let Some(t) = self.clip_get(CLIP_TEXT) {
887                            let s = String::from_utf8_lossy(&t).replace('\0', "");
888                            if !s.is_empty() {
889                                doc.insert(&s);
890                            }
891                        }
892                    }
893                    EdAct::Undo if !ro => {
894                        doc.undo();
895                    }
896                    EdAct::Redo if !ro => {
897                        doc.redo();
898                    }
899                    _ => {}
900                }
901            }
902            let cps = self.text_take(super::ctx::TEXTQ_MAX);
903            if !ro {
904                let s = typed_text(&cps, false);
905                if !s.is_empty() {
906                    doc.type_text(&s, h.grammar().closers);
907                    v.t0 = self.now_ms;
908                    v.prefx = -1;
909                    if let Some(a) = assist.as_deref_mut() {
910                        Ui::ce_assist_typed(doc, &mut v, a, &s);
911                    }
912                }
913            }
914        }
915        // the popups follow the caret and the text; a quiet word stays quiet while the caret
916        // is in it
917        if v.pop.open {
918            v.pop.filter(doc);
919        }
920        if let Some((l, a)) = v.pop.quiet {
921            let c = doc.clamp(doc.selection().caret);
922            if c.line != l || c.byte <= a || word_around(doc.line(l), c.byte).0 != a {
923                v.pop.quiet = None;
924            }
925        }
926        if let Some(a) = assist.as_deref_mut().filter(|_| v.sig.open) {
927            let key = (doc.revision(), doc.selection().caret);
928            if v.sig.key != key {
929                v.sig.key = key;
930                v.sig.help = a.signature(doc, key.1);
931                v.sig.open = v.sig.help.is_some();
932            }
933        }
934        // the caret never stays in folded lines: put there (an app's set_selection, undo, a
935        // jump), they unfold; folded around it (a click, the app), it goes to the fold's first
936        // line
937        let c = doc.clamp(doc.selection().caret);
938        if doc.is_hidden(c.line) {
939            if doc.selection() != v.last_sel || doc.revision() != v.last_rev {
940                doc.reveal(c.line);
941            } else {
942                let l = doc.visible_line(c.line, false);
943                doc.set_selection(TextSel::at(TextPos::new(l, doc.line(l).len())));
944            }
945            self.repass();
946        }
947        let changed = doc.revision() != rev0;
948        if changed {
949            out |= EV_ACT;
950            v.max_cols = line_sizes(doc);
951        }
952        if changed || doc.selection() != sel0 {
953            self.repass();
954        }
955        let n = doc.line_count();
956        let total_h = doc.visible_lines() as i32 * m.lh;
957        // ---- the find bar's matches
958        if v.find.open {
959            Ui::ce_find_update(&mut v.find, doc);
960        }
961        // ---- wheel, the caret into view
962        let wy = self.wp.ctx.wheel_y;
963        let wx = self.wp.ctx.wheel_x;
964        let over = ev & EV_HOVER != 0 || self.wp.ctx.hot == id.wrapping_add(1) || self.wp.ctx.hot == id.wrapping_add(2);
965        if over && (wy != 0 || wx != 0) {
966            let (dx, dy) = if self.mods & MOD_SHIFT != 0 { (wy, 0) } else { (wx, wy) };
967            let sy = (v.sy - dy * m.lh * 3 / 120).clamp(0, (total_h - th).max(0));
968            let sx = (v.sx - dx * m.cw * 6 / 120).clamp(0, (content_w - tw_px).max(0));
969            if sy != v.sy || sx != v.sx {
970                v.sy = sy;
971                v.sx = sx;
972                self.wp.ctx.wheel_y = 0;
973                self.wp.ctx.wheel_x = 0;
974            }
975        }
976        // the finger's way (touch.rs)
977        let (px, py) = self.pan_take(id);
978        v.sy = (v.sy + py).clamp(0, (total_h - th).max(0));
979        v.sx = (v.sx + px).clamp(0, (content_w - tw_px).max(0));
980        let caret = doc.clamp(doc.selection().caret);
981        let ccol = v.long.col(doc, caret.line, caret.byte, tw) as i32;
982        if (focused || v.find.open) && (doc.selection() != v.last_sel || doc.revision() != v.last_rev || core::mem::take(&mut v.show_caret)) {
983            let cy = doc.row_of_line(caret.line) as i32 * m.lh;
984            if cy < v.sy {
985                v.sy = cy;
986            }
987            if cy + m.lh > v.sy + th {
988                v.sy = cy + m.lh - th;
989            }
990            let cxp = ccol * m.cw;
991            if cxp < v.sx + m.cw * 2 {
992                v.sx = (cxp - m.cw * 4).max(0);
993            }
994            if cxp > v.sx + tw_px - m.cw * 2 {
995                v.sx = cxp - tw_px + m.cw * 4;
996            }
997        }
998        // code_reveal: the line in the middle unless it is in view (after the caret: asked for)
999        if let Some(l) = reveal {
1000            let ly = doc.row_of_line(l) as i32 * m.lh;
1001            if ly < v.sy || ly + m.lh > v.sy + th {
1002                v.sy = ly + m.lh / 2 - th / 2;
1003            }
1004        }
1005        v.last_sel = doc.selection();
1006        v.last_rev = doc.revision();
1007        v.sy = v.sy.clamp(0, (total_h - th).max(0));
1008        v.sx = v.sx.clamp(0, (content_w - tw_px).max(0));
1009        // ---- the rows in view (lines that are not folded away) and the lines they span;
1010        // highlighting states up to the last of them
1011        let first_row = (v.sy / m.lh) as usize;
1012        let rows_n = doc.visible_lines();
1013        let last_row = (((v.sy + th) / m.lh) as usize + 1).min(rows_n).max(first_row.min(rows_n));
1014        let mut rows: Vec<usize> = Vec::with_capacity(last_row.saturating_sub(first_row));
1015        if doc.hidden().is_empty() {
1016            rows.extend(first_row..last_row);
1017        } else if last_row > first_row {
1018            let mut l = doc.line_of_row(first_row);
1019            for _ in first_row..last_row {
1020                rows.push(l);
1021                l = doc.visible_line(l + 1, true);
1022                if l <= *rows.last().unwrap_or(&0) {
1023                    break;
1024                }
1025            }
1026        }
1027        let first = rows.first().copied().unwrap_or(0).min(n);
1028        let last = rows.last().map_or(first, |&l| l + 1).min(n);
1029        // the top of a row's line (a row of `rows`: k)
1030        let sy0 = v.sy;
1031        let lh0 = m.lh;
1032        let row_y = move |k: usize| ty + (first_row + k) as i32 * lh0 - sy0;
1033        let mut toks = Vec::new();
1034        Ui::ce_hl_sync(&mut v.hl, doc, h);
1035        Ui::ce_hl_upto(&mut v.hl, doc, h, last, &mut toks, &v.long, first);
1036        let bracket = if focused { Ui::ce_bracket(&mut v, doc, h) } else { None };
1037
1038        // ---- frame and gutter
1039        if frame {
1040            let mut sp = Sp { value: NAT_W, value2: m.lh, flags: 1, ..Sp::default() };
1041            let _ = st.textbox_metrics(self, &mut sp);
1042            sp.place(r);
1043            sp.ev = ev;
1044            sp.value = focused as i32;
1045            sp.value2 = disabled as i32;
1046            st.textbox_frame(self, &sp);
1047        }
1048        self.clip_push(inner[0], inner[1], inner[2], inner[3]);
1049        self.draw_color(theme.bg);
1050        self.draw_rect(inner[0], ty, inner[2], inner[1] + inner[3] - ty);
1051        if gw > 0 {
1052            self.draw_color(theme.gutter_bg);
1053            self.draw_rect(inner[0], ty, gw, inner[1] + inner[3] - ty);
1054        }
1055        self.ltr_begin(inner[0], inner[2]);
1056        // the lines move with the position (scrollcopy.rs)
1057        let copy = total_h > th || hbar;
1058        if copy {
1059            self.scroll_copy_begin(id, (tx - v.sx) as i64, (ty - v.sy) as i64);
1060        }
1061        let (sa, sb) = doc.selection().range();
1062        let sel_col = if focused { theme.sel } else { theme.sel_inactive };
1063        let ital = |k: u8| theme.italic >> k & 1 != 0;
1064        let bold = |k: u8| theme.bold >> k & 1 != 0;
1065        let col0 = (v.sx / m.cw.max(1)) as usize;
1066        let cols = (tw_px / m.cw.max(1)) as usize + 2;
1067        let tx0 = tx - v.sx;
1068        // the bytes of the visible lines in view (long lines: a few columns more on both sides;
1069        // the rest of a line is not looked at), by line from `first` (hidden ones: none)
1070        let vis: Vec<(usize, usize)> = (first..last)
1071            .map(|l| {
1072                let len = doc.line(l).len();
1073                if len < COL_LONG {
1074                    (0, len)
1075                } else {
1076                    (v.long.floor(doc, l, col0.saturating_sub(4), tw).0, v.long.byte(doc, l, col0 + cols + 4, tw))
1077                }
1078            })
1079            .collect();
1080        // the line of the execution: its background across the text
1081        let exec = doc.exec_line();
1082        // the line numbers, breakpoints, folding chevrons
1083        let numx = inner[0] + bpw + mk + numw - self.dp(10);
1084        for (k, &l) in rows.iter().enumerate() {
1085            let y = row_y(k);
1086            if nums {
1087                let num = format!("{}", l + 1);
1088                let nw = num.len() as i32 * m.cw;
1089                let c = if l == caret.line { theme.gutter_cur } else { theme.gutter_fg };
1090                self.ce_cells(&m, numx - nw, y + m.base, num.as_bytes(), c, false, false);
1091            }
1092            if bpw > 0 {
1093                if let Some(b) = doc.breakpoint(l) {
1094                    self.ce_breakpoint(b, inner[0], y, bpw, m.lh, &theme);
1095                }
1096                if exec == Some(l) {
1097                    self.ce_exec_mark(inner[0], y, bpw, m.lh, &theme);
1098                }
1099            }
1100            if fw > 0 {
1101                if let Some((_, folded)) = doc.fold_at(l) {
1102                    let px = (fw * 3 / 4).max(8);
1103                    self.draw_color(if l == caret.line { theme.gutter_cur } else { theme.gutter_fg });
1104                    self.draw_icon(if folded { IC_CHEV_RIGHT_SMALL } else { IC_CHEV_DOWN_SMALL }, inner[0] + gw - fw + (fw - px) / 2, y + (m.lh - px) / 2, px << 8);
1105                }
1106            }
1107        }
1108        // the diagnostics of the visible lines: per line the most severe one starting there
1109        // marks the gutter; underlines (severity, x0, x1, line top) are drawn after the text
1110        let dvis: Vec<usize> = doc.mark_range(first, last).filter(|&i| last_line(&doc.marks()[i]) >= first).collect();
1111        if mk > 0 {
1112            let mut best: Vec<(usize, u8)> = Vec::new();
1113            for &i in &dvis {
1114                let d = &doc.marks()[i];
1115                match best.last_mut() {
1116                    Some(b) if b.0 == d.a.line => b.1 = b.1.min(d.sev),
1117                    _ if d.a.line >= first => best.push((d.a.line, d.sev)),
1118                    _ => {}
1119                }
1120            }
1121            for (l, sev) in best {
1122                if doc.is_hidden(l) {
1123                    continue;
1124                }
1125                let y = ty + doc.row_of_line(l) as i32 * m.lh - v.sy;
1126                self.ce_marker(sev, inner[0] + bpw, y, mk, m.lh, theme.diag[sev as usize]);
1127            }
1128        }
1129        let mut waves: Vec<(u8, i32, i32, i32)> = Vec::new();
1130        for &i in &dvis {
1131            let d = &doc.marks()[i];
1132            for l in d.a.line.max(first)..=d.b.line.min(last.max(1) - 1) {
1133                let (b0, b1) = vis[l - first];
1134                let (a, b) = (if d.a.line == l { d.a.byte } else { 0 }, if d.b.line == l { d.b.byte } else { usize::MAX });
1135                if !touches(d, l) || b < b0 || a > b1 || doc.is_hidden(l) {
1136                    continue;
1137                }
1138                let (c0, c1) = mark_cols(d, l, doc.line(l).len(), |b| v.long.col(doc, l, b, tw));
1139                waves.push((d.sev, tx0 + c0 as i32 * m.cw, tx0 + c1 as i32 * m.cw, ty + doc.row_of_line(l) as i32 * m.lh - v.sy));
1140            }
1141        }
1142        // the diagnostics under the pointer (an underline, a marker)
1143        let mut hov: Vec<usize> = Vec::new();
1144        let pointer_line = doc.line_of_row(((my - ty + v.sy).max(0) / m.lh.max(1)) as usize);
1145        if ev & EV_HOVER != 0 && v.drag == 0 && !dvis.is_empty() && my >= ty && my < ty + th {
1146            let l = pointer_line;
1147            let col = ((mx - tx0).max(0) / m.cw.max(1)) as usize;
1148            let on_mark = mk > 0 && mx >= inner[0] + bpw && mx < inner[0] + bpw + mk;
1149            let on_text = mx >= tx && mx < tx + tw_px;
1150            hov = dvis
1151                .iter()
1152                .copied()
1153                .filter(|&i| {
1154                    let d = &doc.marks()[i];
1155                    if on_mark {
1156                        d.a.line == l
1157                    } else {
1158                        on_text && touches(d, l) && {
1159                            let (c0, c1) = mark_cols(d, l, doc.line(l).len(), |b| v.long.col(doc, l, b, tw));
1160                            col >= c0 && col < c1
1161                        }
1162                    }
1163                })
1164                .collect();
1165            hov.sort_by_key(|&i| doc.marks()[i].sev);
1166        }
1167        let area = [tx - self.dp(6), ty, inner[0] + inner[2] - tx + self.dp(6), th];
1168        self.clip_push(area[0], area[1], area[2], area[3]);
1169        for (ri, &l) in rows.iter().enumerate() {
1170            let y = row_y(ri);
1171            let t = doc.line(l);
1172            // the execution line, the current line, the selection, the find matches, the
1173            // bracket pair
1174            if exec == Some(l) {
1175                self.draw_color(theme.exec_line);
1176                self.draw_rect(area[0], y, area[2], m.lh);
1177                if theme.exec_line == theme.bg {
1178                    self.draw_color(theme.exec_mark);
1179                    self.draw_frame(area[0], y, area[2], m.lh, 0, hair);
1180                }
1181            } else if l == caret.line && flags & CE_NO_LINE == 0 && (focused || v.find.open) && sa == sb {
1182                self.draw_color(theme.line_hl);
1183                self.draw_rect(area[0], y, area[2], m.lh);
1184            }
1185            let (ls, le) = (TextPos::new(l, 0), TextPos::new(l, t.len()));
1186            let col_of = |b: usize| v.long.col(doc, l, b, tw) as i32 * m.cw;
1187            if sa < sb && sa <= le && sb >= ls {
1188                let (lo, hi) = (ls.max(sa), le.min(sb));
1189                let x0 = col_of(lo.byte);
1190                let x1 = col_of(hi.byte) + if sb > le { m.cw / 2 } else { 0 };
1191                if x1 > x0 {
1192                    self.draw_color(sel_col);
1193                    self.draw_rect(tx0 + x0, y, x1 - x0, m.lh);
1194                }
1195            }
1196            // (the matches of a line do not overlap: by start and by end in order)
1197            let (b0, b1) = vis[l - first];
1198            let ms = if v.find.open { &v.find.matches[..] } else { &[] };
1199            let k0 = ms.partition_point(|x| x.1 <= TextPos::new(l, b0));
1200            let k1 = ms.partition_point(|x| x.0 < TextPos::new(l, b1)).max(k0);
1201            for (k, &(a, b)) in ms[k0..k1].iter().enumerate() {
1202                let cur = Some(k0 + k) == v.find.current;
1203                let x0 = col_of(a.byte);
1204                let x1 = col_of(b.byte);
1205                self.draw_color(if cur { theme.find_cur } else { theme.find });
1206                if theme.find_frame {
1207                    self.draw_frame(tx0 + x0, y, (x1 - x0).max(m.cw / 2), m.lh, 0, if cur { hair * 2 } else { hair });
1208                } else {
1209                    self.draw_rect(tx0 + x0, y, (x1 - x0).max(m.cw / 2), m.lh);
1210                }
1211            }
1212            if let Some((p, q)) = bracket {
1213                for at in [p, q] {
1214                    if at.line == l {
1215                        let x0 = col_of(at.byte);
1216                        self.draw_color(theme.bracket);
1217                        self.draw_frame(tx0 + x0, y, m.cw, m.lh, 0, hair);
1218                    }
1219                }
1220            }
1221            // the text by tokens (the plain text between them: SY_TEXT), visible cells only; a
1222            // long line from the cluster at its first visible column, with its kept tokens
1223            let long = t.len() >= COL_LONG;
1224            let (mut i, mut col) = if long { v.long.floor(doc, l, col0, tw) } else { (0, 0) };
1225            let kept;
1226            let tk: &[Token] = if long {
1227                kept = v.long.tokens(doc, l, tw, h, v.hl.states[l]).1;
1228                &kept
1229            } else {
1230                h.line(t, v.hl.states[l], &mut toks);
1231                &toks
1232            };
1233            let mut k = if long { tk.partition_point(|x| x.end <= i) } else { 0 };
1234            let mut at = i;
1235            let tb = t.as_bytes();
1236            loop {
1237                // the next run: the plain text before the next token, the token
1238                let (a, b, kind) = match tk.get(k) {
1239                    Some(x) if x.start > at => (at, x.start, SY_TEXT),
1240                    Some(x) => {
1241                        k += 1;
1242                        (x.start, x.end, x.kind)
1243                    }
1244                    None if at < t.len() => (at, t.len(), SY_TEXT),
1245                    None => break,
1246                };
1247                at = b;
1248                if col > col0 + cols {
1249                    break;
1250                }
1251                let inside = sa < sb && TextPos::new(l, a) >= sa && TextPos::new(l, b) <= sb;
1252                let color = if theme.sel_text != 0 && inside { theme.sel_text } else { theme.tokens[kind as usize % SY_COUNT] };
1253                // cells of a .. b: ASCII runs at once, other clusters one by one
1254                i = i.max(a);
1255                while i < b && col <= col0 + cols {
1256                    if tb[i] == b'\t' {
1257                        col += tw - col % tw;
1258                        i += 1;
1259                        continue;
1260                    }
1261                    if tb[i] < 0x80 && !m.per_cell {
1262                        // (up to the end of the view)
1263                        let lim = b.min(i + (col0 + cols + 1 - col));
1264                        let mut j = i;
1265                        while j < lim && tb[j] < 0x80 && tb[j] != b'\t' {
1266                            j += 1;
1267                        }
1268                        // only the visible part
1269                        let s0 = i + col0.saturating_sub(col).min(j - i);
1270                        let c0 = col + (s0 - i);
1271                        let s1 = (s0 + (col0 + cols).saturating_sub(c0)).min(j);
1272                        if s1 > s0 && tb[s0..s1].iter().any(|&c| c != b' ') {
1273                            self.ce_cells(&m, tx0 + c0 as i32 * m.cw, y + m.base, &tb[s0..s1], color, bold(kind), ital(kind));
1274                        }
1275                        col += j - i;
1276                        i = j;
1277                        continue;
1278                    }
1279                    let e = segment::grapheme_next(t, i).min(b.max(i + 1));
1280                    let ch = t[i..].chars().next().unwrap_or(' ');
1281                    let w = char_cols(ch);
1282                    if col + w > col0 && col < col0 + cols && ch != ' ' {
1283                        self.ce_cells(&m, tx0 + col as i32 * m.cw, y + m.base, &tb[i..e], color, bold(kind), ital(kind));
1284                    }
1285                    col += w;
1286                    i = e;
1287                }
1288            }
1289            // a folded line: a box with "..." after its end (a click unfolds)
1290            if doc.fold_at(l).is_some_and(|f| f.1) {
1291                let end = v.long.col(doc, l, t.len(), tw) as i32 * m.cw;
1292                let (bx, bw) = (tx0 + end + m.cw / 2, m.cw * 3);
1293                self.draw_color(theme.line_hl);
1294                self.draw_rrect(bx, y + hair, bw, m.lh - hair * 2, self.dp(3));
1295                self.draw_color(theme.gutter_fg);
1296                self.draw_frame(bx, y + hair, bw, m.lh - hair * 2, self.dp(3), hair);
1297                self.ce_cells(&m, bx, y + m.base, b"...", theme.gutter_fg, false, false);
1298            }
1299        }
1300        // the underlines: infos first, errors on top; only their visible part (whole waves)
1301        waves.sort_by_key(|w| core::cmp::Reverse(w.0));
1302        let (lim0, lim1) = (tx - m.cw * 2, tx + tw_px + m.cw * 2);
1303        let period = self.dp(2).max(2) * 2;
1304        for (sev, x0, x1, y) in waves {
1305            let x0 = if x0 < lim0 { x0 + (lim0 - x0) / period * period } else { x0 };
1306            let x1 = x1.min(lim1);
1307            if x1 > x0 {
1308                self.ce_wave(x0, x1, y, &m, theme.diag[sev as usize]);
1309            }
1310        }
1311        self.clip_pop();
1312        // the caret
1313        let cx_px = tx0 + ccol * m.cw;
1314        let cy_px = ty + doc.row_of_line(caret.line) as i32 * m.lh - v.sy;
1315        if focused {
1316            let pe = self.ed_preedit();
1317            if !pe.text.is_empty() {
1318                self.ed_preedit_draw(&pe, cx_px, cy_px, m.lh, cy_px + m.base, (m.font, m.size), theme.fg, theme.bg);
1319            } else if doc.selection().is_empty() && self.ed_caret_on(v.t0) && cy_px + m.lh > ty && cy_px < ty + th && cx_px >= tx - 1 {
1320                self.draw_color(theme.caret);
1321                self.draw_rect(cx_px, cy_px, self.dp(2).max(1), m.lh);
1322            }
1323            plat::ime_inline(true); // (E4) the editor draws the composition itself
1324            self.ime_pos(cx_px, cy_px, m.lh);
1325        }
1326        if copy {
1327            self.scroll_copy_end();
1328        }
1329        self.ltr_end();
1330        self.clip_pop();
1331        // ---- scroll bars
1332        if total_h > th {
1333            let mut pos = v.sy;
1334            self.next_rect_px(inner[0] + inner[2] - sbw, ty, sbw, th);
1335            if self.scrollbar(id.wrapping_add(1), &mut pos, total_h, th, true) & EV_ACT != 0 {
1336                v.sy = pos.clamp(0, (total_h - th).max(0));
1337            }
1338        }
1339        if hbar {
1340            let mut pos = v.sx;
1341            self.next_rect_px(tx, ty + th, tw_px, sbw);
1342            if self.scrollbar(id.wrapping_add(2), &mut pos, content_w, tw_px, false) & EV_ACT != 0 {
1343                v.sx = pos.clamp(0, (content_w - tw_px).max(0));
1344            }
1345        }
1346        // a finger may roll it (touch.rs)
1347        self.pan_view(id, hit0, super::touch::AX_ALL, [v.sx, v.sy], [(content_w - tw_px).max(0), (total_h - th).max(0)]);
1348        // ---- the find bar
1349        if v.find.open {
1350            self.ce_find_bar(id, doc, &mut v, [inner[0], inner[1], inner[2], bar_h], ro);
1351        }
1352        // ---- the record: the text (or the visible lines), caret and anchor as offsets
1353        let big = v.max_cols.3 > LABEL_MAX;
1354        let (l0, l1) = if big { (first, last) } else { (0, n) };
1355        if v.label.0 != doc.uid() || v.label.1 != doc.revision() || v.label.2 != l0 || v.label.3 != l1 {
1356            let mut s = String::new();
1357            for (k, l) in doc.lines()[l0..l1].iter().enumerate() {
1358                if k > 0 {
1359                    s.push('\n');
1360                }
1361                if s.len() + l.len() > LABEL_MAX {
1362                    break;
1363                }
1364                s.push_str(l);
1365            }
1366            v.label = (doc.uid(), doc.revision(), l0, l1, s);
1367        }
1368        let rel = |p: TextPos| -> usize {
1369            if p.line < l0 || p.line >= l1 {
1370                0
1371            } else {
1372                doc.lines()[l0..p.line].iter().map(|l| l.len() + 1).sum::<usize>() + p.byte
1373            }
1374        };
1375        let (fa, fc) = if big { (rel(doc.selection().anchor), rel(doc.selection().caret)) } else { (doc.flat(doc.selection().anchor), doc.flat(doc.selection().caret)) };
1376        self.wp.ctx.last_rect = r;
1377        self.wp.ctx.last_state = if focused { SS_FOCUSED as u32 } else { 0 } | if disabled { SS_DISABLED as u32 } else { 0 };
1378        let label = core::mem::take(&mut v.label.4);
1379        {
1380            let kind = (TB_MULTI | if ro { TB_READONLY } else { 0 }) >> 24;
1381            let s = self.sem(K_TEXTBOX, id, Some(&label), if focused { fc as i32 } else { 0 });
1382            s.max = if focused { fa as i32 } else { 0 };
1383            s.min = kind as i32;
1384        }
1385        v.label.4 = label;
1386        // the word under the pointer: the assist's hover text (asked once per word and revision)
1387        let mut hover_text: Option<String> = None;
1388        let mut word_key = 0u64;
1389        if let Some(a) = assist.as_deref_mut() {
1390            if ev & EV_HOVER != 0 && v.drag == 0 && mx >= tx && mx < tx + tw_px && my >= ty && my < ty + th {
1391                let l = pointer_line;
1392                let col = ((mx - tx0).max(0) / m.cw.max(1)) as usize;
1393                let b = v.long.floor(doc, l, col, tw).0;
1394                let t = doc.line(l);
1395                if t[b.min(t.len())..].chars().next().is_some_and(is_word) {
1396                    let (wa, _) = word_around(t, b);
1397                    let key = (doc.uid(), doc.revision(), l, wa);
1398                    if v.hov.key != key {
1399                        v.hov.key = key;
1400                        v.hov.text = a.hover(doc, TextPos::new(l, b));
1401                    }
1402                    hover_text = v.hov.text.clone();
1403                    word_key = ((l as u64 + 1) << 24) ^ (wa as u64 + 1);
1404                }
1405            }
1406        }
1407        // the messages under the pointer: the tooltip of the record above (part: the first
1408        // diagnostic, in the gutter or the text, and the word; only while the pointer is here,
1409        // so the part tooltips of other widgets keep theirs); the hover text below them
1410        if ev & EV_HOVER != 0 && (!dvis.is_empty() || assist.is_some()) {
1411            let mut text = if hov.is_empty() { String::new() } else { diag_text(doc, &hov) };
1412            if let Some(s) = hover_text.as_deref().filter(|s| !s.is_empty()) {
1413                if !text.is_empty() {
1414                    text.push_str("\n\n");
1415                }
1416                text.push_str(&wrap(s, 72));
1417            }
1418            if text.is_empty() {
1419                self.tooltip_part("", 0);
1420            } else {
1421                // (another text, another part: a tooltip blocked by a press shows the new one)
1422                let dpart = if hov.is_empty() { 0 } else { 1 + hov[0] as u64 + if mx < tx { 1 << 40 } else { 0 } };
1423                let th = text.bytes().fold(0xCBF2_9CE4_8422_2325u64, |h, b| (h ^ b as u64).wrapping_mul(0x100_0000_01B3));
1424                let part = dpart.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ word_key.wrapping_mul(0xC2B2_AE3D_27D4_EB4F) ^ th;
1425                self.tooltip_part(&text, part.max(1));
1426            }
1427        }
1428        // the diagnostics at the caret: the status, a record at the caret's line (after the
1429        // editor's own, so an app's tooltip or context menu stays the editor's)
1430        let at = doc.marks_at(caret);
1431        v.caret_diag = at.first().map(|&i| doc.diag_of(&doc.marks()[i]));
1432        if !at.is_empty() {
1433            let state = self.wp.ctx.last_state;
1434            let cy = (ty + doc.row_of_line(caret.line) as i32 * m.lh - v.sy).clamp(ty, (ty + th - m.lh).max(ty));
1435            self.wp.ctx.last_rect = [inner[0], cy, inner[2], m.lh];
1436            self.wp.ctx.last_state = 0;
1437            self.sem(K_TEXT, 0, Some(&diag_text(doc, &at)), doc.marks()[at[0]].sev as i32);
1438            self.wp.ctx.last_rect = r;
1439            self.wp.ctx.last_state = state;
1440        }
1441        if focused {
1442            self.ed_hold(id, fa, fc);
1443            v.held = Some((fa, fc));
1444            out |= EV_FOCUS;
1445        }
1446        // (passes that only repeat a frame keep the count of the one that did the work)
1447        if v.hl.count > 0 || doc.revision() != rev0 || doc.selection() != sel0 || (v.sx, v.sy) != view0 {
1448            v.hl_last = v.hl.count;
1449        }
1450        v.status = CodeStatus {
1451            line: caret.line + 1,
1452            col: ccol as usize + 1,
1453            matches: if v.find.open { v.find.matches.len() } else { 0 },
1454            current: if v.find.open { v.find.current.map_or(0, |c| c + 1) } else { 0 },
1455            hl_lines: v.hl_last,
1456            lines: n,
1457            diags: doc.diagnostic_counts(),
1458            diag: at.first().map(|&i| doc.marks()[i].sev),
1459            popup: if v.pop.open { v.pop.shown.len() } else { 0 },
1460            popup_sel: v.pop.sel,
1461            sig: v.sig.open,
1462            hidden: n - doc.visible_lines(),
1463        };
1464        // ---- the popups of the language services at the caret's line
1465        if focused {
1466            let cy = ty + doc.row_of_line(caret.line) as i32 * m.lh - v.sy;
1467            let in_view = cy + m.lh > ty && cy < ty + th;
1468            let pf = PopFont { font: m.font, size: m.size, cw: m.cw, lh: m.lh, base: m.base };
1469            if v.pop.open && in_view {
1470                let scol = v.long.col(doc, v.pop.start.line, v.pop.start.byte, tw) as i32;
1471                if self.ca_complete_popup(id.wrapping_add(14), &mut v.pop, doc, tx0 + scol * m.cw, cy + m.lh, cy, &pf, &theme) {
1472                    out |= EV_ACT;
1473                    v.max_cols = line_sizes(doc);
1474                    self.repass();
1475                }
1476            }
1477            if let Some(sh) = v.sig.help.as_ref().filter(|_| v.sig.open && in_view) {
1478                self.ca_signature_popup(sh, cx_px - m.cw * 2, cy, cy + m.lh, &pf, &theme);
1479            }
1480        }
1481        self.m.code.views.insert(id, v);
1482        let now = self.frame;
1483        if self.m.code.views.len() > 8 {
1484            self.m.code.views.retain(|_, v| now.wrapping_sub(v.seen) < 600);
1485        }
1486        self.wp.ctx.last_rect = r;
1487        out
1488    }
1489
1490    // the cells of s at x (the font's advance is the cell; a proportional fallback cell by cell)
1491    #[allow(clippy::too_many_arguments)]
1492    fn ce_cells(&mut self, m: &Metrics, x: i32, y: i32, s: &[u8], c: u32, bold: bool, italic: bool) {
1493        let font = if italic { m.italic } else { m.font };
1494        let passes = if bold { 2 } else { 1 };
1495        for p in 0..passes {
1496            let dx = p * ((m.size >> 8) / 16 + 1);
1497            if m.per_cell {
1498                let mut i = 0;
1499                let mut k = 0;
1500                while i < s.len() {
1501                    let n = match s[i] {
1502                        0..=0x7F => 1,
1503                        0x80..=0xDF => 2,
1504                        0xE0..=0xEF => 3,
1505                        _ => 4,
1506                    }
1507                    .min(s.len() - i);
1508                    if s[i] != b' ' {
1509                        self.cmd_text(x + k * m.cw + dx, y, &s[i..i + n], font, m.size, c, 0);
1510                    }
1511                    i += n;
1512                    k += 1;
1513                }
1514            } else {
1515                self.cmd_text(x + dx, y, s, font, m.size, c, 0);
1516            }
1517        }
1518    }
1519
1520    // a wavy underline from x0 to x1 at the foot of the line whose top is y (one command: a
1521    // Wave of this colour and size, the same object every frame)
1522    fn ce_wave(&mut self, x0: i32, x1: i32, y: i32, m: &Metrics, color: u32) {
1523        let hp = self.dp(2).max(2);
1524        let amp = self.dp(2).max(2);
1525        let width = self.dp(1).max(1) << 8;
1526        let top = (y + m.base + self.dp(1)).min(y + m.lh - amp - 1);
1527        let key = (hp as u32) << 16 | (amp as u32) << 8 | (width >> 8) as u32;
1528        let w = &mut self.m.code.waves;
1529        let d = match w.iter().find(|e| e.0 == color && e.1 == key) {
1530            Some(e) => e.2.clone(),
1531            None => {
1532                if w.len() >= 24 {
1533                    w.clear();
1534                }
1535                let d: DrawRef = Rc::new(Wave { color, hp, amp, width });
1536                w.push((color, key, d.clone()));
1537                d
1538            }
1539        };
1540        let pad = (width >> 9) + 1;
1541        self.cmd_custom(x0 - pad, top - pad, x1 - x0 + pad * 2, amp + pad * 2, Some(d), key as usize, color);
1542    }
1543
1544    // the gutter's marker of a diagnostic, centred in the cell (x, y, w: cell, h: lh): a filled
1545    // circle with a cross (error), a triangle (warning), an "i" (info)
1546    fn ce_marker(&mut self, sev: u8, x: i32, y: i32, cell: i32, lh: i32, color: u32) {
1547        let cp = match sev {
1548            DIAG_ERROR => IC_ERROR_FILL,
1549            DIAG_WARNING => IC_WARNING,
1550            _ => IC_INFO,
1551        };
1552        let px = (lh * 2 / 3).max(8);
1553        self.draw_color(color);
1554        self.draw_icon(cp, x + (cell - px) / 2, y + (lh - px) / 2, px << 8);
1555    }
1556
1557    // a breakpoint in the gutter cell (x, y, w: cell, h: lh): a filled circle, a ring when
1558    // disabled, a bar across it with a condition (shapes: high contrast tells them apart)
1559    fn ce_breakpoint(&mut self, kind: u8, x: i32, y: i32, cell: i32, lh: i32, t: &CodeTheme) {
1560        let r = (lh * 3 / 10).max(3);
1561        let (cx, cy) = ((x + cell / 2) << 8, (y + lh / 2) << 8);
1562        self.draw_color(t.breakpoint);
1563        match kind {
1564            BP_DISABLED => self.draw_circle(cx, cy, (r << 8) - 128, (self.dp(2).max(1) << 8) - 64),
1565            _ => self.draw_circle(cx, cy, r << 8, 0),
1566        }
1567        if kind == BP_COND {
1568            self.draw_color(t.bg);
1569            let bh = self.dp(2).max(2);
1570            self.draw_rect(x + cell / 2 - r + r / 3, y + lh / 2 - bh / 2, (r - r / 3) * 2, bh);
1571        }
1572    }
1573
1574    // the execution arrow in the gutter cell
1575    fn ce_exec_mark(&mut self, x: i32, y: i32, cell: i32, lh: i32, t: &CodeTheme) {
1576        let px = (lh * 3 / 4).max(8);
1577        self.draw_color(t.exec_mark);
1578        self.draw_icon(IC_CARET_RIGHT, x + (cell - px) / 2, y + (lh - px) / 2, px << 8);
1579    }
1580
1581    // a key of the language services -> consumed: the completion list's keys while it is open,
1582    // Ctrl+Space, Ctrl+Shift+Space, Esc for signature help, F12, F2, Shift+Alt+F, Ctrl+Shift+[
1583    // and ] (fold / unfold at the caret's line); page: rows of a page
1584    fn ce_assist_key(doc: &mut CodeDoc, v: &mut View, assist: Option<&mut (dyn CodeAssist + '_)>, k: u32, ro: bool, page: i32) -> bool {
1585        let vk = k & 0xFFFF;
1586        let mods = k >> 16;
1587        if mods & MOD_ALTGR != 0 || mods & (MOD_CTRL | MOD_ALT) == MOD_CTRL | MOD_ALT {
1588            return false;
1589        }
1590        let ctrl = mods & MOD_CTRL != 0;
1591        let shift = mods & MOD_SHIFT != 0;
1592        let alt = mods & MOD_ALT != 0;
1593        let caret = doc.selection().caret;
1594        if v.pop.open && !ctrl && !alt {
1595            let page = (POP_ROWS as isize - 1).min(page.max(1) as isize);
1596            match vk {
1597                VK_UP if !shift => v.pop.step(-1),
1598                VK_DOWN if !shift => v.pop.step(1),
1599                VK_PRIOR if !shift => v.pop.step(-page),
1600                VK_NEXT if !shift => v.pop.step(page),
1601                // (Enter after a word typed out in full: the line break, not the item)
1602                VK_RETURN if !shift && v.pop.typed_whole() => {
1603                    v.pop.close();
1604                    return false;
1605                }
1606                VK_RETURN | VK_TAB if !shift && !ro => {
1607                    v.pop.accept(doc);
1608                    v.prefx = -1;
1609                }
1610                VK_ESCAPE if !shift => {
1611                    v.pop.close();
1612                    let (a, _) = word_around(doc.line(caret.line), caret.byte);
1613                    v.pop.quiet = Some((caret.line, a));
1614                }
1615                _ => return false,
1616            }
1617            return true;
1618        }
1619        if v.sig.open && vk == VK_ESCAPE && mods == 0 {
1620            v.sig.open = false;
1621            return true;
1622        }
1623        match vk {
1624            VK_SPACE if ctrl && !alt => {
1625                let Some(a) = assist else { return false };
1626                v.show_caret = true;
1627                if shift {
1628                    v.sig.key = (doc.revision(), caret);
1629                    v.sig.help = a.signature(doc, caret);
1630                    v.sig.open = v.sig.help.is_some();
1631                } else if !ro {
1632                    v.pop.request(doc, a, true);
1633                }
1634                true
1635            }
1636            VK_F12 if mods == 0 => {
1637                v.events.push(CodeEv::Definition(caret));
1638                true
1639            }
1640            VK_F2 if mods == 0 && !ro => {
1641                v.events.push(CodeEv::Rename(caret));
1642                true
1643            }
1644            // Shift+Alt+F
1645            0x46 if shift && alt && !ctrl && !ro => {
1646                v.events.push(CodeEv::Format);
1647                true
1648            }
1649            VK_OEM_4 | VK_OEM_6 if ctrl && shift && !alt => {
1650                let l = doc.clamp(caret).line;
1651                if vk == VK_OEM_4 {
1652                    // the innermost open range around the caret
1653                    let r = doc.fold_ranges().into_iter().filter(|r| r.0 <= l && l <= r.1 && !r.2).max_by_key(|r| r.0);
1654                    if let Some(r) = r {
1655                        doc.set_folded(r.0, true);
1656                        doc.set_selection(TextSel::at(TextPos::new(r.0, doc.line(r.0).len())));
1657                    }
1658                } else if !doc.set_folded(l, false) {
1659                    let r = doc.fold_ranges().into_iter().filter(|r| r.0 <= l && l <= r.1 && r.2).max_by_key(|r| r.0);
1660                    if let Some(r) = r {
1661                        doc.set_folded(r.0, false);
1662                    }
1663                }
1664                true
1665            }
1666            _ => false,
1667        }
1668    }
1669
1670    // after typing s: the completion list opens (a word started, a trigger character) or
1671    // closes (another character), signature help opens at ( and ,
1672    fn ce_assist_typed(doc: &mut CodeDoc, v: &mut View, a: &mut (dyn CodeAssist + '_), s: &str) {
1673        let Some(c) = s.chars().next_back() else { return };
1674        let caret = doc.selection().caret;
1675        if v.pop.open {
1676            if !is_word(c) {
1677                v.pop.close();
1678                if a.triggers().contains(c) {
1679                    v.pop.request(doc, a, false);
1680                }
1681            }
1682        } else if a.triggers().contains(c) {
1683            v.pop.request(doc, a, false);
1684        } else if is_word(c) {
1685            let (w0, _) = word_around(doc.line(caret.line), caret.byte);
1686            if v.pop.quiet != Some((caret.line, w0)) {
1687                v.pop.request(doc, a, false);
1688            }
1689        }
1690        if c == '(' || c == ',' || (c == ')' && v.sig.open) {
1691            v.sig.key = (doc.revision(), caret);
1692            v.sig.help = a.signature(doc, caret);
1693            v.sig.open = v.sig.help.is_some();
1694        }
1695    }
1696
1697    // a key of the editor itself -> consumed
1698    #[allow(clippy::too_many_arguments)]
1699    fn ce_key(&mut self, id: u32, doc: &mut CodeDoc, v: &mut View, h: &Highlighter, k: u32, flags: u32, m: &Metrics) -> bool {
1700        let ro = flags & CE_READONLY != 0;
1701        let vk = k & 0xFFFF;
1702        let mods = k >> 16;
1703        let altgr = mods & MOD_ALTGR != 0 || mods & (MOD_CTRL | MOD_ALT) == MOD_CTRL | MOD_ALT;
1704        if altgr {
1705            return false;
1706        }
1707        let ctrl = mods & MOD_CTRL != 0;
1708        let shift = mods & MOD_SHIFT != 0;
1709        let alt = mods & MOD_ALT != 0;
1710        let sel = doc.selection();
1711        if alt {
1712            return match vk {
1713                VK_UP | VK_DOWN if !ro && !ctrl && !shift => {
1714                    doc.move_lines(if vk == VK_UP { -1 } else { 1 });
1715                    true
1716                }
1717                _ => false,
1718            };
1719        }
1720        if ctrl {
1721            return match (vk, shift) {
1722                (0x46, false) | (0x48, false) if flags & CE_NO_FIND == 0 => {
1723                    let q = if sel.is_empty() || sel.start().line != sel.end().line { v.find.query.clone() } else { doc.selected_text() };
1724                    v.find.open = true;
1725                    v.find.replace = vk == 0x48 && !ro;
1726                    v.find.query = q;
1727                    v.find.from = sel.start();
1728                    v.find.key.2 = u64::MAX;
1729                    self.focus(id.wrapping_add(3));
1730                    true
1731                }
1732                (VK_OEM_2, false) | (VK_DIVIDE, false) => {
1733                    if !ro {
1734                        doc.toggle_comment(h.grammar().comment);
1735                    }
1736                    true
1737                }
1738                (VK_UP, false) | (VK_DOWN, false) => {
1739                    v.sy += if vk == VK_UP { -m.lh } else { m.lh };
1740                    v.sy = v.sy.max(0);
1741                    true
1742                }
1743                _ => false,
1744            };
1745        }
1746        match vk {
1747            // (read-only: Tab moves the focus on)
1748            VK_TAB if !ro => {
1749                if shift {
1750                    doc.back_tab();
1751                } else {
1752                    doc.tab();
1753                }
1754                true
1755            }
1756            VK_F3 => {
1757                self.ce_find_step(doc, v, shift);
1758                true
1759            }
1760            // (without diagnostics F8 is the window's: the splitters)
1761            VK_F8 if doc.diagnostic_count() > 0 => {
1762                if let Some(p) = doc.next_diagnostic(sel.caret, shift) {
1763                    doc.set_selection(TextSel::at(p));
1764                }
1765                v.prefx = -1;
1766                true
1767            }
1768            VK_HOME => {
1769                // the first non-blank, else column 0
1770                let t = doc.line(sel.caret.line);
1771                let ind = t.len() - t.trim_start_matches([' ', '\t']).len();
1772                let to = if sel.caret.byte == ind { 0 } else { ind };
1773                let p = TextPos::new(sel.caret.line, to);
1774                doc.set_selection(if shift { TextSel::new(sel.anchor, p) } else { TextSel::at(p) });
1775                v.prefx = -1;
1776                true
1777            }
1778            VK_ESCAPE => {
1779                if v.find.open {
1780                    v.find.open = false;
1781                } else {
1782                    self.wp.ctx.focus = 0;
1783                }
1784                self.repass();
1785                true
1786            }
1787            _ => false,
1788        }
1789    }
1790
1791    // copy (cut) the selection; without one the caret's whole line
1792    fn ce_copy(&mut self, doc: &mut CodeDoc, cut: bool, ro: bool) {
1793        let sel = doc.selection();
1794        if sel.is_empty() {
1795            let l = sel.caret.line;
1796            let mut s = String::from(doc.line(l));
1797            s.push('\n');
1798            self.clip_set(CLIP_TEXT, s.as_bytes());
1799            if cut && !ro {
1800                let (a, b) = if l + 1 < doc.line_count() {
1801                    (TextPos::new(l, 0), TextPos::new(l + 1, 0))
1802                } else if l > 0 {
1803                    (TextPos::new(l - 1, doc.line(l - 1).len()), TextPos::new(l, doc.line(l).len()))
1804                } else {
1805                    (TextPos::new(0, 0), TextPos::new(0, doc.line(0).len()))
1806                };
1807                doc.delete(a, b);
1808            }
1809            return;
1810        }
1811        let s = doc.selected_text();
1812        self.clip_set(CLIP_TEXT, s.as_bytes());
1813        if cut {
1814            doc.delete(sel.start(), sel.end());
1815        }
1816    }
1817
1818    // the next / previous match after the selection; it becomes the selection
1819    fn ce_find_step(&mut self, doc: &mut CodeDoc, v: &mut View, back: bool) {
1820        if v.find.query.is_empty() {
1821            return;
1822        }
1823        Ui::ce_find_update(&mut v.find, doc);
1824        let sel = doc.selection();
1825        let ms = &v.find.matches;
1826        if ms.is_empty() {
1827            v.find.current = None;
1828            return;
1829        }
1830        let mut i = if back {
1831            let k = ms.partition_point(|x| x.0 < sel.start());
1832            if k == 0 { ms.len() - 1 } else { k - 1 }
1833        } else {
1834            let k = ms.partition_point(|x| x.0 < sel.end() || (x.0 == sel.start() && x.1 == sel.end() && !sel.is_empty()));
1835            if k >= ms.len() { 0 } else { k }
1836        };
1837        // (matches in folded lines are left out)
1838        let n = ms.len();
1839        for _ in 0..n {
1840            if !doc.is_hidden(ms[i].0.line) {
1841                break;
1842            }
1843            i = if back { (i + n - 1) % n } else { (i + 1) % n };
1844        }
1845        if doc.is_hidden(ms[i].0.line) {
1846            v.find.current = None;
1847            return;
1848        }
1849        v.find.current = Some(i);
1850        doc.set_selection(TextSel::new(ms[i].0, ms[i].1));
1851        v.find.from = ms[i].0;
1852        self.repass();
1853    }
1854
1855    // the find bar: query, case, whole word, previous / next, matches, close; replace row
1856    fn ce_find_bar(&mut self, id: u32, doc: &mut CodeDoc, v: &mut View, r: [i32; 4], ro: bool) {
1857        let ids = |k: u32| id.wrapping_add(k);
1858        self.draw_color(self.color(T_LAYER) | 0xFF00_0000);
1859        self.draw_rect(r[0], r[1], r[2], r[3]);
1860        self.draw_color(self.color(T_DIVIDER));
1861        self.draw_rect(r[0], r[1] + r[3] - self.hair(), r[2], self.hair());
1862        let pad = self.dp(6);
1863        let row_h = self.dp(32);
1864        let bw = self.dp(32);
1865        let (fq, fr) = (ids(3), ids(4));
1866        // Esc / F3 / Enter in the boxes
1867        let in_bar = self.wp.ctx.focus == fq || self.wp.ctx.focus == fr;
1868        if in_bar {
1869            if self.ed_key_chord(VK_ESCAPE, 0) {
1870                v.find.open = false;
1871                self.ed_focus_quiet(id);
1872                return;
1873            }
1874            if self.ed_key_chord(VK_F3, 0) {
1875                self.ce_find_step(doc, v, false);
1876            }
1877            if self.ed_key_chord(VK_F3, MOD_SHIFT) {
1878                self.ce_find_step(doc, v, true);
1879            }
1880        }
1881        let shift_enter = self.wp.ctx.focus == fq && self.ed_key_chord(VK_RETURN, MOD_SHIFT);
1882        let x0 = r[0] + pad;
1883        let y0 = r[1] + (self.dp(40) - row_h) / 2;
1884        let count_w = self.dp(90);
1885        let fw = (r[2] - pad * 3 - bw * 5 - count_w).max(self.dp(80));
1886        self.next_rect_px(x0, y0, fw, row_h);
1887        let before = v.find.query.clone();
1888        // Enter taken before the box (its EV_COMMIT also comes from a focus change)
1889        let enter = self.wp.ctx.focus == fq && self.ed_key_chord(VK_RETURN, 0);
1890        self.textbox(fq, Some(tr("Suchen")), &mut v.find.query, 1024);
1891        if v.find.query != before {
1892            // incremental: the first match from where the search began
1893            Ui::ce_find_update(&mut v.find, doc);
1894            let from = v.find.from;
1895            let ms = &v.find.matches;
1896            let k = ms.partition_point(|x| x.0 < from);
1897            // (the first one that is not folded away, round the end)
1898            let n = ms.len();
1899            let k = (0..n).map(|j| (k + j) % n).find(|&j| !doc.is_hidden(ms[j].0.line));
1900            v.find.current = k;
1901            if let Some(i) = v.find.current {
1902                doc.set_selection(TextSel::new(ms[i].0, ms[i].1));
1903            }
1904            self.repass();
1905        }
1906        if shift_enter {
1907            self.ce_find_step(doc, v, true);
1908        } else if enter {
1909            self.ce_find_step(doc, v, false);
1910        }
1911        let mut x = x0 + fw + pad;
1912        let tool = |ui: &mut Ui, kid: u32, x: i32, label: &str, tip: &str, on: bool| -> bool {
1913            ui.next_rect_px(x, y0, bw, row_h);
1914            ui.next_flags(ST_SUBTLE | if on { ST_CHECKED } else { 0 });
1915            let e = ui.button(kid, label);
1916            ui.tooltip(tip);
1917            e & EV_ACT != 0
1918        };
1919        if tool(self, ids(5), x, "Aa", tr("Groß-/Kleinschreibung"), v.find.case != 0) {
1920            v.find.case ^= 1;
1921            self.repass();
1922        }
1923        x += bw;
1924        if tool(self, ids(6), x, "W", tr("Ganzes Wort"), v.find.word != 0) {
1925            v.find.word ^= 1;
1926            self.repass();
1927        }
1928        x += bw;
1929        self.next_rect_px(x, y0, bw, row_h);
1930        if self.icon_button(ids(7), IC_CHEV_UP, tr("Vorheriger Treffer")) & EV_ACT != 0 {
1931            self.ce_find_step(doc, v, true);
1932        }
1933        x += bw;
1934        self.next_rect_px(x, y0, bw, row_h);
1935        if self.icon_button(ids(8), IC_CHEV_DOWN, tr("Nächster Treffer")) & EV_ACT != 0 {
1936            self.ce_find_step(doc, v, false);
1937        }
1938        x += bw;
1939        Ui::ce_find_update(&mut v.find, doc);
1940        // the current match: the one the selection is
1941        let sel = doc.selection();
1942        v.find.current = v.find.matches.binary_search_by(|m| m.0.cmp(&sel.start()).then(m.1.cmp(&sel.end()))).ok();
1943        let count = if v.find.query.is_empty() {
1944            String::new()
1945        } else if v.find.matches.is_empty() {
1946            String::from(tr("Keine Treffer"))
1947        } else {
1948            let more = if v.find.matches.len() >= FIND_MAX { "+" } else { "" };
1949            format!("{}/{}{}", v.find.current.map_or(String::from("?"), |c| format!("{}", c + 1)), v.find.matches.len(), more)
1950        };
1951        self.next_rect_px(x + pad / 2, y0, count_w, row_h);
1952        self.ts_font(TS_CAPTION);
1953        let tc = self.color(T_TEXT2);
1954        let (f, s) = self.text.get_font();
1955        let base = self.ts_base(TS_CAPTION);
1956        let lr = self.place(count_w, row_h);
1957        self.cmd_text(lr[0], lr[1] + (row_h - self.ts_line(TS_CAPTION)) / 2 + base, count.as_bytes(), f, s, tc, count_w);
1958        self.wp.ctx.last_state = 0;
1959        self.sem(K_TEXT, 0, Some(&count), 0);
1960        x += count_w;
1961        self.next_rect_px(x, y0, bw, row_h);
1962        if self.icon_button(ids(9), IC_CANCEL, tr("Schließen")) & EV_ACT != 0 {
1963            v.find.open = false;
1964            self.ed_focus_quiet(id);
1965        }
1966        // the replace row
1967        if v.find.replace && !ro {
1968            let y1 = y0 + self.dp(36);
1969            self.next_rect_px(x0, y1, fw, row_h);
1970            let renter = self.wp.ctx.focus == fr && self.ed_key_chord(VK_RETURN, 0);
1971            self.textbox(fr, Some(tr("Ersetzen")), &mut v.find.rep, 1024);
1972            let mut xr = x0 + fw + pad;
1973            let q = v.find.query.clone();
1974            let rep = v.find.rep.clone();
1975            let opts = v.find.opts();
1976            self.next_rect_px(xr, y1, self.dp(96), row_h);
1977            if self.button(ids(10), tr("Ersetzen")) & EV_ACT != 0 || renter {
1978                doc.replace_next(&q, &rep, opts);
1979                self.repass();
1980            }
1981            xr += self.dp(100);
1982            self.next_rect_px(xr, y1, self.dp(110), row_h);
1983            if self.button(ids(11), tr("Alle ersetzen")) & EV_ACT != 0 {
1984                doc.replace_all(&q, &rep, opts);
1985            }
1986        }
1987    }
1988}
1989
1990// a caret move (key vk) from c0 that ended in folded lines at c: on to the next line that is
1991// not hidden, in the move's direction (Left / Right: that line's end / start; else the
1992// preferred x)
1993fn skip_hidden(view: &CView, c0: TextPos, c: TextPos, vk: u32, prefx: i32) -> TextPos {
1994    let doc = view.doc;
1995    if !doc.is_hidden(c.line) {
1996        return c;
1997    }
1998    let l = doc.visible_line(c.line, c > c0);
1999    match vk {
2000        VK_LEFT => TextPos::new(l, doc.line(l).len()),
2001        VK_RIGHT => TextPos::new(l, 0),
2002        _ => TextPos::new(l, view.hit_x(l, 0, prefx.max(0))),
2003    }
2004}
2005
2006// Backspace in leading blanks: back to the previous tab stop
2007fn soft_back(doc: &CodeDoc, a: TextPos, b: TextPos) -> (TextPos, TextPos) {
2008    if a.line != b.line || b.byte == 0 {
2009        return (a, b);
2010    }
2011    let t = doc.line(b.line);
2012    if !t[..b.byte].bytes().all(|c| c == b' ') {
2013        return (a, b);
2014    }
2015    let tw = doc.tab_width.clamp(1, 16) as usize;
2016    let col = b.byte;
2017    let target = (col - 1) / tw * tw;
2018    (TextPos::new(b.line, target), b)
2019}
2020
2021// the last line a diagnostic marks something on (a part over lines ending at a line's start
2022// ends on the line before)
2023fn last_line(d: &Mark) -> usize {
2024    if d.b.byte == 0 && d.b.line > d.a.line { d.b.line - 1 } else { d.b.line }
2025}
2026
2027// a diagnostic marks something on line l
2028fn touches(d: &Mark, l: usize) -> bool {
2029    d.a.line <= l && l <= last_line(d)
2030}
2031
2032// the columns a diagnostic underlines on line l (len bytes; col: the column of a byte): over
2033// lines to the line's end and one cell for its break; at least one cell
2034fn mark_cols(d: &Mark, l: usize, len: usize, col: impl Fn(usize) -> usize) -> (usize, usize) {
2035    let c0 = if d.a.line == l { col(d.a.byte) } else { 0 };
2036    let c1 = if d.b.line == l { col(d.b.byte) } else { col(len) + 1 };
2037    (c0, c1.max(c0 + 1))
2038}
2039
2040// the messages of diagnostics (indexes of doc.marks()): "Fehler: ...", joined by " · "
2041fn diag_text(doc: &CodeDoc, ix: &[usize]) -> String {
2042    let mut s = String::new();
2043    for (k, &i) in ix.iter().enumerate() {
2044        let d = &doc.marks()[i];
2045        if k > 0 {
2046            s.push_str(" · ");
2047        }
2048        s.push_str(tr(["Fehler", "Warnung", "Hinweis"][d.sev.min(DIAG_INFO) as usize]));
2049        s.push_str(": ");
2050        s.push_str(&d.msg);
2051    }
2052    s
2053}
2054
2055// the matches of lines first .. first + n
2056fn doc_find_range(doc: &CodeDoc, q: &str, opts: u8, first: usize, n: usize) -> Vec<(TextPos, TextPos)> {
2057    doc.find_lines(q, opts, first, n)
2058}
2059
2060// the widest line and the size of all lines (bytes) at the document's revision
2061fn line_sizes(doc: &CodeDoc) -> (u32, u64, usize, usize) {
2062    let (mut w, mut t) = (0, 0);
2063    for l in doc.lines() {
2064        w = w.max(l.len());
2065        t += l.len() + 1;
2066    }
2067    (doc.uid(), doc.revision(), w + 1, t)
2068}