longcipher_leptos_components/components/editor/
selection.rs1use serde::{Deserialize, Serialize};
6
7use super::cursor::CursorPosition;
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
11pub struct Selection {
12 pub start: CursorPosition,
14 pub end: CursorPosition,
16}
17
18impl Selection {
19 #[must_use]
21 pub const fn new(start: CursorPosition, end: CursorPosition) -> Self {
22 Self { start, end }
23 }
24
25 #[must_use]
27 pub const fn empty(position: CursorPosition) -> Self {
28 Self {
29 start: position,
30 end: position,
31 }
32 }
33
34 #[must_use]
36 pub fn is_empty(&self) -> bool {
37 self.start == self.end
38 }
39
40 #[must_use]
42 pub fn contains(&self, position: CursorPosition) -> bool {
43 let (min, max) = self.normalized();
44 position >= min && position < max
45 }
46
47 #[must_use]
49 pub fn normalized(&self) -> (CursorPosition, CursorPosition) {
50 if self.start.is_before(&self.end) {
51 (self.start, self.end)
52 } else {
53 (self.end, self.start)
54 }
55 }
56
57 #[must_use]
59 pub fn overlaps(&self, other: &Self) -> bool {
60 let (self_start, self_end) = self.normalized();
61 let (other_start, other_end) = other.normalized();
62
63 !(self_end <= other_start || other_end <= self_start)
64 }
65
66 #[must_use]
68 pub fn merge(&self, other: &Self) -> Option<Self> {
69 let (self_start, self_end) = self.normalized();
70 let (other_start, other_end) = other.normalized();
71
72 if self_end >= other_start && other_end >= self_start {
74 Some(Self {
75 start: self_start.min(other_start),
76 end: self_end.max(other_end),
77 })
78 } else {
79 None
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
86pub enum SelectionMode {
87 #[default]
89 Character,
90 Word,
92 Line,
94 Block,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100#[allow(dead_code)]
101pub enum SelectionDirection {
102 Forward,
104 Backward,
106}
107
108#[must_use]
110#[allow(dead_code)]
111pub fn word_at_position(text: &str, line: usize, column: usize) -> Option<(usize, usize)> {
112 let lines: Vec<&str> = text.lines().collect();
113 let line_text = lines.get(line)?;
114
115 if column > line_text.len() {
116 return None;
117 }
118
119 let mut start = column;
121 for (i, c) in line_text[..column].char_indices().rev() {
122 if !is_word_char(c) {
123 start = i + c.len_utf8();
124 break;
125 }
126 if i == 0 {
127 start = 0;
128 }
129 }
130
131 let mut end = column;
133 for (i, c) in line_text[column..].char_indices() {
134 if !is_word_char(c) {
135 end = column + i;
136 break;
137 }
138 end = column + i + c.len_utf8();
139 }
140
141 if start == end {
142 None
143 } else {
144 Some((start, end))
145 }
146}
147
148#[allow(dead_code)]
150fn is_word_char(c: char) -> bool {
151 c.is_alphanumeric() || c == '_'
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_selection_normalized() {
160 let sel = Selection::new(CursorPosition::new(1, 5), CursorPosition::new(0, 3));
161
162 let (start, end) = sel.normalized();
163 assert_eq!(start, CursorPosition::new(0, 3));
164 assert_eq!(end, CursorPosition::new(1, 5));
165 }
166
167 #[test]
168 fn test_selection_overlaps() {
169 let a = Selection::new(CursorPosition::new(0, 0), CursorPosition::new(0, 5));
170 let b = Selection::new(CursorPosition::new(0, 3), CursorPosition::new(0, 10));
171 let c = Selection::new(CursorPosition::new(0, 6), CursorPosition::new(0, 10));
172
173 assert!(a.overlaps(&b));
174 assert!(!a.overlaps(&c));
175 }
176
177 #[test]
178 fn test_word_at_position() {
179 let text = "hello world foo_bar";
180
181 assert_eq!(word_at_position(text, 0, 2), Some((0, 5)));
182 assert_eq!(word_at_position(text, 0, 8), Some((6, 11)));
183 assert_eq!(word_at_position(text, 0, 15), Some((12, 19)));
184 }
185}