5.2 KB155 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import {atom} from 'jotai';
9import {minimalDisambiguousPaths} from 'shared/minimalDisambiguousPaths';
10import {tracker} from './analytics';
11import {File} from './ChangedFile';
12import {Column, Row} from './ComponentUtils';
13import {T, t} from './i18n';
14import {Internal} from './Internal';
15import {readAtom, writeAtom} from './jotaiUtils';
16import type {PartialSelection} from './partialSelection';
17import platform from './platform';
18import {repoRootAtom} from './repositoryData';
19import type {AbsolutePath, RepoRelativePath} from './types';
20import {ChangedFileMode} from './types';
21import {showModal} from './useModal';
22import {registerDisposable} from './utils';
23
24import './UncommittedChanges.css';
25
26/** All known suggested edits, if applicable.
27 * n.b. we get absolute paths from the suggested edits API */
28const allSuggestedEdits = atom<Array<AbsolutePath>>([]);
29registerDisposable(
30 allSuggestedEdits,
31 platform.suggestedEdits?.onDidChangeSuggestedEdits(edits => {
32 writeAtom(allSuggestedEdits, edits);
33 }) ?? {dispose: () => {}},
34 import.meta.hot,
35);
36
37/** Filter all known suggested edits to relevant repo-relative paths */
38const currentSuggestedEdits = atom<Array<RepoRelativePath>>(get => {
39 const allEdits = get(allSuggestedEdits);
40 const repoRoot = get(repoRootAtom);
41 return allEdits
42 .filter(path => path.startsWith(repoRoot))
43 .map(path => path.slice(repoRoot.length + 1));
44});
45
46/**
47 * If there are pending suggested edits as determined by the platform (typically suggestions from an AI),
48 * and they intersect with the given files,
49 * show a modal to confirm how to resolve those edits before proceeding.
50 * Different operations may have a different behavior for resolving.
51 * For example, `commit` should accept the edits before continuing,
52 * but `revert` should reject the edits before continuing.
53 *
54 * We intentionally don't expose all possible ways of resolving edits for simplicity as a user.
55 * We don't give any option to leave edits pending, because that should almost never be what you want.
56 *
57 * `source` is used for analytics purposes.
58 */
59export async function confirmSuggestedEditsForFiles(
60 source: string,
61 action: 'accept' | 'reject',
62 files: PartialSelection | Array<RepoRelativePath>,
63): Promise<boolean> {
64 const suggestedEdits = readAtom(currentSuggestedEdits);
65 if (suggestedEdits == null || suggestedEdits.length === 0) {
66 return true; // nothing to warn about
67 }
68
69 const toWarnAbout =
70 files == null
71 ? suggestedEdits
72 : Array.isArray(files)
73 ? suggestedEdits.filter(filepath => files.includes(filepath))
74 : suggestedEdits.filter(filepath => files.isFullyOrPartiallySelected(filepath));
75 if (toWarnAbout.length === 0) {
76 return true; // nothing to warn about
77 }
78
79 const buttons = [
80 t('Cancel'),
81 action === 'accept'
82 ? {label: t('Accept Edits and Continue'), primary: true}
83 : {label: t('Discard Edits and Continue'), primary: true},
84 ];
85 const answer = await showModal({
86 type: 'confirm',
87 buttons,
88 title: Internal.PendingSuggestedEditsMessage ?? <T>You have pending suggested edits</T>,
89 message: (
90 <Column alignStart>
91 <Column alignStart>
92 <SimpleChangedFilesList files={toWarnAbout} />
93 </Column>
94 <Row>
95 {action === 'accept' ? (
96 <T>Do you want to accept these suggested edits and continue?</T>
97 ) : (
98 <T>Do you want to discard these suggested edits and continue?</T>
99 )}
100 </Row>
101 </Column>
102 ),
103 });
104 tracker.track('WarnAboutSuggestedEdits', {
105 extras: {
106 source,
107 answer: typeof answer === 'string' ? answer : answer?.label,
108 },
109 });
110
111 switch (answer) {
112 default:
113 case buttons[0]:
114 return false;
115 case buttons[1]: {
116 const fullEdits = readAtom(allSuggestedEdits);
117 const absolutePaths = fullEdits.filter(path =>
118 toWarnAbout.find(filepath => path.endsWith(filepath)),
119 );
120 platform.suggestedEdits?.resolveSuggestedEdits(action, absolutePaths);
121 return true;
122 }
123 }
124}
125
126/** Simplified list of changed files, for rendering a list of files when we don't have the full context of the file.
127 * Just pretend everything is modified and hide extra actions like opening diff views.
128 */
129function SimpleChangedFilesList({files}: {files: Array<string>}) {
130 const disambiguated = minimalDisambiguousPaths(files);
131 return (
132 <div className="changed-files-list-container">
133 <div className="changed-files-list">
134 {files.map((path, i) => (
135 <File
136 file={{
137 label: disambiguated[i],
138 path,
139 tooltip: path,
140 // These are wrong, but we don't have the full context of the file to know if it's added, removed, etc
141 visualStatus: 'M',
142 status: 'M',
143 // Similar to the above, we assume it's a regular change
144 // rather than a submodule update, which is unlikely to be suggested
145 mode: ChangedFileMode.Regular,
146 }}
147 key={path}
148 displayType="short"
149 />
150 ))}
151 </div>
152 </div>
153 );
154}
155