1use super::*;
2use crate::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(
152 &mut self,
153 state: &mut ListState,
154 visible_height: u32,
155 f: impl Fn(&mut Context, usize),
156 ) -> Response {
157 if state.items.is_empty() {
158 return Response::none();
159 }
160 state.selected = state.selected.min(state.items.len().saturating_sub(1));
161 let focused = self.register_focusable();
162 let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
163 let old_selected = state.selected;
164
165 if focused {
166 let mut consumed_indices = Vec::new();
167 for (i, key) in self.available_key_presses() {
168 match key.code {
169 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
170 let _ = handle_vertical_nav(
171 &mut state.selected,
172 state.items.len().saturating_sub(1),
173 key.code.clone(),
174 );
175 consumed_indices.push(i);
176 }
177 KeyCode::PageUp => {
178 state.selected = state.selected.saturating_sub(visible_height as usize);
179 consumed_indices.push(i);
180 }
181 KeyCode::PageDown => {
182 state.selected = (state.selected + visible_height as usize)
183 .min(state.items.len().saturating_sub(1));
184 consumed_indices.push(i);
185 }
186 KeyCode::Home => {
187 state.selected = 0;
188 consumed_indices.push(i);
189 }
190 KeyCode::End => {
191 state.selected = state.items.len().saturating_sub(1);
192 consumed_indices.push(i);
193 }
194 _ => {}
195 }
196 }
197 self.consume_indices(consumed_indices);
198 }
199
200 let vh = visible_height as usize;
201 if state.selected < state.viewport_offset {
204 state.viewport_offset = state.selected;
205 }
206 if vh > 0 && state.selected >= state.viewport_offset + vh {
207 state.viewport_offset = state.selected - vh + 1;
208 }
209 let start = state.viewport_offset;
210 let end = (start + vh).min(state.items.len());
211
212 self.commands
213 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
214 direction: Direction::Column,
215 gap: 0,
216 align: Align::Start,
217 align_self: None,
218 justify: Justify::Start,
219 border: None,
220 border_sides: BorderSides::all(),
221 border_style: Style::new().fg(self.theme.border),
222 bg_color: None,
223 padding: Padding::default(),
224 margin: Margin::default(),
225 constraints: Constraints::default(),
226 title: None,
227 grow: 0,
228 group_name: None,
229 })));
230
231 if start > 0 {
232 let hidden = start.to_string();
233 let mut line = String::with_capacity(hidden.len() + 10);
234 line.push_str(" ↑ ");
235 line.push_str(&hidden);
236 line.push_str(" more");
237 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
238 }
239
240 for idx in start..end {
241 f(self, idx);
242 }
243
244 let remaining = state.items.len().saturating_sub(end);
245 if remaining > 0 {
246 let hidden = remaining.to_string();
247 let mut line = String::with_capacity(hidden.len() + 10);
248 line.push_str(" ↓ ");
249 line.push_str(&hidden);
250 line.push_str(" more");
251 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
252 }
253
254 self.commands.push(Command::EndContainer);
255 self.rollback.last_text_idx = None;
256 response.changed = state.selected != old_selected;
257 response
258 }
259
260 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Response {
264 if !state.open {
265 return Response::none();
266 }
267
268 state.last_selected = None;
269 let interaction_id = self.next_interaction_id();
270
271 let filtered: Vec<usize> = state.filtered_indices_cached().to_vec();
272 let sel = state.selected().min(filtered.len().saturating_sub(1));
273 state.set_selected(sel);
274
275 let mut consumed_indices = Vec::new();
276
277 for (i, key) in self.available_key_presses() {
278 match key.code {
279 KeyCode::Esc => {
280 state.open = false;
281 consumed_indices.push(i);
282 }
283 KeyCode::Up => {
284 let s = state.selected();
285 state.set_selected(s.saturating_sub(1));
286 consumed_indices.push(i);
287 }
288 KeyCode::Down => {
289 let s = state.selected();
290 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
291 consumed_indices.push(i);
292 }
293 KeyCode::Enter => {
294 if let Some(&cmd_idx) = filtered.get(state.selected()) {
295 state.last_selected = Some(cmd_idx);
296 state.open = false;
297 }
298 consumed_indices.push(i);
299 }
300 KeyCode::Backspace => {
301 if state.cursor > 0 {
302 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
303 let end_idx = byte_index_for_char(&state.input, state.cursor);
304 state.input.replace_range(byte_idx..end_idx, "");
305 state.cursor -= 1;
306 state.set_selected(0);
307 }
308 consumed_indices.push(i);
309 }
310 KeyCode::Char(ch) => {
311 let byte_idx = byte_index_for_char(&state.input, state.cursor);
312 state.input.insert(byte_idx, ch);
313 state.cursor += 1;
314 state.set_selected(0);
315 consumed_indices.push(i);
316 }
317 _ => {}
318 }
319 }
320 self.consume_indices(consumed_indices);
321
322 let filtered: Vec<usize> = state.filtered_indices_cached().to_vec();
323
324 let _ = self.modal(|ui| {
325 let primary = ui.theme.primary;
326 let palette_pad = ui.theme.spacing.xs();
327 let palette_input_padx = ui.theme.spacing.xs();
328 let _ = ui
329 .container()
330 .border(Border::Rounded)
331 .border_style(Style::new().fg(primary))
332 .p(palette_pad)
333 .max_w(60)
334 .col(|ui| {
335 let border_color = ui.theme.primary;
336 let _ = ui
337 .bordered(Border::Rounded)
338 .border_style(Style::new().fg(border_color))
339 .px(palette_input_padx)
340 .col(|ui| {
341 let display = if state.input.is_empty() {
342 "Type to search...".to_string()
343 } else {
344 state.input.clone()
345 };
346 let style = if state.input.is_empty() {
347 Style::new().dim().fg(ui.theme.text_dim)
348 } else {
349 Style::new().fg(ui.theme.text)
350 };
351 ui.styled(display, style);
352 });
353
354 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
355 let cmd = &state.commands[cmd_idx];
356 let is_selected = list_idx == state.selected();
357 let style = if is_selected {
358 Style::new().bold().fg(ui.theme.primary)
359 } else {
360 Style::new().fg(ui.theme.text)
361 };
362 let prefix = if is_selected { "▸ " } else { " " };
363 let shortcut_text = cmd
364 .shortcut
365 .as_deref()
366 .map(|s| {
367 let mut text = String::with_capacity(s.len() + 4);
368 text.push_str(" (");
369 text.push_str(s);
370 text.push(')');
371 text
372 })
373 .unwrap_or_default();
374 let mut line = String::with_capacity(
375 prefix.len() + cmd.label.len() + shortcut_text.len(),
376 );
377 line.push_str(prefix);
378 line.push_str(&cmd.label);
379 line.push_str(&shortcut_text);
380 ui.styled(line, style);
381 if is_selected && !cmd.description.is_empty() {
382 let mut desc = String::with_capacity(4 + cmd.description.len());
383 desc.push_str(" ");
384 desc.push_str(&cmd.description);
385 ui.styled(desc, Style::new().dim().fg(ui.theme.text_dim));
386 }
387 }
388
389 if filtered.is_empty() {
390 ui.styled(
391 " No matching commands",
392 Style::new().dim().fg(ui.theme.text_dim),
393 );
394 }
395 });
396 });
397
398 let mut response = self.response_for(interaction_id);
399 response.changed = state.last_selected.is_some();
400 response
401 }
402
403 pub fn markdown(&mut self, text: &str) -> Response {
413 self.commands
414 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
415 direction: Direction::Column,
416 gap: 0,
417 align: Align::Start,
418 align_self: None,
419 justify: Justify::Start,
420 border: None,
421 border_sides: BorderSides::all(),
422 border_style: Style::new().fg(self.theme.border),
423 bg_color: None,
424 padding: Padding::default(),
425 margin: Margin::default(),
426 constraints: Constraints::default(),
427 title: None,
428 grow: 0,
429 group_name: None,
430 })));
431 self.skip_interaction_slot();
432
433 let text_style = Style::new().fg(self.theme.text);
434 let bold_style = Style::new().fg(self.theme.text).bold();
435 let code_style = Style::new().fg(self.theme.accent);
436 let border_style = Style::new().fg(self.theme.border).dim();
437
438 let mut in_code_block = false;
439 let mut code_block_lang = String::new();
440 let mut code_block_lines: Vec<String> = Vec::new();
441 let mut table_lines: Vec<String> = Vec::new();
442
443 for line in text.lines() {
444 let trimmed = line.trim();
445
446 if in_code_block {
447 if trimmed.starts_with("```") {
448 in_code_block = false;
449 let code_content = code_block_lines.join("\n");
450 let theme = self.theme;
451 let code_pad = theme.spacing.xs();
452 let highlighted: Option<Vec<Vec<(String, Style)>>> =
453 crate::syntax::highlight_code(&code_content, &code_block_lang, &theme);
454 let _ = self.container().bg(theme.surface).p(code_pad).col(|ui| {
455 if let Some(ref hl_lines) = highlighted {
456 for segs in hl_lines {
457 if segs.is_empty() {
458 ui.text(" ");
459 } else {
460 ui.line(|ui| {
461 for (t, s) in segs {
462 ui.styled(t, *s);
463 }
464 });
465 }
466 }
467 } else {
468 for cl in &code_block_lines {
469 ui.styled(cl, code_style);
470 }
471 }
472 });
473 code_block_lang.clear();
474 code_block_lines.clear();
475 } else {
476 code_block_lines.push(line.to_string());
477 }
478 continue;
479 }
480
481 if trimmed.starts_with('|') && trimmed.matches('|').count() >= 2 {
483 table_lines.push(trimmed.to_string());
484 continue;
485 }
486 if !table_lines.is_empty() {
488 self.render_markdown_table(
489 &table_lines,
490 text_style,
491 bold_style,
492 code_style,
493 border_style,
494 );
495 table_lines.clear();
496 }
497
498 if trimmed.is_empty() {
499 self.text(" ");
500 continue;
501 }
502 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
503 self.styled("─".repeat(40), border_style);
504 continue;
505 }
506 if let Some(quote) = trimmed.strip_prefix("> ") {
507 let quote_style = Style::new().fg(self.theme.text_dim).italic();
508 let bar_style = Style::new().fg(self.theme.border);
509 self.line(|ui| {
510 ui.styled("│ ", bar_style);
511 ui.styled(quote, quote_style);
512 });
513 } else if let Some(heading) = trimmed.strip_prefix("### ") {
514 self.styled(heading, Style::new().bold().fg(self.theme.accent));
515 } else if let Some(heading) = trimmed.strip_prefix("## ") {
516 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
517 } else if let Some(heading) = trimmed.strip_prefix("# ") {
518 self.styled(heading, Style::new().bold().fg(self.theme.primary));
519 } else if let Some(item) = trimmed
520 .strip_prefix("- ")
521 .or_else(|| trimmed.strip_prefix("* "))
522 {
523 self.line_wrap(|ui| {
524 ui.styled(" • ", text_style);
525 Self::render_md_inline_into(ui, item, text_style, bold_style, code_style);
526 });
527 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
528 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
529 if parts.len() == 2 {
530 self.line_wrap(|ui| {
531 let mut prefix = String::with_capacity(4 + parts[0].len());
532 prefix.push_str(" ");
533 prefix.push_str(parts[0]);
534 prefix.push_str(". ");
535 ui.styled(prefix, text_style);
536 Self::render_md_inline_into(
537 ui, parts[1], text_style, bold_style, code_style,
538 );
539 });
540 } else {
541 self.text(trimmed);
542 }
543 } else if let Some(lang) = trimmed.strip_prefix("```") {
544 in_code_block = true;
545 code_block_lang = lang.trim().to_string();
546 } else {
547 self.render_md_inline(trimmed, text_style, bold_style, code_style);
548 }
549 }
550
551 if in_code_block && !code_block_lines.is_empty() {
552 for cl in &code_block_lines {
553 self.styled(cl, code_style);
554 }
555 }
556
557 if !table_lines.is_empty() {
559 self.render_markdown_table(
560 &table_lines,
561 text_style,
562 bold_style,
563 code_style,
564 border_style,
565 );
566 }
567
568 self.commands.push(Command::EndContainer);
569 self.rollback.last_text_idx = None;
570 Response::none()
571 }
572
573 fn render_markdown_table(
575 &mut self,
576 lines: &[String],
577 text_style: Style,
578 bold_style: Style,
579 code_style: Style,
580 border_style: Style,
581 ) {
582 if lines.is_empty() {
583 return;
584 }
585
586 let is_separator = |line: &str| -> bool {
588 let inner = line.trim_matches('|').trim();
589 !inner.is_empty()
590 && inner
591 .chars()
592 .all(|c| c == '-' || c == ':' || c == '|' || c == ' ')
593 };
594
595 let parse_row = |line: &str| -> Vec<String> {
596 let trimmed = line.trim().trim_start_matches('|').trim_end_matches('|');
597 trimmed.split('|').map(|c| c.trim().to_string()).collect()
598 };
599
600 let mut header: Option<Vec<String>> = None;
601 let mut data_rows: Vec<Vec<String>> = Vec::new();
602 let mut found_separator = false;
603
604 for (i, line) in lines.iter().enumerate() {
605 if is_separator(line) {
606 found_separator = true;
607 continue;
608 }
609 if i == 0 && !found_separator {
610 header = Some(parse_row(line));
611 } else {
612 data_rows.push(parse_row(line));
613 }
614 }
615
616 if !found_separator && header.is_none() && !data_rows.is_empty() {
618 header = Some(data_rows.remove(0));
619 }
620
621 let all_rows: Vec<&Vec<String>> = header.iter().chain(data_rows.iter()).collect();
623 let col_count = all_rows.iter().map(|r| r.len()).max().unwrap_or(0);
624 if col_count == 0 {
625 return;
626 }
627 let mut col_widths = vec![0usize; col_count];
628 let stripped_rows: Vec<Vec<String>> = all_rows
630 .iter()
631 .map(|row| row.iter().map(|c| Self::md_strip(c)).collect())
632 .collect();
633 for row in &stripped_rows {
634 for (i, cell) in row.iter().enumerate() {
635 if i < col_count {
636 col_widths[i] = col_widths[i].max(UnicodeWidthStr::width(cell.as_str()));
637 }
638 }
639 }
640
641 let mut top = String::from("┌");
643 for (i, &w) in col_widths.iter().enumerate() {
644 for _ in 0..w + 2 {
645 top.push('─');
646 }
647 top.push(if i < col_count - 1 { '┬' } else { '┐' });
648 }
649 self.styled(&top, border_style);
650
651 if let Some(ref hdr) = header {
653 self.line(|ui| {
654 ui.styled("│", border_style);
655 for (i, w) in col_widths.iter().enumerate() {
656 let raw = hdr.get(i).map(String::as_str).unwrap_or("");
657 let display_text = Self::md_strip(raw);
658 let cell_w = UnicodeWidthStr::width(display_text.as_str());
659 let padding: String = " ".repeat(w.saturating_sub(cell_w));
660 ui.styled(" ", bold_style);
661 ui.styled(&display_text, bold_style);
662 ui.styled(padding, bold_style);
663 ui.styled(" │", border_style);
664 }
665 });
666
667 let mut sep = String::from("├");
669 for (i, &w) in col_widths.iter().enumerate() {
670 for _ in 0..w + 2 {
671 sep.push('─');
672 }
673 sep.push(if i < col_count - 1 { '┼' } else { '┤' });
674 }
675 self.styled(&sep, border_style);
676 }
677
678 for row in &data_rows {
680 self.line(|ui| {
681 ui.styled("│", border_style);
682 for (i, w) in col_widths.iter().enumerate() {
683 let raw = row.get(i).map(String::as_str).unwrap_or("");
684 let display_text = Self::md_strip(raw);
685 let cell_w = UnicodeWidthStr::width(display_text.as_str());
686 let padding: String = " ".repeat(w.saturating_sub(cell_w));
687 ui.styled(" ", text_style);
688 Self::render_md_inline_into(ui, raw, text_style, bold_style, code_style);
689 ui.styled(padding, text_style);
690 ui.styled(" │", border_style);
691 }
692 });
693 }
694
695 let mut bot = String::from("└");
697 for (i, &w) in col_widths.iter().enumerate() {
698 for _ in 0..w + 2 {
699 bot.push('─');
700 }
701 bot.push(if i < col_count - 1 { '┴' } else { '┘' });
702 }
703 self.styled(&bot, border_style);
704 }
705
706 pub(crate) fn parse_inline_segments(
707 text: &str,
708 base: Style,
709 bold: Style,
710 code: Style,
711 ) -> Vec<(String, Style)> {
712 let mut segments: Vec<(String, Style)> = Vec::new();
717 let bytes = text.as_bytes();
718 let mut current = String::new();
719 let mut i: usize = 0;
720
721 while i < bytes.len() {
722 if bytes[i] == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
724 let after_open = i + 2;
725 if let Some(rel_end) = text[after_open..].find("**") {
726 let close = after_open + rel_end;
727 if !current.is_empty() {
728 segments.push((std::mem::take(&mut current), base));
729 }
730 let inner = text[after_open..close].to_string();
731 segments.push((inner, bold));
732 i = close + 2;
733 continue;
734 }
735 }
736
737 if bytes[i] == b'*'
739 && (i + 1 >= bytes.len() || bytes[i + 1] != b'*')
740 && (i == 0 || bytes[i - 1] != b'*')
741 {
742 let after_open = i + 1;
743 if let Some(rel_end) = text[after_open..].find('*') {
744 let close = after_open + rel_end;
745 if !current.is_empty() {
746 segments.push((std::mem::take(&mut current), base));
747 }
748 let inner = text[after_open..close].to_string();
749 segments.push((inner, base.italic()));
750 i = close + 1;
751 continue;
752 }
753 }
754
755 if bytes[i] == b'`' {
757 let after_open = i + 1;
758 if let Some(rel_end) = text[after_open..].find('`') {
759 let close = after_open + rel_end;
760 if !current.is_empty() {
761 segments.push((std::mem::take(&mut current), base));
762 }
763 let inner = text[after_open..close].to_string();
764 segments.push((inner, code));
765 i = close + 1;
766 continue;
767 }
768 }
769
770 let ch = text[i..]
773 .chars()
774 .next()
775 .expect("non-empty tail past bounds check");
776 current.push(ch);
777 i += ch.len_utf8();
778 }
779
780 if !current.is_empty() {
781 segments.push((current, base));
782 }
783 segments
784 }
785
786 fn render_md_inline(
791 &mut self,
792 text: &str,
793 text_style: Style,
794 bold_style: Style,
795 code_style: Style,
796 ) {
797 let items = Self::split_md_links(text);
798
799 if items.len() == 1 {
801 if let MdInline::Text(ref t) = items[0] {
802 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
803 if segs.len() <= 1 {
804 self.text(text)
805 .wrap()
806 .fg(text_style.fg.unwrap_or(Color::Reset));
807 } else {
808 self.line_wrap(|ui| {
809 for (s, st) in segs {
810 ui.styled(s, st);
811 }
812 });
813 }
814 return;
815 }
816 }
817
818 self.line_wrap(|ui| {
820 for item in &items {
821 match item {
822 MdInline::Text(t) => {
823 let segs =
824 Self::parse_inline_segments(t, text_style, bold_style, code_style);
825 for (s, st) in segs {
826 ui.styled(s, st);
827 }
828 }
829 MdInline::Link { text, url } => {
830 ui.link(text.clone(), url.clone());
831 }
832 MdInline::Image { alt, .. } => {
833 ui.styled(alt.as_str(), code_style);
835 }
836 }
837 }
838 });
839 }
840
841 fn render_md_inline_into(
847 ui: &mut Context,
848 text: &str,
849 text_style: Style,
850 bold_style: Style,
851 code_style: Style,
852 ) {
853 let items = Self::split_md_links(text);
854 for item in &items {
855 match item {
856 MdInline::Text(t) => {
857 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
858 for (s, st) in segs {
859 ui.styled(s, st);
860 }
861 }
862 MdInline::Link { text, url } => {
863 ui.link(text.clone(), url.clone());
864 }
865 MdInline::Image { alt, .. } => {
866 ui.styled(alt.as_str(), code_style);
867 }
868 }
869 }
870 }
871
872 fn split_md_links(text: &str) -> Vec<MdInline> {
874 let chars: Vec<char> = text.chars().collect();
875 let mut items: Vec<MdInline> = Vec::new();
876 let mut current = String::new();
877 let mut i = 0;
878
879 while i < chars.len() {
880 if chars[i] == '!' && i + 1 < chars.len() && chars[i + 1] == '[' {
882 if let Some((alt, _url, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1) {
883 if !current.is_empty() {
884 items.push(MdInline::Text(std::mem::take(&mut current)));
885 }
886 items.push(MdInline::Image { alt });
887 i += 1 + consumed;
888 continue;
889 }
890 }
891 if chars[i] == '[' {
893 if let Some((link_text, url, consumed)) = Self::parse_md_bracket_paren(&chars, i) {
894 if !current.is_empty() {
895 items.push(MdInline::Text(std::mem::take(&mut current)));
896 }
897 items.push(MdInline::Link {
898 text: link_text,
899 url,
900 });
901 i += consumed;
902 continue;
903 }
904 }
905 current.push(chars[i]);
906 i += 1;
907 }
908 if !current.is_empty() {
909 items.push(MdInline::Text(current));
910 }
911 if items.is_empty() {
912 items.push(MdInline::Text(String::new()));
913 }
914 items
915 }
916
917 fn parse_md_bracket_paren(chars: &[char], start: usize) -> Option<(String, String, usize)> {
920 if start >= chars.len() || chars[start] != '[' {
921 return None;
922 }
923 let mut depth = 0i32;
925 let mut bracket_end = None;
926 for (j, &ch) in chars.iter().enumerate().skip(start) {
927 if ch == '[' {
928 depth += 1;
929 } else if ch == ']' {
930 depth -= 1;
931 if depth == 0 {
932 bracket_end = Some(j);
933 break;
934 }
935 }
936 }
937 let bracket_end = bracket_end?;
938 if bracket_end + 1 >= chars.len() || chars[bracket_end + 1] != '(' {
940 return None;
941 }
942 let paren_start = bracket_end + 2;
944 let mut paren_end = None;
945 let mut paren_depth = 1i32;
946 for (j, &ch) in chars.iter().enumerate().skip(paren_start) {
947 if ch == '(' {
948 paren_depth += 1;
949 } else if ch == ')' {
950 paren_depth -= 1;
951 if paren_depth == 0 {
952 paren_end = Some(j);
953 break;
954 }
955 }
956 }
957 let paren_end = paren_end?;
958 let text: String = chars[start + 1..bracket_end].iter().collect();
959 let url: String = chars[paren_start..paren_end].iter().collect();
960 let consumed = paren_end - start + 1;
961 Some((text, url, consumed))
962 }
963
964 fn md_strip(text: &str) -> String {
969 let chars: Vec<char> = text.chars().collect();
973 let char_to_byte = {
974 let mut v = Vec::with_capacity(chars.len() + 1);
975 let mut acc = 0usize;
976 v.push(0);
977 for ch in &chars {
978 acc += ch.len_utf8();
979 v.push(acc);
980 }
981 v
982 };
983 let bytes = text.as_bytes();
984 let mut result = String::with_capacity(text.len());
985 let mut ci: usize = 0;
986
987 while ci < chars.len() {
988 if chars[ci] == '!' && ci + 1 < chars.len() && chars[ci + 1] == '[' {
990 if let Some((alt, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci + 1) {
991 result.push_str(&alt);
992 ci += 1 + consumed;
993 continue;
994 }
995 }
996 if chars[ci] == '[' {
998 if let Some((link_text, _, consumed)) = Self::parse_md_bracket_paren(&chars, ci) {
999 result.push_str(&link_text);
1000 ci += consumed;
1001 continue;
1002 }
1003 }
1004
1005 let bi = char_to_byte[ci];
1006
1007 if bytes[bi] == b'*' && bi + 1 < bytes.len() && bytes[bi + 1] == b'*' {
1009 let after_open = bi + 2;
1010 if let Some(rel_end) = text[after_open..].find("**") {
1011 let close = after_open + rel_end;
1012 let inner = &text[after_open..close];
1013 result.push_str(inner);
1014 ci += 2 + inner.chars().count() + 2;
1015 continue;
1016 }
1017 }
1018
1019 if bytes[bi] == b'*'
1021 && (bi + 1 >= bytes.len() || bytes[bi + 1] != b'*')
1022 && (bi == 0 || bytes[bi - 1] != b'*')
1023 {
1024 let after_open = bi + 1;
1025 if let Some(rel_end) = text[after_open..].find('*') {
1026 let close = after_open + rel_end;
1027 let inner = &text[after_open..close];
1028 result.push_str(inner);
1029 ci += 1 + inner.chars().count() + 1;
1030 continue;
1031 }
1032 }
1033
1034 if bytes[bi] == b'`' {
1036 let after_open = bi + 1;
1037 if let Some(rel_end) = text[after_open..].find('`') {
1038 let close = after_open + rel_end;
1039 let inner = &text[after_open..close];
1040 result.push_str(inner);
1041 ci += 1 + inner.chars().count() + 1;
1042 continue;
1043 }
1044 }
1045
1046 result.push(chars[ci]);
1047 ci += 1;
1048 }
1049 result
1050 }
1051
1052 pub fn key_seq(&self, seq: &str) -> bool {
1059 if seq.is_empty() {
1060 return false;
1061 }
1062 if (self.rollback.modal_active || self.prev_modal_active)
1063 && self.rollback.overlay_depth == 0
1064 {
1065 return false;
1066 }
1067 let target: Vec<char> = seq.chars().collect();
1068 let mut matched = 0;
1069 for (_, key) in self.available_key_presses() {
1070 if let KeyCode::Char(c) = key.code {
1071 if c == target[matched] {
1072 matched += 1;
1073 if matched == target.len() {
1074 return true;
1075 }
1076 } else {
1077 matched = 0;
1078 if c == target[0] {
1079 matched = 1;
1080 }
1081 }
1082 }
1083 }
1084 false
1085 }
1086}