Skip to content

Commit 6369b5a

Browse files
committed
Add support for diff lines
The API now sends back diffs on a line by line basis with spacers such that left and right side diffs can be aligned. Update the JSON decoders to support this new format and render the UI appropriately. Also add line numbers and change indicators in a diff gutter as well as line spacer patterns.
1 parent d418c53 commit 6369b5a

9 files changed

Lines changed: 4437 additions & 22540 deletions

File tree

api-stubs/branch-diff-from-transcripts.json

Lines changed: 2599 additions & 0 deletions
Large diffs are not rendered by default.

api-stubs/branch-diff-with-multiline-string.json

Lines changed: 1487 additions & 0 deletions
Large diffs are not rendered by default.

api-stubs/branch-diff.json

Lines changed: 0 additions & 20990 deletions
This file was deleted.

src/UnisonShare/DefinitionDiff.elm

Lines changed: 156 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import Code.Hash as Hash exposing (Hash)
44
import Code.Syntax.SyntaxConfig exposing (SyntaxConfig)
55
import Code.Syntax.SyntaxSegment as SyntaxSegment exposing (SyntaxSegment)
66
import Html exposing (Html, code, div, header, pre, span, text)
7-
import Html.Attributes exposing (class)
7+
import Html.Attributes exposing (class, style)
88
import Json.Decode as Decode
99
import Json.Decode.Extra exposing (when)
1010
import Json.Decode.Pipeline exposing (required, requiredAt)
11-
import Lib.Decode.Helpers exposing (nonEmptyList)
11+
import Lib.Decode.Helpers exposing (nonEmptyList, whenKindIs)
12+
import List.Extra as ListE
1213
import List.Nonempty as NEL
1314
import UI.Tooltip as Tooltip
1415

@@ -18,23 +19,38 @@ type alias DiffSyntaxSegments =
1819

1920

2021
type DiffSegment
21-
= Old DiffSyntaxSegments
22-
| New DiffSyntaxSegments
23-
| Both DiffSyntaxSegments
22+
= Both DiffSyntaxSegments
23+
| OneSided DiffSyntaxSegments
2424
| AnnotationChange { segment : SyntaxSegment, fromHash : Hash, toHash : Hash }
2525
| SegmentChange { from : SyntaxSegment, to : SyntaxSegment }
2626

2727

28+
type DiffLine
29+
= ChangedLine (List DiffSegment)
30+
| UnchangedLine (List DiffSegment)
31+
-- Spacer includes numLines such that we can avoid a jagged background
32+
-- pattern when it spans over multiple lines by making it 1 tall DOM
33+
-- element instead of small 1 line height elements
34+
| Spacer { numLines : Int }
35+
36+
2837
type alias DiffDetails =
2938
{ type_ : DefinitionType
30-
, newDef : DiffSyntaxSegments
31-
, oldDef : DiffSyntaxSegments
39+
, left : List DiffLine
40+
, right : List DiffLine
41+
}
42+
43+
44+
type alias MismatchedDetails =
45+
{ type_ : DefinitionType
46+
, left : DiffSyntaxSegments
47+
, right : DiffSyntaxSegments
3248
}
3349

3450

3551
type DefinitionDiff
36-
= Diff DiffDetails (NEL.Nonempty DiffSegment)
37-
| Mismatched DiffDetails
52+
= Diff DiffDetails
53+
| Mismatched MismatchedDetails
3854

3955

4056

@@ -75,10 +91,8 @@ viewTooltip content =
7591
|> Tooltip.withArrow Tooltip.Start
7692

7793

78-
{-| View diff segments from the perspective of viewing the old definition
79-
-}
80-
viewOldDiffSegment : SyntaxConfig msg -> DiffSegment -> List (Html msg)
81-
viewOldDiffSegment syntaxConfig segment =
94+
viewDiffSegment : SyntaxConfig msg -> DiffSegment -> List (Html msg)
95+
viewDiffSegment syntaxConfig segment =
8296
let
8397
viewSegment =
8498
SyntaxSegment.view syntaxConfig
@@ -87,42 +101,11 @@ viewOldDiffSegment syntaxConfig segment =
87101
viewSegments syntaxConfig className
88102
in
89103
case segment of
90-
Old segments ->
91-
viewSegments_ "diff-segment old" segments
92-
93104
Both segments ->
94105
viewSegments_ "diff-segment both" segments
95106

96-
New _ ->
97-
[]
98-
99-
AnnotationChange change ->
100-
[ viewSegment change.segment ]
101-
102-
SegmentChange { from } ->
103-
[ viewSegment from ]
104-
105-
106-
{-| View diff segments from the perspective of viewing the new definition
107-
-}
108-
viewNewDiffSegment : SyntaxConfig msg -> DiffSegment -> List (Html msg)
109-
viewNewDiffSegment syntaxConfig segment =
110-
let
111-
viewSegment =
112-
SyntaxSegment.view syntaxConfig
113-
114-
viewSegments_ className =
115-
viewSegments syntaxConfig className
116-
in
117-
case segment of
118-
Old _ ->
119-
[]
120-
121-
New segments ->
122-
viewSegments_ "diff-segment new" segments
123-
124-
Both segments ->
125-
viewSegments_ "diff-segment both" segments
107+
OneSided segments ->
108+
viewSegments_ "diff-segment one-sided" segments
126109

127110
AnnotationChange change ->
128111
[ viewTooltip
@@ -152,41 +135,110 @@ viewNewDiffSegment syntaxConfig segment =
152135
]
153136

154137

155-
viewDiff : (Bool -> SyntaxConfig msg) -> NEL.Nonempty DiffSegment -> Html msg
156-
viewDiff toSyntaxConfig segments =
138+
viewDiffLine : (DiffSegment -> List (Html msg)) -> String -> Int -> ( Maybe Int, DiffLine ) -> Html msg
139+
viewDiffLine viewSeg changeIndicator gutterWidth ( ln, line ) =
140+
let
141+
gutter indicator =
142+
span [ class "gutter" ]
143+
[ span [ class "line-number" ]
144+
[ text
145+
(String.padLeft
146+
gutterWidth
147+
' '
148+
(ln |> Maybe.map String.fromInt |> Maybe.withDefault "")
149+
)
150+
]
151+
, text " "
152+
, span [ class "change-indicator" ] [ text indicator ]
153+
, text " "
154+
]
155+
in
156+
case line of
157+
ChangedLine segments ->
158+
div [ class "diff-line changed-line" ]
159+
[ gutter changeIndicator
160+
, span [] (List.concatMap viewSeg segments)
161+
]
162+
163+
UnchangedLine segments ->
164+
div [ class "diff-line unchanged-line" ]
165+
[ gutter " "
166+
, span [] (List.concatMap viewSeg segments)
167+
]
168+
169+
Spacer { numLines } ->
170+
div
171+
[ class "diff-line spacer-line"
172+
, style "height" ("calc(var(--diff-line-height) * " ++ String.fromInt numLines ++ ")")
173+
]
174+
[]
175+
176+
177+
viewDiff : (Bool -> SyntaxConfig msg) -> DiffDetails -> Html msg
178+
viewDiff toSyntaxConfig { left, right } =
157179
let
158-
old =
159-
segments
160-
|> NEL.toList
161-
|> List.concatMap (viewOldDiffSegment (toSyntaxConfig False))
162-
163-
new =
164-
segments
165-
|> NEL.toList
166-
|> List.concatMap (viewNewDiffSegment (toSyntaxConfig True))
180+
toGutterWidth len =
181+
String.length (String.fromInt len)
182+
183+
toViewDiffSegment isNew =
184+
viewDiffSegment (toSyntaxConfig isNew)
185+
186+
withLineNumbers diffLine ( i, lines ) =
187+
case diffLine of
188+
ChangedLine _ ->
189+
( i + 1, lines ++ [ ( Just (i + 1), diffLine ) ] )
190+
191+
UnchangedLine _ ->
192+
( i + 1, lines ++ [ ( Just (i + 1), diffLine ) ] )
193+
194+
Spacer _ ->
195+
case ListE.unconsLast lines of
196+
Just ( ( _, Spacer { numLines } ), lines_ ) ->
197+
( i, lines_ ++ [ ( Nothing, Spacer { numLines = numLines + 1 } ) ] )
198+
199+
_ ->
200+
( i, lines ++ [ ( Nothing, diffLine ) ] )
201+
202+
viewLeftDiffLine =
203+
viewDiffLine (toViewDiffSegment False) "-" (toGutterWidth (List.length left))
204+
205+
viewRightDiffLine =
206+
viewDiffLine (toViewDiffSegment False) "-" (toGutterWidth (List.length right))
207+
208+
before =
209+
left
210+
|> List.foldl withLineNumbers ( 0, [] )
211+
|> Tuple.second
212+
|> List.map viewLeftDiffLine
213+
214+
after =
215+
right
216+
|> List.foldl withLineNumbers ( 0, [] )
217+
|> Tuple.second
218+
|> List.map viewRightDiffLine
167219
in
168220
div [ class "diff-side-by-side" ]
169-
[ pre [ class "monochrome diff-side old" ]
170-
[ header [ class "diff-old-header" ] [ text "Before" ]
171-
, code [] old
221+
[ pre [ class "monochrome diff-side left" ]
222+
[ header [ class "diff-left-header" ] [ text "Before" ]
223+
, code [] before
172224
]
173-
, pre [ class "monochrome diff-side new" ]
174-
[ header [ class "diff-new-header" ] [ text "After" ]
175-
, code [] new
225+
, pre [ class "monochrome diff-side right" ]
226+
[ header [ class "diff-right-header" ] [ text "After" ]
227+
, code [] after
176228
]
177229
]
178230

179231

180232
view : (Bool -> SyntaxConfig msg) -> DefinitionDiff -> Html msg
181233
view toSyntaxConfig defDiff =
182234
case defDiff of
183-
Diff _ diff ->
184-
div [] [ viewDiff toSyntaxConfig diff ]
235+
Diff details ->
236+
div [] [ viewDiff toSyntaxConfig details ]
185237

186-
Mismatched { oldDef, newDef } ->
238+
Mismatched { left, right } ->
187239
div [ class "diff-side-by-side" ]
188-
[ pre [ class "monochrome diff-side" ] [ code [] (viewSegments (toSyntaxConfig False) "mismatched old" oldDef) ]
189-
, pre [ class "monochrome diff-side" ] [ code [] (viewSegments (toSyntaxConfig True) "mismatched new" newDef) ]
240+
[ pre [ class "monochrome diff-side" ] [ code [] (viewSegments (toSyntaxConfig False) "mismatched old" left) ]
241+
, pre [ class "monochrome diff-side" ] [ code [] (viewSegments (toSyntaxConfig True) "mismatched new" right) ]
190242
]
191243

192244

@@ -199,24 +251,25 @@ decodeDiffSyntaxSegments =
199251
nonEmptyList SyntaxSegment.decode
200252

201253

254+
decodeSingleDiffSyntaxSegment : Decode.Decoder DiffSyntaxSegments
255+
decodeSingleDiffSyntaxSegment =
256+
Decode.map NEL.fromElement SyntaxSegment.decode
257+
258+
202259
decodeSegment : Decode.Decoder DiffSegment
203260
decodeSegment =
204261
let
205262
decodeDiffTag =
206263
Decode.field "diffTag" Decode.string
207264

208-
decodeOld =
209-
Decode.succeed Old
210-
|> required "elements" decodeDiffSyntaxSegments
211-
212-
decodeNew =
213-
Decode.succeed New
214-
|> required "elements" decodeDiffSyntaxSegments
215-
216265
decodeBoth =
217266
Decode.succeed Both
218267
|> required "elements" decodeDiffSyntaxSegments
219268

269+
decodeOneSided =
270+
Decode.succeed OneSided
271+
|> required "elements" decodeDiffSyntaxSegments
272+
220273
mkAnnotationChange segment fromHash toHash =
221274
AnnotationChange
222275
{ segment = segment
@@ -241,61 +294,62 @@ decodeSegment =
241294
(SyntaxSegment.decode_ { segmentField = "toSegment", annotationField = "annotation" })
242295
in
243296
Decode.oneOf
244-
[ when decodeDiffTag ((==) "old") decodeOld
245-
, when decodeDiffTag ((==) "new") decodeNew
246-
, when decodeDiffTag ((==) "both") decodeBoth
297+
[ when decodeDiffTag ((==) "both") decodeBoth
298+
, when decodeDiffTag ((==) "oneSided") decodeOneSided
247299
, when decodeDiffTag ((==) "annotationChange") decodeAnnotationChange
248300
, when decodeDiffTag ((==) "segmentChange") decodeSegmentChange
249301
]
250302

251303

304+
decodeDiffLine : Decode.Decoder DiffLine
305+
decodeDiffLine =
306+
Decode.oneOf
307+
[ whenKindIs "changed" (Decode.map ChangedLine (Decode.field "value" (Decode.list decodeSegment)))
308+
, whenKindIs "unchanged" (Decode.map UnchangedLine (Decode.field "value" (Decode.list decodeSegment)))
309+
310+
-- The spacer numLines will be flatten later on
311+
-- TODO: we should probably do the flattening and add line numbers during parsing...
312+
, whenKindIs "spacer" (Decode.succeed (Spacer { numLines = 1 }))
313+
]
314+
315+
252316
decodeDiff : DefinitionType -> Decode.Decoder DefinitionDiff
253317
decodeDiff definitionType =
254318
let
255-
( oldKey, newKey, definitionKey ) =
256-
case definitionType of
257-
Term ->
258-
( "left", "right", "termDefinition" )
259-
260-
Type ->
261-
( "left", "right", "typeDefinition" )
262-
263-
mkDiff diff oldDef newDef =
319+
mkDiff left right =
264320
Diff
265321
{ type_ = definitionType
266-
, oldDef = oldDef
267-
, newDef = newDef
322+
, left = left
323+
, right = right
268324
}
269-
diff
270325
in
271326
Decode.succeed mkDiff
272-
|> requiredAt [ "diff", "diff", "contents" ] (nonEmptyList decodeSegment)
273-
|> requiredAt [ oldKey, definitionKey, "contents" ] decodeDiffSyntaxSegments
274-
|> requiredAt [ newKey, definitionKey, "contents" ] decodeDiffSyntaxSegments
327+
|> requiredAt [ "diff", "diff", "contents", "left" ] (Decode.list decodeDiffLine)
328+
|> requiredAt [ "diff", "diff", "contents", "right" ] (Decode.list decodeDiffLine)
275329

276330

277331
decodeMismatched : DefinitionType -> Decode.Decoder DefinitionDiff
278332
decodeMismatched definitionType =
279333
let
280-
( oldKey, newKey, definitionKey ) =
334+
definitionKey =
281335
case definitionType of
282336
Term ->
283-
( "left", "right", "termDefinition" )
337+
"termDefinition"
284338

285339
Type ->
286-
( "left", "right", "typeDefinition" )
340+
"typeDefinition"
287341

288-
mkMismatched oldDef newDef =
342+
mkMismatched left right =
289343
Mismatched
290344
{ type_ = definitionType
291-
, oldDef = oldDef
292-
, newDef = newDef
345+
, left = left
346+
, right = right
293347
}
294348
in
295349
Decode.succeed mkMismatched
296350
-- TODO: what about builtins?
297-
|> requiredAt [ oldKey, definitionKey, "contents" ] decodeDiffSyntaxSegments
298-
|> requiredAt [ newKey, definitionKey, "contents" ] decodeDiffSyntaxSegments
351+
|> requiredAt [ "left", definitionKey, "contents" ] decodeDiffSyntaxSegments
352+
|> requiredAt [ "right", definitionKey, "contents" ] decodeDiffSyntaxSegments
299353

300354

301355
decode : DefinitionType -> Decode.Decoder DefinitionDiff

0 commit comments

Comments
 (0)