use std::ops::Range; use gpui::{Context, Window}; use ropey::Rope; use sum_tree::Bias; use crate::input::{InputState, RopeExt}; impl InputState { /// Select the word at the given offset on double-click. /// /// The offset is the UTF-8 offset. pub(super) fn select_word(&mut self, offset: usize, _: &mut Window, cx: &mut Context) { 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() } /// Select the line at the given offset on triple-click. /// /// The offset is the UTF-8 offset. pub(super) fn select_line(&mut self, offset: usize, _: &mut Window, cx: &mut Context) { let range = TextSelector::line_range(&self.text, offset); self.selected_range = (range.start..range.end).into(); self.selected_word_range = None; cx.notify() } } struct TextSelector; impl TextSelector { /// Select a line in the given text at the specified offset. /// /// The offset is the UTF-8 offset. /// /// Returns the start and end offsets of the selected line. pub fn line_range(text: &Rope, offset: usize) -> Range { let offset = text.clip_offset(offset, Bias::Left); let row = text.offset_to_point(offset).row; let start = text.line_start_offset(row); let end = text.line_end_offset(row); start..end } /// Select a word in the given text at the specified offset. /// /// The offset is the UTF-8 offset. /// /// Returns the start and end offsets of the selected word. pub fn word_range(text: &Rope, offset: usize) -> Option> { let offset = text.clip_offset(offset, Bias::Left); let char = text.char_at(offset)?; let end = offset + char.len_utf8(); let prev_chars = text.chars_at(offset).reversed().take(128); let next_chars = text.chars_at(end).take(128); Some(word_range_from_chars(offset, char, prev_chars, next_chars)) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CharType { /// a-z, A-Z, 0-9, _ Word, /// '\t', ' ', '\u{00A0}' etc. Whitespace, /// \n, \r Newline, /// . , ; : ( ) [ ] { } ... or CJK characters: `汉`, `🎉` etc. Other, } impl From for CharType { fn from(c: char) -> Self { match c { c if is_word_char(c) => 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 { matches!( (self, CharType::from(c)), (CharType::Word, CharType::Word) | (CharType::Whitespace, CharType::Whitespace) ) } } fn is_word_char(c: char) -> bool { matches!(c, '_') // ASCII alphanumeric characters, for English, numbers: `Hello123`, etc. || c.is_ascii_alphanumeric() // Latin script in Unicode for French, German, Spanish, etc. || matches!(c, '\u{00C0}'..='\u{00FF}') || matches!(c, '\u{0100}'..='\u{017F}') || matches!(c, '\u{0180}'..='\u{024F}') // Cyrillic for Russian, Ukrainian, etc. || matches!(c, '\u{0400}'..='\u{04FF}') // Vietnamese || matches!(c, '\u{1E00}'..='\u{1EFF}') || matches!(c, '\u{0300}'..='\u{036F}') } pub(crate) fn word_range_from_chars( offset: usize, c: char, prev_chars: impl Iterator, next_chars: impl Iterator, ) -> Range { let char_type = CharType::from(c); let mut start = offset; let mut end = offset + c.len_utf8(); for prev in prev_chars.take(128) { if char_type.is_connectable(prev) { start -= prev.len_utf8(); } else { break; } } for next in next_chars.take(128) { if char_type.is_connectable(next) { end += next.len_utf8(); } else { break; } } start..end }