gpui_component/input/
indent.rs

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