1use crate::input::InputModeKind;
2use crate::input::{
3 Indent, IndentInline, InputBaseState, Outdent, OutdentInline, RopeExt, cursor::CursorSelection,
4 element::TextElement, layout::LastLayout, mode::LayoutMode,
5};
6use gpui::{
7 Bounds, Context, Hsla, Path, PathBuilder, Pixels, SharedString, TextRun, TextStyle, Window,
8 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' => {
44 if self.tab_size > 0 {
45 count += self.tab_size - count % self.tab_size;
46 }
47 }
48 ' ' => count += 1,
49 _ => break,
50 }
51 }
52 count
53 }
54}
55
56impl LayoutMode {
57 #[inline]
62 pub(super) fn is_indentable(&self) -> bool {
63 matches!(
64 self,
65 LayoutMode::PlainText { .. } | LayoutMode::CodeEditor { .. }
66 )
67 }
68
69 #[inline]
70 pub(super) fn has_indent_guides(&self) -> bool {
71 match self {
72 LayoutMode::CodeEditor { indent_guides, .. } => *indent_guides,
73 _ => false,
74 }
75 }
76
77 #[inline]
78 pub(super) fn tab_size(&self) -> TabSize {
79 match self {
80 LayoutMode::PlainText { tab, .. } => *tab,
81 LayoutMode::CodeEditor { tab, .. } => *tab,
82 _ => TabSize::default(),
83 }
84 }
85}
86
87impl<M: InputModeKind> TextElement<M> {
88 fn measure_indent_width(&self, style: &TextStyle, column: usize, window: &Window) -> Pixels {
90 let font_size = style.font_size.to_pixels(window.rem_size());
91 let layout = window.text_system().shape_line(
92 SharedString::from(" ".repeat(column)),
93 font_size,
94 &[TextRun {
95 len: column,
96 font: style.font(),
97 color: Hsla::default(),
98 background_color: None,
99 strikethrough: None,
100 underline: None,
101 }],
102 None,
103 );
104
105 layout.width
106 }
107
108 pub(super) fn layout_indent_guides(
109 &self,
110 state: &InputBaseState<M>,
111 bounds: &Bounds<Pixels>,
112 last_layout: &LastLayout,
113 text_style: &TextStyle,
114 window: &mut Window,
115 ) -> Option<Path<Pixels>> {
116 if !state.is_multi_line() || !state.mode.has_indent_guides() {
117 return None;
118 }
119
120 let indent_width =
121 self.measure_indent_width(text_style, state.mode.tab_size().tab_size, window);
122
123 let tab_size = state.mode.tab_size();
124 let line_height = last_layout.line_height;
125 let mut builder = PathBuilder::stroke(px(1.));
126 let mut offset_y = last_layout.visible_top;
127 let mut last_indents = vec![];
128
129 for (&buffer_line, line_layout) in last_layout
130 .visible_buffer_lines
131 .iter()
132 .zip(last_layout.lines.iter())
133 {
134 let line = state.text.slice_line(buffer_line);
135 let mut current_indents = vec![];
136 if line.len() > 0 {
137 let indent_count = tab_size.indent_count(&line);
138 for offset in (0..indent_count).step_by(tab_size.tab_size) {
139 let x = if indent_count > 0 {
140 indent_width * offset as f32 / tab_size.tab_size as f32
141 } else {
142 px(0.)
143 };
144
145 let pos = point(x + last_layout.line_number_width, offset_y);
146
147 builder.move_to(pos);
148 builder.line_to(point(pos.x, pos.y + line_height));
149 current_indents.push(pos.x);
150 }
151 } else if last_indents.len() > 0 {
152 for x in &last_indents {
153 let pos = point(*x, offset_y);
154 builder.move_to(pos);
155 builder.line_to(point(pos.x, pos.y + line_height));
156 }
157 current_indents = last_indents.clone();
158 }
159
160 offset_y += line_layout.wrapped_lines.len() * line_height;
161 last_indents = current_indents;
162 }
163
164 builder.translate(bounds.origin);
165 let path = builder.build().unwrap();
166 Some(path)
167 }
168}
169
170impl InputBaseState<crate::input::EditorMode> {
172 #[doc(hidden)]
174 pub fn indent_guides(mut self, indent_guides: bool) -> Self {
175 if let LayoutMode::CodeEditor {
176 indent_guides: l, ..
177 } = &mut self.mode
178 {
179 *l = indent_guides;
180 }
181 self
182 }
183
184 pub fn set_indent_guides(
186 &mut self,
187 indent_guides: bool,
188 _: &mut Window,
189 cx: &mut Context<Self>,
190 ) {
191 if let LayoutMode::CodeEditor {
192 indent_guides: l, ..
193 } = &mut self.mode
194 {
195 *l = indent_guides;
196 }
197 cx.notify();
198 }
199}
200
201impl<M: InputModeKind> InputBaseState<M> {
202 pub(super) fn indent_inline(
203 &mut self,
204 _: &IndentInline,
205 window: &mut Window,
206 cx: &mut Context<Self>,
207 ) {
208 if M::accept_inline_completion(self, window, cx) {
210 return;
211 }
212 self.indent(false, window, cx);
213 }
214
215 pub(super) fn indent_block(&mut self, _: &Indent, window: &mut Window, cx: &mut Context<Self>) {
216 self.indent(true, window, cx);
217 }
218
219 pub(super) fn outdent_inline(
220 &mut self,
221 _: &OutdentInline,
222 window: &mut Window,
223 cx: &mut Context<Self>,
224 ) {
225 self.outdent(false, window, cx);
226 }
227
228 pub(super) fn outdent_block(
229 &mut self,
230 _: &Outdent,
231 window: &mut Window,
232 cx: &mut Context<Self>,
233 ) {
234 self.outdent(true, window, cx);
235 }
236
237 pub(super) fn indent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
238 self.apply_indent(IndentDirection::Indent, block, window, cx);
239 }
240
241 pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
242 self.apply_indent(IndentDirection::Outdent, block, window, cx);
243 }
244
245 fn apply_indent(
251 &mut self,
252 direction: IndentDirection,
253 block: bool,
254 window: &mut Window,
255 cx: &mut Context<Self>,
256 ) {
257 if !self.is_editable() || !self.is_multi_line() || !self.mode.is_indentable() {
258 cx.propagate();
259 return;
260 }
261
262 let tab_indent = self.mode.tab_size().to_string();
263 let tab_len = tab_indent.len();
264
265 let has_non_collapsed = self.selections.iter().any(|sel| !sel.is_collapsed());
267 let use_block = has_non_collapsed || block;
268
269 let before: Vec<CursorSelection> = self.selections.iter().copied().collect();
270
271 let (edits, new_selections) = if use_block {
272 self.compute_block_indent(direction, &tab_indent, tab_len)
273 } else {
274 self.compute_inline_indent(direction, tab_len)
275 };
276
277 if edits.is_empty() {
278 return;
279 }
280
281 self.undo_manager.begin_transaction();
282 self.replace_text_in_ranges(&edits, window, cx);
283 self.selections.replace_all(new_selections);
284 let after: Vec<CursorSelection> = self.selections.iter().copied().collect();
285 self.undo_manager.record_selections(before, after);
286 self.undo_manager.commit_transaction();
287
288 self.scroll_to(self.cursor(), None, cx);
289 cx.notify();
290 }
291
292 fn compute_block_indent(
295 &self,
296 direction: IndentDirection,
297 tab_indent: &str,
298 tab_len: usize,
299 ) -> (Vec<(std::ops::Range<usize>, String)>, Vec<CursorSelection>) {
300 let mut rows: std::collections::HashSet<usize> = std::collections::HashSet::new();
301 for sel in self.selections.iter() {
302 let start_row = self.text.offset_to_point(sel.start).row;
303 let end_row = self.text.offset_to_point(sel.end).row;
304 for row in start_row..=end_row {
305 rows.insert(row);
306 }
307 }
308
309 let mut rows: Vec<usize> = rows.into_iter().collect();
310 rows.sort_unstable();
311
312 let mut edits: Vec<(std::ops::Range<usize>, String)> = Vec::new();
313 for row in rows {
314 let line_start = self.text.line_start_offset(row);
315 match direction {
316 IndentDirection::Indent => {
317 edits.push((line_start..line_start, tab_indent.to_string()));
318 }
319 IndentDirection::Outdent => {
320 if self
321 .text
322 .slice(line_start..)
323 .chars()
324 .take(tab_indent.chars().count())
325 .eq(tab_indent.chars())
326 {
327 edits.push((line_start..line_start + tab_len, String::new()));
328 }
329 }
330 }
331 }
332
333 let map_offset = |offset: usize| match direction {
336 IndentDirection::Indent => {
337 offset + edits.partition_point(|(range, _)| range.start <= offset) * tab_len
338 }
339 IndentDirection::Outdent => {
340 let preceding = edits.partition_point(|(range, _)| range.end <= offset);
341 let partial = edits
342 .get(preceding)
343 .map_or(0, |(range, _)| offset.saturating_sub(range.start));
344 offset - preceding * tab_len - partial
345 }
346 };
347 let new_selections = self
348 .selections
349 .iter()
350 .map(|sel| {
351 let mut selection = *sel;
352 selection.start = map_offset(sel.start);
353 selection.end = map_offset(sel.end);
354 selection.column_anchor = None;
355 selection
356 })
357 .collect();
358
359 (edits, new_selections)
360 }
361
362 fn compute_inline_indent(
365 &self,
366 direction: IndentDirection,
367 tab_len: usize,
368 ) -> (Vec<(std::ops::Range<usize>, String)>, Vec<CursorSelection>) {
369 let tab_indent = self.mode.tab_size().to_string();
370
371 let mut ranges: Vec<std::ops::Range<usize>> = Vec::with_capacity(self.selections.len());
374 for sel in self.selections.iter() {
375 let cursor = sel.cursor_offset();
376 match direction {
377 IndentDirection::Indent => ranges.push(cursor..cursor),
378 IndentDirection::Outdent => {
379 let row = self.text.offset_to_point(cursor).row;
380 let start = self.text.line_start_offset(row);
381 if self
382 .text
383 .slice(start..)
384 .chars()
385 .take(tab_indent.chars().count())
386 .eq(tab_indent.chars())
387 {
388 ranges.push(start..start + tab_len);
389 }
390 }
391 }
392 }
393
394 ranges.sort_by_key(|range| range.start);
396 let mut edits: Vec<(std::ops::Range<usize>, String)> = Vec::new();
397 let mut last_end: Option<usize> = None;
398 for range in ranges {
399 if let Some(last_end) = last_end {
400 if range.start < last_end {
401 continue;
402 }
403 }
404 last_end = Some(range.end);
405 let text = match direction {
406 IndentDirection::Indent => tab_indent.to_string(),
407 IndentDirection::Outdent => String::new(),
408 };
409 edits.push((range, text));
410 }
411
412 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(self.selections.len());
415 for sel in self.selections.iter() {
416 let cursor = sel.cursor_offset();
417 let new_offset = match direction {
418 IndentDirection::Indent => {
419 let inserted_before = edits
420 .iter()
421 .filter(|(range, _)| range.start <= cursor)
422 .count();
423 cursor + inserted_before * tab_len
424 }
425 IndentDirection::Outdent => {
426 let removed_before: usize = edits
427 .iter()
428 .map(|(range, _)| range.end.min(cursor) - range.start.min(cursor))
429 .sum();
430 cursor - removed_before
431 }
432 };
433 let mut selection = CursorSelection::new(sel.id, new_offset, new_offset);
434 selection.column_anchor = None;
435 new_selections.push(selection);
436 }
437
438 (edits, new_selections)
439 }
440}
441
442#[derive(Debug, Copy, Clone, PartialEq)]
443enum IndentDirection {
444 Indent,
445 Outdent,
446}
447
448#[cfg(test)]
449mod tests {
450 use ropey::RopeSlice;
451
452 use super::TabSize;
453
454 #[test]
455 fn test_tab_size() {
456 let tab = TabSize {
457 tab_size: 2,
458 hard_tabs: false,
459 };
460 assert_eq!(tab.to_string(), " ");
461 let tab = TabSize {
462 tab_size: 4,
463 hard_tabs: false,
464 };
465 assert_eq!(tab.to_string(), " ");
466
467 let tab = TabSize {
468 tab_size: 2,
469 hard_tabs: true,
470 };
471 assert_eq!(tab.to_string(), "\t");
472 let tab = TabSize {
473 tab_size: 4,
474 hard_tabs: true,
475 };
476 assert_eq!(tab.to_string(), "\t");
477 }
478
479 #[test]
480 fn test_tab_size_indent_count() {
481 let tab = TabSize {
482 tab_size: 4,
483 hard_tabs: false,
484 };
485 assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
486 assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 2);
487 assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 4);
488 assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
489 assert_eq!(tab.indent_count(&RopeSlice::from(" \tabc")), 4);
490 assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc ")), 5);
491 assert_eq!(tab.indent_count(&RopeSlice::from(" \t \tabc")), 8);
492 assert_eq!(tab.indent_count(&RopeSlice::from(" \tabc")), 8);
493 assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
494 }
495}
496
497impl<M: crate::input::MultiLineMode> InputBaseState<M> {
499 #[doc(hidden)]
501 pub fn tab_size(mut self, tab: TabSize) -> Self {
502 match &mut self.mode {
503 LayoutMode::PlainText { tab: t, .. } => *t = tab,
504 LayoutMode::CodeEditor { tab: t, .. } => *t = tab,
505 _ => {}
506 }
507 self
508 }
509
510 pub fn set_tab_size(&mut self, tab: TabSize, cx: &mut Context<Self>) {
511 match &mut self.mode {
512 LayoutMode::PlainText { tab: value, .. }
513 | LayoutMode::CodeEditor { tab: value, .. } => *value = tab,
514 _ => {}
515 }
516 cx.notify();
517 }
518}