use crate::{misc::EasyUnwrapUnchecked, prelude::TryIntoPatch};
#[cfg_attr(feature = "c_compatible", repr(C))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "bitcode", derive(bitcode::Encode, bitcode::Decode))]
#[cfg_attr(
feature = "wincode",
derive(wincode::SchemaWrite, wincode::SchemaRead)
)]
#[allow(clippy::derived_hash_with_manual_eq)] #[derive(Debug, Clone, Copy, Hash)]
#[cfg_attr(feature = "c_compatible", repr(C))]
pub struct TextPosition {
pub line: usize,
pub column: usize,
}
impl const Eq for TextPosition {}
impl const PartialEq for TextPosition {
fn eq(&self, other: &Self) -> bool {
self.line == other.line && self.column == other.column
}
}
#[cfg(feature = "std")]
impl const Ord for TextPosition {
fn min(self, other: Self) -> Self
where
Self: Sized,
{
if self < other {
self
} else {
other
}
}
fn max(self, other: Self) -> Self
where
Self: Sized,
{
if self > other {
self
} else {
other
}
}
fn clamp(self, min: Self, max: Self) -> Self
where
Self: Sized,
{
self.min(min).max(max)
}
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
if self.line > other.line {
core::cmp::Ordering::Greater
} else if self.line < other.line {
core::cmp::Ordering::Less
} else if self.column > other.column {
core::cmp::Ordering::Greater
} else if self.column < other.column {
core::cmp::Ordering::Less
} else {
core::cmp::Ordering::Equal
}
}
}
#[cfg(feature = "std")]
impl const PartialOrd for TextPosition {
fn ge(&self, other: &Self) -> bool {
self.gt(other) || self == other
}
fn gt(&self, other: &Self) -> bool {
match self.line.cmp(&other.line) {
core::cmp::Ordering::Greater => true,
core::cmp::Ordering::Equal => self.column > other.column,
core::cmp::Ordering::Less => false,
}
}
fn lt(&self, other: &Self) -> bool {
match self.line.cmp(&other.line) {
core::cmp::Ordering::Greater => false,
core::cmp::Ordering::Equal => self.column < other.column,
core::cmp::Ordering::Less => true,
}
}
fn le(&self, other: &Self) -> bool {
self.lt(other) || self == other
}
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl TextPosition {
#[allow(missing_docs)]
#[must_use]
pub const fn new(line: usize, column: usize) -> Self {
Self {
line,
column,
}
}
pub const fn advance_char(&mut self) {
self.column += 1;
}
pub const fn advance_char_by(&mut self, by: usize) {
self.column += by;
}
pub const fn advance_line(&mut self) {
self.line += 1;
self.column = 0;
}
pub const fn advance_line_by(&mut self, by: usize) {
self.line += by;
self.column = 0;
}
#[must_use]
pub fn from_offset(offset: usize, text: &str) -> Self {
let (line, col) = line_and_column_from_offset(offset, text);
Self::new(line, col)
}
#[must_use]
pub fn from_offset_patched<T: TryIntoPatch<usize> + Copy>(
offset: T,
text: &str,
) -> Option<Self> {
let (line, col) = line_and_column_from_offset_patched(offset, text)?;
Some(Self::new(line, col))
}
#[must_use]
pub fn offset_from_pos(&self, text: &str) -> Option<usize> {
offset_from_line_and_column(self.line, self.column, text)
}
#[must_use]
pub fn get_char_at_pos(&self, text: &str) -> Option<char> {
Some(
text.chars()
.nth(offset_from_line_and_column(self.line, self.column, text)?)
.easy_unwrap_unchecked(),
)
}
}
impl Default for TextPosition {
fn default() -> Self {
Self {
line: usize::MAX,
column: usize::MAX,
}
}
}
#[must_use]
pub fn line_and_column_from_offset(
offset: usize,
text: &str,
) -> (usize, usize) {
let mut line = 1;
let mut col = 1;
for (i, ch) in text.char_indices() {
if i >= offset {
break;
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
#[must_use]
pub fn line_and_column_from_offset_patched<T: TryIntoPatch<usize> + Copy>(
offset: T,
text: &str,
) -> Option<(usize, usize)> {
let mut line = 1;
let mut col = 1;
for (i, ch) in text.char_indices() {
if i >= offset.try_into_value()? {
break;
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
Some((line, col))
}
#[must_use]
pub fn offset_from_line_and_column(
line: usize,
col: usize,
text: &str,
) -> Option<usize> {
let mut current_line = 1;
let mut current_col = 1;
for (i, ch) in text.char_indices() {
if current_line == line && current_col == col {
return Some(i);
}
if ch == '\n' {
current_line += 1;
current_col = 1;
} else {
current_col += 1;
}
}
if current_line == line && current_col == col {
return Some(text.len());
}
None
}
#[must_use]
pub fn offset_from_line_and_column_patched<T: TryIntoPatch<usize> + Copy>(
line: T,
col: T,
text: &str,
) -> Option<usize> {
let target_line = line.try_into_value()?;
let target_col = col.try_into_value()?;
let mut current_line = 1;
let mut current_col = 1;
for (i, ch) in text.char_indices() {
if current_line == target_line && current_col == target_col {
return Some(i);
}
if ch == '\n' {
current_line += 1;
current_col = 1;
} else {
current_col += 1;
}
}
if current_line == target_line && current_col == target_col {
return Some(text.len());
}
None
}