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 // Filter out indent guides that are inside a fold
169 !snapshot.is_line_folded(MultiBufferRow(
170 indent_guide.multibuffer_row_range.start.0.saturating_sub(1),
171 ))
172 })
173 .collect()
174}
175
176async fn resolve_indented_range(
177 snapshot: DisplaySnapshot,
178 buffer_row: MultiBufferRow,
179) -> Option<ActiveIndentedRange> {
180 let (buffer_row, buffer_snapshot, buffer_id) =
181 if let Some((_, buffer_id, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
182 (buffer_row.0, snapshot, buffer_id)
183 } else {
184 let (snapshot, point) = snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
185
186 let buffer_id = snapshot.remote_id();
187 (point.start.row, snapshot, buffer_id)
188 };
189
190 buffer_snapshot
191 .enclosing_indent(buffer_row)
192 .await
193 .map(|(row_range, indent)| ActiveIndentedRange {
194 row_range,
195 indent,
196 buffer_id,
197 })
198}
199
200fn should_recalculate_indented_range(
201 prev_row: MultiBufferRow,
202 new_row: MultiBufferRow,
203 current_indent_range: &ActiveIndentedRange,
204 snapshot: &DisplaySnapshot,
205) -> bool {
206 if prev_row.0 == new_row.0 {
207 return false;
208 }
209 if let Some((_, _, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
210 if !current_indent_range.row_range.contains(&new_row.0) {
211 return true;
212 }
213
214 let old_line_indent = snapshot.line_indent_for_row(prev_row.0);
215 let new_line_indent = snapshot.line_indent_for_row(new_row.0);
216
217 if old_line_indent.is_line_empty()
218 || new_line_indent.is_line_empty()
219 || old_line_indent != new_line_indent
220 || snapshot.max_point().row == new_row.0
221 {
222 return true;
223 }
224
225 let next_line_indent = snapshot.line_indent_for_row(new_row.0 + 1);
226 next_line_indent.is_line_empty() || next_line_indent != old_line_indent
227 } else {
228 true
229 }
230}