use std::{char, ops::Range};
use gpui::{Context, Window};
use ropey::Rope;
use sum_tree::Bias;
use crate::{input::InputState, RopeExt as _};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CharType {
Word,
Whitespace,
Newline,
Other,
}
impl From<char> for CharType {
fn from(c: char) -> Self {
match c {
'_' => CharType::Word,
c if c.is_ascii_alphanumeric() => CharType::Word,
c if c == '\n' || c == '\r' => CharType::Newline,
c if c.is_whitespace() => CharType::Whitespace,
_ => CharType::Other,
}
}
}
impl CharType {
fn is_connectable(self, c: char) -> bool {
let other = CharType::from(c);
match (self, other) {
(CharType::Word, CharType::Word) => true,
(CharType::Whitespace, CharType::Whitespace) => true,
_ => false,
}
}
}
impl InputState {
pub(super) fn select_word(&mut self, offset: usize, _: &mut Window, cx: &mut Context<Self>) {
let Some(range) = TextSelector::word_range(&self.text, offset) else {
return;
};
self.selected_range = (range.start..range.end).into();
self.selected_word_range = Some(self.selected_range);
cx.notify()
}
}
struct TextSelector;
impl TextSelector {
pub fn word_range(text: &Rope, offset: usize) -> Option<Range<usize>> {
let offset = text.clip_offset(offset, Bias::Left);
let Some(char) = text.char_at(offset) else {
return None;
};
let char_type = CharType::from(char);
let mut start = offset;
let mut end = offset + char.len_utf8();
let prev_chars = text.chars_at(start).reversed().take(128);
let next_chars = text.chars_at(end).take(128);
for ch in prev_chars {
if char_type.is_connectable(ch) {
start -= ch.len_utf8();
} else {
break;
}
}
for ch in next_chars {
if char_type.is_connectable(ch) {
end += ch.len_utf8();
} else {
break;
}
}
Some(start..end)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ropey::Rope;
#[test]
fn test_char_type_from_char() {
assert_eq!(CharType::from('a'), CharType::Word);
assert_eq!(CharType::from('Z'), CharType::Word);
assert_eq!(CharType::from('0'), CharType::Word);
assert_eq!(CharType::from('_'), CharType::Word);
assert_eq!(CharType::from('.'), CharType::Other);
assert_eq!(CharType::from(','), CharType::Other);
assert_eq!(CharType::from(';'), CharType::Other);
assert_eq!(CharType::from('!'), CharType::Other);
assert_eq!(CharType::from('?'), CharType::Other);
assert_eq!(CharType::from('['), CharType::Other);
assert_eq!(CharType::from('{'), CharType::Other);
assert_eq!(CharType::from(' '), CharType::Whitespace);
assert_eq!(CharType::from('\t'), CharType::Whitespace);
assert_eq!(CharType::from('\u{00A0}'), CharType::Whitespace);
assert_eq!(CharType::from('\n'), CharType::Newline);
assert_eq!(CharType::from('\r'), CharType::Newline);
assert_eq!(CharType::from('ζ±'), CharType::Other);
assert_eq!(CharType::from('Γ©'), CharType::Other);
}
#[test]
fn test_word_range() {
use indoc::indoc;
let rope = Rope::from(indoc! {
r#"
test text:
abcde δΈζπ test
hello[()]
test_connector ____
Rope
"#
});
let tests = vec![
(0, 0, Some("test")),
(0, 4, Some(" ")),
(1, 0, Some("abcde")),
(1, 4, Some("abcde")),
(1, 5, Some(" ")),
(1, 6, Some("δΈ")),
(1, 9, Some("ζ")),
(1, 13, Some("π")),
(1, 20, Some("test")),
(2, 5, Some("[")),
(2, 6, Some("(")),
(2, 7, Some(")")),
(2, 8, Some("]")),
(3, 5, Some("test_connector")),
(3, 14, Some(" ")),
(3, 16, Some("____")),
(4, 0, Some("Rope")),
];
for (line, column, expected) in tests {
let line_start_offset = rope.line_start_offset(line);
let offset = line_start_offset + column;
let range = TextSelector::word_range(&rope, offset);
let actual = range.map(|r| rope.slice(r).to_string());
let expect = expected.map(|s| s.to_string());
assert_eq!(actual, expect, "line {}, column {}", line, column);
}
}
}