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