use kode_core::{EditStep, Editor, Position, Transaction};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FormattingState {
pub bold: bool,
pub italic: bool,
pub code: bool,
pub strikethrough: bool,
pub heading_level: u8,
pub bullet_list: bool,
pub ordered_list: bool,
pub blockquote: bool,
}
pub struct MarkdownCommands;
impl MarkdownCommands {
pub fn toggle_bold(editor: &mut Editor) {
Self::toggle_inline_mark(editor, "**");
}
pub fn toggle_italic(editor: &mut Editor) {
Self::toggle_inline_mark(editor, "*");
}
pub fn toggle_inline_code(editor: &mut Editor) {
Self::toggle_inline_mark(editor, "`");
}
pub fn toggle_strikethrough(editor: &mut Editor) {
Self::toggle_inline_mark(editor, "~~");
}
pub fn set_heading(editor: &mut Editor, level: u8) {
let mut cursor = editor.cursor();
let line_text = editor.buffer().line(cursor.line).to_string();
if line_text.trim().is_empty() && cursor.line > 0 {
let prev_line_text = editor.buffer().line(cursor.line - 1).to_string();
if prev_line_text.trim_start().starts_with('#') {
cursor = Position::new(cursor.line - 1, editor.buffer().line_len(cursor.line - 1));
editor.set_cursor(cursor);
} else if level > 0 {
return; }
}
let line_text = editor.buffer().line(cursor.line).to_string();
let trimmed = line_text.trim_start();
let content_start_chars = if trimmed.starts_with('#') {
let hashes = trimmed.chars().take_while(|&c| c == '#').count();
let rest: String = trimmed.chars().skip(hashes).collect();
let space_after = if rest.starts_with(' ') { 1 } else { 0 };
hashes + space_after
} else {
0
};
let content: String = trimmed.chars().skip(content_start_chars).collect();
let content = content.trim_end_matches('\n');
let new_line = if level == 0 {
content.to_string()
} else {
let prefix: String = "#".repeat(level as usize);
format!("{prefix} {content}")
};
let line_start = Position::new(cursor.line, 0);
let line_end_col = editor.buffer().line_len(cursor.line);
let line_end = Position::new(cursor.line, line_end_col);
editor.set_selection(line_start, line_end);
editor.insert(&new_line);
let new_prefix_chars = if level == 0 { 0 } else { level as usize + 1 };
let old_content_col = cursor.col.saturating_sub(content_start_chars);
let new_col = (new_prefix_chars + old_content_col).min(new_line.chars().count());
editor.set_cursor(Position::new(cursor.line, new_col));
}
pub fn toggle_blockquote(editor: &mut Editor) {
let sel = editor.selection();
let cursor = editor.cursor();
let start_line = sel.start().line;
let end_line = sel.end().line;
let all_quoted = (start_line..=end_line).all(|line| {
let text = editor.buffer().line(line).to_string();
text.starts_with("> ") || text.starts_with(">")
});
let mut steps = Vec::new();
let mut offset_delta: isize = 0;
for line in start_line..=end_line {
let line_text = editor.buffer().line(line).to_string();
let line_start_char = editor.buffer().line_to_char(line);
let adjusted_offset = (line_start_char as isize + offset_delta) as usize;
let content = line_text.trim_end_matches('\n');
let content_chars = content.chars().count();
if all_quoted {
let unquoted = if let Some(s) = content.strip_prefix("> ") {
s
} else if let Some(s) = content.strip_prefix('>') {
s
} else {
content
};
let new_chars = unquoted.chars().count();
steps.push(EditStep::replace(adjusted_offset, content.to_string(), unquoted.to_string()));
offset_delta += new_chars as isize - content_chars as isize;
} else {
let new_text = format!("> {content}");
let new_chars = new_text.chars().count();
steps.push(EditStep::replace(adjusted_offset, content.to_string(), new_text));
offset_delta += new_chars as isize - content_chars as isize;
}
}
if !steps.is_empty() {
editor.apply_transaction(Transaction::new(steps));
let prefix_delta: isize = if all_quoted { -2 } else { 2 };
let new_col = (cursor.col as isize + prefix_delta).max(0) as usize;
let line_len = editor.buffer().line_len(cursor.line);
editor.set_cursor(Position::new(cursor.line, new_col.min(line_len)));
}
}
pub fn toggle_bullet_list(editor: &mut Editor) {
Self::toggle_list_prefix(editor, "- ");
}
pub fn toggle_ordered_list(editor: &mut Editor) {
let sel = editor.selection();
let start_line = sel.start().line;
let end_line = sel.end().line;
let all_ordered = (start_line..=end_line).all(|line| {
let text = editor.buffer().line(line).to_string();
let trimmed = text.trim_start();
Self::strip_ordered_prefix(trimmed).is_some()
});
let mut steps = Vec::new();
let mut offset_delta: isize = 0;
for line in start_line..=end_line {
let line_text = editor.buffer().line(line).to_string();
let line_start_char = editor.buffer().line_to_char(line);
let adjusted_offset = (line_start_char as isize + offset_delta) as usize;
let content = line_text.trim_end_matches('\n');
let content_chars = content.chars().count();
if all_ordered {
let trimmed = content.trim_start();
let leading_indent = &content[..content.len() - trimmed.len()]; let stripped = Self::strip_ordered_prefix(trimmed).unwrap_or(trimmed);
let new_text = format!("{leading_indent}{stripped}");
let new_chars = new_text.chars().count();
steps.push(EditStep::replace(adjusted_offset, content.to_string(), new_text));
offset_delta += new_chars as isize - content_chars as isize;
} else {
let num = line - start_line + 1;
let inner = content
.strip_prefix("- ")
.or_else(|| content.strip_prefix("* "))
.or_else(|| content.strip_prefix("+ "))
.unwrap_or_else(|| Self::strip_block_prefix(content));
let new_text = format!("{num}. {inner}");
let new_chars = new_text.chars().count();
steps.push(EditStep::replace(adjusted_offset, content.to_string(), new_text));
offset_delta += new_chars as isize - content_chars as isize;
}
}
if !steps.is_empty() {
editor.apply_transaction(Transaction::new(steps));
}
}
pub fn insert_link(editor: &mut Editor, url: &str) {
let selected = editor.selected_text();
if selected.is_empty() {
editor.insert(&format!("[]({})", url));
let cursor = editor.cursor();
let url_chars = url.chars().count();
let new_col = cursor.col - url_chars - 3; editor.set_cursor(Position::new(cursor.line, new_col));
} else {
editor.insert(&format!("[{}]({})", selected, url));
}
}
pub fn insert_code_block(editor: &mut Editor, language: &str) {
let has_selection = !editor.selection().is_cursor();
let selected = editor.selected_text();
if has_selection {
editor.insert(&format!("```{language}\n{selected}\n```"));
} else {
editor.insert(&format!("```{language}\n\n```"));
let cursor = editor.cursor();
if cursor.line > 0 {
editor.set_cursor(Position::new(cursor.line - 1, 0));
}
}
}
pub fn insert_paragraph_break(editor: &mut Editor, newline: &str) {
let cursor = editor.cursor();
let line_text = editor.buffer().line(cursor.line).to_string();
let before_cursor: String = line_text.chars().take(cursor.col).collect();
let active = Self::active_inline_markers(&before_cursor);
if active.is_empty() {
editor.insert(newline);
} else {
let mut buf = String::new();
for m in active.iter().rev() {
buf.push_str(m);
}
buf.push_str(newline);
for m in &active {
buf.push_str(m);
}
editor.insert(&buf);
}
}
pub fn insert_horizontal_rule(editor: &mut Editor) {
let cursor = editor.cursor();
let at_line_start = cursor.col == 0;
let prefix = if at_line_start { "" } else { "\n" };
editor.insert(&format!("{prefix}---\n"));
}
pub fn formatting_at_cursor(editor: &Editor) -> FormattingState {
let cursor = editor.cursor();
let line_text = editor.buffer().line(cursor.line).to_string();
let before_cursor: String = line_text.chars().take(cursor.col).collect();
let active = Self::active_inline_markers(&before_cursor);
let trimmed = line_text.trim_start();
let heading_level = if trimmed.starts_with("### ") || trimmed == "###" {
3
} else if trimmed.starts_with("## ") || trimmed == "##" {
2
} else if trimmed.starts_with("# ") || trimmed == "#" {
1
} else {
0
};
let ordered_list = {
let digit_count = trimmed.chars().take_while(|c| c.is_ascii_digit()).count();
if digit_count > 0 {
let rest = &trimmed[digit_count..];
rest.starts_with(". ") || rest.starts_with(") ")
} else {
false
}
};
FormattingState {
bold: active.contains(&"**"),
italic: active.contains(&"*"),
code: active.contains(&"`"),
strikethrough: active.contains(&"~~"),
heading_level,
bullet_list: trimmed.starts_with("- ")
|| trimmed.starts_with("* ")
|| trimmed.starts_with("+ "),
ordered_list,
blockquote: trimmed.starts_with("> ") || trimmed == ">",
}
}
pub fn active_inline_markers(text: &str) -> Vec<&'static str> {
let mut bold_count = 0usize;
let mut italic_count = 0usize;
let mut code_count = 0usize;
let mut strike_count = 0usize;
let mut open_stack: Vec<&'static str> = Vec::new();
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if chars[i] == '`' {
code_count += 1;
if code_count % 2 == 1 {
open_stack.push("`");
} else {
Self::remove_last_marker(&mut open_stack, "`");
}
i += 1;
if code_count % 2 == 1 {
while i < len && chars[i] != '`' {
i += 1;
}
}
continue;
}
if code_count % 2 == 0 {
if chars[i] == '~' && i + 1 < len && chars[i + 1] == '~' {
strike_count += 1;
if strike_count % 2 == 1 {
open_stack.push("~~");
} else {
Self::remove_last_marker(&mut open_stack, "~~");
}
i += 2;
continue;
}
if chars[i] == '*' && i + 1 < len && chars[i + 1] == '*' {
if i + 2 < len && chars[i + 2] == '*' {
bold_count += 1;
italic_count += 1;
if bold_count % 2 == 1 {
open_stack.push("**");
} else {
Self::remove_last_marker(&mut open_stack, "**");
}
if italic_count % 2 == 1 {
open_stack.push("*");
} else {
Self::remove_last_marker(&mut open_stack, "*");
}
i += 3;
continue;
}
bold_count += 1;
if bold_count % 2 == 1 {
open_stack.push("**");
} else {
Self::remove_last_marker(&mut open_stack, "**");
}
i += 2;
continue;
}
if chars[i] == '*' {
italic_count += 1;
if italic_count % 2 == 1 {
open_stack.push("*");
} else {
Self::remove_last_marker(&mut open_stack, "*");
}
i += 1;
continue;
}
}
i += 1;
}
open_stack
}
fn remove_last_marker(stack: &mut Vec<&'static str>, marker: &str) {
if let Some(pos) = stack.iter().rposition(|m| *m == marker) {
stack.remove(pos);
}
}
fn toggle_inline_mark(editor: &mut Editor, mark: &str) {
let sel = editor.selection();
let mark_chars = mark.chars().count();
if sel.is_cursor() {
editor.insert(&format!("{mark}{mark}"));
let cursor = editor.cursor();
let new_col = cursor.col - mark_chars;
editor.set_cursor(Position::new(cursor.line, new_col));
return;
}
let selected = editor.selected_text();
let start = sel.start();
let end = sel.end();
let start_char = editor.buffer().pos_to_char(start);
let end_char = editor.buffer().pos_to_char(end);
let total_chars = editor.buffer().len_chars();
let has_mark_before = start_char >= mark_chars && {
let before: String = editor.buffer().rope()
.slice((start_char - mark_chars)..start_char)
.to_string();
before == mark
};
let has_mark_after = end_char + mark_chars <= total_chars && {
let after: String = editor.buffer().rope()
.slice(end_char..(end_char + mark_chars))
.to_string();
after == mark
};
if has_mark_before && has_mark_after {
let outer_start = Position::new(start.line, start.col - mark_chars);
let outer_end = Position::new(end.line, end.col + mark_chars);
editor.set_selection(outer_start, outer_end);
editor.insert(&selected);
} else {
editor.insert(&format!("{mark}{selected}{mark}"));
}
}
fn toggle_list_prefix(editor: &mut Editor, prefix: &str) {
let sel = editor.selection();
let start_line = sel.start().line;
let end_line = sel.end().line;
let all_prefixed = (start_line..=end_line).all(|line| {
let text = editor.buffer().line(line).to_string();
text.starts_with(prefix)
});
let prefix_chars = prefix.chars().count();
let mut steps = Vec::new();
let mut offset_delta: isize = 0;
for line in start_line..=end_line {
let line_text = editor.buffer().line(line).to_string();
let line_start_char = editor.buffer().line_to_char(line);
let adjusted_offset = (line_start_char as isize + offset_delta) as usize;
let content = line_text.trim_end_matches('\n');
let content_chars = content.chars().count();
if all_prefixed {
let stripped: String = content.chars().skip(prefix_chars).collect();
let new_chars = stripped.chars().count();
steps.push(EditStep::replace(adjusted_offset, content.to_string(), stripped));
offset_delta += new_chars as isize - content_chars as isize;
} else {
if content.starts_with(prefix) {
continue; }
let stripped = Self::strip_block_prefix(content);
let new_text = format!("{prefix}{stripped}");
let new_chars = new_text.chars().count();
steps.push(EditStep::replace(adjusted_offset, content.to_string(), new_text));
offset_delta += new_chars as isize - content_chars as isize;
}
}
if !steps.is_empty() {
editor.apply_transaction(Transaction::new(steps));
}
}
fn strip_block_prefix(s: &str) -> &str {
let trimmed = s.trim_start();
if trimmed.starts_with('#') {
let after_hashes = trimmed.trim_start_matches('#');
let stripped = after_hashes.strip_prefix(' ').unwrap_or(after_hashes);
return stripped;
}
if let Some(after) = trimmed.strip_prefix('>') {
return after.strip_prefix(' ').unwrap_or(after);
}
s
}
fn strip_ordered_prefix(s: &str) -> Option<&str> {
let digit_count = s.chars().take_while(|c| c.is_ascii_digit()).count();
if digit_count == 0 {
return None;
}
let rest = &s[digit_count..]; if let Some(after) = rest.strip_prefix(". ") {
Some(after)
} else if let Some(after) = rest.strip_prefix(") ") {
Some(after)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use kode_core::Position;
#[test]
fn toggle_bold_no_selection() {
let mut ed = Editor::new("hello");
ed.set_cursor(Position::new(0, 5));
MarkdownCommands::toggle_bold(&mut ed);
assert_eq!(ed.text(), "hello****");
assert_eq!(ed.cursor(), Position::new(0, 7)); }
#[test]
fn toggle_bold_with_selection() {
let mut ed = Editor::new("hello world");
ed.set_selection(Position::new(0, 6), Position::new(0, 11));
MarkdownCommands::toggle_bold(&mut ed);
assert_eq!(ed.text(), "hello **world**");
}
#[test]
fn toggle_bold_remove() {
let mut ed = Editor::new("hello **world**");
ed.set_selection(Position::new(0, 8), Position::new(0, 13));
MarkdownCommands::toggle_bold(&mut ed);
assert_eq!(ed.text(), "hello world");
}
#[test]
fn set_heading_level() {
let mut ed = Editor::new("Hello world");
ed.set_cursor(Position::new(0, 0));
MarkdownCommands::set_heading(&mut ed, 2);
assert_eq!(ed.text(), "## Hello world");
MarkdownCommands::set_heading(&mut ed, 1);
assert_eq!(ed.text(), "# Hello world");
MarkdownCommands::set_heading(&mut ed, 0);
assert_eq!(ed.text(), "Hello world");
}
#[test]
fn set_heading_non_ascii() {
let mut ed = Editor::new("日本語");
ed.set_cursor(Position::new(0, 0));
MarkdownCommands::set_heading(&mut ed, 2);
assert_eq!(ed.text(), "## 日本語");
assert_eq!(ed.cursor().col, 3);
}
#[test]
fn set_heading_empty_line() {
let mut ed = Editor::new("");
ed.set_cursor(Position::new(0, 0));
MarkdownCommands::set_heading(&mut ed, 1);
assert_eq!(ed.text(), "# ");
MarkdownCommands::set_heading(&mut ed, 0);
assert_eq!(ed.text(), "");
}
#[test]
fn toggle_blockquote() {
let mut ed = Editor::new("Hello\nWorld");
ed.set_selection(Position::new(0, 0), Position::new(1, 5));
MarkdownCommands::toggle_blockquote(&mut ed);
assert_eq!(ed.text(), "> Hello\n> World");
ed.undo();
assert_eq!(ed.text(), "Hello\nWorld");
}
#[test]
fn toggle_blockquote_atomic_undo() {
let mut ed = Editor::new("Line 1\nLine 2\nLine 3");
ed.set_selection(Position::new(0, 0), Position::new(2, 6));
MarkdownCommands::toggle_blockquote(&mut ed);
assert_eq!(ed.text(), "> Line 1\n> Line 2\n> Line 3");
ed.undo();
assert_eq!(ed.text(), "Line 1\nLine 2\nLine 3");
}
#[test]
fn toggle_bullet_list() {
let mut ed = Editor::new("Item 1\nItem 2");
ed.set_selection(Position::new(0, 0), Position::new(1, 6));
MarkdownCommands::toggle_bullet_list(&mut ed);
assert_eq!(ed.text(), "- Item 1\n- Item 2");
ed.undo();
assert_eq!(ed.text(), "Item 1\nItem 2");
}
#[test]
fn toggle_bullet_list_mixed_lines() {
let mut ed = Editor::new("- already\nplain");
ed.set_selection(Position::new(0, 0), Position::new(1, 5));
MarkdownCommands::toggle_bullet_list(&mut ed);
assert_eq!(ed.text(), "- already\n- plain");
}
#[test]
fn toggle_ordered_list() {
let mut ed = Editor::new("First\nSecond");
ed.set_selection(Position::new(0, 0), Position::new(1, 6));
MarkdownCommands::toggle_ordered_list(&mut ed);
assert_eq!(ed.text(), "1. First\n2. Second");
}
#[test]
fn toggle_ordered_list_remove_with_dots_in_content() {
let mut ed = Editor::new("1. Dr. Smith\n2. Mr. Jones");
ed.set_selection(Position::new(0, 0), Position::new(1, 13));
MarkdownCommands::toggle_ordered_list(&mut ed);
assert_eq!(ed.text(), "Dr. Smith\nMr. Jones");
}
#[test]
fn toggle_ordered_list_preserves_indent() {
let mut ed = Editor::new(" 1. nested");
ed.set_selection(Position::new(0, 0), Position::new(0, 11));
MarkdownCommands::toggle_ordered_list(&mut ed);
assert_eq!(ed.text(), " nested");
}
#[test]
fn insert_link_with_selection() {
let mut ed = Editor::new("click here for more");
ed.set_selection(Position::new(0, 6), Position::new(0, 10));
MarkdownCommands::insert_link(&mut ed, "https://example.com");
assert_eq!(ed.text(), "click [here](https://example.com) for more");
}
#[test]
fn insert_link_non_ascii_url() {
let mut ed = Editor::new("click ");
ed.set_cursor(Position::new(0, 6));
MarkdownCommands::insert_link(&mut ed, "https://日本.jp");
assert_eq!(ed.text(), "click [](https://日本.jp)");
assert_eq!(ed.cursor(), Position::new(0, 7));
}
#[test]
fn insert_code_block() {
let mut ed = Editor::new("Some text\n");
ed.set_cursor(Position::new(1, 0));
MarkdownCommands::insert_code_block(&mut ed, "sql");
assert_eq!(ed.text(), "Some text\n```sql\n\n```");
assert_eq!(ed.cursor(), Position::new(2, 0));
}
#[test]
fn insert_code_block_at_start_of_empty_doc() {
let mut ed = Editor::new("");
ed.set_cursor(Position::new(0, 0));
MarkdownCommands::insert_code_block(&mut ed, "rust");
assert_eq!(ed.text(), "```rust\n\n```");
assert_eq!(ed.cursor(), Position::new(1, 0));
}
#[test]
fn insert_code_block_with_selection() {
let mut ed = Editor::new("SELECT 1");
ed.select_all();
MarkdownCommands::insert_code_block(&mut ed, "sql");
assert_eq!(ed.text(), "```sql\nSELECT 1\n```");
}
#[test]
fn toggle_italic() {
let mut ed = Editor::new("hello world");
ed.set_selection(Position::new(0, 6), Position::new(0, 11));
MarkdownCommands::toggle_italic(&mut ed);
assert_eq!(ed.text(), "hello *world*");
}
#[test]
fn toggle_inline_code() {
let mut ed = Editor::new("use this function");
ed.set_selection(Position::new(0, 9), Position::new(0, 17));
MarkdownCommands::toggle_inline_code(&mut ed);
assert_eq!(ed.text(), "use this `function`");
}
#[test]
fn toggle_bold_undo_restores_original() {
let mut ed = Editor::new("hello world");
ed.set_selection(Position::new(0, 6), Position::new(0, 11));
MarkdownCommands::toggle_bold(&mut ed);
assert_eq!(ed.text(), "hello **world**");
ed.undo();
assert_eq!(ed.text(), "hello world");
}
#[test]
fn paragraph_break_inside_bold() {
let mut ed = Editor::new("**bold text**");
ed.set_cursor(Position::new(0, 7));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "**bold **\n\n**text**");
}
#[test]
fn paragraph_break_inside_italic() {
let mut ed = Editor::new("*italic text*");
ed.set_cursor(Position::new(0, 8));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "*italic *\n\n*text*");
}
#[test]
fn paragraph_break_inside_inline_code() {
let mut ed = Editor::new("`some code`");
ed.set_cursor(Position::new(0, 5));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "`some`\n\n` code`");
}
#[test]
fn paragraph_break_inside_strikethrough() {
let mut ed = Editor::new("~~struck text~~");
ed.set_cursor(Position::new(0, 8));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "~~struck~~\n\n~~ text~~");
}
#[test]
fn paragraph_break_no_markers() {
let mut ed = Editor::new("plain text");
ed.set_cursor(Position::new(0, 5));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "plain\n\n text");
}
#[test]
fn paragraph_break_closed_markers_not_reopened() {
let mut ed = Editor::new("**bold** and more");
ed.set_cursor(Position::new(0, 13));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "**bold** and \n\nmore");
}
#[test]
fn paragraph_break_nested_bold_italic() {
let mut ed = Editor::new("***bold italic text***");
ed.set_cursor(Position::new(0, 15));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "***bold italic ***\n\n***text***");
}
#[test]
fn paragraph_break_soft_break_inside_bold() {
let mut ed = Editor::new("**bold text**");
ed.set_cursor(Position::new(0, 7));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n");
assert_eq!(ed.text(), "**bold **\n**text**");
}
#[test]
fn paragraph_break_markers_inside_code_ignored() {
let mut ed = Editor::new("`code **not bold**`");
ed.set_cursor(Position::new(0, 10));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "`code **no`\n\n`t bold**`");
}
#[test]
fn active_markers_empty() {
assert!(MarkdownCommands::active_inline_markers("plain text").is_empty());
}
#[test]
fn active_markers_bold_open() {
assert_eq!(MarkdownCommands::active_inline_markers("**bold "), vec!["**"]);
}
#[test]
fn active_markers_bold_closed() {
assert!(MarkdownCommands::active_inline_markers("**bold** after").is_empty());
}
#[test]
fn active_markers_italic_open() {
assert_eq!(MarkdownCommands::active_inline_markers("*italic "), vec!["*"]);
}
#[test]
fn active_markers_code_open() {
assert_eq!(MarkdownCommands::active_inline_markers("`code "), vec!["`"]);
}
#[test]
fn active_markers_strike_open() {
assert_eq!(MarkdownCommands::active_inline_markers("~~strike "), vec!["~~"]);
}
#[test]
fn active_markers_bold_italic_open() {
let markers = MarkdownCommands::active_inline_markers("***bold italic ");
assert_eq!(markers, vec!["**", "*"]);
}
#[test]
fn active_markers_code_hides_bold() {
assert_eq!(MarkdownCommands::active_inline_markers("`code **bold "), vec!["`"]);
}
#[test]
fn toggle_bold_with_unicode() {
let mut ed = Editor::new("hello café world");
ed.set_selection(Position::new(0, 6), Position::new(0, 10));
MarkdownCommands::toggle_bold(&mut ed);
assert_eq!(ed.text(), "hello **café** world");
}
#[test]
fn active_markers_triple_star_open() {
let markers = MarkdownCommands::active_inline_markers("Text ***bold italic");
assert_eq!(markers, vec!["**", "*"]);
}
#[test]
fn paragraph_break_inside_triple_star_bold_italic() {
let mut ed = Editor::new("Text ***bold italicstuff*** end");
ed.set_cursor(Position::new(0, 19));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "Text ***bold italic***\n\n***stuff*** end");
}
#[test]
fn active_markers_bold_near_triple_star_boundary() {
let markers = MarkdownCommands::active_inline_markers("**bold te");
assert_eq!(markers, vec!["**"]);
}
#[test]
fn paragraph_break_at_bold_italic_boundary() {
let mut ed = Editor::new("**bold text***italic text*");
ed.set_cursor(Position::new(0, 9));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "**bold te**\n\n**xt***italic text*");
}
#[test]
fn active_markers_adjacent_bold_spans_between() {
let markers = MarkdownCommands::active_inline_markers("**first** ");
assert!(markers.is_empty());
}
#[test]
fn paragraph_break_between_adjacent_bold_spans() {
let mut ed = Editor::new("**first** **second**");
ed.set_cursor(Position::new(0, 10));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "**first** \n\n**second**");
}
#[test]
fn paragraph_break_single_char_bold_cursor_after_open() {
let mut ed = Editor::new("**X**");
ed.set_cursor(Position::new(0, 2));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "****\n\n**X**");
}
#[test]
fn paragraph_break_single_char_bold_cursor_inside() {
let mut ed = Editor::new("Before **X** after");
ed.set_cursor(Position::new(0, 10));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n\n");
assert_eq!(ed.text(), "Before **X**\n\n**** after");
}
#[test]
fn active_markers_single_char_bold_cursor_outside() {
let markers = MarkdownCommands::active_inline_markers("Before **X** aft");
assert!(markers.is_empty());
}
#[test]
fn paragraph_break_soft_break_no_markers() {
let mut ed = Editor::new("plain text here");
ed.set_cursor(Position::new(0, 6));
MarkdownCommands::insert_paragraph_break(&mut ed, "\n");
assert_eq!(ed.text(), "plain \ntext here");
}
}