Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 40 additions & 50 deletions lib/HunkView.ml
Original file line number Diff line number Diff line change
Expand Up @@ -147,43 +147,43 @@ let render_hunk_lines (hunk_lines : WordDiff.line_content Patch.line list)
in
Ui.vcat (process_lines 0 0 [] hunk_lines)

let render_hunk_without_word_diff (hunk : string Patch.hunk)
(mode : rendering_mode) : Ui.t =
let render_line mine_num their_num diff_type content =
let line_number = render_line_number mine_num their_num diff_type mode in
let content_ui =
style_text content
(match diff_type with
| `Added -> Notty.A.(fg green)
| `Removed -> Notty.A.(fg red)
| `Unchanged -> Notty.A.empty)
mode
in
Ui.hcat [ line_number; content_ui ]
in
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 content ->
( mine_num + 1,
their_num + 1,
render_line mine_num their_num `Unchanged content )
| `Mine content ->
( mine_num + 1,
their_num,
render_line mine_num their_num `Removed content )
| `Their content ->
( mine_num,
their_num + 1,
render_line mine_num their_num `Added content )
in
process_lines new_mine new_their (ui :: acc) rest
in
Ui.vcat
(process_lines hunk.Patch.mine_start hunk.Patch.their_start []
hunk.Patch.lines)
(* let render_hunk_without_word_diff (hunk : string Patch.hunk) *)
(* (mode : rendering_mode) : Ui.t = *)
(* let render_line mine_num their_num diff_type content = *)
(* let line_number = render_line_number mine_num their_num diff_type mode in *)
(* let content_ui = *)
(* style_text content *)
(* (match diff_type with *)
(* | `Added -> Notty.A.(fg green) *)
(* | `Removed -> Notty.A.(fg red) *)
(* | `Unchanged -> Notty.A.empty) *)
(* mode *)
(* in *)
(* Ui.hcat [ line_number; content_ui ] *)
(* in *)
(* 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 content -> *)
(* ( mine_num + 1, *)
(* their_num + 1, *)
(* render_line mine_num their_num `Unchanged content ) *)
(* | `Mine content -> *)
(* ( mine_num + 1, *)
(* their_num, *)
(* render_line mine_num their_num `Removed content ) *)
(* | `Their content -> *)
(* ( mine_num, *)
(* their_num + 1, *)
(* render_line mine_num their_num `Added content ) *)
(* in *)
(* process_lines new_mine new_their (ui :: acc) rest *)
(* in *)
(* Ui.vcat *)
(* (process_lines hunk.Patch.mine_start hunk.Patch.their_start [] *)
(* hunk.Patch.lines) *)

(* Helper functions for side-by-side view *)

Expand Down Expand Up @@ -241,19 +241,9 @@ 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_without_word_diff hunk mode
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
in
Ui.vcat [ hunk_summary; hunk_content ]

Expand Down
110 changes: 106 additions & 4 deletions lib/WordDiff.ml
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,113 @@ let diff_words (s1 : string) (s2 : string) : line_content * line_content =

construct_diff words1 words2 common [] []

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_aux 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_aux (i - 1) j + 1)
(edit_distance_aux i (j - 1) + 1))
(edit_distance_aux (i - 1) (j - 1) + cost_to_drop_both)
in
Hashtbl.add memo (i, j) result;
result)
in
edit_distance_aux (Array.length s) (Array.length t)

let is_approximately_equal s1 s2 =
let compare_char c1 c2 = c1 = c2 in
let s1_array = Array.of_seq (String.to_seq s1) in
let s2_array = Array.of_seq (String.to_seq s2) in
let threshold = max 3 (max (String.length s1) (String.length s2) / 3) in
edit_distance compare_char s1_array s2_array <= threshold

let lacs words1 words2 =
let m = List.length words1 and n = List.length words2 in
let dp = Array.make_matrix (m + 1) (n + 1) [] in
for i = 1 to m do
for j = 1 to n do
if
is_approximately_equal
(List.nth words1 (i - 1))
(List.nth words2 (j - 1))
then dp.(i).(j) <- List.nth words1 (i - 1) :: dp.(i - 1).(j - 1)
else if List.length dp.(i - 1).(j) > List.length dp.(i).(j - 1) then
dp.(i).(j) <- dp.(i - 1).(j)
else dp.(i).(j) <- dp.(i).(j - 1)
done
done;
List.rev dp.(m).(n)

let pair_lines (s1 : string array) (s2 : string array) :
(string option * string option) list =
let words1 = Array.to_list s1 in
let words2 = Array.to_list s2 in
let common = lacs words1 words2 in

let rec construct_pairs w1 w2 lcs acc =
match (w1, w2, lcs) with
| [], [], [] -> List.rev acc
| x :: xs, y :: ys, z :: zs -> (
match (is_approximately_equal x z, is_approximately_equal y z) with
| true, true ->
construct_pairs xs ys zs ((Some x, Some y) :: acc)
| false, true ->
construct_pairs xs (y :: ys) (z :: zs) ((Some x, None) :: acc)
| true, false ->
construct_pairs (x :: xs) ys (z :: zs) ((None, Some y) :: acc)
| false, false ->
construct_pairs xs ys (z :: zs) ((Some x, Some y) :: acc))
| x :: xs, [], lcs ->
construct_pairs xs [] lcs ((Some x, None) :: acc)
| [], y :: ys, lcs ->
construct_pairs [] ys lcs ((None, Some y) :: acc)
| x :: xs, y :: ys, [] ->
construct_pairs xs ys [] ((Some x, Some y) :: acc)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pairing these lines is wrong!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, do you mind elaborating more on why it is wrong?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try it on the following test!

- aaaaaaaaa a
- egdg xsd fi sdgfyu s gd csue c e yeg scy
+ aaaaaaaaa b
+ yugyu fft yu utf ytfutf uyg gygu yftuf gg uf ft

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LACS -> A sequence of lines
Here, we have two lines in 1 and two lines in 2.
So LCAS is a matter of lines. Here, it will be a length of 1, and the LCAS are (Some aaaaaaaaa a, Some aaaaaaaaa b)

So here, in the accumulator, I am adding x and y as being paired, when x and y are not part of the LACS.
Here, the example, x and y corresponds to egdg ... and yugyu ...
So they should not be paired.

| [], [], _ :: _ -> assert false
in

construct_pairs 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 mine_array = Array.of_list mine in
let their_array = Array.of_list their in
let paired_lines = pair_lines mine_array their_array in

let diff_words line1 line2 =
match (line1, line2) with
| Some l1, Some l2 -> diff_words l1 l2
| Some l, None | None, Some l ->
let words = String.split_on_char ' ' l in
(List.map (fun w -> Changed w) words, [])
| None, None -> ([], [])
in

let mine_content, their_content =
List.fold_left
(fun (mine_acc, their_acc) (m, t) ->
let mine_diff, their_diff = diff_words m t in
(mine_diff :: mine_acc, their_diff :: their_acc))
([], []) paired_lines
in

Block.Changed {
mine = List.rev mine_content;
their = List.rev their_content;
order;
}
4 changes: 4 additions & 0 deletions lib/WordDiff.mli
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ val compute : string Block.t -> line_content Block.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

val pair_lines :
string array -> string array -> (string option * string option) list
Loading