Improve the git diff preview from the file viewer
CI / check (push) Waiting to run

This commit is contained in:
2026-09-21 06:47:46 +02:00
parent a2d1c5df83
commit dbb7e40c6c
10 changed files with 98 additions and 221 deletions
+4 -7
View File
@@ -52,7 +52,7 @@ describe('a change that only removes a line', () => {
expect(c.querySelector('.ce-gutter .chg')).toBeNull()
})
it('hands the removed line to the picked panel', async () => {
it('opens the full-screen Diff from the ruled line', async () => {
const c = await openRow()
const scroll = await waitFor(() => {
const el = c.querySelector<HTMLElement>('.ce-scroll')
@@ -61,11 +61,8 @@ describe('a change that only removes a line', () => {
})
// 'tail' took the place of the removed line, and sits on line 2.
fireEvent.click(scroll, { clientY: 10 + 20 + 5 })
const peek = await waitFor(() => {
const p = c.querySelector<HTMLElement>('.peek')
if (!p) throw new Error('peek not ready')
return p
})
expect(Array.from(peek.querySelectorAll('.ln-row.del')).map((el) => el.textContent?.replace(/^\d+/, ''))).toEqual(['gone'])
await waitFor(() => expect(c.querySelector('.split-overlay')).toBeTruthy())
const left = Array.from(c.querySelectorAll('.split-pane.left .ln-row.del'))
expect(left.map((el) => el.textContent?.replace(/^\d+/, ''))).toEqual(['gone'])
})
})
+9 -14
View File
@@ -2,44 +2,39 @@ import { describe, it, expect } from 'vitest'
import { buildDiffView } from '../src/renderer/src/editor'
import { makeDiff } from '../src/renderer/src/diff'
/** The current-side view of one text pair, plus the map the overlay reads. */
/** The current-side view of one text pair: the lines Actual marks. */
function view(original: string, updated: string): ReturnType<typeof buildDiffView> {
return buildDiffView(makeDiff('M', original, updated))
}
describe('buildDiffView', () => {
it('maps an added line to the line it replaced', () => {
const { lines, peek } = view('a\nold\nb', 'a\nnew\nb')
it('marks a rewritten line in place', () => {
const lines = view('a\nold\nb', 'a\nnew\nb')
expect(lines.map((l) => l.text)).toEqual(['a', 'new', 'b'])
expect(lines[1].row).toBe('add')
expect(peek.get(2)).toEqual({ anchor: 2, from: 2, to: 2 })
expect(lines.every((l) => !l.gap)).toBe(true)
})
it('puts a pure deletion on the line that follows it', () => {
const { lines, peek } = view('a\ngone\nb', 'a\nb')
const lines = view('a\ngone\nb', 'a\nb')
expect(lines[1].text).toBe('b')
expect(lines[1].gap).toBe('above')
expect(lines[1].row).toBeNull()
expect(peek.get(2)).toEqual({ anchor: 2, from: 2, to: 2 })
})
it('puts a deletion at end of file under the last line', () => {
const { lines, peek } = view('a\nb\ntail', 'a\nb')
const lines = view('a\nb\ntail', 'a\nb')
expect(lines[1].gap).toBe('below')
expect(peek.get(2)).toEqual({ anchor: 3, from: 3, to: 3 })
})
it('anchors an insertion but marks nothing as removed', () => {
const { lines, peek } = view('a\nb', 'a\nNEW\nb')
it('marks an insertion and leaves the neighbours alone', () => {
const lines = view('a\nb', 'a\nNEW\nb')
expect(lines[1].row).toBe('add')
expect(peek.get(2)).toEqual({ anchor: 1, from: null, to: null })
expect(lines.every((l) => !l.gap)).toBe(true)
})
it('leaves an unchanged file without marks or peek targets', () => {
const { lines, peek } = view('a\nb\n', 'a\nb\n')
expect(peek.size).toBe(0)
it('leaves an unchanged file without marks', () => {
const lines = view('a\nb\n', 'a\nb\n')
expect(lines.every((l) => !l.row && !l.gap)).toBe(true)
})
})
+8 -12
View File
@@ -74,7 +74,7 @@ describe('Editor view modes', () => {
expect(find(c, '.seg button.on', 'Original')).toBeTruthy()
})
it('clicking a band in Actual reveals the original, clicking it again hides it', async () => {
it('clicking a band in Actual opens the full-screen Diff on that line', async () => {
const c = await openChanged()
const scroll = await waitFor(() => {
const el = c.querySelector<HTMLElement>('.ce-scroll')
@@ -84,18 +84,14 @@ describe('Editor view modes', () => {
// Line 30 is the first rewritten line. Unwrapped, its band is arithmetic:
// a 10px top pad plus 20px per line, and the hit test reads clientY.
fireEvent.click(scroll, { clientY: 10 + 29 * 20 + 5 })
const peek = await waitFor(() => {
const p = c.querySelector<HTMLElement>('.peek')
if (!p) throw new Error('peek not ready')
return p
})
const removed = Array.from(peek.querySelectorAll('.ln-row.del'))
expect(removed).toHaveLength(1)
expect(removed[0].textContent).toContain("'plan' => $user->plan,")
await waitFor(() => expect(c.querySelector('.split-overlay')).toBeTruthy())
const right = Array.from(c.querySelectorAll('.split-pane.right .ln-row'))
expect(right.some((el) => el.textContent?.includes("'plan'"))).toBe(true)
// A second click on the same band drops it.
fireEvent.click(scroll, { clientY: 10 + 29 * 20 + 5 })
await waitFor(() => expect(c.querySelector('.peek')).toBeNull())
// Esc puts the pane back in Actual.
act(() => { window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })) })
await waitFor(() => expect(c.querySelector('.split-overlay')).toBeNull())
expect(c.querySelector('.seg button.on')?.textContent).toBe('Actual')
})
it('⌘M cycles Actual → Original → Diff, and nothing else', async () => {
+4 -12
View File
@@ -103,7 +103,7 @@ describe('a file that is staged and then edited again', () => {
expect(splitText(c, 'right')).toEqual(['a', 'STAGED', 'c', 'AFTER-STAGING'])
})
it('the picked panel behind Actual always reaches back to HEAD', async () => {
it('the Diff opened from Actual always reaches back to HEAD', async () => {
const c = await boot()
fireEvent.click(group(c, 'Changes')[0])
const scroll = await waitFor(() => {
@@ -113,17 +113,9 @@ describe('a file that is staged and then edited again', () => {
})
// Line 2 is 'STAGED'. Unwrapped, its band runs from 10 + (2-1)*20.
fireEvent.click(scroll, { clientY: 10 + 20 + 5 })
const peek = await waitFor(() => {
const p = c.querySelector<HTMLElement>('.peek')
if (!p) throw new Error('peek not ready')
return p
})
const removed = Array.from(peek.querySelectorAll('.ln-row.del'))
expect(removed.map((el) => el.textContent?.replace(/^\d+/, ''))).toEqual(['b'])
// A click anywhere off the code drops it.
fireEvent.mouseDown(document.body)
await waitFor(() => expect(c.querySelector('.peek')).toBeNull())
await waitFor(() => expect(c.querySelector('.split-overlay')).toBeTruthy())
expect(splitText(c, 'left')).toEqual(['a', 'b', 'c', ''])
expect(splitText(c, 'right')).toEqual(['a', 'STAGED', 'c', 'AFTER-STAGING'])
})
it('labels which pair the diff is comparing', async () => {