ff801963e62b52221b8dab9ddd133b845d5f94c968a65c7cc00c08d75396d5e2ad71d30eac0c8887d25d84294a17afb23f283370a869b09778cc2f383b264b 22 KB

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