1use super::*;
2use crate::{DEFAULT_CHORD_TIMEOUT_TICKS, RichLogState};
3
4impl Context {
5 pub fn rich_log(&mut self, state: &mut RichLogState) -> Response {
7 let focused = self.register_focusable();
8 let (interaction_id, mut response) = self.begin_widget_interaction(focused);
9
10 let widget_height = if response.rect.height > 0 {
11 response.rect.height as usize
12 } else {
13 self.area_height as usize
14 };
15 let viewport_height = widget_height.saturating_sub(2);
16 let effective_height = if viewport_height == 0 {
17 state.entries.len().max(1)
18 } else {
19 viewport_height
20 };
21 let show_indicator = state.entries.len() > effective_height;
22 let visible_rows = if show_indicator {
23 effective_height.saturating_sub(1).max(1)
24 } else {
25 effective_height
26 };
27 let max_offset = state.entries.len().saturating_sub(visible_rows);
28 if state.auto_scroll && state.scroll_offset == usize::MAX {
29 state.scroll_offset = max_offset;
30 } else {
31 state.scroll_offset = state.scroll_offset.min(max_offset);
32 }
33 let old_offset = state.scroll_offset;
34
35 if focused {
36 let mut consumed_indices = Vec::new();
37 for (i, key) in self.available_key_presses() {
38 match key.code {
39 KeyCode::Up | KeyCode::Char('k') => {
40 state.scroll_offset = state.scroll_offset.saturating_sub(1);
41 consumed_indices.push(i);
42 }
43 KeyCode::Down | KeyCode::Char('j') => {
44 state.scroll_offset = (state.scroll_offset + 1).min(max_offset);
45 consumed_indices.push(i);
46 }
47 KeyCode::PageUp => {
48 state.scroll_offset = state.scroll_offset.saturating_sub(10);
49 consumed_indices.push(i);
50 }
51 KeyCode::PageDown => {
52 state.scroll_offset = (state.scroll_offset + 10).min(max_offset);
53 consumed_indices.push(i);
54 }
55 KeyCode::Home => {
56 state.scroll_offset = 0;
57 consumed_indices.push(i);
58 }
59 KeyCode::End => {
60 state.scroll_offset = max_offset;
61 consumed_indices.push(i);
62 }
63 _ => {}
64 }
65 }
66 self.consume_indices(consumed_indices);
67 }
68
69 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
70 let mut consumed = Vec::new();
71 for (i, mouse) in self.mouse_events_in_rect(rect) {
72 let delta = self.scroll_lines_per_event as usize;
73 match mouse.kind {
74 MouseKind::ScrollUp => {
75 state.scroll_offset = state.scroll_offset.saturating_sub(delta);
76 consumed.push(i);
77 }
78 MouseKind::ScrollDown => {
79 state.scroll_offset = (state.scroll_offset + delta).min(max_offset);
80 consumed.push(i);
81 }
82 _ => {}
83 }
84 }
85 self.consume_indices(consumed);
86 }
87
88 state.scroll_offset = state.scroll_offset.min(max_offset);
89 let start = state
90 .scroll_offset
91 .min(state.entries.len().saturating_sub(visible_rows));
92 let end = (start + visible_rows).min(state.entries.len());
93
94 self.commands
95 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
96 direction: Direction::Column,
97 gap: 0,
98 align: Align::Start,
99 align_self: None,
100 justify: Justify::Start,
101 border: Some(Border::Single),
102 border_sides: BorderSides::all(),
103 border_style: Style::new().fg(self.theme.border),
104 bg_color: None,
105 padding: Padding::default(),
106 margin: Margin::default(),
107 constraints: Constraints::default(),
108 title: None,
109 grow: 0,
110 group_name: None,
111 })));
112
113 for entry in state
114 .entries
115 .iter()
116 .skip(start)
117 .take(end.saturating_sub(start))
118 {
119 self.commands.push(Command::RichText {
120 segments: entry.segments.clone(),
121 wrap: false,
122 align: Align::Start,
123 margin: Margin::default(),
124 constraints: Constraints::default(),
125 });
126 }
127
128 if show_indicator {
129 let end_pos = end.min(state.entries.len());
130 let line = format!(
131 "{}-{} / {}",
132 start.saturating_add(1),
133 end_pos,
134 state.entries.len()
135 );
136 self.styled(line, Style::new().dim().fg(self.theme.text_dim));
137 }
138
139 self.commands.push(Command::EndContainer);
140 self.rollback.last_text_idx = None;
141 response.changed = state.scroll_offset != old_offset;
142 response
143 }
144
145 pub fn virtual_list(
156 &mut self,
157 state: &mut ListState,
158 visible_height: u32,
159 f: impl Fn(&mut Context, usize),
160 ) -> Response {
161 self.virtual_list_impl(state, visible_height, false, f)
162 }
163
164 pub fn virtual_list_variable(
201 &mut self,
202 state: &mut ListState,
203 visible_height: u32,
204 f: impl Fn(&mut Context, usize),
205 ) -> Response {
206 self.virtual_list_impl(state, visible_height, true, f)
207 }
208
209 fn virtual_list_impl(
210 &mut self,
211 state: &mut ListState,
212 visible_height: u32,
213 variable: bool,
214 f: impl Fn(&mut Context, usize),
215 ) -> Response {
216 if state.items.is_empty() {
217 return Response::none();
218 }
219 state.selected = state.selected.min(state.items.len().saturating_sub(1));
220 let use_heights = variable && state.has_item_heights();
221 let focused = self.register_focusable();
222 let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
223 let old_selected = state.selected;
224
225 if focused {
226 let mut consumed_indices = Vec::new();
227 for (i, key) in self.available_key_presses() {
228 match key.code {
229 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
230 let _ = handle_vertical_nav(
231 &mut state.selected,
232 state.items.len().saturating_sub(1),
233 key.code.clone(),
234 );
235 consumed_indices.push(i);
236 }
237 KeyCode::PageUp => {
238 state.selected = if use_heights {
239 page_up_target(state, state.selected, visible_height)
240 } else {
241 state.selected.saturating_sub(visible_height as usize)
242 };
243 consumed_indices.push(i);
244 }
245 KeyCode::PageDown => {
246 state.selected = if use_heights {
247 page_down_target(state, state.selected, visible_height)
248 } else {
249 (state.selected + visible_height as usize)
250 .min(state.items.len().saturating_sub(1))
251 };
252 consumed_indices.push(i);
253 }
254 KeyCode::Home => {
255 state.selected = 0;
256 consumed_indices.push(i);
257 }
258 KeyCode::End => {
259 state.selected = state.items.len().saturating_sub(1);
260 consumed_indices.push(i);
261 }
262 _ => {}
263 }
264 }
265 self.consume_indices(consumed_indices);
266 }
267
268 let vh = visible_height as usize;
269 let (start, end) = if use_heights {
270 row_visible_range(state, vh)
271 } else {
272 if state.selected < state.viewport_offset {
278 state.viewport_offset = state.selected;
279 }
280 if vh > 0 && state.selected >= state.viewport_offset + vh {
281 state.viewport_offset = state.selected - vh + 1;
282 }
283 let start = state.viewport_offset;
284 (start, (start + vh).min(state.items.len()))
285 };
286
287 self.commands
288 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
289 direction: Direction::Column,
290 gap: 0,
291 align: Align::Start,
292 align_self: None,
293 justify: Justify::Start,
294 border: None,
295 border_sides: BorderSides::all(),
296 border_style: Style::new().fg(self.theme.border),
297 bg_color: None,
298 padding: Padding::default(),
299 margin: Margin::default(),
300 constraints: Constraints::default(),
301 title: None,
302 grow: 0,
303 group_name: None,
304 })));
305
306 if start > 0 {
307 let hidden = start.to_string();
308 let mut line = String::with_capacity(hidden.len() + 10);
309 line.push_str(" ↑ ");
310 line.push_str(&hidden);
311 line.push_str(" more");
312 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
313 }
314
315 for idx in start..end {
316 f(self, idx);
317 }
318
319 let remaining = state.items.len().saturating_sub(end);
320 if remaining > 0 {
321 let hidden = remaining.to_string();
322 let mut line = String::with_capacity(hidden.len() + 10);
323 line.push_str(" ↓ ");
324 line.push_str(&hidden);
325 line.push_str(" more");
326 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
327 }
328
329 self.commands.push(Command::EndContainer);
330 self.rollback.last_text_idx = None;
331 response.changed = state.selected != old_selected;
332 response
333 }
334
335 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Response {
339 if !state.open {
340 return Response::none();
341 }
342
343 state.last_selected = None;
344 let interaction_id = self.next_interaction_id();
345
346 let filtered: Vec<usize> = state.filtered_indices_cached().to_vec();
347 let sel = state.selected().min(filtered.len().saturating_sub(1));
348 state.set_selected(sel);
349
350 let mut consumed_indices = Vec::new();
351
352 for (i, key) in self.available_key_presses() {
353 match key.code {
354 KeyCode::Esc => {
355 state.open = false;
356 consumed_indices.push(i);
357 }
358 KeyCode::Up => {
359 let s = state.selected();
360 state.set_selected(s.saturating_sub(1));
361 consumed_indices.push(i);
362 }
363 KeyCode::Down => {
364 let s = state.selected();
365 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
366 consumed_indices.push(i);
367 }
368 KeyCode::Enter => {
369 if let Some(&cmd_idx) = filtered.get(state.selected()) {
370 state.last_selected = Some(cmd_idx);
371 state.open = false;
372 }
373 consumed_indices.push(i);
374 }
375 KeyCode::Backspace => {
376 if state.cursor > 0 {
377 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
378 let end_idx = byte_index_for_char(&state.input, state.cursor);
379 state.input.replace_range(byte_idx..end_idx, "");
380 state.cursor -= 1;
381 state.set_selected(0);
382 }
383 consumed_indices.push(i);
384 }
385 KeyCode::Char(ch) => {
386 let byte_idx = byte_index_for_char(&state.input, state.cursor);
387 state.input.insert(byte_idx, ch);
388 state.cursor += 1;
389 state.set_selected(0);
390 consumed_indices.push(i);
391 }
392 _ => {}
393 }
394 }
395 self.consume_indices(consumed_indices);
396
397 let filtered: Vec<usize> = state.filtered_indices_cached().to_vec();
398
399 let _ = self.modal(|ui| {
400 let primary = ui.theme.primary;
401 let palette_pad = ui.theme.spacing.xs();
402 let palette_input_padx = ui.theme.spacing.xs();
403 let _ = ui
404 .container()
405 .border(Border::Rounded)
406 .border_style(Style::new().fg(primary))
407 .p(palette_pad)
408 .max_w(60)
409 .col(|ui| {
410 let border_color = ui.theme.primary;
411 let _ = ui
412 .bordered(Border::Rounded)
413 .border_style(Style::new().fg(border_color))
414 .px(palette_input_padx)
415 .col(|ui| {
416 let display = if state.input.is_empty() {
417 "Type to search...".to_string()
418 } else {
419 state.input.clone()
420 };
421 let style = if state.input.is_empty() {
422 Style::new().dim().fg(ui.theme.text_dim)
423 } else {
424 Style::new().fg(ui.theme.text)
425 };
426 ui.styled(display, style);
427 });
428
429 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
430 let cmd = &state.commands[cmd_idx];
431 let is_selected = list_idx == state.selected();
432 let style = if is_selected {
433 Style::new().bold().fg(ui.theme.primary)
434 } else {
435 Style::new().fg(ui.theme.text)
436 };
437 let prefix = if is_selected { "▸ " } else { " " };
438 let shortcut_text = cmd
439 .shortcut
440 .as_deref()
441 .map(|s| {
442 let mut text = String::with_capacity(s.len() + 4);
443 text.push_str(" (");
444 text.push_str(s);
445 text.push(')');
446 text
447 })
448 .unwrap_or_default();
449 let mut line = String::with_capacity(
450 prefix.len() + cmd.label.len() + shortcut_text.len(),
451 );
452 line.push_str(prefix);
453 line.push_str(&cmd.label);
454 line.push_str(&shortcut_text);
455 ui.styled(line, style);
456 if is_selected && !cmd.description.is_empty() {
457 let mut desc = String::with_capacity(4 + cmd.description.len());
458 desc.push_str(" ");
459 desc.push_str(&cmd.description);
460 ui.styled(desc, Style::new().dim().fg(ui.theme.text_dim));
461 }
462 }
463
464 if filtered.is_empty() {
465 ui.styled(
466 " No matching commands",
467 Style::new().dim().fg(ui.theme.text_dim),
468 );
469 }
470 });
471 });
472
473 let mut response = self.response_for(interaction_id);
474 response.changed = state.last_selected.is_some();
475 response
476 }
477
478 pub fn markdown(&mut self, text: &str) -> Response {
488 self.commands
489 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
490 direction: Direction::Column,
491 gap: 0,
492 align: Align::Start,
493 align_self: None,
494 justify: Justify::Start,
495 border: None,
496 border_sides: BorderSides::all(),
497 border_style: Style::new().fg(self.theme.border),
498 bg_color: None,
499 padding: Padding::default(),
500 margin: Margin::default(),
501 constraints: Constraints::default(),
502 title: None,
503 grow: 0,
504 group_name: None,
505 })));
506 self.skip_interaction_slot();
507
508 let text_style = Style::new().fg(self.theme.text);
509 let bold_style = Style::new().fg(self.theme.text).bold();
510 let code_style = Style::new().fg(self.theme.accent);
511 let border_style = Style::new().fg(self.theme.border).dim();
512
513 let mut in_code_block = false;
514 let mut code_block_lang = String::new();
515 let mut code_block_lines: Vec<String> = Vec::new();
516 let mut table_lines: Vec<String> = Vec::new();
517
518 for line in text.lines() {
519 let trimmed = line.trim();
520
521 if in_code_block {
522 if trimmed.starts_with("```") {
523 in_code_block = false;
524 let code_content = code_block_lines.join("\n");
525 let theme = self.theme;
526 let code_pad = theme.spacing.xs();
527 let highlighted: Option<Vec<Vec<(String, Style)>>> =
528 crate::syntax::highlight_code(&code_content, &code_block_lang, &theme);
529 let _ = self.container().bg(theme.surface).p(code_pad).col(|ui| {
530 if let Some(ref hl_lines) = highlighted {
531 for segs in hl_lines {
532 if segs.is_empty() {
533 ui.text(" ");
534 } else {
535 ui.line(|ui| {
536 for (t, s) in segs {
537 ui.styled(t, *s);
538 }
539 });
540 }
541 }
542 } else {
543 for cl in &code_block_lines {
544 ui.styled(cl, code_style);
545 }
546 }
547 });
548 code_block_lang.clear();
549 code_block_lines.clear();
550 } else {
551 code_block_lines.push(line.to_string());
552 }
553 continue;
554 }
555
556 if trimmed.starts_with('|') && trimmed.matches('|').count() >= 2 {
558 table_lines.push(trimmed.to_string());
559 continue;
560 }
561 if !table_lines.is_empty() {
563 self.render_markdown_table(
564 &table_lines,
565 text_style,
566 bold_style,
567 code_style,
568 border_style,
569 );
570 table_lines.clear();
571 }
572
573 if trimmed.is_empty() {
574 self.text(" ");
575 continue;
576 }
577 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
578 self.styled("─".repeat(40), border_style);
579 continue;
580 }
581 if let Some(quote) = trimmed.strip_prefix("> ") {
582 let quote_style = Style::new().fg(self.theme.text_dim).italic();
583 let bar_style = Style::new().fg(self.theme.border);
584 self.line(|ui| {
585 ui.styled("│ ", bar_style);
586 ui.styled(quote, quote_style);
587 });
588 } else if let Some(heading) = trimmed.strip_prefix("### ") {
589 self.styled(heading, Style::new().bold().fg(self.theme.accent));
590 } else if let Some(heading) = trimmed.strip_prefix("## ") {
591 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
592 } else if let Some(heading) = trimmed.strip_prefix("# ") {
593 self.styled(heading, Style::new().bold().fg(self.theme.primary));
594 } else if let Some(item) = trimmed
595 .strip_prefix("- ")
596 .or_else(|| trimmed.strip_prefix("* "))
597 {
598 self.line_wrap(|ui| {
599 ui.styled(" • ", text_style);
600 Self::render_md_inline_into(ui, item, text_style, bold_style, code_style);
601 });
602 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
603 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
604 if parts.len() == 2 {
605 self.line_wrap(|ui| {
606 let mut prefix = String::with_capacity(4 + parts[0].len());
607 prefix.push_str(" ");
608 prefix.push_str(parts[0]);
609 prefix.push_str(". ");
610 ui.styled(prefix, text_style);
611 Self::render_md_inline_into(
612 ui, parts[1], text_style, bold_style, code_style,
613 );
614 });
615 } else {
616 self.text(trimmed);
617 }
618 } else if let Some(lang) = trimmed.strip_prefix("```") {
619 in_code_block = true;
620 code_block_lang = lang.trim().to_string();
621 } else {
622 self.render_md_inline(trimmed, text_style, bold_style, code_style);
623 }
624 }
625
626 if in_code_block && !code_block_lines.is_empty() {
627 for cl in &code_block_lines {
628 self.styled(cl, code_style);
629 }
630 }
631
632 if !table_lines.is_empty() {
634 self.render_markdown_table(
635 &table_lines,
636 text_style,
637 bold_style,
638 code_style,
639 border_style,
640 );
641 }
642
643 self.commands.push(Command::EndContainer);
644 self.rollback.last_text_idx = None;
645 Response::none()
646 }
647
648 fn render_markdown_table(
650 &mut self,
651 lines: &[String],
652 text_style: Style,
653 bold_style: Style,
654 code_style: Style,
655 border_style: Style,
656 ) {
657 if lines.is_empty() {
658 return;
659 }
660
661 let is_separator = |line: &str| -> bool {
663 let inner = line.trim_matches('|').trim();
664 !inner.is_empty()
665 && inner
666 .chars()
667 .all(|c| c == '-' || c == ':' || c == '|' || c == ' ')
668 };
669
670 let parse_row = |line: &str| -> Vec<String> {
671 let trimmed = line.trim().trim_start_matches('|').trim_end_matches('|');
672 trimmed.split('|').map(|c| c.trim().to_string()).collect()
673 };
674
675 let mut header: Option<Vec<String>> = None;
676 let mut data_rows: Vec<Vec<String>> = Vec::new();
677 let mut found_separator = false;
678
679 for (i, line) in lines.iter().enumerate() {
680 if is_separator(line) {
681 found_separator = true;
682 continue;
683 }
684 if i == 0 && !found_separator {
685 header = Some(parse_row(line));
686 } else {
687 data_rows.push(parse_row(line));
688 }
689 }
690
691 if !found_separator && header.is_none() && !data_rows.is_empty() {
693 header = Some(data_rows.remove(0));
694 }
695
696 let all_rows: Vec<&Vec<String>> = header.iter().chain(data_rows.iter()).collect();
698 let col_count = all_rows.iter().map(|r| r.len()).max().unwrap_or(0);
699 if col_count == 0 {
700 return;
701 }
702 let mut col_widths = vec![0usize; col_count];
703 let stripped_rows: Vec<Vec<String>> = all_rows
705 .iter()
706 .map(|row| row.iter().map(|c| Self::md_strip(c)).collect())
707 .collect();
708 for row in &stripped_rows {
709 for (i, cell) in row.iter().enumerate() {
710 if i < col_count {
711 col_widths[i] = col_widths[i].max(UnicodeWidthStr::width(cell.as_str()));
712 }
713 }
714 }
715
716 let mut top = String::from("┌");
718 for (i, &w) in col_widths.iter().enumerate() {
719 for _ in 0..w + 2 {
720 top.push('─');
721 }
722 top.push(if i < col_count - 1 { '┬' } else { '┐' });
723 }
724 self.styled(&top, border_style);
725
726 if let Some(ref hdr) = header {
728 self.line(|ui| {
729 ui.styled("│", border_style);
730 for (i, w) in col_widths.iter().enumerate() {
731 let raw = hdr.get(i).map(String::as_str).unwrap_or("");
732 let display_text = Self::md_strip(raw);
733 let cell_w = UnicodeWidthStr::width(display_text.as_str());
734 let padding: String = " ".repeat(w.saturating_sub(cell_w));
735 ui.styled(" ", bold_style);
736 ui.styled(&display_text, bold_style);
737 ui.styled(padding, bold_style);
738 ui.styled(" │", border_style);
739 }
740 });
741
742 let mut sep = String::from("├");
744 for (i, &w) in col_widths.iter().enumerate() {
745 for _ in 0..w + 2 {
746 sep.push('─');
747 }
748 sep.push(if i < col_count - 1 { '┼' } else { '┤' });
749 }
750 self.styled(&sep, border_style);
751 }
752
753 for row in &data_rows {
755 self.line(|ui| {
756 ui.styled("│", border_style);
757 for (i, w) in col_widths.iter().enumerate() {
758 let raw = row.get(i).map(String::as_str).unwrap_or("");
759 let display_text = Self::md_strip(raw);
760 let cell_w = UnicodeWidthStr::width(display_text.as_str());
761 let padding: String = " ".repeat(w.saturating_sub(cell_w));
762 ui.styled(" ", text_style);
763 Self::render_md_inline_into(ui, raw, text_style, bold_style, code_style);
764 ui.styled(padding, text_style);
765 ui.styled(" │", border_style);
766 }
767 });
768 }
769
770 let mut bot = String::from("└");
772 for (i, &w) in col_widths.iter().enumerate() {
773 for _ in 0..w + 2 {
774 bot.push('─');
775 }
776 bot.push(if i < col_count - 1 { '┴' } else { '┘' });
777 }
778 self.styled(&bot, border_style);
779 }
780
781 pub(crate) fn parse_inline_segments(
782 text: &str,
783 base: Style,
784 bold: Style,
785 code: Style,
786 ) -> Vec<(String, Style)> {
787 let mut segments: Vec<(String, Style)> = Vec::new();
792 let bytes = text.as_bytes();
793 let mut current = String::new();
794 let mut i: usize = 0;
795
796 while i < bytes.len() {
797 if bytes[i] == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
799 let after_open = i + 2;
800 if let Some(rel_end) = text[after_open..].find("**") {
801 let close = after_open + rel_end;
802 if !current.is_empty() {
803 segments.push((std::mem::take(&mut current), base));
804 }
805 let inner = text[after_open..close].to_string();
806 segments.push((inner, bold));
807 i = close + 2;
808 continue;
809 }
810 }
811
812 if bytes[i] == b'*'
814 && (i + 1 >= bytes.len() || bytes[i + 1] != b'*')
815 && (i == 0 || bytes[i - 1] != b'*')
816 {
817 let after_open = i + 1;
818 if let Some(rel_end) = text[after_open..].find('*') {
819 let close = after_open + rel_end;
820 if !current.is_empty() {
821 segments.push((std::mem::take(&mut current), base));
822 }
823 let inner = text[after_open..close].to_string();
824 segments.push((inner, base.italic()));
825 i = close + 1;
826 continue;
827 }
828 }
829
830 if bytes[i] == b'`' {
832 let after_open = i + 1;
833 if let Some(rel_end) = text[after_open..].find('`') {
834 let close = after_open + rel_end;
835 if !current.is_empty() {
836 segments.push((std::mem::take(&mut current), base));
837 }
838 let inner = text[after_open..close].to_string();
839 segments.push((inner, code));
840 i = close + 1;
841 continue;
842 }
843 }
844
845 let ch = text[i..]
848 .chars()
849 .next()
850 .expect("non-empty tail past bounds check");
851 current.push(ch);
852 i += ch.len_utf8();
853 }
854
855 if !current.is_empty() {
856 segments.push((current, base));
857 }
858 segments
859 }
860
861 fn render_md_inline(
866 &mut self,
867 text: &str,
868 text_style: Style,
869 bold_style: Style,
870 code_style: Style,
871 ) {
872 let items = Self::split_md_links(text);
873
874 if items.len() == 1
876 && let MdInline::Text(ref t) = items[0]
877 {
878 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
879 if segs.len() <= 1 {
880 self.text(text)
881 .wrap()
882 .fg(text_style.fg.unwrap_or(Color::Reset));
883 } else {
884 self.line_wrap(|ui| {
885 for (s, st) in segs {
886 ui.styled(s, st);
887 }
888 });
889 }
890 return;
891 }
892
893 self.line_wrap(|ui| {
895 for item in &items {
896 match item {
897 MdInline::Text(t) => {
898 let segs =
899 Self::parse_inline_segments(t, text_style, bold_style, code_style);
900 for (s, st) in segs {
901 ui.styled(s, st);
902 }
903 }
904 MdInline::Link { text, url } => {
905 ui.link(text.clone(), url.clone());
906 }
907 MdInline::Image { alt, .. } => {
908 ui.styled(alt.as_str(), code_style);
910 }
911 }
912 }
913 });
914 }
915
916 fn render_md_inline_into(
922 ui: &mut Context,
923 text: &str,
924 text_style: Style,
925 bold_style: Style,
926 code_style: Style,
927 ) {
928 let items = Self::split_md_links(text);
929 for item in &items {
930 match item {
931 MdInline::Text(t) => {
932 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
933 for (s, st) in segs {
934 ui.styled(s, st);
935 }
936 }
937 MdInline::Link { text, url } => {
938 ui.link(text.clone(), url.clone());
939 }
940 MdInline::Image { alt, .. } => {
941 ui.styled(alt.as_str(), code_style);
942 }
943 }
944 }
945 }
946
947 fn split_md_links(text: &str) -> Vec<MdInline> {
949 let chars: Vec<char> = text.chars().collect();
950 let mut items: Vec<MdInline> = Vec::new();
951 let mut current = String::new();
952 let mut i = 0;
953
954 while i < chars.len() {
955 if chars[i] == '!'
957 && i + 1 < chars.len()
958 && chars[i + 1] == '['
959 && let Some((alt, _url, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1)
960 {
961 if !current.is_empty() {
962 items.push(MdInline::Text(std::mem::take(&mut current)));
963 }
964 items.push(MdInline::Image { alt });
965 i += 1 + consumed;
966 continue;
967 }
968 if chars[i] == '['
970 && let Some((link_text, url, consumed)) = Self::parse_md_bracket_paren(&chars, i)
971 {
972 if !current.is_empty() {
973 items.push(MdInline::Text(std::mem::take(&mut current)));
974 }
975 items.push(MdInline::Link {
976 text: link_text,
977 url,
978 });
979 i += consumed;
980 continue;
981 }
982 current.push(chars[i]);
983 i += 1;
984 }
985 if !current.is_empty() {
986 items.push(MdInline::Text(current));
987 }
988 if items.is_empty() {
989 items.push(MdInline::Text(String::new()));
990 }
991 items
992 }
993
994 fn parse_md_bracket_paren(chars: &[char], start: usize) -> Option<(String, String, usize)> {
997 if start >= chars.len() || chars[start] != '[' {
998 return None;
999 }
1000 let mut depth = 0i32;
1002 let mut bracket_end = None;
1003 for (j, &ch) in chars.iter().enumerate().skip(start) {
1004 if ch == '[' {
1005 depth += 1;
1006 } else if ch == ']' {
1007 depth -= 1;
1008 if depth == 0 {
1009 bracket_end = Some(j);
1010 break;
1011 }
1012 }
1013 }
1014 let bracket_end = bracket_end?;
1015 if bracket_end + 1 >= chars.len() || chars[bracket_end + 1] != '(' {
1017 return None;
1018 }
1019 let paren_start = bracket_end + 2;
1021 let mut paren_end = None;
1022 let mut paren_depth = 1i32;
1023 for (j, &ch) in chars.iter().enumerate().skip(paren_start) {
1024 if ch == '(' {
1025 paren_depth += 1;
1026 } else if ch == ')' {
1027 paren_depth -= 1;
1028 if paren_depth == 0 {
1029 paren_end = Some(j);
1030 break;
1031 }
1032 }
1033 }
1034 let paren_end = paren_end?;
1035 let text: String = chars[start + 1..bracket_end].iter().collect();
1036 let url: String = chars[paren_start..paren_end].iter().collect();
1037 let consumed = paren_end - start + 1;
1038 Some((text, url, consumed))
1039 }
1040
1041 fn md_strip(text: &str) -> String {
1046 let chars: Vec<char> = text.chars().collect();
1050 let char_to_byte = {
1051 let mut v = Vec::with_capacity(chars.len() + 1);
1052 let mut acc = 0usize;
1053 v.push(0);
1054 for ch in &chars {
1055 acc += ch.len_utf8();
1056 v.push(acc);
1057 }
1058 v
1059 };
1060 let bytes = text.as_bytes();
1061 let mut result = String::with_capacity(text.len());
1062 let mut ci: usize = 0;
1063
1064 while ci < chars.len() {
1065 if chars[ci] == '!'
1067 && ci + 1 < chars.len()
1068 && chars[ci + 1] == '['
1069 && let Some((alt, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci + 1)
1070 {
1071 result.push_str(&alt);
1072 ci += 1 + consumed;
1073 continue;
1074 }
1075 if chars[ci] == '['
1077 && let Some((link_text, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci)
1078 {
1079 result.push_str(&link_text);
1080 ci += consumed;
1081 continue;
1082 }
1083
1084 let bi = char_to_byte[ci];
1085
1086 if bytes[bi] == b'*' && bi + 1 < bytes.len() && bytes[bi + 1] == b'*' {
1088 let after_open = bi + 2;
1089 if let Some(rel_end) = text[after_open..].find("**") {
1090 let close = after_open + rel_end;
1091 let inner = &text[after_open..close];
1092 result.push_str(inner);
1093 ci += 2 + inner.chars().count() + 2;
1094 continue;
1095 }
1096 }
1097
1098 if bytes[bi] == b'*'
1100 && (bi + 1 >= bytes.len() || bytes[bi + 1] != b'*')
1101 && (bi == 0 || bytes[bi - 1] != b'*')
1102 {
1103 let after_open = bi + 1;
1104 if let Some(rel_end) = text[after_open..].find('*') {
1105 let close = after_open + rel_end;
1106 let inner = &text[after_open..close];
1107 result.push_str(inner);
1108 ci += 1 + inner.chars().count() + 1;
1109 continue;
1110 }
1111 }
1112
1113 if bytes[bi] == b'`' {
1115 let after_open = bi + 1;
1116 if let Some(rel_end) = text[after_open..].find('`') {
1117 let close = after_open + rel_end;
1118 let inner = &text[after_open..close];
1119 result.push_str(inner);
1120 ci += 1 + inner.chars().count() + 1;
1121 continue;
1122 }
1123 }
1124
1125 result.push(chars[ci]);
1126 ci += 1;
1127 }
1128 result
1129 }
1130
1131 pub fn key_chord(&mut self, seq: &str) -> bool {
1172 self.key_chord_timeout(seq, DEFAULT_CHORD_TIMEOUT_TICKS)
1173 }
1174
1175 pub fn key_chord_timeout(&mut self, seq: &str, timeout_ticks: u64) -> bool {
1194 let target = parse_chord(seq);
1195 if target.is_empty() {
1196 return false;
1197 }
1198 if (self.rollback.modal_active || self.prev_modal_active)
1201 && self.rollback.overlay_depth == 0
1202 {
1203 return false;
1204 }
1205
1206 if self.tick.saturating_sub(self.chord.last_tick) > timeout_ticks {
1208 self.chord.pending.clear();
1209 }
1210
1211 let char_presses: Vec<(usize, char)> = self
1215 .available_key_presses()
1216 .filter_map(|(i, key)| match key.code {
1217 KeyCode::Char(c) => Some((i, c)),
1218 _ => None,
1219 })
1220 .collect();
1221
1222 let tick = self.tick;
1223 let mut completed_index: Option<usize> = None;
1224 let mut buf: Vec<char> = self.chord.pending.chars().collect();
1225
1226 for (i, c) in char_presses {
1227 buf.push(c);
1228 retain_longest_prefix(&mut buf, &target);
1232 self.chord.last_tick = tick;
1233 if buf.len() == target.len() {
1234 completed_index = Some(i);
1235 buf.clear();
1236 break;
1237 }
1238 }
1239
1240 self.chord.pending = buf.into_iter().collect();
1241 if let Some(i) = completed_index {
1242 self.consume_indices([i]);
1243 true
1244 } else {
1245 false
1246 }
1247 }
1248
1249 #[deprecated(
1257 since = "0.21.0",
1258 note = "renamed to `key_chord`; now matches across frames"
1259 )]
1260 pub fn key_seq(&mut self, seq: &str) -> bool {
1261 self.key_chord(seq)
1262 }
1263}
1264
1265fn parse_chord(seq: &str) -> Vec<char> {
1270 let mut out = Vec::new();
1271 let mut rest = seq;
1272 while !rest.is_empty() {
1273 if let Some(tail) = rest.strip_prefix("<space>") {
1274 out.push(' ');
1275 rest = tail;
1276 } else if let Some(tail) = rest.strip_prefix("<leader>") {
1277 out.push(' ');
1278 rest = tail;
1279 } else {
1280 let c = rest.chars().next().expect("rest is non-empty");
1281 out.push(c);
1282 rest = &rest[c.len_utf8()..];
1283 }
1284 }
1285 out
1286}
1287
1288fn retain_longest_prefix(buf: &mut Vec<char>, target: &[char]) {
1296 let mut start = 0;
1299 while start < buf.len() {
1300 if buf[start..].iter().zip(target).all(|(b, t)| b == t) {
1301 break;
1302 }
1303 start += 1;
1304 }
1305 if start > 0 {
1306 buf.drain(0..start);
1307 }
1308}
1309
1310fn item_at_row(row_prefix: &[u32], target_row: u32, n: usize) -> usize {
1320 if n == 0 {
1324 return 0;
1325 }
1326 let count = row_prefix.partition_point(|&r| r <= target_row);
1327 count.saturating_sub(1).min(n - 1)
1328}
1329
1330fn row_visible_range(state: &mut ListState, vh: usize) -> (usize, usize) {
1339 state.ensure_row_prefix();
1340 let n = state.items.len();
1341 if n == 0 || vh == 0 {
1342 state.viewport_offset = state.viewport_offset.min(n.saturating_sub(1));
1343 state.viewport_row_offset = 0;
1344 return (state.viewport_offset, state.viewport_offset);
1345 }
1346
1347 let vh_rows = vh as u32;
1348 let row_prefix = state.row_prefix();
1349 let sel = state.selected.min(n - 1);
1351 let sel_top = row_prefix[sel];
1352 let sel_bottom = row_prefix[sel + 1]; let mut top = state.viewport_offset.min(n - 1);
1355
1356 if sel_top < row_prefix[top] {
1359 top = sel;
1360 }
1361
1362 while top < sel && sel_bottom.saturating_sub(row_prefix[top]) > vh_rows {
1367 top += 1;
1368 }
1369
1370 let top_row = row_prefix[top];
1375 let target_bottom = top_row.saturating_add(vh_rows);
1376 let end = row_prefix
1384 .partition_point(|&r| r <= target_bottom)
1385 .saturating_sub(1)
1386 .clamp(top + 1, n);
1387
1388 state.viewport_offset = top;
1389 state.viewport_row_offset = top_row as usize;
1390 (top, end)
1391}
1392
1393fn page_down_target(state: &mut ListState, from: usize, visible_height: u32) -> usize {
1397 state.ensure_row_prefix();
1398 let n = state.items.len();
1399 if n == 0 {
1400 return 0;
1401 }
1402 let from = from.min(n - 1);
1403 let row_prefix = state.row_prefix();
1404 let from_top = row_prefix[from];
1405 let target = from_top.saturating_add(visible_height.max(1));
1406 let next = item_at_row(row_prefix, target, n);
1407 next.max(from + 1).min(n - 1)
1408}
1409
1410fn page_up_target(state: &mut ListState, from: usize, visible_height: u32) -> usize {
1414 state.ensure_row_prefix();
1415 let n = state.items.len();
1416 if n == 0 {
1417 return 0;
1418 }
1419 let from = from.min(n - 1);
1420 let row_prefix = state.row_prefix();
1421 let from_bottom = row_prefix[from + 1];
1422 let target = from_bottom.saturating_sub(visible_height.max(1));
1423 let prev = item_at_row(row_prefix, target, n);
1424 prev.min(from.saturating_sub(1))
1425}