1use imbl::Vector;
5use ropey::Rope;
6use strop_core::id::DisplayColumn;
7
8struct Run {
9 first: usize,
10 end: usize,
11 columns: Vector<DisplayColumn>,
12}
13#[derive(Default)]
14pub struct IndentGuides {
15 runs: Vec<Run>,
16 prefixes: Vec<usize>,
17}
18#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
19pub struct GuideFrame {
20 pub first_line: usize,
21 pub rows: Vec<Vec<DisplayColumn>>,
22}
23impl GuideFrame {
24 pub fn columns(&self, line: usize) -> &[DisplayColumn] {
25 line.checked_sub(self.first_line)
26 .and_then(|line| self.rows.get(line))
27 .map_or(&[], Vec::as_slice)
28 }
29}
30impl IndentGuides {
31 pub fn build(rope: &Rope, tab: usize, cancelled: impl Fn() -> bool) -> Option<Self> {
32 let mut runs: Vec<Run> = Vec::new();
33 let mut prefixes = Vec::with_capacity(rope.len_lines());
34 let mut scopes = Vector::new();
35 let mut previous = None;
36 let mut blank_start = None;
37 let tab = tab.max(1);
38 for (row, line) in rope.lines().enumerate() {
39 if row % 128 == 0 && cancelled() {
40 return None;
41 }
42 let mut indent = 0usize;
43 let mut content = false;
44 let mut prefix = 0;
45 for ch in line.chars() {
46 if prefix % 2048 == 0 && cancelled() {
47 return None;
48 }
49 match ch {
50 ' ' => indent = indent.saturating_add(1),
51 '\t' => indent = indent.saturating_add(tab - indent % tab),
52 '\n' | '\r' => break,
53 _ => {
54 content = true;
55 break;
56 }
57 }
58 prefix += ch.len_utf8();
59 }
60 prefixes.push(prefix);
61 if !content {
62 blank_start.get_or_insert(row);
63 continue;
64 }
65 while scopes
66 .back()
67 .is_some_and(|column: &DisplayColumn| column.get() >= indent)
68 {
69 scopes.pop_back();
70 }
71 if let Some(parent) = previous.filter(|parent| *parent < indent) {
72 let column = DisplayColumn::new(parent);
73 if scopes.back() != Some(&column) {
74 scopes.push_back(column);
75 }
76 }
77 previous = Some(indent);
78 let first = blank_start.take().unwrap_or(row);
79 if let Some(last) = runs.last_mut().filter(|last| last.columns == scopes) {
80 last.end = row + 1;
81 } else {
82 runs.push(Run {
83 first,
84 end: row + 1,
85 columns: scopes.clone(),
86 });
87 }
88 }
89 if let Some(first) = blank_start {
90 runs.push(Run {
91 first,
92 end: rope.len_lines(),
93 columns: Vector::new(),
94 });
95 }
96 (!cancelled()).then_some(Self { runs, prefixes })
97 }
98 pub fn unaffected_by(&self, edit: &strop_core::InputEdit) -> bool {
101 edit.start_point.0 == edit.old_end_point.0
102 && edit.start_point.0 == edit.new_end_point.0
103 && self
104 .prefixes
105 .get(edit.start_point.0)
106 .is_some_and(|prefix| edit.start_point.1 > *prefix)
107 }
108 pub fn frame(&self, first: usize, end: usize, left: usize, right: usize) -> GuideFrame {
109 let mut result = GuideFrame {
110 first_line: first,
111 rows: Vec::with_capacity(end.saturating_sub(first)),
112 };
113 let mut run = self.runs.partition_point(|run| run.end <= first);
114 for line in first..end {
115 while self.runs.get(run).is_some_and(|run| run.end <= line) {
116 run += 1;
117 }
118 result.rows.push(
119 self.runs
120 .get(run)
121 .filter(|run| run.first <= line)
122 .map_or_else(Vec::new, |run| {
123 run.columns
124 .iter()
125 .copied()
126 .filter(|column| column.get() >= left && column.get() < right)
127 .collect()
128 }),
129 );
130 }
131 result
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 #[test]
139 fn alignment_does_not_invent_intermediate_rails() {
140 let rope = Rope::from_str(" flat\n aligned\n deeper\n\n back\n");
141 let index = IndentGuides::build(&rope, 4, || false).unwrap();
142 let frame = index.frame(0, 5, 0, 40);
143 assert_eq!(
144 frame.rows,
145 vec![vec![], vec![], vec![DisplayColumn::new(4)], vec![], vec![]]
146 );
147 }
148 #[test]
149 fn nested_tabs_and_dedents_use_display_columns() {
150 let rope = Rope::from_str("root\n\tchild\n\t\tinner\n\tpeer\nend\n");
151 let frame = IndentGuides::build(&rope, 4, || false)
152 .unwrap()
153 .frame(0, 5, 0, 8);
154 assert_eq!(
155 frame.rows,
156 vec![
157 vec![],
158 vec![DisplayColumn::new(0)],
159 vec![DisplayColumn::new(0), DisplayColumn::new(4)],
160 vec![DisplayColumn::new(0)],
161 vec![]
162 ]
163 );
164 }
165 #[test]
166 fn blank_rows_follow_the_surrounding_scope_not_the_previous_indent() {
167 let rope = Rope::from_str("root\n\n child\n nested\n\n peer\n\nend\n");
168 let frame = IndentGuides::build(&rope, 4, || false)
169 .unwrap()
170 .frame(0, 8, 0, 20);
171 assert_eq!(frame.columns(1), &[DisplayColumn::new(0)]);
172 assert_eq!(frame.columns(4), &[DisplayColumn::new(0)]);
173 assert!(frame.columns(6).is_empty());
174 }
175}