gpui_component/input/
mode.rs

1use std::rc::Rc;
2use std::{cell::RefCell, ops::Range};
3
4use gpui::{App, SharedString};
5use ropey::Rope;
6use tree_sitter::InputEdit;
7
8use super::text_wrapper::TextWrapper;
9use crate::highlighter::DiagnosticSet;
10use crate::highlighter::SyntaxHighlighter;
11use crate::input::RopeExt as _;
12
13#[derive(Debug, Copy, Clone)]
14pub struct TabSize {
15    /// Default is 2
16    pub tab_size: usize,
17    /// Set true to use `\t` as tab indent, default is false
18    pub hard_tabs: bool,
19}
20
21impl Default for TabSize {
22    fn default() -> Self {
23        Self {
24            tab_size: 2,
25            hard_tabs: false,
26        }
27    }
28}
29
30impl TabSize {
31    pub(super) fn to_string(&self) -> SharedString {
32        if self.hard_tabs {
33            "\t".into()
34        } else {
35            " ".repeat(self.tab_size).into()
36        }
37    }
38}
39
40#[derive(Default, Clone)]
41pub enum InputMode {
42    #[default]
43    SingleLine,
44    MultiLine {
45        tab: TabSize,
46        rows: usize,
47    },
48    AutoGrow {
49        rows: usize,
50        min_rows: usize,
51        max_rows: usize,
52    },
53    CodeEditor {
54        tab: TabSize,
55        rows: usize,
56        /// Show line number
57        line_number: bool,
58        language: SharedString,
59        highlighter: Rc<RefCell<Option<SyntaxHighlighter>>>,
60        diagnostics: DiagnosticSet,
61    },
62}
63
64#[allow(unused)]
65impl InputMode {
66    #[inline]
67    pub(super) fn is_single_line(&self) -> bool {
68        matches!(self, InputMode::SingleLine)
69    }
70
71    #[inline]
72    pub(super) fn is_code_editor(&self) -> bool {
73        matches!(self, InputMode::CodeEditor { .. })
74    }
75
76    #[inline]
77    pub(super) fn is_auto_grow(&self) -> bool {
78        matches!(self, InputMode::AutoGrow { .. })
79    }
80
81    #[inline]
82    pub(super) fn is_multi_line(&self) -> bool {
83        matches!(
84            self,
85            InputMode::MultiLine { .. } | InputMode::AutoGrow { .. } | InputMode::CodeEditor { .. }
86        )
87    }
88
89    pub(super) fn set_rows(&mut self, new_rows: usize) {
90        match self {
91            InputMode::MultiLine { rows, .. } => {
92                *rows = new_rows;
93            }
94            InputMode::CodeEditor { rows, .. } => {
95                *rows = new_rows;
96            }
97            InputMode::AutoGrow {
98                rows,
99                min_rows,
100                max_rows,
101            } => {
102                *rows = new_rows.clamp(*min_rows, *max_rows);
103            }
104            _ => {}
105        }
106    }
107
108    pub(super) fn update_auto_grow(&mut self, text_wrapper: &TextWrapper) {
109        if self.is_single_line() {
110            return;
111        }
112
113        let wrapped_lines = text_wrapper.len();
114        self.set_rows(wrapped_lines);
115    }
116
117    /// At least 1 row be return.
118    pub(super) fn rows(&self) -> usize {
119        match self {
120            InputMode::MultiLine { rows, .. } => *rows,
121            InputMode::CodeEditor { rows, .. } => *rows,
122            InputMode::AutoGrow { rows, .. } => *rows,
123            _ => 1,
124        }
125        .max(1)
126    }
127
128    /// At least 1 row be return.
129    #[allow(unused)]
130    pub(super) fn min_rows(&self) -> usize {
131        match self {
132            InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => 1,
133            InputMode::AutoGrow { min_rows, .. } => *min_rows,
134            _ => 1,
135        }
136        .max(1)
137    }
138
139    #[allow(unused)]
140    pub(super) fn max_rows(&self) -> usize {
141        match self {
142            InputMode::MultiLine { .. } | InputMode::CodeEditor { .. } => usize::MAX,
143            InputMode::AutoGrow { max_rows, .. } => *max_rows,
144            _ => 1,
145        }
146    }
147
148    /// Return false if the mode is not [`InputMode::CodeEditor`].
149    #[allow(unused)]
150    #[inline]
151    pub(super) fn line_number(&self) -> bool {
152        match self {
153            InputMode::CodeEditor { line_number, .. } => *line_number,
154            _ => false,
155        }
156    }
157
158    #[inline]
159    pub(super) fn tab_size(&self) -> Option<&TabSize> {
160        match self {
161            InputMode::MultiLine { tab, .. } => Some(tab),
162            InputMode::CodeEditor { tab, .. } => Some(tab),
163            _ => None,
164        }
165    }
166
167    pub(super) fn update_highlighter(
168        &mut self,
169        selected_range: &Range<usize>,
170        text: &Rope,
171        new_text: &str,
172        force: bool,
173        cx: &mut App,
174    ) {
175        match &self {
176            InputMode::CodeEditor {
177                language,
178                highlighter,
179                ..
180            } => {
181                if !force && highlighter.borrow().is_some() {
182                    return;
183                }
184
185                let mut highlighter = highlighter.borrow_mut();
186                if highlighter.is_none() {
187                    let new_highlighter = SyntaxHighlighter::new(language);
188                    highlighter.replace(new_highlighter);
189                }
190
191                let Some(highlighter) = highlighter.as_mut() else {
192                    return;
193                };
194
195                // When full text changed, the selected_range may be out of bound (The before version).
196                let mut selected_range = selected_range.clone();
197                selected_range.end = selected_range.end.min(text.len());
198
199                // If insert a chart, this is 1.
200                // If backspace or delete, this is -1.
201                // If selected to delete, this is the length of the selected text.
202                // let changed_len = new_text.len() as isize - selected_range.len() as isize;
203                let changed_len = new_text.len() as isize - selected_range.len() as isize;
204                let new_end = (selected_range.end as isize + changed_len) as usize;
205
206                let start_pos = text.offset_to_point(selected_range.start);
207                let old_end_pos = text.offset_to_point(selected_range.end);
208                let new_end_pos = text.offset_to_point(new_end);
209
210                let edit = InputEdit {
211                    start_byte: selected_range.start,
212                    old_end_byte: selected_range.end,
213                    new_end_byte: new_end,
214                    start_position: start_pos,
215                    old_end_position: old_end_pos,
216                    new_end_position: new_end_pos,
217                };
218
219                highlighter.update(Some(edit), text);
220            }
221            _ => {}
222        }
223    }
224
225    #[allow(unused)]
226    pub(super) fn diagnostics(&self) -> Option<&DiagnosticSet> {
227        match self {
228            InputMode::CodeEditor { diagnostics, .. } => Some(diagnostics),
229            _ => None,
230        }
231    }
232
233    pub(super) fn diagnostics_mut(&mut self) -> Option<&mut DiagnosticSet> {
234        match self {
235            InputMode::CodeEditor { diagnostics, .. } => Some(diagnostics),
236            _ => None,
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::TabSize;
244
245    #[test]
246    fn test_tab_size() {
247        let tab = TabSize {
248            tab_size: 2,
249            hard_tabs: false,
250        };
251        assert_eq!(tab.to_string(), "  ");
252        let tab = TabSize {
253            tab_size: 4,
254            hard_tabs: false,
255        };
256        assert_eq!(tab.to_string(), "    ");
257
258        let tab = TabSize {
259            tab_size: 2,
260            hard_tabs: true,
261        };
262        assert_eq!(tab.to_string(), "\t");
263        let tab = TabSize {
264            tab_size: 4,
265            hard_tabs: true,
266        };
267        assert_eq!(tab.to_string(), "\t");
268    }
269}