feat: version diffs work now
This commit is contained in:
@@ -19,6 +19,43 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.git-diff-commit-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--vscode-editorWidget-border);
|
||||
}
|
||||
|
||||
.git-diff-commit-label {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.git-diff-commit-select {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.git-diff-commit-button {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--vscode-button-border);
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.git-diff-commit-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.git-diff-message,
|
||||
.git-diff-error {
|
||||
padding: 12px;
|
||||
|
||||
@@ -3,6 +3,12 @@ import { DiffEditor } from '@monaco-editor/react';
|
||||
import { useAppStore } from '../../store';
|
||||
import './GitDiffView.css';
|
||||
|
||||
interface CommitFileDiff {
|
||||
filePath: string;
|
||||
original: string;
|
||||
modified: string;
|
||||
}
|
||||
|
||||
interface GitDiffViewProps {
|
||||
filePath: string;
|
||||
}
|
||||
@@ -35,9 +41,9 @@ function detectLanguage(filePath: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function toModelPath(filePath: string, side: 'original' | 'modified'): string {
|
||||
function toModelPath(filePath: string, side: 'original' | 'modified', scope: string): string {
|
||||
const normalized = filePath.replace(/^\/+/, '');
|
||||
return `inmemory://model/git-diff/${side}/${normalized}`;
|
||||
return `inmemory://model/git-diff/${scope}/${side}/${normalized}`;
|
||||
}
|
||||
|
||||
export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
||||
@@ -46,6 +52,40 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [original, setOriginal] = useState('');
|
||||
const [modified, setModified] = useState('');
|
||||
const [commitFiles, setCommitFiles] = useState<CommitFileDiff[]>([]);
|
||||
const [selectedCommitFilePath, setSelectedCommitFilePath] = useState<string>('');
|
||||
const isCommitDiff = filePath.startsWith('commit:');
|
||||
const commitHash = isCommitDiff ? filePath.slice('commit:'.length) : '';
|
||||
const selectedCommitFile = commitFiles.find((entry) => entry.filePath === selectedCommitFilePath) ?? null;
|
||||
const selectedCommitFileIndex = selectedCommitFilePath
|
||||
? commitFiles.findIndex((entry) => entry.filePath === selectedCommitFilePath)
|
||||
: -1;
|
||||
const canSelectPreviousFile = selectedCommitFileIndex > 0;
|
||||
const canSelectNextFile = selectedCommitFileIndex >= 0 && selectedCommitFileIndex < commitFiles.length - 1;
|
||||
const displayedOriginal = selectedCommitFile ? selectedCommitFile.original : original;
|
||||
const displayedModified = selectedCommitFile ? selectedCommitFile.modified : modified;
|
||||
const activeFilePath = selectedCommitFile ? selectedCommitFile.filePath : filePath;
|
||||
const modelScope = isCommitDiff ? `commit-${commitHash}` : 'working-tree';
|
||||
|
||||
const selectPreviousCommitFile = () => {
|
||||
if (!canSelectPreviousFile) {
|
||||
return;
|
||||
}
|
||||
const previousFile = commitFiles[selectedCommitFileIndex - 1];
|
||||
if (previousFile) {
|
||||
setSelectedCommitFilePath(previousFile.filePath);
|
||||
}
|
||||
};
|
||||
|
||||
const selectNextCommitFile = () => {
|
||||
if (!canSelectNextFile) {
|
||||
return;
|
||||
}
|
||||
const nextFile = commitFiles[selectedCommitFileIndex + 1];
|
||||
if (nextFile) {
|
||||
setSelectedCommitFilePath(nextFile.filePath);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadDiff = async () => {
|
||||
@@ -67,9 +107,27 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = await window.electronAPI.git.getDiffContent(projectPath, filePath);
|
||||
setOriginal(diff.original || '');
|
||||
setModified(diff.modified || '');
|
||||
if (isCommitDiff) {
|
||||
const diff = await window.electronAPI.git.getCommitDiffContent(projectPath, commitHash);
|
||||
const files = diff.files || [];
|
||||
setCommitFiles(files);
|
||||
|
||||
if (files.length > 0) {
|
||||
setSelectedCommitFilePath(files[0].filePath);
|
||||
setOriginal(files[0].original || '');
|
||||
setModified(files[0].modified || '');
|
||||
} else {
|
||||
setSelectedCommitFilePath('');
|
||||
setOriginal(diff.original || '');
|
||||
setModified(diff.modified || '');
|
||||
}
|
||||
} else {
|
||||
setCommitFiles([]);
|
||||
setSelectedCommitFilePath('');
|
||||
const diff = await window.electronAPI.git.getDiffContent(projectPath, filePath);
|
||||
setOriginal(diff.original || '');
|
||||
setModified(diff.modified || '');
|
||||
}
|
||||
} catch {
|
||||
setError('Failed to load diff.');
|
||||
} finally {
|
||||
@@ -78,12 +136,12 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
||||
};
|
||||
|
||||
void loadDiff();
|
||||
}, [activeProject, filePath]);
|
||||
}, [activeProject, filePath, isCommitDiff, commitHash]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="git-diff-view">
|
||||
<div className="git-diff-header">Diff: {filePath}</div>
|
||||
<div className="git-diff-header">Diff: {isCommitDiff ? `Commit ${commitHash}` : filePath}</div>
|
||||
<div className="git-diff-message">Loading diff...</div>
|
||||
</div>
|
||||
);
|
||||
@@ -92,7 +150,7 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="git-diff-view">
|
||||
<div className="git-diff-header">Diff: {filePath}</div>
|
||||
<div className="git-diff-header">Diff: {isCommitDiff ? `Commit ${commitHash}` : filePath}</div>
|
||||
<div className="git-diff-error">{error}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -100,16 +158,54 @@ export const GitDiffView: React.FC<GitDiffViewProps> = ({ filePath }) => {
|
||||
|
||||
return (
|
||||
<div className="git-diff-view">
|
||||
<div className="git-diff-header">Diff: {filePath}</div>
|
||||
<div className="git-diff-header">Diff: {isCommitDiff ? `Commit ${commitHash}` : filePath}</div>
|
||||
{isCommitDiff && commitFiles.length > 0 && (
|
||||
<div className="git-diff-commit-nav">
|
||||
<label htmlFor="git-diff-commit-files" className="git-diff-commit-label">
|
||||
Changed files
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="git-diff-commit-button"
|
||||
onClick={selectPreviousCommitFile}
|
||||
disabled={!canSelectPreviousFile}
|
||||
aria-label="Previous file"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<select
|
||||
id="git-diff-commit-files"
|
||||
className="git-diff-commit-select"
|
||||
value={selectedCommitFilePath}
|
||||
onChange={(event) => setSelectedCommitFilePath(event.target.value)}
|
||||
aria-label="Changed files"
|
||||
>
|
||||
{commitFiles.map((entry) => (
|
||||
<option key={entry.filePath} value={entry.filePath}>
|
||||
{entry.filePath}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="git-diff-commit-button"
|
||||
onClick={selectNextCommitFile}
|
||||
disabled={!canSelectNextFile}
|
||||
aria-label="Next file"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="git-diff-editor-wrap">
|
||||
<DiffEditor
|
||||
original={original}
|
||||
modified={modified}
|
||||
originalModelPath={toModelPath(filePath, 'original')}
|
||||
modifiedModelPath={toModelPath(filePath, 'modified')}
|
||||
original={displayedOriginal}
|
||||
modified={displayedModified}
|
||||
originalModelPath={toModelPath(activeFilePath, 'original', modelScope)}
|
||||
modifiedModelPath={toModelPath(activeFilePath, 'modified', modelScope)}
|
||||
keepCurrentOriginalModel
|
||||
keepCurrentModifiedModel
|
||||
language={detectLanguage(filePath)}
|
||||
language={detectLanguage(activeFilePath)}
|
||||
theme="vs-dark"
|
||||
height="100%"
|
||||
options={{
|
||||
|
||||
@@ -112,9 +112,18 @@
|
||||
}
|
||||
|
||||
.git-sidebar-history-item {
|
||||
width: 100%;
|
||||
border: none;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--vscode-input-background);
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.git-sidebar-history-item:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.git-sidebar-history-subject {
|
||||
|
||||
@@ -26,6 +26,7 @@ export const GitSidebar: React.FC = () => {
|
||||
const commitMessageInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const getDiffTabId = (filePath: string): string => `git-diff:${filePath}`;
|
||||
const getCommitDiffTabId = (commitHash: string): string => `git-diff:commit:${commitHash}`;
|
||||
|
||||
const getActionProgressMessage = (action: 'fetch' | 'pull' | 'push' | 'commit'): string => {
|
||||
if (action === 'push') {
|
||||
@@ -51,6 +52,17 @@ export const GitSidebar: React.FC = () => {
|
||||
[openTab],
|
||||
);
|
||||
|
||||
const openCommitDiffTab = useCallback(
|
||||
(commitHash: string, isTransient: boolean) => {
|
||||
openTab({
|
||||
type: 'git-diff',
|
||||
id: getCommitDiffTabId(commitHash),
|
||||
isTransient,
|
||||
});
|
||||
},
|
||||
[openTab],
|
||||
);
|
||||
|
||||
const resolveProjectPath = useCallback(async (): Promise<string | null> => {
|
||||
if (!activeProject) {
|
||||
return null;
|
||||
@@ -367,14 +379,21 @@ export const GitSidebar: React.FC = () => {
|
||||
) : (
|
||||
<div className="git-sidebar-history-list" role="list" aria-label="Version History">
|
||||
{historyEntries.map((entry) => (
|
||||
<div key={entry.hash} className="git-sidebar-history-item">
|
||||
<button
|
||||
key={entry.hash}
|
||||
type="button"
|
||||
className="git-sidebar-history-item"
|
||||
onClick={() => openCommitDiffTab(entry.hash, true)}
|
||||
onDoubleClick={() => openCommitDiffTab(entry.hash, false)}
|
||||
title={`${entry.shortHash}: ${entry.subject}`}
|
||||
>
|
||||
<div className="git-sidebar-history-subject">{entry.subject}</div>
|
||||
<div className="git-sidebar-history-meta">
|
||||
<span>{entry.shortHash}</span>
|
||||
<span>{entry.author}</span>
|
||||
<span>{new Date(entry.date).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,15 +4,36 @@ import './TabBar.css';
|
||||
|
||||
const MAX_CHAT_TITLE_LENGTH = 18;
|
||||
|
||||
function getGitDiffResource(tabId: string): string {
|
||||
return tabId.startsWith('git-diff:') ? tabId.slice('git-diff:'.length) : tabId;
|
||||
}
|
||||
|
||||
function getCommitHashFromGitDiffTabId(tabId: string): string | null {
|
||||
const resource = getGitDiffResource(tabId);
|
||||
if (!resource.startsWith('commit:')) {
|
||||
return null;
|
||||
}
|
||||
return resource.slice('commit:'.length);
|
||||
}
|
||||
|
||||
const getTabTitle = (
|
||||
tab: Tab,
|
||||
postTitles: Map<string, string>,
|
||||
media: { id: string; originalName: string }[],
|
||||
chatTitles: Map<string, string>,
|
||||
importDefTitles: Map<string, string>
|
||||
importDefTitles: Map<string, string>,
|
||||
commitTitles: Map<string, string>
|
||||
): string => {
|
||||
if (tab.type === 'git-diff') {
|
||||
const filePath = tab.id.startsWith('git-diff:') ? tab.id.slice('git-diff:'.length) : tab.id;
|
||||
const filePath = getGitDiffResource(tab.id);
|
||||
const commitHash = getCommitHashFromGitDiffTabId(tab.id);
|
||||
if (commitHash) {
|
||||
const commitTitle = commitTitles.get(commitHash);
|
||||
if (commitTitle) {
|
||||
return commitTitle;
|
||||
}
|
||||
return `Commit ${commitHash.slice(0, 7)}`;
|
||||
}
|
||||
const filename = filePath.split('/').pop();
|
||||
return filename || filePath;
|
||||
}
|
||||
@@ -138,6 +159,7 @@ export const TabBar: React.FC = () => {
|
||||
tabs,
|
||||
activeTabId,
|
||||
media,
|
||||
activeProject,
|
||||
dirtyPosts,
|
||||
sidebarVisible,
|
||||
toggleSidebar,
|
||||
@@ -152,6 +174,7 @@ export const TabBar: React.FC = () => {
|
||||
const [postTitles, setPostTitles] = useState<Map<string, string>>(new Map());
|
||||
const [chatTitles, setChatTitles] = useState<Map<string, string>>(new Map());
|
||||
const [importDefTitles, setImportDefTitles] = useState<Map<string, string>>(new Map());
|
||||
const [commitTitles, setCommitTitles] = useState<Map<string, string>>(new Map());
|
||||
|
||||
// Fetch post titles from database for post tabs
|
||||
useEffect(() => {
|
||||
@@ -289,6 +312,65 @@ export const TabBar: React.FC = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch commit subjects for commit-based git-diff tabs
|
||||
useEffect(() => {
|
||||
const commitHashes = tabs
|
||||
.filter((tab) => tab.type === 'git-diff')
|
||||
.map((tab) => getCommitHashFromGitDiffTabId(tab.id))
|
||||
.filter((hash): hash is string => Boolean(hash));
|
||||
|
||||
if (commitHashes.length === 0 || !activeProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
const missingHashes = commitHashes.filter((hash) => !commitTitles.has(hash));
|
||||
if (missingHashes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
const fetchCommitTitles = async () => {
|
||||
try {
|
||||
const projectPath = activeProject.dataPath
|
||||
? activeProject.dataPath
|
||||
: await window.electronAPI?.app.getDefaultProjectPath(activeProject.id);
|
||||
|
||||
if (!projectPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const history = await window.electronAPI?.git.getHistory(projectPath, 200);
|
||||
if (!history || cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCommitTitles((previous) => {
|
||||
const updated = new Map(previous);
|
||||
let changed = false;
|
||||
|
||||
for (const hash of missingHashes) {
|
||||
const match = history.find((entry) => entry.hash === hash);
|
||||
if (match) {
|
||||
updated.set(hash, `${match.shortHash} ${match.subject}`);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed ? updated : previous;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch commit titles:', error);
|
||||
}
|
||||
};
|
||||
|
||||
void fetchCommitTitles();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [tabs, activeProject]);
|
||||
|
||||
// Check if arrows are needed based on scroll position
|
||||
const updateArrowVisibility = useCallback(() => {
|
||||
const container = tabsContainerRef.current;
|
||||
@@ -419,7 +501,7 @@ export const TabBar: React.FC = () => {
|
||||
{tabs.map((tab) => {
|
||||
const isActive = tab.id === activeTabId;
|
||||
const isDirty = tab.type === 'post' && dirtyPosts.has(tab.id);
|
||||
const title = getTabTitle(tab, postTitles, media, chatTitles, importDefTitles);
|
||||
const title = getTabTitle(tab, postTitles, media, chatTitles, importDefTitles, commitTitles);
|
||||
const icon = getTabIcon(tab);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user