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' => 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 self.apply_indent(IndentDirection::Indent, block, window, cx);
235 }
236
237 pub(super) fn outdent(&mut self, block: bool, window: &mut Window, cx: &mut Context<Self>) {
238 self.apply_indent(IndentDirection::Outdent, block, window, cx);
239 }
240
241 fn apply_indent(
247 &mut self,
248 direction: IndentDirection,
249 block: bool,
250 window: &mut Window,
251 cx: &mut Context<Self>,
252 ) {
253 if !self.is_editable() || !self.is_multi_line() || !self.mode.is_indentable() {
254 cx.propagate();
255 return;
256 }
257
258 let tab_indent = self.mode.tab_size().to_string();
259 let tab_len = tab_indent.len();
260
261 let has_non_collapsed = self.selections.iter().any(|sel| !sel.is_collapsed());
263 let use_block = has_non_collapsed || block;
264
265 let before: Vec<CursorSelection> = self.selections.iter().copied().collect();
266
267 let (edits, new_selections) = if use_block {
268 self.compute_block_indent(direction, &tab_indent, tab_len)
269 } else {
270 self.compute_inline_indent(direction, tab_len)
271 };
272
273 if edits.is_empty() {
274 return;
275 }
276
277 self.undo_manager.begin_transaction();
278 self.replace_text_in_ranges(&edits, window, cx);
279 self.selections.replace_all(new_selections);
280 let after: Vec<CursorSelection> = self.selections.iter().copied().collect();
281 self.undo_manager.record_selections(before, after);
282 self.undo_manager.commit_transaction();
283
284 self.scroll_to(self.cursor(), None, cx);
285 cx.notify();
286 }
287
288 fn compute_block_indent(
291 &self,
292 direction: IndentDirection,
293 tab_indent: &str,
294 tab_len: usize,
295 ) -> (Vec<(std::ops::Range<usize>, String)>, Vec<CursorSelection>) {
296 let mut rows: std::collections::HashSet<usize> = std::collections::HashSet::new();
297 for sel in self.selections.iter() {
298 let start_row = self.text.offset_to_point(sel.start).row;
299 let end_row = self.text.offset_to_point(sel.end).row;
300 for row in start_row..=end_row {
301 rows.insert(row);
302 }
303 }
304
305 let mut rows: Vec<usize> = rows.into_iter().collect();
306 rows.sort_unstable();
307
308 let mut edits: Vec<(std::ops::Range<usize>, String)> = Vec::new();
309 for row in rows {
310 let line_start = self.text.line_start_offset(row);
311 match direction {
312 IndentDirection::Indent => {
313 edits.push((line_start..line_start, tab_indent.to_string()));
314 }
315 IndentDirection::Outdent => {
316 if self
317 .text
318 .slice(line_start..)
319 .chars()
320 .take(tab_indent.chars().count())
321 .eq(tab_indent.chars())
322 {
323 edits.push((line_start..line_start + tab_len, String::new()));
324 }
325 }
326 }
327 }
328
329 let map_offset = |offset: usize| match direction {
332 IndentDirection::Indent => {
333 offset + edits.partition_point(|(range, _)| range.start <= offset) * tab_len
334 }
335 IndentDirection::Outdent => {
336 let preceding = edits.partition_point(|(range, _)| range.end <= offset);
337 let partial = edits
338 .get(preceding)
339 .map_or(0, |(range, _)| offset.saturating_sub(range.start));
340 offset - preceding * tab_len - partial
341 }
342 };
343 let new_selections = self
344 .selections
345 .iter()
346 .map(|sel| {
347 let mut selection = *sel;
348 selection.start = map_offset(sel.start);
349 selection.end = map_offset(sel.end);
350 selection.column_anchor = None;
351 selection
352 })
353 .collect();
354
355 (edits, new_selections)
356 }
357
358 fn compute_inline_indent(
361 &self,
362 direction: IndentDirection,
363 tab_len: usize,
364 ) -> (Vec<(std::ops::Range<usize>, String)>, Vec<CursorSelection>) {
365 let tab_indent = self.mode.tab_size().to_string();
366
367 let mut ranges: Vec<std::ops::Range<usize>> = Vec::with_capacity(self.selections.len());
370 for sel in self.selections.iter() {
371 let cursor = sel.cursor_offset();
372 match direction {
373 IndentDirection::Indent => ranges.push(cursor..cursor),
374 IndentDirection::Outdent => {
375 let row = self.text.offset_to_point(cursor).row;
376 let start = self.text.line_start_offset(row);
377 if self
378 .text
379 .slice(start..)
380 .chars()
381 .take(tab_indent.chars().count())
382 .eq(tab_indent.chars())
383 {
384 ranges.push(start..start + tab_len);
385 }
386 }
387 }
388 }
389
390 ranges.sort_by_key(|range| range.start);
392 let mut edits: Vec<(std::ops::Range<usize>, String)> = Vec::new();
393 let mut last_end: Option<usize> = None;
394 for range in ranges {
395 if let Some(last_end) = last_end {
396 if range.start < last_end {
397 continue;
398 }
399 }
400 last_end = Some(range.end);
401 let text = match direction {
402 IndentDirection::Indent => tab_indent.to_string(),
403 IndentDirection::Outdent => String::new(),
404 };
405 edits.push((range, text));
406 }
407
408 let mut new_selections: Vec<CursorSelection> = Vec::with_capacity(self.selections.len());
411 for sel in self.selections.iter() {
412 let cursor = sel.cursor_offset();
413 let new_offset = match direction {
414 IndentDirection::Indent => {
415 let inserted_before = edits
416 .iter()
417 .filter(|(range, _)| range.start <= cursor)
418 .count();
419 cursor + inserted_before * tab_len
420 }
421 IndentDirection::Outdent => {
422 let removed_before: usize = edits
423 .iter()
424 .map(|(range, _)| range.end.min(cursor) - range.start.min(cursor))
425 .sum();
426 cursor - removed_before
427 }
428 };
429 let mut selection = CursorSelection::new(sel.id, new_offset, new_offset);
430 selection.column_anchor = None;
431 new_selections.push(selection);
432 }
433
434 (edits, new_selections)
435 }
436}
437
438#[derive(Debug, Copy, Clone, PartialEq)]
439enum IndentDirection {
440 Indent,
441 Outdent,
442}
443
444#[cfg(test)]
445mod tests {
446 use ropey::RopeSlice;
447
448 use super::TabSize;
449
450 #[test]
451 fn test_tab_size() {
452 let tab = TabSize {
453 tab_size: 2,
454 hard_tabs: false,
455 };
456 assert_eq!(tab.to_string(), " ");
457 let tab = TabSize {
458 tab_size: 4,
459 hard_tabs: false,
460 };
461 assert_eq!(tab.to_string(), " ");
462
463 let tab = TabSize {
464 tab_size: 2,
465 hard_tabs: true,
466 };
467 assert_eq!(tab.to_string(), "\t");
468 let tab = TabSize {
469 tab_size: 4,
470 hard_tabs: true,
471 };
472 assert_eq!(tab.to_string(), "\t");
473 }
474
475 #[test]
476 fn test_tab_size_indent_count() {
477 let tab = TabSize {
478 tab_size: 4,
479 hard_tabs: false,
480 };
481 assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
482 assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 2);
483 assert_eq!(tab.indent_count(&RopeSlice::from(" abc")), 4);
484 assert_eq!(tab.indent_count(&RopeSlice::from("\tabc")), 4);
485 assert_eq!(tab.indent_count(&RopeSlice::from(" \tabc")), 6);
486 assert_eq!(tab.indent_count(&RopeSlice::from(" \t abc ")), 6);
487 assert_eq!(tab.indent_count(&RopeSlice::from("abc")), 0);
488 }
489}
490
491impl<M: crate::input::MultiLineMode> InputBaseState<M> {
493 #[doc(hidden)]
495 pub fn tab_size(mut self, tab: TabSize) -> Self {
496 match &mut self.mode {
497 LayoutMode::PlainText { tab: t, .. } => *t = tab,
498 LayoutMode::CodeEditor { tab: t, .. } => *t = tab,
499 _ => {}
500 }
501 self
502 }
503
504 pub fn set_tab_size(&mut self, tab: TabSize, cx: &mut Context<Self>) {
505 match &mut self.mode {
506 LayoutMode::PlainText { tab: value, .. }
507 | LayoutMode::CodeEditor { tab: value, .. } => *value = tab,
508 _ => {}
509 }
510 cx.notify();
511 }
512}