rgpui 0.1.0

GUI UI framework
Documentation
use crate::HashMap;
use std::{cmp::Ordering, ops::Range};

/// 使用包含嵌入位置标记的单个字符串
/// 构造字符串和该字符串中的偏移量列表。
pub fn marked_text_offsets_by(
    marked_text: &str,
    markers: Vec<char>,
) -> (String, HashMap<char, Vec<usize>>) {
    let mut extracted_markers: HashMap<char, Vec<usize>> = Default::default();
    let mut unmarked_text = String::new();

    for char in marked_text.chars() {
        if markers.contains(&char) {
            let char_offsets = extracted_markers.entry(char).or_default();
            char_offsets.push(unmarked_text.len());
        } else {
            unmarked_text.push(char);
        }
    }

    (unmarked_text, extracted_markers)
}

/// 使用包含嵌入范围标记的单个字符串
/// 构造字符串和该字符串中的范围列表,使用任意字符作为
/// 范围标记。通过使用多种不同的范围标记字符,你可以构造
/// 相互重叠的范围。
///
/// 返回的范围将按其范围标记字符分组。
pub fn marked_text_ranges_by(
    marked_text: &str,
    markers: Vec<TextRangeMarker>,
) -> (String, HashMap<TextRangeMarker, Vec<Range<usize>>>) {
    let all_markers = markers.iter().flat_map(|m| m.markers()).collect();

    let (unmarked_text, mut marker_offsets) = marked_text_offsets_by(marked_text, all_markers);
    let range_lookup = markers
        .into_iter()
        .map(|marker| {
            (
                marker.clone(),
                match marker {
                    TextRangeMarker::Empty(empty_marker_char) => marker_offsets
                        .remove(&empty_marker_char)
                        .unwrap_or_default()
                        .into_iter()
                        .map(|empty_index| empty_index..empty_index)
                        .collect::<Vec<Range<usize>>>(),
                    TextRangeMarker::Range(start_marker, end_marker) => {
                        let starts = marker_offsets.remove(&start_marker).unwrap_or_default();
                        let ends = marker_offsets.remove(&end_marker).unwrap_or_default();
                        assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
                        starts
                            .into_iter()
                            .zip(ends)
                            .map(|(start, end)| {
                                assert!(end >= start, "marked ranges must be disjoint");
                                start..end
                            })
                            .collect::<Vec<Range<usize>>>()
                    }
                    TextRangeMarker::ReverseRange(start_marker, end_marker) => {
                        let starts = marker_offsets.remove(&start_marker).unwrap_or_default();
                        let ends = marker_offsets.remove(&end_marker).unwrap_or_default();
                        assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced");
                        starts
                            .into_iter()
                            .zip(ends)
                            .map(|(start, end)| {
                                assert!(end >= start, "marked ranges must be disjoint");
                                end..start
                            })
                            .collect::<Vec<Range<usize>>>()
                    }
                },
            )
        })
        .collect();

    (unmarked_text, range_lookup)
}

/// 使用包含嵌入范围标记的单个字符串
/// 构造字符串和该字符串中的范围列表。用于标记范围的字符如下:
///
/// 1. 要标记一段文本范围,用 `«` 和 `»` 尖括号包围它,
///    可在 US 键盘上使用 `alt-|` 和 `alt-shift-|` 键输入。
///
///    ```text
///    foo «selected text» bar
///    ```
///
/// 2. 要标记文本中的单个位置,使用 `ˇ` 抑扬符,
///    可在 US 键盘上使用 `alt-shift-t` 键输入。
///
///    ```text
///    the cursors are hereˇ and hereˇ.
///    ```
///
/// 3. 要标记方向有意义的范围(如选区),
///    在其中一个边界旁边放置一个抑扬符,在内部:
///
///    ```text
///    one «ˇreversed» selection and one «forwardˇ» selection
///    ```
///
/// 输入字符串中的任何 • 字符将被替换为空格。这使得
/// 测试尾部空格的情况更容易,这些空格通常会被从
/// 源代码中修剪掉。
#[track_caller]
pub fn marked_text_ranges(
    marked_text: &str,
    ranges_are_directed: bool,
) -> (String, Vec<Range<usize>>) {
    let mut unmarked_text = String::with_capacity(marked_text.len());
    let mut ranges = Vec::new();
    let mut prev_marked_ix = 0;
    let mut current_range_start = None;
    let mut current_range_cursor = None;

    let marked_text = marked_text.replace('', " ");
    for (marked_ix, marker) in marked_text.match_indices(&['«', '»', 'ˇ']) {
        unmarked_text.push_str(&marked_text[prev_marked_ix..marked_ix]);
        let unmarked_len = unmarked_text.len();
        let len = marker.len();
        prev_marked_ix = marked_ix + len;

        match marker {
            "ˇ" => {
                if current_range_start.is_some() {
                    if current_range_cursor.is_some() {
                        panic!("重复的点标记 'ˇ' 在索引 {marked_ix}");
                    }

                    current_range_cursor = Some(unmarked_len);
                } else {
                    ranges.push(unmarked_len..unmarked_len);
                }
            }
            "«" => {
                if current_range_start.is_some() {
                    panic!("意外的范围起始标记 '«' 在索引 {marked_ix}");
                }
                current_range_start = Some(unmarked_len);
            }
            "»" => {
                let current_range_start = if let Some(start) = current_range_start.take() {
                    start
                } else {
                    panic!("意外的范围结束标记 '»' 在索引 {marked_ix}");
                };

                let mut reversed = false;
                if let Some(current_range_cursor) = current_range_cursor.take() {
                    if current_range_cursor == current_range_start {
                        reversed = true;
                    } else if current_range_cursor != unmarked_len {
                        panic!("范围中间有意外的 'ˇ' 标记");
                    }
                } else if ranges_are_directed {
                    panic!("缺少 'ˇ' 标记以指示范围方向");
                }

                ranges.push(if reversed {
                    unmarked_len..current_range_start
                } else {
                    current_range_start..unmarked_len
                });
            }
            _ => unreachable!(),
        }
    }

    unmarked_text.push_str(&marked_text[prev_marked_ix..]);
    (unmarked_text, ranges)
}

#[track_caller]
pub fn marked_text_offsets(marked_text: &str) -> (String, Vec<usize>) {
    let (text, ranges) = marked_text_ranges(marked_text, false);
    (
        text,
        ranges
            .into_iter()
            .map(|range| {
                assert_eq!(range.start, range.end);
                range.start
            })
            .collect(),
    )
}

pub fn generate_marked_text(
    unmarked_text: &str,
    ranges: &[Range<usize>],
    indicate_cursors: bool,
) -> String {
    let mut marked_text = unmarked_text.to_string();
    for range in ranges.iter().rev() {
        if indicate_cursors {
            match range.start.cmp(&range.end) {
                Ordering::Less => {
                    marked_text.insert_str(range.end, "ˇ»");
                    marked_text.insert(range.start, '«');
                }
                Ordering::Equal => {
                    marked_text.insert(range.start, 'ˇ');
                }
                Ordering::Greater => {
                    marked_text.insert(range.start, '»');
                    marked_text.insert_str(range.end, "«ˇ");
                }
            }
        } else {
            match range.start.cmp(&range.end) {
                Ordering::Equal => {
                    marked_text.insert(range.start, 'ˇ');
                }
                _ => {
                    marked_text.insert(range.end, '»');
                    marked_text.insert(range.start, '«');
                }
            }
        }
    }
    marked_text
}

#[derive(Clone, Eq, PartialEq, Hash)]
pub enum TextRangeMarker {
    Empty(char),
    Range(char, char),
    ReverseRange(char, char),
}

impl TextRangeMarker {
    fn markers(&self) -> Vec<char> {
        match self {
            Self::Empty(m) => vec![*m],
            Self::Range(l, r) => vec![*l, *r],
            Self::ReverseRange(l, r) => vec![*l, *r],
        }
    }
}

impl From<char> for TextRangeMarker {
    fn from(marker: char) -> Self {
        Self::Empty(marker)
    }
}

impl From<(char, char)> for TextRangeMarker {
    fn from((left_marker, right_marker): (char, char)) -> Self {
        Self::Range(left_marker, right_marker)
    }
}

#[cfg(test)]
mod tests {
    use super::{generate_marked_text, marked_text_ranges};

    #[allow(clippy::reversed_empty_ranges)]
    #[test]
    fn test_marked_text() {
        let (text, ranges) = marked_text_ranges("one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six", true);

        assert_eq!(text, "one two three four five six");
        assert_eq!(ranges.len(), 4);
        assert_eq!(ranges[0], 7..4);
        assert_eq!(ranges[1], 8..13);
        assert_eq!(ranges[2], 18..14);
        assert_eq!(ranges[3], 23..23);

        assert_eq!(
            generate_marked_text(&text, &ranges, true),
            "one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six"
        );
    }
}