1use std::ops::Range;
2
3use crate::highlight::LineClasses;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum LayerMode {
8 Shown,
9 Hidden,
10 Only,
11}
12
13impl LayerMode {
14 pub const ALL: [LayerMode; 3] = [LayerMode::Shown, LayerMode::Hidden, LayerMode::Only];
15
16 pub fn next(self) -> LayerMode {
17 match self {
18 LayerMode::Hidden => LayerMode::Only,
19 LayerMode::Only => LayerMode::Shown,
20 LayerMode::Shown => LayerMode::Hidden,
21 }
22 }
23
24 pub fn label(self) -> &'static str {
25 match self {
26 LayerMode::Shown => "shown",
27 LayerMode::Hidden => "hidden",
28 LayerMode::Only => "only",
29 }
30 }
31
32 pub fn parse(value: &str) -> Option<LayerMode> {
33 LayerMode::ALL
34 .into_iter()
35 .find(|mode| mode.label() == value)
36 }
37}
38
39#[derive(Debug, Clone, Default, PartialEq, Eq)]
42pub struct Projection {
43 pub lines: Vec<String>,
44 pub orig: Vec<usize>,
46 pub syntax: Vec<LineClasses>,
48}
49
50pub fn project(src: &str, comments: &[Range<usize>], mode: LayerMode) -> Projection {
51 let mut projection = Projection::default();
52 let mut next_comment = 0;
53 let mut line_start = 0;
54
55 for (index, raw) in src.split_inclusive('\n').enumerate() {
56 let line_end = line_start + raw.len();
57 let line = raw.trim_end_matches('\n').trim_end_matches('\r');
58
59 while next_comment < comments.len() && comments[next_comment].end <= line_start {
60 next_comment += 1;
61 }
62 let cuts: Vec<Range<usize>> = comments[next_comment..]
64 .iter()
65 .take_while(|comment| comment.start < line_end)
66 .map(|comment| {
67 let end = comment.end.min(line_start + line.len()) - line_start;
68 (comment.start.max(line_start) - line_start).min(end)..end
69 })
70 .collect();
71
72 let kept = match mode {
73 LayerMode::Shown => Some(line.to_string()),
74 LayerMode::Hidden => without_cuts(line, &cuts),
75 LayerMode::Only => only_cuts(line, &cuts),
76 };
77 if let Some(text) = kept {
78 projection.lines.push(text);
79 projection.orig.push(index);
80 }
81 line_start = line_end;
82 }
83 projection
84}
85
86fn without_cuts(line: &str, cuts: &[Range<usize>]) -> Option<String> {
88 if cuts.is_empty() {
89 return Some(line.to_string());
90 }
91 let mut out = line[..cuts[0].start].to_string();
92 for (cut, next) in cuts.iter().zip(cuts.iter().skip(1)) {
93 push_joined(&mut out, &line[cut.end..next.start]);
94 }
95 push_joined(&mut out, &line[cuts[cuts.len() - 1].end..]);
96
97 let trimmed = out.trim_end();
98 (!trimmed.trim_start().is_empty()).then(|| trimmed.to_string())
99}
100
101fn push_joined(out: &mut String, segment: &str) {
104 if out.is_empty() || out.ends_with(char::is_whitespace) {
105 out.push_str(segment.trim_start());
106 } else {
107 out.push_str(segment);
108 }
109}
110
111fn only_cuts(line: &str, cuts: &[Range<usize>]) -> Option<String> {
113 let first = cuts.first()?;
114 let indent = &line[..line.len() - line.trim_start().len()];
115 let mut out = if first.start == 0 {
116 String::new()
117 } else {
118 indent.to_string()
119 };
120 for (i, cut) in cuts.iter().enumerate() {
121 let text = &line[cut.clone()];
122 if i == 0 && cut.start == 0 {
123 out.push_str(text);
124 } else {
125 if i > 0 {
126 out.push(' ');
127 }
128 out.push_str(text.trim());
129 }
130 }
131 Some(out.trim_end().to_string())
132}