| b69ab31 | | | 1 | /** |
| b69ab31 | | | 2 | * Portions Copyright (c) Meta Platforms, Inc. and affiliates. |
| b69ab31 | | | 3 | * |
| b69ab31 | | | 4 | * This source code is licensed under the MIT license found in the |
| b69ab31 | | | 5 | * LICENSE file in the root directory of this source tree. |
| b69ab31 | | | 6 | */ |
| b69ab31 | | | 7 | |
| b69ab31 | | | 8 | /* |
| b69ab31 | | | 9 | |
| b69ab31 | | | 10 | Copyright (c) 2020 Jun Wu |
| b69ab31 | | | 11 | |
| b69ab31 | | | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy |
| b69ab31 | | | 13 | of this software and associated documentation files (the "Software"), to deal |
| b69ab31 | | | 14 | in the Software without restriction, including without limitation the rights |
| b69ab31 | | | 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| b69ab31 | | | 16 | copies of the Software, and to permit persons to whom the Software is |
| b69ab31 | | | 17 | furnished to do so, subject to the following conditions: |
| b69ab31 | | | 18 | |
| b69ab31 | | | 19 | The above copyright notice and this permission notice shall be included in all |
| b69ab31 | | | 20 | copies or substantial portions of the Software. |
| b69ab31 | | | 21 | |
| b69ab31 | | | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| b69ab31 | | | 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| b69ab31 | | | 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| b69ab31 | | | 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| b69ab31 | | | 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| b69ab31 | | | 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| b69ab31 | | | 28 | SOFTWARE. |
| b69ab31 | | | 29 | |
| b69ab31 | | | 30 | */ |
| b69ab31 | | | 31 | |
| b69ab31 | | | 32 | // Read D43857949 about the choice of the diff library. |
| b69ab31 | | | 33 | import diffSequences from 'diff-sequences'; |
| b69ab31 | | | 34 | |
| b69ab31 | | | 35 | /** Index of a line. Starts from 0. */ |
| b69ab31 | | | 36 | export type LineIdx = number; |
| b69ab31 | | | 37 | |
| b69ab31 | | | 38 | /** |
| b69ab31 | | | 39 | * Calculate the line differences. For performance, this function only |
| b69ab31 | | | 40 | * returns the line indexes for different chunks. The line contents |
| b69ab31 | | | 41 | * are not returned. |
| b69ab31 | | | 42 | * |
| b69ab31 | | | 43 | * @param aLines lines on the "a" side. |
| b69ab31 | | | 44 | * @param bLines lines on the "b" side. |
| b69ab31 | | | 45 | * @param readable whether to use `readableDiffBlocks`. |
| b69ab31 | | | 46 | * @returns A list of `(a1, a2, b1, b2)` tuples for the line ranges that |
| b69ab31 | | | 47 | * are different between "a" and "b". |
| b69ab31 | | | 48 | */ |
| b69ab31 | | | 49 | export function diffLines( |
| b69ab31 | | | 50 | aLines: string[], |
| b69ab31 | | | 51 | bLines: string[], |
| b69ab31 | | | 52 | readable = true, |
| b69ab31 | | | 53 | ): [LineIdx, LineIdx, LineIdx, LineIdx][] { |
| b69ab31 | | | 54 | return (readable ? readableDiffBlocks : diffBlocks)(aLines, bLines) |
| b69ab31 | | | 55 | .filter(([sign, _range]) => sign === '!') |
| b69ab31 | | | 56 | .map(([_sign, range]) => range); |
| b69ab31 | | | 57 | } |
| b69ab31 | | | 58 | |
| b69ab31 | | | 59 | /** |
| b69ab31 | | | 60 | * Calculate the line differences. For performance, this function returns |
| b69ab31 | | | 61 | * line ranges not line contents. |
| b69ab31 | | | 62 | * |
| b69ab31 | | | 63 | * Similar to Mercurial's `mdiff.allblocks`. |
| b69ab31 | | | 64 | * |
| b69ab31 | | | 65 | * @param aLines lines on the "a" side. |
| b69ab31 | | | 66 | * @param bLines lines on the "b" side. |
| b69ab31 | | | 67 | * @returns A list of `[sign, [a1, a2, b1, b2]]` tuples for the line ranges. |
| b69ab31 | | | 68 | * If `sign` is `'='`, the a1 to a2 range on the a side, and b1 to b2 range |
| b69ab31 | | | 69 | * on the b side are the same on both sides. Otherwise, `sign` is `'!'` |
| b69ab31 | | | 70 | * meaning the ranges are different. |
| b69ab31 | | | 71 | */ |
| b69ab31 | | | 72 | export function diffBlocks(aLines: string[], bLines: string[]): Array<Block> { |
| b69ab31 | | | 73 | // Avoid O(string length) comparison. |
| b69ab31 | | | 74 | const [aList, bList] = stringsToInts([aLines, bLines]); |
| b69ab31 | | | 75 | return diffIntBlocks(aList, bList); |
| b69ab31 | | | 76 | } |
| b69ab31 | | | 77 | |
| b69ab31 | | | 78 | /** |
| b69ab31 | | | 79 | * Similar to `diffBlocks`, but attempt to produce more human-readable diff. |
| b69ab31 | | | 80 | * |
| b69ab31 | | | 81 | * Most of the time, a diff is less human-readable because "insignificant" |
| b69ab31 | | | 82 | * lines like "{" or "}" are matched. Practically, matching the actual code |
| b69ab31 | | | 83 | * (significant) lines should have high priority for "readability". |
| b69ab31 | | | 84 | * |
| b69ab31 | | | 85 | * To prioritize significant lines, run the main diff algorithm on the |
| b69ab31 | | | 86 | * significant lines first, force match the equal lines. Those equal lines |
| b69ab31 | | | 87 | * will split the insignificant lines into smaller regions. Run the diff |
| b69ab31 | | | 88 | * algorithm on those regions. |
| b69ab31 | | | 89 | * |
| b69ab31 | | | 90 | * Because insignificant lines are skipped, this function might not always |
| b69ab31 | | | 91 | * produce the theoretical minimal diff like `diffBlocks`. |
| b69ab31 | | | 92 | */ |
| b69ab31 | | | 93 | export function readableDiffBlocks(aLines: string[], bLines: string[]): Array<Block> { |
| b69ab31 | | | 94 | const aIsSignificant = aLines.map(isSignificantLine); |
| b69ab31 | | | 95 | const bIsSignificant = bLines.map(isSignificantLine); |
| b69ab31 | | | 96 | const insignificantCount = |
| b69ab31 | | | 97 | aIsSignificant.reduce((a, b) => a + (b ? 0 : 1), 0) + |
| b69ab31 | | | 98 | bIsSignificant.reduce((a, b) => a + (b ? 0 : 1), 0); |
| b69ab31 | | | 99 | if (insignificantCount > (aLines.length + bLines.length + 3) / 2) { |
| b69ab31 | | | 100 | // Too many insignificant lines. The algorithm might be a bad fit. |
| b69ab31 | | | 101 | // Use regular diff instead. |
| b69ab31 | | | 102 | return diffBlocks(aLines, bLines); |
| b69ab31 | | | 103 | } |
| b69ab31 | | | 104 | |
| b69ab31 | | | 105 | // Assign integer ids to lines. |
| b69ab31 | | | 106 | const [aFull, bFull] = stringsToInts([aLines, bLines]); |
| b69ab31 | | | 107 | |
| b69ab31 | | | 108 | // Significant lines. |
| b69ab31 | | | 109 | const aSignificant = aFull.filter((_l, i) => aIsSignificant[i]); |
| b69ab31 | | | 110 | const bSignificant = bFull.filter((_l, i) => bIsSignificant[i]); |
| b69ab31 | | | 111 | |
| b69ab31 | | | 112 | // Index offset. aSignificant[i] == aFull[aSigToFull[i]]. |
| b69ab31 | | | 113 | const aSigToFull = calculateSigToFull(aIsSignificant); |
| b69ab31 | | | 114 | const bSigToFull = calculateSigToFull(bIsSignificant); |
| b69ab31 | | | 115 | |
| b69ab31 | | | 116 | let aLast = 0; |
| b69ab31 | | | 117 | let bLast = 0; |
| b69ab31 | | | 118 | const result: Array<Block> = []; |
| b69ab31 | | | 119 | const push = (blocks: ReadonlyArray<Block>, aOffset = 0, bOffset = 0) => { |
| b69ab31 | | | 120 | blocks.forEach(([sign, [origA1, origA2, origB1, origB2]]) => { |
| b69ab31 | | | 121 | const a1 = origA1 + aOffset; |
| b69ab31 | | | 122 | const a2 = origA2 + aOffset; |
| b69ab31 | | | 123 | const b1 = origB1 + bOffset; |
| b69ab31 | | | 124 | const b2 = origB2 + bOffset; |
| b69ab31 | | | 125 | const last = result.at(-1); |
| b69ab31 | | | 126 | if (last?.[0] === sign && last[1][1] === a1 && last[1][3] === b1) { |
| b69ab31 | | | 127 | last[1][1] = a2; |
| b69ab31 | | | 128 | last[1][3] = b2; |
| b69ab31 | | | 129 | } else { |
| b69ab31 | | | 130 | result.push([sign, [a1, a2, b1, b2]]); |
| b69ab31 | | | 131 | } |
| b69ab31 | | | 132 | }); |
| b69ab31 | | | 133 | }; |
| b69ab31 | | | 134 | |
| b69ab31 | | | 135 | diffIntBlocks(aSignificant, bSignificant).forEach( |
| b69ab31 | | | 136 | ([sign, [aSigStart, aSigEnd, bSigStart, _bSigEnd]]) => { |
| b69ab31 | | | 137 | if (sign === '=') { |
| b69ab31 | | | 138 | for (let a1Sig = aSigStart; a1Sig < aSigEnd; a1Sig++) { |
| b69ab31 | | | 139 | const b1Sig = bSigStart + (a1Sig - aSigStart); |
| b69ab31 | | | 140 | const [a1, b1] = [aSigToFull[a1Sig], bSigToFull[b1Sig]]; |
| b69ab31 | | | 141 | const [a2, b2] = [a1 + 1, b1 + 1]; |
| b69ab31 | | | 142 | // Force match the two sides. Run regular diff on the insignificant region. |
| b69ab31 | | | 143 | // .. aLast | aLast .. a1 | a1 .. a2 | a2 ... |
| b69ab31 | | | 144 | // .. aLast | bLast .. b1 | b1 .. b2 | b2 ... |
| b69ab31 | | | 145 | // ---------------------------------------------------------------- |
| b69ab31 | | | 146 | // already in | insignificant | significant = | rest |
| b69ab31 | | | 147 | // result | to diff | force matched | to figure out later |
| b69ab31 | | | 148 | push(diffIntBlocks(aFull.slice(aLast, a1), bFull.slice(bLast, b1)), aLast, bLast); |
| b69ab31 | | | 149 | push([['=', [a1, a2, b1, b2]]]); |
| b69ab31 | | | 150 | [aLast, bLast] = [a2, b2]; |
| b69ab31 | | | 151 | } |
| b69ab31 | | | 152 | } |
| b69ab31 | | | 153 | }, |
| b69ab31 | | | 154 | ); |
| b69ab31 | | | 155 | if (aLast < aFull.length || bLast < bFull.length) { |
| b69ab31 | | | 156 | push(diffIntBlocks(aFull.slice(aLast), bFull.slice(bLast)), aLast, bLast); |
| b69ab31 | | | 157 | } |
| b69ab31 | | | 158 | return result; |
| b69ab31 | | | 159 | } |
| b69ab31 | | | 160 | |
| b69ab31 | | | 161 | /** |
| b69ab31 | | | 162 | * Given a boolean array a[i], calculate b[i] so b[i] is the i-th `true` index in a[i], |
| b69ab31 | | | 163 | * aka. a.slice(0, b[i]) has a total of `i` `true` values. |
| b69ab31 | | | 164 | */ |
| b69ab31 | | | 165 | function calculateSigToFull(isSignificant: ReadonlyArray<boolean>): ReadonlyArray<number> { |
| b69ab31 | | | 166 | const result: number[] = []; |
| b69ab31 | | | 167 | for (let i = 0; i < isSignificant.length; i++) { |
| b69ab31 | | | 168 | if (isSignificant[i]) { |
| b69ab31 | | | 169 | result.push(i); |
| b69ab31 | | | 170 | } |
| b69ab31 | | | 171 | } |
| b69ab31 | | | 172 | result.push(isSignificant.length); |
| b69ab31 | | | 173 | return result; |
| b69ab31 | | | 174 | } |
| b69ab31 | | | 175 | |
| b69ab31 | | | 176 | /** |
| b69ab31 | | | 177 | * Test if a line is "significant". Used by `readableDiffBlocks`. |
| b69ab31 | | | 178 | * Blank lines, "{", "}", "/**" are insignificant. |
| b69ab31 | | | 179 | */ |
| b69ab31 | | | 180 | function isSignificantLine(line: string): boolean { |
| b69ab31 | | | 181 | return line.match(/^[\s{}/*]*$/) == null; |
| b69ab31 | | | 182 | } |
| b69ab31 | | | 183 | |
| b69ab31 | | | 184 | /** |
| b69ab31 | | | 185 | * Similar to `diffBlocks`, but takes integer array instead. |
| b69ab31 | | | 186 | */ |
| b69ab31 | | | 187 | function diffIntBlocks(aList: ReadonlyArray<number>, bList: ReadonlyArray<number>): Array<Block> { |
| b69ab31 | | | 188 | // Skip common prefix and suffix. |
| b69ab31 | | | 189 | let aLen = aList.length; |
| b69ab31 | | | 190 | let bLen = bList.length; |
| b69ab31 | | | 191 | const minLen = Math.min(aLen, bLen); |
| b69ab31 | | | 192 | let commonPrefixLen = 0; |
| b69ab31 | | | 193 | let commonSuffixLen = 0; |
| b69ab31 | | | 194 | while (commonPrefixLen < minLen && aList[commonPrefixLen] === bList[commonPrefixLen]) { |
| b69ab31 | | | 195 | commonPrefixLen += 1; |
| b69ab31 | | | 196 | } |
| b69ab31 | | | 197 | while (aLen > commonPrefixLen && bLen > commonPrefixLen && aList[aLen - 1] === bList[bLen - 1]) { |
| b69ab31 | | | 198 | aLen -= 1; |
| b69ab31 | | | 199 | bLen -= 1; |
| b69ab31 | | | 200 | commonSuffixLen += 1; |
| b69ab31 | | | 201 | } |
| b69ab31 | | | 202 | aLen -= commonPrefixLen; |
| b69ab31 | | | 203 | bLen -= commonPrefixLen; |
| b69ab31 | | | 204 | |
| b69ab31 | | | 205 | const blocks: Array<Block> = []; |
| b69ab31 | | | 206 | if (commonPrefixLen > 0) { |
| b69ab31 | | | 207 | blocks.push(['=', [0, commonPrefixLen, 0, commonPrefixLen]]); |
| b69ab31 | | | 208 | } |
| b69ab31 | | | 209 | |
| b69ab31 | | | 210 | // Run the diff algorithm. |
| b69ab31 | | | 211 | let a1 = 0; |
| b69ab31 | | | 212 | let b1 = 0; |
| b69ab31 | | | 213 | |
| b69ab31 | | | 214 | function isCommon(aIndex: number, bIndex: number) { |
| b69ab31 | | | 215 | return aList[aIndex + commonPrefixLen] === bList[bIndex + commonPrefixLen]; |
| b69ab31 | | | 216 | } |
| b69ab31 | | | 217 | |
| b69ab31 | | | 218 | function foundSequence(n: LineIdx, a2: LineIdx, b2: LineIdx) { |
| b69ab31 | | | 219 | if (a1 !== a2 || b1 !== b2) { |
| b69ab31 | | | 220 | blocks.push([ |
| b69ab31 | | | 221 | '!', |
| b69ab31 | | | 222 | [a1 + commonPrefixLen, a2 + commonPrefixLen, b1 + commonPrefixLen, b2 + commonPrefixLen], |
| b69ab31 | | | 223 | ]); |
| b69ab31 | | | 224 | } |
| b69ab31 | | | 225 | if (n > 0) { |
| b69ab31 | | | 226 | blocks.push([ |
| b69ab31 | | | 227 | '=', |
| b69ab31 | | | 228 | [ |
| b69ab31 | | | 229 | a2 + commonPrefixLen, |
| b69ab31 | | | 230 | a2 + n + commonPrefixLen, |
| b69ab31 | | | 231 | b2 + commonPrefixLen, |
| b69ab31 | | | 232 | b2 + n + commonPrefixLen, |
| b69ab31 | | | 233 | ], |
| b69ab31 | | | 234 | ]); |
| b69ab31 | | | 235 | } |
| b69ab31 | | | 236 | a1 = a2 + n; |
| b69ab31 | | | 237 | b1 = b2 + n; |
| b69ab31 | | | 238 | } |
| b69ab31 | | | 239 | |
| b69ab31 | | | 240 | diffSequences(aLen, bLen, isCommon, foundSequence); |
| b69ab31 | | | 241 | foundSequence(commonSuffixLen, aLen, bLen); |
| b69ab31 | | | 242 | |
| b69ab31 | | | 243 | return blocks; |
| b69ab31 | | | 244 | } |
| b69ab31 | | | 245 | |
| b69ab31 | | | 246 | /** |
| b69ab31 | | | 247 | * Post process `blocks` from `diffBlocks` to collapse unchanged lines. |
| b69ab31 | | | 248 | * `contextLineCount` lines before or after a `!` (changed) block are |
| b69ab31 | | | 249 | * not collapsed. |
| b69ab31 | | | 250 | * |
| b69ab31 | | | 251 | * If `isALineExpanded(aLine, bLine)` returns `true`, then the _block_ |
| b69ab31 | | | 252 | * is expanded. |
| b69ab31 | | | 253 | * |
| b69ab31 | | | 254 | * Split `=` blocks into `=` and `~` blocks. The `~` blocks are expected |
| b69ab31 | | | 255 | * to be collapsed. |
| b69ab31 | | | 256 | */ |
| b69ab31 | | | 257 | export function collapseContextBlocks( |
| b69ab31 | | | 258 | blocks: Array<Block>, |
| b69ab31 | | | 259 | isLineExpanded: (aLine: LineIdx, bLine: LineIdx) => boolean, |
| b69ab31 | | | 260 | contextLineCount = 3, |
| b69ab31 | | | 261 | ): Array<ContextBlock> { |
| b69ab31 | | | 262 | const collapsedBlocks: Array<ContextBlock> = []; |
| b69ab31 | | | 263 | blocks.forEach((block, i) => { |
| b69ab31 | | | 264 | const [sign, [a1, a2, b1, b2]] = block; |
| b69ab31 | | | 265 | if (sign === '!') { |
| b69ab31 | | | 266 | collapsedBlocks.push(block); |
| b69ab31 | | | 267 | } else if (sign === '=') { |
| b69ab31 | | | 268 | // a1 ... a1 + topContext ... a2 - bottomContext ... a2 |
| b69ab31 | | | 269 | // ^^^ collapse this range (c1 to c2) |
| b69ab31 | | | 270 | // The topContext and bottomContext can be 0 lines if they are not adjacent |
| b69ab31 | | | 271 | // to a diff block. |
| b69ab31 | | | 272 | const topContext = i == 0 || blocks[i - 1][0] !== '!' ? 0 : contextLineCount; |
| b69ab31 | | | 273 | const bottomContext = |
| b69ab31 | | | 274 | i + 1 == blocks.length || blocks[i + 1][0] !== '!' ? 0 : contextLineCount; |
| b69ab31 | | | 275 | const c1 = Math.min(a1 + topContext, a2); |
| b69ab31 | | | 276 | const c2 = Math.max(c1, a2 - bottomContext); |
| b69ab31 | | | 277 | const aToB = b1 - a1; |
| b69ab31 | | | 278 | if (c1 >= c2 || isLineExpanded(c1, c1 + aToB) || isLineExpanded(c2 - 1, c2 - 1 + aToB)) { |
| b69ab31 | | | 279 | // Nothing to collapse. |
| b69ab31 | | | 280 | collapsedBlocks.push(block); |
| b69ab31 | | | 281 | } else { |
| b69ab31 | | | 282 | // Split. Collapse c1 .. c2 range. |
| b69ab31 | | | 283 | if (c1 > a1) { |
| b69ab31 | | | 284 | collapsedBlocks.push(['=', [a1, c1, b1, c1 + aToB]]); |
| b69ab31 | | | 285 | } |
| b69ab31 | | | 286 | collapsedBlocks.push(['~', [c1, c2, c1 + aToB, c2 + aToB]]); |
| b69ab31 | | | 287 | if (c2 < a2) { |
| b69ab31 | | | 288 | collapsedBlocks.push(['=', [c2, a2, c2 + aToB, b2]]); |
| b69ab31 | | | 289 | } |
| b69ab31 | | | 290 | } |
| b69ab31 | | | 291 | } |
| b69ab31 | | | 292 | }); |
| b69ab31 | | | 293 | return collapsedBlocks; |
| b69ab31 | | | 294 | } |
| b69ab31 | | | 295 | |
| b69ab31 | | | 296 | /** |
| b69ab31 | | | 297 | * Merge diffBlocks(a, b) and diffBlocks(c, b). |
| b69ab31 | | | 298 | * Any difference (between a and b, or c and b) generates a `!` block. |
| b69ab31 | | | 299 | * The (a1, a2) line numbers in the output blocks are changed to (b1, b2). |
| b69ab31 | | | 300 | * Preserve empty (a1 == a2, b1 == b2) '!' blocks for context line calculation. |
| b69ab31 | | | 301 | */ |
| b69ab31 | | | 302 | export function mergeBlocks(abBlocks: Array<Block>, cbBlocks: Array<Block>): Array<Block> { |
| b69ab31 | | | 303 | let i = 0; // Index of abBlocks. |
| b69ab31 | | | 304 | let j = 0; // Index of cbBlocks. |
| b69ab31 | | | 305 | let start = 0; // "Current" line index of b. |
| b69ab31 | | | 306 | const result: Array<Block> = []; |
| b69ab31 | | | 307 | |
| b69ab31 | | | 308 | const push = (sign: Sign, end: number) => { |
| b69ab31 | | | 309 | const last = result.at(-1); |
| b69ab31 | | | 310 | if (last?.[0] === sign) { |
| b69ab31 | | | 311 | last[1][1] = end; |
| b69ab31 | | | 312 | last[1][3] = end; |
| b69ab31 | | | 313 | } else { |
| b69ab31 | | | 314 | result.push([sign, [start, end, start, end]]); |
| b69ab31 | | | 315 | } |
| b69ab31 | | | 316 | start = end; |
| b69ab31 | | | 317 | }; |
| b69ab31 | | | 318 | |
| b69ab31 | | | 319 | while (i < abBlocks.length && j < cbBlocks.length) { |
| b69ab31 | | | 320 | const [sign1, [, , b11, b12]] = abBlocks[i]; |
| b69ab31 | | | 321 | if (b11 === b12 && b12 === start && sign1 === '!') { |
| b69ab31 | | | 322 | push(sign1, start); |
| b69ab31 | | | 323 | } |
| b69ab31 | | | 324 | if (b12 <= start) { |
| b69ab31 | | | 325 | ++i; |
| b69ab31 | | | 326 | continue; |
| b69ab31 | | | 327 | } |
| b69ab31 | | | 328 | const [sign2, [, , b21, b22]] = cbBlocks[j]; |
| b69ab31 | | | 329 | if (b21 === b22 && b21 === start && sign2 === '!') { |
| b69ab31 | | | 330 | push(sign2, start); |
| b69ab31 | | | 331 | } |
| b69ab31 | | | 332 | if (b22 <= start) { |
| b69ab31 | | | 333 | ++j; |
| b69ab31 | | | 334 | continue; |
| b69ab31 | | | 335 | } |
| b69ab31 | | | 336 | |
| b69ab31 | | | 337 | // Minimal "end" so there cannot be 2 different signs in the start-end range |
| b69ab31 | | | 338 | // on either side. Note 2 sides might have different signs. |
| b69ab31 | | | 339 | const end = Math.min(...[b11, b12, b21, b22].filter(i => i > start)); |
| b69ab31 | | | 340 | |
| b69ab31 | | | 341 | // Figure out the sign of the start-end range. |
| b69ab31 | | | 342 | let sign: Sign = '='; |
| b69ab31 | | | 343 | if ( |
| b69ab31 | | | 344 | (start >= b11 && end <= b12 && sign1 === '!') || |
| b69ab31 | | | 345 | (start >= b21 && end <= b22 && sign2 === '!') |
| b69ab31 | | | 346 | ) { |
| b69ab31 | | | 347 | sign = '!'; |
| b69ab31 | | | 348 | } |
| b69ab31 | | | 349 | push(sign, end); |
| b69ab31 | | | 350 | } |
| b69ab31 | | | 351 | |
| b69ab31 | | | 352 | return result; |
| b69ab31 | | | 353 | } |
| b69ab31 | | | 354 | |
| b69ab31 | | | 355 | /** Indicates whether a block is same or different on both sides. */ |
| b69ab31 | | | 356 | export type Sign = '=' | '!'; |
| b69ab31 | | | 357 | |
| b69ab31 | | | 358 | /** Return type of `diffBlocks`. */ |
| b69ab31 | | | 359 | export type Block = [Sign, [LineIdx, LineIdx, LineIdx, LineIdx]]; |
| b69ab31 | | | 360 | |
| b69ab31 | | | 361 | /** Return type of `collapseContextLines`. */ |
| b69ab31 | | | 362 | export type ContextBlock = [Sign | '~', [LineIdx, LineIdx, LineIdx, LineIdx]]; |
| b69ab31 | | | 363 | |
| b69ab31 | | | 364 | /** |
| b69ab31 | | | 365 | * Split lines by `\n`. Preserve the end of lines. |
| b69ab31 | | | 366 | */ |
| b69ab31 | | | 367 | export function splitLines(s: string): string[] { |
| b69ab31 | | | 368 | let pos = 0; |
| b69ab31 | | | 369 | let nextPos = 0; |
| b69ab31 | | | 370 | const result = []; |
| b69ab31 | | | 371 | while (pos < s.length) { |
| b69ab31 | | | 372 | nextPos = s.indexOf('\n', pos); |
| b69ab31 | | | 373 | if (nextPos === -1) { |
| b69ab31 | | | 374 | nextPos = s.length - 1; |
| b69ab31 | | | 375 | } |
| b69ab31 | | | 376 | result.push(s.slice(pos, nextPos + 1)); |
| b69ab31 | | | 377 | pos = nextPos + 1; |
| b69ab31 | | | 378 | } |
| b69ab31 | | | 379 | return result; |
| b69ab31 | | | 380 | } |
| b69ab31 | | | 381 | |
| b69ab31 | | | 382 | /** |
| b69ab31 | | | 383 | * Make strings with the same content use the same integer |
| b69ab31 | | | 384 | * for fast comparison. |
| b69ab31 | | | 385 | */ |
| b69ab31 | | | 386 | function stringsToInts(linesArray: string[][]): number[][] { |
| b69ab31 | | | 387 | // This is similar to diff-match-patch's diff_linesToChars_ but is not |
| b69ab31 | | | 388 | // limited to 65536 unique lines. |
| b69ab31 | | | 389 | const lineMap = new Map<string, number>(); |
| b69ab31 | | | 390 | return linesArray.map(lines => |
| b69ab31 | | | 391 | lines.map(line => { |
| b69ab31 | | | 392 | const existingId = lineMap.get(line); |
| b69ab31 | | | 393 | if (existingId != null) { |
| b69ab31 | | | 394 | return existingId; |
| b69ab31 | | | 395 | } else { |
| b69ab31 | | | 396 | const id = lineMap.size; |
| b69ab31 | | | 397 | lineMap.set(line, id); |
| b69ab31 | | | 398 | return id; |
| b69ab31 | | | 399 | } |
| b69ab31 | | | 400 | }), |
| b69ab31 | | | 401 | ); |
| b69ab31 | | | 402 | } |