1use std::{ops::Range, time::Duration};
2
3use collections::HashSet;
4use gpui::{AppContext, Task};
5use language::language_settings::language_settings;
6use multi_buffer::{IndentGuide, MultiBufferRow};
7use text::{LineIndent, Point};
8use ui::ViewContext;
9use util::ResultExt;
10
11use crate::{DisplaySnapshot, Editor};
12
13struct ActiveIndentedRange {
14 row_range: Range<MultiBufferRow>,
15 indent: LineIndent,
16}
17
18#[derive(Default)]
19pub struct ActiveIndentGuidesState {
20 pub dirty: bool,
21 cursor_row: MultiBufferRow,
22 pending_refresh: Option<Task<()>>,
23 active_indent_range: Option<ActiveIndentedRange>,
24}
25
26impl ActiveIndentGuidesState {
27 pub fn should_refresh(&self) -> bool {
28 self.pending_refresh.is_none() && self.dirty
29 }
30}
31
32impl Editor {
33 pub fn indent_guides(
34 &self,
35 visible_buffer_range: Range<MultiBufferRow>,
36 snapshot: &DisplaySnapshot,
37 cx: &mut ViewContext<Editor>,
38 ) -> Option<Vec<IndentGuide>> {
39 let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
40 if let Some(buffer) = self.buffer().read(cx).as_singleton() {
41 language_settings(
42 buffer.read(cx).language().map(|l| l.name()),
43 buffer.read(cx).file(),
44 cx,
45 )
46 .indent_guides
47 .enabled
48 } else {
49 true
50 }
51 });
52
53 if !show_indent_guides {
54 return None;
55 }
56
57 Some(indent_guides_in_range(
58 self,
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: &[IndentGuide],
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.indent_level() == active_indent_range.indent.len(indent_guide.tab_size)
137 });
138
139 let mut matches = HashSet::default();
140 for (i, indent) in candidates {
141 // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
142 if active_indent_range.row_range.start <= indent.end_row
143 && indent.start_row <= active_indent_range.row_range.end
144 {
145 matches.insert(i);
146 }
147 }
148 Some(matches)
149 }
150}
151
152pub fn indent_guides_in_range(
153 editor: &Editor,
154 visible_buffer_range: Range<MultiBufferRow>,
155 ignore_disabled_for_language: bool,
156 snapshot: &DisplaySnapshot,
157 cx: &AppContext,
158) -> Vec<IndentGuide> {
159 let start_anchor = snapshot
160 .buffer_snapshot
161 .anchor_before(Point::new(visible_buffer_range.start.0, 0));
162 let end_anchor = snapshot
163 .buffer_snapshot
164 .anchor_after(Point::new(visible_buffer_range.end.0, 0));
165
166 snapshot
167 .buffer_snapshot
168 .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
169 .filter(|indent_guide| {
170 if editor.is_buffer_folded(indent_guide.buffer_id, cx) {
171 return false;
172 }
173
174 let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
175 // Filter out indent guides that are inside a fold
176 // All indent guides that are starting "offscreen" have a start value of the first visible row minus one
177 // 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
178 let is_folded = snapshot.is_line_folded(start);
179 let line_indent = snapshot.line_indent_for_buffer_row(start);
180 let contained_in_fold =
181 line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
182 !(is_folded && contained_in_fold)
183 })
184 .collect()
185}
186
187async fn resolve_indented_range(
188 snapshot: DisplaySnapshot,
189 buffer_row: MultiBufferRow,
190) -> Option<ActiveIndentedRange> {
191 snapshot
192 .buffer_snapshot
193 .enclosing_indent(buffer_row)
194 .await
195 .map(|(row_range, indent)| ActiveIndentedRange { row_range, indent })
196}
197
198fn should_recalculate_indented_range(
199 prev_row: MultiBufferRow,
200 new_row: MultiBufferRow,
201 current_indent_range: &ActiveIndentedRange,
202 snapshot: &DisplaySnapshot,
203) -> bool {
204 if prev_row.0 == new_row.0 {
205 return false;
206 }
207 if snapshot.buffer_snapshot.is_singleton() {
208 if !current_indent_range.row_range.contains(&new_row) {
209 return true;
210 }
211
212 let old_line_indent = snapshot.buffer_snapshot.line_indent_for_row(prev_row);
213 let new_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row);
214
215 if old_line_indent.is_line_empty()
216 || new_line_indent.is_line_empty()
217 || old_line_indent != new_line_indent
218 || snapshot.buffer_snapshot.max_point().row == new_row.0
219 {
220 return true;
221 }
222
223 let next_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row + 1);
224 next_line_indent.is_line_empty() || next_line_indent != old_line_indent
225 } else {
226 true
227 }
228}