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