Merge changes Ic301cc54,I828c4f89

* changes:
  Show but don't highlight ignored whitespace
  Make context collapsing work for multi section
This commit is contained in:
Ben Rohlfs
2019-06-06 16:52:14 +00:00
committed by Gerrit Code Review
13 changed files with 584 additions and 329 deletions

View File

@@ -35,6 +35,9 @@
if (group.dueToRebase) {
sectionEl.classList.add('dueToRebase');
}
if (group.ignoredWhitespaceOnly) {
sectionEl.classList.add('ignoredWhitespaceOnly');
}
const pairs = group.getSideBySidePairs();
for (let i = 0; i < pairs.length; i++) {
sectionEl.appendChild(this._createRow(sectionEl, pairs[i].left,

View File

@@ -35,6 +35,9 @@
if (group.dueToRebase) {
sectionEl.classList.add('dueToRebase');
}
if (group.ignoredWhitespaceOnly) {
sectionEl.classList.add('ignoredWhitespaceOnly');
}
for (let i = 0; i < group.lines.length; ++i) {
sectionEl.appendChild(this._createRow(sectionEl, group.lines[i]));

View File

@@ -289,7 +289,7 @@ limitations under the License.
const contextIndex = groups.findIndex(group =>
group.element === sectionEl
);
groups.splice(...[contextIndex, 1].concat(newGroups));
groups.splice(contextIndex, 1, ...newGroups);
for (const newGroup of newGroups) {
this._builder.emitGroup(newGroup, sectionEl);

View File

@@ -231,33 +231,12 @@
group => { return group.element; });
};
// TODO(wyatta): Move this completely into the processor.
GrDiffBuilder.prototype._insertContextGroups = function(groups, lines,
hiddenRange) {
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) {
groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx));
}
const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
ctxLine.contextGroups =
[new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines)];
groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL,
[ctxLine]));
if (linesAfterCtx.length > 0) {
groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesAfterCtx));
}
};
GrDiffBuilder.prototype._createContextControl = function(section, line) {
if (!line.contextGroups) return null;
const numLines = line.contextGroups.reduce(
(sum, contextGroup) => sum + contextGroup.lines.length, 0);
const numLines =
line.contextGroups[line.contextGroups.length - 1].lineRange.left.end -
line.contextGroups[0].lineRange.left.start + 1;
if (numLines === 0) return null;
@@ -266,24 +245,24 @@
if (showPartialLinks) {
td.appendChild(this._createContextButton(
GrDiffBuilder.ContextButtonType.ABOVE, section, line));
GrDiffBuilder.ContextButtonType.ABOVE, section, line, numLines));
td.appendChild(document.createTextNode(' - '));
}
td.appendChild(this._createContextButton(
GrDiffBuilder.ContextButtonType.ALL, section, line));
GrDiffBuilder.ContextButtonType.ALL, section, line, numLines));
if (showPartialLinks) {
td.appendChild(document.createTextNode(' - '));
td.appendChild(this._createContextButton(
GrDiffBuilder.ContextButtonType.BELOW, section, line));
GrDiffBuilder.ContextButtonType.BELOW, section, line, numLines));
}
return td;
};
GrDiffBuilder.prototype._createContextButton = function(type, section, line) {
const contextLines = line.contextGroups[0].lines;
GrDiffBuilder.prototype._createContextButton = function(type, section, line,
numLines) {
const context = PARTIAL_CONTEXT_AMOUNT;
const button = this._createElement('gr-button', 'showContext');
@@ -291,20 +270,20 @@
button.setAttribute('no-uppercase', true);
let text;
const groups = []; // The groups that replace this one if tapped.
let groups = []; // The groups that replace this one if tapped.
if (type === GrDiffBuilder.ContextButtonType.ALL) {
text = 'Show ' + contextLines.length + ' common line';
if (contextLines.length > 1) { text += 's'; }
text = 'Show ' + numLines + ' common line';
if (numLines > 1) { text += 's'; }
groups.push(...line.contextGroups);
} else if (type === GrDiffBuilder.ContextButtonType.ABOVE) {
text = '+' + context + '↑';
this._insertContextGroups(groups, contextLines,
[context, contextLines.length]);
groups = GrDiffGroup.hideInContextControl(line.contextGroups,
context, undefined);
} else if (type === GrDiffBuilder.ContextButtonType.BELOW) {
text = '+' + context + '↓';
this._insertContextGroups(groups, contextLines,
[0, contextLines.length - context]);
groups = GrDiffGroup.hideInContextControl(line.contextGroups,
0, numLines - context);
}
Polymer.dom(button).textContent = text;

View File

@@ -90,26 +90,37 @@ limitations under the License.
});
test('context control buttons', () => {
const section = {};
const line = {contextGroups: [{lines: []}]};
// Create 10 lines.
const lines = [];
for (let i = 0; i < 10; i++) {
line.contextGroups[0].lines.push('lorem upsum');
const line = new GrDiffLine(GrDiffLine.Type.BOTH);
line.beforeNumber = i + 1;
line.afterNumber = i + 1;
line.text = 'lorem upsum';
lines.push(line);
}
const contextLine = {
contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)],
};
const section = {};
// Does not include +10 buttons when there are fewer than 11 lines.
let td = builder._createContextControl(section, line);
let td = builder._createContextControl(section, contextLine);
let buttons = td.querySelectorAll('gr-button.showContext');
assert.equal(buttons.length, 1);
assert.equal(Polymer.dom(buttons[0]).textContent, 'Show 10 common lines');
// Add another line.
line.contextGroups[0].lines.push('lorem upsum');
const line = new GrDiffLine(GrDiffLine.Type.BOTH);
line.text = 'lorem upsum';
line.beforeNumber = 11;
line.afterNumber = 11;
contextLine.contextGroups[0].addLine(line);
// Includes +10 buttons when there are at least 11 lines.
td = builder._createContextControl(section, line);
td = builder._createContextControl(section, contextLine);
buttons = td.querySelectorAll('gr-button.showContext');
assert.equal(buttons.length, 3);

View File

@@ -271,9 +271,6 @@
.then(diff => {
this._loadedWhitespaceLevel = whitespaceLevel;
this._reportDiff(diff);
if (this._getIgnoreWhitespace() !== WHITESPACE_IGNORE_NONE) {
return this._translateChunksToIgnore(diff);
}
return diff;
})
.catch(e => {
@@ -734,49 +731,6 @@
matchers.some(matcher => matcher(threadEl)));
},
/**
* Take a diff that was loaded with a ignore-whitespace other than
* IGNORE_NONE, and convert delta chunks labeled as common into shared
* chunks.
* @param {!Object} diff
* @returns {!Object}
*/
_translateChunksToIgnore(diff) {
const newDiff = Object.assign({}, diff);
const mergedContent = [];
// Was the last chunk visited a shared chunk?
let lastWasShared = false;
for (const chunk of diff.content) {
if (lastWasShared && chunk.common && chunk.b) {
// The last chunk was shared and this chunk should be ignored, so
// add its revision content to the previous chunk.
mergedContent[mergedContent.length - 1].ab.push(...chunk.b);
} else if (chunk.common && !chunk.b) {
// If the chunk should be ignored, but it doesn't have revision
// content, then drop it and continue without updating lastWasShared.
continue;
} else if (lastWasShared && chunk.ab) {
// Both the last chunk and the current chunk are shared. Merge this
// chunk's shared content into the previous shared content.
mergedContent[mergedContent.length - 1].ab.push(...chunk.ab);
} else if (!lastWasShared && chunk.common && chunk.b) {
// If the previous chunk was not shared, but this one should be
// ignored, then add it as a shared chunk.
mergedContent.push({ab: chunk.b});
} else {
// Otherwise add the chunk as is.
mergedContent.push(chunk);
}
lastWasShared = !!mergedContent[mergedContent.length - 1].ab;
}
newDiff.content = mergedContent;
return newDiff;
},
_getIgnoreWhitespace() {
if (!this.prefs || !this.prefs.ignore_whitespace) {
return WHITESPACE_IGNORE_NONE;

View File

@@ -1257,89 +1257,5 @@ limitations under the License.
assert.deepEqual(element._filterThreadElsForLocation(threadEls, line,
Gerrit.DiffSide.RIGHT), [r]);
});
suite('_translateChunksToIgnore', () => {
let content;
setup(() => {
content = [
{ab: ['one', 'two']},
{a: ['three'], b: ['different three']},
{b: ['four']},
{ab: ['five', 'six']},
{a: ['seven']},
{ab: ['eight', 'nine']},
];
});
test('does nothing to unmarked diff', () => {
assert.deepEqual(element._translateChunksToIgnore({content}),
{content});
});
test('merges marked delta chunk', () => {
content[1].common = true;
assert.deepEqual(element._translateChunksToIgnore({content}), {
content: [
{ab: ['one', 'two', 'different three']},
{b: ['four']},
{ab: ['five', 'six']},
{a: ['seven']},
{ab: ['eight', 'nine']},
],
});
});
test('merges marked addition chunk', () => {
content[2].common = true;
assert.deepEqual(element._translateChunksToIgnore({content}), {
content: [
{ab: ['one', 'two']},
{a: ['three'], b: ['different three']},
{ab: ['four', 'five', 'six']},
{a: ['seven']},
{ab: ['eight', 'nine']},
],
});
});
test('merges multiple marked delta', () => {
content[1].common = true;
content[2].common = true;
assert.deepEqual(element._translateChunksToIgnore({content}), {
content: [
{ab: ['one', 'two', 'different three', 'four', 'five', 'six']},
{a: ['seven']},
{ab: ['eight', 'nine']},
],
});
});
test('marked deletion chunks are omitted', () => {
content[4].common = true;
assert.deepEqual(element._translateChunksToIgnore({content}), {
content: [
{ab: ['one', 'two']},
{a: ['three'], b: ['different three']},
{b: ['four']},
{ab: ['five', 'six', 'eight', 'nine']},
],
});
});
test('marked deltas can start shared chunks', () => {
content[0] = {a: ['one'], b: ['two'], common: true};
assert.deepEqual(element._translateChunksToIgnore({content}), {
content: [
{ab: ['two']},
{a: ['three'], b: ['different three']},
{b: ['four']},
{ab: ['five', 'six']},
{a: ['seven']},
{ab: ['eight', 'nine']},
],
});
});
});
});
</script>

View File

@@ -162,8 +162,7 @@
}
// Process the next section and incorporate the result.
const result = this._processNext(
state, content[state.sectionIndex], content.length);
const result = this._processNext(state, content);
for (const group of result.groups) {
this.push('groups', group);
currentBatch += group.lines.length;
@@ -172,7 +171,7 @@
state.lineNums.right += result.lineDelta.right;
// Increment the index and recurse.
state.sectionIndex++;
state.sectionIndex = result.newSectionIndex;
if (currentBatch >= this._asyncThreshold) {
currentBatch = 0;
this._nextStepHandle = this.async(nextStep, 1);
@@ -201,41 +200,111 @@
},
/**
* Process the next section of the diff.
* Process the next uncommon section, or the next common sections.
*
* @param {!Object} state
* @param {!Object} section
* @param {number} numSections
* @param {!Array<!Object>} sections
*/
_processNext(state, section, numSections) {
const lines = this._linesFromSection(
section, state.lineNums.left + 1, state.lineNums.right + 1);
const lineDelta = {
left: section.ab ? section.ab.length : section.a ? section.a.length : 0,
right: section.ab ? section.ab.length :
section.b ? section.b.length : 0,
};
let groups;
if (section.ab) { // If it's a shared section.
let sectionEnd = null;
if (state.sectionIndex === 0) {
sectionEnd = 'first';
} else if (state.sectionIndex === numSections - 1) {
sectionEnd = 'last';
}
groups = this._sharedGroupsFromLines(
lines,
lineDelta.left,
numSections > 1 ? this.context : WHOLE_FILE,
state.lineNums.left,
state.lineNums.right,
sectionEnd);
} else { // Otherwise it's a delta section.
const deltaGroup = new GrDiffGroup(GrDiffGroup.Type.DELTA, lines);
deltaGroup.dueToRebase = section.due_to_rebase;
groups = [deltaGroup];
_processNext(state, sections) {
const firstUncommonSectionIndex = this._firstUncommonSectionIndex(
sections, state.sectionIndex);
if (firstUncommonSectionIndex === state.sectionIndex) {
const section = sections[state.sectionIndex];
return {
lineDelta: {
left: section.a ? section.a.length : 0,
right: section.b ? section.b.length : 0,
},
groups: [this._sectionToGroup(
section, state.lineNums.left + 1, state.lineNums.right + 1)],
newSectionIndex: state.sectionIndex + 1,
};
}
return {lineDelta, groups};
return this._processCommonSections(
state, sections, firstUncommonSectionIndex);
},
_firstUncommonSectionIndex(sections, offset) {
let sectionIndex = offset;
while (sectionIndex < sections.length &&
this._isCommonSection(sections[sectionIndex])) {
sectionIndex++;
}
return sectionIndex;
},
_isCommonSection(section) {
return section.ab || section.common;
},
/**
* Process a stretch of common sections.
*
* 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>} sections
* @param {number} firstUncommonSectionIndex
* @return {!Object}
*/
_processCommonSections(state, sections, firstUncommonSectionIndex) {
const commonSections = sections.slice(
state.sectionIndex, firstUncommonSectionIndex);
const lineCount = commonSections.reduce(
(sum, section) => sum + this._commonSectionLength(section), 0);
let groups = this._sectionsToGroups(
commonSections, state.lineNums.left + 1, state.lineNums.right + 1);
if (this.context !== WHOLE_FILE) {
const hiddenStart = state.sectionIndex === 0 ? 0 : this.context;
const hiddenEnd = lineCount - (
firstUncommonSectionIndex === sections.length ?
0 : this.context);
groups = GrDiffGroup.hideInContextControl(
groups, hiddenStart, hiddenEnd);
}
return {
lineDelta: {
left: lineCount,
right: lineCount,
},
groups,
newSectionIndex: firstUncommonSectionIndex,
};
},
_commonSectionLength(section) {
console.assert(section.ab || section.common);
console.assert(
!section.a || (section.b && section.a.length === section.b.length));
return (section.ab || section.a).length;
},
_sectionsToGroups(sections, offsetLeft, offsetRight) {
return sections.map(section => {
const group = this._sectionToGroup(section, offsetLeft, offsetRight);
const sectionLength = this._commonSectionLength(section);
offsetLeft += sectionLength;
offsetRight += sectionLength;
return group;
});
},
_sectionToGroup(section, offsetLeft, offsetRight) {
const type = section.ab ? GrDiffGroup.Type.BOTH : GrDiffGroup.Type.DELTA;
const lines = this._linesFromSection(section, offsetLeft, offsetRight);
const group = new GrDiffGroup(type, lines);
group.dueToRebase = section.due_to_rebase;
group.ignoredWhitespaceOnly = section.common;
return group;
},
_linesFromSection(section, offsetLeft, offsetRight) {
@@ -247,14 +316,14 @@
if (section.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._deltaLinesFromRows(
lines = lines.concat(this._linesFromRows(
GrDiffLine.Type.REMOVE, section.a, offsetLeft,
section[DiffHighlights.REMOVED]));
}
if (section.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._deltaLinesFromRows(
lines = lines.concat(this._linesFromRows(
GrDiffLine.Type.ADD, section.b, offsetRight,
section[DiffHighlights.ADDED]));
}
@@ -264,7 +333,7 @@
/**
* @return {!Array<!Object>} Array of GrDiffLines
*/
_deltaLinesFromRows(lineType, rows, offset, opt_highlights) {
_linesFromRows(lineType, rows, offset, opt_highlights) {
// Normalize highlights if they have been passed.
if (opt_highlights) {
opt_highlights = this._normalizeIntralineHighlights(rows,
@@ -294,67 +363,6 @@
return line;
},
/**
* Take rows of a shared diff section and produce an array of corresponding
* (potentially collapsed) groups.
* @param {!Array<string>} lines
* @param {number} numLines
* @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
*/
_sharedGroupsFromLines(lines, numLines, context, startLineNumLeft,
startLineNumRight, opt_sectionEnd) {
// 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 hiddenRangeStart = opt_sectionEnd === 'first' ? 0 : context;
const hiddenRangeEnd = opt_sectionEnd === 'last' ?
numLines : numLines - context;
const result = [];
// If there is a range to hide.
if (context !== WHOLE_FILE && hiddenRangeEnd - hiddenRangeStart > 1) {
const linesBeforeCtx = [];
const hiddenLines = [];
const linesAfterCtx = [];
for (const line of lines) {
// In the case there are no changes, these are the same.
// In the case of ignored whitespace changes, either only one is set,
// or the are the same.
const lineOffset = line.beforeNumber ?
line.beforeNumber - startLineNumLeft - 1 :
line.afterNumber - startLineNumRight - 1;
if (lineOffset < hiddenRangeStart) linesBeforeCtx.push(line);
else if (hiddenRangeEnd <= lineOffset) linesAfterCtx.push(line);
else hiddenLines.push(line);
}
if (linesBeforeCtx.length > 0) {
result.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx));
}
const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
ctxLine.contextGroups =
[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;
},
_makeFileComments() {
const line = new GrDiffLine(GrDiffLine.Type.BOTH);
line.beforeNumber = GrDiffLine.FILE;

View File

@@ -264,6 +264,110 @@ limitations under the License.
});
});
test('for interleaved ab and common: true chunks', () => {
element.context = 10;
const content = [
{a: ['all work and no play make andybons a dull boy']},
{ab: new Array(3)
.fill('all work and no play make jill a dull girl')},
{
a: new Array(3).fill(
'all work and no play make jill a dull girl'),
b: new Array(3).fill(
' all work and no play make jill a dull girl'),
common: true,
},
{ab: new Array(3)
.fill('all work and no play make jill a dull girl')},
{
a: new Array(3).fill(
'all work and no play make jill a dull girl'),
b: new Array(3).fill(
' all work and no play make jill a dull girl'),
common: true,
},
{ab: new Array(3)
.fill('all work and no play make jill a dull girl')},
];
return element.process(content).then(() => {
const groups = element.groups;
// group[0] is the file group
// group[1] is the "a" group
// The first three interleaved chunks are completely shown because
// they are part of the context (3 * 3 <= 10)
assert.equal(groups[2].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[2].lines.length, 3);
for (const l of groups[2].lines) {
assert.equal(
l.text, 'all work and no play make jill a dull girl');
}
assert.equal(groups[3].type, GrDiffGroup.Type.DELTA);
assert.equal(groups[3].lines.length, 6);
assert.equal(groups[3].adds.length, 3);
assert.equal(groups[3].removes.length, 3);
for (const l of groups[3].removes) {
assert.equal(
l.text, 'all work and no play make jill a dull girl');
}
for (const l of groups[3].adds) {
assert.equal(
l.text, ' all work and no play make jill a dull girl');
}
assert.equal(groups[4].type, GrDiffGroup.Type.BOTH);
assert.equal(groups[4].lines.length, 3);
for (const l of groups[4].lines) {
assert.equal(
l.text, 'all work and no play make jill a dull girl');
}
// The next chunk is partially shown, so it results in two groups
assert.equal(groups[5].type, GrDiffGroup.Type.DELTA);
assert.equal(groups[5].lines.length, 2);
assert.equal(groups[5].adds.length, 1);
assert.equal(groups[5].removes.length, 1);
for (const l of groups[5].removes) {
assert.equal(
l.text, 'all work and no play make jill a dull girl');
}
for (const l of groups[5].adds) {
assert.equal(
l.text, ' all work and no play make jill a dull girl');
}
assert.equal(groups[6].type, GrDiffGroup.Type.CONTEXT_CONTROL);
assert.equal(groups[6].lines[0].contextGroups.length, 2);
assert.equal(groups[6].lines[0].contextGroups[0].lines.length, 4);
assert.equal(groups[6].lines[0].contextGroups[0].removes.length, 2);
assert.equal(groups[6].lines[0].contextGroups[0].adds.length, 2);
for (const l of groups[6].lines[0].contextGroups[0].removes) {
assert.equal(
l.text, 'all work and no play make jill a dull girl');
}
for (const l of groups[6].lines[0].contextGroups[0].adds) {
assert.equal(
l.text, ' all work and no play make jill a dull girl');
}
// The final chunk is completely hidden
assert.equal(
groups[6].lines[0].contextGroups[1].type,
GrDiffGroup.Type.BOTH);
assert.equal(groups[6].lines[0].contextGroups[1].lines.length, 3);
for (const l of groups[6].lines[0].contextGroups[1].lines) {
assert.equal(
l.text, 'all work and no play make jill a dull girl');
}
});
});
test('in the middle, larger than context', () => {
element.context = 10;
const content = [
@@ -506,10 +610,13 @@ limitations under the License.
sandbox.stub(element, 'async');
element._isScrolling = true;
element.process(content);
// Just the files group - no more processing during scrolling.
assert.equal(element.groups.length, 1);
element._isScrolling = false;
element.process(content);
assert.equal(element.groups.length, 33);
// More groups have been processed. How many does not matter here.
assert.isAtLeast(element.groups.length, 2);
});
test('image diffs', () => {
@@ -528,22 +635,25 @@ limitations under the License.
assert.equal(element.groups[0].lines.length, 1);
});
suite('gr-diff-processor helpers', () => {
suite('_processNext', () => {
let rows;
setup(() => {
rows = loremIpsum.split(' ');
});
test('_processNext WHOLE_FILE', () => {
test('WHOLE_FILE', () => {
element.context = WHOLE_FILE;
const state = {
lineNums: {left: 10, right: 100},
sectionIndex: 1,
};
const result = element._processNext(
state, {ab: rows}, 3);
const sections = [
{a: ['foo']},
{ab: rows},
{a: ['bar']},
];
const result = element._processNext(state, sections);
// Results in one, uncollapsed group with all rows.
assert.equal(result.groups.length, 1);
@@ -564,14 +674,18 @@ limitations under the License.
state.lineNums.right + rows.length);
});
test('_processNext context', () => {
test('with context', () => {
element.context = 10;
const state = {
lineNums: {left: 10, right: 100},
sectionIndex: 1,
};
const result = element._processNext(
state, {ab: rows}, 3);
const sections = [
{a: ['foo']},
{ab: rows},
{a: ['bar']},
];
const result = element._processNext(state, sections);
const expectedCollapseSize = rows.length - 2 * element.context;
assert.equal(result.groups.length, 3, 'Results in three groups');
@@ -587,14 +701,18 @@ limitations under the License.
expectedCollapseSize);
});
test('_processNext first', () => {
test('first', () => {
element.context = 10;
const state = {
lineNums: {left: 10, right: 100},
sectionIndex: 0,
};
const result = element._processNext(
state, {ab: rows}, 3);
const sections = [
{ab: rows},
{a: ['foo']},
{a: ['bar']},
];
const result = element._processNext(state, sections);
const expectedCollapseSize = rows.length - element.context;
assert.equal(result.groups.length, 2, 'Results in two groups');
@@ -608,7 +726,7 @@ limitations under the License.
expectedCollapseSize);
});
test('_processNext few-rows', () => {
test('few-rows', () => {
// Only ten rows.
rows = rows.slice(0, 10);
element.context = 10;
@@ -616,32 +734,48 @@ limitations under the License.
lineNums: {left: 10, right: 100},
sectionIndex: 0,
};
const result = element._processNext(
state, {ab: rows}, 3);
const sections = [
{ab: rows},
{a: ['foo']},
{a: ['bar']},
];
const result = element._processNext(state, sections);
// Results in one uncollapsed group with all rows.
assert.equal(result.groups.length, 1, 'Results in one group');
assert.equal(result.groups[0].lines.length, rows.length);
});
test('_processNext no single line collapse', () => {
test('no single line collapse', () => {
rows = rows.slice(0, 7);
element.context = 3;
const state = {
lineNums: {left: 10, right: 100},
sectionIndex: 1,
};
const result = element._processNext(
state, {ab: rows}, 3);
const sections = [
{a: ['foo']},
{ab: rows},
{a: ['bar']},
];
const result = element._processNext(state, sections);
// Results in one uncollapsed group with all rows.
assert.equal(result.groups.length, 1, 'Results in one group');
assert.equal(result.groups[0].lines.length, rows.length);
});
});
test('_deltaLinesFromRows', () => {
suite('gr-diff-processor helpers', () => {
let rows;
setup(() => {
rows = loremIpsum.split(' ');
});
test('_linesFromRows', () => {
const startLineNum = 10;
let result = element._deltaLinesFromRows(GrDiffLine.Type.ADD, rows,
let result = element._linesFromRows(GrDiffLine.Type.ADD, rows,
startLineNum + 1);
assert.equal(result.length, rows.length);
@@ -652,7 +786,7 @@ limitations under the License.
startLineNum + rows.length);
assert.notOk(result[result.length - 1].beforeNumber);
result = element._deltaLinesFromRows(GrDiffLine.Type.REMOVE, rows,
result = element._linesFromRows(GrDiffLine.Type.REMOVE, rows,
startLineNum + 1);
assert.equal(result.length, rows.length);

View File

@@ -22,20 +22,35 @@
/**
* A chunk of the diff that should be rendered together.
*
* @param {!GrDiffGroup.Type} type
* @param {!Array<!GrDiffLine>=} opt_lines
*/
function GrDiffGroup(type, opt_lines) {
/** @type {!GrDiffGroup.Type} */
this.type = type;
/** @type{!Array<!GrDiffLine>} */
/** @type {boolean} */
this.dueToRebase = false;
/**
* True means all changes in this line are whitespace changes that should
* not be highlighted as changed as per the user settings.
* @type{boolean}
*/
this.ignoredWhitespaceOnly = false;
/** @type {?HTMLElement} */
this.element = null;
/** @type {!Array<!GrDiffLine>} */
this.lines = [];
/** @type{!Array<!GrDiffLine>} */
/** @type {!Array<!GrDiffLine>} */
this.adds = [];
/** @type{!Array<!GrDiffLine>} */
/** @type {!Array<!GrDiffLine>} */
this.removes = [];
/** @type{boolean|undefined} */
this.dueToRebase = undefined;
/** Both start and end line are inclusive. */
this.lineRange = {
left: {start: null, end: null},
right: {start: null, end: null},
@@ -46,8 +61,7 @@
}
}
GrDiffGroup.prototype.element = null;
/** @enum {string} */
GrDiffGroup.Type = {
/** Unchanged context. */
BOTH: 'both',
@@ -59,6 +73,139 @@
DELTA: 'delta',
};
/**
* Hides lines in the given range behind a context control group.
*
* Groups that would be partially visible are split into their visible and
* hidden parts, respectively.
* The groups need to be "common groups", meaning they have to have either
* originated from an `ab` chunk, or from an `a`+`b` chunk with
* `common: true`.
*
* If the hidden range is 1 line or less, nothing is hidden and no context
* control group is created.
*
* @param {!Array<!GrDiffGroup>} groups Common groups, ordered by their line
* ranges.
* @param {number} hiddenStart The first element to be hidden, as a
* non-negative line number offset relative to the first group's start
* line, left and right respectively.
* @param {number} hiddenEnd The first visible element after the hidden range,
* as a non-negative line number offset relative to the first group's
* start line, left and right respectively.
* @return {!Array<!GrDiffGroup>}
*/
GrDiffGroup.hideInContextControl = function(groups, hiddenStart, hiddenEnd) {
if (groups.length === 0) return [];
// Clamp hiddenStart and hiddenEnd - inspired by e.g. substring
hiddenStart = Math.max(hiddenStart, 0);
hiddenEnd = Math.max(hiddenEnd, hiddenStart);
let before = [];
let hidden = groups;
let after = [];
const numHidden = hiddenEnd - hiddenStart;
// Only collapse if there is more than 1 line to be hidden.
if (numHidden > 1) {
if (hiddenStart) {
[before, hidden] = GrDiffGroup._splitCommonGroups(hidden, hiddenStart);
}
if (hiddenEnd) {
[hidden, after] = GrDiffGroup._splitCommonGroups(
hidden, hiddenEnd - hiddenStart);
}
} else {
[hidden, after] = [[], hidden];
}
const result = [...before];
if (hidden.length) {
const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL);
ctxLine.contextGroups = hidden;
const ctxGroup = new GrDiffGroup(
GrDiffGroup.Type.CONTEXT_CONTROL, [ctxLine]);
result.push(ctxGroup);
}
result.push(...after);
return result;
};
/**
* Splits a list of common groups into two lists of groups.
*
* Groups where all lines are before or all lines are after the split will be
* retained as is and put into the first or second list respectively. Groups
* with some lines before and some lines after the split will be split into
* two groups, which will be put into the first and second list.
*
* @param {!Array<!GrDiffGroup>} groups
* @param {number} split A line number offset relative to the first group's
* start line at which the groups should be split.
* @return {!Array<!Array<!GrDiffGroup>>} The outer array has 2 elements, the
* list of groups before and the list of groups after the split.
*/
GrDiffGroup._splitCommonGroups = function(groups, split) {
if (groups.length === 0) return [[], []];
const leftSplit = groups[0].lineRange.left.start + split;
const rightSplit = groups[0].lineRange.right.start + split;
const beforeGroups = [];
const afterGroups = [];
for (const group of groups) {
if (group.lineRange.left.end < leftSplit ||
group.lineRange.right.end < rightSplit) {
beforeGroups.push(group);
continue;
}
if (leftSplit <= group.lineRange.left.start ||
rightSplit <= group.lineRange.right.start) {
afterGroups.push(group);
continue;
}
const before = [];
const after = [];
for (const line of group.lines) {
if ((line.beforeNumber && line.beforeNumber < leftSplit) ||
(line.afterNumber && line.afterNumber < rightSplit)) {
before.push(line);
} else {
after.push(line);
}
}
if (before.length) {
beforeGroups.push(before.length === group.lines.length ?
group : group.cloneWithLines(before));
}
if (after.length) {
afterGroups.push(after.length === group.lines.length ?
group : group.cloneWithLines(after));
}
}
return [beforeGroups, afterGroups];
};
/**
* Creates a new group with the same properties but different lines.
*
* The element property is not copied, because the original element is still a
* rendering of the old lines, so that would not make sense.
*
* @param {!Array<!GrDiffLine>} lines
* @return {!GrDiffGroup}
*/
GrDiffGroup.prototype.cloneWithLines = function(lines) {
const group = new GrDiffGroup(this.type, lines);
group.dueToRebase = this.dueToRebase;
group.ignoredWhitespaceOnly = this.ignoredWhitespaceOnly;
return group;
};
/** @param {!GrDiffLine} line */
GrDiffGroup.prototype.addLine = function(line) {
this.lines.push(line);
@@ -77,6 +224,7 @@
this._updateRange(line);
};
/** @return {!Array<{left: GrDiffLine, right: GrDiffLine}>} */
GrDiffGroup.prototype.getSideBySidePairs = function() {
if (this.type === GrDiffGroup.Type.BOTH ||
this.type === GrDiffGroup.Type.CONTEXT_CONTROL) {

View File

@@ -30,12 +30,9 @@ limitations under the License.
suite('gr-diff-group tests', () => {
test('delta line pairs', () => {
let group = new GrDiffGroup(GrDiffGroup.Type.DELTA);
const l1 = new GrDiffLine(GrDiffLine.Type.ADD);
const l2 = new GrDiffLine(GrDiffLine.Type.ADD);
const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE);
l1.afterNumber = 128;
l2.afterNumber = 129;
l3.beforeNumber = 64;
const l1 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 128);
const l2 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 129);
const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE, 64, 0);
group.addLine(l1);
group.addLine(l2);
group.addLine(l3);
@@ -66,17 +63,9 @@ limitations under the License.
});
test('group/header line pairs', () => {
const l1 = new GrDiffLine(GrDiffLine.Type.BOTH);
l1.beforeNumber = 64;
l1.afterNumber = 128;
const l2 = new GrDiffLine(GrDiffLine.Type.BOTH);
l2.beforeNumber = 65;
l2.afterNumber = 129;
const l3 = new GrDiffLine(GrDiffLine.Type.BOTH);
l3.beforeNumber = 66;
l3.afterNumber = 130;
const l1 = new GrDiffLine(GrDiffLine.Type.BOTH, 64, 128);
const l2 = new GrDiffLine(GrDiffLine.Type.BOTH, 65, 129);
const l3 = new GrDiffLine(GrDiffLine.Type.BOTH, 66, 130);
let group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]);
@@ -124,6 +113,97 @@ limitations under the License.
assert.throws(group.addLine.bind(group, l2));
assert.doesNotThrow(group.addLine.bind(group, l3));
});
suite('hideInContextControl', () => {
let groups;
setup(() => {
groups = [
new GrDiffGroup(GrDiffGroup.Type.BOTH, [
new GrDiffLine(GrDiffLine.Type.BOTH, 5, 7),
new GrDiffLine(GrDiffLine.Type.BOTH, 6, 8),
new GrDiffLine(GrDiffLine.Type.BOTH, 7, 9),
]),
new GrDiffGroup(GrDiffGroup.Type.DELTA, [
new GrDiffLine(GrDiffLine.Type.REMOVE, 8),
new GrDiffLine(GrDiffLine.Type.ADD, 0, 10),
new GrDiffLine(GrDiffLine.Type.REMOVE, 9),
new GrDiffLine(GrDiffLine.Type.ADD, 0, 11),
new GrDiffLine(GrDiffLine.Type.REMOVE, 10),
new GrDiffLine(GrDiffLine.Type.ADD, 0, 12),
]),
new GrDiffGroup(GrDiffGroup.Type.BOTH, [
new GrDiffLine(GrDiffLine.Type.BOTH, 11, 13),
new GrDiffLine(GrDiffLine.Type.BOTH, 12, 14),
new GrDiffLine(GrDiffLine.Type.BOTH, 13, 15),
]),
];
});
test('hides hidden groups in context control', () => {
const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 3, 6);
assert.equal(collapsedGroups.length, 3);
assert.equal(collapsedGroups[0], groups[0]);
assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL);
assert.equal(collapsedGroups[1].lines.length, 1);
assert.equal(
collapsedGroups[1].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
assert.equal(
collapsedGroups[1].lines[0].contextGroups.length, 1);
assert.equal(
collapsedGroups[1].lines[0].contextGroups[0], groups[1]);
assert.equal(collapsedGroups[2], groups[2]);
});
test('splits partially hidden groups', () => {
const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 4, 7);
assert.equal(collapsedGroups.length, 4);
assert.equal(collapsedGroups[0], groups[0]);
assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.DELTA);
assert.deepEqual(collapsedGroups[1].adds, [groups[1].adds[0]]);
assert.deepEqual(collapsedGroups[1].removes, [groups[1].removes[0]]);
assert.equal(collapsedGroups[2].type, GrDiffGroup.Type.CONTEXT_CONTROL);
assert.equal(collapsedGroups[2].lines.length, 1);
assert.equal(
collapsedGroups[2].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL);
assert.equal(
collapsedGroups[2].lines[0].contextGroups.length, 2);
assert.equal(
collapsedGroups[2].lines[0].contextGroups[0].type,
GrDiffGroup.Type.DELTA);
assert.deepEqual(
collapsedGroups[2].lines[0].contextGroups[0].adds,
groups[1].adds.slice(1));
assert.deepEqual(
collapsedGroups[2].lines[0].contextGroups[0].removes,
groups[1].removes.slice(1));
assert.equal(
collapsedGroups[2].lines[0].contextGroups[1].type,
GrDiffGroup.Type.BOTH);
assert.deepEqual(
collapsedGroups[2].lines[0].contextGroups[1].lines,
[groups[2].lines[0]]);
assert.equal(collapsedGroups[3].type, GrDiffGroup.Type.BOTH);
assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1));
});
test('groups unchanged if the hidden range is empty', () => {
assert.deepEqual(
GrDiffGroup.hideInContextControl(groups, 0, 0), groups);
});
test('groups unchanged if there is only 1 line to hide', () => {
assert.deepEqual(
GrDiffGroup.hideInContextControl(groups, 3, 4), groups);
});
});
});
</script>

View File

@@ -20,21 +20,27 @@
// Prevent redefinition.
if (window.GrDiffLine) { return; }
function GrDiffLine(type) {
/**
* @param {GrDiffLine.Type} type
* @param {number|string=} opt_beforeLine
* @param {number|string=} opt_afterLine
*/
function GrDiffLine(type, opt_beforeLine, opt_afterLine) {
this.type = type;
/** @type {number|string} */
this.beforeNumber = opt_beforeLine || 0;
/** @type {number|string} */
this.afterNumber = opt_afterLine || 0;
this.highlights = [];
/** @type {?Array<Object>} ?Array<!GrDiffGroup> */
this.contextGroups = null;
this.text = '';
}
/** @type {number|string} */
GrDiffLine.prototype.afterNumber = 0;
/** @type {number|string} */
GrDiffLine.prototype.beforeNumber = 0;
/** @type {?Array<Object>} ?Array<!GrDiffLine> */
GrDiffLine.prototype.contextGroups = null;
GrDiffLine.prototype.text = '';
GrDiffLine.Type = {
ADD: 'add',

View File

@@ -136,6 +136,8 @@ limitations under the License.
.content.remove {
background-color: var(--light-remove-highlight-color);
}
/* dueToRebase */
.dueToRebase .content.add .intraline,
.delta.total.dueToRebase .content.add {
background-color: var(--dark-rebased-add-highlight-color);
@@ -150,6 +152,17 @@ limitations under the License.
.dueToRebase .content.remove {
background-color: var(--light-remove-add-highlight-color);
}
/* ignoredWhitespaceOnly */
.ignoredWhitespaceOnly .content.add .intraline,
.delta.total.ignoredWhitespaceOnly .content.add,
.ignoredWhitespaceOnly .content.add,
.ignoredWhitespaceOnly .content.remove .intraline,
.delta.total.ignoredWhitespaceOnly .content.remove,
.ignoredWhitespaceOnly .content.remove {
background: none;
}
.content .contentText:empty:after {
/* Newline, to ensure empty lines are one line-height tall. */
content: '\A';