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