indent_guides.rs

  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}