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 if self.accept_inline_completion(window, cx) {
227 return;
228 }
229 self.indent(false, window, cx);
230 }
231
232 pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
233 self.indent(true, window, cx);
234 }
235
236 pub(super) fn outdent_inline(
237 &mut self,
238 _: &OutdentInline,
239 window: &mut Window,
240 cx: &mut Context<Self>,
241 ) {
242 self.outdent(false, window, cx);
243 }
244
245 pub(super) fn outdent_block(
246 &mut self,
247 _: &Outdent,
248 window: &mut Window,
249 cx: &mut Context<Self>,
250 ) {
251 self.outdent(true, window, cx);
252 }
253
254 pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
255 if !self.mode.is_indentable() {
256 cx.propagate();
257 return;
258 };
259
260 let tab_indent = self.mode.tab_size().to_string();
261 let selected_range = self.selected_range;
262 let mut added_len = 0;
263 let is_selected = !self.selected_range.is_empty();
264
265 if is_selected || block {
266 let start_offset = self.start_of_line_of_selection(window, cx);
267 let mut offset = start_offset;
268
269 let selected_text = self
270 .text_for_range(
271 self.range_to_utf16(&(offset..selected_range.end)),
272 &mut None,
273 window,
274 cx,
275 )
276 .unwrap_or("".into());
277
278 for line in selected_text.split('\n') {
279 self.replace_text_in_range_silent(
280 Some(self.range_to_utf16(&(offset..offset))),
281 &tab_indent,
282 window,
283 cx,
284 );
285 added_len += tab_indent.len();
286 offset += line.len() + tab_indent.len() + 1;
288 }
289
290 if is_selected {
291 self.selected_range = (start_offset..selected_range.end + added_len).into();
292 } else {
293 self.selected_range =
294 (selected_range.start + added_len..selected_range.end + added_len).into();
295 }
296 } else {
297 let offset = self.selected_range.start;
299 self.replace_text_in_range_silent(
300 Some(self.range_to_utf16(&(offset..offset))),
301 &tab_indent,
302 window,
303 cx,
304 );
305 added_len = tab_indent.len();
306
307 self.selected_range =
308 (selected_range.start + added_len..selected_range.end + added_len).into();
309 }
310 }
311
312 pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
313 if !self.mode.is_indentable() {
314 cx.propagate();
315 return;
316 };
317
318 let tab_indent = self.mode.tab_size().to_string();
319 let selected_range = self.selected_range;
320 let mut removed_len = 0;
321 let is_selected = !self.selected_range.is_empty();
322
323 if is_selected || block {
324 let start_offset = self.start_of_line_of_selection(window, cx);
325 let mut offset = start_offset;
326
327 let selected_text = self
328 .text_for_range(
329 self.range_to_utf16(&(offset..selected_range.end)),
330 &mut None,
331 window,
332 cx,
333 )
334 .unwrap_or("".into());
335
336 for line in selected_text.split('\n') {
337 if line.starts_with(tab_indent.as_ref()) {
338 self.replace_text_in_range_silent(
339 Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
340 "",
341 window,
342 cx,
343 );
344 removed_len += tab_indent.len();
345
346 offset += line.len().saturating_sub(tab_indent.len()) + 1;
348 } else {
349 offset += line.len() + 1;
350 }
351 }
352
353 if is_selected {
354 self.selected_range =
355 (start_offset..selected_range.end.saturating_sub(removed_len)).into();
356 } else {
357 self.selected_range = (selected_range.start.saturating_sub(removed_len)
358 ..selected_range.end.saturating_sub(removed_len))
359 .into();
360 }
361 } else {
362 let start_offset = self.selected_range.start;
364 let offset = self.start_of_line_of_selection(window, cx);
365 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
366 if self
368 .text
369 .slice(offset..self.text.len())
370 .to_string()
371 .starts_with(tab_indent.as_ref())
372 {
373 self.replace_text_in_range_silent(
374 Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
375 "",
376 window,
377 cx,
378 );
379 removed_len = tab_indent.len();
380 let new_offset = start_offset.saturating_sub(removed_len);
381 self.selected_range = (new_offset..new_offset).into();
382 }
383 }
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use ropey::RopeSlice;
390
391 use super::TabSize;
392
393 #[test]
394 fn test_tab_size() {
395 let tab = TabSize {
396 tab_size: 2,
397 hard_tabs: false,
398 };
399 assert_eq!(tab.to_string(), " ");
400 let tab = TabSize {
401 tab_size: 4,
402 hard_tabs: false,
403 };
404 assert_eq!(tab.to_string(), " ");
405
406 let tab = TabSize {
407 tab_size: 2,
408 hard_tabs: true,
409 };
410 assert_eq!(tab.to_string(), "\t");
411 let tab = TabSize {
412 tab_size: 4,
413 hard_tabs: true,
414 };
415 assert_eq!(tab.to_string(), "\t");
416 }
417
418 #[test]
419 fn test_tab_size_indent_count() {
420 let tab = TabSize {
421 tab_size: 4,
422 hard_tabs: false,
423 };
424 assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
425 assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 2);
426 assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 4);
427 assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
428 assert_eq!(tab.indent_count(&RopeSlice::from(" \tabc")), 6);
429 assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc ")), 6);
430 assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
431 }
432}