|
|
|
|
@@ -14,24 +14,44 @@
|
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
|
|
|
|
|
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
|
|
|
|
|
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
|
|
|
|
|
import {GrDiffLine, GrDiffLineType, FILE} from '../gr-diff/gr-diff-line.js';
|
|
|
|
|
import {GrDiffGroup, GrDiffGroupType, hideInContextControl} from '../gr-diff/gr-diff-group.js';
|
|
|
|
|
import {util} from '../../../scripts/util.js';
|
|
|
|
|
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
|
|
|
|
|
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
|
|
|
|
|
import {PolymerElement} from '@polymer/polymer/polymer-element';
|
|
|
|
|
import {
|
|
|
|
|
GrDiffLine,
|
|
|
|
|
GrDiffLineType,
|
|
|
|
|
FILE,
|
|
|
|
|
Highlights,
|
|
|
|
|
} from '../gr-diff/gr-diff-line';
|
|
|
|
|
import {
|
|
|
|
|
GrDiffGroup,
|
|
|
|
|
GrDiffGroupType,
|
|
|
|
|
hideInContextControl,
|
|
|
|
|
} from '../gr-diff/gr-diff-group';
|
|
|
|
|
import {CancelablePromise, util} from '../../../scripts/util';
|
|
|
|
|
import {customElement, property} from '@polymer/decorators';
|
|
|
|
|
import {DiffContent} from '../../../types/common';
|
|
|
|
|
import {DiffSide} from '../gr-diff/gr-diff-utils';
|
|
|
|
|
|
|
|
|
|
const WHOLE_FILE = -1;
|
|
|
|
|
|
|
|
|
|
const DiffSide = {
|
|
|
|
|
LEFT: 'left',
|
|
|
|
|
RIGHT: 'right',
|
|
|
|
|
};
|
|
|
|
|
interface State {
|
|
|
|
|
lineNums: {
|
|
|
|
|
left: number;
|
|
|
|
|
right: number;
|
|
|
|
|
};
|
|
|
|
|
chunkIndex: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DiffHighlights = {
|
|
|
|
|
ADDED: 'edit_b',
|
|
|
|
|
REMOVED: 'edit_a',
|
|
|
|
|
};
|
|
|
|
|
interface ChunkEnd {
|
|
|
|
|
offset: number;
|
|
|
|
|
keyLocation: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface KeyLocations {
|
|
|
|
|
left: {[key: string]: boolean};
|
|
|
|
|
right: {[key: string]: boolean};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The maximum size for an addition or removal chunk before it is broken down
|
|
|
|
|
@@ -67,63 +87,31 @@ const MAX_GROUP_SIZE = 120;
|
|
|
|
|
* "expand context" widget. This may require splitting a chunk/group so
|
|
|
|
|
* that the part that is within the context or has comments is shown, while
|
|
|
|
|
* the rest is not.
|
|
|
|
|
*
|
|
|
|
|
* @extends PolymerElement
|
|
|
|
|
*/
|
|
|
|
|
class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
LegacyElementMixin(
|
|
|
|
|
PolymerElement)) {
|
|
|
|
|
static get is() { return 'gr-diff-processor'; }
|
|
|
|
|
@customElement('gr-diff-processor')
|
|
|
|
|
export class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
LegacyElementMixin(PolymerElement)
|
|
|
|
|
) {
|
|
|
|
|
@property({type: Number})
|
|
|
|
|
context = 3;
|
|
|
|
|
|
|
|
|
|
static get properties() {
|
|
|
|
|
return {
|
|
|
|
|
@property({type: Array, notify: true})
|
|
|
|
|
groups: GrDiffGroup[] = [];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The amount of context around collapsed groups.
|
|
|
|
|
*/
|
|
|
|
|
context: Number,
|
|
|
|
|
@property({type: Object})
|
|
|
|
|
keyLocations: KeyLocations = {left: {}, right: {}};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The array of groups output by the processor.
|
|
|
|
|
*/
|
|
|
|
|
groups: {
|
|
|
|
|
type: Array,
|
|
|
|
|
notify: true,
|
|
|
|
|
},
|
|
|
|
|
@property({type: Number})
|
|
|
|
|
_asyncThreshold = 64;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Locations that should not be collapsed, including the locations of
|
|
|
|
|
* comments.
|
|
|
|
|
*/
|
|
|
|
|
keyLocations: {
|
|
|
|
|
type: Object,
|
|
|
|
|
value() { return {left: {}, right: {}}; },
|
|
|
|
|
},
|
|
|
|
|
@property({type: Number})
|
|
|
|
|
_nextStepHandle: number | null = null;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The maximum number of lines to process synchronously.
|
|
|
|
|
*/
|
|
|
|
|
_asyncThreshold: {
|
|
|
|
|
type: Number,
|
|
|
|
|
value: 64,
|
|
|
|
|
},
|
|
|
|
|
@property({type: Object})
|
|
|
|
|
_processPromise: CancelablePromise<void> | null = null;
|
|
|
|
|
|
|
|
|
|
/** @type {?number} */
|
|
|
|
|
_nextStepHandle: Number,
|
|
|
|
|
/**
|
|
|
|
|
* The promise last returned from `process()` while the asynchronous
|
|
|
|
|
* processing is running - `null` otherwise. Provides a `cancel()`
|
|
|
|
|
* method that rejects it with `{isCancelled: true}`.
|
|
|
|
|
*
|
|
|
|
|
* @type {?Object}
|
|
|
|
|
*/
|
|
|
|
|
_processPromise: {
|
|
|
|
|
type: Object,
|
|
|
|
|
value: null,
|
|
|
|
|
},
|
|
|
|
|
_isScrolling: Boolean,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
@property({type: Boolean})
|
|
|
|
|
_isScrolling?: boolean;
|
|
|
|
|
|
|
|
|
|
/** @override */
|
|
|
|
|
attached() {
|
|
|
|
|
@@ -140,22 +128,23 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
|
|
|
|
|
_handleWindowScroll() {
|
|
|
|
|
this._isScrolling = true;
|
|
|
|
|
this.debounce('resetIsScrolling', () => {
|
|
|
|
|
this._isScrolling = false;
|
|
|
|
|
}, 50);
|
|
|
|
|
this.debounce(
|
|
|
|
|
'resetIsScrolling',
|
|
|
|
|
() => {
|
|
|
|
|
this._isScrolling = false;
|
|
|
|
|
},
|
|
|
|
|
50
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Asynchronously process the diff chunks into groups. As it processes, it
|
|
|
|
|
* will splice groups into the `groups` property of the component.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Array<!Gerrit.DiffChunk>} chunks
|
|
|
|
|
* @param {boolean} isBinary
|
|
|
|
|
*
|
|
|
|
|
* @return {!Promise<!Array<!Object>>} A promise that resolves with an
|
|
|
|
|
* array of GrDiffGroups when the diff is completely processed.
|
|
|
|
|
* @return A promise that resolves with an
|
|
|
|
|
* array of GrDiffGroups when the diff is completely processed.
|
|
|
|
|
*/
|
|
|
|
|
process(chunks, isBinary) {
|
|
|
|
|
process(chunks: DiffContent[], isBinary: boolean) {
|
|
|
|
|
// Cancel any still running process() calls, because they append to the
|
|
|
|
|
// same groups field.
|
|
|
|
|
this.cancel();
|
|
|
|
|
@@ -165,61 +154,65 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
|
|
|
|
|
// If it's a binary diff, we won't be rendering hunks of text differences
|
|
|
|
|
// so finish processing.
|
|
|
|
|
if (isBinary) { return Promise.resolve(); }
|
|
|
|
|
if (isBinary) {
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._processPromise = util.makeCancelable(
|
|
|
|
|
new Promise(resolve => {
|
|
|
|
|
const state = {
|
|
|
|
|
lineNums: {left: 0, right: 0},
|
|
|
|
|
chunkIndex: 0,
|
|
|
|
|
};
|
|
|
|
|
new Promise(resolve => {
|
|
|
|
|
const state = {
|
|
|
|
|
lineNums: {left: 0, right: 0},
|
|
|
|
|
chunkIndex: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
chunks = this._splitLargeChunks(chunks);
|
|
|
|
|
chunks = this._splitCommonChunksWithKeyLocations(chunks);
|
|
|
|
|
chunks = this._splitLargeChunks(chunks);
|
|
|
|
|
chunks = this._splitCommonChunksWithKeyLocations(chunks);
|
|
|
|
|
|
|
|
|
|
let currentBatch = 0;
|
|
|
|
|
const nextStep = () => {
|
|
|
|
|
if (this._isScrolling) {
|
|
|
|
|
this._nextStepHandle = this.async(nextStep, 100);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// If we are done, resolve the promise.
|
|
|
|
|
if (state.chunkIndex >= chunks.length) {
|
|
|
|
|
resolve();
|
|
|
|
|
this._nextStepHandle = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let currentBatch = 0;
|
|
|
|
|
const nextStep = () => {
|
|
|
|
|
if (this._isScrolling) {
|
|
|
|
|
this._nextStepHandle = this.async(nextStep, 100);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// If we are done, resolve the promise.
|
|
|
|
|
if (state.chunkIndex >= chunks.length) {
|
|
|
|
|
resolve();
|
|
|
|
|
this._nextStepHandle = null;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process the next chunk and incorporate the result.
|
|
|
|
|
const stateUpdate = this._processNext(state, chunks);
|
|
|
|
|
for (const group of stateUpdate.groups) {
|
|
|
|
|
this.push('groups', group);
|
|
|
|
|
currentBatch += group.lines.length;
|
|
|
|
|
}
|
|
|
|
|
state.lineNums.left += stateUpdate.lineDelta.left;
|
|
|
|
|
state.lineNums.right += stateUpdate.lineDelta.right;
|
|
|
|
|
// Process the next chunk and incorporate the result.
|
|
|
|
|
const stateUpdate = this._processNext(state, chunks);
|
|
|
|
|
for (const group of stateUpdate.groups) {
|
|
|
|
|
this.push('groups', group);
|
|
|
|
|
currentBatch += group.lines.length;
|
|
|
|
|
}
|
|
|
|
|
state.lineNums.left += stateUpdate.lineDelta.left;
|
|
|
|
|
state.lineNums.right += stateUpdate.lineDelta.right;
|
|
|
|
|
|
|
|
|
|
// Increment the index and recurse.
|
|
|
|
|
state.chunkIndex = stateUpdate.newChunkIndex;
|
|
|
|
|
if (currentBatch >= this._asyncThreshold) {
|
|
|
|
|
currentBatch = 0;
|
|
|
|
|
this._nextStepHandle = this.async(nextStep, 1);
|
|
|
|
|
} else {
|
|
|
|
|
nextStep.call(this);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// Increment the index and recurse.
|
|
|
|
|
state.chunkIndex = stateUpdate.newChunkIndex;
|
|
|
|
|
if (currentBatch >= this._asyncThreshold) {
|
|
|
|
|
currentBatch = 0;
|
|
|
|
|
this._nextStepHandle = this.async(nextStep, 1);
|
|
|
|
|
} else {
|
|
|
|
|
nextStep.call(this);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
nextStep.call(this);
|
|
|
|
|
}));
|
|
|
|
|
return this._processPromise
|
|
|
|
|
.finally(() => { this._processPromise = null; });
|
|
|
|
|
nextStep.call(this);
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
return this._processPromise.finally(() => {
|
|
|
|
|
this._processPromise = null;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cancel any jobs that are running.
|
|
|
|
|
*/
|
|
|
|
|
cancel() {
|
|
|
|
|
if (this._nextStepHandle != null) {
|
|
|
|
|
if (this._nextStepHandle !== null) {
|
|
|
|
|
this.cancelAsync(this._nextStepHandle);
|
|
|
|
|
this._nextStepHandle = null;
|
|
|
|
|
}
|
|
|
|
|
@@ -230,14 +223,12 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Process the next uncollapsible chunk, or the next collapsible chunks.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Object} state
|
|
|
|
|
* @param {!Array<!Object>} chunks
|
|
|
|
|
* @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
|
|
|
|
|
*/
|
|
|
|
|
_processNext(state, chunks) {
|
|
|
|
|
const firstUncollapsibleChunkIndex =
|
|
|
|
|
this._firstUncollapsibleChunkIndex(chunks, state.chunkIndex);
|
|
|
|
|
_processNext(state: State, chunks: DiffContent[]) {
|
|
|
|
|
const firstUncollapsibleChunkIndex = this._firstUncollapsibleChunkIndex(
|
|
|
|
|
chunks,
|
|
|
|
|
state.chunkIndex
|
|
|
|
|
);
|
|
|
|
|
if (firstUncollapsibleChunkIndex === state.chunkIndex) {
|
|
|
|
|
const chunk = chunks[state.chunkIndex];
|
|
|
|
|
return {
|
|
|
|
|
@@ -245,34 +236,44 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
left: this._linesLeft(chunk).length,
|
|
|
|
|
right: this._linesRight(chunk).length,
|
|
|
|
|
},
|
|
|
|
|
groups: [this._chunkToGroup(
|
|
|
|
|
chunk, state.lineNums.left + 1, state.lineNums.right + 1)],
|
|
|
|
|
groups: [
|
|
|
|
|
this._chunkToGroup(
|
|
|
|
|
chunk,
|
|
|
|
|
state.lineNums.left + 1,
|
|
|
|
|
state.lineNums.right + 1
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
newChunkIndex: state.chunkIndex + 1,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this._processCollapsibleChunks(
|
|
|
|
|
state, chunks, firstUncollapsibleChunkIndex);
|
|
|
|
|
state,
|
|
|
|
|
chunks,
|
|
|
|
|
firstUncollapsibleChunkIndex
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_linesLeft(chunk) {
|
|
|
|
|
_linesLeft(chunk: DiffContent) {
|
|
|
|
|
return chunk.ab || chunk.a || [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_linesRight(chunk) {
|
|
|
|
|
_linesRight(chunk: DiffContent) {
|
|
|
|
|
return chunk.ab || chunk.b || [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_firstUncollapsibleChunkIndex(chunks, offset) {
|
|
|
|
|
_firstUncollapsibleChunkIndex(chunks: DiffContent[], offset: number) {
|
|
|
|
|
let chunkIndex = offset;
|
|
|
|
|
while (chunkIndex < chunks.length &&
|
|
|
|
|
this._isCollapsibleChunk(chunks[chunkIndex])) {
|
|
|
|
|
while (
|
|
|
|
|
chunkIndex < chunks.length &&
|
|
|
|
|
this._isCollapsibleChunk(chunks[chunkIndex])
|
|
|
|
|
) {
|
|
|
|
|
chunkIndex++;
|
|
|
|
|
}
|
|
|
|
|
return chunkIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_isCollapsibleChunk(chunk) {
|
|
|
|
|
_isCollapsibleChunk(chunk: DiffContent) {
|
|
|
|
|
return (chunk.ab || chunk.common) && !chunk.keyLocation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -280,36 +281,38 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
* Process a stretch of collapsible chunks.
|
|
|
|
|
*
|
|
|
|
|
* Outputs up to three groups:
|
|
|
|
|
* 1) Visible context before the hidden common code, unless it's the
|
|
|
|
|
* very beginning of the file.
|
|
|
|
|
* 2) Context hidden behind a context bar, unless empty.
|
|
|
|
|
* 3) Visible context after the hidden common code, unless it's the very
|
|
|
|
|
* end of the file.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Object} state
|
|
|
|
|
* @param {!Array<Object>} chunks
|
|
|
|
|
* @param {number} firstUncollapsibleChunkIndex
|
|
|
|
|
* @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}}
|
|
|
|
|
* 1) Visible context before the hidden common code, unless it's the
|
|
|
|
|
* very beginning of the file.
|
|
|
|
|
* 2) Context hidden behind a context bar, unless empty.
|
|
|
|
|
* 3) Visible context after the hidden common code, unless it's the very
|
|
|
|
|
* end of the file.
|
|
|
|
|
*/
|
|
|
|
|
_processCollapsibleChunks(
|
|
|
|
|
state, chunks, firstUncollapsibleChunkIndex) {
|
|
|
|
|
state: State,
|
|
|
|
|
chunks: DiffContent[],
|
|
|
|
|
firstUncollapsibleChunkIndex: number
|
|
|
|
|
) {
|
|
|
|
|
const collapsibleChunks = chunks.slice(
|
|
|
|
|
state.chunkIndex, firstUncollapsibleChunkIndex);
|
|
|
|
|
state.chunkIndex,
|
|
|
|
|
firstUncollapsibleChunkIndex
|
|
|
|
|
);
|
|
|
|
|
const lineCount = collapsibleChunks.reduce(
|
|
|
|
|
(sum, chunk) => sum + this._commonChunkLength(chunk), 0);
|
|
|
|
|
(sum, chunk) => sum + this._commonChunkLength(chunk),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let groups = this._chunksToGroups(
|
|
|
|
|
collapsibleChunks,
|
|
|
|
|
state.lineNums.left + 1,
|
|
|
|
|
state.lineNums.right + 1);
|
|
|
|
|
collapsibleChunks,
|
|
|
|
|
state.lineNums.left + 1,
|
|
|
|
|
state.lineNums.right + 1
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (this.context !== WHOLE_FILE) {
|
|
|
|
|
const hiddenStart = state.chunkIndex === 0 ? 0 : this.context;
|
|
|
|
|
const hiddenEnd = lineCount - (
|
|
|
|
|
firstUncollapsibleChunkIndex === chunks.length ?
|
|
|
|
|
0 : this.context);
|
|
|
|
|
groups = hideInContextControl(
|
|
|
|
|
groups, hiddenStart, hiddenEnd);
|
|
|
|
|
const hiddenEnd =
|
|
|
|
|
lineCount -
|
|
|
|
|
(firstUncollapsibleChunkIndex === chunks.length ? 0 : this.context);
|
|
|
|
|
groups = hideInContextControl(groups, hiddenStart, hiddenEnd);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
@@ -322,21 +325,21 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_commonChunkLength(chunk) {
|
|
|
|
|
console.assert(chunk.ab || chunk.common);
|
|
|
|
|
_commonChunkLength(chunk: DiffContent) {
|
|
|
|
|
console.assert(!!chunk.ab || !!chunk.common);
|
|
|
|
|
console.assert(
|
|
|
|
|
!chunk.a || (chunk.b && chunk.a.length === chunk.b.length),
|
|
|
|
|
`common chunk needs same number of a and b lines: `, chunk);
|
|
|
|
|
!chunk.a || (!!chunk.b && chunk.a.length === chunk.b.length),
|
|
|
|
|
'common chunk needs same number of a and b lines: ',
|
|
|
|
|
chunk
|
|
|
|
|
);
|
|
|
|
|
return this._linesLeft(chunk).length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {!Array<!Object>} chunks
|
|
|
|
|
* @param {number} offsetLeft
|
|
|
|
|
* @param {number} offsetRight
|
|
|
|
|
* @return {!Array<!Object>} (GrDiffGroup)
|
|
|
|
|
*/
|
|
|
|
|
_chunksToGroups(chunks, offsetLeft, offsetRight) {
|
|
|
|
|
_chunksToGroups(
|
|
|
|
|
chunks: DiffContent[],
|
|
|
|
|
offsetLeft: number,
|
|
|
|
|
offsetRight: number
|
|
|
|
|
): GrDiffGroup[] {
|
|
|
|
|
return chunks.map(chunk => {
|
|
|
|
|
const group = this._chunkToGroup(chunk, offsetLeft, offsetRight);
|
|
|
|
|
const chunkLength = this._commonChunkLength(chunk);
|
|
|
|
|
@@ -346,76 +349,83 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {!Object} chunk
|
|
|
|
|
* @param {number} offsetLeft
|
|
|
|
|
* @param {number} offsetRight
|
|
|
|
|
* @return {!Object} (GrDiffGroup)
|
|
|
|
|
*/
|
|
|
|
|
_chunkToGroup(chunk, offsetLeft, offsetRight) {
|
|
|
|
|
_chunkToGroup(
|
|
|
|
|
chunk: DiffContent,
|
|
|
|
|
offsetLeft: number,
|
|
|
|
|
offsetRight: number
|
|
|
|
|
): GrDiffGroup {
|
|
|
|
|
const type = chunk.ab ? GrDiffGroupType.BOTH : GrDiffGroupType.DELTA;
|
|
|
|
|
const lines = this._linesFromChunk(chunk, offsetLeft, offsetRight);
|
|
|
|
|
const group = new GrDiffGroup(type, lines);
|
|
|
|
|
group.keyLocation = chunk.keyLocation;
|
|
|
|
|
group.dueToRebase = chunk.due_to_rebase;
|
|
|
|
|
group.ignoredWhitespaceOnly = chunk.common;
|
|
|
|
|
group.keyLocation = !!chunk.keyLocation;
|
|
|
|
|
group.dueToRebase = !!chunk.due_to_rebase;
|
|
|
|
|
group.ignoredWhitespaceOnly = !!chunk.common;
|
|
|
|
|
return group;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_linesFromChunk(chunk, offsetLeft, offsetRight) {
|
|
|
|
|
_linesFromChunk(chunk: DiffContent, offsetLeft: number, offsetRight: number) {
|
|
|
|
|
if (chunk.ab) {
|
|
|
|
|
return chunk.ab.map((row, i) => this._lineFromRow(
|
|
|
|
|
GrDiffLineType.BOTH, offsetLeft, offsetRight, row, i));
|
|
|
|
|
return chunk.ab.map((row, i) =>
|
|
|
|
|
this._lineFromRow(GrDiffLineType.BOTH, offsetLeft, offsetRight, row, i)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
let lines = [];
|
|
|
|
|
let lines: GrDiffLine[] = [];
|
|
|
|
|
if (chunk.a) {
|
|
|
|
|
// Avoiding a.push(...b) because that causes callstack overflows for
|
|
|
|
|
// large b, which can occur when large files are added removed.
|
|
|
|
|
lines = lines.concat(this._linesFromRows(
|
|
|
|
|
GrDiffLineType.REMOVE, chunk.a, offsetLeft,
|
|
|
|
|
chunk[DiffHighlights.REMOVED]));
|
|
|
|
|
lines = lines.concat(
|
|
|
|
|
this._linesFromRows(
|
|
|
|
|
GrDiffLineType.REMOVE,
|
|
|
|
|
chunk.a,
|
|
|
|
|
offsetLeft,
|
|
|
|
|
chunk.edit_a
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (chunk.b) {
|
|
|
|
|
// Avoiding a.push(...b) because that causes callstack overflows for
|
|
|
|
|
// large b, which can occur when large files are added removed.
|
|
|
|
|
lines = lines.concat(this._linesFromRows(
|
|
|
|
|
GrDiffLineType.ADD, chunk.b, offsetRight,
|
|
|
|
|
chunk[DiffHighlights.ADDED]));
|
|
|
|
|
lines = lines.concat(
|
|
|
|
|
this._linesFromRows(
|
|
|
|
|
GrDiffLineType.ADD,
|
|
|
|
|
chunk.b,
|
|
|
|
|
offsetRight,
|
|
|
|
|
chunk.edit_b
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return lines;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} lineType (GrDiffLineType)
|
|
|
|
|
* @param {!Array<string>} rows
|
|
|
|
|
* @param {number} offset
|
|
|
|
|
* @param {?Array<!Gerrit.IntralineInfo>=} opt_intralineInfos
|
|
|
|
|
* @return {!Array<!Object>} (GrDiffLine)
|
|
|
|
|
*/
|
|
|
|
|
_linesFromRows(lineType, rows, offset, opt_intralineInfos) {
|
|
|
|
|
const grDiffHighlights = opt_intralineInfos ?
|
|
|
|
|
this._convertIntralineInfos(rows, opt_intralineInfos) : undefined;
|
|
|
|
|
return rows.map((row, i) => this._lineFromRow(
|
|
|
|
|
lineType, offset, offset, row, i, grDiffHighlights));
|
|
|
|
|
_linesFromRows(
|
|
|
|
|
lineType: GrDiffLineType,
|
|
|
|
|
rows: string[],
|
|
|
|
|
offset: number,
|
|
|
|
|
intralineInfos?: number[][]
|
|
|
|
|
): GrDiffLine[] {
|
|
|
|
|
const grDiffHighlights = intralineInfos
|
|
|
|
|
? this._convertIntralineInfos(rows, intralineInfos)
|
|
|
|
|
: undefined;
|
|
|
|
|
return rows.map((row, i) =>
|
|
|
|
|
this._lineFromRow(lineType, offset, offset, row, i, grDiffHighlights)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {string} type (GrDiffLineType)
|
|
|
|
|
* @param {number} offsetLeft
|
|
|
|
|
* @param {number} offsetRight
|
|
|
|
|
* @param {string} row
|
|
|
|
|
* @param {number} i
|
|
|
|
|
* @param {!Array<!Object>=} opt_highlights
|
|
|
|
|
* @return {!Object} (GrDiffLine)
|
|
|
|
|
*/
|
|
|
|
|
_lineFromRow(type, offsetLeft, offsetRight, row, i, opt_highlights) {
|
|
|
|
|
_lineFromRow(
|
|
|
|
|
type: GrDiffLineType,
|
|
|
|
|
offsetLeft: number,
|
|
|
|
|
offsetRight: number,
|
|
|
|
|
row: string,
|
|
|
|
|
i: number,
|
|
|
|
|
highlights: Highlights[] = []
|
|
|
|
|
): GrDiffLine {
|
|
|
|
|
const line = new GrDiffLine(type);
|
|
|
|
|
line.text = row;
|
|
|
|
|
if (type !== GrDiffLineType.ADD) line.beforeNumber = offsetLeft + i;
|
|
|
|
|
if (type !== GrDiffLineType.REMOVE) line.afterNumber = offsetRight + i;
|
|
|
|
|
if (opt_highlights) {
|
|
|
|
|
if (highlights) {
|
|
|
|
|
line.hasIntralineInfo = true;
|
|
|
|
|
line.highlights = opt_highlights.filter(hl => hl.contentIndex === i);
|
|
|
|
|
line.highlights = highlights.filter(hl => hl.contentIndex === i);
|
|
|
|
|
} else {
|
|
|
|
|
line.hasIntralineInfo = false;
|
|
|
|
|
}
|
|
|
|
|
@@ -441,10 +451,10 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
* into 2 chunks, one max sized one and the rest (for reasons that are
|
|
|
|
|
* unclear to me).
|
|
|
|
|
*
|
|
|
|
|
* @param {!Array<!Gerrit.DiffChunk>} chunks Chunks as returned from the server
|
|
|
|
|
* @return {!Array<!Gerrit.DiffChunk>} Finer grained chunks.
|
|
|
|
|
* @param chunks Chunks as returned from the server
|
|
|
|
|
* @return Finer grained chunks.
|
|
|
|
|
*/
|
|
|
|
|
_splitLargeChunks(chunks) {
|
|
|
|
|
_splitLargeChunks(chunks: DiffContent[]): DiffContent[] {
|
|
|
|
|
const newChunks = [];
|
|
|
|
|
|
|
|
|
|
for (const chunk of chunks) {
|
|
|
|
|
@@ -476,10 +486,10 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
* the selected context, treat them as separate chunks within the model so
|
|
|
|
|
* that the content (and context surrounding it) renders correctly.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Array<!Object>} chunks DiffContents as returned from server.
|
|
|
|
|
* @return {!Array<!Object>} Finer grained DiffContents.
|
|
|
|
|
* @param chunks DiffContents as returned from server.
|
|
|
|
|
* @return Finer grained DiffContents.
|
|
|
|
|
*/
|
|
|
|
|
_splitCommonChunksWithKeyLocations(chunks) {
|
|
|
|
|
_splitCommonChunksWithKeyLocations(chunks: DiffContent[]): DiffContent[] {
|
|
|
|
|
const result = [];
|
|
|
|
|
let leftLineNum = 1;
|
|
|
|
|
let rightLineNum = 1;
|
|
|
|
|
@@ -497,32 +507,45 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (chunk.common && chunk.a.length != chunk.b.length) {
|
|
|
|
|
if (chunk.common && chunk.a!.length !== chunk.b!.length) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
'DiffContent with common=true must always have equal length');
|
|
|
|
|
'DiffContent with common=true must always have equal length'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
const numLines = this._commonChunkLength(chunk);
|
|
|
|
|
const chunkEnds = this._findChunkEndsAtKeyLocations(
|
|
|
|
|
numLines, leftLineNum, rightLineNum);
|
|
|
|
|
numLines,
|
|
|
|
|
leftLineNum,
|
|
|
|
|
rightLineNum
|
|
|
|
|
);
|
|
|
|
|
leftLineNum += numLines;
|
|
|
|
|
rightLineNum += numLines;
|
|
|
|
|
|
|
|
|
|
if (chunk.ab) {
|
|
|
|
|
result.push(...this._splitAtChunkEnds(chunk.ab, chunkEnds)
|
|
|
|
|
.map(({lines, keyLocation}) => {
|
|
|
|
|
result.push(
|
|
|
|
|
...this._splitAtChunkEnds(chunk.ab, chunkEnds).map(
|
|
|
|
|
({lines, keyLocation}) => {
|
|
|
|
|
return {
|
|
|
|
|
...chunk,
|
|
|
|
|
ab: lines,
|
|
|
|
|
keyLocation,
|
|
|
|
|
};
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
} else if (chunk.common) {
|
|
|
|
|
const aChunks = this._splitAtChunkEnds(chunk.a, chunkEnds);
|
|
|
|
|
const bChunks = this._splitAtChunkEnds(chunk.b, chunkEnds);
|
|
|
|
|
result.push(...aChunks.map(({lines, keyLocation}, i) => {
|
|
|
|
|
return {
|
|
|
|
|
...chunk, a: lines, b: bChunks[i].lines, keyLocation};
|
|
|
|
|
}));
|
|
|
|
|
const aChunks = this._splitAtChunkEnds(chunk.a!, chunkEnds);
|
|
|
|
|
const bChunks = this._splitAtChunkEnds(chunk.b!, chunkEnds);
|
|
|
|
|
result.push(
|
|
|
|
|
...aChunks.map(({lines, keyLocation}, i) => {
|
|
|
|
|
return {
|
|
|
|
|
...chunk,
|
|
|
|
|
a: lines,
|
|
|
|
|
b: bChunks[i].lines,
|
|
|
|
|
keyLocation,
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -530,16 +553,22 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return {!Array<{offset: number, keyLocation: boolean}>} Offsets of the
|
|
|
|
|
* new chunk ends, including whether it's a key location.
|
|
|
|
|
* @return Offsets of the new chunk ends, including whether it's a key
|
|
|
|
|
* location.
|
|
|
|
|
*/
|
|
|
|
|
_findChunkEndsAtKeyLocations(numLines, leftOffset, rightOffset) {
|
|
|
|
|
_findChunkEndsAtKeyLocations(
|
|
|
|
|
numLines: number,
|
|
|
|
|
leftOffset: number,
|
|
|
|
|
rightOffset: number
|
|
|
|
|
): ChunkEnd[] {
|
|
|
|
|
const result = [];
|
|
|
|
|
let lastChunkEnd = 0;
|
|
|
|
|
for (let i=0; i<numLines; i++) {
|
|
|
|
|
for (let i = 0; i < numLines; i++) {
|
|
|
|
|
// If this line should not be collapsed.
|
|
|
|
|
if (this.keyLocations[DiffSide.LEFT][leftOffset + i] ||
|
|
|
|
|
this.keyLocations[DiffSide.RIGHT][rightOffset + i]) {
|
|
|
|
|
if (
|
|
|
|
|
this.keyLocations[DiffSide.LEFT][leftOffset + i] ||
|
|
|
|
|
this.keyLocations[DiffSide.RIGHT][rightOffset + i]
|
|
|
|
|
) {
|
|
|
|
|
// 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.
|
|
|
|
|
@@ -560,12 +589,14 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_splitAtChunkEnds(lines, chunkEnds) {
|
|
|
|
|
_splitAtChunkEnds(lines: string[], chunkEnds: ChunkEnd[]) {
|
|
|
|
|
const result = [];
|
|
|
|
|
let lastChunkEndOffset = 0;
|
|
|
|
|
for (const {offset, keyLocation} of chunkEnds) {
|
|
|
|
|
result.push(
|
|
|
|
|
{lines: lines.slice(lastChunkEndOffset, offset), keyLocation});
|
|
|
|
|
result.push({
|
|
|
|
|
lines: lines.slice(lastChunkEndOffset, offset),
|
|
|
|
|
keyLocation,
|
|
|
|
|
});
|
|
|
|
|
lastChunkEndOffset = offset;
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
@@ -574,12 +605,11 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
/**
|
|
|
|
|
* Converts `IntralineInfo`s return by the API to `GrLineHighlights` used
|
|
|
|
|
* for rendering.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Array<string>} rows
|
|
|
|
|
* @param {!Array<!Gerrit.IntralineInfo>} intralineInfos
|
|
|
|
|
* @return {!Array<!Object>} (Highlights[] from GrDiffLine)
|
|
|
|
|
*/
|
|
|
|
|
_convertIntralineInfos(rows, intralineInfos) {
|
|
|
|
|
_convertIntralineInfos(
|
|
|
|
|
rows: string[],
|
|
|
|
|
intralineInfos: number[][]
|
|
|
|
|
): Highlights[] {
|
|
|
|
|
let rowIndex = 0;
|
|
|
|
|
let idx = 0;
|
|
|
|
|
const normalized = [];
|
|
|
|
|
@@ -595,7 +625,7 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
idx++;
|
|
|
|
|
j++;
|
|
|
|
|
}
|
|
|
|
|
let lineHighlight = {
|
|
|
|
|
let lineHighlight: Highlights = {
|
|
|
|
|
contentIndex: rowIndex,
|
|
|
|
|
startIndex: idx,
|
|
|
|
|
};
|
|
|
|
|
@@ -625,12 +655,9 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
* 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 chunk
|
|
|
|
|
* or a delta it is returned as the single element of the result array.
|
|
|
|
|
*
|
|
|
|
|
* @param {!Gerrit.DiffChunk} chunk A raw chunk from a diff response.
|
|
|
|
|
* @return {!Array<!Array<!Object>>}
|
|
|
|
|
*/
|
|
|
|
|
_breakdownChunk(chunk) {
|
|
|
|
|
let key = null;
|
|
|
|
|
_breakdownChunk(chunk: DiffContent): DiffContent[] {
|
|
|
|
|
let key: 'a' | 'b' | 'ab' | null = null;
|
|
|
|
|
if (chunk.a && !chunk.b) {
|
|
|
|
|
key = 'a';
|
|
|
|
|
} else if (chunk.b && !chunk.a) {
|
|
|
|
|
@@ -639,31 +666,31 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
key = 'ab';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!key) { return [chunk]; }
|
|
|
|
|
if (!key) {
|
|
|
|
|
return [chunk];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this._breakdown(chunk[key], MAX_GROUP_SIZE)
|
|
|
|
|
.map(subChunkLines => {
|
|
|
|
|
const subChunk = {};
|
|
|
|
|
subChunk[key] = subChunkLines;
|
|
|
|
|
if (chunk.due_to_rebase) {
|
|
|
|
|
subChunk.due_to_rebase = true;
|
|
|
|
|
}
|
|
|
|
|
return subChunk;
|
|
|
|
|
});
|
|
|
|
|
return this._breakdown(chunk[key]!, MAX_GROUP_SIZE).map(subChunkLines => {
|
|
|
|
|
const subChunk: DiffContent = {};
|
|
|
|
|
subChunk[key!] = subChunkLines;
|
|
|
|
|
if (chunk.due_to_rebase) {
|
|
|
|
|
subChunk.due_to_rebase = true;
|
|
|
|
|
}
|
|
|
|
|
return subChunk;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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]; }
|
|
|
|
|
_breakdown<T>(array: T[], size: number): T[][] {
|
|
|
|
|
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);
|
|
|
|
|
@@ -672,4 +699,8 @@ class GrDiffProcessor extends GestureEventListeners(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
customElements.define(GrDiffProcessor.is, GrDiffProcessor);
|
|
|
|
|
declare global {
|
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
|
'gr-diff-processor': GrDiffProcessor;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|