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