use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Position {
pub line: u32,
pub character: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Range {
pub start: Position,
pub end: Position,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Change {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub range: Option<Range>,
pub text: String,
}
fn line_starts(text: &str) -> Vec<usize> {
let bytes = text.as_bytes();
let mut starts = vec![0];
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\r' if bytes.get(i + 1) == Some(&b'\n') => {
starts.push(i + 2);
i += 2;
}
b'\r' | b'\n' => {
starts.push(i + 1);
i += 1;
}
_ => i += 1,
}
}
starts
}
pub fn offset(text: &str, pos: Position) -> Result<usize, String> {
let starts = line_starts(text);
let line = pos.line as usize;
let Some(&start) = starts.get(line) else {
return Err(format!("line {} is past the end of a {}-line text", pos.line, starts.len()));
};
let end = starts.get(line + 1).map_or(text.len(), |&next| {
let mut e = next;
let b = text.as_bytes();
if e > start && b[e - 1] == b'\n' {
e -= 1;
}
if e > start && b[e - 1] == b'\r' {
e -= 1;
}
e
});
let mut units = 0u32;
for (i, c) in text[start..end].char_indices() {
if units == pos.character {
return Ok(start + i);
}
units += c.len_utf16() as u32;
if units > pos.character {
return Err(format!("character {} of line {} falls inside a character", pos.character, pos.line));
}
}
if units == pos.character {
Ok(end)
} else {
Err(format!("character {} is past the end of line {} ({units} long)", pos.character, pos.line))
}
}
pub fn apply(text: &str, changes: &[Change]) -> Result<String, String> {
let mut out = text.to_owned();
for change in changes {
out = match &change.range {
None => change.text.clone(),
Some(r) => {
let (a, b) = (offset(&out, r.start)?, offset(&out, r.end)?);
if a > b {
return Err(format!("a range ends before it starts: {r:?}"));
}
format!("{}{}{}", &out[..a], change.text, &out[b..])
}
};
}
Ok(out)
}
pub fn position(text: &str, offset: usize) -> Result<Position, String> {
if offset > text.len() || !text.is_char_boundary(offset) {
return Err(format!("offset {offset} is not a character boundary of a {}-byte text", text.len()));
}
let starts = line_starts(text);
let line = starts.partition_point(|&s| s <= offset) - 1;
let character = text[starts[line]..offset].encode_utf16().count() as u32;
Ok(Position { line: line as u32, character })
}
pub fn digest(text: &str) -> String {
use sha2::{Digest, Sha256};
format!("{:x}", Sha256::digest(text.as_bytes()))
}
#[cfg(test)]
#[path = "text_tests.rs"]
mod tests;