indent_guides.rs

  1use std::{ops::Range, time::Duration};
  2
  3use collections::HashSet;
  4use gpui::{AppContext, Task};
  5use language::language_settings::language_settings;
  6use multi_buffer::{IndentGuide, MultiBufferRow};
  7use text::{LineIndent, Point};
  8use ui::ViewContext;
  9use util::ResultExt;
 10
 11use crate::{DisplaySnapshot, Editor};
 12
 13struct ActiveIndentedRange {
 14    row_range: Range<MultiBufferRow>,
 15    indent: LineIndent,
 16}
 17
 18#[derive(Default)]
 19pub struct ActiveIndentGuidesState {
 20    pub dirty: bool,
 21    cursor_row: MultiBufferRow,
 22    pending_refresh: Option<Task<()>>,
 23    active_indent_range: Option<ActiveIndentedRange>,
 24}
 25
 26impl ActiveIndentGuidesState {
 27    pub fn should_refresh(&self) -> bool {
 28        self.pending_refresh.is_none() && self.dirty
 29    }
 30}
 31
 32impl Editor {
 33    pub fn indent_guides(
 34        &self,
 35        visible_buffer_range: Range<MultiBufferRow>,
 36        snapshot: &DisplaySnapshot,
 37        cx: &mut ViewContext<Editor>,
 38    ) -> Option<Vec<IndentGuide>> {
 39        let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
 40            if let Some(buffer) = self.buffer().read(cx).as_singleton() {
 41                language_settings(
 42                    buffer.read(cx).language().map(|l| l.name()),
 43                    buffer.read(cx).file(),
 44                    cx,
 45                )
 46                .indent_guides
 47                .enabled
 48            } else {
 49                true
 50            }
 51        });
 52
 53        if !show_indent_guides {
 54            return None;
 55        }
 56
 57        Some(indent_guides_in_range(
 58            self,
 59            visible_buffer_range,
 60            self.should_show_indent_guides() == Some(true),
 61            snapshot,
 62            cx,
 63        ))
 64    }
 65
 66    pub fn find_active_indent_guide_indices(
 67        &mut self,
 68        indent_guides: &[IndentGuide],
 69        snapshot: &DisplaySnapshot,
 70        cx: &mut ViewContext<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 = Some(cx.spawn(|editor, mut cx| async move {
117                        let result = cx.background_executor().spawn(future).await;
118                        editor
119                            .update(&mut cx, |editor, _| {
120                                editor.active_indent_guides_state.active_indent_range = result;
121                                editor.active_indent_guides_state.pending_refresh = None;
122                            })
123                            .log_err();
124                    }));
125                    return None;
126                }
127            }
128        }
129
130        let active_indent_range = state.active_indent_range.as_ref()?;
131
132        let candidates = indent_guides
133            .iter()
134            .enumerate()
135            .filter(|(_, indent_guide)| {
136                indent_guide.indent_level() == active_indent_range.indent.len(indent_guide.tab_size)
137            });
138
139        let mut matches = HashSet::default();
140        for (i, indent) in candidates {
141            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
142            if active_indent_range.row_range.start <= indent.end_row
143                && indent.start_row <= active_indent_range.row_range.end
144            {
145                matches.insert(i);
146            }
147        }
148        Some(matches)
149    }
150}
151
152pub fn indent_guides_in_range(
153    editor: &Editor,
154    visible_buffer_range: Range<MultiBufferRow>,
155    ignore_disabled_for_language: bool,
156    snapshot: &DisplaySnapshot,
157    cx: &AppContext,
158) -> Vec<IndentGuide> {
159    let start_anchor = snapshot
160        .buffer_snapshot
161        .anchor_before(Point::new(visible_buffer_range.start.0, 0));
162    let end_anchor = snapshot
163        .buffer_snapshot
164        .anchor_after(Point::new(visible_buffer_range.end.0, 0));
165
166    snapshot
167        .buffer_snapshot
168        .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
169        .filter(|indent_guide| {
170            if editor.is_buffer_folded(indent_guide.buffer_id, cx) {
171                return false;
172            }
173
174            let start = MultiBufferRow(indent_guide.start_row.0.saturating_sub(1));
175            // Filter out indent guides that are inside a fold
176            // All indent guides that are starting "offscreen" have a start value of the first visible row minus one
177            // 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
178            let is_folded = snapshot.is_line_folded(start);
179            let line_indent = snapshot.line_indent_for_buffer_row(start);
180            let contained_in_fold =
181                line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
182            !(is_folded && contained_in_fold)
183        })
184        .collect()
185}
186
187async fn resolve_indented_range(
188    snapshot: DisplaySnapshot,
189    buffer_row: MultiBufferRow,
190) -> Option<ActiveIndentedRange> {
191    snapshot
192        .buffer_snapshot
193        .enclosing_indent(buffer_row)
194        .await
195        .map(|(row_range, indent)| ActiveIndentedRange { row_range, indent })
196}
197
198fn should_recalculate_indented_range(
199    prev_row: MultiBufferRow,
200    new_row: MultiBufferRow,
201    current_indent_range: &ActiveIndentedRange,
202    snapshot: &DisplaySnapshot,
203) -> bool {
204    if prev_row.0 == new_row.0 {
205        return false;
206    }
207    if snapshot.buffer_snapshot.is_singleton() {
208        if !current_indent_range.row_range.contains(&new_row) {
209            return true;
210        }
211
212        let old_line_indent = snapshot.buffer_snapshot.line_indent_for_row(prev_row);
213        let new_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row);
214
215        if old_line_indent.is_line_empty()
216            || new_line_indent.is_line_empty()
217            || old_line_indent != new_line_indent
218            || snapshot.buffer_snapshot.max_point().row == new_row.0
219        {
220            return true;
221        }
222
223        let next_line_indent = snapshot.buffer_snapshot.line_indent_for_row(new_row + 1);
224        next_line_indent.is_line_empty() || next_line_indent != old_line_indent
225    } else {
226        true
227    }
228}