diff --git a/lib/HunkView.ml b/lib/HunkView.ml index b2fd616..a6bc02d 100644 --- a/lib/HunkView.ml +++ b/lib/HunkView.ml @@ -1,9 +1,15 @@ open Nottui module W = Nottui_widgets +(* Constants and Formatting Functions *) +let added_marker (content : Ui.t) : Ui.t = Ui.hcat [ W.string "+"; content ] +let removed_marker (content : Ui.t) : Ui.t = Ui.hcat [ W.string "-"; content ] +let unchanged_marker (content : Ui.t) : Ui.t = Ui.hcat [ W.string ""; content ] + (* Types *) type line = Change of string | Common of string | Empty +type rendering_mode = Color | TextMarkers (* Utility Functions *) @@ -37,25 +43,43 @@ let split_and_align_hunk hunks : line list * line list = in process_hunk [] [] hunks -(* Normal Mode *) +(* Styling Functions *) + +let style_text (text : string) (attr : Notty.attr) (mode : rendering_mode) : + Ui.t = + match mode with Color -> W.string ~attr text | TextMarkers -> W.string text + +let style_word (word : string) + (change_type : [ `Added | `Removed | `Unchanged ]) (mode : rendering_mode) : + Ui.t = + let styled_word = + match (mode, change_type) with + | Color, `Added -> W.string ~attr:Notty.A.(fg green) word + | Color, `Removed -> W.string ~attr:Notty.A.(fg red) word + | Color, `Unchanged -> W.string word + | TextMarkers, `Added -> added_marker (W.string word) + | TextMarkers, `Removed -> removed_marker (W.string word) + | TextMarkers, `Unchanged -> unchanged_marker (W.string word) + in + Ui.hcat [ styled_word; W.string " " ] + +(* Rendering Functions *) -let ui_hunk_summary (hunk : string Patch.hunk) : Nottui.ui = +let render_hunk_summary (hunk : string Patch.hunk) (mode : rendering_mode) : + Ui.t = let mine_info = - if hunk.Patch.mine_len = 0 then "0,0" - else Printf.sprintf "%d,%d" (hunk.Patch.mine_start + 1) hunk.Patch.mine_len + Printf.sprintf "%d,%d" (hunk.Patch.mine_start + 1) hunk.Patch.mine_len in let their_info = - if hunk.Patch.their_len = 0 then "0,0" - else - Printf.sprintf "%d,%d" (hunk.Patch.their_start + 1) hunk.Patch.their_len + Printf.sprintf "%d,%d" (hunk.Patch.their_start + 1) hunk.Patch.their_len in let mine_summary = - W.string ~attr:Notty.A.(fg red) (Printf.sprintf "-%s" mine_info) + style_text (Printf.sprintf "-%s" mine_info) Notty.A.(fg red) mode in let their_summary = - W.string ~attr:Notty.A.(fg green) (Printf.sprintf "+%s" their_info) + style_text (Printf.sprintf "+%s" their_info) Notty.A.(fg green) mode in - let at_symbols = W.string ~attr:Notty.A.(fg lightblue) "@@" in + let at_symbols = style_text "@@" Notty.A.(fg lightblue) mode in Ui.hcat [ at_symbols; @@ -67,35 +91,91 @@ let ui_hunk_summary (hunk : string Patch.hunk) : Nottui.ui = at_symbols; ] -let ui_unified_diff (hunk : string Patch.hunk) : Nottui.ui = - let hunk_summary = ui_hunk_summary hunk in - let hunk_content = - let blocks = Block.of_hunk hunk.Patch.lines in - let single_line_changes = - List.for_all - (function - | Block.Changed { mine; their; _ } -> - List.length mine = 1 && List.length their = 1 - | _ -> true) - blocks - in - if single_line_changes then - let word_diff_blocks = List.map WordDiff.compute blocks in - let word_diff_lines = Block.to_hunk word_diff_blocks in - WordDiff.render_hunk_lines word_diff_lines - else WordDiff.render_hunk hunk +let render_line_number (mine_num : int) (their_num : int) + (diff_type : [ `Added | `Removed | `Unchanged ]) (mode : rendering_mode) : + Ui.t = + match mode with + | TextMarkers -> ( + match diff_type with + | `Added -> W.string (Printf.sprintf " %2d + " (their_num + 1)) + | `Removed -> W.string (Printf.sprintf "%2d - " (mine_num + 1)) + | `Unchanged -> + W.string (Printf.sprintf "%2d %2d " (mine_num + 1) (their_num + 1))) + | Color -> ( + match diff_type with + | `Added -> + W.string + ~attr:Notty.A.(fg green) + (Printf.sprintf " %2d + " (their_num + 1)) + | `Removed -> + W.string + ~attr:Notty.A.(fg red) + (Printf.sprintf "%2d - " (mine_num + 1)) + | `Unchanged -> + Ui.hcat + [ + W.string + ~attr:Notty.A.(fg cyan) + (Printf.sprintf "%2d " (mine_num + 1)); + W.string + ~attr:Notty.A.(fg cyan) + (Printf.sprintf "%2d " (their_num + 1)); + ]) + +let render_word_diff (words : WordDiff.word list) + (diff_type : [ `Added | `Removed | `Unchanged ]) (mode : rendering_mode) : + Ui.t = + let render_word = function + | WordDiff.Changed word -> style_word word diff_type mode + | WordDiff.Unchanged word -> style_word word `Unchanged mode in - Ui.vcat [ hunk_summary; hunk_content ] + Ui.hcat (List.map render_word words) -let current_hunks (z_patches : string Patch.t Zipper.t) : Nottui.ui = - let p = Zipper.get_focus z_patches in - let hunks = List.map ui_unified_diff p.Patch.hunks in - Ui.vcat hunks +let render_diff_line (mine_num : int) (their_num : int) + (diff_type : [ `Added | `Removed | `Unchanged ]) + (content : WordDiff.word list) (mode : rendering_mode) : Ui.t = + let line_number = render_line_number mine_num their_num diff_type mode in + let content_ui = render_word_diff content diff_type mode in + Ui.hcat [ line_number; content_ui ] + +let render_hunk_lines (hunk_lines : WordDiff.line_content Patch.line list) + (mode : rendering_mode) : Ui.t = + let rec process_lines mine_num their_num acc = function + | [] -> List.rev acc + | line :: rest -> + let new_mine, new_their, ui = + match line with + | `Common words -> + ( mine_num + 1, + their_num + 1, + render_diff_line mine_num their_num `Unchanged words mode ) + | `Mine words -> + ( mine_num + 1, + their_num, + render_diff_line mine_num their_num `Removed words mode ) + | `Their words -> + ( mine_num, + their_num + 1, + render_diff_line mine_num their_num `Added words mode ) + in + process_lines new_mine new_their (ui :: acc) rest + in + Ui.vcat (process_lines 0 0 [] hunk_lines) + +let render_hunk (hunk : string Patch.hunk) (mode : rendering_mode) : Ui.t = + let content = + let blocks = + Block.of_hunk hunk.Patch.lines + |> List.map WordDiff.compute |> Block.to_hunk + in + render_hunk_lines blocks mode + in + Ui.vcat [ content ] -(** Side by side diff view implementation **) +(* Helper functions for side-by-side view *) let lines_with_numbers (lines : line list) (attr_change : Notty.attr) - (prefix : string) : Nottui.ui list = + (marker : Ui.t -> Ui.t) (mode : rendering_mode) : Ui.t list = let rec process_lines line_num acc = function | [] -> List.rev acc | line :: rest -> @@ -105,53 +185,90 @@ let lines_with_numbers (lines : line list) (attr_change : Notty.attr) let content = Printf.sprintf "%3d %s" line_num s in (content, Notty.A.empty, line_num + 1) | Change s -> - let content = Printf.sprintf "%3d %s %s" line_num prefix s in + let content = Printf.sprintf "%3d %s" line_num s in (content, attr_change, line_num + 1) | Empty -> let content = Printf.sprintf " " in (content, Notty.A.empty, line_num) in - let new_acc = W.string ~attr content :: acc in - process_lines next_num new_acc rest + let line_ui = + match mode with + | Color -> W.string ~attr content + | TextMarkers -> marker (W.string content) + in + process_lines next_num (line_ui :: acc) rest in process_lines 1 [] lines let create_summary (start_line_num : int) (hunk_length : int) - (attr : Notty.attr) (change_type : [ `Add | `Remove ]) : Nottui.ui = + (attr : Notty.attr) (change_type : [ `Add | `Remove ]) : Ui.t = let sign = match change_type with `Add -> "+" | `Remove -> "-" in - if hunk_length > 0 then - W.string ~attr - (Printf.sprintf "@@ %s%d,%d @@" sign start_line_num hunk_length) - else W.string ~attr (Printf.sprintf "@@ %s0,0 @@" sign) - -let ui_of_hunk_side_by_side (hunk : string Patch.hunk) : Nottui.ui = - let attr_mine = Notty.A.(fg red ++ st bold) in - let attr_their = Notty.A.(fg green ++ st bold) in - - let mine_lines, their_lines = split_and_align_hunk hunk.Patch.lines in - - let content_mine = lines_with_numbers mine_lines attr_mine "-" in - let content_their = lines_with_numbers their_lines attr_their "+" in - let summary_mine = - create_summary - (hunk.Patch.mine_start + 1) - hunk.Patch.mine_len attr_mine `Remove + let summary = + Printf.sprintf "@@ %s%d,%d @@" sign start_line_num hunk_length in - let summary_their = - create_summary - (hunk.Patch.their_start + 1) - hunk.Patch.their_len attr_their `Add + W.string ~attr summary + +(* Main View Functions *) + +let ui_unified_diff (hunk : string Patch.hunk) (mode : rendering_mode) : Ui.t = + let hunk_summary = render_hunk_summary hunk mode in + let hunk_content = + let blocks = Block.of_hunk hunk.Patch.lines in + let single_line_changes = + List.for_all + (function + | Block.Changed { mine; their; _ } -> + List.length mine = 1 && List.length their = 1 + | _ -> true) + blocks + in + if single_line_changes then + let word_diff_blocks = List.map WordDiff.compute blocks in + let word_diff_lines = Block.to_hunk word_diff_blocks in + render_hunk_lines word_diff_lines mode + else render_hunk hunk mode in - let space = Ui.space 1 0 in - Ui.hcat - [ - Ui.resize ~w:0 ~sw:2 (Ui.vcat (summary_mine :: content_mine)); - space; - Ui.resize ~w:0 ~sw:2 (Ui.vcat (summary_their :: content_their)); - ] + Ui.vcat [ hunk_summary; hunk_content ] -let current_hunks_side_by_side (z_patches : string Patch.t Zipper.t) : Nottui.ui - = +let current_hunks (z_patches : string Patch.t Zipper.t) (mode : rendering_mode) + : Ui.t = let p = Zipper.get_focus z_patches in - let hunks_ui = List.map ui_of_hunk_side_by_side p.Patch.hunks in - Ui.vcat hunks_ui + let hunks = List.map (fun hunk -> ui_unified_diff hunk mode) p.Patch.hunks in + Ui.vcat hunks + +let current_hunks_side_by_side (z_patches : string Patch.t Zipper.t) + (mode : rendering_mode) : Ui.t = + let p = Zipper.get_focus z_patches in + let render_side_by_side hunk = + let mine_lines, their_lines = split_and_align_hunk hunk.Patch.lines in + let render_lines lines marker attr = + lines_with_numbers lines attr marker mode + in + let content_mine = + render_lines mine_lines removed_marker Notty.A.(fg red) + in + let content_their = + render_lines their_lines added_marker Notty.A.(fg green) + in + let summary_mine = + create_summary + (hunk.Patch.mine_start + 1) + hunk.Patch.mine_len + Notty.A.(fg red) + `Remove + in + let summary_their = + create_summary + (hunk.Patch.their_start + 1) + hunk.Patch.their_len + Notty.A.(fg green) + `Add + in + Ui.hcat + [ + Ui.resize ~w:0 ~sw:2 (Ui.vcat (summary_mine :: content_mine)); + Ui.space 1 0; + Ui.resize ~w:0 ~sw:2 (Ui.vcat (summary_their :: content_their)); + ] + in + Ui.vcat (List.map render_side_by_side p.Patch.hunks) diff --git a/lib/HunkView.mli b/lib/HunkView.mli index 4e408cb..e0b129b 100644 --- a/lib/HunkView.mli +++ b/lib/HunkView.mli @@ -1,7 +1,10 @@ -val current_hunks : string Patch.t Zipper.t -> Nottui.ui +type rendering_mode = Color | TextMarkers + +val current_hunks : string Patch.t Zipper.t -> rendering_mode -> Nottui.ui (** [current_hunks zipper] returns the current hunks in a patch zipper for normal view. *) -val current_hunks_side_by_side : string Patch.t Zipper.t -> Nottui.ui +val current_hunks_side_by_side : + string Patch.t Zipper.t -> rendering_mode -> Nottui.ui (** [current_hunks_side_by_side zipper] returns the current hunks in a patch zipper for side-by-side view. *) diff --git a/lib/InteractiveViewer.ml b/lib/InteractiveViewer.ml index cab3902..fa67c08 100644 --- a/lib/InteractiveViewer.ml +++ b/lib/InteractiveViewer.ml @@ -5,18 +5,26 @@ open Lwd_infix type view_mode = SideBySide | Normal let view_mode : view_mode Lwd.var = Lwd.var Normal +let render_mode : HunkView.rendering_mode Lwd.var = Lwd.var HunkView.Color let toggle_view_mode () : unit = match Lwd.peek view_mode with | Normal -> Lwd.set view_mode SideBySide | SideBySide -> Lwd.set view_mode Normal +let toggle_render_mode () : unit = + match Lwd.peek render_mode with + | HunkView.Color -> Lwd.set render_mode HunkView.TextMarkers + | HunkView.TextMarkers -> Lwd.set render_mode HunkView.Color + let help_visible = Lwd.var false let quit = Lwd.var false let toggle_help_visibility () = Lwd.set help_visible (not (Lwd.peek help_visible)) +let help_string = "[h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode" + let view (patches : string Patch.t list) = let z_patches_var : string Patch.t Zipper.t Lwd.var = match Zipper.zipper_of_list patches with @@ -24,10 +32,12 @@ let view (patches : string Patch.t list) = | None -> failwith "zipper_of_list: empty list" in let hunks_ui = - let$ mode = Lwd.get view_mode and$ z_patches = Lwd.get z_patches_var in + let$ mode = Lwd.get view_mode + and$ z_patches = Lwd.get z_patches_var + and$ render_mode = Lwd.get render_mode in match mode with - | Normal -> HunkView.current_hunks z_patches - | SideBySide -> HunkView.current_hunks_side_by_side z_patches + | Normal -> HunkView.current_hunks z_patches render_mode + | SideBySide -> HunkView.current_hunks_side_by_side z_patches render_mode in let curr_scroll_state = Lwd.var W.default_scroll_state in let change_scroll_state _action state = @@ -83,11 +93,11 @@ let view (patches : string Patch.t list) = | `ASCII 't', [] -> toggle_view_mode (); `Handled + | `ASCII 'r', [] -> + toggle_render_mode (); + `Handled | _ -> `Unhandled) - (W.string - "Type 'h' to go to the help panel, 'q' to quit, 'n' to go to \ - the next operation, 'p' to go to the previous operation. \ - Press 't' to toggle view mode."); + (W.string help_string); ] in Lwd.return ui diff --git a/lib/WordDiff.ml b/lib/WordDiff.ml index aaf94ce..f4aea64 100644 --- a/lib/WordDiff.ml +++ b/lib/WordDiff.ml @@ -1,6 +1,3 @@ -open Nottui -module W = Nottui_widgets - type word = Unchanged of string | Changed of string type line_content = word list @@ -20,6 +17,32 @@ let lcs xs' ys' = done; a.(0).(0) +let edit_distance (type a) (compare : a -> a -> bool) (s : a array) + (t : a array) : int = + let memo = Hashtbl.create ((Array.length s + 1) * (Array.length t + 1)) in + + let rec edit_distance_helper i j = + match (i, j) with + | 0, x | x, 0 -> x + | i, j -> ( + match Hashtbl.find_opt memo (i, j) with + | Some result -> result + | None -> + let result = + let cost_to_drop_both = + if compare s.(i - 1) t.(j - 1) then 0 else 1 + in + min + (min + (edit_distance_helper (i - 1) j + 1) + (edit_distance_helper i (j - 1) + 1)) + (edit_distance_helper (i - 1) (j - 1) + cost_to_drop_both) + in + Hashtbl.add memo (i, j) result; + result) + in + edit_distance_helper (Array.length s) (Array.length t) + let diff_words (s1 : string) (s2 : string) : line_content * line_content = let words1 = Array.to_list (string_to_words s1) in let words2 = Array.to_list (string_to_words s2) in @@ -54,108 +77,85 @@ let diff_words (s1 : string) (s2 : string) : line_content * line_content = construct_diff words1 words2 common [] [] +let pair_lines lines1 lines2 = + let distances = + Array.make_matrix (Array.length lines1) (Array.length lines2) 0 + in + for i = 0 to Array.length lines1 - 1 do + for j = 0 to Array.length lines2 - 1 do + distances.(i).(j) <- + edit_distance ( = ) + (string_to_words lines1.(i)) + (string_to_words lines2.(j)) + done + done; + + (* greedily pair lines based on minimum distance *) + let paired = ref [] in + let used1 = Array.make (Array.length lines1) false in + let used2 = Array.make (Array.length lines2) false in + + for _ = 1 to min (Array.length lines1) (Array.length lines2) do + let min_dist = ref max_int in + let min_i = ref (-1) in + let min_j = ref (-1) in + + for i = 0 to Array.length lines1 - 1 do + for j = 0 to Array.length lines2 - 1 do + if (not used1.(i)) && (not used2.(j)) && distances.(i).(j) < !min_dist + then ( + min_dist := distances.(i).(j); + min_i := i; + min_j := j) + done + done; + + if !min_i <> -1 && !min_j <> -1 then ( + paired := (!min_i, !min_j) :: !paired; + used1.(!min_i) <- true; + used2.(!min_j) <- true) + done; + + let final_pairs = ref !paired in + for i = 0 to Array.length lines1 - 1 do + if not used1.(i) then final_pairs := (i, -1) :: !final_pairs + done; + for j = 0 to Array.length lines2 - 1 do + if not used2.(j) then final_pairs := (-1, j) :: !final_pairs + done; + + List.sort compare !final_pairs + let compute (block : string Block.t) : line_content Block.t = match block with | Block.Common line -> Block.Common [ Unchanged line ] | Block.Changed { mine; their; order } -> - let mine_str = String.concat " " mine in - let their_str = String.concat " " their in - let mine_words, their_words = diff_words mine_str their_str in - Block.Changed { mine = [ mine_words ]; their = [ their_words ]; order } - -let word_to_ui word attr = W.string ~attr (word ^ " ") - -let render_diff_line mine_num their_num attr diff_type words = - let format_line_number = - match diff_type with - | `Added -> W.string ~attr (Printf.sprintf " %2d + " (their_num + 1)) - | `Deleted -> W.string ~attr (Printf.sprintf "%2d - " (mine_num + 1)) - | `Equal -> - W.string ~attr:Notty.A.empty - (Printf.sprintf "%2d %2d " (mine_num + 1) (their_num + 1)) - in - Ui.hcat - [ - format_line_number; - Ui.hcat - (List.map - (function - | Changed word when diff_type = `Deleted || diff_type = `Added -> - word_to_ui word attr - | Unchanged word -> word_to_ui word Notty.A.empty - | _ -> Ui.empty) - words); - ] - -let render_hunk_lines (hunk_lines : line_content Patch.line list) : Nottui.ui = - let rec process_lines mine_num their_num acc = function - | [] -> List.rev acc - | line :: rest -> - let new_mine, new_their, ui = - match line with - | `Common words -> - ( mine_num + 1, - their_num + 1, - render_diff_line mine_num their_num Notty.A.empty `Equal words - ) - | `Mine words -> - ( mine_num + 1, - their_num, - render_diff_line mine_num their_num - Notty.A.(fg red) - `Deleted words ) - | `Their words -> - ( mine_num, - their_num + 1, - render_diff_line mine_num their_num - Notty.A.(fg green) - `Added words ) - in - process_lines new_mine new_their (ui :: acc) rest - in - let lines_ui = process_lines 0 0 [] hunk_lines in - Ui.vcat lines_ui - -let render_diff_line_str (mine_num : int) (their_num : int) (attr : Notty.attr) - (diff_type : [ `Equal | `Deleted | `Added ]) (content : string) : Ui.t = - let format_line_number = - match diff_type with - | `Added -> W.string ~attr (Printf.sprintf " %2d + " (their_num + 1)) - | `Deleted -> W.string ~attr (Printf.sprintf "%2d - " (mine_num + 1)) - | `Equal -> - W.string ~attr:Notty.A.empty - (Printf.sprintf "%2d %2d " (mine_num + 1) (their_num + 1)) - in - let content_ui = W.string ~attr content in - Ui.hcat [ format_line_number; content_ui ] - -let render_line_diff (mine_num : int) (their_num : int) - (line : string Patch.line) : int * int * Ui.t = - match line with - | `Common s -> - ( mine_num + 1, - their_num + 1, - render_diff_line_str mine_num their_num Notty.A.empty `Equal s ) - | `Mine s -> - ( mine_num + 1, - their_num, - render_diff_line_str mine_num their_num Notty.A.(fg red) `Deleted s ) - | `Their s -> - ( mine_num, - their_num + 1, - render_diff_line_str mine_num their_num Notty.A.(fg green) `Added s ) - -let render_hunk (hunk : string Patch.hunk) : Nottui.ui = - let lines_ui = - let rec process_lines mine_num their_num acc = function - | [] -> List.rev acc - | line :: rest -> - let new_mine, new_their, ui = - render_line_diff mine_num their_num line - in - process_lines new_mine new_their (ui :: acc) rest - in - process_lines hunk.Patch.mine_start hunk.Patch.their_start [] - hunk.Patch.lines - in - Ui.vcat lines_ui + let mine_array = Array.of_list mine in + let their_array = Array.of_list their in + let pairs = pair_lines mine_array their_array in + + let result_mine = ref [] in + let result_their = ref [] in + + List.iter + (fun (i, j) -> + match (i, j) with + | -1, j -> + let their_content = their_array.(j) in + result_their := + (diff_words "" their_content |> snd) :: !result_their + | i, -1 -> + let mine_content = mine_array.(i) in + result_mine := (diff_words mine_content "" |> fst) :: !result_mine + | i, j -> + let mine_content = mine_array.(i) in + let their_content = their_array.(j) in + let mine_diff, their_diff = + diff_words mine_content their_content + in + result_mine := mine_diff :: !result_mine; + result_their := their_diff :: !result_their) + pairs; + + Block.Changed + { mine = List.rev !result_mine; their = List.rev !result_their; order } diff --git a/lib/WordDiff.mli b/lib/WordDiff.mli index 0dae84a..ae7347d 100644 --- a/lib/WordDiff.mli +++ b/lib/WordDiff.mli @@ -2,9 +2,8 @@ type word = Unchanged of string | Changed of string type line_content = word list val compute : string Block.t -> line_content Block.t -val render_hunk_lines : line_content Patch.line list -> Nottui.ui -val render_hunk : string Patch.hunk -> Nottui.Ui.t (* for tests *) val lcs : 'a list -> 'a list -> 'a list val diff_words : string -> string -> line_content * line_content +val edit_distance : ('a -> 'a -> bool) -> 'a array -> 'a array -> int diff --git a/test/cram/test.t b/test/cram/test.t index e96fbb8..9e78eee 100644 --- a/test/cram/test.t +++ b/test/cram/test.t @@ -21,28 +21,28 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff n Operation 2 of 14, 1 hunk 3 additions, 1 removal Modification of bin/main.ml @@ -1,7 +1,9 @@ - 1 1 open Diffcessible - 2 2 - 3 3 let main () = - 4 - Interactive_viewer.start () - 4 + let s = In_channel.input_all In_channel.stdin in - 5 + let patch = Patch.to_diffs s in - 6 + Interactive_viewer.start patch - 5 7 - 6 8 open Cmdliner - 7 9 + 1 1 open Diffcessible + 2 2 + 3 3 let main () = + 4 - Interactive_viewer.start () + 4 + let s = In_channel.input_all In_channel.stdin in + 5 + let patch = Patch.to_diffs s in + 6 + Interactive_viewer.start patch + 5 7 + 6 8 open Cmdliner + 7 9 - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff n n Operation 3 of 14, 1 hunk 1 addition, 1 removal @@ -63,7 +63,7 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff h Help Panel: @@ -90,9 +90,9 @@ This is a cram test for the new executable. 2 additions, 1 removal Modification of file.txt @@ -2,1 +2,2 @@ - 2 - Hi everyone! - 2 + Hello World! - 3 + This is the diffcessible project. + 1 - Hi everyone! + 1 + This is the diffcessible project. + 2 + Hello World! @@ -105,15 +105,15 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal more-examples.diff n Operation 2 of 2, 1 hunk 2 additions, 1 removal Modification of file.txt @@ -3,1 +5,2 @@ - 3 - This file starts at line 3. - 5 + This file starts at line 5. - 6 + This is the second test case in this file. + 1 - This file starts at line 3. + 1 + This is the second test case in this file. + 2 + This file starts at line 5. @@ -126,7 +126,7 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode @@ -139,8 +139,8 @@ This is a cram test for the new executable. 2 additions, 1 removal Modification of file.txt @@ -2,1 @@ @@ +2,2 @@ - 1 - Hi everyone! 1 + Hello World! - 2 + This is the diffcessible project. + 1 Hi everyone! 1 Hello World! + 2 This is the diffcessible project. @@ -154,15 +154,15 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal more-examples.diff n t Operation 2 of 2, 1 hunk 2 additions, 1 removal Modification of file.txt @@ -3,1 @@ @@ +5,2 @@ - 1 - This file starts at line 3. 1 + This file starts at line 5. - 2 + This is the second test case in this file. + 1 This file starts at line 3. 1 This file starts at line 5. + 2 This is the second test case in this file. @@ -176,7 +176,7 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff n n t Operation 3 of 14, 1 hunk @@ -185,7 +185,7 @@ This is a cram test for the new executable. @@ -1,3 @@ @@ +1,3 @@ 1 (library 1 (library 2 (name diffcessible) 2 (name diffcessible) - 3 - (libraries notty nottui lwd)) 3 + (libraries notty nottui lwd patch)) + 3 (libraries notty nottui lwd)) 3 (libraries notty nottui lwd patch)) @@ -198,7 +198,7 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff n n n t Operation 4 of 14, 1 hunk @@ -207,20 +207,20 @@ This is a cram test for the new executable. @@ -1,39 @@ @@ +1,25 @@ 1 open Nottui 1 open Nottui 2 module W = Nottui_widgets 2 module W = Nottui_widgets - 3 - open Lwd_infix 3 + (* open Lwd_infix *) - 4 - - 5 - type patch = unit + 3 open Lwd_infix 3 (* open Lwd_infix *) + 4 + 5 type patch = unit 6 4 7 let pure_str s = Lwd.pure (W.string s) 5 let pure_str s = Lwd.pure (W.string s) - 8 - - 9 - let string_of_counter c = - 10 - let$ c = c in - 11 - W.string (string_of_int c) - 12 - + 8 + 9 let string_of_counter c = + 10 let$ c = c in + 11 W.string (string_of_int c) + 12 13 let quit = Lwd.var false 6 let quit = Lwd.var false - 14 - let counter = Lwd.var 0 7 + let string_of_operation = Format.asprintf "%a" (Patch.pp_operation ~ - 15 - let counter_d = Lwd.get counter - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + 14 let counter = Lwd.var 0 7 let string_of_operation = Format.asprintf "%a" (Patch.pp_operation ~gi + 15 let counter_d = Lwd.get counter + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff n n n n t @@ -230,9 +230,9 @@ This is a cram test for the new executable. @@ -1,5 @@ @@ +1,3 @@ 1 (** Render and navigate through a diff. *) 1 (** Render and navigate through a diff. *) 2 2 - 3 - type patch = unit 3 + val start : Patch.t list -> unit - 4 - - 5 - val start : patch -> unit + 3 type patch = unit 3 val start : Patch.t list -> unit + 4 + 5 val start : patch -> unit @@ -243,7 +243,7 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff n n n n n t @@ -251,7 +251,7 @@ This is a cram test for the new executable. 1 addition, 1 removal Rename with modifications dir1/file.txt to dir2/file.txt @@ -1,1 @@ @@ +1,1 @@ - 1 - This is the original content. 1 + This is the modified content. + 1 This is the original content. 1 This is the modified content. @@ -266,15 +266,15 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff n n n n n n n t Operation 8 of 14, 1 hunk 2 additions, 1 removal Rename with modifications dir1/file.txt to dir2/file.txt @@ -1,1 @@ @@ +1,2 @@ - 1 - This is the original content. 1 + Here is some additional line. - 2 + Deleted line 1 and added this. + 1 This is the original content. 1 Here is some additional line. + 2 Deleted line 1 and added this. @@ -288,14 +288,14 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff n n n n n n n n n t Operation 10 of 14, 1 hunk 0 additions, 1 removal Deletion of dir1/file.txt - @@ -1,1 @@ @@ +0,0 @@ - 1 - some text + @@ -1,1 @@ @@ +1,0 @@ + 1 some text @@ -310,15 +310,15 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff n n n n n n n n n n n n t Operation 13 of 14, 1 hunk 2 additions, 0 removals Creation of dir2/sample.txt - @@ -0,0 @@ @@ +1,2 @@ - 1 + some text - 2 + lorem ipsum + @@ -1,0 @@ @@ +1,2 @@ + 1 some text + 2 lorem ipsum @@ -332,14 +332,14 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff n n n n n n n n n n n n n n n t Operation 14 of 14, 1 hunk 1 addition, 0 removals Rename with modifications dir1/file.txt to dir2/file.txt - @@ -0,0 @@ @@ +1,1 @@ - 1 + new text + @@ -1,0 @@ @@ +1,1 @@ + 1 new text @@ -354,15 +354,15 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal example.diff n n n n n n n n n n n n n n n n t Operation 14 of 14, 1 hunk 1 addition, 0 removals Rename with modifications dir1/file.txt to dir2/file.txt - @@ -0,0 @@ @@ +1,1 @@ - 1 + new text + @@ -1,0 @@ @@ +1,1 @@ + 1 new text @@ -377,7 +377,7 @@ This is a cram test for the new executable. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode diff --git a/test/cram/word-diff.t/run.t b/test/cram/word-diff.t/run.t index 873b0b9..d0acd96 100644 --- a/test/cram/word-diff.t/run.t +++ b/test/cram/word-diff.t/run.t @@ -19,7 +19,7 @@ 3 3 Actions speak louder than words. @@ -11,2 +11,2 @@ 1 - The early bird catches the worm. - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode $ dummy_terminal examples.diff n Operation 2 of 2, 3 hunks 3 additions, 3 removals @@ -40,6 +40,6 @@ - Type 'h' to go to the help panel, 'q' to quit, 'n' to go to the next operation, 'p' to go to the previous operation. Press 't' to toggle view mode. + [h]elp [q]uit [n/p]avigate [t]oggle view [r]ender mode diff --git a/test/unit/TestBlock.ml b/test/unit/TestBlock.ml index 01668c7..d6612b2 100644 --- a/test/unit/TestBlock.ml +++ b/test/unit/TestBlock.ml @@ -5,8 +5,8 @@ let example_hunk1 : string Patch.line list = let example_blocks1 : string Block.t list = [ - Block.Changed { mine = [ "A" ]; their = [ "B" ]; order = Block.Mine }; - Block.Changed { mine = [ "C" ]; their = [ "D" ]; order = Block.Mine }; + Block.Changed { mine = [ "A" ]; their = [ "B" ]; order = Mine }; + Block.Changed { mine = [ "C" ]; their = [ "D" ]; order = Mine }; ] let empty_hunk : string Patch.line list = [] @@ -22,15 +22,13 @@ let mine_only_hunk : string Patch.line list = [ `Mine "A"; `Mine "B"; `Mine "C" ] let mine_only_blocks : string Block.t list = - [ Block.Changed { mine = [ "A"; "B"; "C" ]; their = []; order = Block.Mine } ] + [ Block.Changed { mine = [ "A"; "B"; "C" ]; their = []; order = Mine } ] let their_only_hunk : string Patch.line list = [ `Their "X"; `Their "Y"; `Their "Z" ] let their_only_blocks : string Block.t list = - [ - Block.Changed { mine = []; their = [ "X"; "Y"; "Z" ]; order = Block.Their }; - ] + [ Block.Changed { mine = []; their = [ "X"; "Y"; "Z" ]; order = Their } ] let complex_hunk : string Patch.line list = [ @@ -48,10 +46,10 @@ let complex_hunk : string Patch.line list = let complex_blocks : string Block.t list = [ Block.Common "A"; - Block.Changed { mine = [ "B" ]; their = [ "C" ]; order = Block.Mine }; - Block.Changed { mine = [ "D" ]; their = [ "E" ]; order = Block.Mine }; + Block.Changed { mine = [ "B" ]; their = [ "C" ]; order = Mine }; + Block.Changed { mine = [ "D" ]; their = [ "E" ]; order = Mine }; Block.Common "F"; - Block.Changed { mine = [ "G" ]; their = [ "H"; "I" ]; order = Block.Mine }; + Block.Changed { mine = [ "G" ]; their = [ "H"; "I" ]; order = Mine }; ] let alternating_add_remove_hunk1 : string Patch.line list = @@ -59,8 +57,8 @@ let alternating_add_remove_hunk1 : string Patch.line list = let alternating_add_remove_blocks1 : string Block.t list = [ - Block.Changed { mine = [ "A" ]; their = [ "B" ]; order = Block.Mine }; - Block.Changed { mine = [ "C" ]; their = []; order = Block.Mine }; + Block.Changed { mine = [ "A" ]; their = [ "B" ]; order = Mine }; + Block.Changed { mine = [ "C" ]; their = []; order = Mine }; ] let alternating_remove_add_hunk1 : string Patch.line list = @@ -68,8 +66,8 @@ let alternating_remove_add_hunk1 : string Patch.line list = let alternating_remove_add_blocks1 : string Block.t list = [ - Block.Changed { mine = [ "Y" ]; their = [ "X" ]; order = Block.Their }; - Block.Changed { mine = []; their = [ "Z" ]; order = Block.Their }; + Block.Changed { mine = [ "Y" ]; their = [ "X" ]; order = Their }; + Block.Changed { mine = []; their = [ "Z" ]; order = Their }; ] let complex_alternating_hunk : string Patch.line list = @@ -91,13 +89,13 @@ let complex_alternating_hunk : string Patch.line list = let complex_alternating_blocks : string Block.t list = [ - Block.Changed { mine = [ "A" ]; their = [ "B" ]; order = Block.Mine }; - Block.Changed { mine = [ "C" ]; their = [ "D" ]; order = Block.Mine }; - Block.Changed { mine = [ "E" ]; their = [ "F" ]; order = Block.Mine }; - Block.Changed { mine = [ "G" ]; their = [ "H" ]; order = Block.Mine }; + Block.Changed { mine = [ "A" ]; their = [ "B" ]; order = Mine }; + Block.Changed { mine = [ "C" ]; their = [ "D" ]; order = Mine }; + Block.Changed { mine = [ "E" ]; their = [ "F" ]; order = Mine }; + Block.Changed { mine = [ "G" ]; their = [ "H" ]; order = Mine }; Block.Common "I"; - Block.Changed { mine = [ "K" ]; their = [ "J" ]; order = Block.Their }; - Block.Changed { mine = [ "M" ]; their = [ "L" ]; order = Block.Their }; + Block.Changed { mine = [ "K" ]; their = [ "J" ]; order = Their }; + Block.Changed { mine = [ "M" ]; their = [ "L" ]; order = Their }; ] let multiple_consecutive_changes_hunk : string Patch.line list = @@ -119,13 +117,10 @@ let multiple_consecutive_changes_hunk : string Patch.line list = let multiple_consecutive_changes_blocks : string Block.t list = [ - Block.Changed - { mine = [ "A"; "B" ]; their = [ "C"; "D" ]; order = Block.Mine }; - Block.Changed - { mine = [ "E"; "F" ]; their = [ "G"; "H" ]; order = Block.Mine }; + Block.Changed { mine = [ "A"; "B" ]; their = [ "C"; "D" ]; order = Mine }; + Block.Changed { mine = [ "E"; "F" ]; their = [ "G"; "H" ]; order = Mine }; Block.Common "I"; - Block.Changed - { mine = [ "L"; "M" ]; their = [ "J"; "K" ]; order = Block.Their }; + Block.Changed { mine = [ "L"; "M" ]; their = [ "J"; "K" ]; order = Their }; ] let string_of_block = function @@ -134,10 +129,7 @@ let string_of_block = function Printf.sprintf "Changed { mine = [%s]; their = [%s]; order = %s }" (String.concat "; " (List.map (Printf.sprintf "%S") mine)) (String.concat "; " (List.map (Printf.sprintf "%S") their)) - (match order with - | Block.Mine -> "Mine" - | Block.Their -> "Their" - | Block.None -> "None") + (match order with Mine -> "Mine" | Their -> "Their" | None -> "None") let string_of_blocks blocks = "[" ^ String.concat "; " (List.map string_of_block blocks) ^ "]" diff --git a/test/unit/TestWordDiff.ml b/test/unit/TestWordDiff.ml index 8a325b5..d9613bf 100644 --- a/test/unit/TestWordDiff.ml +++ b/test/unit/TestWordDiff.ml @@ -12,8 +12,8 @@ let test_lcs_4 = ([], [], []) (* Empty lists *) let test_lcs_5 = ([ "a"; "a"; "b"; "b"; "c" ], [ "a"; "b"; "c"; "c" ], [ "a"; "b"; "c" ]) -(* Repeating elements *) +(* Repeating elements *) let test_lcs_6 = ([ "a"; "b"; "c" ], [ "d"; "e"; "f" ], []) (* All different, same length *) @@ -121,6 +121,57 @@ let test_diff_words () = List.iter test_case [ test_diff_1; test_diff_2; test_diff_3; test_diff_4; test_diff_5 ] +let test_edit_distance_1 = + ( [| 'k'; 'i'; 't'; 't'; 'e'; 'n' |], + [| 's'; 'i'; 't'; 't'; 'i'; 'n'; 'g' |], + 3 ) + +let test_edit_distance_2 = + ( [| 'S'; 'u'; 'n'; 'd'; 'a'; 'y' |], + [| 'S'; 'a'; 't'; 'u'; 'r'; 'd'; 'a'; 'y' |], + 3 ) + +let test_edit_distance_3 = ([| 'a'; 'b'; 'c' |], [| 'a'; 'b'; 'c' |], 0) +let test_edit_distance_4 = ([| 'a'; 'b'; 'c' |], [| 'd'; 'e'; 'f' |], 3) +let test_edit_distance_5 = ([||], [| 'a'; 'b'; 'c' |], 3) +let test_edit_distance_6 = ([| 'a'; 'b'; 'c' |], [||], 3) +let test_edit_distance_7 = ([||], [||], 0) (* Both empty *) +let test_edit_distance_8 = ([| 'a' |], [| 'b' |], 1) +let test_edit_distance_9 = ([| 'a'; 'a'; 'a' |], [| 'a'; 'a'; 'a'; 'a' |], 1) +let test_edit_distance_10 = ([| 'a'; 'a'; 'a'; 'a' |], [| 'a'; 'a'; 'a' |], 1) +let test_edit_distance_11 = ([| '1'; '2'; '3' |], [| '2'; '3'; '4' |], 2) +(* Shift *) + +let test_edit_distance_12 = + ([| 'a'; 'b'; 'c'; 'd'; 'e'; 'f' |], [| 'a'; 'z'; 'c'; 'd'; 'e'; 'f' |], 1) + +let test_edit_distance () = + let test_case (input1, input2, expected) = + let result = WordDiff.edit_distance Char.equal input1 input2 in + assert_with_message (result = expected) + (Printf.sprintf + "edit_distance failed\nInput1: %s\nInput2: %s\nExpected: %d\nGot: %d" + (String.of_seq (Array.to_seq input1)) + (String.of_seq (Array.to_seq input2)) + expected result) + in + List.iter test_case + [ + test_edit_distance_1; + test_edit_distance_2; + test_edit_distance_3; + test_edit_distance_4; + test_edit_distance_5; + test_edit_distance_6; + test_edit_distance_7; + test_edit_distance_8; + test_edit_distance_9; + test_edit_distance_10; + test_edit_distance_11; + test_edit_distance_12; + ] + +(* Updated run_tests function *) let run_tests () = let run_test name f = try @@ -134,6 +185,7 @@ let run_tests () = (Printexc.to_string exn) in run_test "test_lcs" test_lcs; - run_test "test_diff_words" test_diff_words + run_test "test_diff_words" test_diff_words; + run_test "test_edit_distance" test_edit_distance let () = run_tests ()