Skip to main content

gpui_base/input/editor/
indent.rs

1use crate::input::InputModeKind;
2use crate::input::{
3    Indent, IndentInline, InputBaseState, Outdent, OutdentInline, RopeExt, cursor::CursorSelection,
4    element::TextElement, layout::LastLayout, mode::LayoutMode,
5};
6use gpui::{
7    Bounds, Context, Hsla, Path, PathBuilder, Pixels, SharedString, TextRun, TextStyle, Window,
8    point, px,
9};
10use ropey::RopeSlice;
11
12#[derive(Debug, Copy, Clone)]
13pub struct TabSize {
14    /// Default is 2
15    pub tab_size: usize,
16    /// Set true to use `\t` as tab indent, default is false
17    pub hard_tabs: bool,
18}
19
20impl Default for TabSize {
21    fn default() -> Self {
22        Self {
23            tab_size: 2,
24            hard_tabs: false,
25        }
26    }
27}
28
29impl TabSize {
30    pub(super) fn to_string(&self) -> SharedString {
31        if self.hard_tabs {
32            "\t".into()
33        } else {
34            " ".repeat(self.tab_size).into()
35        }
36    }
37
38    /// Count the indent size of the line in spaces.
39    pub fn indent_count(&self, line: &RopeSlice) -> usize {
40        let mut count = 0;
41        for ch in line.chars() {
42            match ch {
43                '\t' => {
44                    if self.tab_size > 0 {
45                        count += self.tab_size - count % self.tab_size;
46                    }
47                }
48                ' ' => count += 1,
49                _ => break,
50            }
51        }
52        count
53    }
54}
55
56impl LayoutMode {
57    /// Whether this layout indents blocks.
58    ///
59    /// Callers gate this on the input being multi-line: indenting a one-line
60    /// text field has nothing to indent.
61    #[inline]
62    pub(super) fn is_indentable(&self) -> bool {
63        matches!(
64            self,
65            LayoutMode::PlainText { .. } | LayoutMode::CodeEditor { .. }
66        )
67    }
68
69    #[inline]
70    pub(super) fn has_indent_guides(&self) -> bool {
71        match self {
72            LayoutMode::CodeEditor { indent_guides, .. } => *indent_guides,
73            _ => false,
74        }
75    }
76
77    #[inline]
78    pub(super) fn tab_size(&self) -> TabSize {
79        match self {
80            LayoutMode::PlainText { tab, .. } => *tab,
81            LayoutMode::CodeEditor { tab, .. } => *tab,
82            _ => TabSize::default(),
83        }
84    }
85}
86
87impl<M: InputModeKind> TextElement<M> {
88    /// Measure the indent width in pixels for given column count.
89    fn measure_indent_width(&self, style: &TextStyle, column: usize, window: &Window) -> Pixels {
90        let font_size = style.font_size.to_pixels(window.rem_size());
91        let layout = window.text_system().shape_line(
92            SharedString::from(" ".repeat(column)),
93            font_size,
94            &[TextRun {
95                len: column,
96                font: style.font(),
97                color: Hsla::default(),
98                background_color: None,
99                strikethrough: None,
100                underline: None,
101            }],
102            None,
103        );
104
105        layout.width
106    }
107
108    pub(super) fn layout_indent_guides(
109        &self,
110        state: &InputBaseState<M>,
111        bounds: &Bounds<Pixels>,
112        last_layout: &LastLayout,
113        text_style: &TextStyle,
114        window: &mut Window,
115    ) -> Option<Path<Pixels>> {
116        if !state.is_multi_line() || !state.mode.has_indent_guides() {
117            return None;
118        }
119
120        let indent_width =
121            self.measure_indent_width(text_style, state.mode.tab_size().tab_size, window);
122
123        let tab_size = state.mode.tab_size();
124        let line_height = last_layout.line_height;
125        let mut builder = PathBuilder::stroke(px(1.));
126        let mut offset_y = last_layout.visible_top;
127        let mut last_indents = vec![];
128
129        for (&buffer_line, line_layout) in last_layout
130            .visible_buffer_lines
131            .iter()
132            .zip(last_layout.lines.iter())
133        {
134            let line = state.text.slice_line(buffer_line);
135            let mut current_indents = vec![];
136            if line.len() > 0 {
137                let indent_count = tab_size.indent_count(&line);
138                for offset in (0..indent_count).step_by(tab_size.tab_size) {
139                    let x = if indent_count > 0 {
140                        indent_width * offset as f32 / tab_size.tab_size as f32
141                    } else {
142                        px(0.)
143                    };
144
145                    let pos = point(x + last_layout.line_number_width, offset_y);
146
147                    builder.move_to(pos);
148                    builder.line_to(point(pos.x, pos.y + line_height));
149                    current_indents.push(pos.x);
150                }
151            } else if last_indents.len() > 0 {
152                for x in &last_indents {
153                    let pos = point(*x, offset_y);
154                    builder.move_to(pos);
155                    builder.line_to(point(pos.x, pos.y + line_height));
156                }
157                current_indents = last_indents.clone();
158            }
159
160            offset_y += line_layout.wrapped_lines.len() * line_height;
161            last_indents = current_indents;
162        }
163
164        builder.translate(bounds.origin);
165        let path = builder.build().unwrap();
166        Some(path)
167    }
168}
169
170/// Indent guides are a code-editor affordance.
171impl InputBaseState<crate::input::EditorMode> {
172    /// Set whether to show indent guides, default is true.
173    #[doc(hidden)]
174    pub fn indent_guides(mut self, indent_guides: bool) -> Self {
175        if let LayoutMode::CodeEditor {
176            indent_guides: l, ..
177        } = &mut self.mode
178        {
179            *l = indent_guides;
180        }
181        self
182    }
183
184    /// Set indent guides at runtime.
185    pub fn set_indent_guides(
186        &mut self,
187        indent_guides: bool,
188        _: &mut Window,
189        cx: &mut Context<Self>,
190    ) {
191        if let LayoutMode::CodeEditor {
192            indent_guides: l, ..
193        } = &mut self.mode
194        {
195            *l = indent_guides;
196        }
197        cx.notify();
198    }
199}
200
201impl<M: InputModeKind> InputBaseState<M> {
202    pub(super) fn indent_inline(
203        &mut self,
204        _: &IndentInline,
205        window: &mut Window,
206        cx: &mut Context<Self>,
207    ) {
208        // First, try to accept inline completion if present
209        if M::accept_inline_completion(self, window, cx) {
210            return;
211        }
212        self.indent(false, window, cx);
213    }
214
215    pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
216        self.indent(true, window, cx);
217    }
218
219    pub(super) fn outdent_inline(
220        &mut self,
221        _: &OutdentInline,
222        window: &mut Window,
223        cx: &mut Context<Self>,
224    ) {
225        self.outdent(false, window, cx);
226    }
227
228    pub(super) fn outdent_block(
229        &mut self,
230        _: &Outdent,
231        window: &mut Window,
232        cx: &mut Context<Self>,
233    ) {
234        self.outdent(true, window, cx);
235    }
236
237    pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
238        self.apply_indent(IndentDirection::Indent, block, window, cx);
239    }
240
241    pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
242        self.apply_indent(IndentDirection::Outdent, block, window, cx);
243    }
244
245    /// Apply an indent or outdent across all selections as one batch edit.
246    ///
247    /// A batch keeps the whole operation a single undo transaction (instead of
248    /// one push per line) and restores the correct multi-selection extents on
249    /// undo/redo.
250    fn apply_indent(
251        &mut self,
252        direction: IndentDirection,
253        block: bool,
254        window: &mut Window,
255        cx: &mut Context<Self>,
256    ) {
257        if !self.is_editable() || !self.is_multi_line() || !self.mode.is_indentable() {
258            cx.propagate();
259            return;
260        }
261
262        let tab_indent = self.mode.tab_size().to_string();
263        let tab_len = tab_indent.len();
264
265        // Non-collapsed selections and explicit block operations indent whole lines.
266        let has_non_collapsed = self.selections.iter().any(|sel| !sel.is_collapsed());
267        let use_block = has_non_collapsed || block;
268
269        let before: Vec<CursorSelection> = self.selections.iter().copied().collect();
270
271        let (edits, new_selections) = if use_block {
272            self.compute_block_indent(direction, &tab_indent, tab_len)
273        } else {
274            self.compute_inline_indent(direction, tab_len)
275        };
276
277        if edits.is_empty() {
278            return;
279        }
280
281        self.undo_manager.begin_transaction();
282        self.replace_text_in_ranges(&edits, window, cx);
283        self.selections.replace_all(new_selections);
284        let after: Vec<CursorSelection> = self.selections.iter().copied().collect();
285        self.undo_manager.record_selections(before, after);
286        self.undo_manager.commit_transaction();
287
288        self.scroll_to(self.cursor(), None, cx);
289        cx.notify();
290    }
291
292    /// Build the per-line edits and resulting selections for a block
293    /// indent/outdent across every selection.
294    fn compute_block_indent(
295        &self,
296        direction: IndentDirection,
297        tab_indent: &str,
298        tab_len: usize,
299    ) -> (Vec<(std::ops::Range<usize>, String)>, Vec<CursorSelection>) {
300        let mut rows: std::collections::HashSet<usize> = std::collections::HashSet::new();
301        for sel in self.selections.iter() {
302            let start_row = self.text.offset_to_point(sel.start).row;
303            let end_row = self.text.offset_to_point(sel.end).row;
304            for row in start_row..=end_row {
305                rows.insert(row);
306            }
307        }
308
309        let mut rows: Vec<usize> = rows.into_iter().collect();
310        rows.sort_unstable();
311
312        let mut edits: Vec<(std::ops::Range<usize>, String)> = Vec::new();
313        for row in rows {
314            let line_start = self.text.line_start_offset(row);
315            match direction {
316                IndentDirection::Indent => {
317                    edits.push((line_start..line_start, tab_indent.to_string()));
318                }
319                IndentDirection::Outdent => {
320                    if self
321                        .text
322                        .slice(line_start..)
323                        .chars()
324                        .take(tab_indent.chars().count())
325                        .eq(tab_indent.chars())
326                    {
327                        edits.push((line_start..line_start + tab_len, String::new()));
328                    }
329                }
330            }
331        }
332
333        // Map both endpoints through every earlier edit, including edits belonging
334        // to other cursors. A point inside removed indentation stays on its line.
335        let map_offset = |offset: usize| match direction {
336            IndentDirection::Indent => {
337                offset + edits.partition_point(|(range, _)| range.start <= offset) * tab_len
338            }
339            IndentDirection::Outdent => {
340                let preceding = edits.partition_point(|(range, _)| range.end <= offset);
341                let partial = edits
342                    .get(preceding)
343                    .map_or(0, |(range, _)| offset.saturating_sub(range.start));
344                offset - preceding * tab_len - partial
345            }
346        };
347        let new_selections = self
348            .selections
349            .iter()
350            .map(|sel| {
351                let mut selection = *sel;
352                selection.start = map_offset(sel.start);
353                selection.end = map_offset(sel.end);
354                selection.column_anchor = None;
355                selection
356            })
357            .collect();
358
359        (edits, new_selections)
360    }
361
362    /// Build the per-cursor edits and resulting cursors for a collapsed inline
363    /// indent/outdent.
364    fn compute_inline_indent(
365        &self,
366        direction: IndentDirection,
367        tab_len: usize,
368    ) -> (Vec<(std::ops::Range<usize>, String)>, Vec<CursorSelection>) {
369        let tab_indent = self.mode.tab_size().to_string();
370
371        // The edit range for each cursor: an insertion point for indent, the
372        // removed range for a removable outdent.
373        let mut ranges: Vec<std::ops::Range<usize>> = Vec::with_capacity(self.selections.len());
374        for sel in self.selections.iter() {
375            let cursor = sel.cursor_offset();
376            match direction {
377                IndentDirection::Indent => ranges.push(cursor..cursor),
378                IndentDirection::Outdent => {
379                    let row = self.text.offset_to_point(cursor).row;
380                    let start = self.text.line_start_offset(row);
381                    if self
382                        .text
383                        .slice(start..)
384                        .chars()
385                        .take(tab_indent.chars().count())
386                        .eq(tab_indent.chars())
387                    {
388                        ranges.push(start..start + tab_len);
389                    }
390                }
391            }
392        }
393
394        // Build disjoint edits, dropping any that would overlap a previous one.
395        ranges.sort_by_key(|range| range.start);
396        let mut edits: Vec<(std::ops::Range<usize>, String)> = Vec::new();
397        let mut last_end: Option<usize> = None;
398        for range in ranges {
399            if let Some(last_end) = last_end {
400                if range.start < last_end {
401                    continue;
402                }
403            }
404            last_end = Some(range.end);
405            let text = match direction {
406                IndentDirection::Indent => tab_indent.to_string(),
407                IndentDirection::Outdent => String::new(),
408            };
409            edits.push((range, text));
410        }
411
412        // Shift every cursor by the surviving edits before (or at) it. Cursors
413        // whose own edit was dropped or not applicable keep their position.
414        let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(self.selections.len());
415        for sel in self.selections.iter() {
416            let cursor = sel.cursor_offset();
417            let new_offset = match direction {
418                IndentDirection::Indent => {
419                    let inserted_before = edits
420                        .iter()
421                        .filter(|(range, _)| range.start <= cursor)
422                        .count();
423                    cursor + inserted_before * tab_len
424                }
425                IndentDirection::Outdent => {
426                    let removed_before: usize = edits
427                        .iter()
428                        .map(|(range, _)| range.end.min(cursor) - range.start.min(cursor))
429                        .sum();
430                    cursor - removed_before
431                }
432            };
433            let mut selection = CursorSelection::new(sel.id, new_offset, new_offset);
434            selection.column_anchor = None;
435            new_selections.push(selection);
436        }
437
438        (edits, new_selections)
439    }
440}
441
442#[derive(Debug, Copy, Clone, PartialEq)]
443enum IndentDirection {
444    Indent,
445    Outdent,
446}
447
448#[cfg(test)]
449mod tests {
450    use ropey::RopeSlice;
451
452    use super::TabSize;
453
454    #[test]
455    fn test_tab_size() {
456        let tab = TabSize {
457            tab_size: 2,
458            hard_tabs: false,
459        };
460        assert_eq!(tab.to_string(), "  ");
461        let tab = TabSize {
462            tab_size: 4,
463            hard_tabs: false,
464        };
465        assert_eq!(tab.to_string(), "    ");
466
467        let tab = TabSize {
468            tab_size: 2,
469            hard_tabs: true,
470        };
471        assert_eq!(tab.to_string(), "\t");
472        let tab = TabSize {
473            tab_size: 4,
474            hard_tabs: true,
475        };
476        assert_eq!(tab.to_string(), "\t");
477    }
478
479    #[test]
480    fn test_tab_size_indent_count() {
481        let tab = TabSize {
482            tab_size: 4,
483            hard_tabs: false,
484        };
485        assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
486        assert_eq!(tab.indent_count(&RopeSlice::from("  abc")), 2);
487        assert_eq!(tab.indent_count(&RopeSlice::from("    abc")), 4);
488        assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
489        assert_eq!(tab.indent_count(&RopeSlice::from("  \tabc")), 4);
490        assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc  ")), 5);
491        assert_eq!(tab.indent_count(&RopeSlice::from(" \t \tabc")), 8);
492        assert_eq!(tab.indent_count(&RopeSlice::from("    \tabc")), 8);
493        assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
494    }
495}
496
497/// Tab size only means something where there is more than one line to indent.
498impl<M: crate::input::MultiLineMode> InputBaseState<M> {
499    /// Set the tab size for the input.
500    #[doc(hidden)]
501    pub fn tab_size(mut self, tab: TabSize) -> Self {
502        match &mut self.mode {
503            LayoutMode::PlainText { tab: t, .. } => *t = tab,
504            LayoutMode::CodeEditor { tab: t, .. } => *t = tab,
505            _ => {}
506        }
507        self
508    }
509
510    pub fn set_tab_size(&mut self, tab: TabSize, cx: &mut Context<Self>) {
511        match &mut self.mode {
512            LayoutMode::PlainText { tab: value, .. }
513            | LayoutMode::CodeEditor { tab: value, .. } => *value = tab,
514            _ => {}
515        }
516        cx.notify();
517    }
518}