1use super::*;
2use crate::{RichLogState, DEFAULT_CHORD_TIMEOUT_TICKS};
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 if let MdInline::Text(ref t) = items[0] {
877 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
878 if segs.len() <= 1 {
879 self.text(text)
880 .wrap()
881 .fg(text_style.fg.unwrap_or(Color::Reset));
882 } else {
883 self.line_wrap(|ui| {
884 for (s, st) in segs {
885 ui.styled(s, st);
886 }
887 });
888 }
889 return;
890 }
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] == '!' && i + 1 < chars.len() && chars[i + 1] == '[' {
957 if let Some((alt, _url, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1) {
958 if !current.is_empty() {
959 items.push(MdInline::Text(std::mem::take(&mut current)));
960 }
961 items.push(MdInline::Image { alt });
962 i += 1 + consumed;
963 continue;
964 }
965 }
966 if chars[i] == '[' {
968 if let Some((link_text, url, consumed)) = Self::parse_md_bracket_paren(&chars, i) {
969 if !current.is_empty() {
970 items.push(MdInline::Text(std::mem::take(&mut current)));
971 }
972 items.push(MdInline::Link {
973 text: link_text,
974 url,
975 });
976 i += consumed;
977 continue;
978 }
979 }
980 current.push(chars[i]);
981 i += 1;
982 }
983 if !current.is_empty() {
984 items.push(MdInline::Text(current));
985 }
986 if items.is_empty() {
987 items.push(MdInline::Text(String::new()));
988 }
989 items
990 }
991
992 fn parse_md_bracket_paren(chars: &[char], start: usize) -> Option<(String, String, usize)> {
995 if start >= chars.len() || chars[start] != '[' {
996 return None;
997 }
998 let mut depth = 0i32;
1000 let mut bracket_end = None;
1001 for (j, &ch) in chars.iter().enumerate().skip(start) {
1002 if ch == '[' {
1003 depth += 1;
1004 } else if ch == ']' {
1005 depth -= 1;
1006 if depth == 0 {
1007 bracket_end = Some(j);
1008 break;
1009 }
1010 }
1011 }
1012 let bracket_end = bracket_end?;
1013 if bracket_end + 1 >= chars.len() || chars[bracket_end + 1] != '(' {
1015 return None;
1016 }
1017 let paren_start = bracket_end + 2;
1019 let mut paren_end = None;
1020 let mut paren_depth = 1i32;
1021 for (j, &ch) in chars.iter().enumerate().skip(paren_start) {
1022 if ch == '(' {
1023 paren_depth += 1;
1024 } else if ch == ')' {
1025 paren_depth -= 1;
1026 if paren_depth == 0 {
1027 paren_end = Some(j);
1028 break;
1029 }
1030 }
1031 }
1032 let paren_end = paren_end?;
1033 let text: String = chars[start + 1..bracket_end].iter().collect();
1034 let url: String = chars[paren_start..paren_end].iter().collect();
1035 let consumed = paren_end - start + 1;
1036 Some((text, url, consumed))
1037 }
1038
1039 fn md_strip(text: &str) -> String {
1044 let chars: Vec<char> = text.chars().collect();
1048 let char_to_byte = {
1049 let mut v = Vec::with_capacity(chars.len() + 1);
1050 let mut acc = 0usize;
1051 v.push(0);
1052 for ch in &chars {
1053 acc += ch.len_utf8();
1054 v.push(acc);
1055 }
1056 v
1057 };
1058 let bytes = text.as_bytes();
1059 let mut result = String::with_capacity(text.len());
1060 let mut ci: usize = 0;
1061
1062 while ci < chars.len() {
1063 if chars[ci] == '!' && ci + 1 < chars.len() && chars[ci + 1] == '[' {
1065 if let Some((alt, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci + 1) {
1066 result.push_str(&alt);
1067 ci += 1 + consumed;
1068 continue;
1069 }
1070 }
1071 if chars[ci] == '[' {
1073 if let Some((link_text, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci) {
1074 result.push_str(&link_text);
1075 ci += consumed;
1076 continue;
1077 }
1078 }
1079
1080 let bi = char_to_byte[ci];
1081
1082 if bytes[bi] == b'*' && bi + 1 < bytes.len() && bytes[bi + 1] == b'*' {
1084 let after_open = bi + 2;
1085 if let Some(rel_end) = text[after_open..].find("**") {
1086 let close = after_open + rel_end;
1087 let inner = &text[after_open..close];
1088 result.push_str(inner);
1089 ci += 2 + inner.chars().count() + 2;
1090 continue;
1091 }
1092 }
1093
1094 if bytes[bi] == b'*'
1096 && (bi + 1 >= bytes.len() || bytes[bi + 1] != b'*')
1097 && (bi == 0 || bytes[bi - 1] != b'*')
1098 {
1099 let after_open = bi + 1;
1100 if let Some(rel_end) = text[after_open..].find('*') {
1101 let close = after_open + rel_end;
1102 let inner = &text[after_open..close];
1103 result.push_str(inner);
1104 ci += 1 + inner.chars().count() + 1;
1105 continue;
1106 }
1107 }
1108
1109 if bytes[bi] == b'`' {
1111 let after_open = bi + 1;
1112 if let Some(rel_end) = text[after_open..].find('`') {
1113 let close = after_open + rel_end;
1114 let inner = &text[after_open..close];
1115 result.push_str(inner);
1116 ci += 1 + inner.chars().count() + 1;
1117 continue;
1118 }
1119 }
1120
1121 result.push(chars[ci]);
1122 ci += 1;
1123 }
1124 result
1125 }
1126
1127 pub fn key_chord(&mut self, seq: &str) -> bool {
1168 self.key_chord_timeout(seq, DEFAULT_CHORD_TIMEOUT_TICKS)
1169 }
1170
1171 pub fn key_chord_timeout(&mut self, seq: &str, timeout_ticks: u64) -> bool {
1190 let target = parse_chord(seq);
1191 if target.is_empty() {
1192 return false;
1193 }
1194 if (self.rollback.modal_active || self.prev_modal_active)
1197 && self.rollback.overlay_depth == 0
1198 {
1199 return false;
1200 }
1201
1202 if self.tick.saturating_sub(self.chord.last_tick) > timeout_ticks {
1204 self.chord.pending.clear();
1205 }
1206
1207 let char_presses: Vec<(usize, char)> = self
1211 .available_key_presses()
1212 .filter_map(|(i, key)| match key.code {
1213 KeyCode::Char(c) => Some((i, c)),
1214 _ => None,
1215 })
1216 .collect();
1217
1218 let tick = self.tick;
1219 let mut completed_index: Option<usize> = None;
1220 let mut buf: Vec<char> = self.chord.pending.chars().collect();
1221
1222 for (i, c) in char_presses {
1223 buf.push(c);
1224 retain_longest_prefix(&mut buf, &target);
1228 self.chord.last_tick = tick;
1229 if buf.len() == target.len() {
1230 completed_index = Some(i);
1231 buf.clear();
1232 break;
1233 }
1234 }
1235
1236 self.chord.pending = buf.into_iter().collect();
1237 if let Some(i) = completed_index {
1238 self.consume_indices([i]);
1239 true
1240 } else {
1241 false
1242 }
1243 }
1244
1245 #[deprecated(
1253 since = "0.21.0",
1254 note = "renamed to `key_chord`; now matches across frames"
1255 )]
1256 pub fn key_seq(&mut self, seq: &str) -> bool {
1257 self.key_chord(seq)
1258 }
1259}
1260
1261fn parse_chord(seq: &str) -> Vec<char> {
1266 let mut out = Vec::new();
1267 let mut rest = seq;
1268 while !rest.is_empty() {
1269 if let Some(tail) = rest.strip_prefix("<space>") {
1270 out.push(' ');
1271 rest = tail;
1272 } else if let Some(tail) = rest.strip_prefix("<leader>") {
1273 out.push(' ');
1274 rest = tail;
1275 } else {
1276 let c = rest.chars().next().expect("rest is non-empty");
1277 out.push(c);
1278 rest = &rest[c.len_utf8()..];
1279 }
1280 }
1281 out
1282}
1283
1284fn retain_longest_prefix(buf: &mut Vec<char>, target: &[char]) {
1292 let mut start = 0;
1295 while start < buf.len() {
1296 if buf[start..].iter().zip(target).all(|(b, t)| b == t) {
1297 break;
1298 }
1299 start += 1;
1300 }
1301 if start > 0 {
1302 buf.drain(0..start);
1303 }
1304}
1305
1306fn item_at_row(row_prefix: &[u32], target_row: u32, n: usize) -> usize {
1316 if n == 0 {
1320 return 0;
1321 }
1322 let count = row_prefix.partition_point(|&r| r <= target_row);
1323 count.saturating_sub(1).min(n - 1)
1324}
1325
1326fn row_visible_range(state: &mut ListState, vh: usize) -> (usize, usize) {
1335 state.ensure_row_prefix();
1336 let n = state.items.len();
1337 if n == 0 || vh == 0 {
1338 state.viewport_offset = state.viewport_offset.min(n.saturating_sub(1));
1339 state.viewport_row_offset = 0;
1340 return (state.viewport_offset, state.viewport_offset);
1341 }
1342
1343 let vh_rows = vh as u32;
1344 let row_prefix = state.row_prefix();
1345 let sel = state.selected.min(n - 1);
1347 let sel_top = row_prefix[sel];
1348 let sel_bottom = row_prefix[sel + 1]; let mut top = state.viewport_offset.min(n - 1);
1351
1352 if sel_top < row_prefix[top] {
1355 top = sel;
1356 }
1357
1358 while top < sel && sel_bottom.saturating_sub(row_prefix[top]) > vh_rows {
1363 top += 1;
1364 }
1365
1366 let top_row = row_prefix[top];
1371 let target_bottom = top_row.saturating_add(vh_rows);
1372 let end = row_prefix
1380 .partition_point(|&r| r <= target_bottom)
1381 .saturating_sub(1)
1382 .clamp(top + 1, n);
1383
1384 state.viewport_offset = top;
1385 state.viewport_row_offset = top_row as usize;
1386 (top, end)
1387}
1388
1389fn page_down_target(state: &mut ListState, from: usize, visible_height: u32) -> usize {
1393 state.ensure_row_prefix();
1394 let n = state.items.len();
1395 if n == 0 {
1396 return 0;
1397 }
1398 let from = from.min(n - 1);
1399 let row_prefix = state.row_prefix();
1400 let from_top = row_prefix[from];
1401 let target = from_top.saturating_add(visible_height.max(1));
1402 let next = item_at_row(row_prefix, target, n);
1403 next.max(from + 1).min(n - 1)
1404}
1405
1406fn page_up_target(state: &mut ListState, from: usize, visible_height: u32) -> usize {
1410 state.ensure_row_prefix();
1411 let n = state.items.len();
1412 if n == 0 {
1413 return 0;
1414 }
1415 let from = from.min(n - 1);
1416 let row_prefix = state.row_prefix();
1417 let from_bottom = row_prefix[from + 1];
1418 let target = from_bottom.saturating_sub(visible_height.max(1));
1419 let prev = item_at_row(row_prefix, target, n);
1420 prev.min(from.saturating_sub(1))
1421}