ailloli_ui_editor/input/
selection.rs1use std::ops::Range;
4
5use ailloli_ui_core::Rect;
6
7use crate::code::{EditorLanguage, SyntaxKind, SyntaxToken};
8use crate::layout::{first_layout_baseline, EditorTextRun};
9use crate::EditorStyle;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum SelectionGranularity {
22 Character,
24 Word,
26 Line,
28 Token,
30}
31
32pub fn selection_rects_for_run(
57 content_x: f32,
58 content_y: f32,
59 run: &EditorTextRun,
60 lo_local: usize,
61 hi_local: usize,
62 style: EditorStyle,
63) -> Vec<Rect> {
64 let text_len = run.layout.text().len();
65 let lo_local = lo_local.min(text_len);
66 let hi_local = hi_local.min(text_len);
67 if hi_local <= lo_local {
68 return Vec::new();
69 }
70
71 let first_baseline = first_layout_baseline(&run.layout);
72 let text_origin_y = run.baseline_y - first_baseline;
73 let mut out = Vec::new();
74 for line in &run.layout.lines {
75 let line_start = line.text_range.start.min(text_len);
76 let line_end = line.text_range.end.min(text_len);
77 let start = lo_local.max(line_start);
78 let end = hi_local.min(line_end);
79 if end <= start {
80 continue;
81 }
82
83 let x0 = if start <= line_start {
84 0.0
85 } else {
86 run.layout.caret_rect_at(start, 0.0).x
87 };
88 let x1 = if end >= line_end {
89 line.width
90 } else {
91 run.layout.caret_rect_at(end, 0.0).x
92 };
93 let x_left = x0.min(x1);
94 let width = (x1 - x0).abs().max(1.0);
95 let caret = run.layout.caret_rect_at(line_start, 0.0);
96 let fallback_h = style.px_size as f32 + 2.0;
97 out.push(Rect::new(
98 (content_x + x_left).round(),
99 (content_y + text_origin_y + caret.y).round(),
100 width,
101 caret.h.max(fallback_h),
102 ));
103 }
104 out
105}
106
107pub fn select_word_at(
124 text: &str,
125 byte: usize,
126 syntax_tokens: Option<&[SyntaxToken]>,
127 language: EditorLanguage,
128) -> Option<Range<usize>> {
129 if let Some(range) = syntax_tokens.and_then(|tokens| select_token_at(byte, tokens)) {
130 return Some(range);
131 }
132 lexical_word_range_at(text, byte, language)
133}
134
135pub fn select_token_at(byte: usize, syntax_tokens: &[SyntaxToken]) -> Option<Range<usize>> {
155 syntax_tokens
156 .iter()
157 .filter(|token| selectable_syntax_kind(token.kind))
158 .find(|token| byte_in_range(byte, &token.range))
159 .map(|token| token.range.clone())
160}
161
162pub fn select_line_at(text: &str, byte: usize) -> Range<usize> {
177 let byte = clamp_boundary(text, byte.min(text.len()));
178 let start = text[..byte].rfind('\n').map(|idx| idx + 1).unwrap_or(0);
179 let end = text[byte..]
180 .find('\n')
181 .map(|idx| byte + idx)
182 .unwrap_or(text.len());
183 start..end
184}
185
186fn selectable_syntax_kind(kind: SyntaxKind) -> bool {
188 matches!(
189 kind,
190 SyntaxKind::Keyword
191 | SyntaxKind::Type
192 | SyntaxKind::Function
193 | SyntaxKind::Number
194 | SyntaxKind::Operator
195 | SyntaxKind::Punctuation
196 | SyntaxKind::Identifier
197 )
198}
199
200fn byte_in_range(byte: usize, range: &Range<usize>) -> bool {
202 range.start < range.end && range.start <= byte && byte < range.end
203}
204
205fn lexical_word_range_at(
207 text: &str,
208 byte: usize,
209 language: EditorLanguage,
210) -> Option<Range<usize>> {
211 if text.is_empty() {
212 return None;
213 }
214 let mut at = clamp_boundary(text, byte.min(text.len()));
215 if at == text.len() {
216 at = previous_boundary(text, at);
217 }
218 if at >= text.len() {
219 return None;
220 }
221 let bytes = text.as_bytes();
222 if matches!(language, EditorLanguage::Rust) {
223 if let Some(range) = rust_lifetime_range_at(text, at) {
224 return Some(range);
225 }
226 if bytes.get(at) == Some(&b'#') && at > 0 && bytes.get(at - 1) == Some(&b'r') {
227 at -= 1;
228 }
229 if let Some(range) = rust_raw_identifier_range_at(text, at) {
230 return Some(range);
231 }
232 }
233 if is_ident_byte(bytes[at]) {
234 return Some(identifier_range_at(text, at));
235 }
236 if is_selectable_punctuation(bytes[at]) {
237 return Some(at..at + 1);
238 }
239 None
240}
241
242fn rust_raw_identifier_range_at(text: &str, at: usize) -> Option<Range<usize>> {
244 let bytes = text.as_bytes();
245 let mut start = at;
246 while start > 0 && is_ident_byte(bytes[start - 1]) {
247 start -= 1;
248 }
249 if start >= 2 && &bytes[start - 2..start] == b"r#" {
250 start -= 2;
251 }
252 if &bytes.get(start..start + 2)? != b"r#" {
253 return None;
254 }
255 let mut end = start + 2;
256 if !bytes.get(end).is_some_and(|byte| is_ident_start(*byte)) {
257 return None;
258 }
259 end += 1;
260 while end < bytes.len() && is_ident_byte(bytes[end]) {
261 end += 1;
262 }
263 (start <= at && at < end).then_some(start..end)
264}
265
266fn rust_lifetime_range_at(text: &str, at: usize) -> Option<Range<usize>> {
268 let bytes = text.as_bytes();
269 let start = if bytes.get(at) == Some(&b'\'') {
270 at
271 } else if at > 0 && is_ident_byte(bytes[at]) && bytes.get(at - 1) == Some(&b'\'') {
272 at - 1
273 } else {
274 return None;
275 };
276 let first = *bytes.get(start + 1)?;
277 if !is_ident_start(first) {
278 return None;
279 }
280 let mut end = start + 2;
281 while end < bytes.len() && is_ident_byte(bytes[end]) {
282 end += 1;
283 }
284 if bytes.get(end) == Some(&b'\'') {
285 return None;
286 }
287 Some(start..end)
288}
289
290fn identifier_range_at(text: &str, at: usize) -> Range<usize> {
292 let bytes = text.as_bytes();
293 let mut start = at;
294 while start > 0 && is_ident_byte(bytes[start - 1]) {
295 start -= 1;
296 }
297 let mut end = at + 1;
298 while end < bytes.len() && is_ident_byte(bytes[end]) {
299 end += 1;
300 }
301 start..end
302}
303
304fn is_ident_start(byte: u8) -> bool {
306 byte == b'_' || byte.is_ascii_alphabetic()
307}
308
309fn is_ident_byte(byte: u8) -> bool {
311 is_ident_start(byte) || byte.is_ascii_digit()
312}
313
314fn is_selectable_punctuation(byte: u8) -> bool {
316 matches!(
317 byte,
318 b'!' | b'%'
319 | b'&'
320 | b'*'
321 | b'+'
322 | b'-'
323 | b'.'
324 | b'/'
325 | b':'
326 | b';'
327 | b'<'
328 | b'='
329 | b'>'
330 | b'?'
331 | b'|'
332 | b'('
333 | b')'
334 | b'['
335 | b']'
336 | b'{'
337 | b'}'
338 | b','
339 | b'#'
340 )
341}
342
343fn clamp_boundary(text: &str, mut byte: usize) -> usize {
345 byte = byte.min(text.len());
346 while byte > 0 && !text.is_char_boundary(byte) {
347 byte -= 1;
348 }
349 byte
350}
351
352fn previous_boundary(text: &str, byte: usize) -> usize {
354 let mut byte = clamp_boundary(text, byte);
355 if byte == 0 {
356 return 0;
357 }
358 byte -= 1;
359 clamp_boundary(text, byte)
360}