Diff view for a file chosen in the commit preview: one column, side by side or inline, with changed words marked
This commit is contained in:
+117
-5
@@ -11,7 +11,9 @@ import { layout } from "./lib/layout";
|
||||
import { textWidth } from "./lib/measure";
|
||||
import { colorOf } from "./lib/palette";
|
||||
import { addRecent } from "./lib/recents";
|
||||
import type { CommitDetails, FilterState, Ref, RefKind, RepoData } from "./lib/types";
|
||||
import type { FileDiff, Layout } from "./lib/diff";
|
||||
import type { ChangedFile, CommitDetails, FilterState, Ref, RefKind, RepoData } from "./lib/types";
|
||||
import DiffView from "./components/DiffView.vue";
|
||||
import GraphCell from "./components/GraphCell.vue";
|
||||
import LabelStack from "./components/LabelStack.vue";
|
||||
import PreviewPanel from "./components/PreviewPanel.vue";
|
||||
@@ -35,15 +37,17 @@ const sidebar = ref<InstanceType<typeof Sidebar>>();
|
||||
const SIDEBAR_MIN = 160;
|
||||
const SIDEBAR_MAX = 700;
|
||||
const PREVIEW_MIN = 80;
|
||||
const LAYOUTS: Layout[] = ["unified", "split", "inline"];
|
||||
|
||||
interface Settings {
|
||||
sidebarWidth: number;
|
||||
previewHeight: number;
|
||||
previewShown: boolean;
|
||||
diffLayout: Layout;
|
||||
/** Repository roots, most recently opened first. */
|
||||
recents: string[];
|
||||
}
|
||||
const settings = reactive<Settings>({ sidebarWidth: 270, previewHeight: 220, previewShown: true, recents: [] });
|
||||
const settings = reactive<Settings>({ sidebarWidth: 270, previewHeight: 220, previewShown: true, diffLayout: "unified", recents: [] });
|
||||
let settingsLoaded = false;
|
||||
|
||||
async function loadSettings() {
|
||||
@@ -53,6 +57,7 @@ async function loadSettings() {
|
||||
}
|
||||
if (typeof saved?.previewHeight === "number") settings.previewHeight = Math.max(PREVIEW_MIN, saved.previewHeight);
|
||||
if (typeof saved?.previewShown === "boolean") settings.previewShown = saved.previewShown;
|
||||
if (saved?.diffLayout && LAYOUTS.includes(saved.diffLayout)) settings.diffLayout = saved.diffLayout;
|
||||
if (Array.isArray(saved?.recents)) {
|
||||
// A repository opened before the settings arrived is already at the front.
|
||||
settings.recents = saved.recents.reduceRight((list, p) => (list.includes(p) ? list : [...list, p]), settings.recents);
|
||||
@@ -140,6 +145,7 @@ async function applyRepo(data: RepoData) {
|
||||
toggled.value = new Set();
|
||||
selected.value = 0;
|
||||
detailsCache.clear();
|
||||
diffCache.clear();
|
||||
if (scroller.value) scroller.value.scrollTop = 0;
|
||||
settings.recents = addRecent(settings.recents, data.root);
|
||||
await getCurrentWindow().setTitle(`gites — ${data.root}`);
|
||||
@@ -269,6 +275,67 @@ watch([selectedHash, () => settings.previewShown], ([hash, shown]) => {
|
||||
}, 120);
|
||||
});
|
||||
|
||||
// ---- diff of one file, shown in place of the commit graph ---------------------
|
||||
|
||||
const diffCache = new Map<string, FileDiff>();
|
||||
const diffFile = shallowRef<ChangedFile | null>(null);
|
||||
const diffData = shallowRef<FileDiff | null>(null);
|
||||
const diffError = ref<string | null>(null);
|
||||
const diffView = ref<InstanceType<typeof DiffView>>();
|
||||
|
||||
async function openDiff(file: ChangedFile, force = false) {
|
||||
const hash = details.value?.hash;
|
||||
if (!hash) return;
|
||||
diffFile.value = file;
|
||||
diffError.value = null;
|
||||
const key = `${hash}\0${file.path}`;
|
||||
const cached = diffCache.get(key);
|
||||
if (cached && !force) {
|
||||
diffData.value = cached;
|
||||
return;
|
||||
}
|
||||
diffData.value = null;
|
||||
try {
|
||||
const d = await invoke<FileDiff>("file_diff", { hash, path: file.path, oldPath: file.old_path, force });
|
||||
diffCache.set(key, d);
|
||||
if (diffFile.value === file && details.value?.hash === hash) diffData.value = d;
|
||||
} catch (e) {
|
||||
if (diffFile.value === file) diffError.value = String(e);
|
||||
}
|
||||
}
|
||||
|
||||
function closeDiff() {
|
||||
diffFile.value = null;
|
||||
nextTick(scrollToSelected);
|
||||
}
|
||||
|
||||
/** Opens the diff of the next (1) or previous (-1) file of the commit. */
|
||||
function stepFile(dir: 1 | -1) {
|
||||
const files = details.value?.files ?? [];
|
||||
const i = files.findIndex((f) => f.path === diffFile.value?.path);
|
||||
const next = files[i + dir];
|
||||
if (next) openDiff(next);
|
||||
else showToast(dir > 0 ? "this is the last file" : "this is the first file");
|
||||
}
|
||||
|
||||
/** Opens the diff of the selected commit's first file. */
|
||||
function openFirstFile() {
|
||||
if (!settings.previewShown) return showToast("show the preview panel first (p)");
|
||||
const d = details.value;
|
||||
if (!d || d.hash !== selectedHash.value) return;
|
||||
if (!d.files.length) return showToast("this commit changed no files");
|
||||
openDiff(d.files[0]);
|
||||
}
|
||||
|
||||
function cycleLayout() {
|
||||
settings.diffLayout = LAYOUTS[(LAYOUTS.indexOf(settings.diffLayout) + 1) % LAYOUTS.length];
|
||||
}
|
||||
|
||||
// Another commit (from a parent link, or a reload) closes the diff.
|
||||
watch(selectedHash, () => {
|
||||
if (diffFile.value) closeDiff();
|
||||
});
|
||||
|
||||
const selectedLabels = computed(() => (selectedHash.value ? labels.value.get(selectedHash.value) ?? [] : []));
|
||||
const selectedColor = computed(() => colorOf(graph.value.rows[selected.value]?.color ?? 0));
|
||||
|
||||
@@ -330,7 +397,28 @@ function jumpTo(r: Ref) {
|
||||
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (menuOpen.value || e.target instanceof HTMLInputElement || e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
const actions: Record<string, () => void> = {
|
||||
const view = diffView.value;
|
||||
const diffActions: Record<string, () => void> = {
|
||||
ArrowDown: () => view?.scrollLines(1),
|
||||
j: () => view?.scrollLines(1),
|
||||
ArrowUp: () => view?.scrollLines(-1),
|
||||
k: () => view?.scrollLines(-1),
|
||||
PageDown: () => view?.scrollPages(1),
|
||||
PageUp: () => view?.scrollPages(-1),
|
||||
Home: () => view?.toTop(),
|
||||
g: () => view?.toTop(),
|
||||
End: () => view?.toBottom(),
|
||||
G: () => view?.toBottom(),
|
||||
n: () => view?.jumpHunk(1),
|
||||
N: () => view?.jumpHunk(-1),
|
||||
"]": () => stepFile(1),
|
||||
"[": () => stepFile(-1),
|
||||
v: cycleLayout,
|
||||
Escape: closeDiff,
|
||||
y: copyHash,
|
||||
p: () => (settings.previewShown = !settings.previewShown),
|
||||
};
|
||||
const actions: Record<string, () => void> = diffFile.value ? diffActions : {
|
||||
ArrowDown: () => select(selected.value + 1),
|
||||
j: () => select(selected.value + 1),
|
||||
ArrowUp: () => select(selected.value - 1),
|
||||
@@ -351,6 +439,7 @@ function onKey(e: KeyboardEvent) {
|
||||
r: load,
|
||||
o: () => (menuOpen.value = true),
|
||||
p: () => (settings.previewShown = !settings.previewShown),
|
||||
Enter: openFirstFile,
|
||||
"/": () => sidebar.value?.focusPattern(),
|
||||
};
|
||||
const action = actions[e.key];
|
||||
@@ -442,7 +531,18 @@ onUnmounted(() => {
|
||||
|
||||
<div v-else class="center">
|
||||
|
||||
<div ref="scroller" class="list" @scroll="scrollTop = ($event.target as HTMLElement).scrollTop">
|
||||
<DiffView
|
||||
v-if="diffFile"
|
||||
ref="diffView"
|
||||
:file="diffFile"
|
||||
:diff="diffData"
|
||||
:error="diffError"
|
||||
:layout="settings.diffLayout"
|
||||
@layout="(l) => (settings.diffLayout = l)"
|
||||
@close="closeDiff"
|
||||
@force="openDiff(diffFile!, true)"
|
||||
/>
|
||||
<div v-show="!diffFile" ref="scroller" class="list" @scroll="scrollTop = ($event.target as HTMLElement).scrollTop">
|
||||
<div v-if="repo && rows.length === 0" class="empty">No commits to show. Check the filters on the left.</div>
|
||||
<div class="spacer-rows" :style="{ height: rows.length * ROW_H + 'px' }">
|
||||
<div
|
||||
@@ -499,17 +599,29 @@ onUnmounted(() => {
|
||||
:labels="selectedLabels"
|
||||
:color="selectedColor"
|
||||
:format-time="formatTime"
|
||||
:chosen="diffFile?.path ?? null"
|
||||
@jump="jumpToHash"
|
||||
@copy="copyText"
|
||||
@open="openDiff"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="keys">
|
||||
<footer v-if="diffFile" class="keys">
|
||||
<span><kbd>↑</kbd><kbd>↓</kbd> / <kbd>j</kbd><kbd>k</kbd> scroll</span>
|
||||
<span><kbd>PgUp</kbd><kbd>PgDn</kbd> page</span>
|
||||
<span><kbd>g</kbd><kbd>G</kbd> top / bottom</span>
|
||||
<span><kbd>n</kbd><kbd>N</kbd> next / previous change</span>
|
||||
<span><kbd>[</kbd><kbd>]</kbd> previous / next file</span>
|
||||
<span><kbd>v</kbd> switch layout</span>
|
||||
<span><kbd>Esc</kbd> back to graph</span>
|
||||
</footer>
|
||||
<footer v-else class="keys">
|
||||
<span><kbd>↑</kbd><kbd>↓</kbd> / <kbd>j</kbd><kbd>k</kbd> move</span>
|
||||
<span><kbd>g</kbd><kbd>G</kbd> top / bottom</span>
|
||||
<span><kbd>space</kbd> fold run</span>
|
||||
<span><kbd>Enter</kbd> diff</span>
|
||||
<span><kbd>c</kbd> fold all</span>
|
||||
<span><kbd>y</kbd> copy hash</span>
|
||||
<span><kbd>h</kbd> hide branch</span>
|
||||
|
||||
Reference in New Issue
Block a user