1use crate::utils::calculate_indentation_width_default;
4use std::borrow::Cow;
5use std::collections::HashSet;
6use std::ops::Range;
7
8fn find_char_boundary(s: &str, byte_idx: usize) -> usize {
15 if byte_idx >= s.len() {
16 return s.len();
17 }
18
19 if s.is_char_boundary(byte_idx) {
21 return byte_idx;
22 }
23
24 let mut pos = byte_idx;
27 while pos > 0 && !s.is_char_boundary(pos) {
28 pos -= 1;
29 }
30 pos
31}
32
33pub(crate) fn byte_to_char_count(s: &str, byte_idx: usize) -> usize {
41 let safe_byte_idx = find_char_boundary(s, byte_idx);
42 s[..safe_byte_idx].chars().count() + 1 }
44
45#[derive(Debug)]
46pub struct LineIndex<'a> {
47 line_starts: Cow<'a, [usize]>,
48 content: &'a str,
49 code_block_lines: Option<HashSet<usize>>,
50}
51
52impl<'a> LineIndex<'a> {
53 pub fn new(content: &'a str) -> Self {
54 let mut line_starts = vec![0];
55 let mut pos = 0;
56
57 for c in content.chars() {
58 pos += c.len_utf8();
59 if c == '\n' {
60 line_starts.push(pos);
61 }
62 }
63
64 let mut index = Self {
65 line_starts: Cow::Owned(line_starts),
66 content,
67 code_block_lines: None,
68 };
69
70 index.compute_code_block_lines();
72
73 index
74 }
75
76 pub fn with_line_starts(content: &'a str, line_starts: Vec<usize>) -> Self {
80 let mut index = Self {
81 line_starts: Cow::Owned(line_starts),
82 content,
83 code_block_lines: None,
84 };
85
86 index.compute_code_block_lines();
88
89 index
90 }
91
92 pub fn with_line_starts_and_code_blocks(
97 content: &'a str,
98 line_starts: Vec<usize>,
99 code_block_byte_ranges: &[(usize, usize)],
100 ) -> Self {
101 let mut code_block_lines = HashSet::new();
102
103 for &(block_start, block_end) in code_block_byte_ranges {
104 let start_line = match line_starts.binary_search(&block_start) {
105 Ok(idx) => idx,
106 Err(idx) => idx.saturating_sub(1),
107 };
108 let end_line = if block_end == 0 {
109 0
110 } else {
111 match line_starts.binary_search(&block_end) {
112 Ok(idx) => idx.saturating_sub(1),
114 Err(idx) => idx.saturating_sub(1),
115 }
116 };
117 for line_idx in start_line..=end_line {
118 code_block_lines.insert(line_idx);
119 }
120 }
121
122 Self {
123 line_starts: Cow::Owned(line_starts),
124 content,
125 code_block_lines: Some(code_block_lines),
126 }
127 }
128
129 #[inline]
133 pub(crate) fn for_positions(content: &'a str, line_starts: &'a [usize]) -> Self {
134 Self {
135 line_starts: Cow::Borrowed(line_starts),
136 content,
137 code_block_lines: None,
138 }
139 }
140
141 fn get_line(&self, line_idx: usize) -> Option<&'a str> {
144 let start = *self.line_starts.get(line_idx)?;
145 let end = self
146 .line_starts
147 .get(line_idx + 1)
148 .copied()
149 .unwrap_or(self.content.len());
150 let line = &self.content[start..end];
151 let line = line.strip_suffix('\n').unwrap_or(line);
153 let line = line.strip_suffix('\r').unwrap_or(line);
154 Some(line)
155 }
156
157 pub fn line_col_to_byte_range(&self, line: usize, column: usize) -> Range<usize> {
158 let line = line.saturating_sub(1);
159 let line_start = *self.line_starts.get(line).unwrap_or(&self.content.len());
160
161 let current_line = self.get_line(line).unwrap_or("");
162 let char_col = column.saturating_sub(1);
164 let char_count = current_line.chars().count();
165 let safe_char_col = char_col.min(char_count);
166
167 let byte_offset = current_line
169 .char_indices()
170 .nth(safe_char_col)
171 .map_or(current_line.len(), |(idx, _)| idx);
172
173 let start = line_start + byte_offset;
174 start..start
175 }
176
177 pub fn line_col_to_byte_range_with_length(&self, line: usize, column: usize, length: usize) -> Range<usize> {
184 let line = line.saturating_sub(1);
185 let line_start = *self.line_starts.get(line).unwrap_or(&self.content.len());
186 let line_end = self.line_starts.get(line + 1).copied().unwrap_or(self.content.len());
187 let mut current_line = &self.content[line_start..line_end];
188 if let Some(stripped) = current_line.strip_suffix('\n') {
189 current_line = stripped.strip_suffix('\r').unwrap_or(stripped);
190 }
191 if current_line.is_ascii() {
192 let line_len = current_line.len();
193 let start_byte = column.saturating_sub(1).min(line_len);
194 let end_byte = start_byte.saturating_add(length).min(line_len);
195 let start = line_start + start_byte;
196 let end = line_start + end_byte;
197 return start..end;
198 }
199 let char_col = column.saturating_sub(1);
201 let char_count = current_line.chars().count();
202 let safe_char_col = char_col.min(char_count);
203
204 let mut char_indices = current_line.char_indices();
206 let start_byte = char_indices
207 .nth(safe_char_col)
208 .map_or(current_line.len(), |(idx, _)| idx);
209
210 let end_char_col = (safe_char_col + length).min(char_count);
212 let end_byte = current_line
213 .char_indices()
214 .nth(end_char_col)
215 .map_or(current_line.len(), |(idx, _)| idx);
216
217 let start = line_start + start_byte;
218 let end = line_start + end_byte;
219 start..end
220 }
221
222 pub fn whole_line_range(&self, line: usize) -> Range<usize> {
225 let line_idx = line.saturating_sub(1);
226 let start = *self.line_starts.get(line_idx).unwrap_or(&self.content.len());
227 let end = self
228 .line_starts
229 .get(line_idx + 1)
230 .copied()
231 .unwrap_or(self.content.len());
232 start..end
233 }
234
235 pub fn multi_line_range(&self, start_line: usize, end_line: usize) -> Range<usize> {
238 let start_idx = start_line.saturating_sub(1);
239 let end_idx = end_line.saturating_sub(1);
240
241 let start = *self.line_starts.get(start_idx).unwrap_or(&self.content.len());
242 let end = self.line_starts.get(end_idx + 1).copied().unwrap_or(self.content.len());
243 start..end
244 }
245
246 pub fn line_text_range(&self, line: usize, start_col: usize, end_col: usize) -> Range<usize> {
253 let line_idx = line.saturating_sub(1);
254 let line_start = *self.line_starts.get(line_idx).unwrap_or(&self.content.len());
255
256 let current_line = self.get_line(line_idx).unwrap_or("");
258 let char_count = current_line.chars().count();
259
260 let start_char_col = start_col.saturating_sub(1).min(char_count);
262 let end_char_col = end_col.saturating_sub(1).min(char_count);
263
264 let mut char_indices = current_line.char_indices();
265 let start_byte = char_indices
266 .nth(start_char_col)
267 .map_or(current_line.len(), |(idx, _)| idx);
268
269 let end_byte = current_line
270 .char_indices()
271 .nth(end_char_col)
272 .map_or(current_line.len(), |(idx, _)| idx);
273
274 let start = line_start + start_byte;
275 let end = line_start + end_byte.max(start_byte);
276 start..end
277 }
278
279 pub fn line_content_range(&self, line: usize) -> Range<usize> {
282 let line_idx = line.saturating_sub(1);
283 let line_start = *self.line_starts.get(line_idx).unwrap_or(&self.content.len());
284
285 let current_line = self.get_line(line_idx).unwrap_or("");
286 let line_end = line_start + current_line.len();
287 line_start..line_end
288 }
289
290 pub fn get_line_start_byte(&self, line_num: usize) -> Option<usize> {
292 if line_num == 0 {
293 return None; }
295 self.line_starts.get(line_num - 1).copied()
297 }
298
299 pub fn is_code_block(&self, line: usize) -> bool {
301 if let Some(ref code_block_lines) = self.code_block_lines {
302 code_block_lines.contains(&line)
303 } else {
304 self.is_code_fence(line)
306 }
307 }
308
309 pub fn is_code_fence(&self, line: usize) -> bool {
311 self.get_line(line).is_some_and(|l| {
312 let trimmed = l.trim();
313 trimmed.starts_with("```") || trimmed.starts_with("~~~")
314 })
315 }
316
317 pub fn is_tilde_code_block(&self, line: usize) -> bool {
319 self.get_line(line).is_some_and(|l| l.trim().starts_with("~~~"))
320 }
321
322 pub fn get_content(&self) -> &str {
324 self.content
325 }
326
327 fn compute_code_block_lines(&mut self) {
329 let mut code_block_lines = HashSet::new();
330 let lines: Vec<&str> = self.content.lines().collect();
331
332 let mut in_block = false;
334 let mut active_fence_type = ' '; let mut block_indent = 0;
336 let mut block_fence_length = 0;
337 let mut in_markdown_block = false;
338 let mut nested_fence_start = None;
339 let mut nested_fence_end = None;
340
341 for (i, line) in lines.iter().enumerate() {
343 let trimmed = line.trim();
344 let indent = line.len() - trimmed.len();
345
346 if calculate_indentation_width_default(line) >= 4 {
348 code_block_lines.insert(i);
349 continue; }
351
352 if !in_block {
354 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
356 let char_type = if trimmed.starts_with("```") { '`' } else { '~' };
357 let count = trimmed.chars().take_while(|&c| c == char_type).count();
358 let info_string = if trimmed.len() > count {
359 trimmed[count..].trim()
360 } else {
361 ""
362 };
363
364 in_block = true;
366 active_fence_type = char_type;
367 block_indent = indent;
368 block_fence_length = count;
369 in_markdown_block = info_string == "markdown";
370 nested_fence_start = None;
371 nested_fence_end = None;
372
373 code_block_lines.insert(i);
374 }
375 } else {
376 code_block_lines.insert(i);
378
379 if in_markdown_block && nested_fence_start.is_none() && trimmed.starts_with("```") {
381 let count = trimmed.chars().take_while(|&c| c == '`').count();
383 let remaining = if trimmed.len() > count {
384 trimmed[count..].trim()
385 } else {
386 ""
387 };
388
389 if !remaining.is_empty() {
390 nested_fence_start = Some(i);
391 }
392 }
393
394 if in_markdown_block
396 && nested_fence_start.is_some()
397 && nested_fence_end.is_none()
398 && trimmed.starts_with("```")
399 && trimmed.trim_start_matches('`').trim().is_empty()
400 {
401 nested_fence_end = Some(i);
402 }
403
404 if trimmed.starts_with(&active_fence_type.to_string().repeat(3)) {
406 let count = trimmed.chars().take_while(|&c| c == active_fence_type).count();
407 let remaining = if trimmed.len() > count {
408 trimmed[count..].trim()
409 } else {
410 ""
411 };
412
413 let is_valid_closing_fence =
419 count >= block_fence_length && remaining.is_empty() && indent <= block_indent;
420
421 let is_nested_closing = nested_fence_end.is_some() && i == nested_fence_end.unwrap();
424
425 if is_valid_closing_fence && !is_nested_closing {
427 in_block = false;
428 in_markdown_block = false;
429 }
430 }
431 }
432 }
433
434 self.code_block_lines = Some(code_block_lines);
435 }
436}
437
438pub fn calculate_single_line_range(line: usize, start_col: usize, length: usize) -> (usize, usize, usize, usize) {
440 (line, start_col, line, start_col + length)
441}
442
443pub fn calculate_line_range(line: usize, line_content: &str) -> (usize, usize, usize, usize) {
448 let trimmed_char_len = line_content.trim_end().chars().count();
449 (line, 1, line, trimmed_char_len + 1)
450}
451
452pub fn calculate_match_range(
461 line: usize,
462 line_content: &str,
463 match_start_byte: usize,
464 match_len_bytes: usize,
465) -> (usize, usize, usize, usize) {
466 let line_len = line_content.len();
467 if match_start_byte > line_len {
468 let char_count = line_content.chars().count();
470 return (line, char_count + 1, line, char_count + 1);
471 }
472
473 let safe_match_start = find_char_boundary(line_content, match_start_byte);
475 let safe_match_end_byte = find_char_boundary(line_content, (match_start_byte + match_len_bytes).min(line_len));
476
477 let char_start = byte_to_char_count(line_content, safe_match_start);
479 let char_len = if safe_match_end_byte > safe_match_start {
480 line_content[safe_match_start..safe_match_end_byte].chars().count()
482 } else {
483 0
484 };
485 (line, char_start, line, char_start + char_len)
486}
487
488pub fn calculate_trailing_range(line: usize, line_content: &str, content_end: usize) -> (usize, usize, usize, usize) {
494 let safe_content_end = find_char_boundary(line_content, content_end);
496 let char_content_end = byte_to_char_count(line_content, safe_content_end);
497 let line_char_len = line_content.chars().count() + 1;
498 (line, char_content_end, line, line_char_len)
499}
500
501pub fn calculate_heading_range(
508 first_line: usize,
509 last_line: usize,
510 last_line_content: &str,
511) -> (usize, usize, usize, usize) {
512 let trimmed_char_len = last_line_content.trim_end().chars().count();
513 (first_line, 1, last_line, trimmed_char_len + 1)
514}
515
516pub fn calculate_emphasis_range(
522 line: usize,
523 line_content: &str,
524 start_pos: usize,
525 end_pos: usize,
526) -> (usize, usize, usize, usize) {
527 let safe_start_pos = find_char_boundary(line_content, start_pos);
529 let safe_end_pos = find_char_boundary(line_content, end_pos);
530 let char_start = byte_to_char_count(line_content, safe_start_pos);
531 let char_end = byte_to_char_count(line_content, safe_end_pos);
532 (line, char_start, line, char_end)
533}
534
535pub fn calculate_html_tag_range(
537 line: usize,
538 line_content: &str,
539 tag_start: usize,
540 tag_len: usize,
541) -> (usize, usize, usize, usize) {
542 calculate_match_range(line, line_content, tag_start, tag_len)
543}
544
545pub fn calculate_url_range(
547 line: usize,
548 line_content: &str,
549 url_start: usize,
550 url_len: usize,
551) -> (usize, usize, usize, usize) {
552 calculate_match_range(line, line_content, url_start, url_len)
553}
554
555pub fn calculate_list_marker_range(
557 line: usize,
558 line_content: &str,
559 marker_start: usize,
560 marker_len: usize,
561) -> (usize, usize, usize, usize) {
562 calculate_match_range(line, line_content, marker_start, marker_len)
563}
564
565pub fn calculate_excess_range(line: usize, line_content: &str, limit: usize) -> (usize, usize, usize, usize) {
567 let char_limit = std::cmp::min(limit, line_content.chars().count());
568 let line_char_len = line_content.chars().count() + 1;
569 (line, char_limit + 1, line, line_char_len)
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn borrowed_positions_match_owned_index_at_unicode_and_line_boundaries() {
578 for content in ["", "a", "a\n", "\r\n\r", "Γ©π¦\r\n# heading\nε°Ύ\r", "\n\n"] {
579 let starts: Vec<_> = std::iter::once(0)
580 .chain(content.match_indices('\n').map(|(offset, _)| offset + 1))
581 .collect();
582 let owned = LineIndex::with_line_starts_and_code_blocks(content, starts.clone(), &[(0, content.len())]);
583 let borrowed = LineIndex::for_positions(content, &starts);
584 assert!(matches!(borrowed.line_starts, Cow::Borrowed(_)));
585 for line in 0..starts.len() + 3 {
586 assert_eq!(borrowed.get_line_start_byte(line), owned.get_line_start_byte(line));
587 assert_eq!(borrowed.whole_line_range(line), owned.whole_line_range(line));
588 assert_eq!(borrowed.line_content_range(line), owned.line_content_range(line));
589 for column in 0..12 {
590 assert_eq!(
591 borrowed.line_col_to_byte_range(line, column),
592 owned.line_col_to_byte_range(line, column)
593 );
594 for length in [0, 1, 4, 20] {
595 assert_eq!(
596 borrowed.line_col_to_byte_range_with_length(line, column, length),
597 owned.line_col_to_byte_range_with_length(line, column, length)
598 );
599 assert_eq!(
600 borrowed.line_text_range(line, column, length),
601 owned.line_text_range(line, column, length)
602 );
603 }
604 }
605 for last in line..starts.len() + 3 {
606 assert_eq!(
607 borrowed.multi_line_range(line, last),
608 owned.multi_line_range(line, last)
609 );
610 }
611 }
612 }
613 }
614
615 #[test]
616 fn test_single_line_range() {
617 let (start_line, start_col, end_line, end_col) = calculate_single_line_range(5, 10, 3);
618 assert_eq!(start_line, 5);
619 assert_eq!(start_col, 10);
620 assert_eq!(end_line, 5);
621 assert_eq!(end_col, 13);
622 }
623
624 #[test]
625 fn test_heading_range_over_one_line() {
626 let (start_line, start_col, end_line, end_col) = calculate_heading_range(4, 4, "# A heading ");
627 assert_eq!((start_line, start_col, end_line, end_col), (4, 1, 4, 12));
628 }
629
630 #[test]
631 fn test_heading_range_over_several_lines() {
632 let (start_line, start_col, end_line, end_col) = calculate_heading_range(4, 6, "second line ");
636 assert_eq!((start_line, start_col, end_line, end_col), (4, 1, 6, 12));
637 }
638
639 #[test]
640 fn test_line_range() {
641 let content = "# This is a heading ";
642 let (start_line, start_col, end_line, end_col) = calculate_line_range(1, content);
643 assert_eq!(start_line, 1);
644 assert_eq!(start_col, 1);
645 assert_eq!(end_line, 1);
646 assert_eq!(end_col, 20); }
648
649 #[test]
650 fn test_line_range_non_ascii() {
651 let content = "δ½ ε₯½ heading ";
655 let (_, start_col, _, end_col) = calculate_line_range(1, content);
656 assert_eq!(start_col, 1);
657 assert_eq!(end_col, 11);
658 }
659
660 #[test]
661 fn test_match_range() {
662 let content = "Text <div>content</div> more";
663 let tag_start = 5; let tag_len = 5; let (start_line, start_col, end_line, end_col) = calculate_match_range(1, content, tag_start, tag_len);
666 assert_eq!(start_line, 1);
667 assert_eq!(start_col, 6); assert_eq!(end_line, 1);
669 assert_eq!(end_col, 11); }
671
672 #[test]
673 fn test_trailing_range() {
674 let content = "Text content "; let content_end = 12; let (start_line, start_col, end_line, end_col) = calculate_trailing_range(1, content, content_end);
677 assert_eq!(start_line, 1);
678 assert_eq!(start_col, 13); assert_eq!(end_line, 1);
680 assert_eq!(end_col, 16); }
682
683 #[test]
684 fn test_excess_range() {
685 let content = "This line is too long for the limit";
686 let limit = 20;
687 let (start_line, start_col, end_line, end_col) = calculate_excess_range(1, content, limit);
688 assert_eq!(start_line, 1);
689 assert_eq!(start_col, 21); assert_eq!(end_line, 1);
691 assert_eq!(end_col, 36); }
693
694 #[test]
695 fn test_whole_line_range() {
696 let content = "Line 1\nLine 2\nLine 3";
697 let line_index = LineIndex::new(content);
698
699 let range = line_index.whole_line_range(1);
701 assert_eq!(range, 0..7); let range = line_index.whole_line_range(2);
705 assert_eq!(range, 7..14); let range = line_index.whole_line_range(3);
709 assert_eq!(range, 14..20); }
711
712 #[test]
713 fn test_line_content_range() {
714 let content = "Line 1\nLine 2\nLine 3";
715 let line_index = LineIndex::new(content);
716
717 let range = line_index.line_content_range(1);
719 assert_eq!(range, 0..6); let range = line_index.line_content_range(2);
723 assert_eq!(range, 7..13); let range = line_index.line_content_range(3);
727 assert_eq!(range, 14..20); }
729
730 #[test]
731 fn test_line_text_range() {
732 let content = "Hello world\nAnother line";
733 let line_index = LineIndex::new(content);
734
735 let range = line_index.line_text_range(1, 1, 5); assert_eq!(range, 0..4);
738
739 let range = line_index.line_text_range(2, 1, 7); assert_eq!(range, 12..18);
742
743 let range = line_index.line_text_range(1, 1, 100); assert_eq!(range, 0..11); }
747
748 #[test]
749 fn test_calculate_match_range_bounds_checking() {
750 let line_content = "] not a link [";
752 let (line, start_col, end_line, end_col) = calculate_match_range(121, line_content, 57, 10);
753 assert_eq!(line, 121);
754 assert_eq!(start_col, 15); assert_eq!(end_line, 121);
756 assert_eq!(end_col, 15); let line_content = "short";
760 let (line, start_col, end_line, end_col) = calculate_match_range(1, line_content, 2, 10);
761 assert_eq!(line, 1);
762 assert_eq!(start_col, 3); assert_eq!(end_line, 1);
764 assert_eq!(end_col, 6); let line_content = "normal text here";
768 let (line, start_col, end_line, end_col) = calculate_match_range(5, line_content, 7, 4);
769 assert_eq!(line, 5);
770 assert_eq!(start_col, 8); assert_eq!(end_line, 5);
772 assert_eq!(end_col, 12); let line_content = "test line";
776 let (line, start_col, end_line, end_col) = calculate_match_range(10, line_content, 5, 0);
777 assert_eq!(line, 10);
778 assert_eq!(start_col, 6); assert_eq!(end_line, 10);
780 assert_eq!(end_col, 6); }
782
783 #[test]
788 fn test_issue_154_korean_character_boundary() {
789 let line_content = "- 2023 λ
μ΄ μ΄ν μ£Όκ° μμΉ +1,000% (10 λ°° μμΉ) ";
792
793 let (line, start_col, end_line, end_col) = calculate_match_range(1, line_content, 19, 1);
796
797 assert!(start_col > 0);
799 assert_eq!(line, 1);
800 assert_eq!(end_line, 1);
801 assert!(end_col >= start_col);
802 }
803
804 #[test]
805 fn test_calculate_match_range_korean() {
806 let line_content = "μλ
νμΈμ";
809 let (line, start_col, end_line, end_col) = calculate_match_range(1, line_content, 3, 3);
811 assert_eq!(line, 1);
812 assert_eq!(start_col, 2); assert_eq!(end_line, 1);
814 assert_eq!(end_col, 3); let (line, start_col, end_line, _end_col) = calculate_match_range(1, line_content, 4, 3);
818 assert_eq!(line, 1);
819 assert_eq!(start_col, 2); assert_eq!(end_line, 1);
821 }
822
823 #[test]
824 fn test_calculate_match_range_chinese() {
825 let line_content = "δ½ ε₯½δΈη";
828 let (line, start_col, end_line, end_col) = calculate_match_range(1, line_content, 6, 3);
830 assert_eq!(line, 1);
831 assert_eq!(start_col, 3); assert_eq!(end_line, 1);
833 assert_eq!(end_col, 4); }
835
836 #[test]
837 fn test_calculate_match_range_japanese() {
838 let line_content = "γγγ«γ‘γ―";
841 let (line, start_col, end_line, end_col) = calculate_match_range(1, line_content, 9, 3);
843 assert_eq!(line, 1);
844 assert_eq!(start_col, 4); assert_eq!(end_line, 1);
846 assert_eq!(end_col, 5); }
848
849 #[test]
850 fn test_calculate_match_range_mixed_unicode() {
851 let line_content = "Hello δΈη";
856
857 let (line, start_col, end_line, end_col) = calculate_match_range(1, line_content, 5, 1);
859 assert_eq!(line, 1);
860 assert_eq!(start_col, 6); assert_eq!(end_line, 1);
862 assert_eq!(end_col, 7); let (line, start_col, end_line, end_col) = calculate_match_range(1, line_content, 6, 3);
866 assert_eq!(line, 1);
867 assert_eq!(start_col, 7); assert_eq!(end_line, 1);
869 assert_eq!(end_col, 8); }
871
872 #[test]
873 fn test_calculate_trailing_range_korean() {
874 let line_content = "μλ
νμΈμ ";
876 let (line, start_col, end_line, end_col) = calculate_trailing_range(1, line_content, 15);
878 assert_eq!(line, 1);
879 assert!(start_col > 0);
880 assert_eq!(end_line, 1);
881 assert!(end_col > start_col);
882 }
883
884 #[test]
885 fn test_calculate_emphasis_range_chinese() {
886 let line_content = "θΏζ―**ιθ¦**η";
888 let (line, start_col, end_line, end_col) = calculate_emphasis_range(1, line_content, 6, 12);
890 assert_eq!(line, 1);
891 assert!(start_col > 0);
892 assert_eq!(end_line, 1);
893 assert!(end_col > start_col);
894 }
895
896 #[test]
897 fn test_line_col_to_byte_range_korean() {
898 let content = "μλ
νμΈμ\nWorld";
900 let line_index = LineIndex::new(content);
901
902 let range = line_index.line_col_to_byte_range(1, 1);
904 assert_eq!(range, 0..0);
905
906 let range = line_index.line_col_to_byte_range(1, 2);
908 assert_eq!(range, 3..3); let range = line_index.line_col_to_byte_range(1, 3);
912 assert_eq!(range, 6..6); }
914
915 #[test]
916 fn test_line_col_to_byte_range_with_length_chinese() {
917 let content = "δ½ ε₯½δΈη\nTest";
919 let line_index = LineIndex::new(content);
920
921 let range = line_index.line_col_to_byte_range_with_length(1, 1, 2);
923 assert_eq!(range, 0..6); let range = line_index.line_col_to_byte_range_with_length(1, 2, 1);
927 assert_eq!(range, 3..6); }
929
930 #[test]
931 fn test_line_text_range_japanese() {
932 let content = "γγγ«γ‘γ―\nHello";
934 let line_index = LineIndex::new(content);
935
936 let range = line_index.line_text_range(1, 2, 4);
938 assert_eq!(range, 3..9); }
940
941 #[test]
942 fn test_find_char_boundary_edge_cases() {
943 let s = "μλ
";
945
946 assert_eq!(find_char_boundary(s, 0), 0);
948
949 assert_eq!(find_char_boundary(s, 1), 0);
951
952 assert_eq!(find_char_boundary(s, 2), 0);
954
955 assert_eq!(find_char_boundary(s, 3), 3);
957
958 assert_eq!(find_char_boundary(s, 4), 3);
960
961 assert_eq!(find_char_boundary(s, 100), s.len());
963 }
964
965 #[test]
966 fn test_byte_to_char_count_unicode() {
967 let s = "μλ
νμΈμ";
969
970 assert_eq!(byte_to_char_count(s, 0), 1);
972
973 assert_eq!(byte_to_char_count(s, 3), 2);
975
976 assert_eq!(byte_to_char_count(s, 6), 3);
978
979 assert_eq!(byte_to_char_count(s, 9), 4);
981
982 assert_eq!(byte_to_char_count(s, 12), 5);
984
985 assert_eq!(byte_to_char_count(s, 15), 6);
987 }
988
989 #[test]
990 fn test_all_range_functions_with_emoji() {
991 let line_content = "Hello π World π";
993
994 let (line, start_col, end_line, end_col) = calculate_match_range(1, line_content, 6, 4);
996 assert_eq!(line, 1);
997 assert!(start_col > 0);
998 assert_eq!(end_line, 1);
999 assert!(end_col > start_col);
1000
1001 let (line, start_col, end_line, end_col) = calculate_trailing_range(1, line_content, 12);
1003 assert_eq!(line, 1);
1004 assert!(start_col > 0);
1005 assert_eq!(end_line, 1);
1006 assert!(end_col > start_col);
1007
1008 let (line, start_col, end_line, end_col) = calculate_emphasis_range(1, line_content, 0, 5);
1010 assert_eq!(line, 1);
1011 assert_eq!(start_col, 1);
1012 assert_eq!(end_line, 1);
1013 assert!(end_col > start_col);
1014 }
1015}