Source file display_util_internal.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
open Core_kernel
module Color = struct
type t =
| Red
| Green
| Plain
[@@deriving sexp_of, compare]
let equal = [%compare.equal: t]
end
module Line = struct
type t =
{ color : Color.t
; content : string
}
[@@deriving fields, sexp_of]
let empty = { color = Color.Plain; content = "" }
let to_text ~green ~red ~plain t =
let formatting =
match t.color with
| Red -> red
| Green -> green
| Plain -> plain
in
formatting t.content
;;
let plain content = { color = Plain; content }
let red content = { color = Red; content }
let green content = { color = Green; content }
let length t = String.length t.content
let first t = String.find t.content ~f:(fun x -> Char.( <> ) x ' ') |> Option.value_exn
let last t = t.content.[String.length t.content - 1]
let concat a b =
assert (Color.equal a.color b.color);
let b = String.lstrip b.content in
let { color; content } = a in
let content =
if Char.( = ) (last a) '(' || Char.( = ) b.[0] ')'
then content ^ b
else content ^ " " ^ b
in
{ color; content }
;;
end
module Linear_diff = struct
type t =
| Same_open_paren
| Same_close_paren
| Same of Sexp.t
| Add of Sexp.t
| Delete of Sexp.t
| Replace of (Sexp.t * Sexp.t)
[@@deriving sexp_of]
let rec of_diff = function
| Diff.Same x -> [ Same x ]
| Add x -> [ Add x ]
| Delete x -> [ Delete x ]
| Replace (x, y) -> [ Replace (x, y) ]
| Enclose xs -> [ Same_open_paren ] @ List.bind xs ~f:of_diff @ [ Same_close_paren ]
;;
end
module Display_options = struct
type t =
{ collapse_threshold : int
; num_shown : int
}
[@@deriving sexp_of, fields]
let create ?(collapse_threshold = 10) ?(num_shown = 3) () =
Fields.create ~collapse_threshold ~num_shown
;;
let default = create ()
end
module Line_pair = struct
type t =
| Same of Line.t
| Different of (Line.t * Line.t)
let fst = function
| Same x -> x
| Different (x, _) -> x
;;
let snd = function
| Same x -> x
| Different (_, x) -> x
;;
let is_same = function
| Same _ -> true
| Different _ -> false
;;
end
module Hideable_line_pair = struct
type t =
| Line_pair of Line_pair.t
| Hidden of int
| All_hidden
end
let spaces ~indentation = String.make (indentation * 1) ' '
let sexp_to_lines ~indentation sexp =
let spaces = spaces ~indentation in
Sexp.to_string_hum sexp |> String.split_lines |> List.map ~f:(fun x -> spaces ^ x)
;;
let same x = Line_pair.Same (Line.plain x)
let diff_to_lines ~indentation = function
| Linear_diff.Same_open_paren -> [ same (spaces ~indentation ^ "(") ]
| Same_close_paren -> [ same (spaces ~indentation:(indentation - 1) ^ ")") ]
| Same sexp ->
let lines = sexp_to_lines ~indentation sexp in
List.map lines ~f:same
| Add sexp ->
let lines = sexp_to_lines ~indentation sexp in
List.map lines ~f:(fun x -> Line_pair.Different (Line.empty, Line.green x))
| Delete sexp ->
let lines = sexp_to_lines ~indentation sexp in
List.map lines ~f:(fun x -> Line_pair.Different (Line.red x, Line.empty))
| Replace (sexp_a, sexp_b) ->
let rec loop ~lines_a ~lines_b ~acc =
match lines_a, lines_b with
| [], [] -> List.rev acc
| a :: lines_a, [] ->
let elt = Line_pair.Different (Line.red a, Line.empty) in
loop ~lines_a ~lines_b ~acc:(elt :: acc)
| [], b :: lines_b ->
let elt = Line_pair.Different (Line.empty, Line.green b) in
loop ~lines_a ~lines_b ~acc:(elt :: acc)
| a :: lines_a, b :: lines_b ->
let elt = Line_pair.Different (Line.red a, Line.green b) in
loop ~lines_a ~lines_b ~acc:(elt :: acc)
in
let lines_a = sexp_to_lines ~indentation sexp_a in
let lines_b = sexp_to_lines ~indentation sexp_b in
loop ~lines_a ~lines_b ~acc:[]
;;
let diff_to_indentation_delta = function
| Linear_diff.Same _ | Replace _ | Add _ | Delete _ -> 0
| Same_open_paren -> 1
| Same_close_paren -> -1
;;
let combine a b =
match a, b with
| Line_pair.Different _, _ | _, Line_pair.Different _ -> None
| Line_pair.Same a, Line_pair.Same b ->
let combine () = Some (Line_pair.Same (Line.concat a b)) in
if Char.( = ) (Line.first b) ')'
then combine ()
else if Char.( = ) (Line.last a) '('
then combine ()
else None
;;
let combine_lines lines =
List.fold_right lines ~init:[] ~f:(fun line lines ->
match line, lines with
| a, b :: rest ->
(match combine a b with
| None -> a :: b :: rest
| Some x -> x :: rest)
| line, lines -> line :: lines)
;;
let hide_lines ~display_options lines =
if List.for_all lines ~f:Line_pair.is_same
then [ Hideable_line_pair.All_hidden ]
else (
let combined =
List.fold_right lines ~init:[] ~f:(fun line lines ->
match line, lines with
| Line_pair.Same _, (Line_pair.Same _ :: _ as list) :: rest ->
(line :: list) :: rest
| _ -> [ line ] :: lines)
in
List.map combined ~f:(fun lines ->
let lines = List.map lines ~f:(fun x -> Hideable_line_pair.Line_pair x) in
let num_shown = Display_options.num_shown display_options in
if List.length lines >= Display_options.collapse_threshold display_options
&& (num_shown * 2) + 1 < List.length lines
then (
let start = List.take lines num_shown in
let end_ = List.rev (List.take (List.rev lines) num_shown) in
let num_hidden = List.length lines - List.length start - List.length end_ in
start @ [ Hideable_line_pair.Hidden num_hidden ] @ end_)
else lines)
|> List.concat)
;;
let display ?(display_options = Display_options.default) changes =
let indentation = 0 in
let indentation, lines =
List.fold_map changes ~init:indentation ~f:(fun indentation change ->
let lines = diff_to_lines ~indentation change in
let delta = diff_to_indentation_delta change in
let indentation = indentation + delta in
indentation, lines)
in
assert (indentation = 0);
List.concat lines |> combine_lines |> hide_lines ~display_options
;;
let hide_message ~num_hidden = sprintf "...%d unchanged lines..." num_hidden
let all_hidden_message = "(no changes)"
let display ?display_options diff ~on_hidden ~on_all_hidden ~on_line_pair =
let diff = Linear_diff.of_diff diff in
let lines = display ?display_options diff in
let length ~project =
List.map lines ~f:(function
| Hideable_line_pair.Hidden num_hidden ->
String.length (hide_message ~num_hidden)
| All_hidden -> String.length all_hidden_message
| Line_pair x -> Line.length (project x))
|> List.max_elt ~compare:Int.compare
|> Option.value ~default:0
in
let left_length = length ~project:Line_pair.fst in
let right_length = length ~project:Line_pair.snd in
let pad_to_left = left_length + 2 in
let pad_to_right = right_length in
let width = pad_to_left + pad_to_right in
List.map lines ~f:(function
| Hideable_line_pair.Hidden num_hidden -> on_hidden ~num_hidden ~width
| Hideable_line_pair.All_hidden -> on_all_hidden ~width
| Line_pair line_pair ->
let left = Line_pair.fst line_pair in
let right = Line_pair.snd line_pair in
let left_padding = String.make (pad_to_left - Line.length left) ' ' in
let right_padding = String.make (pad_to_right - Line.length right) ' ' in
on_line_pair ~left ~right ~left_padding ~right_padding)
;;