indent_guides.rs

  1use std::{ops::Range, time::Duration};
  2
  3use collections::HashSet;
  4use gpui::{App, 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
106                .background_executor()
107                .spawn(resolve_indented_range(snapshot, cursor_row));
108
109            // Try to resolve the indent in a short amount of time, otherwise move it to a background task.
110            match cx
111                .background_executor()
112                .block_with_timeout(Duration::from_micros(200), task)
113            {
114                Ok(result) => state.active_indent_range = result,
115                Err(future) => {
116                    state.pending_refresh =
117                        Some(cx.spawn_in(window, |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.indent_level() == active_indent_range.indent.len(indent_guide.tab_size)
138            });
139
140        let mut matches = HashSet::default();
141        for (i, indent) in candidates {
142            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
143            if active_indent_range.row_range.start <= indent.end_row
144                && indent.start_row <= active_indent_range.row_range.end
145            {
146                matches.insert(i);
147            }
148        }
149        Some(matches)
150    }
151}
152
153pub fn indent_guides_in_range(
154    editor: &Editor,
155    visible_buffer_range: Range<MultiBufferRow>,
156    ignore_disabled_for_language: bool,
157    snapshot: &DisplaySnapshot,
158    cx: &App,
159) -> Vec<IndentGuide> {
160    let start_anchor = snapshot
161        .buffer_snapshot
162        .anchor_before(Point::new(visible_buffer_range.start.0, 0));
163    let end_anchor = snapshot
164        .buffer_snapshot
165        .anchor_after(Point::new(visible_buffer_range.end.0, 0));
166
167    snapshot
168        .buffer_snapshot
169        .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
170        .filter(|indent_guide| {
171            if editor.is_buffer_folded(indent_guide.buffer_id, cx) {
172                return false;
173            }
174
175            let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
176            // Filter out indent guides that are inside a fold
177            // All indent guides that are starting "offscreen" have a start value of the first visible row minus one
178            // 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
179            let is_folded = snapshot.is_line_folded(start);
180            let line_indent = snapshot.line_indent_for_buffer_row(start);
181            let contained_in_fold =
182                line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
183            !(is_folded && contained_in_fold)
184        })
185        .collect()
186}
187
188async fn resolve_indented_range(
189    snapshot: DisplaySnapshot,
190    buffer_row: MultiBufferRow,
191) -> Option<ActiveIndentedRange> {
192    snapshot
193        .buffer_snapshot
194        .enclosing_indent(buffer_row)
195        .await
196        .map(|(row_range, indent)| ActiveIndentedRange { row_range, indent })
197}
198
199fn should_recalculate_indented_range(
200    prev_row: MultiBufferRow,
201    new_row: MultiBufferRow,
202    current_indent_range: &ActiveIndentedRange,
203    snapshot: &DisplaySnapshot,
204) -> bool {
205    if prev_row.0 == new_row.0 {
206        return false;
207    }
208    if snapshot.buffer_snapshot.is_singleton() {
209        if !current_indent_range.row_range.contains(&new_row) {
210            return true;
211        }
212
213        let old_line_indent = snapshot.buffer_snapshot.line_indent_for_row(prev_row);
214        let new_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row);
215
216        if old_line_indent.is_line_empty()
217            || new_line_indent.is_line_empty()
218            || old_line_indent != new_line_indent
219            || snapshot.buffer_snapshot.max_point().row == new_row.0
220        {
221            return true;
222        }
223
224        let next_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row + 1);
225        next_line_indent.is_line_empty() || next_line_indent != old_line_indent
226    } else {
227        true
228    }
229}