1use crate::{EditorHook, HookContext, HookOutcome, KeyCode, KeyEvent, Point, Selection};
2
3use super::config::MarkdownConfig;
4use super::table::{
5 TableRowKind, clean_table_line, find_unescaped_pipes, split_table_cells, table_block_at,
6};
7
8#[derive(Debug, Clone)]
10pub struct MarkdownHook {
11 interactive_tasks: bool,
12 table_navigation: bool,
13}
14
15impl Default for MarkdownHook {
16 fn default() -> Self {
17 Self::new()
18 }
19}
20
21impl MarkdownHook {
22 pub fn new() -> Self {
24 Self {
25 interactive_tasks: true,
26 table_navigation: true,
27 }
28 }
29
30 pub fn with_config(config: MarkdownConfig) -> Self {
32 Self {
33 interactive_tasks: config.interactive_tasks,
34 table_navigation: config.table_navigation,
35 }
36 }
37
38 pub fn set_interactive_tasks(&mut self, interactive: bool) {
40 self.interactive_tasks = interactive;
41 }
42
43 pub fn interactive_tasks(&self) -> bool {
45 self.interactive_tasks
46 }
47
48 pub fn set_table_navigation(&mut self, enabled: bool) {
50 self.table_navigation = enabled;
51 }
52
53 pub fn table_navigation(&self) -> bool {
55 self.table_navigation
56 }
57
58 fn toggle_marker_at_row(ctx: &mut HookContext, row: usize) -> bool {
59 if row >= ctx.buffer.len_lines() {
60 return false;
61 }
62 let line = ctx.buffer.line_to_string(row);
63 let line_start = ctx.buffer.point_to_offset(Point::new(row, 0));
64 let old_cursor = ctx.buffer.cursor_offset();
65
66 for (empty, checked) in [("- [ ] ", "- [x] "), ("* [ ] ", "* [x] ")] {
68 if let Some(idx) = line.find(empty) {
69 let s = line_start + idx;
70 ctx.buffer.replace_range(s..s + 6, checked);
71 ctx.buffer.set_cursor_offset(old_cursor);
72 return true;
73 }
74 }
75 for (checked, empty) in [
77 ("- [x] ", "- [ ] "),
78 ("- [X] ", "- [ ] "),
79 ("* [x] ", "- [ ] "),
80 ("* [X] ", "- [ ] "),
81 ] {
82 if let Some(idx) = line.find(checked) {
83 let s = line_start + idx;
84 ctx.buffer.replace_range(s..s + 6, empty);
85 ctx.buffer.set_cursor_offset(old_cursor);
86 return true;
87 }
88 }
89 false
90 }
91
92 fn toggle_checkbox(ctx: &mut HookContext) -> bool {
93 let row = ctx.buffer.cursor_point().row;
94 let line = ctx.buffer.line_to_string(row);
95 let line_start = ctx.buffer.point_to_offset(Point::new(row, 0));
96
97 if let Some(idx) = line.find("- [ ] ") {
98 let target_start = line_start + idx;
99 ctx.buffer
100 .replace_range(target_start..target_start + 6, "- [x] ");
101 return true;
102 } else if let Some(idx) = line.find("- [x] ") {
103 let target_start = line_start + idx;
104 ctx.buffer
105 .replace_range(target_start..target_start + 6, "- [ ] ");
106 return true;
107 } else if let Some(idx) = line.find("* [ ] ") {
108 let target_start = line_start + idx;
109 ctx.buffer
110 .replace_range(target_start..target_start + 6, "* [x] ");
111 return true;
112 } else if let Some(idx) = line.find("* [x] ") {
113 let target_start = line_start + idx;
114 ctx.buffer
115 .replace_range(target_start..target_start + 6, "* [ ] ");
116 return true;
117 }
118 false
119 }
120
121 fn table_cell_starts(stripped: &str) -> Vec<usize> {
125 let bytes = stripped.as_bytes();
126 let mut starts = Vec::new();
127 if !stripped.trim_start().starts_with('|') {
128 starts.push(0);
129 }
130 for p in find_unescaped_pipes(stripped) {
131 let mut s = (p + 1).min(stripped.len());
132 while s < stripped.len() && (bytes[s] == b' ' || bytes[s] == b'\t') {
133 s += 1;
134 }
135 starts.push(s);
136 }
137 starts
138 }
139
140 fn table_tab_target(ctx: &mut HookContext, backwards: bool) -> Option<usize> {
145 let row = ctx.buffer.cursor_point().row;
146 let block = table_block_at(ctx.buffer, row)?;
147 if !matches!(
148 block.kind_at(row)?,
149 TableRowKind::Header | TableRowKind::Body
150 ) {
151 return None;
152 }
153 let line_start = ctx.buffer.point_to_offset(Point::new(row, 0));
154 let stripped = clean_table_line(&ctx.buffer.line_to_string(row)).to_string();
155 let cursor_col = ctx
156 .buffer
157 .cursor_offset()
158 .saturating_sub(line_start)
159 .min(stripped.len());
160 let starts = Self::table_cell_starts(&stripped);
161
162 if !backwards {
163 if let Some(&s) = starts.iter().find(|&&s| s > cursor_col) {
164 return Some(line_start + s);
165 }
166 for r in row + 1..=block.end_row {
168 if matches!(
169 block.kind_at(r),
170 Some(TableRowKind::Header) | Some(TableRowKind::Body)
171 ) {
172 let next_start = ctx.buffer.point_to_offset(Point::new(r, 0));
173 let next_stripped = clean_table_line(&ctx.buffer.line_to_string(r)).to_string();
174 let next_cells = Self::table_cell_starts(&next_stripped);
175 return Some(next_start + next_cells.first().copied().unwrap_or(0));
176 }
177 }
178 let indent_len = stripped.len() - stripped.trim_start().len();
179 let indent = &stripped[..indent_len];
180 let skeleton = format!("{}|{}", indent, " |".repeat(block.col_count));
181 let line_end = line_start + stripped.len();
182 ctx.buffer.set_cursor_offset(line_end);
183 ctx.buffer.insert(&format!("\n{skeleton}"));
184 return Some(line_end + 1 + indent_len + 2);
185 }
186
187 if let Some(&s) = starts.iter().rev().find(|&&s| s < cursor_col) {
188 return Some(line_start + s);
189 }
190 for r in (block.header_row..row).rev() {
192 if matches!(
193 block.kind_at(r),
194 Some(TableRowKind::Header) | Some(TableRowKind::Body)
195 ) {
196 let prev_start = ctx.buffer.point_to_offset(Point::new(r, 0));
197 let prev_stripped = clean_table_line(&ctx.buffer.line_to_string(r)).to_string();
198 let prev_cells = Self::table_cell_starts(&prev_stripped);
199 return Some(prev_start + prev_cells.last().copied().unwrap_or(0));
200 }
201 }
202 None
203 }
204}
205
206impl EditorHook for MarkdownHook {
207 fn on_key(&mut self, ctx: &mut HookContext, event: &KeyEvent) -> HookOutcome {
208 if event.modifiers.ctrl || event.modifiers.meta {
209 match &event.code {
210 KeyCode::Char('b') => {
211 if let Some(sel) = ctx.selection.take() {
212 let range = sel.byte_range();
213 let text = ctx.buffer.text().byte_slice(range.clone()).to_string();
214 let wrapped = format!("**{}**", text);
215 ctx.buffer.replace_range(range.clone(), &wrapped);
216 *ctx.selection = Some(Selection::range(range.start + 2, range.end + 2));
217 } else {
218 ctx.buffer.insert("****");
219 ctx.buffer.move_cursor_left();
220 ctx.buffer.move_cursor_left();
221 }
222 return HookOutcome::Consumed;
223 }
224 KeyCode::Char('i') => {
225 if let Some(sel) = ctx.selection.take() {
226 let range = sel.byte_range();
227 let text = ctx.buffer.text().byte_slice(range.clone()).to_string();
228 let wrapped = format!("*{}*", text);
229 ctx.buffer.replace_range(range.clone(), &wrapped);
230 *ctx.selection = Some(Selection::range(range.start + 1, range.end + 1));
231 } else {
232 ctx.buffer.insert("**");
233 ctx.buffer.move_cursor_left();
234 }
235 return HookOutcome::Consumed;
236 }
237 KeyCode::Char('k') => {
238 if let Some(sel) = ctx.selection.take() {
239 let range = sel.byte_range();
240 let text = ctx.buffer.text().byte_slice(range.clone()).to_string();
241 let wrapped = format!("[{}](url)", text);
242 ctx.buffer.replace_range(range.clone(), &wrapped);
243 let url_start = range.start + 1 + text.len() + 2;
244 *ctx.selection = Some(Selection::range(url_start, url_start + 3));
245 } else {
246 ctx.buffer.insert("[](url)");
247 ctx.buffer.move_cursor_left();
248 ctx.buffer.move_cursor_left();
249 ctx.buffer.move_cursor_left();
250 ctx.buffer.move_cursor_left();
251 ctx.buffer.move_cursor_left();
252 }
253 return HookOutcome::Consumed;
254 }
255 KeyCode::Enter if Self::toggle_checkbox(ctx) => {
256 return HookOutcome::Consumed;
257 }
258 _ => {}
259 }
260 }
261
262 if event.code == KeyCode::Enter && !event.modifiers.shift {
263 let cursor = ctx.buffer.cursor_offset();
264 let row = ctx.buffer.cursor_point().row;
265 let line = ctx.buffer.line_to_string(row);
266 let trimmed = line.trim_start();
267 let indent_len = line.len() - trimmed.len();
268 let indent = &line[..indent_len];
269
270 if self.table_navigation
273 && let Some(block) = table_block_at(ctx.buffer, row)
274 && let Some(kind) = block.kind_at(row)
275 && matches!(kind, TableRowKind::Header | TableRowKind::Body)
276 {
277 let stripped = clean_table_line(&line).to_string();
278 let (_, cells) = split_table_cells(&stripped);
279 let all_empty = cells.iter().all(|c| {
280 stripped
281 .get(c.clone())
282 .map(|s| s.trim().is_empty())
283 .unwrap_or(true)
284 });
285 if all_empty {
286 let line_start = ctx.buffer.point_to_offset(Point::new(row, 0));
288 ctx.buffer.delete_range(line_start..cursor);
289 return HookOutcome::Consumed;
290 }
291 let table_indent_len = stripped.len() - stripped.trim_start().len();
292 let table_indent = &stripped[..table_indent_len];
293 let skeleton = format!("{}|{}", table_indent, " |".repeat(block.col_count));
294 ctx.buffer.insert(&format!("\n{skeleton}"));
295 return HookOutcome::Consumed;
296 }
297
298 if trimmed.starts_with("- [ ] ") || trimmed.starts_with("- [x] ") {
299 if trimmed == "- [ ] \n"
300 || trimmed == "- [ ] \r\n"
301 || trimmed == "- [ ] "
302 || trimmed == "- [x] \n"
303 || trimmed == "- [x] \r\n"
304 || trimmed == "- [x] "
305 {
306 let line_start = ctx.buffer.point_to_offset(Point::new(row, 0));
307 ctx.buffer.delete_range(line_start..cursor);
308 return HookOutcome::Consumed;
309 }
310 ctx.buffer.insert(&format!("\n{}- [ ] ", indent));
311 return HookOutcome::Consumed;
312 }
313
314 if trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("+ ") {
315 let bullet = &trimmed[..2];
316 if trimmed == "- \n"
317 || trimmed == "- \r\n"
318 || trimmed == "- "
319 || trimmed == "* \n"
320 || trimmed == "* \r\n"
321 || trimmed == "* "
322 || trimmed == "+ \n"
323 || trimmed == "+ \r\n"
324 || trimmed == "+ "
325 {
326 let line_start = ctx.buffer.point_to_offset(Point::new(row, 0));
327 ctx.buffer.delete_range(line_start..cursor);
328 return HookOutcome::Consumed;
329 }
330 ctx.buffer.insert(&format!("\n{}{}", indent, bullet));
331 return HookOutcome::Consumed;
332 }
333
334 if let Some(dot_idx) = trimmed.find(". ") {
335 let num_str = &trimmed[..dot_idx];
336 if let Ok(num) = num_str.parse::<usize>() {
337 let rest = &trimmed[dot_idx + 2..];
338 if rest == "\n" || rest == "\r\n" || rest.is_empty() {
339 let line_start = ctx.buffer.point_to_offset(Point::new(row, 0));
340 ctx.buffer.delete_range(line_start..cursor);
341 return HookOutcome::Consumed;
342 }
343 ctx.buffer.insert(&format!("\n{}{}. ", indent, num + 1));
344 return HookOutcome::Consumed;
345 }
346 }
347 }
348
349 if event.code == KeyCode::Tab
353 && !event.modifiers.ctrl
354 && !event.modifiers.meta
355 && !event.modifiers.alt
356 && self.table_navigation
357 && let Some(target) = Self::table_tab_target(ctx, event.modifiers.shift)
358 {
359 ctx.buffer.set_cursor_offset(target);
360 *ctx.selection = None;
361 return HookOutcome::Consumed;
362 }
363
364 HookOutcome::PassThrough
365 }
366
367 fn on_click(&mut self, ctx: &mut HookContext, row: usize, _col: usize) -> HookOutcome {
368 if !self.interactive_tasks {
369 return HookOutcome::PassThrough;
370 }
371 if Self::toggle_marker_at_row(ctx, row) {
372 return HookOutcome::Consumed;
373 }
374 HookOutcome::PassThrough
375 }
376
377 fn status_text(&self) -> Option<&str> {
378 Some("MARKDOWN")
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::{EditorBuffer, HookContext, HookOutcome, KeyEvent, PromptState, Selection};
386
387 #[test]
388 fn test_markdown_hook_bold_wrapping() {
389 let mut buffer = EditorBuffer::new("hello world");
390 let mut selection = Some(Selection::range(0, 5));
391 let mut cursor_style = crate::CursorStyle::Bar;
392 let mut prompt = PromptState::new();
393 let mut effects = Vec::new();
394 let mut hook = MarkdownHook::new();
395
396 let mut ctx = HookContext::new(
397 &mut buffer,
398 &mut selection,
399 &mut cursor_style,
400 &mut prompt,
401 &mut effects,
402 );
403 let event = KeyEvent {
404 code: KeyCode::Char('b'),
405 modifiers: crate::Modifiers {
406 ctrl: true,
407 ..Default::default()
408 },
409 };
410
411 let outcome = hook.on_key(&mut ctx, &event);
412 assert_eq!(outcome, HookOutcome::Consumed);
413 assert_eq!(ctx.buffer.text().to_string(), "**hello** world");
414 assert_eq!(ctx.selection.unwrap().byte_range(), 2..7);
415 }
416
417 #[test]
418 fn test_markdown_hook_checkbox_toggle() {
419 let mut buffer = EditorBuffer::new("- [ ] Task item");
420 let mut selection = None;
421 let mut cursor_style = crate::CursorStyle::Bar;
422 let mut prompt = PromptState::new();
423 let mut effects = Vec::new();
424 let mut hook = MarkdownHook::new();
425
426 let mut ctx = HookContext::new(
427 &mut buffer,
428 &mut selection,
429 &mut cursor_style,
430 &mut prompt,
431 &mut effects,
432 );
433 let event = KeyEvent {
434 code: KeyCode::Enter,
435 modifiers: crate::Modifiers {
436 ctrl: true,
437 ..Default::default()
438 },
439 };
440
441 let outcome = hook.on_key(&mut ctx, &event);
442 assert_eq!(outcome, HookOutcome::Consumed);
443 assert_eq!(ctx.buffer.text().to_string(), "- [x] Task item");
444
445 let outcome2 = hook.on_key(&mut ctx, &event);
446 assert_eq!(outcome2, HookOutcome::Consumed);
447 assert_eq!(ctx.buffer.text().to_string(), "- [ ] Task item");
448 }
449
450 #[test]
451 fn test_markdown_hook_numbered_list_continuation() {
452 let mut buffer = EditorBuffer::new("1. First item");
453 buffer.set_cursor_offset(13);
454 let mut selection = None;
455 let mut cursor_style = crate::CursorStyle::Bar;
456 let mut prompt = PromptState::new();
457 let mut effects = Vec::new();
458 let mut hook = MarkdownHook::new();
459
460 let mut ctx = HookContext::new(
461 &mut buffer,
462 &mut selection,
463 &mut cursor_style,
464 &mut prompt,
465 &mut effects,
466 );
467 let event = KeyEvent::plain(KeyCode::Enter);
468
469 let outcome = hook.on_key(&mut ctx, &event);
470 assert_eq!(outcome, HookOutcome::Consumed);
471 assert_eq!(ctx.buffer.text().to_string(), "1. First item\n2. ");
472 }
473
474 #[test]
475 fn test_markdown_hook_on_click_toggles_task() {
476 let mut buffer = EditorBuffer::new("- [ ] Task one\n- [x] Task two");
477 let mut selection = None;
478 let mut cursor_style = crate::CursorStyle::Bar;
479 let mut prompt = PromptState::new();
480 let mut effects = Vec::new();
481 let mut hook = MarkdownHook::new();
482
483 buffer.set_cursor_offset(0);
485 let mut ctx = HookContext::new(
486 &mut buffer,
487 &mut selection,
488 &mut cursor_style,
489 &mut prompt,
490 &mut effects,
491 );
492 assert_eq!(hook.on_click(&mut ctx, 1, 0), HookOutcome::Consumed);
493 assert_eq!(
494 ctx.buffer.text().to_string(),
495 "- [ ] Task one\n- [ ] Task two"
496 );
497 assert_eq!(ctx.buffer.cursor_offset(), 0);
498
499 let mut ctx = HookContext::new(
501 &mut buffer,
502 &mut selection,
503 &mut cursor_style,
504 &mut prompt,
505 &mut effects,
506 );
507 assert_eq!(hook.on_click(&mut ctx, 0, 2), HookOutcome::Consumed);
508 assert_eq!(
509 ctx.buffer.text().to_string(),
510 "- [x] Task one\n- [ ] Task two"
511 );
512
513 ctx.buffer.replace_range(0..14, "- [X] Task one");
515 let mut ctx = HookContext::new(
516 &mut buffer,
517 &mut selection,
518 &mut cursor_style,
519 &mut prompt,
520 &mut effects,
521 );
522 assert_eq!(hook.on_click(&mut ctx, 0, 3), HookOutcome::Consumed);
523 assert_eq!(
524 ctx.buffer.text().to_string(),
525 "- [ ] Task one\n- [ ] Task two"
526 );
527
528 let mut plain = EditorBuffer::new("hello");
530 let mut ctx = HookContext::new(
531 &mut plain,
532 &mut selection,
533 &mut cursor_style,
534 &mut prompt,
535 &mut effects,
536 );
537 assert_eq!(hook.on_click(&mut ctx, 0, 0), HookOutcome::PassThrough);
538 }
539
540 #[test]
541 fn test_markdown_hook_on_click_respects_config() {
542 let mut buffer = EditorBuffer::new("- [ ] Task");
543 let mut selection = None;
544 let mut cursor_style = crate::CursorStyle::Bar;
545 let mut prompt = PromptState::new();
546 let mut effects = Vec::new();
547 let mut hook = MarkdownHook::with_config(MarkdownConfig {
548 interactive_tasks: false,
549 ..Default::default()
550 });
551 let mut ctx = HookContext::new(
552 &mut buffer,
553 &mut selection,
554 &mut cursor_style,
555 &mut prompt,
556 &mut effects,
557 );
558 assert_eq!(hook.on_click(&mut ctx, 0, 0), HookOutcome::PassThrough);
559 assert_eq!(ctx.buffer.text().to_string(), "- [ ] Task");
560 }
561
562 #[test]
563 fn test_table_hook_tab_moves_between_cells() {
564 let mut buffer = EditorBuffer::new("| a | b |\n| --- | --- |\n| c | d |");
565 let mut selection = None;
566 let mut cursor_style = crate::CursorStyle::Bar;
567 let mut prompt = PromptState::new();
568 let mut effects = Vec::new();
569 let mut hook = MarkdownHook::new();
570 buffer.set_cursor_offset(0);
571
572 let mut ctx = HookContext::new(
573 &mut buffer,
574 &mut selection,
575 &mut cursor_style,
576 &mut prompt,
577 &mut effects,
578 );
579 assert_eq!(
580 hook.on_key(&mut ctx, &KeyEvent::plain(KeyCode::Tab)),
581 HookOutcome::Consumed
582 );
583 assert_eq!(ctx.buffer.cursor_offset(), 2); let mut ctx = HookContext::new(
586 &mut buffer,
587 &mut selection,
588 &mut cursor_style,
589 &mut prompt,
590 &mut effects,
591 );
592 assert_eq!(
593 hook.on_key(&mut ctx, &KeyEvent::plain(KeyCode::Tab)),
594 HookOutcome::Consumed
595 );
596 assert_eq!(ctx.buffer.cursor_offset(), 6); let back = KeyEvent {
600 code: KeyCode::Tab,
601 modifiers: crate::Modifiers {
602 shift: true,
603 ..Default::default()
604 },
605 };
606 let mut ctx = HookContext::new(
607 &mut buffer,
608 &mut selection,
609 &mut cursor_style,
610 &mut prompt,
611 &mut effects,
612 );
613 assert_eq!(hook.on_key(&mut ctx, &back), HookOutcome::Consumed);
614 assert_eq!(ctx.buffer.cursor_offset(), 2);
615 }
616
617 #[test]
618 fn test_table_hook_tab_appends_row_at_end() {
619 let mut buffer = EditorBuffer::new("| a |\n| --- |\n| b |");
620 let mut selection = None;
621 let mut cursor_style = crate::CursorStyle::Bar;
622 let mut prompt = PromptState::new();
623 let mut effects = Vec::new();
624 let mut hook = MarkdownHook::new();
625 buffer.set_cursor_offset(buffer.len_bytes());
626
627 let mut ctx = HookContext::new(
628 &mut buffer,
629 &mut selection,
630 &mut cursor_style,
631 &mut prompt,
632 &mut effects,
633 );
634 assert_eq!(
635 hook.on_key(&mut ctx, &KeyEvent::plain(KeyCode::Tab)),
636 HookOutcome::Consumed
637 );
638 assert_eq!(ctx.buffer.text().to_string(), "| a |\n| --- |\n| b |\n| |");
639 }
640
641 #[test]
642 fn test_table_hook_tab_passthrough_outside_tables() {
643 let mut buffer = EditorBuffer::new("plain text");
644 let mut selection = None;
645 let mut cursor_style = crate::CursorStyle::Bar;
646 let mut prompt = PromptState::new();
647 let mut effects = Vec::new();
648 let mut hook = MarkdownHook::new();
649 buffer.set_cursor_offset(3);
650
651 let mut ctx = HookContext::new(
652 &mut buffer,
653 &mut selection,
654 &mut cursor_style,
655 &mut prompt,
656 &mut effects,
657 );
658 assert_eq!(
659 hook.on_key(&mut ctx, &KeyEvent::plain(KeyCode::Tab)),
660 HookOutcome::PassThrough
661 );
662
663 let mut disabled = EditorBuffer::new("| a |\n| --- |\n| b |");
664 disabled.set_cursor_offset(0);
665 let mut hook_off = MarkdownHook::with_config(MarkdownConfig {
666 table_navigation: false,
667 ..Default::default()
668 });
669 let mut ctx = HookContext::new(
670 &mut disabled,
671 &mut selection,
672 &mut cursor_style,
673 &mut prompt,
674 &mut effects,
675 );
676 assert_eq!(
677 hook_off.on_key(&mut ctx, &KeyEvent::plain(KeyCode::Tab)),
678 HookOutcome::PassThrough
679 );
680 }
681
682 #[test]
683 fn test_table_hook_enter_continues_and_exits() {
684 let mut buffer = EditorBuffer::new("| a | b |\n| --- | --- |\n| c | d |");
686 buffer.set_cursor_offset(buffer.len_bytes());
687 let mut selection = None;
688 let mut cursor_style = crate::CursorStyle::Bar;
689 let mut prompt = PromptState::new();
690 let mut effects = Vec::new();
691 let mut hook = MarkdownHook::new();
692 let mut ctx = HookContext::new(
693 &mut buffer,
694 &mut selection,
695 &mut cursor_style,
696 &mut prompt,
697 &mut effects,
698 );
699 assert_eq!(
700 hook.on_key(&mut ctx, &KeyEvent::plain(KeyCode::Enter)),
701 HookOutcome::Consumed
702 );
703 assert_eq!(
704 ctx.buffer.text().to_string(),
705 "| a | b |\n| --- | --- |\n| c | d |\n| | |"
706 );
707
708 let mut empty = EditorBuffer::new("| a |\n| --- |\n| |");
710 empty.set_cursor_offset(empty.len_bytes());
711 let mut ctx = HookContext::new(
712 &mut empty,
713 &mut selection,
714 &mut cursor_style,
715 &mut prompt,
716 &mut effects,
717 );
718 assert_eq!(
719 hook.on_key(&mut ctx, &KeyEvent::plain(KeyCode::Enter)),
720 HookOutcome::Consumed
721 );
722 assert_eq!(ctx.buffer.text().to_string(), "| a |\n| --- |\n");
723 }
724}