1use alloc::format;
39use alloc::string::String;
40use alloc::vec::Vec;
41
42use crate::code_doc::{is_word, CodeDoc};
43use crate::consts::*;
44use crate::ed_core::TextPos;
45use crate::highlight::*;
46use crate::i18n::tr;
47use crate::Ui;
48
49pub const CI_TEXT: u8 = 0;
51pub const CI_KEYWORD: u8 = 1;
52pub const CI_VARIABLE: u8 = 2;
54pub const CI_PARAM: u8 = 3;
55pub const CI_FUNCTION: u8 = 4;
56pub const CI_METHOD: u8 = 5;
57pub const CI_MODULE: u8 = 6;
59pub const CI_CONSTANT: u8 = 7;
60pub const CI_FIELD: u8 = 8;
61pub const CI_VALUE: u8 = 9;
63
64pub const POP_ROWS: usize = 10;
66
67#[derive(Clone, Debug, Default, PartialEq)]
69pub struct CompItem {
70 pub label: String,
71 pub detail: String,
72 pub doc: String,
73 pub kind: u8,
74 pub insert: String,
76}
77
78impl CompItem {
79 pub fn new(label: &str, kind: u8, detail: &str) -> CompItem {
80 CompItem { label: String::from(label), detail: String::from(detail), doc: String::new(), kind, insert: String::new() }
81 }
82}
83
84#[derive(Clone, Debug, Default, PartialEq)]
86pub struct CompList {
87 pub start: TextPos,
88 pub items: Vec<CompItem>,
89}
90
91#[derive(Clone, Debug, Default, PartialEq)]
93pub struct SigHelp {
94 pub label: String,
96 pub params: Vec<(usize, usize)>,
98 pub active: usize,
100 pub doc: String,
101}
102
103pub trait CodeAssist {
105 fn complete(&mut self, doc: &CodeDoc, at: TextPos, explicit: bool) -> Option<CompList> {
107 let _ = (doc, at, explicit);
108 None
109 }
110 fn hover(&mut self, doc: &CodeDoc, at: TextPos) -> Option<String> {
112 let _ = (doc, at);
113 None
114 }
115 fn signature(&mut self, doc: &CodeDoc, at: TextPos) -> Option<SigHelp> {
117 let _ = (doc, at);
118 None
119 }
120 fn triggers(&self) -> &str {
122 "."
123 }
124}
125
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128#[non_exhaustive]
129pub enum CodeEv {
130 Definition(TextPos),
132 Rename(TextPos),
134 Format,
136 Breakpoint(usize),
138}
139
140#[derive(Default)]
144pub(crate) struct Pop {
145 pub(crate) open: bool,
146 pub(crate) start: TextPos,
147 pub(crate) items: Vec<CompItem>,
148 pub(crate) shown: Vec<usize>,
150 pub(crate) sel: usize,
151 pub(crate) top: usize,
152 pub(crate) typed: Option<String>,
154 pub(crate) quiet: Option<(usize, usize)>,
156}
157
158impl Pop {
159 pub(crate) fn close(&mut self) {
160 self.open = false;
161 self.items.clear();
162 self.shown.clear();
163 self.typed = None;
164 }
165
166 pub(crate) fn request(&mut self, doc: &CodeDoc, a: &mut (dyn CodeAssist + '_), explicit: bool) {
169 let caret = doc.selection().caret;
170 match a.complete(doc, caret, explicit) {
171 Some(l) if !l.items.is_empty() && l.start.line == caret.line && l.start.byte <= caret.byte => {
172 self.open = true;
173 self.start = l.start;
174 self.items = l.items;
175 self.typed = None;
176 self.quiet = None;
177 if !self.filter(doc) {
178 self.quiet = Some((caret.line, l.start.byte));
179 }
180 }
181 _ => {
182 self.close();
183 let (a, _) = word_around(doc.line(caret.line), caret.byte);
184 self.quiet = Some((caret.line, a));
185 }
186 }
187 }
188
189 pub(crate) fn filter(&mut self, doc: &CodeDoc) -> bool {
192 if !self.open {
193 return false;
194 }
195 let c = doc.selection().caret;
196 let t = doc.line(c.line);
197 if c.line != self.start.line || c.byte < self.start.byte || !doc.selection().is_empty() || self.start.byte > t.len() {
198 self.close();
199 return false;
200 }
201 let typed = &t[self.start.byte..c.byte];
202 if !typed.chars().all(is_word) {
203 self.close();
204 return false;
205 }
206 if self.typed.as_deref() == Some(typed) {
207 return true;
208 }
209 let low = typed.to_lowercase();
210 let mut exact: Vec<usize> = Vec::new();
211 let mut other: Vec<usize> = Vec::new();
212 for (i, it) in self.items.iter().enumerate() {
213 if it.label.starts_with(typed) {
214 exact.push(i);
215 } else if it.label.to_lowercase().starts_with(&low) {
216 other.push(i);
217 }
218 }
219 exact.extend(other);
220 self.shown = exact;
221 if self.shown.is_empty() {
222 self.close();
223 return false;
224 }
225 self.typed = Some(String::from(typed));
227 self.sel = 0;
228 self.top = 0;
229 true
230 }
231
232 pub(crate) fn scroll_to_sel(&mut self) {
233 if self.sel < self.top {
234 self.top = self.sel;
235 }
236 if self.sel >= self.top + POP_ROWS {
237 self.top = self.sel + 1 - POP_ROWS;
238 }
239 self.top = self.top.min(self.shown.len().saturating_sub(POP_ROWS));
240 }
241
242 pub(crate) fn typed_whole(&self) -> bool {
244 let it = self.shown.get(self.sel).and_then(|&i| self.items.get(i));
245 match (it, self.typed.as_deref()) {
246 (Some(it), Some(t)) => t == if it.insert.is_empty() { it.label.as_str() } else { it.insert.as_str() },
247 _ => false,
248 }
249 }
250
251 pub(crate) fn step(&mut self, delta: isize) {
253 let n = self.shown.len() as isize;
254 if n == 0 {
255 return;
256 }
257 self.sel = (self.sel as isize + delta).clamp(0, n - 1) as usize;
258 self.scroll_to_sel();
259 }
260
261 pub(crate) fn accept(&mut self, doc: &mut CodeDoc) {
263 let Some(it) = self.shown.get(self.sel).and_then(|&i| self.items.get(i)) else { return self.close() };
264 let text = if it.insert.is_empty() { it.label.clone() } else { it.insert.clone() };
265 let c = doc.selection().caret;
266 let t = doc.line(c.line);
267 let end = c.byte + t[c.byte.min(t.len())..].chars().take_while(|&ch| is_word(ch)).map(char::len_utf8).sum::<usize>();
268 let (a, b) = (self.start, TextPos::new(c.line, end));
269 let _ = doc.replace_kind(a, b, &text, crate::code_doc::CK_NONE, tr("Vervollständigen"));
270 self.close();
271 }
272}
273
274#[derive(Default)]
276pub(crate) struct Sig {
277 pub(crate) open: bool,
278 pub(crate) help: Option<SigHelp>,
279 pub(crate) key: (u64, TextPos),
280}
281
282#[derive(Default)]
284pub(crate) struct Hov {
285 pub(crate) key: (u32, u64, usize, usize),
286 pub(crate) text: Option<String>,
287}
288
289pub(crate) fn word_around(t: &str, b: usize) -> (usize, usize) {
291 let b = b.min(t.len());
292 let a = t[..b].char_indices().rev().take_while(|(_, c)| is_word(*c)).last().map_or(b, |(i, _)| i);
293 let e = t[b..].char_indices().find(|(_, c)| !is_word(*c)).map_or(t.len(), |(i, _)| b + i);
294 (a, e)
295}
296
297pub(crate) fn wrap(s: &str, max: usize) -> String {
299 let mut out = String::with_capacity(s.len() + 8);
300 for (k, line) in s.split('\n').enumerate() {
301 if k > 0 {
302 out.push('\n');
303 }
304 let mut n = 0;
305 for (j, w) in line.split(' ').enumerate() {
306 let wn = w.chars().count();
307 if j > 0 && n + 1 + wn > max {
308 out.push('\n');
309 n = 0;
310 } else if j > 0 {
311 out.push(' ');
312 n += 1;
313 }
314 out.push_str(w);
315 n += wn;
316 }
317 }
318 out
319}
320
321pub(crate) struct PopFont {
323 pub(crate) font: u32,
324 pub(crate) size: i32,
325 pub(crate) cw: i32,
326 pub(crate) lh: i32,
327 pub(crate) base: i32,
328}
329
330fn badge(kind: u8, t: &CodeTheme) -> (&'static str, u32) {
332 match kind {
333 CI_KEYWORD => ("k", t.tokens[SY_KEYWORD as usize]),
334 CI_VARIABLE => ("v", t.tokens[SY_NUMBER as usize]),
335 CI_PARAM => ("p", t.tokens[SY_NUMBER as usize]),
336 CI_FUNCTION => ("f", t.tokens[SY_FUNC as usize]),
337 CI_METHOD => ("m", t.tokens[SY_FUNC as usize]),
338 CI_MODULE => ("M", t.tokens[SY_TYPE as usize]),
339 CI_CONSTANT => ("c", t.tokens[SY_CONST as usize]),
340 CI_FIELD => ("F", t.tokens[SY_KEY as usize]),
341 CI_VALUE => ("w", t.tokens[SY_KEY as usize]),
342 _ => ("t", t.fg),
343 }
344}
345
346impl Ui {
347 fn ca_layer_begin(&mut self) -> ([i32; 4], u32, [i32; 4], u32) {
349 let (w, h) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
350 let prev = self.layer_set(L_POPUP);
351 let clip = self.wp.ctx.uiclip;
352 self.wp.ctx.uiclip = [0, 0, w, h];
353 ([0, 0, w, h], prev, clip, self.wp.ctx.last_state)
354 }
355
356 fn ca_layer_end(&mut self, prev: u32, clip: [i32; 4], lr: [i32; 4], ls: u32) {
357 self.wp.ctx.uiclip = clip;
358 self.layer_set(prev);
359 self.wp.ctx.last_rect = lr;
360 self.wp.ctx.last_state = ls;
361 }
362
363 pub(crate) fn ca_complete_popup(&mut self, id: u32, pop: &mut Pop, doc: &mut CodeDoc, x: i32, below: i32, above: i32, f: &PopFont, t: &CodeTheme) -> bool {
367 if !pop.open || pop.shown.is_empty() {
368 return false;
369 }
370 let lr = self.wp.ctx.last_rect;
371 let (win, prev, clip, ls) = self.ca_layer_begin();
372 let pad = self.dp(4);
373 let rh = f.lh.max(self.dp(22));
374 let rows = pop.shown.len().min(POP_ROWS);
375 let first = pop.top.min(pop.shown.len().saturating_sub(rows));
376 let lchars = pop.shown.iter().map(|&i| pop.items[i].label.chars().count()).max().unwrap_or(1).clamp(6, 40) as i32;
378 self.ts_font(TS_CAPTION);
379 let (cf, cs) = self.text.get_font();
380 let cap_line = self.ts_line(TS_CAPTION);
381 let cap_base = self.ts_base(TS_CAPTION);
382 let max_d = self.dp(300);
383 let mut dw = 0;
384 for &i in &pop.shown[first..first + rows] {
385 dw = dw.max(self.text.width(&pop.items[i].detail).min(max_d));
386 }
387 let badge_w = rh;
388 let w = (badge_w + pad + lchars * f.cw + if dw > 0 { self.dp(16) + dw } else { 0 } + pad * 2).max(self.dp(180)).min(win[2] - self.dp(8));
389 let docs = pop.shown.get(pop.sel).map(|&i| wrap(&pop.items[i].doc, ((w - pad * 4) / (cap_line / 2).max(1)).max(20) as usize)).unwrap_or_default();
391 let doc_lines = if docs.is_empty() { 0 } else { docs.split('\n').count().min(4) as i32 };
392 let foot = if doc_lines > 0 { doc_lines * cap_line + pad * 2 + self.hair() } else { 0 };
393 let h = rows as i32 * rh + pad * 2 + foot;
394 let mut y = below;
395 if y + h > win[3] && above - h >= 0 {
396 y = above - h;
397 }
398 let x = (x - pad * 2 - badge_w).min(win[2] - w - self.dp(4)).max(self.dp(4));
400 let hair = self.hair();
402 let bg = self.color(T_FLYOUT) | 0xFF00_0000;
403 self.cmd_rrect(x, y, w, h, self.dp(4), hair, bg, self.color(T_FLYOUT_STROKE), self.color(T_FLYOUT_STROKE));
404 let ry0 = y + pad;
405 for (k, &i) in pop.shown[first..first + rows].iter().enumerate() {
406 let it = &pop.items[i];
407 let ry = ry0 + k as i32 * rh;
408 let chosen = first + k == pop.sel;
409 let fg = if chosen && t.sel_text != 0 { t.sel_text } else { t.fg };
410 if chosen {
411 self.draw_color(t.sel);
412 self.draw_rrect(x + pad, ry, w - pad * 2, rh, self.dp(3));
413 }
414 let (letter, bc) = badge(it.kind, t);
415 let bc = if chosen && t.sel_text != 0 { t.sel_text } else { bc };
416 let bs = (rh * 2 / 3).max(8);
417 let (bx, by) = (x + pad + (badge_w - bs) / 2, ry + (rh - bs) / 2);
418 self.draw_color(bc);
419 self.draw_frame(bx, by, bs, bs, self.dp(2), hair);
420 let lw = self.text.width(letter);
421 self.cmd_text(bx + (bs - lw) / 2, by + (bs - cap_line) / 2 + cap_base, letter.as_bytes(), cf, cs, bc, 0);
422 let lx = x + pad + badge_w + pad;
423 let ly = ry + (rh - f.lh) / 2 + f.base;
424 self.cmd_text(lx, ly, it.label.as_bytes(), f.font, f.size, fg, lchars * f.cw);
425 if !it.detail.is_empty() {
426 let dx = x + w - pad * 2 - dw;
427 let dc = if chosen && t.sel_text != 0 { t.sel_text } else { t.gutter_fg };
428 self.cmd_text(dx.max(lx + lchars * f.cw + self.dp(8)), ry + (rh - cap_line) / 2 + cap_base, it.detail.as_bytes(), cf, cs, dc, dw);
429 }
430 }
431 if doc_lines > 0 {
432 let fy = ry0 + rows as i32 * rh + pad;
433 self.draw_color(self.color(T_DIVIDER));
434 self.draw_rect(x + pad, fy, w - pad * 2, hair);
435 for (k, l) in docs.split('\n').take(4).enumerate() {
436 self.cmd_text(x + pad * 2, fy + pad + k as i32 * cap_line + cap_base, l.as_bytes(), cf, cs, t.fg, w - pad * 4);
437 }
438 }
439 self.wp.ctx.last_rect = [x, y, w, h];
441 let ev = self.behave(id, 0);
442 let mut accepted = false;
443 if ev & EV_HOVER != 0 {
444 let wy = self.wp.ctx.wheel_y;
445 if wy != 0 && pop.shown.len() > POP_ROWS {
446 let d = if wy > 0 { -3 } else { 3 };
447 pop.top = (pop.top as isize + d).clamp(0, (pop.shown.len() - POP_ROWS) as isize) as usize;
448 self.wp.ctx.wheel_y = 0;
449 self.repass();
450 }
451 }
452 if ev & EV_ACT != 0 {
453 let my = self.wp.ctx.my;
454 let k = ((my - ry0) / rh.max(1)).clamp(0, rows as i32 - 1) as usize;
455 if my >= ry0 && my < ry0 + rows as i32 * rh {
456 pop.sel = first + k;
457 pop.accept(doc);
458 accepted = true;
459 }
460 }
461 self.wp.ctx.last_rect = [x, y, w, h];
463 self.wp.ctx.last_state = 0;
464 self.sem(K_LIST, id, Some(tr("Vorschläge")), pop.sel as i32);
465 if !accepted {
466 for k in 0..rows {
467 let it = &pop.items[pop.shown[first + k]];
468 let label = if it.detail.is_empty() { it.label.clone() } else { format!("{} – {}", it.label, it.detail) };
469 self.wp.ctx.last_rect = [x + pad, ry0 + k as i32 * rh, w - pad * 2, rh];
470 self.wp.ctx.last_state = if first + k == pop.sel { SS_SELECTED as u32 } else { 0 };
471 self.sem(K_LISTITEM, 0, Some(&label), (first + k) as i32);
472 }
473 if doc_lines > 0 {
474 self.wp.ctx.last_rect = [x, ry0 + rows as i32 * rh, w, foot];
475 self.wp.ctx.last_state = 0;
476 self.sem(K_TEXT, 0, Some(&docs.replace('\n', " ")), 0);
477 }
478 }
479 self.ca_layer_end(prev, clip, lr, ls);
480 accepted
481 }
482
483 pub(crate) fn ca_signature_popup(&mut self, sig: &SigHelp, x: i32, top: i32, foot: i32, f: &PopFont, t: &CodeTheme) {
486 let lr = self.wp.ctx.last_rect;
487 let (win, prev, clip, ls) = self.ca_layer_begin();
488 let pad = self.dp(6);
489 self.ts_font(TS_CAPTION);
490 let (cf, cs) = self.text.get_font();
491 let cap_line = self.ts_line(TS_CAPTION);
492 let cap_base = self.ts_base(TS_CAPTION);
493 let chars = sig.label.chars().count() as i32;
494 let doc = wrap(&sig.doc, 70);
495 let doc_lines = if doc.is_empty() { 0 } else { doc.split('\n').count().min(3) as i32 };
496 let mut dw = 0;
497 for l in doc.split('\n').take(3) {
498 dw = dw.max(self.text.width(l));
499 }
500 let w = (chars * f.cw).max(dw) + pad * 2;
501 let w = w.min(win[2] - self.dp(8));
502 let h = f.lh + pad * 2 + doc_lines * cap_line;
503 let mut y = top - h - self.dp(2);
504 if y < 0 {
505 y = foot + self.dp(2);
506 }
507 let x = x.min(win[2] - w - self.dp(4)).max(self.dp(4));
508 let hair = self.hair();
509 let bg = self.color(T_FLYOUT) | 0xFF00_0000;
510 self.cmd_rrect(x, y, w, h, self.dp(4), hair, bg, self.color(T_FLYOUT_STROKE), self.color(T_FLYOUT_STROKE));
511 let lx = x + pad;
513 let ly = y + pad + f.base;
514 self.cmd_text(lx, ly, sig.label.as_bytes(), f.font, f.size, t.fg, w - pad * 2);
515 let mut said = String::new();
516 if let Some(&(a, b)) = sig.params.get(sig.active) {
517 if b <= sig.label.len() && a < b {
518 let c0 = sig.label[..a].chars().count() as i32;
519 let c1 = sig.label[..b].chars().count() as i32;
520 let px = lx + c0 * f.cw;
521 let part = &sig.label[a..b];
522 let c = t.tokens[SY_FUNC as usize];
523 self.cmd_text(px, ly, part.as_bytes(), f.font, f.size, c, 0);
524 self.cmd_text(px + (f.size >> 8) / 16 + 1, ly, part.as_bytes(), f.font, f.size, c, 0);
525 self.draw_color(c);
526 self.draw_rect(px, ly + self.dp(2), (c1 - c0) * f.cw, hair.max(self.dp(1)));
527 said = String::from(part);
528 }
529 }
530 for (k, l) in doc.split('\n').take(3).enumerate() {
531 self.cmd_text(lx, y + pad + f.lh + k as i32 * cap_line + cap_base, l.as_bytes(), cf, cs, t.gutter_fg, w - pad * 2);
532 }
533 self.wp.ctx.last_rect = [x, y, w, h];
534 self.wp.ctx.last_state = 0;
535 let mut text = sig.label.clone();
536 if !said.is_empty() {
537 text.push_str(" – ");
538 text.push_str(&said);
539 }
540 if !doc.is_empty() {
541 text.push_str(" – ");
542 text.push_str(&doc.replace('\n', " "));
543 }
544 self.sem(K_TEXT, 0, Some(&text), 0);
545 self.ca_layer_end(prev, clip, lr, ls);
546 }
547}