1use std::{ops::Range, time::Duration};
2
3use collections::HashSet;
4use gpui::{App, AppContext as _, 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.background_spawn(resolve_indented_range(snapshot, cursor_row));
106
107 // Try to resolve the indent in a short amount of time, otherwise move it to a background task.
108 match cx
109 .background_executor()
110 .block_with_timeout(Duration::from_micros(200), task)
111 {
112 Ok(result) => state.active_indent_range = result,
113 Err(future) => {
114 state.pending_refresh = Some(cx.spawn_in(window, async move |editor, cx| {
115 let result = cx.background_spawn(future).await;
116 editor
117 .update(cx, |editor, _| {
118 editor.active_indent_guides_state.active_indent_range = result;
119 editor.active_indent_guides_state.pending_refresh = None;
120 })
121 .log_err();
122 }));
123 return None;
124 }
125 }
126 }
127
128 let active_indent_range = state.active_indent_range.as_ref()?;
129
130 let candidates = indent_guides
131 .iter()
132 .enumerate()
133 .filter(|(_, indent_guide)| {
134 indent_guide.indent_level() == 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 editor: &Editor,
152 visible_buffer_range: Range<MultiBufferRow>,
153 ignore_disabled_for_language: bool,
154 snapshot: &DisplaySnapshot,
155 cx: &App,
156) -> Vec<IndentGuide> {
157 let start_anchor = snapshot
158 .buffer_snapshot
159 .anchor_before(Point::new(visible_buffer_range.start.0, 0));
160 let end_anchor = snapshot
161 .buffer_snapshot
162 .anchor_after(Point::new(visible_buffer_range.end.0, 0));
163
164 snapshot
165 .buffer_snapshot
166 .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
167 .filter(|indent_guide| {
168 if editor.is_buffer_folded(indent_guide.buffer_id, cx) {
169 return false;
170 }
171
172 let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
173 // Filter out indent guides that are inside a fold
174 // All indent guides that are starting "offscreen" have a start value of the first visible row minus one
175 // 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
176 let is_folded = snapshot.is_line_folded(start);
177 let line_indent = snapshot.line_indent_for_buffer_row(start);
178 let contained_in_fold =
179 line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
180 !(is_folded && contained_in_fold)
181 })
182 .collect()
183}
184
185async fn resolve_indented_range(
186 snapshot: DisplaySnapshot,
187 buffer_row: MultiBufferRow,
188) -> Option<ActiveIndentedRange> {
189 snapshot
190 .buffer_snapshot
191 .enclosing_indent(buffer_row)
192 .await
193 .map(|(row_range, indent)| ActiveIndentedRange { row_range, indent })
194}
195
196fn should_recalculate_indented_range(
197 prev_row: MultiBufferRow,
198 new_row: MultiBufferRow,
199 current_indent_range: &ActiveIndentedRange,
200 snapshot: &DisplaySnapshot,
201) -> bool {
202 if prev_row.0 == new_row.0 {
203 return false;
204 }
205 if snapshot.buffer_snapshot.is_singleton() {
206 if !current_indent_range.row_range.contains(&new_row) {
207 return true;
208 }
209
210 let old_line_indent = snapshot.buffer_snapshot.line_indent_for_row(prev_row);
211 let new_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row);
212
213 if old_line_indent.is_line_empty()
214 || new_line_indent.is_line_empty()
215 || old_line_indent != new_line_indent
216 || snapshot.buffer_snapshot.max_point().row == new_row.0
217 {
218 return true;
219 }
220
221 let next_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row + 1);
222 next_line_indent.is_line_empty() || next_line_indent != old_line_indent
223 } else {
224 true
225 }
226}