| /** | |
| * @license | |
| * Copyright (C) 2016 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| (function() { | |
| 'use strict'; | |
| const WHOLE_FILE = -1; | |
| const DiffSide = { | |
| LEFT: 'left', | |
| RIGHT: 'right', | |
| }; | |
| const DiffGroupType = { | |
| ADDED: 'b', | |
| BOTH: 'ab', | |
| REMOVED: 'a', | |
| }; | |
| const DiffHighlights = { | |
| ADDED: 'edit_b', | |
| REMOVED: 'edit_a', | |
| }; | |
| /** | |
| * The maximum size for an addition or removal chunk before it is broken down | |
| * into a series of chunks that are this size at most. | |
| * | |
| * Note: The value of 120 is chosen so that it is larger than the default | |
| * _asyncThreshold of 64, but feel free to tune this constant to your | |
| * performance needs. | |
| */ | |
| const MAX_GROUP_SIZE = 120; | |
| Polymer({ | |
| is: 'gr-diff-processor', | |
| properties: { | |
| /** | |
| * The amount of context around collapsed groups. | |
| */ | |
| context: Number, | |
| /** | |
| * The array of groups output by the processor. | |
| */ | |
| groups: { | |
| type: Array, | |
| notify: true, | |
| }, | |
| /** | |
| * Locations that should not be collapsed, including the locations of | |
| * comments. | |
| */ | |
| keyLocations: { | |
| type: Object, | |
| value() { return {left: {}, right: {}}; }, | |
| }, | |
| /** | |
| * The maximum number of lines to process synchronously. | |
| */ | |
| _asyncThreshold: { | |
| type: Number, | |
| value: 64, | |
| }, | |
| /** @type {number|undefined} */ | |
| _nextStepHandle: Number, | |
| _isScrolling: Boolean, | |
| }, | |
| attached() { | |
| this.listen(window, 'scroll', '_handleWindowScroll'); | |
| }, | |
| detached() { | |
| this.cancel(); | |
| this.unlisten(window, 'scroll', '_handleWindowScroll'); | |
| }, | |
| _handleWindowScroll() { | |
| this._isScrolling = true; | |
| this.debounce('resetIsScrolling', () => { | |
| this._isScrolling = false; | |
| }, 50); | |
| }, | |
| /** | |
| * Asynchronously process the diff object into groups. As it processes, it | |
| * will splice groups into the `groups` property of the component. | |
| * @return {Promise} A promise that resolves when the diff is completely | |
| * processed. | |
| */ | |
| process(content, isBinary) { | |
| this.groups = []; | |
| this.push('groups', this._makeFileComments()); | |
| // If it's a binary diff, we won't be rendering hunks of text differences | |
| // so finish processing. | |
| if (isBinary) { return Promise.resolve(); } | |
| return new Promise(resolve => { | |
| const state = { | |
| lineNums: {left: 0, right: 0}, | |
| sectionIndex: 0, | |
| }; | |
| content = this._splitCommonGroupsWithComments(content); | |
| let currentBatch = 0; | |
| const nextStep = () => { | |
| if (this._isScrolling) { | |
| this.async(nextStep, 100); | |
| return; | |
| } | |
| // If we are done, resolve the promise. | |
| if (state.sectionIndex >= content.length) { | |
| resolve(this.groups); | |
| this._nextStepHandle = undefined; | |
| return; | |
| } | |
| // Process the next section and incorporate the result. | |
| const result = this._processNext(state, content); | |
| for (const group of result.groups) { | |
| this.push('groups', group); | |
| currentBatch += group.lines.length; | |
| } | |
| state.lineNums.left += result.lineDelta.left; | |
| state.lineNums.right += result.lineDelta.right; | |
| // Increment the index and recurse. | |
| state.sectionIndex++; | |
| if (currentBatch >= this._asyncThreshold) { | |
| currentBatch = 0; | |
| this._nextStepHandle = this.async(nextStep, 1); | |
| } else { | |
| nextStep.call(this); | |
| } | |
| }; | |
| nextStep.call(this); | |
| }); | |
| }, | |
| /** | |
| * Cancel any jobs that are running. | |
| */ | |
| cancel() { | |
| if (this._nextStepHandle !== undefined) { | |
| this.cancelAsync(this._nextStepHandle); | |
| this._nextStepHandle = undefined; | |
| } | |
| }, | |
| /** | |
| * Process the next section of the diff. | |
| */ | |
| _processNext(state, content) { | |
| const section = content[state.sectionIndex]; | |
| const rows = { | |
| both: section[DiffGroupType.BOTH] || null, | |
| added: section[DiffGroupType.ADDED] || null, | |
| removed: section[DiffGroupType.REMOVED] || null, | |
| }; | |
| const highlights = { | |
| added: section[DiffHighlights.ADDED] || null, | |
| removed: section[DiffHighlights.REMOVED] || null, | |
| }; | |
| if (rows.both) { // If it's a shared section. | |
| let sectionEnd = null; | |
| if (state.sectionIndex === 0) { | |
| sectionEnd = 'first'; | |
| } else if (state.sectionIndex === content.length - 1) { | |
| sectionEnd = 'last'; | |
| } | |
| const sharedGroups = this._sharedGroupsFromRows( | |
| rows.both, | |
| content.length > 1 ? this.context : WHOLE_FILE, | |
| state.lineNums.left, | |
| state.lineNums.right, | |
| sectionEnd); | |
| return { | |
| lineDelta: { | |
| left: rows.both.length, | |
| right: rows.both.length, | |
| }, | |
| groups: sharedGroups, | |
| }; | |
| } else { // Otherwise it's a delta section. | |
| const deltaGroup = this._deltaGroupFromRows( | |
| rows.added, | |
| rows.removed, | |
| state.lineNums.left, | |
| state.lineNums.right, | |
| highlights); | |
| deltaGroup.dueToRebase = section.due_to_rebase; | |
| return { | |
| lineDelta: { | |
| left: rows.removed ? rows.removed.length : 0, | |
| right: rows.added ? rows.added.length : 0, | |
| }, | |
| groups: [deltaGroup], | |
| }; | |
| } | |
| }, | |
| /** | |
| * Take rows of a shared diff section and produce an array of corresponding | |
| * (potentially collapsed) groups. | |
| * @param {!Array<string>} rows | |
| * @param {number} context | |
| * @param {number} startLineNumLeft | |
| * @param {number} startLineNumRight | |
| * @param {?string=} opt_sectionEnd String representing whether this is the | |
| * first section or the last section or neither. Use the values 'first', | |
| * 'last' and null respectively. | |
| * @return {!Array<!Object>} Array of GrDiffGroup | |
| */ | |
| _sharedGroupsFromRows(rows, context, startLineNumLeft, | |
| startLineNumRight, opt_sectionEnd) { | |
| const result = []; | |
| const lines = []; | |
| let line; | |
| // Map each row to a GrDiffLine. | |
| for (let i = 0; i < rows.length; i++) { | |
| line = new GrDiffLine(GrDiffLine.Type.BOTH); | |
| line.text = rows[i]; | |
| line.beforeNumber = ++startLineNumLeft; | |
| line.afterNumber = ++startLineNumRight; | |
| lines.push(line); | |
| } | |
| // Find the hidden range based on the user's context preference. If this | |
| // is the first or the last section of the diff, make sure the collapsed | |
| // part of the section extends to the edge of the file. | |
| const hiddenRange = [context, rows.length - context]; | |
| if (opt_sectionEnd === 'first') { | |
| hiddenRange[0] = 0; | |
| } else if (opt_sectionEnd === 'last') { | |
| hiddenRange[1] = rows.length; | |
| } | |
| // If there is a range to hide. | |
| if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 1) { | |
| const linesBeforeCtx = lines.slice(0, hiddenRange[0]); | |
| const hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]); | |
| const linesAfterCtx = lines.slice(hiddenRange[1]); | |
| if (linesBeforeCtx.length > 0) { | |
| result.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx)); | |
| } | |
| const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL); | |
| ctxLine.contextGroup = | |
| new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines); | |
| result.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, | |
| [ctxLine])); | |
| if (linesAfterCtx.length > 0) { | |
| result.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx)); | |
| } | |
| } else { | |
| result.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)); | |
| } | |
| return result; | |
| }, | |
| /** | |
| * Take the rows of a delta diff section and produce the corresponding | |
| * group. | |
| * @param {!Array<string>} rowsAdded | |
| * @param {!Array<string>} rowsRemoved | |
| * @param {number} startLineNumLeft | |
| * @param {number} startLineNumRight | |
| * @return {!Object} (Gr-Diff-Group) | |
| */ | |
| _deltaGroupFromRows(rowsAdded, rowsRemoved, startLineNumLeft, | |
| startLineNumRight, highlights) { | |
| let lines = []; | |
| if (rowsRemoved) { | |
| lines = lines.concat(this._deltaLinesFromRows(GrDiffLine.Type.REMOVE, | |
| rowsRemoved, startLineNumLeft, highlights.removed)); | |
| } | |
| if (rowsAdded) { | |
| lines = lines.concat(this._deltaLinesFromRows(GrDiffLine.Type.ADD, | |
| rowsAdded, startLineNumRight, highlights.added)); | |
| } | |
| return new GrDiffGroup(GrDiffGroup.Type.DELTA, lines); | |
| }, | |
| /** | |
| * @return {!Array<!Object>} Array of GrDiffLines | |
| */ | |
| _deltaLinesFromRows(lineType, rows, startLineNum, | |
| opt_highlights) { | |
| // Normalize highlights if they have been passed. | |
| if (opt_highlights) { | |
| opt_highlights = this._normalizeIntralineHighlights(rows, | |
| opt_highlights); | |
| } | |
| const lines = []; | |
| let line; | |
| for (let i = 0; i < rows.length; i++) { | |
| line = new GrDiffLine(lineType); | |
| line.text = rows[i]; | |
| if (lineType === GrDiffLine.Type.ADD) { | |
| line.afterNumber = ++startLineNum; | |
| } else { | |
| line.beforeNumber = ++startLineNum; | |
| } | |
| if (opt_highlights) { | |
| line.highlights = opt_highlights.filter(hl => hl.contentIndex === i); | |
| } | |
| lines.push(line); | |
| } | |
| return lines; | |
| }, | |
| _makeFileComments() { | |
| const line = new GrDiffLine(GrDiffLine.Type.BOTH); | |
| line.beforeNumber = GrDiffLine.FILE; | |
| line.afterNumber = GrDiffLine.FILE; | |
| return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]); | |
| }, | |
| /** | |
| * In order to show comments out of the bounds of the selected context, | |
| * treat them as separate chunks within the model so that the content (and | |
| * context surrounding it) renders correctly. | |
| * @param {?} content The diff content object. (has to be iterable) | |
| * @return {!Object} A new diff content object with regions split up. | |
| */ | |
| _splitCommonGroupsWithComments(content) { | |
| const result = []; | |
| let leftLineNum = 0; | |
| let rightLineNum = 0; | |
| // If the context is set to "whole file", then break down the shared | |
| // chunks so they can be rendered incrementally. Note: this is not enabled | |
| // for any other context preference because manipulating the chunks in | |
| // this way violates assumptions by the context grouper logic. | |
| if (this.context === -1) { | |
| const newContent = []; | |
| for (const group of content) { | |
| if (group.ab && group.ab.length > MAX_GROUP_SIZE * 2) { | |
| // Split large shared groups in two, where the first is the maximum | |
| // group size. | |
| newContent.push({ab: group.ab.slice(0, MAX_GROUP_SIZE)}); | |
| newContent.push({ab: group.ab.slice(MAX_GROUP_SIZE)}); | |
| } else { | |
| newContent.push(group); | |
| } | |
| } | |
| content = newContent; | |
| } | |
| // For each section in the diff. | |
| for (let i = 0; i < content.length; i++) { | |
| // If it isn't a common group, append it as-is and update line numbers. | |
| if (!content[i].ab) { | |
| if (content[i].a) { | |
| leftLineNum += content[i].a.length; | |
| } | |
| if (content[i].b) { | |
| rightLineNum += content[i].b.length; | |
| } | |
| for (const group of this._breakdownGroup(content[i])) { | |
| result.push(group); | |
| } | |
| continue; | |
| } | |
| const chunk = content[i].ab; | |
| let currentChunk = {ab: []}; | |
| // For each line in the common group. | |
| for (const subChunk of chunk) { | |
| leftLineNum++; | |
| rightLineNum++; | |
| // If this line should not be collapsed. | |
| if (this.keyLocations[DiffSide.LEFT][leftLineNum] || | |
| this.keyLocations[DiffSide.RIGHT][rightLineNum]) { | |
| // If any lines have been accumulated into the chunk leading up to | |
| // this non-collapse line, then add them as a chunk and start a new | |
| // one. | |
| if (currentChunk.ab && currentChunk.ab.length > 0) { | |
| result.push(currentChunk); | |
| currentChunk = {ab: []}; | |
| } | |
| // Add the non-collapse line as its own chunk. | |
| result.push({ab: [subChunk]}); | |
| } else { | |
| // Append the current line to the current chunk. | |
| currentChunk.ab.push(subChunk); | |
| } | |
| } | |
| if (currentChunk.ab && currentChunk.ab.length > 0) { | |
| result.push(currentChunk); | |
| } | |
| } | |
| return result; | |
| }, | |
| /** | |
| * The `highlights` array consists of a list of <skip length, mark length> | |
| * pairs, where the skip length is the number of characters between the | |
| * end of the previous edit and the start of this edit, and the mark | |
| * length is the number of edited characters following the skip. The start | |
| * of the edits is from the beginning of the related diff content lines. | |
| * | |
| * Note that the implied newline character at the end of each line is | |
| * included in the length calculation, and thus it is possible for the | |
| * edits to span newlines. | |
| * | |
| * A line highlight object consists of three fields: | |
| * - contentIndex: The index of the diffChunk `content` field (the line | |
| * being referred to). | |
| * - startIndex: Where the highlight should begin. | |
| * - endIndex: (optional) Where the highlight should end. If omitted, the | |
| * highlight is meant to be a continuation onto the next line. | |
| */ | |
| _normalizeIntralineHighlights(content, highlights) { | |
| let contentIndex = 0; | |
| let idx = 0; | |
| const normalized = []; | |
| for (const hl of highlights) { | |
| let line = content[contentIndex] + '\n'; | |
| let j = 0; | |
| while (j < hl[0]) { | |
| if (idx === line.length) { | |
| idx = 0; | |
| line = content[++contentIndex] + '\n'; | |
| continue; | |
| } | |
| idx++; | |
| j++; | |
| } | |
| let lineHighlight = { | |
| contentIndex, | |
| startIndex: idx, | |
| }; | |
| j = 0; | |
| while (line && j < hl[1]) { | |
| if (idx === line.length) { | |
| idx = 0; | |
| line = content[++contentIndex] + '\n'; | |
| normalized.push(lineHighlight); | |
| lineHighlight = { | |
| contentIndex, | |
| startIndex: idx, | |
| }; | |
| continue; | |
| } | |
| idx++; | |
| j++; | |
| } | |
| lineHighlight.endIndex = idx; | |
| normalized.push(lineHighlight); | |
| } | |
| return normalized; | |
| }, | |
| /** | |
| * If a group is an addition or a removal, break it down into smaller groups | |
| * of that type using the MAX_GROUP_SIZE. If the group is a shared section | |
| * or a delta it is returned as the single element of the result array. | |
| * @param {!Object} group A raw chunk from a diff response. | |
| * @return {!Array<!Array<!Object>>} | |
| */ | |
| _breakdownGroup(group) { | |
| let key = null; | |
| if (group.a && !group.b) { | |
| key = 'a'; | |
| } else if (group.b && !group.a) { | |
| key = 'b'; | |
| } else if (group.ab) { | |
| key = 'ab'; | |
| } | |
| if (!key) { return [group]; } | |
| return this._breakdown(group[key], MAX_GROUP_SIZE) | |
| .map(subgroupLines => { | |
| const subGroup = {}; | |
| subGroup[key] = subgroupLines; | |
| if (group.due_to_rebase) { | |
| subGroup.due_to_rebase = true; | |
| } | |
| return subGroup; | |
| }); | |
| }, | |
| /** | |
| * Given an array and a size, return an array of arrays where no inner array | |
| * is larger than that size, preserving the original order. | |
| * @param {!Array<T>} array | |
| * @param {number} size | |
| * @return {!Array<!Array<T>>} | |
| * @template T | |
| */ | |
| _breakdown(array, size) { | |
| if (!array.length) { return []; } | |
| if (array.length < size) { return [array]; } | |
| const head = array.slice(0, array.length - size); | |
| const tail = array.slice(array.length - size); | |
| return this._breakdown(head, size).concat([tail]); | |
| }, | |
| }); | |
| })(); |