1use crate::input::InputModeKind;
2use crate::input::{
3 Indent, IndentInline, InputBaseState, Outdent, OutdentInline, RopeExt, element::TextElement,
4 layout::LastLayout, mode::LayoutMode,
5};
6use gpui::{
7 Bounds, Context, EntityInputHandler as _, Hsla, Path, PathBuilder, Pixels, SharedString,
8 TextRun, TextStyle, Window, point, px,
9};
10use ropey::RopeSlice;
11
12#[derive(Debug, Copy, Clone)]
13pub struct TabSize {
14 pub tab_size: usize,
16 pub hard_tabs: bool,
18}
19
20impl Default for TabSize {
21 fn default() -> Self {
22 Self {
23 tab_size: 2,
24 hard_tabs: false,
25 }
26 }
27}
28
29impl TabSize {
30 pub(super) fn to_string(&self) -> SharedString {
31 if self.hard_tabs {
32 "\t".into()
33 } else {
34 " ".repeat(self.tab_size).into()
35 }
36 }
37
38 pub fn indent_count(&self, line: &RopeSlice) -> usize {
40 let mut count = 0;
41 for ch in line.chars() {
42 match ch {
43 '\t' => count += self.tab_size,
44 ' ' => count += 1,
45 _ => break,
46 }
47 }
48 count
49 }
50}
51
52impl LayoutMode {
53 #[inline]
58 pub(super) fn is_indentable(&self) -> bool {
59 matches!(
60 self,
61 LayoutMode::PlainText { .. } | LayoutMode::CodeEditor { .. }
62 )
63 }
64
65 #[inline]
66 pub(super) fn has_indent_guides(&self) -> bool {
67 match self {
68 LayoutMode::CodeEditor { indent_guides, .. } => *indent_guides,
69 _ => false,
70 }
71 }
72
73 #[inline]
74 pub(super) fn tab_size(&self) -> TabSize {
75 match self {
76 LayoutMode::PlainText { tab, .. } => *tab,
77 LayoutMode::CodeEditor { tab, .. } => *tab,
78 _ => TabSize::default(),
79 }
80 }
81}
82
83impl<M: InputModeKind> TextElement<M> {
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: &InputBaseState<M>,
107 bounds: &Bounds<Pixels>,
108 last_layout: &LastLayout,
109 text_style: &TextStyle,
110 window: &mut Window,
111 ) -> Option<Path<Pixels>> {
112 if !state.is_multi_line() || !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 mut builder = PathBuilder::stroke(px(1.));
122 let mut offset_y = last_layout.visible_top;
123 let mut last_indents = vec![];
124
125 for (&buffer_line, line_layout) in last_layout
126 .visible_buffer_lines
127 .iter()
128 .zip(last_layout.lines.iter())
129 {
130 let line = state.text.slice_line(buffer_line);
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 InputBaseState<crate::input::EditorMode> {
168 #[doc(hidden)]
170 pub fn indent_guides(mut self, indent_guides: bool) -> Self {
171 if let LayoutMode::CodeEditor {
172 indent_guides: l, ..
173 } = &mut self.mode
174 {
175 *l = indent_guides;
176 }
177 self
178 }
179
180 pub fn set_indent_guides(
182 &mut self,
183 indent_guides: bool,
184 _: &mut Window,
185 cx: &mut Context<Self>,
186 ) {
187 if let LayoutMode::CodeEditor {
188 indent_guides: l, ..
189 } = &mut self.mode
190 {
191 *l = indent_guides;
192 }
193 cx.notify();
194 }
195}
196
197impl<M: InputModeKind> InputBaseState<M> {
198 pub(super) fn indent_inline(
199 &mut self,
200 _: &IndentInline,
201 window: &mut Window,
202 cx: &mut Context<Self>,
203 ) {
204 if M::accept_inline_completion(self, window, cx) {
206 return;
207 }
208 self.indent(false, window, cx);
209 }
210
211 pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
212 self.indent(true, window, cx);
213 }
214
215 pub(super) fn outdent_inline(
216 &mut self,
217 _: &OutdentInline,
218 window: &mut Window,
219 cx: &mut Context<Self>,
220 ) {
221 self.outdent(false, window, cx);
222 }
223
224 pub(super) fn outdent_block(
225 &mut self,
226 _: &Outdent,
227 window: &mut Window,
228 cx: &mut Context<Self>,
229 ) {
230 self.outdent(true, window, cx);
231 }
232
233 pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
234 if !self.is_multi_line() || !self.mode.is_indentable() {
235 cx.propagate();
236 return;
237 };
238
239 let tab_indent = self.mode.tab_size().to_string();
240 let selected_range = self.selected_range;
241 let mut added_len = 0;
242 let is_selected = !self.selected_range.is_empty();
243
244 if is_selected || block {
245 let start_offset = self.start_of_line_of_selection(window, cx);
246 let mut offset = start_offset;
247
248 let selected_text = self
249 .text_for_range(
250 self.range_to_utf16(&(offset..selected_range.end)),
251 &mut None,
252 window,
253 cx,
254 )
255 .unwrap_or("".into());
256
257 for line in selected_text.split('\n') {
258 self.replace_text_in_range_silent(
259 Some(self.range_to_utf16(&(offset..offset))),
260 &tab_indent,
261 window,
262 cx,
263 );
264 added_len += tab_indent.len();
265 offset += line.len() + tab_indent.len() + 1;
267 }
268
269 if is_selected {
270 self.selected_range = (start_offset..selected_range.end + added_len).into();
271 } else {
272 self.selected_range =
273 (selected_range.start + added_len..selected_range.end + added_len).into();
274 }
275 } else {
276 let offset = self.selected_range.start;
278 self.replace_text_in_range_silent(
279 Some(self.range_to_utf16(&(offset..offset))),
280 &tab_indent,
281 window,
282 cx,
283 );
284 added_len = tab_indent.len();
285
286 self.selected_range =
287 (selected_range.start + added_len..selected_range.end + added_len).into();
288 }
289 }
290
291 pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
292 if !self.is_multi_line() || !self.mode.is_indentable() {
293 cx.propagate();
294 return;
295 };
296
297 let tab_indent = self.mode.tab_size().to_string();
298 let selected_range = self.selected_range;
299 let mut removed_len = 0;
300 let is_selected = !self.selected_range.is_empty();
301
302 if is_selected || block {
303 let start_offset = self.start_of_line_of_selection(window, cx);
304 let mut offset = start_offset;
305
306 let selected_text = self
307 .text_for_range(
308 self.range_to_utf16(&(offset..selected_range.end)),
309 &mut None,
310 window,
311 cx,
312 )
313 .unwrap_or("".into());
314
315 for line in selected_text.split('\n') {
316 if line.starts_with(tab_indent.as_ref()) {
317 self.replace_text_in_range_silent(
318 Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
319 "",
320 window,
321 cx,
322 );
323 removed_len += tab_indent.len();
324
325 offset += line.len().saturating_sub(tab_indent.len()) + 1;
327 } else {
328 offset += line.len() + 1;
329 }
330 }
331
332 if is_selected {
333 self.selected_range =
334 (start_offset..selected_range.end.saturating_sub(removed_len)).into();
335 } else {
336 self.selected_range = (selected_range.start.saturating_sub(removed_len)
337 ..selected_range.end.saturating_sub(removed_len))
338 .into();
339 }
340 } else {
341 let start_offset = self.selected_range.start;
343 let offset = self.start_of_line_of_selection(window, cx);
344 let offset = self.offset_from_utf16(self.offset_to_utf16(offset));
345
346 if self
347 .text
348 .slice(offset..self.text.len())
349 .chars()
350 .take(tab_indent.chars().count())
351 .eq(tab_indent.chars())
352 {
353 self.replace_text_in_range_silent(
354 Some(self.range_to_utf16(&(offset..offset + tab_indent.len()))),
355 "",
356 window,
357 cx,
358 );
359 removed_len = tab_indent.len();
360 let new_offset = start_offset.saturating_sub(removed_len);
361 self.selected_range = (new_offset..new_offset).into();
362 }
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use ropey::RopeSlice;
370
371 use super::TabSize;
372
373 #[test]
374 fn test_tab_size() {
375 let tab = TabSize {
376 tab_size: 2,
377 hard_tabs: false,
378 };
379 assert_eq!(tab.to_string(), " ");
380 let tab = TabSize {
381 tab_size: 4,
382 hard_tabs: false,
383 };
384 assert_eq!(tab.to_string(), " ");
385
386 let tab = TabSize {
387 tab_size: 2,
388 hard_tabs: true,
389 };
390 assert_eq!(tab.to_string(), "\t");
391 let tab = TabSize {
392 tab_size: 4,
393 hard_tabs: true,
394 };
395 assert_eq!(tab.to_string(), "\t");
396 }
397
398 #[test]
399 fn test_tab_size_indent_count() {
400 let tab = TabSize {
401 tab_size: 4,
402 hard_tabs: false,
403 };
404 assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
405 assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 2);
406 assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 4);
407 assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
408 assert_eq!(tab.indent_count(&RopeSlice::from(" \tabc")), 6);
409 assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc ")), 6);
410 assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
411 }
412}
413
414impl<M: crate::input::MultiLineMode> InputBaseState<M> {
416 #[doc(hidden)]
418 pub fn tab_size(mut self, tab: TabSize) -> Self {
419 match &mut self.mode {
420 LayoutMode::PlainText { tab: t, .. } => *t = tab,
421 LayoutMode::CodeEditor { tab: t, .. } => *t = tab,
422 _ => {}
423 }
424 self
425 }
426
427 pub fn set_tab_size(&mut self, tab: TabSize, cx: &mut Context<Self>) {
428 match &mut self.mode {
429 LayoutMode::PlainText { tab: value, .. }
430 | LayoutMode::CodeEditor { tab: value, .. } => *value = tab,
431 _ => {}
432 }
433 cx.notify();
434 }
435}