indent_guides.rs

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