feat: Redesign git changes to split stage/unstaged files. (#1359)
* feat: Redesign git changes to split stage/unstaged files. Signed-off-by: Paolo Insogna <paolo@cowtech.it> * fixup Signed-off-by: Paolo Insogna <paolo@cowtech.it> * fixup Signed-off-by: Paolo Insogna <paolo@cowtech.it> * refactor: streamline git changes panel * fix: label staged and working diff tabs * fix: isolate staged and working diff files * fix: scope staged and working diff updates * fix: scope git row revert to working changes --------- Signed-off-by: Paolo Insogna <paolo@cowtech.it> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
9af0de0056
commit
e16097b05d
@@ -9,6 +9,7 @@ type Deferred<T> = {
|
||||
};
|
||||
|
||||
type GitAPI = Parameters<ReturnType<typeof useGitStore.getState>['fetchStatus']>[1];
|
||||
type DirectoryGitState = NonNullable<ReturnType<ReturnType<typeof useGitStore.getState>['getDirectoryState']>>;
|
||||
|
||||
const createDeferred = <T>(): Deferred<T> => {
|
||||
let resolve!: (value: T) => void;
|
||||
@@ -20,16 +21,44 @@ const createDeferred = <T>(): Deferred<T> => {
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
const createStatus = (diffStats?: GitStatus['diffStats']): GitStatus => ({
|
||||
const createStatus = (diffStats?: GitStatus['diffStats'], files: GitStatus['files'] = []): GitStatus => ({
|
||||
current: 'main',
|
||||
tracking: null,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
files: [],
|
||||
isClean: true,
|
||||
files,
|
||||
isClean: files.length === 0,
|
||||
diffStats,
|
||||
});
|
||||
|
||||
const createDirectoryState = (status: GitStatus): DirectoryGitState => ({
|
||||
isGitRepo: true,
|
||||
status,
|
||||
branches: null,
|
||||
log: null,
|
||||
identity: null,
|
||||
diffCache: new Map(),
|
||||
indexRevision: 0,
|
||||
lastRepoCheckAt: 0,
|
||||
lastStatusFetch: 0,
|
||||
lastStatusChange: 0,
|
||||
lastLogFetch: 0,
|
||||
lastBranchesFetch: 0,
|
||||
lastIdentityFetch: 0,
|
||||
logMaxCount: 25,
|
||||
isLoadingStatus: false,
|
||||
isLoadingLog: false,
|
||||
isLoadingBranches: false,
|
||||
isLoadingIdentity: false,
|
||||
});
|
||||
|
||||
const setDirectoryStatus = (status: GitStatus) => {
|
||||
useGitStore.setState({
|
||||
directories: new Map([['/repo', createDirectoryState(status)]]),
|
||||
activeDirectory: '/repo',
|
||||
});
|
||||
};
|
||||
|
||||
const createGitApi = (getGitStatus: GitAPI['getGitStatus']): GitAPI => ({
|
||||
checkIsGitRepository: async () => true,
|
||||
getGitStatus,
|
||||
@@ -91,4 +120,134 @@ describe('useGitStore', () => {
|
||||
const [fullResult, lightResult] = await Promise.all([fullPromise, lightPromise]);
|
||||
expect(lightResult).toBe(fullResult);
|
||||
});
|
||||
|
||||
test('optimistically stages modified files and preserves untouched file references', () => {
|
||||
const target = { path: 'src/index.ts', index: ' ', working_dir: 'M' };
|
||||
const untouched = { path: 'README.md', index: ' ', working_dir: 'M' };
|
||||
const initialStatus = createStatus(undefined, [target, untouched]);
|
||||
setDirectoryStatus(initialStatus);
|
||||
|
||||
const previousStatus = useGitStore.getState().moveStatusPathsOptimistically('/repo', ['src/index.ts'], 'stage');
|
||||
const status = useGitStore.getState().getDirectoryState('/repo')?.status;
|
||||
const state = useGitStore.getState().getDirectoryState('/repo');
|
||||
|
||||
expect(previousStatus).toBe(initialStatus);
|
||||
expect(status?.files).toEqual([
|
||||
{ path: 'src/index.ts', index: 'M', working_dir: ' ' },
|
||||
untouched,
|
||||
]);
|
||||
expect(status?.files[1]).toBe(untouched);
|
||||
expect(state?.indexRevision).toBe(1);
|
||||
});
|
||||
|
||||
test('optimistically stages untracked files as added files', () => {
|
||||
setDirectoryStatus(createStatus(undefined, [
|
||||
{ path: 'new-file.ts', index: '?', working_dir: '?' },
|
||||
]));
|
||||
|
||||
useGitStore.getState().moveStatusPathsOptimistically('/repo', ['new-file.ts'], 'stage');
|
||||
const status = useGitStore.getState().getDirectoryState('/repo')?.status;
|
||||
|
||||
expect(status?.files).toEqual([
|
||||
{ path: 'new-file.ts', index: 'A', working_dir: ' ' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('optimistically unstages staged files', () => {
|
||||
setDirectoryStatus(createStatus(undefined, [
|
||||
{ path: 'src/index.ts', index: 'M', working_dir: ' ' },
|
||||
]));
|
||||
|
||||
useGitStore.getState().moveStatusPathsOptimistically('/repo', ['src/index.ts'], 'unstage');
|
||||
const status = useGitStore.getState().getDirectoryState('/repo')?.status;
|
||||
|
||||
expect(status?.files).toEqual([
|
||||
{ path: 'src/index.ts', index: ' ', working_dir: 'M' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('optimistically unstages staged added files back to untracked files', () => {
|
||||
setDirectoryStatus(createStatus(undefined, [
|
||||
{ path: 'new-file.ts', index: 'A', working_dir: ' ' },
|
||||
]));
|
||||
|
||||
useGitStore.getState().moveStatusPathsOptimistically('/repo', ['new-file.ts'], 'unstage');
|
||||
const status = useGitStore.getState().getDirectoryState('/repo')?.status;
|
||||
|
||||
expect(status?.files).toEqual([
|
||||
{ path: 'new-file.ts', index: ' ', working_dir: '?' },
|
||||
]);
|
||||
});
|
||||
|
||||
test('keeps conflicted files unchanged during optimistic moves', () => {
|
||||
const conflicted = { path: 'conflict.ts', index: 'U', working_dir: 'U' };
|
||||
setDirectoryStatus(createStatus(undefined, [conflicted]));
|
||||
|
||||
useGitStore.getState().moveStatusPathsOptimistically('/repo', ['conflict.ts'], 'stage');
|
||||
const status = useGitStore.getState().getDirectoryState('/repo')?.status;
|
||||
|
||||
expect(status?.files).toEqual([conflicted]);
|
||||
expect(status?.files[0]).toBe(conflicted);
|
||||
});
|
||||
|
||||
test('preserves diff stats during optimistic moves', () => {
|
||||
const diffStats = { 'src/index.ts': { insertions: 2, deletions: 1 } };
|
||||
setDirectoryStatus(createStatus(diffStats, [
|
||||
{ path: 'src/index.ts', index: ' ', working_dir: 'M' },
|
||||
]));
|
||||
|
||||
useGitStore.getState().moveStatusPathsOptimistically('/repo', ['src/index.ts'], 'stage');
|
||||
const status = useGitStore.getState().getDirectoryState('/repo')?.status;
|
||||
|
||||
expect(status?.diffStats).toBe(diffStats);
|
||||
});
|
||||
|
||||
test('does nothing when optimistic move has no matching path', () => {
|
||||
const initialStatus = createStatus(undefined, [
|
||||
{ path: 'src/index.ts', index: ' ', working_dir: 'M' },
|
||||
]);
|
||||
setDirectoryStatus(initialStatus);
|
||||
|
||||
const previousStatus = useGitStore.getState().moveStatusPathsOptimistically('/repo', ['missing.ts'], 'stage');
|
||||
|
||||
expect(previousStatus).toBe(initialStatus);
|
||||
expect(useGitStore.getState().getDirectoryState('/repo')?.status).toBe(initialStatus);
|
||||
expect(useGitStore.getState().getDirectoryState('/repo')?.indexRevision).toBe(0);
|
||||
});
|
||||
|
||||
test('does nothing without status for optimistic moves', () => {
|
||||
useGitStore.setState({
|
||||
directories: new Map([['/repo', { ...createDirectoryState(createStatus()), status: null }]]),
|
||||
activeDirectory: '/repo',
|
||||
});
|
||||
|
||||
const previousStatus = useGitStore.getState().moveStatusPathsOptimistically('/repo', ['src/index.ts'], 'stage');
|
||||
|
||||
expect(previousStatus).toBeNull();
|
||||
expect(useGitStore.getState().getDirectoryState('/repo')?.status).toBeNull();
|
||||
});
|
||||
|
||||
test('removes entries that become clean during optimistic moves', () => {
|
||||
setDirectoryStatus(createStatus(undefined, [
|
||||
{ path: 'clean.ts', index: ' ', working_dir: ' ' },
|
||||
]));
|
||||
|
||||
useGitStore.getState().moveStatusPathsOptimistically('/repo', ['clean.ts'], 'stage');
|
||||
const status = useGitStore.getState().getDirectoryState('/repo')?.status;
|
||||
|
||||
expect(status?.files).toEqual([]);
|
||||
expect(status?.isClean).toBe(true);
|
||||
});
|
||||
|
||||
test('restores previous status for optimistic rollback', () => {
|
||||
const initialStatus = createStatus(undefined, [
|
||||
{ path: 'src/index.ts', index: ' ', working_dir: 'M' },
|
||||
]);
|
||||
setDirectoryStatus(initialStatus);
|
||||
|
||||
const previousStatus = useGitStore.getState().moveStatusPathsOptimistically('/repo', ['src/index.ts'], 'stage');
|
||||
useGitStore.getState().restoreStatus('/repo', previousStatus);
|
||||
|
||||
expect(useGitStore.getState().getDirectoryState('/repo')?.status).toBe(initialStatus);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ interface DirectoryGitState {
|
||||
log: GitLogResponse | null;
|
||||
identity: GitIdentitySummary | null;
|
||||
diffCache: Map<string, { original: string; modified: string; fetchedAt: number; isBinary?: boolean }>;
|
||||
indexRevision: number;
|
||||
lastRepoCheckAt: number;
|
||||
lastStatusFetch: number;
|
||||
lastStatusChange: number;
|
||||
@@ -61,6 +62,9 @@ interface GitStore {
|
||||
|
||||
ensureStatus: (directory: string, git: GitAPI) => Promise<void>;
|
||||
ensureAll: (directory: string, git: GitAPI) => Promise<void>;
|
||||
moveStatusPathsOptimistically: (directory: string, paths: string[], direction: 'stage' | 'unstage') => GitStatus | null;
|
||||
restoreStatus: (directory: string, status: GitStatus | null) => void;
|
||||
bumpIndexRevision: (directory: string) => void;
|
||||
|
||||
getDiff: (directory: string, filePath: string) => { original: string; modified: string; fetchedAt: number; isBinary?: boolean } | null;
|
||||
setDiff: (directory: string, filePath: string, diff: { original: string; modified: string; isBinary?: boolean }) => void;
|
||||
@@ -122,6 +126,7 @@ const createEmptyDirectoryState = (): DirectoryGitState => ({
|
||||
log: null,
|
||||
identity: null,
|
||||
diffCache: new Map(),
|
||||
indexRevision: 0,
|
||||
lastRepoCheckAt: 0,
|
||||
lastStatusFetch: 0,
|
||||
lastStatusChange: 0,
|
||||
@@ -278,6 +283,72 @@ const getChangedFilePaths = (oldStatus: GitStatus | null, newStatus: GitStatus |
|
||||
return changed;
|
||||
};
|
||||
|
||||
const hasIndexStatusChanged = (oldStatus: GitStatus | null, newStatus: GitStatus | null): boolean => {
|
||||
if (!oldStatus && !newStatus) return false;
|
||||
if (!oldStatus || !newStatus) return true;
|
||||
|
||||
const oldFiles = oldStatus.files ?? [];
|
||||
const newFiles = newStatus.files ?? [];
|
||||
const normalizeIndexStatus = (value?: string | null): string => {
|
||||
const trimmed = value?.trim() ?? '';
|
||||
return trimmed === '?' ? '' : trimmed;
|
||||
};
|
||||
|
||||
const oldIndexByPath = new Map(oldFiles.map((file) => [file.path, normalizeIndexStatus(file.index)] as const));
|
||||
const newIndexByPath = new Map(newFiles.map((file) => [file.path, normalizeIndexStatus(file.index)] as const));
|
||||
const paths = new Set<string>([...oldIndexByPath.keys(), ...newIndexByPath.keys()]);
|
||||
|
||||
for (const path of paths) {
|
||||
if ((oldIndexByPath.get(path) ?? '') !== (newIndexByPath.get(path) ?? '')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isBlankStatusCode = (value?: string | null): boolean => !value || value.trim().length === 0;
|
||||
const isConflictStatusCode = (value?: string | null): boolean => (value || '').trim() === 'U';
|
||||
|
||||
const toStagedStatusFile = (file: GitStatus['files'][number]): GitStatus['files'][number] => {
|
||||
const index = (file.index || '').trim();
|
||||
const workingDir = (file.working_dir || '').trim();
|
||||
|
||||
if (isConflictStatusCode(index) || isConflictStatusCode(workingDir)) {
|
||||
return file;
|
||||
}
|
||||
|
||||
const nextIndex = index === '?' || workingDir === '?'
|
||||
? 'A'
|
||||
: index || workingDir || ' ';
|
||||
|
||||
return {
|
||||
...file,
|
||||
index: nextIndex,
|
||||
working_dir: ' ',
|
||||
};
|
||||
};
|
||||
|
||||
const toUnstagedStatusFile = (file: GitStatus['files'][number]): GitStatus['files'][number] => {
|
||||
const index = (file.index || '').trim();
|
||||
const workingDir = (file.working_dir || '').trim();
|
||||
|
||||
if (isConflictStatusCode(index) || isConflictStatusCode(workingDir)) {
|
||||
return file;
|
||||
}
|
||||
|
||||
const nextWorkingDir = workingDir || (index === 'A' || index === '?' ? '?' : index) || ' ';
|
||||
|
||||
return {
|
||||
...file,
|
||||
index: ' ',
|
||||
working_dir: nextWorkingDir,
|
||||
};
|
||||
};
|
||||
|
||||
const isCleanStatusFile = (file: GitStatus['files'][number]): boolean =>
|
||||
isBlankStatusCode(file.index) && isBlankStatusCode(file.working_dir);
|
||||
|
||||
export const useGitStore = create<GitStore>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
@@ -369,6 +440,7 @@ export const useGitStore = create<GitStore>()(
|
||||
const currentDirState = newDirectories.get(directory) ?? createEmptyDirectoryState();
|
||||
|
||||
const changedPaths = getChangedFilePaths(currentDirState.status, newStatus);
|
||||
const indexStatusChanged = hasIndexStatusChanged(currentDirState.status, newStatus);
|
||||
|
||||
const oldPaths = new Set((currentDirState.status?.files ?? []).map((f) => f.path));
|
||||
const newPaths = new Set((newStatus.files ?? []).map((f) => f.path));
|
||||
@@ -402,6 +474,7 @@ export const useGitStore = create<GitStore>()(
|
||||
isGitRepo: true,
|
||||
status: mergedStatus,
|
||||
diffCache: nextDiffCache,
|
||||
indexRevision: indexStatusChanged ? currentDirState.indexRevision + 1 : currentDirState.indexRevision,
|
||||
lastRepoCheckAt: shouldProbeRepository ? now : currentDirState.lastRepoCheckAt,
|
||||
lastStatusFetch: Date.now(),
|
||||
lastStatusChange: hasFileContentChange ? Date.now() : currentDirState.lastStatusChange,
|
||||
@@ -445,6 +518,95 @@ export const useGitStore = create<GitStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
moveStatusPathsOptimistically: (directory, paths, direction) => {
|
||||
const normalizedPaths = new Set(paths.map((path) => path.trim()).filter(Boolean));
|
||||
if (normalizedPaths.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { directories } = get();
|
||||
const dirState = directories.get(directory);
|
||||
const previousStatus = dirState?.status ?? null;
|
||||
if (!dirState || !previousStatus) {
|
||||
return previousStatus;
|
||||
}
|
||||
|
||||
let didChange = false;
|
||||
const nextFiles: GitStatus['files'] = [];
|
||||
|
||||
for (const file of previousStatus.files) {
|
||||
if (!normalizedPaths.has(file.path)) {
|
||||
nextFiles.push(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextFile = direction === 'stage'
|
||||
? toStagedStatusFile(file)
|
||||
: toUnstagedStatusFile(file);
|
||||
|
||||
if (nextFile !== file) {
|
||||
didChange = true;
|
||||
}
|
||||
|
||||
if (!isCleanStatusFile(nextFile)) {
|
||||
nextFiles.push(nextFile);
|
||||
} else {
|
||||
didChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!didChange) {
|
||||
return previousStatus;
|
||||
}
|
||||
|
||||
const nextDirectories = new Map(directories);
|
||||
nextDirectories.set(directory, {
|
||||
...dirState,
|
||||
status: {
|
||||
...previousStatus,
|
||||
files: nextFiles,
|
||||
isClean: nextFiles.length === 0,
|
||||
},
|
||||
indexRevision: dirState.indexRevision + 1,
|
||||
lastStatusChange: Date.now(),
|
||||
});
|
||||
set({ directories: nextDirectories });
|
||||
|
||||
return previousStatus;
|
||||
},
|
||||
|
||||
restoreStatus: (directory, status) => {
|
||||
const { directories } = get();
|
||||
const dirState = directories.get(directory);
|
||||
if (!dirState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextDirectories = new Map(directories);
|
||||
nextDirectories.set(directory, {
|
||||
...dirState,
|
||||
status,
|
||||
indexRevision: dirState.indexRevision + 1,
|
||||
lastStatusChange: Date.now(),
|
||||
});
|
||||
set({ directories: nextDirectories });
|
||||
},
|
||||
|
||||
bumpIndexRevision: (directory) => {
|
||||
const { directories } = get();
|
||||
const dirState = directories.get(directory);
|
||||
if (!dirState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextDirectories = new Map(directories);
|
||||
nextDirectories.set(directory, {
|
||||
...dirState,
|
||||
indexRevision: dirState.indexRevision + 1,
|
||||
});
|
||||
set({ directories: nextDirectories });
|
||||
},
|
||||
|
||||
fetchBranches: async (directory, git) => {
|
||||
{
|
||||
const newDirectories = new Map(get().directories);
|
||||
|
||||
@@ -25,6 +25,7 @@ type ContextPanelTab = {
|
||||
dedupeKey: string;
|
||||
label: string | null;
|
||||
readOnly: boolean;
|
||||
stagedDiff: boolean;
|
||||
touchedAt: number;
|
||||
};
|
||||
|
||||
@@ -34,6 +35,7 @@ type ContextPanelTabDescriptor = {
|
||||
dedupeKey?: string | null;
|
||||
label?: string | null;
|
||||
readOnly?: boolean;
|
||||
stagedDiff?: boolean;
|
||||
};
|
||||
|
||||
type ContextPanelDirectoryState = {
|
||||
@@ -204,6 +206,7 @@ const createContextPanelTab = (descriptor: ContextPanelTabDescriptor): ContextPa
|
||||
dedupeKey,
|
||||
label: normalizeContextTabLabel(descriptor.label),
|
||||
readOnly: descriptor.readOnly === true,
|
||||
stagedDiff: descriptor.stagedDiff === true,
|
||||
touchedAt: Date.now(),
|
||||
};
|
||||
};
|
||||
@@ -243,6 +246,7 @@ const sanitizeContextPanelTabs = (tabs: unknown): ContextPanelTab[] => {
|
||||
dedupeKey?: unknown;
|
||||
label?: unknown;
|
||||
readOnly?: unknown;
|
||||
stagedDiff?: unknown;
|
||||
touchedAt?: unknown;
|
||||
};
|
||||
|
||||
@@ -269,6 +273,7 @@ const sanitizeContextPanelTabs = (tabs: unknown): ContextPanelTab[] => {
|
||||
dedupeKey,
|
||||
label: normalizeContextTabLabel(typeof candidate.label === 'string' ? candidate.label : null),
|
||||
readOnly: candidate.readOnly === true,
|
||||
stagedDiff: candidate.stagedDiff === true,
|
||||
touchedAt: typeof candidate.touchedAt === 'number' && Number.isFinite(candidate.touchedAt)
|
||||
? candidate.touchedAt
|
||||
: Date.now(),
|
||||
@@ -327,6 +332,7 @@ const upsertContextPanelTab = (
|
||||
targetPath: nextTab.targetPath,
|
||||
dedupeKey: nextTab.dedupeKey,
|
||||
label: nextTab.label,
|
||||
stagedDiff: nextTab.stagedDiff,
|
||||
touchedAt: Date.now(),
|
||||
}
|
||||
: tab));
|
||||
@@ -504,6 +510,7 @@ interface UIStore {
|
||||
mainTabGuard: MainTabGuard | null;
|
||||
sidebarOpenBeforeFullscreenTab: boolean | null;
|
||||
pendingDiffFile: string | null;
|
||||
pendingDiffStaged: boolean;
|
||||
pendingFileNavigation: PendingFileNavigation | null;
|
||||
pendingFileFocusPath: string | null;
|
||||
isMobile: boolean;
|
||||
@@ -611,7 +618,7 @@ interface UIStore {
|
||||
setRightSidebarWidth: (width: number) => void;
|
||||
setRightSidebarTab: (tab: RightSidebarTab) => void;
|
||||
openContextPanelTab: (directory: string, tab: ContextPanelTabDescriptor) => void;
|
||||
openContextDiff: (directory: string, filePath: string) => void;
|
||||
openContextDiff: (directory: string, filePath: string, staged?: boolean) => void;
|
||||
openContextFile: (directory: string, filePath: string) => void;
|
||||
openContextFileAtLine: (directory: string, filePath: string, line: number, column?: number) => void;
|
||||
openContextOverview: (directory: string) => void;
|
||||
@@ -635,10 +642,10 @@ interface UIStore {
|
||||
setSessionDropdownOpen: (open: boolean) => void;
|
||||
setActiveMainTab: (tab: MainTab) => void;
|
||||
setMainTabGuard: (guard: MainTabGuard | null) => void;
|
||||
setPendingDiffFile: (filePath: string | null) => void;
|
||||
setPendingDiffFile: (filePath: string | null, staged?: boolean) => void;
|
||||
setPendingFileNavigation: (navigation: PendingFileNavigation | null) => void;
|
||||
setPendingFileFocusPath: (path: string | null) => void;
|
||||
navigateToDiff: (filePath: string) => void;
|
||||
navigateToDiff: (filePath: string, staged?: boolean) => void;
|
||||
consumePendingDiffFile: () => string | null;
|
||||
setIsMobile: (isMobile: boolean) => void;
|
||||
toggleCommandPalette: () => void;
|
||||
@@ -772,6 +779,7 @@ export const useUIStore = create<UIStore>()(
|
||||
mainTabGuard: null,
|
||||
sidebarOpenBeforeFullscreenTab: null,
|
||||
pendingDiffFile: null,
|
||||
pendingDiffStaged: false,
|
||||
pendingFileNavigation: null,
|
||||
pendingFileFocusPath: null,
|
||||
isMobile: false,
|
||||
@@ -975,15 +983,19 @@ export const useUIStore = create<UIStore>()(
|
||||
});
|
||||
},
|
||||
|
||||
openContextDiff: (directory, filePath) => {
|
||||
openContextDiff: (directory, filePath, staged = false) => {
|
||||
const normalizedDirectory = normalizeDirectoryPath((directory || '').trim());
|
||||
const normalizedFilePath = (filePath || '').trim();
|
||||
if (!normalizedDirectory || !normalizedFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
get().openContextPanelTab(normalizedDirectory, { mode: 'diff', targetPath: normalizedFilePath });
|
||||
get().setPendingDiffFile(normalizedFilePath);
|
||||
get().openContextPanelTab(normalizedDirectory, {
|
||||
mode: 'diff',
|
||||
targetPath: normalizedFilePath,
|
||||
dedupeKey: staged ? 'staged' : null,
|
||||
stagedDiff: staged,
|
||||
});
|
||||
},
|
||||
|
||||
openContextFile: (directory, filePath) => {
|
||||
@@ -1334,8 +1346,8 @@ export const useUIStore = create<UIStore>()(
|
||||
set({ activeMainTab: tab });
|
||||
},
|
||||
|
||||
setPendingDiffFile: (filePath) => {
|
||||
set({ pendingDiffFile: filePath });
|
||||
setPendingDiffFile: (filePath, staged = false) => {
|
||||
set({ pendingDiffFile: filePath, pendingDiffStaged: filePath ? staged : false });
|
||||
},
|
||||
|
||||
setPendingFileNavigation: (navigation) => {
|
||||
@@ -1346,18 +1358,18 @@ export const useUIStore = create<UIStore>()(
|
||||
set({ pendingFileFocusPath: path });
|
||||
},
|
||||
|
||||
navigateToDiff: (filePath) => {
|
||||
navigateToDiff: (filePath, staged = false) => {
|
||||
const guard = get().mainTabGuard;
|
||||
if (guard && !guard('diff')) {
|
||||
return;
|
||||
}
|
||||
set({ pendingDiffFile: filePath, activeMainTab: 'diff' });
|
||||
set({ pendingDiffFile: filePath, pendingDiffStaged: staged, activeMainTab: 'diff' });
|
||||
},
|
||||
|
||||
consumePendingDiffFile: () => {
|
||||
const { pendingDiffFile } = get();
|
||||
if (pendingDiffFile) {
|
||||
set({ pendingDiffFile: null });
|
||||
set({ pendingDiffFile: null, pendingDiffStaged: false });
|
||||
}
|
||||
return pendingDiffFile;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user