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