use crate::editor::EditorState;
pub(crate) fn raw_source_lines(source: &str) -> Vec<&str> {
if source.is_empty() {
return vec![""];
}
let mut lines: Vec<&str> = source.split('\n').collect();
if lines.last() == Some(&"") && lines.len() > 1 {
lines.pop();
}
lines
}
pub(crate) fn raw_source_line_count(source: &str) -> usize {
if source.is_empty() {
return 1;
}
let count = source.split('\n').count();
if count > 1 && source.ends_with('\n') {
count - 1
} else {
count
}
}
pub(crate) fn revealed_source_line_count(source: &str) -> usize {
let total = raw_source_line_count(source);
let body = source.strip_suffix('\n').unwrap_or(source);
let trailing = body
.rsplit('\n')
.take_while(|line| line.trim().is_empty())
.count();
total.saturating_sub(trailing).max(1)
}
pub(super) fn raw_line_byte_start(block_source: &str, line_idx: usize) -> usize {
let mut byte = 0usize;
for (i, line) in block_source.split('\n').enumerate() {
if i == line_idx {
return byte;
}
byte += line.len() + 1;
}
block_source.len()
}
pub(crate) struct RawBlockCursor {
pub source: String,
pub raw_line: usize,
pub col: usize,
}
pub(crate) fn raw_block_cursor(state: &EditorState, cursor_byte: usize) -> RawBlockCursor {
let source: String = state
.parsed
.source_map
.original_range_for_byte(cursor_byte)
.map(|r| {
let contents = state.buffer.contents();
let end = r.end.min(contents.len());
contents.get(r.start..end).unwrap_or("").to_owned()
})
.unwrap_or_default();
let (raw_line, col) = cursor_position_in_block(state, cursor_byte, &source);
RawBlockCursor {
source,
raw_line,
col,
}
}
fn cursor_position_in_block(
state: &EditorState,
cursor_byte: usize,
raw_source: &str,
) -> (usize, usize) {
if raw_source.is_empty() {
return (0, 0);
}
let block_start_byte = state
.parsed
.source_map
.original_range_for_byte(cursor_byte)
.map(|r| r.start)
.unwrap_or(0);
let cursor_offset_in_block = cursor_byte.saturating_sub(block_start_byte);
let lines = raw_source_lines(raw_source);
let mut byte_pos = 0usize;
for (line_idx, line) in lines.iter().enumerate() {
let line_end = byte_pos + line.len();
if cursor_offset_in_block <= line_end {
let col_bytes = cursor_offset_in_block.saturating_sub(byte_pos);
let col = line[..col_bytes.min(line.len())].chars().count();
return (line_idx, col);
}
byte_pos = line_end + 1; }
let last_line = lines.last().copied().unwrap_or("");
(lines.len().saturating_sub(1), last_line.chars().count())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raw_source_lines_no_trailing_newline() {
let lines = raw_source_lines("hello\nworld");
assert_eq!(lines, vec!["hello", "world"]);
}
#[test]
fn raw_source_lines_trailing_newline() {
let lines = raw_source_lines("hello\nworld\n");
assert_eq!(lines, vec!["hello", "world"]);
}
#[test]
fn raw_source_lines_single() {
let lines = raw_source_lines("hello");
assert_eq!(lines, vec!["hello"]);
}
#[test]
fn raw_source_lines_empty() {
let lines = raw_source_lines("");
assert_eq!(lines, vec![""]);
}
#[test]
fn revealed_count_drops_trailing_blank_lines() {
assert_eq!(revealed_source_line_count("\n\n"), 1);
assert_eq!(revealed_source_line_count("\n"), 1);
assert_eq!(revealed_source_line_count(""), 1);
assert_eq!(
revealed_source_line_count("```mermaid\nflowchart LR\n\n A --> B\n```\n"),
5
);
assert_eq!(revealed_source_line_count("text\n\n\n"), 1);
assert_eq!(revealed_source_line_count(""), 1);
assert_eq!(revealed_source_line_count("\n"), 1);
assert_eq!(revealed_source_line_count("\n\n"), 1);
}
#[test]
fn raw_source_line_count_agrees_with_raw_source_lines() {
for source in [
"",
"hello",
"hello\n",
"\n",
"\n\n",
"hello\nworld",
"hello\nworld\n",
"hello\n\nworld",
"hello\n\nworld\n",
"hello\nworld\n\n",
"```mermaid\nflowchart LR\n A --> B\n```",
"```mermaid\nflowchart LR\n A --> B\n```\n",
] {
assert_eq!(
raw_source_line_count(source),
raw_source_lines(source).len(),
"count and split disagree for {source:?}"
);
}
}
}