refactor: Separate matching in side-by-side algorithm

This commit is contained in:
Rodrigo Fernandes 2019-11-26 19:02:25 +00:00
parent d8e0a99070
commit 5c35de28eb
No known key found for this signature in database
GPG key ID: 67157D2E3D4258B4
4 changed files with 293 additions and 253 deletions

View file

@ -141,11 +141,11 @@ const htmlSideExample1 =
' <div class="d2h-code-side-line d2h-info">@@ -1 +1 @@</div>\n' + ' <div class="d2h-code-side-line d2h-info">@@ -1 +1 @@</div>\n' +
" </td>\n" + " </td>\n" +
"</tr><tr>\n" + "</tr><tr>\n" +
' <td class="d2h-code-side-linenumber d2h-del">\n' + ' <td class="d2h-code-side-linenumber d2h-del d2h-change">\n' +
" 1\n" + " 1\n" +
" </td>\n" + " </td>\n" +
' <td class="d2h-del">\n' + ' <td class="d2h-del d2h-change">\n' +
' <div class="d2h-code-side-line d2h-del">\n' + ' <div class="d2h-code-side-line d2h-del d2h-change">\n' +
' <span class="d2h-code-line-prefix">-</span>\n' + ' <span class="d2h-code-line-prefix">-</span>\n' +
' <span class="d2h-code-line-ctn"><del>test</del></span>\n' + ' <span class="d2h-code-line-ctn"><del>test</del></span>\n' +
" </div>\n" + " </div>\n" +
@ -165,11 +165,11 @@ const htmlSideExample1 =
' <div class="d2h-code-side-line d2h-info"></div>\n' + ' <div class="d2h-code-side-line d2h-info"></div>\n' +
" </td>\n" + " </td>\n" +
"</tr><tr>\n" + "</tr><tr>\n" +
' <td class="d2h-code-side-linenumber d2h-ins">\n' + ' <td class="d2h-code-side-linenumber d2h-ins d2h-change">\n' +
" 1\n" + " 1\n" +
" </td>\n" + " </td>\n" +
' <td class="d2h-ins">\n' + ' <td class="d2h-ins d2h-change">\n' +
' <div class="d2h-code-side-line d2h-ins">\n' + ' <div class="d2h-code-side-line d2h-ins d2h-change">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' + ' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn"><ins>test1</ins></span>\n' + ' <span class="d2h-code-line-ctn"><ins>test1</ins></span>\n' +
" </div>\n" + " </div>\n" +

View file

@ -75,7 +75,7 @@ describe("SideBySideRenderer", () => {
isCombined: false isCombined: false
}; };
const fileHtml = sideBySideRenderer.generateSideBySideFileHtml(file); const fileHtml = sideBySideRenderer.generateFileHtml(file);
const expectedLeft = const expectedLeft =
"<tr>\n" + "<tr>\n" +
@ -94,11 +94,11 @@ describe("SideBySideRenderer", () => {
" </div>\n" + " </div>\n" +
" </td>\n" + " </td>\n" +
"</tr><tr>\n" + "</tr><tr>\n" +
' <td class="d2h-code-side-linenumber d2h-del">\n' + ' <td class="d2h-code-side-linenumber d2h-del d2h-change">\n' +
" 20\n" + " 20\n" +
" </td>\n" + " </td>\n" +
' <td class="d2h-del">\n' + ' <td class="d2h-del d2h-change">\n' +
' <div class="d2h-code-side-line d2h-del">\n' + ' <div class="d2h-code-side-line d2h-del d2h-change">\n' +
' <span class="d2h-code-line-prefix">-</span>\n' + ' <span class="d2h-code-line-prefix">-</span>\n' +
' <span class="d2h-code-line-ctn"><del>removed</del></span>\n' + ' <span class="d2h-code-line-ctn"><del>removed</del></span>\n' +
" </div>\n" + " </div>\n" +
@ -133,11 +133,11 @@ describe("SideBySideRenderer", () => {
" </div>\n" + " </div>\n" +
" </td>\n" + " </td>\n" +
"</tr><tr>\n" + "</tr><tr>\n" +
' <td class="d2h-code-side-linenumber d2h-ins">\n' + ' <td class="d2h-code-side-linenumber d2h-ins d2h-change">\n' +
" 20\n" + " 20\n" +
" </td>\n" + " </td>\n" +
' <td class="d2h-ins">\n' + ' <td class="d2h-ins d2h-change">\n' +
' <div class="d2h-code-side-line d2h-ins">\n' + ' <div class="d2h-code-side-line d2h-ins d2h-change">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' + ' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn"><ins>added</ins></span>\n' + ' <span class="d2h-code-line-ctn"><ins>added</ins></span>\n' +
" </div>\n" + " </div>\n" +
@ -163,38 +163,76 @@ describe("SideBySideRenderer", () => {
it("should work for insertions", () => { it("should work for insertions", () => {
const hoganUtils = new HoganJsUtils({}); const hoganUtils = new HoganJsUtils({});
const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {}); const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {});
const fileHtml = sideBySideRenderer.generateSingleLineHtml(false, CSSLineClass.INSERTS, "test", 30, "+"); const fileHtml = sideBySideRenderer.generateSingleLineHtml(undefined, {
const expected = type: CSSLineClass.INSERTS,
"<tr>\n" + prefix: "+",
' <td class="d2h-code-side-linenumber d2h-ins">\n' + content: "test",
" 30\n" + number: 30
" </td>\n" + });
' <td class="d2h-ins">\n' +
' <div class="d2h-code-side-line d2h-ins">\n' + const expected = {
' <span class="d2h-code-line-prefix">+</span>\n' + left:
' <span class="d2h-code-line-ctn">test</span>\n' + "<tr>\n" +
" </div>\n" + ' <td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">\n' +
" </td>\n" + " \n" +
"</tr>"; " </td>\n" +
' <td class="d2h-cntx d2h-emptyplaceholder">\n' +
' <div class="d2h-code-side-line d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">\n' +
" </div>\n" +
" </td>\n" +
"</tr>",
right:
"<tr>\n" +
' <td class="d2h-code-side-linenumber d2h-ins">\n' +
" 30\n" +
" </td>\n" +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-side-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn">test</span>\n' +
" </div>\n" +
" </td>\n" +
"</tr>"
};
expect(fileHtml).toEqual(expected); expect(fileHtml).toEqual(expected);
}); });
it("should work for deletions", () => { it("should work for deletions", () => {
const hoganUtils = new HoganJsUtils({}); const hoganUtils = new HoganJsUtils({});
const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {}); const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {});
const fileHtml = sideBySideRenderer.generateSingleLineHtml(false, CSSLineClass.DELETES, "test", 30, "-"); const fileHtml = sideBySideRenderer.generateSingleLineHtml(
const expected = {
"<tr>\n" + type: CSSLineClass.DELETES,
' <td class="d2h-code-side-linenumber d2h-del">\n' + prefix: "-",
" 30\n" + content: "test",
" </td>\n" + number: 30
' <td class="d2h-del">\n' + },
' <div class="d2h-code-side-line d2h-del">\n' + undefined
' <span class="d2h-code-line-prefix">-</span>\n' + );
' <span class="d2h-code-line-ctn">test</span>\n' + const expected = {
" </div>\n" + left:
" </td>\n" + "<tr>\n" +
"</tr>"; ' <td class="d2h-code-side-linenumber d2h-del">\n' +
" 30\n" +
" </td>\n" +
' <td class="d2h-del">\n' +
' <div class="d2h-code-side-line d2h-del">\n' +
' <span class="d2h-code-line-prefix">-</span>\n' +
' <span class="d2h-code-line-ctn">test</span>\n' +
" </div>\n" +
" </td>\n" +
"</tr>",
right:
"<tr>\n" +
' <td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">\n' +
" \n" +
" </td>\n" +
' <td class="d2h-cntx d2h-emptyplaceholder">\n' +
' <div class="d2h-code-side-line d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">\n' +
" </div>\n" +
" </td>\n" +
"</tr>"
};
expect(fileHtml).toEqual(expected); expect(fileHtml).toEqual(expected);
}); });

View file

@ -1,7 +1,16 @@
import HoganJsUtils from "./hoganjs-utils"; import HoganJsUtils from "./hoganjs-utils";
import * as Rematch from "./rematch"; import * as Rematch from "./rematch";
import * as renderUtils from "./render-utils"; import * as renderUtils from "./render-utils";
import { DiffLine, LineType, DiffFile } from "./types"; import {
DiffLine,
LineType,
DiffFile,
DiffBlock,
DiffLineContext,
DiffLineDeleted,
DiffLineInserted,
DiffLineContent
} from "./types";
export interface SideBySideRendererConfig extends renderUtils.RenderConfig { export interface SideBySideRendererConfig extends renderUtils.RenderConfig {
renderNothingWhenEmpty?: boolean; renderNothingWhenEmpty?: boolean;
@ -35,7 +44,7 @@ export default class SideBySideRenderer {
.map(file => { .map(file => {
let diffs; let diffs;
if (file.blocks.length) { if (file.blocks.length) {
diffs = this.generateSideBySideFileHtml(file); diffs = this.generateFileHtml(file);
} else { } else {
diffs = this.generateEmptyDiff(); diffs = this.generateEmptyDiff();
} }
@ -82,147 +91,172 @@ export default class SideBySideRenderer {
}; };
} }
// TODO: Make this private after improving tests generateFileHtml(file: DiffFile): FileHtml {
generateSideBySideFileHtml(file: DiffFile): FileHtml {
const matcher = Rematch.newMatcherFn( const matcher = Rematch.newMatcherFn(
Rematch.newDistanceFn((e: DiffLine) => renderUtils.deconstructLine(e.content, file.isCombined).content) Rematch.newDistanceFn((e: DiffLine) => renderUtils.deconstructLine(e.content, file.isCombined).content)
); );
const fileHtml = { return file.blocks
right: "", .map(block => {
left: "" const fileHtml = {
}; left: this.makeHeaderHtml(block.header),
right: this.makeHeaderHtml("")
};
file.blocks.forEach(block => { this.applyLineGroupping(block).forEach(([contextLines, oldLines, newLines]) => {
fileHtml.left += this.makeSideHtml(block.header); if (oldLines.length && newLines.length && !contextLines.length) {
fileHtml.right += this.makeSideHtml(""); this.applyRematchMatching(oldLines, newLines, matcher).map(([oldLines, newLines]) => {
const { left, right } = this.applyLineDiff(file, oldLines, newLines);
let oldLines: DiffLine[] = []; fileHtml.left += left;
let newLines: DiffLine[] = []; fileHtml.right += right;
});
const processChangeBlock = (): void => { } else if (contextLines.length) {
let matches; contextLines.forEach(line => {
let insertType: renderUtils.CSSLineClass; const { prefix, content } = renderUtils.deconstructLine(line.content, file.isCombined);
let deleteType: renderUtils.CSSLineClass; const { left, right } = this.generateSingleLineHtml(
{
const comparisons = oldLines.length * newLines.length; type: renderUtils.CSSLineClass.CONTEXT,
prefix: prefix,
const maxLineSizeInBlock = Math.max.apply( content: content,
null, number: line.oldNumber
oldLines.concat(newLines).map(elem => elem.content.length) },
); {
type: renderUtils.CSSLineClass.CONTEXT,
const doMatching = prefix: prefix,
comparisons < this.config.matchingMaxComparisons && content: content,
maxLineSizeInBlock < this.config.maxLineSizeInBlockForComparison && number: line.newNumber
(this.config.matching === "lines" || this.config.matching === "words"); }
);
if (doMatching) { fileHtml.left += left;
matches = matcher(oldLines, newLines); fileHtml.right += right;
insertType = renderUtils.CSSLineClass.INSERT_CHANGES; });
deleteType = renderUtils.CSSLineClass.DELETE_CHANGES; } else if (oldLines.length || newLines.length) {
} else { const { left, right } = this.processLines(file.isCombined, oldLines, newLines);
matches = [[oldLines, newLines]]; fileHtml.left += left;
insertType = renderUtils.CSSLineClass.INSERTS; fileHtml.right += right;
deleteType = renderUtils.CSSLineClass.DELETES; } else {
} console.error("Unknown state reached while processing groups of lines", contextLines, oldLines, newLines);
matches.forEach(match => {
oldLines = match[0];
newLines = match[1];
const common = Math.min(oldLines.length, newLines.length);
const max = Math.max(oldLines.length, newLines.length);
for (let j = 0; j < common; j++) {
const oldLine = oldLines[j];
const newLine = newLines[j];
const diff = renderUtils.diffHighlight(oldLine.content, newLine.content, file.isCombined, this.config);
fileHtml.left += this.generateSingleLineHtml(
file.isCombined,
deleteType,
diff.oldLine.content,
oldLine.oldNumber,
diff.oldLine.prefix
);
fileHtml.right += this.generateSingleLineHtml(
file.isCombined,
insertType,
diff.newLine.content,
newLine.newNumber,
diff.newLine.prefix
);
}
if (max > common) {
const oldSlice = oldLines.slice(common);
const newSlice = newLines.slice(common);
const tmpHtml = this.processLines(file.isCombined, oldSlice, newSlice);
fileHtml.left += tmpHtml.left;
fileHtml.right += tmpHtml.right;
} }
}); });
return fileHtml;
})
.reduce(
(accomulated, html) => {
return { left: accomulated.left + html.left, right: accomulated.right + html.right };
},
{ left: "", right: "" }
);
}
applyLineGroupping(block: DiffBlock): DiffLineGroups {
const blockLinesGroups: DiffLineGroups = [];
let oldLines: (DiffLineDeleted & DiffLineContent)[] = [];
let newLines: (DiffLineInserted & DiffLineContent)[] = [];
for (let i = 0; i < block.lines.length; i++) {
const diffLine = block.lines[i];
if (
(diffLine.type !== LineType.INSERT && newLines.length) ||
(diffLine.type === LineType.CONTEXT && oldLines.length > 0)
) {
blockLinesGroups.push([[], oldLines, newLines]);
oldLines = []; oldLines = [];
newLines = []; newLines = [];
};
for (let i = 0; i < block.lines.length; i++) {
const diffLine = block.lines[i];
const { prefix, content } = renderUtils.deconstructLine(diffLine.content, file.isCombined);
if (
diffLine.type !== LineType.INSERT &&
(newLines.length > 0 || (diffLine.type !== LineType.DELETE && oldLines.length > 0))
) {
processChangeBlock();
}
if (diffLine.type === LineType.CONTEXT) {
fileHtml.left += this.generateSingleLineHtml(
file.isCombined,
renderUtils.toCSSClass(diffLine.type),
content,
diffLine.oldNumber,
prefix
);
fileHtml.right += this.generateSingleLineHtml(
file.isCombined,
renderUtils.toCSSClass(diffLine.type),
content,
diffLine.newNumber,
prefix
);
} else if (diffLine.type === LineType.INSERT && !oldLines.length) {
fileHtml.left += this.generateSingleLineHtml(file.isCombined, renderUtils.CSSLineClass.CONTEXT, "");
fileHtml.right += this.generateSingleLineHtml(
file.isCombined,
renderUtils.toCSSClass(diffLine.type),
content,
diffLine.newNumber,
prefix
);
} else if (diffLine.type === LineType.DELETE) {
oldLines.push(diffLine);
} else if (diffLine.type === LineType.INSERT && Boolean(oldLines.length)) {
newLines.push(diffLine);
} else {
console.error("unknown state in html side-by-side generator");
processChangeBlock();
}
} }
processChangeBlock(); if (diffLine.type === LineType.CONTEXT) {
}); blockLinesGroups.push([[diffLine], [], []]);
} else if (diffLine.type === LineType.INSERT && oldLines.length === 0) {
blockLinesGroups.push([[], [], [diffLine]]);
} else if (diffLine.type === LineType.INSERT && oldLines.length > 0) {
newLines.push(diffLine);
} else if (diffLine.type === LineType.DELETE) {
oldLines.push(diffLine);
}
}
if (oldLines.length || newLines.length) {
blockLinesGroups.push([[], oldLines, newLines]);
oldLines = [];
newLines = [];
}
return blockLinesGroups;
}
applyRematchMatching(
oldLines: DiffLine[],
newLines: DiffLine[],
matcher: Rematch.MatcherFn<DiffLine>
): DiffLine[][][] {
const comparisons = oldLines.length * newLines.length;
const maxLineSizeInBlock = Math.max.apply(
null,
[0].concat(oldLines.concat(newLines).map(elem => elem.content.length))
);
const doMatching =
comparisons < this.config.matchingMaxComparisons &&
maxLineSizeInBlock < this.config.maxLineSizeInBlockForComparison &&
(this.config.matching === "lines" || this.config.matching === "words");
const matches = doMatching ? matcher(oldLines, newLines) : [[oldLines, newLines]];
return matches;
}
applyLineDiff(file: DiffFile, oldLines: DiffLine[], newLines: DiffLine[]): FileHtml {
const fileHtml = {
left: "",
right: ""
};
const common = Math.min(oldLines.length, newLines.length);
// Matched lines
for (let j = 0; j < common; j++) {
const oldLine = oldLines[j];
const newLine = newLines[j];
const diff = renderUtils.diffHighlight(oldLine.content, newLine.content, file.isCombined, this.config);
const preparedOldLine =
oldLine.oldNumber !== undefined
? {
type: renderUtils.CSSLineClass.DELETE_CHANGES,
prefix: diff.oldLine.prefix,
content: diff.oldLine.content,
number: oldLine.oldNumber
}
: undefined;
const preparedNewLine =
newLine.newNumber !== undefined
? {
type: renderUtils.CSSLineClass.INSERT_CHANGES,
prefix: diff.newLine.prefix,
content: diff.newLine.content,
number: newLine.newNumber
}
: undefined;
const { left, right } = this.generateSingleLineHtml(preparedOldLine, preparedNewLine);
fileHtml.left += left;
fileHtml.right += right;
}
// Remaining lines
const { left, right } = this.processLines(file.isCombined, oldLines.slice(common), newLines.slice(common));
fileHtml.left += left;
fileHtml.right += right;
return fileHtml; return fileHtml;
} }
// TODO: Make this private after improving tests // TODO: Make this private after improving tests
makeSideHtml(blockHeader: string): string { makeHeaderHtml(blockHeader: string): string {
return this.hoganUtils.render(genericTemplatesPath, "block-header", { return this.hoganUtils.render(genericTemplatesPath, "block-header", {
CSSLineClass: renderUtils.CSSLineClass, CSSLineClass: renderUtils.CSSLineClass,
blockHeader: blockHeader, blockHeader: blockHeader,
@ -243,105 +277,71 @@ export default class SideBySideRenderer {
const oldLine = oldLines[i]; const oldLine = oldLines[i];
const newLine = newLines[i]; const newLine = newLines[i];
let oldContent; const preparedOldLine =
let newContent; oldLine !== undefined && oldLine.oldNumber !== undefined
let oldPrefix; ? {
let newPrefix; ...renderUtils.deconstructLine(oldLine.content, isCombined),
type: renderUtils.toCSSClass(oldLine.type),
number: oldLine.oldNumber
}
: undefined;
if (oldLine) { const preparedNewLine =
const { prefix, content } = renderUtils.deconstructLine(oldLine.content, isCombined); newLine !== undefined && newLine.newNumber !== undefined
oldContent = content; ? {
oldPrefix = prefix; ...renderUtils.deconstructLine(newLine.content, isCombined),
} else { type: renderUtils.toCSSClass(newLine.type),
oldContent = ""; number: newLine.newNumber
oldPrefix = ""; }
} : undefined;
if (newLine) { const { left, right } = this.generateSingleLineHtml(preparedOldLine, preparedNewLine);
const { prefix, content } = renderUtils.deconstructLine(newLine.content, isCombined); fileHtml.left += left;
newContent = content; fileHtml.right += right;
newPrefix = prefix;
} else {
newContent = "";
oldPrefix = "";
}
if (oldLine && newLine) {
fileHtml.left += this.generateSingleLineHtml(
isCombined,
renderUtils.toCSSClass(oldLine.type),
oldContent,
oldLine.oldNumber,
oldPrefix
);
fileHtml.right += this.generateSingleLineHtml(
isCombined,
renderUtils.toCSSClass(newLine.type),
newContent,
newLine.newNumber,
newPrefix
);
} else if (oldLine) {
fileHtml.left += this.generateSingleLineHtml(
isCombined,
renderUtils.toCSSClass(oldLine.type),
oldContent,
oldLine.oldNumber,
oldPrefix
);
fileHtml.right += this.generateSingleLineHtml(isCombined, renderUtils.CSSLineClass.CONTEXT, "");
} else if (newLine) {
fileHtml.left += this.generateSingleLineHtml(isCombined, renderUtils.CSSLineClass.CONTEXT, "");
fileHtml.right += this.generateSingleLineHtml(
isCombined,
renderUtils.toCSSClass(newLine.type),
newContent,
newLine.newNumber,
newPrefix
);
}
} }
return fileHtml; return fileHtml;
} }
// TODO: Make this private after improving tests // TODO: Make this private after improving tests
generateSingleLineHtml( generateSingleLineHtml(oldLine?: DiffPreparedLine, newLine?: DiffPreparedLine): FileHtml {
isCombined: boolean, const lineClass = "d2h-code-side-linenumber";
type: renderUtils.CSSLineClass, const contentClass = "d2h-code-side-line";
content: string,
number?: number,
possiblePrefix?: string
): string {
let lineWithoutPrefix = content;
let prefix = possiblePrefix;
let lineClass = "d2h-code-side-linenumber";
let contentClass = "d2h-code-side-line";
let preparedType: string = type;
if (!number && !content) { return {
lineClass += " d2h-code-side-emptyplaceholder"; left: this.hoganUtils.render(genericTemplatesPath, "line", {
contentClass += " d2h-code-side-emptyplaceholder"; type: oldLine?.type || `${renderUtils.CSSLineClass.CONTEXT} d2h-emptyplaceholder`,
preparedType += " d2h-emptyplaceholder"; lineClass: oldLine !== undefined ? lineClass : `${lineClass} d2h-code-side-emptyplaceholder`,
prefix = "&nbsp;"; contentClass: oldLine !== undefined ? contentClass : `${contentClass} d2h-code-side-emptyplaceholder`,
lineWithoutPrefix = "&nbsp;"; prefix: oldLine?.prefix === " " ? "&nbsp;" : oldLine?.prefix || "&nbsp;",
} else if (!prefix) { content: oldLine?.content || "&nbsp;",
const lineWithPrefix = renderUtils.deconstructLine(content, isCombined); lineNumber: oldLine?.number
prefix = lineWithPrefix.prefix; }),
lineWithoutPrefix = lineWithPrefix.content; right: this.hoganUtils.render(genericTemplatesPath, "line", {
} type: newLine?.type || `${renderUtils.CSSLineClass.CONTEXT} d2h-emptyplaceholder`,
lineClass: newLine !== undefined ? lineClass : `${lineClass} d2h-code-side-emptyplaceholder`,
return this.hoganUtils.render(genericTemplatesPath, "line", { contentClass: newLine !== undefined ? contentClass : `${contentClass} d2h-code-side-emptyplaceholder`,
type: preparedType, prefix: newLine?.prefix === " " ? "&nbsp;" : newLine?.prefix || "&nbsp;",
lineClass: lineClass, content: newLine?.content || "&nbsp;",
contentClass: contentClass, lineNumber: newLine?.number
prefix: prefix === " " ? "&nbsp;" : prefix, })
content: lineWithoutPrefix, };
lineNumber: number
});
} }
} }
type DiffLineGroups = [
(DiffLineContext & DiffLineContent)[],
(DiffLineDeleted & DiffLineContent)[],
(DiffLineInserted & DiffLineContent)[]
][];
type DiffPreparedLine = {
type: renderUtils.CSSLineClass;
prefix: string;
content: string;
number: number;
};
type FileHtml = { type FileHtml = {
right: string; right: string;
left: string; left: string;

View file

@ -27,10 +27,12 @@ export interface DiffLineContext {
newNumber: number; newNumber: number;
} }
export type DiffLine = (DiffLineDeleted | DiffLineInserted | DiffLineContext) & { export type DiffLineContent = {
content: string; content: string;
}; };
export type DiffLine = (DiffLineDeleted | DiffLineInserted | DiffLineContext) & DiffLineContent;
export interface DiffBlock { export interface DiffBlock {
oldStartLine: number; oldStartLine: number;
oldStartLine2?: number; oldStartLine2?: number;