use ropey::Rope;
use std::ops::{Deref, DerefMut, Range};
use std::rc::Rc;
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
use tree_sitter::{InputEdit, Point};
use undo::Record;
static NEXT_VERSION: AtomicU64 = AtomicU64::new(1);
use crate::highlight::{HighlightSpan, Highlighter};
use crate::inline::{StyledRegion, extract_all_inline_styles, styles_in_range};
use crate::marker::{
LineMarkers, ParsedNodes, collect_node_infos, is_line_in_checked_task, is_line_in_code_block,
markers_at_from_infos,
};
use crate::parser::{MarkdownParser, MarkdownTree};
fn compute_line_byte_range(rope: &Rope, line_idx: usize) -> Range<usize> {
let start_char = rope.line_to_char(line_idx);
let end_char = if line_idx + 1 < rope.len_lines() {
rope.line_to_char(line_idx + 1)
} else {
rope.len_chars()
};
let start_byte = rope.char_to_byte(start_char);
let end_byte = rope.char_to_byte(end_char);
let line_slice = rope.line(line_idx);
let len = line_slice.len_bytes();
let has_newline = line_slice.get_byte(len.saturating_sub(1)) == Some(b'\n');
let adjusted_end = if has_newline {
end_byte.saturating_sub(1)
} else {
end_byte
};
start_byte..adjusted_end
}
#[derive(Clone)]
pub struct RenderSnapshot {
pub rope: Rope,
pub inline_styles: Rc<Vec<StyledRegion>>,
pub code_highlights: Rc<Vec<(Range<usize>, Vec<HighlightSpan>)>>,
parsed: Rc<ParsedNodes>,
line_count: usize,
}
impl RenderSnapshot {
pub fn line_count(&self) -> usize {
self.line_count
}
fn line_byte_range(&self, line_idx: usize) -> Range<usize> {
compute_line_byte_range(&self.rope, line_idx)
}
pub fn line_markers(&self, line_idx: usize) -> LineMarkers {
let range = self.line_byte_range(line_idx);
let markers = markers_at_from_infos(&self.parsed.nodes, &self.rope, range.start, range.end);
let in_checked_task = is_line_in_checked_task(&self.parsed.nodes, range.start);
let in_code_block = is_line_in_code_block(&self.parsed.nodes, range.start);
LineMarkers {
range,
line_number: line_idx,
markers,
in_checked_task,
in_code_block,
}
}
pub fn inline_styles_for_line(&self, line_idx: usize) -> Vec<StyledRegion> {
let range = self.line_byte_range(line_idx);
let mut styles: Vec<StyledRegion> = styles_in_range(&self.inline_styles, &range)
.into_iter()
.cloned()
.collect();
let markers = self.line_markers(line_idx);
for marker in &markers.markers {
if let crate::marker::MarkerKind::Checkbox { checked } = marker.kind {
let checkbox_range = marker.range.start..marker.range.start + 3;
styles.push(StyledRegion {
full_range: checkbox_range.clone(),
content_range: checkbox_range,
style: crate::inline::TextStyle::default(),
link_url: None,
is_image: false,
checkbox: Some(checked),
display_text: None,
});
}
}
styles.sort_by_key(|s| s.full_range.start);
styles
}
pub fn code_highlights_for_line(&self, line_idx: usize) -> Vec<HighlightSpan> {
let range = self.line_byte_range(line_idx);
let mut result = Vec::new();
for (block_range, highlights) in self.code_highlights.iter() {
if range.start < block_range.end && range.end > block_range.start {
for span in highlights {
if span.range.start < range.end && span.range.end > range.start {
result.push(span.clone());
}
}
}
}
result
}
}
#[derive(Clone, Debug)]
struct CodeHighlightCache {
highlights: Rc<Vec<(Range<usize>, Vec<HighlightSpan>)>>,
valid: bool,
}
impl Default for CodeHighlightCache {
fn default() -> Self {
Self {
highlights: Rc::new(Vec::new()),
valid: false,
}
}
}
#[derive(Clone, Debug)]
pub struct TextEdit {
offset: usize,
deleted: String,
inserted: String,
cursor_before: usize,
cursor_after: usize,
}
impl TextEdit {
pub fn new(
offset: usize,
deleted: String,
inserted: String,
cursor_before: usize,
cursor_after: usize,
) -> Self {
Self {
offset,
deleted,
inserted,
cursor_before,
cursor_after,
}
}
}
impl undo::Edit for TextEdit {
type Target = BufferContent;
type Output = usize;
fn edit(&mut self, target: &mut BufferContent) -> Self::Output {
target.apply_edit(self.offset, &self.deleted, &self.inserted);
self.cursor_after
}
fn undo(&mut self, target: &mut BufferContent) -> Self::Output {
target.apply_edit(self.offset, &self.inserted, &self.deleted);
self.cursor_before
}
}
pub struct BufferContent {
text: Rope,
parser: MarkdownParser,
tree: Option<MarkdownTree>,
highlighter: Highlighter,
code_highlight_cache: CodeHighlightCache,
parsed: Rc<ParsedNodes>,
inline_styles: Rc<Vec<StyledRegion>>,
version: u64,
}
impl BufferContent {
pub fn new() -> Self {
Self {
text: Rope::new(),
parser: MarkdownParser::default(),
highlighter: Highlighter::new(),
tree: None,
code_highlight_cache: CodeHighlightCache::default(),
parsed: Rc::new(ParsedNodes::default()),
inline_styles: Rc::new(Vec::new()),
version: NEXT_VERSION.fetch_add(1, Ordering::Relaxed),
}
}
pub fn version(&self) -> u64 {
self.version
}
fn update_caches(&mut self) {
self.parsed = Rc::new(
self.tree
.as_ref()
.map(|t| collect_node_infos(&t.block_tree().root_node()))
.unwrap_or_default(),
);
self.inline_styles = Rc::new(if let Some(ref tree) = self.tree {
extract_all_inline_styles(tree, &self.text)
} else {
Vec::new()
});
}
fn apply_edit(&mut self, offset: usize, to_delete: &str, to_insert: &str) {
let delete_len = to_delete.len();
let insert_len = to_insert.len();
let start_point = self.byte_to_point(offset);
let old_end_point = if delete_len > 0 {
self.byte_to_point(offset + delete_len)
} else {
start_point
};
let new_end_position = if insert_len > 0 {
self.compute_new_end_point(start_point, to_insert)
} else {
start_point
};
let edit = InputEdit {
start_byte: offset,
old_end_byte: offset + delete_len,
new_end_byte: offset + insert_len,
start_position: start_point,
old_end_position: old_end_point,
new_end_position,
};
if delete_len > 0 {
let char_start = self.text.byte_to_char(offset);
let char_end = self.text.byte_to_char(offset + delete_len);
self.text.remove(char_start..char_end);
}
if insert_len > 0 {
let char_offset = self.text.byte_to_char(offset);
self.text.insert(char_offset, to_insert);
}
if let Some(ref mut tree) = self.tree {
tree.edit(&edit);
}
self.tree = self.parser.parse_rope(&self.text, self.tree.as_ref());
self.normalize_ordered_lists();
self.update_caches();
self.code_highlight_cache.valid = false;
self.version += 1;
}
fn normalize_ordered_lists(&mut self) -> bool {
let Some(tree) = &self.tree else {
return false;
};
let corrections = self.find_ordered_list_corrections(tree.block_tree().root_node());
if corrections.is_empty() {
return false;
}
for (marker_range, correct_number, is_parenthesis) in corrections.into_iter().rev() {
let new_marker = if is_parenthesis {
format!("{}) ", correct_number)
} else {
format!("{}. ", correct_number)
};
let char_start = self.text.byte_to_char(marker_range.start);
let char_end = self.text.byte_to_char(marker_range.end);
self.text.remove(char_start..char_end);
let char_offset = self.text.byte_to_char(marker_range.start);
self.text.insert(char_offset, &new_marker);
}
self.tree = self.parser.parse_rope(&self.text, None);
true
}
fn find_ordered_list_corrections(
&self,
root: tree_sitter::Node,
) -> Vec<(Range<usize>, usize, bool)> {
let mut corrections = Vec::new();
self.collect_list_corrections(&root, &mut corrections);
corrections
}
fn collect_list_corrections(
&self,
node: &tree_sitter::Node,
corrections: &mut Vec<(Range<usize>, usize, bool)>,
) {
if node.kind() == "list" {
let is_ordered = self.is_ordered_list(node);
if is_ordered {
let mut item_number = 1;
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32)
&& child.kind() == "list_item"
&& let Some((marker_range, current_number, is_parenthesis)) =
self.extract_ordered_marker(&child)
{
if current_number != item_number {
corrections.push((marker_range, item_number, is_parenthesis));
}
item_number += 1;
}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
self.collect_list_corrections(&child, corrections);
}
}
}
fn is_ordered_list(&self, list_node: &tree_sitter::Node) -> bool {
for i in 0..list_node.child_count() {
if let Some(child) = list_node.child(i as u32)
&& child.kind() == "list_item"
{
for j in 0..child.child_count() {
if let Some(marker) = child.child(j as u32) {
return marker.kind().starts_with("list_marker_decimal")
|| marker.kind() == "list_marker_dot"
|| marker.kind() == "list_marker_parenthesis";
}
}
}
}
false
}
fn extract_ordered_marker(
&self,
list_item: &tree_sitter::Node,
) -> Option<(Range<usize>, usize, bool)> {
for i in 0..list_item.child_count() {
if let Some(marker) = list_item.child(i as u32)
&& (marker.kind().starts_with("list_marker_decimal")
|| marker.kind() == "list_marker_dot"
|| marker.kind() == "list_marker_parenthesis")
{
let start = marker.start_byte();
let end = marker.end_byte();
let is_parenthesis = marker.kind() == "list_marker_parenthesis";
let char_start = self.text.byte_to_char(start);
let char_end = self.text.byte_to_char(end);
let slice = self.text.slice(char_start..char_end);
let number: usize = slice
.chars()
.take_while(|c| c.is_ascii_digit())
.collect::<String>()
.parse()
.unwrap_or(1);
return Some((start..end, number, is_parenthesis));
}
}
None
}
fn byte_to_point(&self, byte_offset: usize) -> Point {
let byte_offset = byte_offset.min(self.text.len_bytes());
let char_offset = self.text.byte_to_char(byte_offset);
let line = self.text.char_to_line(char_offset);
let line_start_char = self.text.line_to_char(line);
let line_start_byte = self.text.char_to_byte(line_start_char);
let column = byte_offset - line_start_byte;
Point::new(line, column)
}
fn compute_new_end_point(&self, start: Point, text: &str) -> Point {
let newlines: Vec<_> = text.match_indices('\n').collect();
if newlines.is_empty() {
Point::new(start.row, start.column + text.len())
} else {
let last_newline_pos = newlines.last().unwrap().0;
let column = text.len() - last_newline_pos - 1;
Point::new(start.row + newlines.len(), column)
}
}
pub fn text(&self) -> String {
self.text.to_string()
}
pub fn len_bytes(&self) -> usize {
self.text.len_bytes()
}
pub fn len_chars(&self) -> usize {
self.text.len_chars()
}
pub fn is_empty(&self) -> bool {
self.text.len_bytes() == 0
}
pub fn byte_at(&self, offset: usize) -> Option<u8> {
if offset >= self.text.len_bytes() {
return None;
}
self.text
.byte_slice(offset..offset + 1)
.as_str()
.and_then(|s| s.bytes().next())
}
pub fn rope(&self) -> &Rope {
&self.text
}
pub fn tree(&self) -> Option<&MarkdownTree> {
self.tree.as_ref()
}
pub fn parsed(&self) -> &ParsedNodes {
&self.parsed
}
pub fn line_markers(&self, line_idx: usize) -> LineMarkers {
let range = self.line_byte_range(line_idx);
let markers = markers_at_from_infos(&self.parsed.nodes, &self.text, range.start, range.end);
let in_checked_task = is_line_in_checked_task(&self.parsed.nodes, range.start);
let in_code_block = is_line_in_code_block(&self.parsed.nodes, range.start);
LineMarkers {
range,
line_number: line_idx,
markers,
in_checked_task,
in_code_block,
}
}
#[cfg(test)]
pub fn lines(&self) -> Vec<LineMarkers> {
(0..self.line_count())
.map(|i| self.line_markers(i))
.collect()
}
pub fn render_snapshot(&mut self) -> RenderSnapshot {
if !self.code_highlight_cache.valid {
self.rebuild_code_highlight_cache();
}
RenderSnapshot {
rope: self.text.clone(),
inline_styles: Rc::clone(&self.inline_styles),
code_highlights: Rc::clone(&self.code_highlight_cache.highlights),
parsed: Rc::clone(&self.parsed),
line_count: self.text.len_lines(),
}
}
pub fn inline_styles_for_range(&self, range: &Range<usize>) -> Vec<StyledRegion> {
styles_in_range(&self.inline_styles, range)
.into_iter()
.cloned()
.collect()
}
pub fn byte_to_line(&self, byte_offset: usize) -> usize {
let char_offset = self.text.byte_to_char(byte_offset);
self.text.char_to_line(char_offset)
}
pub fn line_to_byte(&self, line: usize) -> usize {
let char_offset = self.text.line_to_char(line);
self.text.char_to_byte(char_offset)
}
pub fn line_count(&self) -> usize {
self.text.len_lines()
}
pub fn is_line_empty(&self, line_idx: usize) -> bool {
if line_idx >= self.line_count() {
return true;
}
let line = self.line_markers(line_idx);
if line.is_fence() {
return false;
}
self.slice_cow(line.content_start()..line.range.end)
.trim()
.is_empty()
}
#[cfg(test)]
pub fn line(&self, line_idx: usize) -> String {
let line = self.text.line(line_idx);
let mut s = line.to_string();
if s.ends_with('\n') {
s.pop();
}
s
}
pub fn line_byte_range(&self, line_idx: usize) -> Range<usize> {
compute_line_byte_range(&self.text, line_idx)
}
fn slice(&self, range: Range<usize>) -> String {
let char_start = self.text.byte_to_char(range.start);
let char_end = self.text.byte_to_char(range.end);
self.text.slice(char_start..char_end).to_string()
}
pub fn slice_cow(&self, range: Range<usize>) -> std::borrow::Cow<'_, str> {
let len = self.text.len_bytes();
let start = range.start.min(len);
let end = range.end.min(len);
if start >= end {
return std::borrow::Cow::Borrowed("");
}
let char_start = self.text.byte_to_char(start);
let char_end = self.text.byte_to_char(end);
let slice = self.text.slice(char_start..char_end);
match slice.as_str() {
Some(s) => std::borrow::Cow::Borrowed(s),
None => std::borrow::Cow::Owned(slice.to_string()),
}
}
pub fn code_highlights_for_range(&mut self, range: Range<usize>) -> Vec<HighlightSpan> {
if !self.code_highlight_cache.valid {
self.rebuild_code_highlight_cache();
}
let mut result = Vec::new();
for (block_range, highlights) in self.code_highlight_cache.highlights.iter() {
if range.start < block_range.end && range.end > block_range.start {
for span in highlights {
if span.range.start < range.end && span.range.end > range.start {
result.push(span.clone());
}
}
}
}
result
}
fn rebuild_code_highlight_cache(&mut self) {
let highlights = Rc::make_mut(&mut self.code_highlight_cache.highlights);
highlights.clear();
for code_block in &self.parsed.code_blocks {
let language = code_block.info_string_range.as_ref().and_then(|range| {
let char_start = self.text.byte_to_char(range.start);
let char_end = self.text.byte_to_char(range.end);
let slice = self.text.slice(char_start..char_end);
let lang = slice.to_string().trim().to_string();
if lang.is_empty() { None } else { Some(lang) }
});
if let Some(lang) = language
&& !code_block.content_range.is_empty()
{
let char_start = self.text.byte_to_char(code_block.content_range.start);
let char_end = self.text.byte_to_char(code_block.content_range.end);
let slice = self.text.slice(char_start..char_end);
let code_content = slice.to_string();
let mut spans = self.highlighter.highlight(&code_content, &lang);
for span in &mut spans {
span.range.start += code_block.content_range.start;
span.range.end += code_block.content_range.start;
}
highlights.push((code_block.block_range.clone(), spans));
}
}
self.code_highlight_cache.valid = true;
}
}
impl Default for BufferContent {
fn default() -> Self {
Self::new()
}
}
pub struct Buffer {
content: BufferContent,
history: Record<TextEdit>,
}
impl Deref for Buffer {
type Target = BufferContent;
fn deref(&self) -> &Self::Target {
&self.content
}
}
impl DerefMut for Buffer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.content
}
}
impl Buffer {
pub fn new() -> Self {
Self {
content: BufferContent::new(),
history: Record::new(),
}
}
pub fn is_dirty(&self) -> bool {
!self.history.is_saved()
}
pub fn mark_clean(&mut self) {
self.history.set_saved();
}
pub fn insert(&mut self, byte_offset: usize, text: &str, cursor_before: usize) -> usize {
let cursor_after = byte_offset + text.len();
let edit = TextEdit::new(
byte_offset,
String::new(),
text.to_string(),
cursor_before,
cursor_after,
);
self.history.edit(&mut self.content, edit)
}
pub fn delete(&mut self, byte_range: Range<usize>, cursor_before: usize) -> usize {
let deleted = self.content.slice(byte_range.clone());
let cursor_after = byte_range.start;
let edit = TextEdit::new(
byte_range.start,
deleted,
String::new(),
cursor_before,
cursor_after,
);
self.history.edit(&mut self.content, edit)
}
pub fn replace(&mut self, byte_range: Range<usize>, text: &str, cursor_before: usize) -> usize {
let deleted = self.content.slice(byte_range.clone());
let cursor_after = byte_range.start + text.len();
let edit = TextEdit::new(
byte_range.start,
deleted,
text.to_string(),
cursor_before,
cursor_after,
);
self.history.edit(&mut self.content, edit)
}
pub fn undo(&mut self) -> Option<usize> {
self.history.undo(&mut self.content)
}
pub fn redo(&mut self) -> Option<usize> {
self.history.redo(&mut self.content)
}
pub fn can_undo(&self) -> bool {
self.history.can_undo()
}
pub fn can_redo(&self) -> bool {
self.history.can_redo()
}
pub fn from_file(path: &std::path::Path) -> std::io::Result<(Self, bool)> {
let content = std::fs::read_to_string(path).unwrap_or_default();
let mut buffer: Buffer = content.parse().expect("Buffer parsing is infallible");
buffer.history.set_saved();
Ok((buffer, false))
}
}
impl Default for Buffer {
fn default() -> Self {
Self::new()
}
}
impl FromStr for Buffer {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let text = Rope::from_str(s);
let mut parser = MarkdownParser::default();
let tree = parser.parse_rope(&text, None);
let mut content = BufferContent {
text,
parser,
highlighter: Highlighter::new(),
tree,
code_highlight_cache: CodeHighlightCache::default(),
parsed: Rc::new(ParsedNodes::default()),
inline_styles: Rc::new(Vec::new()),
version: NEXT_VERSION.fetch_add(1, Ordering::Relaxed),
};
content.update_caches();
let mut history = Record::new();
history.set_saved();
Ok(Self { content, history })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_buffer_is_empty() {
let buf = Buffer::new();
assert!(buf.is_empty());
assert_eq!(buf.len_bytes(), 0);
}
#[test]
fn test_from_str() {
let buf: Buffer = "hello world".parse().unwrap();
assert_eq!(buf.text(), "hello world");
assert_eq!(buf.len_bytes(), 11);
assert!(!buf.is_empty());
}
#[test]
fn test_insert() {
let mut buf: Buffer = "hello world".parse().unwrap();
buf.insert(5, " beautiful", 5);
assert_eq!(buf.text(), "hello beautiful world");
}
#[test]
fn test_delete() {
let mut buf: Buffer = "hello world".parse().unwrap();
buf.delete(5..11, 11);
assert_eq!(buf.text(), "hello");
}
#[test]
fn test_replace() {
let mut buf: Buffer = "hello world".parse().unwrap();
buf.replace(6..11, "rust", 11);
assert_eq!(buf.text(), "hello rust");
}
#[test]
fn test_line_operations() {
let buf: Buffer = "line one\nline two\nline three".parse().unwrap();
assert_eq!(buf.line_count(), 3);
assert_eq!(buf.line(0), "line one");
assert_eq!(buf.line(1), "line two");
assert_eq!(buf.line(2), "line three");
}
#[test]
fn test_byte_to_line() {
let buf: Buffer = "abc\ndef\nghi".parse().unwrap();
assert_eq!(buf.byte_to_line(0), 0);
assert_eq!(buf.byte_to_line(3), 0);
assert_eq!(buf.byte_to_line(4), 1);
assert_eq!(buf.byte_to_line(8), 2);
}
#[test]
fn test_tree_exists_after_parse() {
let buf: Buffer = "# Hello\n\nSome **bold** text.".parse().unwrap();
assert!(buf.tree().is_some());
}
#[test]
fn test_tree_updated_after_edit() {
let mut buf: Buffer = "# Hello".parse().unwrap();
let tree1 = buf.tree().map(|t| t.block_tree().root_node().to_sexp());
buf.insert(7, " World", 7);
let tree2 = buf.tree().map(|t| t.block_tree().root_node().to_sexp());
assert_ne!(tree1, tree2);
}
#[test]
fn test_undo_insert() {
let mut buf: Buffer = "hello".parse().unwrap();
buf.insert(5, " world", 5);
assert_eq!(buf.text(), "hello world");
let cursor = buf.undo();
assert_eq!(cursor, Some(5));
assert_eq!(buf.text(), "hello");
}
#[test]
fn test_redo_insert() {
let mut buf: Buffer = "hello".parse().unwrap();
buf.insert(5, " world", 5);
buf.undo();
let cursor = buf.redo();
assert_eq!(cursor, Some(11));
assert_eq!(buf.text(), "hello world");
}
#[test]
fn test_undo_delete() {
let mut buf: Buffer = "hello world".parse().unwrap();
buf.delete(5..11, 11);
assert_eq!(buf.text(), "hello");
let cursor = buf.undo();
assert_eq!(cursor, Some(11));
assert_eq!(buf.text(), "hello world");
}
#[test]
fn test_dirty_state() {
let mut buf: Buffer = "hello".parse().unwrap();
assert!(!buf.is_dirty());
buf.insert(5, " world", 5);
assert!(buf.is_dirty());
buf.mark_clean();
assert!(!buf.is_dirty());
buf.insert(11, "!", 11);
assert!(buf.is_dirty());
}
#[test]
fn test_dirty_after_undo_to_save_point() {
let mut buf: Buffer = "hello".parse().unwrap();
buf.insert(5, " world", 5);
buf.mark_clean();
assert!(!buf.is_dirty());
buf.insert(11, "!", 11);
assert!(buf.is_dirty());
buf.undo();
assert!(!buf.is_dirty());
}
#[test]
fn test_dirty_save_point_unreachable() {
let mut buf: Buffer = "hello".parse().unwrap();
buf.insert(5, " world", 5);
buf.mark_clean();
buf.undo();
assert!(buf.is_dirty());
buf.insert(5, " rust", 5);
assert!(buf.is_dirty());
buf.undo();
assert!(buf.is_dirty());
}
#[test]
fn test_multiple_undo_redo() {
let mut buf: Buffer = "a".parse().unwrap();
buf.insert(1, "b", 1);
buf.insert(2, "c", 2);
buf.insert(3, "d", 3);
assert_eq!(buf.text(), "abcd");
buf.undo();
assert_eq!(buf.text(), "abc");
buf.undo();
assert_eq!(buf.text(), "ab");
buf.undo();
assert_eq!(buf.text(), "a");
buf.redo();
assert_eq!(buf.text(), "ab");
buf.redo();
assert_eq!(buf.text(), "abc");
buf.redo();
assert_eq!(buf.text(), "abcd");
}
#[test]
fn test_ordered_list_no_normalization() {
let buf: Buffer = "1. First\n5. Second\n9. Third\n".parse().unwrap();
assert_eq!(buf.text(), "1. First\n5. Second\n9. Third\n");
}
#[test]
fn test_ordered_list_parenthesis_no_normalization() {
let buf: Buffer = "1) First\n5) Second\n9) Third\n".parse().unwrap();
assert_eq!(buf.text(), "1) First\n5) Second\n9) Third\n");
}
#[test]
fn test_unordered_list_unchanged() {
let buf: Buffer = "- First\n- Second\n- Third\n".parse().unwrap();
assert_eq!(buf.text(), "- First\n- Second\n- Third\n");
}
use crate::marker::MarkerKind;
#[test]
fn test_lines_empty_buffer() {
let buf: Buffer = "".parse().unwrap();
let lines = buf.lines();
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].range, 0..0);
assert!(lines[0].markers.is_empty());
}
#[test]
fn test_lines_single_newline() {
let buf: Buffer = "\n".parse().unwrap();
let lines = buf.lines();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].range, 0..0);
assert_eq!(lines[1].range, 1..1);
}
#[test]
fn test_lines_paragraph() {
let buf: Buffer = "Hello\n\nWorld\n".parse().unwrap();
let lines = buf.lines();
assert_eq!(lines.len(), 4);
assert_eq!(lines[0].range, 0..5);
assert_eq!(lines[1].range, 6..6); assert_eq!(lines[2].range, 7..12);
}
#[test]
fn test_heading_markers() {
let buf: Buffer = "# Hello\n".parse().unwrap();
let lines = buf.lines();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].heading_level(), Some(1));
assert_eq!(lines[0].marker_range(), Some(0..2));
}
#[test]
fn test_heading_levels() {
let buf: Buffer = "# H1\n## H2\n### H3\n".parse().unwrap();
let lines = buf.lines();
assert_eq!(lines[0].heading_level(), Some(1));
assert_eq!(lines[1].heading_level(), Some(2));
assert_eq!(lines[2].heading_level(), Some(3));
}
#[test]
fn test_unordered_list_markers() {
let buf: Buffer = "- Item 1\n- Item 2\n".parse().unwrap();
let lines = buf.lines();
assert!(
lines[0]
.markers
.iter()
.any(|m| matches!(m.kind, MarkerKind::ListItem { ordered: false, .. }))
);
assert_eq!(lines[0].marker_range(), Some(0..2));
}
#[test]
fn test_ordered_list_markers() {
let buf: Buffer = "1. First\n2. Second\n".parse().unwrap();
let lines = buf.lines();
assert!(
lines[0]
.markers
.iter()
.any(|m| matches!(m.kind, MarkerKind::ListItem { ordered: true, .. }))
);
}
#[test]
fn test_checkbox_markers() {
let buf: Buffer = "- [ ] Unchecked\n- [x] Checked\n".parse().unwrap();
let lines = buf.lines();
assert_eq!(lines[0].checkbox(), Some(false));
assert_eq!(lines[1].checkbox(), Some(true));
}
#[test]
fn test_blockquote_markers() {
let buf: Buffer = "> Quote\n".parse().unwrap();
let lines = buf.lines();
assert!(lines[0].has_border());
assert!(
lines[0]
.markers
.iter()
.any(|m| matches!(m.kind, MarkerKind::BlockQuote))
);
}
#[test]
fn test_is_line_empty_blockquote_only() {
let buf: Buffer = "> hey\n> \n> > hey".parse().unwrap();
assert!(!buf.is_line_empty(0)); assert!(buf.is_line_empty(1)); assert!(!buf.is_line_empty(2)); }
#[test]
fn test_marker_range_includes_trailing_space() {
let buf: Buffer = "> hey".parse().unwrap();
let lines = buf.lines();
assert_eq!(lines[0].marker_range(), Some(0..2));
let buf: Buffer = "- item".parse().unwrap();
let lines = buf.lines();
assert_eq!(lines[0].marker_range(), Some(0..2));
let buf: Buffer = "> > hey".parse().unwrap();
let lines = buf.lines();
assert_eq!(lines[0].marker_range(), Some(0..4)); }
#[test]
fn test_nested_blockquote_lines() {
let buf: Buffer = "> Level 1\n> > Level 2\n".parse().unwrap();
let lines = buf.lines();
assert_eq!(
lines[0]
.markers
.iter()
.filter(|m| matches!(m.kind, MarkerKind::BlockQuote))
.count(),
1
);
assert_eq!(
lines[1]
.markers
.iter()
.filter(|m| matches!(m.kind, MarkerKind::BlockQuote))
.count(),
2
);
}
#[test]
fn test_list_in_blockquote_lines() {
let buf: Buffer = "> - Item\n".parse().unwrap();
let lines = buf.lines();
assert!(
lines[0]
.markers
.iter()
.any(|m| matches!(m.kind, MarkerKind::BlockQuote))
);
assert!(
lines[0]
.markers
.iter()
.any(|m| matches!(m.kind, MarkerKind::ListItem { .. }))
);
}
#[test]
fn test_code_block_fence_lines() {
let buf: Buffer = "```rust\nlet x = 1;\n```\n".parse().unwrap();
let lines = buf.lines();
assert!(lines[0].is_fence());
assert!(!lines[1].is_fence());
assert!(lines[2].is_fence());
}
#[test]
fn test_thematic_break_lines() {
let buf: Buffer = "---\n".parse().unwrap();
let lines = buf.lines();
assert!(
lines[0]
.markers
.iter()
.any(|m| matches!(m.kind, MarkerKind::ThematicBreak))
);
}
#[test]
fn test_nested_list_continuation() {
let buf: Buffer = "- Item 1\n - Nested\n".parse().unwrap();
let lines = buf.lines();
let continuation = lines[1].continuation_rope(buf.rope());
assert!(continuation.contains("- "));
}
#[test]
fn test_substitution() {
let buf: Buffer = "- Item\n".parse().unwrap();
let lines = buf.lines();
let sub = lines[0].substitution_rope(buf.rope());
assert_eq!(sub, "");
}
#[test]
fn test_list_in_blockquote_continuation() {
let buf: Buffer = "> - Item\n".parse().unwrap();
let lines = buf.lines();
let continuation = lines[0].continuation_rope(buf.rope());
assert_eq!(continuation, "> - ");
}
#[test]
fn test_multiline_blockquote_with_list_continuation() {
let buf: Buffer = "> hey\n>\n> - foo\n".parse().unwrap();
let lines = buf.lines();
let continuation = lines[2].continuation_rope(buf.rope());
assert_eq!(continuation, "> - ");
}
#[test]
fn test_code_block_in_blockquote() {
let buf: Buffer = "> ```rust\n> fn main() {}\n> ```\n".parse().unwrap();
let lines = buf.lines();
assert!(lines[0].is_fence());
assert!(lines[0].has_border());
}
}