diffComputer.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { LcsDiff } from '../../../base/common/diff/diff.js';
  6. import * as strings from '../../../base/common/strings.js';
  7. const MINIMUM_MATCHING_CHARACTER_LENGTH = 3;
  8. function computeDiff(originalSequence, modifiedSequence, continueProcessingPredicate, pretty) {
  9. const diffAlgo = new LcsDiff(originalSequence, modifiedSequence, continueProcessingPredicate);
  10. return diffAlgo.ComputeDiff(pretty);
  11. }
  12. class LineSequence {
  13. constructor(lines) {
  14. const startColumns = [];
  15. const endColumns = [];
  16. for (let i = 0, length = lines.length; i < length; i++) {
  17. startColumns[i] = getFirstNonBlankColumn(lines[i], 1);
  18. endColumns[i] = getLastNonBlankColumn(lines[i], 1);
  19. }
  20. this.lines = lines;
  21. this._startColumns = startColumns;
  22. this._endColumns = endColumns;
  23. }
  24. getElements() {
  25. const elements = [];
  26. for (let i = 0, len = this.lines.length; i < len; i++) {
  27. elements[i] = this.lines[i].substring(this._startColumns[i] - 1, this._endColumns[i] - 1);
  28. }
  29. return elements;
  30. }
  31. getStrictElement(index) {
  32. return this.lines[index];
  33. }
  34. getStartLineNumber(i) {
  35. return i + 1;
  36. }
  37. getEndLineNumber(i) {
  38. return i + 1;
  39. }
  40. createCharSequence(shouldIgnoreTrimWhitespace, startIndex, endIndex) {
  41. const charCodes = [];
  42. const lineNumbers = [];
  43. const columns = [];
  44. let len = 0;
  45. for (let index = startIndex; index <= endIndex; index++) {
  46. const lineContent = this.lines[index];
  47. const startColumn = (shouldIgnoreTrimWhitespace ? this._startColumns[index] : 1);
  48. const endColumn = (shouldIgnoreTrimWhitespace ? this._endColumns[index] : lineContent.length + 1);
  49. for (let col = startColumn; col < endColumn; col++) {
  50. charCodes[len] = lineContent.charCodeAt(col - 1);
  51. lineNumbers[len] = index + 1;
  52. columns[len] = col;
  53. len++;
  54. }
  55. }
  56. return new CharSequence(charCodes, lineNumbers, columns);
  57. }
  58. }
  59. class CharSequence {
  60. constructor(charCodes, lineNumbers, columns) {
  61. this._charCodes = charCodes;
  62. this._lineNumbers = lineNumbers;
  63. this._columns = columns;
  64. }
  65. getElements() {
  66. return this._charCodes;
  67. }
  68. getStartLineNumber(i) {
  69. return this._lineNumbers[i];
  70. }
  71. getStartColumn(i) {
  72. return this._columns[i];
  73. }
  74. getEndLineNumber(i) {
  75. return this._lineNumbers[i];
  76. }
  77. getEndColumn(i) {
  78. return this._columns[i] + 1;
  79. }
  80. }
  81. class CharChange {
  82. constructor(originalStartLineNumber, originalStartColumn, originalEndLineNumber, originalEndColumn, modifiedStartLineNumber, modifiedStartColumn, modifiedEndLineNumber, modifiedEndColumn) {
  83. this.originalStartLineNumber = originalStartLineNumber;
  84. this.originalStartColumn = originalStartColumn;
  85. this.originalEndLineNumber = originalEndLineNumber;
  86. this.originalEndColumn = originalEndColumn;
  87. this.modifiedStartLineNumber = modifiedStartLineNumber;
  88. this.modifiedStartColumn = modifiedStartColumn;
  89. this.modifiedEndLineNumber = modifiedEndLineNumber;
  90. this.modifiedEndColumn = modifiedEndColumn;
  91. }
  92. static createFromDiffChange(diffChange, originalCharSequence, modifiedCharSequence) {
  93. let originalStartLineNumber;
  94. let originalStartColumn;
  95. let originalEndLineNumber;
  96. let originalEndColumn;
  97. let modifiedStartLineNumber;
  98. let modifiedStartColumn;
  99. let modifiedEndLineNumber;
  100. let modifiedEndColumn;
  101. if (diffChange.originalLength === 0) {
  102. originalStartLineNumber = 0;
  103. originalStartColumn = 0;
  104. originalEndLineNumber = 0;
  105. originalEndColumn = 0;
  106. }
  107. else {
  108. originalStartLineNumber = originalCharSequence.getStartLineNumber(diffChange.originalStart);
  109. originalStartColumn = originalCharSequence.getStartColumn(diffChange.originalStart);
  110. originalEndLineNumber = originalCharSequence.getEndLineNumber(diffChange.originalStart + diffChange.originalLength - 1);
  111. originalEndColumn = originalCharSequence.getEndColumn(diffChange.originalStart + diffChange.originalLength - 1);
  112. }
  113. if (diffChange.modifiedLength === 0) {
  114. modifiedStartLineNumber = 0;
  115. modifiedStartColumn = 0;
  116. modifiedEndLineNumber = 0;
  117. modifiedEndColumn = 0;
  118. }
  119. else {
  120. modifiedStartLineNumber = modifiedCharSequence.getStartLineNumber(diffChange.modifiedStart);
  121. modifiedStartColumn = modifiedCharSequence.getStartColumn(diffChange.modifiedStart);
  122. modifiedEndLineNumber = modifiedCharSequence.getEndLineNumber(diffChange.modifiedStart + diffChange.modifiedLength - 1);
  123. modifiedEndColumn = modifiedCharSequence.getEndColumn(diffChange.modifiedStart + diffChange.modifiedLength - 1);
  124. }
  125. return new CharChange(originalStartLineNumber, originalStartColumn, originalEndLineNumber, originalEndColumn, modifiedStartLineNumber, modifiedStartColumn, modifiedEndLineNumber, modifiedEndColumn);
  126. }
  127. }
  128. function postProcessCharChanges(rawChanges) {
  129. if (rawChanges.length <= 1) {
  130. return rawChanges;
  131. }
  132. const result = [rawChanges[0]];
  133. let prevChange = result[0];
  134. for (let i = 1, len = rawChanges.length; i < len; i++) {
  135. const currChange = rawChanges[i];
  136. const originalMatchingLength = currChange.originalStart - (prevChange.originalStart + prevChange.originalLength);
  137. const modifiedMatchingLength = currChange.modifiedStart - (prevChange.modifiedStart + prevChange.modifiedLength);
  138. // Both of the above should be equal, but the continueProcessingPredicate may prevent this from being true
  139. const matchingLength = Math.min(originalMatchingLength, modifiedMatchingLength);
  140. if (matchingLength < MINIMUM_MATCHING_CHARACTER_LENGTH) {
  141. // Merge the current change into the previous one
  142. prevChange.originalLength = (currChange.originalStart + currChange.originalLength) - prevChange.originalStart;
  143. prevChange.modifiedLength = (currChange.modifiedStart + currChange.modifiedLength) - prevChange.modifiedStart;
  144. }
  145. else {
  146. // Add the current change
  147. result.push(currChange);
  148. prevChange = currChange;
  149. }
  150. }
  151. return result;
  152. }
  153. class LineChange {
  154. constructor(originalStartLineNumber, originalEndLineNumber, modifiedStartLineNumber, modifiedEndLineNumber, charChanges) {
  155. this.originalStartLineNumber = originalStartLineNumber;
  156. this.originalEndLineNumber = originalEndLineNumber;
  157. this.modifiedStartLineNumber = modifiedStartLineNumber;
  158. this.modifiedEndLineNumber = modifiedEndLineNumber;
  159. this.charChanges = charChanges;
  160. }
  161. static createFromDiffResult(shouldIgnoreTrimWhitespace, diffChange, originalLineSequence, modifiedLineSequence, continueCharDiff, shouldComputeCharChanges, shouldPostProcessCharChanges) {
  162. let originalStartLineNumber;
  163. let originalEndLineNumber;
  164. let modifiedStartLineNumber;
  165. let modifiedEndLineNumber;
  166. let charChanges = undefined;
  167. if (diffChange.originalLength === 0) {
  168. originalStartLineNumber = originalLineSequence.getStartLineNumber(diffChange.originalStart) - 1;
  169. originalEndLineNumber = 0;
  170. }
  171. else {
  172. originalStartLineNumber = originalLineSequence.getStartLineNumber(diffChange.originalStart);
  173. originalEndLineNumber = originalLineSequence.getEndLineNumber(diffChange.originalStart + diffChange.originalLength - 1);
  174. }
  175. if (diffChange.modifiedLength === 0) {
  176. modifiedStartLineNumber = modifiedLineSequence.getStartLineNumber(diffChange.modifiedStart) - 1;
  177. modifiedEndLineNumber = 0;
  178. }
  179. else {
  180. modifiedStartLineNumber = modifiedLineSequence.getStartLineNumber(diffChange.modifiedStart);
  181. modifiedEndLineNumber = modifiedLineSequence.getEndLineNumber(diffChange.modifiedStart + diffChange.modifiedLength - 1);
  182. }
  183. if (shouldComputeCharChanges && diffChange.originalLength > 0 && diffChange.originalLength < 20 && diffChange.modifiedLength > 0 && diffChange.modifiedLength < 20 && continueCharDiff()) {
  184. // Compute character changes for diff chunks of at most 20 lines...
  185. const originalCharSequence = originalLineSequence.createCharSequence(shouldIgnoreTrimWhitespace, diffChange.originalStart, diffChange.originalStart + diffChange.originalLength - 1);
  186. const modifiedCharSequence = modifiedLineSequence.createCharSequence(shouldIgnoreTrimWhitespace, diffChange.modifiedStart, diffChange.modifiedStart + diffChange.modifiedLength - 1);
  187. let rawChanges = computeDiff(originalCharSequence, modifiedCharSequence, continueCharDiff, true).changes;
  188. if (shouldPostProcessCharChanges) {
  189. rawChanges = postProcessCharChanges(rawChanges);
  190. }
  191. charChanges = [];
  192. for (let i = 0, length = rawChanges.length; i < length; i++) {
  193. charChanges.push(CharChange.createFromDiffChange(rawChanges[i], originalCharSequence, modifiedCharSequence));
  194. }
  195. }
  196. return new LineChange(originalStartLineNumber, originalEndLineNumber, modifiedStartLineNumber, modifiedEndLineNumber, charChanges);
  197. }
  198. }
  199. export class DiffComputer {
  200. constructor(originalLines, modifiedLines, opts) {
  201. this.shouldComputeCharChanges = opts.shouldComputeCharChanges;
  202. this.shouldPostProcessCharChanges = opts.shouldPostProcessCharChanges;
  203. this.shouldIgnoreTrimWhitespace = opts.shouldIgnoreTrimWhitespace;
  204. this.shouldMakePrettyDiff = opts.shouldMakePrettyDiff;
  205. this.originalLines = originalLines;
  206. this.modifiedLines = modifiedLines;
  207. this.original = new LineSequence(originalLines);
  208. this.modified = new LineSequence(modifiedLines);
  209. this.continueLineDiff = createContinueProcessingPredicate(opts.maxComputationTime);
  210. this.continueCharDiff = createContinueProcessingPredicate(opts.maxComputationTime === 0 ? 0 : Math.min(opts.maxComputationTime, 5000)); // never run after 5s for character changes...
  211. }
  212. computeDiff() {
  213. if (this.original.lines.length === 1 && this.original.lines[0].length === 0) {
  214. // empty original => fast path
  215. if (this.modified.lines.length === 1 && this.modified.lines[0].length === 0) {
  216. return {
  217. quitEarly: false,
  218. changes: []
  219. };
  220. }
  221. return {
  222. quitEarly: false,
  223. changes: [{
  224. originalStartLineNumber: 1,
  225. originalEndLineNumber: 1,
  226. modifiedStartLineNumber: 1,
  227. modifiedEndLineNumber: this.modified.lines.length,
  228. charChanges: [{
  229. modifiedEndColumn: 0,
  230. modifiedEndLineNumber: 0,
  231. modifiedStartColumn: 0,
  232. modifiedStartLineNumber: 0,
  233. originalEndColumn: 0,
  234. originalEndLineNumber: 0,
  235. originalStartColumn: 0,
  236. originalStartLineNumber: 0
  237. }]
  238. }]
  239. };
  240. }
  241. if (this.modified.lines.length === 1 && this.modified.lines[0].length === 0) {
  242. // empty modified => fast path
  243. return {
  244. quitEarly: false,
  245. changes: [{
  246. originalStartLineNumber: 1,
  247. originalEndLineNumber: this.original.lines.length,
  248. modifiedStartLineNumber: 1,
  249. modifiedEndLineNumber: 1,
  250. charChanges: [{
  251. modifiedEndColumn: 0,
  252. modifiedEndLineNumber: 0,
  253. modifiedStartColumn: 0,
  254. modifiedStartLineNumber: 0,
  255. originalEndColumn: 0,
  256. originalEndLineNumber: 0,
  257. originalStartColumn: 0,
  258. originalStartLineNumber: 0
  259. }]
  260. }]
  261. };
  262. }
  263. const diffResult = computeDiff(this.original, this.modified, this.continueLineDiff, this.shouldMakePrettyDiff);
  264. const rawChanges = diffResult.changes;
  265. const quitEarly = diffResult.quitEarly;
  266. // The diff is always computed with ignoring trim whitespace
  267. // This ensures we get the prettiest diff
  268. if (this.shouldIgnoreTrimWhitespace) {
  269. const lineChanges = [];
  270. for (let i = 0, length = rawChanges.length; i < length; i++) {
  271. lineChanges.push(LineChange.createFromDiffResult(this.shouldIgnoreTrimWhitespace, rawChanges[i], this.original, this.modified, this.continueCharDiff, this.shouldComputeCharChanges, this.shouldPostProcessCharChanges));
  272. }
  273. return {
  274. quitEarly: quitEarly,
  275. changes: lineChanges
  276. };
  277. }
  278. // Need to post-process and introduce changes where the trim whitespace is different
  279. // Note that we are looping starting at -1 to also cover the lines before the first change
  280. const result = [];
  281. let originalLineIndex = 0;
  282. let modifiedLineIndex = 0;
  283. for (let i = -1 /* !!!! */, len = rawChanges.length; i < len; i++) {
  284. const nextChange = (i + 1 < len ? rawChanges[i + 1] : null);
  285. const originalStop = (nextChange ? nextChange.originalStart : this.originalLines.length);
  286. const modifiedStop = (nextChange ? nextChange.modifiedStart : this.modifiedLines.length);
  287. while (originalLineIndex < originalStop && modifiedLineIndex < modifiedStop) {
  288. const originalLine = this.originalLines[originalLineIndex];
  289. const modifiedLine = this.modifiedLines[modifiedLineIndex];
  290. if (originalLine !== modifiedLine) {
  291. // These lines differ only in trim whitespace
  292. // Check the leading whitespace
  293. {
  294. let originalStartColumn = getFirstNonBlankColumn(originalLine, 1);
  295. let modifiedStartColumn = getFirstNonBlankColumn(modifiedLine, 1);
  296. while (originalStartColumn > 1 && modifiedStartColumn > 1) {
  297. const originalChar = originalLine.charCodeAt(originalStartColumn - 2);
  298. const modifiedChar = modifiedLine.charCodeAt(modifiedStartColumn - 2);
  299. if (originalChar !== modifiedChar) {
  300. break;
  301. }
  302. originalStartColumn--;
  303. modifiedStartColumn--;
  304. }
  305. if (originalStartColumn > 1 || modifiedStartColumn > 1) {
  306. this._pushTrimWhitespaceCharChange(result, originalLineIndex + 1, 1, originalStartColumn, modifiedLineIndex + 1, 1, modifiedStartColumn);
  307. }
  308. }
  309. // Check the trailing whitespace
  310. {
  311. let originalEndColumn = getLastNonBlankColumn(originalLine, 1);
  312. let modifiedEndColumn = getLastNonBlankColumn(modifiedLine, 1);
  313. const originalMaxColumn = originalLine.length + 1;
  314. const modifiedMaxColumn = modifiedLine.length + 1;
  315. while (originalEndColumn < originalMaxColumn && modifiedEndColumn < modifiedMaxColumn) {
  316. const originalChar = originalLine.charCodeAt(originalEndColumn - 1);
  317. const modifiedChar = originalLine.charCodeAt(modifiedEndColumn - 1);
  318. if (originalChar !== modifiedChar) {
  319. break;
  320. }
  321. originalEndColumn++;
  322. modifiedEndColumn++;
  323. }
  324. if (originalEndColumn < originalMaxColumn || modifiedEndColumn < modifiedMaxColumn) {
  325. this._pushTrimWhitespaceCharChange(result, originalLineIndex + 1, originalEndColumn, originalMaxColumn, modifiedLineIndex + 1, modifiedEndColumn, modifiedMaxColumn);
  326. }
  327. }
  328. }
  329. originalLineIndex++;
  330. modifiedLineIndex++;
  331. }
  332. if (nextChange) {
  333. // Emit the actual change
  334. result.push(LineChange.createFromDiffResult(this.shouldIgnoreTrimWhitespace, nextChange, this.original, this.modified, this.continueCharDiff, this.shouldComputeCharChanges, this.shouldPostProcessCharChanges));
  335. originalLineIndex += nextChange.originalLength;
  336. modifiedLineIndex += nextChange.modifiedLength;
  337. }
  338. }
  339. return {
  340. quitEarly: quitEarly,
  341. changes: result
  342. };
  343. }
  344. _pushTrimWhitespaceCharChange(result, originalLineNumber, originalStartColumn, originalEndColumn, modifiedLineNumber, modifiedStartColumn, modifiedEndColumn) {
  345. if (this._mergeTrimWhitespaceCharChange(result, originalLineNumber, originalStartColumn, originalEndColumn, modifiedLineNumber, modifiedStartColumn, modifiedEndColumn)) {
  346. // Merged into previous
  347. return;
  348. }
  349. let charChanges = undefined;
  350. if (this.shouldComputeCharChanges) {
  351. charChanges = [new CharChange(originalLineNumber, originalStartColumn, originalLineNumber, originalEndColumn, modifiedLineNumber, modifiedStartColumn, modifiedLineNumber, modifiedEndColumn)];
  352. }
  353. result.push(new LineChange(originalLineNumber, originalLineNumber, modifiedLineNumber, modifiedLineNumber, charChanges));
  354. }
  355. _mergeTrimWhitespaceCharChange(result, originalLineNumber, originalStartColumn, originalEndColumn, modifiedLineNumber, modifiedStartColumn, modifiedEndColumn) {
  356. const len = result.length;
  357. if (len === 0) {
  358. return false;
  359. }
  360. const prevChange = result[len - 1];
  361. if (prevChange.originalEndLineNumber === 0 || prevChange.modifiedEndLineNumber === 0) {
  362. // Don't merge with inserts/deletes
  363. return false;
  364. }
  365. if (prevChange.originalEndLineNumber + 1 === originalLineNumber && prevChange.modifiedEndLineNumber + 1 === modifiedLineNumber) {
  366. prevChange.originalEndLineNumber = originalLineNumber;
  367. prevChange.modifiedEndLineNumber = modifiedLineNumber;
  368. if (this.shouldComputeCharChanges && prevChange.charChanges) {
  369. prevChange.charChanges.push(new CharChange(originalLineNumber, originalStartColumn, originalLineNumber, originalEndColumn, modifiedLineNumber, modifiedStartColumn, modifiedLineNumber, modifiedEndColumn));
  370. }
  371. return true;
  372. }
  373. return false;
  374. }
  375. }
  376. function getFirstNonBlankColumn(txt, defaultValue) {
  377. const r = strings.firstNonWhitespaceIndex(txt);
  378. if (r === -1) {
  379. return defaultValue;
  380. }
  381. return r + 1;
  382. }
  383. function getLastNonBlankColumn(txt, defaultValue) {
  384. const r = strings.lastNonWhitespaceIndex(txt);
  385. if (r === -1) {
  386. return defaultValue;
  387. }
  388. return r + 2;
  389. }
  390. function createContinueProcessingPredicate(maximumRuntime) {
  391. if (maximumRuntime === 0) {
  392. return () => true;
  393. }
  394. const startTime = Date.now();
  395. return () => {
  396. return Date.now() - startTime < maximumRuntime;
  397. };
  398. }