indent_guides.rs

  1use std::{ops::Range, time::Duration};
  2
  3use collections::HashSet;
  4use gpui::{AppContext, Task};
  5use language::{language_settings::language_settings, 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 show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
 41            if let Some(buffer) = self.buffer().read(cx).as_singleton() {
 42                language_settings(
 43                    buffer.read(cx).language().map(|l| l.name()),
 44                    buffer.read(cx).file(),
 45                    cx,
 46                )
 47                .indent_guides
 48                .enabled
 49            } else {
 50                true
 51            }
 52        });
 53
 54        if !show_indent_guides {
 55            return None;
 56        }
 57
 58        Some(indent_guides_in_range(
 59            self,
 60            visible_buffer_range,
 61            self.should_show_indent_guides() == Some(true),
 62            snapshot,
 63            cx,
 64        ))
 65    }
 66
 67    pub fn find_active_indent_guide_indices(
 68        &mut self,
 69        indent_guides: &[MultiBufferIndentGuide],
 70        snapshot: &DisplaySnapshot,
 71        cx: &mut ViewContext<Editor>,
 72    ) -> Option<HashSet<usize>> {
 73        let selection = self.selections.newest::<Point>(cx);
 74        let cursor_row = MultiBufferRow(selection.head().row);
 75
 76        let state = &mut self.active_indent_guides_state;
 77
 78        if state
 79            .active_indent_range
 80            .as_ref()
 81            .map(|active_indent_range| {
 82                should_recalculate_indented_range(
 83                    state.cursor_row,
 84                    cursor_row,
 85                    active_indent_range,
 86                    snapshot,
 87                )
 88            })
 89            .unwrap_or(true)
 90        {
 91            state.dirty = true;
 92        } else {
 93            state.cursor_row = cursor_row;
 94        }
 95
 96        if state.should_refresh() {
 97            state.cursor_row = cursor_row;
 98            state.dirty = false;
 99
100            if indent_guides.is_empty() {
101                return None;
102            }
103
104            let snapshot = snapshot.clone();
105
106            let task = cx
107                .background_executor()
108                .spawn(resolve_indented_range(snapshot, cursor_row));
109
110            // Try to resolve the indent in a short amount of time, otherwise move it to a background task.
111            match cx
112                .background_executor()
113                .block_with_timeout(Duration::from_micros(200), task)
114            {
115                Ok(result) => state.active_indent_range = result,
116                Err(future) => {
117                    state.pending_refresh = Some(cx.spawn(|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.buffer_id == active_indent_range.buffer_id
138                    && indent_guide.indent_level()
139                        == active_indent_range.indent.len(indent_guide.tab_size)
140            });
141
142        let mut matches = HashSet::default();
143        for (i, indent) in candidates {
144            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
145            if active_indent_range.row_range.start <= indent.end_row
146                && indent.start_row <= active_indent_range.row_range.end
147            {
148                matches.insert(i);
149            }
150        }
151        Some(matches)
152    }
153}
154
155pub fn indent_guides_in_range(
156    editor: &Editor,
157    visible_buffer_range: Range<MultiBufferRow>,
158    ignore_disabled_for_language: bool,
159    snapshot: &DisplaySnapshot,
160    cx: &AppContext,
161) -> Vec<MultiBufferIndentGuide> {
162    let start_anchor = snapshot
163        .buffer_snapshot
164        .anchor_before(Point::new(visible_buffer_range.start.0, 0));
165    let end_anchor = snapshot
166        .buffer_snapshot
167        .anchor_after(Point::new(visible_buffer_range.end.0, 0));
168
169    snapshot
170        .buffer_snapshot
171        .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
172        .into_iter()
173        .filter(|indent_guide| {
174            if editor.buffer_folded(indent_guide.buffer_id, cx) {
175                return false;
176            }
177
178            let start =
179                MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
180            // Filter out indent guides that are inside a fold
181            // All indent guides that are starting "offscreen" have a start value of the first visible row minus one
182            // 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
183            let is_folded = snapshot.is_line_folded(start);
184            let line_indent = snapshot.line_indent_for_buffer_row(start);
185            let contained_in_fold =
186                line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
187            !(is_folded && contained_in_fold)
188        })
189        .collect()
190}
191
192async fn resolve_indented_range(
193    snapshot: DisplaySnapshot,
194    buffer_row: MultiBufferRow,
195) -> Option<ActiveIndentedRange> {
196    let (buffer_row, buffer_snapshot, buffer_id) =
197        if let Some((_, buffer_id, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
198            (buffer_row.0, snapshot, buffer_id)
199        } else {
200            let (snapshot, point) = snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
201
202            let buffer_id = snapshot.remote_id();
203            (point.start.row, snapshot, buffer_id)
204        };
205
206    buffer_snapshot
207        .enclosing_indent(buffer_row)
208        .await
209        .map(|(row_range, indent)| ActiveIndentedRange {
210            row_range,
211            indent,
212            buffer_id,
213        })
214}
215
216fn should_recalculate_indented_range(
217    prev_row: MultiBufferRow,
218    new_row: MultiBufferRow,
219    current_indent_range: &ActiveIndentedRange,
220    snapshot: &DisplaySnapshot,
221) -> bool {
222    if prev_row.0 == new_row.0 {
223        return false;
224    }
225    if let Some((_, _, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
226        if !current_indent_range.row_range.contains(&new_row.0) {
227            return true;
228        }
229
230        let old_line_indent = snapshot.line_indent_for_row(prev_row.0);
231        let new_line_indent = snapshot.line_indent_for_row(new_row.0);
232
233        if old_line_indent.is_line_empty()
234            || new_line_indent.is_line_empty()
235            || old_line_indent != new_line_indent
236            || snapshot.max_point().row == new_row.0
237        {
238            return true;
239        }
240
241        let next_line_indent = snapshot.line_indent_for_row(new_row.0 + 1);
242        next_line_indent.is_line_empty() || next_line_indent != old_line_indent
243    } else {
244        true
245    }
246}