addons/shared/__tests__/diff.test.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {Block} from '../diff';
b69ab319
b69ab3110import {
b69ab3111 collapseContextBlocks,
b69ab3112 diffBlocks,
b69ab3113 mergeBlocks,
b69ab3114 readableDiffBlocks,
b69ab3115 splitLines,
b69ab3116} from '../diff';
b69ab3117
b69ab3118describe('diffBlocks', () => {
b69ab3119 it('returns a "=" block for unchanged content', () => {
b69ab3120 const lines = splitLines('a\nb\nc\nd\ne\n');
b69ab3121 expect(diffBlocks(lines, lines)).toMatchObject([['=', [0, 5, 0, 5]]]);
b69ab3122 });
b69ab3123
b69ab3124 it('returns a "!" block for totally different contents', () => {
b69ab3125 const aLines = splitLines('x\ny\n');
b69ab3126 const bLines = splitLines('a\nb\nc\n');
b69ab3127 expect(diffBlocks(aLines, bLines)).toMatchObject([['!', [0, 2, 0, 3]]]);
b69ab3128 });
b69ab3129
b69ab3130 it('returns "= ! =" blocks when a line was changed in the middle', () => {
b69ab3131 const aLines = splitLines('a\nb\nc\nd\ne\n');
b69ab3132 const bLines = splitLines('a\nb\nc\nd1\nd2\ne\n');
b69ab3133 expect(diffBlocks(aLines, bLines)).toMatchObject([
b69ab3134 ['=', [0, 3, 0, 3]],
b69ab3135 ['!', [3, 4, 3, 5]],
b69ab3136 ['=', [4, 5, 5, 6]],
b69ab3137 ]);
b69ab3138 });
b69ab3139
b69ab3140 it('matches mdiff.blocks (known good diff algorithm), excluding empty blocks', () => {
b69ab3141 // Test cases are generated by:
b69ab3142 //
b69ab3143 // ```
b69ab3144 // #!sl dbsh
b69ab3145 // import json
b69ab3146 // allblocks = e.mdiff.allblocks
b69ab3147 // cases = []
b69ab3148 // for bits in range(16):
b69ab3149 // a = ['a\n', 'b\n', 'c\n', 'd\n']
b69ab3150 // b = [bits & (1 << i) and c.upper() or c for i, c in enumerate(a)]
b69ab3151 // a = ''.join(a)
b69ab3152 // b = ''.join(b)
b69ab3153 // blocks = [[s, l] for l, s in allblocks(a, b) if l[0] < l[1] or l[2] < l[3]] # skip empty blocks
b69ab3154 // cases.append(json.dumps(blocks).replace(' ', ''))
b69ab3155 // print(' '.join(cases))
b69ab3156 // ```
b69ab3157 //
b69ab3158 // String is used to prettier from wrapping lines.
b69ab3159 const testCaseStr =
b69ab3160 '[["=",[0,4,0,4]]] [["!",[0,1,0,1]],["=",[1,4,1,4]]] [["=",[0,1,0,1]],["!",[1,2,1,2]],["=",[2,4,2,4]]] [["!",[0,2,0,2]],["=",[2,4,2,4]]] [["=",[0,2,0,2]],["!",[2,3,2,3]],["=",[3,4,3,4]]] [["!",[0,1,0,1]],["=",[1,2,1,2]],["!",[2,3,2,3]],["=",[3,4,3,4]]] [["=",[0,1,0,1]],["!",[1,3,1,3]],["=",[3,4,3,4]]] [["!",[0,3,0,3]],["=",[3,4,3,4]]] [["=",[0,3,0,3]],["!",[3,4,3,4]]] [["!",[0,1,0,1]],["=",[1,3,1,3]],["!",[3,4,3,4]]] [["=",[0,1,0,1]],["!",[1,2,1,2]],["=",[2,3,2,3]],["!",[3,4,3,4]]] [["!",[0,2,0,2]],["=",[2,3,2,3]],["!",[3,4,3,4]]] [["=",[0,2,0,2]],["!",[2,4,2,4]]] [["!",[0,1,0,1]],["=",[1,2,1,2]],["!",[2,4,2,4]]] [["=",[0,1,0,1]],["!",[1,4,1,4]]] [["!",[0,4,0,4]]]';
b69ab3161 const testCases: Array<Block[]> = testCaseStr.split(' ').map(s => JSON.parse(s));
b69ab3162 testCases.forEach((expected, bits) => {
b69ab3163 // eslint-disable-next-line no-bitwise
b69ab3164 const hasBit = (i: number): boolean => (bits & (1 << i)) > 0;
b69ab3165 const a = ['a\n', 'b\n', 'c\n', 'd\n'];
b69ab3166 const b = a.map((s, i) => (hasBit(i) ? s.toUpperCase() : s));
b69ab3167 const actual = diffBlocks(a, b);
b69ab3168 expect(actual).toEqual(expected);
b69ab3169 });
b69ab3170 });
b69ab3171});
b69ab3172
b69ab3173describe('readableDiffBlocks', () => {
b69ab3174 it('prefers changing insignificant lines to insignificant lines 1', () => {
b69ab3175 // https://stackoverflow.com/questions/40550751/unexpected-result-in-git-diff
b69ab3176 const a = `sub _process_message {
b69ab3177 my ($self, $message) = @_;
b69ab3178
b69ab3179 my $method = ref($message) eq 'HASH' ? $message->{method} : undef;
b69ab3180
b69ab3181 return $self->send_error(ERROR_REQUEST_INVALID)
b69ab3182 unless defined($method);
b69ab3183`;
b69ab3184 const b = `sub _process_message {
b69ab3185 my ($self, $message) = @_;
b69ab3186
b69ab3187 my $time = [ gettimeofday ];
b69ab3188
b69ab3189 my $method = ref($message) eq 'HASH' ? $message->{method} : undef;
b69ab3190 return $self->send_error(ERROR_REQUEST_INVALID)
b69ab3191 unless defined($method);
b69ab3192`;
b69ab3193 // Does not produce this:
b69ab3194 // sub _process_message {
b69ab3195 // my ($self, $message) = @_;
b69ab3196 //
b69ab3197 // - my $method = ref($message) eq 'HASH' ? $message->{method} : undef;
b69ab3198 // + my $time = [ gettimeofday ];
b69ab3199 //
b69ab31100 // + my $method = ref($message) eq 'HASH' ? $message->{method} : undef;
b69ab31101 // return $self->send_error(ERROR_REQUEST_INVALID)
b69ab31102 // unless defined($method);
b69ab31103 expect(renderDiff(a, b, readableDiffBlocks)).toMatchInlineSnapshot(`
b69ab31104 " sub _process_message {
b69ab31105 my ($self, $message) = @_;
b69ab31106
b69ab31107 + my $time = [ gettimeofday ];
b69ab31108 +
b69ab31109 my $method = ref($message) eq 'HASH' ? $message->{method} : undef;
b69ab31110 -
b69ab31111 return $self->send_error(ERROR_REQUEST_INVALID)
b69ab31112 unless defined($method);
b69ab31113 "
b69ab31114 `);
b69ab31115 });
b69ab31116
b69ab31117 it('prefers changing insignificant lines to insignificant lines 2', () => {
b69ab31118 // https://gitlab.com/jssfr/diffsample/-/compare/bob...alice
b69ab31119 const a = `void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
b69ab31120{
b69ab31121 if (!Chunk_bounds_check(src, src_start, n)) return;
b69ab31122 if (!Chunk_bounds_check(dst, dst_start, n)) return;
b69ab31123
b69ab31124 memcpy(dst->data + dst_start, src->data + src_start, n);
b69ab31125}
b69ab31126
b69ab31127int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
b69ab31128{
b69ab31129 if (chunk == NULL) return 0;
b69ab31130
b69ab31131 return start <= chunk->length && n <= chunk->length - start;
b69ab31132}
b69ab31133`;
b69ab31134 const b = `int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
b69ab31135{
b69ab31136 if (chunk == NULL) return 0;
b69ab31137
b69ab31138 return start <= chunk->length && n <= chunk->length - start;
b69ab31139}
b69ab31140
b69ab31141void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
b69ab31142{
b69ab31143 if (!Chunk_bounds_check(src, src_start, n)) return;
b69ab31144 if (!Chunk_bounds_check(dst, dst_start, n)) return;
b69ab31145
b69ab31146 memcpy(dst->data + dst_start, src->data + src_start, n);
b69ab31147}
b69ab31148`;
b69ab31149 // Does not produce this:
b69ab31150 // -void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
b69ab31151 // +int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
b69ab31152 // {
b69ab31153 // - if (!Chunk_bounds_check(src, src_start, n)) return;
b69ab31154 // - if (!Chunk_bounds_check(dst, dst_start, n)) return;
b69ab31155 // + if (chunk == NULL) return 0;
b69ab31156 //
b69ab31157 // - // copy the bytes
b69ab31158 // - memcpy(dst->data + dst_start, src->data + src_start, n);
b69ab31159 // + return start <= chunk->length && n <= chunk->length - start;
b69ab31160 // }
b69ab31161 //
b69ab31162 // -int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
b69ab31163 // +void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
b69ab31164 // {
b69ab31165 // - if (chunk == NULL) return 0;
b69ab31166 // + if (!Chunk_bounds_check(src, src_start, n)) return;
b69ab31167 // + if (!Chunk_bounds_check(dst, dst_start, n)) return;
b69ab31168 //
b69ab31169 // - return start <= chunk->length && n <= chunk->length - start;
b69ab31170 // + memcpy(dst->data + dst_start, src->data + src_start, n);
b69ab31171 // }
b69ab31172 expect(renderDiff(a, b, readableDiffBlocks)).toMatchInlineSnapshot(`
b69ab31173 "+int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
b69ab31174 +{
b69ab31175 + if (chunk == NULL) return 0;
b69ab31176 +
b69ab31177 + return start <= chunk->length && n <= chunk->length - start;
b69ab31178 +}
b69ab31179 +
b69ab31180 void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
b69ab31181 {
b69ab31182 if (!Chunk_bounds_check(src, src_start, n)) return;
b69ab31183 if (!Chunk_bounds_check(dst, dst_start, n)) return;
b69ab31184
b69ab31185 memcpy(dst->data + dst_start, src->data + src_start, n);
b69ab31186 }
b69ab31187 -
b69ab31188 -int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
b69ab31189 -{
b69ab31190 - if (chunk == NULL) return 0;
b69ab31191 -
b69ab31192 - return start <= chunk->length && n <= chunk->length - start;
b69ab31193 -}
b69ab31194 "
b69ab31195 `);
b69ab31196 expect(renderDiff(b, a, readableDiffBlocks)).toMatchInlineSnapshot(`
b69ab31197 "-int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
b69ab31198 -{
b69ab31199 - if (chunk == NULL) return 0;
b69ab31200 -
b69ab31201 - return start <= chunk->length && n <= chunk->length - start;
b69ab31202 -}
b69ab31203 -
b69ab31204 void Chunk_copy(Chunk *src, size_t src_start, Chunk *dst, size_t dst_start, size_t n)
b69ab31205 {
b69ab31206 if (!Chunk_bounds_check(src, src_start, n)) return;
b69ab31207 if (!Chunk_bounds_check(dst, dst_start, n)) return;
b69ab31208
b69ab31209 memcpy(dst->data + dst_start, src->data + src_start, n);
b69ab31210 }
b69ab31211 +
b69ab31212 +int Chunk_bounds_check(Chunk *chunk, size_t start, size_t n)
b69ab31213 +{
b69ab31214 + if (chunk == NULL) return 0;
b69ab31215 +
b69ab31216 + return start <= chunk->length && n <= chunk->length - start;
b69ab31217 +}
b69ab31218 "
b69ab31219 `);
b69ab31220 });
b69ab31221
b69ab31222 it('sometimes produces non-minimal diff', () => {
b69ab31223 const a = `b
b69ab31224{
b69ab31225 b1
b69ab31226}
b69ab31227
b69ab31228a
b69ab31229{
b69ab31230 a1
b69ab31231}
b69ab31232`;
b69ab31233 const b = `a
b69ab31234{
b69ab31235 a1
b69ab31236}
b69ab31237
b69ab31238b
b69ab31239{
b69ab31240 b1
b69ab31241}
b69ab31242`;
b69ab31243 // The regular diff produces the minimal diff with 8 changed lines.
b69ab31244 expect(renderDiff(a, b, diffBlocks)).toMatchInlineSnapshot(`
b69ab31245 "-b
b69ab31246 +a
b69ab31247 {
b69ab31248 - b1
b69ab31249 + a1
b69ab31250 }
b69ab31251
b69ab31252 -a
b69ab31253 +b
b69ab31254 {
b69ab31255 - a1
b69ab31256 + b1
b69ab31257 }
b69ab31258 "
b69ab31259 `);
b69ab31260 // The "readable" diff has 10 changed lines, but is easier to read by a human.
b69ab31261 expect(renderDiff(a, b, readableDiffBlocks)).toMatchInlineSnapshot(`
b69ab31262 "-b
b69ab31263 -{
b69ab31264 - b1
b69ab31265 -}
b69ab31266 -
b69ab31267 a
b69ab31268 {
b69ab31269 a1
b69ab31270 }
b69ab31271 +
b69ab31272 +b
b69ab31273 +{
b69ab31274 + b1
b69ab31275 +}
b69ab31276 "
b69ab31277 `);
b69ab31278 });
b69ab31279
b69ab31280 it('avoids the pitfall of the patience diff flaw', () => {
b69ab31281 // Textbook patience diff will match the unique line "x" unconditionally,
b69ab31282 // and produces suboptimal result deleting and inserting multiple
b69ab31283 // insignificant lines. Our diff uses a simple heuristic to avoid that.
b69ab31284 const a = `{
b69ab31285{
b69ab31286{
b69ab31287x
b69ab31288`;
b69ab31289 const b = `x
b69ab31290{
b69ab31291{
b69ab31292{
b69ab31293`;
b69ab31294
b69ab31295 expect(renderDiff(a, b, readableDiffBlocks)).toMatchInlineSnapshot(`
b69ab31296 "+x
b69ab31297 {
b69ab31298 {
b69ab31299 {
b69ab31300 -x
b69ab31301 "
b69ab31302 `);
b69ab31303 });
b69ab31304});
b69ab31305
b69ab31306describe('collapseContextBlocks', () => {
b69ab31307 it('collapses everything in a "=" block', () => {
b69ab31308 expect(collapseContextBlocks([['=', [0, 5, 0, 5]]], () => false)).toMatchObject([
b69ab31309 ['~', [0, 5, 0, 5]],
b69ab31310 ]);
b69ab31311 });
b69ab31312
b69ab31313 it('collapses the top part of a "=" block', () => {
b69ab31314 expect(
b69ab31315 collapseContextBlocks(
b69ab31316 [
b69ab31317 ['=', [0, 5, 0, 5]],
b69ab31318 ['!', [5, 6, 5, 7]],
b69ab31319 ],
b69ab31320 () => false,
b69ab31321 ),
b69ab31322 ).toMatchObject([
b69ab31323 ['~', [0, 2, 0, 2]],
b69ab31324 ['=', [2, 5, 2, 5]],
b69ab31325 ['!', [5, 6, 5, 7]],
b69ab31326 ]);
b69ab31327 });
b69ab31328
b69ab31329 it('collapses the bottom part of a "=" block', () => {
b69ab31330 expect(
b69ab31331 collapseContextBlocks(
b69ab31332 [
b69ab31333 ['!', [0, 2, 0, 3]],
b69ab31334 ['=', [2, 8, 3, 9]],
b69ab31335 ],
b69ab31336 () => false,
b69ab31337 ),
b69ab31338 ).toMatchObject([
b69ab31339 ['!', [0, 2, 0, 3]],
b69ab31340 ['=', [2, 5, 3, 6]],
b69ab31341 ['~', [5, 8, 6, 9]],
b69ab31342 ]);
b69ab31343 });
b69ab31344
b69ab31345 it('splits a "=" block in 3 blocks on demand', () => {
b69ab31346 expect(
b69ab31347 collapseContextBlocks(
b69ab31348 [
b69ab31349 ['!', [0, 1, 0, 2]],
b69ab31350 ['=', [1, 10, 2, 11]],
b69ab31351 ['!', [10, 11, 11, 12]],
b69ab31352 ],
b69ab31353 () => false,
b69ab31354 ),
b69ab31355 ).toMatchObject([
b69ab31356 ['!', [0, 1, 0, 2]],
b69ab31357 ['=', [1, 4, 2, 5]],
b69ab31358 ['~', [4, 7, 5, 8]],
b69ab31359 ['=', [7, 10, 8, 11]],
b69ab31360 ['!', [10, 11, 11, 12]],
b69ab31361 ]);
b69ab31362 });
b69ab31363
b69ab31364 it('respects isExpanded function', () => {
b69ab31365 expect(
b69ab31366 collapseContextBlocks(
b69ab31367 [
b69ab31368 ['!', [0, 1, 0, 2]],
b69ab31369 ['=', [1, 10, 2, 11]],
b69ab31370 ['!', [10, 11, 11, 12]],
b69ab31371 ],
b69ab31372 (aLine, _bLine) => aLine === 4,
b69ab31373 ),
b69ab31374 ).toMatchObject([
b69ab31375 ['!', [0, 1, 0, 2]],
b69ab31376 ['=', [1, 10, 2, 11]],
b69ab31377 ['!', [10, 11, 11, 12]],
b69ab31378 ]);
b69ab31379 });
b69ab31380
b69ab31381 it('skips "~" if "=" block is too small', () => {
b69ab31382 expect(
b69ab31383 collapseContextBlocks(
b69ab31384 [
b69ab31385 ['!', [0, 1, 0, 2]],
b69ab31386 ['=', [1, 7, 2, 8]],
b69ab31387 ['!', [7, 8, 8, 9]],
b69ab31388 ],
b69ab31389 () => false,
b69ab31390 ),
b69ab31391 ).toMatchObject([
b69ab31392 ['!', [0, 1, 0, 2]],
b69ab31393 ['=', [1, 7, 2, 8]],
b69ab31394 ['!', [7, 8, 8, 9]],
b69ab31395 ]);
b69ab31396 });
b69ab31397
b69ab31398 it('preserves context around empty ! block', () => {
b69ab31399 expect(
b69ab31400 collapseContextBlocks(
b69ab31401 [
b69ab31402 ['=', [0, 5, 0, 5]],
b69ab31403 ['!', [5, 5, 5, 5]],
b69ab31404 ['=', [5, 6, 5, 6]],
b69ab31405 ],
b69ab31406 () => false,
b69ab31407 ),
b69ab31408 ).toEqual([
b69ab31409 ['~', [0, 2, 0, 2]],
b69ab31410 ['=', [2, 5, 2, 5]],
b69ab31411 ['!', [5, 5, 5, 5]],
b69ab31412 ['=', [5, 6, 5, 6]],
b69ab31413 ]);
b69ab31414 });
b69ab31415
b69ab31416 it('handles adjacent "=" blocks', () => {
b69ab31417 expect(
b69ab31418 collapseContextBlocks(
b69ab31419 [
b69ab31420 ['=', [0, 2, 0, 2]],
b69ab31421 ['=', [2, 8, 2, 8]],
b69ab31422 ],
b69ab31423 () => false,
b69ab31424 ),
b69ab31425 ).toMatchObject([
b69ab31426 ['~', [0, 2, 0, 2]],
b69ab31427 ['~', [2, 8, 2, 8]],
b69ab31428 ]);
b69ab31429 });
b69ab31430});
b69ab31431
b69ab31432describe('mergeBlocks', () => {
b69ab31433 it('should handle empty blocks', () => {
b69ab31434 const result = mergeBlocks([], []);
b69ab31435 expect(result).toEqual([]);
b69ab31436 });
b69ab31437
b69ab31438 it('should merge blocks', () => {
b69ab31439 const abBlocks: Array<Block> = [
b69ab31440 ['!', [0, 0, 0, 1]],
b69ab31441 ['!', [0, 0, 1, 4]],
b69ab31442 ['!', [0, 0, 4, 7]],
b69ab31443 ];
b69ab31444 const cbBlocks: Array<Block> = [
b69ab31445 ['!', [0, 0, 0, 2]],
b69ab31446 ['!', [0, 0, 2, 3]],
b69ab31447 ['!', [0, 0, 3, 6]],
b69ab31448 ['!', [0, 0, 6, 7]],
b69ab31449 ];
b69ab31450 const result = mergeBlocks(abBlocks, cbBlocks);
b69ab31451 expect(result).toEqual([['!', [0, 7, 0, 7]]]);
b69ab31452 });
b69ab31453
b69ab31454 it('should handle blocks with different signs', () => {
b69ab31455 let abBlocks: Array<Block> = [
b69ab31456 ['!', [0, 1, 0, 1]],
b69ab31457 ['!', [1, 2, 1, 2]],
b69ab31458 ['=', [2, 5, 3, 6]],
b69ab31459 ];
b69ab31460 let cbBlocks: Array<Block> = [
b69ab31461 ['!', [0, 2, 0, 3]],
b69ab31462 ['=', [2, 3, 3, 4]],
b69ab31463 ['=', [3, 5, 4, 6]],
b69ab31464 ];
b69ab31465 let result = mergeBlocks(abBlocks, cbBlocks);
b69ab31466 expect(result).toEqual([
b69ab31467 ['!', [0, 3, 0, 3]],
b69ab31468 ['=', [3, 6, 3, 6]],
b69ab31469 ]);
b69ab31470
b69ab31471 abBlocks = [
b69ab31472 ['!', [0, 0, 0, 3]],
b69ab31473 ['=', [0, 4, 3, 7]],
b69ab31474 ];
b69ab31475 cbBlocks = [
b69ab31476 ['=', [0, 1, 0, 1]],
b69ab31477 ['=', [1, 4, 1, 4]],
b69ab31478 ['!', [4, 4, 4, 6]],
b69ab31479 ['=', [4, 5, 6, 7]],
b69ab31480 ];
b69ab31481 result = mergeBlocks(abBlocks, cbBlocks);
b69ab31482 expect(result).toEqual([
b69ab31483 ['!', [0, 3, 0, 3]],
b69ab31484 ['=', [3, 4, 3, 4]],
b69ab31485 ['!', [4, 6, 4, 6]],
b69ab31486 ['=', [6, 7, 6, 7]],
b69ab31487 ]);
b69ab31488 });
b69ab31489
b69ab31490 it('should preserve empty ranges', () => {
b69ab31491 const abBlocks: Array<Block> = [
b69ab31492 ['=', [0, 1, 0, 1]],
b69ab31493 ['!', [1, 2, 1, 1]],
b69ab31494 ['=', [2, 6, 1, 5]],
b69ab31495 ];
b69ab31496 const cbBlocks: Array<Block> = [
b69ab31497 ['=', [0, 4, 0, 4]],
b69ab31498 ['!', [4, 5, 4, 4]],
b69ab31499 ['=', [5, 6, 4, 5]],
b69ab31500 ];
b69ab31501 const result = mergeBlocks(abBlocks, cbBlocks);
b69ab31502 expect(result).toEqual([
b69ab31503 ['=', [0, 1, 0, 1]],
b69ab31504 ['!', [1, 1, 1, 1]],
b69ab31505 ['=', [1, 4, 1, 4]],
b69ab31506 ['!', [4, 4, 4, 4]],
b69ab31507 ['=', [4, 5, 4, 5]],
b69ab31508 ]);
b69ab31509 });
b69ab31510});
b69ab31511
b69ab31512function renderDiff(
b69ab31513 a: string,
b69ab31514 b: string,
b69ab31515 diffFunc: (aLines: string[], bLines: string[]) => Array<Block>,
b69ab31516): string {
b69ab31517 const aLines = splitLines(a);
b69ab31518 const bLines = splitLines(b);
b69ab31519 const blocks = diffFunc(aLines, bLines);
b69ab31520 return blocks
b69ab31521 .flatMap(([sign, [a1, a2, b1, b2]]) => {
b69ab31522 if (sign === '=') {
b69ab31523 return aLines.slice(a1, a2).map(l => ` ${l}`);
b69ab31524 } else {
b69ab31525 return aLines
b69ab31526 .slice(a1, a2)
b69ab31527 .map(l => `-${l}`)
b69ab31528 .concat(bLines.slice(b1, b2).map(l => `+${l}`));
b69ab31529 }
b69ab31530 })
b69ab31531 .join('');
b69ab31532}