blob: c4aaa2d399aac2895933b8e96f5ccca6040ce991 [file] [log] [blame]
Dmitrii Filippov9fcdcb72020-04-07 13:26:52 +02001/**
2 * @license
Ben Rohlfs94fcbbc2022-05-27 10:45:03 +02003 * Copyright 2020 Google LLC
4 * SPDX-License-Identifier: Apache-2.0
Dmitrii Filippov9fcdcb72020-04-07 13:26:52 +02005 */
Ben Rohlfs96ef2c72023-10-30 18:02:35 +01006import {BlameInfo, CommentRange} from '../../../types/common';
Ben Rohlfs5303ec92023-05-19 15:47:53 +02007import {Side, SpecialFilePath} from '../../../constants/constants';
Ben Rohlfsd49003e2022-02-09 14:31:07 +01008import {
Ben Rohlfs5303ec92023-05-19 15:47:53 +02009 DiffContextExpandedExternalDetail,
Ben Rohlfsd49003e2022-02-09 14:31:07 +010010 DiffPreferencesInfo,
11 DiffResponsiveMode,
Ben Rohlfscc32c892023-05-17 15:27:54 +020012 DisplayLine,
Ben Rohlfsb9956102023-05-12 17:07:06 +020013 FILE,
14 LOST,
15 LineNumber,
Ben Rohlfsd49003e2022-02-09 14:31:07 +010016 RenderPreferences,
17} from '../../../api/diff';
Ben Rohlfs5303ec92023-05-19 15:47:53 +020018import {GrDiffGroup} from './gr-diff-group';
Ben Rohlfsd49003e2022-02-09 14:31:07 +010019
20/**
21 * In JS, unicode code points above 0xFFFF occupy two elements of a string.
22 * For example '𐀏'.length is 2. An occurrence of such a code point is called a
23 * surrogate pair.
24 *
25 * This regex segments a string along tabs ('\t') and surrogate pairs, since
26 * these are two cases where '1 char' does not automatically imply '1 column'.
27 *
28 * TODO: For human languages whose orthographies use combining marks, this
29 * approach won't correctly identify the grapheme boundaries. In those cases,
30 * a grapheme consists of multiple code points that should count as only one
31 * character against the column limit. Getting that correct (if it's desired)
32 * is probably beyond the limits of a regex, but there are nonstandard APIs to
33 * do this, and proposed (but, as of Nov 2017, unimplemented) standard APIs.
34 *
35 * Further reading:
36 * On Unicode in JS: https://mathiasbynens.be/notes/javascript-unicode
37 * Graphemes: http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
38 * A proposed JS API: https://github.com/tc39/proposal-intl-segmenter
39 */
Ben Rohlfs2f05cdd2022-10-31 08:00:50 +000040export const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/;
Dhruv Srivastava668d5522021-05-19 08:32:53 +020041
Ben Rohlfsd49003e2022-02-09 14:31:07 +010042export function getResponsiveMode(
Ben Rohlfse15af732023-01-11 09:39:24 +010043 prefs?: DiffPreferencesInfo,
Ben Rohlfsd49003e2022-02-09 14:31:07 +010044 renderPrefs?: RenderPreferences
45): DiffResponsiveMode {
46 if (renderPrefs?.responsive_mode) {
47 return renderPrefs.responsive_mode;
48 }
49 // Backwards compatibility to the line_wrapping param.
Ben Rohlfse15af732023-01-11 09:39:24 +010050 if (prefs?.line_wrapping) {
Ben Rohlfsd49003e2022-02-09 14:31:07 +010051 return 'FULL_RESPONSIVE';
52 }
53 return 'NONE';
54}
55
Ben Rohlfs2f05cdd2022-10-31 08:00:50 +000056export function isResponsive(responsiveMode?: DiffResponsiveMode) {
Ben Rohlfsd49003e2022-02-09 14:31:07 +010057 return (
58 responsiveMode === 'FULL_RESPONSIVE' || responsiveMode === 'SHRINK_ONLY'
59 );
60}
61
Dmitrii Filippov9fcdcb72020-04-07 13:26:52 +020062/**
63 * Compare two ranges. Either argument may be falsy, but will only return
64 * true if both are falsy or if neither are falsy and have the same position
65 * values.
Dmitrii Filippov9fcdcb72020-04-07 13:26:52 +020066 */
Ben Rohlfs1d487062020-09-26 11:26:03 +020067export function rangesEqual(a?: CommentRange, b?: CommentRange): boolean {
Ben Rohlfs5e2d1e72020-08-03 19:33:31 +020068 if (!a && !b) {
69 return true;
70 }
71 if (!a || !b) {
72 return false;
73 }
74 return (
75 a.start_line === b.start_line &&
76 a.start_character === b.start_character &&
77 a.end_line === b.end_line &&
78 a.end_character === b.end_character
79 );
Dmitrii Filippov9fcdcb72020-04-07 13:26:52 +020080}
Ben Rohlfs4fa7c532020-08-24 18:16:13 +020081
Frank Borden23f91e12020-12-15 17:56:35 -080082export function isLongCommentRange(range: CommentRange): boolean {
Ben Rohlfsddabbf02021-02-23 10:39:36 +010083 return range.end_line - range.start_line > 10;
Frank Borden23f91e12020-12-15 17:56:35 -080084}
85
Ben Rohlfs1fdd9592021-04-16 12:36:04 +000086export function getLineNumberByChild(node?: Node) {
87 return getLineNumber(getLineElByChild(node));
88}
89
90export function lineNumberToNumber(lineNumber?: LineNumber | null): number {
Ben Rohlfsb9956102023-05-12 17:07:06 +020091 if (typeof lineNumber !== 'number') return 0;
Ben Rohlfs1fdd9592021-04-16 12:36:04 +000092 return lineNumber;
93}
94
95export function getLineElByChild(node?: Node): HTMLElement | null {
96 while (node) {
97 if (node instanceof Element) {
98 if (node.classList.contains('lineNum')) {
99 return node as HTMLElement;
100 }
101 if (node.classList.contains('section')) {
102 return null;
103 }
104 }
Ben Rohlfs2f05cdd2022-10-31 08:00:50 +0000105 node =
106 (node as Element).assignedSlot ??
107 (node as ShadowRoot).host ??
108 node.previousSibling ??
109 node.parentNode ??
110 undefined;
Ben Rohlfs1fdd9592021-04-16 12:36:04 +0000111 }
112 return null;
113}
114
115export function getSideByLineEl(lineEl: Element) {
116 return lineEl.classList.contains(Side.RIGHT) ? Side.RIGHT : Side.LEFT;
117}
118
Ben Rohlfs4fa7c532020-08-24 18:16:13 +0200119export function getLineNumber(lineEl?: Element | null): LineNumber | null {
120 if (!lineEl) return null;
121 const lineNumberStr = lineEl.getAttribute('data-value');
122 if (!lineNumberStr) return null;
123 if (lineNumberStr === FILE) return FILE;
Ben Rohlfsb9956102023-05-12 17:07:06 +0200124 if (lineNumberStr === LOST) return LOST;
Ben Rohlfs4fa7c532020-08-24 18:16:13 +0200125 const lineNumber = Number(lineNumberStr);
126 return Number.isInteger(lineNumber) ? lineNumber : null;
127}
Ben Rohlfsf2962112020-11-30 14:38:52 +0100128
129export function getLine(threadEl: HTMLElement): LineNumber {
130 const lineAtt = threadEl.getAttribute('line-num');
Ben Rohlfsb9956102023-05-12 17:07:06 +0200131 if (lineAtt === LOST) return lineAtt;
132 if (!lineAtt || lineAtt === FILE) return FILE;
Ben Rohlfsf2962112020-11-30 14:38:52 +0100133 const line = Number(lineAtt);
134 if (isNaN(line)) throw new Error(`cannot parse line number: ${lineAtt}`);
135 if (line < 1) throw new Error(`line number smaller than 1: ${line}`);
136 return line;
137}
138
139export function getSide(threadEl: HTMLElement): Side | undefined {
Ben Rohlfs80166042022-02-15 18:20:17 +0100140 const sideAtt = threadEl.getAttribute('diff-side');
Ben Rohlfsf2962112020-11-30 14:38:52 +0100141 if (!sideAtt) {
142 console.warn('comment thread without side');
143 return undefined;
144 }
145 if (sideAtt !== Side.LEFT && sideAtt !== Side.RIGHT)
146 throw Error(`unexpected value for side: ${sideAtt}`);
147 return sideAtt as Side;
148}
149
150export function getRange(threadEl: HTMLElement): CommentRange | undefined {
151 const rangeAtt = threadEl.getAttribute('range');
152 if (!rangeAtt) return undefined;
153 const range = JSON.parse(rangeAtt) as CommentRange;
Milutin Kristofic14666252023-05-09 11:12:34 +0200154 if (!range.start_line) return undefined;
Ben Rohlfsf2962112020-11-30 14:38:52 +0100155 return range;
156}
157
Ben Rohlfscc32c892023-05-17 15:27:54 +0200158/**
Ben Rohlfs60425a12023-05-10 13:42:38 +0200159 * This is all the data that gr-diff extracts from comment thread elements,
160 * see `GrDiffThreadElement`. Otherwise gr-diff treats such elements as a black
161 * box.
Ben Rohlfscc32c892023-05-17 15:27:54 +0200162 */
163export interface GrDiffCommentThread {
164 side: Side;
165 line: LineNumber;
166 range?: CommentRange;
167 rootId?: string;
168}
169
Ben Rohlfs60425a12023-05-10 13:42:38 +0200170/**
171 * Retrieves all the data from a comment thread element that the gr-diff API
172 * contract defines for such elements.
173 */
174export function getDataFromCommentThreadEl(
175 threadEl?: EventTarget | null
Ben Rohlfscc32c892023-05-17 15:27:54 +0200176): GrDiffCommentThread | undefined {
177 if (!isThreadEl(threadEl)) return undefined;
178 const side = getSide(threadEl);
179 const line = getLine(threadEl);
180 const range = getRange(threadEl);
181 if (!side) return undefined;
182 if (!line) return undefined;
183 return {side, line, range, rootId: threadEl.rootId};
184}
185
186export interface KeyLocations {
187 left: {[key: string]: boolean};
188 right: {[key: string]: boolean};
189}
190
Ben Rohlfs78e185e2023-05-19 11:55:38 +0200191/**
192 * "Context" is the number of lines that we are showing around diff chunks and
193 * commented lines. This typically comes from a user preference and is set to
194 * something like 3 or 10.
195 *
196 * `FULL_CONTEXT` means that the user wants to see the entire file. We could
197 * also call this "infinite context".
198 */
199export const FULL_CONTEXT = -1;
200
201export enum FullContext {
202 /** User has opted into showing the full context. */
203 YES = 'YES',
204 /** User has opted into showing only limited context. */
205 NO = 'NO',
206 /**
207 * User has not decided yet. Will see a warning message with two options then,
208 * if the file is too large.
209 */
210 UNDECIDED = 'UNDECIDED',
211}
212
213export function computeContext(
214 prefsContext: number | undefined,
215 showFullContext: FullContext,
216 defaultContext: number
217) {
218 if (showFullContext === FullContext.YES) {
219 return FULL_CONTEXT;
220 }
221 if (
Milutin Kristofic103b3192023-08-17 20:00:09 +0200222 prefsContext !== undefined &&
Ben Rohlfs78e185e2023-05-19 11:55:38 +0200223 !(showFullContext === FullContext.NO && prefsContext === FULL_CONTEXT)
224 ) {
225 return prefsContext;
226 }
227 return defaultContext;
228}
229
Ben Rohlfs5303ec92023-05-19 15:47:53 +0200230export function computeLineLength(
231 prefs: DiffPreferencesInfo,
232 path: string | undefined
233): number {
234 if (path === SpecialFilePath.COMMIT_MESSAGE) {
235 return 72;
236 }
237 const lineLength = prefs.line_length;
238 if (Number.isInteger(lineLength) && lineLength > 0) {
239 return lineLength;
240 }
241 return 100;
242}
243
Ben Rohlfscc32c892023-05-17 15:27:54 +0200244export function computeKeyLocations(
245 lineOfInterest: DisplayLine | undefined,
246 comments: GrDiffCommentThread[]
247) {
248 const keyLocations: KeyLocations = {left: {}, right: {}};
249
250 if (lineOfInterest) {
251 keyLocations[lineOfInterest.side][lineOfInterest.lineNum] = true;
252 }
253
254 for (const comment of comments) {
255 keyLocations[comment.side][comment.line] = true;
256 if (comment.range?.start_line) {
257 keyLocations[comment.side][comment.range.start_line] = true;
258 }
259 }
260
261 return keyLocations;
262}
263
264export function compareComments(
265 c1: GrDiffCommentThread,
266 c2: GrDiffCommentThread
267): number {
268 if (c1.side !== c2.side) {
269 return c1.side === Side.RIGHT ? 1 : -1;
270 }
271
272 if (c1.line !== c2.line) {
273 if (c1.line === FILE && c2.line !== FILE) return -1;
274 if (c1.line !== FILE && c2.line === FILE) return 1;
275 if (c1.line === LOST && c2.line !== LOST) return -1;
276 if (c1.line !== LOST && c2.line === LOST) return 1;
277 return (c1.line as number) - (c2.line as number);
278 }
279
280 if (c1.rootId !== c2.rootId) {
281 if (!c1.rootId) return -1;
282 if (!c2.rootId) return 1;
283 return c1.rootId > c2.rootId ? 1 : -1;
284 }
285
286 if (c1.range && c2.range) {
287 const r1 = JSON.stringify(c1.range);
288 const r2 = JSON.stringify(c2.range);
289 return r1 > r2 ? 1 : -1;
290 }
291 if (c1.range) return 1;
292 if (c2.range) return -1;
293
294 return 0;
295}
296
Ben Rohlfsf2962112020-11-30 14:38:52 +0100297// TODO: This type should be exposed to gr-diff clients in a separate type file.
298// For Gerrit these are instances of GrCommentThread, but other gr-diff users
299// have different HTML elements in use for comment threads.
David Ostrovskyf91f9662021-02-22 19:34:37 +0100300// TODO: Also document the required HTML attributes that thread elements must
Ben Rohlfs60425a12023-05-10 13:42:38 +0200301// have, e.g. 'diff-side', 'range' (optional), 'line-num'.
302// Comment widgets are also required to have `comment-thread` in their css
303// class list.
Ben Rohlfsf2962112020-11-30 14:38:52 +0100304export interface GrDiffThreadElement extends HTMLElement {
305 rootId: string;
306}
307
Ben Rohlfs60425a12023-05-10 13:42:38 +0200308export function isThreadEl(
309 node?: Node | EventTarget | null
310): node is GrDiffThreadElement {
Ben Rohlfsf2962112020-11-30 14:38:52 +0100311 return (
Ben Rohlfs60425a12023-05-10 13:42:38 +0200312 !!node &&
313 (node as Node).nodeType === Node.ELEMENT_NODE &&
Ben Rohlfsf2962112020-11-30 14:38:52 +0100314 (node as Element).classList.contains('comment-thread')
315 );
316}
Dhruv Srivastava668d5522021-05-19 08:32:53 +0200317
Ben Rohlfs5303ec92023-05-19 15:47:53 +0200318export interface DiffContextExpandedEventDetail
319 extends DiffContextExpandedExternalDetail {
320 /** The context control group that should be replaced by `groups`. */
321 contextGroup: GrDiffGroup;
322 groups: GrDiffGroup[];
323 numLines: number;
324}
Ben Rohlfs96ef2c72023-10-30 18:02:35 +0100325
326export function findBlame(blameInfos: BlameInfo[], line?: LineNumber) {
327 if (typeof line !== 'number') return undefined;
328 return blameInfos.find(info =>
329 info.ranges.find(range => range.start <= line && line <= range.end)
330 );
331}