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