1use std::{cmp::Ordering, ops::Range, time::Duration};
2
3use collections::HashSet;
4use gpui::{App, AppContext as _, Context, Task, Window};
5use language::language_settings::language_settings;
6use multi_buffer::{IndentGuide, MultiBufferRow, ToPoint};
7use text::{LineIndent, Point};
8use util::ResultExt;
9
10use crate::{DisplaySnapshot, Editor};
11
12struct ActiveIndentedRange {
13 row_range: Range<MultiBufferRow>,
14 indent: LineIndent,
15}
16
17#[derive(Default)]
18pub struct ActiveIndentGuidesState {
19 pub dirty: bool,
20 cursor_row: MultiBufferRow,
21 pending_refresh: Option<Task<()>>,
22 active_indent_range: Option<ActiveIndentedRange>,
23}
24
25impl ActiveIndentGuidesState {
26 pub const fn should_refresh(&self) -> bool {
27 self.pending_refresh.is_none() && self.dirty
28 }
29}
30
31impl Editor {
32 pub fn indent_guides(
33 &self,
34 visible_buffer_range: Range<MultiBufferRow>,
35 snapshot: &DisplaySnapshot,
36 cx: &mut Context<Editor>,
37 ) -> Option<Vec<IndentGuide>> {
38 let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
39 if let Some(buffer) = self.buffer().read(cx).as_singleton() {
40 language_settings(
41 buffer.read(cx).language().map(|l| l.name()),
42 buffer.read(cx).file(),
43 cx,
44 )
45 .indent_guides
46 .enabled
47 } else {
48 true
49 }
50 });
51
52 if !show_indent_guides {
53 return None;
54 }
55
56 Some(indent_guides_in_range(
57 self,
58 visible_buffer_range,
59 self.should_show_indent_guides() == Some(true),
60 snapshot,
61 cx,
62 ))
63 }
64
65 pub fn find_active_indent_guide_indices(
66 &mut self,
67 indent_guides: &[IndentGuide],
68 snapshot: &DisplaySnapshot,
69 window: &mut Window,
70 cx: &mut Context<Editor>,
71 ) -> Option<HashSet<usize>> {
72 let selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
73 let cursor_row = MultiBufferRow(selection.head().row);
74
75 let state = &mut self.active_indent_guides_state;
76
77 if state
78 .active_indent_range
79 .as_ref()
80 .map(|active_indent_range| {
81 should_recalculate_indented_range(
82 state.cursor_row,
83 cursor_row,
84 active_indent_range,
85 snapshot,
86 )
87 })
88 .unwrap_or(true)
89 {
90 state.dirty = true;
91 } else {
92 state.cursor_row = cursor_row;
93 }
94
95 if state.should_refresh() {
96 state.cursor_row = cursor_row;
97 state.dirty = false;
98
99 if indent_guides.is_empty() {
100 return None;
101 }
102
103 let snapshot = snapshot.clone();
104
105 let task = cx.background_spawn(resolve_indented_range(snapshot, cursor_row));
106
107 // Try to resolve the indent in a short amount of time, otherwise move it to a background task.
108 match cx
109 .background_executor()
110 .block_with_timeout(Duration::from_micros(200), task)
111 {
112 Ok(result) => state.active_indent_range = result,
113 Err(future) => {
114 state.pending_refresh = Some(cx.spawn_in(window, async move |editor, cx| {
115 let result = cx.background_spawn(future).await;
116 editor
117 .update(cx, |editor, _| {
118 editor.active_indent_guides_state.active_indent_range = result;
119 editor.active_indent_guides_state.pending_refresh = None;
120 })
121 .log_err();
122 }));
123 return None;
124 }
125 }
126 }
127
128 let active_indent_range = state.active_indent_range.as_ref()?;
129
130 let candidates = indent_guides
131 .iter()
132 .enumerate()
133 .filter(|(_, indent_guide)| {
134 indent_guide.indent_level() == active_indent_range.indent.len(indent_guide.tab_size)
135 });
136
137 let mut matches = HashSet::default();
138 for (i, indent) in candidates {
139 // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
140 if active_indent_range.row_range.start <= indent.end_row
141 && indent.start_row <= active_indent_range.row_range.end
142 {
143 matches.insert(i);
144 }
145 }
146 Some(matches)
147 }
148}
149
150pub fn indent_guides_in_range(
151 editor: &Editor,
152 visible_buffer_range: Range<MultiBufferRow>,
153 ignore_disabled_for_language: bool,
154 snapshot: &DisplaySnapshot,
155 cx: &App,
156) -> Vec<IndentGuide> {
157 let start_offset = snapshot
158 .buffer_snapshot()
159 .point_to_offset(Point::new(visible_buffer_range.start.0, 0));
160 let end_offset = snapshot
161 .buffer_snapshot()
162 .point_to_offset(Point::new(visible_buffer_range.end.0, 0));
163 let start_anchor = snapshot.buffer_snapshot().anchor_before(start_offset);
164 let end_anchor = snapshot.buffer_snapshot().anchor_after(end_offset);
165
166 let mut fold_ranges = Vec::<Range<Point>>::new();
167 let folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
168 for fold in folds {
169 let start = fold.range.start.to_point(&snapshot.buffer_snapshot());
170 let end = fold.range.end.to_point(&snapshot.buffer_snapshot());
171 if let Some(last_range) = fold_ranges.last_mut()
172 && last_range.end >= start
173 {
174 last_range.end = last_range.end.max(end);
175 continue;
176 }
177 fold_ranges.push(start..end);
178 }
179
180 snapshot
181 .buffer_snapshot()
182 .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
183 .filter(|indent_guide| {
184 if editor.is_buffer_folded(indent_guide.buffer_id, cx) {
185 return false;
186 }
187
188 let has_containing_fold = fold_ranges
189 .binary_search_by(|fold_range| {
190 if fold_range.start >= Point::new(indent_guide.start_row.0, 0) {
191 Ordering::Greater
192 } else if fold_range.end < Point::new(indent_guide.end_row.0, 0) {
193 Ordering::Less
194 } else {
195 Ordering::Equal
196 }
197 })
198 .is_ok();
199
200 !has_containing_fold
201 })
202 .collect()
203}
204
205async fn resolve_indented_range(
206 snapshot: DisplaySnapshot,
207 buffer_row: MultiBufferRow,
208) -> Option<ActiveIndentedRange> {
209 snapshot
210 .buffer_snapshot()
211 .enclosing_indent(buffer_row)
212 .await
213 .map(|(row_range, indent)| ActiveIndentedRange { row_range, indent })
214}
215
216fn should_recalculate_indented_range(
217 prev_row: MultiBufferRow,
218 new_row: MultiBufferRow,
219 current_indent_range: &ActiveIndentedRange,
220 snapshot: &DisplaySnapshot,
221) -> bool {
222 if prev_row.0 == new_row.0 {
223 return false;
224 }
225 if snapshot.buffer_snapshot().is_singleton() {
226 if !current_indent_range.row_range.contains(&new_row) {
227 return true;
228 }
229
230 let old_line_indent = snapshot.buffer_snapshot().line_indent_for_row(prev_row);
231 let new_line_indent = snapshot.buffer_snapshot().line_indent_for_row(new_row);
232
233 if old_line_indent.is_line_empty()
234 || new_line_indent.is_line_empty()
235 || old_line_indent != new_line_indent
236 || snapshot.buffer_snapshot().max_point().row == new_row.0
237 {
238 return true;
239 }
240
241 let next_line_indent = snapshot.buffer_snapshot().line_indent_for_row(new_row + 1);
242 next_line_indent.is_line_empty() || next_line_indent != old_line_indent
243 } else {
244 true
245 }
246}