Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/renderer/src/components/graph/GraphCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { stripCoAuthorTrailers } from '@/lib/coauthors'
import { reflowMessage } from '@/lib/reflow'
import { subscribeAvatars } from './avatars'
import {
branchEndpoint,
captionAlpha,
captionCenterOffset,
contentSize,
Expand Down Expand Up @@ -819,13 +820,24 @@ export function GraphCanvas({
}
if (e.key === 'Home' || e.key === 'End') {
e.preventDefault()
const edge = e.key === 'Home' ? 'first' : 'last'
const current = s.selectedHash ? s.layout.nodeByHash.get(s.selectedHash) : null
const branchRow = current
? null
: s.layout.rows.find((r) => rowMatchesSelection(r, s.selectedBranch))
if (current) {
// Within the selected commit's branch: Home → its oldest commit,
// End → its newest.
const target = rowEndpoint(s.layout, current, e.key === 'Home' ? 'first' : 'last')
const target = rowEndpoint(s.layout, current, edge)
onSelectNode(target)
reveal(target.commit.hash)
} else if (branchRow) {
// A branch is selected: same edges, walked from its own chain.
const target = branchEndpoint(s.layout, branchRow, edge)
if (target) {
onSelectNode(target)
reveal(target.commit.hash)
}
} else if (e.key === 'Home') {
// Nothing selected: Home keeps its classic meaning — frame the
// home changeset.
Expand Down
38 changes: 38 additions & 0 deletions src/renderer/src/components/graph/geometry.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, test } from 'bun:test'
import type { Commit } from '@shared/types'
import {
branchEndpoint,
CAPSULE_HALF_H,
CAPTION_FULL_ZOOM,
CAPTION_GAP_WORLD,
Expand Down Expand Up @@ -231,6 +232,43 @@ describe('graph geometry', () => {
expect(f1.row).toBe(g2.row)
expect(rowEndpoint(layout, g2, 'first').commit.hash).toBe('g1')
expect(rowEndpoint(layout, f1, 'last').commit.hash).toBe('f2')

// The branch-selected targets walk the same edges from the row itself,
// and a lane shared by two chains never leaks into the neighbor.
const feat1 = layout.rows.find((r) => r.name === 'feat1')
const feat2 = layout.rows.find((r) => r.name === 'feat2')
if (!feat1 || !feat2) throw new Error('missing row')
expect(branchEndpoint(layout, feat1, 'first')?.commit.hash).toBe('f1')
expect(branchEndpoint(layout, feat1, 'last')?.commit.hash).toBe('f2')
expect(branchEndpoint(layout, feat2, 'first')?.commit.hash).toBe('g1')
expect(branchEndpoint(layout, feat2, 'last')?.commit.hash).toBe('g2')
})

test('Home/End targets when the branch is selected: its oldest and newest commit', () => {
const layout = sampleLayout()
const main = layout.rows.find((r) => r.name === 'main')
const feature = layout.rows.find((r) => r.name === 'feature')
if (!main || !feature) throw new Error('missing row')
expect(branchEndpoint(layout, main, 'first')?.commit.hash).toBe('a')
expect(branchEndpoint(layout, main, 'last')?.commit.hash).toBe('m')
// A single-commit branch: both edges are that commit.
expect(branchEndpoint(layout, feature, 'first')?.commit.hash).toBe('f')
expect(branchEndpoint(layout, feature, 'last')?.commit.hash).toBe('f')
})

test('an empty branch resolves both Home/End edges to its anchor commit', () => {
// 'fresh' points at the HEAD tip: zero commits of its own.
const layout = layoutGraph({
commits: [commit('b', ['a'], 'HEAD -> main, fresh'), commit('a', [])],
remotes: [],
headBranch: 'main',
detached: false,
defaultBranch: 'main'
})
const fresh = layout.rows.find((r) => r.name === 'fresh')
if (!fresh) throw new Error('missing empty row')
expect(branchEndpoint(layout, fresh, 'first')?.commit.hash).toBe('b')
expect(branchEndpoint(layout, fresh, 'last')?.commit.hash).toBe('b')
})

describe('revealRowDy — keep the clicked row clear of the diff pane', () => {
Expand Down
18 changes: 18 additions & 0 deletions src/renderer/src/components/graph/geometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,24 @@ export function rowEndpoint(
return best
}

/** The oldest ('first') or newest ('last') commit of a BRANCH row — the
* Home/End keyboard targets when the branch itself is selected. Walks the
* row's own chain, so a lane shared with another chain never leaks in. An
* empty branch owns no commits; both edges resolve to its anchor commit
* (the tip it points at). */
export function branchEndpoint(
layout: GraphLayout,
row: GraphRow,
edge: 'first' | 'last'
): GraphNode | null {
let best: GraphNode | null = null
for (const n of layout.nodes) {
if (n.chain !== row.chain) continue
if (!best || (edge === 'first' ? n.column < best.column : n.column > best.column)) best = n
}
return best ?? layout.nodeByHash.get(row.tipHash) ?? null
}

/**
* Keyboard navigation target from `node`: left/right walk the same row (older/
* newer), up/down jump to the column-nearest node one row away.
Expand Down
Loading