//! Cursor position and document statistics — pure helpers over the text. /// (line, column) of `cursor` (byte offset), both 0-based; column counts /// characters of the line, not bytes. #[must_use] pub fn line_col(text: &str, cursor: usize) -> (usize, usize) { let floored = if text.is_char_boundary(cursor) { usize::min(cursor, text.len()) } else { let mut p = cursor; while p > 0 && !text.is_char_boundary(p) { p -= 1; } p }; let before = text.get(..floored).unwrap_or(""); let line = before.matches('\n').count(); let col = before.chars().rev().take_while(|c| *c != '\n').count(); (line, col) } /// Words in the text: runs of characters that are alphanumeric or `_`. #[must_use] pub fn word_count(text: &str) -> usize { let mut count = 0usize; let mut in_word = false; for c in text.chars() { if c.is_alphanumeric() || c == '_' { if !in_word { count += 1; } in_word = true; } else { in_word = false; } } count } #[cfg(test)] mod tests { use super::*; #[test] fn line_col_counts_chars() { let text = "ab é\nxyz"; let byte_x = text.find('x').unwrap_or(0); assert_eq!(line_col(text, byte_x), (1, 0)); assert_eq!(line_col(text, 0), (0, 0)); let end = text.len(); assert_eq!(line_col(text, end), (1, 3)); } #[test] fn line_col_at_char_start() { let mut text = String::from("éx"); let byte_after_e = text.find('x').unwrap_or(0); assert_eq!(line_col(&text, byte_after_e), (0, 1)); text.push_str("\nq"); let byte_q = text.find('q').unwrap_or(0); assert_eq!(line_col(&text, byte_q), (1, 0)); } #[test] fn word_count_runs() { assert_eq!(word_count(""), 0); assert_eq!(word_count("hello world"), 2); assert_eq!(word_count("éx_ one-two #tag"), 4); assert_eq!(word_count("...,,, "), 0); } }