Dmitrii Filippov | 9fcdcb7 | 2020-04-07 13:26:52 +0200 | [diff] [blame] | 1 | /** |
| 2 | * @license |
Ben Rohlfs | 94fcbbc | 2022-05-27 10:45:03 +0200 | [diff] [blame] | 3 | * Copyright 2020 Google LLC |
| 4 | * SPDX-License-Identifier: Apache-2.0 |
Dmitrii Filippov | 9fcdcb7 | 2020-04-07 13:26:52 +0200 | [diff] [blame] | 5 | */ |
Ben Rohlfs | 96ef2c7 | 2023-10-30 18:02:35 +0100 | [diff] [blame] | 6 | import {BlameInfo, CommentRange} from '../../../types/common'; |
Ben Rohlfs | 5303ec9 | 2023-05-19 15:47:53 +0200 | [diff] [blame] | 7 | import {Side, SpecialFilePath} from '../../../constants/constants'; |
Ben Rohlfs | d49003e | 2022-02-09 14:31:07 +0100 | [diff] [blame] | 8 | import { |
Ben Rohlfs | 5303ec9 | 2023-05-19 15:47:53 +0200 | [diff] [blame] | 9 | DiffContextExpandedExternalDetail, |
Ben Rohlfs | d49003e | 2022-02-09 14:31:07 +0100 | [diff] [blame] | 10 | DiffPreferencesInfo, |
| 11 | DiffResponsiveMode, |
Ben Rohlfs | cc32c89 | 2023-05-17 15:27:54 +0200 | [diff] [blame] | 12 | DisplayLine, |
Ben Rohlfs | b995610 | 2023-05-12 17:07:06 +0200 | [diff] [blame] | 13 | FILE, |
| 14 | LOST, |
| 15 | LineNumber, |
Ben Rohlfs | d49003e | 2022-02-09 14:31:07 +0100 | [diff] [blame] | 16 | RenderPreferences, |
| 17 | } from '../../../api/diff'; |
Ben Rohlfs | 5303ec9 | 2023-05-19 15:47:53 +0200 | [diff] [blame] | 18 | import {GrDiffGroup} from './gr-diff-group'; |
Ben Rohlfs | d49003e | 2022-02-09 14:31:07 +0100 | [diff] [blame] | 19 | |
| 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 Rohlfs | 2f05cdd | 2022-10-31 08:00:50 +0000 | [diff] [blame] | 40 | export const REGEX_TAB_OR_SURROGATE_PAIR = /\t|[\uD800-\uDBFF][\uDC00-\uDFFF]/; |
Dhruv Srivastava | 668d552 | 2021-05-19 08:32:53 +0200 | [diff] [blame] | 41 | |
Ben Rohlfs | d49003e | 2022-02-09 14:31:07 +0100 | [diff] [blame] | 42 | export function getResponsiveMode( |
Ben Rohlfs | e15af73 | 2023-01-11 09:39:24 +0100 | [diff] [blame] | 43 | prefs?: DiffPreferencesInfo, |
Ben Rohlfs | d49003e | 2022-02-09 14:31:07 +0100 | [diff] [blame] | 44 | renderPrefs?: RenderPreferences |
| 45 | ): DiffResponsiveMode { |
| 46 | if (renderPrefs?.responsive_mode) { |
| 47 | return renderPrefs.responsive_mode; |
| 48 | } |
| 49 | // Backwards compatibility to the line_wrapping param. |
Ben Rohlfs | e15af73 | 2023-01-11 09:39:24 +0100 | [diff] [blame] | 50 | if (prefs?.line_wrapping) { |
Ben Rohlfs | d49003e | 2022-02-09 14:31:07 +0100 | [diff] [blame] | 51 | return 'FULL_RESPONSIVE'; |
| 52 | } |
| 53 | return 'NONE'; |
| 54 | } |
| 55 | |
Ben Rohlfs | 2f05cdd | 2022-10-31 08:00:50 +0000 | [diff] [blame] | 56 | export function isResponsive(responsiveMode?: DiffResponsiveMode) { |
Ben Rohlfs | d49003e | 2022-02-09 14:31:07 +0100 | [diff] [blame] | 57 | return ( |
| 58 | responsiveMode === 'FULL_RESPONSIVE' || responsiveMode === 'SHRINK_ONLY' |
| 59 | ); |
| 60 | } |
| 61 | |
Dmitrii Filippov | 9fcdcb7 | 2020-04-07 13:26:52 +0200 | [diff] [blame] | 62 | /** |
| 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 Filippov | 9fcdcb7 | 2020-04-07 13:26:52 +0200 | [diff] [blame] | 66 | */ |
Ben Rohlfs | 1d48706 | 2020-09-26 11:26:03 +0200 | [diff] [blame] | 67 | export function rangesEqual(a?: CommentRange, b?: CommentRange): boolean { |
Ben Rohlfs | 5e2d1e7 | 2020-08-03 19:33:31 +0200 | [diff] [blame] | 68 | 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 Filippov | 9fcdcb7 | 2020-04-07 13:26:52 +0200 | [diff] [blame] | 80 | } |
Ben Rohlfs | 4fa7c53 | 2020-08-24 18:16:13 +0200 | [diff] [blame] | 81 | |
Frank Borden | 23f91e1 | 2020-12-15 17:56:35 -0800 | [diff] [blame] | 82 | export function isLongCommentRange(range: CommentRange): boolean { |
Ben Rohlfs | ddabbf0 | 2021-02-23 10:39:36 +0100 | [diff] [blame] | 83 | return range.end_line - range.start_line > 10; |
Frank Borden | 23f91e1 | 2020-12-15 17:56:35 -0800 | [diff] [blame] | 84 | } |
| 85 | |
Ben Rohlfs | 1fdd959 | 2021-04-16 12:36:04 +0000 | [diff] [blame] | 86 | export function getLineNumberByChild(node?: Node) { |
| 87 | return getLineNumber(getLineElByChild(node)); |
| 88 | } |
| 89 | |
| 90 | export function lineNumberToNumber(lineNumber?: LineNumber | null): number { |
Ben Rohlfs | b995610 | 2023-05-12 17:07:06 +0200 | [diff] [blame] | 91 | if (typeof lineNumber !== 'number') return 0; |
Ben Rohlfs | 1fdd959 | 2021-04-16 12:36:04 +0000 | [diff] [blame] | 92 | return lineNumber; |
| 93 | } |
| 94 | |
| 95 | export 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 Rohlfs | 2f05cdd | 2022-10-31 08:00:50 +0000 | [diff] [blame] | 105 | node = |
| 106 | (node as Element).assignedSlot ?? |
| 107 | (node as ShadowRoot).host ?? |
| 108 | node.previousSibling ?? |
| 109 | node.parentNode ?? |
| 110 | undefined; |
Ben Rohlfs | 1fdd959 | 2021-04-16 12:36:04 +0000 | [diff] [blame] | 111 | } |
| 112 | return null; |
| 113 | } |
| 114 | |
| 115 | export function getSideByLineEl(lineEl: Element) { |
| 116 | return lineEl.classList.contains(Side.RIGHT) ? Side.RIGHT : Side.LEFT; |
| 117 | } |
| 118 | |
Ben Rohlfs | 4fa7c53 | 2020-08-24 18:16:13 +0200 | [diff] [blame] | 119 | export 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 Rohlfs | b995610 | 2023-05-12 17:07:06 +0200 | [diff] [blame] | 124 | if (lineNumberStr === LOST) return LOST; |
Ben Rohlfs | 4fa7c53 | 2020-08-24 18:16:13 +0200 | [diff] [blame] | 125 | const lineNumber = Number(lineNumberStr); |
| 126 | return Number.isInteger(lineNumber) ? lineNumber : null; |
| 127 | } |
Ben Rohlfs | f296211 | 2020-11-30 14:38:52 +0100 | [diff] [blame] | 128 | |
| 129 | export function getLine(threadEl: HTMLElement): LineNumber { |
| 130 | const lineAtt = threadEl.getAttribute('line-num'); |
Ben Rohlfs | b995610 | 2023-05-12 17:07:06 +0200 | [diff] [blame] | 131 | if (lineAtt === LOST) return lineAtt; |
| 132 | if (!lineAtt || lineAtt === FILE) return FILE; |
Ben Rohlfs | f296211 | 2020-11-30 14:38:52 +0100 | [diff] [blame] | 133 | 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 | |
| 139 | export function getSide(threadEl: HTMLElement): Side | undefined { |
Ben Rohlfs | 8016604 | 2022-02-15 18:20:17 +0100 | [diff] [blame] | 140 | const sideAtt = threadEl.getAttribute('diff-side'); |
Ben Rohlfs | f296211 | 2020-11-30 14:38:52 +0100 | [diff] [blame] | 141 | 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 | |
| 150 | export 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 Kristofic | 1466625 | 2023-05-09 11:12:34 +0200 | [diff] [blame] | 154 | if (!range.start_line) return undefined; |
Ben Rohlfs | f296211 | 2020-11-30 14:38:52 +0100 | [diff] [blame] | 155 | return range; |
| 156 | } |
| 157 | |
Ben Rohlfs | cc32c89 | 2023-05-17 15:27:54 +0200 | [diff] [blame] | 158 | /** |
Ben Rohlfs | 60425a1 | 2023-05-10 13:42:38 +0200 | [diff] [blame] | 159 | * 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 Rohlfs | cc32c89 | 2023-05-17 15:27:54 +0200 | [diff] [blame] | 162 | */ |
| 163 | export interface GrDiffCommentThread { |
| 164 | side: Side; |
| 165 | line: LineNumber; |
| 166 | range?: CommentRange; |
| 167 | rootId?: string; |
| 168 | } |
| 169 | |
Ben Rohlfs | 60425a1 | 2023-05-10 13:42:38 +0200 | [diff] [blame] | 170 | /** |
| 171 | * Retrieves all the data from a comment thread element that the gr-diff API |
| 172 | * contract defines for such elements. |
| 173 | */ |
| 174 | export function getDataFromCommentThreadEl( |
| 175 | threadEl?: EventTarget | null |
Ben Rohlfs | cc32c89 | 2023-05-17 15:27:54 +0200 | [diff] [blame] | 176 | ): 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 | |
| 186 | export interface KeyLocations { |
| 187 | left: {[key: string]: boolean}; |
| 188 | right: {[key: string]: boolean}; |
| 189 | } |
| 190 | |
Ben Rohlfs | 78e185e | 2023-05-19 11:55:38 +0200 | [diff] [blame] | 191 | /** |
| 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 | */ |
| 199 | export const FULL_CONTEXT = -1; |
| 200 | |
| 201 | export 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 | |
| 213 | export 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 Kristofic | 103b319 | 2023-08-17 20:00:09 +0200 | [diff] [blame] | 222 | prefsContext !== undefined && |
Ben Rohlfs | 78e185e | 2023-05-19 11:55:38 +0200 | [diff] [blame] | 223 | !(showFullContext === FullContext.NO && prefsContext === FULL_CONTEXT) |
| 224 | ) { |
| 225 | return prefsContext; |
| 226 | } |
| 227 | return defaultContext; |
| 228 | } |
| 229 | |
Ben Rohlfs | 5303ec9 | 2023-05-19 15:47:53 +0200 | [diff] [blame] | 230 | export 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 Rohlfs | cc32c89 | 2023-05-17 15:27:54 +0200 | [diff] [blame] | 244 | export 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 | |
| 264 | export 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 Rohlfs | f296211 | 2020-11-30 14:38:52 +0100 | [diff] [blame] | 297 | // 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 Ostrovsky | f91f966 | 2021-02-22 19:34:37 +0100 | [diff] [blame] | 300 | // TODO: Also document the required HTML attributes that thread elements must |
Ben Rohlfs | 60425a1 | 2023-05-10 13:42:38 +0200 | [diff] [blame] | 301 | // 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 Rohlfs | f296211 | 2020-11-30 14:38:52 +0100 | [diff] [blame] | 304 | export interface GrDiffThreadElement extends HTMLElement { |
| 305 | rootId: string; |
| 306 | } |
| 307 | |
Ben Rohlfs | 60425a1 | 2023-05-10 13:42:38 +0200 | [diff] [blame] | 308 | export function isThreadEl( |
| 309 | node?: Node | EventTarget | null |
| 310 | ): node is GrDiffThreadElement { |
Ben Rohlfs | f296211 | 2020-11-30 14:38:52 +0100 | [diff] [blame] | 311 | return ( |
Ben Rohlfs | 60425a1 | 2023-05-10 13:42:38 +0200 | [diff] [blame] | 312 | !!node && |
| 313 | (node as Node).nodeType === Node.ELEMENT_NODE && |
Ben Rohlfs | f296211 | 2020-11-30 14:38:52 +0100 | [diff] [blame] | 314 | (node as Element).classList.contains('comment-thread') |
| 315 | ); |
| 316 | } |
Dhruv Srivastava | 668d552 | 2021-05-19 08:32:53 +0200 | [diff] [blame] | 317 | |
Ben Rohlfs | 5303ec9 | 2023-05-19 15:47:53 +0200 | [diff] [blame] | 318 | export 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 Rohlfs | 96ef2c7 | 2023-10-30 18:02:35 +0100 | [diff] [blame] | 325 | |
| 326 | export 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 | } |