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 visible_buffer_range,
60 self.should_show_indent_guides() == Some(true),
61 snapshot,
62 cx,
63 ))
64 }
65
66 pub fn find_active_indent_guide_indices(
67 &mut self,
68 indent_guides: &[MultiBufferIndentGuide],
69 snapshot: &DisplaySnapshot,
70 cx: &mut ViewContext<Editor>,
71 ) -> Option<HashSet<usize>> {
72 let selection = self.selections.newest::<Point>(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
106 .background_executor()
107 .spawn(resolve_indented_range(snapshot, cursor_row));
108
109 // Try to resolve the indent in a short amount of time, otherwise move it to a background task.
110 match cx
111 .background_executor()
112 .block_with_timeout(Duration::from_micros(200), task)
113 {
114 Ok(result) => state.active_indent_range = result,
115 Err(future) => {
116 state.pending_refresh = Some(cx.spawn(|editor, mut cx| async move {
117 let result = cx.background_executor().spawn(future).await;
118 editor
119 .update(&mut cx, |editor, _| {
120 editor.active_indent_guides_state.active_indent_range = result;
121 editor.active_indent_guides_state.pending_refresh = None;
122 })
123 .log_err();
124 }));
125 return None;
126 }
127 }
128 }
129
130 let active_indent_range = state.active_indent_range.as_ref()?;
131
132 let candidates = indent_guides
133 .iter()
134 .enumerate()
135 .filter(|(_, indent_guide)| {
136 indent_guide.buffer_id == active_indent_range.buffer_id
137 && indent_guide.indent_level()
138 == active_indent_range.indent.len(indent_guide.tab_size)
139 });
140
141 let mut matches = HashSet::default();
142 for (i, indent) in candidates {
143 // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
144 if active_indent_range.row_range.start <= indent.end_row
145 && indent.start_row <= active_indent_range.row_range.end
146 {
147 matches.insert(i);
148 }
149 }
150 Some(matches)
151 }
152}
153
154pub fn indent_guides_in_range(
155 visible_buffer_range: Range<MultiBufferRow>,
156 ignore_disabled_for_language: bool,
157 snapshot: &DisplaySnapshot,
158 cx: &AppContext,
159) -> Vec<MultiBufferIndentGuide> {
160 let start_anchor = snapshot
161 .buffer_snapshot
162 .anchor_before(Point::new(visible_buffer_range.start.0, 0));
163 let end_anchor = snapshot
164 .buffer_snapshot
165 .anchor_after(Point::new(visible_buffer_range.end.0, 0));
166
167 snapshot
168 .buffer_snapshot
169 .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
170 .into_iter()
171 .filter(|indent_guide| {
172 let start =
173 MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
174 // Filter out indent guides that are inside a fold
175 let is_folded = snapshot.is_line_folded(start);
176 let line_indent = snapshot.line_indent_for_buffer_row(start);
177
178 let contained_in_fold =
179 line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
180
181 !(is_folded && contained_in_fold)
182 })
183 .collect()
184}
185
186async fn resolve_indented_range(
187 snapshot: DisplaySnapshot,
188 buffer_row: MultiBufferRow,
189) -> Option<ActiveIndentedRange> {
190 let (buffer_row, buffer_snapshot, buffer_id) =
191 if let Some((_, buffer_id, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
192 (buffer_row.0, snapshot, buffer_id)
193 } else {
194 let (snapshot, point) = snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
195
196 let buffer_id = snapshot.remote_id();
197 (point.start.row, snapshot, buffer_id)
198 };
199
200 buffer_snapshot
201 .enclosing_indent(buffer_row)
202 .await
203 .map(|(row_range, indent)| ActiveIndentedRange {
204 row_range,
205 indent,
206 buffer_id,
207 })
208}
209
210fn should_recalculate_indented_range(
211 prev_row: MultiBufferRow,
212 new_row: MultiBufferRow,
213 current_indent_range: &ActiveIndentedRange,
214 snapshot: &DisplaySnapshot,
215) -> bool {
216 if prev_row.0 == new_row.0 {
217 return false;
218 }
219 if let Some((_, _, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
220 if !current_indent_range.row_range.contains(&new_row.0) {
221 return true;
222 }
223
224 let old_line_indent = snapshot.line_indent_for_row(prev_row.0);
225 let new_line_indent = snapshot.line_indent_for_row(new_row.0);
226
227 if old_line_indent.is_line_empty()
228 || new_line_indent.is_line_empty()
229 || old_line_indent != new_line_indent
230 || snapshot.max_point().row == new_row.0
231 {
232 return true;
233 }
234
235 let next_line_indent = snapshot.line_indent_for_row(new_row.0 + 1);
236 next_line_indent.is_line_empty() || next_line_indent != old_line_indent
237 } else {
238 true
239 }
240}