1extern crate ratatui as tui;
6
7use ansi_term::Colour;
11use std::ffi::c_char;
12use std::fmt::Write;
13use std::sync::{Arc, Mutex};
14use std::{borrow::Cow, vec};
15
16use tui::{prelude::Line, style::Color};
17
18pub const COLOR_BATCH_LINE_NUMBER_DEFAULT: Colour = Colour::Fixed(240);
20pub const COLOR_BATCH_LINE_NUMBER_ADD: Colour = Colour::RGB(56, 119, 120);
21pub const COLOR_BATCH_LINE_NUMBER_REM: Colour = Colour::RGB(118, 0, 0);
22pub const COLOR_BATCH_LINE_ADD: Colour = Colour::Green;
23pub const COLOR_BATCH_LINE_REM: Colour = Colour::Red;
24pub const COLOR_BATCH_LINE_REVERSE_FG: Colour = Colour::White;
25pub const COLOR_WATCH_LINE_NUMBER_DEFAULT: Color = Color::DarkGray;
26pub const COLOR_WATCH_LINE_NUMBER_ADD: Color = Color::Rgb(56, 119, 120);
27pub const COLOR_WATCH_LINE_NUMBER_REM: Color = Color::Rgb(118, 0, 0);
28pub const COLOR_WATCH_LINE_ADD: Color = Color::Green;
29pub const COLOR_WATCH_LINE_REM: Color = Color::Red;
30pub const COLOR_WATCH_LINE_REVERSE_FG: Color = Color::White;
31pub const PLUGIN_ABI_VERSION: u32 = 2;
32pub const PLUGIN_ABI_VERSION_V1: u32 = 1;
33pub const PLUGIN_OUTPUT_BATCH: u32 = 0;
34pub const PLUGIN_OUTPUT_WATCH: u32 = 1;
35
36pub type DiffModeMutex = Arc<Mutex<Box<dyn DiffMode>>>;
38
39#[repr(C)]
40#[derive(Clone, Copy)]
41pub struct PluginSlice {
42 pub ptr: *const u8,
43 pub len: usize,
44}
45
46#[repr(C)]
47#[derive(Clone, Copy)]
48pub struct PluginOwnedBytes {
49 pub ptr: *mut u8,
50 pub len: usize,
51 pub cap: usize,
52}
53
54#[repr(C)]
55#[derive(Clone, Copy)]
56pub struct PluginDiffRequestV1 {
57 pub dest: PluginSlice,
58 pub src: PluginSlice,
59 pub output_kind: u32,
60 pub color: bool,
61 pub line_number: bool,
62 pub only_diffline: bool,
63}
64
65#[repr(C)]
66#[derive(Clone, Copy)]
67pub struct PluginDiffRequest {
68 pub dest: PluginSlice,
69 pub src: PluginSlice,
70 pub output_kind: u32,
71 pub color: bool,
72 pub line_number: bool,
73 pub only_diffline: bool,
74 pub ignore_spaceblock: bool,
75}
76
77#[repr(C)]
78#[derive(Clone, Copy)]
79pub struct PluginMetadata {
80 pub abi_version: u32,
81 pub supports_only_diffline: bool,
82 pub plugin_name: *const c_char,
83 pub header_text: *const c_char,
84}
85
86pub enum OutputVecData<'a> {
88 Lines(Vec<Line<'a>>),
89 Strings(Vec<String>),
90}
91
92pub enum OutputVecElementData<'a> {
94 Line(Line<'a>),
95 String(String),
96 None(),
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub enum DifferenceType {
102 Same,
103 Add,
104 Rem,
105}
106
107pub struct DiffRow<'a> {
108 pub watch_line: Line<'a>,
109 pub batch_line: String,
110 pub line_number: Option<usize>,
111 pub diff_type: DifferenceType,
112}
113
114pub trait StringExt {
118 fn expand_tabs(&self, tab_size: u16) -> Cow<'_, str>;
119}
120
121impl<T> StringExt for T
122where
123 T: AsRef<str>,
124{
125 fn expand_tabs(&self, tab_size: u16) -> Cow<'_, str> {
126 let s = self.as_ref();
127 let tab = '\t';
128
129 if s.contains(tab) {
130 let mut res = String::new();
131 let mut last_pos = 0;
132
133 while let Some(pos) = &s[last_pos..].find(tab) {
134 res.push_str(&s[last_pos..*pos + last_pos]);
135
136 let spaces_to_add = if tab_size != 0 {
137 tab_size - (*pos as u16 % tab_size)
138 } else {
139 0
140 };
141
142 if spaces_to_add != 0 {
143 let _ = write!(res, "{:width$}", "", width = spaces_to_add as usize);
144 }
145
146 last_pos += *pos + 1;
147 }
148
149 res.push_str(&s[last_pos..]);
150
151 Cow::from(res)
152 } else {
153 Cow::from(s)
154 }
155 }
156}
157
158#[derive(Debug, Clone, Copy)]
160pub struct DiffModeOptions {
161 color: bool,
163
164 line_number: bool,
166
167 only_diffline: bool,
169
170 ignore_spaceblock: bool,
172}
173
174impl DiffModeOptions {
175 pub fn new() -> Self {
176 Self {
177 color: false,
178 line_number: false,
179 only_diffline: false,
180 ignore_spaceblock: false,
181 }
182 }
183
184 pub fn get_color(&self) -> bool {
185 self.color
186 }
187
188 pub fn set_color(&mut self, color: bool) {
189 self.color = color;
190 }
191
192 pub fn get_line_number(&self) -> bool {
193 self.line_number
194 }
195
196 pub fn set_line_number(&mut self, line_number: bool) {
197 self.line_number = line_number;
198 }
199
200 pub fn get_only_diffline(&self) -> bool {
201 self.only_diffline
202 }
203
204 pub fn set_only_diffline(&mut self, only_diffline: bool) {
205 self.only_diffline = only_diffline;
206 }
207
208 pub fn get_ignore_spaceblock(&self) -> bool {
209 self.ignore_spaceblock
210 }
211
212 pub fn set_ignore_spaceblock(&mut self, ignore_spaceblock: bool) {
213 self.ignore_spaceblock = ignore_spaceblock;
214 }
215}
216
217impl Default for DiffModeOptions {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223pub trait DiffMode: Send {
225 fn generate_watch_diff(&mut self, dest: &str, src: &str) -> Vec<Line<'static>>;
227
228 fn generate_batch_diff(&mut self, dest: &str, src: &str) -> Vec<String>;
230
231 fn get_header_text(&self) -> String;
233
234 fn get_support_only_diffline(&self) -> bool;
236
237 fn set_option(&mut self, options: DiffModeOptions);
239}
240
241pub trait DiffModeExt: DiffMode {
243 fn get_option<T: 'static>(&self) -> DiffModeOptions;
244
245 fn get_header_width<T: 'static>(&self) -> usize;
246}
247
248pub fn expand_line_tab(data: &str, tab_size: u16) -> String {
249 let mut result_vec: Vec<String> = vec![];
250 for d in data.lines() {
251 let l = d.expand_tabs(tab_size).to_string();
252 result_vec.push(l);
253 }
254
255 result_vec.join("\n")
256}
257
258pub fn normalize_space_blocks(data: &str) -> String {
259 let mut normalized = String::with_capacity(data.len());
260 let mut in_spaceblock = false;
261
262 for ch in data.chars() {
263 if ch == '\n' {
264 normalized.push('\n');
265 in_spaceblock = false;
266 continue;
267 }
268
269 if ch.is_whitespace() {
270 if !in_spaceblock {
271 normalized.push(' ');
272 in_spaceblock = true;
273 }
274 } else {
275 normalized.push(ch);
276 in_spaceblock = false;
277 }
278 }
279
280 normalized
281}
282
283pub fn text_eq_ignoring_space_blocks(left: &str, right: &str, enabled: bool) -> bool {
284 if !enabled {
285 return left == right;
286 }
287
288 normalize_space_blocks(left) == normalize_space_blocks(right)
289}
290
291pub fn gen_counter_str(
292 is_color: bool,
293 counter: usize,
294 header_width: usize,
295 diff_type: DifferenceType,
296) -> String {
297 let mut counter_str = counter.to_string();
298 let mut seprator = " | ".to_string();
299 let mut prefix_width = 0;
300 let mut suffix_width = 0;
301
302 if is_color {
303 let style: ansi_term::Style = match diff_type {
304 DifferenceType::Same => ansi_term::Style::default().fg(COLOR_BATCH_LINE_NUMBER_DEFAULT),
305 DifferenceType::Add => ansi_term::Style::default().fg(COLOR_BATCH_LINE_NUMBER_ADD),
306 DifferenceType::Rem => ansi_term::Style::default().fg(COLOR_BATCH_LINE_NUMBER_REM),
307 };
308 counter_str = style.paint(counter_str).to_string();
309 seprator = style.paint(seprator).to_string();
310 prefix_width = style.prefix().to_string().len();
311 suffix_width = style.suffix().to_string().len();
312 }
313
314 let width = header_width + prefix_width + suffix_width;
315 format!("{counter_str:>width$}{seprator}")
316}
317
318pub fn expand_output_vec_element_data(
319 is_batch: bool,
320 data: Vec<OutputVecElementData>,
321) -> OutputVecData {
322 let mut lines = Vec::new();
323 let mut strings = Vec::new();
324
325 for element in data {
326 match element {
327 OutputVecElementData::Line(line) => {
328 lines.push(line);
329 }
330 OutputVecElementData::String(string) => {
331 strings.push(string);
332 }
333 _ => {}
334 }
335 }
336
337 if is_batch {
338 OutputVecData::Strings(strings)
339 } else {
340 OutputVecData::Lines(lines)
341 }
342}
343
344pub fn render_diff_rows_as_watch<'a>(
345 rows: Vec<DiffRow<'a>>,
346 is_line_number: bool,
347 header_width: usize,
348) -> Vec<Line<'a>> {
349 rows.into_iter()
350 .map(|mut row| {
351 if is_line_number {
352 let style = tui::style::Style::default().fg(match row.diff_type {
353 DifferenceType::Same => COLOR_WATCH_LINE_NUMBER_DEFAULT,
354 DifferenceType::Add => COLOR_WATCH_LINE_NUMBER_ADD,
355 DifferenceType::Rem => COLOR_WATCH_LINE_NUMBER_REM,
356 });
357 let prefix = match row.line_number {
358 Some(line_number) => format!("{line_number:>header_width$} | "),
359 None => format!("{:>header_width$} | ", ""),
360 };
361 row.watch_line
362 .spans
363 .insert(0, tui::text::Span::styled(prefix, style));
364 }
365 row.watch_line
366 })
367 .collect()
368}
369
370pub fn render_diff_rows_as_batch<'a>(
371 rows: Vec<DiffRow<'a>>,
372 is_color: bool,
373 is_line_number: bool,
374 header_width: usize,
375) -> Vec<String> {
376 rows.into_iter()
377 .map(|row| {
378 if is_line_number {
379 match row.line_number {
380 Some(line_number) => format!(
381 "{}{}",
382 gen_counter_str(is_color, line_number, header_width, row.diff_type),
383 row.batch_line
384 ),
385 None => format!("{:>header_width$} | {}", "", row.batch_line),
386 }
387 } else {
388 row.batch_line
389 }
390 })
391 .collect()
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use tui::text::Span;
398
399 #[test]
400 fn expand_tabs_replaces_tabs_with_spaces() {
401 assert_eq!("a\tb".expand_tabs(4), "a b");
402 }
403
404 #[test]
405 fn expand_tabs_with_zero_tab_size_removes_tab_padding() {
406 assert_eq!("a\tb".expand_tabs(0), "ab");
407 }
408
409 #[test]
410 fn diff_mode_options_round_trip_each_flag() {
411 let mut options = DiffModeOptions::new();
412 options.set_color(true);
413 options.set_line_number(true);
414 options.set_only_diffline(true);
415 options.set_ignore_spaceblock(true);
416
417 assert!(options.get_color());
418 assert!(options.get_line_number());
419 assert!(options.get_only_diffline());
420 assert!(options.get_ignore_spaceblock());
421 }
422
423 #[test]
424 fn expand_line_tab_expands_each_line_independently() {
425 assert_eq!(expand_line_tab("a\tb\n12\tc", 4), "a b\n12 c");
426 }
427
428 #[test]
429 fn normalize_space_blocks_collapses_runs_per_line() {
430 assert_eq!(
431 normalize_space_blocks("a b\t\tc\n d e\n"),
432 "a b c\n d e\n"
433 );
434 }
435
436 #[test]
437 fn text_eq_ignoring_space_blocks_matches_equivalent_text() {
438 assert!(text_eq_ignoring_space_blocks("a b", "a b", true));
439 assert!(text_eq_ignoring_space_blocks("a\tb", "a b", true));
440 assert!(!text_eq_ignoring_space_blocks("ab", "a b", true));
441 assert!(!text_eq_ignoring_space_blocks("a b", "a b", false));
442 }
443
444 #[test]
445 fn gen_counter_str_without_color_is_plain_text() {
446 assert_eq!(
447 gen_counter_str(false, 12, 4, DifferenceType::Same),
448 " 12 | "
449 );
450 }
451
452 #[test]
453 fn gen_counter_str_with_color_wraps_output_in_ansi_sequences() {
454 let counter = gen_counter_str(true, 7, 3, DifferenceType::Add);
455
456 assert!(counter.contains("\u{1b}["));
457 assert!(counter.contains("7"));
458 assert!(counter.ends_with(" | \u{1b}[0m"));
459 }
460
461 #[test]
462 fn expand_output_vec_element_data_returns_batch_strings() {
463 let output = expand_output_vec_element_data(
464 true,
465 vec![
466 OutputVecElementData::String("first".to_string()),
467 OutputVecElementData::Line(Line::from(vec![Span::raw("ignored")])),
468 OutputVecElementData::String("second".to_string()),
469 ],
470 );
471
472 match output {
473 OutputVecData::Strings(strings) => {
474 assert_eq!(strings, vec!["first".to_string(), "second".to_string()]);
475 }
476 OutputVecData::Lines(_) => panic!("expected string output"),
477 }
478 }
479
480 #[test]
481 fn expand_output_vec_element_data_returns_watch_lines() {
482 let output = expand_output_vec_element_data(
483 false,
484 vec![
485 OutputVecElementData::String("ignored".to_string()),
486 OutputVecElementData::Line(Line::from("watch line")),
487 ],
488 );
489
490 match output {
491 OutputVecData::Lines(lines) => {
492 assert_eq!(lines.len(), 1);
493 assert_eq!(lines[0].spans[0].content.as_ref(), "watch line");
494 }
495 OutputVecData::Strings(_) => panic!("expected line output"),
496 }
497 }
498}