Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/resize-panes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kitlangton/ghui": patch
---

Add split-pane resize shortcuts and expose them in the command palette.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ running.
- `f`: open the changed-files navigator while viewing a diff
- `left` / `right`: choose the deleted or added side while in split diff comment mode
- `[` / `]`: switch files while viewing or commenting on a diff
- `<` / `>`: resize split panes in wide layout or split diff view
- `s`: toggle draft or ready-for-review state
- `m`: merge
- `x`: close with confirmation
Expand Down
48 changes: 42 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ const DETAIL_PREFETCH_BEHIND = 1
const DETAIL_PREFETCH_AHEAD = 3
const DETAIL_PREFETCH_CONCURRENCY = 3
const DETAIL_PREFETCH_DELAY_MS = 120
const DEFAULT_SPLIT_RATIO = 0.56
const DEFAULT_DIFF_SPLIT_RATIO = 0.5
const SPLIT_RESIZE_COLUMNS = 4
const MIN_LIST_PANE_WIDTH = 44
const MIN_DETAIL_PANE_WIDTH = 28
const MIN_DIFF_SIDE_WIDTH = 24
const appendPullRequestPage = (existing: readonly PullRequestItem[], incoming: readonly PullRequestItem[]) => {
const seen = new Set(existing.map((pullRequest) => pullRequest.url))
const mergedIncoming = mergeCachedDetails(incoming, existing)
Expand Down Expand Up @@ -342,6 +348,7 @@ const pullRequestsAtom = githubRuntime
)
.pipe(Atom.keepAlive)
const wrapIndex = (index: number, length: number) => (length === 0 ? 0 : ((index % length) + length) % length)
const clampNumber = (value: number, min: number, max: number) => Math.max(min, Math.min(value, max))
const selectedIndexAtom = Atom.make(0)
const noticeAtom = Atom.make<string | null>(null)
const filterQueryAtom = Atom.make("")
Expand All @@ -357,6 +364,8 @@ const diffScrollTopAtom = Atom.make(0)
const diffRenderViewAtom = Atom.make<DiffView>("split")
const diffWrapModeAtom = Atom.make<DiffWrapMode>("none")
const diffWhitespaceModeAtom = Atom.make<DiffWhitespaceMode>(initialDiffWhitespaceMode)
const diffSplitRatioAtom = Atom.make(DEFAULT_DIFF_SPLIT_RATIO)
const listDetailSplitRatioAtom = Atom.make(DEFAULT_SPLIT_RATIO)
const diffCommentAnchorIndexAtom = Atom.make(0)
const diffPreferredSideAtom = Atom.make<DiffCommentSide | null>(null)
const diffCommentRangeStartIndexAtom = Atom.make<number | null>(null)
Expand Down Expand Up @@ -725,6 +734,8 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
const [diffRenderView, setDiffRenderView] = useAtom(diffRenderViewAtom)
const [diffWrapMode, setDiffWrapMode] = useAtom(diffWrapModeAtom)
const [diffWhitespaceMode, setDiffWhitespaceMode] = useAtom(diffWhitespaceModeAtom)
const [diffSplitRatio, setDiffSplitRatio] = useAtom(diffSplitRatioAtom)
const [listDetailSplitRatio, setListDetailSplitRatio] = useAtom(listDetailSplitRatioAtom)
const [diffCommentAnchorIndex, setDiffCommentAnchorIndex] = useAtom(diffCommentAnchorIndexAtom)
const [diffPreferredSide, setDiffPreferredSide] = useAtom(diffPreferredSideAtom)
const [diffCommentRangeStartIndex, setDiffCommentRangeStartIndex] = useAtom(diffCommentRangeStartIndexAtom)
Expand Down Expand Up @@ -839,8 +850,10 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
const isWideLayout = terminalWidth >= 100
const splitGap = 1
const sectionPadding = 1
const leftPaneWidth = isWideLayout ? Math.max(44, Math.floor((contentWidth - splitGap) * 0.56)) : contentWidth
const rightPaneWidth = isWideLayout ? Math.max(28, contentWidth - leftPaneWidth - splitGap) : contentWidth
const splitAvailableWidth = Math.max(1, contentWidth - splitGap)
const maxListPaneWidth = Math.max(MIN_LIST_PANE_WIDTH, splitAvailableWidth - MIN_DETAIL_PANE_WIDTH)
const leftPaneWidth = isWideLayout ? clampNumber(Math.floor(splitAvailableWidth * listDetailSplitRatio), MIN_LIST_PANE_WIDTH, maxListPaneWidth) : contentWidth
const rightPaneWidth = isWideLayout ? Math.max(MIN_DETAIL_PANE_WIDTH, contentWidth - leftPaneWidth - splitGap) : contentWidth
const dividerJunctionAt = Math.max(1, leftPaneWidth)
const leftContentWidth = isWideLayout ? Math.max(24, leftPaneWidth - 2) : Math.max(24, contentWidth - sectionPadding * 2)
const rightContentWidth = isWideLayout ? Math.max(24, rightPaneWidth - sectionPadding * 2) : Math.max(24, contentWidth - sectionPadding * 2)
Expand Down Expand Up @@ -995,12 +1008,12 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
[selectedDiffState, readyDiffFiles],
)
const stackedDiffFiles = useMemo(
() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth),
[readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth],
() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth, diffSplitRatio),
[readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth, diffSplitRatio],
)
const diffCommentAnchors = useMemo(
() => (diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : []),
[diffFullView, stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth],
() => (diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth, diffSplitRatio) : []),
[diffFullView, stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth, diffSplitRatio],
)
const selectedDiffCommentAnchorIndex = Math.max(0, Math.min(diffCommentAnchorIndex, diffCommentAnchors.length - 1))
const selectedDiffCommentAnchor = diffCommentAnchors[selectedDiffCommentAnchorIndex] ?? null
Expand Down Expand Up @@ -1607,6 +1620,21 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
const isSelectedPullRequestDetailLoading = selectedPullRequest !== null && !selectedPullRequest.detailLoaded
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))

const resizeListPane = (columns: number) => {
if (!isWideLayout) return
const nextLeftPaneWidth = clampNumber(leftPaneWidth + columns, MIN_LIST_PANE_WIDTH, maxListPaneWidth)
setListDetailSplitRatio(nextLeftPaneWidth / splitAvailableWidth)
}

const resizeDiffSplit = (columns: number) => {
if (effectiveDiffRenderView !== "split") return
const availableWidth = Math.max(1, contentWidth)
const minRatio = Math.min(0.5, MIN_DIFF_SIDE_WIDTH / availableWidth)
const maxRatio = 1 - minRatio
preserveCurrentDiffLocation()
setDiffSplitRatio((current) => clampNumber(current + columns / availableWidth, minRatio, maxRatio))
}

const loadPullRequestReviewComments = (pullRequest: PullRequestItem, force = false) => {
const key = pullRequestDiffKey(pullRequest)
const previousLoadState = registry.get(diffCommentsLoadedAtom)[key]
Expand Down Expand Up @@ -2929,6 +2957,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
loadedPullRequestCount,
hasMorePullRequests,
isLoadingMorePullRequests,
listSplitResizable: isWideLayout && !detailFullView && !diffFullView && !commentsViewActive,
selectedPullRequest,
detailFullView,
diffFullView,
Expand Down Expand Up @@ -2961,6 +2990,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
openRepositoryPicker,
loadMorePullRequests,
switchViewTo,
resizeListSplit: (delta) => resizeListPane(delta * SPLIT_RESIZE_COLUMNS),
openDetails: () => {
setDetailFullView(true)
setDetailScrollOffset(0)
Expand All @@ -2986,6 +3016,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
flashNotice(`Refreshing diff for #${selectedPullRequest.number}`)
},
toggleDiffRenderView,
resizeDiffSplit: (delta) => resizeDiffSplit(delta * SPLIT_RESIZE_COLUMNS),
toggleDiffWrapMode,
toggleDiffWhitespaceMode,
openChangedFilesModal,
Expand Down Expand Up @@ -3306,6 +3337,8 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
toggleRange: () => runCommandById("diff.toggle-range"),
toggleView: () => runCommandById("diff.toggle-view"),
toggleWrap: () => runCommandById("diff.toggle-wrap"),
splitView: effectiveDiffRenderView === "split",
resizeSplit: (delta) => resizeDiffSplit(delta * SPLIT_RESIZE_COLUMNS),
reload: () => runCommandById("diff.reload"),
nextThread: () => runCommandById("diff.next-thread"),
previousThread: () => runCommandById("diff.previous-thread"),
Expand Down Expand Up @@ -3355,12 +3388,14 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
visibleCount: visiblePullRequests.length,
hasFilter: filterQuery.length > 0,
canScrollDetailPreview: isWideLayout && selectedPullRequest !== null,
canResizeSplit: isWideLayout && !detailFullView && !diffFullView && !commentsViewActive,
runCommandById: (id) => {
runCommandById(id)
},
switchQueueMode,
scrollDetailPreviewBy,
scrollDetailPreviewTo,
resizeSplit: (delta) => resizeListPane(delta * SPLIT_RESIZE_COLUMNS),
clearFilter: () => {
runCommandById("filter.clear")
},
Expand Down Expand Up @@ -3623,6 +3658,7 @@ export const App = ({ systemThemeGeneration = 0 }: AppProps) => {
view={effectiveDiffRenderView}
whitespaceMode={diffWhitespaceMode}
wrapMode={diffWrapMode}
splitRatio={diffSplitRatio}
paneWidth={contentWidth}
height={wideBodyHeight}
loadingIndicator={loadingIndicator}
Expand Down
45 changes: 45 additions & 0 deletions src/appCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface AppCommandActions {
readonly openRepositoryPicker: () => void
readonly loadMorePullRequests: () => void
readonly switchViewTo: (view: PullRequestView) => void
readonly resizeListSplit: (delta: 1 | -1) => void
readonly openDetails: () => void
readonly closeDetails: () => void
readonly openDiffView: () => void
Expand All @@ -25,6 +26,7 @@ interface AppCommandActions {
readonly openDeleteSelectedComment: () => void
readonly reloadDiff: () => void
readonly toggleDiffRenderView: () => void
readonly resizeDiffSplit: (delta: 1 | -1) => void
readonly toggleDiffWrapMode: () => void
readonly toggleDiffWhitespaceMode: () => void
readonly openChangedFilesModal: () => void
Expand Down Expand Up @@ -53,6 +55,7 @@ interface BuildAppCommandsInput {
readonly loadedPullRequestCount: number
readonly hasMorePullRequests: boolean
readonly isLoadingMorePullRequests: boolean
readonly listSplitResizable: boolean
readonly selectedPullRequest: PullRequestItem | null
readonly detailFullView: boolean
readonly diffFullView: boolean
Expand Down Expand Up @@ -82,6 +85,7 @@ export const buildAppCommands = ({
loadedPullRequestCount,
hasMorePullRequests,
isLoadingMorePullRequests,
listSplitResizable,
selectedPullRequest,
detailFullView,
diffFullView,
Expand All @@ -108,6 +112,7 @@ export const buildAppCommands = ({
const selectedDiffLineReason = diffFullView && diffReady ? (selectedDiffCommentAnchorLabel ? null : "No diff line selected.") : diffOpenReadyReason
const diffThreadReason = diffFullView && diffReady ? (hasDiffCommentThreads ? null : "No diff comments loaded.") : diffOpenReadyReason
const changedFilesReason = diffFullView && diffReady ? (readyDiffFileCount > 0 ? null : "No changed files loaded.") : diffOpenReadyReason
const diffSplitReason = diffFullView ? (effectiveDiffRenderView === "split" ? null : "Split diff view is not active.") : "Open a diff first."
const selectedCommentReason = selectedPullRequest ? (commentsViewActive ? (hasSelectedComment ? null : "No comment selected.") : "Open comments first.") : noPullRequestReason
const ownCommentReason = selectedCommentReason ?? (canEditSelectedComment ? null : "Only your own (synced) comments can be edited or deleted.")
const loadMoreDisabledReason = isLoadingMorePullRequests ? "Already loading more pull requests." : hasMorePullRequests ? null : "No more pull requests loaded by this view."
Expand Down Expand Up @@ -195,6 +200,26 @@ export const buildAppCommands = ({
keywords: ["next page", "pagination", "more"],
run: actions.loadMorePullRequests,
}),
defineCommand({
id: "list.resize-left",
title: "Resize list split left",
scope: "Navigation",
subtitle: "Give more space to pull request details",
shortcut: "<",
disabledReason: listSplitResizable ? null : "Split layout is not active.",
keywords: ["pane", "split", "width", "shrink list"],
run: () => actions.resizeListSplit(-1),
}),
defineCommand({
id: "list.resize-right",
title: "Resize list split right",
scope: "Navigation",
subtitle: "Give more space to the pull request list",
shortcut: ">",
disabledReason: listSplitResizable ? null : "Split layout is not active.",
keywords: ["pane", "split", "width", "grow list"],
run: () => actions.resizeListSplit(1),
}),
forSelected({
id: "detail.open",
title: "Open pull request details",
Expand Down Expand Up @@ -293,6 +318,26 @@ export const buildAppCommands = ({
disabledReason: diffFullView ? null : "Open a diff first.",
run: actions.toggleDiffRenderView,
}),
defineCommand({
id: "diff.resize-split-left",
title: "Resize diff split left",
scope: "Diff",
subtitle: "Give more space to the right side",
shortcut: "<",
disabledReason: diffSplitReason,
keywords: ["pane", "split", "width", "old side"],
run: () => actions.resizeDiffSplit(-1),
}),
defineCommand({
id: "diff.resize-split-right",
title: "Resize diff split right",
scope: "Diff",
subtitle: "Give more space to the left side",
shortcut: ">",
disabledReason: diffSplitReason,
keywords: ["pane", "split", "width", "new side"],
run: () => actions.resizeDiffSplit(1),
}),
defineCommand({
id: "diff.toggle-wrap",
title: "Toggle diff word wrap",
Expand Down
4 changes: 4 additions & 0 deletions src/keymap/diffView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export interface DiffViewCtx {
readonly toggleRange: () => void
readonly toggleView: () => void
readonly toggleWrap: () => void
readonly splitView: boolean
readonly resizeSplit: (delta: number) => void
readonly reload: () => void
readonly nextThread: () => void
readonly previousThread: () => void
Expand All @@ -33,6 +35,8 @@ export const diffViewKeymap = Diff(
{ id: "diff.toggle-range", title: "Toggle comment range", keys: ["v"], run: (s) => s.toggleRange() },
{ id: "diff.toggle-view", title: "Toggle split/unified", keys: ["shift+v"], run: (s) => s.toggleView() },
{ id: "diff.toggle-wrap", title: "Toggle wrap", keys: ["w"], run: (s) => s.toggleWrap() },
{ id: "diff.resize-left", title: "Resize split left", keys: ["<"], enabled: (s) => (s.splitView ? true : "Split diff view is not active."), run: (s) => s.resizeSplit(-1) },
{ id: "diff.resize-right", title: "Resize split right", keys: [">"], enabled: (s) => (s.splitView ? true : "Split diff view is not active."), run: (s) => s.resizeSplit(1) },
{ id: "diff.reload", title: "Reload diff", keys: ["r"], run: (s) => s.reload() },
{ id: "diff.next-thread", title: "Next thread", keys: ["n"], run: (s) => s.nextThread() },
{ id: "diff.previous-thread", title: "Previous thread", keys: ["p"], run: (s) => s.previousThread() },
Expand Down
16 changes: 16 additions & 0 deletions src/keymap/listNav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ export interface ListNavCtx {
readonly visibleCount: number
readonly hasFilter: boolean
readonly canScrollDetailPreview: boolean
readonly canResizeSplit: boolean
readonly runCommandById: (id: string) => void
readonly switchQueueMode: (delta: 1 | -1) => void
readonly scrollDetailPreviewBy: (delta: number) => void
readonly scrollDetailPreviewTo: (line: number) => void
readonly resizeSplit: (delta: number) => void
readonly clearFilter: () => void
readonly stepSelected: (delta: number) => void
readonly stepSelectedUp: (count?: number) => void
Expand Down Expand Up @@ -38,6 +40,20 @@ export const listNavKeymap = List(
{ id: "list.toggle-draft", title: "Toggle draft", keys: ["s", "shift+s"], run: (s) => s.runCommandById("pull.toggle-draft") },
{ id: "list.copy", title: "Copy metadata", keys: ["y"], run: (s) => s.runCommandById("pull.copy-metadata") },
{ id: "list.detail.open", title: "Open details", keys: ["return"], run: (s) => s.runCommandById("detail.open") },
{
id: "list.resize-left",
title: "Resize split left",
keys: ["<"],
enabled: (s) => (s.canResizeSplit ? true : "Split layout is not active."),
run: (s) => s.resizeSplit(-1),
},
{
id: "list.resize-right",
title: "Resize split right",
keys: [">"],
enabled: (s) => (s.canResizeSplit ? true : "Split layout is not active."),
run: (s) => s.resizeSplit(1),
},

// Queue mode tabs
{ id: "list.next-tab", title: "Next view", keys: ["tab"], run: (s) => s.switchQueueMode(1) },
Expand Down
Loading