diff --git a/addons/isl/package.json b/addons/isl/package.json index 9e80da8641d13..fb33f486baa97 100644 --- a/addons/isl/package.json +++ b/addons/isl/package.json @@ -6,14 +6,12 @@ "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.2.1", - "@types/diff": "^5.0.2", "@types/jest": "^27.0.1", "@types/node": "^16.7.13", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "@vscode/webview-ui-toolkit": "^1.0.0", - "diff": "^5.0.0", - "diff-match-patch": "^1.0.5", + "diff-sequences": "^29.4.3", "isl-server": "0.0.0", "react": "^18.1.0", "react-dom": "^18.1.0", @@ -23,7 +21,6 @@ "typescript": "^4.4.2" }, "devDependencies": { - "@types/diff-match-patch": "^1.0.32", "rewire": "^6.0.0", "ts-loader": "^9.3.1" }, diff --git a/addons/isl/src/linelog.ts b/addons/isl/src/linelog.ts index 426e4a2f44fa7..5f0785966db10 100644 --- a/addons/isl/src/linelog.ts +++ b/addons/isl/src/linelog.ts @@ -29,13 +29,8 @@ SOFTWARE. */ -import {diff_match_patch} from 'diff-match-patch'; - -const dmp = new diff_match_patch(); - -// The timeout does not seem to affect dmp performance. -// But bumping it produces better diff results. -dmp.Diff_Timeout = 10000; +// Read D43857949 about the choice of the diff library. +import diffSequences from 'diff-sequences'; /** Operation code. */ enum Op { @@ -400,13 +395,13 @@ class LineLog { const [aRev, bRev] = rev ? [rev, rev] : [this.maxRev, this.maxRev + 1]; const b = text; - const lines = splitLines(b); + const bLines = splitLines(b); this.checkOut(aRev); - const a = this.content; - const blocks = diffLines(a, b); + const aLines = splitLines(this.content); + const blocks = diffLines(aLines, bLines); blocks.reverse().forEach(([a1, a2, b1, b2]) => { - this.editChunk(a1, a2, bRev, lines.slice(b1, b2)); + this.editChunk(a1, a2, bRev, bLines.slice(b1, b2)); }); this.content = b; this.lastCheckoutKey = `${bRev},null`; @@ -429,41 +424,59 @@ class LineLog { } /** - * Calculate the differences. + * Calculate the line differences. For performance, this function only + * returns the line indexes for different chunks. The line contents + * are not returned. * - * @param a Content of the "a" side. - * @param b Content of the "b" side. + * @param aLines lines on the "a" side. + * @param bLines lines on the "b" side. * @returns A list of `(a1, a2, b1, b2)` tuples for the line ranges that * are different between "a" and "b". */ -function diffLines(a: string, b: string): [LineIdx, LineIdx, LineIdx, LineIdx][] { - const {chars1, chars2} = dmp.diff_linesToChars_(a, b); +function diffLines(aLines: string[], bLines: string[]): [LineIdx, LineIdx, LineIdx, LineIdx][] { + // Avoid O(string length) comparison. + const [aList, bList] = stringsToInts([aLines, bLines]); + + // Skip common prefix and suffix. + let aLen = aList.length; + let bLen = bList.length; + const minLen = Math.min(aLen, bLen); + let commonPrefixLen = 0; + while (commonPrefixLen < minLen && aList[commonPrefixLen] === bList[commonPrefixLen]) { + commonPrefixLen += 1; + } + while (aLen > commonPrefixLen && bLen > commonPrefixLen && aList[aLen - 1] === bList[bLen - 1]) { + aLen -= 1; + bLen -= 1; + } + aLen -= commonPrefixLen; + bLen -= commonPrefixLen; + + // Run the diff algorithm. const blocks: [LineIdx, LineIdx, LineIdx, LineIdx][] = []; - let a1 = 0, - a2 = 0, - b1 = 0, - b2 = 0; - const push = (len: number) => { + let a1 = 0; + let b1 = 0; + + function isCommon(aIndex: number, bIndex: number) { + return aList[aIndex + commonPrefixLen] === bList[bIndex + commonPrefixLen]; + } + + function foundSequence(n: LineIdx, a2: LineIdx, b2: LineIdx) { if (a1 !== a2 || b1 !== b2) { - blocks.push([a1, a2, b1, b2]); - } - a1 = a2 = a2 + len; - b1 = b2 = b2 + len; - }; - dmp.diff_main(chars1, chars2, false).forEach(x => { - const [op, chars] = x; - const len = chars.length; - if (op === 0) { - push(len); - } - if (op < 0) { - a2 += len; + blocks.push([ + a1 + commonPrefixLen, + a2 + commonPrefixLen, + b1 + commonPrefixLen, + b2 + commonPrefixLen, + ]); } - if (op > 0) { - b2 += len; - } - }); - push(0); + a1 = a2 + n; + b1 = b2 + n; + } + + diffSequences(aLen, bLen, isCommon, foundSequence); + foundSequence(0, aLen, bLen); + return blocks; } @@ -485,6 +498,28 @@ function splitLines(s: string): string[] { return result; } +/** + * Make strings with the same content use the same integer + * for fast comparasion. + */ +function stringsToInts(linesArray: string[][]): number[][] { + // This is similar to diff-match-patch's diff_linesToChars_ but is not + // limited to 65536 unique lines. + const lineMap = new Map(); + return linesArray.map(lines => + lines.map(line => { + const existingId = lineMap.get(line); + if (existingId != null) { + return existingId; + } else { + const id = lineMap.size; + lineMap.set(line, id); + return id; + } + }), + ); +} + /** If the assertion fails, throw an `Error` with the given `message`. */ function assert(condition: boolean, message: string) { if (!condition) { diff --git a/addons/yarn.lock b/addons/yarn.lock index b882dab276153..0f3047ed29469 100644 --- a/addons/yarn.lock +++ b/addons/yarn.lock @@ -2517,11 +2517,6 @@ dependencies: "@types/node" "*" -"@types/diff-match-patch@^1.0.32": - version "1.0.32" - resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz#d9c3b8c914aa8229485351db4865328337a3d09f" - integrity sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A== - "@types/diff@^5.0.2": version "5.0.2" resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.2.tgz#dd565e0086ccf8bc6522c6ebafd8a3125c91c12b" @@ -4813,11 +4808,6 @@ didyoumean@^1.2.2: resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== -diff-match-patch@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" - integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== - diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" @@ -4828,6 +4818,11 @@ diff-sequences@^28.1.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== +diff-sequences@^29.4.3: + version "29.4.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" + integrity sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"