1use gpui::{
2 Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString,
3 TextRun, TextStyle, Window, point, px,
4};
5use ropey::RopeSlice;
6
7use crate::{
8 RopeExt,
9 input::{
10 Indent, IndentInline, InputState, LastLayout, Outdent, OutdentInline, element::TextElement,
11 mode::InputMode,
12 },
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 match self {
60 InputMode::PlainText { multi_line, .. } | InputMode::CodeEditor { multi_line, .. } => {
61 *multi_line
62 }
63 _ => false,
64 }
65 }
66
67 #[inline]
68 pub(super) fn has_indent_guides(&self) -> bool {
69 match self {
70 InputMode::CodeEditor {
71 indent_guides,
72 multi_line,
73 ..
74 } => *indent_guides && *multi_line,
75 _ => false,
76 }
77 }
78
79 #[inline]
80 pub(super) fn tab_size(&self) -> TabSize {
81 match self {
82 InputMode::PlainText { tab, .. } => *tab,
83 InputMode::CodeEditor { tab, .. } => *tab,
84 _ => TabSize::default(),
85 }
86 }
87}
88
89impl TextElement {
90 fn measure_indent_width(&self, style: &TextStyle, column: usize, window: &Window) -> Pixels {
92 let font_size = style.font_size.to_pixels(window.rem_size());
93 let layout = window.text_system().shape_line(
94 SharedString::from(" ".repeat(column)),
95 font_size,
96 &[TextRun {
97 len: column,
98 font: style.font(),
99 color: Hsla::default(),
100 background_color: None,
101 strikethrough: None,
102 underline: None,
103 }],
104 None,
105 );
106
107 layout.width
108 }
109
110 pub(super) fn layout_indent_guides(
111 &self,
112 state: &InputState,
113 bounds: &Bounds<Pixels>,
114 last_layout: &LastLayout,
115 text_style: &TextStyle,
116 window: &mut Window,
117 ) -> Option<Path<Pixels>> {
118 if !state.mode.has_indent_guides() {
119 return None;
120 }
121
122 let indent_width =
123 self.measure_indent_width(text_style, state.mode.tab_size().tab_size, window);
124
125 let tab_size = state.mode.tab_size();
126 let line_height = last_layout.line_height;
127 let visible_range = last_layout.visible_range.clone();
128 let mut builder = PathBuilder::stroke(px(1.));
129 let mut offset_y = last_layout.visible_top;
130 let mut last_indents = vec![];
131 for ix in visible_range {
132 let line = state.text.slice_line(ix);
133 let Some(line_layout) = last_layout.line(ix) else {
134 continue;
135 };
136
137 let mut current_indents = vec![];
138 if line.len() > 0 {
139 let indent_count = tab_size.indent_count(&line);
140 for offset in (0..indent_count).step_by(tab_size.tab_size) {
141 let x = if indent_count > 0 {
142 indent_width * offset as f32 / tab_size.tab_size as f32
143 } else {
144 px(0.)
145 };
146
147 let pos = point(x + last_layout.line_number_width, offset_y);
148
149 builder.move_to(pos);
150 builder.line_to(point(pos.x, pos.y + line_height));
151 current_indents.push(pos.x);
152 }
153 } else if last_indents.len() > 0 {
154 for x in &last_indents {
155 let pos = point(*x, offset_y);
156 builder.move_to(pos);
157 builder.line_to(point(pos.x, pos.y + line_height));
158 }
159 current_indents = last_indents.clone();
160 }
161
162 offset_y += line_layout.wrapped_lines.len() * line_height;
163 last_indents = current_indents;
164 }
165
166 builder.translate(bounds.origin);
167 let path = builder.build().unwrap();
168 Some(path)
169 }
170}
171
172impl InputState {
173 pub fn indent_guides(mut self, indent_guides: bool) -> Self {
177 debug_assert!(self.mode.is_code_editor() && self.mode.is_multi_line());
178 if let InputMode::CodeEditor {
179 indent_guides: l, ..
180 } = &mut self.mode
181 {
182 *l = indent_guides;
183 }
184 self
185 }
186
187 pub fn set_indent_guides(
191 &mut self,
192 indent_guides: bool,
193 _: &mut Window,
194 cx: &mut Context<Self>,
195 ) {
196 debug_assert!(self.mode.is_code_editor());
197 if let InputMode::CodeEditor {
198 indent_guides: l, ..
199 } = &mut self.mode
200 {
201 *l = indent_guides;
202 }
203 cx.notify();
204 }
205
206 pub fn tab_size(mut self, tab: TabSize) -> Self {
210 debug_assert!(self.mode.is_multi_line() || self.mode.is_code_editor());
211 match &mut self.mode {
212 InputMode::PlainText { tab: t, .. } => *t = tab,
213 InputMode::CodeEditor { tab: t, .. } => *t = tab,
214 _ => {}
215 }
216 self
217 }
218
219 pub(super) fn indent_inline(
220 &mut self,
221 _: &IndentInline,
222 window: &mut Window,
223 cx: &mut Context<Self>,
224 ) {
225 self.indent(false, window, cx);
226 }
227
228 pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
229 self.indent(true, window, cx);
230 }
231
232 pub(super) fn outdent_inline(
233 &mut self,
234 _: &OutdentInline,
235 window: &mut Window,
236 cx: &mut Context<Self>,
237 ) {
238 self.outdent(false, window, cx);
239 }
240
241 pub(super) fn outdent_block(
242 &mut self,
243 _: &Outdent,
244 window: &mut Window,
245 cx: &mut Context<Self>,
246 ) {
247 self.outdent(true, window, cx);
248 }
249
250 pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
251 if !self.mode.is_indentable() {
252 cx.propagate();
253 return;
254 };
255
256 let tab_indent = self.mode.tab_size().to_string();
257 let selected_range = self.selected_range;
258 let mut added_len = 0;
259 let is_selected = !self.selected_range.is_empty();
260
261 if is_selected || block {
262 let start_offset = self.start_of_line_of_selection(window, cx);
263 let mut offset = start_offset;
264
265 let selected_text = self
266 .text_for_range(
267 self.range_to_utf16(&(offset..selected_range.end)),
268 &mut None,
269 window,
270 cx,
271 )
272 .unwrap_or("".into());
273
274 for line in selected_text.split('\n') {
275 self.replace_text_in_range_silent(
276 Some(self.range_to_utf16(&(offset..offset))),
277 &tab_indent,
278 window,
279 cx,
280 );
281 added_len += tab_indent.len();
282 offset += line.len() + tab_indent.len() + 1;
284 }
285
286 if is_selected {
287 self.selected_range = (start_offset..selected_range.end + added_len).into();
288 } else {
289 self.selected_range =
290 (selected_range.start + added_len..selected_range.end + added_len).into();
291 }
292 } else {
293 let offset = self.selected_range.start;
295 self.replace_text_in_range_silent(
296 Some(self.range_to_utf16(&(offset..offset))),
297 &tab_indent,
298 window,
299 cx,
300 );
301 added_len = tab_indent.len();
302
303 self.selected_range =
304 (selected_range.start + added_len..selected_range.end + added_len).into();
305 }
306 }
307
308 pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
309 if !self.mode.is_indentable() {
310 cx.propagate();
311 return;
312 };
313
314 let tab_indent = self.mode.tab_size().to_string();
315 let selected_range = self.selected_range;
316 let mut removed_len = 0;
317 let is_selected = !self.selected_range.is_empty();
318
319 if is_selected || block {
320 let start_offset = self.start_of_line_of_selection(window, cx);
321 let mut offset = start_offset;
322
323 let selected_text = self
324 .text_for_range(
325 self.range_to_utf16(&(offset..selected_range.end)),
326 &mut None,
327 window,
328 cx,
329 )
330 .unwrap_or("".into());
331
332 for line in selected_text.split('\n') {
333 if line.starts_with(tab_indent.as_ref()) {
334 self.replace_text_in_range_silent(
335 Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
336 "",
337 window,
338 cx,
339 );
340 removed_len += tab_indent.len();
341
342 offset += line.len().saturating_sub(tab_indent.len()) + 1;
344 } else {
345 offset += line.len() + 1;
346 }
347 }
348
349 if is_selected {
350 self.selected_range =
351 (start_offset..selected_range.end.saturating_sub(removed_len)).into();
352 } else {
353 self.selected_range = (selected_range.start.saturating_sub(removed_len)
354 ..selected_range.end.saturating_sub(removed_len))
355 .into();
356 }
357 } else {
358 let start_offset = self.selected_range.start;
360 let offset = self.start_of_line_of_selection(window, cx);
361 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
362 if self
364 .text
365 .slice(offset..self.text.len())
366 .to_string()
367 .starts_with(tab_indent.as_ref())
368 {
369 self.replace_text_in_range_silent(
370 Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
371 "",
372 window,
373 cx,
374 );
375 removed_len = tab_indent.len();
376 let new_offset = start_offset.saturating_sub(removed_len);
377 self.selected_range = (new_offset..new_offset).into();
378 }
379 }
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use ropey::RopeSlice;
386
387 use super::TabSize;
388
389 #[test]
390 fn test_tab_size() {
391 let tab = TabSize {
392 tab_size: 2,
393 hard_tabs: false,
394 };
395 assert_eq!(tab.to_string(), " ");
396 let tab = TabSize {
397 tab_size: 4,
398 hard_tabs: false,
399 };
400 assert_eq!(tab.to_string(), " ");
401
402 let tab = TabSize {
403 tab_size: 2,
404 hard_tabs: true,
405 };
406 assert_eq!(tab.to_string(), "\t");
407 let tab = TabSize {
408 tab_size: 4,
409 hard_tabs: true,
410 };
411 assert_eq!(tab.to_string(), "\t");
412 }
413
414 #[test]
415 fn test_tab_size_indent_count() {
416 let tab = TabSize {
417 tab_size: 4,
418 hard_tabs: false,
419 };
420 assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
421 assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 2);
422 assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 4);
423 assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
424 assert_eq!(tab.indent_count(&RopeSlice::from(" \tabc")), 6);
425 assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc ")), 6);
426 assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
427 }
428}