Skip to main content

gpui_base/input/editor/
indent.rs

1use crate::input::InputModeKind;
2use crate::input::{
3    Indent, IndentInline, InputBaseState, Outdent, OutdentInline, RopeExt, element::TextElement,
4    layout::LastLayout, mode::LayoutMode,
5};
6use gpui::{
7    Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString,
8    TextRun, TextStyle, Window, 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        if !self.is_multi_line() || !self.mode.is_indentable() {
235            cx.propagate();
236            return;
237        };
238
239        let tab_indent = self.mode.tab_size().to_string();
240        let selected_range = self.selected_range;
241        let mut added_len = 0;
242        let is_selected = !self.selected_range.is_empty();
243
244        if is_selected || block {
245            let start_offset = self.start_of_line_of_selection(window, cx);
246            let mut offset = start_offset;
247
248            let selected_text = self
249                .text_for_range(
250                    self.range_to_utf16(&(offset..selected_range.end)),
251                    &mut None,
252                    window,
253                    cx,
254                )
255                .unwrap_or("".into());
256
257            for line in selected_text.split('\n') {
258                self.replace_text_in_range_silent(
259                    Some(self.range_to_utf16(&(offset..offset))),
260                    &tab_indent,
261                    window,
262                    cx,
263                );
264                added_len += tab_indent.len();
265                // +1 for "\n", the `\r` is included in the `line`.
266                offset += line.len() + tab_indent.len() + 1;
267            }
268
269            if is_selected {
270                self.selected_range = (start_offset..selected_range.end + added_len).into();
271            } else {
272                self.selected_range =
273                    (selected_range.start + added_len..selected_range.end + added_len).into();
274            }
275        } else {
276            // Selected none
277            let offset = self.selected_range.start;
278            self.replace_text_in_range_silent(
279                Some(self.range_to_utf16(&(offset..offset))),
280                &tab_indent,
281                window,
282                cx,
283            );
284            added_len = tab_indent.len();
285
286            self.selected_range =
287                (selected_range.start + added_len..selected_range.end + added_len).into();
288        }
289    }
290
291    pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
292        if !self.is_multi_line() || !self.mode.is_indentable() {
293            cx.propagate();
294            return;
295        };
296
297        let tab_indent = self.mode.tab_size().to_string();
298        let selected_range = self.selected_range;
299        let mut removed_len = 0;
300        let is_selected = !self.selected_range.is_empty();
301
302        if is_selected || block {
303            let start_offset = self.start_of_line_of_selection(window, cx);
304            let mut offset = start_offset;
305
306            let selected_text = self
307                .text_for_range(
308                    self.range_to_utf16(&(offset..selected_range.end)),
309                    &mut None,
310                    window,
311                    cx,
312                )
313                .unwrap_or("".into());
314
315            for line in selected_text.split('\n') {
316                if line.starts_with(tab_indent.as_ref()) {
317                    self.replace_text_in_range_silent(
318                        Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
319                        "",
320                        window,
321                        cx,
322                    );
323                    removed_len += tab_indent.len();
324
325                    // +1 for "\n"
326                    offset += line.len().saturating_sub(tab_indent.len()) + 1;
327                } else {
328                    offset += line.len() + 1;
329                }
330            }
331
332            if is_selected {
333                self.selected_range =
334                    (start_offset..selected_range.end.saturating_sub(removed_len)).into();
335            } else {
336                self.selected_range = (selected_range.start.saturating_sub(removed_len)
337                    ..selected_range.end.saturating_sub(removed_len))
338                    .into();
339            }
340        } else {
341            // Selected none
342            let start_offset = self.selected_range.start;
343            let offset = self.start_of_line_of_selection(window, cx);
344            let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
345
346            if self
347                .text
348                .slice(offset..self.text.len())
349                .chars()
350                .take(tab_indent.chars().count())
351                .eq(tab_indent.chars())
352            {
353                self.replace_text_in_range_silent(
354                    Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
355                    "",
356                    window,
357                    cx,
358                );
359                removed_len = tab_indent.len();
360                let new_offset = start_offset.saturating_sub(removed_len);
361                self.selected_range = (new_offset..new_offset).into();
362            }
363        }
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use ropey::RopeSlice;
370
371    use super::TabSize;
372
373    #[test]
374    fn test_tab_size() {
375        let tab = TabSize {
376            tab_size: 2,
377            hard_tabs: false,
378        };
379        assert_eq!(tab.to_string(), "  ");
380        let tab = TabSize {
381            tab_size: 4,
382            hard_tabs: false,
383        };
384        assert_eq!(tab.to_string(), "    ");
385
386        let tab = TabSize {
387            tab_size: 2,
388            hard_tabs: true,
389        };
390        assert_eq!(tab.to_string(), "\t");
391        let tab = TabSize {
392            tab_size: 4,
393            hard_tabs: true,
394        };
395        assert_eq!(tab.to_string(), "\t");
396    }
397
398    #[test]
399    fn test_tab_size_indent_count() {
400        let tab = TabSize {
401            tab_size: 4,
402            hard_tabs: false,
403        };
404        assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
405        assert_eq!(tab.indent_count(&RopeSlice::from("  abc")), 2);
406        assert_eq!(tab.indent_count(&RopeSlice::from("    abc")), 4);
407        assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
408        assert_eq!(tab.indent_count(&RopeSlice::from("  \tabc")), 6);
409        assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc  ")), 6);
410        assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
411    }
412}
413
414/// Tab size only means something where there is more than one line to indent.
415impl<M: crate::input::MultiLineMode> InputBaseState<M> {
416    /// Set the tab size for the input.
417    #[doc(hidden)]
418    pub fn tab_size(mut self, tab: TabSize) -> Self {
419        match &mut self.mode {
420            LayoutMode::PlainText { tab: t, .. } => *t = tab,
421            LayoutMode::CodeEditor { tab: t, .. } => *t = tab,
422            _ => {}
423        }
424        self
425    }
426
427    pub fn set_tab_size(&mut self, tab: TabSize, cx: &mut Context<Self>) {
428        match &mut self.mode {
429            LayoutMode::PlainText { tab: value, .. }
430            | LayoutMode::CodeEditor { tab: value, .. } => *value = tab,
431            _ => {}
432        }
433        cx.notify();
434    }
435}