1use std::{cmp::Ordering, ops::Range, time::Duration};
2
3use collections::HashSet;
4use gpui::{App, AppContext as _, Context, Task, Window};
5use language::language_settings::LanguageSettings;
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 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 LanguageSettings::for_buffer(buffer.read(cx), cx)
41 .indent_guides
42 .enabled
43 } else {
44 true
45 }
46 });
47
48 if !show_indent_guides {
49 return None;
50 }
51
52 Some(indent_guides_in_range(
53 self,
54 visible_buffer_range,
55 self.should_show_indent_guides() == Some(true),
56 snapshot,
57 cx,
58 ))
59 }
60
61 pub fn find_active_indent_guide_indices(
62 &mut self,
63 indent_guides: &[IndentGuide],
64 snapshot: &DisplaySnapshot,
65 window: &mut Window,
66 cx: &mut Context<Editor>,
67 ) -> Option<HashSet<usize>> {
68 let selection = self.selections.newest::<Point>(&self.display_snapshot(cx));
69 let cursor_row = MultiBufferRow(selection.head().row);
70
71 let state = &mut self.active_indent_guides_state;
72
73 if state
74 .active_indent_range
75 .as_ref()
76 .map(|active_indent_range| {
77 should_recalculate_indented_range(
78 state.cursor_row,
79 cursor_row,
80 active_indent_range,
81 snapshot,
82 )
83 })
84 .unwrap_or(state.cursor_row != cursor_row)
85 {
86 state.dirty = true;
87 } else {
88 state.cursor_row = cursor_row;
89 }
90
91 if state.should_refresh() {
92 state.cursor_row = cursor_row;
93 state.dirty = false;
94
95 if indent_guides.is_empty() {
96 return None;
97 }
98
99 let snapshot = snapshot.clone();
100
101 let task = cx.background_spawn(resolve_indented_range(snapshot, cursor_row));
102
103 // Try to resolve the indent in a short amount of time, otherwise move it to a background task.
104 match cx
105 .foreground_executor()
106 .block_with_timeout(Duration::from_micros(200), task)
107 {
108 Ok(result) => state.active_indent_range = result,
109 Err(future) => {
110 state.pending_refresh = Some(cx.spawn_in(window, async move |editor, cx| {
111 let result = cx.background_spawn(future).await;
112 editor
113 .update(cx, |editor, _| {
114 editor.active_indent_guides_state.active_indent_range = result;
115 editor.active_indent_guides_state.pending_refresh = None;
116 })
117 .log_err();
118 }));
119 return None;
120 }
121 }
122 }
123
124 let active_indent_range = state.active_indent_range.as_ref()?;
125
126 let candidates = indent_guides
127 .iter()
128 .enumerate()
129 .filter(|(_, indent_guide)| {
130 indent_guide.indent_level() == active_indent_range.indent.len(indent_guide.tab_size)
131 });
132
133 let mut matches = HashSet::default();
134 for (i, indent) in candidates {
135 // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
136 if active_indent_range.row_range.start <= indent.end_row
137 && indent.start_row <= active_indent_range.row_range.end
138 {
139 matches.insert(i);
140 }
141 }
142 Some(matches)
143 }
144}
145
146pub fn indent_guides_in_range(
147 editor: &Editor,
148 visible_buffer_range: Range<MultiBufferRow>,
149 ignore_disabled_for_language: bool,
150 snapshot: &DisplaySnapshot,
151 cx: &App,
152) -> Vec<IndentGuide> {
153 let start_offset = snapshot
154 .buffer_snapshot()
155 .point_to_offset(Point::new(visible_buffer_range.start.0, 0));
156 let end_offset = snapshot
157 .buffer_snapshot()
158 .point_to_offset(Point::new(visible_buffer_range.end.0, 0));
159 let start_anchor = snapshot.buffer_snapshot().anchor_before(start_offset);
160 let end_anchor = snapshot.buffer_snapshot().anchor_after(end_offset);
161
162 let mut fold_ranges = Vec::<Range<Point>>::new();
163 let folds = snapshot.folds_in_range(start_offset..end_offset).peekable();
164 for fold in folds {
165 let start = fold.range.start.to_point(&snapshot.buffer_snapshot());
166 let end = fold.range.end.to_point(&snapshot.buffer_snapshot());
167 if let Some(last_range) = fold_ranges.last_mut()
168 && last_range.end >= start
169 {
170 last_range.end = last_range.end.max(end);
171 continue;
172 }
173 fold_ranges.push(start..end);
174 }
175
176 snapshot
177 .buffer_snapshot()
178 .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
179 .filter(|indent_guide| {
180 if editor.has_indent_guides_disabled_for_buffer(indent_guide.buffer_id) {
181 return false;
182 }
183
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}