1use core::cmp::{max, min};
14use core::fmt;
15
16#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
20pub struct CharIdx(pub usize);
21
22impl CharIdx {
23 #[inline]
24 pub const fn new(v: usize) -> Self {
25 Self(v)
26 }
27
28 #[inline]
29 pub const fn get(self) -> usize {
30 self.0
31 }
32
33 #[inline]
34 pub const fn saturating_add(self, delta: usize) -> Self {
35 Self(self.0.saturating_add(delta))
36 }
37
38 #[inline]
39 pub const fn saturating_sub(self, delta: usize) -> Self {
40 Self(self.0.saturating_sub(delta))
41 }
42}
43
44impl fmt::Debug for CharIdx {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 f.debug_tuple("CharIdx").field(&self.0).finish()
47 }
48}
49
50#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
52pub struct LineIdx(pub usize);
53
54impl LineIdx {
55 #[inline]
56 pub const fn new(v: usize) -> Self {
57 Self(v)
58 }
59
60 #[inline]
61 pub const fn get(self) -> usize {
62 self.0
63 }
64}
65
66impl fmt::Debug for LineIdx {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.debug_tuple("LineIdx").field(&self.0).finish()
69 }
70}
71
72#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
74pub struct ColIdx(pub usize);
75
76impl ColIdx {
77 #[inline]
78 pub const fn new(v: usize) -> Self {
79 Self(v)
80 }
81
82 #[inline]
83 pub const fn get(self) -> usize {
84 self.0
85 }
86}
87
88impl fmt::Debug for ColIdx {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 f.debug_tuple("ColIdx").field(&self.0).finish()
91 }
92}
93
94#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
96pub struct LineCol {
97 pub line: LineIdx,
98 pub col: ColIdx,
99}
100
101impl LineCol {
102 #[inline]
103 pub const fn new(line: usize, col: usize) -> Self {
104 Self {
105 line: LineIdx(line),
106 col: ColIdx(col),
107 }
108 }
109}
110
111impl fmt::Debug for LineCol {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 f.debug_struct("LineCol")
114 .field("line", &self.line.0)
115 .field("col", &self.col.0)
116 .finish()
117 }
118}
119
120#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
127pub struct CharRange {
128 pub start: CharIdx,
129 pub end: CharIdx,
130}
131
132impl CharRange {
133 #[inline]
134 pub const fn new(start: CharIdx, end: CharIdx) -> Self {
135 Self { start, end }
136 }
137
138 #[inline]
139 pub const fn is_empty(self) -> bool {
140 self.start.0 >= self.end.0
141 }
142
143 #[inline]
144 pub const fn len(self) -> usize {
145 self.end.0.saturating_sub(self.start.0)
146 }
147
148 #[inline]
150 pub const fn normalized(self) -> Self {
151 if self.start.0 <= self.end.0 {
152 self
153 } else {
154 Self {
155 start: self.end,
156 end: self.start,
157 }
158 }
159 }
160
161 #[inline]
163 pub fn clamp_to_len(self, max_len: usize) -> Self {
164 let s = min(self.start.0, max_len);
165 let e = min(self.end.0, max_len);
166 Self {
167 start: CharIdx(s),
168 end: CharIdx(e),
169 }
170 .normalized()
171 }
172}
173
174impl fmt::Debug for CharRange {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 f.debug_struct("CharRange")
177 .field("start", &self.start.0)
178 .field("end", &self.end.0)
179 .finish()
180 }
181}
182
183#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Debug)]
192pub struct GoalCol {
193 pub goal_col: ColIdx,
194}
195
196impl GoalCol {
197 #[inline]
198 pub const fn new(goal_col: usize) -> Self {
199 Self {
200 goal_col: ColIdx(goal_col),
201 }
202 }
203}
204
205pub fn char_to_line_col(
214 char_idx: CharIdx,
215 line_count: usize,
216 mut line_to_char: impl FnMut(usize) -> usize,
217) -> LineCol {
218 if line_count == 0 {
219 return LineCol::new(0, 0);
220 }
221
222 let target = char_idx.0;
224 let mut lo = 0usize;
225 let mut hi = line_count - 1;
226
227 while lo < hi {
228 let mid = (lo + hi + 1) / 2;
230 let mid_start = line_to_char(mid);
231 if mid_start <= target {
232 lo = mid;
233 } else {
234 hi = mid - 1;
235 }
236 }
237
238 let line = lo;
239 let line_start = line_to_char(line);
240 let col = target.saturating_sub(line_start);
241
242 LineCol {
243 line: LineIdx(line),
244 col: ColIdx(col),
245 }
246}
247
248pub fn line_col_to_char(
258 pos: LineCol,
259 line_count: usize,
260 mut line_to_char: impl FnMut(usize) -> usize,
261 mut line_len_chars: impl FnMut(usize) -> usize,
262) -> CharIdx {
263 if line_count == 0 {
264 return CharIdx(0);
265 }
266
267 let line = min(pos.line.0, line_count - 1);
268 let line_start = line_to_char(line);
269 let line_len = line_len_chars(line);
270 let col = min(pos.col.0, line_len);
271
272 CharIdx(line_start.saturating_add(col))
273}
274
275#[inline]
277pub fn clamp_char(char_idx: CharIdx, len_chars: usize) -> CharIdx {
278 CharIdx(min(char_idx.0, len_chars))
279}
280
281#[inline]
283pub fn clamp_range(range: CharRange, len_chars: usize) -> CharRange {
284 range.normalized().clamp_to_len(len_chars)
285}
286
287#[inline]
289pub fn clamp_col_to_line(goal: ColIdx, line_len_chars: usize) -> ColIdx {
290 ColIdx(min(goal.0, line_len_chars))
291}
292
293#[inline]
299pub fn line_len_without_newline(
300 line_len_chars_including_newline: usize,
301 ends_with_newline: bool,
302) -> usize {
303 if ends_with_newline {
304 line_len_chars_including_newline.saturating_sub(1)
305 } else {
306 line_len_chars_including_newline
307 }
308}
309
310#[inline]
314pub fn move_char_clamped(current: CharIdx, delta: isize, len_chars: usize) -> CharIdx {
315 if delta == 0 {
316 return clamp_char(current, len_chars);
317 }
318
319 if delta > 0 {
320 let d = delta as usize;
321 CharIdx(min(current.0.saturating_add(d), len_chars))
322 } else {
323 let d = (-delta) as usize;
324 CharIdx(current.0.saturating_sub(d))
325 }
326}
327
328#[inline]
330pub fn ordered_pair(a: CharIdx, b: CharIdx) -> (CharIdx, CharIdx) {
331 if a.0 <= b.0 { (a, b) } else { (b, a) }
332}
333
334#[inline]
343pub fn apply_goal_col(goal_col: ColIdx, target_line_len: usize) -> ColIdx {
344 clamp_col_to_line(goal_col, target_line_len)
345}
346
347#[inline]
357pub fn line_editable_bounds(
358 line_start: CharIdx,
359 line_len_chars_including_newline: usize,
360 ends_with_newline: bool,
361) -> (CharIdx, CharIdx) {
362 let editable_len =
363 line_len_without_newline(line_len_chars_including_newline, ends_with_newline);
364 let start = line_start;
365 let end = CharIdx(line_start.0.saturating_add(editable_len));
366 (start, end)
367}
368
369#[inline]
373pub fn clamp_cursor_to_line_editable(
374 cursor: CharIdx,
375 line_start: CharIdx,
376 editable_end: CharIdx,
377) -> CharIdx {
378 CharIdx(max(line_start.0, min(cursor.0, editable_end.0)))
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn char_to_line_col_uses_binary_search_correctly() {
387 let line_starts = [0usize, 4, 6];
388 let line_count = line_starts.len();
389
390 let pos = char_to_line_col(CharIdx(5), line_count, |line| line_starts[line]);
391 assert_eq!(pos, LineCol::new(1, 1));
392 }
393
394 #[test]
395 fn line_col_to_char_clamps_line_and_column() {
396 let line_starts = [0usize, 4, 6];
397 let line_lens = [3usize, 1, 0];
398 let line_count = line_starts.len();
399
400 let idx = line_col_to_char(
401 LineCol::new(99, 99),
402 line_count,
403 |line| line_starts[line],
404 |line| line_lens[line],
405 );
406 assert_eq!(idx, CharIdx(6));
407 }
408
409 #[test]
410 fn clamp_helpers_keep_values_in_bounds() {
411 assert_eq!(clamp_char(CharIdx(9), 4), CharIdx(4));
412 assert_eq!(clamp_col_to_line(ColIdx(8), 3), ColIdx(3));
413 assert_eq!(apply_goal_col(ColIdx(8), 3), ColIdx(3));
414 }
415
416 #[test]
417 fn clamp_range_normalizes_and_clamps() {
418 let out = clamp_range(CharRange::new(CharIdx(9), CharIdx(2)), 5);
419 assert_eq!(out.start, CharIdx(2));
420 assert_eq!(out.end, CharIdx(5));
421 }
422
423 #[test]
424 fn line_helpers_respect_newline_exclusion() {
425 assert_eq!(line_len_without_newline(5, true), 4);
426 assert_eq!(line_len_without_newline(5, false), 5);
427
428 let (start, end) = line_editable_bounds(CharIdx(10), 4, true);
429 assert_eq!(start, CharIdx(10));
430 assert_eq!(end, CharIdx(13));
431
432 assert_eq!(
433 clamp_cursor_to_line_editable(CharIdx(20), start, end),
434 CharIdx(13)
435 );
436 assert_eq!(
437 clamp_cursor_to_line_editable(CharIdx(8), start, end),
438 CharIdx(10)
439 );
440 }
441
442 #[test]
443 fn movement_and_ordering_helpers_are_saturating() {
444 assert_eq!(move_char_clamped(CharIdx(2), -5, 10), CharIdx(0));
445 assert_eq!(move_char_clamped(CharIdx(2), 99, 10), CharIdx(10));
446 assert_eq!(
447 ordered_pair(CharIdx(9), CharIdx(3)),
448 (CharIdx(3), CharIdx(9))
449 );
450 }
451}