1use kode_core::{EditStep, Editor, Position, Transaction};
2
3#[derive(Clone, Debug, Default, PartialEq)]
7pub struct FormattingState {
8 pub bold: bool,
9 pub italic: bool,
10 pub code: bool,
11 pub strikethrough: bool,
12 pub heading_level: u8,
14 pub bullet_list: bool,
15 pub ordered_list: bool,
16 pub blockquote: bool,
17}
18
19pub struct MarkdownCommands;
27
28impl MarkdownCommands {
29 pub fn toggle_bold(editor: &mut Editor) {
33 Self::toggle_inline_mark(editor, "**");
34 }
35
36 pub fn toggle_italic(editor: &mut Editor) {
38 Self::toggle_inline_mark(editor, "*");
39 }
40
41 pub fn toggle_inline_code(editor: &mut Editor) {
43 Self::toggle_inline_mark(editor, "`");
44 }
45
46 pub fn toggle_strikethrough(editor: &mut Editor) {
48 Self::toggle_inline_mark(editor, "~~");
49 }
50
51 pub fn set_heading(editor: &mut Editor, level: u8) {
54 let mut cursor = editor.cursor();
55 let line_text = editor.buffer().line(cursor.line).to_string();
56
57 if line_text.trim().is_empty() && cursor.line > 0 {
62 let prev_line_text = editor.buffer().line(cursor.line - 1).to_string();
63 if prev_line_text.trim_start().starts_with('#') {
64 cursor = Position::new(cursor.line - 1, editor.buffer().line_len(cursor.line - 1));
65 editor.set_cursor(cursor);
66 } else if level > 0 {
67 return; }
69 }
70
71 let line_text = editor.buffer().line(cursor.line).to_string();
72
73 let trimmed = line_text.trim_start();
75 let content_start_chars = if trimmed.starts_with('#') {
76 let hashes = trimmed.chars().take_while(|&c| c == '#').count();
77 let rest: String = trimmed.chars().skip(hashes).collect();
78 let space_after = if rest.starts_with(' ') { 1 } else { 0 };
79 hashes + space_after
80 } else {
81 0
82 };
83
84 let content: String = trimmed.chars().skip(content_start_chars).collect();
86 let content = content.trim_end_matches('\n');
87
88 let new_line = if level == 0 {
90 content.to_string()
91 } else {
92 let prefix: String = "#".repeat(level as usize);
93 format!("{prefix} {content}")
94 };
95
96 let line_start = Position::new(cursor.line, 0);
98 let line_end_col = editor.buffer().line_len(cursor.line);
99 let line_end = Position::new(cursor.line, line_end_col);
100
101 editor.set_selection(line_start, line_end);
102 editor.insert(&new_line);
103
104 let new_prefix_chars = if level == 0 { 0 } else { level as usize + 1 };
109 let old_content_col = cursor.col.saturating_sub(content_start_chars);
110 let new_col = (new_prefix_chars + old_content_col).min(new_line.chars().count());
111 editor.set_cursor(Position::new(cursor.line, new_col));
112 }
113
114 pub fn toggle_blockquote(editor: &mut Editor) {
118 let sel = editor.selection();
119 let cursor = editor.cursor();
120 let start_line = sel.start().line;
121 let end_line = sel.end().line;
122
123 let all_quoted = (start_line..=end_line).all(|line| {
125 let text = editor.buffer().line(line).to_string();
126 text.starts_with("> ") || text.starts_with(">")
127 });
128
129 let mut steps = Vec::new();
131 let mut offset_delta: isize = 0;
132 for line in start_line..=end_line {
133 let line_text = editor.buffer().line(line).to_string();
134 let line_start_char = editor.buffer().line_to_char(line);
135 let adjusted_offset = (line_start_char as isize + offset_delta) as usize;
136 let content = line_text.trim_end_matches('\n');
137 let content_chars = content.chars().count();
138
139 if all_quoted {
140 let unquoted = if let Some(s) = content.strip_prefix("> ") {
141 s
142 } else if let Some(s) = content.strip_prefix('>') {
143 s
144 } else {
145 content
146 };
147 let new_chars = unquoted.chars().count();
148 steps.push(EditStep::replace(adjusted_offset, content.to_string(), unquoted.to_string()));
149 offset_delta += new_chars as isize - content_chars as isize;
150 } else {
151 let new_text = format!("> {content}");
152 let new_chars = new_text.chars().count();
153 steps.push(EditStep::replace(adjusted_offset, content.to_string(), new_text));
154 offset_delta += new_chars as isize - content_chars as isize;
155 }
156 }
157
158 if !steps.is_empty() {
159 editor.apply_transaction(Transaction::new(steps));
160 let prefix_delta: isize = if all_quoted { -2 } else { 2 };
162 let new_col = (cursor.col as isize + prefix_delta).max(0) as usize;
163 let line_len = editor.buffer().line_len(cursor.line);
164 editor.set_cursor(Position::new(cursor.line, new_col.min(line_len)));
165 }
166 }
167
168 pub fn toggle_bullet_list(editor: &mut Editor) {
172 Self::toggle_list_prefix(editor, "- ");
173 }
174
175 pub fn toggle_ordered_list(editor: &mut Editor) {
177 let sel = editor.selection();
178 let start_line = sel.start().line;
179 let end_line = sel.end().line;
180
181 let all_ordered = (start_line..=end_line).all(|line| {
182 let text = editor.buffer().line(line).to_string();
183 let trimmed = text.trim_start();
184 Self::strip_ordered_prefix(trimmed).is_some()
185 });
186
187 let mut steps = Vec::new();
188 let mut offset_delta: isize = 0;
189 for line in start_line..=end_line {
190 let line_text = editor.buffer().line(line).to_string();
191 let line_start_char = editor.buffer().line_to_char(line);
192 let adjusted_offset = (line_start_char as isize + offset_delta) as usize;
193 let content = line_text.trim_end_matches('\n');
194 let content_chars = content.chars().count();
195
196 if all_ordered {
197 let trimmed = content.trim_start();
198 let leading_indent = &content[..content.len() - trimmed.len()]; let stripped = Self::strip_ordered_prefix(trimmed).unwrap_or(trimmed);
200 let new_text = format!("{leading_indent}{stripped}");
201 let new_chars = new_text.chars().count();
202 steps.push(EditStep::replace(adjusted_offset, content.to_string(), new_text));
203 offset_delta += new_chars as isize - content_chars as isize;
204 } else {
205 let num = line - start_line + 1;
206 let inner = content
208 .strip_prefix("- ")
209 .or_else(|| content.strip_prefix("* "))
210 .or_else(|| content.strip_prefix("+ "))
211 .unwrap_or_else(|| Self::strip_block_prefix(content));
212 let new_text = format!("{num}. {inner}");
213 let new_chars = new_text.chars().count();
214 steps.push(EditStep::replace(adjusted_offset, content.to_string(), new_text));
215 offset_delta += new_chars as isize - content_chars as isize;
216 }
217 }
218
219 if !steps.is_empty() {
220 editor.apply_transaction(Transaction::new(steps));
221 }
222 }
223
224 pub fn insert_link(editor: &mut Editor, url: &str) {
227 let selected = editor.selected_text();
228 if selected.is_empty() {
229 editor.insert(&format!("[]({})", url));
230 let cursor = editor.cursor();
232 let url_chars = url.chars().count();
233 let new_col = cursor.col - url_chars - 3; editor.set_cursor(Position::new(cursor.line, new_col));
235 } else {
236 editor.insert(&format!("[{}]({})", selected, url));
237 }
238 }
239
240 pub fn insert_code_block(editor: &mut Editor, language: &str) {
242 let has_selection = !editor.selection().is_cursor();
243 let selected = editor.selected_text();
244
245 if has_selection {
246 editor.insert(&format!("```{language}\n{selected}\n```"));
247 } else {
248 editor.insert(&format!("```{language}\n\n```"));
249 let cursor = editor.cursor();
251 if cursor.line > 0 {
252 editor.set_cursor(Position::new(cursor.line - 1, 0));
253 }
254 }
255 }
256
257 pub fn insert_paragraph_break(editor: &mut Editor, newline: &str) {
264 let cursor = editor.cursor();
265 let line_text = editor.buffer().line(cursor.line).to_string();
266 let before_cursor: String = line_text.chars().take(cursor.col).collect();
268
269 let active = Self::active_inline_markers(&before_cursor);
270 if active.is_empty() {
271 editor.insert(newline);
272 } else {
273 let mut buf = String::new();
275 for m in active.iter().rev() {
276 buf.push_str(m);
277 }
278 buf.push_str(newline);
279 for m in &active {
280 buf.push_str(m);
281 }
282 editor.insert(&buf);
283 }
284 }
285
286 pub fn insert_horizontal_rule(editor: &mut Editor) {
288 let cursor = editor.cursor();
289 let at_line_start = cursor.col == 0;
290 let prefix = if at_line_start { "" } else { "\n" };
291 editor.insert(&format!("{prefix}---\n"));
292 }
293
294 pub fn formatting_at_cursor(editor: &Editor) -> FormattingState {
303 let cursor = editor.cursor();
304 let line_text = editor.buffer().line(cursor.line).to_string();
305 let before_cursor: String = line_text.chars().take(cursor.col).collect();
306
307 let active = Self::active_inline_markers(&before_cursor);
309
310 let trimmed = line_text.trim_start();
312 let heading_level = if trimmed.starts_with("### ") || trimmed == "###" {
313 3
314 } else if trimmed.starts_with("## ") || trimmed == "##" {
315 2
316 } else if trimmed.starts_with("# ") || trimmed == "#" {
317 1
318 } else {
319 0
320 };
321
322 let ordered_list = {
323 let digit_count = trimmed.chars().take_while(|c| c.is_ascii_digit()).count();
324 if digit_count > 0 {
325 let rest = &trimmed[digit_count..];
326 rest.starts_with(". ") || rest.starts_with(") ")
327 } else {
328 false
329 }
330 };
331
332 FormattingState {
333 bold: active.contains(&"**"),
334 italic: active.contains(&"*"),
335 code: active.contains(&"`"),
336 strikethrough: active.contains(&"~~"),
337 heading_level,
338 bullet_list: trimmed.starts_with("- ")
339 || trimmed.starts_with("* ")
340 || trimmed.starts_with("+ "),
341 ordered_list,
342 blockquote: trimmed.starts_with("> ") || trimmed == ">",
343 }
344 }
345
346 pub fn active_inline_markers(text: &str) -> Vec<&'static str> {
357 let mut bold_count = 0usize;
360 let mut italic_count = 0usize;
361 let mut code_count = 0usize;
362 let mut strike_count = 0usize;
363
364 let mut open_stack: Vec<&'static str> = Vec::new();
367
368 let chars: Vec<char> = text.chars().collect();
369 let len = chars.len();
370 let mut i = 0;
371
372 while i < len {
374 if chars[i] == '`' {
375 code_count += 1;
376 if code_count % 2 == 1 {
377 open_stack.push("`");
378 } else {
379 Self::remove_last_marker(&mut open_stack, "`");
380 }
381 i += 1;
382 if code_count % 2 == 1 {
384 while i < len && chars[i] != '`' {
385 i += 1;
386 }
387 }
389 continue;
390 }
391
392 if code_count % 2 == 0 {
394 if chars[i] == '~' && i + 1 < len && chars[i + 1] == '~' {
395 strike_count += 1;
396 if strike_count % 2 == 1 {
397 open_stack.push("~~");
398 } else {
399 Self::remove_last_marker(&mut open_stack, "~~");
400 }
401 i += 2;
402 continue;
403 }
404
405 if chars[i] == '*' && i + 1 < len && chars[i + 1] == '*' {
406 if i + 2 < len && chars[i + 2] == '*' {
408 bold_count += 1;
409 italic_count += 1;
410 if bold_count % 2 == 1 {
411 open_stack.push("**");
412 } else {
413 Self::remove_last_marker(&mut open_stack, "**");
414 }
415 if italic_count % 2 == 1 {
416 open_stack.push("*");
417 } else {
418 Self::remove_last_marker(&mut open_stack, "*");
419 }
420 i += 3;
421 continue;
422 }
423 bold_count += 1;
424 if bold_count % 2 == 1 {
425 open_stack.push("**");
426 } else {
427 Self::remove_last_marker(&mut open_stack, "**");
428 }
429 i += 2;
430 continue;
431 }
432
433 if chars[i] == '*' {
434 italic_count += 1;
435 if italic_count % 2 == 1 {
436 open_stack.push("*");
437 } else {
438 Self::remove_last_marker(&mut open_stack, "*");
439 }
440 i += 1;
441 continue;
442 }
443 }
444
445 i += 1;
446 }
447
448 open_stack
450 }
451
452 fn remove_last_marker(stack: &mut Vec<&'static str>, marker: &str) {
454 if let Some(pos) = stack.iter().rposition(|m| *m == marker) {
455 stack.remove(pos);
456 }
457 }
458
459 fn toggle_inline_mark(editor: &mut Editor, mark: &str) {
460 let sel = editor.selection();
461 let mark_chars = mark.chars().count();
462
463 if sel.is_cursor() {
464 editor.insert(&format!("{mark}{mark}"));
466 let cursor = editor.cursor();
467 let new_col = cursor.col - mark_chars;
468 editor.set_cursor(Position::new(cursor.line, new_col));
469 return;
470 }
471
472 let selected = editor.selected_text();
473 let start = sel.start();
474 let end = sel.end();
475
476 let start_char = editor.buffer().pos_to_char(start);
479 let end_char = editor.buffer().pos_to_char(end);
480 let total_chars = editor.buffer().len_chars();
481
482 let has_mark_before = start_char >= mark_chars && {
484 let before: String = editor.buffer().rope()
485 .slice((start_char - mark_chars)..start_char)
486 .to_string();
487 before == mark
488 };
489 let has_mark_after = end_char + mark_chars <= total_chars && {
490 let after: String = editor.buffer().rope()
491 .slice(end_char..(end_char + mark_chars))
492 .to_string();
493 after == mark
494 };
495
496 if has_mark_before && has_mark_after {
497 let outer_start = Position::new(start.line, start.col - mark_chars);
499 let outer_end = Position::new(end.line, end.col + mark_chars);
500 editor.set_selection(outer_start, outer_end);
501 editor.insert(&selected);
502 } else {
503 editor.insert(&format!("{mark}{selected}{mark}"));
505 }
506 }
507
508 fn toggle_list_prefix(editor: &mut Editor, prefix: &str) {
509 let sel = editor.selection();
510 let start_line = sel.start().line;
511 let end_line = sel.end().line;
512
513 let all_prefixed = (start_line..=end_line).all(|line| {
514 let text = editor.buffer().line(line).to_string();
515 text.starts_with(prefix)
516 });
517
518 let prefix_chars = prefix.chars().count();
519 let mut steps = Vec::new();
520 let mut offset_delta: isize = 0;
521 for line in start_line..=end_line {
522 let line_text = editor.buffer().line(line).to_string();
523 let line_start_char = editor.buffer().line_to_char(line);
524 let adjusted_offset = (line_start_char as isize + offset_delta) as usize;
525 let content = line_text.trim_end_matches('\n');
526 let content_chars = content.chars().count();
527
528 if all_prefixed {
529 let stripped: String = content.chars().skip(prefix_chars).collect();
530 let new_chars = stripped.chars().count();
531 steps.push(EditStep::replace(adjusted_offset, content.to_string(), stripped));
532 offset_delta += new_chars as isize - content_chars as isize;
533 } else {
534 if content.starts_with(prefix) {
535 continue; }
537 let stripped = Self::strip_block_prefix(content);
539 let new_text = format!("{prefix}{stripped}");
540 let new_chars = new_text.chars().count();
541 steps.push(EditStep::replace(adjusted_offset, content.to_string(), new_text));
542 offset_delta += new_chars as isize - content_chars as isize;
543 }
544 }
545
546 if !steps.is_empty() {
547 editor.apply_transaction(Transaction::new(steps));
548 }
549 }
550
551 fn strip_block_prefix(s: &str) -> &str {
554 let trimmed = s.trim_start();
555 if trimmed.starts_with('#') {
557 let after_hashes = trimmed.trim_start_matches('#');
558 let stripped = after_hashes.strip_prefix(' ').unwrap_or(after_hashes);
559 return stripped;
560 }
561 if let Some(after) = trimmed.strip_prefix('>') {
563 return after.strip_prefix(' ').unwrap_or(after);
564 }
565 s
566 }
567
568 fn strip_ordered_prefix(s: &str) -> Option<&str> {
571 let digit_count = s.chars().take_while(|c| c.is_ascii_digit()).count();
572 if digit_count == 0 {
573 return None;
574 }
575 let rest = &s[digit_count..]; if let Some(after) = rest.strip_prefix(". ") {
577 Some(after)
578 } else if let Some(after) = rest.strip_prefix(") ") {
579 Some(after)
580 } else {
581 None
582 }
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::*;
589 use kode_core::Position;
590
591 #[test]
592 fn toggle_bold_no_selection() {
593 let mut ed = Editor::new("hello");
594 ed.set_cursor(Position::new(0, 5));
595 MarkdownCommands::toggle_bold(&mut ed);
596 assert_eq!(ed.text(), "hello****");
597 assert_eq!(ed.cursor(), Position::new(0, 7)); }
599
600 #[test]
601 fn toggle_bold_with_selection() {
602 let mut ed = Editor::new("hello world");
603 ed.set_selection(Position::new(0, 6), Position::new(0, 11));
604 MarkdownCommands::toggle_bold(&mut ed);
605 assert_eq!(ed.text(), "hello **world**");
606 }
607
608 #[test]
609 fn toggle_bold_remove() {
610 let mut ed = Editor::new("hello **world**");
611 ed.set_selection(Position::new(0, 8), Position::new(0, 13));
613 MarkdownCommands::toggle_bold(&mut ed);
614 assert_eq!(ed.text(), "hello world");
615 }
616
617 #[test]
618 fn set_heading_level() {
619 let mut ed = Editor::new("Hello world");
620 ed.set_cursor(Position::new(0, 0));
621 MarkdownCommands::set_heading(&mut ed, 2);
622 assert_eq!(ed.text(), "## Hello world");
623
624 MarkdownCommands::set_heading(&mut ed, 1);
626 assert_eq!(ed.text(), "# Hello world");
627
628 MarkdownCommands::set_heading(&mut ed, 0);
630 assert_eq!(ed.text(), "Hello world");
631 }
632
633 #[test]
634 fn set_heading_non_ascii() {
635 let mut ed = Editor::new("日本語");
636 ed.set_cursor(Position::new(0, 0));
637 MarkdownCommands::set_heading(&mut ed, 2);
638 assert_eq!(ed.text(), "## 日本語");
639 assert_eq!(ed.cursor().col, 3);
642 }
643
644 #[test]
645 fn set_heading_empty_line() {
646 let mut ed = Editor::new("");
647 ed.set_cursor(Position::new(0, 0));
648 MarkdownCommands::set_heading(&mut ed, 1);
649 assert_eq!(ed.text(), "# ");
650 MarkdownCommands::set_heading(&mut ed, 0);
651 assert_eq!(ed.text(), "");
652 }
653
654 #[test]
655 fn toggle_blockquote() {
656 let mut ed = Editor::new("Hello\nWorld");
657 ed.set_selection(Position::new(0, 0), Position::new(1, 5));
658 MarkdownCommands::toggle_blockquote(&mut ed);
659 assert_eq!(ed.text(), "> Hello\n> World");
660
661 ed.undo();
663 assert_eq!(ed.text(), "Hello\nWorld");
664 }
665
666 #[test]
667 fn toggle_blockquote_atomic_undo() {
668 let mut ed = Editor::new("Line 1\nLine 2\nLine 3");
669 ed.set_selection(Position::new(0, 0), Position::new(2, 6));
670 MarkdownCommands::toggle_blockquote(&mut ed);
671 assert_eq!(ed.text(), "> Line 1\n> Line 2\n> Line 3");
672
673 ed.undo();
675 assert_eq!(ed.text(), "Line 1\nLine 2\nLine 3");
676 }
677
678 #[test]
679 fn toggle_bullet_list() {
680 let mut ed = Editor::new("Item 1\nItem 2");
681 ed.set_selection(Position::new(0, 0), Position::new(1, 6));
682 MarkdownCommands::toggle_bullet_list(&mut ed);
683 assert_eq!(ed.text(), "- Item 1\n- Item 2");
684
685 ed.undo();
687 assert_eq!(ed.text(), "Item 1\nItem 2");
688 }
689
690 #[test]
691 fn toggle_bullet_list_mixed_lines() {
692 let mut ed = Editor::new("- already\nplain");
693 ed.set_selection(Position::new(0, 0), Position::new(1, 5));
694 MarkdownCommands::toggle_bullet_list(&mut ed);
695 assert_eq!(ed.text(), "- already\n- plain");
697 }
698
699 #[test]
700 fn toggle_ordered_list() {
701 let mut ed = Editor::new("First\nSecond");
702 ed.set_selection(Position::new(0, 0), Position::new(1, 6));
703 MarkdownCommands::toggle_ordered_list(&mut ed);
704 assert_eq!(ed.text(), "1. First\n2. Second");
705 }
706
707 #[test]
708 fn toggle_ordered_list_remove_with_dots_in_content() {
709 let mut ed = Editor::new("1. Dr. Smith\n2. Mr. Jones");
710 ed.set_selection(Position::new(0, 0), Position::new(1, 13));
711 MarkdownCommands::toggle_ordered_list(&mut ed);
712 assert_eq!(ed.text(), "Dr. Smith\nMr. Jones");
714 }
715
716 #[test]
717 fn toggle_ordered_list_preserves_indent() {
718 let mut ed = Editor::new(" 1. nested");
719 ed.set_selection(Position::new(0, 0), Position::new(0, 11));
720 MarkdownCommands::toggle_ordered_list(&mut ed);
721 assert_eq!(ed.text(), " nested");
722 }
723
724 #[test]
725 fn insert_link_with_selection() {
726 let mut ed = Editor::new("click here for more");
727 ed.set_selection(Position::new(0, 6), Position::new(0, 10));
728 MarkdownCommands::insert_link(&mut ed, "https://example.com");
729 assert_eq!(ed.text(), "click [here](https://example.com) for more");
730 }
731
732 #[test]
733 fn insert_link_non_ascii_url() {
734 let mut ed = Editor::new("click ");
735 ed.set_cursor(Position::new(0, 6));
736 MarkdownCommands::insert_link(&mut ed, "https://日本.jp");
737 assert_eq!(ed.text(), "click [](https://日本.jp)");
738 assert_eq!(ed.cursor(), Position::new(0, 7));
740 }
741
742 #[test]
743 fn insert_code_block() {
744 let mut ed = Editor::new("Some text\n");
745 ed.set_cursor(Position::new(1, 0));
746 MarkdownCommands::insert_code_block(&mut ed, "sql");
747 assert_eq!(ed.text(), "Some text\n```sql\n\n```");
748 assert_eq!(ed.cursor(), Position::new(2, 0));
749 }
750
751 #[test]
752 fn insert_code_block_at_start_of_empty_doc() {
753 let mut ed = Editor::new("");
754 ed.set_cursor(Position::new(0, 0));
755 MarkdownCommands::insert_code_block(&mut ed, "rust");
756 assert_eq!(ed.text(), "```rust\n\n```");
757 assert_eq!(ed.cursor(), Position::new(1, 0));
758 }
759
760 #[test]
761 fn insert_code_block_with_selection() {
762 let mut ed = Editor::new("SELECT 1");
763 ed.select_all();
764 MarkdownCommands::insert_code_block(&mut ed, "sql");
765 assert_eq!(ed.text(), "```sql\nSELECT 1\n```");
766 }
767
768 #[test]
769 fn toggle_italic() {
770 let mut ed = Editor::new("hello world");
771 ed.set_selection(Position::new(0, 6), Position::new(0, 11));
772 MarkdownCommands::toggle_italic(&mut ed);
773 assert_eq!(ed.text(), "hello *world*");
774 }
775
776 #[test]
777 fn toggle_inline_code() {
778 let mut ed = Editor::new("use this function");
779 ed.set_selection(Position::new(0, 9), Position::new(0, 17));
780 MarkdownCommands::toggle_inline_code(&mut ed);
781 assert_eq!(ed.text(), "use this `function`");
782 }
783
784 #[test]
785 fn toggle_bold_undo_restores_original() {
786 let mut ed = Editor::new("hello world");
787 ed.set_selection(Position::new(0, 6), Position::new(0, 11));
788 MarkdownCommands::toggle_bold(&mut ed);
789 assert_eq!(ed.text(), "hello **world**");
790
791 ed.undo();
792 assert_eq!(ed.text(), "hello world");
793 }
794
795 #[test]
798 fn paragraph_break_inside_bold() {
799 let mut ed = Editor::new("**bold text**");
800 ed.set_cursor(Position::new(0, 7));
805 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
806 assert_eq!(ed.text(), "**bold **\n\n**text**");
807 }
808
809 #[test]
810 fn paragraph_break_inside_italic() {
811 let mut ed = Editor::new("*italic text*");
812 ed.set_cursor(Position::new(0, 8));
814 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
815 assert_eq!(ed.text(), "*italic *\n\n*text*");
816 }
817
818 #[test]
819 fn paragraph_break_inside_inline_code() {
820 let mut ed = Editor::new("`some code`");
821 ed.set_cursor(Position::new(0, 5));
823 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
824 assert_eq!(ed.text(), "`some`\n\n` code`");
825 }
826
827 #[test]
828 fn paragraph_break_inside_strikethrough() {
829 let mut ed = Editor::new("~~struck text~~");
830 ed.set_cursor(Position::new(0, 8));
832 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
833 assert_eq!(ed.text(), "~~struck~~\n\n~~ text~~");
834 }
835
836 #[test]
837 fn paragraph_break_no_markers() {
838 let mut ed = Editor::new("plain text");
839 ed.set_cursor(Position::new(0, 5));
840 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
841 assert_eq!(ed.text(), "plain\n\n text");
842 }
843
844 #[test]
845 fn paragraph_break_closed_markers_not_reopened() {
846 let mut ed = Editor::new("**bold** and more");
848 ed.set_cursor(Position::new(0, 13));
850 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
851 assert_eq!(ed.text(), "**bold** and \n\nmore");
852 }
853
854 #[test]
855 fn paragraph_break_nested_bold_italic() {
856 let mut ed = Editor::new("***bold italic text***");
857 ed.set_cursor(Position::new(0, 15));
862 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
863 assert_eq!(ed.text(), "***bold italic ***\n\n***text***");
864 }
865
866 #[test]
867 fn paragraph_break_soft_break_inside_bold() {
868 let mut ed = Editor::new("**bold text**");
869 ed.set_cursor(Position::new(0, 7));
870 MarkdownCommands::insert_paragraph_break(&mut ed, "\n");
871 assert_eq!(ed.text(), "**bold **\n**text**");
872 }
873
874 #[test]
875 fn paragraph_break_markers_inside_code_ignored() {
876 let mut ed = Editor::new("`code **not bold**`");
878 ed.set_cursor(Position::new(0, 10));
879 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
880 assert_eq!(ed.text(), "`code **no`\n\n`t bold**`");
888 }
889
890 #[test]
893 fn active_markers_empty() {
894 assert!(MarkdownCommands::active_inline_markers("plain text").is_empty());
895 }
896
897 #[test]
898 fn active_markers_bold_open() {
899 assert_eq!(MarkdownCommands::active_inline_markers("**bold "), vec!["**"]);
900 }
901
902 #[test]
903 fn active_markers_bold_closed() {
904 assert!(MarkdownCommands::active_inline_markers("**bold** after").is_empty());
905 }
906
907 #[test]
908 fn active_markers_italic_open() {
909 assert_eq!(MarkdownCommands::active_inline_markers("*italic "), vec!["*"]);
910 }
911
912 #[test]
913 fn active_markers_code_open() {
914 assert_eq!(MarkdownCommands::active_inline_markers("`code "), vec!["`"]);
915 }
916
917 #[test]
918 fn active_markers_strike_open() {
919 assert_eq!(MarkdownCommands::active_inline_markers("~~strike "), vec!["~~"]);
920 }
921
922 #[test]
923 fn active_markers_bold_italic_open() {
924 let markers = MarkdownCommands::active_inline_markers("***bold italic ");
925 assert_eq!(markers, vec!["**", "*"]);
926 }
927
928 #[test]
929 fn active_markers_code_hides_bold() {
930 assert_eq!(MarkdownCommands::active_inline_markers("`code **bold "), vec!["`"]);
932 }
933
934 #[test]
935 fn toggle_bold_with_unicode() {
936 let mut ed = Editor::new("hello café world");
937 ed.set_selection(Position::new(0, 6), Position::new(0, 10));
939 MarkdownCommands::toggle_bold(&mut ed);
940 assert_eq!(ed.text(), "hello **café** world");
941 }
942
943 #[test]
946 fn active_markers_triple_star_open() {
947 let markers = MarkdownCommands::active_inline_markers("Text ***bold italic");
949 assert_eq!(markers, vec!["**", "*"]);
950 }
951
952 #[test]
953 fn paragraph_break_inside_triple_star_bold_italic() {
954 let mut ed = Editor::new("Text ***bold italicstuff*** end");
957 ed.set_cursor(Position::new(0, 19));
959 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
960 assert_eq!(ed.text(), "Text ***bold italic***\n\n***stuff*** end");
961 }
962
963 #[test]
966 fn active_markers_bold_near_triple_star_boundary() {
967 let markers = MarkdownCommands::active_inline_markers("**bold te");
969 assert_eq!(markers, vec!["**"]);
970 }
971
972 #[test]
973 fn paragraph_break_at_bold_italic_boundary() {
974 let mut ed = Editor::new("**bold text***italic text*");
978 ed.set_cursor(Position::new(0, 9));
979 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
980 assert_eq!(ed.text(), "**bold te**\n\n**xt***italic text*");
981 }
982
983 #[test]
986 fn active_markers_adjacent_bold_spans_between() {
987 let markers = MarkdownCommands::active_inline_markers("**first** ");
989 assert!(markers.is_empty());
990 }
991
992 #[test]
993 fn paragraph_break_between_adjacent_bold_spans() {
994 let mut ed = Editor::new("**first** **second**");
996 ed.set_cursor(Position::new(0, 10));
998 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
999 assert_eq!(ed.text(), "**first** \n\n**second**");
1000 }
1001
1002 #[test]
1005 fn paragraph_break_single_char_bold_cursor_after_open() {
1006 let mut ed = Editor::new("**X**");
1011 ed.set_cursor(Position::new(0, 2));
1012 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
1013 assert_eq!(ed.text(), "****\n\n**X**");
1014 }
1015
1016 #[test]
1017 fn paragraph_break_single_char_bold_cursor_inside() {
1018 let mut ed = Editor::new("Before **X** after");
1023 ed.set_cursor(Position::new(0, 10));
1024 MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
1025 assert_eq!(ed.text(), "Before **X**\n\n**** after");
1026 }
1027
1028 #[test]
1029 fn active_markers_single_char_bold_cursor_outside() {
1030 let markers = MarkdownCommands::active_inline_markers("Before **X** aft");
1032 assert!(markers.is_empty());
1033 }
1034
1035 #[test]
1040 fn paragraph_break_soft_break_no_markers() {
1041 let mut ed = Editor::new("plain text here");
1042 ed.set_cursor(Position::new(0, 6));
1043 MarkdownCommands::insert_paragraph_break(&mut ed, "\n");
1044 assert_eq!(ed.text(), "plain \ntext here");
1045 }
1046}