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