gpui_component/input/
indent.rs

1use gpui::{
2    Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString,
3    TextRun, TextStyle, Window, point, px,
4};
5use ropey::RopeSlice;
6
7use crate::{
8    RopeExt,
9    input::{
10        Indent, IndentInline, InputState, LastLayout, Outdent, OutdentInline, element::TextElement,
11        mode::InputMode,
12    },
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        match self {
60            InputMode::PlainText { multi_line, .. } | InputMode::CodeEditor { multi_line, .. } => {
61                *multi_line
62            }
63            _ => false,
64        }
65    }
66
67    #[inline]
68    pub(super) fn has_indent_guides(&self) -> bool {
69        match self {
70            InputMode::CodeEditor {
71                indent_guides,
72                multi_line,
73                ..
74            } => *indent_guides && *multi_line,
75            _ => false,
76        }
77    }
78
79    #[inline]
80    pub(super) fn tab_size(&self) -> TabSize {
81        match self {
82            InputMode::PlainText { tab, .. } => *tab,
83            InputMode::CodeEditor { tab, .. } => *tab,
84            _ => TabSize::default(),
85        }
86    }
87}
88
89impl TextElement {
90    /// Measure the indent width in pixels for given column count.
91    fn measure_indent_width(&self, style: &TextStyle, column: usize, window: &Window) -> Pixels {
92        let font_size = style.font_size.to_pixels(window.rem_size());
93        let layout = window.text_system().shape_line(
94            SharedString::from(" ".repeat(column)),
95            font_size,
96            &[TextRun {
97                len: column,
98                font: style.font(),
99                color: Hsla::default(),
100                background_color: None,
101                strikethrough: None,
102                underline: None,
103            }],
104            None,
105        );
106
107        layout.width
108    }
109
110    pub(super) fn layout_indent_guides(
111        &self,
112        state: &InputState,
113        bounds: &Bounds<Pixels>,
114        last_layout: &LastLayout,
115        text_style: &TextStyle,
116        window: &mut Window,
117    ) -> Option<Path<Pixels>> {
118        if !state.mode.has_indent_guides() {
119            return None;
120        }
121
122        let indent_width =
123            self.measure_indent_width(text_style, state.mode.tab_size().tab_size, window);
124
125        let tab_size = state.mode.tab_size();
126        let line_height = last_layout.line_height;
127        let visible_range = last_layout.visible_range.clone();
128        let mut builder = PathBuilder::stroke(px(1.));
129        let mut offset_y = last_layout.visible_top;
130        let mut last_indents = vec![];
131        for ix in visible_range {
132            let line = state.text.slice_line(ix);
133            let Some(line_layout) = last_layout.line(ix) else {
134                continue;
135            };
136
137            let mut current_indents = vec![];
138            if line.len() > 0 {
139                let indent_count = tab_size.indent_count(&line);
140                for offset in (0..indent_count).step_by(tab_size.tab_size) {
141                    let x = if indent_count > 0 {
142                        indent_width * offset as f32 / tab_size.tab_size as f32
143                    } else {
144                        px(0.)
145                    };
146
147                    let pos = point(x + last_layout.line_number_width, offset_y);
148
149                    builder.move_to(pos);
150                    builder.line_to(point(pos.x, pos.y + line_height));
151                    current_indents.push(pos.x);
152                }
153            } else if last_indents.len() > 0 {
154                for x in &last_indents {
155                    let pos = point(*x, offset_y);
156                    builder.move_to(pos);
157                    builder.line_to(point(pos.x, pos.y + line_height));
158                }
159                current_indents = last_indents.clone();
160            }
161
162            offset_y += line_layout.wrapped_lines.len() * line_height;
163            last_indents = current_indents;
164        }
165
166        builder.translate(bounds.origin);
167        let path = builder.build().unwrap();
168        Some(path)
169    }
170}
171
172impl InputState {
173    /// Set whether to show indent guides in code editor mode, default is true.
174    ///
175    /// Only for [`InputMode::CodeEditor`] mode.
176    pub fn indent_guides(mut self, indent_guides: bool) -> Self {
177        debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
178        if let InputMode::CodeEditor {
179            indent_guides: l, ..
180        } = &mut self.mode
181        {
182            *l = indent_guides;
183        }
184        self
185    }
186
187    /// Set indent guides in code editor mode.
188    ///
189    /// Only for [`InputMode::CodeEditor`] mode.
190    pub fn set_indent_guides(
191        &mut self,
192        indent_guides: bool,
193        _: &mut Window,
194        cx: &mut Context<Self>,
195    ) {
196        debug_assert!(self.mode.is_code_editor());
197        if let InputMode::CodeEditor {
198            indent_guides: l, ..
199        } = &mut self.mode
200        {
201            *l = indent_guides;
202        }
203        cx.notify();
204    }
205
206    /// Set the tab size for the input.
207    ///
208    /// Only for [`InputMode::PlainText`] and [`InputMode::CodeEditor`] mode with multi_line.
209    pub fn tab_size(mut self, tab: TabSize) -> Self {
210        debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
211        match &mut self.mode {
212            InputMode::PlainText { tab: t, .. } => *t = tab,
213            InputMode::CodeEditor { tab: t, .. } => *t = tab,
214            _ => {}
215        }
216        self
217    }
218
219    pub(super) fn indent_inline(
220        &mut self,
221        _: &IndentInline,
222        window: &mut Window,
223        cx: &mut Context<Self>,
224    ) {
225        // First, try to accept inline completion if present
226        if self.accept_inline_completion(window, cx) {
227            return;
228        }
229        self.indent(false, window, cx);
230    }
231
232    pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
233        self.indent(true, window, cx);
234    }
235
236    pub(super) fn outdent_inline(
237        &mut self,
238        _: &OutdentInline,
239        window: &mut Window,
240        cx: &mut Context<Self>,
241    ) {
242        self.outdent(false, window, cx);
243    }
244
245    pub(super) fn outdent_block(
246        &mut self,
247        _: &Outdent,
248        window: &mut Window,
249        cx: &mut Context<Self>,
250    ) {
251        self.outdent(true, window, cx);
252    }
253
254    pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
255        if !self.mode.is_indentable() {
256            cx.propagate();
257            return;
258        };
259
260        let tab_indent = self.mode.tab_size().to_string();
261        let selected_range = self.selected_range;
262        let mut added_len = 0;
263        let is_selected = !self.selected_range.is_empty();
264
265        if is_selected || block {
266            let start_offset = self.start_of_line_of_selection(window, cx);
267            let mut offset = start_offset;
268
269            let selected_text = self
270                .text_for_range(
271                    self.range_to_utf16(&(offset..selected_range.end)),
272                    &mut None,
273                    window,
274                    cx,
275                )
276                .unwrap_or("".into());
277
278            for line in selected_text.split('\n') {
279                self.replace_text_in_range_silent(
280                    Some(self.range_to_utf16(&(offset..offset))),
281                    &tab_indent,
282                    window,
283                    cx,
284                );
285                added_len += tab_indent.len();
286                // +1 for "\n", the `\r` is included in the `line`.
287                offset += line.len() + tab_indent.len() + 1;
288            }
289
290            if is_selected {
291                self.selected_range = (start_offset..selected_range.end + added_len).into();
292            } else {
293                self.selected_range =
294                    (selected_range.start + added_len..selected_range.end + added_len).into();
295            }
296        } else {
297            // Selected none
298            let offset = self.selected_range.start;
299            self.replace_text_in_range_silent(
300                Some(self.range_to_utf16(&(offset..offset))),
301                &tab_indent,
302                window,
303                cx,
304            );
305            added_len = tab_indent.len();
306
307            self.selected_range =
308                (selected_range.start + added_len..selected_range.end + added_len).into();
309        }
310    }
311
312    pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
313        if !self.mode.is_indentable() {
314            cx.propagate();
315            return;
316        };
317
318        let tab_indent = self.mode.tab_size().to_string();
319        let selected_range = self.selected_range;
320        let mut removed_len = 0;
321        let is_selected = !self.selected_range.is_empty();
322
323        if is_selected || block {
324            let start_offset = self.start_of_line_of_selection(window, cx);
325            let mut offset = start_offset;
326
327            let selected_text = self
328                .text_for_range(
329                    self.range_to_utf16(&(offset..selected_range.end)),
330                    &mut None,
331                    window,
332                    cx,
333                )
334                .unwrap_or("".into());
335
336            for line in selected_text.split('\n') {
337                if line.starts_with(tab_indent.as_ref()) {
338                    self.replace_text_in_range_silent(
339                        Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
340                        "",
341                        window,
342                        cx,
343                    );
344                    removed_len += tab_indent.len();
345
346                    // +1 for "\n"
347                    offset += line.len().saturating_sub(tab_indent.len()) + 1;
348                } else {
349                    offset += line.len() + 1;
350                }
351            }
352
353            if is_selected {
354                self.selected_range =
355                    (start_offset..selected_range.end.saturating_sub(removed_len)).into();
356            } else {
357                self.selected_range = (selected_range.start.saturating_sub(removed_len)
358                    ..selected_range.end.saturating_sub(removed_len))
359                    .into();
360            }
361        } else {
362            // Selected none
363            let start_offset = self.selected_range.start;
364            let offset = self.start_of_line_of_selection(window, cx);
365            let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
366            // FIXME: To improve performance
367            if self
368                .text
369                .slice(offset..self.text.len())
370                .to_string()
371                .starts_with(tab_indent.as_ref())
372            {
373                self.replace_text_in_range_silent(
374                    Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
375                    "",
376                    window,
377                    cx,
378                );
379                removed_len = tab_indent.len();
380                let new_offset = start_offset.saturating_sub(removed_len);
381                self.selected_range = (new_offset..new_offset).into();
382            }
383        }
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use ropey::RopeSlice;
390
391    use super::TabSize;
392
393    #[test]
394    fn test_tab_size() {
395        let tab = TabSize {
396            tab_size: 2,
397            hard_tabs: false,
398        };
399        assert_eq!(tab.to_string(), "  ");
400        let tab = TabSize {
401            tab_size: 4,
402            hard_tabs: false,
403        };
404        assert_eq!(tab.to_string(), "    ");
405
406        let tab = TabSize {
407            tab_size: 2,
408            hard_tabs: true,
409        };
410        assert_eq!(tab.to_string(), "\t");
411        let tab = TabSize {
412            tab_size: 4,
413            hard_tabs: true,
414        };
415        assert_eq!(tab.to_string(), "\t");
416    }
417
418    #[test]
419    fn test_tab_size_indent_count() {
420        let tab = TabSize {
421            tab_size: 4,
422            hard_tabs: false,
423        };
424        assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
425        assert_eq!(tab.indent_count(&RopeSlice::from("  abc")), 2);
426        assert_eq!(tab.indent_count(&RopeSlice::from("    abc")), 4);
427        assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
428        assert_eq!(tab.indent_count(&RopeSlice::from("  \tabc")), 6);
429        assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc  ")), 6);
430        assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
431    }
432}