1use std::borrow::Cow;
2use std::mem::take;
3
4use ahash::{AHashMap, AHashSet};
5use itertools::{Itertools, chain, enumerate};
6use smol_str::SmolStr;
7use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
8use sqruff_lib_core::helpers::skip_last;
9use sqruff_lib_core::lint_fix::LintFix;
10use sqruff_lib_core::parser::segments::base::{ErasedSegment, SegmentBuilder, Tables};
11use strum_macros::EnumString;
12
13use super::elements::{ReflowBlock, ReflowElement, ReflowPoint, ReflowSequenceType};
14use super::helpers::fixes_from_results;
15use super::rebreak::{LinePosition, RebreakSpan, identify_rebreak_spans};
16use crate::core::rules::base::LintResult;
17use crate::utils::reflow::elements::IndentStats;
18
19fn has_untemplated_newline(point: &ReflowPoint) -> bool {
20 if !point
21 .class_types()
22 .intersects(const { &SyntaxSet::new(&[SyntaxKind::Newline, SyntaxKind::Placeholder]) })
23 {
24 return false;
25 }
26 point.segments().iter().any(|segment| {
27 segment.is_type(SyntaxKind::Newline)
28 && (segment
29 .get_position_marker()
30 .is_none_or(|position_marker| position_marker.is_literal()))
31 })
32}
33
34#[derive(Debug, Clone)]
35struct IndentPoint {
36 idx: usize,
37 indent_impulse: isize,
38 indent_trough: isize,
39 initial_indent_balance: isize,
40 last_line_break_idx: Option<usize>,
41 is_line_break: bool,
42 untaken_indents: Vec<isize>,
43}
44
45impl IndentPoint {
46 fn closing_indent_balance(&self) -> isize {
47 self.initial_indent_balance + self.indent_impulse
48 }
49}
50
51#[derive(Debug, Clone)]
52struct IndentLine {
53 initial_indent_balance: isize,
54 indent_points: Vec<IndentPoint>,
55}
56
57impl IndentLine {
58 pub(crate) fn is_all_comments(&self, elements: &ReflowSequenceType) -> bool {
59 self.block_segments(elements).all(|seg| {
60 matches!(
61 seg.get_type(),
62 SyntaxKind::InlineComment | SyntaxKind::BlockComment | SyntaxKind::Comment
63 )
64 })
65 }
66
67 fn block_segments<'a>(
68 &self,
69 elements: &'a ReflowSequenceType,
70 ) -> impl Iterator<Item = &'a ErasedSegment> {
71 self.blocks(elements).map(|it| it.segment())
72 }
73
74 fn blocks<'a>(
75 &self,
76 elements: &'a ReflowSequenceType,
77 ) -> impl Iterator<Item = &'a ReflowBlock> {
78 let slice = if self
79 .indent_points
80 .last()
81 .unwrap()
82 .last_line_break_idx
83 .is_none()
84 {
85 0..self.indent_points.last().unwrap().idx
86 } else {
87 self.indent_points.first().unwrap().idx..self.indent_points.last().unwrap().idx
88 };
89
90 elements[slice].iter().filter_map(ReflowElement::as_block)
91 }
92}
93
94impl IndentLine {
95 fn from_points(indent_points: Vec<IndentPoint>) -> Self {
96 let starting_balance = if indent_points.last().unwrap().last_line_break_idx.is_some() {
97 indent_points[0].closing_indent_balance()
98 } else {
99 0
100 };
101
102 IndentLine {
103 initial_indent_balance: starting_balance,
104 indent_points,
105 }
106 }
107
108 fn closing_balance(&self) -> isize {
109 self.indent_points.last().unwrap().closing_indent_balance()
110 }
111
112 fn opening_balance(&self) -> isize {
113 if self
114 .indent_points
115 .last()
116 .unwrap()
117 .last_line_break_idx
118 .is_none()
119 {
120 return 0;
121 }
122
123 self.indent_points[0].closing_indent_balance()
124 }
125
126 fn desired_indent_units(&self, forced_indents: &[usize]) -> isize {
127 let relevant_untaken_indents: usize = if self.indent_points[0].indent_trough != 0 {
128 self.indent_points[0]
129 .untaken_indents
130 .iter()
131 .filter(|&&i| {
132 i <= self.initial_indent_balance
133 - (self.indent_points[0].indent_impulse
134 - self.indent_points[0].indent_trough)
135 })
136 .count()
137 } else {
138 self.indent_points[0].untaken_indents.len()
139 };
140
141 self.initial_indent_balance - relevant_untaken_indents as isize
142 + forced_indents.len() as isize
143 }
144}
145
146impl std::fmt::Display for IndentLine {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 let indent_points_str = self
149 .indent_points
150 .iter()
151 .map(|ip| {
152 format!(
153 "iPt@{}({}, {}, {}, {:?}, {}, {:?})",
154 ip.idx,
155 ip.indent_impulse,
156 ip.indent_trough,
157 ip.initial_indent_balance,
158 ip.last_line_break_idx,
159 ip.is_line_break,
160 ip.untaken_indents
161 )
162 })
163 .collect::<Vec<String>>()
164 .join(", ");
165
166 write!(
167 f,
168 "IndentLine(iib={}, ipts=[{}])",
169 self.initial_indent_balance, indent_points_str
170 )
171 }
172}
173
174fn revise_comment_lines(lines: &mut [IndentLine], elements: &ReflowSequenceType) {
175 let mut comment_line_buffer = Vec::new();
176 let mut changes = Vec::new();
177
178 for (idx, line) in enumerate(&mut *lines) {
179 if line.is_all_comments(elements) {
180 comment_line_buffer.push(idx);
181 } else {
182 for comment_line_idx in comment_line_buffer.drain(..) {
183 changes.push((comment_line_idx, line.initial_indent_balance));
184 }
185 }
186 }
187
188 let changes = changes.into_iter().chain(
189 comment_line_buffer
190 .into_iter()
191 .map(|comment_line_idx| (comment_line_idx, 0)),
192 );
193 for (comment_line_idx, initial_indent_balance) in changes {
194 lines[comment_line_idx].initial_indent_balance = initial_indent_balance;
195 }
196}
197
198#[derive(Clone, Copy, Debug, Eq, PartialEq)]
199pub enum IndentUnit {
200 Tab,
201 Space(usize),
202}
203
204impl Default for IndentUnit {
205 fn default() -> Self {
206 IndentUnit::Space(4)
207 }
208}
209
210impl IndentUnit {
211 pub fn from_type_and_size(indent_type: &str, indent_size: usize) -> Self {
212 match indent_type {
213 "tab" => IndentUnit::Tab,
214 "space" => IndentUnit::Space(indent_size),
215 _ => unreachable!("Invalid indent type {}", indent_type),
216 }
217 }
218}
219
220pub fn construct_single_indent(indent_unit: IndentUnit) -> Cow<'static, str> {
221 match indent_unit {
222 IndentUnit::Tab => "\t".into(),
223 IndentUnit::Space(space_size) => " ".repeat(space_size).into(),
224 }
225}
226
227fn prune_untaken_indents(
228 untaken_indents: Vec<isize>,
229 incoming_balance: isize,
230 indent_stats: &IndentStats,
231 has_newline: bool,
232) -> Vec<isize> {
233 let new_balance_threshold = if indent_stats.trough < indent_stats.impulse {
234 incoming_balance + indent_stats.impulse + indent_stats.trough
235 } else {
236 incoming_balance + indent_stats.impulse
237 };
238
239 let mut pruned_untaken_indents: Vec<_> = untaken_indents
240 .iter()
241 .filter(|&x| x <= &new_balance_threshold)
242 .copied()
243 .collect();
244
245 if indent_stats.impulse > indent_stats.trough && !has_newline {
246 for i in indent_stats.trough..indent_stats.impulse {
247 let indent_val = incoming_balance + i + 1;
248
249 if !indent_stats
250 .implicit_indents
251 .contains(&(indent_val - incoming_balance))
252 {
253 pruned_untaken_indents.push(indent_val);
254 }
255 }
256 }
257
258 pruned_untaken_indents
259}
260
261fn update_crawl_balances(
262 untaken_indents: Vec<isize>,
263 incoming_balance: isize,
264 indent_stats: &IndentStats,
265 has_newline: bool,
266) -> (isize, Vec<isize>) {
267 let new_untaken_indents =
268 prune_untaken_indents(untaken_indents, incoming_balance, indent_stats, has_newline);
269 let new_balance = incoming_balance + indent_stats.impulse;
270
271 (new_balance, new_untaken_indents)
272}
273
274fn crawl_indent_points(
275 elements: &ReflowSequenceType,
276 allow_implicit_indents: bool,
277) -> Vec<IndentPoint> {
278 let mut acc = Vec::new();
279
280 let mut last_line_break_idx = None;
281 let mut indent_balance = 0;
282 let mut untaken_indents = Vec::new();
283 let mut cached_indent_stats = None;
284 let mut cached_point = None;
285
286 for (idx, elem) in enumerate(elements) {
287 if let ReflowElement::Point(elem) = elem {
288 let mut indent_stats =
289 IndentStats::from_combination(cached_indent_stats.clone(), elem.indent_impulse());
290
291 if !indent_stats.implicit_indents.is_empty() {
292 let mut unclosed_bracket = false;
293
294 if allow_implicit_indents
295 && elements[idx + 1]
296 .class_types()
297 .contains(SyntaxKind::StartBracket)
298 {
299 let depth = elements[idx + 1]
300 .as_block()
301 .unwrap()
302 .depth_info()
303 .stack_depth;
304
305 let elems = &elements[idx + 1..];
306 unclosed_bracket = elems.is_empty();
307
308 for elem_j in elems {
309 if let Some(elem_j) = elem_j.as_point() {
310 if elem_j.num_newlines() > 0 {
311 unclosed_bracket = true;
312 break;
313 }
314 } else if elem_j.class_types().contains(SyntaxKind::EndBracket)
315 && elem_j.as_block().unwrap().depth_info().stack_depth == depth
316 {
317 unclosed_bracket = false;
318 break;
319 } else {
320 unclosed_bracket = true;
321 }
322 }
323 }
324
325 if unclosed_bracket || !allow_implicit_indents {
326 indent_stats.implicit_indents = Default::default();
327 }
328 }
329
330 if cached_indent_stats.is_some() {
332 let cached_point: &IndentPoint = cached_point.as_ref().unwrap();
333
334 if cached_point.is_line_break {
335 acc.push(IndentPoint {
336 idx: cached_point.idx,
337 indent_impulse: indent_stats.impulse,
338 indent_trough: indent_stats.trough,
339 initial_indent_balance: indent_balance,
340 last_line_break_idx: cached_point.last_line_break_idx,
341 is_line_break: true,
342 untaken_indents: take(&mut untaken_indents),
343 });
344 (indent_balance, untaken_indents) =
348 update_crawl_balances(untaken_indents, indent_balance, &indent_stats, true);
349
350 let implicit_indents = take(&mut indent_stats.implicit_indents);
351 indent_stats = IndentStats {
352 impulse: 0,
353 trough: 0,
354 implicit_indents,
355 };
356 } else {
357 acc.push(IndentPoint {
358 idx: cached_point.idx,
359 indent_impulse: 0,
360 indent_trough: 0,
361 initial_indent_balance: indent_balance,
362 last_line_break_idx: cached_point.last_line_break_idx,
363 is_line_break: false,
364 untaken_indents: untaken_indents.clone(),
365 });
366 }
367 }
368
369 cached_indent_stats = None;
371 cached_point = None;
372
373 let has_newline = has_untemplated_newline(elem) && Some(idx) != last_line_break_idx;
375
376 let indent_point = IndentPoint {
378 idx,
379 indent_impulse: indent_stats.impulse,
380 indent_trough: indent_stats.trough,
381 initial_indent_balance: indent_balance,
382 last_line_break_idx,
383 is_line_break: has_newline,
384 untaken_indents: untaken_indents.clone(),
385 };
386
387 if has_newline {
388 last_line_break_idx = idx.into();
389 }
390
391 if elements[idx + 1].class_types().intersects(
392 const {
393 &SyntaxSet::new(&[
394 SyntaxKind::Comment,
395 SyntaxKind::InlineComment,
396 SyntaxKind::BlockComment,
397 ])
398 },
399 ) {
400 cached_indent_stats = indent_stats.clone().into();
401 cached_point = indent_point.clone().into();
402
403 continue;
404 } else if has_newline
405 || indent_stats.impulse != 0
406 || indent_stats.trough != 0
407 || idx == 0
408 || elements[idx + 1].segments()[0].is_type(SyntaxKind::EndOfFile)
409 {
410 acc.push(indent_point);
411 }
412
413 (indent_balance, untaken_indents) =
414 update_crawl_balances(untaken_indents, indent_balance, &indent_stats, has_newline);
415 }
416 }
417
418 acc
419}
420
421fn map_line_buffers(
422 elements: &ReflowSequenceType,
423 allow_implicit_indents: bool,
424) -> (Vec<IndentLine>, Vec<usize>) {
425 let mut lines = Vec::new();
426 let mut point_buffer = Vec::new();
427 let mut previous_points = AHashMap::new();
428 let mut untaken_indent_locs = AHashMap::new();
429 let mut imbalanced_locs = Vec::new();
430
431 for indent_point in crawl_indent_points(elements, allow_implicit_indents) {
432 point_buffer.push(indent_point.clone());
433 previous_points.insert(indent_point.idx, indent_point.clone());
434
435 if !indent_point.is_line_break {
436 let indent_stats = elements[indent_point.idx]
437 .as_point()
438 .unwrap()
439 .indent_impulse();
440
441 if (indent_stats.implicit_indents.is_empty() || !allow_implicit_indents)
442 && indent_point.indent_impulse > indent_point.indent_trough
443 {
444 untaken_indent_locs.insert(
445 indent_point.initial_indent_balance + indent_point.indent_impulse,
446 indent_point.idx,
447 );
448 }
449
450 continue;
451 }
452
453 lines.push(IndentLine::from_points(point_buffer.clone()));
454
455 let following_class_types = elements[indent_point.idx + 1].class_types();
456 if indent_point.indent_trough != 0 && !following_class_types.contains(SyntaxKind::EndOfFile)
457 {
458 let passing_indents = Range::new(
459 indent_point.initial_indent_balance,
460 indent_point.initial_indent_balance + indent_point.indent_trough,
461 -1,
462 )
463 .reversed();
464
465 for i in passing_indents {
466 let Some(&loc) = untaken_indent_locs.get(&i) else {
467 break;
468 };
469
470 if elements[loc + 1]
471 .class_types()
472 .contains(SyntaxKind::StartBracket)
473 {
474 continue;
475 }
476
477 if point_buffer.iter().any(|ip| ip.idx == loc) {
478 continue;
479 }
480
481 let mut _pt = None;
482 for j in loc..indent_point.idx {
483 if let Some(pt) = previous_points.get(&j) {
484 if pt.is_line_break {
485 _pt = Some(pt);
486 break;
487 }
488 }
489 }
490
491 let _pt = _pt.unwrap();
492
493 if (_pt.idx + 1..indent_point.idx).step_by(2).all(|k| {
495 elements[k].class_types().intersects(
496 const {
497 &SyntaxSet::new(&[
498 SyntaxKind::Comment,
499 SyntaxKind::InlineComment,
500 SyntaxKind::BlockComment,
501 ])
502 },
503 )
504 }) {
505 continue;
507 }
508
509 imbalanced_locs.push(loc);
510 }
511 }
512
513 untaken_indent_locs
514 .retain(|&k, _| k <= indent_point.initial_indent_balance + indent_point.indent_trough);
515 point_buffer = vec![indent_point];
516 }
517
518 if point_buffer.len() > 1 {
519 lines.push(IndentLine::from_points(point_buffer));
520 }
521
522 (lines, imbalanced_locs)
523}
524
525fn deduce_line_current_indent(
526 elements: &ReflowSequenceType,
527 last_line_break_idx: Option<usize>,
528) -> SmolStr {
529 let mut indent_seg = None;
530
531 if elements[0].segments().is_empty() {
532 return "".into();
533 } else if let Some(last_line_break_idx) = last_line_break_idx {
534 indent_seg = elements[last_line_break_idx]
535 .as_point()
536 .unwrap()
537 .get_indent_segment();
538 } else if matches!(elements[0], ReflowElement::Point(_))
539 && elements[0].segments()[0]
540 .get_position_marker()
541 .is_some_and(|marker| marker.working_loc() == (1, 1))
542 {
543 if elements[0].segments()[0].is_type(SyntaxKind::Placeholder) {
544 unimplemented!()
545 } else {
546 for segment in elements[0].segments().iter().rev() {
547 if segment.is_type(SyntaxKind::Whitespace) && !segment.is_templated() {
548 indent_seg = Some(segment.clone());
549 break;
550 }
551 }
552
553 if let Some(ref seg) = indent_seg {
554 if !seg.is_type(SyntaxKind::Whitespace) {
555 indent_seg = None;
556 }
557 }
558 }
559 }
560
561 let Some(indent_seg) = indent_seg else {
562 return "".into();
563 };
564
565 if indent_seg.is_type(SyntaxKind::Placeholder) {
566 unimplemented!()
567 } else if indent_seg.get_position_marker().is_none() || !indent_seg.is_templated() {
568 return indent_seg.raw().clone();
569 } else {
570 unimplemented!()
571 }
572}
573
574fn lint_line_starting_indent(
575 tables: &Tables,
576 elements: &mut ReflowSequenceType,
577 indent_line: &IndentLine,
578 single_indent: &str,
579 forced_indents: &[usize],
580) -> Vec<LintResult> {
581 let indent_points = &indent_line.indent_points;
582 let initial_point_idx = indent_points[0].idx;
584 let before = elements[initial_point_idx + 1].segments()[0].clone();
585
586 let current_indent =
588 deduce_line_current_indent(elements, indent_points.last().unwrap().last_line_break_idx);
589 let initial_point = elements[initial_point_idx].as_point().unwrap();
590 let desired_indent_units = indent_line.desired_indent_units(forced_indents);
591 let desired_starting_indent = desired_indent_units
592 .try_into()
593 .map_or(String::new(), |n| single_indent.repeat(n));
594
595 if current_indent == desired_starting_indent {
596 return Vec::new();
597 }
598
599 if initial_point_idx > 0 && initial_point_idx < elements.len() - 1 {
600 if elements[initial_point_idx + 1].class_types().intersects(
601 const {
602 &SyntaxSet::new(&[
603 SyntaxKind::Comment,
604 SyntaxKind::BlockComment,
605 SyntaxKind::InlineComment,
606 ])
607 },
608 ) {
609 let last_indent =
610 deduce_line_current_indent(elements, indent_points[0].last_line_break_idx);
611
612 if current_indent.len() == last_indent.len() {
613 return Vec::new();
614 }
615 }
616
617 if elements[initial_point_idx - 1]
618 .class_types()
619 .contains(SyntaxKind::BlockComment)
620 && elements[initial_point_idx + 1]
621 .class_types()
622 .contains(SyntaxKind::BlockComment)
623 && current_indent.len() > desired_starting_indent.len()
624 {
625 return Vec::new();
626 }
627 }
628
629 let (new_results, new_point) = if indent_points[0].idx == 0 && !indent_points[0].is_line_break {
630 let init_seg = &elements[indent_points[0].idx].segments()[0];
631 let fixes = if init_seg.is_type(SyntaxKind::Placeholder) {
632 unimplemented!()
633 } else {
634 initial_point
635 .segments()
636 .iter()
637 .cloned()
638 .map(LintFix::delete)
639 .collect_vec()
640 };
641
642 (
643 vec![LintResult::new(
644 initial_point.segments()[0].clone().into(),
645 fixes,
646 Some("First line should not be indented.".into()),
647 None,
648 )],
649 ReflowPoint::new(Vec::new()),
650 )
651 } else {
652 initial_point.indent_to(
653 tables,
654 &desired_starting_indent,
655 None,
656 before.into(),
657 None,
658 None,
659 )
660 };
661
662 elements[initial_point_idx] = new_point.into();
663
664 new_results
665}
666
667fn lint_line_untaken_positive_indents(
668 tables: &Tables,
669 elements: &mut [ReflowElement],
670 indent_line: &IndentLine,
671 single_indent: &str,
672 imbalanced_indent_locs: &[usize],
673) -> (Vec<LintResult>, Vec<usize>) {
674 for ip in &indent_line.indent_points {
676 if imbalanced_indent_locs.contains(&ip.idx) {
677 let desired_indent = single_indent
679 .repeat((ip.closing_indent_balance() - ip.untaken_indents.len() as isize) as usize);
680 let target_point = elements[ip.idx].as_point().unwrap();
681
682 let (results, new_point) = target_point.indent_to(
683 tables,
684 &desired_indent,
685 None,
686 Some(elements[ip.idx + 1].segments()[0].clone()),
687 Some("reflow.indent.imbalance"),
688 None,
689 );
690
691 elements[ip.idx] = ReflowElement::Point(new_point);
692 return (results, vec![ip.closing_indent_balance() as usize]);
694 }
695 }
696
697 let starting_balance = indent_line.opening_balance();
699 let last_ip = indent_line.indent_points.last().unwrap();
700 if last_ip.initial_indent_balance + last_ip.indent_trough <= starting_balance {
702 return (vec![], vec![]);
703 }
704
705 let mut closing_trough = last_ip.initial_indent_balance
707 + if last_ip.indent_trough == 0 {
708 last_ip.indent_impulse
709 } else {
710 last_ip.indent_trough
711 };
712
713 let mut _bal = 0;
716 for elem in &elements[last_ip.idx + 1..] {
717 if let ReflowElement::Point(_) = elem {
718 let stats = elem.as_point().unwrap().indent_impulse();
719 if stats.impulse > 0 {
721 break;
722 }
723 closing_trough = _bal + stats.trough;
724 _bal += stats.impulse;
725 } else if !elem.class_types().intersects(
726 const {
727 &SyntaxSet::new(&[
728 SyntaxKind::Comment,
729 SyntaxKind::InlineComment,
730 SyntaxKind::BlockComment,
731 ])
732 },
733 ) {
734 break;
735 }
736 }
737
738 if !indent_line
741 .indent_points
742 .last()
743 .unwrap()
744 .untaken_indents
745 .contains(&closing_trough)
746 {
747 return (vec![], vec![]);
751 }
752
753 let mut target_point_idx = 0;
756 let mut desired_indent = String::new();
757 for ip in &indent_line.indent_points {
758 if ip.closing_indent_balance() == closing_trough {
759 target_point_idx = ip.idx;
760 desired_indent = single_indent
761 .repeat((ip.closing_indent_balance() - ip.untaken_indents.len() as isize) as usize);
762 break;
763 }
764 }
765
766 let target_point = elements[target_point_idx].as_point().unwrap();
767
768 let (results, new_point) = target_point.indent_to(
769 tables,
770 &desired_indent,
771 None,
772 Some(elements[target_point_idx + 1].segments()[0].clone()),
773 Some("reflow.indent.positive"),
774 None,
775 );
776
777 elements[target_point_idx] = ReflowElement::Point(new_point);
778 (results, vec![closing_trough as usize])
780}
781
782fn lint_line_untaken_negative_indents(
783 tables: &Tables,
784 elements: &mut ReflowSequenceType,
785 indent_line: &IndentLine,
786 single_indent: &str,
787 forced_indents: &[usize],
788) -> Vec<LintResult> {
789 let mut results = Vec::new();
790
791 if indent_line.closing_balance() >= indent_line.opening_balance() {
792 return Vec::new();
793 }
794
795 for ip in skip_last(indent_line.indent_points.iter()) {
796 if ip.is_line_break || ip.indent_impulse >= 0 {
797 continue;
798 }
799
800 if ip.initial_indent_balance + ip.indent_trough >= indent_line.opening_balance() {
801 continue;
802 }
803
804 let covered_indents: AHashSet<isize> = Range::new(
805 ip.initial_indent_balance,
806 ip.initial_indent_balance + ip.indent_trough,
807 -1,
808 )
809 .collect();
810
811 let untaken_indents: AHashSet<_> = ip
812 .untaken_indents
813 .iter()
814 .copied()
815 .collect::<AHashSet<_>>()
816 .difference(&forced_indents.iter().map(|it| *it as isize).collect())
817 .copied()
818 .collect();
819
820 if covered_indents.is_subset(&untaken_indents) {
821 continue;
822 }
823
824 if elements.get(ip.idx + 1).is_some_and(|elem| {
825 elem.class_types().intersects(
826 const { &SyntaxSet::new(&[SyntaxKind::StatementTerminator, SyntaxKind::Comma]) },
827 )
828 }) {
829 continue;
830 }
831
832 let desired_indent = single_indent.repeat(
833 (ip.closing_indent_balance() - ip.untaken_indents.len() as isize
834 + forced_indents.len() as isize)
835 .max(0) as usize,
836 );
837
838 let target_point = elements[ip.idx].as_point().unwrap();
839 let (mut new_results, new_point) = target_point.indent_to(
840 tables,
841 &desired_indent,
842 None,
843 elements[ip.idx + 1].segments()[0].clone().into(),
844 None,
845 "reflow.indent.negative".into(),
846 );
847 elements[ip.idx] = new_point.into();
848 results.append(&mut new_results);
849 }
850
851 results
852}
853
854fn lint_line_buffer_indents(
855 tables: &Tables,
856 elements: &mut ReflowSequenceType,
857 indent_line: IndentLine,
858 single_indent: &str,
859 forced_indents: &mut Vec<usize>,
860 imbalanced_indent_locs: &[usize],
861) -> Vec<LintResult> {
862 let mut results = Vec::new();
863
864 let mut new_results = lint_line_starting_indent(
865 tables,
866 elements,
867 &indent_line,
868 single_indent,
869 forced_indents,
870 );
871 results.append(&mut new_results);
872
873 let (mut new_results, mut new_indents) = lint_line_untaken_positive_indents(
874 tables,
875 elements,
876 &indent_line,
877 single_indent,
878 imbalanced_indent_locs,
879 );
880
881 if !new_results.is_empty() {
882 results.append(&mut new_results);
883 forced_indents.append(&mut new_indents);
884 return results;
885 }
886
887 results.extend(lint_line_untaken_negative_indents(
888 tables,
889 elements,
890 &indent_line,
891 single_indent,
892 forced_indents,
893 ));
894
895 forced_indents.retain(|&i| (i as isize) < indent_line.closing_balance());
896
897 results
898}
899
900pub fn lint_indent_points(
901 tables: &Tables,
902 elements: ReflowSequenceType,
903 single_indent: &str,
904 _skip_indentation_in: AHashSet<String>,
905 allow_implicit_indents: bool,
906) -> (ReflowSequenceType, Vec<LintResult>) {
907 let (mut lines, imbalanced_indent_locs) = map_line_buffers(&elements, allow_implicit_indents);
908
909 let mut results = Vec::new();
910 let mut elem_buffer = elements.clone();
911 let mut forced_indents = Vec::new();
912
913 revise_comment_lines(&mut lines, &elements);
914
915 for line in lines {
916 let line_results = lint_line_buffer_indents(
917 tables,
918 &mut elem_buffer,
919 line,
920 single_indent,
921 &mut forced_indents,
922 &imbalanced_indent_locs,
923 );
924
925 results.extend(line_results);
926 }
927
928 (elem_buffer, results)
929}
930
931fn source_char_len(elements: &[ReflowElement]) -> usize {
932 let mut char_len = 0;
933 let mut last_source_slice = None;
934
935 for seg in elements.iter().flat_map(|elem| elem.segments()) {
936 if seg.is_type(SyntaxKind::Indent) || seg.is_type(SyntaxKind::Dedent) {
937 continue;
938 }
939
940 let Some(pos_marker) = seg.get_position_marker() else {
941 break;
942 };
943
944 let source_slice = pos_marker.source_slice.clone();
945 let source_str = pos_marker.source_str();
946
947 if let Some(pos) = source_str.find('\n') {
948 char_len += pos;
949 break;
950 }
951
952 let slice_len = source_slice.end - source_slice.start;
953
954 if Some(source_slice.clone()) != last_source_slice {
955 if !seg.raw().is_empty() && slice_len == 0 {
956 char_len += seg.raw().len();
957 } else if slice_len == 0 {
958 continue;
959 } else if pos_marker.is_literal() {
960 char_len += seg.raw().len();
961 last_source_slice = Some(source_slice);
962 } else {
963 char_len += source_slice.end - source_slice.start;
964 last_source_slice = Some(source_slice);
965 }
966 }
967 }
968
969 char_len
970}
971
972fn rebreak_priorities(spans: Vec<RebreakSpan>) -> AHashMap<usize, usize> {
973 let mut rebreak_priority = AHashMap::with_capacity(spans.len());
974
975 for span in spans {
976 let rebreak_indices: &[usize] = match span.line_position {
977 LinePosition::Leading => &[span.start_idx - 1],
978 LinePosition::Trailing => &[span.end_idx + 1],
979 LinePosition::Alone => &[span.start_idx - 1, span.end_idx + 1],
980 _ => {
981 unimplemented!()
982 }
983 };
984
985 let span_raw = span.target.raw().to_uppercase();
986 let mut priority = 6;
987
988 if span_raw == "," {
989 priority = 1;
990 } else if span.target.is_type(SyntaxKind::AssignmentOperator) {
991 priority = 2;
992 } else if span_raw == "OR" {
993 priority = 3;
994 } else if span_raw == "AND" {
995 priority = 4;
996 } else if span.target.is_type(SyntaxKind::ComparisonOperator) {
997 priority = 5;
998 } else if ["*", "/", "%"].contains(&span_raw.as_str()) {
999 priority = 7;
1000 }
1001
1002 for rebreak_idx in rebreak_indices {
1003 rebreak_priority.insert(*rebreak_idx, priority);
1004 }
1005 }
1006
1007 rebreak_priority
1008}
1009
1010type MatchedIndentsType = AHashMap<FloatTypeWrapper, Vec<usize>>;
1011
1012fn increment_balance(
1013 input_balance: isize,
1014 indent_stats: &IndentStats,
1015 elem_idx: usize,
1016) -> (isize, MatchedIndentsType) {
1017 let mut balance = input_balance;
1018 let mut matched_indents = AHashMap::new();
1019
1020 if indent_stats.trough < 0 {
1021 for b in (0..indent_stats.trough.abs()).step_by(1) {
1022 let key = FloatTypeWrapper::new((balance + -b) as f64);
1023 matched_indents
1024 .entry(key)
1025 .or_insert_with(Vec::new)
1026 .push(elem_idx);
1027 }
1028 balance += indent_stats.impulse;
1029 } else if indent_stats.impulse > 0 {
1030 for b in 0..indent_stats.impulse {
1031 let key = FloatTypeWrapper::new((balance + b + 1) as f64);
1032 matched_indents
1033 .entry(key)
1034 .or_insert_with(Vec::new)
1035 .push(elem_idx);
1036 }
1037 balance += indent_stats.impulse;
1038 }
1039
1040 (balance, matched_indents)
1041}
1042
1043fn match_indents(
1044 line_elements: ReflowSequenceType,
1045 rebreak_priorities: AHashMap<usize, usize>,
1046 newline_idx: usize,
1047 allow_implicit_indents: bool,
1048) -> MatchedIndentsType {
1049 let mut balance = 0;
1050 let mut matched_indents: MatchedIndentsType = AHashMap::new();
1051 let mut implicit_indents = AHashMap::new();
1052
1053 for (idx, e) in enumerate(&line_elements) {
1054 let ReflowElement::Point(point) = e else {
1055 continue;
1056 };
1057
1058 let indent_stats = point.indent_impulse();
1059
1060 let e_idx =
1061 (newline_idx as isize - line_elements.len() as isize + idx as isize + 1) as usize;
1062
1063 if !indent_stats.implicit_indents.is_empty() {
1064 implicit_indents.insert(e_idx, indent_stats.implicit_indents.clone());
1065 }
1066
1067 let nmi;
1068 (balance, nmi) = increment_balance(balance, indent_stats, e_idx);
1069 for (b, indices) in nmi {
1070 matched_indents.entry(b).or_default().extend(indices);
1071 }
1072
1073 let Some(&priority) = rebreak_priorities.get(&idx) else {
1074 continue;
1075 };
1076
1077 let balance = FloatTypeWrapper::new(balance as f64 + 0.5 + (priority as f64 / 100.0));
1078 matched_indents.entry(balance).or_default().push(e_idx);
1079 }
1080
1081 matched_indents.retain(|_key, value| value != &[newline_idx]);
1082
1083 if allow_implicit_indents {
1084 let keys: Vec<_> = matched_indents.keys().copied().collect();
1085 for indent_level in keys {
1086 let major_points: AHashSet<_> = matched_indents[&indent_level]
1087 .iter()
1088 .copied()
1089 .collect::<AHashSet<_>>()
1090 .difference(&AHashSet::from([newline_idx]))
1091 .copied()
1092 .collect::<AHashSet<_>>()
1093 .difference(&implicit_indents.keys().copied().collect::<AHashSet<_>>())
1094 .copied()
1095 .collect();
1096
1097 if major_points.is_empty() {
1098 matched_indents.remove(&indent_level);
1099 }
1100 }
1101 }
1102
1103 matched_indents
1104}
1105
1106#[derive(Clone, Copy, PartialEq, Debug, Default, Eq, EnumString)]
1107#[strum(serialize_all = "lowercase")]
1108pub enum TrailingComments {
1109 #[default]
1110 Before,
1111 After,
1112}
1113
1114fn fix_long_line_with_comment(
1115 tables: &Tables,
1116 line_buffer: &ReflowSequenceType,
1117 elements: &ReflowSequenceType,
1118 current_indent: &str,
1119 line_length_limit: usize,
1120 last_indent_idx: Option<usize>,
1121 trailing_comments: TrailingComments,
1122) -> (ReflowSequenceType, Vec<LintFix>) {
1123 if line_buffer
1124 .last()
1125 .unwrap()
1126 .segments()
1127 .last()
1128 .unwrap()
1129 .raw()
1130 .contains("noqa")
1131 {
1132 return (elements.clone(), Vec::new());
1133 }
1134
1135 if line_buffer
1136 .last()
1137 .unwrap()
1138 .segments()
1139 .last()
1140 .unwrap()
1141 .raw()
1142 .len()
1143 + current_indent.len()
1144 > line_length_limit
1145 {
1146 return (elements.clone(), Vec::new());
1147 }
1148
1149 let comment_seg = line_buffer.last().unwrap().segments().last().unwrap();
1150 let first_seg = line_buffer.first().unwrap().segments().first().unwrap();
1151 let last_elem_idx = elements
1152 .iter()
1153 .position(|elem| elem == line_buffer.last().unwrap())
1154 .unwrap();
1155
1156 if trailing_comments == TrailingComments::After {
1157 let mut elements = elements.clone();
1158 let anchor_point = line_buffer[line_buffer.len() - 2].as_point().unwrap();
1159 let (results, new_point) = anchor_point.indent_to(
1160 tables,
1161 current_indent,
1162 None,
1163 comment_seg.clone().into(),
1164 None,
1165 None,
1166 );
1167 elements.splice(
1168 last_elem_idx - 1..last_elem_idx,
1169 [new_point.into()].iter().cloned(),
1170 );
1171 return (elements, fixes_from_results(results.into_iter()).collect());
1172 }
1173
1174 let mut fixes = chain(
1175 Some(LintFix::delete(comment_seg.clone())),
1176 line_buffer[line_buffer.len() - 2]
1177 .segments()
1178 .iter()
1179 .filter(|ws| ws.is_type(SyntaxKind::Whitespace))
1180 .map(|ws| LintFix::delete(ws.clone())),
1181 )
1182 .collect_vec();
1183
1184 let new_point;
1185 let anchor;
1186 let prev_elems: Vec<ReflowElement>;
1187
1188 if let Some(idx) = last_indent_idx {
1189 new_point = ReflowPoint::new(vec![
1190 SegmentBuilder::newline(tables.next_id(), "\n"),
1191 SegmentBuilder::whitespace(tables.next_id(), current_indent),
1192 ]);
1193 prev_elems = elements[..=idx].to_vec();
1194 anchor = elements[idx + 1].segments()[0].clone();
1195 } else {
1196 new_point = ReflowPoint::new(vec![SegmentBuilder::newline(tables.next_id(), "\n")]);
1197 prev_elems = Vec::new();
1198 anchor = first_seg.clone();
1199 }
1200
1201 fixes.push(LintFix::create_before(
1202 anchor,
1203 chain(
1204 Some(comment_seg.clone()),
1205 new_point.segments().iter().cloned(),
1206 )
1207 .collect_vec(),
1208 ));
1209
1210 let elements: Vec<_> = prev_elems
1211 .into_iter()
1212 .chain(Some(line_buffer.last().unwrap().clone()))
1213 .chain(Some(new_point.into()))
1214 .chain(line_buffer.iter().take(line_buffer.len() - 2).cloned())
1215 .chain(elements.iter().skip(last_elem_idx + 1).cloned())
1216 .collect();
1217
1218 (elements, fixes)
1219}
1220
1221fn fix_long_line_with_fractional_targets(
1222 tables: &Tables,
1223 elements: &mut [ReflowElement],
1224 target_breaks: Vec<usize>,
1225 desired_indent: &str,
1226) -> Vec<LintResult> {
1227 let mut line_results = Vec::new();
1228
1229 for e_idx in target_breaks {
1230 let e = elements[e_idx].as_point().unwrap();
1231 let (new_results, new_point) = e.indent_to(
1232 tables,
1233 desired_indent,
1234 elements[e_idx - 1].segments().last().cloned(),
1235 elements[e_idx + 1].segments()[0].clone().into(),
1236 None,
1237 None,
1238 );
1239
1240 elements[e_idx] = new_point.into();
1241 line_results.extend(new_results);
1242 }
1243
1244 line_results
1245}
1246
1247fn fix_long_line_with_integer_targets(
1248 tables: &Tables,
1249 elements: &mut [ReflowElement],
1250 mut target_breaks: Vec<usize>,
1251 line_length_limit: usize,
1252 inner_indent: &str,
1253 outer_indent: &str,
1254) -> Vec<LintResult> {
1255 let mut line_results = Vec::new();
1256
1257 let mut purge_before = 0;
1258 for &e_idx in &target_breaks {
1259 let Some(pos_marker) = elements[e_idx + 1].segments()[0].get_position_marker() else {
1260 break;
1261 };
1262
1263 if pos_marker.working_line_pos > line_length_limit {
1264 break;
1265 }
1266
1267 let e = elements[e_idx].as_point().unwrap();
1268 if e.indent_impulse().trough < 0 {
1269 continue;
1270 }
1271
1272 purge_before = e_idx;
1273 }
1274
1275 target_breaks.retain(|&e_idx| e_idx >= purge_before);
1276
1277 for e_idx in target_breaks {
1278 let e = elements[e_idx].as_point().unwrap().clone();
1279 let indent_stats = e.indent_impulse();
1280
1281 let new_indent = if indent_stats.impulse < 0 {
1282 if elements[e_idx + 1].class_types().intersects(
1283 const { &SyntaxSet::new(&[SyntaxKind::StatementTerminator, SyntaxKind::Comma]) },
1284 ) {
1285 break;
1286 }
1287
1288 outer_indent
1289 } else {
1290 inner_indent
1291 };
1292
1293 let (new_results, new_point) = e.indent_to(
1294 tables,
1295 new_indent,
1296 elements[e_idx - 1].segments().last().cloned(),
1297 elements[e_idx + 1].segments().first().cloned(),
1298 None,
1299 None,
1300 );
1301
1302 elements[e_idx] = new_point.into();
1303 line_results.extend(new_results);
1304
1305 if indent_stats.trough < 0 {
1306 break;
1307 }
1308 }
1309
1310 line_results
1311}
1312
1313pub fn lint_line_length(
1314 tables: &Tables,
1315 elements: &ReflowSequenceType,
1316 root_segment: &ErasedSegment,
1317 single_indent: &str,
1318 line_length_limit: usize,
1319 allow_implicit_indents: bool,
1320 trailing_comments: TrailingComments,
1321) -> (ReflowSequenceType, Vec<LintResult>) {
1322 if line_length_limit == 0 {
1323 return (elements.clone(), Vec::new());
1324 }
1325
1326 let mut elem_buffer = elements.clone();
1327 let mut line_buffer = Vec::new();
1328 let mut results = Vec::new();
1329
1330 let mut last_indent_idx = None;
1331 for (i, elem) in enumerate(elements) {
1332 if elem
1333 .as_point()
1334 .filter(|point| {
1335 elem_buffer[i + 1]
1336 .class_types()
1337 .contains(SyntaxKind::EndOfFile)
1338 || has_untemplated_newline(point)
1339 })
1340 .is_some()
1341 {
1342 } else {
1344 line_buffer.push(elem.clone());
1345 continue;
1346 }
1347
1348 if line_buffer.is_empty() {
1349 continue;
1350 }
1351
1352 let current_indent = if let Some(last_indent_idx) = last_indent_idx {
1353 deduce_line_current_indent(&elem_buffer, Some(last_indent_idx))
1354 } else {
1355 "".into()
1356 };
1357
1358 let char_len = source_char_len(&line_buffer);
1359 let line_len = current_indent.len() + char_len;
1360
1361 let first_seg = line_buffer[0].segments()[0].clone();
1362 let line_no = first_seg.get_position_marker().unwrap().working_line_no;
1363
1364 if line_len <= line_length_limit {
1365 tracing::info!(
1366 "Line #{}. Length {} <= {}. OK.",
1367 line_no,
1368 line_len,
1369 line_length_limit,
1370 )
1371 } else {
1372 let line_elements = chain(line_buffer.clone(), Some(elem.clone())).collect_vec();
1373 let mut fixes: Vec<LintFix> = Vec::new();
1374
1375 let mut combined_elements = line_elements.clone();
1376 combined_elements.push(elements[i + 1].clone());
1377
1378 let spans = identify_rebreak_spans(&combined_elements, root_segment.clone());
1379 let rebreak_priorities = rebreak_priorities(spans);
1380
1381 let matched_indents =
1382 match_indents(line_elements, rebreak_priorities, i, allow_implicit_indents);
1383
1384 let desc = format!("Line is too long ({line_len} > {line_length_limit}).");
1385
1386 if line_buffer.len() > 1
1387 && line_buffer
1388 .last()
1389 .unwrap()
1390 .segments()
1391 .last()
1392 .unwrap()
1393 .is_type(SyntaxKind::InlineComment)
1394 {
1395 (elem_buffer, fixes) = fix_long_line_with_comment(
1396 tables,
1397 &line_buffer,
1398 elements,
1399 ¤t_indent,
1400 line_length_limit,
1401 last_indent_idx,
1402 trailing_comments,
1403 );
1404 } else if matched_indents.is_empty() {
1405 tracing::debug!("Handling as unfixable line.");
1406 } else {
1407 tracing::debug!("Handling as normal line.");
1408 let target_balance = matched_indents
1409 .keys()
1410 .map(|k| k.into_f64())
1411 .fold(f64::INFINITY, f64::min);
1412 let mut desired_indent = current_indent.to_string();
1413
1414 if target_balance >= 1.0 {
1415 desired_indent += single_indent;
1416 }
1417
1418 let mut target_breaks =
1419 matched_indents[&FloatTypeWrapper::new(target_balance)].clone();
1420
1421 if let Some(pos) = target_breaks.iter().position(|&x| x == i) {
1422 target_breaks.remove(pos);
1423 }
1424
1425 let line_results = if target_balance % 1.0 == 0.0 {
1426 fix_long_line_with_integer_targets(
1427 tables,
1428 &mut elem_buffer,
1429 target_breaks,
1430 line_length_limit,
1431 &desired_indent,
1432 ¤t_indent,
1433 )
1434 } else {
1435 fix_long_line_with_fractional_targets(
1436 tables,
1437 &mut elem_buffer,
1438 target_breaks,
1439 &desired_indent,
1440 )
1441 };
1442
1443 fixes = fixes_from_results(line_results.into_iter()).collect();
1444 }
1445
1446 results.push(LintResult::new(first_seg.into(), fixes, desc.into(), None))
1447 }
1448
1449 line_buffer.clear();
1450 last_indent_idx = Some(i);
1451 }
1452
1453 (elem_buffer, results)
1454}
1455
1456#[derive(Default, Hash, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
1457struct FloatTypeWrapper(u64);
1458
1459impl FloatTypeWrapper {
1460 fn new(value: f64) -> Self {
1461 Self(value.to_bits())
1462 }
1463
1464 fn into_f64(self) -> f64 {
1465 f64::from_bits(self.0)
1466 }
1467}
1468
1469impl std::fmt::Debug for FloatTypeWrapper {
1470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1471 write!(f, "{:?}", f64::from_bits(self.0))
1472 }
1473}
1474
1475impl std::fmt::Display for FloatTypeWrapper {
1476 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1477 write!(f, "{:?}", f64::from_bits(self.0))
1478 }
1479}
1480
1481#[derive(Clone)]
1482pub(crate) struct Range {
1483 index: isize,
1484 start: isize,
1485 step: isize,
1486 length: isize,
1487}
1488
1489impl Range {
1490 pub(crate) fn new(start: isize, stop: isize, step: isize) -> Self {
1491 Self {
1492 index: 0,
1493 start,
1494 step,
1495 length: if step.is_negative() && start > stop {
1496 (start - stop - 1) / (-step) + 1
1497 } else if start < stop {
1498 if step.is_positive() && step == 1 {
1499 stop - start
1500 } else {
1501 (stop - start - 1) / step + 1
1502 }
1503 } else {
1504 0
1505 },
1506 }
1507 }
1508
1509 fn reversed(self) -> Self {
1510 let length = self.length;
1511 let stop = self.start - self.step;
1512 let start = stop + length * self.step;
1513 let step = -self.step;
1514
1515 Self {
1516 index: 0,
1517 start,
1518 step,
1519 length,
1520 }
1521 }
1522}
1523
1524impl Iterator for Range {
1525 type Item = isize;
1526
1527 fn next(&mut self) -> Option<Self::Item> {
1528 let index = self.index;
1529 self.index += 1;
1530 if index < self.length {
1531 Some(self.start + index * self.step)
1532 } else {
1533 None
1534 }
1535 }
1536}
1537
1538#[cfg(test)]
1539mod tests {
1540 use pretty_assertions::assert_eq;
1541 use sqruff_lib::core::test_functions::parse_ansi_string;
1542
1543 use super::{IndentLine, IndentPoint};
1544 use crate::utils::reflow::sequence::ReflowSequence;
1545
1546 #[test]
1547 fn test_reflow_point_get_indent() {
1548 let cases = [
1549 ("select 1", 1, None),
1550 ("select\n 1", 1, " ".into()),
1551 ("select\n \n \n 1", 1, " ".into()),
1552 ];
1553
1554 for (raw_sql_in, elem_idx, indent_out) in cases {
1555 let root = parse_ansi_string(raw_sql_in);
1556 let config = <_>::default();
1557 let seq = ReflowSequence::from_root(root, &config);
1558 let elem = seq.elements()[elem_idx].as_point().unwrap();
1559
1560 assert_eq!(indent_out, elem.get_indent().as_deref());
1561 }
1562 }
1563
1564 #[test]
1565 fn test_reflow_desired_indent_units() {
1566 let cases: [(IndentLine, &[usize], isize); 7] = [
1567 (
1569 IndentLine {
1570 initial_indent_balance: 0,
1571 indent_points: vec![IndentPoint {
1572 idx: 0,
1573 indent_impulse: 0,
1574 indent_trough: 0,
1575 initial_indent_balance: 0,
1576 last_line_break_idx: None,
1577 is_line_break: false,
1578 untaken_indents: Vec::new(),
1579 }],
1580 },
1581 &[],
1582 0,
1583 ),
1584 (
1586 IndentLine {
1587 initial_indent_balance: 3,
1588 indent_points: vec![IndentPoint {
1589 idx: 6,
1590 indent_impulse: 0,
1591 indent_trough: 0,
1592 initial_indent_balance: 3,
1593 last_line_break_idx: 1.into(),
1594 is_line_break: true,
1595 untaken_indents: Vec::new(),
1596 }],
1597 },
1598 &[],
1599 3,
1600 ),
1601 (
1602 IndentLine {
1603 initial_indent_balance: 3,
1604 indent_points: vec![IndentPoint {
1605 idx: 6,
1606 indent_impulse: 0,
1607 indent_trough: 0,
1608 initial_indent_balance: 3,
1609 last_line_break_idx: Some(1),
1610 is_line_break: true,
1611 untaken_indents: vec![1],
1612 }],
1613 },
1614 &[],
1615 2,
1616 ),
1617 (
1618 IndentLine {
1619 initial_indent_balance: 3,
1620 indent_points: vec![IndentPoint {
1621 idx: 6,
1622 indent_impulse: 0,
1623 indent_trough: 0,
1624 initial_indent_balance: 3,
1625 last_line_break_idx: Some(1),
1626 is_line_break: true,
1627 untaken_indents: vec![1, 2],
1628 }],
1629 },
1630 &[],
1631 1,
1632 ),
1633 (
1634 IndentLine {
1635 initial_indent_balance: 3,
1636 indent_points: vec![IndentPoint {
1637 idx: 6,
1638 indent_impulse: 0,
1639 indent_trough: 0,
1640 initial_indent_balance: 3,
1641 last_line_break_idx: Some(1),
1642 is_line_break: true,
1643 untaken_indents: vec![2],
1644 }],
1645 },
1646 &[2], 3,
1648 ),
1649 (
1650 IndentLine {
1651 initial_indent_balance: 3,
1652 indent_points: vec![IndentPoint {
1653 idx: 6,
1654 indent_impulse: 0,
1655 indent_trough: 0,
1656 initial_indent_balance: 3,
1657 last_line_break_idx: Some(1),
1658 is_line_break: true,
1659 untaken_indents: vec![3],
1660 }],
1661 },
1662 &[],
1663 2,
1664 ),
1665 (
1666 IndentLine {
1667 initial_indent_balance: 3,
1668 indent_points: vec![IndentPoint {
1669 idx: 6,
1670 indent_impulse: 0,
1671 indent_trough: -1,
1672 initial_indent_balance: 3,
1673 last_line_break_idx: Some(1),
1674 is_line_break: true,
1675 untaken_indents: vec![3],
1676 }],
1677 },
1678 &[],
1679 3,
1680 ),
1681 ];
1682
1683 for (indent_line, forced_indents, expected_units) in cases {
1684 assert_eq!(
1685 indent_line.desired_indent_units(forced_indents),
1686 expected_units
1687 );
1688 }
1689 }
1690}