1use std::cmp::Ordering;
5use std::hash::Hash;
6use std::ops::{Add, Range};
7
8use crate::intern::Token;
9
10pub trait SliderHeuristic {
16 fn best_slider_end(&mut self, tokens: &[Token], hunk: Range<u32>, earliest_end: u32) -> u32;
28}
29
30impl<F> SliderHeuristic for F
31where
32 F: FnMut(&[Token], Range<u32>, u32) -> u32,
33{
34 fn best_slider_end(&mut self, tokens: &[Token], hunk: Range<u32>, earliest_end: u32) -> u32 {
35 self(tokens, hunk, earliest_end)
36 }
37}
38
39pub struct NoSliderHeuristic;
44
45impl SliderHeuristic for NoSliderHeuristic {
46 fn best_slider_end(&mut self, _tokens: &[Token], hunk: Range<u32>, _earliest_end: u32) -> u32 {
47 hunk.end
48 }
49}
50
51pub struct IndentHeuristic<IndentOfToken> {
57 indent_of_token: IndentOfToken,
59}
60
61impl<IndentOfToken> IndentHeuristic<IndentOfToken> {
62 pub fn new(indent_of_token: IndentOfToken) -> Self {
68 Self { indent_of_token }
69 }
70}
71
72impl<IndentOfToken: Fn(Token) -> IndentLevel> SliderHeuristic for IndentHeuristic<IndentOfToken> {
73 fn best_slider_end(&mut self, tokens: &[Token], hunk: Range<u32>, earliest_end: u32) -> u32 {
74 const MAX_SLIDING: u32 = 100;
75 let mut top_slider_end = earliest_end;
78 if top_slider_end < hunk.start - 1 {
80 top_slider_end = hunk.start - 1;
81 }
82 if hunk.end > top_slider_end + MAX_SLIDING {
83 top_slider_end = hunk.end - MAX_SLIDING;
84 }
85 let group_size = hunk.end - hunk.start;
86 let mut best_score = Score::for_range(
87 top_slider_end - group_size..top_slider_end,
88 tokens,
89 &self.indent_of_token,
90 );
91 let mut best_slider_end = top_slider_end;
92 for slider_end in (top_slider_end + 1)..=hunk.end {
93 let score = Score::for_range(slider_end - group_size..slider_end, tokens, &self.indent_of_token);
94 if score.is_improvement_over(best_score) {
95 best_score = score;
96 best_slider_end = slider_end;
97 }
98 }
99 best_slider_end
100 }
101}
102
103#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd)]
108pub struct IndentLevel(u8);
109
110impl IndentLevel {
111 const BLANK: IndentLevel = IndentLevel(u8::MAX);
113 const MAX: IndentLevel = IndentLevel(200);
115
116 pub fn for_ascii_line(src: impl IntoIterator<Item = u8>, tab_width: u8) -> IndentLevel {
127 let mut indent_level = IndentLevel(0);
128 let tab_width = tab_width.max(1);
129 for c in src {
130 match c {
131 b' ' => indent_level = indent_level.increased_by(1),
132 b'\t' => indent_level = indent_level.increased_by(tab_width - indent_level.0 % tab_width),
133 b'\r' | b'\n' | b'\x0C' => (),
134 _ => return indent_level,
135 }
136 if indent_level >= Self::MAX {
137 return Self::MAX;
138 }
139 }
140 IndentLevel::BLANK
141 }
142
143 pub fn for_line(src: impl IntoIterator<Item = char>, tab_width: u8) -> IndentLevel {
154 let mut indent_level = IndentLevel(0);
155 let tab_width = tab_width.max(1);
156 for c in src {
157 match c {
158 ' ' => indent_level = indent_level.increased_by(1),
159 '\t' => indent_level = indent_level.increased_by(tab_width - indent_level.0 % tab_width),
160 '\r' | '\n' | '\x0C' => (),
161 _ => return indent_level,
162 }
163 if indent_level >= Self::MAX {
164 return Self::MAX;
165 }
166 }
167 IndentLevel::BLANK
168 }
169
170 fn increased_by(self, amount: u8) -> Self {
171 IndentLevel(self.0.saturating_add(amount).min(Self::MAX.0))
172 }
173
174 fn map_or<T>(self, default: T, f: impl FnOnce(u8) -> T) -> T {
175 if self == Self::BLANK { default } else { f(self.0) }
176 }
177
178 fn or(self, default: Self) -> Self {
179 if self == Self::BLANK { default } else { self }
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub struct Indents {
188 indent: IndentLevel,
190 prev_indent: IndentLevel,
192 next_indent: IndentLevel,
194 leading_blanks: u8,
196 trailing_blanks: u8,
198 at_eof: bool,
200}
201
202const MAX_BLANKS: usize = 20;
204
205impl Indents {
206 fn at_token(tokens: &[Token], token_idx: usize, indent_of_token: impl Fn(Token) -> IndentLevel) -> Indents {
207 let (leading_blank_lines, indent_previous_line) = tokens[..token_idx]
208 .iter()
209 .rev()
210 .enumerate()
211 .find_map(|(i, &token)| {
212 if i == MAX_BLANKS {
213 Some((i, IndentLevel(0)))
214 } else {
215 let level = indent_of_token(token);
216 if level == IndentLevel::BLANK {
217 None
218 } else {
219 Some((i, level))
220 }
221 }
222 })
223 .unwrap_or((token_idx, IndentLevel::BLANK));
224 let at_eof = token_idx == tokens.len();
225 let (trailing_blank_lines, indent_next_line) = if at_eof {
226 (0, IndentLevel::BLANK)
227 } else {
228 tokens[token_idx + 1..]
229 .iter()
230 .enumerate()
231 .find_map(|(i, &token)| {
232 if i == MAX_BLANKS {
233 Some((i, IndentLevel(0)))
234 } else {
235 let level = indent_of_token(token);
236 if level == IndentLevel::BLANK {
237 None
238 } else {
239 Some((i, level))
240 }
241 }
242 })
243 .unwrap_or((tokens.len() - token_idx - 1, IndentLevel::BLANK))
244 };
245 let indent = tokens
246 .get(token_idx)
247 .map_or(IndentLevel::BLANK, |&token| indent_of_token(token));
248 Indents {
249 indent,
250 prev_indent: indent_previous_line,
251 next_indent: indent_next_line,
252 leading_blanks: leading_blank_lines as u8,
253 trailing_blanks: trailing_blank_lines as u8,
254 at_eof,
255 }
256 }
257
258 fn score(&self) -> Score {
259 let mut penalty = 0;
260 if self.prev_indent == IndentLevel::BLANK && self.leading_blanks == 0 {
261 penalty += START_OF_FILE_PENALTY;
262 }
263 if self.at_eof {
264 penalty += END_OF_FILE_PENALTY;
265 }
266
267 let trailing_blank_lines = if self.indent == IndentLevel::BLANK {
268 self.trailing_blanks as i32 + 1
269 } else {
270 0
271 };
272 let total_blank_lines = trailing_blank_lines + self.leading_blanks as i32;
273 penalty += TOTAL_BLANK_LINE_WEIGHT * total_blank_lines + trailing_blank_lines * TRAILING_BLANK_LINES_WEIGHT;
274 let indent = self.indent.or(self.next_indent);
275 if indent != IndentLevel::BLANK && self.prev_indent != IndentLevel::BLANK {
276 match indent.0.cmp(&self.prev_indent.0) {
277 Ordering::Equal => {}
278 Ordering::Less if self.next_indent.0 <= indent.0 => {
281 penalty += if total_blank_lines != 0 {
282 RELATIVE_DEDENT_WITH_BLANK_PENALTY
283 } else {
284 RELATIVE_DEDENT_PENALTY
285 }
286 }
287 Ordering::Less => {
288 penalty += if total_blank_lines != 0 {
289 RELATIVE_OUTDENT_WITH_BLANK_PENALTY
290 } else {
291 RELATIVE_OUTDENT_PENALTY
292 }
293 }
294 Ordering::Greater => {
295 penalty += if total_blank_lines != 0 {
296 RELATIVE_INDENT_WITH_BLANK_PENALTY
297 } else {
298 RELATIVE_INDENT_PENALTY
299 }
300 }
301 }
302 }
303 Score {
304 indent: indent.map_or(-1, i32::from),
305 penalty,
306 }
307 }
308}
309
310const START_OF_FILE_PENALTY: i32 = 1;
312const END_OF_FILE_PENALTY: i32 = 21;
314const TOTAL_BLANK_LINE_WEIGHT: i32 = -30;
316const TRAILING_BLANK_LINES_WEIGHT: i32 = 6;
318
319const RELATIVE_INDENT_PENALTY: i32 = -4;
321const RELATIVE_INDENT_WITH_BLANK_PENALTY: i32 = 10;
323
324const RELATIVE_OUTDENT_PENALTY: i32 = 24;
326const RELATIVE_OUTDENT_WITH_BLANK_PENALTY: i32 = 17;
328
329const RELATIVE_DEDENT_PENALTY: i32 = 23;
331const RELATIVE_DEDENT_WITH_BLANK_PENALTY: i32 = 17;
333
334const INDENT_WEIGHT: i32 = 60;
336
337#[derive(PartialEq, Eq, Clone, Copy)]
342struct Score {
343 indent: i32,
345 penalty: i32,
347}
348
349impl Score {
350 fn for_range(range: Range<u32>, tokens: &[Token], indent_of_token: impl Fn(Token) -> IndentLevel) -> Score {
351 Indents::at_token(tokens, range.start as usize, &indent_of_token).score()
352 + Indents::at_token(tokens, range.end as usize, &indent_of_token).score()
353 }
354}
355
356impl Add for Score {
357 type Output = Score;
358
359 fn add(self, rhs: Self) -> Self::Output {
360 Score {
361 indent: self.indent + rhs.indent,
362 penalty: self.penalty + rhs.penalty,
363 }
364 }
365}
366
367impl Score {
368 fn is_improvement_over(self, prev_score: Self) -> bool {
369 let indent_score = match prev_score.indent.cmp(&self.indent) {
371 Ordering::Less => INDENT_WEIGHT,
372 Ordering::Greater => -INDENT_WEIGHT,
373 Ordering::Equal => 0,
374 };
375 (indent_score + self.penalty - prev_score.penalty) <= 0
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::{IndentLevel, Indents};
382 use crate::intern::Token;
383
384 #[test]
385 fn trailing_blanks_are_counted_not_positioned() {
386 const BLANK: Token = Token(0);
391 const CODE: Token = Token(1);
392 let indent = |token: Token| {
393 if token == BLANK {
394 IndentLevel::BLANK
395 } else {
396 IndentLevel(0)
397 }
398 };
399
400 let mut tokens = vec![CODE; 300];
401 tokens.extend([BLANK; 3]);
402 let split = tokens.len() - 4;
403 let indents = Indents::at_token(&tokens, split, indent);
404 assert_eq!(indents.trailing_blanks, 3);
405 }
406
407 #[test]
408 fn ascii_indent_clamps_before_overflow() {
409 assert_eq!(
410 IndentLevel::for_ascii_line(std::iter::repeat_n(b' ', 255), 1),
411 IndentLevel::MAX
412 );
413 assert_eq!(
414 IndentLevel::for_ascii_line(std::iter::repeat_n(b'\t', 8), u8::MAX),
415 IndentLevel::MAX
416 );
417 }
418
419 #[test]
420 fn unicode_indent_treats_zero_tab_width_as_one() {
421 assert_eq!(IndentLevel::for_line(['\t', 'x'], 0), IndentLevel(1));
422 }
423}