diff --git a/.ocamlformat b/.ocamlformat index 031f054..f57542d 100644 --- a/.ocamlformat +++ b/.ocamlformat @@ -1 +1,2 @@ -version = 0.26.1 \ No newline at end of file +version = 0.26.1 +parse-docstrings=true diff --git a/example.diff b/example.diff index 34ca53b..2a52d48 100644 --- a/example.diff +++ b/example.diff @@ -1,3 +1,52 @@ +--- a/document.txt ++++ b/document.txt +@@ -1,5 +1,5 @@ +-The quick brown fox jumps over the lazy dog. ++The fast brown fox leaps over the sleeping dog. + She sells seashells by the seashore. +-The rain in Spain stays mainly in the plain. ++The rain in France falls mainly on the plain. + To be or not to be, that is the question. +-All that glitters is not gold. ++Not all that glitters is gold. +@@ -7,3 +7,3 @@ + A stitch in time saves nine. +-An apple a day keeps the doctor away. ++An apple each day keeps the doctor away. + Actions speak louder than words. +@@ -11,2 +11,2 @@ +-The early bird catches the worm. ++The early riser catches the worm. +@@ -14,2 +14,2 @@ +-Practice makes perfect. ++Practice leads to perfection. +@@ -17,3 +17,3 @@ + When in Rome, do as the Romans do. +-Don't put all your eggs in one basket. ++Never put all your eggs in one basket. + A penny saved is a penny earned. +@@ -21,2 +21,2 @@ +-Every cloud has a silver lining. ++Every cloud possesses a silver lining. +@@ -24,2 +24,2 @@ +-Two wrongs don't make a right. ++Two wrongs never make a right. +@@ -27,1 +27,2 @@ + The pen is mightier than the sword. ++Words can be sharper than any blade. + +--- a/sample.txt ++++ b/sample.txt +@@ -1,2 +1,2 @@ +-foo bar baz ++foo bat bak +@@ -4,2 +4,1 @@ +-The quick brown fox jumps over the lazy dog. ++The quick black cat jumps over the lazy dog. +@@ -7,2 +6,2 @@ +-She sells sea shells by the sea shore. ++He sells sea shells by the sea floor. + diff --git a/bin/dune b/bin/dune index fca70de..fd7f225 100644 --- a/bin/dune diff --git a/lib/Block.ml b/lib/Block.ml new file mode 100644 index 0000000..1b20566 --- /dev/null +++ b/lib/Block.ml @@ -0,0 +1,59 @@ +type block_origin = Mine | Their | None + +type 'a t = + | Common of 'a + | Changed of { mine : 'a list; their : 'a list; order : block_origin } + +let rec first_change_order (hunk_lines : 'a Patch.line list) : block_origin = + match hunk_lines with + | [] -> None + | `Common _ :: rest -> first_change_order rest + | `Mine _ :: _ -> Mine + | `Their _ :: _ -> Their + +let of_hunk (hunk_lines : 'a Patch.line list) : 'a t list = + let collect_consecutive_added lines = + let rec aux acc lines = + match lines with + | `Mine x :: rest -> aux (x :: acc) rest + | rest -> (List.rev acc, rest) + in + aux [] lines + in + let collect_consecutive_removed lines = + let rec aux acc lines = + match lines with + | `Their x :: rest -> aux (x :: acc) rest + | rest -> (List.rev acc, rest) + in + aux [] lines + in + + let make_block ~adds ~dels remaining_lines = + let order = first_change_order remaining_lines in + Changed { mine = adds; their = dels; order } + in + let rec process acc = function + | [] -> List.rev acc + | `Mine x :: rest as lines -> + let adds, rest' = collect_consecutive_added (`Mine x :: rest) in + let dels, rest'' = collect_consecutive_removed rest' in + process (make_block ~adds ~dels lines :: acc) rest'' + | `Their x :: rest as lines -> + let dels, rest' = collect_consecutive_removed (`Their x :: rest) in + let adds, rest'' = collect_consecutive_added rest' in + process (make_block ~adds ~dels lines :: acc) rest'' + | `Common x :: rest -> process (Common x :: acc) rest + in + process [] hunk_lines + +let to_hunk (blocks : 'a t list) : 'a Patch.line list = + List.concat_map + (function + | Common x -> [ `Common x ] + | Changed { mine; their; order } -> + let mine_lines = List.map (fun x -> `Mine x) mine in + let their_lines = List.map (fun x -> `Their x) their in + if order = Mine then mine_lines @ their_lines + else their_lines @ mine_lines) + blocks diff --git a/lib/Block.mli b/lib/Block.mli new file mode 100644 index 0000000..252052b --- /dev/null +++ b/lib/Block.mli @@ -0,0 +1,12 @@ +type block_origin = Mine | Their | None + +type 'a t = + | Common of 'a + | Changed of { mine : 'a list; their : 'a list; order : block_origin } + +val of_hunk : 'a Patch.line list -> 'a t list + +(* Turns a hunk into a list of blocks by grouping consecutive changed lines. *) +val to_hunk : 'a t list -> 'a Patch.line list +(* Turns back a list of blocks into a hunk. + [x |> of_hunk |> to_hunk] should yield [x] back. *) diff --git a/lib/HunkView.ml b/lib/HunkView.ml index a6ecd55..b2fd616 100644 --- a/lib/HunkView.ml +++ b/lib/HunkView.ml @@ -1,9 +1,45 @@ open Nottui module W = Nottui_widgets -(* Implementation of Single View Mode *) +(* Types *) -let ui_hunk_summary (hunk : Patch.hunk) : Nottui.ui = +type line = Change of string | Common of string | Empty + +(* Utility Functions *) + +let split_and_align_hunk hunks : line list * line list = + let rec process_hunk mine_acc their_acc = function + | [] -> (List.rev mine_acc, List.rev their_acc) + | `Common line :: rest -> + process_hunk (Common line :: mine_acc) (Common line :: their_acc) rest + | changes -> + let rec buffer_changes mine their = function + | `Mine line :: rest -> + buffer_changes (Change line :: mine) their rest + | `Their line :: rest -> + buffer_changes mine (Change line :: their) rest + | remaining -> (List.rev mine, List.rev their, remaining) + in + let mine_changes, their_changes, rest = buffer_changes [] [] changes in + let max_len = + max (List.length mine_changes) (List.length their_changes) + in + let rec pad_and_append orig_acc changes i = + if i < max_len then + if i < List.length changes then + pad_and_append (List.nth changes i :: orig_acc) changes (i + 1) + else pad_and_append (Empty :: orig_acc) changes (i + 1) + else orig_acc + in + let new_mine_acc = pad_and_append mine_acc mine_changes 0 in + let new_their_acc = pad_and_append their_acc their_changes 0 in + process_hunk new_mine_acc new_their_acc rest + in + process_hunk [] [] hunks + +(* Normal Mode *) + +let ui_hunk_summary (hunk : string Patch.hunk) : Nottui.ui = 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 @@ -31,99 +67,36 @@ let ui_hunk_summary (hunk : Patch.hunk) : Nottui.ui = at_symbols; ] -let rec process_lines (mine_num : int) (their_num : int) (acc : Nottui.ui list) - = function - | [] -> List.rev acc - | line :: rest -> - let new_mine, new_their, ui_element = - match line with - | `Common line -> - let ui = - W.string ~attr:Notty.A.empty - (Printf.sprintf "%2d %2d %s" (mine_num + 1) (their_num + 1) - line) - in - (mine_num + 1, their_num + 1, ui) - | `Their line -> - let ui = - W.string - ~attr:Notty.A.(fg green) - (Printf.sprintf " %2d + %s" (their_num + 1) line) - in - (mine_num, their_num + 1, ui) - | `Mine line -> - let ui = - W.string - ~attr:Notty.A.(fg red) - (Printf.sprintf "%2d - %s" (mine_num + 1) line) - in - (mine_num + 1, their_num, ui) - in - process_lines new_mine new_their (ui_element :: acc) rest - -let ui_unified_diff (hunk : Patch.hunk) : Nottui.ui = - let lines_ui = - process_lines hunk.Patch.mine_start hunk.Patch.their_start [] - hunk.Patch.lines +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 in - let lines_ui_vcat = Ui.vcat lines_ui in - - Ui.vcat [ ui_hunk_summary hunk; lines_ui_vcat ] + Ui.vcat [ hunk_summary; hunk_content ] -let current_hunks (z_patches : Patch.t Zipper.t) : Nottui.ui = +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 (** Side by side diff view implementation **) -type line = Change of string | Common of string | Empty - -(* - - [Common, Their, Their, Mine] - - List A [Common, Their, Their] - List B [Common, Mine, Empty] -*) - -let split_and_align_hunk hunks : line list * line list = - let rec process_hunk (mine_acc : line list) (their_acc : line list) = function - | [] -> (List.rev mine_acc, List.rev their_acc) - | `Common line :: rest -> - process_hunk (Common line :: mine_acc) (Common line :: their_acc) rest - | changes -> - let rec buffer_changes (mine : line list) (their : line list) = function - | `Mine line :: rest -> - buffer_changes (Change line :: mine) their rest - | `Their line :: rest -> - buffer_changes mine (Change line :: their) rest - | remaining -> (List.rev mine, List.rev their, remaining) - in - let mine_changes, their_changes, rest = buffer_changes [] [] changes in - let max_len = - max (List.length mine_changes) (List.length their_changes) - in - let pad_and_append (orig_acc : line list) (changes : line list) : - line list = - let rec pad_append (acc : line list) (i : int) : line list = - if i < max_len then - if i < List.length changes then - pad_append (List.nth changes i :: acc) (i + 1) - else pad_append (Empty :: acc) (i + 1) - else acc - in - pad_append orig_acc 0 - in - let new_mine_acc = pad_and_append mine_acc mine_changes in - let new_their_acc = pad_and_append their_acc their_changes in - process_hunk new_mine_acc new_their_acc rest - in - process_hunk [] [] hunks - let lines_with_numbers (lines : line list) (attr_change : Notty.attr) (prefix : string) : Nottui.ui list = - let rec process_lines (line_num : int) (acc : Nottui.ui list) = function + let rec process_lines line_num acc = function | [] -> List.rev acc | line :: rest -> let content, attr, next_num = @@ -151,7 +124,7 @@ let create_summary (start_line_num : int) (hunk_length : int) (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 : Patch.hunk) : Nottui.ui = +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 @@ -177,7 +150,8 @@ let ui_of_hunk_side_by_side (hunk : Patch.hunk) : Nottui.ui = Ui.resize ~w:0 ~sw:2 (Ui.vcat (summary_their :: content_their)); ] -let current_hunks_side_by_side (z_patches : Patch.t Zipper.t) : Nottui.ui = +let current_hunks_side_by_side (z_patches : string Patch.t Zipper.t) : Nottui.ui + = 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 diff --git a/lib/HunkView.mli b/lib/HunkView.mli index ed0ce7f..4e408cb 100644 --- a/lib/HunkView.mli +++ b/lib/HunkView.mli @@ -1,5 +1,7 @@ -val current_hunks : Patch.t Zipper.t -> Nottui.ui -(** [current_hunks zipper] returns the current hunks in a patch zipper for normal view. *) +val current_hunks : string Patch.t Zipper.t -> Nottui.ui +(** [current_hunks zipper] returns the current hunks in a patch zipper for + normal view. *) -val current_hunks_side_by_side : Patch.t Zipper.t -> Nottui.ui -(** [current_hunks_side_by_side zipper] returns the current hunks in a patch zipper for side-by-side view. *) +val current_hunks_side_by_side : string Patch.t Zipper.t -> 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 e72f164..cab3902 100644 --- a/lib/InteractiveViewer.ml +++ b/lib/InteractiveViewer.ml @@ -17,8 +17,8 @@ let quit = Lwd.var false let toggle_help_visibility () = Lwd.set help_visible (not (Lwd.peek help_visible)) -let view (patches : Patch.t list) = - let z_patches_var : Patch.t Zipper.t Lwd.var = +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 | Some z -> Lwd.var z | None -> failwith "zipper_of_list: empty list" diff --git a/lib/InteractiveViewer.mli b/lib/InteractiveViewer.mli index e909aaa..0a0c7c0 100644 --- a/lib/InteractiveViewer.mli +++ b/lib/InteractiveViewer.mli @@ -1,5 +1,6 @@ -val start : ?term:Notty_unix.Term.t -> Patch.t list -> unit +val start : ?term:Notty_unix.Term.t -> string Patch.t list -> unit (** [start patches] starts diffcessible with the given patches *) -val start_test : Patch.t list -> char list -> int -> int -> unit -(** [start_test patches input start_line start_col] starts diffcessible with the given patches and input *) +val start_test : string Patch.t list -> char list -> int -> int -> unit +(** [start_test patches input start_line start_col] starts diffcessible with the + given patches and input *) diff --git a/lib/OperationView.ml b/lib/OperationView.ml index ebd72d6..2b0e946 100644 --- a/lib/OperationView.ml +++ b/lib/OperationView.ml @@ -1,7 +1,7 @@ open Nottui module W = Nottui_widgets -let operation_info (z_patches : Patch.t Zipper.t) : Nottui.ui = +let operation_info (z_patches : string Patch.t Zipper.t) : Nottui.ui = let p = Zipper.get_focus z_patches in let num_hunks = List.length p.Patch.hunks in let hunk_text = @@ -45,7 +45,7 @@ let ui_of_operation (operation : Patch.operation) : Nottui.ui = Ui.hcat [ W.string "Modification of "; W.string ~attr:blue_bold_attr path ] -let current_operation (z_patches : Patch.t Zipper.t) : Nottui.ui = +let current_operation (z_patches : string Patch.t Zipper.t) : Nottui.ui = let p = Zipper.get_focus z_patches in ui_of_operation p.Patch.operation @@ -58,7 +58,7 @@ let additions_and_removals lines : int * int = in List.fold_left add_line (0, 0) lines -let accumulate_count (hunks : Patch.hunk list) : int * int = +let accumulate_count (hunks : string Patch.hunk list) : int * int = List.fold_left (fun (add_acc, remove_acc) hunk -> let add_in_hunk, remove_in_hunk = @@ -67,7 +67,7 @@ let accumulate_count (hunks : Patch.hunk list) : int * int = (add_acc + add_in_hunk, remove_acc + remove_in_hunk)) (0, 0) hunks -let change_summary (z_patches : Patch.t Zipper.t) : Nottui.ui = +let change_summary (z_patches : string Patch.t Zipper.t) : Nottui.ui = let p = Zipper.get_focus z_patches in let total_additions, total_removals = accumulate_count p.Patch.hunks in let format_plural n singular plural = diff --git a/lib/OperationView.mli b/lib/OperationView.mli index bd03498..8437ccc 100644 --- a/lib/OperationView.mli +++ b/lib/OperationView.mli @@ -1,11 +1,11 @@ -val operation_info : Patch.t Zipper.t -> Nottui.ui -(** [operation_info zipper] returns a UI element that displays information about the - current operation in the zipper. *) +val operation_info : string Patch.t Zipper.t -> Nottui.ui +(** [operation_info zipper] returns a UI element that displays information about + the current operation in the zipper. *) -val change_summary : Patch.t Zipper.t -> Nottui.ui -(** [change_summary zipper] returns a UI element that displays a summary of the changes - that will be made by the current operation in the zipper. *) +val change_summary : string Patch.t Zipper.t -> Nottui.ui +(** [change_summary zipper] returns a UI element that displays a summary of the + changes that will be made by the current operation in the zipper. *) -val current_operation : Patch.t Zipper.t -> Nottui.ui -(** [current_operation zipper] returns a UI element that displays the current operation - in the zipper. *) +val current_operation : string Patch.t Zipper.t -> Nottui.ui +(** [current_operation zipper] returns a UI element that displays the current + operation in the zipper. *) diff --git a/lib/PatchNavigation.ml b/lib/PatchNavigation.ml index 1f397f1..122de27 100644 --- a/lib/PatchNavigation.ml +++ b/lib/PatchNavigation.ml @@ -2,5 +2,5 @@ module W = Nottui_widgets type direction = Prev | Next -let navigate (dir : direction) : Patch.t Zipper.t -> Patch.t Zipper.t = +let navigate (dir : direction) = match dir with Prev -> Zipper.prev | Next -> Zipper.next diff --git a/lib/PatchNavigation.mli b/lib/PatchNavigation.mli index ff5f140..de430c8 100644 --- a/lib/PatchNavigation.mli +++ b/lib/PatchNavigation.mli @@ -2,6 +2,6 @@ type direction = | Prev | Next (** [direction] represents the direction of navigation. *) -val navigate : direction -> Patch.t Zipper.t -> Patch.t Zipper.t +val navigate : direction -> 'a Patch.t Zipper.t -> 'a Patch.t Zipper.t (** [navigate direction zipper] returns the zipper that is the result of navigating in the given direction. *) diff --git a/lib/WordDiff.ml b/lib/WordDiff.ml new file mode 100644 index 0000000..aaf94ce --- /dev/null +++ b/lib/WordDiff.ml @@ -0,0 +1,161 @@ +open Nottui +module W = Nottui_widgets + +type word = Unchanged of string | Changed of string +type line_content = word list + +let string_to_words s = Array.of_list (String.split_on_char ' ' s) +let longest xs ys = if List.length xs > List.length ys then xs else ys + +let lcs xs' ys' = + let xs = Array.of_list xs' and ys = Array.of_list ys' in + let n = Array.length xs and m = Array.length ys in + let a = Array.make_matrix (n + 1) (m + 1) [] in + for i = n - 1 downto 0 do + for j = m - 1 downto 0 do + a.(i).(j) <- + (if xs.(i) = ys.(j) then xs.(i) :: a.(i + 1).(j + 1) + else longest a.(i).(j + 1) a.(i + 1).(j)) + done + done; + a.(0).(0) + +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 + let common = lcs words1 words2 in + + let rec construct_diff w1 w2 lcs acc_mine acc_their = + match (w1, w2, lcs) with + | [], [], [] -> (List.rev acc_mine, List.rev acc_their) + | x :: xs, y :: ys, z :: zs -> ( + match (x = z, y = z) with + | true, true -> + construct_diff xs ys zs (Unchanged x :: acc_mine) + (Unchanged y :: acc_their) + | false, true -> + construct_diff xs (y :: ys) (z :: zs) (Changed x :: acc_mine) + acc_their + | true, false -> + construct_diff (x :: xs) ys (z :: zs) acc_mine + (Changed y :: acc_their) + | false, false -> + construct_diff xs ys (z :: zs) (Changed x :: acc_mine) + (Changed y :: acc_their)) + | x :: xs, [], lcs -> + construct_diff xs [] lcs (Changed x :: acc_mine) acc_their + | [], y :: ys, lcs -> + construct_diff [] ys lcs acc_mine (Changed y :: acc_their) + | x :: xs, y :: ys, [] -> + construct_diff xs ys [] (Changed x :: acc_mine) (Changed y :: acc_their) + | [], [], _ :: _ -> assert false + (* Since lcs is the longest common subsequence, this case cannot happen *) + in + + construct_diff words1 words2 common [] [] + +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 diff --git a/lib/WordDiff.mli b/lib/WordDiff.mli new file mode 100644 index 0000000..0dae84a --- /dev/null +++ b/lib/WordDiff.mli @@ -0,0 +1,10 @@ +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 diff --git a/lib/zipper.ml b/lib/zipper.ml index fd05c42..2d530eb 100644 --- a/lib/zipper.ml +++ b/lib/zipper.ml @@ -13,8 +13,9 @@ type 'a t = { current_index : int; (* 0-based index *) } -(** [zipper_of_list lst] converts a list into a zipper. If the list is empty, it returns [None]. - Otherwise, it returns [Some zipper] with the first element of the list as the focus. *) +(** [zipper_of_list lst] converts a list into a zipper. If the list is empty, it + returns [None]. Otherwise, it returns [Some zipper] with the first element + of the list as the focus. *) let zipper_of_list lst : 'a t option = match lst with | [] -> None @@ -28,8 +29,8 @@ let zipper_of_list lst : 'a t option = current_index = 0; } -(** [next z] moves the focus of the zipper [z] to the next element, if any. - If the focus is on the last element, the zipper remains unchanged. *) +(** [next z] moves the focus of the zipper [z] to the next element, if any. If + the focus is on the last element, the zipper remains unchanged. *) let next z = match z.after with | [] -> z @@ -43,7 +44,7 @@ let next z = } (** [prev z] moves the focus of the zipper [z] to the previous element, if any. - If the focus is on the first element, the zipper remains unchanged. *) + If the focus is on the first element, the zipper remains unchanged. *) let prev z = match z.before with | [] -> z @@ -59,14 +60,18 @@ let prev z = (** [get_focus z] returns the current focus of the zipper [z]. *) let get_focus z = z.focus -(** [get_before z] returns the list of elements before the focus in the zipper [z]. *) +(** [get_before z] returns the list of elements before the focus in the zipper + [z]. *) let get_before z = z.before -(** [get_after z] returns the list of elements after the focus in the zipper [z]. *) +(** [get_after z] returns the list of elements after the focus in the zipper + [z]. *) let get_after z = z.after -(** [get_total_length z] returns the total length of the list represented by the zipper [z]. *) +(** [get_total_length z] returns the total length of the list represented by the + zipper [z]. *) let get_total_length z = z.total_length -(** [get_current_index z] returns the current index of the focus in the zipper [z]. *) +(** [get_current_index z] returns the current index of the focus in the zipper + [z]. *) let get_current_index z = z.current_index diff --git a/lib/zipper.mli b/lib/zipper.mli index 6d92c44..d64e2ac 100644 --- a/lib/zipper.mli +++ b/lib/zipper.mli @@ -1,13 +1,15 @@ type 'a t -(** Abstract type for a zipper for elements of type ['a] that allows for efficient traversal of a list. *) +(** Abstract type for a zipper for elements of type ['a] that allows for + efficient traversal of a list. *) val zipper_of_list : 'a list -> 'a t option -(** [zipper_of_list lst] converts a list into a zipper. If the list is empty, it returns [None]. - Otherwise, it returns [Some zipper] with the first element of the list as the focus. *) +(** [zipper_of_list lst] converts a list into a zipper. If the list is empty, it + returns [None]. Otherwise, it returns [Some zipper] with the first element + of the list as the focus. *) val next : 'a t -> 'a t -(** [next z] moves the focus of the zipper [z] to the next element, if any. - If the focus is on the last element, the zipper remains unchanged. *) +(** [next z] moves the focus of the zipper [z] to the next element, if any. If + the focus is on the last element, the zipper remains unchanged. *) val prev : 'a t -> 'a t (** [prev z] moves the focus of the zipper [z] to the previous element, if any. @@ -17,13 +19,17 @@ val get_focus : 'a t -> 'a (** [get_focus z] returns the current focus of the zipper [z]. *) val get_before : 'a t -> 'a list -(** [get_before z] returns the list of elements before the focus in the zipper [z]. *) +(** [get_before z] returns the list of elements before the focus in the zipper + [z]. *) val get_after : 'a t -> 'a list -(** [get_after z] returns the list of elements after the focus in the zipper [z]. *) +(** [get_after z] returns the list of elements after the focus in the zipper + [z]. *) val get_total_length : 'a t -> int -(** [get_total_length z] returns the total length of the list represented by the zipper [z]. *) +(** [get_total_length z] returns the total length of the list represented by the + zipper [z]. *) val get_current_index : 'a t -> int -(** [get_current_index z] returns the current index (0-based) of the focus in the list. *) +(** [get_current_index z] returns the current index (0-based) of the focus in + the list. *) diff --git a/test/cram/example.diff b/test/cram/example.diff index 608e84a..a0cb3eb 100644 --- a/test/cram/example.diff +++ b/test/cram/example.diff @@ -159,4 +159,4 @@ index e69de29..eee417f 100644 --- a/dir1/file.txt +++ b/dir2/file.txt @@ -0,0 +1 @@ -+new text \ No newline at end of file ++new text diff --git a/test/cram/test.t b/test/cram/test.t index f0678f9..e96fbb8 100644 --- a/test/cram/test.t +++ b/test/cram/test.t @@ -6,11 +6,11 @@ This is a cram test for the new executable. 1 addition, 1 removal Modification of bin/dune @@ -1,4 +1,4 @@ - 1 1 (executable - 2 2 (public_name diffcessible) - 3 3 (name main) - 4 - (libraries diffcessible cmdliner)) - 4 + (libraries diffcessible cmdliner patch)) + 1 1 (executable + 2 2 (public_name diffcessible) + 3 3 (name main) + 4 - (libraries diffcessible cmdliner)) + 4 + (libraries diffcessible cmdliner patch)) @@ -48,10 +48,10 @@ This is a cram test for the new executable. 1 addition, 1 removal Modification of lib/dune @@ -1,3 +1,3 @@ - 1 1 (library - 2 2 (name diffcessible) - 3 - (libraries notty nottui lwd)) - 3 + (libraries notty nottui lwd patch)) + 1 1 (library + 2 2 (name diffcessible) + 3 - (libraries notty nottui lwd)) + 3 + (libraries notty nottui lwd patch)) @@ -383,5 +383,3 @@ This is a cram test for the new executable. - - diff --git a/test/cram/word-diff.t/examples.diff b/test/cram/word-diff.t/examples.diff new file mode 100644 index 0000000..8fa35ad --- /dev/null +++ b/test/cram/word-diff.t/examples.diff @@ -0,0 +1,51 @@ +--- a/document.txt ++++ b/document.txt +@@ -1,5 +1,5 @@ +-The quick brown fox jumps over the lazy dog. ++The fast brown fox leaps over the sleeping dog. + She sells seashells by the seashore. +-The rain in Spain stays mainly in the plain. ++The rain in France falls mainly on the plain. + To be or not to be, that is the question. +-All that glitters is not gold. ++Not all that glitters is gold. +@@ -7,3 +7,3 @@ + A stitch in time saves nine. +-An apple a day keeps the doctor away. ++An apple each day keeps the doctor away. + Actions speak louder than words. +@@ -11,2 +11,2 @@ +-The early bird catches the worm. ++The early riser catches the worm. +@@ -14,2 +14,2 @@ +-Practice makes perfect. ++Practice leads to perfection. +@@ -17,3 +17,3 @@ + When in Rome, do as the Romans do. +-Don't put all your eggs in one basket. ++Never put all your eggs in one basket. + A penny saved is a penny earned. +@@ -21,2 +21,2 @@ +-Every cloud has a silver lining. ++Every cloud possesses a silver lining. +@@ -24,2 +24,2 @@ +-Two wrongs don't make a right. ++Two wrongs never make a right. +@@ -27,1 +27,2 @@ + The pen is mightier than the sword. ++Words can be sharper than any blade. + +--- a/sample.txt ++++ b/sample.txt +@@ -1,2 +1,2 @@ +-foo bar baz ++foo bat bak +@@ -4,2 +4,1 @@ +-The quick brown fox jumps over the lazy dog. ++The quick black cat jumps over the lazy dog. +@@ -7,2 +6,2 @@ +-She sells sea shells by the sea shore. ++He sells sea shells by the sea floor. + + + diff --git a/test/cram/word-diff.t/run.t b/test/cram/word-diff.t/run.t new file mode 100644 index 0000000..873b0b9 --- /dev/null +++ b/test/cram/word-diff.t/run.t @@ -0,0 +1,45 @@ +# Run the executable + $ dummy_terminal examples.diff + Operation 1 of 2, 8 hunks + 10 additions, 9 removals + Rename with modifications a/document.txt to b/document.txt + @@ -1,5 +1,5 @@ + 1 - The quick brown fox jumps over the lazy dog. + 1 + The fast brown fox leaps over the sleeping dog. + 2 2 She sells seashells by the seashore. + 3 - The rain in Spain stays mainly in the plain. + 3 + The rain in France falls mainly on the plain. + 4 4 To be or not to be, that is the question. + 5 - All that glitters is not gold. + 5 + Not all that glitters is gold. + @@ -7,3 +7,3 @@ + 1 1 A stitch in time saves nine. + 2 - An apple a day keeps the doctor away. + 2 + An apple each day keeps the doctor away. + 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. + $ dummy_terminal examples.diff n + Operation 2 of 2, 3 hunks + 3 additions, 3 removals + Rename with modifications a/sample.txt to b/sample.txt + @@ -1,2 +1,2 @@ + 1 - foo bar baz + 1 + foo bat bak + @@ -4,2 +4,1 @@ + 1 - The quick brown fox jumps over the lazy dog. + 1 + The quick black cat jumps over the lazy dog. + @@ -7,2 +6,2 @@ + 1 - She sells sea shells by the sea shore. + 1 + He sells sea shells by the sea floor. + + + + + + + + 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. + + diff --git a/test/unit/TestBlock.ml b/test/unit/TestBlock.ml new file mode 100644 index 0000000..01668c7 --- /dev/null +++ b/test/unit/TestBlock.ml @@ -0,0 +1,236 @@ +open Diffcessible + +let example_hunk1 : string Patch.line list = + [ `Mine "A"; `Their "B"; `Mine "C"; `Their "D" ] + +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 }; + ] + +let empty_hunk : string Patch.line list = [] +let empty_blocks : string Block.t list = [] + +let common_only_hunk : string Patch.line list = + [ `Common "A"; `Common "B"; `Common "C" ] + +let common_only_blocks : string Block.t list = + [ Block.Common "A"; Block.Common "B"; Block.Common "C" ] + +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 } ] + +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 }; + ] + +let complex_hunk : string Patch.line list = + [ + `Common "A"; + `Mine "B"; + `Their "C"; + `Mine "D"; + `Their "E"; + `Common "F"; + `Mine "G"; + `Their "H"; + `Their "I"; + ] + +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.Common "F"; + Block.Changed { mine = [ "G" ]; their = [ "H"; "I" ]; order = Block.Mine }; + ] + +let alternating_add_remove_hunk1 : string Patch.line list = + [ `Mine "A"; `Their "B"; `Mine "C" ] + +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 }; + ] + +let alternating_remove_add_hunk1 : string Patch.line list = + [ `Their "X"; `Mine "Y"; `Their "Z" ] + +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 }; + ] + +let complex_alternating_hunk : string Patch.line list = + [ + `Mine "A"; + `Their "B"; + `Mine "C"; + `Their "D"; + `Mine "E"; + `Their "F"; + `Mine "G"; + `Their "H"; + `Common "I"; + `Their "J"; + `Mine "K"; + `Their "L"; + `Mine "M"; + ] + +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.Common "I"; + Block.Changed { mine = [ "K" ]; their = [ "J" ]; order = Block.Their }; + Block.Changed { mine = [ "M" ]; their = [ "L" ]; order = Block.Their }; + ] + +let multiple_consecutive_changes_hunk : string Patch.line list = + [ + `Mine "A"; + `Mine "B"; + `Their "C"; + `Their "D"; + `Mine "E"; + `Mine "F"; + `Their "G"; + `Their "H"; + `Common "I"; + `Their "J"; + `Their "K"; + `Mine "L"; + `Mine "M"; + ] + +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.Common "I"; + Block.Changed + { mine = [ "L"; "M" ]; their = [ "J"; "K" ]; order = Block.Their }; + ] + +let string_of_block = function + | Block.Common x -> Printf.sprintf "Common %S" x + | Block.Changed { mine; their; order } -> + 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") + +let string_of_blocks blocks = + "[" ^ String.concat "; " (List.map string_of_block blocks) ^ "]" + +let string_of_hunk_line = function + | `Common s -> Printf.sprintf "`Common %S" s + | `Mine s -> Printf.sprintf "`Mine %S" s + | `Their s -> Printf.sprintf "`Their %S" s + +let string_of_hunk hunk = + "[" ^ String.concat "; " (List.map string_of_hunk_line hunk) ^ "]" + +let assert_with_message condition message = + if not condition then failwith message + +let test_of_hunk () = + let test_case name hunk expected = + let result = Block.of_hunk hunk in + assert_with_message (result = expected) + (Printf.sprintf "of_hunk failed for %s\nExpected: %s\nGot: %s" name + (string_of_blocks expected) + (string_of_blocks result)) + in + test_case "example_hunk1" example_hunk1 example_blocks1; + test_case "empty_hunk" empty_hunk empty_blocks; + test_case "common_only_hunk" common_only_hunk common_only_blocks; + test_case "mine_only_hunk" mine_only_hunk mine_only_blocks; + test_case "their_only_hunk" their_only_hunk their_only_blocks; + test_case "complex_hunk" complex_hunk complex_blocks; + test_case "alternating_add_remove_hunk1" alternating_add_remove_hunk1 + alternating_add_remove_blocks1; + test_case "alternating_remove_add_hunk1" alternating_remove_add_hunk1 + alternating_remove_add_blocks1; + test_case "complex_alternating_hunk" complex_alternating_hunk + complex_alternating_blocks; + test_case "multiple_consecutive_changes_hunk" + multiple_consecutive_changes_hunk multiple_consecutive_changes_blocks + +let test_to_hunk () = + let test_case name blocks expected = + let result = Block.to_hunk blocks in + assert_with_message (result = expected) + (Printf.sprintf "to_hunk failed for %s\nExpected: %s\nGot: %s" name + (string_of_hunk expected) (string_of_hunk result)) + in + test_case "example_blocks1" example_blocks1 example_hunk1; + test_case "empty_blocks" empty_blocks empty_hunk; + test_case "common_only_blocks" common_only_blocks common_only_hunk; + test_case "mine_only_blocks" mine_only_blocks mine_only_hunk; + test_case "their_only_blocks" their_only_blocks their_only_hunk; + test_case "complex_blocks" complex_blocks complex_hunk; + test_case "alternating_add_remove_blocks1" alternating_add_remove_blocks1 + alternating_add_remove_hunk1; + test_case "alternating_remove_add_blocks1" alternating_remove_add_blocks1 + alternating_remove_add_hunk1; + test_case "complex_alternating_blocks" complex_alternating_blocks + complex_alternating_hunk; + test_case "multiple_consecutive_changes_blocks" + multiple_consecutive_changes_blocks multiple_consecutive_changes_hunk + +let test_roundtrip () = + let test_roundtrip_for name hunk = + let roundtrip = hunk |> Block.of_hunk |> Block.to_hunk in + assert_with_message (roundtrip = hunk) + (Printf.sprintf "Roundtrip failed for %s\nOriginal: %s\nRoundtrip: %s" + name (string_of_hunk hunk) (string_of_hunk roundtrip)) + in + test_roundtrip_for "example_hunk1" example_hunk1; + test_roundtrip_for "empty_hunk" empty_hunk; + test_roundtrip_for "common_only_hunk" common_only_hunk; + test_roundtrip_for "mine_only_hunk" mine_only_hunk; + test_roundtrip_for "their_only_hunk" their_only_hunk; + test_roundtrip_for "complex_hunk" complex_hunk; + test_roundtrip_for "alternating_add_remove_hunk1" alternating_add_remove_hunk1; + test_roundtrip_for "alternating_remove_add_hunk1" alternating_remove_add_hunk1; + test_roundtrip_for "complex_alternating_hunk" complex_alternating_hunk; + test_roundtrip_for "multiple_consecutive_changes_hunk" + multiple_consecutive_changes_hunk + +let run_tests () = + let run_test name f = + try + Printf.printf "Running %s...\n" name; + f (); + Printf.printf "%s passed.\n\n" name + with + | Failure msg -> Printf.printf "%s failed:\n%s\n\n" name msg + | exn -> + Printf.printf "%s failed with unexpected exception:\n%s\n\n" name + (Printexc.to_string exn) + in + run_test "TestBlock::test_of_hunk" test_of_hunk; + run_test "TestBlock::test_to_hunk" test_to_hunk; + run_test "TestBlock::test_roundtrip" test_roundtrip + +let () = run_tests () diff --git a/test/unit/TestWordDiff.ml b/test/unit/TestWordDiff.ml new file mode 100644 index 0000000..8a325b5 --- /dev/null +++ b/test/unit/TestWordDiff.ml @@ -0,0 +1,139 @@ +open Diffcessible + +let test_lcs_1 = ([ "a"; "b"; "c" ], [ "a"; "b"; "c" ], [ "a"; "b"; "c" ]) +let test_lcs_2 = ([ "a"; "b"; "c" ], [ "d"; "e"; "f" ], []) + +let test_lcs_3 = + ( [ "a"; "b"; "c"; "d"; "f"; "g"; "h"; "j"; "q"; "z" ], + [ "a"; "b"; "c"; "d"; "e"; "f"; "g"; "i"; "j"; "k"; "r"; "x"; "y"; "z" ], + [ "a"; "b"; "c"; "d"; "f"; "g"; "j"; "z" ] ) + +let test_lcs_4 = ([], [], []) (* Empty lists *) + +let test_lcs_5 = + ([ "a"; "a"; "b"; "b"; "c" ], [ "a"; "b"; "c"; "c" ], [ "a"; "b"; "c" ]) +(* Repeating elements *) + +let test_lcs_6 = ([ "a"; "b"; "c" ], [ "d"; "e"; "f" ], []) + +(* All different, same length *) +let test_lcs_7 = ([], [ "a"; "b"; "c" ], []) (* One empty, one non-empty *) +let test_lcs_8 = ([ "a"; "b"; "c" ], [], []) (* One non-empty, one empty *) + +let test_diff_1 = + ( "abc def", + "abc ghi", + ( [ WordDiff.Unchanged "abc"; WordDiff.Changed "def" ], + [ WordDiff.Unchanged "abc"; WordDiff.Changed "ghi" ] ) ) + +let test_diff_2 = + ( "", + "abc def", + ([ WordDiff.Changed "" ], [ WordDiff.Changed "abc"; WordDiff.Changed "def" ]) + ) + +let test_diff_3 = + ( "abc def", + "", + ([ WordDiff.Changed "abc"; WordDiff.Changed "def" ], [ WordDiff.Changed "" ]) + ) + +let test_diff_4 = + ( "abc def ghi", + "abc def jkl", + ( [ + WordDiff.Unchanged "abc"; + WordDiff.Unchanged "def"; + WordDiff.Changed "ghi"; + ], + [ + WordDiff.Unchanged "abc"; + WordDiff.Unchanged "def"; + WordDiff.Changed "jkl"; + ] ) ) + +let test_diff_5 = + ( " abc def ", + " abc def ", + ( [ + WordDiff.Unchanged ""; + WordDiff.Changed ""; + WordDiff.Unchanged "abc"; + WordDiff.Changed ""; + WordDiff.Unchanged "def"; + WordDiff.Unchanged ""; + WordDiff.Changed ""; + ], + [ + WordDiff.Unchanged ""; + WordDiff.Unchanged "abc"; + WordDiff.Unchanged "def"; + WordDiff.Unchanged ""; + ] ) ) + +let string_of_word = function + | WordDiff.Unchanged s -> Printf.sprintf "Unchanged %S" s + | WordDiff.Changed s -> Printf.sprintf "Changed %S" s + +let string_of_line_content lc = + "[" ^ String.concat "; " (List.map string_of_word lc) ^ "]" + +let string_of_diff_result (mine, their) = + Printf.sprintf "(%s, %s)" + (string_of_line_content mine) + (string_of_line_content their) + +let assert_with_message condition message = + if not condition then failwith message + +let test_lcs () = + let test_case (input1, input2, expected) = + let result = WordDiff.lcs input1 input2 in + assert_with_message (result = expected) + (Printf.sprintf + "LCS failed\nInput1: %s\nInput2: %s\nExpected: %s\nGot: %s" + (String.concat " " input1) (String.concat " " input2) + (String.concat " " expected) + (String.concat " " result)) + in + List.iter test_case + [ + test_lcs_1; + test_lcs_2; + test_lcs_3; + test_lcs_4; + test_lcs_5; + test_lcs_6; + test_lcs_7; + test_lcs_8; + ] + +let test_diff_words () = + let test_case (input1, input2, expected) = + let result = WordDiff.diff_words input1 input2 in + assert_with_message (result = expected) + (Printf.sprintf + "diff_words failed\nInput1: %S\nInput2: %S\nExpected: %s\nGot: %s" + input1 input2 + (string_of_diff_result expected) + (string_of_diff_result result)) + in + List.iter test_case + [ test_diff_1; test_diff_2; test_diff_3; test_diff_4; test_diff_5 ] + +let run_tests () = + let run_test name f = + try + Printf.printf "Running %s...\n" name; + f (); + Printf.printf "%s passed.\n\n" name + with + | Failure msg -> Printf.printf "%s failed:\n%s\n\n" name msg + | exn -> + Printf.printf "%s failed with unexpected exception:\n%s\n\n" name + (Printexc.to_string exn) + in + run_test "test_lcs" test_lcs; + run_test "test_diff_words" test_diff_words + +let () = run_tests () diff --git a/test/unit/dune b/test/unit/dune index d1636b2..98ea2e4 100644 --- a/test/unit/dune +++ b/test/unit/dune @@ -1,3 +1,4 @@ (tests - (names TestZipper) - (libraries diffcessible notty nottui lwd patch)) + (names TestZipper TestBlock TestWordDiff) + (libraries diffcessible notty nottui lwd patch) + (modules TestZipper TestBlock TestWordDiff)) diff --git a/vendor/patch/src/patch.ml b/vendor/patch/src/patch.ml index 6ae4cc2..33017bf 100644 --- a/vendor/patch/src/patch.ml +++ b/vendor/patch/src/patch.ml @@ -43,12 +43,14 @@ module String = struct let equal = String.equal end -type hunk = { +type 'a line = [`Common of 'a | `Mine of 'a | `Their of 'a ] + +type 'a hunk = { mine_start : int ; mine_len : int ; their_start : int ; their_len : int ; - lines : [ `Common of string | `Mine of string | `Their of string ] list ; + lines : 'a line list ; } let unified_diff hunk = @@ -239,9 +241,9 @@ let pp_operation ~git ppf op = Format.fprintf ppf "rename from %s\n" old_name; Format.fprintf ppf "rename to %s\n" new_name -type t = { +type 'a t = { operation : operation ; - hunks : hunk list ; + hunks : 'a hunk list ; mine_no_nl : bool ; their_no_nl : bool ; } diff --git a/vendor/patch/src/patch.mli b/vendor/patch/src/patch.mli index c3c0a6a..d291bb0 100644 --- a/vendor/patch/src/patch.mli +++ b/vendor/patch/src/patch.mli @@ -1,14 +1,16 @@ -type hunk = { +type 'a line = [`Common of 'a | `Mine of 'a | `Their of 'a ] + +type 'a hunk = { mine_start : int ; mine_len : int ; their_start : int ; their_len : int ; - lines : [ `Common of string | `Mine of string | `Their of string ] list ; + lines : 'a line list ; } -val mine : hunk -> string list -val their : hunk -> string list -val pp_hunk : Format.formatter -> hunk -> unit +val mine : 'a hunk -> 'a list +val their : 'a hunk -> 'a list +val pp_hunk : Format.formatter -> string hunk -> unit type operation = | Edit of string @@ -21,15 +23,15 @@ val pp_operation : git:bool -> Format.formatter -> operation -> unit val operation_eq : operation -> operation -> bool -type t = { +type 'a t = { operation : operation ; - hunks : hunk list ; + hunks : 'a hunk list ; mine_no_nl : bool ; their_no_nl : bool ; } -val pp : git:bool -> Format.formatter -> t -> unit +val pp : git:bool -> Format.formatter -> string t -> unit -val to_diffs : string -> t list +val to_diffs : string -> string t list -val patch : string option -> t -> string option +val patch : string option -> string t -> string option