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

Inline codediff #185

Merged
merged 15 commits into from
Jun 6, 2024
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ node_modules
*.ipynb
webdiff/static/components
webdiff/static/js/file_diff.js
webdiff/static/js/file_diff.js.map

wheelhouse
.vscode
Expand Down
238 changes: 238 additions & 0 deletions ts/codediff/__tests__/addcharacterdiffs_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/** @jest-environment jsdom */
import $ from 'jquery';
import { CharacterDiff, addCharacterDiffs, codesToHtml, computeCharacterDiffs, simplifyCodes, splitIntoWords } from "../char-diffs";
import { htmlTextMapper } from "../html-text-mapper";

(globalThis as any).$ = $;

describe('add character diffs', () => {

test('simplifyCodes', () => {
const x = 'replace';
const y = 'equal';
expect(
simplifyCodes([[null, 0, 2], [x, 2, 4]])).toEqual(
[[null, 0, 2], [x, 2, 4]]);
expect(
simplifyCodes([[x, 0, 2], [x, 2, 4]])).toEqual(
[[x, 0, 4]]);
expect(
simplifyCodes([[x, 0, 2], [x, 2, 4], [y, 4, 6]])).toEqual(
[[x, 0, 4], [y, 4, 6]]);
});

test('codesToHtml', () => {
const str = 'hello';
const map = {
getHtmlSubstring: function(a: number, b: number) { return str.substring(a, b) }
} as htmlTextMapper;
const codes: CharacterDiff[] = [[null, 0, 1], ['replace', 1, 3], ['equal', 3, 5]];
expect(codesToHtml(map, codes)).toEqual(
'h<span class="char-replace">el</span><span class="char-equal">lo</span>');
});

test('char diffs -- simple', () => {
var before = $('<div>').text(" return '' + date.getFullYear();").get(0)!;
var after = $('<div>').text(" return 'xx' + date.getFullYear();").get(0)!;

var beforeText = $(before).text(),
afterText = $(after).text();

addCharacterDiffs(before, after);
expect($(before).text()).toEqual(beforeText);
expect($(after).text()).toEqual(afterText);
expect($(before).html()).toEqual(" return '' + date.getFullYear();");
expect($(after).html()).toEqual(" return '<span class=\"char-insert\">xx</span>' + date.getFullYear();");
});

test('char diffs with trailing markup', () => {
var before = $('<div>').html("<q>''</q>").get(0)!;
var after = $('<div>').html("<q>'xx'</q>").get(0)!;

var beforeText = $(before).text(),
afterText = $(after).text();

addCharacterDiffs(before, after);
expect($(before).text()).toEqual(beforeText);
expect($(after).text()).toEqual(afterText);
expect($(before).html()).toEqual("<q>''</q>");
expect($(after).html()).toEqual("<q>'</q><span class=\"char-insert\"><q>xx</q></span><q>'</q>");
});

test('char diffs with markup', () => {
var before = $('<div>').html(" <kw>return</kw> <q>''</q> + date.getFullYear();").get(0)!;
var after = $('<div>').html(" <kw>return</kw> <q>'xx'</q> + date.getFullYear();").get(0)!;

var beforeText = $(before).text(),
afterText = $(after).text();

addCharacterDiffs(before, after);
expect($(before).text()).toEqual(beforeText);
expect($(after).text()).toEqual(afterText);
expect($(before).html()).toEqual(" <kw>return</kw> <q>''</q> + date.getFullYear();");
expect($(after).html()).toEqual(" <kw>return</kw> <q>'</q><span class=\"char-insert\"><q>xx</q></span><q>'</q> + date.getFullYear();");
});

test('mixed inserts and markup', () => {
var beforeCode = '<span class="hljs-string">"q"</span>, s';
var afterCode = '<span class="hljs-string">"q"</span><span class="hljs-comment">/*, s*/</span>';
var beforeEl = $('<div>').html(beforeCode).get(0)!;
var afterEl = $('<div>').html(afterCode).get(0)!;
// XXX this is strange -- is this just asserting that there are no exceptions?
addCharacterDiffs(beforeEl, afterEl);
});

function assertCharDiff(beforeText: string, beforeExpectation: string,
afterText: string, afterExpectation: string) {
const codes = computeCharacterDiffs(beforeText, afterText)!;
expect(codes).not.toBeNull();
// 'Declined to generate a diff when one was expected.');

var beforeCodes = codes[0],
afterCodes = codes[1];

var process = function(codes: CharacterDiff[], txt: string) {
return codes.map(function(code) {
var part = txt.substring(code[1], code[2]);
if (code[0] != null) part = '[' + part + ']';
return part;
}).join('');
};

var beforeActual = process(beforeCodes, beforeText),
afterActual = process(afterCodes, afterText);

expect(beforeActual).toEqual(beforeExpectation);
expect(afterActual).toEqual(afterExpectation);
}

// See https://github.com/danvk/github-syntax/issues/17
test('pure add with assertCharDiff', () => {
assertCharDiff(
'output.writeBytes(obj.sequence)',
'output.writeBytes(obj.sequence)',
'output.writeBytes(obj.sequence.toArray)',
'output.writeBytes(obj.sequence[.toArray])');
});


test('splitIntoWords', () => {
expect(splitIntoWords(
'<ImageDiffModeSelector filePair={filePair}')).toEqual(
['<', 'Image', 'Diff', 'Mode', 'Selector', ' ', 'file', 'Pair', '=', '{',
'file', 'Pair', '}']);
expect(splitIntoWords(
'<DiffView filePair={filePair}')).toEqual(
['<', 'Diff', 'View', ' ', 'file', 'Pair', '=', '{', 'file', 'Pair', '}']);
expect(splitIntoWords(
'Test1TEST23testAbc{}')).toEqual(
['Test', '1', 'TEST', '23', 'test', 'Abc', '{', '}']);
expect(splitIntoWords(
' FooBar')).toEqual(
[' ', ' ', ' ', 'Foo', 'Bar']);
});

test('char diffs on word boundaries', () => {
assertCharDiff(
'<ImageDiffModeSelector filePair={filePair}',
'<[Image]Diff[ModeSelector] filePair={filePair}',
'<DiffView filePair={filePair}',
'<Diff[View] filePair={filePair}'
);

assertCharDiff(
'mode={this.state.imageDiffMode}',
'[mode]={this.state.imageDiffMode}',
'imageDiffMode={this.state.imageDiffMode}',
'[imageDiffMode]={this.state.imageDiffMode}'
);

assertCharDiff(
'changeHandler={this.changeImageDiffModeHandler}/>',
'changeHandler={this.changeImageDiffModeHandler}/>',
'changeImageDiffModeHandler={this.changeImageDiffModeHandler} />',
'change[ImageDiffMode]Handler={this.changeImageDiffModeHandler}[ ]/>'
);

// XXX this could be more specific.
assertCharDiff(
'var lis = this.props.filePairs.map((file_pair, idx) => {',
'var lis = this.props.filePairs.map((file[_pair], idx) => {',
'var lis = this.props.filePairs.map((filePair, idx) => {',
'var lis = this.props.filePairs.map((file[Pair], idx) => {'
);

assertCharDiff(
' return <li key={idx}>{content}</li>',
' return <li key={idx}>{content}</li>',
' return <li key={idx}>{content}</li>;',
' return <li key={idx}>{content}</li>[;]'
);

assertCharDiff(
'import net.sf.samtools._',
'import [net.sf].samtools._',
'import htsjdk.samtools._',
'import [htsjdk].samtools._'
);
});

test('add a comma', () => {
assertCharDiff(
' foo: "bar"',
' foo: "bar"',
' foo: "bar",',
' foo: "bar"[,]');
});

test('whitespace diff', () => {
assertCharDiff(
' ',
'[ ]',
'',
'');

assertCharDiff(
'',
'',
' ',
'[ ]');

assertCharDiff(
' <div className="examine-page">',
' <div className="examine-page">',
' <div className="examine-page">',
'[ ] <div className="examine-page">');

assertCharDiff(
'foobar',
'foobar',
' foobar',
'[ ]foobar');

assertCharDiff(
' foobar',
'[ ]foobar',
'foobar',
'foobar');
});

test('char diff thresholds', () => {
// Not a useful diff -- only one character in common!
expect(computeCharacterDiffs('foo.bar', 'blah.baz')).toBeNull();
expect(computeCharacterDiffs('foo.', 'blah.')).toBeNull();

// with the "bar"s equal, it's become useful.
assertCharDiff(
'foo.bar',
'[foo].bar',
'blah.bar',
'[blah].bar');

// pure adds/deletes shouldn't be flagged as char diffs.
expect(computeCharacterDiffs(
'',
' date.getSeconds() + date.getMilliseconds();')).toBeNull();
});

});
71 changes: 71 additions & 0 deletions ts/codediff/__tests__/diffranges_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { addSkips } from "../codes";
import { OpCode } from "../difflib";

test('generates same diff ranges as jsdifflib', () => {

// These are the opcodes for test.html
var opcodes: OpCode[] = [
["equal", 0, 9, 0, 9],
["replace", 9,11, 9,11],
["equal", 11,14,11,14],
["delete", 14,16,14,14],
["equal", 16,18,14,16],
["insert", 18,18,16,17],
["equal", 18,27,17,26],
["replace",27,28,26,27],
["equal", 28,31,27,30],
["delete", 31,32,30,30],
["equal", 32,43,30,41]
]

var ranges = addSkips(opcodes, 3, 0);
expect(ranges).toEqual( [
{type: 'skip', before: [ 0, 6], after: [ 0, 6]},
{type: 'equal', before: [ 6, 9], after: [ 6, 9]},
{type: 'replace', before: [ 9, 11], after: [ 9, 11]},
{type: 'equal', before: [11, 14], after: [11, 14]},
{type: 'delete', before: [14, 16], after: [14, 14]},
{type: 'equal', before: [16, 18], after: [14, 16]},
{type: 'insert', before: [18, 18], after: [16, 17]},
{type: 'equal', before: [18, 21], after: [17, 20]},
{type: 'skip', before: [21, 24], after: [20, 23]},
{type: 'equal', before: [24, 27], after: [23, 26]},
{type: 'replace', before: [27, 28], after: [26, 27]},
{type: 'equal', before: [28, 31], after: [27, 30]},
{type: 'delete', before: [31, 32], after: [30, 30]},
{type: 'equal', before: [32, 35], after: [30, 33]},
{type: 'skip', before: [35, 43], after: [33, 41]}
]);

var ranges = addSkips(opcodes, 3, 5); // minJumpSize = 5
expect(ranges).toEqual( [
{type: 'skip', before: [ 0, 6], after: [ 0, 6]},
{type: 'equal', before: [ 6, 9], after: [ 6, 9]},
{type: 'replace', before: [ 9, 11], after: [ 9, 11]},
{type: 'equal', before: [11, 14], after: [11, 14]},
{type: 'delete', before: [14, 16], after: [14, 14]},
{type: 'equal', before: [16, 18], after: [14, 16]},
{type: 'insert', before: [18, 18], after: [16, 17]},
{type: 'equal', before: [18, 27], after: [17, 26]}, // (eq-skip-eq above)
{type: 'replace', before: [27, 28], after: [26, 27]},
{type: 'equal', before: [28, 31], after: [27, 30]},
{type: 'delete', before: [31, 32], after: [30, 30]},
{type: 'equal', before: [32, 35], after: [30, 33]},
{type: 'skip', before: [35, 43], after: [33, 41]}
]);

var ranges = addSkips(opcodes, 3, 10); // minJumpSize = 10
expect(ranges).toEqual( [
{type: 'equal', before: [ 0, 9], after: [ 0, 9]}, // was skip
{type: 'replace', before: [ 9, 11], after: [ 9, 11]},
{type: 'equal', before: [11, 14], after: [11, 14]},
{type: 'delete', before: [14, 16], after: [14, 14]},
{type: 'equal', before: [16, 18], after: [14, 16]},
{type: 'insert', before: [18, 18], after: [16, 17]},
{type: 'equal', before: [18, 27], after: [17, 26]}, // was skip
{type: 'replace', before: [27, 28], after: [26, 27]},
{type: 'equal', before: [28, 31], after: [27, 30]},
{type: 'delete', before: [31, 32], after: [30, 30]},
{type: 'equal', before: [32, 43], after: [30, 41]} // was skip
]);
});
65 changes: 65 additions & 0 deletions ts/codediff/__tests__/guesslanguage_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { guessLanguageUsingContents, guessLanguageUsingFileName } from "../language";

import highlightjs from 'highlight.js';

(globalThis as any).hljs = highlightjs;

describe('guess language', () => {

test('guessLanguageUsingFileName', () => {
const guess = guessLanguageUsingFileName;

expect(guess('/foo/bar/blah.html')).toEqual('html');
expect(guess('bar.html')).toEqual('html');
expect(guess('foo.css')).toEqual('css');
expect(guess('foo.py')).toEqual('python');
expect(guess('foo.sh')).toEqual('bash');
expect(guess('foo.js')).toEqual('javascript');
expect(guess('README.md')).toEqual('markdown');
expect(guess('Makefile')).toEqual('makefile');
expect(guess('foo.nonexistent')).toEqual(null);
expect(guess('html')).toEqual(null);
});

test('guessLanguageUsingContentsShebang', () => {
const guess = guessLanguageUsingContents;
expect(guess(
'#!/usr/bin/env python\n' +
'print 1 + 1\n')).toEqual('python');

expect(guess(
'#!/usr/bin/env python\n' +
'print 1 + 1\n')).toEqual('python');

expect(guess(
'#!/usr/local/bin/python\n' +
'print 1 + 1\n')).toEqual('python');

expect(guess(
'#!/usr/bin/env node\n' +
'1\n')).toEqual('javascript');

expect(guess(
'#!/bin/bash\n' +
'open $(git remote -v | grep push | cut -f2 | sed "s/.git .*/\/issues/")\n')).toEqual(
'bash');
});

// Unclear when/how this test ever passed.
// hljs makes some weird guesses here: "arcade" and "angelscript".
// hljs language detection seems quite bad in general,
// see https://stackoverflow.com/q/24544484/388951
test.skip('guessLanguageUsingContentsTokens', () => {
const guess = guessLanguageUsingContents;
expect(guess(
'function foo() {\n' +
' console.log("hello");\n' +
'}\n')).toEqual( 'javascript');

expect(guess(
'class Foo:\n' +
' def __init__(self):\n' +
' pass\n')).toEqual( 'python');
});

});
Loading