use alloc::collections::BTreeMap;
use alloc::format;
use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec::Vec;
use gfx::{CAP_ROUND, JOIN_ROUND};
use text::segment;
use text::FS_SYN_ITALIC;
use crate::cmd::{Draw, DrawRef, Gfx};
use crate::code_assist::*;
use crate::code_doc::*;
use crate::code_long::{Longs, COL_LONG};
use crate::consts::*;
use crate::ed_core::*;
use crate::highlight::*;
use crate::i18n::tr;
use crate::keys::*;
use crate::{Sp, Ui};
pub const CE_READONLY: u32 = 1;
pub const CE_NO_GUTTER: u32 = 2;
pub const CE_NO_FRAME: u32 = 4;
pub const CE_NO_FIND: u32 = 8;
pub const CE_NO_LINE: u32 = 16;
pub const CE_MARKS: u32 = 32;
pub const CE_BREAKPOINTS: u32 = 64;
pub const CE_FOLDS: u32 = 128;
const NAT_W: i32 = 560;
const NAT_H: i32 = 320;
const LABEL_MAX: usize = 64 * 1024; const BRACKET_LINES: usize = 3000; const UNKNOWN: u32 = u32::MAX;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct CodeStatus {
pub line: usize,
pub col: usize,
pub matches: usize,
pub current: usize,
pub hl_lines: usize,
pub lines: usize,
pub diags: [usize; 3],
pub diag: Option<u8>,
pub popup: usize,
pub popup_sel: usize,
pub sig: bool,
pub hidden: usize,
}
#[derive(Default)]
struct Hl {
grammar: usize,
uid: u32,
rev: u64,
states: Vec<u32>,
valid: usize,
known: usize,
dirty: usize,
count: usize,
}
#[derive(Default)]
struct Find {
open: bool,
replace: bool,
query: String,
rep: String,
case: i32,
word: i32,
matches: Vec<(TextPos, TextPos)>,
key: (String, u8, u64, u32),
current: Option<usize>,
from: TextPos,
}
impl Find {
fn opts(&self) -> u8 {
(if self.case != 0 { FIND_CASE } else { 0 }) | (if self.word != 0 { FIND_WORD } else { 0 })
}
}
#[derive(Default)]
struct View {
seen: u32,
sx: i32,
sy: i32,
t0: u32,
prefx: i32,
drag: u8,
word: (TextPos, TextPos),
held: Option<(usize, usize)>,
label: (u32, u64, usize, usize, String),
last_sel: TextSel,
last_rev: u64,
hl: Hl,
find: Find,
max_cols: (u32, u64, usize, usize),
bracket: Option<(u32, u64, TextPos, Option<(TextPos, TextPos)>)>,
status: CodeStatus,
hl_last: usize,
caret_diag: Option<Diag>,
long: Longs,
pop: Pop,
sig: Sig,
hov: Hov,
events: Vec<CodeEv>,
show_caret: bool,
}
#[derive(Default)]
pub(crate) struct S {
views: BTreeMap<u32, View>,
theme: Option<CodeTheme>,
size_pt: f32,
find_req: Option<(u32, String, bool)>,
waves: Vec<(u32, u32, DrawRef)>,
}
struct Wave {
color: u32,
hp: i32,
amp: i32,
width: i32,
}
impl Draw for Wave {
fn draw(&self, g: &mut Gfx, r: [i32; 4]) {
let pad = (self.width >> 9) + 1;
let (x0, x1, top) = (r[0] + pad, r[0] + r[2] - pad, r[1] + pad);
let (c0, c1) = (g.dc.clip[0] - g.dc.ox, g.dc.clip[2] - g.dc.ox);
let k0 = ((c0 - x0) / self.hp - 2).max(0);
let y = |k: i32| ((top + (k & 1) * self.amp) << 8) + 128;
let sh = g.shapes();
sh.path.begin(sh.dc);
let mut k = k0;
let mut x = x0 + k * self.hp;
sh.path.move_fx(x << 8, y(k));
while x < x1 && x <= c1 + self.hp * 2 {
x = (x + self.hp).min(x1);
k += 1;
sh.path.line_fx(x << 8, y(k));
}
sh.dc.set_color(self.color);
sh.path.stroke(sh.ras, sh.dc, sh.sf, self.width, CAP_ROUND << 4 | JOIN_ROUND);
}
}
#[derive(Clone, Copy)]
struct Metrics {
font: u32,
italic: u32,
size: i32,
cw: i32,
lh: i32,
base: i32,
per_cell: bool,
}
struct CView<'a> {
doc: &'a CodeDoc,
cw: i32,
lh: i32,
tw: usize,
long: &'a Longs,
}
impl EdView for CView<'_> {
fn lines(&self) -> usize {
self.doc.line_count()
}
fn text(&self, line: usize) -> &str {
self.doc.line(line)
}
fn rows(&self, _: usize) -> usize {
1
}
fn row_range(&self, line: usize, _: usize) -> (usize, usize) {
(0, self.doc.line(line).len())
}
fn x_at(&self, line: usize, _: usize, byte: usize) -> i32 {
self.long.col(self.doc, line, byte, self.tw) as i32 * self.cw
}
fn hit_x(&self, line: usize, _: usize, x: i32) -> usize {
let col = ((x.max(0) + self.cw / 2) / self.cw.max(1)) as usize;
self.long.byte(self.doc, line, col, self.tw)
}
fn row_y(&self, line: usize, _: usize) -> (i32, i32) {
(self.doc.row_of_line(line) as i32 * self.lh, self.lh)
}
fn row_at(&self, y: i32) -> (usize, usize) {
(self.doc.line_of_row((y.max(0) / self.lh.max(1)) as usize), 0)
}
}
fn code_word(t: &str, byte: usize) -> (usize, usize) {
let b = byte.min(t.len());
let at = t[b..].chars().next().or_else(|| t[..b].chars().next_back());
match at {
Some(c) if is_word(c) => {
let a = t[..b].char_indices().rev().take_while(|(_, c)| is_word(*c)).last().map_or(b, |(i, _)| i);
let e = t[b..].char_indices().find(|(_, c)| !is_word(*c)).map_or(t.len(), |(i, _)| b + i);
(a, e)
}
_ => word_range(t, b),
}
}
impl Ui {
pub fn code_theme_set(&mut self, theme: Option<CodeTheme>) {
self.m.code.theme = theme;
self.repass();
}
pub fn code_font_size(&mut self, pt: f32) {
self.m.code.size_pt = pt;
self.repass();
}
pub fn code_find(&mut self, id: u32, query: &str, replace: bool) {
self.m.code.find_req = Some((id, String::from(query), replace));
self.repass();
}
pub fn code_status(&self, id: u32) -> CodeStatus {
self.m.code.views.get(&id).map_or(CodeStatus::default(), |v| v.status)
}
pub fn code_diag(&self, id: u32) -> Option<Diag> {
self.m.code.views.get(&id).and_then(|v| v.caret_diag.clone())
}
pub fn code_events(&self, id: u32) -> Vec<CodeEv> {
self.m.code.views.get(&id).map_or(Vec::new(), |v| v.events.clone())
}
pub fn code_popup_item(&self, id: u32) -> Option<CompItem> {
let v = self.m.code.views.get(&id)?;
let p = &v.pop;
p.shown.get(p.sel).and_then(|&i| p.items.get(i)).filter(|_| p.open).cloned()
}
fn ce_metrics(&mut self) -> Metrics {
let pt = if self.m.code.size_pt > 0.0 { self.m.code.size_pt } else { 10.0 };
let (font, _) = self.mono_font(pt);
let dpi = self.wp.rt.dpi as f32;
let scale = self.text_scale as f32 / 100.0;
let mut size = ((pt.clamp(4.0, 96.0) * 4.0 / 3.0 * dpi / 96.0 * scale * 256.0) as i32).max(256);
let prev = self.text.get_font();
self.text.set_font(font, size);
let per_cell = self.text.measure("i") != self.text.measure("M");
let cw = if per_cell {
self.text.width("M").max(self.text.width("W")).max(1)
} else {
let adv = self.text.measure("0000000000");
let cw = ((adv + 1280) / 2560).max(1);
if adv > 0 {
size = (size as i64 * (cw as i64 * 2560) / adv as i64) as i32;
self.text.set_font(font, size);
}
cw
};
let (asc, desc) = (self.text.ascent(), self.text.descent());
let lh = (self.text.line_h().max(asc + desc) + self.dp(3)).max(1);
let base = ((lh - asc - desc) >> 1) + asc;
self.text.set_font(prev.0, prev.1);
let it = self.text.style(font, FS_SYN_ITALIC);
Metrics { font, italic: if it >= 0 { it as u32 } else { font }, size, cw, lh, base, per_cell }
}
fn ce_hl_sync(hl: &mut Hl, doc: &CodeDoc, h: &Highlighter) {
let g = h.grammar() as *const Grammar as usize;
let n = doc.line_count();
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 };
match changes {
Some(cs) => {
for c in cs {
if hl.dirty <= hl.valid {
hl.dirty = 0; }
let lo = (c.first + 1).min(hl.states.len());
let hi = (c.first + c.old).min(hl.states.len()).max(lo);
hl.states.splice(lo..hi, core::iter::repeat_n(UNKNOWN, c.new.saturating_sub(1)));
let d = c.new as isize - c.old as isize;
let shift = |x: usize| -> usize {
if x >= c.first + c.old {
(x as isize + d) as usize
} else {
x.min(c.first)
}
};
hl.known = shift(hl.known);
hl.dirty = if hl.dirty > c.first { shift(hl.dirty).max(c.first + c.new) } else { c.first + c.new };
hl.valid = hl.valid.min(c.first);
}
}
None => {
hl.states.clear();
hl.states.push(0);
hl.valid = 0;
hl.known = 0;
hl.dirty = 0;
hl.grammar = g;
hl.uid = doc.uid();
}
}
hl.states.resize(n + 1, UNKNOWN);
if hl.states[0] == UNKNOWN {
hl.states[0] = 0;
}
hl.known = hl.known.min(n);
hl.valid = hl.valid.min(n);
hl.rev = doc.revision();
}
fn ce_hl_upto(hl: &mut Hl, doc: &CodeDoc, h: &Highlighter, upto: usize, toks: &mut Vec<Token>, long: &Longs, keep: usize) {
let upto = upto.min(doc.line_count());
let tw = doc.tab_width.clamp(1, 16) as usize;
while hl.valid < upto {
let k = hl.valid;
let t = doc.line(k);
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) };
hl.count += 1;
let next = k + 1;
if next >= hl.dirty && next <= hl.known && hl.states[next] == st {
hl.valid = hl.known;
hl.dirty = 0;
continue;
}
hl.states[next] = st;
hl.valid = next;
if hl.valid > hl.known {
hl.known = hl.valid;
}
}
}
fn ce_bracket(v: &mut View, doc: &CodeDoc, h: &Highlighter) -> Option<(TextPos, TextPos)> {
let caret = doc.selection().caret;
if let Some((uid, rev, at, r)) = v.bracket {
if uid == doc.uid() && rev == doc.revision() && at == caret {
return r;
}
}
let pairs: Vec<(u8, u8)> = h.grammar().pairs.as_bytes().chunks(2).filter(|c| c.len() == 2).map(|c| (c[0], c[1])).collect();
let tw = doc.tab_width.clamp(1, 16) as usize;
let long = &v.long;
let mut toks = Vec::new();
let mut cur: Option<(usize, Rc<Vec<Token>>, usize)> = None;
let mut code_at = |hl: &mut Hl, l: usize, b: usize| -> bool {
if cur.as_ref().is_none_or(|c| c.0 != l) {
Ui::ce_hl_upto(hl, doc, h, l, &mut toks, long, usize::MAX);
let t = doc.line(l);
let tk = if t.len() >= COL_LONG {
long.tokens(doc, l, tw, h, hl.states[l]).1
} else {
h.line(t, hl.states[l], &mut toks);
Rc::new(toks.clone())
};
cur = Some((l, tk, 0));
}
let Some((_, tk, last)) = &mut cur else { return true };
let mut k = (*last).min(tk.len());
while k < tk.len() && tk[k].end <= b {
k += 1;
}
while k > 0 && tk[k - 1].end > b {
k -= 1;
}
*last = k;
!tk.get(k).is_some_and(|t| t.start <= b && matches!(t.kind, SY_STRING | SY_COMMENT | SY_CODE | SY_ESCAPE))
};
let line = doc.line(caret.line).as_bytes();
let mut found = None;
for at in [caret.byte, caret.byte.wrapping_sub(1)] {
let Some(&c) = line.get(at) else { continue };
let Some(&(o, cl)) = pairs.iter().find(|p| p.0 == c || p.1 == c) else { continue };
if !code_at(&mut v.hl, caret.line, at) {
continue;
}
let fwd = c == o;
let (want, other) = if fwd { (cl, o) } else { (o, cl) };
let mut depth = 0;
let mut l = caret.line;
let mut b = at as isize;
let mut steps = 0;
'scan: loop {
let t = doc.line(l).as_bytes();
loop {
let is = |x: &u8| *x == want || *x == other;
let next = if fwd {
t.get((b + 1) as usize..).and_then(|r| r.iter().position(is)).map(|k| b + 1 + k as isize)
} else {
t[..b.max(0) as usize].iter().rposition(is).map(|k| k as isize)
};
let Some(nb) = next else { break };
b = nb;
let x = t[b as usize];
if !code_at(&mut v.hl, l, b as usize) {
continue;
}
if x == other {
depth += 1;
} else if depth == 0 {
found = Some((TextPos::new(caret.line, at), TextPos::new(l, b as usize)));
break 'scan;
} else {
depth -= 1;
}
}
steps += 1;
if steps > BRACKET_LINES {
break;
}
if fwd {
l += 1;
if l >= doc.line_count() {
break;
}
b = -1;
} else {
if l == 0 {
break;
}
l -= 1;
b = doc.line(l).len() as isize;
}
}
if found.is_some() {
break;
}
}
v.bracket = Some((doc.uid(), doc.revision(), caret, found));
found
}
fn ce_find_update(f: &mut Find, doc: &CodeDoc) {
let key = (f.query.clone(), f.opts(), doc.revision(), doc.uid());
if f.key == key {
return;
}
let same_query = f.key.0 == key.0 && f.key.1 == key.1 && f.key.3 == key.3;
let changes = if same_query { doc.changes_since(f.key.2).map(|c| c.to_vec()) } else { None };
match changes {
Some(cs) => {
for c in cs {
let lo = f.matches.partition_point(|m| m.0.line < c.first);
let hi = f.matches.partition_point(|m| m.0.line < c.first + c.old);
let d = c.new as isize - c.old as isize;
for m in f.matches[hi..].iter_mut() {
m.0.line = (m.0.line as isize + d) as usize;
m.1.line = (m.1.line as isize + d) as usize;
}
let fresh = doc_find_range(doc, &f.query, f.opts(), c.first, c.new);
f.matches.splice(lo..hi, fresh);
}
f.matches.truncate(FIND_MAX);
}
None => f.matches = doc.find_all(&f.query, f.opts()),
}
f.key = key;
}
#[cfg_attr(jfgui_dev, track_caller)]
pub fn code_editor(&mut self, id: u32, doc: &mut CodeDoc, h: &Highlighter, flags: u32) -> u32 {
let _src = self.src_scope();
self.code_editor_with(id, doc, h, None, flags)
}
#[cfg_attr(jfgui_dev, track_caller)]
pub fn code_editor_with(&mut self, id: u32, doc: &mut CodeDoc, h: &Highlighter, mut assist: Option<&mut dyn CodeAssist>, flags: u32) -> u32 {
let _src = self.src_scope();
let ro = flags & CE_READONLY != 0;
let stflags = self.take_flags();
let r = self.place(self.dp(NAT_W), self.dp(NAT_H));
let hair = self.hair();
let st = self.style;
let frame = flags & CE_NO_FRAME == 0;
let m = self.ce_metrics();
let theme = self.m.code.theme.clone().unwrap_or_else(|| self.code_theme());
let mut v = self.m.code.views.remove(&id).unwrap_or_default();
v.seen = self.frame;
v.hl.count = 0;
v.events.clear();
let view0 = (v.sx, v.sy);
let rev0 = doc.revision();
let sel0 = doc.selection();
let tw = doc.tab_width.clamp(1, 16) as usize;
if let Some((rid, q, rep)) = self.m.code.find_req.take() {
if rid == id && flags & CE_NO_FIND == 0 {
v.find.open = true;
v.find.replace = rep;
v.find.query = q;
v.find.from = doc.selection().start();
v.find.key.2 = u64::MAX;
self.focus(id.wrapping_add(3));
} else {
self.m.code.find_req = Some((rid, q, rep));
}
}
let sbw = st.scrollbar_thickness(self);
let n = doc.line_count();
let digits = format!("{}", n).len().max(2) as i32;
let bpw = if flags & CE_BREAKPOINTS != 0 || !doc.breakpoints().is_empty() || doc.exec_line().is_some() { m.lh } else { 0 };
let mk = if flags & CE_MARKS != 0 || doc.diagnostic_count() > 0 { m.lh } else { 0 };
let nums = flags & CE_NO_GUTTER == 0;
let numw = if nums { digits * m.cw + self.dp(18) } else { 0 };
let fw = if flags & CE_FOLDS != 0 || doc.fold_count() > 0 { (m.lh * 3 / 4).max(self.dp(12)) } else { 0 };
let gw = bpw + mk + numw + fw;
let bar_h = if v.find.open { self.dp(if v.find.replace { 76 } else { 40 }) } else { 0 };
let inner = [r[0] + hair, r[1] + hair, (r[2] - hair * 2).max(1), (r[3] - hair * 2).max(1)];
let ty = inner[1] + bar_h;
let tx = inner[0] + gw + self.dp(6);
if v.max_cols.0 != doc.uid() || v.max_cols.1 != doc.revision() || v.max_cols.2 == 0 {
v.max_cols = line_sizes(doc);
}
let content_w = (v.max_cols.2 as i32 + 2).saturating_mul(m.cw);
let tw_px0 = (inner[0] + inner[2] - sbw - tx).max(m.cw);
let hbar = content_w > tw_px0;
let th = (inner[1] + inner[3] - ty - if hbar { sbw } else { 0 }).max(m.lh);
let tw_px = tw_px0;
let total_h = doc.visible_lines() as i32 * m.lh;
let page = th;
let mut hb = HB_FOCUS | HB_TEXT | CUR_IBEAM << HB_CURSOR_SHIFT;
if stflags & ST_DISABLED != 0 {
hb |= HB_DISABLED;
}
if doc.diagnostic_count() > 0 || assist.is_some() {
hb |= HB_MOVES; }
self.wp.ctx.last_rect = [inner[0], ty, inner[2], inner[1] + inner[3] - ty];
let hit0 = self.wp.ctx.hits[self.wp.ctx.hit_cur].len();
let ev = self.behave(id, hb);
let disabled = self.wp.ctx.last_state == SS_DISABLED as u32;
let focused = !disabled && self.ed_focused(id);
let mut out = ev & (EV_HOVER | EV_HELD | EV_DOUBLE | EV_RCLICK | EV_DRAG);
if focused {
if let (Some(sel), Some(held)) = (self.textbox_selection(id), v.held) {
if sel != held {
let base = doc.flat(TextPos::new(v.label.2, 0));
let (a, c) = (doc.unflat(sel.0 + base), doc.unflat(sel.1 + base));
doc.set_selection(TextSel::new(a, c));
}
}
if let Some(t) = self.a11y_text(id) {
if !ro {
let e = doc.end();
doc.replace(TextPos::default(), e, &t);
}
}
} else if v.held.is_some() {
v.held = None;
v.drag = 0;
self.ed_drop(id);
out |= EV_COMMIT;
}
if !focused {
v.pop.close();
v.sig.open = false;
}
let (mx, my) = (self.rtl_map(self.wp.ctx.mx, inner[0], inner[2]), self.wp.ctx.my);
let (cx, cy) = (mx - tx + v.sx, my - ty + v.sy);
let in_gutter = mx < tx && gw > 0;
let c = &self.wp.ctx;
let press = c.pressed & (1 << BTN_LEFT) != 0 && ev & EV_HOVER != 0 && c.active == id;
if press && !disabled {
let view = CView { doc: &*doc, cw: m.cw, lh: m.lh, tw, long: &v.long };
let p = clamp_pos(&view, TextPos::new(view.row_at(cy).0, view.hit_x(view.row_at(cy).0, 0, cx)));
let row_line = view.row_at(cy).0;
v.prefx = -1;
v.t0 = self.now_ms;
v.pop.close();
v.sig.open = false;
let fold_box = doc.fold_at(row_line).is_some_and(|f| f.1) && {
let end = v.long.col(doc, row_line, doc.line(row_line).len(), tw) as i32 * m.cw;
cx >= end + m.cw / 2 && cx < end + m.cw * 4
};
if in_gutter && bpw > 0 && mx < inner[0] + bpw {
v.events.push(CodeEv::Breakpoint(row_line));
v.drag = 0;
} else if in_gutter && fw > 0 && mx >= inner[0] + gw - fw && doc.fold_at(row_line).is_some() {
doc.toggle_fold(row_line);
v.drag = 0;
self.repass();
} else if fold_box && !in_gutter {
doc.set_folded(row_line, false);
v.drag = 0;
self.repass();
} else if in_gutter {
let l = view.row_at(cy).0;
let e = if l + 1 < n { TextPos::new(l + 1, 0) } else { TextPos::new(l, doc.line(l).len()) };
v.word = (TextPos::new(l, 0), e);
if self.mods & MOD_SHIFT != 0 {
let a = doc.selection().anchor;
doc.set_selection(TextSel::new(a, e));
} else {
doc.set_selection(TextSel::new(v.word.0, e));
}
v.drag = 3;
} else if ev & EV_DOUBLE != 0 {
let (a, b) = code_word(doc.line(p.line), p.byte);
v.word = (TextPos::new(p.line, a), TextPos::new(p.line, b));
doc.set_selection(TextSel::new(v.word.0, v.word.1));
v.drag = 2;
} else if self.mods & MOD_SHIFT != 0 {
let a = doc.selection().anchor;
doc.set_selection(TextSel::new(a, p));
v.drag = 1;
} else {
doc.set_selection(TextSel::at(p));
v.drag = 1;
if self.mods & MOD_CTRL != 0 {
v.events.push(CodeEv::Definition(p));
v.drag = 0;
}
}
} else if ev & EV_HELD != 0 && v.drag != 0 {
if my < ty {
v.sy -= m.lh;
} else if my >= ty + th {
v.sy += m.lh;
}
if mx >= tx + tw_px {
v.sx += m.cw * 2;
} else if mx < tx && v.drag != 3 {
v.sx -= m.cw * 2;
}
v.sy = v.sy.clamp(0, (total_h - th).max(0));
v.sx = v.sx.clamp(0, (content_w - tw_px).max(0));
let (cx, cy) = (mx - tx + v.sx, my - ty + v.sy);
let view = CView { doc: &*doc, cw: m.cw, lh: m.lh, tw, long: &v.long };
let l = view.row_at(cy).0;
let p = TextPos::new(l, view.hit_x(l, 0, cx));
match v.drag {
3 => {
let e = if l + 1 < n { TextPos::new(l + 1, 0) } else { TextPos::new(l, doc.line(l).len()) };
if TextPos::new(l, 0) < v.word.0 {
doc.set_selection(TextSel::new(v.word.1, TextPos::new(l, 0)));
} else {
doc.set_selection(TextSel::new(v.word.0, e));
}
}
2 => {
let (a, b) = code_word(doc.line(l), p.byte);
let (wa, wb) = (TextPos::new(l, a), TextPos::new(l, b));
if wa < v.word.0 {
doc.set_selection(TextSel::new(v.word.1, wa));
} else {
doc.set_selection(TextSel::new(v.word.0, wb.max(v.word.1)));
}
}
_ => {
let a = doc.selection().anchor;
doc.set_selection(TextSel::new(a, p));
}
}
} else if ev & EV_HELD == 0 {
v.drag = 0;
}
if focused {
loop {
let k0 = self.ed_key_first();
if k0 == 0 {
break;
}
let k = self.rtl_text_key(k0);
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) {
self.ed_key_take();
v.t0 = self.now_ms;
self.repass();
continue;
}
let act = {
let view = CView { doc: &*doc, cw: m.cw, lh: m.lh, tw, long: &v.long };
ed_key(&view, doc.selection(), &mut v.prefx, k, page, false)
};
let Some(act) = act else { break };
self.ed_key_take();
v.t0 = self.now_ms;
match act {
EdAct::Sel(s) => {
let c0 = doc.selection().caret;
let s = if doc.hidden().is_empty() || !matches!(k & 0xFFFF, VK_UP | VK_DOWN | VK_LEFT | VK_RIGHT | VK_PRIOR | VK_NEXT) {
s
} else {
let view = CView { doc: &*doc, cw: m.cw, lh: m.lh, tw, long: &v.long };
let c = skip_hidden(&view, c0, s.caret, k & 0xFFFF, v.prefx);
if s.anchor == s.caret { TextSel::at(c) } else { TextSel::new(s.anchor, c) }
};
doc.set_selection(s);
}
EdAct::Del(a, b, _) if !ro => {
let back = k & 0xFFFF == VK_BACK;
let empty = doc.selection().is_empty();
let (a, b) = if back && empty && !doc.use_tabs { soft_back(doc, a, b) } else { (a, b) };
doc.delete_kind(a, b, if !empty { CK_NONE } else if back { CK_BACK } else { CK_DEL });
}
EdAct::Enter if !ro => {
doc.newline(h.grammar().openers);
}
EdAct::Copy => self.ce_copy(doc, false, ro),
EdAct::Cut => self.ce_copy(doc, !ro, ro),
EdAct::Paste if !ro => {
if let Some(t) = self.clip_get(CLIP_TEXT) {
let s = String::from_utf8_lossy(&t).replace('\0', "");
if !s.is_empty() {
doc.insert(&s);
}
}
}
EdAct::Undo if !ro => {
doc.undo();
}
EdAct::Redo if !ro => {
doc.redo();
}
_ => {}
}
}
let cps = self.text_take(super::ctx::TEXTQ_MAX);
if !ro {
let s = typed_text(&cps, false);
if !s.is_empty() {
doc.type_text(&s, h.grammar().closers);
v.t0 = self.now_ms;
v.prefx = -1;
if let Some(a) = assist.as_deref_mut() {
Ui::ce_assist_typed(doc, &mut v, a, &s);
}
}
}
}
if v.pop.open {
v.pop.filter(doc);
}
if let Some((l, a)) = v.pop.quiet {
let c = doc.clamp(doc.selection().caret);
if c.line != l || c.byte <= a || word_around(doc.line(l), c.byte).0 != a {
v.pop.quiet = None;
}
}
if let Some(a) = assist.as_deref_mut().filter(|_| v.sig.open) {
let key = (doc.revision(), doc.selection().caret);
if v.sig.key != key {
v.sig.key = key;
v.sig.help = a.signature(doc, key.1);
v.sig.open = v.sig.help.is_some();
}
}
let c = doc.clamp(doc.selection().caret);
if doc.is_hidden(c.line) {
if doc.selection() != v.last_sel || doc.revision() != v.last_rev {
doc.reveal(c.line);
} else {
let l = doc.visible_line(c.line, false);
doc.set_selection(TextSel::at(TextPos::new(l, doc.line(l).len())));
}
self.repass();
}
let changed = doc.revision() != rev0;
if changed {
out |= EV_ACT;
v.max_cols = line_sizes(doc);
}
if changed || doc.selection() != sel0 {
self.repass();
}
let n = doc.line_count();
let total_h = doc.visible_lines() as i32 * m.lh;
if v.find.open {
Ui::ce_find_update(&mut v.find, doc);
}
let wy = self.wp.ctx.wheel_y;
let wx = self.wp.ctx.wheel_x;
let over = ev & EV_HOVER != 0 || self.wp.ctx.hot == id.wrapping_add(1) || self.wp.ctx.hot == id.wrapping_add(2);
if over && (wy != 0 || wx != 0) {
let (dx, dy) = if self.mods & MOD_SHIFT != 0 { (wy, 0) } else { (wx, wy) };
let sy = (v.sy - dy * m.lh * 3 / 120).clamp(0, (total_h - th).max(0));
let sx = (v.sx - dx * m.cw * 6 / 120).clamp(0, (content_w - tw_px).max(0));
if sy != v.sy || sx != v.sx {
v.sy = sy;
v.sx = sx;
self.wp.ctx.wheel_y = 0;
self.wp.ctx.wheel_x = 0;
}
}
let (px, py) = self.pan_take(id);
v.sy = (v.sy + py).clamp(0, (total_h - th).max(0));
v.sx = (v.sx + px).clamp(0, (content_w - tw_px).max(0));
let caret = doc.clamp(doc.selection().caret);
let ccol = v.long.col(doc, caret.line, caret.byte, tw) as i32;
if (focused || v.find.open) && (doc.selection() != v.last_sel || doc.revision() != v.last_rev || core::mem::take(&mut v.show_caret)) {
let cy = doc.row_of_line(caret.line) as i32 * m.lh;
if cy < v.sy {
v.sy = cy;
}
if cy + m.lh > v.sy + th {
v.sy = cy + m.lh - th;
}
let cxp = ccol * m.cw;
if cxp < v.sx + m.cw * 2 {
v.sx = (cxp - m.cw * 4).max(0);
}
if cxp > v.sx + tw_px - m.cw * 2 {
v.sx = cxp - tw_px + m.cw * 4;
}
}
v.last_sel = doc.selection();
v.last_rev = doc.revision();
v.sy = v.sy.clamp(0, (total_h - th).max(0));
v.sx = v.sx.clamp(0, (content_w - tw_px).max(0));
let first_row = (v.sy / m.lh) as usize;
let rows_n = doc.visible_lines();
let last_row = (((v.sy + th) / m.lh) as usize + 1).min(rows_n).max(first_row.min(rows_n));
let mut rows: Vec<usize> = Vec::with_capacity(last_row.saturating_sub(first_row));
if doc.hidden().is_empty() {
rows.extend(first_row..last_row);
} else if last_row > first_row {
let mut l = doc.line_of_row(first_row);
for _ in first_row..last_row {
rows.push(l);
l = doc.visible_line(l + 1, true);
if l <= *rows.last().unwrap_or(&0) {
break;
}
}
}
let first = rows.first().copied().unwrap_or(0).min(n);
let last = rows.last().map_or(first, |&l| l + 1).min(n);
let sy0 = v.sy;
let lh0 = m.lh;
let row_y = move |k: usize| ty + (first_row + k) as i32 * lh0 - sy0;
let mut toks = Vec::new();
Ui::ce_hl_sync(&mut v.hl, doc, h);
Ui::ce_hl_upto(&mut v.hl, doc, h, last, &mut toks, &v.long, first);
let bracket = if focused { Ui::ce_bracket(&mut v, doc, h) } else { None };
if frame {
let mut sp = Sp { value: NAT_W, value2: m.lh, flags: 1, ..Sp::default() };
let _ = st.textbox_metrics(self, &mut sp);
sp.place(r);
sp.ev = ev;
sp.value = focused as i32;
sp.value2 = disabled as i32;
st.textbox_frame(self, &sp);
}
self.clip_push(inner[0], inner[1], inner[2], inner[3]);
self.draw_color(theme.bg);
self.draw_rect(inner[0], ty, inner[2], inner[1] + inner[3] - ty);
if gw > 0 {
self.draw_color(theme.gutter_bg);
self.draw_rect(inner[0], ty, gw, inner[1] + inner[3] - ty);
}
self.ltr_begin(inner[0], inner[2]);
let copy = total_h > th || hbar;
if copy {
self.scroll_copy_begin(id, (tx - v.sx) as i64, (ty - v.sy) as i64);
}
let (sa, sb) = doc.selection().range();
let sel_col = if focused { theme.sel } else { theme.sel_inactive };
let ital = |k: u8| theme.italic >> k & 1 != 0;
let bold = |k: u8| theme.bold >> k & 1 != 0;
let col0 = (v.sx / m.cw.max(1)) as usize;
let cols = (tw_px / m.cw.max(1)) as usize + 2;
let tx0 = tx - v.sx;
let vis: Vec<(usize, usize)> = (first..last)
.map(|l| {
let len = doc.line(l).len();
if len < COL_LONG {
(0, len)
} else {
(v.long.floor(doc, l, col0.saturating_sub(4), tw).0, v.long.byte(doc, l, col0 + cols + 4, tw))
}
})
.collect();
let exec = doc.exec_line();
let numx = inner[0] + bpw + mk + numw - self.dp(10);
for (k, &l) in rows.iter().enumerate() {
let y = row_y(k);
if nums {
let num = format!("{}", l + 1);
let nw = num.len() as i32 * m.cw;
let c = if l == caret.line { theme.gutter_cur } else { theme.gutter_fg };
self.ce_cells(&m, numx - nw, y + m.base, num.as_bytes(), c, false, false);
}
if bpw > 0 {
if let Some(b) = doc.breakpoint(l) {
self.ce_breakpoint(b, inner[0], y, bpw, m.lh, &theme);
}
if exec == Some(l) {
self.ce_exec_mark(inner[0], y, bpw, m.lh, &theme);
}
}
if fw > 0 {
if let Some((_, folded)) = doc.fold_at(l) {
let px = (fw * 3 / 4).max(8);
self.draw_color(if l == caret.line { theme.gutter_cur } else { theme.gutter_fg });
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);
}
}
}
let dvis: Vec<usize> = doc.mark_range(first, last).filter(|&i| last_line(&doc.marks()[i]) >= first).collect();
if mk > 0 {
let mut best: Vec<(usize, u8)> = Vec::new();
for &i in &dvis {
let d = &doc.marks()[i];
match best.last_mut() {
Some(b) if b.0 == d.a.line => b.1 = b.1.min(d.sev),
_ if d.a.line >= first => best.push((d.a.line, d.sev)),
_ => {}
}
}
for (l, sev) in best {
if doc.is_hidden(l) {
continue;
}
let y = ty + doc.row_of_line(l) as i32 * m.lh - v.sy;
self.ce_marker(sev, inner[0] + bpw, y, mk, m.lh, theme.diag[sev as usize]);
}
}
let mut waves: Vec<(u8, i32, i32, i32)> = Vec::new();
for &i in &dvis {
let d = &doc.marks()[i];
for l in d.a.line.max(first)..=d.b.line.min(last.max(1) - 1) {
let (b0, b1) = vis[l - first];
let (a, b) = (if d.a.line == l { d.a.byte } else { 0 }, if d.b.line == l { d.b.byte } else { usize::MAX });
if !touches(d, l) || b < b0 || a > b1 || doc.is_hidden(l) {
continue;
}
let (c0, c1) = mark_cols(d, l, doc.line(l).len(), |b| v.long.col(doc, l, b, tw));
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));
}
}
let mut hov: Vec<usize> = Vec::new();
let pointer_line = doc.line_of_row(((my - ty + v.sy).max(0) / m.lh.max(1)) as usize);
if ev & EV_HOVER != 0 && v.drag == 0 && !dvis.is_empty() && my >= ty && my < ty + th {
let l = pointer_line;
let col = ((mx - tx0).max(0) / m.cw.max(1)) as usize;
let on_mark = mk > 0 && mx >= inner[0] + bpw && mx < inner[0] + bpw + mk;
let on_text = mx >= tx && mx < tx + tw_px;
hov = dvis
.iter()
.copied()
.filter(|&i| {
let d = &doc.marks()[i];
if on_mark {
d.a.line == l
} else {
on_text && touches(d, l) && {
let (c0, c1) = mark_cols(d, l, doc.line(l).len(), |b| v.long.col(doc, l, b, tw));
col >= c0 && col < c1
}
}
})
.collect();
hov.sort_by_key(|&i| doc.marks()[i].sev);
}
let area = [tx - self.dp(6), ty, inner[0] + inner[2] - tx + self.dp(6), th];
self.clip_push(area[0], area[1], area[2], area[3]);
for (ri, &l) in rows.iter().enumerate() {
let y = row_y(ri);
let t = doc.line(l);
if exec == Some(l) {
self.draw_color(theme.exec_line);
self.draw_rect(area[0], y, area[2], m.lh);
if theme.exec_line == theme.bg {
self.draw_color(theme.exec_mark);
self.draw_frame(area[0], y, area[2], m.lh, 0, hair);
}
} else if l == caret.line && flags & CE_NO_LINE == 0 && (focused || v.find.open) && sa == sb {
self.draw_color(theme.line_hl);
self.draw_rect(area[0], y, area[2], m.lh);
}
let (ls, le) = (TextPos::new(l, 0), TextPos::new(l, t.len()));
let col_of = |b: usize| v.long.col(doc, l, b, tw) as i32 * m.cw;
if sa < sb && sa <= le && sb >= ls {
let (lo, hi) = (ls.max(sa), le.min(sb));
let x0 = col_of(lo.byte);
let x1 = col_of(hi.byte) + if sb > le { m.cw / 2 } else { 0 };
if x1 > x0 {
self.draw_color(sel_col);
self.draw_rect(tx0 + x0, y, x1 - x0, m.lh);
}
}
let (b0, b1) = vis[l - first];
let ms = if v.find.open { &v.find.matches[..] } else { &[] };
let k0 = ms.partition_point(|x| x.1 <= TextPos::new(l, b0));
let k1 = ms.partition_point(|x| x.0 < TextPos::new(l, b1)).max(k0);
for (k, &(a, b)) in ms[k0..k1].iter().enumerate() {
let cur = Some(k0 + k) == v.find.current;
let x0 = col_of(a.byte);
let x1 = col_of(b.byte);
self.draw_color(if cur { theme.find_cur } else { theme.find });
if theme.find_frame {
self.draw_frame(tx0 + x0, y, (x1 - x0).max(m.cw / 2), m.lh, 0, if cur { hair * 2 } else { hair });
} else {
self.draw_rect(tx0 + x0, y, (x1 - x0).max(m.cw / 2), m.lh);
}
}
if let Some((p, q)) = bracket {
for at in [p, q] {
if at.line == l {
let x0 = col_of(at.byte);
self.draw_color(theme.bracket);
self.draw_frame(tx0 + x0, y, m.cw, m.lh, 0, hair);
}
}
}
let long = t.len() >= COL_LONG;
let (mut i, mut col) = if long { v.long.floor(doc, l, col0, tw) } else { (0, 0) };
let kept;
let tk: &[Token] = if long {
kept = v.long.tokens(doc, l, tw, h, v.hl.states[l]).1;
&kept
} else {
h.line(t, v.hl.states[l], &mut toks);
&toks
};
let mut k = if long { tk.partition_point(|x| x.end <= i) } else { 0 };
let mut at = i;
let tb = t.as_bytes();
loop {
let (a, b, kind) = match tk.get(k) {
Some(x) if x.start > at => (at, x.start, SY_TEXT),
Some(x) => {
k += 1;
(x.start, x.end, x.kind)
}
None if at < t.len() => (at, t.len(), SY_TEXT),
None => break,
};
at = b;
if col > col0 + cols {
break;
}
let inside = sa < sb && TextPos::new(l, a) >= sa && TextPos::new(l, b) <= sb;
let color = if theme.sel_text != 0 && inside { theme.sel_text } else { theme.tokens[kind as usize % SY_COUNT] };
i = i.max(a);
while i < b && col <= col0 + cols {
if tb[i] == b'\t' {
col += tw - col % tw;
i += 1;
continue;
}
if tb[i] < 0x80 && !m.per_cell {
let lim = b.min(i + (col0 + cols + 1 - col));
let mut j = i;
while j < lim && tb[j] < 0x80 && tb[j] != b'\t' {
j += 1;
}
let s0 = i + col0.saturating_sub(col).min(j - i);
let c0 = col + (s0 - i);
let s1 = (s0 + (col0 + cols).saturating_sub(c0)).min(j);
if s1 > s0 && tb[s0..s1].iter().any(|&c| c != b' ') {
self.ce_cells(&m, tx0 + c0 as i32 * m.cw, y + m.base, &tb[s0..s1], color, bold(kind), ital(kind));
}
col += j - i;
i = j;
continue;
}
let e = segment::grapheme_next(t, i).min(b.max(i + 1));
let ch = t[i..].chars().next().unwrap_or(' ');
let w = char_cols(ch);
if col + w > col0 && col < col0 + cols && ch != ' ' {
self.ce_cells(&m, tx0 + col as i32 * m.cw, y + m.base, &tb[i..e], color, bold(kind), ital(kind));
}
col += w;
i = e;
}
}
if doc.fold_at(l).is_some_and(|f| f.1) {
let end = v.long.col(doc, l, t.len(), tw) as i32 * m.cw;
let (bx, bw) = (tx0 + end + m.cw / 2, m.cw * 3);
self.draw_color(theme.line_hl);
self.draw_rrect(bx, y + hair, bw, m.lh - hair * 2, self.dp(3));
self.draw_color(theme.gutter_fg);
self.draw_frame(bx, y + hair, bw, m.lh - hair * 2, self.dp(3), hair);
self.ce_cells(&m, bx, y + m.base, b"...", theme.gutter_fg, false, false);
}
}
waves.sort_by_key(|w| core::cmp::Reverse(w.0));
let (lim0, lim1) = (tx - m.cw * 2, tx + tw_px + m.cw * 2);
let period = self.dp(2).max(2) * 2;
for (sev, x0, x1, y) in waves {
let x0 = if x0 < lim0 { x0 + (lim0 - x0) / period * period } else { x0 };
let x1 = x1.min(lim1);
if x1 > x0 {
self.ce_wave(x0, x1, y, &m, theme.diag[sev as usize]);
}
}
self.clip_pop();
let cx_px = tx0 + ccol * m.cw;
let cy_px = ty + doc.row_of_line(caret.line) as i32 * m.lh - v.sy;
if focused {
let pe = self.ed_preedit();
if !pe.text.is_empty() {
self.ed_preedit_draw(&pe, cx_px, cy_px, m.lh, cy_px + m.base, (m.font, m.size), theme.fg, theme.bg);
} 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 {
self.draw_color(theme.caret);
self.draw_rect(cx_px, cy_px, self.dp(2).max(1), m.lh);
}
self.ime_pos(cx_px, cy_px, m.lh);
}
if copy {
self.scroll_copy_end();
}
self.ltr_end();
self.clip_pop();
if total_h > th {
let mut pos = v.sy;
self.next_rect_px(inner[0] + inner[2] - sbw, ty, sbw, th);
if self.scrollbar(id.wrapping_add(1), &mut pos, total_h, th, true) & EV_ACT != 0 {
v.sy = pos.clamp(0, (total_h - th).max(0));
}
}
if hbar {
let mut pos = v.sx;
self.next_rect_px(tx, ty + th, tw_px, sbw);
if self.scrollbar(id.wrapping_add(2), &mut pos, content_w, tw_px, false) & EV_ACT != 0 {
v.sx = pos.clamp(0, (content_w - tw_px).max(0));
}
}
self.pan_view(id, hit0, super::touch::AX_ALL, [v.sx, v.sy], [(content_w - tw_px).max(0), (total_h - th).max(0)]);
if v.find.open {
self.ce_find_bar(id, doc, &mut v, [inner[0], inner[1], inner[2], bar_h], ro);
}
let big = v.max_cols.3 > LABEL_MAX;
let (l0, l1) = if big { (first, last) } else { (0, n) };
if v.label.0 != doc.uid() || v.label.1 != doc.revision() || v.label.2 != l0 || v.label.3 != l1 {
let mut s = String::new();
for (k, l) in doc.lines()[l0..l1].iter().enumerate() {
if k > 0 {
s.push('\n');
}
if s.len() + l.len() > LABEL_MAX {
break;
}
s.push_str(l);
}
v.label = (doc.uid(), doc.revision(), l0, l1, s);
}
let rel = |p: TextPos| -> usize {
if p.line < l0 || p.line >= l1 {
0
} else {
doc.lines()[l0..p.line].iter().map(|l| l.len() + 1).sum::<usize>() + p.byte
}
};
let (fa, fc) = if big { (rel(doc.selection().anchor), rel(doc.selection().caret)) } else { (doc.flat(doc.selection().anchor), doc.flat(doc.selection().caret)) };
self.wp.ctx.last_rect = r;
self.wp.ctx.last_state = if focused { SS_FOCUSED as u32 } else { 0 } | if disabled { SS_DISABLED as u32 } else { 0 };
let label = core::mem::take(&mut v.label.4);
{
let kind = (TB_MULTI | if ro { TB_READONLY } else { 0 }) >> 24;
let s = self.sem(K_TEXTBOX, id, Some(&label), if focused { fc as i32 } else { 0 });
s.max = if focused { fa as i32 } else { 0 };
s.min = kind as i32;
}
v.label.4 = label;
let mut hover_text: Option<String> = None;
let mut word_key = 0u64;
if let Some(a) = assist.as_deref_mut() {
if ev & EV_HOVER != 0 && v.drag == 0 && mx >= tx && mx < tx + tw_px && my >= ty && my < ty + th {
let l = pointer_line;
let col = ((mx - tx0).max(0) / m.cw.max(1)) as usize;
let b = v.long.floor(doc, l, col, tw).0;
let t = doc.line(l);
if t[b.min(t.len())..].chars().next().is_some_and(is_word) {
let (wa, _) = word_around(t, b);
let key = (doc.uid(), doc.revision(), l, wa);
if v.hov.key != key {
v.hov.key = key;
v.hov.text = a.hover(doc, TextPos::new(l, b));
}
hover_text = v.hov.text.clone();
word_key = ((l as u64 + 1) << 24) ^ (wa as u64 + 1);
}
}
}
if ev & EV_HOVER != 0 && (!dvis.is_empty() || assist.is_some()) {
let mut text = if hov.is_empty() { String::new() } else { diag_text(doc, &hov) };
if let Some(s) = hover_text.as_deref().filter(|s| !s.is_empty()) {
if !text.is_empty() {
text.push_str("\n\n");
}
text.push_str(&wrap(s, 72));
}
if text.is_empty() {
self.tooltip_part("", 0);
} else {
let dpart = if hov.is_empty() { 0 } else { 1 + hov[0] as u64 + if mx < tx { 1 << 40 } else { 0 } };
let th = text.bytes().fold(0xCBF2_9CE4_8422_2325u64, |h, b| (h ^ b as u64).wrapping_mul(0x100_0000_01B3));
let part = dpart.wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ word_key.wrapping_mul(0xC2B2_AE3D_27D4_EB4F) ^ th;
self.tooltip_part(&text, part.max(1));
}
}
let at = doc.marks_at(caret);
v.caret_diag = at.first().map(|&i| doc.diag_of(&doc.marks()[i]));
if !at.is_empty() {
let state = self.wp.ctx.last_state;
let cy = (ty + doc.row_of_line(caret.line) as i32 * m.lh - v.sy).clamp(ty, (ty + th - m.lh).max(ty));
self.wp.ctx.last_rect = [inner[0], cy, inner[2], m.lh];
self.wp.ctx.last_state = 0;
self.sem(K_TEXT, 0, Some(&diag_text(doc, &at)), doc.marks()[at[0]].sev as i32);
self.wp.ctx.last_rect = r;
self.wp.ctx.last_state = state;
}
if focused {
self.ed_hold(id, fa, fc);
v.held = Some((fa, fc));
out |= EV_FOCUS;
}
if v.hl.count > 0 || doc.revision() != rev0 || doc.selection() != sel0 || (v.sx, v.sy) != view0 {
v.hl_last = v.hl.count;
}
v.status = CodeStatus {
line: caret.line + 1,
col: ccol as usize + 1,
matches: if v.find.open { v.find.matches.len() } else { 0 },
current: if v.find.open { v.find.current.map_or(0, |c| c + 1) } else { 0 },
hl_lines: v.hl_last,
lines: n,
diags: doc.diagnostic_counts(),
diag: at.first().map(|&i| doc.marks()[i].sev),
popup: if v.pop.open { v.pop.shown.len() } else { 0 },
popup_sel: v.pop.sel,
sig: v.sig.open,
hidden: n - doc.visible_lines(),
};
if focused {
let cy = ty + doc.row_of_line(caret.line) as i32 * m.lh - v.sy;
let in_view = cy + m.lh > ty && cy < ty + th;
let pf = PopFont { font: m.font, size: m.size, cw: m.cw, lh: m.lh, base: m.base };
if v.pop.open && in_view {
let scol = v.long.col(doc, v.pop.start.line, v.pop.start.byte, tw) as i32;
if self.ca_complete_popup(id.wrapping_add(14), &mut v.pop, doc, tx0 + scol * m.cw, cy + m.lh, cy, &pf, &theme) {
out |= EV_ACT;
v.max_cols = line_sizes(doc);
self.repass();
}
}
if let Some(sh) = v.sig.help.as_ref().filter(|_| v.sig.open && in_view) {
self.ca_signature_popup(sh, cx_px - m.cw * 2, cy, cy + m.lh, &pf, &theme);
}
}
self.m.code.views.insert(id, v);
let now = self.frame;
if self.m.code.views.len() > 8 {
self.m.code.views.retain(|_, v| now.wrapping_sub(v.seen) < 600);
}
self.wp.ctx.last_rect = r;
out
}
#[allow(clippy::too_many_arguments)]
fn ce_cells(&mut self, m: &Metrics, x: i32, y: i32, s: &[u8], c: u32, bold: bool, italic: bool) {
let font = if italic { m.italic } else { m.font };
let passes = if bold { 2 } else { 1 };
for p in 0..passes {
let dx = p * ((m.size >> 8) / 16 + 1);
if m.per_cell {
let mut i = 0;
let mut k = 0;
while i < s.len() {
let n = match s[i] {
0..=0x7F => 1,
0x80..=0xDF => 2,
0xE0..=0xEF => 3,
_ => 4,
}
.min(s.len() - i);
if s[i] != b' ' {
self.cmd_text(x + k * m.cw + dx, y, &s[i..i + n], font, m.size, c, 0);
}
i += n;
k += 1;
}
} else {
self.cmd_text(x + dx, y, s, font, m.size, c, 0);
}
}
}
fn ce_wave(&mut self, x0: i32, x1: i32, y: i32, m: &Metrics, color: u32) {
let hp = self.dp(2).max(2);
let amp = self.dp(2).max(2);
let width = self.dp(1).max(1) << 8;
let top = (y + m.base + self.dp(1)).min(y + m.lh - amp - 1);
let key = (hp as u32) << 16 | (amp as u32) << 8 | (width >> 8) as u32;
let w = &mut self.m.code.waves;
let d = match w.iter().find(|e| e.0 == color && e.1 == key) {
Some(e) => e.2.clone(),
None => {
if w.len() >= 24 {
w.clear();
}
let d: DrawRef = Rc::new(Wave { color, hp, amp, width });
w.push((color, key, d.clone()));
d
}
};
let pad = (width >> 9) + 1;
self.cmd_custom(x0 - pad, top - pad, x1 - x0 + pad * 2, amp + pad * 2, Some(d), key as usize, color);
}
fn ce_marker(&mut self, sev: u8, x: i32, y: i32, cell: i32, lh: i32, color: u32) {
let cp = match sev {
DIAG_ERROR => IC_ERROR_FILL,
DIAG_WARNING => IC_WARNING,
_ => IC_INFO,
};
let px = (lh * 2 / 3).max(8);
self.draw_color(color);
self.draw_icon(cp, x + (cell - px) / 2, y + (lh - px) / 2, px << 8);
}
fn ce_breakpoint(&mut self, kind: u8, x: i32, y: i32, cell: i32, lh: i32, t: &CodeTheme) {
let r = (lh * 3 / 10).max(3);
let (cx, cy) = ((x + cell / 2) << 8, (y + lh / 2) << 8);
self.draw_color(t.breakpoint);
match kind {
BP_DISABLED => self.draw_circle(cx, cy, (r << 8) - 128, (self.dp(2).max(1) << 8) - 64),
_ => self.draw_circle(cx, cy, r << 8, 0),
}
if kind == BP_COND {
self.draw_color(t.bg);
let bh = self.dp(2).max(2);
self.draw_rect(x + cell / 2 - r + r / 3, y + lh / 2 - bh / 2, (r - r / 3) * 2, bh);
}
}
fn ce_exec_mark(&mut self, x: i32, y: i32, cell: i32, lh: i32, t: &CodeTheme) {
let px = (lh * 3 / 4).max(8);
self.draw_color(t.exec_mark);
self.draw_icon(IC_CARET_RIGHT, x + (cell - px) / 2, y + (lh - px) / 2, px << 8);
}
fn ce_assist_key(doc: &mut CodeDoc, v: &mut View, assist: Option<&mut (dyn CodeAssist + '_)>, k: u32, ro: bool, page: i32) -> bool {
let vk = k & 0xFFFF;
let mods = k >> 16;
if mods & MOD_ALTGR != 0 || mods & (MOD_CTRL | MOD_ALT) == MOD_CTRL | MOD_ALT {
return false;
}
let ctrl = mods & MOD_CTRL != 0;
let shift = mods & MOD_SHIFT != 0;
let alt = mods & MOD_ALT != 0;
let caret = doc.selection().caret;
if v.pop.open && !ctrl && !alt {
let page = (POP_ROWS as isize - 1).min(page.max(1) as isize);
match vk {
VK_UP if !shift => v.pop.step(-1),
VK_DOWN if !shift => v.pop.step(1),
VK_PRIOR if !shift => v.pop.step(-page),
VK_NEXT if !shift => v.pop.step(page),
VK_RETURN if !shift && v.pop.typed_whole() => {
v.pop.close();
return false;
}
VK_RETURN | VK_TAB if !shift && !ro => {
v.pop.accept(doc);
v.prefx = -1;
}
VK_ESCAPE if !shift => {
v.pop.close();
let (a, _) = word_around(doc.line(caret.line), caret.byte);
v.pop.quiet = Some((caret.line, a));
}
_ => return false,
}
return true;
}
if v.sig.open && vk == VK_ESCAPE && mods == 0 {
v.sig.open = false;
return true;
}
match vk {
VK_SPACE if ctrl && !alt => {
let Some(a) = assist else { return false };
v.show_caret = true;
if shift {
v.sig.key = (doc.revision(), caret);
v.sig.help = a.signature(doc, caret);
v.sig.open = v.sig.help.is_some();
} else if !ro {
v.pop.request(doc, a, true);
}
true
}
VK_F12 if mods == 0 => {
v.events.push(CodeEv::Definition(caret));
true
}
VK_F2 if mods == 0 && !ro => {
v.events.push(CodeEv::Rename(caret));
true
}
0x46 if shift && alt && !ctrl && !ro => {
v.events.push(CodeEv::Format);
true
}
VK_OEM_4 | VK_OEM_6 if ctrl && shift && !alt => {
let l = doc.clamp(caret).line;
if vk == VK_OEM_4 {
let r = doc.fold_ranges().into_iter().filter(|r| r.0 <= l && l <= r.1 && !r.2).max_by_key(|r| r.0);
if let Some(r) = r {
doc.set_folded(r.0, true);
doc.set_selection(TextSel::at(TextPos::new(r.0, doc.line(r.0).len())));
}
} else if !doc.set_folded(l, false) {
let r = doc.fold_ranges().into_iter().filter(|r| r.0 <= l && l <= r.1 && r.2).max_by_key(|r| r.0);
if let Some(r) = r {
doc.set_folded(r.0, false);
}
}
true
}
_ => false,
}
}
fn ce_assist_typed(doc: &mut CodeDoc, v: &mut View, a: &mut (dyn CodeAssist + '_), s: &str) {
let Some(c) = s.chars().next_back() else { return };
let caret = doc.selection().caret;
if v.pop.open {
if !is_word(c) {
v.pop.close();
if a.triggers().contains(c) {
v.pop.request(doc, a, false);
}
}
} else if a.triggers().contains(c) {
v.pop.request(doc, a, false);
} else if is_word(c) {
let (w0, _) = word_around(doc.line(caret.line), caret.byte);
if v.pop.quiet != Some((caret.line, w0)) {
v.pop.request(doc, a, false);
}
}
if c == '(' || c == ',' || (c == ')' && v.sig.open) {
v.sig.key = (doc.revision(), caret);
v.sig.help = a.signature(doc, caret);
v.sig.open = v.sig.help.is_some();
}
}
#[allow(clippy::too_many_arguments)]
fn ce_key(&mut self, id: u32, doc: &mut CodeDoc, v: &mut View, h: &Highlighter, k: u32, flags: u32, m: &Metrics) -> bool {
let ro = flags & CE_READONLY != 0;
let vk = k & 0xFFFF;
let mods = k >> 16;
let altgr = mods & MOD_ALTGR != 0 || mods & (MOD_CTRL | MOD_ALT) == MOD_CTRL | MOD_ALT;
if altgr {
return false;
}
let ctrl = mods & MOD_CTRL != 0;
let shift = mods & MOD_SHIFT != 0;
let alt = mods & MOD_ALT != 0;
let sel = doc.selection();
if alt {
return match vk {
VK_UP | VK_DOWN if !ro && !ctrl && !shift => {
doc.move_lines(if vk == VK_UP { -1 } else { 1 });
true
}
_ => false,
};
}
if ctrl {
return match (vk, shift) {
(0x46, false) | (0x48, false) if flags & CE_NO_FIND == 0 => {
let q = if sel.is_empty() || sel.start().line != sel.end().line { v.find.query.clone() } else { doc.selected_text() };
v.find.open = true;
v.find.replace = vk == 0x48 && !ro;
v.find.query = q;
v.find.from = sel.start();
v.find.key.2 = u64::MAX;
self.focus(id.wrapping_add(3));
true
}
(VK_OEM_2, false) | (VK_DIVIDE, false) => {
if !ro {
doc.toggle_comment(h.grammar().comment);
}
true
}
(VK_UP, false) | (VK_DOWN, false) => {
v.sy += if vk == VK_UP { -m.lh } else { m.lh };
v.sy = v.sy.max(0);
true
}
_ => false,
};
}
match vk {
VK_TAB if !ro => {
if shift {
doc.back_tab();
} else {
doc.tab();
}
true
}
VK_F3 => {
self.ce_find_step(doc, v, shift);
true
}
VK_F8 if doc.diagnostic_count() > 0 => {
if let Some(p) = doc.next_diagnostic(sel.caret, shift) {
doc.set_selection(TextSel::at(p));
}
v.prefx = -1;
true
}
VK_HOME => {
let t = doc.line(sel.caret.line);
let ind = t.len() - t.trim_start_matches([' ', '\t']).len();
let to = if sel.caret.byte == ind { 0 } else { ind };
let p = TextPos::new(sel.caret.line, to);
doc.set_selection(if shift { TextSel::new(sel.anchor, p) } else { TextSel::at(p) });
v.prefx = -1;
true
}
VK_ESCAPE => {
if v.find.open {
v.find.open = false;
} else {
self.wp.ctx.focus = 0;
}
self.repass();
true
}
_ => false,
}
}
fn ce_copy(&mut self, doc: &mut CodeDoc, cut: bool, ro: bool) {
let sel = doc.selection();
if sel.is_empty() {
let l = sel.caret.line;
let mut s = String::from(doc.line(l));
s.push('\n');
self.clip_set(CLIP_TEXT, s.as_bytes());
if cut && !ro {
let (a, b) = if l + 1 < doc.line_count() {
(TextPos::new(l, 0), TextPos::new(l + 1, 0))
} else if l > 0 {
(TextPos::new(l - 1, doc.line(l - 1).len()), TextPos::new(l, doc.line(l).len()))
} else {
(TextPos::new(0, 0), TextPos::new(0, doc.line(0).len()))
};
doc.delete(a, b);
}
return;
}
let s = doc.selected_text();
self.clip_set(CLIP_TEXT, s.as_bytes());
if cut {
doc.delete(sel.start(), sel.end());
}
}
fn ce_find_step(&mut self, doc: &mut CodeDoc, v: &mut View, back: bool) {
if v.find.query.is_empty() {
return;
}
Ui::ce_find_update(&mut v.find, doc);
let sel = doc.selection();
let ms = &v.find.matches;
if ms.is_empty() {
v.find.current = None;
return;
}
let mut i = if back {
let k = ms.partition_point(|x| x.0 < sel.start());
if k == 0 { ms.len() - 1 } else { k - 1 }
} else {
let k = ms.partition_point(|x| x.0 < sel.end() || (x.0 == sel.start() && x.1 == sel.end() && !sel.is_empty()));
if k >= ms.len() { 0 } else { k }
};
let n = ms.len();
for _ in 0..n {
if !doc.is_hidden(ms[i].0.line) {
break;
}
i = if back { (i + n - 1) % n } else { (i + 1) % n };
}
if doc.is_hidden(ms[i].0.line) {
v.find.current = None;
return;
}
v.find.current = Some(i);
doc.set_selection(TextSel::new(ms[i].0, ms[i].1));
v.find.from = ms[i].0;
self.repass();
}
fn ce_find_bar(&mut self, id: u32, doc: &mut CodeDoc, v: &mut View, r: [i32; 4], ro: bool) {
let ids = |k: u32| id.wrapping_add(k);
self.draw_color(self.color(T_LAYER) | 0xFF00_0000);
self.draw_rect(r[0], r[1], r[2], r[3]);
self.draw_color(self.color(T_DIVIDER));
self.draw_rect(r[0], r[1] + r[3] - self.hair(), r[2], self.hair());
let pad = self.dp(6);
let row_h = self.dp(32);
let bw = self.dp(32);
let (fq, fr) = (ids(3), ids(4));
let in_bar = self.wp.ctx.focus == fq || self.wp.ctx.focus == fr;
if in_bar {
if self.ed_key_chord(VK_ESCAPE, 0) {
v.find.open = false;
self.ed_focus_quiet(id);
return;
}
if self.ed_key_chord(VK_F3, 0) {
self.ce_find_step(doc, v, false);
}
if self.ed_key_chord(VK_F3, MOD_SHIFT) {
self.ce_find_step(doc, v, true);
}
}
let shift_enter = self.wp.ctx.focus == fq && self.ed_key_chord(VK_RETURN, MOD_SHIFT);
let x0 = r[0] + pad;
let y0 = r[1] + (self.dp(40) - row_h) / 2;
let count_w = self.dp(90);
let fw = (r[2] - pad * 3 - bw * 5 - count_w).max(self.dp(80));
self.next_rect_px(x0, y0, fw, row_h);
let before = v.find.query.clone();
let enter = self.wp.ctx.focus == fq && self.ed_key_chord(VK_RETURN, 0);
self.textbox(fq, Some(tr("Suchen")), &mut v.find.query, 1024);
if v.find.query != before {
Ui::ce_find_update(&mut v.find, doc);
let from = v.find.from;
let ms = &v.find.matches;
let k = ms.partition_point(|x| x.0 < from);
let n = ms.len();
let k = (0..n).map(|j| (k + j) % n).find(|&j| !doc.is_hidden(ms[j].0.line));
v.find.current = k;
if let Some(i) = v.find.current {
doc.set_selection(TextSel::new(ms[i].0, ms[i].1));
}
self.repass();
}
if shift_enter {
self.ce_find_step(doc, v, true);
} else if enter {
self.ce_find_step(doc, v, false);
}
let mut x = x0 + fw + pad;
let tool = |ui: &mut Ui, kid: u32, x: i32, label: &str, tip: &str, on: bool| -> bool {
ui.next_rect_px(x, y0, bw, row_h);
ui.next_flags(ST_SUBTLE | if on { ST_CHECKED } else { 0 });
let e = ui.button(kid, label);
ui.tooltip(tip);
e & EV_ACT != 0
};
if tool(self, ids(5), x, "Aa", tr("Groß-/Kleinschreibung"), v.find.case != 0) {
v.find.case ^= 1;
self.repass();
}
x += bw;
if tool(self, ids(6), x, "W", tr("Ganzes Wort"), v.find.word != 0) {
v.find.word ^= 1;
self.repass();
}
x += bw;
self.next_rect_px(x, y0, bw, row_h);
if self.icon_button(ids(7), IC_CHEV_UP, tr("Vorheriger Treffer")) & EV_ACT != 0 {
self.ce_find_step(doc, v, true);
}
x += bw;
self.next_rect_px(x, y0, bw, row_h);
if self.icon_button(ids(8), IC_CHEV_DOWN, tr("Nächster Treffer")) & EV_ACT != 0 {
self.ce_find_step(doc, v, false);
}
x += bw;
Ui::ce_find_update(&mut v.find, doc);
let sel = doc.selection();
v.find.current = v.find.matches.binary_search_by(|m| m.0.cmp(&sel.start()).then(m.1.cmp(&sel.end()))).ok();
let count = if v.find.query.is_empty() {
String::new()
} else if v.find.matches.is_empty() {
String::from(tr("Keine Treffer"))
} else {
let more = if v.find.matches.len() >= FIND_MAX { "+" } else { "" };
format!("{}/{}{}", v.find.current.map_or(String::from("?"), |c| format!("{}", c + 1)), v.find.matches.len(), more)
};
self.next_rect_px(x + pad / 2, y0, count_w, row_h);
self.ts_font(TS_CAPTION);
let tc = self.color(T_TEXT2);
let (f, s) = self.text.get_font();
let base = self.ts_base(TS_CAPTION);
let lr = self.place(count_w, row_h);
self.cmd_text(lr[0], lr[1] + (row_h - self.ts_line(TS_CAPTION)) / 2 + base, count.as_bytes(), f, s, tc, count_w);
self.wp.ctx.last_state = 0;
self.sem(K_TEXT, 0, Some(&count), 0);
x += count_w;
self.next_rect_px(x, y0, bw, row_h);
if self.icon_button(ids(9), IC_CANCEL, tr("Schließen")) & EV_ACT != 0 {
v.find.open = false;
self.ed_focus_quiet(id);
}
if v.find.replace && !ro {
let y1 = y0 + self.dp(36);
self.next_rect_px(x0, y1, fw, row_h);
let renter = self.wp.ctx.focus == fr && self.ed_key_chord(VK_RETURN, 0);
self.textbox(fr, Some(tr("Ersetzen")), &mut v.find.rep, 1024);
let mut xr = x0 + fw + pad;
let q = v.find.query.clone();
let rep = v.find.rep.clone();
let opts = v.find.opts();
self.next_rect_px(xr, y1, self.dp(96), row_h);
if self.button(ids(10), tr("Ersetzen")) & EV_ACT != 0 || renter {
doc.replace_next(&q, &rep, opts);
self.repass();
}
xr += self.dp(100);
self.next_rect_px(xr, y1, self.dp(110), row_h);
if self.button(ids(11), tr("Alle ersetzen")) & EV_ACT != 0 {
doc.replace_all(&q, &rep, opts);
}
}
}
}
fn skip_hidden(view: &CView, c0: TextPos, c: TextPos, vk: u32, prefx: i32) -> TextPos {
let doc = view.doc;
if !doc.is_hidden(c.line) {
return c;
}
let l = doc.visible_line(c.line, c > c0);
match vk {
VK_LEFT => TextPos::new(l, doc.line(l).len()),
VK_RIGHT => TextPos::new(l, 0),
_ => TextPos::new(l, view.hit_x(l, 0, prefx.max(0))),
}
}
fn soft_back(doc: &CodeDoc, a: TextPos, b: TextPos) -> (TextPos, TextPos) {
if a.line != b.line || b.byte == 0 {
return (a, b);
}
let t = doc.line(b.line);
if !t[..b.byte].bytes().all(|c| c == b' ') {
return (a, b);
}
let tw = doc.tab_width.clamp(1, 16) as usize;
let col = b.byte;
let target = (col - 1) / tw * tw;
(TextPos::new(b.line, target), b)
}
fn last_line(d: &Mark) -> usize {
if d.b.byte == 0 && d.b.line > d.a.line { d.b.line - 1 } else { d.b.line }
}
fn touches(d: &Mark, l: usize) -> bool {
d.a.line <= l && l <= last_line(d)
}
fn mark_cols(d: &Mark, l: usize, len: usize, col: impl Fn(usize) -> usize) -> (usize, usize) {
let c0 = if d.a.line == l { col(d.a.byte) } else { 0 };
let c1 = if d.b.line == l { col(d.b.byte) } else { col(len) + 1 };
(c0, c1.max(c0 + 1))
}
fn diag_text(doc: &CodeDoc, ix: &[usize]) -> String {
let mut s = String::new();
for (k, &i) in ix.iter().enumerate() {
let d = &doc.marks()[i];
if k > 0 {
s.push_str(" · ");
}
s.push_str(tr(["Fehler", "Warnung", "Hinweis"][d.sev.min(DIAG_INFO) as usize]));
s.push_str(": ");
s.push_str(&d.msg);
}
s
}
fn doc_find_range(doc: &CodeDoc, q: &str, opts: u8, first: usize, n: usize) -> Vec<(TextPos, TextPos)> {
doc.find_lines(q, opts, first, n)
}
fn line_sizes(doc: &CodeDoc) -> (u32, u64, usize, usize) {
let (mut w, mut t) = (0, 0);
for l in doc.lines() {
w = w.max(l.len());
t += l.len() + 1;
}
(doc.uid(), doc.revision(), w + 1, t)
}