Skip to content

Commit 657e98b

Browse files
Merge pull request #9465 from gitbutlerapp/edit-mode-refactoring
Reduxify Mode Service and start working on RemoveFile removal
2 parents 54e480d + 56da948 commit 657e98b

File tree

28 files changed

+408
-325
lines changed

28 files changed

+408
-325
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src/components/BranchList.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
conflictResolutionConfirmationModal?.show();
8787
return;
8888
}
89-
modeService!.enterEditMode(args.commitId, stackId);
89+
modeService!.enterEditMode({ commitId: args.commitId, stackId, projectId });
9090
}
9191
9292
const selectedCommit = $derived(

apps/desktop/src/components/CommitView.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@
136136
137137
async function editPatch() {
138138
if (!canEdit()) return;
139-
await modeService!.enterEditMode(commitKey.commitId, stackId);
139+
await modeService!.enterEditMode({ commitId: commitKey.commitId, stackId, projectId });
140140
}
141141
142142
function cancelEdit() {

apps/desktop/src/components/EditMode.svelte

Lines changed: 81 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,15 @@
77
import {
88
conflictEntryHint,
99
getConflictState,
10-
getInitialFileStatus,
11-
type ConflictEntryPresence,
12-
type ConflictState
10+
type ConflictEntryPresence
1311
} from '$lib/conflictEntryPresence';
14-
import { type RemoteFile } from '$lib/files/file';
15-
import { UncommitedFilesWatcher } from '$lib/files/watcher';
12+
import { FileService } from '$lib/files/fileService';
1613
import { ModeService, type EditModeMetadata } from '$lib/mode/modeService';
1714
import { vscodePath } from '$lib/project/project';
1815
import { ProjectsService } from '$lib/project/projectsService';
1916
import { SETTINGS, type Settings } from '$lib/settings/userSettings';
2017
import { UserService } from '$lib/user/userService';
21-
import { computeFileStatus } from '$lib/utils/fileStatus';
18+
import { computeChangeStatus } from '$lib/utils/fileStatus';
2219
import { getEditorUri, openExternalUrl } from '$lib/utils/url';
2320
import { getContext } from '@gitbutler/shared/context';
2421
import { getContextStoreBySymbol } from '@gitbutler/shared/context';
@@ -28,9 +25,19 @@
2825
import Modal from '@gitbutler/ui/Modal.svelte';
2926
import Avatar from '@gitbutler/ui/avatar/Avatar.svelte';
3027
import FileListItem from '@gitbutler/ui/file/FileListItem.svelte';
28+
import { isDefined } from '@gitbutler/ui/utils/typeguards';
3129
import { SvelteSet } from 'svelte/reactivity';
30+
import {
31+
derived,
32+
fromStore,
33+
readable,
34+
toStore,
35+
type Readable,
36+
type Writable
37+
} from 'svelte/store';
38+
import type { FileInfo } from '$lib/files/file';
39+
import type { TreeChange } from '$lib/hunks/change';
3240
import type { FileStatus } from '@gitbutler/ui/file/types';
33-
import type { Writable } from 'svelte/store';
3441
3542
type Props = {
3643
projectId: string;
@@ -43,19 +50,28 @@
4350
const projectResult = $derived(projectService.getProject(projectId));
4451
4552
const remoteCommitService = getContext(CommitService);
46-
const uncommitedFileWatcher = getContext(UncommitedFilesWatcher);
4753
const modeService = getContext(ModeService);
4854
const userSettings = getContextStoreBySymbol<Settings, Writable<Settings>>(SETTINGS);
49-
50-
const uncommitedFiles = $derived(uncommitedFileWatcher.uncommittedFiles(projectId));
55+
const fileService = getContext(FileService);
5156
5257
const userService = getContext(UserService);
5358
const user = userService.user;
5459
5560
let modeServiceAborting = $state<'inert' | 'loading' | 'completed'>('inert');
5661
let modeServiceSaving = $state<'inert' | 'loading' | 'completed'>('inert');
5762
58-
let initialFiles = $state<[RemoteFile, ConflictEntryPresence | undefined][]>([]);
63+
const initialFiles = $derived(modeService.initialEditModeState({ projectId }));
64+
const uncommittedFiles = $derived(modeService.changesSinceInitialEditState({ projectId }));
65+
66+
function readFromWorkspace(
67+
filePath: string,
68+
projectId: string
69+
): Readable<{ data: FileInfo; isLarge: boolean } | undefined> {
70+
return readable(undefined as { data: FileInfo; isLarge: boolean } | undefined, (set) => {
71+
fileService.readFromWorkspace(filePath, projectId).then(set);
72+
});
73+
}
74+
5975
let commit = $state<Commit>();
6076
6177
async function getCommitData() {
@@ -79,89 +95,53 @@
7995
let contextMenu = $state<ReturnType<typeof FileContextMenu> | undefined>(undefined);
8096
let confirmSaveModal = $state<ReturnType<typeof Modal> | undefined>(undefined);
8197
82-
$effect(() => {
83-
modeService.getInitialIndexState().then((files) => {
84-
initialFiles = files;
85-
});
86-
});
87-
8898
interface FileEntry {
89-
conflicted: boolean;
90-
name: string;
9199
path: string;
92100
status?: FileStatus;
101+
conflicted: boolean;
93102
conflictHint?: string;
94-
conflictState?: ConflictState;
95-
conflictEntryPresence?: ConflictEntryPresence;
96103
}
97104
98105
const initialFileMap = $derived(
99-
new Map<string, RemoteFile>(initialFiles.map(([file]) => [file.path, file]))
100-
);
101-
102-
const uncommitedFileMap = $derived(
103-
new Map<string, RemoteFile>($uncommitedFiles.map(([file]) => [file.path, file]))
106+
new Map<string, { file: TreeChange; conflictEntryPresence?: ConflictEntryPresence }>(
107+
initialFiles.current?.data?.map(([f, c]) => [
108+
f.path,
109+
{ file: f, conflictEntryPresence: c }
110+
]) || []
111+
)
104112
);
105113
106114
const files = $derived.by(() => {
115+
if (!initialFiles.current.data || !uncommittedFiles.current.data) return [];
116+
107117
const outputMap = new Map<string, FileEntry>();
108118
109-
// Create output
110-
{
111-
initialFiles.forEach(([initialFile, conflictEntryPresence]) => {
112-
const conflictState =
113-
conflictEntryPresence && getConflictState(initialFile, conflictEntryPresence);
114-
115-
const uncommitedFileChange = uncommitedFileMap.get(initialFile.path);
116-
117-
outputMap.set(initialFile.path, {
118-
name: initialFile.filename,
119-
path: initialFile.path,
120-
conflicted: !!conflictEntryPresence,
121-
conflictHint: conflictEntryPresence
122-
? conflictEntryHint(conflictEntryPresence)
123-
: undefined,
124-
status: getInitialFileStatus(uncommitedFileChange, conflictEntryPresence),
125-
conflictState,
126-
conflictEntryPresence
127-
});
119+
initialFiles.current.data.forEach(([initialFile, conflictEntryPresence]) => {
120+
outputMap.set(initialFile.path, {
121+
path: initialFile.path,
122+
conflicted: !!conflictEntryPresence,
123+
conflictHint: conflictEntryPresence ? conflictEntryHint(conflictEntryPresence) : undefined
128124
});
125+
});
129126
130-
$uncommitedFiles.forEach(([uncommitedFile]) => {
131-
const existingFile = initialFileMap.get(uncommitedFile.path);
132-
determineOutput: {
133-
if (existingFile) {
134-
const fileChanged = existingFile.hunks.some(
135-
(hunk) => !uncommitedFile.hunks.map((hunk) => hunk.diff).includes(hunk.diff)
136-
);
137-
138-
if (fileChanged) {
139-
// All initial entries should have been added to the map,
140-
// so we can safely assert that it will be present
141-
const outputFile = outputMap.get(uncommitedFile.path)!;
142-
if (outputFile.conflicted && outputFile.conflictEntryPresence) {
143-
outputFile.conflictState = getConflictState(
144-
uncommitedFile,
145-
outputFile.conflictEntryPresence
146-
);
147-
}
148-
149-
if (!outputFile.conflicted) {
150-
outputFile.status = 'M';
151-
}
152-
}
153-
break determineOutput;
154-
}
155-
156-
outputMap.set(uncommitedFile.path, {
157-
name: uncommitedFile.filename,
158-
path: uncommitedFile.path,
159-
conflicted: false,
160-
status: computeFileStatus(uncommitedFile)
161-
});
127+
uncommittedFiles.current.data.forEach((uncommitedFile) => {
128+
const outputFile = outputMap.get(uncommitedFile.path)!;
129+
if (outputFile) {
130+
// We don't want to set a status if the file is
131+
// conflicted because it will _always_ show up as
132+
// modified
133+
if (!outputFile.conflicted) {
134+
outputFile.status = computeChangeStatus(uncommitedFile);
162135
}
136+
return;
137+
}
138+
139+
outputMap.set(uncommitedFile.path, {
140+
path: uncommitedFile.path,
141+
conflicted: false,
142+
status: computeChangeStatus(uncommitedFile)
163143
});
164-
}
144+
});
165145
166146
const orderedOutput = Array.from(outputMap.values());
167147
orderedOutput.sort((a, b) => {
@@ -181,23 +161,34 @@
181161
const conflictedFiles = $derived(files.filter((file) => file.conflicted));
182162
183163
let manuallyResolvedFiles = new SvelteSet<string>();
164+
const filesWithConflictedStatues = $derived(
165+
conflictedFiles.map((f) => [f, isConflicted(f)] as [FileEntry, Readable<boolean>])
166+
);
184167
const stillConflictedFiles = $derived(
185-
conflictedFiles.filter(
186-
(file) => !manuallyResolvedFiles.has(file.path) && file.conflictState !== 'resolved'
187-
)
168+
filesWithConflictedStatues.filter(([_, status]) => fromStore(status).current).map(([f]) => f)
188169
);
189170
190-
function isConflicted(file: FileEntry): boolean {
191-
return (
192-
file.conflicted && file.conflictState !== 'resolved' && !manuallyResolvedFiles.has(file.path)
193-
);
171+
function isConflicted(fileEntry: FileEntry): Readable<boolean> {
172+
const file = readFromWorkspace(fileEntry.path, projectId);
173+
const conflictState = derived(file, (file) => {
174+
if (!isDefined(file?.data.content)) return 'unknown';
175+
const { conflictEntryPresence } = initialFileMap.get(fileEntry.path) || {};
176+
if (!conflictEntryPresence) return 'unknown';
177+
return getConflictState(conflictEntryPresence, file.data.content);
178+
});
179+
180+
const manuallyResolved = toStore(() => manuallyResolvedFiles.has(fileEntry.path));
181+
182+
return derived([conflictState, manuallyResolved], ([conflictState, manuallyResolved]) => {
183+
return fileEntry.conflicted && conflictState === 'conflicted' && !manuallyResolved;
184+
});
194185
}
195186
196187
async function abort() {
197188
modeServiceAborting = 'loading';
198189
199190
try {
200-
await modeService.abortEditAndReturnToWorkspace();
191+
await modeService.abortEditAndReturnToWorkspace({ projectId });
201192
modeServiceAborting = 'completed';
202193
} finally {
203194
modeServiceAborting = 'inert';
@@ -208,7 +199,7 @@
208199
modeServiceSaving = 'loading';
209200
210201
try {
211-
await modeService.saveEditAndReturnToWorkspace();
202+
await modeService.saveEditAndReturnToWorkspace({ projectId });
212203
modeServiceSaving = 'completed';
213204
} finally {
214205
modeServiceAborting = 'inert';
@@ -285,11 +276,13 @@
285276
}}
286277
>
287278
{#each files as file (file.path)}
279+
{@const conflictedStore = isConflicted(file)}
280+
{@const conflicted = fromStore(conflictedStore).current}
288281
<div class="file">
289282
<FileListItem
290283
filePath={file.path}
291284
fileStatus={file.status}
292-
conflicted={isConflicted(file)}
285+
{conflicted}
293286
onresolveclick={file.conflicted
294287
? () => manuallyResolvedFiles.add(file.path)
295288
: undefined}

apps/desktop/src/components/History.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125

126126
{#if entry.details}
127127
<SnapshotCard
128+
{projectId}
128129
isWithinRestore={withinRestoreItems.includes(entry.id)}
129130
{entry}
130131
onRestoreClick={() => {

apps/desktop/src/components/NotOnGitButlerBranch.svelte

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import Chrome from '$components/Chrome.svelte';
33
import DecorativeSplitView from '$components/DecorativeSplitView.svelte';
4+
import ReduxResult from '$components/ReduxResult.svelte';
45
import directionDoubtSvg from '$lib/assets/illustrations/direction-doubt.svg?raw';
56
import BaseBranchService from '$lib/baseBranch/baseBranchService.svelte';
67
import { ModeService } from '$lib/mode/modeService';
@@ -26,8 +27,7 @@
2627
const [setBaseBranchTarget, targetBranchSwitch] = baseBranchService.setTarget;
2728
2829
const modeService = getContext(ModeService);
29-
// TODO: On filesystem change this should be reloaded
30-
const mode = modeService.mode;
30+
const mode = $derived(modeService.mode({ projectId }));
3131
3232
const [worktreeService] = inject(WorktreeService);
3333
const changes = worktreeService.treeChanges(projectId);
@@ -41,8 +41,9 @@
4141
});
4242
}
4343
44-
let conflicts = $derived(
45-
$mode?.type === 'OutsideWorkspace' && $mode.subject?.worktreeConflicts.length > 0
44+
const conflicts = $derived(
45+
mode.current.data?.type === 'OutsideWorkspace' &&
46+
mode.current.data.subject.worktreeConflicts.length > 0
4647
);
4748
4849
let selectedHandlingOfUncommitted: OptionsType = $state('stash');
@@ -107,20 +108,24 @@
107108
Some files can’t be applied due to conflicts:
108109
</p>
109110
<div class="switchrepo__file-list">
110-
{#if $mode?.type === 'OutsideWorkspace'}
111-
{#each $mode.subject?.worktreeConflicts || [] as path}
112-
<FileListItem
113-
filePath={path}
114-
clickable={false}
115-
conflicted
116-
conflictHint="Resolve to apply"
117-
isLast={path ===
118-
$mode.subject?.worktreeConflicts[
119-
$mode.subject?.worktreeConflicts.length - 1
120-
]}
121-
/>
122-
{/each}
123-
{/if}
111+
<ReduxResult result={mode.current} {projectId}>
112+
{#snippet children(mode, _env)}
113+
{#if mode.type === 'OutsideWorkspace'}
114+
{#each mode.subject.worktreeConflicts || [] as path}
115+
<FileListItem
116+
filePath={path}
117+
clickable={false}
118+
conflicted
119+
conflictHint="Resolve to apply"
120+
isLast={path ===
121+
mode.subject.worktreeConflicts[
122+
mode.subject.worktreeConflicts.length - 1
123+
]}
124+
/>
125+
{/each}
126+
{/if}
127+
{/snippet}
128+
</ReduxResult>
124129
</div>
125130
{/if}
126131
</div>

apps/desktop/src/components/SnapshotCard.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
path: string;
2222
}
2323
| undefined;
24+
projectId: string;
2425
}
2526
2627
const {
28+
projectId,
2729
entry,
2830
isWithinRestore = true,
2931
selectedFile = undefined,
@@ -167,7 +169,7 @@
167169
const operation = mapOperation(entry.details);
168170
169171
const modeService = getContext(ModeService);
170-
const mode = modeService.mode;
172+
const mode = $derived(modeService.mode({ projectId }));
171173
</script>
172174

173175
<div
@@ -183,7 +185,7 @@
183185
onclick={() => {
184186
onRestoreClick();
185187
}}
186-
disabled={$mode?.type !== 'OpenWorkspace'}>Revert</Button
188+
disabled={mode.current.data?.type !== 'OpenWorkspace'}>Revert</Button
187189
>
188190
</div>
189191
<span class="snapshot-time text-11">

0 commit comments

Comments
 (0)