use super::structured_document::*;
use std::cmp::min;
pub type EditResult = Result<(), EditError>;
#[derive(Debug, Clone, PartialEq)]
pub enum EditError {
InvalidPosition,
InvalidBlockIndex,
EmptyDocument,
}
pub struct StructuredEditor {
document: StructuredDocument,
cursor: DocumentPosition,
selection: Option<(DocumentPosition, DocumentPosition)>, paragraph_cb: Option<Box<dyn FnMut(BlockType) + 'static>>,
}
impl StructuredEditor {
fn renumber_ordered_from(&mut self, start_index: usize, start_number: u64) {
let mut n = start_number;
let blocks_len = self.document.block_count();
if start_index >= blocks_len {
return;
}
let blocks = self.document.blocks_mut();
let mut i = start_index;
while i < blocks.len() {
match blocks[i].block_type {
BlockType::ListItem { ordered: true, .. } => {
blocks[i].block_type = BlockType::ListItem {
ordered: true,
number: Some(n),
checkbox: None,
};
n += 1;
i += 1;
}
_ => break,
}
}
}
pub fn new() -> Self {
StructuredEditor {
document: StructuredDocument::new(),
cursor: DocumentPosition::start(),
selection: None,
paragraph_cb: None,
}
}
pub fn with_document(document: StructuredDocument) -> Self {
StructuredEditor {
document,
cursor: DocumentPosition::start(),
selection: None,
paragraph_cb: None,
}
}
pub fn document(&self) -> &StructuredDocument {
&self.document
}
pub fn document_mut(&mut self) -> &mut StructuredDocument {
&mut self.document
}
pub fn set_paragraph_change_callback(
&mut self,
cb: Option<Box<dyn FnMut(BlockType) + 'static>>,
) {
self.paragraph_cb = cb;
self.trigger_paragraph_change();
}
fn trigger_paragraph_change(&mut self) {
println!("Triggering paragraph change");
if let Some(cb) = self.paragraph_cb.as_mut() {
println!("Calling paragraph change callback");
let block_type = self
.document
.blocks()
.get(self.cursor.block_index)
.map(|b| b.block_type.clone())
.unwrap_or(BlockType::Paragraph);
cb(block_type);
}
}
fn normalize_cursor(&mut self) {
self.cursor = self.document.clamp_position(self.cursor);
}
pub fn cursor(&self) -> DocumentPosition {
self.cursor
}
pub fn set_cursor(&mut self, pos: DocumentPosition) {
self.cursor = self.document.clamp_position(pos);
self.selection = None; self.trigger_paragraph_change();
}
pub fn selection(&self) -> Option<(DocumentPosition, DocumentPosition)> {
self.selection
}
pub fn set_selection(&mut self, start: DocumentPosition, end: DocumentPosition) {
let start = self.document.clamp_position(start);
let end = self.document.clamp_position_forward(end);
self.selection = Some((start, end));
}
pub fn clear_selection(&mut self) {
self.selection = None;
}
pub fn select_all(&mut self) {
if self.document.block_count() == 0 {
self.selection = None;
return;
}
let start = DocumentPosition::new(0, 0);
let last_idx = self.document.block_count() - 1;
let end = {
let blocks = self.document.blocks();
let last_len = blocks[last_idx].text_len();
DocumentPosition::new(last_idx, last_len)
};
self.selection = Some((start, end));
self.cursor = end;
self.normalize_cursor();
}
pub fn extend_selection_to(&mut self, end: DocumentPosition) {
let end = self.document.clamp_position(end);
if let Some((start, _)) = self.selection {
self.selection = Some((start, end));
} else {
self.selection = Some((self.cursor, end));
}
self.cursor = end;
self.normalize_cursor();
}
fn collect_selection_block_indices(&self) -> Vec<usize> {
let block_count = self.document.block_count();
if block_count == 0 {
return Vec::new();
}
let Some((mut start, mut end)) = self.selection else {
return Vec::new();
};
if start.block_index > end.block_index
|| (start.block_index == end.block_index && start.offset > end.offset)
{
std::mem::swap(&mut start, &mut end);
}
if start.block_index == end.block_index && start.offset == end.offset {
return Vec::new();
}
let start = self.document.clamp_position(start);
let end = self.document.clamp_position_forward(end);
let start_idx = start.block_index.min(block_count - 1);
let end_idx = end.block_index.min(block_count - 1);
let mut indices: Vec<usize> = (start_idx..=end_idx).collect();
if let Some(&last) = indices.last()
&& start_idx != end_idx
&& last == end_idx
{
let include_end = if end.offset == 0 {
self.document
.blocks()
.get(end_idx)
.map(|block| block.is_empty())
.unwrap_or(false)
} else {
true
};
if !include_end {
indices.pop();
}
}
indices.sort_unstable();
indices.dedup();
indices
}
pub fn select_word_at(&mut self, pos: DocumentPosition) {
let pos = self.document.clamp_position(pos);
let blocks = self.document.blocks();
if pos.block_index >= blocks.len() {
return;
}
let block = &blocks[pos.block_index];
let text = block.to_plain_text();
if text.is_empty() || pos.offset >= text.len() {
return;
}
let mut start = pos.offset;
let mut end = pos.offset;
while start > 0 {
let ch = text[..start].chars().next_back().unwrap();
if ch.is_whitespace() || ch.is_ascii_punctuation() {
break;
}
start = text[..start]
.char_indices()
.next_back()
.map(|(i, _)| i)
.unwrap_or(0);
}
let chars = text[end..].char_indices();
for (_, ch) in chars {
if ch.is_whitespace() || ch.is_ascii_punctuation() {
break;
}
end = text[..end]
.chars()
.next()
.map(|c| end + c.len_utf8())
.unwrap_or(end);
}
if start == end {
end = text[end..]
.chars()
.next()
.map(|c| end + c.len_utf8())
.unwrap_or(end);
}
let start_pos = DocumentPosition::new(pos.block_index, start);
let end_pos = DocumentPosition::new(pos.block_index, end);
self.set_selection(start_pos, end_pos);
self.cursor = self.document.clamp_position_forward(end_pos);
}
pub fn select_line_at(&mut self, pos: DocumentPosition) {
let pos = self.document.clamp_position(pos);
let blocks = self.document.blocks();
if pos.block_index >= blocks.len() {
return;
}
let block = &blocks[pos.block_index];
let start_pos = DocumentPosition::new(pos.block_index, 0);
let end_pos = DocumentPosition::new(pos.block_index, block.text_len());
self.set_selection(start_pos, end_pos);
self.cursor = self.document.clamp_position_forward(end_pos);
}
pub fn insert_text(&mut self, text: &str) -> EditResult {
if self.document.is_empty() {
let block = Block::paragraph(0).with_plain_text(text);
self.document.add_block(block);
self.cursor = DocumentPosition::new(0, text.len());
return Ok(());
}
let block_index = self.cursor.block_index;
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
if self.selection.is_some() {
self.delete_selection()?;
}
let offset = self.cursor.offset;
let (content_idx, content_offset) = {
let blocks = self.document.blocks();
let block = &blocks[block_index];
Self::find_content_at_offset_static(&block.content, offset)
};
let inner_within_link: Option<(usize, usize)> = {
let blocks = self.document.blocks();
let block = &blocks[block_index];
if content_idx < block.content.len() {
if let InlineContent::Link { content, .. } = &block.content[content_idx] {
let (inner_idx, inner_off) =
Self::find_content_at_offset_static(content, content_offset);
Some((inner_idx, inner_off))
} else {
None
}
} else {
None
}
};
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
if content_idx >= block.content.len() {
block
.content
.push(InlineContent::Text(TextRun::plain(text)));
} else {
match &mut block.content[content_idx] {
InlineContent::Text(run) => {
run.insert_text(content_offset, text);
}
InlineContent::Link { content, .. } => {
let link_len: usize = content.iter().map(|c| c.text_len()).sum();
if content_offset == 0 {
if content_idx > 0 {
if let InlineContent::Text(prev_run) =
&mut block.content[content_idx - 1]
{
let prev_len = prev_run.len();
prev_run.insert_text(prev_len, text);
} else {
block
.content
.insert(content_idx, InlineContent::Text(TextRun::plain(text)));
}
} else {
block
.content
.insert(content_idx, InlineContent::Text(TextRun::plain(text)));
}
} else if content_offset >= link_len {
if content_idx + 1 < block.content.len() {
if let InlineContent::Text(next_run) =
&mut block.content[content_idx + 1]
{
next_run.insert_text(0, text);
} else {
block.content.insert(
content_idx + 1,
InlineContent::Text(TextRun::plain(text)),
);
}
} else {
block
.content
.push(InlineContent::Text(TextRun::plain(text)));
}
} else {
let (inner_idx, inner_off) =
inner_within_link.unwrap_or((content.len(), 0));
if inner_idx >= content.len() {
content.push(InlineContent::Text(TextRun::plain(text)));
} else {
match &mut content[inner_idx] {
InlineContent::Text(run) => run.insert_text(inner_off, text),
_ => content
.insert(inner_idx, InlineContent::Text(TextRun::plain(text))),
}
}
}
}
InlineContent::HardBreak => {
if content_offset == 0 {
block
.content
.insert(content_idx, InlineContent::Text(TextRun::plain(text)));
} else {
if content_idx + 1 < block.content.len() {
match &mut block.content[content_idx + 1] {
InlineContent::Text(run) => run.insert_text(0, text),
_ => block.content.insert(
content_idx + 1,
InlineContent::Text(TextRun::plain(text)),
),
}
} else {
block
.content
.push(InlineContent::Text(TextRun::plain(text)));
}
}
}
}
}
self.cursor.offset += text.len();
Ok(())
}
pub fn insert_newline(&mut self) -> EditResult {
if self.document.is_empty() {
self.document.add_block(Block::paragraph(0));
self.document.add_block(Block::paragraph(0));
self.cursor = DocumentPosition::new(1, 0);
return Ok(());
}
let block_index = self.cursor.block_index;
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
if self.selection.is_some() {
self.delete_selection()?;
}
let offset = self.cursor.offset;
let (block_type, is_empty) = {
let blocks = self.document.blocks();
let current_block = &blocks[block_index];
(current_block.block_type.clone(), current_block.is_empty())
};
if let BlockType::ListItem {
ordered,
number,
checkbox,
} = &block_type
{
if is_empty || offset == 0 {
let blocks = self.document.blocks_mut();
blocks[block_index].block_type = BlockType::Paragraph;
self.cursor.offset = 0;
return Ok(());
}
let right_content = {
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
block.split_content_at(offset)
};
let new_number = if *ordered { number.unwrap_or(1) + 1 } else { 0 };
let new_checkbox = if checkbox.is_some() {
Some(false)
} else {
None
};
let mut new_item = Block::new(
0,
BlockType::ListItem {
ordered: *ordered,
number: if *ordered { Some(new_number) } else { None },
checkbox: new_checkbox,
},
);
new_item.content = right_content;
self.document.insert_block(block_index + 1, new_item);
self.cursor = DocumentPosition::new(block_index + 1, 0);
if *ordered {
let cur_left_num = number.unwrap_or_else(|| {
let blocks = self.document.blocks();
let mut start = block_index;
while start > 0 {
if let BlockType::ListItem { ordered: true, .. } =
blocks[start - 1].block_type
{
start -= 1;
} else {
break;
}
}
let first = match blocks[start].block_type {
BlockType::ListItem {
ordered: true,
number,
..
} => number.unwrap_or(1),
_ => 1,
};
let idx_in_run = block_index - start;
first + idx_in_run as u64
});
let start_n = cur_left_num + 1;
self.renumber_ordered_from(block_index + 1, start_n);
}
} else {
let right_content = {
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
block.split_content_at(offset)
};
let mut new_para = Block::paragraph(0);
new_para.content = right_content;
self.document.insert_block(block_index + 1, new_para);
self.cursor = DocumentPosition::new(block_index + 1, 0);
}
Ok(())
}
pub fn insert_hard_break(&mut self) -> EditResult {
if self.document.is_empty() {
let mut block = Block::paragraph(0);
block.content.push(InlineContent::HardBreak);
self.document.add_block(block);
self.cursor = DocumentPosition::new(0, 1);
return Ok(());
}
let block_index = self.cursor.block_index;
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
if self.selection.is_some() {
self.delete_selection()?;
}
let offset = self.cursor.offset;
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
let right_content = block.split_content_at(offset);
block.content.push(InlineContent::HardBreak);
block.content.extend(right_content);
self.cursor.offset += 1;
Ok(())
}
pub fn delete_backward(&mut self) -> EditResult {
if self.document.is_empty() {
return Err(EditError::EmptyDocument);
}
if self.selection.is_some() {
return self.delete_selection();
}
let block_index = self.cursor.block_index;
let offset = self.cursor.offset;
if offset == 0 {
if block_index == 0 {
return Ok(()); }
let (prev_type, cur_type) = {
let blocks = self.document.blocks();
(
blocks[block_index - 1].block_type.clone(),
blocks[block_index].block_type.clone(),
)
};
let mut renumber_after: Option<(usize, u64)> = None;
{
let blocks = self.document.blocks_mut();
let current_block = blocks.remove(block_index);
let prev_block = &mut blocks[block_index - 1];
let prev_len = prev_block.text_len();
prev_block.content.extend(current_block.content);
prev_block.normalize_content();
self.cursor = DocumentPosition::new(block_index - 1, prev_len);
}
match (prev_type, cur_type) {
(
BlockType::ListItem {
ordered: true,
number: prev_num,
..
},
BlockType::ListItem { ordered: true, .. },
) => {
let start_index = block_index; let start_n = prev_num.unwrap_or(1) + 1;
renumber_after = Some((start_index, start_n));
}
(
BlockType::ListItem {
ordered: true,
number: prev_num,
..
},
BlockType::Paragraph,
) => {
let start_index = block_index; let start_n = prev_num.unwrap_or(1) + 1;
renumber_after = Some((start_index, start_n));
}
(BlockType::Paragraph, BlockType::ListItem { ordered: true, .. }) => {
let start_index = block_index; renumber_after = Some((start_index, 1));
}
_ => {}
}
if let Some((start_idx, start_n)) = renumber_after {
self.renumber_ordered_from(start_idx, start_n);
}
} else {
if let Some(prev_grapheme_start) =
self.document.previous_grapheme_offset(block_index, offset)
&& prev_grapheme_start < offset
{
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
block.delete_text_range(prev_grapheme_start, offset);
self.cursor.offset = prev_grapheme_start;
}
}
self.normalize_cursor();
Ok(())
}
pub fn delete_backward_bytes(&mut self, mut byte_count: usize) -> Result<bool, EditError> {
if byte_count == 0 {
return Ok(false);
}
if self.document.is_empty() {
return Err(EditError::EmptyDocument);
}
self.normalize_cursor();
let original_cursor = self.cursor;
let mut start = original_cursor;
{
let doc = self.document();
if doc.block_count() == 0 {
return Ok(false);
}
if start.block_index >= doc.block_count() {
start.block_index = doc.block_count() - 1;
start.offset = doc.blocks()[start.block_index].text_len();
}
while byte_count > 0 {
if start.offset == 0 {
if start.block_index == 0 {
break;
}
start.block_index -= 1;
start.offset = doc.blocks()[start.block_index].text_len();
continue;
}
if let Some(prev_offset) =
doc.previous_grapheme_offset(start.block_index, start.offset)
{
let removed = start.offset - prev_offset;
start.offset = prev_offset;
if removed == 0 {
break;
}
if removed > byte_count {
byte_count = 0;
} else {
byte_count -= removed;
}
} else {
break;
}
}
}
if start == original_cursor {
return Ok(false);
}
self.set_selection(start, original_cursor);
self.delete_selection()?;
Ok(true)
}
pub fn delete_forward(&mut self) -> EditResult {
if self.document.is_empty() {
return Err(EditError::EmptyDocument);
}
if self.selection.is_some() {
return self.delete_selection();
}
let block_index = self.cursor.block_index;
let offset = self.cursor.offset;
let block_count = self.document.block_count();
if block_index >= block_count {
return Err(EditError::InvalidBlockIndex);
}
let block_len = {
let blocks = self.document.blocks();
blocks[block_index].text_len()
};
if offset >= block_len {
if block_index >= block_count - 1 {
return Ok(()); }
let (cur_type, next_type) = {
let blocks = self.document.blocks();
let cur = blocks[block_index].block_type.clone();
let nxt = blocks[block_index + 1].block_type.clone();
(cur, nxt)
};
{
let blocks = self.document.blocks_mut();
let next_block = blocks.remove(block_index + 1);
let block = &mut blocks[block_index];
block.content.extend(next_block.content);
block.normalize_content();
}
match (cur_type, next_type) {
(
BlockType::ListItem {
ordered: true,
number: cur_num,
..
},
BlockType::ListItem { ordered: true, .. },
) => {
let start_index = block_index + 1;
let start_n = cur_num.unwrap_or(1) + 1;
self.renumber_ordered_from(start_index, start_n);
}
(BlockType::Paragraph, BlockType::ListItem { ordered: true, .. }) => {
let start_index = block_index + 1;
self.renumber_ordered_from(start_index, 1);
}
(
BlockType::ListItem {
ordered: true,
number: cur_num,
..
},
BlockType::Paragraph,
) => {
let start_index = block_index + 1;
self.renumber_ordered_from(start_index, cur_num.unwrap_or(1) + 1);
}
_ => {}
}
self.normalize_cursor();
return Ok(());
}
let next_grapheme_end = self
.document
.next_grapheme_offset(block_index, offset)
.unwrap_or(offset);
if next_grapheme_end > offset {
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
block.delete_text_range(offset, next_grapheme_end);
}
self.normalize_cursor();
Ok(())
}
pub fn delete_word_backward(&mut self) -> EditResult {
if self.document.is_empty() {
return Err(EditError::EmptyDocument);
}
if self.selection.is_some() {
return self.delete_selection();
}
let from = self.cursor;
let to = self.word_left_position(from);
if to == from {
return Ok(());
}
self.document.delete_range(to, from);
self.cursor = to;
self.normalize_cursor();
self.selection = None;
Ok(())
}
pub fn delete_word_forward(&mut self) -> EditResult {
if self.document.is_empty() {
return Err(EditError::EmptyDocument);
}
if self.selection.is_some() {
return self.delete_selection();
}
let from = self.cursor;
let to = self.word_right_position(from);
if to == from {
return Ok(());
}
self.document.delete_range(from, to);
self.normalize_cursor();
self.selection = None;
Ok(())
}
pub fn delete_selection(&mut self) -> EditResult {
let Some((start, end)) = self.selection else {
return Ok(());
};
let (start, end) = if start.block_index < end.block_index
|| (start.block_index == end.block_index && start.offset <= end.offset)
{
(start, end)
} else {
(end, start)
};
self.document.delete_range(start, end);
self.cursor = start;
self.selection = None;
self.normalize_cursor();
Ok(())
}
pub fn move_cursor_left(&mut self) {
if self.document.block_count() == 0 {
self.cursor = DocumentPosition::start();
self.selection = None;
return;
}
if self.cursor.block_index >= self.document.block_count() {
self.cursor.block_index = self.document.block_count() - 1;
let blocks = self.document.blocks();
self.cursor.offset = blocks[self.cursor.block_index].text_len();
}
if self.cursor.offset > 0 {
self.cursor = self.document.previous_grapheme_position(self.cursor);
} else if self.cursor.block_index > 0 {
self.cursor.block_index -= 1;
let blocks = self.document.blocks();
self.cursor.offset = blocks[self.cursor.block_index].text_len();
}
self.normalize_cursor();
self.selection = None;
}
pub fn move_cursor_right(&mut self) {
let block_count = self.document.block_count();
if block_count == 0 {
self.cursor = DocumentPosition::start();
self.selection = None;
return;
}
if self.cursor.block_index >= block_count {
self.cursor.block_index = block_count - 1;
let blocks = self.document.blocks();
self.cursor.offset = blocks[self.cursor.block_index].text_len();
}
let blocks = self.document.blocks();
let block_len = blocks[self.cursor.block_index].text_len();
if self.cursor.offset < block_len {
self.cursor = self.document.next_grapheme_position(self.cursor);
} else if self.cursor.block_index < block_count - 1 {
self.cursor.block_index += 1;
self.cursor.offset = 0;
}
self.normalize_cursor();
self.selection = None;
}
pub fn move_cursor_up(&mut self) {
if self.cursor.block_index > 0 {
self.cursor.block_index -= 1;
let blocks = self.document.blocks();
let new_block_len = blocks[self.cursor.block_index].text_len();
self.cursor.offset = self.cursor.offset.min(new_block_len);
self.normalize_cursor();
}
self.selection = None;
}
pub fn move_cursor_down(&mut self) {
let block_count = self.document.block_count();
if block_count == 0 {
self.cursor = DocumentPosition::start();
self.selection = None;
return;
}
if self.cursor.block_index + 1 < block_count {
self.cursor.block_index += 1;
let blocks = self.document.blocks();
let new_block_len = blocks[self.cursor.block_index].text_len();
self.cursor.offset = self.cursor.offset.min(new_block_len);
self.normalize_cursor();
}
self.selection = None;
}
pub fn move_cursor_to_line_start(&mut self) {
self.cursor.offset = 0;
self.normalize_cursor();
self.selection = None;
}
pub fn move_cursor_to_line_end(&mut self) {
let blocks = self.document.blocks();
if self.cursor.block_index < blocks.len() {
self.cursor.offset = blocks[self.cursor.block_index].text_len();
self.normalize_cursor();
}
self.selection = None;
}
pub fn move_word_right(&mut self) {
let new_pos = self.word_right_position(self.cursor);
self.cursor = new_pos;
self.selection = None;
}
pub fn move_word_left(&mut self) {
let new_pos = self.word_left_position(self.cursor);
self.cursor = new_pos;
self.selection = None;
}
pub fn move_word_right_extend(&mut self) {
let new_pos = self.word_right_position(self.cursor);
if new_pos != self.cursor {
self.extend_selection_to(new_pos);
}
}
pub fn move_word_left_extend(&mut self) {
let new_pos = self.word_left_position(self.cursor);
if new_pos != self.cursor {
self.extend_selection_to(new_pos);
}
}
fn word_right_position(&self, pos: DocumentPosition) -> DocumentPosition {
let blocks = self.document.blocks();
if pos.block_index >= blocks.len() {
return self.document.clamp_position(pos);
}
let text = blocks[pos.block_index].to_plain_text();
let mut i = pos.offset.min(text.len());
if i >= text.len() {
if pos.block_index + 1 < blocks.len() {
return DocumentPosition::new(pos.block_index + 1, 0);
}
return self.document.clamp_position(pos);
}
while i < text.len() {
let ch = text[i..].chars().next().unwrap();
if ch.is_whitespace() || ch.is_ascii_punctuation() {
i += ch.len_utf8();
} else {
break;
}
}
while i < text.len() {
let ch = text[i..].chars().next().unwrap();
if !(ch.is_whitespace() || ch.is_ascii_punctuation()) {
i += ch.len_utf8();
} else {
break;
}
}
self.document
.clamp_position_forward(DocumentPosition::new(pos.block_index, i))
}
fn word_left_position(&self, pos: DocumentPosition) -> DocumentPosition {
let blocks = self.document.blocks();
if pos.block_index >= blocks.len() {
return self.document.clamp_position(pos);
}
let text = blocks[pos.block_index].to_plain_text();
let mut i = pos.offset.min(text.len());
if i == 0 {
if pos.block_index > 0 {
return DocumentPosition::new(
pos.block_index - 1,
blocks[pos.block_index - 1].text_len(),
);
}
return self.document.clamp_position(pos);
}
while i > 0 {
let (prev_i, ch) = {
let mut it = text[..i].char_indices();
let (prev_idx, prev_ch) = it.next_back().unwrap();
(prev_idx, prev_ch)
};
if ch.is_whitespace() || ch.is_ascii_punctuation() {
i = prev_i;
} else {
break;
}
}
while i > 0 {
let (prev_i, ch) = {
let mut it = text[..i].char_indices();
let (prev_idx, prev_ch) = it.next_back().unwrap();
(prev_idx, prev_ch)
};
if !(ch.is_whitespace() || ch.is_ascii_punctuation()) {
i = prev_i;
} else {
break;
}
}
self.document
.clamp_position(DocumentPosition::new(pos.block_index, i))
}
pub fn move_cursor_left_extend(&mut self) {
self.normalize_cursor();
let new_pos = if self.document.block_count() == 0 {
self.cursor
} else if self.cursor.offset > 0 {
self.document.previous_grapheme_position(self.cursor)
} else if self.cursor.block_index > 0 {
let blocks = self.document.blocks();
DocumentPosition::new(
self.cursor.block_index - 1,
blocks[self.cursor.block_index - 1].text_len(),
)
} else {
self.cursor
};
if new_pos != self.cursor {
self.extend_selection_to(new_pos);
}
}
pub fn move_cursor_right_extend(&mut self) {
self.normalize_cursor();
let block_count = self.document.block_count();
if block_count == 0 {
return;
}
let current_index = self.cursor.block_index.min(block_count - 1);
let blocks = self.document.blocks();
let block_len = blocks[current_index].text_len();
let new_pos = if self.cursor.offset < block_len {
self.document.next_grapheme_position(self.cursor)
} else if current_index + 1 < block_count {
DocumentPosition::new(current_index + 1, 0)
} else {
self.cursor
};
if new_pos != self.cursor {
self.extend_selection_to(new_pos);
}
}
pub fn move_cursor_up_extend(&mut self) {
if self.cursor.block_index > 0 {
let blocks = self.document.blocks();
let new_block_len = blocks[self.cursor.block_index - 1].text_len();
let new_pos = DocumentPosition::new(
self.cursor.block_index - 1,
self.cursor.offset.min(new_block_len),
);
self.extend_selection_to(new_pos);
}
}
pub fn move_cursor_down_extend(&mut self) {
let blocks = self.document.blocks();
if self.cursor.block_index < blocks.len() - 1 {
let new_block_len = blocks[self.cursor.block_index + 1].text_len();
let new_pos = DocumentPosition::new(
self.cursor.block_index + 1,
self.cursor.offset.min(new_block_len),
);
self.extend_selection_to(new_pos);
}
}
pub fn move_cursor_to_line_start_extend(&mut self) {
let new_pos = DocumentPosition::new(self.cursor.block_index, 0);
if new_pos != self.cursor {
self.extend_selection_to(new_pos);
}
}
pub fn move_cursor_to_line_end_extend(&mut self) {
let blocks = self.document.blocks();
if self.cursor.block_index < blocks.len() {
let new_pos = DocumentPosition::new(
self.cursor.block_index,
blocks[self.cursor.block_index].text_len(),
);
if new_pos != self.cursor {
self.extend_selection_to(new_pos);
}
}
}
pub fn toggle_heading(&mut self) -> EditResult {
let block_index = self.cursor.block_index;
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
block.block_type = match &block.block_type {
BlockType::Paragraph => BlockType::Heading { level: 1 },
BlockType::Heading { level: 1 } => BlockType::Heading { level: 2 },
BlockType::Heading { level: 2 } => BlockType::Heading { level: 3 },
BlockType::Heading { level: 3 } => BlockType::Paragraph,
BlockType::Heading { level } => BlockType::Heading {
level: (*level % 3) + 1,
},
BlockType::ListItem { .. } => BlockType::Heading { level: 1 },
BlockType::CodeBlock { .. } => BlockType::Heading { level: 1 },
BlockType::BlockQuote => BlockType::Heading { level: 1 },
};
Ok(())
}
pub fn insert_inline_at_cursor(&mut self, inline: InlineContent) -> EditResult {
if self.document.is_empty() {
let mut block = Block::paragraph(0);
block.content.push(inline);
let text_len = block.text_len();
self.document.add_block(block);
self.cursor = DocumentPosition::new(0, text_len);
return Ok(());
}
let block_index = self.cursor.block_index;
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
if self.selection.is_some() {
self.delete_selection()?;
}
let offset = self.cursor.offset;
let (left, right) = {
let blocks = self.document.blocks();
let block = &blocks[block_index];
Self::split_content_at_static(&block.content, offset)
};
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
block.content = left;
block.content.push(inline);
block.content.extend(right);
let inserted_len = match block.content.get(block.content.len().saturating_sub(1)) {
Some(InlineContent::Text(run)) => run.len(),
Some(InlineContent::Link { content, .. }) => content.iter().map(|c| c.text_len()).sum(),
Some(InlineContent::HardBreak) => 1,
_ => 0,
};
self.cursor.offset = offset + inserted_len;
self.selection = None;
Ok(())
}
pub fn replace_selection_with_link(&mut self, destination: &str, text: &str) -> EditResult {
self.delete_selection()?;
self.insert_link_at_cursor(destination, text)
}
pub fn insert_link_at_cursor(&mut self, destination: &str, text: &str) -> EditResult {
let link_inline = InlineContent::Link {
link: Link {
destination: destination.to_string(),
title: None,
},
content: vec![InlineContent::Text(TextRun::plain(text))],
};
self.insert_inline_at_cursor(link_inline)
}
pub fn edit_link_at(
&mut self,
block_index: usize,
inline_index: usize,
destination: &str,
text: &str,
) -> EditResult {
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
if inline_index >= block.content.len() {
return Err(EditError::InvalidPosition);
}
if let InlineContent::Link { link, content } = &mut block.content[inline_index] {
link.destination = destination.to_string();
*content = vec![InlineContent::Text(TextRun::plain(text))];
Ok(())
} else {
Err(EditError::InvalidPosition)
}
}
pub fn remove_link_at(&mut self, block_index: usize, inline_index: usize) -> EditResult {
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
if inline_index >= block.content.len() {
return Err(EditError::InvalidPosition);
}
if let InlineContent::Link { content, .. } = block.content.remove(inline_index) {
for (i, item) in content.into_iter().enumerate() {
block.content.insert(inline_index + i, item);
}
Ok(())
} else {
Err(EditError::InvalidPosition)
}
}
pub fn text_in_range(&self, start: DocumentPosition, end: DocumentPosition) -> String {
let doc = self.document();
if doc.block_count() == 0 {
return String::new();
}
let mut s = String::new();
let (mut a, mut b) = (start, end);
if a.block_index > b.block_index || (a.block_index == b.block_index && a.offset > b.offset)
{
std::mem::swap(&mut a, &mut b);
}
for bi in a.block_index..=b.block_index {
let block = &doc.blocks()[bi];
let text = block.to_plain_text();
let from = if bi == a.block_index {
a.offset.min(text.len())
} else {
0
};
let to = if bi == b.block_index {
b.offset.min(text.len())
} else {
text.len()
};
if from < to {
if !s.is_empty() {
s.push_str("\n\n");
}
s.push_str(&text[from..to]);
}
}
s
}
pub fn toggle_bold(&mut self) -> EditResult {
self.toggle_style_attribute(|style| {
style.bold = !style.bold;
})
}
pub fn toggle_italic(&mut self) -> EditResult {
self.toggle_style_attribute(|style| {
style.italic = !style.italic;
})
}
fn toggle_style_attribute<F>(&mut self, mut apply_style: F) -> EditResult
where
F: FnMut(&mut TextStyle),
{
let Some((start, end)) = self.selection else {
return Ok(());
};
let (start, end) = if start.block_index < end.block_index
|| (start.block_index == end.block_index && start.offset <= end.offset)
{
(start, end)
} else {
(end, start)
};
if start.block_index == end.block_index {
let block_index = start.block_index;
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
let (content_before, selected_content, content_after) = {
let blocks = self.document.blocks();
let block = &blocks[block_index];
Self::split_content_for_style(&block.content, start.offset, end.offset)
};
let styled_content = Self::map_style_on_runs(selected_content, &mut apply_style);
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
block.content = content_before;
block.content.extend(styled_content);
block.content.extend(content_after);
return Ok(());
}
let blocks_len = self.document.block_count();
if start.block_index >= blocks_len || end.block_index >= blocks_len {
return Err(EditError::InvalidBlockIndex);
}
{
let blocks = self.document.blocks();
let block = &blocks[start.block_index];
let block_len = block.text_len();
let (before, selected, after) =
Self::split_content_for_style(&block.content, start.offset, block_len);
let styled = Self::map_style_on_runs(selected, &mut apply_style);
let blocks = self.document.blocks_mut();
let block_mut = &mut blocks[start.block_index];
block_mut.content = before.into_iter().chain(styled).chain(after).collect();
}
if end.block_index > start.block_index + 1 {
for i in (start.block_index + 1)..end.block_index {
let styled = {
let blocks = self.document.blocks();
let b = &blocks[i];
Self::map_style_on_runs(b.content.clone(), &mut apply_style)
};
let blocks = self.document.blocks_mut();
blocks[i].content = styled;
}
}
{
let (before, selected, after) = {
let blocks = self.document.blocks();
let block = &blocks[end.block_index];
Self::split_content_for_style(&block.content, 0, end.offset)
};
let styled = Self::map_style_on_runs(selected, &mut apply_style);
let blocks = self.document.blocks_mut();
let block_mut = &mut blocks[end.block_index];
block_mut.content = before.into_iter().chain(styled).chain(after).collect();
}
Ok(())
}
fn split_content_for_style(
content: &[InlineContent],
start_offset: usize,
end_offset: usize,
) -> (Vec<InlineContent>, Vec<InlineContent>, Vec<InlineContent>) {
let mut before = Vec::new();
let mut selected = Vec::new();
let mut after = Vec::new();
let mut current_offset = 0;
for item in content {
let item_len = item.text_len();
let item_start = current_offset;
let item_end = current_offset + item_len;
if item_end <= start_offset {
before.push(item.clone());
} else if item_start >= end_offset {
after.push(item.clone());
} else if item_start >= start_offset && item_end <= end_offset {
selected.push(item.clone());
} else {
match item {
InlineContent::Text(run) => {
let text = &run.text;
let sel_start_in_run = start_offset.saturating_sub(item_start);
let sel_end_in_run = end_offset.saturating_sub(item_start).min(item_len);
if sel_start_in_run > 0 {
let mut before_run = run.clone();
before_run.text = text[..sel_start_in_run].to_string();
before.push(InlineContent::Text(before_run));
}
if sel_end_in_run > sel_start_in_run {
let mut selected_run = run.clone();
selected_run.text = text[sel_start_in_run..sel_end_in_run].to_string();
selected.push(InlineContent::Text(selected_run));
}
if sel_end_in_run < item_len {
let mut after_run = run.clone();
after_run.text = text[sel_end_in_run..].to_string();
after.push(InlineContent::Text(after_run));
}
}
_ => {
if item_start < start_offset {
before.push(item.clone());
} else if item_start < end_offset {
selected.push(item.clone());
} else {
after.push(item.clone());
}
}
}
}
current_offset += item_len;
}
(before, selected, after)
}
pub fn toggle_list(&mut self) -> EditResult {
let block_count = self.document.block_count();
if block_count == 0 {
return Err(EditError::InvalidBlockIndex);
}
let selection_blocks = self.collect_selection_block_indices();
if !selection_blocks.is_empty() {
let blocks_snapshot = self.document.blocks();
let all_plain_bullets = selection_blocks.iter().all(|&idx| {
matches!(
blocks_snapshot.get(idx).map(|b| &b.block_type),
Some(BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
})
)
});
if all_plain_bullets {
return self.set_block_type(BlockType::Paragraph);
}
return self.set_block_type(BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
});
}
let block_index = self.cursor.block_index;
if block_index >= block_count {
return Err(EditError::InvalidBlockIndex);
}
let convert_ordered_to_bullets = {
let blocks = self.document.blocks();
matches!(
blocks.get(block_index).map(|b| &b.block_type),
Some(BlockType::ListItem { ordered: true, .. })
)
};
if convert_ordered_to_bullets {
let (start, end) = {
let blocks = self.document.blocks();
let mut start = block_index;
while start > 0 {
if let BlockType::ListItem { ordered: true, .. } = blocks[start - 1].block_type
{
start -= 1;
} else {
break;
}
}
let mut end = block_index;
while end + 1 < blocks.len() {
if let BlockType::ListItem { ordered: true, .. } = blocks[end + 1].block_type {
end += 1;
} else {
break;
}
}
(start, end)
};
let blocks = self.document.blocks_mut();
for block in blocks.iter_mut().take(end + 1).skip(start) {
block.block_type = BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
};
}
self.trigger_paragraph_change();
return Ok(());
}
let convert_checklist_to_bullets = {
let blocks = self.document.blocks();
matches!(
blocks.get(block_index).map(|b| &b.block_type),
Some(BlockType::ListItem {
ordered: false,
checkbox: Some(_),
..
})
)
};
if convert_checklist_to_bullets {
let (start, end) = {
let blocks = self.document.blocks();
let mut start = block_index;
while start > 0 {
if let BlockType::ListItem {
ordered: false,
checkbox: Some(_),
..
} = blocks[start - 1].block_type
{
start -= 1;
} else {
break;
}
}
let mut end = block_index;
while end + 1 < blocks.len() {
if let BlockType::ListItem {
ordered: false,
checkbox: Some(_),
..
} = blocks[end + 1].block_type
{
end += 1;
} else {
break;
}
}
(start, end)
};
let blocks = self.document.blocks_mut();
for block in blocks.iter_mut().take(end + 1).skip(start) {
block.block_type = BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
};
}
self.trigger_paragraph_change();
return Ok(());
}
let blocks = self.document.blocks_mut();
let block = &mut blocks[block_index];
block.block_type = match &block.block_type {
BlockType::ListItem { ordered: false, .. } => BlockType::Paragraph,
BlockType::ListItem { ordered: true, .. } => BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
},
BlockType::Paragraph | BlockType::Heading { .. } => BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
},
BlockType::CodeBlock { .. } => BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
},
BlockType::BlockQuote => BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
},
};
self.trigger_paragraph_change();
Ok(())
}
pub fn toggle_checklist(&mut self) -> EditResult {
let block_count = self.document.block_count();
if block_count == 0 {
return Err(EditError::InvalidBlockIndex);
}
let selection_blocks = self.collect_selection_block_indices();
if !selection_blocks.is_empty() {
let blocks_snapshot = self.document.blocks();
let all_checklists = selection_blocks.iter().all(|&idx| {
matches!(
blocks_snapshot.get(idx).map(|b| &b.block_type),
Some(BlockType::ListItem {
ordered: false,
number: None,
checkbox: Some(_),
})
)
});
if all_checklists {
return self.set_block_type(BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
});
}
return self.set_block_type(BlockType::ListItem {
ordered: false,
number: None,
checkbox: Some(false),
});
}
let block_index = self.cursor.block_index;
if block_index >= block_count {
return Err(EditError::InvalidBlockIndex);
}
let current_type = {
let blocks = self.document.blocks();
blocks[block_index].block_type.clone()
};
match current_type {
BlockType::ListItem { ordered: true, .. } => {
let (start, end) = {
let blocks = self.document.blocks();
let mut start = block_index;
while start > 0 {
if let BlockType::ListItem { ordered: true, .. } =
blocks[start - 1].block_type
{
start -= 1;
} else {
break;
}
}
let mut end = block_index;
while end + 1 < blocks.len() {
if let BlockType::ListItem { ordered: true, .. } =
blocks[end + 1].block_type
{
end += 1;
} else {
break;
}
}
(start, end)
};
let blocks = self.document.blocks_mut();
for block in blocks.iter_mut().take(end + 1).skip(start) {
block.block_type = BlockType::ListItem {
ordered: false,
number: None,
checkbox: Some(false),
};
}
self.trigger_paragraph_change();
Ok(())
}
BlockType::ListItem {
ordered: false,
checkbox: Some(_),
..
} => {
let blocks = self.document.blocks_mut();
blocks[block_index].block_type = BlockType::Paragraph;
self.trigger_paragraph_change();
Ok(())
}
BlockType::ListItem {
ordered: false,
checkbox: None,
..
} => {
let (start, end) = {
let blocks = self.document.blocks();
let mut start = block_index;
while start > 0 {
if let BlockType::ListItem {
ordered: false,
checkbox: None,
..
} = blocks[start - 1].block_type
{
start -= 1;
} else {
break;
}
}
let mut end = block_index;
while end + 1 < blocks.len() {
if let BlockType::ListItem {
ordered: false,
checkbox: None,
..
} = blocks[end + 1].block_type
{
end += 1;
} else {
break;
}
}
(start, end)
};
let blocks = self.document.blocks_mut();
for block in blocks.iter_mut().take(end + 1).skip(start) {
block.block_type = BlockType::ListItem {
ordered: false,
number: None,
checkbox: Some(false),
};
}
self.trigger_paragraph_change();
Ok(())
}
BlockType::Paragraph
| BlockType::Heading { .. }
| BlockType::CodeBlock { .. }
| BlockType::BlockQuote => {
let blocks = self.document.blocks_mut();
blocks[block_index].block_type = BlockType::ListItem {
ordered: false,
number: None,
checkbox: Some(false),
};
self.trigger_paragraph_change();
Ok(())
}
}
}
pub fn toggle_checkmark_at(&mut self, block_index: usize) -> Result<bool, EditError> {
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
let blocks = self.document.blocks_mut();
if let BlockType::ListItem {
checkbox: Some(state),
..
} = &mut blocks[block_index].block_type
{
*state = !*state;
return Ok(true);
}
Ok(false)
}
pub fn toggle_current_checkmark(&mut self) -> Result<bool, EditError> {
let block_index = self.cursor.block_index;
self.toggle_checkmark_at(block_index)
}
fn ordered_start_number_for_selection(&self, selection_blocks: &[usize]) -> u64 {
if selection_blocks.is_empty() {
return 1;
}
let blocks = self.document.blocks();
if blocks.is_empty() {
return 1;
}
let first_idx = selection_blocks[0].min(blocks.len() - 1);
if let Some(BlockType::ListItem {
ordered: true,
number,
..
}) = blocks.get(first_idx).map(|b| &b.block_type)
{
return number.unwrap_or(1);
}
if first_idx == 0 {
return 1;
}
if let Some(BlockType::ListItem { ordered: true, .. }) =
blocks.get(first_idx - 1).map(|b| &b.block_type)
{
let mut run_start = first_idx - 1;
while run_start > 0 {
if let BlockType::ListItem { ordered: true, .. } = blocks[run_start - 1].block_type
{
run_start -= 1;
} else {
break;
}
}
let first_number = match blocks[run_start].block_type {
BlockType::ListItem {
ordered: true,
number,
..
} => number.unwrap_or(1),
_ => 1,
};
let run_len = first_idx - run_start;
return first_number + run_len as u64;
}
1
}
pub fn toggle_ordered_list(&mut self) -> EditResult {
let block_count = self.document.block_count();
if block_count == 0 {
return Err(EditError::InvalidBlockIndex);
}
let selection_blocks = self.collect_selection_block_indices();
if !selection_blocks.is_empty() {
let blocks_snapshot = self.document.blocks();
let all_ordered = selection_blocks.iter().all(|&idx| {
matches!(
blocks_snapshot.get(idx).map(|b| &b.block_type),
Some(BlockType::ListItem { ordered: true, .. })
)
});
if all_ordered {
let result = self.set_block_type(BlockType::Paragraph);
if result.is_ok()
&& let Some(&last_idx) = selection_blocks.last()
&& last_idx + 1 < block_count
{
self.renumber_ordered_from(last_idx + 1, 1);
}
return result;
}
let start_number = self.ordered_start_number_for_selection(&selection_blocks);
return self.set_block_type(BlockType::ListItem {
ordered: true,
number: Some(start_number),
checkbox: None,
});
}
let block_index = self.cursor.block_index;
if block_index >= block_count {
return Err(EditError::InvalidBlockIndex);
}
let current_type = {
let blocks = self.document.blocks();
blocks[block_index].block_type.clone()
};
match current_type {
BlockType::ListItem { ordered: true, .. } => {
let (_run_start, run_end, _first_num) = {
let blocks = self.document.blocks();
let mut start = block_index;
while start > 0 {
if let BlockType::ListItem { ordered: true, .. } =
blocks[start - 1].block_type
{
start -= 1;
} else {
break;
}
}
let mut end = block_index;
while end + 1 < blocks.len() {
if let BlockType::ListItem { ordered: true, .. } =
blocks[end + 1].block_type
{
end += 1;
} else {
break;
}
}
let first_num = match blocks[start].block_type {
BlockType::ListItem {
ordered: true,
number,
..
} => number.unwrap_or(1),
_ => 1,
};
(start, end, first_num)
};
{
let blocks = self.document.blocks_mut();
blocks[block_index].block_type = BlockType::Paragraph;
}
if block_index < run_end {
let mut n: u64 = 1;
let blocks = self.document.blocks_mut();
for block in blocks.iter_mut().take(run_end + 1).skip(block_index + 1) {
if let BlockType::ListItem { ordered: true, .. } = block.block_type {
block.block_type = BlockType::ListItem {
ordered: true,
number: Some(n),
checkbox: None,
};
n += 1;
} else {
break;
}
}
}
self.trigger_paragraph_change();
Ok(())
}
BlockType::ListItem { ordered: false, .. } => {
let (start, end) = {
let blocks = self.document.blocks();
let mut start = block_index;
while start > 0 {
if let BlockType::ListItem { ordered: false, .. } =
blocks[start - 1].block_type
{
start -= 1;
} else {
break;
}
}
let mut end = block_index;
while end + 1 < blocks.len() {
if let BlockType::ListItem { ordered: false, .. } =
blocks[end + 1].block_type
{
end += 1;
} else {
break;
}
}
(start, end)
};
let blocks = self.document.blocks_mut();
for (num, block) in (1u64..).zip(blocks.iter_mut().take(end + 1).skip(start)) {
block.block_type = BlockType::ListItem {
ordered: true,
number: Some(num),
checkbox: None,
};
}
self.trigger_paragraph_change();
Ok(())
}
BlockType::Paragraph
| BlockType::Heading { .. }
| BlockType::CodeBlock { .. }
| BlockType::BlockQuote => {
let (prev_is_ord, next_is_ord) = {
let blocks = self.document.blocks();
let prev = if block_index > 0 {
matches!(
blocks[block_index - 1].block_type,
BlockType::ListItem { ordered: true, .. }
)
} else {
false
};
let next = if block_index + 1 < blocks.len() {
matches!(
blocks[block_index + 1].block_type,
BlockType::ListItem { ordered: true, .. }
)
} else {
false
};
(prev, next)
};
let current_number = if prev_is_ord {
let (prev_start, prev_end, prev_first) = {
let blocks = self.document.blocks();
let mut start = block_index - 1;
while start > 0 {
if let BlockType::ListItem { ordered: true, .. } =
blocks[start - 1].block_type
{
start -= 1;
} else {
break;
}
}
let first = match blocks[start].block_type {
BlockType::ListItem {
ordered: true,
number,
..
} => number.unwrap_or(1),
_ => 1,
};
(start, block_index - 1, first)
};
let prev_len = prev_end - prev_start + 1;
Some(prev_first + prev_len as u64)
} else {
Some(1)
};
{
let blocks = self.document.blocks_mut();
blocks[block_index].block_type = BlockType::ListItem {
ordered: true,
number: current_number,
checkbox: None,
};
}
if next_is_ord {
let (next_start, next_end) = {
let blocks = self.document.blocks();
let start = block_index + 1; let mut end = start;
while end + 1 < blocks.len() {
if let BlockType::ListItem { ordered: true, .. } =
blocks[end + 1].block_type
{
end += 1;
} else {
break;
}
}
(start, end)
};
let start_num = current_number.unwrap_or(1) + 1;
let blocks = self.document.blocks_mut();
for (n, block) in
(start_num..).zip(blocks.iter_mut().take(next_end + 1).skip(next_start))
{
block.block_type = BlockType::ListItem {
ordered: true,
number: Some(n),
checkbox: None,
};
}
}
self.trigger_paragraph_change();
Ok(())
}
}
}
pub fn toggle_quote(&mut self) -> EditResult {
let block_index = self.cursor.block_index;
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
let blocks = self.document.blocks_mut();
blocks[block_index].block_type = match blocks[block_index].block_type {
BlockType::BlockQuote => BlockType::Paragraph,
_ => BlockType::BlockQuote,
};
self.trigger_paragraph_change();
Ok(())
}
pub fn toggle_code_block(&mut self) -> EditResult {
let block_index = self.cursor.block_index;
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
let blocks = self.document.blocks_mut();
blocks[block_index].block_type = match &blocks[block_index].block_type {
BlockType::CodeBlock { .. } => BlockType::Paragraph,
_ => BlockType::CodeBlock { language: None },
};
self.trigger_paragraph_change();
Ok(())
}
pub fn set_block_type(&mut self, block_type: BlockType) -> EditResult {
let block_count = self.document.block_count();
if block_count == 0 {
return Err(EditError::InvalidBlockIndex);
}
let mut target_blocks = self.collect_selection_block_indices();
if target_blocks.is_empty() {
let cursor_index = self.cursor.block_index;
if cursor_index >= block_count {
return Err(EditError::InvalidBlockIndex);
}
target_blocks.push(cursor_index);
}
target_blocks.sort_unstable();
target_blocks.dedup();
if target_blocks.iter().any(|&idx| idx >= block_count) {
return Err(EditError::InvalidBlockIndex);
}
let ordered_conversion = matches!(block_type, BlockType::ListItem { ordered: true, .. });
let ordered_start_number = if let BlockType::ListItem {
ordered: true,
number,
..
} = &block_type
{
number.unwrap_or(1)
} else {
0
};
let new_types: Vec<BlockType> = (0..target_blocks.len())
.map(|i| match &block_type {
BlockType::Paragraph => BlockType::Paragraph,
BlockType::Heading { level } => BlockType::Heading { level: *level },
BlockType::BlockQuote => BlockType::BlockQuote,
BlockType::CodeBlock { language } => BlockType::CodeBlock {
language: language.clone(),
},
BlockType::ListItem {
ordered, checkbox, ..
} => {
if *ordered {
BlockType::ListItem {
ordered: true,
number: Some(ordered_start_number + i as u64),
checkbox: *checkbox,
}
} else {
BlockType::ListItem {
ordered: false,
number: None,
checkbox: *checkbox,
}
}
}
})
.collect();
{
let blocks = self.document.blocks_mut();
for (idx, new_type) in target_blocks.iter().copied().zip(new_types.into_iter()) {
blocks[idx].block_type = new_type;
}
}
if ordered_conversion
&& let Some(last_idx) = target_blocks.last().copied()
&& last_idx + 1 < block_count
{
let next_number = ordered_start_number + target_blocks.len() as u64;
self.renumber_ordered_from(last_idx + 1, next_number);
}
self.trigger_paragraph_change();
Ok(())
}
pub fn toggle_code(&mut self) -> EditResult {
self.toggle_style_attribute(|style| {
style.code = !style.code;
})
}
pub fn toggle_strikethrough(&mut self) -> EditResult {
self.toggle_style_attribute(|style| {
style.strikethrough = !style.strikethrough;
})
}
pub fn toggle_underline(&mut self) -> EditResult {
self.toggle_style_attribute(|style| {
style.underline = !style.underline;
})
}
pub fn toggle_highlight(&mut self) -> EditResult {
self.toggle_style_attribute(|style| {
style.highlight = !style.highlight;
})
}
pub fn clear_formatting(&mut self) -> EditResult {
let Some((start, end)) = self.selection else {
return Ok(());
};
let (start, end) = if start.block_index < end.block_index
|| (start.block_index == end.block_index && start.offset <= end.offset)
{
(start, end)
} else {
(end, start)
};
if start.block_index == end.block_index {
let block_index = start.block_index;
if block_index >= self.document.block_count() {
return Err(EditError::InvalidBlockIndex);
}
let (before, selected, after) = {
let blocks = self.document.blocks();
let block = &blocks[block_index];
Self::split_content_for_style(&block.content, start.offset, end.offset)
};
let mut clear = |style: &mut TextStyle| {
*style = TextStyle::default();
};
let cleared = Self::map_style_on_runs(selected, &mut clear);
let blocks = self.document.blocks_mut();
let block_mut = &mut blocks[block_index];
block_mut.content = before.into_iter().chain(cleared).chain(after).collect();
return Ok(());
}
let blocks_len = self.document.block_count();
if start.block_index >= blocks_len || end.block_index >= blocks_len {
return Err(EditError::InvalidBlockIndex);
}
let mut clear = |style: &mut TextStyle| {
*style = TextStyle::default();
};
{
let (before, selected, after) = {
let blocks = self.document.blocks();
let block = &blocks[start.block_index];
let len = block.text_len();
Self::split_content_for_style(&block.content, start.offset, len)
};
let cleared = Self::map_style_on_runs(selected, &mut clear);
let blocks = self.document.blocks_mut();
let block_mut = &mut blocks[start.block_index];
block_mut.content = before.into_iter().chain(cleared).chain(after).collect();
}
if end.block_index > start.block_index + 1 {
for i in (start.block_index + 1)..end.block_index {
let cleared_vec = {
let blocks = self.document.blocks();
let b = &blocks[i];
Self::map_style_on_runs(b.content.clone(), &mut clear)
};
let blocks = self.document.blocks_mut();
blocks[i].content = cleared_vec;
}
}
{
let (before, selected, after) = {
let blocks = self.document.blocks();
let block = &blocks[end.block_index];
Self::split_content_for_style(&block.content, 0, end.offset)
};
let cleared = Self::map_style_on_runs(selected, &mut clear);
let blocks = self.document.blocks_mut();
let block_mut = &mut blocks[end.block_index];
block_mut.content = before.into_iter().chain(cleared).chain(after).collect();
}
Ok(())
}
pub fn get_selection_text(&self) -> String {
let Some((start, end)) = self.selection else {
return String::new();
};
let (start, end) = if start.block_index < end.block_index
|| (start.block_index == end.block_index && start.offset <= end.offset)
{
(start, end)
} else {
(end, start)
};
if start.block_index == end.block_index {
let blocks = self.document.blocks();
if start.block_index >= blocks.len() {
return String::new();
}
let block = &blocks[start.block_index];
let text = block.to_plain_text();
if start.offset < text.len() {
let end_offset = end.offset.min(text.len());
return text[start.offset..end_offset].to_string();
}
} else {
let blocks = self.document.blocks();
let mut result = String::new();
for (block_idx, block) in blocks
.iter()
.enumerate()
.take(end.block_index.min(blocks.len() - 1) + 1)
.skip(start.block_index)
{
let text = block.to_plain_text();
if block_idx == start.block_index {
result.push_str(&text[start.offset..]);
} else if block_idx == end.block_index {
let end_offset = end.offset.min(text.len());
result.push_str(&text[..end_offset]);
} else {
result.push_str(&text);
}
if block_idx < end.block_index {
result.push('\n');
}
}
return result;
}
String::new()
}
pub fn cut(&mut self) -> Result<String, EditError> {
let text = self.get_selection_text();
if !text.is_empty() {
self.delete_selection()?;
}
Ok(text)
}
pub fn copy(&self) -> String {
self.get_selection_text()
}
pub fn paste(&mut self, text: &str) -> EditResult {
if let Some((start, end)) = self.selection {
self.document.replace_range(start, end, text);
let paragraphs: Vec<&str> = text.split("\n\n").collect();
let insert_block = start
.block_index
.min(self.document.block_count().saturating_sub(1));
let (block_index, offset) = if paragraphs.len() <= 1 {
let inserted_len = paragraphs.first().map(|s| s.len()).unwrap_or(0);
let left_len = start
.offset
.min(self.document.blocks()[insert_block].text_len());
(insert_block, left_len + inserted_len)
} else {
let last_block = (insert_block + paragraphs.len() - 1)
.min(self.document.block_count().saturating_sub(1));
let last_len = paragraphs.last().map(|s| s.len()).unwrap_or(0);
(last_block, last_len)
};
let block_len = self.document.blocks()[block_index].text_len();
self.cursor = DocumentPosition::new(block_index, min(offset, block_len));
self.selection = None;
Ok(())
} else {
self.insert_text(text)
}
}
fn find_content_at_offset_static(content: &[InlineContent], offset: usize) -> (usize, usize) {
let mut current_offset = 0;
for (idx, item) in content.iter().enumerate() {
let item_len = item.text_len();
if current_offset + item_len >= offset {
return (idx, offset - current_offset);
}
current_offset += item_len;
}
(content.len(), 0)
}
fn split_content_at_static(
content: &[InlineContent],
offset: usize,
) -> (Vec<InlineContent>, Vec<InlineContent>) {
let (idx, content_offset) = Self::find_content_at_offset_static(content, offset);
let mut left = content[..idx].to_vec();
let mut right = content[idx..].to_vec();
if idx < content.len()
&& let Some(InlineContent::Text(run)) = content.get(idx)
&& content_offset > 0
{
if content_offset == run.len() {
left.push(InlineContent::Text(run.clone()));
right.remove(0);
} else if content_offset < run.len() {
let (left_run, right_run) = run.split_at(content_offset);
left.push(InlineContent::Text(left_run));
right.remove(0);
right.insert(0, InlineContent::Text(right_run));
}
}
(left, right)
}
fn map_style_on_runs<F>(items: Vec<InlineContent>, apply: &mut F) -> Vec<InlineContent>
where
F: FnMut(&mut TextStyle),
{
items
.into_iter()
.map(|item| match item {
InlineContent::Text(mut run) => {
apply(&mut run.style);
InlineContent::Text(run)
}
InlineContent::Link { link, content } => {
let mapped = Self::map_style_on_runs(content, apply);
InlineContent::Link {
link,
content: mapped,
}
}
other => other,
})
.collect()
}
}
impl Default for StructuredEditor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insert_text() {
let mut editor = StructuredEditor::new();
editor.insert_text("Hello").unwrap();
assert_eq!(editor.document().to_plain_text(), "Hello");
assert_eq!(editor.cursor().offset, 5);
}
#[test]
fn test_insert_text_multiple() {
let mut editor = StructuredEditor::new();
editor.insert_text("Hello").unwrap();
editor.insert_text(" world").unwrap();
assert_eq!(editor.document().to_plain_text(), "Hello world");
}
#[test]
fn test_delete_backward() {
let mut editor = StructuredEditor::new();
editor.insert_text("Hello").unwrap();
editor.delete_backward().unwrap();
assert_eq!(editor.document().to_plain_text(), "Hell");
assert_eq!(editor.cursor().offset, 4);
}
#[test]
fn cursor_respects_grapheme_clusters() {
let mut editor = StructuredEditor::new();
let flag = "\u{1F1FA}\u{1F1F8}";
editor.insert_text(flag).unwrap();
assert_eq!(editor.cursor().offset, flag.len());
editor.set_cursor(DocumentPosition::new(0, flag.len() / 2));
assert_eq!(
editor.cursor().offset,
0,
"cursor should snap to grapheme start"
);
editor.move_cursor_right();
assert_eq!(editor.cursor().offset, flag.len());
editor.move_cursor_left();
assert_eq!(editor.cursor().offset, 0);
}
#[test]
fn deletion_respects_grapheme_clusters() {
let mut editor = StructuredEditor::new();
let flag = "\u{1F1FA}\u{1F1F8}";
editor.insert_text(flag).unwrap();
editor.set_cursor(DocumentPosition::new(0, flag.len()));
editor.delete_backward().unwrap();
assert_eq!(editor.document().to_plain_text(), "");
assert_eq!(editor.cursor().offset, 0);
editor.insert_text(flag).unwrap();
editor.set_cursor(DocumentPosition::new(0, 0));
editor.delete_forward().unwrap();
assert_eq!(editor.document().to_plain_text(), "");
assert_eq!(editor.cursor().offset, 0);
}
#[test]
fn selection_respects_grapheme_clusters() {
let mut editor = StructuredEditor::new();
let flag = "\u{1F1FA}\u{1F1F8}";
editor.insert_text(flag).unwrap();
editor.move_cursor_to_line_start();
editor.move_cursor_right_extend();
let (start, end) = editor
.selection()
.expect("selection should be created when extending");
assert_eq!(start.offset, 0);
assert_eq!(end.offset, flag.len());
assert_eq!(editor.cursor().offset, flag.len());
}
#[test]
fn test_insert_newline() {
let mut editor = StructuredEditor::new();
editor.insert_text("Hello").unwrap();
editor.insert_newline().unwrap();
editor.insert_text("World").unwrap();
assert_eq!(editor.document().block_count(), 2);
assert_eq!(editor.cursor().block_index, 1);
}
#[test]
fn test_insert_hard_break_in_text() {
let mut editor = StructuredEditor::new();
editor.insert_text("HelloWorld").unwrap();
editor.set_cursor(DocumentPosition::new(0, 5));
editor.insert_hard_break().unwrap();
let block = &editor.document().blocks()[0];
assert_eq!(block.to_plain_text(), "Hello\nWorld");
assert!(matches!(block.content[1], InlineContent::HardBreak));
assert_eq!(editor.cursor(), DocumentPosition::new(0, 6));
}
#[test]
fn test_insert_text_after_trailing_hard_break_appends() {
let mut editor = StructuredEditor::new();
editor.insert_text("Hello").unwrap();
editor.insert_hard_break().unwrap();
editor.insert_text("World").unwrap();
let block = &editor.document().blocks()[0];
assert_eq!(block.content.len(), 3, "Expected text, break, text runs");
assert!(
matches!(block.content[1], InlineContent::HardBreak),
"Second inline should remain the hard break"
);
assert!(
matches!(block.content[2], InlineContent::Text(_)),
"Inserted text should appear after the hard break"
);
assert_eq!(block.to_plain_text(), "Hello\nWorld");
assert_eq!(
editor.cursor(),
DocumentPosition::new(0, block.text_len()),
"Cursor should advance past inserted text"
);
}
#[test]
fn test_cursor_movement() {
let mut editor = StructuredEditor::new();
editor.insert_text("Hello").unwrap();
editor.move_cursor_left();
assert_eq!(editor.cursor().offset, 4);
editor.move_cursor_to_line_start();
assert_eq!(editor.cursor().offset, 0);
editor.move_cursor_to_line_end();
assert_eq!(editor.cursor().offset, 5);
}
#[test]
fn test_insert_text_inside_link() {
let mut editor = StructuredEditor::new();
editor.insert_text("ab").unwrap();
editor.insert_link_at_cursor("dest", "XY").unwrap();
editor.insert_text("cd").unwrap();
editor.set_cursor(DocumentPosition::new(0, 3));
editor.insert_text("!").unwrap();
assert_eq!(editor.document().to_plain_text(), "abX!Ycd");
let block = &editor.document().blocks()[0];
assert!(matches!(block.content[0], InlineContent::Text(_)));
if let InlineContent::Link { content, .. } = &block.content[1] {
let inner_text: String = content.iter().map(|c| c.to_plain_text()).collect();
assert_eq!(inner_text, "X!Y");
} else {
panic!("Expected a link at index 1");
}
}
#[test]
fn test_insert_text_at_start_of_link_inserts_before() {
let mut editor = StructuredEditor::new();
editor.insert_text("ab").unwrap();
editor.insert_link_at_cursor("dest", "XY").unwrap();
editor.insert_text("cd").unwrap();
editor.set_cursor(DocumentPosition::new(0, 2));
editor.insert_text("!").unwrap();
assert_eq!(editor.document().to_plain_text(), "ab!XYcd");
let block = &editor.document().blocks()[0];
assert!(matches!(block.content[0], InlineContent::Text(_)));
if let InlineContent::Link { content, .. } = &block.content[1] {
let inner_text: String = content.iter().map(|c| c.to_plain_text()).collect();
assert_eq!(inner_text, "XY");
} else {
panic!("Expected a link at index 1");
}
}
#[test]
fn test_insert_text_at_end_of_link_inserts_after() {
let mut editor = StructuredEditor::new();
editor.insert_text("ab").unwrap();
editor.insert_link_at_cursor("dest", "XY").unwrap();
editor.insert_text("cd").unwrap();
editor.set_cursor(DocumentPosition::new(0, 4));
editor.insert_text("!").unwrap();
assert_eq!(editor.document().to_plain_text(), "abXY!cd");
let block = &editor.document().blocks()[0];
if let InlineContent::Link { content, .. } = &block.content[1] {
let inner_text: String = content.iter().map(|c| c.to_plain_text()).collect();
assert_eq!(inner_text, "XY");
} else {
panic!("Expected a link at index 1");
}
assert!(matches!(block.content[2], InlineContent::Text(_)));
}
#[test]
fn test_toggle_list_clears_checklist_run() {
let mut editor = StructuredEditor::new();
{
let mut doc = StructuredDocument::new();
for i in 0..3 {
let mut block = Block::new(
0,
BlockType::ListItem {
ordered: false,
number: None,
checkbox: Some(i % 2 == 0),
},
);
block
.content
.push(InlineContent::Text(TextRun::plain(format!("Item {i}"))));
doc.add_block(block);
}
*editor.document_mut() = doc;
}
editor.set_cursor(DocumentPosition::new(1, 0));
editor.toggle_list().unwrap();
let blocks = editor.document().blocks();
assert!(blocks.iter().all(|block| {
matches!(
block.block_type,
BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
}
)
}));
}
#[test]
fn test_set_block_type_multi_block_bullet() {
let mut editor = StructuredEditor::new();
let mut doc = StructuredDocument::new();
for text in ["One", "Two", "Three"] {
let block = Block::paragraph(0).with_plain_text(text);
doc.add_block(block);
}
let last_len = doc.blocks().last().unwrap().text_len();
*editor.document_mut() = doc;
editor.set_selection(
DocumentPosition::new(0, 0),
DocumentPosition::new(2, last_len),
);
editor
.set_block_type(BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
})
.unwrap();
let blocks = editor.document().blocks();
assert!(blocks.iter().all(|block| {
matches!(
block.block_type,
BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
}
)
}));
}
#[test]
fn test_set_block_type_multi_block_ordered() {
let mut editor = StructuredEditor::new();
let mut doc = StructuredDocument::new();
for text in ["One", "Two", "Three"] {
let block = Block::paragraph(0).with_plain_text(text);
doc.add_block(block);
}
let last_len = doc.blocks().last().unwrap().text_len();
*editor.document_mut() = doc;
editor.set_selection(
DocumentPosition::new(0, 0),
DocumentPosition::new(2, last_len),
);
editor
.set_block_type(BlockType::ListItem {
ordered: true,
number: Some(1),
checkbox: None,
})
.unwrap();
let blocks = editor.document().blocks();
for (i, block) in blocks.iter().enumerate() {
match block.block_type {
BlockType::ListItem {
ordered: true,
number,
checkbox: None,
} => assert_eq!(number, Some(1 + i as u64)),
ref other => panic!("unexpected block type: {:?}", other),
}
}
}
#[test]
fn test_toggle_list_multi_block_selection() {
let mut editor = StructuredEditor::new();
let mut doc = StructuredDocument::new();
for text in ["One", "Two", "Three"] {
let block = Block::paragraph(0).with_plain_text(text);
doc.add_block(block);
}
let last_len = doc.blocks().last().unwrap().text_len();
*editor.document_mut() = doc;
editor.set_selection(
DocumentPosition::new(0, 0),
DocumentPosition::new(2, last_len),
);
editor.toggle_list().unwrap();
{
let blocks = editor.document().blocks();
assert!(blocks.iter().all(|block| {
matches!(
block.block_type,
BlockType::ListItem {
ordered: false,
number: None,
checkbox: None,
}
)
}));
}
editor.set_selection(
DocumentPosition::new(0, 0),
DocumentPosition::new(2, last_len),
);
editor.toggle_list().unwrap();
let blocks = editor.document().blocks();
assert!(
blocks
.iter()
.all(|block| matches!(block.block_type, BlockType::Paragraph))
);
}
#[test]
fn test_toggle_ordered_list_multi_block_selection() {
let mut editor = StructuredEditor::new();
let mut doc = StructuredDocument::new();
for text in ["One", "Two", "Three"] {
let block = Block::paragraph(0).with_plain_text(text);
doc.add_block(block);
}
let last_len = doc.blocks().last().unwrap().text_len();
*editor.document_mut() = doc;
editor.set_selection(
DocumentPosition::new(0, 0),
DocumentPosition::new(2, last_len),
);
editor.toggle_ordered_list().unwrap();
{
let blocks = editor.document().blocks();
for (i, block) in blocks.iter().enumerate() {
match block.block_type {
BlockType::ListItem {
ordered: true,
number,
checkbox: None,
} => assert_eq!(number, Some(1 + i as u64)),
ref other => panic!("expected ordered list item, found {:?}", other),
}
}
}
editor.set_selection(
DocumentPosition::new(0, 0),
DocumentPosition::new(2, last_len),
);
editor.toggle_ordered_list().unwrap();
let blocks = editor.document().blocks();
assert!(
blocks
.iter()
.all(|block| matches!(block.block_type, BlockType::Paragraph))
);
}
#[test]
fn test_toggle_ordered_list_continues_numbering_after_run() {
let mut editor = StructuredEditor::new();
let mut doc = StructuredDocument::new();
doc.add_block(
Block::new(
0,
BlockType::ListItem {
ordered: true,
number: Some(3),
checkbox: None,
},
)
.with_plain_text("Item 3"),
);
doc.add_block(
Block::new(
0,
BlockType::ListItem {
ordered: true,
number: Some(4),
checkbox: None,
},
)
.with_plain_text("Item 4"),
);
doc.add_block(Block::paragraph(0).with_plain_text("Next"));
doc.add_block(Block::paragraph(0).with_plain_text("Another"));
let last_len = doc.blocks().last().unwrap().text_len();
*editor.document_mut() = doc;
editor.set_selection(
DocumentPosition::new(2, 0),
DocumentPosition::new(3, last_len),
);
editor.toggle_ordered_list().unwrap();
let blocks = editor.document().blocks();
assert_eq!(
blocks[2].block_type,
BlockType::ListItem {
ordered: true,
number: Some(5),
checkbox: None
}
);
assert_eq!(
blocks[3].block_type,
BlockType::ListItem {
ordered: true,
number: Some(6),
checkbox: None
}
);
}
#[test]
fn test_backspace_inside_link() {
let mut editor = StructuredEditor::new();
editor.insert_text("ab").unwrap();
editor.insert_link_at_cursor("dest", "XYZ").unwrap();
editor.insert_text("cd").unwrap();
editor.set_cursor(DocumentPosition::new(0, 4));
editor.delete_backward().unwrap();
assert_eq!(editor.document().to_plain_text(), "abXZcd");
let block = &editor.document().blocks()[0];
if let InlineContent::Link { content, .. } = &block.content[1] {
let inner_text: String = content.iter().map(|c| c.to_plain_text()).collect();
assert_eq!(inner_text, "XZ");
} else {
panic!("Expected a link at index 1");
}
}
#[test]
fn test_delete_forward_inside_link() {
let mut editor = StructuredEditor::new();
editor.insert_text("ab").unwrap();
editor.insert_link_at_cursor("dest", "XYZ").unwrap();
editor.insert_text("cd").unwrap();
editor.set_cursor(DocumentPosition::new(0, 3));
editor.delete_forward().unwrap();
assert_eq!(editor.document().to_plain_text(), "abXZcd");
let block = &editor.document().blocks()[0];
assert_eq!(block.content.len(), 3);
if let InlineContent::Link { content, .. } = &block.content[1] {
let inner_text: String = content.iter().map(|c| c.to_plain_text()).collect();
assert_eq!(inner_text, "XZ");
} else {
panic!("Expected a link at index 1");
}
}
#[test]
fn test_enter_inside_link_splits_and_preserves_links() {
let mut editor = StructuredEditor::new();
editor.insert_text("A ").unwrap();
editor.insert_link_at_cursor("dest", "bc").unwrap();
editor.insert_text(" D").unwrap();
editor.set_cursor(DocumentPosition::new(0, 3));
editor.insert_newline().unwrap();
assert_eq!(editor.document().block_count(), 2);
assert_eq!(editor.document().blocks()[0].to_plain_text(), "A b");
assert_eq!(editor.document().blocks()[1].to_plain_text(), "c D");
if let InlineContent::Link { link, content } = &editor.document().blocks()[0].content[1] {
assert_eq!(link.destination, "dest");
let inner_text: String = content.iter().map(|c| c.to_plain_text()).collect();
assert_eq!(inner_text, "b");
} else {
panic!("Expected a link in first paragraph after split");
}
if let InlineContent::Link { link, content } = &editor.document().blocks()[1].content[0] {
assert_eq!(link.destination, "dest");
let inner_text: String = content.iter().map(|c| c.to_plain_text()).collect();
assert_eq!(inner_text, "c");
} else {
panic!("Expected a link at start of second paragraph after split");
}
}
#[test]
fn test_delete_selection_across_blocks() {
let mut editor = StructuredEditor::new();
editor.insert_text("First para").unwrap();
editor.insert_newline().unwrap();
editor.insert_text("Second").unwrap();
editor.insert_newline().unwrap();
editor.insert_text("Third para").unwrap();
let start = DocumentPosition::new(0, 3);
let end = DocumentPosition::new(2, 2);
editor.set_selection(start, end);
editor.delete_selection().unwrap();
assert_eq!(editor.document().block_count(), 1);
assert_eq!(editor.document().blocks()[0].to_plain_text(), "Firird para");
assert_eq!(editor.cursor(), DocumentPosition::new(0, 3));
}
#[test]
fn test_toggle_bold_across_blocks() {
let mut editor = StructuredEditor::new();
editor.insert_text("First para").unwrap();
editor.insert_newline().unwrap();
editor.insert_text("Second").unwrap();
editor.insert_newline().unwrap();
editor.insert_text("Third para").unwrap();
let start = DocumentPosition::new(0, 3); let end = DocumentPosition::new(2, 2); editor.set_selection(start, end);
editor.toggle_bold().unwrap();
let doc = editor.document();
let b0 = &doc.blocks()[0];
let parts0: Vec<(String, bool)> = b0
.content
.iter()
.filter_map(|c| {
if let InlineContent::Text(run) = c {
Some((run.text.clone(), run.style.bold))
} else {
None
}
})
.collect();
assert!(parts0.len() >= 2);
assert_eq!(parts0[0].0, "Fir");
assert!(!parts0[0].1);
assert!(parts0[1].1);
let b1 = &doc.blocks()[1];
let parts1: Vec<bool> = b1
.content
.iter()
.filter_map(|c| {
if let InlineContent::Text(run) = c {
Some(run.style.bold)
} else {
None
}
})
.collect();
assert!(!parts1.is_empty());
assert!(parts1.into_iter().all(|b| b));
let b2 = &doc.blocks()[2];
let parts2: Vec<(String, bool)> = b2
.content
.iter()
.filter_map(|c| {
if let InlineContent::Text(run) = c {
Some((run.text.clone(), run.style.bold))
} else {
None
}
})
.collect();
assert!(parts2.len() >= 2);
assert_eq!(parts2[0].0, "Th");
assert!(parts2[0].1);
assert!(!parts2.last().unwrap().1);
}
#[test]
fn test_select_all() {
let mut editor = StructuredEditor::new();
editor.insert_text("Hello").unwrap();
editor.insert_newline().unwrap();
editor.insert_text("World").unwrap();
editor.select_all();
let sel = editor.selection().unwrap();
assert_eq!(sel.0, DocumentPosition::new(0, 0));
assert_eq!(sel.1, DocumentPosition::new(1, 5));
assert_eq!(editor.cursor(), DocumentPosition::new(1, 5));
}
#[test]
fn test_word_navigation() {
let mut editor = StructuredEditor::new();
editor.insert_text("Hello world").unwrap();
assert_eq!(editor.cursor(), DocumentPosition::new(0, 12));
editor.move_word_left();
assert_eq!(editor.cursor(), DocumentPosition::new(0, 7));
editor.move_word_right_extend();
let sel = editor.selection().unwrap();
assert_eq!(sel.0, DocumentPosition::new(0, 7));
assert_eq!(sel.1, DocumentPosition::new(0, 12));
}
}