addons/isl/src/ComparisonView/SplitDiffView/SplitDiffFileHeader.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {EnsureAssignedTogether} from 'shared/EnsureAssignedTogether';
b69ab319import type {DiffType} from 'shared/patch/types';
b69ab3110import type {RepoPath} from 'shared/types/common';
b69ab3111
b69ab3112import {Button} from 'isl-components/Button';
b69ab3113import {Icon} from 'isl-components/Icon';
b69ab3114import {Tooltip} from 'isl-components/Tooltip';
b69ab3115import React from 'react';
b69ab3116import {t} from '../../i18n';
b69ab3117import platform from '../../platform';
b69ab3118
b69ab3119import './SplitDiffHunk.css';
b69ab3120
b69ab3121/**
b69ab3122 * Decides the icon of the file header.
b69ab3123 * Subset of `DiffType` - no Copied for Removed.
b69ab3124 */
b69ab3125export enum IconType {
b69ab3126 Modified = 'Modified',
b69ab3127 Added = 'Added',
b69ab3128 Removed = 'Removed',
b69ab3129}
b69ab3130
b69ab3131export function diffTypeToIconType(diffType?: DiffType): IconType {
b69ab3132 if (diffType != null) {
b69ab3133 const diffTypeStr = diffType as string;
b69ab3134 if (diffTypeStr === 'Added' || diffTypeStr === 'Removed' || diffTypeStr === 'Modified') {
b69ab3135 return diffTypeStr as IconType;
b69ab3136 }
b69ab3137 }
b69ab3138 // "Copied" and "Renamed" should only apply to new files.
b69ab3139 return IconType.Added;
b69ab3140}
b69ab3141
b69ab3142export function FileHeader({
b69ab3143 path,
b69ab3144 copyFrom,
b69ab3145 iconType,
b69ab3146 open,
b69ab3147 onChangeOpen,
b69ab3148 fileActions,
b69ab3149}: {
b69ab3150 path: RepoPath;
b69ab3151 copyFrom?: RepoPath;
b69ab3152 iconType?: IconType;
b69ab3153 fileActions?: JSX.Element;
b69ab3154} & EnsureAssignedTogether<{
b69ab3155 open: boolean;
b69ab3156 onChangeOpen: (open: boolean) => void;
b69ab3157}>) {
b69ab3158 // Even though the enclosing <SplitDiffView> will have border-radius set, we
b69ab3159 // have to define it again here or things don't look right.
b69ab3160 const color =
b69ab3161 iconType === undefined ? 'var(--scm-modified-foreground)' : iconTypeToColor[iconType];
b69ab3162
b69ab3163 const pathSeparator = '/';
b69ab3164 const pathParts = path.split(pathSeparator);
b69ab3165
b69ab3166 // show: dir1 / dir2 / ... / dir9 / file
b69ab3167 // ^^^^ ^^^^ ^^^^
b69ab3168 // copy: full path "dir9/file" "file"
b69ab3169
b69ab3170 // with copyFrom: "dir1/dir2/dir3/foo" renamed to "dir1/dir2/dir4/bar"
b69ab3171 // show: dir1 / dir2 / { dir3 / foo -> dir4 / bar }
b69ab3172 // commonPrefixLen = 2 # (dir1 / dir2)
b69ab3173 // copyFromRest = "dir3/foo"
b69ab3174 let commonPrefixLen = -1;
b69ab3175 let copyFromRest = '';
b69ab3176 if (copyFrom != null && copyFrom !== path) {
b69ab3177 const copyFromParts = copyFrom.split(pathSeparator);
b69ab3178 commonPrefixLen = commonPrefixLength(pathParts, copyFromParts);
b69ab3179 copyFromRest = copyFromParts.slice(commonPrefixLen).join(pathSeparator);
b69ab3180 }
b69ab3181
b69ab3182 const copySpan = (s: string) => <span className="file-header-copyfrom-path">{s}</span>;
b69ab3183 const filePathParts = pathParts.map((part, idx) => {
b69ab3184 const pathSoFar = pathParts.slice(idx).join(pathSeparator);
b69ab3185 let copyFromLeft = null;
b69ab3186 let copyFromRight = null;
b69ab3187 if (idx === commonPrefixLen && copyFromRest.length > 0) {
b69ab3188 // Insert "{" (when commonPrefix is not empty), " copyFromRest ->".
b69ab3189 const prefix = commonPrefixLen > 0 ? '{ ' : '';
b69ab3190 copyFromLeft = (
b69ab3191 <Tooltip
b69ab3192 title={t('Renamed or copied from $path', {replace: {$path: copyFrom ?? ''}})}
b69ab3193 delayMs={100}
b69ab3194 placement="bottom">
b69ab3195 {copySpan(`${prefix}${copyFromRest} →`)}
b69ab3196 </Tooltip>
b69ab3197 );
b69ab3198 }
b69ab3199 if (idx + 1 === pathParts.length && commonPrefixLen > 0 && copyFromRest.length > 0) {
b69ab31100 // Append "}" (when commonPrefix is not empty)
b69ab31101 copyFromRight = copySpan('}');
b69ab31102 }
b69ab31103 return (
b69ab31104 <React.Fragment key={idx}>
b69ab31105 {copyFromLeft}
b69ab31106 <span className={'file-header-copyable-path'}>
b69ab31107 <Tooltip
b69ab31108 component={() => (
b69ab31109 <span className="file-header-copyable-path-hover">
b69ab31110 {t('Copy $path', {replace: {$path: pathSoFar}})}
b69ab31111 </span>
b69ab31112 )}
b69ab31113 delayMs={100}
b69ab31114 placement="bottom">
b69ab31115 <span onClick={() => platform.clipboardCopy(pathSoFar)}>
b69ab31116 {part}
b69ab31117 {idx < pathParts.length - 1 ? pathSeparator : ''}
b69ab31118 </span>
b69ab31119 </Tooltip>
b69ab31120 </span>
b69ab31121 {copyFromRight}
b69ab31122 </React.Fragment>
b69ab31123 );
b69ab31124 });
b69ab31125
b69ab31126 return (
b69ab31127 <div
b69ab31128 className={`split-diff-view-file-header file-header-${open ? 'open' : 'collapsed'}`}
b69ab31129 style={{color}}>
b69ab31130 {onChangeOpen && (
b69ab31131 <Button
b69ab31132 icon
b69ab31133 className="split-diff-view-file-header-open-button"
b69ab31134 data-testid={`split-diff-view-file-header-${open ? 'collapse' : 'expand'}-button`}
b69ab31135 onClick={() => onChangeOpen(!open)}>
b69ab31136 <Icon icon={open ? 'chevron-down' : 'chevron-right'} />
b69ab31137 </Button>
b69ab31138 )}
b69ab31139 {iconType !== undefined && (
b69ab31140 <Tooltip title={iconTypeToTooltip[iconType]}>
b69ab31141 <Icon icon={iconTypeToIcon[iconType]} />
b69ab31142 </Tooltip>
b69ab31143 )}
b69ab31144 <div className="split-diff-view-file-path-parts">{filePathParts}</div>
b69ab31145 {fileActions}
b69ab31146 </div>
b69ab31147 );
b69ab31148}
b69ab31149
b69ab31150const iconTypeToColor: Record<keyof typeof IconType, string> = {
b69ab31151 Modified: 'var(--scm-modified-foreground)',
b69ab31152 Added: 'var(--scm-added-foreground)',
b69ab31153 Removed: 'var(--scm-removed-foreground)',
b69ab31154};
b69ab31155
b69ab31156const iconTypeToIcon: Record<keyof typeof IconType, string> = {
b69ab31157 Modified: 'diff-modified',
b69ab31158 Added: 'diff-added',
b69ab31159 Removed: 'diff-removed',
b69ab31160};
b69ab31161
b69ab31162const iconTypeToTooltip: Record<keyof typeof IconType, string> = {
b69ab31163 Modified: t('This file was modified.'),
b69ab31164 Added: t('This file was added.'),
b69ab31165 Removed: t('This file was removed.'),
b69ab31166};
b69ab31167
b69ab31168function commonPrefixLength<T>(a: Array<T>, b: Array<T>): number {
b69ab31169 let i = 0;
b69ab31170 while (i < a.length && i < b.length && a[i] === b[i]) {
b69ab31171 i++;
b69ab31172 }
b69ab31173 return i;
b69ab31174}