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.len().max(1)
18 } else {
19 viewport_height
20 };
21 let show_indicator = state.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.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.len().saturating_sub(visible_rows));
92 let end = (start + visible_rows).min(state.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.entries().skip(start).take(end.saturating_sub(start)) {
114 self.commands.push(Command::RichText {
115 segments: entry.segments.clone(),
116 wrap: false,
117 align: Align::Start,
118 margin: Margin::default(),
119 constraints: Constraints::default(),
120 });
121 }
122
123 if show_indicator {
124 let end_pos = end.min(state.len());
125 let line = format!("{}-{} / {}", start.saturating_add(1), end_pos, state.len());
126 self.styled(line, Style::new().dim().fg(self.theme.text_dim));
127 }
128
129 self.commands.push(Command::EndContainer);
130 self.rollback.last_text_idx = None;
131 response.changed = state.scroll_offset != old_offset;
132 response
133 }
134
135 pub fn virtual_list(
146 &mut self,
147 state: &mut ListState,
148 visible_height: u32,
149 f: impl Fn(&mut Context, usize),
150 ) -> Response {
151 self.virtual_list_impl(state, visible_height, false, f)
152 }
153
154 pub fn virtual_list_variable(
191 &mut self,
192 state: &mut ListState,
193 visible_height: u32,
194 f: impl Fn(&mut Context, usize),
195 ) -> Response {
196 self.virtual_list_impl(state, visible_height, true, f)
197 }
198
199 fn virtual_list_impl(
200 &mut self,
201 state: &mut ListState,
202 visible_height: u32,
203 variable: bool,
204 f: impl Fn(&mut Context, usize),
205 ) -> Response {
206 if state.is_empty() {
207 return Response::none();
208 }
209 state.selected = state.selected.min(state.len().saturating_sub(1));
210 let use_heights = variable && state.has_item_heights();
211 let focused = self.register_focusable();
212 let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
213 let old_selected = state.selected;
214
215 if focused {
216 let mut consumed_indices = Vec::new();
217 for (i, key) in self.available_key_presses() {
218 match key.code {
219 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
220 let max_index = state.len().saturating_sub(1);
221 let _ =
222 handle_vertical_nav(&mut state.selected, max_index, key.code.clone());
223 consumed_indices.push(i);
224 }
225 KeyCode::PageUp => {
226 state.selected = if use_heights {
227 page_up_target(state, state.selected, visible_height)
228 } else {
229 state.selected.saturating_sub(visible_height as usize)
230 };
231 consumed_indices.push(i);
232 }
233 KeyCode::PageDown => {
234 state.selected = if use_heights {
235 page_down_target(state, state.selected, visible_height)
236 } else {
237 (state.selected + visible_height as usize)
238 .min(state.len().saturating_sub(1))
239 };
240 consumed_indices.push(i);
241 }
242 KeyCode::Home => {
243 state.selected = 0;
244 consumed_indices.push(i);
245 }
246 KeyCode::End => {
247 state.selected = state.len().saturating_sub(1);
248 consumed_indices.push(i);
249 }
250 _ => {}
251 }
252 }
253 self.consume_indices(consumed_indices);
254 }
255
256 let vh = visible_height as usize;
257 let (start, end) = if use_heights {
258 row_visible_range(state, vh)
259 } else {
260 if state.selected < state.viewport_offset {
266 state.viewport_offset = state.selected;
267 }
268 if vh > 0 && state.selected >= state.viewport_offset + vh {
269 state.viewport_offset = state.selected - vh + 1;
270 }
271 let start = state.viewport_offset;
272 (start, (start + vh).min(state.len()))
273 };
274
275 self.commands
276 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
277 direction: Direction::Column,
278 gap: 0,
279 align: Align::Start,
280 align_self: None,
281 justify: Justify::Start,
282 border: None,
283 border_sides: BorderSides::all(),
284 border_style: Style::new().fg(self.theme.border),
285 bg_color: None,
286 padding: Padding::default(),
287 margin: Margin::default(),
288 constraints: Constraints::default(),
289 title: None,
290 grow: 0,
291 group_name: None,
292 })));
293
294 if start > 0 {
295 let hidden = start.to_string();
296 let mut line = String::with_capacity(hidden.len() + 10);
297 line.push_str(" ↑ ");
298 line.push_str(&hidden);
299 line.push_str(" more");
300 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
301 }
302
303 for idx in start..end {
304 f(self, idx);
305 }
306
307 let remaining = state.len().saturating_sub(end);
308 if remaining > 0 {
309 let hidden = remaining.to_string();
310 let mut line = String::with_capacity(hidden.len() + 10);
311 line.push_str(" ↓ ");
312 line.push_str(&hidden);
313 line.push_str(" more");
314 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
315 }
316
317 self.commands.push(Command::EndContainer);
318 self.rollback.last_text_idx = None;
319 response.changed = state.selected != old_selected;
320 response
321 }
322
323 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Response {
327 if !state.open {
328 return Response::none();
329 }
330
331 state.last_selected = None;
332 let interaction_id = self.next_interaction_id();
333
334 let filtered: Vec<usize> = state.filtered_indices_cached().to_vec();
335 let sel = state.selected().min(filtered.len().saturating_sub(1));
336 state.set_selected(sel);
337
338 let mut consumed_indices = Vec::new();
339
340 for (i, key) in self.available_key_presses() {
341 match key.code {
342 KeyCode::Esc => {
343 state.open = false;
344 consumed_indices.push(i);
345 }
346 KeyCode::Up => {
347 let s = state.selected();
348 state.set_selected(s.saturating_sub(1));
349 consumed_indices.push(i);
350 }
351 KeyCode::Down => {
352 let filtered_len = state.filtered_indices_cached().len();
353 let s = state.selected();
354 state.set_selected((s + 1).min(filtered_len.saturating_sub(1)));
355 consumed_indices.push(i);
356 }
357 KeyCode::Enter => {
358 let filtered = state.filtered_indices_cached().to_vec();
359 if let Some(&cmd_idx) = filtered.get(state.selected()) {
360 state.last_selected = Some(cmd_idx);
361 state.open = false;
362 }
363 consumed_indices.push(i);
364 }
365 KeyCode::Backspace => {
366 if state.cursor > 0 {
367 let byte_idx = byte_index_for_grapheme(&state.input, state.cursor - 1);
368 let end_idx = byte_index_for_grapheme(&state.input, state.cursor);
369 state.input.replace_range(byte_idx..end_idx, "");
370 state.cursor -= 1;
371 state.set_selected(0);
372 }
373 consumed_indices.push(i);
374 }
375 KeyCode::Char(ch) if !has_global_shortcut_modifier(key.modifiers) => {
376 let byte_idx = byte_index_for_grapheme(&state.input, state.cursor);
377 state.input.insert(byte_idx, ch);
378 state.cursor = grapheme_count(&state.input[..byte_idx + ch.len_utf8()]);
379 state.set_selected(0);
380 consumed_indices.push(i);
381 }
382 _ => {}
383 }
384 }
385 for (i, text) in self.available_pastes() {
386 let inserted = text
387 .graphemes(true)
388 .filter(|cluster| {
389 cluster
390 .chars()
391 .all(|ch| (ch as u32) >= 0x20 && ch != '\u{7f}')
392 })
393 .collect::<String>();
394 if !inserted.is_empty() {
395 let byte_idx = byte_index_for_grapheme(&state.input, state.cursor);
396 let inserted_end = byte_idx + inserted.len();
397 state.input.insert_str(byte_idx, &inserted);
398 state.cursor = grapheme_count(&state.input[..inserted_end]);
399 state.set_selected(0);
400 }
401 consumed_indices.push(i);
402 }
403 self.consume_indices(consumed_indices);
404
405 let filtered: Vec<usize> = state.filtered_indices_cached().to_vec();
406
407 let _ = self.modal(|ui| {
408 let primary = ui.theme.primary;
409 let palette_pad = ui.theme.spacing.xs();
410 let palette_input_padx = ui.theme.spacing.xs();
411 let _ = ui
412 .container()
413 .border(Border::Rounded)
414 .border_style(Style::new().fg(primary))
415 .p(palette_pad)
416 .max_w(60)
417 .col(|ui| {
418 let border_color = ui.theme.primary;
419 let _ = ui
420 .bordered(Border::Rounded)
421 .border_style(Style::new().fg(border_color))
422 .px(palette_input_padx)
423 .col(|ui| {
424 let display = if state.input.is_empty() {
425 "Type to search...".to_string()
426 } else {
427 state.input.clone()
428 };
429 let style = if state.input.is_empty() {
430 Style::new().dim().fg(ui.theme.text_dim)
431 } else {
432 Style::new().fg(ui.theme.text)
433 };
434 ui.styled(display, style);
435 });
436
437 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
438 let cmd = &state.commands()[cmd_idx];
439 let is_selected = list_idx == state.selected();
440 let style = if is_selected {
441 Style::new().bold().fg(ui.theme.primary)
442 } else {
443 Style::new().fg(ui.theme.text)
444 };
445 let prefix = if is_selected { "▸ " } else { " " };
446 let shortcut_text = cmd
447 .shortcut
448 .as_deref()
449 .map(|s| {
450 let mut text = String::with_capacity(s.len() + 4);
451 text.push_str(" (");
452 text.push_str(s);
453 text.push(')');
454 text
455 })
456 .unwrap_or_default();
457 let mut line = String::with_capacity(
458 prefix.len() + cmd.label.len() + shortcut_text.len(),
459 );
460 line.push_str(prefix);
461 line.push_str(&cmd.label);
462 line.push_str(&shortcut_text);
463 ui.styled(line, style);
464 if is_selected && !cmd.description.is_empty() {
465 let mut desc = String::with_capacity(4 + cmd.description.len());
466 desc.push_str(" ");
467 desc.push_str(&cmd.description);
468 ui.styled(desc, Style::new().dim().fg(ui.theme.text_dim));
469 }
470 }
471
472 if filtered.is_empty() {
473 ui.styled(
474 " No matching commands",
475 Style::new().dim().fg(ui.theme.text_dim),
476 );
477 }
478 });
479 });
480
481 let mut response = self.response_for(interaction_id);
482 response.changed = state.last_selected.is_some();
483 response
484 }
485
486 pub fn markdown(&mut self, text: &str) -> Response {
496 self.commands
497 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
498 direction: Direction::Column,
499 gap: 0,
500 align: Align::Start,
501 align_self: None,
502 justify: Justify::Start,
503 border: None,
504 border_sides: BorderSides::all(),
505 border_style: Style::new().fg(self.theme.border),
506 bg_color: None,
507 padding: Padding::default(),
508 margin: Margin::default(),
509 constraints: Constraints::default(),
510 title: None,
511 grow: 0,
512 group_name: None,
513 })));
514 self.skip_interaction_slot();
515
516 let text_style = Style::new().fg(self.theme.text);
517 let bold_style = Style::new().fg(self.theme.text).bold();
518 let code_style = Style::new().fg(self.theme.accent);
519 let border_style = Style::new().fg(self.theme.border).dim();
520
521 let mut in_code_block = false;
522 let mut code_block_lang = String::new();
523 let mut code_block_lines: Vec<String> = Vec::new();
524 let mut table_lines: Vec<String> = Vec::new();
525
526 for line in text.lines() {
527 let trimmed = line.trim();
528
529 if in_code_block {
530 if trimmed.starts_with("```") {
531 in_code_block = false;
532 let code_content = code_block_lines.join("\n");
533 let theme = self.theme;
534 let code_pad = theme.spacing.xs();
535 let highlighted: Option<Vec<Vec<(String, Style)>>> =
536 crate::syntax::highlight_code(&code_content, &code_block_lang, &theme);
537 let _ = self.container().bg(theme.surface).p(code_pad).col(|ui| {
538 if let Some(ref hl_lines) = highlighted {
539 for segs in hl_lines {
540 if segs.is_empty() {
541 ui.text(" ");
542 } else {
543 ui.line(|ui| {
544 for (t, s) in segs {
545 ui.styled(t, *s);
546 }
547 });
548 }
549 }
550 } else {
551 for cl in &code_block_lines {
552 ui.styled(cl, code_style);
553 }
554 }
555 });
556 code_block_lang.clear();
557 code_block_lines.clear();
558 } else {
559 code_block_lines.push(line.to_string());
560 }
561 continue;
562 }
563
564 if trimmed.starts_with('|') && trimmed.matches('|').count() >= 2 {
566 table_lines.push(trimmed.to_string());
567 continue;
568 }
569 if !table_lines.is_empty() {
571 self.render_markdown_table(
572 &table_lines,
573 text_style,
574 bold_style,
575 code_style,
576 border_style,
577 );
578 table_lines.clear();
579 }
580
581 if trimmed.is_empty() {
582 self.text(" ");
583 continue;
584 }
585 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
586 self.styled("─".repeat(40), border_style);
587 continue;
588 }
589 if let Some(quote) = trimmed.strip_prefix("> ") {
590 let quote_style = Style::new().fg(self.theme.text_dim).italic();
591 let bar_style = Style::new().fg(self.theme.border);
592 self.line(|ui| {
593 ui.styled("│ ", bar_style);
594 ui.styled(quote, quote_style);
595 });
596 } else if let Some(heading) = trimmed.strip_prefix("### ") {
597 self.styled(heading, Style::new().bold().fg(self.theme.accent));
598 } else if let Some(heading) = trimmed.strip_prefix("## ") {
599 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
600 } else if let Some(heading) = trimmed.strip_prefix("# ") {
601 self.styled(heading, Style::new().bold().fg(self.theme.primary));
602 } else if let Some(item) = trimmed
603 .strip_prefix("- ")
604 .or_else(|| trimmed.strip_prefix("* "))
605 {
606 self.line_wrap(|ui| {
607 ui.styled(" • ", text_style);
608 Self::render_md_inline_into(ui, item, text_style, bold_style, code_style);
609 });
610 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
611 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
612 if parts.len() == 2 {
613 self.line_wrap(|ui| {
614 let mut prefix = String::with_capacity(4 + parts[0].len());
615 prefix.push_str(" ");
616 prefix.push_str(parts[0]);
617 prefix.push_str(". ");
618 ui.styled(prefix, text_style);
619 Self::render_md_inline_into(
620 ui, parts[1], text_style, bold_style, code_style,
621 );
622 });
623 } else {
624 self.text(trimmed);
625 }
626 } else if let Some(lang) = trimmed.strip_prefix("```") {
627 in_code_block = true;
628 code_block_lang = lang.trim().to_string();
629 } else {
630 self.render_md_inline(trimmed, text_style, bold_style, code_style);
631 }
632 }
633
634 if in_code_block && !code_block_lines.is_empty() {
635 for cl in &code_block_lines {
636 self.styled(cl, code_style);
637 }
638 }
639
640 if !table_lines.is_empty() {
642 self.render_markdown_table(
643 &table_lines,
644 text_style,
645 bold_style,
646 code_style,
647 border_style,
648 );
649 }
650
651 self.commands.push(Command::EndContainer);
652 self.rollback.last_text_idx = None;
653 Response::none()
654 }
655
656 fn render_markdown_table(
658 &mut self,
659 lines: &[String],
660 text_style: Style,
661 bold_style: Style,
662 code_style: Style,
663 border_style: Style,
664 ) {
665 if lines.is_empty() {
666 return;
667 }
668
669 let is_separator = |line: &str| -> bool {
671 let inner = line.trim_matches('|').trim();
672 !inner.is_empty()
673 && inner
674 .chars()
675 .all(|c| c == '-' || c == ':' || c == '|' || c == ' ')
676 };
677
678 let parse_row = |line: &str| -> Vec<String> {
679 let trimmed = line.trim().trim_start_matches('|').trim_end_matches('|');
680 trimmed.split('|').map(|c| c.trim().to_string()).collect()
681 };
682
683 let mut header: Option<Vec<String>> = None;
684 let mut data_rows: Vec<Vec<String>> = Vec::new();
685 let mut found_separator = false;
686
687 for (i, line) in lines.iter().enumerate() {
688 if is_separator(line) {
689 found_separator = true;
690 continue;
691 }
692 if i == 0 && !found_separator {
693 header = Some(parse_row(line));
694 } else {
695 data_rows.push(parse_row(line));
696 }
697 }
698
699 if !found_separator && header.is_none() && !data_rows.is_empty() {
701 header = Some(data_rows.remove(0));
702 }
703
704 let all_rows: Vec<&Vec<String>> = header.iter().chain(data_rows.iter()).collect();
706 let col_count = all_rows.iter().map(|r| r.len()).max().unwrap_or(0);
707 if col_count == 0 {
708 return;
709 }
710 let mut col_widths = vec![0usize; col_count];
711 let stripped_rows: Vec<Vec<String>> = all_rows
713 .iter()
714 .map(|row| row.iter().map(|c| Self::md_strip(c)).collect())
715 .collect();
716 for row in &stripped_rows {
717 for (i, cell) in row.iter().enumerate() {
718 if i < col_count {
719 col_widths[i] = col_widths[i].max(UnicodeWidthStr::width(cell.as_str()));
720 }
721 }
722 }
723
724 let mut top = String::from("┌");
726 for (i, &w) in col_widths.iter().enumerate() {
727 for _ in 0..w + 2 {
728 top.push('─');
729 }
730 top.push(if i < col_count - 1 { '┬' } else { '┐' });
731 }
732 self.styled(&top, border_style);
733
734 if let Some(ref hdr) = header {
736 self.line(|ui| {
737 ui.styled("│", border_style);
738 for (i, w) in col_widths.iter().enumerate() {
739 let raw = hdr.get(i).map(String::as_str).unwrap_or("");
740 let display_text = Self::md_strip(raw);
741 let cell_w = UnicodeWidthStr::width(display_text.as_str());
742 let padding: String = " ".repeat(w.saturating_sub(cell_w));
743 ui.styled(" ", bold_style);
744 ui.styled(&display_text, bold_style);
745 ui.styled(padding, bold_style);
746 ui.styled(" │", border_style);
747 }
748 });
749
750 let mut sep = String::from("├");
752 for (i, &w) in col_widths.iter().enumerate() {
753 for _ in 0..w + 2 {
754 sep.push('─');
755 }
756 sep.push(if i < col_count - 1 { '┼' } else { '┤' });
757 }
758 self.styled(&sep, border_style);
759 }
760
761 for row in &data_rows {
763 self.line(|ui| {
764 ui.styled("│", border_style);
765 for (i, w) in col_widths.iter().enumerate() {
766 let raw = row.get(i).map(String::as_str).unwrap_or("");
767 let display_text = Self::md_strip(raw);
768 let cell_w = UnicodeWidthStr::width(display_text.as_str());
769 let padding: String = " ".repeat(w.saturating_sub(cell_w));
770 ui.styled(" ", text_style);
771 Self::render_md_inline_into(ui, raw, text_style, bold_style, code_style);
772 ui.styled(padding, text_style);
773 ui.styled(" │", border_style);
774 }
775 });
776 }
777
778 let mut bot = String::from("└");
780 for (i, &w) in col_widths.iter().enumerate() {
781 for _ in 0..w + 2 {
782 bot.push('─');
783 }
784 bot.push(if i < col_count - 1 { '┴' } else { '┘' });
785 }
786 self.styled(&bot, border_style);
787 }
788
789 pub(crate) fn parse_inline_segments(
790 text: &str,
791 base: Style,
792 bold: Style,
793 code: Style,
794 ) -> Vec<(String, Style)> {
795 let mut segments: Vec<(String, Style)> = Vec::new();
800 let bytes = text.as_bytes();
801 let mut current = String::new();
802 let mut i: usize = 0;
803
804 while i < bytes.len() {
805 if bytes[i] == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
807 let after_open = i + 2;
808 if let Some(rel_end) = text[after_open..].find("**") {
809 let close = after_open + rel_end;
810 if !current.is_empty() {
811 segments.push((std::mem::take(&mut current), base));
812 }
813 let inner = text[after_open..close].to_string();
814 segments.push((inner, bold));
815 i = close + 2;
816 continue;
817 }
818 }
819
820 if bytes[i] == b'*'
822 && (i + 1 >= bytes.len() || bytes[i + 1] != b'*')
823 && (i == 0 || bytes[i - 1] != b'*')
824 {
825 let after_open = i + 1;
826 if let Some(rel_end) = text[after_open..].find('*') {
827 let close = after_open + rel_end;
828 if !current.is_empty() {
829 segments.push((std::mem::take(&mut current), base));
830 }
831 let inner = text[after_open..close].to_string();
832 segments.push((inner, base.italic()));
833 i = close + 1;
834 continue;
835 }
836 }
837
838 if bytes[i] == b'`' {
840 let after_open = i + 1;
841 if let Some(rel_end) = text[after_open..].find('`') {
842 let close = after_open + rel_end;
843 if !current.is_empty() {
844 segments.push((std::mem::take(&mut current), base));
845 }
846 let inner = text[after_open..close].to_string();
847 segments.push((inner, code));
848 i = close + 1;
849 continue;
850 }
851 }
852
853 let ch = text[i..]
856 .chars()
857 .next()
858 .expect("non-empty tail past bounds check");
859 current.push(ch);
860 i += ch.len_utf8();
861 }
862
863 if !current.is_empty() {
864 segments.push((current, base));
865 }
866 segments
867 }
868
869 fn render_md_inline(
874 &mut self,
875 text: &str,
876 text_style: Style,
877 bold_style: Style,
878 code_style: Style,
879 ) {
880 let items = Self::split_md_links(text);
881
882 if items.len() == 1
884 && let MdInline::Text(ref t) = items[0]
885 {
886 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
887 if segs.len() <= 1 {
888 self.text(text)
889 .wrap()
890 .fg(text_style.fg.unwrap_or(Color::Reset));
891 } else {
892 self.line_wrap(|ui| {
893 for (s, st) in segs {
894 ui.styled(s, st);
895 }
896 });
897 }
898 return;
899 }
900
901 self.line_wrap(|ui| {
903 for item in &items {
904 match item {
905 MdInline::Text(t) => {
906 let segs =
907 Self::parse_inline_segments(t, text_style, bold_style, code_style);
908 for (s, st) in segs {
909 ui.styled(s, st);
910 }
911 }
912 MdInline::Link { text, url } => {
913 ui.link(text.clone(), url.clone());
914 }
915 MdInline::Image { alt, .. } => {
916 ui.styled(alt.as_str(), code_style);
918 }
919 }
920 }
921 });
922 }
923
924 fn render_md_inline_into(
930 ui: &mut Context,
931 text: &str,
932 text_style: Style,
933 bold_style: Style,
934 code_style: Style,
935 ) {
936 let items = Self::split_md_links(text);
937 for item in &items {
938 match item {
939 MdInline::Text(t) => {
940 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
941 for (s, st) in segs {
942 ui.styled(s, st);
943 }
944 }
945 MdInline::Link { text, url } => {
946 ui.link(text.clone(), url.clone());
947 }
948 MdInline::Image { alt, .. } => {
949 ui.styled(alt.as_str(), code_style);
950 }
951 }
952 }
953 }
954
955 fn split_md_links(text: &str) -> Vec<MdInline> {
957 let chars: Vec<char> = text.chars().collect();
958 let mut items: Vec<MdInline> = Vec::new();
959 let mut current = String::new();
960 let mut i = 0;
961
962 while i < chars.len() {
963 if chars[i] == '!'
965 && i + 1 < chars.len()
966 && chars[i + 1] == '['
967 && let Some((alt, _url, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1)
968 {
969 if !current.is_empty() {
970 items.push(MdInline::Text(std::mem::take(&mut current)));
971 }
972 items.push(MdInline::Image { alt });
973 i += 1 + consumed;
974 continue;
975 }
976 if chars[i] == '['
978 && let Some((link_text, url, consumed)) = Self::parse_md_bracket_paren(&chars, i)
979 {
980 if !current.is_empty() {
981 items.push(MdInline::Text(std::mem::take(&mut current)));
982 }
983 items.push(MdInline::Link {
984 text: link_text,
985 url,
986 });
987 i += consumed;
988 continue;
989 }
990 current.push(chars[i]);
991 i += 1;
992 }
993 if !current.is_empty() {
994 items.push(MdInline::Text(current));
995 }
996 if items.is_empty() {
997 items.push(MdInline::Text(String::new()));
998 }
999 items
1000 }
1001
1002 fn parse_md_bracket_paren(chars: &[char], start: usize) -> Option<(String, String, usize)> {
1005 if start >= chars.len() || chars[start] != '[' {
1006 return None;
1007 }
1008 let mut depth = 0i32;
1010 let mut bracket_end = None;
1011 for (j, &ch) in chars.iter().enumerate().skip(start) {
1012 if ch == '[' {
1013 depth += 1;
1014 } else if ch == ']' {
1015 depth -= 1;
1016 if depth == 0 {
1017 bracket_end = Some(j);
1018 break;
1019 }
1020 }
1021 }
1022 let bracket_end = bracket_end?;
1023 if bracket_end + 1 >= chars.len() || chars[bracket_end + 1] != '(' {
1025 return None;
1026 }
1027 let paren_start = bracket_end + 2;
1029 let mut paren_end = None;
1030 let mut paren_depth = 1i32;
1031 for (j, &ch) in chars.iter().enumerate().skip(paren_start) {
1032 if ch == '(' {
1033 paren_depth += 1;
1034 } else if ch == ')' {
1035 paren_depth -= 1;
1036 if paren_depth == 0 {
1037 paren_end = Some(j);
1038 break;
1039 }
1040 }
1041 }
1042 let paren_end = paren_end?;
1043 let text: String = chars[start + 1..bracket_end].iter().collect();
1044 let url: String = chars[paren_start..paren_end].iter().collect();
1045 let consumed = paren_end - start + 1;
1046 Some((text, url, consumed))
1047 }
1048
1049 fn md_strip(text: &str) -> String {
1054 let chars: Vec<char> = text.chars().collect();
1058 let char_to_byte = {
1059 let mut v = Vec::with_capacity(chars.len() + 1);
1060 let mut acc = 0usize;
1061 v.push(0);
1062 for ch in &chars {
1063 acc += ch.len_utf8();
1064 v.push(acc);
1065 }
1066 v
1067 };
1068 let bytes = text.as_bytes();
1069 let mut result = String::with_capacity(text.len());
1070 let mut ci: usize = 0;
1071
1072 while ci < chars.len() {
1073 if chars[ci] == '!'
1075 && ci + 1 < chars.len()
1076 && chars[ci + 1] == '['
1077 && let Some((alt, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci + 1)
1078 {
1079 result.push_str(&alt);
1080 ci += 1 + consumed;
1081 continue;
1082 }
1083 if chars[ci] == '['
1085 && let Some((link_text, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci)
1086 {
1087 result.push_str(&link_text);
1088 ci += consumed;
1089 continue;
1090 }
1091
1092 let bi = char_to_byte[ci];
1093
1094 if bytes[bi] == b'*' && bi + 1 < bytes.len() && bytes[bi + 1] == b'*' {
1096 let after_open = bi + 2;
1097 if let Some(rel_end) = text[after_open..].find("**") {
1098 let close = after_open + rel_end;
1099 let inner = &text[after_open..close];
1100 result.push_str(inner);
1101 ci += 2 + inner.chars().count() + 2;
1102 continue;
1103 }
1104 }
1105
1106 if bytes[bi] == b'*'
1108 && (bi + 1 >= bytes.len() || bytes[bi + 1] != b'*')
1109 && (bi == 0 || bytes[bi - 1] != b'*')
1110 {
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 if bytes[bi] == b'`' {
1123 let after_open = bi + 1;
1124 if let Some(rel_end) = text[after_open..].find('`') {
1125 let close = after_open + rel_end;
1126 let inner = &text[after_open..close];
1127 result.push_str(inner);
1128 ci += 1 + inner.chars().count() + 1;
1129 continue;
1130 }
1131 }
1132
1133 result.push(chars[ci]);
1134 ci += 1;
1135 }
1136 result
1137 }
1138
1139 pub fn key_chord(&mut self, seq: &str) -> bool {
1180 self.key_chord_timeout(seq, DEFAULT_CHORD_TIMEOUT_TICKS)
1181 }
1182
1183 pub fn key_chord_timeout(&mut self, seq: &str, timeout_ticks: u64) -> bool {
1202 let target = parse_chord(seq);
1203 if target.is_empty() {
1204 return false;
1205 }
1206 if (self.rollback.modal_active || self.prev_modal_active)
1209 && self.rollback.overlay_depth == 0
1210 {
1211 return false;
1212 }
1213
1214 if self.tick.saturating_sub(self.chord.last_tick) > timeout_ticks {
1216 self.chord.pending.clear();
1217 }
1218
1219 let char_presses: Vec<(usize, char)> = self
1223 .available_key_presses()
1224 .filter_map(|(i, key)| match key.code {
1225 KeyCode::Char(c) => Some((i, c)),
1226 _ => None,
1227 })
1228 .collect();
1229
1230 let tick = self.tick;
1231 let mut completed_index: Option<usize> = None;
1232 let mut buf: Vec<char> = self.chord.pending.chars().collect();
1233
1234 for (i, c) in char_presses {
1235 buf.push(c);
1236 retain_longest_prefix(&mut buf, &target);
1240 self.chord.last_tick = tick;
1241 if buf.len() == target.len() {
1242 completed_index = Some(i);
1243 buf.clear();
1244 break;
1245 }
1246 }
1247
1248 self.chord.pending = buf.into_iter().collect();
1249 if let Some(i) = completed_index {
1250 self.consume_indices([i]);
1251 true
1252 } else {
1253 false
1254 }
1255 }
1256
1257 #[deprecated(
1265 since = "0.21.0",
1266 note = "renamed to `key_chord`; now matches across frames"
1267 )]
1268 pub fn key_seq(&mut self, seq: &str) -> bool {
1269 self.key_chord(seq)
1270 }
1271}
1272
1273fn parse_chord(seq: &str) -> Vec<char> {
1278 let mut out = Vec::new();
1279 let mut rest = seq;
1280 while !rest.is_empty() {
1281 if let Some(tail) = rest.strip_prefix("<space>") {
1282 out.push(' ');
1283 rest = tail;
1284 } else if let Some(tail) = rest.strip_prefix("<leader>") {
1285 out.push(' ');
1286 rest = tail;
1287 } else {
1288 let c = rest.chars().next().expect("rest is non-empty");
1289 out.push(c);
1290 rest = &rest[c.len_utf8()..];
1291 }
1292 }
1293 out
1294}
1295
1296fn retain_longest_prefix(buf: &mut Vec<char>, target: &[char]) {
1304 let mut start = 0;
1307 while start < buf.len() {
1308 if buf[start..].iter().zip(target).all(|(b, t)| b == t) {
1309 break;
1310 }
1311 start += 1;
1312 }
1313 if start > 0 {
1314 buf.drain(0..start);
1315 }
1316}
1317
1318fn item_at_row(row_prefix: &[u32], target_row: u32, n: usize) -> usize {
1328 if n == 0 {
1332 return 0;
1333 }
1334 let count = row_prefix.partition_point(|&r| r <= target_row);
1335 count.saturating_sub(1).min(n - 1)
1336}
1337
1338fn row_visible_range(state: &mut ListState, vh: usize) -> (usize, usize) {
1347 state.ensure_row_prefix();
1348 let n = state.len();
1349 if n == 0 || vh == 0 {
1350 state.viewport_offset = state.viewport_offset.min(n.saturating_sub(1));
1351 state.viewport_row_offset = 0;
1352 return (state.viewport_offset, state.viewport_offset);
1353 }
1354
1355 let vh_rows = vh as u32;
1356 let row_prefix = state.row_prefix();
1357 let sel = state.selected.min(n - 1);
1359 let sel_top = row_prefix[sel];
1360 let sel_bottom = row_prefix[sel + 1]; let mut top = state.viewport_offset.min(n - 1);
1363
1364 if sel_top < row_prefix[top] {
1367 top = sel;
1368 }
1369
1370 while top < sel && sel_bottom.saturating_sub(row_prefix[top]) > vh_rows {
1375 top += 1;
1376 }
1377
1378 let top_row = row_prefix[top];
1383 let target_bottom = top_row.saturating_add(vh_rows);
1384 let end = row_prefix
1392 .partition_point(|&r| r <= target_bottom)
1393 .saturating_sub(1)
1394 .clamp(top + 1, n);
1395
1396 state.viewport_offset = top;
1397 state.viewport_row_offset = top_row as usize;
1398 (top, end)
1399}
1400
1401fn page_down_target(state: &mut ListState, from: usize, visible_height: u32) -> usize {
1405 state.ensure_row_prefix();
1406 let n = state.len();
1407 if n == 0 {
1408 return 0;
1409 }
1410 let from = from.min(n - 1);
1411 let row_prefix = state.row_prefix();
1412 let from_top = row_prefix[from];
1413 let target = from_top.saturating_add(visible_height.max(1));
1414 let next = item_at_row(row_prefix, target, n);
1415 next.max(from + 1).min(n - 1)
1416}
1417
1418fn page_up_target(state: &mut ListState, from: usize, visible_height: u32) -> usize {
1422 state.ensure_row_prefix();
1423 let n = state.len();
1424 if n == 0 {
1425 return 0;
1426 }
1427 let from = from.min(n - 1);
1428 let row_prefix = state.row_prefix();
1429 let from_bottom = row_prefix[from + 1];
1430 let target = from_bottom.saturating_sub(visible_height.max(1));
1431 let prev = item_at_row(row_prefix, target, n);
1432 prev.min(from.saturating_sub(1))
1433}