gpui_component/input/
indent.rs

1use gpui::{
2    point, px, Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels,
3    SharedString, TextRun, TextStyle, Window,
4};
5use ropey::RopeSlice;
6
7use crate::{
8    input::{
9        element::TextElement, mode::InputMode, Indent, IndentInline, InputState, LastLayout,
10        Outdent, OutdentInline,
11    },
12    RopeExt,
13};
14
15#[derive(Debug, Copy, Clone)]
16pub struct TabSize {
17    /// Default is 2
18    pub tab_size: usize,
19    /// Set true to use `\t` as tab indent, default is false
20    pub hard_tabs: bool,
21}
22
23impl Default for TabSize {
24    fn default() -> Self {
25        Self {
26            tab_size: 2,
27            hard_tabs: false,
28        }
29    }
30}
31
32impl TabSize {
33    pub(super) fn to_string(&self) -> SharedString {
34        if self.hard_tabs {
35            "\t".into()
36        } else {
37            " ".repeat(self.tab_size).into()
38        }
39    }
40
41    /// Count the indent size of the line in spaces.
42    pub fn indent_count(&self, line: &RopeSlice) -> usize {
43        let mut count = 0;
44        for ch in line.chars() {
45            match ch {
46                '\t' => count += self.tab_size,
47                ' ' => count += 1,
48                _ => break,
49            }
50        }
51
52        count
53    }
54}
55
56impl InputMode {
57    #[inline]
58    pub(super) fn is_indentable(&self) -> bool {
59        matches!(
60            self,
61            InputMode::MultiLine { .. } | InputMode::CodeEditor { .. }
62        )
63    }
64
65    #[inline]
66    pub(super) fn has_indent_guides(&self) -> bool {
67        match self {
68            InputMode::CodeEditor { indent_guides, .. } => *indent_guides,
69            _ => false,
70        }
71    }
72
73    #[inline]
74    pub(super) fn tab_size(&self) -> TabSize {
75        match self {
76            InputMode::MultiLine { tab, .. } => *tab,
77            InputMode::CodeEditor { tab, .. } => *tab,
78            _ => TabSize::default(),
79        }
80    }
81}
82
83impl TextElement {
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: &InputState,
107        bounds: &Bounds<Pixels>,
108        last_layout: &LastLayout,
109        text_style: &TextStyle,
110        window: &mut Window,
111    ) -> Option<Path<Pixels>> {
112        if !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 visible_range = last_layout.visible_range.clone();
122        let mut builder = PathBuilder::stroke(px(1.));
123        let mut offset_y = last_layout.visible_top;
124        let mut last_indents = vec![];
125        for ix in visible_range {
126            let line = state.text.slice_line(ix);
127            let Some(line_layout) = last_layout.line(ix) else {
128                continue;
129            };
130
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
166impl InputState {
167    /// Set whether to show indent guides in code editor mode, default is true.
168    ///
169    /// Only for [`InputMode::CodeEditor`] mode.
170    pub fn indent_guides(mut self, indent_guides: bool) -> Self {
171        debug_assert!(self.mode.is_code_editor());
172        if let InputMode::CodeEditor {
173            indent_guides: l, ..
174        } = &mut self.mode
175        {
176            *l = indent_guides;
177        }
178        self
179    }
180
181    /// Set indent guides in code editor mode.
182    ///
183    /// Only for [`InputMode::CodeEditor`] mode.
184    pub fn set_indent_guides(
185        &mut self,
186        indent_guides: bool,
187        _: &mut Window,
188        cx: &mut Context<Self>,
189    ) {
190        debug_assert!(self.mode.is_code_editor());
191        if let InputMode::CodeEditor {
192            indent_guides: l, ..
193        } = &mut self.mode
194        {
195            *l = indent_guides;
196        }
197        cx.notify();
198    }
199
200    /// Set the tab size for the input.
201    ///
202    /// Only for [`InputMode::MultiLine`] and [`InputMode::CodeEditor`] mode.
203    pub fn tab_size(mut self, tab: TabSize) -> Self {
204        debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
205        match &mut self.mode {
206            InputMode::MultiLine { tab: t, .. } => *t = tab,
207            InputMode::CodeEditor { tab: t, .. } => *t = tab,
208            _ => {}
209        }
210        self
211    }
212
213    pub(super) fn indent_inline(
214        &mut self,
215        _: &IndentInline,
216        window: &mut Window,
217        cx: &mut Context<Self>,
218    ) {
219        self.indent(false, window, cx);
220    }
221
222    pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
223        self.indent(true, window, cx);
224    }
225
226    pub(super) fn outdent_inline(
227        &mut self,
228        _: &OutdentInline,
229        window: &mut Window,
230        cx: &mut Context<Self>,
231    ) {
232        self.outdent(false, window, cx);
233    }
234
235    pub(super) fn outdent_block(
236        &mut self,
237        _: &Outdent,
238        window: &mut Window,
239        cx: &mut Context<Self>,
240    ) {
241        self.outdent(true, window, cx);
242    }
243
244    pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
245        if !self.mode.is_indentable() {
246            cx.propagate();
247            return;
248        };
249
250        let tab_indent = self.mode.tab_size().to_string();
251        let selected_range = self.selected_range;
252        let mut added_len = 0;
253        let is_selected = !self.selected_range.is_empty();
254
255        if is_selected || block {
256            let start_offset = self.start_of_line_of_selection(window, cx);
257            let mut offset = start_offset;
258
259            let selected_text = self
260                .text_for_range(
261                    self.range_to_utf16(&(offset..selected_range.end)),
262                    &mut None,
263                    window,
264                    cx,
265                )
266                .unwrap_or("".into());
267
268            for line in selected_text.split('\n') {
269                self.replace_text_in_range_silent(
270                    Some(self.range_to_utf16(&(offset..offset))),
271                    &tab_indent,
272                    window,
273                    cx,
274                );
275                added_len += tab_indent.len();
276                // +1 for "\n", the `\r` is included in the `line`.
277                offset += line.len() + tab_indent.len() + 1;
278            }
279
280            if is_selected {
281                self.selected_range = (start_offset..selected_range.end + added_len).into();
282            } else {
283                self.selected_range =
284                    (selected_range.start + added_len..selected_range.end + added_len).into();
285            }
286        } else {
287            // Selected none
288            let offset = self.selected_range.start;
289            self.replace_text_in_range_silent(
290                Some(self.range_to_utf16(&(offset..offset))),
291                &tab_indent,
292                window,
293                cx,
294            );
295            added_len = tab_indent.len();
296
297            self.selected_range =
298                (selected_range.start + added_len..selected_range.end + added_len).into();
299        }
300    }
301
302    pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
303        if !self.mode.is_indentable() {
304            cx.propagate();
305            return;
306        };
307
308        let tab_indent = self.mode.tab_size().to_string();
309        let selected_range = self.selected_range;
310        let mut removed_len = 0;
311        let is_selected = !self.selected_range.is_empty();
312
313        if is_selected || block {
314            let start_offset = self.start_of_line_of_selection(window, cx);
315            let mut offset = start_offset;
316
317            let selected_text = self
318                .text_for_range(
319                    self.range_to_utf16(&(offset..selected_range.end)),
320                    &mut None,
321                    window,
322                    cx,
323                )
324                .unwrap_or("".into());
325
326            for line in selected_text.split('\n') {
327                if line.starts_with(tab_indent.as_ref()) {
328                    self.replace_text_in_range_silent(
329                        Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
330                        "",
331                        window,
332                        cx,
333                    );
334                    removed_len += tab_indent.len();
335
336                    // +1 for "\n"
337                    offset += line.len().saturating_sub(tab_indent.len()) + 1;
338                } else {
339                    offset += line.len() + 1;
340                }
341            }
342
343            if is_selected {
344                self.selected_range =
345                    (start_offset..selected_range.end.saturating_sub(removed_len)).into();
346            } else {
347                self.selected_range = (selected_range.start.saturating_sub(removed_len)
348                    ..selected_range.end.saturating_sub(removed_len))
349                    .into();
350            }
351        } else {
352            // Selected none
353            let start_offset = self.selected_range.start;
354            let offset = self.start_of_line_of_selection(window, cx);
355            let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
356            // FIXME: To improve performance
357            if self
358                .text
359                .slice(offset..self.text.len())
360                .to_string()
361                .starts_with(tab_indent.as_ref())
362            {
363                self.replace_text_in_range_silent(
364                    Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
365                    "",
366                    window,
367                    cx,
368                );
369                removed_len = tab_indent.len();
370                let new_offset = start_offset.saturating_sub(removed_len);
371                self.selected_range = (new_offset..new_offset).into();
372            }
373        }
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use ropey::RopeSlice;
380
381    use super::TabSize;
382
383    #[test]
384    fn test_tab_size() {
385        let tab = TabSize {
386            tab_size: 2,
387            hard_tabs: false,
388        };
389        assert_eq!(tab.to_string(), "  ");
390        let tab = TabSize {
391            tab_size: 4,
392            hard_tabs: false,
393        };
394        assert_eq!(tab.to_string(), "    ");
395
396        let tab = TabSize {
397            tab_size: 2,
398            hard_tabs: true,
399        };
400        assert_eq!(tab.to_string(), "\t");
401        let tab = TabSize {
402            tab_size: 4,
403            hard_tabs: true,
404        };
405        assert_eq!(tab.to_string(), "\t");
406    }
407
408    #[test]
409    fn test_tab_size_indent_count() {
410        let tab = TabSize {
411            tab_size: 4,
412            hard_tabs: false,
413        };
414        assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
415        assert_eq!(tab.indent_count(&RopeSlice::from("  abc")), 2);
416        assert_eq!(tab.indent_count(&RopeSlice::from("    abc")), 4);
417        assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
418        assert_eq!(tab.indent_count(&RopeSlice::from("  \tabc")), 6);
419        assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc  ")), 6);
420        assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
421    }
422}