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(buffer.read(cx).language(), buffer.read(cx).file(), cx)
 43                    .indent_guides
 44                    .enabled
 45            } else {
 46                true
 47            }
 48        });
 49
 50        if !show_indent_guides {
 51            return None;
 52        }
 53
 54        Some(indent_guides_in_range(
 55            visible_buffer_range,
 56            self.should_show_indent_guides() == Some(true),
 57            snapshot,
 58            cx,
 59        ))
 60    }
 61
 62    pub fn find_active_indent_guide_indices(
 63        &mut self,
 64        indent_guides: &[MultiBufferIndentGuide],
 65        snapshot: &DisplaySnapshot,
 66        cx: &mut ViewContext<Editor>,
 67    ) -> Option<HashSet<usize>> {
 68        let selection = self.selections.newest::<Point>(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(true)
 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
102                .background_executor()
103                .spawn(resolve_indented_range(snapshot, cursor_row));
104
105            // Try to resolve the indent in a short amount of time, otherwise move it to a background task.
106            match cx
107                .background_executor()
108                .block_with_timeout(Duration::from_micros(200), task)
109            {
110                Ok(result) => state.active_indent_range = result,
111                Err(future) => {
112                    state.pending_refresh = Some(cx.spawn(|editor, mut cx| async move {
113                        let result = cx.background_executor().spawn(future).await;
114                        editor
115                            .update(&mut cx, |editor, _| {
116                                editor.active_indent_guides_state.active_indent_range = result;
117                                editor.active_indent_guides_state.pending_refresh = None;
118                            })
119                            .log_err();
120                    }));
121                    return None;
122                }
123            }
124        }
125
126        let active_indent_range = state.active_indent_range.as_ref()?;
127
128        let candidates = indent_guides
129            .iter()
130            .enumerate()
131            .filter(|(_, indent_guide)| {
132                indent_guide.buffer_id == active_indent_range.buffer_id
133                    && indent_guide.indent_level()
134                        == active_indent_range.indent.len(indent_guide.tab_size)
135            });
136
137        let mut matches = HashSet::default();
138        for (i, indent) in candidates {
139            // Find matches that are either an exact match, partially on screen, or inside the enclosing indent
140            if active_indent_range.row_range.start <= indent.end_row
141                && indent.start_row <= active_indent_range.row_range.end
142            {
143                matches.insert(i);
144            }
145        }
146        Some(matches)
147    }
148}
149
150pub fn indent_guides_in_range(
151    visible_buffer_range: Range<MultiBufferRow>,
152    ignore_disabled_for_language: bool,
153    snapshot: &DisplaySnapshot,
154    cx: &AppContext,
155) -> Vec<MultiBufferIndentGuide> {
156    let start_anchor = snapshot
157        .buffer_snapshot
158        .anchor_before(Point::new(visible_buffer_range.start.0, 0));
159    let end_anchor = snapshot
160        .buffer_snapshot
161        .anchor_after(Point::new(visible_buffer_range.end.0, 0));
162
163    snapshot
164        .buffer_snapshot
165        .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
166        .into_iter()
167        .filter(|indent_guide| {
168            // Filter out indent guides that are inside a fold
169            !snapshot.is_line_folded(MultiBufferRow(
170                indent_guide.multibuffer_row_range.start.0.saturating_sub(1),
171            ))
172        })
173        .collect()
174}
175
176async fn resolve_indented_range(
177    snapshot: DisplaySnapshot,
178    buffer_row: MultiBufferRow,
179) -> Option<ActiveIndentedRange> {
180    let (buffer_row, buffer_snapshot, buffer_id) =
181        if let Some((_, buffer_id, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
182            (buffer_row.0, snapshot, buffer_id)
183        } else {
184            let (snapshot, point) = snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
185
186            let buffer_id = snapshot.remote_id();
187            (point.start.row, snapshot, buffer_id)
188        };
189
190    buffer_snapshot
191        .enclosing_indent(buffer_row)
192        .await
193        .map(|(row_range, indent)| ActiveIndentedRange {
194            row_range,
195            indent,
196            buffer_id,
197        })
198}
199
200fn should_recalculate_indented_range(
201    prev_row: MultiBufferRow,
202    new_row: MultiBufferRow,
203    current_indent_range: &ActiveIndentedRange,
204    snapshot: &DisplaySnapshot,
205) -> bool {
206    if prev_row.0 == new_row.0 {
207        return false;
208    }
209    if let Some((_, _, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
210        if !current_indent_range.row_range.contains(&new_row.0) {
211            return true;
212        }
213
214        let old_line_indent = snapshot.line_indent_for_row(prev_row.0);
215        let new_line_indent = snapshot.line_indent_for_row(new_row.0);
216
217        if old_line_indent.is_line_empty()
218            || new_line_indent.is_line_empty()
219            || old_line_indent != new_line_indent
220            || snapshot.max_point().row == new_row.0
221        {
222            return true;
223        }
224
225        let next_line_indent = snapshot.line_indent_for_row(new_row.0 + 1);
226        next_line_indent.is_line_empty() || next_line_indent != old_line_indent
227    } else {
228        true
229    }
230}