indent_guides.rs

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