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