1use std::{ops::Range, time::Duration};
2
3use collections::HashSet;
4use gpui::{AppContext, Task};
5use language::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 enabled = self.should_show_indent_guides(cx);
41
42 if enabled {
43 Some(indent_guides_in_range(visible_buffer_range, snapshot, cx))
44 } else {
45 None
46 }
47 }
48
49 pub fn find_active_indent_guide_indices(
50 &mut self,
51 indent_guides: &[MultiBufferIndentGuide],
52 snapshot: &DisplaySnapshot,
53 cx: &mut ViewContext<Editor>,
54 ) -> Option<HashSet<usize>> {
55 let selection = self.selections.newest::<Point>(cx);
56 let cursor_row = MultiBufferRow(selection.head().row);
57
58 let state = &mut self.active_indent_guides_state;
59
60 if state
61 .active_indent_range
62 .as_ref()
63 .map(|active_indent_range| {
64 should_recalculate_indented_range(
65 state.cursor_row,
66 cursor_row,
67 active_indent_range,
68 snapshot,
69 )
70 })
71 .unwrap_or(true)
72 {
73 state.dirty = true;
74 } else {
75 state.cursor_row = cursor_row;
76 }
77
78 if state.should_refresh() {
79 state.cursor_row = cursor_row;
80 let snapshot = snapshot.clone();
81 state.dirty = false;
82
83 let task = cx
84 .background_executor()
85 .spawn(resolve_indented_range(snapshot, cursor_row));
86
87 // Try to resolve the indent in a short amount of time, otherwise move it to a background task.
88 match cx
89 .background_executor()
90 .block_with_timeout(Duration::from_micros(200), task)
91 {
92 Ok(result) => state.active_indent_range = result,
93 Err(future) => {
94 state.pending_refresh = Some(cx.spawn(|editor, mut cx| async move {
95 let result = cx.background_executor().spawn(future).await;
96 editor
97 .update(&mut cx, |editor, _| {
98 editor.active_indent_guides_state.active_indent_range = result;
99 editor.active_indent_guides_state.pending_refresh = None;
100 })
101 .log_err();
102 }));
103 return None;
104 }
105 }
106 }
107
108 let active_indent_range = state.active_indent_range.as_ref()?;
109
110 let candidates = indent_guides
111 .iter()
112 .enumerate()
113 .filter(|(_, indent_guide)| {
114 indent_guide.buffer_id == active_indent_range.buffer_id
115 && indent_guide.indent_level()
116 == active_indent_range.indent.len(indent_guide.tab_size)
117 });
118
119 let mut matches = HashSet::default();
120 for (i, indent) in candidates {
121 // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
122 if active_indent_range.row_range.start <= indent.end_row
123 && indent.start_row <= active_indent_range.row_range.end
124 {
125 matches.insert(i);
126 }
127 }
128 Some(matches)
129 }
130}
131
132pub fn indent_guides_in_range(
133 visible_buffer_range: Range<MultiBufferRow>,
134 snapshot: &DisplaySnapshot,
135 cx: &AppContext,
136) -> Vec<MultiBufferIndentGuide> {
137 let start_anchor = snapshot
138 .buffer_snapshot
139 .anchor_before(Point::new(visible_buffer_range.start.0, 0));
140 let end_anchor = snapshot
141 .buffer_snapshot
142 .anchor_after(Point::new(visible_buffer_range.end.0, 0));
143
144 snapshot
145 .buffer_snapshot
146 .indent_guides_in_range(start_anchor..end_anchor, cx)
147 .into_iter()
148 .filter(|indent_guide| {
149 // Filter out indent guides that are inside a fold
150 !snapshot.is_line_folded(MultiBufferRow(
151 indent_guide.multibuffer_row_range.start.0.saturating_sub(1),
152 ))
153 })
154 .collect()
155}
156
157async fn resolve_indented_range(
158 snapshot: DisplaySnapshot,
159 buffer_row: MultiBufferRow,
160) -> Option<ActiveIndentedRange> {
161 let (buffer_row, buffer_snapshot, buffer_id) =
162 if let Some((_, buffer_id, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
163 (buffer_row.0, snapshot, buffer_id)
164 } else {
165 let (snapshot, point) = snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
166
167 let buffer_id = snapshot.remote_id();
168 (point.start.row, snapshot, buffer_id)
169 };
170
171 buffer_snapshot
172 .enclosing_indent(buffer_row)
173 .await
174 .map(|(row_range, indent)| ActiveIndentedRange {
175 row_range,
176 indent,
177 buffer_id,
178 })
179}
180
181fn should_recalculate_indented_range(
182 prev_row: MultiBufferRow,
183 new_row: MultiBufferRow,
184 current_indent_range: &ActiveIndentedRange,
185 snapshot: &DisplaySnapshot,
186) -> bool {
187 if prev_row.0 == new_row.0 {
188 return false;
189 }
190 if let Some((_, _, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
191 if !current_indent_range.row_range.contains(&new_row.0) {
192 return true;
193 }
194
195 let old_line_indent = snapshot.line_indent_for_row(prev_row.0);
196 let new_line_indent = snapshot.line_indent_for_row(new_row.0);
197
198 if old_line_indent.is_line_empty()
199 || new_line_indent.is_line_empty()
200 || old_line_indent != new_line_indent
201 || snapshot.max_point().row == new_row.0
202 {
203 return true;
204 }
205
206 let next_line_indent = snapshot.line_indent_for_row(new_row.0 + 1);
207 next_line_indent.is_line_empty() || next_line_indent != old_line_indent
208 } else {
209 true
210 }
211}