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 pub tab_size: usize,
19 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 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 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 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 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 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 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 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 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 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 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}