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

Support callback in patch functions, not just diffFoo functions #521

Merged
merged 9 commits into from
Jun 7, 2024
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ Broadly, jsdiff's diff functions all take an old text and a new text and perform

#### Universal `options`

Certain options can be provided in the `options` object of *any* method that calculates a diff:
Certain options can be provided in the `options` object of *any* method that calculates a diff (including `diffChars`, `diffLines` etc. as well as `structuredPatch`, `createPatch`, and `createTwoFilesPatch`):

* `callback`: if provided, the diff will be computed in async mode to avoid blocking the event loop while the diff is calculated. The value of the `callback` option should be a function and will be passed the result of the diff as its first argument. Only works with functions that return change objects, like `diffLines`, not those that return patches, like `structuredPatch` or `createPatch`.
* `callback`: if provided, the diff will be computed in async mode to avoid blocking the event loop while the diff is calculated. The value of the `callback` option should be a function and will be passed the computed diff or patch as its first argument.

(Note that if the ONLY option you want to provide is a callback, you can pass the callback function directly as the `options` parameter instead of passing an object with a `callback` property.)

Expand Down
1 change: 1 addition & 0 deletions release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [#490](https://github.com/kpdecker/jsdiff/pull/490) **When calling diffing functions in async mode by passing a `callback` option, the diff result will now be passed as the *first* argument to the callback instead of the second.** (Previously, the first argument was never used at all and would always have value `undefined`.)
- [#489](github.com/kpdecker/jsdiff/pull/489) **`this.options` no longer exists on `Diff` objects.** Instead, `options` is now passed as an argument to methods that rely on options, like `equals(left, right, options)`. This fixes a race condition in async mode, where diffing behaviour could be changed mid-execution if a concurrent usage of the same `Diff` instances overwrote its `options`.
- [#518](https://github.com/kpdecker/jsdiff/pull/518) **`linedelimiters` no longer exists** on patch objects; instead, when a patch with Windows-style CRLF line endings is parsed, **the lines in `lines` will end with `\r`**. There is now a **new `autoConvertLineEndings` option, on by default**, which makes it so that when a patch with Windows-style line endings is applied to a source file with Unix style line endings, the patch gets autoconverted to use Unix-style line endings, and when a patch with Unix-style line endings is applied to a source file with Windows-style line endings, it gets autoconverted to use Windows-style line endings.
- [#521](https://github.com/kpdecker/jsdiff/pull/521) **the `callback` option is now supported by `structuredPatch`, `createPatch`, and `createTwoFilesPatch`**

## v5.2.0

Expand Down
215 changes: 131 additions & 84 deletions src/patch/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,104 +4,125 @@ export function structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHea
if (!options) {
options = {};
}
if (typeof options === 'function') {
options = {callback: options};
}
if (typeof options.context === 'undefined') {
options.context = 4;
}

const diff = diffLines(oldStr, newStr, options);
if(!diff) {
return;
if (!options.callback) {
return diffLinesResultToPatch(diffLines(oldStr, newStr, options));
} else {
const {callback} = options;
diffLines(
oldStr,
newStr,
{
...options,
callback: (diff) => {
const patch = diffLinesResultToPatch(diff);
callback(patch);
}
}
);
}

diff.push({value: '', lines: []}); // Append an empty value to make cleanup easier
function diffLinesResultToPatch(diff) {
if(!diff) {
return;
}

function contextLines(lines) {
return lines.map(function(entry) { return ' ' + entry; });
}
diff.push({value: '', lines: []}); // Append an empty value to make cleanup easier

function contextLines(lines) {
return lines.map(function(entry) { return ' ' + entry; });
}

let hunks = [];
let oldRangeStart = 0, newRangeStart = 0, curRange = [],
oldLine = 1, newLine = 1;
for (let i = 0; i < diff.length; i++) {
const current = diff[i],
lines = current.lines || current.value.replace(/\n$/, '').split('\n');
current.lines = lines;

if (current.added || current.removed) {
// If we have previous context, start with that
if (!oldRangeStart) {
const prev = diff[i - 1];
oldRangeStart = oldLine;
newRangeStart = newLine;

if (prev) {
curRange = options.context > 0 ? contextLines(prev.lines.slice(-options.context)) : [];
oldRangeStart -= curRange.length;
newRangeStart -= curRange.length;
let hunks = [];
let oldRangeStart = 0, newRangeStart = 0, curRange = [],
oldLine = 1, newLine = 1;
for (let i = 0; i < diff.length; i++) {
const current = diff[i],
lines = current.lines || current.value.replace(/\n$/, '').split('\n');
current.lines = lines;

if (current.added || current.removed) {
// If we have previous context, start with that
if (!oldRangeStart) {
const prev = diff[i - 1];
oldRangeStart = oldLine;
newRangeStart = newLine;

if (prev) {
curRange = options.context > 0 ? contextLines(prev.lines.slice(-options.context)) : [];
oldRangeStart -= curRange.length;
newRangeStart -= curRange.length;
}
}
}

// Output our changes
curRange.push(... lines.map(function(entry) {
return (current.added ? '+' : '-') + entry;
}));
// Output our changes
curRange.push(... lines.map(function(entry) {
return (current.added ? '+' : '-') + entry;
}));

// Track the updated file position
if (current.added) {
newLine += lines.length;
} else {
oldLine += lines.length;
}
} else {
// Identical context lines. Track line changes
if (oldRangeStart) {
// Close out any changes that have been output (or join overlapping)
if (lines.length <= options.context * 2 && i < diff.length - 2) {
// Overlapping
curRange.push(... contextLines(lines));
// Track the updated file position
if (current.added) {
newLine += lines.length;
} else {
// end the range and output
let contextSize = Math.min(lines.length, options.context);
curRange.push(... contextLines(lines.slice(0, contextSize)));

let hunk = {
oldStart: oldRangeStart,
oldLines: (oldLine - oldRangeStart + contextSize),
newStart: newRangeStart,
newLines: (newLine - newRangeStart + contextSize),
lines: curRange
};
if (i >= diff.length - 2 && lines.length <= options.context) {
// EOF is inside this hunk
let oldEOFNewline = ((/\n$/).test(oldStr));
let newEOFNewline = ((/\n$/).test(newStr));
let noNlBeforeAdds = lines.length == 0 && curRange.length > hunk.oldLines;
if (!oldEOFNewline && noNlBeforeAdds && oldStr.length > 0) {
// special case: old has no eol and no trailing context; no-nl can end up before adds
// however, if the old file is empty, do not output the no-nl line
curRange.splice(hunk.oldLines, 0, '\\ No newline at end of file');
}
if ((!oldEOFNewline && !noNlBeforeAdds) || !newEOFNewline) {
curRange.push('\\ No newline at end of file');
oldLine += lines.length;
}
} else {
// Identical context lines. Track line changes
if (oldRangeStart) {
// Close out any changes that have been output (or join overlapping)
if (lines.length <= options.context * 2 && i < diff.length - 2) {
// Overlapping
curRange.push(... contextLines(lines));
} else {
// end the range and output
let contextSize = Math.min(lines.length, options.context);
curRange.push(... contextLines(lines.slice(0, contextSize)));

let hunk = {
oldStart: oldRangeStart,
oldLines: (oldLine - oldRangeStart + contextSize),
newStart: newRangeStart,
newLines: (newLine - newRangeStart + contextSize),
lines: curRange
};
if (i >= diff.length - 2 && lines.length <= options.context) {
// EOF is inside this hunk
let oldEOFNewline = ((/\n$/).test(oldStr));
let newEOFNewline = ((/\n$/).test(newStr));
let noNlBeforeAdds = lines.length == 0 && curRange.length > hunk.oldLines;
if (!oldEOFNewline && noNlBeforeAdds && oldStr.length > 0) {
// special case: old has no eol and no trailing context; no-nl can end up before adds
// however, if the old file is empty, do not output the no-nl line
curRange.splice(hunk.oldLines, 0, '\\ No newline at end of file');
}
if ((!oldEOFNewline && !noNlBeforeAdds) || !newEOFNewline) {
curRange.push('\\ No newline at end of file');
}
}
}
hunks.push(hunk);
hunks.push(hunk);

oldRangeStart = 0;
newRangeStart = 0;
curRange = [];
oldRangeStart = 0;
newRangeStart = 0;
curRange = [];
}
}
oldLine += lines.length;
newLine += lines.length;
}
oldLine += lines.length;
newLine += lines.length;
}
}

return {
oldFileName: oldFileName, newFileName: newFileName,
oldHeader: oldHeader, newHeader: newHeader,
hunks: hunks
};
return {
oldFileName: oldFileName, newFileName: newFileName,
oldHeader: oldHeader, newHeader: newHeader,
hunks: hunks
};
}
}

export function formatPatch(diff) {
Expand Down Expand Up @@ -140,11 +161,37 @@ export function formatPatch(diff) {
}

export function createTwoFilesPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options) {
const patchObj = structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options);
if (!patchObj) {
return;
if (typeof options === 'function') {
options = {callback: options};
}

if (!options?.callback) {
const patchObj = structuredPatch(oldFileName, newFileName, oldStr, newStr, oldHeader, newHeader, options);
if (!patchObj) {
return;
}
return formatPatch(patchObj);
} else {
const {callback} = options;
structuredPatch(
oldFileName,
newFileName,
oldStr,
newStr,
oldHeader,
newHeader,
{
...options,
callback: patchObj => {
if (!patchObj) {
callback();
} else {
callback(formatPatch(patchObj));
}
}
}
);
}
return formatPatch(patchObj);
}

export function createPatch(fileName, oldStr, newStr, oldHeader, newHeader, options) {
Expand Down
98 changes: 98 additions & 0 deletions test/patch/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,66 @@ describe('patch/create', function() {
expect(diffResult).to.equal(expectedResult);
});
});


it('takes an optional callback option', function(done) {
createPatch(
'test',
'foo\nbar\nbaz\n', 'foo\nbarcelona\nbaz\n',
'header1', 'header2',
{callback: (res) => {
expect(res).to.eql(
'Index: test\n'
+ '===================================================================\n'
+ '--- test\theader1\n'
+ '+++ test\theader2\n'
+ '@@ -1,3 +1,3 @@\n'
+ ' foo\n'
+ '-bar\n'
+ '+barcelona\n'
+ ' baz\n'
);
done();
}}
);
});

it('lets you provide a callback by passing a function as the `options` parameter', function(done) {
createPatch(
'test',
'foo\nbar\nbaz\n', 'foo\nbarcelona\nbaz\n',
'header1', 'header2',
res => {
expect(res).to.eql(
'Index: test\n'
+ '===================================================================\n'
+ '--- test\theader1\n'
+ '+++ test\theader2\n'
+ '@@ -1,3 +1,3 @@\n'
+ ' foo\n'
+ '-bar\n'
+ '+barcelona\n'
+ ' baz\n'
);
done();
}
);
});

it('still supports early termination when in async mode', function(done) {
createPatch(
'test',
'foo\nbar\nbaz\n', 'food\nbarcelona\nbaz\n',
'header1', 'header2',
{
maxEditLength: 1,
callback: (res) => {
expect(res).to.eql(undefined);
done();
}
}
);
});
});

describe('stripTrailingCr', function() {
Expand Down Expand Up @@ -766,6 +826,44 @@ describe('patch/create', function() {
});
});

it('takes an optional callback option', function(done) {
structuredPatch(
'oldfile', 'newfile',
'foo\nbar\nbaz\n', 'foo\nbarcelona\nbaz\n',
'header1', 'header2',
{callback: (res) => {
expect(res).to.eql({
oldFileName: 'oldfile', newFileName: 'newfile',
oldHeader: 'header1', newHeader: 'header2',
hunks: [{
oldStart: 1, oldLines: 3, newStart: 1, newLines: 3,
lines: [' foo', '-bar', '+barcelona', ' baz']
}]
});
done();
}}
);
});

it('lets you provide a callback by passing a function as the `options` parameter', function(done) {
structuredPatch(
'oldfile', 'newfile',
'foo\nbar\nbaz\n', 'foo\nbarcelona\nbaz\n',
'header1', 'header2',
res => {
expect(res).to.eql({
oldFileName: 'oldfile', newFileName: 'newfile',
oldHeader: 'header1', newHeader: 'header2',
hunks: [{
oldStart: 1, oldLines: 3, newStart: 1, newLines: 3,
lines: [' foo', '-bar', '+barcelona', ' baz']
}]
});
done();
}
);
});

describe('given options.maxEditLength', function() {
const options = { maxEditLength: 1 };

Expand Down