Skip to content

Commit 8994fc2

Browse files
committed
Enhance difference highlighting for keywords (#4785)
1 parent 133fc0c commit 8994fc2

File tree

4 files changed

+310
-57
lines changed

4 files changed

+310
-57
lines changed

lib/elixir/lib/string.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2018,16 +2018,16 @@ defmodule String do
20182018
{x + 1, y, chars1, rest, [{:ins, char} | edits]}
20192019
end
20202020

2021-
defp move_right({x, y, chars1, chars2, edits}) do
2022-
{x + 1, y, chars1, chars2, edits}
2021+
defp move_right({x, y, chars1, [], edits}) do
2022+
{x + 1, y, chars1, [], edits}
20232023
end
20242024

20252025
defp move_down({x, y, [char | rest], chars2, edits}) do
20262026
{x, y + 1, rest, chars2, [{:del, char} | edits]}
20272027
end
20282028

2029-
defp move_down({x, y, chars1, chars2, edits}) do
2030-
{x, y + 1, chars1, chars2, edits}
2029+
defp move_down({x, y, [], chars2, edits}) do
2030+
{x, y + 1, [], chars2, edits}
20312031
end
20322032

20332033
defp follow_snake({x, y, [char | rest1], [char | rest2], edits}) do

lib/ex_unit/examples/difference.exs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,13 @@ defmodule Difference do
6464
end
6565

6666
test "keyword lists" do
67-
assert [file: "nofile", line: 12] == [file: nil, llne: 10]
67+
assert [file: "nofile", line: 12] == [file: nil, lime: 10]
68+
end
69+
70+
test "keyword lists; reverse order" do
71+
keyword1 = [port: 4000, max_connections: 1000]
72+
keyword2 = [max_connections: 1000, port: 4000]
73+
assert keyword1 == keyword2
6874
end
6975

7076
test "tuples" do

lib/ex_unit/lib/ex_unit/diff.ex

Lines changed: 182 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ defmodule ExUnit.Diff do
4040

4141
# Char lists and lists
4242
def script(left, right) when is_list(left) and is_list(right) do
43-
if Inspect.List.printable?(left) and Inspect.List.printable?(right) do
44-
script_string(List.to_string(left), List.to_string(right), ?')
45-
else
46-
keyword? = Inspect.List.keyword?(left) and Inspect.List.keyword?(right)
47-
script_list(left, right, keyword?, [])
43+
cond do
44+
Inspect.List.printable?(left) and Inspect.List.printable?(right) ->
45+
script_string(List.to_string(left), List.to_string(right), ?')
46+
Inspect.List.keyword?(left) and Inspect.List.keyword?(right) ->
47+
script_keyword(left, right)
48+
true ->
49+
script_list(left, right, [])
4850
end
4951
end
5052

@@ -117,58 +119,203 @@ defmodule ExUnit.Diff do
117119
end)
118120
end
119121

120-
defp script_list([], [], _keyword?, acc) do
121-
[[_ | elem_diff] | rest] = Enum.reverse(acc)
122-
[{:eq, "["}, [elem_diff | rest], {:eq, "]"}]
122+
defp script_keyword(list1, list2) do
123+
path = {0, 0, list1, list2, []}
124+
result =
125+
find_script(0, length(list1) + length(list2), [path])
126+
|> format_each_fragment([])
127+
[{:eq, "["}, result, {:eq, "]"}]
128+
end
129+
130+
defp format_each_fragment([{:diff, script}], []),
131+
do: script
132+
133+
defp format_each_fragment([{kind, elems}], []),
134+
do: [format_fragment(kind, elems)]
135+
136+
defp format_each_fragment([_, _] = fragments, acc) do
137+
result =
138+
case fragments do
139+
[diff: script1, diff: script2] ->
140+
[script1, {:eq, ", "}, script2]
141+
142+
[{:diff, script}, {kind, elems}] ->
143+
[script, {kind, ", "}, format_fragment(kind, elems)]
144+
145+
[{kind, elems}, {:diff, script}] ->
146+
[format_fragment(kind, elems), {kind, ", "}, script]
147+
148+
[del: elems1, ins: elems2] ->
149+
[format_fragment(:del, elems1), format_fragment(:ins, elems2)]
150+
151+
[{:eq, elems1}, {kind, elems2}] ->
152+
[format_fragment(:eq, elems1), {kind, ", "}, format_fragment(kind, elems2)]
153+
154+
[{kind, elems1}, {:eq, elems2}] ->
155+
[format_fragment(kind, elems1), {kind, ", "}, format_fragment(:eq, elems2)]
156+
end
157+
Enum.reverse(acc, result)
123158
end
124159

125-
defp script_list([], [elem | rest], keyword?, acc) do
126-
elem_diff = [ins: format_list_elem(elem, keyword?)]
127-
script_list([], rest, keyword?, [[ins: ", "] ++ elem_diff | acc])
160+
defp format_each_fragment([{:diff, script} | rest], acc) do
161+
format_each_fragment(rest, [{:eq, ", "}, script | acc])
128162
end
129163

130-
defp script_list([elem | rest], [], keyword?, acc) do
131-
elem_diff = [del: format_list_elem(elem, keyword?)]
132-
script_list(rest, [], keyword?, [[del: ", "] ++ elem_diff | acc])
164+
defp format_each_fragment([{kind, elems} | rest], acc) do
165+
new_acc = [{kind, ", "}, format_fragment(kind, elems) | acc]
166+
format_each_fragment(rest, new_acc)
133167
end
134168

135-
defp script_list([elem | rest1], [elem | rest2], keyword?, acc) do
136-
elem_diff = [eq: format_list_elem(elem, keyword?)]
137-
script_list(rest1, rest2, keyword?, [[eq: ", "] ++ elem_diff | acc])
169+
defp format_fragment(kind, elems) do
170+
formatter = fn {key, val} ->
171+
format_key_value(key, val, true)
172+
end
173+
{kind, Enum.map_join(elems, ", ", formatter)}
138174
end
139175

140-
defp script_list([{key1, val1} | rest1], [{key2, val2} | rest2], true, acc) do
141-
key_diff =
142-
if key1 != key2 do
143-
script_string(Atom.to_string(key1), Atom.to_string(key2))
144-
else
145-
[eq: Atom.to_string(key1)]
146-
end
147-
value_diff = script_inner(val1, val2)
148-
elem_diff = [[key_diff, {:eq, ": "}], value_diff]
149-
script_list(rest1, rest2, true, [[eq: ", "] ++ elem_diff | acc])
176+
defp find_script(envelope, max, _paths) when envelope > max do
177+
nil
178+
end
179+
180+
defp find_script(envelope, max, paths) do
181+
case each_diagonal(-envelope, envelope, paths, []) do
182+
{:done, edits} ->
183+
compact_reverse(edits, [])
184+
{:next, paths} -> find_script(envelope + 1, max, paths)
185+
end
186+
end
187+
188+
defp compact_reverse([], acc),
189+
do: acc
190+
191+
defp compact_reverse([{:diff, _} = fragment | rest], acc),
192+
do: compact_reverse(rest, [fragment | acc])
193+
194+
defp compact_reverse([{kind, char} | rest], [{kind, chars} | acc]),
195+
do: compact_reverse(rest, [{kind, [char | chars]} | acc])
196+
197+
defp compact_reverse([{kind, char} | rest], acc),
198+
do: compact_reverse(rest, [{kind, [char]} | acc])
199+
200+
defp each_diagonal(diag, limit, _paths, next_paths) when diag > limit do
201+
{:next, Enum.reverse(next_paths)}
202+
end
203+
204+
defp each_diagonal(diag, limit, paths, next_paths) do
205+
{path, rest} = proceed_path(diag, limit, paths)
206+
with {:cont, path} <- follow_snake(path) do
207+
each_diagonal(diag + 2, limit, rest, [path | next_paths])
208+
end
209+
end
210+
211+
defp proceed_path(0, 0, [path]), do: {path, []}
212+
213+
defp proceed_path(diag, limit, [path | _] = paths) when diag == -limit do
214+
{move_down(path), paths}
215+
end
216+
217+
defp proceed_path(diag, limit, [path]) when diag == limit do
218+
{move_right(path), []}
219+
end
220+
221+
defp proceed_path(_diag, _limit, [path1, path2 | rest]) do
222+
if elem(path1, 1) > elem(path2, 1) do
223+
{move_right(path1), [path2 | rest]}
224+
else
225+
{move_down(path2), [path2 | rest]}
226+
end
150227
end
151228

152-
defp script_list([elem1 | rest1], [elem2 | rest2], false, acc) do
229+
defp script_keyword_inner({key, val1}, {key, val2}),
230+
do: [{:eq, format_key(key, true)}, script_inner(val1, val2)]
231+
232+
defp script_keyword_inner(_pair1, _pair2),
233+
do: nil
234+
235+
defp move_right({x, x, [elem1 | rest1] = list1, [elem2 | rest2], edits}) do
236+
if result = script_keyword_inner(elem1, elem2) do
237+
{x + 1, x + 1, rest1, rest2, [{:diff, result} | edits]}
238+
else
239+
{x + 1, x, list1, rest2, [{:ins, elem2} | edits]}
240+
end
241+
end
242+
243+
defp move_right({x, y, list1, [elem | rest], edits}) do
244+
{x + 1, y, list1, rest, [{:ins, elem} | edits]}
245+
end
246+
247+
defp move_right({x, y, list1, [], edits}) do
248+
{x + 1, y, list1, [], edits}
249+
end
250+
251+
defp move_down({x, x, [elem1 | rest1], [elem2 | rest2] = list2, edits}) do
252+
if result = script_keyword_inner(elem1, elem2) do
253+
{x + 1, x + 1, rest1, rest2, [{:diff, result} | edits]}
254+
else
255+
{x, x + 1, rest1, list2, [{:del, elem1} | edits]}
256+
end
257+
end
258+
259+
defp move_down({x, y, [elem | rest], list2, edits}) do
260+
{x, y + 1, rest, list2, [{:del, elem} | edits]}
261+
end
262+
263+
defp move_down({x, y, [], list2, edits}) do
264+
{x, y + 1, [], list2, edits}
265+
end
266+
267+
defp follow_snake({x, y, [elem | rest1], [elem | rest2], edits}) do
268+
follow_snake({x + 1, y + 1, rest1, rest2, [{:eq, elem} | edits]})
269+
end
270+
271+
defp follow_snake({_x, _y, [], [], edits}) do
272+
{:done, edits}
273+
end
274+
275+
defp follow_snake(path) do
276+
{:cont, path}
277+
end
278+
279+
defp script_list([], [], acc) do
280+
[[_ | elem_diff] | rest] = Enum.reverse(acc)
281+
[{:eq, "["}, [elem_diff | rest], {:eq, "]"}]
282+
end
283+
284+
defp script_list([], [elem | rest], acc) do
285+
elem_diff = [ins: inspect(elem)]
286+
script_list([], rest, [[ins: ", "] ++ elem_diff | acc])
287+
end
288+
289+
defp script_list([elem | rest], [], acc) do
290+
elem_diff = [del: inspect(elem)]
291+
script_list(rest, [], [[del: ", "] ++ elem_diff | acc])
292+
end
293+
294+
defp script_list([elem | rest1], [elem | rest2], acc) do
295+
elem_diff = [eq: inspect(elem)]
296+
script_list(rest1, rest2, [[eq: ", "] ++ elem_diff | acc])
297+
end
298+
299+
defp script_list([elem1 | rest1], [elem2 | rest2], acc) do
153300
elem_diff = script_inner(elem1, elem2)
154-
script_list(rest1, rest2, false, [[eq: ", "] ++ elem_diff | acc])
301+
script_list(rest1, rest2, [[eq: ", "] ++ elem_diff | acc])
155302
end
156303

157-
defp script_list(last, [elem | rest], keyword?, acc) do
304+
defp script_list(last, [elem | rest], acc) do
158305
joiner_diff = [del: " |", ins: ",", eq: " "]
159306
elem_diff = script_inner(last, elem)
160307
new_acc = [joiner_diff ++ elem_diff | acc]
161-
script_list([], rest, keyword?, new_acc)
308+
script_list([], rest, new_acc)
162309
end
163310

164-
defp script_list([elem | rest], last, keyword?, acc) do
311+
defp script_list([elem | rest], last, acc) do
165312
joiner_diff = [del: ",", ins: " |", eq: " "]
166313
elem_diff = script_inner(elem, last)
167314
new_acc = [joiner_diff ++ elem_diff | acc]
168-
script_list(rest, [], keyword?, new_acc)
315+
script_list(rest, [], new_acc)
169316
end
170317

171-
defp script_list(last1, last2, keyword?, acc) do
318+
defp script_list(last1, last2, acc) do
172319
elem_diff =
173320
cond do
174321
last1 == [] ->
@@ -178,14 +325,9 @@ defmodule ExUnit.Diff do
178325
true ->
179326
[eq: " | "] ++ script_inner(last1, last2)
180327
end
181-
script_list([], [], keyword?, [elem_diff | acc])
328+
script_list([], [], [elem_diff | acc])
182329
end
183330

184-
defp format_list_elem(elem, false),
185-
do: inspect(elem)
186-
defp format_list_elem({key, val}, true),
187-
do: format_key_value(key, val, true)
188-
189331
defp script_tuple({_tuple1, -1}, {_tuple2, -1}, acc) do
190332
[[_ | elem_diff] | rest] = acc
191333
[{:eq, "{"}, [elem_diff | rest], {:eq, "}"}]

0 commit comments

Comments
 (0)