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 =
115 Some(cx.spawn_in(window, |editor, mut cx| async move {
116 let result = cx.background_spawn(future).await;
117 editor
118 .update(&mut cx, |editor, _| {
119 editor.active_indent_guides_state.active_indent_range = result;
120 editor.active_indent_guides_state.pending_refresh = None;
121 })
122 .log_err();
123 }));
124 return None;
125 }
126 }
127 }
128
129 let active_indent_range = state.active_indent_range.as_ref()?;
130
131 let candidates = indent_guides
132 .iter()
133 .enumerate()
134 .filter(|(_, indent_guide)| {
135 indent_guide.indent_level() == active_indent_range.indent.len(indent_guide.tab_size)
136 });
137
138 let mut matches = HashSet::default();
139 for (i, indent) in candidates {
140 // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
141 if active_indent_range.row_range.start <= indent.end_row
142 && indent.start_row <= active_indent_range.row_range.end
143 {
144 matches.insert(i);
145 }
146 }
147 Some(matches)
148 }
149}
150
151pub fn indent_guides_in_range(
152 editor: &Editor,
153 visible_buffer_range: Range<MultiBufferRow>,
154 ignore_disabled_for_language: bool,
155 snapshot: &DisplaySnapshot,
156 cx: &App,
157) -> Vec<IndentGuide> {
158 let start_anchor = snapshot
159 .buffer_snapshot
160 .anchor_before(Point::new(visible_buffer_range.start.0, 0));
161 let end_anchor = snapshot
162 .buffer_snapshot
163 .anchor_after(Point::new(visible_buffer_range.end.0, 0));
164
165 snapshot
166 .buffer_snapshot
167 .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
168 .filter(|indent_guide| {
169 if editor.is_buffer_folded(indent_guide.buffer_id, cx) {
170 return false;
171 }
172
173 let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
174 // Filter out indent guides that are inside a fold
175 // All indent guides that are starting "offscreen" have a start value of the first visible row minus one
176 // 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
177 let is_folded = snapshot.is_line_folded(start);
178 let line_indent = snapshot.line_indent_for_buffer_row(start);
179 let contained_in_fold =
180 line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
181 !(is_folded && contained_in_fold)
182 })
183 .collect()
184}
185
186async fn resolve_indented_range(
187 snapshot: DisplaySnapshot,
188 buffer_row: MultiBufferRow,
189) -> Option<ActiveIndentedRange> {
190 snapshot
191 .buffer_snapshot
192 .enclosing_indent(buffer_row)
193 .await
194 .map(|(row_range, indent)| ActiveIndentedRange { row_range, indent })
195}
196
197fn should_recalculate_indented_range(
198 prev_row: MultiBufferRow,
199 new_row: MultiBufferRow,
200 current_indent_range: &ActiveIndentedRange,
201 snapshot: &DisplaySnapshot,
202) -> bool {
203 if prev_row.0 == new_row.0 {
204 return false;
205 }
206 if snapshot.buffer_snapshot.is_singleton() {
207 if !current_indent_range.row_range.contains(&new_row) {
208 return true;
209 }
210
211 let old_line_indent = snapshot.buffer_snapshot.line_indent_for_row(prev_row);
212 let new_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row);
213
214 if old_line_indent.is_line_empty()
215 || new_line_indent.is_line_empty()
216 || old_line_indent != new_line_indent
217 || snapshot.buffer_snapshot.max_point().row == new_row.0
218 {
219 return true;
220 }
221
222 let next_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row + 1);
223 next_line_indent.is_line_empty() || next_line_indent != old_line_indent
224 } else {
225 true
226 }
227}