1#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct VimdiffGenCmd {
10 pub final_cmd: String,
12 pub final_target: &'static str,
14}
15
16#[must_use]
22pub fn vimdiff_gen_cmd(layout: &str) -> VimdiffGenCmd {
23 let final_target = if layout.contains("@LOCAL") {
24 "LOCAL"
25 } else if layout.contains("@BASE") {
26 "BASE"
27 } else if layout.contains("@REMOTE") {
28 "REMOTE"
29 } else {
30 "MERGED"
31 };
32
33 let mut cmd = String::new();
34 for (tab_idx, tab) in layout.split('+').enumerate() {
35 if tab_idx == 0 {
36 cmd.push_str("echo");
37 } else {
38 cmd.push_str(" | tabnew");
39 }
40
41 if !tab.contains(',') && !tab.contains('/') {
42 cmd.push_str(" | silent execute 'bufdo diffthis'");
43 }
44
45 cmd = gen_cmd_aux(tab, cmd);
46 }
47
48 cmd.push_str(" | execute 'tabdo windo diffthis'");
49 let final_cmd = format!("-c \"set hidden diffopt-=hiddenoff | {cmd} | tabfirst\"");
50
51 VimdiffGenCmd {
52 final_cmd,
53 final_target,
54 }
55}
56
57#[must_use]
63pub fn vimdiff_resolve_layout<'a>(
64 tool: &str,
65 mergetool_layout: Option<&'a str>,
66 vimdiff_layout_fallback: Option<&'a str>,
67) -> &'a str {
68 if let Some(l) = mergetool_layout.filter(|s| !s.is_empty()) {
69 return l;
70 }
71 if let Some(l) = vimdiff_layout_fallback.filter(|s| !s.is_empty()) {
72 return l;
73 }
74
75 if tool.ends_with("vimdiff1") {
76 return "@LOCAL,REMOTE";
77 }
78 if tool.ends_with("vimdiff2") {
79 return "LOCAL,MERGED,REMOTE";
80 }
81 if tool.ends_with("vimdiff3") {
82 return "MERGED";
83 }
84
85 if tool.contains("vimdiff") {
86 return "(LOCAL,BASE,REMOTE)/MERGED";
87 }
88
89 "(LOCAL,BASE,REMOTE)/MERGED"
90}
91
92#[must_use]
94pub fn vimdiff_executable_for_tool(tool: &str) -> Option<&'static str> {
95 if tool.starts_with("nvimdiff") {
96 return Some("nvim");
97 }
98 if tool.starts_with("gvimdiff") {
99 return Some("gvim");
100 }
101 if tool.starts_with("vimdiff") {
102 return Some("vim");
103 }
104 None
105}
106
107#[must_use]
109pub fn vimdiff_cmd_without_base(final_cmd: &str) -> String {
110 final_cmd
111 .replace("2b", "quit")
112 .replace("3b", "2b")
113 .replace("4b", "3b")
114}
115
116fn substring_bytes(s: &str, start: usize, len: usize) -> &str {
117 let b = s.as_bytes();
118 if start >= b.len() || len == 0 {
119 return "";
120 }
121 let end = (start + len).min(b.len());
122 s.get(start..end).unwrap_or("")
124}
125
126fn gen_cmd_aux(layout: &str, mut cmd: String) -> String {
127 let b = layout.as_bytes();
128 let mut start = 0usize;
129 let mut end = b.len();
130
131 let mut nested = 0i32;
132 let mut nested_min = 100i32;
133 for &ch in b {
134 let c = ch as char;
135 if c == ' ' {
136 continue;
137 }
138 if c == '(' {
139 nested += 1;
140 continue;
141 }
142 if c == ')' {
143 nested -= 1;
144 continue;
145 }
146 nested_min = nested_min.min(nested);
147 }
148
149 let mut nested_min = nested_min;
150 while nested_min > 0 {
151 start += 1;
152 end -= 1;
153 let mut start_minus_one = start.wrapping_sub(1);
154 while start > 0 && substring_bytes(layout, start_minus_one, 1) != "(" {
155 start += 1;
156 start_minus_one = start.wrapping_sub(1);
157 }
158 while end > 0 && substring_bytes(layout, end, 1) != ")" {
159 end -= 1;
160 }
161 nested_min -= 1;
162 }
163
164 let mut index_horizontal: Option<usize> = None;
165 let mut index_vertical: Option<usize> = None;
166 let mut nested = 0i32;
167 let slice = substring_bytes(layout, start, end.saturating_sub(start));
168 for (offset, &ch) in slice.as_bytes().iter().enumerate() {
169 let c = ch as char;
170 if c == ' ' {
171 continue;
172 }
173 if c == '(' {
174 nested += 1;
175 continue;
176 }
177 if c == ')' {
178 nested -= 1;
179 continue;
180 }
181 if nested == 0 {
182 let idx = start + offset;
183 if c == '/' && index_horizontal.is_none() {
184 index_horizontal = Some(idx);
185 } else if c == ',' && index_vertical.is_none() {
186 index_vertical = Some(idx);
187 }
188 }
189 }
190
191 if let Some(index) = index_horizontal {
192 let (before, after) = ("leftabove split", "wincmd j");
193 cmd.push_str(" | ");
194 cmd.push_str(before);
195 cmd = gen_cmd_aux(
196 substring_bytes(layout, start, index.saturating_sub(start)),
197 cmd,
198 );
199 cmd.push_str(" | ");
200 cmd.push_str(after);
201 cmd = gen_cmd_aux(
202 substring_bytes(layout, index + 1, b.len().saturating_sub(index)),
203 cmd,
204 );
205 return cmd;
206 }
207
208 if let Some(index) = index_vertical {
209 let (before, after) = ("leftabove vertical split", "wincmd l");
210 cmd.push_str(" | ");
211 cmd.push_str(before);
212 cmd = gen_cmd_aux(
213 substring_bytes(layout, start, index.saturating_sub(start)),
214 cmd,
215 );
216 cmd.push_str(" | ");
217 cmd.push_str(after);
218 cmd = gen_cmd_aux(
219 substring_bytes(layout, index + 1, b.len().saturating_sub(index)),
220 cmd,
221 );
222 return cmd;
223 }
224
225 let leaf = substring_bytes(layout, start, end.saturating_sub(start));
226 let target: String = leaf
227 .chars()
228 .filter(|c| !matches!(c, ' ' | '@' | '(' | ')' | ';' | '|' | '-'))
229 .collect();
230
231 cmd.push_str(" | ");
232 cmd.push_str(match target.as_str() {
233 "LOCAL" => "1b",
234 "BASE" => "2b",
235 "REMOTE" => "3b",
236 "MERGED" => "4b",
237 _ => {
238 return format!("{cmd} | ERROR: >{target}<");
239 }
240 });
241
242 cmd
243}
244
245#[must_use]
247pub fn vimdiff_final_cmd_script(final_cmd: &str) -> String {
248 final_cmd
249 .strip_prefix("-c \"")
250 .and_then(|s| s.strip_suffix('"'))
251 .unwrap_or(final_cmd)
252 .to_string()
253}
254
255#[must_use]
257pub fn vimdiff_merge_argv_with_base(
258 final_cmd: &str,
259 local: &str,
260 base: &str,
261 remote: &str,
262 merged: &str,
263) -> Vec<String> {
264 vec![
265 "-f".to_string(),
266 "-c".to_string(),
267 vimdiff_final_cmd_script(final_cmd),
268 local.to_string(),
269 base.to_string(),
270 remote.to_string(),
271 merged.to_string(),
272 ]
273}
274
275#[must_use]
277pub fn vimdiff_merge_argv_no_base(
278 final_cmd: &str,
279 local: &str,
280 remote: &str,
281 merged: &str,
282) -> Vec<String> {
283 vec![
284 "-f".to_string(),
285 "-c".to_string(),
286 vimdiff_final_cmd_script(final_cmd),
287 local.to_string(),
288 remote.to_string(),
289 merged.to_string(),
290 ]
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn t7609_vimdiff_gen_cmd_cases() {
299 const CASES: &[&str] = &[
300 "(LOCAL,BASE,REMOTE)/MERGED",
301 "@LOCAL,REMOTE",
302 "LOCAL,MERGED,REMOTE",
303 "MERGED",
304 "LOCAL/MERGED/REMOTE",
305 "(LOCAL/REMOTE),MERGED",
306 "MERGED,(LOCAL/REMOTE)",
307 "(LOCAL,REMOTE)/MERGED",
308 "MERGED/(LOCAL,REMOTE)",
309 "(LOCAL/BASE/REMOTE),MERGED",
310 "(LOCAL,BASE,REMOTE)/MERGED+BASE,LOCAL+BASE,REMOTE+(LOCAL/BASE/REMOTE),MERGED",
311 "((LOCAL,REMOTE)/BASE),MERGED",
312 "((LOCAL,REMOTE)/BASE),((LOCAL/REMOTE),MERGED)",
313 "BASE,REMOTE+BASE,LOCAL",
314 " (( (LOCAL , BASE , REMOTE) / MERGED)) +(BASE) , LOCAL+ BASE , REMOTE+ (((LOCAL / BASE / REMOTE)) , MERGED ) ",
315 "LOCAL,BASE,REMOTE / MERGED + BASE,LOCAL + BASE,REMOTE + (LOCAL / BASE / REMOTE),MERGED",
316 "(LOCAL,@BASE,REMOTE)/MERGED",
317 "LOCAL,@REMOTE",
318 "@REMOTE",
319 ];
320
321 const EXPECTED_CMD: &[&str] = &[
322 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | execute 'tabdo windo diffthis' | tabfirst\"",
323 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 1b | wincmd l | 3b | execute 'tabdo windo diffthis' | tabfirst\"",
324 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 4b | wincmd l | 3b | execute 'tabdo windo diffthis' | tabfirst\"",
325 "-c \"set hidden diffopt-=hiddenoff | echo | silent execute 'bufdo diffthis' | 4b | execute 'tabdo windo diffthis' | tabfirst\"",
326 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | 1b | wincmd j | leftabove split | 4b | wincmd j | 3b | execute 'tabdo windo diffthis' | tabfirst\"",
327 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | leftabove split | 1b | wincmd j | 3b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"",
328 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 4b | wincmd l | leftabove split | 1b | wincmd j | 3b | execute 'tabdo windo diffthis' | tabfirst\"",
329 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 4b | execute 'tabdo windo diffthis' | tabfirst\"",
330 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | 4b | wincmd j | leftabove vertical split | 1b | wincmd l | 3b | execute 'tabdo windo diffthis' | tabfirst\"",
331 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"",
332 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnew | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"",
333 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"",
334 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | leftabove split | leftabove vertical split | 1b | wincmd l | 3b | wincmd j | 2b | wincmd l | leftabove vertical split | leftabove split | 1b | wincmd j | 3b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"",
335 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 2b | wincmd l | 3b | tabnew | leftabove vertical split | 2b | wincmd l | 1b | execute 'tabdo windo diffthis' | tabfirst\"",
336 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnew | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"",
337 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | tabnew | leftabove vertical split | 2b | wincmd l | 1b | tabnew | leftabove vertical split | 2b | wincmd l | 3b | tabnew | leftabove vertical split | leftabove split | 1b | wincmd j | leftabove split | 2b | wincmd j | 3b | wincmd l | 4b | execute 'tabdo windo diffthis' | tabfirst\"",
338 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | 2b | wincmd l | 3b | wincmd j | 4b | execute 'tabdo windo diffthis' | tabfirst\"",
339 "-c \"set hidden diffopt-=hiddenoff | echo | leftabove vertical split | 1b | wincmd l | 3b | execute 'tabdo windo diffthis' | tabfirst\"",
340 "-c \"set hidden diffopt-=hiddenoff | echo | silent execute 'bufdo diffthis' | 3b | execute 'tabdo windo diffthis' | tabfirst\"",
341 ];
342
343 const EXPECTED_TARGET: &[&str] = &[
344 "MERGED", "LOCAL", "MERGED", "MERGED", "MERGED", "MERGED", "MERGED", "MERGED",
345 "MERGED", "MERGED", "MERGED", "MERGED", "MERGED", "MERGED", "MERGED", "MERGED", "BASE",
346 "REMOTE", "REMOTE",
347 ];
348
349 assert_eq!(CASES.len(), EXPECTED_CMD.len());
350 assert_eq!(CASES.len(), EXPECTED_TARGET.len());
351
352 for (i, layout) in CASES.iter().enumerate() {
353 let g = vimdiff_gen_cmd(layout);
354 assert_eq!(
355 g.final_cmd,
356 EXPECTED_CMD[i],
357 "case {} layout {:?}",
358 i + 1,
359 layout
360 );
361 assert_eq!(g.final_target, EXPECTED_TARGET[i], "target case {}", i + 1);
362 }
363 }
364
365 #[test]
366 fn t7609_merge_argv_paths_with_spaces() {
367 let g = vimdiff_gen_cmd("(LOCAL,BASE,REMOTE)/MERGED");
368 let adjusted = vimdiff_cmd_without_base(&g.final_cmd);
369 let argv = vimdiff_merge_argv_no_base(&adjusted, "lo cal", "' '", "mer ged");
370 assert_eq!(
371 argv,
372 vec![
373 "-f".to_string(),
374 "-c".to_string(),
375 "set hidden diffopt-=hiddenoff | echo | leftabove split | leftabove vertical split | 1b | wincmd l | leftabove vertical split | quit | wincmd l | 2b | wincmd j | 3b | execute 'tabdo windo diffthis' | tabfirst".to_string(),
376 "lo cal".to_string(),
377 "' '".to_string(),
378 "mer ged".to_string(),
379 ],
380 "merge_cmd without base: three path args, single -c string"
381 );
382 }
383}