1use std::collections::{HashMap, HashSet};
2
3use crate::diff::RowKind;
4use crate::review::{DiffModes, FileReview};
5
6pub const MIN_MOVED_LINES: usize = 3;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Direction {
12 To,
14 From,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Move {
21 pub direction: Direction,
22 pub path: String,
24 pub line: usize,
25 pub starts_block: bool,
27 pub block_len: usize,
28}
29
30impl Move {
31 pub fn describe(&self, here: &str) -> String {
33 let direction = match self.direction {
34 Direction::To => "to",
35 Direction::From => "from",
36 };
37 let place = if self.path == here {
38 format!("line {}", self.line)
39 } else {
40 format!("{}:{}", self.path, self.line)
41 };
42 format!("⇄ {} lines moved {direction} {place}", self.block_len)
43 }
44}
45
46pub type Moves = HashMap<(usize, usize, usize), Move>;
48
49struct Entry {
50 key: (usize, usize, usize),
51 path: String,
52 line: usize,
53 text: String,
55}
56
57pub fn detect(files: &[&FileReview], modes: DiffModes) -> Moves {
60 let mut removed: Vec<Vec<Entry>> = Vec::new();
61 let mut added: Vec<Vec<Entry>> = Vec::new();
62 for (file_index, file) in files.iter().enumerate() {
63 let view = file.view(modes);
64 for (hunk_index, hunk) in view.hunks.iter().enumerate() {
65 let mut run_kind = RowKind::Context;
66 for (row_index, row) in hunk.rows.iter().enumerate() {
67 let text = row.text.trim();
68 if row.kind != run_kind {
69 run_kind = row.kind;
70 match row.kind {
71 RowKind::Removed => removed.push(Vec::new()),
72 RowKind::Added => added.push(Vec::new()),
73 RowKind::Context => continue,
74 }
75 }
76 if text.is_empty() || row.kind == RowKind::Context {
78 continue;
79 }
80 let (runs, line) = match row.kind {
81 RowKind::Removed => (&mut removed, row.old_line),
82 _ => (&mut added, row.new_line),
83 };
84 if let (Some(run), Some(line)) = (runs.last_mut(), line) {
85 run.push(Entry {
86 key: (file_index, hunk_index, row_index),
87 path: file.path.clone(),
88 line,
89 text: text.to_string(),
90 });
91 }
92 }
93 }
94 }
95
96 let window = |run: &[Entry], at: usize| -> String {
97 run[at..at + MIN_MOVED_LINES]
98 .iter()
99 .map(|entry| entry.text.as_str())
100 .collect::<Vec<_>>()
101 .join("\n")
102 };
103 let mut index: HashMap<String, Vec<(usize, usize)>> = HashMap::new();
104 for (run_index, run) in added.iter().enumerate() {
105 for at in 0..(run.len() + 1).saturating_sub(MIN_MOVED_LINES) {
106 index
107 .entry(window(run, at))
108 .or_default()
109 .push((run_index, at));
110 }
111 }
112
113 let mut moves = Moves::new();
114 let mut claimed: HashSet<(usize, usize)> = HashSet::new();
115 for run in &removed {
116 let mut at = 0;
117 while at + MIN_MOVED_LINES <= run.len() {
118 let best = index
119 .get(&window(run, at))
120 .into_iter()
121 .flatten()
122 .map(|&(target, start)| {
123 let other = &added[target];
124 let len = (0..)
125 .take_while(|&k| {
126 at + k < run.len()
127 && start + k < other.len()
128 && run[at + k].text == other[start + k].text
129 && !claimed.contains(&(target, start + k))
130 })
131 .count();
132 (len, target, start)
133 })
134 .max_by_key(|&(len, _, _)| len);
135 let Some((len, target, start)) = best.filter(|&(len, _, _)| len >= MIN_MOVED_LINES)
136 else {
137 at += 1;
138 continue;
139 };
140 let block = &run[at..at + len];
141 let substance: usize = block
143 .iter()
144 .map(|entry| entry.text.chars().filter(|c| c.is_alphanumeric()).count())
145 .sum();
146 if substance < 10 {
147 at += 1;
148 continue;
149 }
150 let destination = &added[target][start..start + len];
151 for (k, (from, to)) in block.iter().zip(destination).enumerate() {
152 claimed.insert((target, start + k));
153 moves.insert(
154 from.key,
155 Move {
156 direction: Direction::To,
157 path: destination[0].path.clone(),
158 line: destination[0].line,
159 starts_block: k == 0,
160 block_len: len,
161 },
162 );
163 moves.insert(
164 to.key,
165 Move {
166 direction: Direction::From,
167 path: block[0].path.clone(),
168 line: block[0].line,
169 starts_block: k == 0,
170 block_len: len,
171 },
172 );
173 }
174 at += len;
175 }
176 }
177 moves
178}