Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Word diff on multiple lines #78

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
257 changes: 187 additions & 70 deletions lib/HunkView.ml
Original file line number Diff line number Diff line change
@@ -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 *)

Expand Down Expand Up @@ -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;
Expand All @@ -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 ->
Expand All @@ -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)
7 changes: 5 additions & 2 deletions lib/HunkView.mli
Original file line number Diff line number Diff line change
@@ -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. *)
24 changes: 17 additions & 7 deletions lib/InteractiveViewer.ml
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,39 @@ 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
| Some z -> Lwd.var z
| 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 =
Expand Down Expand Up @@ -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
Expand Down
Loading