use std::iter::Sum;
use std::ops::{Add, Range, Sub};
use serde::{Deserialize, Serialize};
use crate::db::FilesGroup;
use crate::ids::FileId;
#[cfg(test)]
#[path = "span_test.rs"]
mod test;
#[derive(
Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct TextWidth(u32);
impl TextWidth {
pub fn from_char(c: char) -> Self {
Self(c.len_utf8() as u32)
}
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Self {
Self(s.len() as u32)
}
pub fn new_for_testing(value: u32) -> Self {
Self(value)
}
}
impl Add for TextWidth {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub for TextWidth {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl Sum for TextWidth {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
Self(iter.map(|x| x.0).sum())
}
}
#[derive(
Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
pub struct TextOffset(TextWidth);
impl TextOffset {
pub fn add_width(self, width: TextWidth) -> Self {
TextOffset(self.0 + width)
}
pub fn sub_width(self, width: TextWidth) -> Self {
TextOffset(self.0 - width)
}
pub fn take_from(self, content: &str) -> &str {
&content[(self.0.0 as usize)..]
}
}
impl Sub for TextOffset {
type Output = TextWidth;
fn sub(self, rhs: Self) -> Self::Output {
TextWidth(self.0.0 - rhs.0.0)
}
}
#[derive(
Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct TextSpan {
pub start: TextOffset,
pub end: TextOffset,
}
impl TextSpan {
pub fn width(self) -> TextWidth {
self.end - self.start
}
pub fn contains(self, other: Self) -> bool {
self.start <= other.start && self.end >= other.end
}
pub fn take(self, content: &str) -> &str {
&content[(self.start.0.0 as usize)..(self.end.0.0 as usize)]
}
pub fn n_chars(self, content: &str) -> usize {
self.take(content).chars().count()
}
pub fn after(self) -> Self {
Self { start: self.end, end: self.end }
}
pub fn start_only(self) -> Self {
Self { start: self.start, end: self.start }
}
pub fn to_str_range(&self) -> Range<usize> {
self.start.0.0 as usize..self.end.0.0 as usize
}
pub fn position_in_file(self, db: &dyn FilesGroup, file: FileId) -> Option<TextPositionSpan> {
let start = self.start.position_in_file(db, file)?;
let end = self.end.position_in_file(db, file)?;
Some(TextPositionSpan { start, end })
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct TextPosition {
pub line: usize,
pub col: usize,
}
impl TextOffset {
fn get_line_number(self, db: &dyn FilesGroup, file: FileId) -> Option<usize> {
let summary = db.file_summary(file)?;
assert!(
self <= summary.last_offset,
"TextOffset out of range. {:?} > {:?}.",
self.0,
summary.last_offset.0
);
Some(summary.line_offsets.binary_search(&self).unwrap_or_else(|x| x - 1))
}
pub fn position_in_file(self, db: &dyn FilesGroup, file: FileId) -> Option<TextPosition> {
let summary = db.file_summary(file)?;
let line_number = self.get_line_number(db, file)?;
let line_offset = summary.line_offsets[line_number];
let content = db.file_content(file)?;
let col = TextSpan { start: line_offset, end: self }.n_chars(&content);
Some(TextPosition { line: line_number, col })
}
}
impl TextPosition {
pub fn offset_in_file(self, db: &dyn FilesGroup, file: FileId) -> Option<TextOffset> {
let file_summary = db.file_summary(file)?;
let content = db.file_content(file)?;
let mut offset =
file_summary.line_offsets.get(self.line).copied().unwrap_or(file_summary.last_offset);
offset = offset.add_width(
offset
.take_from(&content)
.chars()
.take_while(|c| *c != '\n')
.take(self.col)
.map(TextWidth::from_char)
.sum(),
);
Some(offset)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileSummary {
pub line_offsets: Vec<TextOffset>,
pub last_offset: TextOffset,
}
impl FileSummary {
pub fn line_count(&self) -> usize {
self.line_offsets.len()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct TextPositionSpan {
pub start: TextPosition,
pub end: TextPosition,
}
impl TextPositionSpan {
pub fn offset_in_file(self, db: &dyn FilesGroup, file: FileId) -> Option<TextSpan> {
let start = self.start.offset_in_file(db, file)?;
let end = self.end.offset_in_file(db, file)?;
Some(TextSpan { start, end })
}
}