use crate::document::row_map::RawPos;
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(super) fn raw_line_sel_cols(
block_source: &str,
block_start: usize,
line_idx: usize,
raw_text: &str,
range: (usize, usize),
) -> Option<(usize, usize)> {
text_sel_cols(
raw_text,
block_start + raw_line_byte_start(block_source, line_idx),
range,
)
}
pub(super) fn text_sel_cols(
text: &str,
text_start: usize,
(range_start, range_end): (usize, usize),
) -> Option<(usize, usize)> {
let text_end = text_start + text.len();
let lo = range_start.clamp(text_start, text_end);
let hi = range_end.clamp(text_start, text_end);
if lo >= hi {
return None;
}
Some((
text[..lo - text_start].chars().count(),
text[..hi - text_start].chars().count(),
))
}
pub(crate) struct RawBlockCursor {
pub source: String,
pub raw_line: usize,
pub col: usize,
}
pub(crate) fn raw_block_cursor(state: &EditorState) -> RawBlockCursor {
let Some((block, pos)) = cursor_block_pos(state) else {
return RawBlockCursor {
source: String::new(),
raw_line: 0,
col: 0,
};
};
RawBlockCursor {
source: block_source(state, block),
raw_line: pos.line,
col: pos.col,
}
}
pub(crate) fn block_source(state: &EditorState, block: usize) -> String {
let rope = state.buffer.rope();
state
.parsed
.source_map
.original_range_for_block(block)
.map(|r| {
let end = r.end.min(rope.len_bytes());
let start = r.start.min(end);
rope.slice(rope.byte_to_char(start)..rope.byte_to_char(end))
.to_string()
})
.unwrap_or_default()
}
pub(crate) fn cursor_block_pos(state: &EditorState) -> Option<(usize, RawPos)> {
let rope = state.buffer.rope();
let cursor_byte = rope.char_to_byte(state.cursor.offset);
let block = state.parsed.source_map.block_for_byte(cursor_byte)?;
let range = state.parsed.source_map.original_range_for_block(block)?;
let end = range.end.min(rope.len_bytes());
let start = range.start.min(end);
let content_end = if end > start && rope.byte(end - 1) == b'\n' {
end - 1
} else {
end
};
let at = cursor_byte.clamp(start, content_end);
let mut pos = RawPos { line: 0, col: 0 };
for chunk in rope
.slice(rope.byte_to_char(start)..rope.byte_to_char(at))
.chunks()
{
match chunk.rfind('\n') {
Some(i) => {
pos.line += chunk.bytes().filter(|&b| b == b'\n').count();
pos.col = chunk[i + 1..].chars().count();
}
None => pos.col += chunk.chars().count(),
}
}
Some((block, pos))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raw_line_sel_cols_intersects_the_range_with_one_line() {
let src = "ab\nцd\n";
assert_eq!(raw_line_sel_cols(src, 10, 1, "цd", (0, 100)), Some((0, 2)));
assert_eq!(raw_line_sel_cols(src, 10, 1, "цd", (15, 16)), Some((1, 2)));
assert_eq!(raw_line_sel_cols(src, 10, 1, "цd", (10, 13)), None);
}
#[test]
fn cursor_block_pos_counts_lines_as_the_block_source_splits_them() {
use crate::config::Theme;
use crate::document::Buffer;
let theme: &'static Theme = Box::leak(Box::new(Theme::default()));
for src in [
"alpha\nbravo\n\nnext\n",
"a\u{2028}b\nc d\n",
"lone\rcr\nline\n",
"- a\n b\n\n c\n- d\n",
"> q\n> r\n",
"| a |\n|---|\n| ж |\n",
"no newline",
"",
] {
let mut state = EditorState::new(Buffer::from_str(src), theme);
let text = state.buffer.contents();
for offset in 0..=state.buffer.len_chars() {
state.cursor.offset = offset;
let Some((block, pos)) = cursor_block_pos(&state) else {
continue;
};
let range = state
.parsed
.source_map
.original_range_for_block(block)
.unwrap();
let source = &text[range.start..range.end.min(text.len())];
let at = state.buffer.rope().char_to_byte(offset).max(range.start) - range.start;
let lines = raw_source_lines(source);
let mut want = (lines.len() - 1, lines.last().unwrap().chars().count());
let mut line_start = 0;
for (i, line) in lines.iter().enumerate() {
if at <= line_start + line.len() {
want = (i, line[..at.saturating_sub(line_start)].chars().count());
break;
}
line_start += line.len() + 1;
}
assert_eq!((pos.line, pos.col), want, "offset {offset} in {src:?}");
let raw = raw_block_cursor(&state);
assert_eq!(raw.source, source, "offset {offset} in {src:?}");
}
}
}
#[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:?}"
);
}
}
}