blob: c13092f9209c25cef83ae289af89c1c6b67ef327 [file] [log] [blame]
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04001/**
2 * @license
3 * Copyright (C) 2016 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
Ben Rohlfs32b83822020-08-14 22:08:37 +020017import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
18import {PolymerElement} from '@polymer/polymer/polymer-element';
19import {
20 GrDiffLine,
21 GrDiffLineType,
22 FILE,
23 Highlights,
Dhruv Srivastavaac2bbd32021-02-04 22:08:55 +010024 LineNumber,
Ben Rohlfs32b83822020-08-14 22:08:37 +020025} from '../gr-diff/gr-diff-line';
26import {
27 GrDiffGroup,
28 GrDiffGroupType,
29 hideInContextControl,
30} from '../gr-diff/gr-diff-group';
31import {CancelablePromise, util} from '../../../scripts/util';
32import {customElement, property} from '@polymer/decorators';
Ole899f7d22020-11-17 17:18:22 +010033import {DiffContent} from '../../../types/diff';
Ben Rohlfs1d487062020-09-26 11:26:03 +020034import {Side} from '../../../constants/constants';
Wyatt Allen7f2bd972016-06-27 12:19:21 -070035
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010036const WHOLE_FILE = -1;
Wyatt Allen7f2bd972016-06-27 12:19:21 -070037
Ben Rohlfs32b83822020-08-14 22:08:37 +020038interface State {
39 lineNums: {
40 left: number;
41 right: number;
42 };
43 chunkIndex: number;
44}
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010045
Ben Rohlfs32b83822020-08-14 22:08:37 +020046interface ChunkEnd {
47 offset: number;
48 keyLocation: boolean;
49}
50
Ben Rohlfs4fa7c532020-08-24 18:16:13 +020051export interface KeyLocations {
Ben Rohlfs32b83822020-08-14 22:08:37 +020052 left: {[key: string]: boolean};
53 right: {[key: string]: boolean};
54}
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010055
56/**
57 * The maximum size for an addition or removal chunk before it is broken down
58 * into a series of chunks that are this size at most.
59 *
60 * Note: The value of 120 is chosen so that it is larger than the default
61 * _asyncThreshold of 64, but feel free to tune this constant to your
62 * performance needs.
63 */
64const MAX_GROUP_SIZE = 120;
65
Ben Rohlfs04fc1ca2021-02-13 13:06:53 +010066const DEBOUNCER_RESET_IS_SCROLLING = 'resetIsScrolling';
67
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010068/**
69 * Converts the API's `DiffContent`s to `GrDiffGroup`s for rendering.
70 *
71 * Glossary:
72 * - "chunk": A single `DiffContent` as returned by the API.
73 * - "group": A single `GrDiffGroup` as used for rendering.
74 * - "common" chunk/group: A chunk/group that should be considered unchanged
75 * for diffing purposes. This can mean its either actually unchanged, or it
76 * has only whitespace changes.
77 * - "key location": A line number and side of the diff that should not be
78 * collapsed e.g. because a comment is attached to it, or because it was
79 * provided in the URL and thus should be visible
80 * - "uncollapsible" chunk/group: A chunk/group that is either not "common",
81 * or cannot be collapsed because it contains a key location
82 *
83 * Here a a number of tasks this processor performs:
84 * - splitting large chunks to allow more granular async rendering
85 * - adding a group for the "File" pseudo line that file-level comments can
86 * be attached to
87 * - replacing common parts of the diff that are outside the user's
88 * context setting and do not have comments with a group representing the
89 * "expand context" widget. This may require splitting a chunk/group so
90 * that the part that is within the context or has comments is shown, while
91 * the rest is not.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010092 */
Ben Rohlfs32b83822020-08-14 22:08:37 +020093@customElement('gr-diff-processor')
Ben Rohlfsd94011e2021-03-08 23:37:58 +010094export class GrDiffProcessor extends LegacyElementMixin(PolymerElement) {
Ben Rohlfs32b83822020-08-14 22:08:37 +020095 @property({type: Number})
96 context = 3;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010097
Ben Rohlfs32b83822020-08-14 22:08:37 +020098 @property({type: Array, notify: true})
99 groups: GrDiffGroup[] = [];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100100
Ben Rohlfs32b83822020-08-14 22:08:37 +0200101 @property({type: Object})
102 keyLocations: KeyLocations = {left: {}, right: {}};
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100103
Ben Rohlfs32b83822020-08-14 22:08:37 +0200104 @property({type: Number})
105 _asyncThreshold = 64;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100106
Ben Rohlfs32b83822020-08-14 22:08:37 +0200107 @property({type: Number})
108 _nextStepHandle: number | null = null;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100109
Ben Rohlfs32b83822020-08-14 22:08:37 +0200110 @property({type: Object})
111 _processPromise: CancelablePromise<void> | null = null;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100112
Ben Rohlfs32b83822020-08-14 22:08:37 +0200113 @property({type: Boolean})
114 _isScrolling?: boolean;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100115
116 /** @override */
Ben Rohlfs5f520da2021-03-10 14:58:43 +0100117 connectedCallback() {
118 super.connectedCallback();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100119 this.listen(window, 'scroll', '_handleWindowScroll');
120 }
121
122 /** @override */
Ben Rohlfs5f520da2021-03-10 14:58:43 +0100123 disconnectedCallback() {
Ben Rohlfs04fc1ca2021-02-13 13:06:53 +0100124 this.cancelDebouncer(DEBOUNCER_RESET_IS_SCROLLING);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100125 this.cancel();
126 this.unlisten(window, 'scroll', '_handleWindowScroll');
Ben Rohlfs5f520da2021-03-10 14:58:43 +0100127 super.disconnectedCallback();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100128 }
129
130 _handleWindowScroll() {
131 this._isScrolling = true;
Ben Rohlfs32b83822020-08-14 22:08:37 +0200132 this.debounce(
Ben Rohlfs04fc1ca2021-02-13 13:06:53 +0100133 DEBOUNCER_RESET_IS_SCROLLING,
Ben Rohlfs32b83822020-08-14 22:08:37 +0200134 () => {
135 this._isScrolling = false;
136 },
137 50
138 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100139 }
Renan Oliveiraa132fae2019-09-19 14:38:22 +0000140
Wyatt Allen32b03fc2016-08-05 15:56:33 -0700141 /**
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100142 * Asynchronously process the diff chunks into groups. As it processes, it
143 * will splice groups into the `groups` property of the component.
Wyatt Allen32b03fc2016-08-05 15:56:33 -0700144 *
Ben Rohlfs32b83822020-08-14 22:08:37 +0200145 * @return A promise that resolves with an
146 * array of GrDiffGroups when the diff is completely processed.
Wyatt Allen32b03fc2016-08-05 15:56:33 -0700147 */
Ben Rohlfs32b83822020-08-14 22:08:37 +0200148 process(chunks: DiffContent[], isBinary: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100149 // Cancel any still running process() calls, because they append to the
150 // same groups field.
151 this.cancel();
Wyatt Allen32b03fc2016-08-05 15:56:33 -0700152
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100153 this.groups = [];
Dhruv Srivastavaac2bbd32021-02-04 22:08:55 +0100154 this.push('groups', this._makeGroup('LOST'));
155 this.push('groups', this._makeGroup(FILE));
Wyatt Allen7f2bd972016-06-27 12:19:21 -0700156
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100157 // If it's a binary diff, we won't be rendering hunks of text differences
158 // so finish processing.
Ben Rohlfs32b83822020-08-14 22:08:37 +0200159 if (isBinary) {
160 return Promise.resolve();
161 }
Wyatt Allen7f2bd972016-06-27 12:19:21 -0700162
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100163 this._processPromise = util.makeCancelable(
Ben Rohlfs32b83822020-08-14 22:08:37 +0200164 new Promise(resolve => {
165 const state = {
166 lineNums: {left: 0, right: 0},
167 chunkIndex: 0,
168 };
Wyatt Allen7f2bd972016-06-27 12:19:21 -0700169
Ben Rohlfs32b83822020-08-14 22:08:37 +0200170 chunks = this._splitLargeChunks(chunks);
171 chunks = this._splitCommonChunksWithKeyLocations(chunks);
Wyatt Allen7f2bd972016-06-27 12:19:21 -0700172
Ben Rohlfs32b83822020-08-14 22:08:37 +0200173 let currentBatch = 0;
174 const nextStep = () => {
175 if (this._isScrolling) {
176 this._nextStepHandle = this.async(nextStep, 100);
177 return;
178 }
179 // If we are done, resolve the promise.
180 if (state.chunkIndex >= chunks.length) {
181 resolve();
182 this._nextStepHandle = null;
183 return;
184 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100185
Ben Rohlfs32b83822020-08-14 22:08:37 +0200186 // Process the next chunk and incorporate the result.
187 const stateUpdate = this._processNext(state, chunks);
188 for (const group of stateUpdate.groups) {
189 this.push('groups', group);
190 currentBatch += group.lines.length;
191 }
192 state.lineNums.left += stateUpdate.lineDelta.left;
193 state.lineNums.right += stateUpdate.lineDelta.right;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100194
Ben Rohlfs32b83822020-08-14 22:08:37 +0200195 // Increment the index and recurse.
196 state.chunkIndex = stateUpdate.newChunkIndex;
197 if (currentBatch >= this._asyncThreshold) {
198 currentBatch = 0;
199 this._nextStepHandle = this.async(nextStep, 1);
200 } else {
201 nextStep.call(this);
202 }
203 };
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100204
Ben Rohlfs32b83822020-08-14 22:08:37 +0200205 nextStep.call(this);
206 })
207 );
208 return this._processPromise.finally(() => {
209 this._processPromise = null;
210 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100211 }
212
213 /**
214 * Cancel any jobs that are running.
215 */
216 cancel() {
Ben Rohlfs32b83822020-08-14 22:08:37 +0200217 if (this._nextStepHandle !== null) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100218 this.cancelAsync(this._nextStepHandle);
219 this._nextStepHandle = null;
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100220 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100221 if (this._processPromise) {
222 this._processPromise.cancel();
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100223 }
224 }
225
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100226 /**
227 * Process the next uncollapsible chunk, or the next collapsible chunks.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100228 */
Ben Rohlfs32b83822020-08-14 22:08:37 +0200229 _processNext(state: State, chunks: DiffContent[]) {
230 const firstUncollapsibleChunkIndex = this._firstUncollapsibleChunkIndex(
231 chunks,
232 state.chunkIndex
233 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100234 if (firstUncollapsibleChunkIndex === state.chunkIndex) {
235 const chunk = chunks[state.chunkIndex];
236 return {
237 lineDelta: {
238 left: this._linesLeft(chunk).length,
239 right: this._linesRight(chunk).length,
240 },
Ben Rohlfs32b83822020-08-14 22:08:37 +0200241 groups: [
242 this._chunkToGroup(
243 chunk,
244 state.lineNums.left + 1,
245 state.lineNums.right + 1
246 ),
247 ],
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100248 newChunkIndex: state.chunkIndex + 1,
249 };
250 }
251
252 return this._processCollapsibleChunks(
Ben Rohlfs32b83822020-08-14 22:08:37 +0200253 state,
254 chunks,
255 firstUncollapsibleChunkIndex
256 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100257 }
258
Ben Rohlfs32b83822020-08-14 22:08:37 +0200259 _linesLeft(chunk: DiffContent) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100260 return chunk.ab || chunk.a || [];
261 }
262
Ben Rohlfs32b83822020-08-14 22:08:37 +0200263 _linesRight(chunk: DiffContent) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100264 return chunk.ab || chunk.b || [];
265 }
266
Ben Rohlfs32b83822020-08-14 22:08:37 +0200267 _firstUncollapsibleChunkIndex(chunks: DiffContent[], offset: number) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100268 let chunkIndex = offset;
Ben Rohlfs32b83822020-08-14 22:08:37 +0200269 while (
270 chunkIndex < chunks.length &&
271 this._isCollapsibleChunk(chunks[chunkIndex])
272 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100273 chunkIndex++;
274 }
275 return chunkIndex;
276 }
277
Ben Rohlfs32b83822020-08-14 22:08:37 +0200278 _isCollapsibleChunk(chunk: DiffContent) {
Renan Oliveira81d677e2020-11-04 15:31:02 +0100279 return (chunk.ab || chunk.common || chunk.skip) && !chunk.keyLocation;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100280 }
281
282 /**
283 * Process a stretch of collapsible chunks.
284 *
285 * Outputs up to three groups:
Ben Rohlfs32b83822020-08-14 22:08:37 +0200286 * 1) Visible context before the hidden common code, unless it's the
287 * very beginning of the file.
288 * 2) Context hidden behind a context bar, unless empty.
289 * 3) Visible context after the hidden common code, unless it's the very
290 * end of the file.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100291 */
292 _processCollapsibleChunks(
Ben Rohlfs32b83822020-08-14 22:08:37 +0200293 state: State,
294 chunks: DiffContent[],
295 firstUncollapsibleChunkIndex: number
296 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100297 const collapsibleChunks = chunks.slice(
Ben Rohlfs32b83822020-08-14 22:08:37 +0200298 state.chunkIndex,
299 firstUncollapsibleChunkIndex
300 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100301 const lineCount = collapsibleChunks.reduce(
Ben Rohlfs32b83822020-08-14 22:08:37 +0200302 (sum, chunk) => sum + this._commonChunkLength(chunk),
303 0
304 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100305
306 let groups = this._chunksToGroups(
Ben Rohlfs32b83822020-08-14 22:08:37 +0200307 collapsibleChunks,
308 state.lineNums.left + 1,
309 state.lineNums.right + 1
310 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100311
Renan Oliveira2310dd42020-11-09 14:15:20 +0100312 const hasSkippedGroup = !!groups.find(g => g.skip);
313 if (this.context !== WHOLE_FILE || hasSkippedGroup) {
314 const contextNumLines = this.context > 0 ? this.context : 0;
315 const hiddenStart = state.chunkIndex === 0 ? 0 : contextNumLines;
Ben Rohlfs32b83822020-08-14 22:08:37 +0200316 const hiddenEnd =
317 lineCount -
318 (firstUncollapsibleChunkIndex === chunks.length ? 0 : this.context);
319 groups = hideInContextControl(groups, hiddenStart, hiddenEnd);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100320 }
321
322 return {
323 lineDelta: {
324 left: lineCount,
325 right: lineCount,
326 },
327 groups,
328 newChunkIndex: firstUncollapsibleChunkIndex,
329 };
330 }
331
Ben Rohlfs32b83822020-08-14 22:08:37 +0200332 _commonChunkLength(chunk: DiffContent) {
Renan Oliveira81d677e2020-11-04 15:31:02 +0100333 if (chunk.skip) {
334 return chunk.skip;
335 }
Ben Rohlfs32b83822020-08-14 22:08:37 +0200336 console.assert(!!chunk.ab || !!chunk.common);
Renan Oliveira81d677e2020-11-04 15:31:02 +0100337
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100338 console.assert(
Ben Rohlfs32b83822020-08-14 22:08:37 +0200339 !chunk.a || (!!chunk.b && chunk.a.length === chunk.b.length),
340 'common chunk needs same number of a and b lines: ',
341 chunk
342 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100343 return this._linesLeft(chunk).length;
344 }
345
Ben Rohlfs32b83822020-08-14 22:08:37 +0200346 _chunksToGroups(
347 chunks: DiffContent[],
348 offsetLeft: number,
349 offsetRight: number
350 ): GrDiffGroup[] {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100351 return chunks.map(chunk => {
352 const group = this._chunkToGroup(chunk, offsetLeft, offsetRight);
353 const chunkLength = this._commonChunkLength(chunk);
354 offsetLeft += chunkLength;
355 offsetRight += chunkLength;
356 return group;
357 });
358 }
359
Ben Rohlfs32b83822020-08-14 22:08:37 +0200360 _chunkToGroup(
361 chunk: DiffContent,
362 offsetLeft: number,
363 offsetRight: number
364 ): GrDiffGroup {
Renan Oliveira81d677e2020-11-04 15:31:02 +0100365 const type =
366 chunk.ab || chunk.skip ? GrDiffGroupType.BOTH : GrDiffGroupType.DELTA;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100367 const lines = this._linesFromChunk(chunk, offsetLeft, offsetRight);
368 const group = new GrDiffGroup(type, lines);
Ben Rohlfs32b83822020-08-14 22:08:37 +0200369 group.keyLocation = !!chunk.keyLocation;
370 group.dueToRebase = !!chunk.due_to_rebase;
Renan Oliveirafb4ac832020-11-26 21:17:59 +0100371 group.moveDetails = chunk.move_details;
Renan Oliveira81d677e2020-11-04 15:31:02 +0100372 group.skip = chunk.skip;
Ben Rohlfs32b83822020-08-14 22:08:37 +0200373 group.ignoredWhitespaceOnly = !!chunk.common;
Renan Oliveira81d677e2020-11-04 15:31:02 +0100374 if (chunk.skip) {
375 group.lineRange = {
Ole Rehmsen8e744f42021-02-05 11:10:20 +0100376 left: {start_line: offsetLeft, end_line: offsetLeft + chunk.skip - 1},
377 right: {
378 start_line: offsetRight,
379 end_line: offsetRight + chunk.skip - 1,
380 },
Renan Oliveira81d677e2020-11-04 15:31:02 +0100381 };
382 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100383 return group;
384 }
385
Ben Rohlfs32b83822020-08-14 22:08:37 +0200386 _linesFromChunk(chunk: DiffContent, offsetLeft: number, offsetRight: number) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100387 if (chunk.ab) {
Ben Rohlfs32b83822020-08-14 22:08:37 +0200388 return chunk.ab.map((row, i) =>
389 this._lineFromRow(GrDiffLineType.BOTH, offsetLeft, offsetRight, row, i)
390 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100391 }
Ben Rohlfs32b83822020-08-14 22:08:37 +0200392 let lines: GrDiffLine[] = [];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100393 if (chunk.a) {
394 // Avoiding a.push(...b) because that causes callstack overflows for
395 // large b, which can occur when large files are added removed.
Ben Rohlfs32b83822020-08-14 22:08:37 +0200396 lines = lines.concat(
397 this._linesFromRows(
398 GrDiffLineType.REMOVE,
399 chunk.a,
400 offsetLeft,
401 chunk.edit_a
402 )
403 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100404 }
405 if (chunk.b) {
406 // Avoiding a.push(...b) because that causes callstack overflows for
407 // large b, which can occur when large files are added removed.
Ben Rohlfs32b83822020-08-14 22:08:37 +0200408 lines = lines.concat(
409 this._linesFromRows(
410 GrDiffLineType.ADD,
411 chunk.b,
412 offsetRight,
413 chunk.edit_b
414 )
415 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100416 }
417 return lines;
418 }
419
Ben Rohlfs32b83822020-08-14 22:08:37 +0200420 _linesFromRows(
421 lineType: GrDiffLineType,
422 rows: string[],
423 offset: number,
424 intralineInfos?: number[][]
425 ): GrDiffLine[] {
426 const grDiffHighlights = intralineInfos
427 ? this._convertIntralineInfos(rows, intralineInfos)
428 : undefined;
429 return rows.map((row, i) =>
430 this._lineFromRow(lineType, offset, offset, row, i, grDiffHighlights)
431 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100432 }
433
Ben Rohlfs32b83822020-08-14 22:08:37 +0200434 _lineFromRow(
435 type: GrDiffLineType,
436 offsetLeft: number,
437 offsetRight: number,
438 row: string,
439 i: number,
Ben Rohlfsa7a4f512020-08-25 13:40:59 +0200440 highlights?: Highlights[]
Ben Rohlfs32b83822020-08-14 22:08:37 +0200441 ): GrDiffLine {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100442 const line = new GrDiffLine(type);
443 line.text = row;
Ben Rohlfs5e2d1e72020-08-03 19:33:31 +0200444 if (type !== GrDiffLineType.ADD) line.beforeNumber = offsetLeft + i;
445 if (type !== GrDiffLineType.REMOVE) line.afterNumber = offsetRight + i;
Ben Rohlfs32b83822020-08-14 22:08:37 +0200446 if (highlights) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100447 line.hasIntralineInfo = true;
Ben Rohlfs32b83822020-08-14 22:08:37 +0200448 line.highlights = highlights.filter(hl => hl.contentIndex === i);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100449 } else {
450 line.hasIntralineInfo = false;
451 }
452 return line;
453 }
454
Dhruv Srivastavaac2bbd32021-02-04 22:08:55 +0100455 _makeGroup(number: LineNumber) {
Ben Rohlfs5e2d1e72020-08-03 19:33:31 +0200456 const line = new GrDiffLine(GrDiffLineType.BOTH);
Dhruv Srivastavaac2bbd32021-02-04 22:08:55 +0100457 line.beforeNumber = number;
458 line.afterNumber = number;
Ben Rohlfs5e2d1e72020-08-03 19:33:31 +0200459 return new GrDiffGroup(GrDiffGroupType.BOTH, [line]);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100460 }
461
462 /**
463 * Split chunks into smaller chunks of the same kind.
464 *
465 * This is done to prevent doing too much work on the main thread in one
466 * uninterrupted rendering step, which would make the browser unresponsive.
467 *
468 * Note that in the case of unmodified chunks, we only split chunks if the
469 * context is set to file (because otherwise they are split up further down
470 * the processing into the visible and hidden context), and only split it
471 * into 2 chunks, one max sized one and the rest (for reasons that are
472 * unclear to me).
473 *
Ben Rohlfs32b83822020-08-14 22:08:37 +0200474 * @param chunks Chunks as returned from the server
475 * @return Finer grained chunks.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100476 */
Ben Rohlfs32b83822020-08-14 22:08:37 +0200477 _splitLargeChunks(chunks: DiffContent[]): DiffContent[] {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100478 const newChunks = [];
479
480 for (const chunk of chunks) {
481 if (!chunk.ab) {
482 for (const subChunk of this._breakdownChunk(chunk)) {
483 newChunks.push(subChunk);
484 }
485 continue;
486 }
487
488 // If the context is set to "whole file", then break down the shared
489 // chunks so they can be rendered incrementally. Note: this is not
490 // enabled for any other context preference because manipulating the
491 // chunks in this way violates assumptions by the context grouper logic.
492 if (this.context === -1 && chunk.ab.length > MAX_GROUP_SIZE * 2) {
493 // Split large shared chunks in two, where the first is the maximum
494 // group size.
495 newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)});
496 newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)});
497 } else {
498 newChunks.push(chunk);
499 }
500 }
501 return newChunks;
502 }
503
504 /**
505 * In order to show key locations, such as comments, out of the bounds of
506 * the selected context, treat them as separate chunks within the model so
507 * that the content (and context surrounding it) renders correctly.
508 *
Ben Rohlfs32b83822020-08-14 22:08:37 +0200509 * @param chunks DiffContents as returned from server.
510 * @return Finer grained DiffContents.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100511 */
Ben Rohlfs32b83822020-08-14 22:08:37 +0200512 _splitCommonChunksWithKeyLocations(chunks: DiffContent[]): DiffContent[] {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100513 const result = [];
514 let leftLineNum = 1;
515 let rightLineNum = 1;
516
517 for (const chunk of chunks) {
518 // If it isn't a common chunk, append it as-is and update line numbers.
Renan Oliveira81d677e2020-11-04 15:31:02 +0100519 if (!chunk.ab && !chunk.skip && !chunk.common) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100520 if (chunk.a) {
521 leftLineNum += chunk.a.length;
522 }
523 if (chunk.b) {
524 rightLineNum += chunk.b.length;
525 }
526 result.push(chunk);
527 continue;
528 }
529
Ben Rohlfs32b83822020-08-14 22:08:37 +0200530 if (chunk.common && chunk.a!.length !== chunk.b!.length) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100531 throw new Error(
Ben Rohlfs32b83822020-08-14 22:08:37 +0200532 'DiffContent with common=true must always have equal length'
533 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100534 }
535 const numLines = this._commonChunkLength(chunk);
536 const chunkEnds = this._findChunkEndsAtKeyLocations(
Ben Rohlfs32b83822020-08-14 22:08:37 +0200537 numLines,
538 leftLineNum,
539 rightLineNum
540 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100541 leftLineNum += numLines;
542 rightLineNum += numLines;
543
Renan Oliveira81d677e2020-11-04 15:31:02 +0100544 if (chunk.skip) {
545 result.push({
546 ...chunk,
547 skip: chunk.skip,
548 keyLocation: false,
549 });
550 } else if (chunk.ab) {
Ben Rohlfs32b83822020-08-14 22:08:37 +0200551 result.push(
552 ...this._splitAtChunkEnds(chunk.ab, chunkEnds).map(
553 ({lines, keyLocation}) => {
Tao Zhou4cd35cb2020-07-22 11:28:22 +0200554 return {
555 ...chunk,
556 ab: lines,
557 keyLocation,
558 };
Ben Rohlfs32b83822020-08-14 22:08:37 +0200559 }
560 )
561 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100562 } else if (chunk.common) {
Ben Rohlfs32b83822020-08-14 22:08:37 +0200563 const aChunks = this._splitAtChunkEnds(chunk.a!, chunkEnds);
564 const bChunks = this._splitAtChunkEnds(chunk.b!, chunkEnds);
565 result.push(
566 ...aChunks.map(({lines, keyLocation}, i) => {
567 return {
568 ...chunk,
569 a: lines,
570 b: bChunks[i].lines,
571 keyLocation,
572 };
573 })
574 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100575 }
576 }
577
578 return result;
579 }
580
581 /**
Ben Rohlfs32b83822020-08-14 22:08:37 +0200582 * @return Offsets of the new chunk ends, including whether it's a key
583 * location.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100584 */
Ben Rohlfs32b83822020-08-14 22:08:37 +0200585 _findChunkEndsAtKeyLocations(
586 numLines: number,
587 leftOffset: number,
588 rightOffset: number
589 ): ChunkEnd[] {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100590 const result = [];
591 let lastChunkEnd = 0;
Ben Rohlfs32b83822020-08-14 22:08:37 +0200592 for (let i = 0; i < numLines; i++) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100593 // If this line should not be collapsed.
Ben Rohlfs32b83822020-08-14 22:08:37 +0200594 if (
Ben Rohlfs1d487062020-09-26 11:26:03 +0200595 this.keyLocations[Side.LEFT][leftOffset + i] ||
596 this.keyLocations[Side.RIGHT][rightOffset + i]
Ben Rohlfs32b83822020-08-14 22:08:37 +0200597 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100598 // If any lines have been accumulated into the chunk leading up to
599 // this non-collapse line, then add them as a chunk and start a new
600 // one.
601 if (i > lastChunkEnd) {
602 result.push({offset: i, keyLocation: false});
603 lastChunkEnd = i;
604 }
605
606 // Add the non-collapse line as its own chunk.
607 result.push({offset: i + 1, keyLocation: true});
608 }
609 }
610
611 if (numLines > lastChunkEnd) {
612 result.push({offset: numLines, keyLocation: false});
613 }
614
615 return result;
616 }
617
Ben Rohlfs32b83822020-08-14 22:08:37 +0200618 _splitAtChunkEnds(lines: string[], chunkEnds: ChunkEnd[]) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100619 const result = [];
620 let lastChunkEndOffset = 0;
621 for (const {offset, keyLocation} of chunkEnds) {
Ben Rohlfs32b83822020-08-14 22:08:37 +0200622 result.push({
623 lines: lines.slice(lastChunkEndOffset, offset),
624 keyLocation,
625 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100626 lastChunkEndOffset = offset;
627 }
628 return result;
629 }
630
631 /**
632 * Converts `IntralineInfo`s return by the API to `GrLineHighlights` used
633 * for rendering.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100634 */
Ben Rohlfs32b83822020-08-14 22:08:37 +0200635 _convertIntralineInfos(
636 rows: string[],
637 intralineInfos: number[][]
638 ): Highlights[] {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100639 let rowIndex = 0;
640 let idx = 0;
641 const normalized = [];
642 for (const [skipLength, markLength] of intralineInfos) {
643 let line = rows[rowIndex] + '\n';
644 let j = 0;
645 while (j < skipLength) {
646 if (idx === line.length) {
647 idx = 0;
648 line = rows[++rowIndex] + '\n';
649 continue;
650 }
651 idx++;
652 j++;
653 }
Ben Rohlfs32b83822020-08-14 22:08:37 +0200654 let lineHighlight: Highlights = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100655 contentIndex: rowIndex,
656 startIndex: idx,
657 };
658
659 j = 0;
660 while (line && j < markLength) {
661 if (idx === line.length) {
662 idx = 0;
663 line = rows[++rowIndex] + '\n';
664 normalized.push(lineHighlight);
665 lineHighlight = {
666 contentIndex: rowIndex,
667 startIndex: idx,
668 };
669 continue;
670 }
671 idx++;
672 j++;
673 }
674 lineHighlight.endIndex = idx;
675 normalized.push(lineHighlight);
676 }
677 return normalized;
678 }
679
680 /**
681 * If a group is an addition or a removal, break it down into smaller groups
682 * of that type using the MAX_GROUP_SIZE. If the group is a shared chunk
683 * or a delta it is returned as the single element of the result array.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100684 */
Ben Rohlfs32b83822020-08-14 22:08:37 +0200685 _breakdownChunk(chunk: DiffContent): DiffContent[] {
686 let key: 'a' | 'b' | 'ab' | null = null;
Renan Oliveira176d3a32020-12-17 17:49:09 +0100687 const {a, b, ab, move_details} = chunk;
688 if (a?.length && !b?.length) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100689 key = 'a';
Renan Oliveira176d3a32020-12-17 17:49:09 +0100690 } else if (b?.length && !a?.length) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100691 key = 'b';
Renan Oliveira176d3a32020-12-17 17:49:09 +0100692 } else if (ab?.length) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100693 key = 'ab';
694 }
695
Renan Oliveira176d3a32020-12-17 17:49:09 +0100696 // Move chunks should not be divided because of move label
697 // positioned in the top of the chunk
698 if (!key || move_details) {
Ben Rohlfs32b83822020-08-14 22:08:37 +0200699 return [chunk];
700 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100701
Ben Rohlfs32b83822020-08-14 22:08:37 +0200702 return this._breakdown(chunk[key]!, MAX_GROUP_SIZE).map(subChunkLines => {
703 const subChunk: DiffContent = {};
704 subChunk[key!] = subChunkLines;
705 if (chunk.due_to_rebase) {
706 subChunk.due_to_rebase = true;
707 }
Renan Oliveiraf411dd02020-11-18 18:46:12 +0100708 if (chunk.move_details) {
709 subChunk.move_details = chunk.move_details;
710 }
Ben Rohlfs32b83822020-08-14 22:08:37 +0200711 return subChunk;
712 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100713 }
714
715 /**
716 * Given an array and a size, return an array of arrays where no inner array
717 * is larger than that size, preserving the original order.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100718 */
Ben Rohlfs32b83822020-08-14 22:08:37 +0200719 _breakdown<T>(array: T[], size: number): T[][] {
720 if (!array.length) {
721 return [];
722 }
723 if (array.length < size) {
724 return [array];
725 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100726
727 const head = array.slice(0, array.length - size);
728 const tail = array.slice(array.length - size);
729
730 return this._breakdown(head, size).concat([tail]);
731 }
732}
733
Ben Rohlfs32b83822020-08-14 22:08:37 +0200734declare global {
735 interface HTMLElementTagNameMap {
736 'gr-diff-processor': GrDiffProcessor;
737 }
738}