addons/isl/src/SuggestedEdits.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 {atom} from 'jotai';
b69ab319import {minimalDisambiguousPaths} from 'shared/minimalDisambiguousPaths';
b69ab3110import {tracker} from './analytics';
b69ab3111import {File} from './ChangedFile';
b69ab3112import {Column, Row} from './ComponentUtils';
b69ab3113import {T, t} from './i18n';
b69ab3114import {Internal} from './Internal';
b69ab3115import {readAtom, writeAtom} from './jotaiUtils';
b69ab3116import type {PartialSelection} from './partialSelection';
b69ab3117import platform from './platform';
b69ab3118import {repoRootAtom} from './repositoryData';
b69ab3119import type {AbsolutePath, RepoRelativePath} from './types';
b69ab3120import {ChangedFileMode} from './types';
b69ab3121import {showModal} from './useModal';
b69ab3122import {registerDisposable} from './utils';
b69ab3123
b69ab3124import './UncommittedChanges.css';
b69ab3125
b69ab3126/** All known suggested edits, if applicable.
b69ab3127 * n.b. we get absolute paths from the suggested edits API */
b69ab3128const allSuggestedEdits = atom<Array<AbsolutePath>>([]);
b69ab3129registerDisposable(
b69ab3130 allSuggestedEdits,
b69ab3131 platform.suggestedEdits?.onDidChangeSuggestedEdits(edits => {
b69ab3132 writeAtom(allSuggestedEdits, edits);
b69ab3133 }) ?? {dispose: () => {}},
b69ab3134 import.meta.hot,
b69ab3135);
b69ab3136
b69ab3137/** Filter all known suggested edits to relevant repo-relative paths */
b69ab3138const currentSuggestedEdits = atom<Array<RepoRelativePath>>(get => {
b69ab3139 const allEdits = get(allSuggestedEdits);
b69ab3140 const repoRoot = get(repoRootAtom);
b69ab3141 return allEdits
b69ab3142 .filter(path => path.startsWith(repoRoot))
b69ab3143 .map(path => path.slice(repoRoot.length + 1));
b69ab3144});
b69ab3145
b69ab3146/**
b69ab3147 * If there are pending suggested edits as determined by the platform (typically suggestions from an AI),
b69ab3148 * and they intersect with the given files,
b69ab3149 * show a modal to confirm how to resolve those edits before proceeding.
b69ab3150 * Different operations may have a different behavior for resolving.
b69ab3151 * For example, `commit` should accept the edits before continuing,
b69ab3152 * but `revert` should reject the edits before continuing.
b69ab3153 *
b69ab3154 * We intentionally don't expose all possible ways of resolving edits for simplicity as a user.
b69ab3155 * We don't give any option to leave edits pending, because that should almost never be what you want.
b69ab3156 *
b69ab3157 * `source` is used for analytics purposes.
b69ab3158 */
b69ab3159export async function confirmSuggestedEditsForFiles(
b69ab3160 source: string,
b69ab3161 action: 'accept' | 'reject',
b69ab3162 files: PartialSelection | Array<RepoRelativePath>,
b69ab3163): Promise<boolean> {
b69ab3164 const suggestedEdits = readAtom(currentSuggestedEdits);
b69ab3165 if (suggestedEdits == null || suggestedEdits.length === 0) {
b69ab3166 return true; // nothing to warn about
b69ab3167 }
b69ab3168
b69ab3169 const toWarnAbout =
b69ab3170 files == null
b69ab3171 ? suggestedEdits
b69ab3172 : Array.isArray(files)
b69ab3173 ? suggestedEdits.filter(filepath => files.includes(filepath))
b69ab3174 : suggestedEdits.filter(filepath => files.isFullyOrPartiallySelected(filepath));
b69ab3175 if (toWarnAbout.length === 0) {
b69ab3176 return true; // nothing to warn about
b69ab3177 }
b69ab3178
b69ab3179 const buttons = [
b69ab3180 t('Cancel'),
b69ab3181 action === 'accept'
b69ab3182 ? {label: t('Accept Edits and Continue'), primary: true}
b69ab3183 : {label: t('Discard Edits and Continue'), primary: true},
b69ab3184 ];
b69ab3185 const answer = await showModal({
b69ab3186 type: 'confirm',
b69ab3187 buttons,
b69ab3188 title: Internal.PendingSuggestedEditsMessage ?? <T>You have pending suggested edits</T>,
b69ab3189 message: (
b69ab3190 <Column alignStart>
b69ab3191 <Column alignStart>
b69ab3192 <SimpleChangedFilesList files={toWarnAbout} />
b69ab3193 </Column>
b69ab3194 <Row>
b69ab3195 {action === 'accept' ? (
b69ab3196 <T>Do you want to accept these suggested edits and continue?</T>
b69ab3197 ) : (
b69ab3198 <T>Do you want to discard these suggested edits and continue?</T>
b69ab3199 )}
b69ab31100 </Row>
b69ab31101 </Column>
b69ab31102 ),
b69ab31103 });
b69ab31104 tracker.track('WarnAboutSuggestedEdits', {
b69ab31105 extras: {
b69ab31106 source,
b69ab31107 answer: typeof answer === 'string' ? answer : answer?.label,
b69ab31108 },
b69ab31109 });
b69ab31110
b69ab31111 switch (answer) {
b69ab31112 default:
b69ab31113 case buttons[0]:
b69ab31114 return false;
b69ab31115 case buttons[1]: {
b69ab31116 const fullEdits = readAtom(allSuggestedEdits);
b69ab31117 const absolutePaths = fullEdits.filter(path =>
b69ab31118 toWarnAbout.find(filepath => path.endsWith(filepath)),
b69ab31119 );
b69ab31120 platform.suggestedEdits?.resolveSuggestedEdits(action, absolutePaths);
b69ab31121 return true;
b69ab31122 }
b69ab31123 }
b69ab31124}
b69ab31125
b69ab31126/** Simplified list of changed files, for rendering a list of files when we don't have the full context of the file.
b69ab31127 * Just pretend everything is modified and hide extra actions like opening diff views.
b69ab31128 */
b69ab31129function SimpleChangedFilesList({files}: {files: Array<string>}) {
b69ab31130 const disambiguated = minimalDisambiguousPaths(files);
b69ab31131 return (
b69ab31132 <div className="changed-files-list-container">
b69ab31133 <div className="changed-files-list">
b69ab31134 {files.map((path, i) => (
b69ab31135 <File
b69ab31136 file={{
b69ab31137 label: disambiguated[i],
b69ab31138 path,
b69ab31139 tooltip: path,
b69ab31140 // These are wrong, but we don't have the full context of the file to know if it's added, removed, etc
b69ab31141 visualStatus: 'M',
b69ab31142 status: 'M',
b69ab31143 // Similar to the above, we assume it's a regular change
b69ab31144 // rather than a submodule update, which is unlikely to be suggested
b69ab31145 mode: ChangedFileMode.Regular,
b69ab31146 }}
b69ab31147 key={path}
b69ab31148 displayType="short"
b69ab31149 />
b69ab31150 ))}
b69ab31151 </div>
b69ab31152 </div>
b69ab31153 );
b69ab31154}