addons/isl/src/ChangedFile.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 {ReactNode} from 'react';
b69ab319import type {Comparison} from 'shared/Comparison';
b69ab3110import type {Place, UIChangedFile, VisualChangedFileStatus} from './UncommittedChanges';
b69ab3111import type {UseUncommittedSelection} from './partialSelection';
b69ab3112import type {ChangedFileStatus, GeneratedStatus} from './types';
b69ab3113
b69ab3114import {Button} from 'isl-components/Button';
b69ab3115import {Checkbox} from 'isl-components/Checkbox';
b69ab3116import {Icon} from 'isl-components/Icon';
b69ab3117import {isMac} from 'isl-components/OperatingSystem';
b69ab3118import {Subtle} from 'isl-components/Subtle';
b69ab3119import {Tooltip} from 'isl-components/Tooltip';
b69ab3120import {useAtomValue} from 'jotai';
b69ab3121import React from 'react';
b69ab3122import {ComparisonType, labelForComparison, revsetForComparison} from 'shared/Comparison';
b69ab3123import {useContextMenu} from 'shared/ContextMenu';
b69ab3124import {basename, notEmpty} from 'shared/utils';
b69ab3125import {copyUrlForFile, supportsBrowseUrlForHash} from './BrowseRepo';
b69ab3126import {type ChangedFilesDisplayType} from './ChangedFileDisplayTypePicker';
b69ab3127import {generatedStatusDescription, generatedStatusToLabel} from './GeneratedFile';
b69ab3128import {PartialFileSelectionWithMode} from './PartialFileSelection';
b69ab3129import {confirmSuggestedEditsForFiles} from './SuggestedEdits';
b69ab3130import {SuspenseBoundary} from './SuspenseBoundary';
b69ab3131import {holdingAltAtom, holdingCtrlAtom} from './atoms/keyboardAtoms';
b69ab3132import {externalMergeToolAtom} from './externalMergeTool';
b69ab3133import {T, t} from './i18n';
b69ab3134import {readAtom} from './jotaiUtils';
b69ab3135import {CONFLICT_SIDE_LABELS} from './mergeConflicts/consts';
b69ab3136import {AddOperation} from './operations/AddOperation';
b69ab3137import {ForgetOperation} from './operations/ForgetOperation';
b69ab3138import {PurgeOperation} from './operations/PurgeOperation';
b69ab3139import {ResolveInExternalMergeToolOperation} from './operations/ResolveInExternalMergeToolOperation';
b69ab3140import {ResolveOperation, ResolveTool} from './operations/ResolveOperation';
b69ab3141import {RevertOperation} from './operations/RevertOperation';
b69ab3142import {RmOperation} from './operations/RmOperation';
b69ab3143import {useRunOperation} from './operationsState';
b69ab3144import {useUncommittedSelection} from './partialSelection';
b69ab3145import platform from './platform';
b69ab3146import {optimisticMergeConflicts} from './previews';
b69ab3147import {copyAndShowToast} from './toast';
b69ab3148import {ChangedFileMode, ConflictType, succeedableRevset} from './types';
b69ab3149import {usePromise} from './usePromise';
b69ab3150
b69ab3151/**
b69ab3152 * Is the alt key currently held down, used to show full file paths.
b69ab3153 * On windows, this actually uses the ctrl key instead to avoid conflicting with OS focus behaviors.
b69ab3154 */
b69ab3155const holdingModifiedKeyAtom = isMac ? holdingAltAtom : holdingCtrlAtom;
b69ab3156
b69ab3157export function File({
b69ab3158 file,
b69ab3159 displayType,
b69ab3160 comparison,
b69ab3161 selection,
b69ab3162 place,
b69ab3163 generatedStatus,
b69ab3164}: {
b69ab3165 file: UIChangedFile;
b69ab3166 displayType: ChangedFilesDisplayType;
b69ab3167 comparison?: Comparison;
b69ab3168 selection?: UseUncommittedSelection;
b69ab3169 place?: Place;
b69ab3170 generatedStatus?: GeneratedStatus;
b69ab3171}) {
b69ab3172 const clipboardCopy = (text: string) => copyAndShowToast(text);
b69ab3173
b69ab3174 // Renamed files are files which have a copy field, where that path was also removed.
b69ab3175
b69ab3176 // Visually show renamed files as if they were modified, even though sl treats them as added.
b69ab3177 const [statusName, icon] = nameAndIconForFileStatus[file.visualStatus];
b69ab3178
b69ab3179 const generated = generatedStatusToLabel(generatedStatus);
b69ab3180
b69ab3181 const contextMenu = useContextMenu(() => {
b69ab3182 const options = [
b69ab3183 {label: t('Copy File Path'), onClick: () => clipboardCopy(file.path)},
b69ab3184 {label: t('Copy Filename'), onClick: () => clipboardCopy(basename(file.path))},
b69ab3185 {label: t('Open File'), onClick: () => platform.openFile(file.path)},
b69ab3186 ];
b69ab3187
b69ab3188 if (platform.openContainingFolder != null) {
b69ab3189 options.push({
b69ab3190 label: t('Open Containing Folder'),
b69ab3191 onClick: () => platform.openContainingFolder?.(file.path),
b69ab3192 });
b69ab3193 }
b69ab3194 if (comparison != null && platform.openDiff != null) {
b69ab3195 options.push({
b69ab3196 label: t('Open Diff View ($comparison)', {
b69ab3197 replace: {$comparison: labelForComparison(comparison)},
b69ab3198 }),
b69ab3199 onClick: () => platform.openDiff?.(file.path, comparison),
b69ab31100 });
b69ab31101 }
b69ab31102
b69ab31103 if (comparison != null && readAtom(supportsBrowseUrlForHash)) {
b69ab31104 options.push({
b69ab31105 label: t('Copy file URL'),
b69ab31106 onClick: () => {
b69ab31107 copyUrlForFile(file.path, comparison);
b69ab31108 },
b69ab31109 });
b69ab31110 }
b69ab31111 return options;
b69ab31112 });
b69ab31113
b69ab31114 const runOperation = useRunOperation();
b69ab31115
b69ab31116 // Hold "alt" key to show full file paths instead of short form.
b69ab31117 // This is a quick way to see where a file comes from without
b69ab31118 // needing to go through the menu to change the rendering type.
b69ab31119 const isHoldingAlt = useAtomValue(holdingModifiedKeyAtom);
b69ab31120
b69ab31121 const tooltip = [file.tooltip, generatedStatusDescription(generatedStatus)]
b69ab31122 .filter(notEmpty)
b69ab31123 .join('\n\n');
b69ab31124
b69ab31125 const openFile = () => {
b69ab31126 if (file.mode === ChangedFileMode.Submodule) {
b69ab31127 return;
b69ab31128 }
b69ab31129 if (file.visualStatus === 'U') {
b69ab31130 const tool = readAtom(externalMergeToolAtom);
b69ab31131 if (tool != null) {
b69ab31132 runOperation(new ResolveInExternalMergeToolOperation(tool, file.path));
b69ab31133 return;
b69ab31134 }
b69ab31135 }
b69ab31136 platform.openFile(file.path);
b69ab31137 };
b69ab31138
b69ab31139 return (
b69ab31140 <>
b69ab31141 <div
b69ab31142 className={`changed-file file-${statusName} file-${generated}`}
b69ab31143 data-testid={`changed-file-${file.path}`}
b69ab31144 onContextMenu={contextMenu}
b69ab31145 key={file.path}
b69ab31146 tabIndex={0}
b69ab31147 onKeyUp={e => {
b69ab31148 if (e.key === 'Enter') {
b69ab31149 openFile();
b69ab31150 }
b69ab31151 }}>
b69ab31152 <FileSelectionCheckbox file={file} selection={selection} />
b69ab31153 <span className="changed-file-path" onClick={openFile}>
b69ab31154 <Icon icon={icon} />
b69ab31155 <Tooltip title={tooltip} delayMs={2_000} placement="right">
b69ab31156 <span
b69ab31157 className="changed-file-path-text"
b69ab31158 onCopy={e => {
b69ab31159 const selection = document.getSelection();
b69ab31160 if (selection) {
b69ab31161 // we inserted LTR markers, remove them again on copy
b69ab31162 e.clipboardData.setData(
b69ab31163 'text/plain',
b69ab31164 selection.toString().replace(/\u200E/g, ''),
b69ab31165 );
b69ab31166 e.preventDefault();
b69ab31167 }
b69ab31168 }}>
b69ab31169 {escapeForRTL(
b69ab31170 displayType === 'tree'
b69ab31171 ? file.path.slice(file.path.lastIndexOf('/') + 1)
b69ab31172 : // Holding alt takes precedence over fish/short styles, but not tree.
b69ab31173 displayType === 'fullPaths' || isHoldingAlt
b69ab31174 ? file.path
b69ab31175 : displayType === 'fish'
b69ab31176 ? file.path
b69ab31177 .split('/')
b69ab31178 .map((a, i, arr) => (i === arr.length - 1 ? a : a[0]))
b69ab31179 .join('/')
b69ab31180 : file.label,
b69ab31181 )}
b69ab31182 </span>
b69ab31183 </Tooltip>
b69ab31184 </span>
b69ab31185 {comparison != null && <FileActions file={file} comparison={comparison} place={place} />}
b69ab31186 </div>
b69ab31187 {place === 'main' &&
b69ab31188 selection?.isExpanded(file.path) &&
b69ab31189 file.mode !== ChangedFileMode.Submodule && <MaybePartialSelection file={file} />}
b69ab31190 </>
b69ab31191 );
b69ab31192}
b69ab31193
b69ab31194const revertableStatues = new Set(['M', 'R', '!']);
b69ab31195const conflictStatuses = new Set<ChangedFileStatus>(['U', 'Resolved']);
b69ab31196function FileActions({
b69ab31197 comparison,
b69ab31198 file,
b69ab31199 place,
b69ab31200}: {
b69ab31201 comparison: Comparison;
b69ab31202 file: UIChangedFile;
b69ab31203 place?: Place;
b69ab31204}) {
b69ab31205 const runOperation = useRunOperation();
b69ab31206 const conflicts = useAtomValue(optimisticMergeConflicts);
b69ab31207
b69ab31208 const conflictData = conflicts?.files?.find(f => f.path === file.path);
b69ab31209 const label = labelForConflictType(conflictData?.conflictType);
b69ab31210 let conflictLabel = null;
b69ab31211 if (label) {
b69ab31212 conflictLabel = <Subtle>{label}</Subtle>;
b69ab31213 }
b69ab31214
b69ab31215 const actions: Array<React.ReactNode> = [];
b69ab31216
b69ab31217 if (platform.openDiff != null && !conflictStatuses.has(file.status)) {
b69ab31218 actions.push(
b69ab31219 <Tooltip title={t('Open diff view')} key="open-diff-view" delayMs={1000}>
b69ab31220 <Button
b69ab31221 className="file-show-on-hover"
b69ab31222 icon
b69ab31223 data-testid="file-open-diff-button"
b69ab31224 onClick={() => {
b69ab31225 platform.openDiff?.(file.path, comparison);
b69ab31226 }}>
b69ab31227 <Icon icon="request-changes" />
b69ab31228 </Button>
b69ab31229 </Tooltip>,
b69ab31230 );
b69ab31231 }
b69ab31232
b69ab31233 if (
b69ab31234 (revertableStatues.has(file.status) && comparison.type !== ComparisonType.Committed) ||
b69ab31235 // special case: reverting does actually work for added files in the head commit
b69ab31236 (comparison.type === ComparisonType.HeadChanges && file.status === 'A')
b69ab31237 ) {
b69ab31238 actions.push(
b69ab31239 <Tooltip
b69ab31240 title={
b69ab31241 comparison.type === ComparisonType.UncommittedChanges
b69ab31242 ? t('Revert back to last commit')
b69ab31243 : t('Revert changes made by this commit')
b69ab31244 }
b69ab31245 key="revert"
b69ab31246 delayMs={1000}>
b69ab31247 <Button
b69ab31248 className="file-show-on-hover"
b69ab31249 key={file.path}
b69ab31250 icon
b69ab31251 data-testid="file-revert-button"
b69ab31252 onClick={async () => {
b69ab31253 if (!(await confirmSuggestedEditsForFiles('revert', 'reject', [file.path]))) {
b69ab31254 return;
b69ab31255 }
b69ab31256
b69ab31257 const ok = await platform.confirm(
b69ab31258 comparison.type === ComparisonType.UncommittedChanges
b69ab31259 ? t('Are you sure you want to revert $file?', {replace: {$file: file.path}})
b69ab31260 : t(
b69ab31261 'Are you sure you want to revert $file back to how it was just before the last commit? Uncommitted changes to this file will be lost.',
b69ab31262 {replace: {$file: file.path}},
b69ab31263 ),
b69ab31264 );
b69ab31265 if (!ok) {
b69ab31266 return;
b69ab31267 }
b69ab31268 runOperation(
b69ab31269 new RevertOperation(
b69ab31270 [file.path],
b69ab31271 comparison.type === ComparisonType.UncommittedChanges
b69ab31272 ? undefined
b69ab31273 : succeedableRevset(revsetForComparison(comparison)),
b69ab31274 ),
b69ab31275 );
b69ab31276 }}>
b69ab31277 <Icon icon="discard" />
b69ab31278 </Button>
b69ab31279 </Tooltip>,
b69ab31280 );
b69ab31281 }
b69ab31282
b69ab31283 if (comparison.type === ComparisonType.UncommittedChanges) {
b69ab31284 if (file.status === 'A') {
b69ab31285 actions.push(
b69ab31286 <Tooltip
b69ab31287 title={t('Stop tracking this file, without removing from the filesystem')}
b69ab31288 key="forget"
b69ab31289 delayMs={1000}>
b69ab31290 <Button
b69ab31291 className="file-show-on-hover"
b69ab31292 key={file.path}
b69ab31293 icon
b69ab31294 onClick={() => {
b69ab31295 runOperation(new ForgetOperation(file.path));
b69ab31296 }}>
b69ab31297 <Icon icon="circle-slash" />
b69ab31298 </Button>
b69ab31299 </Tooltip>,
b69ab31300 );
b69ab31301 } else if (file.status === '?') {
b69ab31302 const removeHint =
b69ab31303 file.mode === ChangedFileMode.Submodule
b69ab31304 ? t(
b69ab31305 'Please manually remove the submodule and clean up properly to avoid leaving unwanted side effects.',
b69ab31306 )
b69ab31307 : t('Remove file');
b69ab31308 actions.push(
b69ab31309 <Tooltip title={t('Start tracking this file')} key="add" delayMs={1000}>
b69ab31310 <Button
b69ab31311 className="file-show-on-hover"
b69ab31312 key={file.path}
b69ab31313 icon
b69ab31314 onClick={() => runOperation(new AddOperation(file.path))}>
b69ab31315 <Icon icon="add" />
b69ab31316 </Button>
b69ab31317 </Tooltip>,
b69ab31318 <Tooltip title={removeHint} key="remove" delayMs={1000}>
b69ab31319 <Button
b69ab31320 className="file-show-on-hover"
b69ab31321 disabled={file.mode === ChangedFileMode.Submodule}
b69ab31322 key={file.path}
b69ab31323 icon
b69ab31324 data-testid="file-action-delete"
b69ab31325 onClick={async () => {
b69ab31326 const ok = await platform.confirm(
b69ab31327 t('Are you sure you want to delete $file?', {replace: {$file: file.path}}),
b69ab31328 );
b69ab31329 if (!ok) {
b69ab31330 return;
b69ab31331 }
b69ab31332 runOperation(new PurgeOperation([file.path]));
b69ab31333 }}>
b69ab31334 <Icon icon="trash" />
b69ab31335 </Button>
b69ab31336 </Tooltip>,
b69ab31337 );
b69ab31338 } else if (file.status === 'Resolved') {
b69ab31339 actions.push(
b69ab31340 <Tooltip title={t('Mark as unresolved')} key="unresolve-mark">
b69ab31341 <Button
b69ab31342 key={file.path}
b69ab31343 icon
b69ab31344 onClick={() => runOperation(new ResolveOperation(file.path, ResolveTool.unmark))}>
b69ab31345 <Icon icon="circle-slash" />
b69ab31346 </Button>
b69ab31347 </Tooltip>,
b69ab31348 );
b69ab31349 } else if (file.status === 'U') {
b69ab31350 actions.push(
b69ab31351 <Tooltip title={t('Mark as resolved')} key="resolve-mark">
b69ab31352 <Button
b69ab31353 className="file-show-on-hover"
b69ab31354 data-testid="file-action-resolve"
b69ab31355 key={file.path}
b69ab31356 icon
b69ab31357 onClick={() => runOperation(new ResolveOperation(file.path, ResolveTool.mark))}>
b69ab31358 <Icon icon="check" />
b69ab31359 </Button>
b69ab31360 </Tooltip>,
b69ab31361 );
b69ab31362 if (
b69ab31363 conflictData?.conflictType &&
b69ab31364 [ConflictType.DeletedInSource, ConflictType.DeletedInDest].includes(
b69ab31365 conflictData.conflictType,
b69ab31366 )
b69ab31367 ) {
b69ab31368 actions.push(
b69ab31369 <Tooltip title={t('Delete file')} key="resolve-delete">
b69ab31370 <Button
b69ab31371 className="file-show-on-hover"
b69ab31372 data-testid="file-action-resolve-delete"
b69ab31373 icon
b69ab31374 onClick={() => {
b69ab31375 runOperation(new RmOperation(file.path, /* force */ true));
b69ab31376 // then explicitly mark the file as resolved
b69ab31377 runOperation(new ResolveOperation(file.path, ResolveTool.mark));
b69ab31378 }}>
b69ab31379 <Icon icon="trash" />
b69ab31380 </Button>
b69ab31381 </Tooltip>,
b69ab31382 );
b69ab31383 } else {
b69ab31384 actions.push(
b69ab31385 <Tooltip
b69ab31386 title={t('Take $local', {
b69ab31387 replace: {$local: CONFLICT_SIDE_LABELS.local},
b69ab31388 })}
b69ab31389 key="resolve-local">
b69ab31390 <Button
b69ab31391 className="file-show-on-hover"
b69ab31392 key={file.path}
b69ab31393 icon
b69ab31394 onClick={() => runOperation(new ResolveOperation(file.path, ResolveTool.local))}>
b69ab31395 <Icon icon="fold-up" />
b69ab31396 </Button>
b69ab31397 </Tooltip>,
b69ab31398 <Tooltip
b69ab31399 title={t('Take $incoming', {
b69ab31400 replace: {$incoming: CONFLICT_SIDE_LABELS.incoming},
b69ab31401 })}
b69ab31402 key="resolve-other">
b69ab31403 <Button
b69ab31404 className="file-show-on-hover"
b69ab31405 key={file.path}
b69ab31406 icon
b69ab31407 onClick={() => runOperation(new ResolveOperation(file.path, ResolveTool.other))}>
b69ab31408 <Icon icon="fold-down" />
b69ab31409 </Button>
b69ab31410 </Tooltip>,
b69ab31411 <Tooltip
b69ab31412 title={t('Combine both $incoming and $local', {
b69ab31413 replace: {
b69ab31414 $local: CONFLICT_SIDE_LABELS.local,
b69ab31415 $incoming: CONFLICT_SIDE_LABELS.incoming,
b69ab31416 },
b69ab31417 })}
b69ab31418 key="resolve-both">
b69ab31419 <Button
b69ab31420 className="file-show-on-hover"
b69ab31421 key={file.path}
b69ab31422 icon
b69ab31423 onClick={() => runOperation(new ResolveOperation(file.path, ResolveTool.both))}>
b69ab31424 <Icon icon="fold" />
b69ab31425 </Button>
b69ab31426 </Tooltip>,
b69ab31427 );
b69ab31428 }
b69ab31429 }
b69ab31430
b69ab31431 if (place === 'main' && conflicts == null && file.mode !== ChangedFileMode.Submodule) {
b69ab31432 actions.push(<PartialSelectionAction file={file} key="partial-selection" />);
b69ab31433 }
b69ab31434 }
b69ab31435 return (
b69ab31436 <div className="file-actions" data-testid="file-actions">
b69ab31437 {conflictLabel}
b69ab31438 {actions}
b69ab31439 </div>
b69ab31440 );
b69ab31441}
b69ab31442
b69ab31443function labelForConflictType(type?: ConflictType) {
b69ab31444 switch (type) {
b69ab31445 case ConflictType.DeletedInSource:
b69ab31446 return t('(Deleted in $incoming)', {
b69ab31447 replace: {$incoming: CONFLICT_SIDE_LABELS.incoming},
b69ab31448 });
b69ab31449
b69ab31450 case ConflictType.DeletedInDest:
b69ab31451 return t('(Deleted in $local)', {replace: {$local: CONFLICT_SIDE_LABELS.local}});
b69ab31452 default:
b69ab31453 return null;
b69ab31454 }
b69ab31455}
b69ab31456
b69ab31457/**
b69ab31458 * We render file paths with CSS text-direction: rtl,
b69ab31459 * which allows the ellipsis overflow to appear on the left.
b69ab31460 * However, rtl can have weird effects, such as moving leading '.' to the end.
b69ab31461 * To fix this, it's enough to add a left-to-right marker at the start of the path
b69ab31462 */
b69ab31463function escapeForRTL(s: string): ReactNode {
b69ab31464 return '\u200E' + s + '\u200E';
b69ab31465}
b69ab31466
b69ab31467function FileSelectionCheckbox({
b69ab31468 file,
b69ab31469 selection,
b69ab31470}: {
b69ab31471 file: UIChangedFile;
b69ab31472 selection?: UseUncommittedSelection;
b69ab31473}) {
b69ab31474 const checked = selection?.isFullyOrPartiallySelected(file.path) ?? false;
b69ab31475 return selection == null ? null : (
b69ab31476 <Checkbox
b69ab31477 aria-label={t('$label $file', {
b69ab31478 replace: {$label: checked ? 'unselect' : 'select', $file: file.path},
b69ab31479 })}
b69ab31480 checked={checked}
b69ab31481 indeterminate={selection.isPartiallySelected(file.path)}
b69ab31482 data-testid={'file-selection-checkbox'}
b69ab31483 onChange={checked => {
b69ab31484 if (checked) {
b69ab31485 if (file.renamedFrom != null) {
b69ab31486 // Selecting a renamed file also selects the original, so they are committed/amended together
b69ab31487 // the UI merges them visually anyway.
b69ab31488 selection.select(file.renamedFrom, file.path);
b69ab31489 } else {
b69ab31490 selection.select(file.path);
b69ab31491 }
b69ab31492 } else {
b69ab31493 if (file.renamedFrom != null) {
b69ab31494 selection.deselect(file.renamedFrom, file.path);
b69ab31495 } else {
b69ab31496 selection.deselect(file.path);
b69ab31497 }
b69ab31498 }
b69ab31499 }}
b69ab31500 />
b69ab31501 );
b69ab31502}
b69ab31503
b69ab31504function PartialSelectionAction({file}: {file: UIChangedFile}) {
b69ab31505 const selection = useUncommittedSelection();
b69ab31506
b69ab31507 const handleClick = () => {
b69ab31508 selection.toggleExpand(file.path);
b69ab31509 };
b69ab31510
b69ab31511 return (
b69ab31512 <Tooltip
b69ab31513 component={() => (
b69ab31514 <div style={{maxWidth: '300px'}}>
b69ab31515 <div>
b69ab31516 <T>Toggle chunk selection</T>
b69ab31517 </div>
b69ab31518 <div>
b69ab31519 <Subtle>
b69ab31520 <T>
b69ab31521 Shows changed files in your commit and lets you select individual chunks or lines to
b69ab31522 include.
b69ab31523 </T>
b69ab31524 </Subtle>
b69ab31525 </div>
b69ab31526 </div>
b69ab31527 )}>
b69ab31528 <Button className="file-show-on-hover" icon onClick={handleClick}>
b69ab31529 <Icon icon="diff" />
b69ab31530 </Button>
b69ab31531 </Tooltip>
b69ab31532 );
b69ab31533}
b69ab31534
b69ab31535// Left margin to "indented" by roughly a checkbox width.
b69ab31536const leftMarginStyle: React.CSSProperties = {marginLeft: 'calc(2.5 * var(--pad))'};
b69ab31537
b69ab31538function MaybePartialSelection({file}: {file: UIChangedFile}) {
b69ab31539 const fallback = (
b69ab31540 <div style={leftMarginStyle}>
b69ab31541 <Icon icon="loading" />
b69ab31542 </div>
b69ab31543 );
b69ab31544 return (
b69ab31545 <SuspenseBoundary fallback={fallback}>
b69ab31546 <PartialSelectionPanel file={file} />
b69ab31547 </SuspenseBoundary>
b69ab31548 );
b69ab31549}
b69ab31550
b69ab31551function PartialSelectionPanel({file}: {file: UIChangedFile}) {
b69ab31552 const path = file.path;
b69ab31553 const selection = useUncommittedSelection();
b69ab31554 const chunkSelect = usePromise(selection.getChunkSelect(path));
b69ab31555
b69ab31556 return (
b69ab31557 <div style={leftMarginStyle}>
b69ab31558 <PartialFileSelectionWithMode
b69ab31559 chunkSelection={chunkSelect}
b69ab31560 setChunkSelection={state => selection.editChunkSelect(path, state)}
b69ab31561 mode="unified"
b69ab31562 />
b69ab31563 </div>
b69ab31564 );
b69ab31565}
b69ab31566
b69ab31567/**
b69ab31568 * Map for changed files statuses into classNames (for color & styles) and icon names.
b69ab31569 */
b69ab31570const nameAndIconForFileStatus: Record<VisualChangedFileStatus, [string, string]> = {
b69ab31571 A: ['added', 'diff-added'],
b69ab31572 M: ['modified', 'diff-modified'],
b69ab31573 R: ['removed', 'diff-removed'],
b69ab31574 '?': ['ignored', 'question'],
b69ab31575 '!': ['missing', 'warning'],
b69ab31576 U: ['unresolved', 'diff-ignored'],
b69ab31577 Resolved: ['resolved', 'pass'],
b69ab31578 Renamed: ['modified', 'diff-renamed'],
b69ab31579 Copied: ['added', 'diff-added'],
b69ab31580};