use super::*;
pub fn offset_from_utf16(text: &str, offset: usize) -> usize {
let mut utf8_offset = 0;
let mut utf16_count = 0;
for ch in text.chars() {
if utf16_count >= offset {
break;
}
utf16_count += ch.len_utf16();
utf8_offset += ch.len_utf8();
}
utf8_offset
}
pub fn offset_to_utf16(text: &str, offset: usize) -> usize {
let mut utf16_offset = 0;
let mut utf8_count = 0;
for ch in text.chars() {
if utf8_count >= offset {
break;
}
utf8_count += ch.len_utf8();
utf16_offset += ch.len_utf16();
}
utf16_offset
}
pub fn range_from_utf16(text: &str, range: Range<usize>) -> Range<usize> {
offset_from_utf16(text, range.start)..offset_from_utf16(text, range.end)
}
pub fn range_to_utf16(text: &str, range: Range<usize>) -> Range<usize> {
offset_to_utf16(text, range.start)..offset_to_utf16(text, range.end)
}
pub fn composition_selection(
text: &str,
start: usize,
selection: Option<Range<usize>>,
) -> Range<usize> {
let range = selection
.map(|range| range_from_utf16(text, range))
.unwrap_or(text.len()..text.len());
start + range.start..start + range.end
}
pub fn previous_boundary(text: &str, offset: usize) -> usize {
text.grapheme_indices(true)
.rev()
.find_map(|(idx, _)| (idx < offset).then_some(idx))
.unwrap_or(0)
}
pub fn next_boundary(text: &str, offset: usize) -> usize {
text.grapheme_indices(true)
.find_map(|(idx, _)| (idx > offset).then_some(idx))
.unwrap_or(text.len())
}
pub fn joins_group(last: Option<(EditKind, usize)>, kind: EditKind, at: usize) -> bool {
last.is_some_and(|(last_kind, offset)| last_kind == kind && at == offset)
}
pub fn normalize(text: &str, shape: Shape) -> String {
let text = text.replace("\r\n", "\n").replace('\r', "\n");
if shape.is_multiline() {
text
} else {
text.replace('\n', " ")
}
}
pub fn line_start(text: &str, offset: usize) -> usize {
text[..offset].rfind('\n').map_or(0, |at| at + 1)
}
pub fn line_end(text: &str, offset: usize) -> usize {
text[offset..]
.find('\n')
.map_or(text.len(), |at| offset + at)
}
pub(super) fn is_word(segment: &str) -> bool {
segment.chars().any(char::is_alphanumeric)
}
pub fn previous_word_boundary(text: &str, offset: usize) -> usize {
text.split_word_bound_indices()
.filter(|(start, _)| *start < offset)
.rfind(|(_, segment)| is_word(segment))
.map(|(start, _)| start)
.unwrap_or(0)
}
pub fn next_word_boundary(text: &str, offset: usize) -> usize {
text.split_word_bound_indices()
.filter(|(start, segment)| start + segment.len() > offset)
.find(|(_, segment)| is_word(segment))
.map(|(start, segment)| start + segment.len())
.unwrap_or(text.len())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Granularity {
Char,
Word,
Line,
}
impl Granularity {
pub fn of_clicks(click_count: usize) -> Self {
match click_count {
0 | 1 => Self::Char,
2 => Self::Word,
_ => Self::Line,
}
}
pub fn around(self, text: &str, offset: usize) -> Range<usize> {
match self {
Self::Char => offset..offset,
Self::Word => {
let segments = || {
text.split_word_bound_indices()
.map(|(start, segment)| (start..start + segment.len(), is_word(segment)))
};
let after = segments().find(|(range, _)| range.contains(&offset));
let before = segments().find(|(range, _)| range.end == offset);
match (after, before) {
(Some((range, true)), _) | (_, Some((range, true))) => range,
(Some((range, _)), _) | (_, Some((range, _))) => range,
(None, None) => offset..offset,
}
}
Self::Line => line_start(text, offset)..line_end(text, offset),
}
}
}
pub fn drag_selection<T: Ord + std::marker::Copy>(pressed: Range<T>, span: Range<T>) -> (T, T) {
if span.start < pressed.start {
(pressed.end, span.start)
} else {
(pressed.start, span.end.max(pressed.end))
}
}