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