@kitlangton/ghui 0.1.21 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -4
- package/src/App.tsx +620 -792
- package/src/appCommands.ts +70 -16
- package/src/domain.ts +13 -0
- package/src/index.tsx +1 -7
- package/src/keyboard/opentuiAdapter.ts +43 -0
- package/src/keymap/all.ts +103 -0
- package/src/keymap/closeModal.ts +13 -0
- package/src/keymap/commandPalette.ts +16 -0
- package/src/keymap/commentModal.ts +73 -0
- package/src/keymap/commentThreadModal.ts +24 -0
- package/src/keymap/detailView.ts +30 -0
- package/src/keymap/diffView.ts +91 -0
- package/src/keymap/filterMode.ts +13 -0
- package/src/keymap/helpers.ts +24 -0
- package/src/keymap/labelModal.ts +16 -0
- package/src/keymap/listNav.ts +119 -0
- package/src/keymap/mergeModal.ts +23 -0
- package/src/keymap/openRepositoryModal.ts +13 -0
- package/src/keymap/themeModal.ts +49 -0
- package/src/mergeActions.ts +4 -1
- package/src/services/GitHubService.ts +42 -6
- package/src/services/MockGitHubService.ts +37 -2
- package/src/themeStore.ts +28 -11
- package/src/ui/CommandPalette.tsx +123 -63
- package/src/ui/DetailsPane.tsx +192 -54
- package/src/ui/FooterHints.tsx +9 -18
- package/src/ui/LoadingLogo.tsx +75 -0
- package/src/ui/PullRequestDiffPane.tsx +25 -18
- package/src/ui/PullRequestList.tsx +6 -2
- package/src/ui/comments.tsx +143 -0
- package/src/ui/diff.ts +198 -6
- package/src/ui/modals.tsx +4 -39
- package/src/ui/primitives.tsx +5 -1
- package/src/ui/singleLineInput.ts +2 -1
- package/src/ui/spinner.ts +1 -0
package/src/App.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { DiffRenderable, PasteEvent, ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import { RegistryContext, useAtom, useAtomRefresh, useAtomSet, useAtomValue } from "@effect/atom-react"
|
|
3
|
-
import {
|
|
3
|
+
import { useKeymap } from "@ghui/keymap/react"
|
|
4
|
+
import { appKeymap, type AppCtx } from "./keymap/all.js"
|
|
5
|
+
import { useOpenTuiSubscribe } from "./keyboard/opentuiAdapter.js"
|
|
4
6
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
5
7
|
import { Cause, Effect, Layer, Schedule } from "effect"
|
|
6
8
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
@@ -11,7 +13,7 @@ import { buildAppCommands } from "./appCommands.js"
|
|
|
11
13
|
import type { AppCommand } from "./commands.js"
|
|
12
14
|
import { clampCommandIndex, commandEnabled, defineCommand, filterCommands, sortCommandsByScope } from "./commands.js"
|
|
13
15
|
import { config } from "./config.js"
|
|
14
|
-
import { type CreatePullRequestCommentInput, type DiffCommentSide, type ListPullRequestPageInput, type LoadStatus, type PullRequestItem, type PullRequestLabel, type PullRequestMergeAction, type PullRequestReviewComment } from "./domain.js"
|
|
16
|
+
import { type CreatePullRequestCommentInput, type DiffCommentSide, type ListPullRequestPageInput, type LoadStatus, type PullRequestConversationItem, type PullRequestItem, type PullRequestLabel, type PullRequestMergeAction, type PullRequestReviewComment } from "./domain.js"
|
|
15
17
|
import { formatShortDate, formatTimestamp } from "./date.js"
|
|
16
18
|
import { errorMessage } from "./errors.js"
|
|
17
19
|
import { availableMergeActions, mergeInfoFromPullRequest } from "./mergeActions.js"
|
|
@@ -22,12 +24,13 @@ import { BrowserOpener } from "./services/BrowserOpener.js"
|
|
|
22
24
|
import { Clipboard } from "./services/Clipboard.js"
|
|
23
25
|
import { CommandRunner } from "./services/CommandRunner.js"
|
|
24
26
|
import { GitHubService } from "./services/GitHubService.js"
|
|
25
|
-
import { loadStoredThemeId, saveStoredThemeId } from "./themeStore.js"
|
|
27
|
+
import { loadStoredDiffWhitespaceMode, loadStoredThemeId, saveStoredDiffWhitespaceMode, saveStoredThemeId } from "./themeStore.js"
|
|
26
28
|
import { colors, filterThemeDefinitions, mixHex, setActiveTheme, themeDefinitions, type ThemeId } from "./ui/colors.js"
|
|
27
29
|
import { backspace as editorBackspace, deleteForward as editorDeleteForward, deleteToLineEnd, deleteToLineStart, deleteWordBackward, deleteWordForward, insertText, moveLeft as editorMoveLeft, moveLineEnd, moveLineStart, moveRight as editorMoveRight, moveVertically, moveWordBackward, moveWordForward, type CommentEditorValue } from "./ui/commentEditor.js"
|
|
28
|
-
import { buildStackedDiffFiles, diffCommentLocationKey, getStackedDiffCommentAnchors,
|
|
29
|
-
import { DETAIL_BODY_SCROLL_LIMIT, DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailHeaderHeight, getDetailJunctionRows,
|
|
30
|
+
import { buildStackedDiffFiles, diffAnchorOnSide, diffCommentAnchorLabel, diffCommentLineLabel, diffCommentLocationKey, diffCommentSideLabel, getStackedDiffCommentAnchors, minimizeWhitespaceDiffFiles, PullRequestDiffState, pullRequestDiffKey, safeDiffFileIndex, scrollTopForVisibleLine, splitPatchFiles, stackedDiffFileIndexAtLine, type DiffCommentAnchor, type DiffCommentKind, type DiffView, type DiffWhitespaceMode, type DiffWrapMode, type StackedDiffCommentAnchor, verticalDiffAnchor } from "./ui/diff.js"
|
|
31
|
+
import { DETAIL_BODY_SCROLL_LIMIT, DetailBody, DetailHeader, DetailPlaceholder, DetailsPane, getDetailHeaderHeight, getDetailJunctionRows, getScrollableDetailBodyHeight, LoadingPane, type DetailConversationStatus, type DetailPlaceholderContent } from "./ui/DetailsPane.js"
|
|
30
32
|
import { FooterHints, initialRetryProgress, RetryProgress } from "./ui/FooterHints.js"
|
|
33
|
+
import { LoadingLogoPane } from "./ui/LoadingLogo.js"
|
|
31
34
|
import { Divider, fitCell, PlainLine, SeparatorColumn } from "./ui/primitives.js"
|
|
32
35
|
import { CommandPalette } from "./ui/CommandPalette.js"
|
|
33
36
|
import { CloseModal, CommentModal, CommentThreadModal, filterLabels, initialCloseModalState, initialCommandPaletteState, initialCommentModalState, initialCommentThreadModalState, initialLabelModalState, initialMergeModalState, initialModal, initialOpenRepositoryModalState, initialThemeModalState, LabelModal, MergeModal, Modal, OpenRepositoryModal, ThemeModal, type CloseModalState, type CommandPaletteState, type CommentModalState, type CommentThreadModalState, type LabelModalState, type MergeModalState, type ModalState, type ModalTag, type OpenRepositoryModalState, type ThemeModalState } from "./ui/modals.js"
|
|
@@ -35,6 +38,7 @@ import { groupBy, reviewLabel } from "./ui/pullRequests.js"
|
|
|
35
38
|
import { PullRequestDiffPane } from "./ui/PullRequestDiffPane.js"
|
|
36
39
|
import { buildPullRequestListRows, pullRequestListRowIndex, PullRequestList } from "./ui/PullRequestList.js"
|
|
37
40
|
import { editSingleLineInput, isSingleLineInputKey, printableKeyText, singleLineText } from "./ui/singleLineInput.js"
|
|
41
|
+
import { SPINNER_FRAMES } from "./ui/spinner.js"
|
|
38
42
|
|
|
39
43
|
const parseOptionalPositiveInt = (value: string | undefined, fallback: number | null) => {
|
|
40
44
|
if (value === undefined) return fallback
|
|
@@ -54,7 +58,10 @@ const githubRuntime = Atom.runtime(
|
|
|
54
58
|
Layer.provideMerge(Observability.layer),
|
|
55
59
|
),
|
|
56
60
|
)
|
|
57
|
-
const initialThemeId = await
|
|
61
|
+
const [initialThemeId, initialDiffWhitespaceMode] = await Promise.all([
|
|
62
|
+
Effect.runPromise(loadStoredThemeId),
|
|
63
|
+
Effect.runPromise(loadStoredDiffWhitespaceMode),
|
|
64
|
+
])
|
|
58
65
|
|
|
59
66
|
interface PullRequestLoad {
|
|
60
67
|
readonly view: PullRequestView
|
|
@@ -96,6 +103,11 @@ interface AppliedDiffLineColorState {
|
|
|
96
103
|
readonly entries: readonly AppliedDiffLineColor[]
|
|
97
104
|
}
|
|
98
105
|
|
|
106
|
+
interface DiffCommentRangeSelection {
|
|
107
|
+
readonly start: StackedDiffCommentAnchor
|
|
108
|
+
readonly end: StackedDiffCommentAnchor
|
|
109
|
+
}
|
|
110
|
+
|
|
99
111
|
interface DetailHydration {
|
|
100
112
|
readonly token: symbol
|
|
101
113
|
notifyError: boolean
|
|
@@ -106,7 +118,6 @@ const FOCUS_RETURN_REFRESH_MIN_MS = 60_000
|
|
|
106
118
|
const FOCUSED_IDLE_REFRESH_MS = 5 * 60_000
|
|
107
119
|
const AUTO_REFRESH_JITTER_MS = 10_000
|
|
108
120
|
const DIFF_STICKY_HEADER_LINES = 2
|
|
109
|
-
const LOADING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const
|
|
110
121
|
const MAX_REPOSITORY_CACHE_ENTRIES = 8
|
|
111
122
|
const LOAD_MORE_SELECTION_THRESHOLD = 8
|
|
112
123
|
const LOAD_MORE_SCROLL_THRESHOLD = 3
|
|
@@ -114,7 +125,6 @@ const DETAIL_PREFETCH_BEHIND = 1
|
|
|
114
125
|
const DETAIL_PREFETCH_AHEAD = 3
|
|
115
126
|
const DETAIL_PREFETCH_CONCURRENCY = 3
|
|
116
127
|
const DETAIL_PREFETCH_DELAY_MS = 120
|
|
117
|
-
|
|
118
128
|
const appendPullRequestPage = (existing: readonly PullRequestItem[], incoming: readonly PullRequestItem[]) => {
|
|
119
129
|
const seen = new Set(existing.map((pullRequest) => pullRequest.url))
|
|
120
130
|
const mergedIncoming = mergeCachedDetails(incoming, existing)
|
|
@@ -179,7 +189,6 @@ const noticeAtom = Atom.make<string | null>(null)
|
|
|
179
189
|
const filterQueryAtom = Atom.make("")
|
|
180
190
|
const filterDraftAtom = Atom.make("")
|
|
181
191
|
const filterModeAtom = Atom.make(false)
|
|
182
|
-
const pendingGAtom = Atom.make(false)
|
|
183
192
|
const detailFullViewAtom = Atom.make(false)
|
|
184
193
|
const detailScrollOffsetAtom = Atom.make(0)
|
|
185
194
|
const diffFullViewAtom = Atom.make(false)
|
|
@@ -187,10 +196,14 @@ const diffFileIndexAtom = Atom.make(0)
|
|
|
187
196
|
const diffScrollTopAtom = Atom.make(0)
|
|
188
197
|
const diffRenderViewAtom = Atom.make<DiffView>("split")
|
|
189
198
|
const diffWrapModeAtom = Atom.make<DiffWrapMode>("none")
|
|
190
|
-
const
|
|
199
|
+
const diffWhitespaceModeAtom = Atom.make<DiffWhitespaceMode>(initialDiffWhitespaceMode)
|
|
191
200
|
const diffCommentAnchorIndexAtom = Atom.make(0)
|
|
201
|
+
const diffPreferredSideAtom = Atom.make<DiffCommentSide | null>(null)
|
|
202
|
+
const diffCommentRangeStartIndexAtom = Atom.make<number | null>(null)
|
|
192
203
|
const diffCommentThreadsAtom = Atom.make<Record<string, readonly PullRequestReviewComment[]>>({}).pipe(Atom.keepAlive)
|
|
193
204
|
const diffCommentsLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
205
|
+
const pullRequestConversationAtom = Atom.make<Record<string, readonly PullRequestConversationItem[]>>({}).pipe(Atom.keepAlive)
|
|
206
|
+
const pullRequestConversationLoadedAtom = Atom.make<Record<string, "loading" | "ready">>({}).pipe(Atom.keepAlive)
|
|
194
207
|
const pullRequestDiffCacheAtom = Atom.make<Record<string, PullRequestDiffState>>({}).pipe(Atom.keepAlive)
|
|
195
208
|
|
|
196
209
|
const activeModalAtom = Atom.make<Modal>(initialModal)
|
|
@@ -324,6 +337,9 @@ const pullRequestDiffAtom = Atom.family((key: string) => {
|
|
|
324
337
|
const listPullRequestCommentsAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
325
338
|
GitHubService.use((github) => github.listPullRequestComments(input.repository, input.number))
|
|
326
339
|
)
|
|
340
|
+
const listPullRequestConversationAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
341
|
+
GitHubService.use((github) => github.listPullRequestConversation(input.repository, input.number))
|
|
342
|
+
)
|
|
327
343
|
const getPullRequestMergeInfoAtom = githubRuntime.fn<{ readonly repository: string; readonly number: number }>()((input) =>
|
|
328
344
|
GitHubService.use((github) => github.getPullRequestMergeInfo(input.repository, input.number))
|
|
329
345
|
)
|
|
@@ -381,12 +397,13 @@ const pullRequestDiffAtomKey = pullRequestRevisionAtomKey
|
|
|
381
397
|
const parsePullRequestDetailAtomKey = (key: string) => parsePullRequestRevisionAtomKey(key, "detail")
|
|
382
398
|
const parsePullRequestDiffAtomKey = (key: string) => parsePullRequestRevisionAtomKey(key, "diff")
|
|
383
399
|
|
|
384
|
-
const isShiftG = (key: { readonly name: string; readonly shift?: boolean }) => key.name === "G" || key.name === "g" && key.shift
|
|
385
400
|
|
|
386
|
-
|
|
401
|
+
|
|
402
|
+
const diffCommentThreadMapKey = (diffKey: string, location: Pick<PullRequestReviewComment, "path" | "side" | "line">) =>
|
|
403
|
+
`${diffKey}:${diffCommentLocationKey(location)}`
|
|
387
404
|
|
|
388
405
|
const diffCommentThreadKey = (pullRequest: PullRequestItem, comment: Pick<PullRequestReviewComment, "path" | "side" | "line">) =>
|
|
389
|
-
|
|
406
|
+
diffCommentThreadMapKey(pullRequestDiffKey(pullRequest), comment)
|
|
390
407
|
|
|
391
408
|
const groupDiffCommentThreads = (pullRequest: PullRequestItem, comments: readonly PullRequestReviewComment[]) => {
|
|
392
409
|
const threads: Record<string, PullRequestReviewComment[]> = {}
|
|
@@ -411,13 +428,43 @@ const originalDiffLineColor = (anchor: DiffCommentAnchor): DiffLineColorConfig =
|
|
|
411
428
|
return { gutter: colors.diff.lineNumberBg, content: colors.diff.contextBg }
|
|
412
429
|
}
|
|
413
430
|
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
431
|
+
const selectedDiffCommentAccentByKind = {
|
|
432
|
+
addition: () => colors.status.passing,
|
|
433
|
+
deletion: () => colors.status.failing,
|
|
434
|
+
context: () => colors.muted,
|
|
435
|
+
} satisfies Record<DiffCommentKind, () => string>
|
|
436
|
+
|
|
437
|
+
const selectedDiffCommentAccent = (kind: DiffCommentKind) => selectedDiffCommentAccentByKind[kind]()
|
|
438
|
+
|
|
439
|
+
const mixDiffLineContentColor = (base: string, accent: string, amount: number) =>
|
|
440
|
+
mixHex(base === "transparent" ? colors.background : base, accent, amount)
|
|
441
|
+
|
|
442
|
+
const diffCommentLineColor = (anchor: DiffCommentAnchor, kind: "selected" | "range" | "thread"): DiffLineColorConfig => {
|
|
443
|
+
const original = originalDiffLineColor(anchor)
|
|
444
|
+
const accent = kind === "thread" ? colors.status.pending : selectedDiffCommentAccent(anchor.kind)
|
|
445
|
+
if (kind === "thread") return { ...original, gutter: mixHex(original.gutter, accent, 0.45) }
|
|
446
|
+
return {
|
|
447
|
+
gutter: mixHex(original.gutter, accent, kind === "selected" ? 0.68 : 0.42),
|
|
448
|
+
content: mixDiffLineContentColor(original.content, accent, kind === "selected" ? 0.2 : 0.1),
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const sameDiffCommentTarget = (left: DiffCommentAnchor, right: DiffCommentAnchor) =>
|
|
453
|
+
left.path === right.path && left.side === right.side
|
|
454
|
+
|
|
455
|
+
const diffCommentRangeSelection = (start: StackedDiffCommentAnchor | null, end: StackedDiffCommentAnchor | null): DiffCommentRangeSelection | null => {
|
|
456
|
+
if (!start || !end || !sameDiffCommentTarget(start, end)) return null
|
|
457
|
+
return start.line <= end.line ? { start, end } : { start: end, end: start }
|
|
419
458
|
}
|
|
420
459
|
|
|
460
|
+
const diffCommentRangeContains = (range: DiffCommentRangeSelection, anchor: StackedDiffCommentAnchor) =>
|
|
461
|
+
sameDiffCommentTarget(range.start, anchor) && anchor.line >= range.start.line && anchor.line <= range.end.line
|
|
462
|
+
|
|
463
|
+
const diffCommentRangeLabel = (range: DiffCommentRangeSelection) =>
|
|
464
|
+
range.start.line === range.end.line
|
|
465
|
+
? diffCommentAnchorLabel(range.end)
|
|
466
|
+
: `${diffCommentSideLabel(range.end)} ${diffCommentLineLabel(range.start)}-${diffCommentLineLabel(range.end)}`
|
|
467
|
+
|
|
421
468
|
const diffSideTargets = (diff: DiffRenderable, anchor: DiffCommentAnchor, view: DiffView) => {
|
|
422
469
|
const withSides = diff as unknown as DiffRenderableRuntimeSides
|
|
423
470
|
if (view === "split") {
|
|
@@ -488,7 +535,6 @@ export const App = () => {
|
|
|
488
535
|
const [filterQuery, setFilterQuery] = useAtom(filterQueryAtom)
|
|
489
536
|
const [filterDraft, setFilterDraft] = useAtom(filterDraftAtom)
|
|
490
537
|
const [filterMode, setFilterMode] = useAtom(filterModeAtom)
|
|
491
|
-
const [pendingG, setPendingG] = useAtom(pendingGAtom)
|
|
492
538
|
const [detailFullView, setDetailFullView] = useAtom(detailFullViewAtom)
|
|
493
539
|
const setDetailScrollOffset = useAtomSet(detailScrollOffsetAtom)
|
|
494
540
|
const [diffFullView, setDiffFullView] = useAtom(diffFullViewAtom)
|
|
@@ -496,10 +542,14 @@ export const App = () => {
|
|
|
496
542
|
const [diffScrollTop, setDiffScrollTop] = useAtom(diffScrollTopAtom)
|
|
497
543
|
const [diffRenderView, setDiffRenderView] = useAtom(diffRenderViewAtom)
|
|
498
544
|
const [diffWrapMode, setDiffWrapMode] = useAtom(diffWrapModeAtom)
|
|
499
|
-
const [
|
|
545
|
+
const [diffWhitespaceMode, setDiffWhitespaceMode] = useAtom(diffWhitespaceModeAtom)
|
|
500
546
|
const [diffCommentAnchorIndex, setDiffCommentAnchorIndex] = useAtom(diffCommentAnchorIndexAtom)
|
|
547
|
+
const [diffPreferredSide, setDiffPreferredSide] = useAtom(diffPreferredSideAtom)
|
|
548
|
+
const [diffCommentRangeStartIndex, setDiffCommentRangeStartIndex] = useAtom(diffCommentRangeStartIndexAtom)
|
|
501
549
|
const [diffCommentThreads, setDiffCommentThreads] = useAtom(diffCommentThreadsAtom)
|
|
502
550
|
const setDiffCommentsLoaded = useAtomSet(diffCommentsLoadedAtom)
|
|
551
|
+
const setPullRequestConversation = useAtomSet(pullRequestConversationAtom)
|
|
552
|
+
const setPullRequestConversationLoaded = useAtomSet(pullRequestConversationLoadedAtom)
|
|
503
553
|
const setPullRequestDiffCache = useAtomSet(pullRequestDiffCacheAtom)
|
|
504
554
|
const [activeModal, setActiveModal] = useAtom(activeModalAtom)
|
|
505
555
|
const [themeId, setThemeId] = useAtom(themeIdAtom)
|
|
@@ -551,6 +601,7 @@ export const App = () => {
|
|
|
551
601
|
const [refreshCompletionMessage, setRefreshCompletionMessage] = useState<string | null>(null)
|
|
552
602
|
const [refreshStartedAt, setRefreshStartedAt] = useState<number | null>(null)
|
|
553
603
|
const [terminalFocused, setTerminalFocused] = useState(true)
|
|
604
|
+
const [startupLoadComplete, setStartupLoadComplete] = useState(false)
|
|
554
605
|
const [loadingMoreKey, setLoadingMoreKey] = useState<string | null>(null)
|
|
555
606
|
const usernameResult = useAtomValue(usernameAtom)
|
|
556
607
|
const loadRepoLabels = useAtomSet(listRepoLabelsAtom, { mode: "promise" })
|
|
@@ -559,6 +610,7 @@ export const App = () => {
|
|
|
559
610
|
const removePullRequestLabel = useAtomSet(removePullRequestLabelAtom, { mode: "promise" })
|
|
560
611
|
const toggleDraftStatus = useAtomSet(toggleDraftAtom, { mode: "promise" })
|
|
561
612
|
const listPullRequestComments = useAtomSet(listPullRequestCommentsAtom, { mode: "promise" })
|
|
613
|
+
const listPullRequestConversation = useAtomSet(listPullRequestConversationAtom, { mode: "promise" })
|
|
562
614
|
const getPullRequestMergeInfo = useAtomSet(getPullRequestMergeInfoAtom, { mode: "promise" })
|
|
563
615
|
const mergePullRequest = useAtomSet(mergePullRequestAtom, { mode: "promise" })
|
|
564
616
|
const closePullRequest = useAtomSet(closePullRequestAtom, { mode: "promise" })
|
|
@@ -579,7 +631,6 @@ export const App = () => {
|
|
|
579
631
|
const wideDetailLines = Math.max(8, terminalHeight - 8)
|
|
580
632
|
const wideBodyHeight = Math.max(8, terminalHeight - 4)
|
|
581
633
|
const noticeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
582
|
-
const pendingGTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
583
634
|
const diffPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
584
635
|
const detailPrefetchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
585
636
|
const detailHydrationRef = useRef(new Map<string, DetailHydration>())
|
|
@@ -589,7 +640,7 @@ export const App = () => {
|
|
|
589
640
|
const terminalFocusedRef = useRef(true)
|
|
590
641
|
const terminalWasBlurredRef = useRef(false)
|
|
591
642
|
const pullRequestStatusRef = useRef<LoadStatus>("loading")
|
|
592
|
-
const refreshPullRequestsRef = useRef<(message?: string) => void>(() => {})
|
|
643
|
+
const refreshPullRequestsRef = useRef<(message?: string, options?: { readonly resetTransientState?: boolean }) => void>(() => {})
|
|
593
644
|
const maybeRefreshPullRequestsRef = useRef<(minimumAgeMs: number) => void>(() => {})
|
|
594
645
|
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
595
646
|
const detailPreviewScrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
@@ -620,9 +671,6 @@ export const App = () => {
|
|
|
620
671
|
if (noticeTimeoutRef.current !== null) {
|
|
621
672
|
clearTimeout(noticeTimeoutRef.current)
|
|
622
673
|
}
|
|
623
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
624
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
625
|
-
}
|
|
626
674
|
if (diffPrefetchTimeoutRef.current !== null) {
|
|
627
675
|
clearTimeout(diffPrefetchTimeoutRef.current)
|
|
628
676
|
}
|
|
@@ -634,7 +682,7 @@ export const App = () => {
|
|
|
634
682
|
const pullRequestLoad = useAtomValue(pullRequestLoadAtom)
|
|
635
683
|
const pullRequests = useAtomValue(displayedPullRequestsAtom)
|
|
636
684
|
const pullRequestStatus = useAtomValue(pullRequestStatusAtom)
|
|
637
|
-
const isInitialLoading = pullRequestStatus === "loading" && pullRequests.length === 0
|
|
685
|
+
const isInitialLoading = !startupLoadComplete && pullRequestStatus === "loading" && pullRequests.length === 0
|
|
638
686
|
const pullRequestError = AsyncResult.isFailure(pullRequestResult) ? errorMessage(Cause.squash(pullRequestResult.cause)) : null
|
|
639
687
|
const username = AsyncResult.isSuccess(usernameResult) ? usernameResult.value : null
|
|
640
688
|
pullRequestStatusRef.current = pullRequestStatus
|
|
@@ -644,6 +692,8 @@ export const App = () => {
|
|
|
644
692
|
const visibleGroups = useAtomValue(visibleGroupsAtom)
|
|
645
693
|
const visiblePullRequests = useAtomValue(visiblePullRequestsAtom)
|
|
646
694
|
const selectedPullRequest = useAtomValue(selectedPullRequestAtom)
|
|
695
|
+
const pullRequestConversation = useAtomValue(pullRequestConversationAtom)
|
|
696
|
+
const pullRequestConversationLoaded = useAtomValue(pullRequestConversationLoadedAtom)
|
|
647
697
|
const selectedRepository = viewRepository(activeView)
|
|
648
698
|
const activeViews = activePullRequestViews(activeView)
|
|
649
699
|
const currentQueueCacheKey = viewCacheKey(activeView)
|
|
@@ -662,22 +712,60 @@ export const App = () => {
|
|
|
662
712
|
}), [visibleGroups, pullRequestStatus, pullRequestError, visibleFilterText, filterMode, filterQuery, loadedPullRequestCount, hasMorePullRequests, isLoadingMorePullRequests])
|
|
663
713
|
const selectedPullRequestRowIndex = pullRequestListRowIndex(pullRequestListRows, selectedPullRequest?.url ?? null)
|
|
664
714
|
const selectedDiffKey = useAtomValue(selectedDiffKeyAtom)
|
|
715
|
+
const selectedConversationItems = selectedDiffKey ? pullRequestConversation[selectedDiffKey] ?? [] : []
|
|
716
|
+
const selectedConversationStatus: DetailConversationStatus = selectedDiffKey ? pullRequestConversationLoaded[selectedDiffKey] ?? "idle" : "idle"
|
|
665
717
|
const selectedDiffState = useAtomValue(selectedDiffStateAtom)
|
|
666
718
|
const effectiveDiffRenderView = contentWidth >= 100 ? diffRenderView : "unified"
|
|
667
|
-
const readyDiffFiles =
|
|
719
|
+
const readyDiffFiles = useMemo(
|
|
720
|
+
() => selectedDiffState?._tag === "Ready"
|
|
721
|
+
? diffWhitespaceMode === "ignore" ? minimizeWhitespaceDiffFiles(selectedDiffState.files) : selectedDiffState.files
|
|
722
|
+
: [],
|
|
723
|
+
[selectedDiffState, diffWhitespaceMode],
|
|
724
|
+
)
|
|
725
|
+
const displayedDiffState = useMemo(
|
|
726
|
+
() => selectedDiffState?._tag === "Ready"
|
|
727
|
+
? PullRequestDiffState.Ready({ patch: readyDiffFiles.map((file) => file.patch).join("\n"), files: readyDiffFiles })
|
|
728
|
+
: selectedDiffState,
|
|
729
|
+
[selectedDiffState, readyDiffFiles],
|
|
730
|
+
)
|
|
668
731
|
const stackedDiffFiles = useMemo(() => buildStackedDiffFiles(readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth), [readyDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth])
|
|
669
732
|
const diffCommentAnchors = useMemo(
|
|
670
733
|
() => diffFullView ? getStackedDiffCommentAnchors(stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth) : [],
|
|
671
734
|
[diffFullView, stackedDiffFiles, effectiveDiffRenderView, diffWrapMode, contentWidth],
|
|
672
735
|
)
|
|
673
|
-
const
|
|
674
|
-
const
|
|
736
|
+
const selectedDiffCommentAnchorIndex = Math.max(0, Math.min(diffCommentAnchorIndex, diffCommentAnchors.length - 1))
|
|
737
|
+
const selectedDiffCommentAnchor = diffCommentAnchors[selectedDiffCommentAnchorIndex] ?? null
|
|
738
|
+
const diffCommentRangeStartAnchor = diffCommentRangeStartIndex === null
|
|
739
|
+
? null
|
|
740
|
+
: diffCommentAnchors[Math.max(0, Math.min(diffCommentRangeStartIndex, diffCommentAnchors.length - 1))] ?? null
|
|
741
|
+
const selectedDiffCommentRange = useMemo(
|
|
742
|
+
() => diffCommentRangeSelection(diffCommentRangeStartAnchor, selectedDiffCommentAnchor),
|
|
743
|
+
[diffCommentRangeStartAnchor, selectedDiffCommentAnchor],
|
|
744
|
+
)
|
|
745
|
+
const selectedDiffCommentRangeAnchors = useMemo(
|
|
746
|
+
() => selectedDiffCommentRange
|
|
747
|
+
? diffCommentAnchors.filter((anchor) => diffCommentRangeContains(selectedDiffCommentRange, anchor))
|
|
748
|
+
: [],
|
|
749
|
+
[diffCommentAnchors, selectedDiffCommentRange],
|
|
750
|
+
)
|
|
751
|
+
const diffCommentRangeActive = selectedDiffCommentRange !== null
|
|
752
|
+
const selectedDiffCommentLabel = selectedDiffCommentRange
|
|
753
|
+
? diffCommentRangeLabel(selectedDiffCommentRange)
|
|
754
|
+
: selectedDiffCommentAnchor ? diffCommentAnchorLabel(selectedDiffCommentAnchor) : null
|
|
755
|
+
const selectedDiffCommentThreadKey = selectedDiffKey && selectedDiffCommentAnchor ? diffCommentThreadMapKey(selectedDiffKey, selectedDiffCommentAnchor) : null
|
|
675
756
|
const selectedDiffCommentThread = selectedDiffCommentThreadKey ? diffCommentThreads[selectedDiffCommentThreadKey] ?? [] : []
|
|
676
757
|
const diffLineColorContextKey = selectedDiffKey ? `${selectedDiffKey}:${effectiveDiffRenderView}:${diffWrapMode}` : null
|
|
677
|
-
const
|
|
678
|
-
()
|
|
679
|
-
|
|
680
|
-
|
|
758
|
+
const diffCommentThreadAnchors = useMemo(() => {
|
|
759
|
+
if (!selectedDiffKey) return [] as readonly StackedDiffCommentAnchor[]
|
|
760
|
+
const seen = new Set<string>()
|
|
761
|
+
return diffCommentAnchors.filter((anchor) => {
|
|
762
|
+
const key = diffCommentLocationKey(anchor)
|
|
763
|
+
if (seen.has(key)) return false
|
|
764
|
+
if ((diffCommentThreads[diffCommentThreadMapKey(selectedDiffKey, anchor)]?.length ?? 0) === 0) return false
|
|
765
|
+
seen.add(key)
|
|
766
|
+
return true
|
|
767
|
+
})
|
|
768
|
+
}, [diffCommentAnchors, diffCommentThreads, selectedDiffKey])
|
|
681
769
|
const groupStarts = useAtomValue(groupStartsAtom)
|
|
682
770
|
const getCurrentGroupIndex = (current: number) => {
|
|
683
771
|
if (groupStarts.length === 0) return 0
|
|
@@ -710,12 +798,36 @@ export const App = () => {
|
|
|
710
798
|
if (!pullRequest) return
|
|
711
799
|
setPullRequestOverrides((current) => ({ ...current, [url]: transform(pullRequest) }))
|
|
712
800
|
}
|
|
713
|
-
const
|
|
801
|
+
const markPullRequestCompleted = (pullRequest: PullRequestItem, state: "closed" | "merged") => {
|
|
802
|
+
setRecentlyCompletedPullRequests((current) => ({
|
|
803
|
+
...current,
|
|
804
|
+
[pullRequest.url]: {
|
|
805
|
+
...pullRequest,
|
|
806
|
+
state,
|
|
807
|
+
autoMergeEnabled: false,
|
|
808
|
+
},
|
|
809
|
+
}))
|
|
810
|
+
}
|
|
811
|
+
const restoreOptimisticPullRequest = (pullRequest: PullRequestItem) => {
|
|
812
|
+
setRecentlyCompletedPullRequests((current) => {
|
|
813
|
+
if (!(pullRequest.url in current)) return current
|
|
814
|
+
const next = { ...current }
|
|
815
|
+
delete next[pullRequest.url]
|
|
816
|
+
return next
|
|
817
|
+
})
|
|
818
|
+
updatePullRequest(pullRequest.url, () => pullRequest)
|
|
819
|
+
}
|
|
820
|
+
const refreshPullRequests = (message?: string, options: { readonly resetTransientState?: boolean } = {}) => {
|
|
714
821
|
refreshGenerationRef.current += 1
|
|
715
822
|
detailHydrationRef.current.clear()
|
|
716
823
|
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
717
824
|
setLoadingMoreKey(null)
|
|
718
825
|
setPullRequestOverrides({})
|
|
826
|
+
if (options.resetTransientState) {
|
|
827
|
+
setRecentlyCompletedPullRequests({})
|
|
828
|
+
setPullRequestConversation({})
|
|
829
|
+
setPullRequestConversationLoaded({})
|
|
830
|
+
}
|
|
719
831
|
if (message) {
|
|
720
832
|
setNotice(null)
|
|
721
833
|
setRefreshCompletionMessage(message)
|
|
@@ -736,7 +848,7 @@ export const App = () => {
|
|
|
736
848
|
setLoadingMoreKey(null)
|
|
737
849
|
setDetailFullView(false)
|
|
738
850
|
setDiffFullView(false)
|
|
739
|
-
|
|
851
|
+
setDiffCommentRangeStartIndex(null)
|
|
740
852
|
setFilterDraft(filterQuery)
|
|
741
853
|
setNotice(null)
|
|
742
854
|
setRefreshCompletionMessage(null)
|
|
@@ -818,6 +930,29 @@ export const App = () => {
|
|
|
818
930
|
})
|
|
819
931
|
return true
|
|
820
932
|
}
|
|
933
|
+
const loadPullRequestConversation = (pullRequest: PullRequestItem, force = false) => {
|
|
934
|
+
const key = pullRequestDiffKey(pullRequest)
|
|
935
|
+
const previousLoadState = registry.get(pullRequestConversationLoadedAtom)[key]
|
|
936
|
+
if (!force && previousLoadState) return
|
|
937
|
+
const generation = refreshGenerationRef.current
|
|
938
|
+
setPullRequestConversationLoaded((current) => ({ ...current, [key]: "loading" }))
|
|
939
|
+
void listPullRequestConversation({ repository: pullRequest.repository, number: pullRequest.number })
|
|
940
|
+
.then((items) => {
|
|
941
|
+
if (generation !== refreshGenerationRef.current) return
|
|
942
|
+
setPullRequestConversation((current) => ({ ...current, [key]: items }))
|
|
943
|
+
setPullRequestConversationLoaded((current) => ({ ...current, [key]: "ready" }))
|
|
944
|
+
})
|
|
945
|
+
.catch((error) => {
|
|
946
|
+
if (generation !== refreshGenerationRef.current) return
|
|
947
|
+
setPullRequestConversationLoaded((current) => {
|
|
948
|
+
if (previousLoadState === "ready") return { ...current, [key]: previousLoadState }
|
|
949
|
+
const next = { ...current }
|
|
950
|
+
delete next[key]
|
|
951
|
+
return next
|
|
952
|
+
})
|
|
953
|
+
flashNotice(errorMessage(error))
|
|
954
|
+
})
|
|
955
|
+
}
|
|
821
956
|
maybeRefreshPullRequestsRef.current = (minimumAgeMs) => {
|
|
822
957
|
if (!terminalFocusedRef.current || pullRequestStatusRef.current === "loading") return
|
|
823
958
|
const lastRefreshAt = lastPullRequestRefreshAtRef.current
|
|
@@ -933,20 +1068,30 @@ export const App = () => {
|
|
|
933
1068
|
setDiffFileIndex(0)
|
|
934
1069
|
setDiffScrollTop(0)
|
|
935
1070
|
setDiffCommentAnchorIndex(0)
|
|
1071
|
+
setDiffPreferredSide(null)
|
|
1072
|
+
setDiffCommentRangeStartIndex(null)
|
|
936
1073
|
detailPreviewScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
937
1074
|
}, [selectedIndex])
|
|
938
1075
|
|
|
1076
|
+
useEffect(() => {
|
|
1077
|
+
setDiffFileIndex((current) => safeDiffFileIndex(readyDiffFiles, current))
|
|
1078
|
+
}, [readyDiffFiles.length])
|
|
1079
|
+
|
|
939
1080
|
useEffect(() => {
|
|
940
1081
|
setDiffCommentAnchorIndex((current) => {
|
|
941
1082
|
if (diffCommentAnchors.length === 0) return 0
|
|
942
1083
|
return Math.max(0, Math.min(current, diffCommentAnchors.length - 1))
|
|
943
1084
|
})
|
|
1085
|
+
setDiffCommentRangeStartIndex((current) => {
|
|
1086
|
+
if (current === null || diffCommentAnchors.length === 0) return null
|
|
1087
|
+
return Math.max(0, Math.min(current, diffCommentAnchors.length - 1))
|
|
1088
|
+
})
|
|
944
1089
|
}, [diffCommentAnchors.length])
|
|
945
1090
|
|
|
946
1091
|
useEffect(() => {
|
|
947
|
-
if (!
|
|
1092
|
+
if (!diffFullView || !selectedDiffCommentAnchor) return
|
|
948
1093
|
setDiffFileIndex((current) => current === selectedDiffCommentAnchor.fileIndex ? current : selectedDiffCommentAnchor.fileIndex)
|
|
949
|
-
}, [
|
|
1094
|
+
}, [diffFullView, selectedDiffCommentAnchor?.fileIndex])
|
|
950
1095
|
|
|
951
1096
|
useEffect(() => {
|
|
952
1097
|
const previous = diffCommentLineColorsRef.current
|
|
@@ -959,27 +1104,28 @@ export const App = () => {
|
|
|
959
1104
|
|
|
960
1105
|
const nextEntries: AppliedDiffLineColor[] = []
|
|
961
1106
|
const appliedKeys = new Set<string>()
|
|
962
|
-
const applyLineColor = (anchor: StackedDiffCommentAnchor,
|
|
1107
|
+
const applyLineColor = (anchor: StackedDiffCommentAnchor, color: DiffLineColorConfig, override = false) => {
|
|
963
1108
|
const key = `${effectiveDiffRenderView}:${anchor.side}:${anchor.renderLine}`
|
|
964
1109
|
if (appliedKeys.has(key) && !override) return
|
|
965
1110
|
appliedKeys.add(key)
|
|
966
1111
|
const entry = { anchor, view: effectiveDiffRenderView } satisfies AppliedDiffLineColor
|
|
967
1112
|
const diff = diffRenderableRefs.current.get(anchor.fileIndex)
|
|
968
|
-
if (diff) setDiffCommentLineColor(diff, entry,
|
|
1113
|
+
if (diff) setDiffCommentLineColor(diff, entry, color)
|
|
969
1114
|
if (!nextEntries.some((existing) => existing.view === entry.view && existing.anchor.side === anchor.side && existing.anchor.renderLine === anchor.renderLine)) {
|
|
970
1115
|
nextEntries.push(entry)
|
|
971
1116
|
}
|
|
972
1117
|
}
|
|
973
1118
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1119
|
+
for (const anchor of diffCommentThreadAnchors) {
|
|
1120
|
+
applyLineColor(anchor, diffCommentLineColor(anchor, "thread"))
|
|
1121
|
+
}
|
|
1122
|
+
if (selectedDiffCommentRangeAnchors.length > 0) {
|
|
1123
|
+
for (const anchor of selectedDiffCommentRangeAnchors) {
|
|
1124
|
+
applyLineColor(anchor, diffCommentLineColor(anchor, "range"), true)
|
|
979
1125
|
}
|
|
980
1126
|
}
|
|
981
|
-
if (
|
|
982
|
-
applyLineColor(selectedDiffCommentAnchor,
|
|
1127
|
+
if (selectedDiffCommentAnchor) {
|
|
1128
|
+
applyLineColor(selectedDiffCommentAnchor, diffCommentLineColor(selectedDiffCommentAnchor, "selected"), true)
|
|
983
1129
|
if (suppressNextDiffCommentScrollRef.current) {
|
|
984
1130
|
suppressNextDiffCommentScrollRef.current = false
|
|
985
1131
|
} else {
|
|
@@ -989,25 +1135,49 @@ export const App = () => {
|
|
|
989
1135
|
suppressNextDiffCommentScrollRef.current = false
|
|
990
1136
|
}
|
|
991
1137
|
diffCommentLineColorsRef.current = { contextKey: diffLineColorContextKey, entries: nextEntries }
|
|
992
|
-
}, [
|
|
1138
|
+
}, [selectedDiffCommentAnchor?.renderLine, selectedDiffCommentAnchor?.localRenderLine, selectedDiffCommentAnchor?.side, selectedDiffCommentAnchor?.fileIndex, selectedDiffCommentRangeAnchors, diffLineColorContextKey, effectiveDiffRenderView, diffCommentThreadAnchors])
|
|
1139
|
+
|
|
1140
|
+
// Scroll the selected line into view when the diff view is opened. Previously
|
|
1141
|
+
// opentui's `focused` scrollbox did this auto-scroll on mount; with the keymap
|
|
1142
|
+
// migration the scrollbox is `focusable={false}` so we have to scroll explicitly.
|
|
1143
|
+
useEffect(() => {
|
|
1144
|
+
if (!diffFullView) return
|
|
1145
|
+
if (!selectedDiffCommentAnchor) return
|
|
1146
|
+
ensureDiffLineVisible(selectedDiffCommentAnchor.renderLine)
|
|
1147
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1148
|
+
}, [diffFullView])
|
|
993
1149
|
const isHydratingPullRequestDetails = pullRequestStatus === "ready" && selectedPullRequest?.state === "open" && !selectedPullRequest.detailLoaded
|
|
994
1150
|
const isRefreshingPullRequests = pullRequestResult.waiting && pullRequestLoad !== null
|
|
995
|
-
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?._tag === "Loading"
|
|
996
|
-
const loadingIndicator =
|
|
1151
|
+
const hasActiveLoadingIndicator = pullRequestResult.waiting || isHydratingPullRequestDetails || isLoadingMorePullRequests || labelModal.loading || closeModal.running || mergeModal.loading || mergeModal.running || selectedDiffState?._tag === "Loading"
|
|
1152
|
+
const loadingIndicator = SPINNER_FRAMES[loadingFrame % SPINNER_FRAMES.length]!
|
|
997
1153
|
|
|
998
1154
|
useEffect(() => {
|
|
999
1155
|
if (!hasActiveLoadingIndicator) return
|
|
1000
1156
|
const interval = globalThis.setInterval(() => {
|
|
1001
|
-
setLoadingFrame((current) =>
|
|
1157
|
+
setLoadingFrame((current) => current + 1)
|
|
1002
1158
|
}, 120)
|
|
1003
1159
|
return () => globalThis.clearInterval(interval)
|
|
1004
1160
|
}, [hasActiveLoadingIndicator])
|
|
1005
1161
|
|
|
1162
|
+
useEffect(() => {
|
|
1163
|
+
if (isInitialLoading) setLoadingFrame(0)
|
|
1164
|
+
}, [isInitialLoading])
|
|
1165
|
+
|
|
1166
|
+
useEffect(() => {
|
|
1167
|
+
if (startupLoadComplete || pullRequestStatus === "loading") return
|
|
1168
|
+
setStartupLoadComplete(true)
|
|
1169
|
+
}, [startupLoadComplete, pullRequestStatus])
|
|
1170
|
+
|
|
1006
1171
|
useEffect(() => {
|
|
1007
1172
|
if (pullRequestStatus !== "ready" || !selectedPullRequest) return
|
|
1008
1173
|
hydratePullRequestDetails(selectedPullRequest, true)
|
|
1009
1174
|
}, [pullRequestStatus, selectedPullRequest?.url, selectedPullRequest?.headRefOid, selectedPullRequest?.state, selectedPullRequest?.detailLoaded, selectedPullRequest?.repository, selectedPullRequest?.number])
|
|
1010
1175
|
|
|
1176
|
+
useEffect(() => {
|
|
1177
|
+
if (pullRequestStatus !== "ready" || !selectedPullRequest) return
|
|
1178
|
+
loadPullRequestConversation(selectedPullRequest)
|
|
1179
|
+
}, [pullRequestStatus, selectedPullRequest?.url, selectedPullRequest?.headRefOid, selectedPullRequest?.repository, selectedPullRequest?.number])
|
|
1180
|
+
|
|
1011
1181
|
useEffect(() => {
|
|
1012
1182
|
if (detailPrefetchTimeoutRef.current !== null) clearTimeout(detailPrefetchTimeoutRef.current)
|
|
1013
1183
|
if (pullRequestStatus !== "ready" || visiblePullRequests.length === 0) return
|
|
@@ -1041,7 +1211,7 @@ export const App = () => {
|
|
|
1041
1211
|
title: `${loadingIndicator} Loading pull request details`,
|
|
1042
1212
|
hint: `${selectedPullRequest.repository} #${selectedPullRequest.number}`,
|
|
1043
1213
|
} : detailPlaceholderContent
|
|
1044
|
-
const detailJunctions = isSelectedPullRequestDetailLoading ? [] : getDetailJunctionRows(selectedPullRequest, rightPaneWidth, true)
|
|
1214
|
+
const detailJunctions = isSelectedPullRequestDetailLoading ? [] : getDetailJunctionRows(selectedPullRequest, rightPaneWidth, true, rightContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
1045
1215
|
|
|
1046
1216
|
const halfPage = Math.max(1, Math.floor(wideBodyHeight / 2))
|
|
1047
1217
|
|
|
@@ -1135,9 +1305,11 @@ export const App = () => {
|
|
|
1135
1305
|
diffCommentLineColorsRef.current = { contextKey: null, entries: [] }
|
|
1136
1306
|
setDiffFullView(true)
|
|
1137
1307
|
setDetailFullView(false)
|
|
1138
|
-
setDiffCommentMode(false)
|
|
1139
1308
|
setDiffFileIndex(0)
|
|
1140
1309
|
setDiffScrollTop(0)
|
|
1310
|
+
setDiffCommentAnchorIndex(0)
|
|
1311
|
+
setDiffPreferredSide(null)
|
|
1312
|
+
setDiffCommentRangeStartIndex(null)
|
|
1141
1313
|
setDiffRenderView(contentWidth >= 100 ? "split" : "unified")
|
|
1142
1314
|
diffScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
1143
1315
|
loadPullRequestDiff(selectedPullRequest, { includeComments: true })
|
|
@@ -1158,53 +1330,13 @@ export const App = () => {
|
|
|
1158
1330
|
const scrollTop = diffScrollRef.current?.scrollTop
|
|
1159
1331
|
if (scrollTop === undefined || stackedDiffFiles.length === 0) return
|
|
1160
1332
|
setDiffScrollTop((current) => current === scrollTop ? current : scrollTop)
|
|
1161
|
-
const nextIndex =
|
|
1333
|
+
const nextIndex = Math.max(0, stackedDiffFileIndexAtLine(stackedDiffFiles, scrollTop))
|
|
1162
1334
|
setDiffFileIndex((current) => current === nextIndex ? current : nextIndex)
|
|
1163
1335
|
}
|
|
1164
1336
|
|
|
1165
|
-
const scrollDiffBy = (y: number) => {
|
|
1166
|
-
diffScrollRef.current?.scrollBy({ x: 0, y })
|
|
1167
|
-
syncDiffScrollState()
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
const scrollDiffTo = (y: number) => {
|
|
1171
|
-
diffScrollRef.current?.scrollTo({ x: 0, y })
|
|
1172
|
-
syncDiffScrollState()
|
|
1173
|
-
}
|
|
1174
1337
|
const scrollDetailPreviewBy = (y: number) => detailPreviewScrollRef.current?.scrollBy({ x: 0, y })
|
|
1175
1338
|
const scrollDetailPreviewTo = (y: number) => detailPreviewScrollRef.current?.scrollTo({ x: 0, y })
|
|
1176
1339
|
|
|
1177
|
-
const clearPendingGTimeout = () => {
|
|
1178
|
-
if (pendingGTimeoutRef.current !== null) {
|
|
1179
|
-
clearTimeout(pendingGTimeoutRef.current)
|
|
1180
|
-
pendingGTimeoutRef.current = null
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
const handleVimGoto = (key: { readonly name: string; readonly shift?: boolean }, gotoStart: () => void, gotoEnd: () => void): boolean => {
|
|
1185
|
-
if (isShiftG(key)) {
|
|
1186
|
-
gotoEnd()
|
|
1187
|
-
setPendingG(false)
|
|
1188
|
-
clearPendingGTimeout()
|
|
1189
|
-
return true
|
|
1190
|
-
}
|
|
1191
|
-
if (key.name === "g") {
|
|
1192
|
-
if (pendingG) {
|
|
1193
|
-
gotoStart()
|
|
1194
|
-
setPendingG(false)
|
|
1195
|
-
clearPendingGTimeout()
|
|
1196
|
-
} else {
|
|
1197
|
-
setPendingG(true)
|
|
1198
|
-
pendingGTimeoutRef.current = setTimeout(() => {
|
|
1199
|
-
setPendingG(false)
|
|
1200
|
-
pendingGTimeoutRef.current = null
|
|
1201
|
-
}, 500)
|
|
1202
|
-
}
|
|
1203
|
-
return true
|
|
1204
|
-
}
|
|
1205
|
-
return false
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
1340
|
const ensureDiffLineVisible = (line: number) => {
|
|
1209
1341
|
const scroll = diffScrollRef.current
|
|
1210
1342
|
if (!scroll) return
|
|
@@ -1226,37 +1358,69 @@ export const App = () => {
|
|
|
1226
1358
|
if (readyDiffFiles.length === 0) return
|
|
1227
1359
|
const nextIndex = safeDiffFileIndex(readyDiffFiles, diffFileIndex + delta)
|
|
1228
1360
|
setDiffFileIndex(nextIndex)
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1361
|
+
setDiffCommentRangeStartIndex(null)
|
|
1362
|
+
const targetSide = diffPreferredSide ?? selectedDiffCommentAnchor?.side
|
|
1363
|
+
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex && anchor.side === targetSide)
|
|
1364
|
+
?? diffCommentAnchors.find((anchor) => anchor.fileIndex === nextIndex)
|
|
1365
|
+
if (nextAnchor) setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1234
1366
|
scrollToDiffFile(nextIndex)
|
|
1235
1367
|
}
|
|
1236
1368
|
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
const currentAnchor = selectedDiffCommentAnchor ?? diffCommentAnchors[0]
|
|
1247
|
-
const currentRowIndex = Math.max(0, currentAnchor ? diffCommentRows.indexOf(currentAnchor.renderLine) : 0)
|
|
1248
|
-
const nextRow = diffCommentRows[Math.max(0, Math.min(diffCommentRows.length - 1, currentRowIndex + delta))]
|
|
1249
|
-
if (nextRow === undefined) return
|
|
1250
|
-
const nextAnchor = diffCommentAnchors.find((anchor) => anchor.renderLine === nextRow && anchor.side === currentAnchor?.side)
|
|
1251
|
-
?? diffCommentAnchors.find((anchor) => anchor.renderLine === nextRow)
|
|
1369
|
+
const navigableDiffCommentAnchors = () => diffCommentRangeStartAnchor
|
|
1370
|
+
? diffCommentAnchors.filter((anchor) => sameDiffCommentTarget(anchor, diffCommentRangeStartAnchor))
|
|
1371
|
+
: diffCommentAnchors
|
|
1372
|
+
|
|
1373
|
+
const moveDiffCommentAnchor = (delta: number, options: { readonly preserveViewportRow?: boolean } = {}) => {
|
|
1374
|
+
const anchors = navigableDiffCommentAnchors()
|
|
1375
|
+
if (anchors.length === 0) return
|
|
1376
|
+
const currentAnchor = selectedDiffCommentAnchor && anchors.includes(selectedDiffCommentAnchor) ? selectedDiffCommentAnchor : anchors[0]
|
|
1377
|
+
const nextAnchor = verticalDiffAnchor(anchors, currentAnchor ?? null, delta, diffPreferredSide)
|
|
1252
1378
|
if (!nextAnchor) return
|
|
1379
|
+
if (options.preserveViewportRow) {
|
|
1380
|
+
const scroll = diffScrollRef.current
|
|
1381
|
+
if (scroll && currentAnchor) {
|
|
1382
|
+
const maxScreenOffset = Math.max(DIFF_STICKY_HEADER_LINES, scroll.viewport.height - 2)
|
|
1383
|
+
const screenOffset = Math.max(DIFF_STICKY_HEADER_LINES, Math.min(maxScreenOffset, currentAnchor.renderLine - scroll.scrollTop))
|
|
1384
|
+
const maxScrollTop = Math.max(0, scroll.scrollHeight - scroll.viewport.height)
|
|
1385
|
+
const nextTop = Math.max(0, Math.min(maxScrollTop, nextAnchor.renderLine - screenOffset))
|
|
1386
|
+
suppressNextDiffCommentScrollRef.current = true
|
|
1387
|
+
scroll.scrollTo({ x: 0, y: nextTop })
|
|
1388
|
+
syncDiffScrollState()
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1253
1391
|
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1254
1392
|
}
|
|
1255
1393
|
|
|
1394
|
+
const moveDiffCommentToBoundary = (boundary: "first" | "last") => {
|
|
1395
|
+
const anchors = navigableDiffCommentAnchors()
|
|
1396
|
+
const nextAnchor = boundary === "first" ? anchors[0] : anchors[anchors.length - 1]
|
|
1397
|
+
if (!nextAnchor) return
|
|
1398
|
+
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1399
|
+
setDiffFileIndex(nextAnchor.fileIndex)
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
const alignSelectedDiffCommentAnchor = (position: "top" | "center" | "bottom") => {
|
|
1403
|
+
if (!selectedDiffCommentAnchor) return
|
|
1404
|
+
const scroll = diffScrollRef.current
|
|
1405
|
+
if (!scroll) return
|
|
1406
|
+
const viewportHeight = Math.max(1, scroll.viewport.height)
|
|
1407
|
+
const offset = position === "top"
|
|
1408
|
+
? DIFF_STICKY_HEADER_LINES
|
|
1409
|
+
: position === "center"
|
|
1410
|
+
? Math.max(DIFF_STICKY_HEADER_LINES, Math.floor(viewportHeight / 2))
|
|
1411
|
+
: Math.max(DIFF_STICKY_HEADER_LINES, viewportHeight - 2)
|
|
1412
|
+
const maxScrollTop = Math.max(0, scroll.scrollHeight - viewportHeight)
|
|
1413
|
+
const nextTop = Math.max(0, Math.min(maxScrollTop, selectedDiffCommentAnchor.renderLine - offset))
|
|
1414
|
+
scroll.scrollTo({ x: 0, y: nextTop })
|
|
1415
|
+
syncDiffScrollState()
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1256
1418
|
const selectDiffCommentSide = (side: DiffCommentSide) => {
|
|
1419
|
+
setDiffPreferredSide(side)
|
|
1257
1420
|
if (!selectedDiffCommentAnchor) return
|
|
1258
|
-
const nextAnchor = diffCommentAnchors
|
|
1421
|
+
const nextAnchor = diffAnchorOnSide(diffCommentAnchors, selectedDiffCommentAnchor, side)
|
|
1259
1422
|
if (!nextAnchor) return
|
|
1423
|
+
setDiffCommentRangeStartIndex(null)
|
|
1260
1424
|
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1261
1425
|
}
|
|
1262
1426
|
|
|
@@ -1265,9 +1429,12 @@ export const App = () => {
|
|
|
1265
1429
|
const nextAnchor = (side ? lineAnchors.find((anchor) => anchor.side === side) : undefined) ?? lineAnchors[0]
|
|
1266
1430
|
if (!nextAnchor) return
|
|
1267
1431
|
suppressNextDiffCommentScrollRef.current = true
|
|
1432
|
+
setDiffPreferredSide(side ?? nextAnchor.side)
|
|
1433
|
+
if (diffCommentRangeStartAnchor && !sameDiffCommentTarget(diffCommentRangeStartAnchor, nextAnchor)) {
|
|
1434
|
+
setDiffCommentRangeStartIndex(null)
|
|
1435
|
+
}
|
|
1268
1436
|
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1269
1437
|
setDiffFileIndex(nextAnchor.fileIndex)
|
|
1270
|
-
setDiffCommentMode(true)
|
|
1271
1438
|
}
|
|
1272
1439
|
|
|
1273
1440
|
const editComment = (transform: (state: CommentEditorValue) => CommentEditorValue) => {
|
|
@@ -1288,6 +1455,39 @@ export const App = () => {
|
|
|
1288
1455
|
setCommentThreadModal({ scrollOffset: 0 })
|
|
1289
1456
|
}
|
|
1290
1457
|
|
|
1458
|
+
const openSelectedDiffComment = () => {
|
|
1459
|
+
if (diffCommentRangeActive) {
|
|
1460
|
+
openDiffCommentModal()
|
|
1461
|
+
return
|
|
1462
|
+
}
|
|
1463
|
+
if (selectedDiffCommentThread.length > 0) openDiffCommentThreadModal()
|
|
1464
|
+
else openDiffCommentModal()
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const toggleDiffCommentRange = () => {
|
|
1468
|
+
if (!selectedDiffCommentAnchor) return
|
|
1469
|
+
setDiffCommentRangeStartIndex((current) => current === null ? selectedDiffCommentAnchorIndex : null)
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const moveDiffCommentThread = (delta: 1 | -1) => {
|
|
1473
|
+
if (diffCommentThreadAnchors.length === 0) {
|
|
1474
|
+
flashNotice("No diff comments")
|
|
1475
|
+
return
|
|
1476
|
+
}
|
|
1477
|
+
const currentIndex = selectedDiffCommentAnchor
|
|
1478
|
+
? diffCommentThreadAnchors.findIndex((anchor) => diffCommentLocationKey(anchor) === diffCommentLocationKey(selectedDiffCommentAnchor))
|
|
1479
|
+
: -1
|
|
1480
|
+
const nextAnchor = currentIndex >= 0
|
|
1481
|
+
? diffCommentThreadAnchors[(currentIndex + delta + diffCommentThreadAnchors.length) % diffCommentThreadAnchors.length]
|
|
1482
|
+
: delta > 0
|
|
1483
|
+
? diffCommentThreadAnchors.find((anchor) => !selectedDiffCommentAnchor || anchor.renderLine > selectedDiffCommentAnchor.renderLine) ?? diffCommentThreadAnchors[0]
|
|
1484
|
+
: [...diffCommentThreadAnchors].reverse().find((anchor) => !selectedDiffCommentAnchor || anchor.renderLine < selectedDiffCommentAnchor.renderLine) ?? diffCommentThreadAnchors[diffCommentThreadAnchors.length - 1]
|
|
1485
|
+
if (!nextAnchor) return
|
|
1486
|
+
setDiffCommentRangeStartIndex(null)
|
|
1487
|
+
setDiffCommentAnchorIndex(diffCommentAnchors.indexOf(nextAnchor))
|
|
1488
|
+
setDiffFileIndex(nextAnchor.fileIndex)
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1291
1491
|
const submitDiffComment = () => {
|
|
1292
1492
|
if (!selectedPullRequest || !selectedDiffCommentAnchor) return
|
|
1293
1493
|
const body = commentModal.body.trim()
|
|
@@ -1296,8 +1496,9 @@ export const App = () => {
|
|
|
1296
1496
|
return
|
|
1297
1497
|
}
|
|
1298
1498
|
|
|
1299
|
-
const
|
|
1300
|
-
const target = selectedDiffCommentAnchor
|
|
1499
|
+
const targetRange = selectedDiffCommentRange
|
|
1500
|
+
const target = targetRange?.end ?? selectedDiffCommentAnchor
|
|
1501
|
+
const threadKey = selectedDiffKey ? diffCommentThreadMapKey(selectedDiffKey, target) : null
|
|
1301
1502
|
const optimisticComment = {
|
|
1302
1503
|
id: `local:${Date.now()}`,
|
|
1303
1504
|
path: target.path,
|
|
@@ -1308,6 +1509,9 @@ export const App = () => {
|
|
|
1308
1509
|
createdAt: new Date(),
|
|
1309
1510
|
url: null,
|
|
1310
1511
|
} satisfies PullRequestReviewComment
|
|
1512
|
+
const rangeInput = targetRange && targetRange.start.line !== targetRange.end.line
|
|
1513
|
+
? { startLine: targetRange.start.line, startSide: targetRange.start.side }
|
|
1514
|
+
: {}
|
|
1311
1515
|
const input = {
|
|
1312
1516
|
repository: selectedPullRequest.repository,
|
|
1313
1517
|
number: selectedPullRequest.number,
|
|
@@ -1316,6 +1520,7 @@ export const App = () => {
|
|
|
1316
1520
|
line: target.line,
|
|
1317
1521
|
side: target.side,
|
|
1318
1522
|
body,
|
|
1523
|
+
...rangeInput,
|
|
1319
1524
|
} satisfies CreatePullRequestCommentInput
|
|
1320
1525
|
|
|
1321
1526
|
if (threadKey) {
|
|
@@ -1325,6 +1530,7 @@ export const App = () => {
|
|
|
1325
1530
|
}))
|
|
1326
1531
|
}
|
|
1327
1532
|
closeActiveModal()
|
|
1533
|
+
setDiffCommentRangeStartIndex(null)
|
|
1328
1534
|
flashNotice(`Commenting on ${target.path}:${target.line}`)
|
|
1329
1535
|
void createPullRequestComment(input).then((comment) => {
|
|
1330
1536
|
if (threadKey) {
|
|
@@ -1400,16 +1606,7 @@ export const App = () => {
|
|
|
1400
1606
|
setCloseModal((current) => ({ ...current, running: true, error: null }))
|
|
1401
1607
|
void closePullRequest({ repository, number })
|
|
1402
1608
|
.then(() => {
|
|
1403
|
-
if (previousPullRequest)
|
|
1404
|
-
setRecentlyCompletedPullRequests((current) => ({
|
|
1405
|
-
...current,
|
|
1406
|
-
[previousPullRequest.url]: {
|
|
1407
|
-
...previousPullRequest,
|
|
1408
|
-
state: "closed",
|
|
1409
|
-
autoMergeEnabled: false,
|
|
1410
|
-
},
|
|
1411
|
-
}))
|
|
1412
|
-
}
|
|
1609
|
+
if (previousPullRequest) markPullRequestCompleted(previousPullRequest, "closed")
|
|
1413
1610
|
closeActiveModal()
|
|
1414
1611
|
refreshPullRequests(`Closed #${number}`)
|
|
1415
1612
|
})
|
|
@@ -1444,6 +1641,12 @@ export const App = () => {
|
|
|
1444
1641
|
setThemeId(id)
|
|
1445
1642
|
}
|
|
1446
1643
|
|
|
1644
|
+
const toggleDiffWhitespaceMode = () => {
|
|
1645
|
+
const next = diffWhitespaceMode === "ignore" ? "show" : "ignore"
|
|
1646
|
+
setDiffWhitespaceMode(next)
|
|
1647
|
+
void Effect.runPromise(saveStoredDiffWhitespaceMode(next)).catch((error) => flashNotice(errorMessage(error)))
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1447
1650
|
const moveThemeSelection = (delta: number) => {
|
|
1448
1651
|
const filteredThemes = filterThemeDefinitions(themeModalRef.current.query)
|
|
1449
1652
|
if (filteredThemes.length === 0) return
|
|
@@ -1539,30 +1742,15 @@ export const App = () => {
|
|
|
1539
1742
|
const { repository, number } = mergeModal.info
|
|
1540
1743
|
const targetPullRequest = pullRequests.find((pullRequest) => pullRequest.repository === repository && pullRequest.number === number)
|
|
1541
1744
|
const previousPullRequest = targetPullRequest ?? null
|
|
1542
|
-
const previousMergeInfo = mergeModal.info
|
|
1543
1745
|
|
|
1544
1746
|
if (targetPullRequest && option.optimisticAutoMergeEnabled !== undefined) {
|
|
1545
1747
|
updatePullRequest(targetPullRequest.url, (pullRequest) => ({ ...pullRequest, autoMergeEnabled: option.optimisticAutoMergeEnabled! }))
|
|
1546
|
-
setMergeModal((current) => ({
|
|
1547
|
-
...current,
|
|
1548
|
-
info: current.info ? { ...current.info, autoMergeEnabled: option.optimisticAutoMergeEnabled! } : current.info,
|
|
1549
|
-
}))
|
|
1550
1748
|
}
|
|
1749
|
+
if (targetPullRequest && option.optimisticState === "merged") markPullRequestCompleted(targetPullRequest, "merged")
|
|
1551
1750
|
|
|
1552
|
-
|
|
1751
|
+
closeActiveModal()
|
|
1553
1752
|
void mergePullRequest({ repository, number, action: option.action })
|
|
1554
1753
|
.then(() => {
|
|
1555
|
-
if (option.refreshOnSuccess && previousPullRequest) {
|
|
1556
|
-
setRecentlyCompletedPullRequests((current) => ({
|
|
1557
|
-
...current,
|
|
1558
|
-
[previousPullRequest.url]: {
|
|
1559
|
-
...previousPullRequest,
|
|
1560
|
-
state: "merged",
|
|
1561
|
-
autoMergeEnabled: false,
|
|
1562
|
-
},
|
|
1563
|
-
}))
|
|
1564
|
-
}
|
|
1565
|
-
closeActiveModal()
|
|
1566
1754
|
if (option.refreshOnSuccess) {
|
|
1567
1755
|
refreshPullRequests(`${option.pastTense} #${number}`)
|
|
1568
1756
|
} else {
|
|
@@ -1570,8 +1758,7 @@ export const App = () => {
|
|
|
1570
1758
|
}
|
|
1571
1759
|
})
|
|
1572
1760
|
.catch((error) => {
|
|
1573
|
-
if (previousPullRequest)
|
|
1574
|
-
setMergeModal((current) => ({ ...current, running: false, info: previousMergeInfo, error: errorMessage(error) }))
|
|
1761
|
+
if (previousPullRequest) restoreOptimisticPullRequest(previousPullRequest)
|
|
1575
1762
|
flashNotice(errorMessage(error))
|
|
1576
1763
|
})
|
|
1577
1764
|
}
|
|
@@ -1685,10 +1872,13 @@ export const App = () => {
|
|
|
1685
1872
|
diffReady: selectedDiffState?._tag === "Ready",
|
|
1686
1873
|
effectiveDiffRenderView,
|
|
1687
1874
|
diffWrapMode,
|
|
1875
|
+
diffWhitespaceMode,
|
|
1688
1876
|
readyDiffFileCount: readyDiffFiles.length,
|
|
1689
1877
|
diffFileIndex,
|
|
1690
|
-
|
|
1691
|
-
selectedDiffCommentAnchorLabel:
|
|
1878
|
+
diffRangeActive: diffCommentRangeActive,
|
|
1879
|
+
selectedDiffCommentAnchorLabel: selectedDiffCommentLabel,
|
|
1880
|
+
selectedDiffCommentThreadCount: selectedDiffCommentThread.length,
|
|
1881
|
+
hasDiffCommentThreads: diffCommentThreadAnchors.length > 0,
|
|
1692
1882
|
actions: {
|
|
1693
1883
|
openCommandPalette,
|
|
1694
1884
|
refreshPullRequests,
|
|
@@ -1716,7 +1906,7 @@ export const App = () => {
|
|
|
1716
1906
|
openDiffView,
|
|
1717
1907
|
closeDiffView: () => {
|
|
1718
1908
|
setDiffFullView(false)
|
|
1719
|
-
|
|
1909
|
+
setDiffCommentRangeStartIndex(null)
|
|
1720
1910
|
},
|
|
1721
1911
|
reloadDiff: () => {
|
|
1722
1912
|
if (!selectedPullRequest) return
|
|
@@ -1725,11 +1915,11 @@ export const App = () => {
|
|
|
1725
1915
|
},
|
|
1726
1916
|
toggleDiffRenderView: () => setDiffRenderView((current) => current === "unified" ? "split" : "unified"),
|
|
1727
1917
|
toggleDiffWrapMode: () => setDiffWrapMode((current) => current === "none" ? "word" : "none"),
|
|
1918
|
+
toggleDiffWhitespaceMode,
|
|
1728
1919
|
jumpDiffFile,
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
},
|
|
1920
|
+
openSelectedDiffComment,
|
|
1921
|
+
toggleDiffCommentRange,
|
|
1922
|
+
moveDiffCommentThread,
|
|
1733
1923
|
openDiffCommentModal,
|
|
1734
1924
|
togglePullRequestDraftStatus: toggleSelectedPullRequestDraftStatus,
|
|
1735
1925
|
openLabelModal,
|
|
@@ -1755,6 +1945,8 @@ export const App = () => {
|
|
|
1755
1945
|
const command = appCommands.find((entry) => entry.id === id)
|
|
1756
1946
|
return command ? runCommand(command, options) : false
|
|
1757
1947
|
}
|
|
1948
|
+
const runCommandByIdRef = useRef(runCommandById)
|
|
1949
|
+
runCommandByIdRef.current = runCommandById
|
|
1758
1950
|
const dynamicPaletteCommands: readonly AppCommand[] = (() => {
|
|
1759
1951
|
if (!commandPaletteActive) return []
|
|
1760
1952
|
const repository = parseRepositoryInput(commandPalette.query)
|
|
@@ -1769,287 +1961,250 @@ export const App = () => {
|
|
|
1769
1961
|
})()
|
|
1770
1962
|
// Dynamic commands always pin to the top of the palette; they came directly from the
|
|
1771
1963
|
// user's typed input so they shouldn't be filtered by fuzzy score against themselves.
|
|
1964
|
+
const staticPaletteCommands = commandPaletteActive
|
|
1965
|
+
? filterCommands(appCommands.filter((command) => command.id !== "command.open" && commandEnabled(command)), commandPalette.query)
|
|
1966
|
+
: []
|
|
1772
1967
|
const commandPaletteCommands = commandPaletteActive
|
|
1773
1968
|
? [
|
|
1774
1969
|
...dynamicPaletteCommands,
|
|
1775
|
-
...
|
|
1970
|
+
...(commandPalette.query.trim().length > 0 ? staticPaletteCommands : sortCommandsByScope(staticPaletteCommands)),
|
|
1776
1971
|
]
|
|
1777
1972
|
: []
|
|
1778
1973
|
const selectedCommandIndex = clampCommandIndex(commandPalette.selectedIndex, commandPaletteCommands)
|
|
1779
1974
|
const selectedCommand = commandPaletteCommands[selectedCommandIndex] ?? null
|
|
1780
1975
|
|
|
1781
|
-
//
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
const globalKeymapActiveRef = useRef(false)
|
|
1785
|
-
globalKeymapActiveRef.current = !commandPaletteActive
|
|
1786
|
-
&& !openRepositoryModalActive
|
|
1787
|
-
&& !labelModalActive
|
|
1788
|
-
&& !commentModalActive
|
|
1789
|
-
&& !commentThreadModalActive
|
|
1790
|
-
&& !closeModalActive
|
|
1791
|
-
&& !mergeModalActive
|
|
1792
|
-
&& !themeModalActive
|
|
1793
|
-
&& !diffFullView
|
|
1794
|
-
&& !detailFullView
|
|
1795
|
-
&& !filterMode
|
|
1796
|
-
const runCommandByIdRef = useRef(runCommandById)
|
|
1797
|
-
runCommandByIdRef.current = runCommandById
|
|
1798
|
-
useBindings(() => ({
|
|
1799
|
-
enabled: () => globalKeymapActiveRef.current,
|
|
1800
|
-
bindings: [
|
|
1801
|
-
{ key: "/", cmd: () => runCommandByIdRef.current("filter.open") },
|
|
1802
|
-
{ key: "r", cmd: () => runCommandByIdRef.current("pull.refresh") },
|
|
1803
|
-
{ key: "t", cmd: () => runCommandByIdRef.current("theme.open") },
|
|
1804
|
-
{ key: "d", cmd: () => runCommandByIdRef.current("diff.open") },
|
|
1805
|
-
{ key: "l", cmd: () => runCommandByIdRef.current("pull.labels") },
|
|
1806
|
-
{ key: "m", cmd: () => runCommandByIdRef.current("pull.merge") },
|
|
1807
|
-
{ key: "shift+m", cmd: () => runCommandByIdRef.current("pull.merge") },
|
|
1808
|
-
{ key: "x", cmd: () => runCommandByIdRef.current("pull.close") },
|
|
1809
|
-
{ key: "o", cmd: () => runCommandByIdRef.current("pull.open-browser") },
|
|
1810
|
-
{ key: "s", cmd: () => runCommandByIdRef.current("pull.toggle-draft") },
|
|
1811
|
-
{ key: "shift+s", cmd: () => runCommandByIdRef.current("pull.toggle-draft") },
|
|
1812
|
-
{ key: "y", cmd: () => runCommandByIdRef.current("pull.copy-metadata") },
|
|
1813
|
-
{ key: "return", cmd: () => runCommandByIdRef.current("detail.open") },
|
|
1814
|
-
],
|
|
1815
|
-
}), [])
|
|
1816
|
-
// Always-on bindings — work even while modals are open.
|
|
1817
|
-
useBindings(() => ({
|
|
1818
|
-
bindings: [
|
|
1819
|
-
{ key: "ctrl+p", cmd: () => runCommandByIdRef.current("command.open") },
|
|
1820
|
-
{ key: "meta+k", cmd: () => runCommandByIdRef.current("command.open") },
|
|
1821
|
-
],
|
|
1822
|
-
}), [])
|
|
1823
|
-
|
|
1824
|
-
// CloseModal: escape closes, enter confirms.
|
|
1825
|
-
const closeModalActiveRef = useRef(false)
|
|
1826
|
-
closeModalActiveRef.current = closeModalActive
|
|
1827
|
-
const closeActiveModalRef = useRef(closeActiveModal)
|
|
1828
|
-
closeActiveModalRef.current = closeActiveModal
|
|
1829
|
-
const confirmClosePullRequestRef = useRef(confirmClosePullRequest)
|
|
1830
|
-
confirmClosePullRequestRef.current = confirmClosePullRequest
|
|
1831
|
-
useBindings(() => ({
|
|
1832
|
-
enabled: () => closeModalActiveRef.current,
|
|
1833
|
-
bindings: [
|
|
1834
|
-
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1835
|
-
{ key: "return", cmd: () => confirmClosePullRequestRef.current() },
|
|
1836
|
-
],
|
|
1837
|
-
}), [])
|
|
1838
|
-
|
|
1839
|
-
// MergeModal: escape, enter (when options>0), up/down/j/k navigation.
|
|
1840
|
-
const mergeModalActiveRef = useRef(false)
|
|
1841
|
-
mergeModalActiveRef.current = mergeModalActive
|
|
1842
|
-
const mergeModalContextRef = useRef({ availableCount: 0, confirm: confirmMergeAction, setMergeModal })
|
|
1843
|
-
mergeModalContextRef.current = {
|
|
1844
|
-
availableCount: availableMergeActions(mergeModal.info).length,
|
|
1845
|
-
confirm: confirmMergeAction,
|
|
1846
|
-
setMergeModal,
|
|
1847
|
-
}
|
|
1848
|
-
const moveMergeSelection = (delta: -1 | 1) => mergeModalContextRef.current.setMergeModal((current) => {
|
|
1849
|
-
const max = Math.max(0, mergeModalContextRef.current.availableCount - 1)
|
|
1976
|
+
// === Helpers used by the keymap layers ===
|
|
1977
|
+
const moveMergeSelection = (delta: -1 | 1) => setMergeModal((current) => {
|
|
1978
|
+
const max = Math.max(0, availableMergeActions(mergeModal.info).length - 1)
|
|
1850
1979
|
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
1851
1980
|
})
|
|
1852
|
-
|
|
1853
|
-
enabled: () => mergeModalActiveRef.current,
|
|
1854
|
-
bindings: [
|
|
1855
|
-
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1856
|
-
{ key: "return", cmd: () => {
|
|
1857
|
-
if (mergeModalContextRef.current.availableCount > 0) mergeModalContextRef.current.confirm()
|
|
1858
|
-
} },
|
|
1859
|
-
{ key: "up", cmd: () => moveMergeSelection(-1) },
|
|
1860
|
-
{ key: "k", cmd: () => moveMergeSelection(-1) },
|
|
1861
|
-
{ key: "down", cmd: () => moveMergeSelection(1) },
|
|
1862
|
-
{ key: "j", cmd: () => moveMergeSelection(1) },
|
|
1863
|
-
],
|
|
1864
|
-
}), [])
|
|
1865
|
-
|
|
1866
|
-
// CommentThreadModal: scroll the thread, shortcut to compose a reply.
|
|
1867
|
-
const commentThreadModalActiveRef = useRef(false)
|
|
1868
|
-
commentThreadModalActiveRef.current = commentThreadModalActive
|
|
1869
|
-
const commentThreadCtxRef = useRef({ openDiffCommentModal, setCommentThreadModal, halfPage })
|
|
1870
|
-
commentThreadCtxRef.current = { openDiffCommentModal, setCommentThreadModal, halfPage }
|
|
1871
|
-
const scrollCommentThread = (delta: number) => commentThreadCtxRef.current.setCommentThreadModal((current) => ({
|
|
1981
|
+
const scrollCommentThread = (delta: number) => setCommentThreadModal((current) => ({
|
|
1872
1982
|
...current,
|
|
1873
1983
|
scrollOffset: Math.max(0, current.scrollOffset + delta),
|
|
1874
1984
|
}))
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
bindings: [
|
|
1878
|
-
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1879
|
-
{ key: "return", cmd: () => commentThreadCtxRef.current.openDiffCommentModal() },
|
|
1880
|
-
{ key: "a", cmd: () => commentThreadCtxRef.current.openDiffCommentModal() },
|
|
1881
|
-
{ key: "c", cmd: () => commentThreadCtxRef.current.openDiffCommentModal() },
|
|
1882
|
-
{ key: "up", cmd: () => scrollCommentThread(-1) },
|
|
1883
|
-
{ key: "k", cmd: () => scrollCommentThread(-1) },
|
|
1884
|
-
{ key: "down", cmd: () => scrollCommentThread(1) },
|
|
1885
|
-
{ key: "j", cmd: () => scrollCommentThread(1) },
|
|
1886
|
-
{ key: "pageup", cmd: () => scrollCommentThread(-commentThreadCtxRef.current.halfPage) },
|
|
1887
|
-
{ key: "ctrl+u", cmd: () => scrollCommentThread(-commentThreadCtxRef.current.halfPage) },
|
|
1888
|
-
{ key: "pagedown", cmd: () => scrollCommentThread(commentThreadCtxRef.current.halfPage) },
|
|
1889
|
-
{ key: "ctrl+d", cmd: () => scrollCommentThread(commentThreadCtxRef.current.halfPage) },
|
|
1890
|
-
{ key: "ctrl+v", cmd: () => scrollCommentThread(commentThreadCtxRef.current.halfPage) },
|
|
1891
|
-
],
|
|
1892
|
-
}), [])
|
|
1893
|
-
|
|
1894
|
-
// LabelModal: nav keys via keymap; text input stays in useKeyboard fallback.
|
|
1895
|
-
const labelModalActiveRef = useRef(false)
|
|
1896
|
-
labelModalActiveRef.current = labelModalActive
|
|
1897
|
-
const labelModalCtxRef = useRef({ toggleLabelAtIndex, setLabelModal, filteredCount: 0 })
|
|
1898
|
-
labelModalCtxRef.current = {
|
|
1899
|
-
toggleLabelAtIndex,
|
|
1900
|
-
setLabelModal,
|
|
1901
|
-
filteredCount: filterLabels(labelModal.availableLabels, labelModal.query).length,
|
|
1902
|
-
}
|
|
1903
|
-
const moveLabelSelection = (delta: -1 | 1) => labelModalCtxRef.current.setLabelModal((current) => {
|
|
1904
|
-
const max = Math.max(0, labelModalCtxRef.current.filteredCount - 1)
|
|
1985
|
+
const moveLabelSelection = (delta: -1 | 1) => setLabelModal((current) => {
|
|
1986
|
+
const max = Math.max(0, filterLabels(labelModal.availableLabels, labelModal.query).length - 1)
|
|
1905
1987
|
return { ...current, selectedIndex: Math.max(0, Math.min(max, current.selectedIndex + delta)) }
|
|
1906
1988
|
})
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1989
|
+
const moveCommandPaletteSelection = (delta: -1 | 1) => setCommandPalette((current) => {
|
|
1990
|
+
const selectedIndex = clampCommandIndex(current.selectedIndex + delta, commandPaletteCommands)
|
|
1991
|
+
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
1992
|
+
})
|
|
1993
|
+
const selectCommandPaletteIndex = (index: number) => setCommandPalette((current) => {
|
|
1994
|
+
const selectedIndex = clampCommandIndex(index, commandPaletteCommands)
|
|
1995
|
+
return selectedIndex === current.selectedIndex ? current : { ...current, selectedIndex }
|
|
1996
|
+
})
|
|
1997
|
+
const runCommandPaletteCommand = (command: AppCommand) => {
|
|
1998
|
+
runCommand(command, { notifyDisabled: true, closePalette: true })
|
|
1999
|
+
}
|
|
2000
|
+
const scrollDetailFullViewBy = (delta: number) => {
|
|
2001
|
+
detailScrollRef.current?.scrollBy({ x: 0, y: delta })
|
|
2002
|
+
setDetailScrollOffset((current) => Math.max(0, current + delta))
|
|
2003
|
+
}
|
|
2004
|
+
const scrollDetailFullViewTo = (y: number) => {
|
|
2005
|
+
detailScrollRef.current?.scrollTo({ x: 0, y })
|
|
2006
|
+
setDetailScrollOffset(y)
|
|
2007
|
+
}
|
|
2008
|
+
const moveSelectedToPreviousGroup = () => setSelectedIndex((current) => {
|
|
2009
|
+
if (visiblePullRequests.length === 0 || groupStarts.length === 0) return 0
|
|
2010
|
+
const currentGroup = getCurrentGroupIndex(current)
|
|
2011
|
+
if (currentGroup <= 0) return groupStarts[groupStarts.length - 1]!
|
|
2012
|
+
return groupStarts[currentGroup - 1]!
|
|
2013
|
+
})
|
|
2014
|
+
const moveSelectedToNextGroup = () => setSelectedIndex((current) => {
|
|
2015
|
+
if (visiblePullRequests.length === 0 || groupStarts.length === 0) return 0
|
|
2016
|
+
const currentGroup = getCurrentGroupIndex(current)
|
|
2017
|
+
if (currentGroup >= groupStarts.length - 1) return groupStarts[0]!
|
|
2018
|
+
return groupStarts[currentGroup + 1]!
|
|
1929
2019
|
})
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
closeThemeModal,
|
|
1934
|
-
updateThemeQuery,
|
|
1935
|
-
moveThemeSelection,
|
|
1936
|
-
}
|
|
1937
|
-
useBindings(() => ({
|
|
1938
|
-
enabled: () => themeModalActiveRef.current,
|
|
1939
|
-
bindings: [
|
|
1940
|
-
{ key: "escape", cmd: () => {
|
|
1941
|
-
if (themeModalCtxRef.current.filterMode) themeModalCtxRef.current.updateThemeQuery("", { filterMode: false })
|
|
1942
|
-
else themeModalCtxRef.current.closeThemeModal(false)
|
|
1943
|
-
} },
|
|
1944
|
-
{ key: "/", cmd: () => themeModalCtxRef.current.updateThemeQuery("", { filterMode: true }) },
|
|
1945
|
-
{ key: "return", cmd: () => {
|
|
1946
|
-
if (themeModalCtxRef.current.filterMode && !themeModalCtxRef.current.hasResults) return
|
|
1947
|
-
themeModalCtxRef.current.closeThemeModal(true)
|
|
1948
|
-
} },
|
|
1949
|
-
{ key: "up", cmd: () => themeModalCtxRef.current.moveThemeSelection(-1) },
|
|
1950
|
-
{ key: "down", cmd: () => themeModalCtxRef.current.moveThemeSelection(1) },
|
|
1951
|
-
{ key: "k", cmd: () => { if (!themeModalCtxRef.current.filterMode) themeModalCtxRef.current.moveThemeSelection(-1) } },
|
|
1952
|
-
{ key: "j", cmd: () => { if (!themeModalCtxRef.current.filterMode) themeModalCtxRef.current.moveThemeSelection(1) } },
|
|
1953
|
-
],
|
|
1954
|
-
}), [])
|
|
1955
|
-
|
|
1956
|
-
// OpenRepositoryModal: escape closes, return submits.
|
|
1957
|
-
const openRepositoryModalActiveRef = useRef(false)
|
|
1958
|
-
openRepositoryModalActiveRef.current = openRepositoryModalActive
|
|
1959
|
-
const openRepositoryFromInputRef = useRef(openRepositoryFromInput)
|
|
1960
|
-
openRepositoryFromInputRef.current = openRepositoryFromInput
|
|
1961
|
-
useBindings(() => ({
|
|
1962
|
-
enabled: () => openRepositoryModalActiveRef.current,
|
|
1963
|
-
bindings: [
|
|
1964
|
-
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1965
|
-
{ key: "return", cmd: () => openRepositoryFromInputRef.current() },
|
|
1966
|
-
],
|
|
1967
|
-
}), [])
|
|
1968
|
-
|
|
1969
|
-
// CommentModal: full text editor — escape, submit, all the cursor/edit bindings.
|
|
1970
|
-
const commentModalActiveRef = useRef(false)
|
|
1971
|
-
commentModalActiveRef.current = commentModalActive
|
|
1972
|
-
const commentModalCtxRef = useRef({ submitDiffComment, editComment })
|
|
1973
|
-
commentModalCtxRef.current = { submitDiffComment, editComment }
|
|
1974
|
-
const editComm = (transform: Parameters<typeof editComment>[0]) => commentModalCtxRef.current.editComment(transform)
|
|
1975
|
-
useBindings(() => ({
|
|
1976
|
-
enabled: () => commentModalActiveRef.current,
|
|
1977
|
-
bindings: [
|
|
1978
|
-
{ key: "escape", cmd: () => closeActiveModalRef.current() },
|
|
1979
|
-
{ key: "ctrl+s", cmd: () => commentModalCtxRef.current.submitDiffComment() },
|
|
1980
|
-
{ key: "ctrl+a", cmd: () => editComm(moveLineStart) },
|
|
1981
|
-
{ key: "ctrl+e", cmd: () => editComm(moveLineEnd) },
|
|
1982
|
-
{ key: "ctrl+b", cmd: () => editComm(editorMoveLeft) },
|
|
1983
|
-
{ key: "ctrl+f", cmd: () => editComm(editorMoveRight) },
|
|
1984
|
-
{ key: "ctrl+w", cmd: () => editComm(deleteWordBackward) },
|
|
1985
|
-
{ key: "ctrl+u", cmd: () => editComm(deleteToLineStart) },
|
|
1986
|
-
{ key: "ctrl+k", cmd: () => editComm(deleteToLineEnd) },
|
|
1987
|
-
{ key: "ctrl+d", cmd: () => editComm(editorDeleteForward) },
|
|
1988
|
-
{ key: "meta+b", cmd: () => editComm(moveWordBackward) },
|
|
1989
|
-
{ key: "meta+left", cmd: () => editComm(moveWordBackward) },
|
|
1990
|
-
{ key: "meta+f", cmd: () => editComm(moveWordForward) },
|
|
1991
|
-
{ key: "meta+right", cmd: () => editComm(moveWordForward) },
|
|
1992
|
-
{ key: "meta+backspace", cmd: () => editComm(deleteWordBackward) },
|
|
1993
|
-
{ key: "meta+delete", cmd: () => editComm(deleteWordForward) },
|
|
1994
|
-
{ key: "backspace", cmd: () => editComm(editorBackspace) },
|
|
1995
|
-
{ key: "delete", cmd: () => editComm(editorDeleteForward) },
|
|
1996
|
-
{ key: "left", cmd: () => editComm(editorMoveLeft) },
|
|
1997
|
-
{ key: "right", cmd: () => editComm(editorMoveRight) },
|
|
1998
|
-
{ key: "up", cmd: () => editComm((state) => moveVertically(state, -1)) },
|
|
1999
|
-
{ key: "down", cmd: () => editComm((state) => moveVertically(state, 1)) },
|
|
2000
|
-
{ key: "home", cmd: () => editComm(moveLineStart) },
|
|
2001
|
-
{ key: "end", cmd: () => editComm(moveLineEnd) },
|
|
2002
|
-
{ key: "shift+return", cmd: () => editComm((state) => insertText(state, "\n")) },
|
|
2003
|
-
{ key: "return", cmd: () => commentModalCtxRef.current.submitDiffComment() },
|
|
2004
|
-
],
|
|
2005
|
-
}), [])
|
|
2006
|
-
|
|
2007
|
-
// CommandPalette: escape closes, return runs, up/k & down/j navigate.
|
|
2008
|
-
const commandPaletteActiveRef = useRef(false)
|
|
2009
|
-
commandPaletteActiveRef.current = commandPaletteActive
|
|
2010
|
-
const commandPaletteCtxRef = useRef({
|
|
2011
|
-
runSelected: () => {},
|
|
2012
|
-
setCommandPalette,
|
|
2013
|
-
paletteCommands: commandPaletteCommands,
|
|
2020
|
+
const stepSelected = (delta: number) => setSelectedIndex((current) => {
|
|
2021
|
+
if (visiblePullRequests.length === 0) return 0
|
|
2022
|
+
return Math.max(0, Math.min(visiblePullRequests.length - 1, current + delta))
|
|
2014
2023
|
})
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2024
|
+
const stepSelectedDown = (count = 1) => {
|
|
2025
|
+
if (visiblePullRequests.length === 0) return
|
|
2026
|
+
if (selectedIndex + count >= visiblePullRequests.length && hasMorePullRequests) {
|
|
2027
|
+
loadMorePullRequests()
|
|
2028
|
+
}
|
|
2029
|
+
stepSelected(count)
|
|
2019
2030
|
}
|
|
2020
|
-
const
|
|
2021
|
-
|
|
2022
|
-
|
|
2031
|
+
const stepSelectedUp = (count = 1) => stepSelected(-count)
|
|
2032
|
+
const stepSelectedDownWithLoadMore = () => {
|
|
2033
|
+
if (visiblePullRequests.length > 0 && selectedIndex >= visiblePullRequests.length - 1 && hasMorePullRequests) {
|
|
2034
|
+
loadMorePullRequests()
|
|
2035
|
+
return
|
|
2036
|
+
}
|
|
2037
|
+
setSelectedIndex((current) => {
|
|
2038
|
+
if (visiblePullRequests.length === 0) return 0
|
|
2039
|
+
return current >= visiblePullRequests.length - 1 ? 0 : current + 1
|
|
2040
|
+
})
|
|
2041
|
+
}
|
|
2042
|
+
const stepSelectedUpWrap = () => setSelectedIndex((current) => {
|
|
2043
|
+
if (visiblePullRequests.length === 0) return 0
|
|
2044
|
+
return current <= 0 ? visiblePullRequests.length - 1 : current - 1
|
|
2023
2045
|
})
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2046
|
+
const handleQuitOrClose = () => {
|
|
2047
|
+
if (themeModalActive) {
|
|
2048
|
+
closeThemeModal(false)
|
|
2049
|
+
return
|
|
2050
|
+
}
|
|
2051
|
+
if (activeModal._tag !== "None") {
|
|
2052
|
+
closeActiveModal()
|
|
2053
|
+
return
|
|
2054
|
+
}
|
|
2055
|
+
runCommandById("app.quit")
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
// === Build the keymap context ===
|
|
2059
|
+
const appCtx: AppCtx = {
|
|
2060
|
+
closeModalActive,
|
|
2061
|
+
mergeModalActive,
|
|
2062
|
+
commentThreadModalActive,
|
|
2063
|
+
labelModalActive,
|
|
2064
|
+
themeModalActive,
|
|
2065
|
+
openRepositoryModalActive,
|
|
2066
|
+
commentModalActive,
|
|
2067
|
+
commandPaletteActive,
|
|
2068
|
+
filterMode,
|
|
2069
|
+
diffFullView,
|
|
2070
|
+
detailFullView,
|
|
2071
|
+
textInputActive: commentModalActive
|
|
2072
|
+
|| commandPaletteActive
|
|
2073
|
+
|| openRepositoryModalActive
|
|
2074
|
+
|| labelModalActive
|
|
2075
|
+
|| filterMode
|
|
2076
|
+
|| (themeModalActive && themeModal.filterMode),
|
|
2077
|
+
closeModal: {
|
|
2078
|
+
closeModal: closeActiveModal,
|
|
2079
|
+
confirmClose: confirmClosePullRequest,
|
|
2080
|
+
},
|
|
2081
|
+
mergeModal: {
|
|
2082
|
+
availableActionCount: availableMergeActions(mergeModal.info).length,
|
|
2083
|
+
closeModal: closeActiveModal,
|
|
2084
|
+
confirmMerge: confirmMergeAction,
|
|
2085
|
+
moveSelection: moveMergeSelection,
|
|
2086
|
+
},
|
|
2087
|
+
commentThreadModal: {
|
|
2088
|
+
halfPage,
|
|
2089
|
+
closeModal: closeActiveModal,
|
|
2090
|
+
openInlineComment: openDiffCommentModal,
|
|
2091
|
+
scrollBy: scrollCommentThread,
|
|
2092
|
+
},
|
|
2093
|
+
labelModal: {
|
|
2094
|
+
closeModal: closeActiveModal,
|
|
2095
|
+
toggleSelected: toggleLabelAtIndex,
|
|
2096
|
+
moveSelection: moveLabelSelection,
|
|
2097
|
+
},
|
|
2098
|
+
themeModal: {
|
|
2099
|
+
filterMode: themeModal.filterMode,
|
|
2100
|
+
hasFilteredResults: filterThemeDefinitions(themeModal.query).length > 0,
|
|
2101
|
+
closeWithoutSaving: () => closeThemeModal(false),
|
|
2102
|
+
clearFilter: () => updateThemeQuery("", { filterMode: false }),
|
|
2103
|
+
enterFilterMode: () => updateThemeQuery("", { filterMode: true }),
|
|
2104
|
+
confirmSelection: () => closeThemeModal(true),
|
|
2105
|
+
moveSelection: moveThemeSelection,
|
|
2106
|
+
},
|
|
2107
|
+
openRepositoryModal: {
|
|
2108
|
+
closeModal: closeActiveModal,
|
|
2109
|
+
openFromInput: openRepositoryFromInput,
|
|
2110
|
+
},
|
|
2111
|
+
commentModal: {
|
|
2112
|
+
closeModal: closeActiveModal,
|
|
2113
|
+
submit: submitDiffComment,
|
|
2114
|
+
insertNewline: () => editComment((state) => insertText(state, "\n")),
|
|
2115
|
+
moveLeft: () => editComment(editorMoveLeft),
|
|
2116
|
+
moveRight: () => editComment(editorMoveRight),
|
|
2117
|
+
moveUp: () => editComment((state) => moveVertically(state, -1)),
|
|
2118
|
+
moveDown: () => editComment((state) => moveVertically(state, 1)),
|
|
2119
|
+
moveLineStart: () => editComment(moveLineStart),
|
|
2120
|
+
moveLineEnd: () => editComment(moveLineEnd),
|
|
2121
|
+
moveWordBackward: () => editComment(moveWordBackward),
|
|
2122
|
+
moveWordForward: () => editComment(moveWordForward),
|
|
2123
|
+
backspace: () => editComment(editorBackspace),
|
|
2124
|
+
deleteForward: () => editComment(editorDeleteForward),
|
|
2125
|
+
deleteWordBackward: () => editComment(deleteWordBackward),
|
|
2126
|
+
deleteWordForward: () => editComment(deleteWordForward),
|
|
2127
|
+
deleteToLineStart: () => editComment(deleteToLineStart),
|
|
2128
|
+
deleteToLineEnd: () => editComment(deleteToLineEnd),
|
|
2129
|
+
},
|
|
2130
|
+
commandPalette: {
|
|
2131
|
+
closeModal: closeActiveModal,
|
|
2132
|
+
runSelected: () => {
|
|
2133
|
+
if (selectedCommand) runCommandPaletteCommand(selectedCommand)
|
|
2134
|
+
},
|
|
2135
|
+
moveSelection: moveCommandPaletteSelection,
|
|
2136
|
+
},
|
|
2137
|
+
filterModeCtx: {
|
|
2138
|
+
cancel: () => {
|
|
2139
|
+
setFilterDraft(filterQuery)
|
|
2140
|
+
setFilterMode(false)
|
|
2141
|
+
},
|
|
2142
|
+
commit: () => {
|
|
2143
|
+
setFilterQuery(filterDraft)
|
|
2144
|
+
setFilterMode(false)
|
|
2145
|
+
},
|
|
2146
|
+
},
|
|
2147
|
+
diff: {
|
|
2148
|
+
halfPage,
|
|
2149
|
+
handleEscape: () => {
|
|
2150
|
+
if (diffCommentRangeActive) setDiffCommentRangeStartIndex(null)
|
|
2151
|
+
else runCommandById("diff.close")
|
|
2152
|
+
},
|
|
2153
|
+
openSelectedComment: openSelectedDiffComment,
|
|
2154
|
+
addComment: () => runCommandById("diff.add-comment"),
|
|
2155
|
+
toggleRange: () => runCommandById("diff.toggle-range"),
|
|
2156
|
+
toggleView: () => runCommandById("diff.toggle-view"),
|
|
2157
|
+
toggleWrap: () => runCommandById("diff.toggle-wrap"),
|
|
2158
|
+
reload: () => runCommandById("diff.reload"),
|
|
2159
|
+
nextThread: () => runCommandById("diff.next-thread"),
|
|
2160
|
+
previousThread: () => runCommandById("diff.previous-thread"),
|
|
2161
|
+
moveAnchor: moveDiffCommentAnchor,
|
|
2162
|
+
moveAnchorToBoundary: moveDiffCommentToBoundary,
|
|
2163
|
+
alignAnchor: alignSelectedDiffCommentAnchor,
|
|
2164
|
+
selectSide: selectDiffCommentSide,
|
|
2165
|
+
nextFile: () => runCommandById("diff.next-file"),
|
|
2166
|
+
previousFile: () => runCommandById("diff.previous-file"),
|
|
2167
|
+
openInBrowser: () => runCommandById("pull.open-browser"),
|
|
2168
|
+
},
|
|
2169
|
+
detail: {
|
|
2170
|
+
halfPage,
|
|
2171
|
+
scrollBy: scrollDetailFullViewBy,
|
|
2172
|
+
scrollTo: scrollDetailFullViewTo,
|
|
2173
|
+
closeDetail: () => runCommandById("detail.close"),
|
|
2174
|
+
openTheme: () => runCommandById("theme.open"),
|
|
2175
|
+
openDiff: () => runCommandById("diff.open"),
|
|
2176
|
+
closePullRequest: () => runCommandById("pull.close"),
|
|
2177
|
+
openLabels: () => runCommandById("pull.labels"),
|
|
2178
|
+
openMerge: () => runCommandById("pull.merge"),
|
|
2179
|
+
toggleDraft: () => runCommandById("pull.toggle-draft"),
|
|
2180
|
+
refresh: () => runCommandById("pull.refresh"),
|
|
2181
|
+
openInBrowser: () => runCommandById("pull.open-browser"),
|
|
2182
|
+
copyMetadata: () => runCommandById("pull.copy-metadata"),
|
|
2183
|
+
},
|
|
2184
|
+
listNav: {
|
|
2185
|
+
halfPage,
|
|
2186
|
+
visibleCount: visiblePullRequests.length,
|
|
2187
|
+
hasFilter: filterQuery.length > 0,
|
|
2188
|
+
canScrollDetailPreview: isWideLayout && selectedPullRequest !== null,
|
|
2189
|
+
runCommandById: (id) => { runCommandById(id) },
|
|
2190
|
+
switchQueueMode,
|
|
2191
|
+
scrollDetailPreviewBy,
|
|
2192
|
+
scrollDetailPreviewTo,
|
|
2193
|
+
clearFilter: () => { runCommandById("filter.clear") },
|
|
2194
|
+
stepSelected,
|
|
2195
|
+
stepSelectedUp,
|
|
2196
|
+
stepSelectedDown,
|
|
2197
|
+
stepSelectedUpWrap,
|
|
2198
|
+
stepSelectedDownWithLoadMore,
|
|
2199
|
+
moveSelectedToPreviousGroup,
|
|
2200
|
+
moveSelectedToNextGroup,
|
|
2201
|
+
setSelected: (index) => setSelectedIndex(index),
|
|
2202
|
+
},
|
|
2203
|
+
openCommandPalette: () => { runCommandById("command.open") },
|
|
2204
|
+
handleQuitOrClose,
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
useKeymap(appKeymap, appCtx, useOpenTuiSubscribe())
|
|
2053
2208
|
|
|
2054
2209
|
useKeyboard((key) => {
|
|
2055
2210
|
if (commandPaletteActive) {
|
|
@@ -2073,18 +2228,9 @@ export const App = () => {
|
|
|
2073
2228
|
return
|
|
2074
2229
|
}
|
|
2075
2230
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
return
|
|
2080
|
-
}
|
|
2081
|
-
if (activeModal._tag !== "None") {
|
|
2082
|
-
closeActiveModal()
|
|
2083
|
-
return
|
|
2084
|
-
}
|
|
2085
|
-
runCommandById("app.quit")
|
|
2086
|
-
return
|
|
2087
|
-
}
|
|
2231
|
+
// q / ctrl+c quit/close-modal logic now lives in the keymap layer
|
|
2232
|
+
// (handleQuitOrClose). This useKeyboard callback only handles raw text
|
|
2233
|
+
// input for modals that need character-by-character accumulation.
|
|
2088
2234
|
|
|
2089
2235
|
if (themeModalActive) {
|
|
2090
2236
|
if (themeModal.filterMode && isSingleLineInputKey(key)) {
|
|
@@ -2113,343 +2259,30 @@ export const App = () => {
|
|
|
2113
2259
|
return
|
|
2114
2260
|
}
|
|
2115
2261
|
|
|
2116
|
-
if (diffFullView) {
|
|
2117
|
-
if (diffCommentMode) {
|
|
2118
|
-
if (key.name === "escape") {
|
|
2119
|
-
setDiffCommentMode(false)
|
|
2120
|
-
return
|
|
2121
|
-
}
|
|
2122
|
-
if (key.name === "c") {
|
|
2123
|
-
runCommandById("diff.comment-mode")
|
|
2124
|
-
return
|
|
2125
|
-
}
|
|
2126
|
-
if (key.name === "return" || key.name === "enter") {
|
|
2127
|
-
if (selectedDiffCommentThread.length > 0) openDiffCommentThreadModal()
|
|
2128
|
-
else openDiffCommentModal()
|
|
2129
|
-
return
|
|
2130
|
-
}
|
|
2131
|
-
if (key.name === "a") {
|
|
2132
|
-
runCommandById("diff.add-comment")
|
|
2133
|
-
return
|
|
2134
|
-
}
|
|
2135
|
-
if (key.name === "pageup" || key.ctrl && key.name === "u") {
|
|
2136
|
-
moveDiffCommentAnchor(-halfPage)
|
|
2137
|
-
return
|
|
2138
|
-
}
|
|
2139
|
-
if (key.name === "pagedown" || key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
2140
|
-
moveDiffCommentAnchor(halfPage)
|
|
2141
|
-
return
|
|
2142
|
-
}
|
|
2143
|
-
if ((key.shift || key.option || key.meta) && (key.name === "up" || key.name === "k") || key.name === "K") {
|
|
2144
|
-
moveDiffCommentAnchor(-8)
|
|
2145
|
-
return
|
|
2146
|
-
}
|
|
2147
|
-
if ((key.shift || key.option || key.meta) && (key.name === "down" || key.name === "j") || key.name === "J") {
|
|
2148
|
-
moveDiffCommentAnchor(8)
|
|
2149
|
-
return
|
|
2150
|
-
}
|
|
2151
|
-
if (key.name === "up" || key.name === "k") {
|
|
2152
|
-
moveDiffCommentAnchor(-1)
|
|
2153
|
-
return
|
|
2154
|
-
}
|
|
2155
|
-
if (key.name === "down" || key.name === "j") {
|
|
2156
|
-
moveDiffCommentAnchor(1)
|
|
2157
|
-
return
|
|
2158
|
-
}
|
|
2159
|
-
if (key.name === "left" || key.name === "h") {
|
|
2160
|
-
selectDiffCommentSide("LEFT")
|
|
2161
|
-
return
|
|
2162
|
-
}
|
|
2163
|
-
if (key.name === "right" || key.name === "l") {
|
|
2164
|
-
selectDiffCommentSide("RIGHT")
|
|
2165
|
-
return
|
|
2166
|
-
}
|
|
2167
|
-
if (key.name === "]" && selectedDiffState?._tag === "Ready") {
|
|
2168
|
-
runCommandById("diff.next-file")
|
|
2169
|
-
return
|
|
2170
|
-
}
|
|
2171
|
-
if (key.name === "[" && selectedDiffState?._tag === "Ready") {
|
|
2172
|
-
runCommandById("diff.previous-file")
|
|
2173
|
-
return
|
|
2174
|
-
}
|
|
2175
|
-
return
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
if (key.name === "escape" || key.name === "return" || key.name === "enter") {
|
|
2179
|
-
runCommandById("diff.close")
|
|
2180
|
-
return
|
|
2181
|
-
}
|
|
2182
|
-
if (key.name === "c" && selectedDiffState?._tag === "Ready") {
|
|
2183
|
-
runCommandById("diff.comment-mode")
|
|
2184
|
-
return
|
|
2185
|
-
}
|
|
2186
|
-
if (key.name === "home") {
|
|
2187
|
-
scrollDiffTo(0)
|
|
2188
|
-
return
|
|
2189
|
-
}
|
|
2190
|
-
if (key.name === "end") {
|
|
2191
|
-
scrollDiffTo(Number.MAX_SAFE_INTEGER)
|
|
2192
|
-
return
|
|
2193
|
-
}
|
|
2194
|
-
if (key.name === "pageup") {
|
|
2195
|
-
scrollDiffBy(-halfPage)
|
|
2196
|
-
return
|
|
2197
|
-
}
|
|
2198
|
-
if (key.name === "pagedown") {
|
|
2199
|
-
scrollDiffBy(halfPage)
|
|
2200
|
-
return
|
|
2201
|
-
}
|
|
2202
|
-
if (handleVimGoto(key, () => scrollDiffTo(0), () => scrollDiffTo(Number.MAX_SAFE_INTEGER))) return
|
|
2203
|
-
if (key.name === "up" || key.name === "k") {
|
|
2204
|
-
scrollDiffBy(-1)
|
|
2205
|
-
return
|
|
2206
|
-
}
|
|
2207
|
-
if (key.name === "down" || key.name === "j") {
|
|
2208
|
-
scrollDiffBy(1)
|
|
2209
|
-
return
|
|
2210
|
-
}
|
|
2211
|
-
if (key.ctrl && key.name === "u") {
|
|
2212
|
-
scrollDiffBy(-halfPage)
|
|
2213
|
-
return
|
|
2214
|
-
}
|
|
2215
|
-
if (key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
2216
|
-
scrollDiffBy(halfPage)
|
|
2217
|
-
return
|
|
2218
|
-
}
|
|
2219
|
-
if (key.name === "v") {
|
|
2220
|
-
runCommandById("diff.toggle-view")
|
|
2221
|
-
return
|
|
2222
|
-
}
|
|
2223
|
-
if (key.name === "w") {
|
|
2224
|
-
runCommandById("diff.toggle-wrap")
|
|
2225
|
-
return
|
|
2226
|
-
}
|
|
2227
|
-
if (key.name === "r" && selectedPullRequest) {
|
|
2228
|
-
runCommandById("diff.reload")
|
|
2229
|
-
return
|
|
2230
|
-
}
|
|
2231
|
-
if ((key.name === "]" || key.name === "right" || key.name === "l") && selectedDiffState?._tag === "Ready") {
|
|
2232
|
-
runCommandById("diff.next-file")
|
|
2233
|
-
return
|
|
2234
|
-
}
|
|
2235
|
-
if ((key.name === "[" || key.name === "left" || key.name === "h") && selectedDiffState?._tag === "Ready") {
|
|
2236
|
-
runCommandById("diff.previous-file")
|
|
2237
|
-
return
|
|
2238
|
-
}
|
|
2239
|
-
if (key.name === "o" && selectedPullRequest) {
|
|
2240
|
-
runCommandById("pull.open-browser")
|
|
2241
|
-
return
|
|
2242
|
-
}
|
|
2243
|
-
return
|
|
2244
|
-
}
|
|
2245
|
-
|
|
2246
|
-
if (detailFullView) {
|
|
2247
|
-
const plainKey = !key.ctrl && !key.meta && !key.option
|
|
2248
|
-
if (key.name === "escape" || (key.name === "return" || key.name === "enter")) {
|
|
2249
|
-
runCommandById("detail.close")
|
|
2250
|
-
return
|
|
2251
|
-
}
|
|
2252
|
-
if (isThemeKey(key)) {
|
|
2253
|
-
runCommandById("theme.open")
|
|
2254
|
-
return
|
|
2255
|
-
}
|
|
2256
|
-
if (plainKey && key.name === "d" && selectedPullRequest) {
|
|
2257
|
-
runCommandById("diff.open")
|
|
2258
|
-
return
|
|
2259
|
-
}
|
|
2260
|
-
if (plainKey && key.name === "x" && selectedPullRequest?.state === "open") {
|
|
2261
|
-
runCommandById("pull.close")
|
|
2262
|
-
return
|
|
2263
|
-
}
|
|
2264
|
-
if (plainKey && key.name === "l" && selectedPullRequest) {
|
|
2265
|
-
runCommandById("pull.labels")
|
|
2266
|
-
return
|
|
2267
|
-
}
|
|
2268
|
-
if (plainKey && (key.name === "m" || key.name === "M") && selectedPullRequest) {
|
|
2269
|
-
runCommandById("pull.merge")
|
|
2270
|
-
return
|
|
2271
|
-
}
|
|
2272
|
-
if (plainKey && (key.name === "s" || key.name === "S") && selectedPullRequest) {
|
|
2273
|
-
runCommandById("pull.toggle-draft")
|
|
2274
|
-
return
|
|
2275
|
-
}
|
|
2276
|
-
if (plainKey && key.name === "r") {
|
|
2277
|
-
runCommandById("pull.refresh")
|
|
2278
|
-
return
|
|
2279
|
-
}
|
|
2280
|
-
if (key.name === "home") {
|
|
2281
|
-
detailScrollRef.current?.scrollTo({ x: 0, y: 0 })
|
|
2282
|
-
setDetailScrollOffset(0)
|
|
2283
|
-
return
|
|
2284
|
-
}
|
|
2285
|
-
if (key.name === "end") {
|
|
2286
|
-
detailScrollRef.current?.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER })
|
|
2287
|
-
setDetailScrollOffset(Number.MAX_SAFE_INTEGER)
|
|
2288
|
-
return
|
|
2289
|
-
}
|
|
2290
|
-
if (key.name === "pageup") {
|
|
2291
|
-
detailScrollRef.current?.scrollBy({ x: 0, y: -halfPage })
|
|
2292
|
-
setDetailScrollOffset((current) => Math.max(0, current - halfPage))
|
|
2293
|
-
return
|
|
2294
|
-
}
|
|
2295
|
-
if (key.name === "pagedown") {
|
|
2296
|
-
detailScrollRef.current?.scrollBy({ x: 0, y: halfPage })
|
|
2297
|
-
setDetailScrollOffset((current) => current + halfPage)
|
|
2298
|
-
return
|
|
2299
|
-
}
|
|
2300
|
-
if (handleVimGoto(key,
|
|
2301
|
-
() => { detailScrollRef.current?.scrollTo({ x: 0, y: 0 }); setDetailScrollOffset(0) },
|
|
2302
|
-
() => { detailScrollRef.current?.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER }); setDetailScrollOffset(Number.MAX_SAFE_INTEGER) },
|
|
2303
|
-
)) return
|
|
2304
|
-
if (key.name === "up" || key.name === "k") {
|
|
2305
|
-
detailScrollRef.current?.scrollBy({ x: 0, y: -1 })
|
|
2306
|
-
setDetailScrollOffset((current) => Math.max(0, current - 1))
|
|
2307
|
-
return
|
|
2308
|
-
}
|
|
2309
|
-
if (key.name === "down" || key.name === "j") {
|
|
2310
|
-
detailScrollRef.current?.scrollBy({ x: 0, y: 1 })
|
|
2311
|
-
setDetailScrollOffset((current) => current + 1)
|
|
2312
|
-
return
|
|
2313
|
-
}
|
|
2314
|
-
if (key.ctrl && key.name === "u") {
|
|
2315
|
-
detailScrollRef.current?.scrollBy({ x: 0, y: -halfPage })
|
|
2316
|
-
setDetailScrollOffset((current) => Math.max(0, current - halfPage))
|
|
2317
|
-
return
|
|
2318
|
-
}
|
|
2319
|
-
if (key.ctrl && (key.name === "d" || key.name === "v")) {
|
|
2320
|
-
detailScrollRef.current?.scrollBy({ x: 0, y: halfPage })
|
|
2321
|
-
setDetailScrollOffset((current) => current + halfPage)
|
|
2322
|
-
return
|
|
2323
|
-
}
|
|
2324
|
-
if (plainKey && key.name === "o" && selectedPullRequest) {
|
|
2325
|
-
runCommandById("pull.open-browser")
|
|
2326
|
-
return
|
|
2327
|
-
}
|
|
2328
|
-
if (plainKey && key.name === "y" && selectedPullRequest) {
|
|
2329
|
-
runCommandById("pull.copy-metadata")
|
|
2330
|
-
return
|
|
2331
|
-
}
|
|
2332
|
-
return
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
2262
|
if (filterMode) {
|
|
2336
2263
|
if (isSingleLineInputKey(key)) {
|
|
2337
2264
|
setFilterDraft((current) => editSingleLineInput(current, key) ?? current)
|
|
2338
2265
|
}
|
|
2339
|
-
return
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
if (key.name === "tab") {
|
|
2343
|
-
switchQueueMode(key.shift ? -1 : 1)
|
|
2344
|
-
return
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
if (key.name === "escape" && filterQuery.length > 0) {
|
|
2348
|
-
runCommandById("filter.clear")
|
|
2349
|
-
return
|
|
2350
|
-
}
|
|
2351
|
-
if (isWideLayout && selectedPullRequest && !detailFullView && !diffFullView) {
|
|
2352
|
-
if (key.name === "home") {
|
|
2353
|
-
scrollDetailPreviewTo(0)
|
|
2354
|
-
return
|
|
2355
|
-
}
|
|
2356
|
-
if (key.name === "end") {
|
|
2357
|
-
scrollDetailPreviewTo(Number.MAX_SAFE_INTEGER)
|
|
2358
|
-
return
|
|
2359
|
-
}
|
|
2360
|
-
if (key.name === "pageup") {
|
|
2361
|
-
scrollDetailPreviewBy(-halfPage)
|
|
2362
|
-
return
|
|
2363
|
-
}
|
|
2364
|
-
if (key.name === "pagedown") {
|
|
2365
|
-
scrollDetailPreviewBy(halfPage)
|
|
2366
|
-
return
|
|
2367
|
-
}
|
|
2368
|
-
}
|
|
2369
|
-
if (
|
|
2370
|
-
key.name === "[" ||
|
|
2371
|
-
((key.option || key.meta) && (key.name === "up" || key.name === "k")) ||
|
|
2372
|
-
(key.shift && key.name === "k") ||
|
|
2373
|
-
key.name === "K"
|
|
2374
|
-
) {
|
|
2375
|
-
setSelectedIndex((current) => {
|
|
2376
|
-
if (visiblePullRequests.length === 0 || groupStarts.length === 0) return 0
|
|
2377
|
-
const currentGroup = getCurrentGroupIndex(current)
|
|
2378
|
-
if (currentGroup <= 0) return groupStarts[groupStarts.length - 1]!
|
|
2379
|
-
return groupStarts[currentGroup - 1]!
|
|
2380
|
-
})
|
|
2381
|
-
return
|
|
2382
2266
|
}
|
|
2383
|
-
if (
|
|
2384
|
-
key.name === "]" ||
|
|
2385
|
-
((key.option || key.meta) && (key.name === "down" || key.name === "j")) ||
|
|
2386
|
-
(key.shift && key.name === "j") ||
|
|
2387
|
-
key.name === "J"
|
|
2388
|
-
) {
|
|
2389
|
-
setSelectedIndex((current) => {
|
|
2390
|
-
if (visiblePullRequests.length === 0 || groupStarts.length === 0) return 0
|
|
2391
|
-
const currentGroup = getCurrentGroupIndex(current)
|
|
2392
|
-
if (currentGroup >= groupStarts.length - 1) return groupStarts[0]!
|
|
2393
|
-
return groupStarts[currentGroup + 1]!
|
|
2394
|
-
})
|
|
2395
|
-
return
|
|
2396
|
-
}
|
|
2397
|
-
if (key.ctrl && key.name === "u") {
|
|
2398
|
-
setSelectedIndex((current) => {
|
|
2399
|
-
if (visiblePullRequests.length === 0) return 0
|
|
2400
|
-
return Math.max(0, current - halfPage)
|
|
2401
|
-
})
|
|
2402
|
-
return
|
|
2403
|
-
}
|
|
2404
|
-
if (key.ctrl && key.name === "d") {
|
|
2405
|
-
setSelectedIndex((current) => {
|
|
2406
|
-
if (visiblePullRequests.length === 0) return 0
|
|
2407
|
-
return Math.min(visiblePullRequests.length - 1, current + halfPage)
|
|
2408
|
-
})
|
|
2409
|
-
return
|
|
2410
|
-
}
|
|
2411
|
-
if (key.name === "up" || key.name === "k") {
|
|
2412
|
-
setSelectedIndex((current) => {
|
|
2413
|
-
if (visiblePullRequests.length === 0) return 0
|
|
2414
|
-
return current <= 0 ? visiblePullRequests.length - 1 : current - 1
|
|
2415
|
-
})
|
|
2416
|
-
return
|
|
2417
|
-
}
|
|
2418
|
-
if (key.name === "down" || key.name === "j") {
|
|
2419
|
-
if (visiblePullRequests.length > 0 && selectedIndex >= visiblePullRequests.length - 1 && hasMorePullRequests) {
|
|
2420
|
-
loadMorePullRequests()
|
|
2421
|
-
return
|
|
2422
|
-
}
|
|
2423
|
-
setSelectedIndex((current) => {
|
|
2424
|
-
if (visiblePullRequests.length === 0) return 0
|
|
2425
|
-
return current >= visiblePullRequests.length - 1 ? 0 : current + 1
|
|
2426
|
-
})
|
|
2427
|
-
return
|
|
2428
|
-
}
|
|
2429
|
-
if (handleVimGoto(key,
|
|
2430
|
-
() => setSelectedIndex(0),
|
|
2431
|
-
() => setSelectedIndex(visiblePullRequests.length === 0 ? 0 : visiblePullRequests.length - 1),
|
|
2432
|
-
)) return
|
|
2433
2267
|
})
|
|
2434
2268
|
|
|
2269
|
+
if (isInitialLoading) {
|
|
2270
|
+
return (
|
|
2271
|
+
<box width={terminalWidth} height={terminalHeight} flexDirection="column" backgroundColor={colors.background}>
|
|
2272
|
+
<LoadingLogoPane content={detailPlaceholderContent} width={contentWidth} height={terminalHeight} frame={loadingFrame} />
|
|
2273
|
+
</box>
|
|
2274
|
+
)
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2435
2277
|
const fullscreenContentWidth = Math.max(24, contentWidth - 2)
|
|
2436
2278
|
const fullscreenBodyLines = Math.max(8, terminalHeight - 8)
|
|
2437
|
-
const
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
paneWidth: contentWidth,
|
|
2442
|
-
showChecks: true,
|
|
2443
|
-
}) > wideBodyHeight
|
|
2444
|
-
const narrowFullscreenDetailScrollable = getDetailsPaneHeight({
|
|
2445
|
-
pullRequest: selectedPullRequest,
|
|
2446
|
-
contentWidth: fullscreenContentWidth,
|
|
2447
|
-
bodyLines: DETAIL_BODY_SCROLL_LIMIT,
|
|
2448
|
-
paneWidth: contentWidth,
|
|
2449
|
-
}) > wideBodyHeight
|
|
2279
|
+
const fullscreenDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, contentWidth, isWideLayout)
|
|
2280
|
+
const fullscreenDetailBodyViewportHeight = Math.max(1, wideBodyHeight - fullscreenDetailHeaderHeight)
|
|
2281
|
+
const fullscreenDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, fullscreenContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
2282
|
+
const fullscreenDetailBodyScrollable = fullscreenDetailBodyHeight > fullscreenDetailBodyViewportHeight
|
|
2450
2283
|
const wideDetailHeaderHeight = getDetailHeaderHeight(selectedPullRequest, rightPaneWidth, true)
|
|
2451
2284
|
const wideDetailBodyViewportHeight = Math.max(1, wideBodyHeight - wideDetailHeaderHeight)
|
|
2452
|
-
const wideDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, rightContentWidth)
|
|
2285
|
+
const wideDetailBodyHeight = getScrollableDetailBodyHeight(selectedPullRequest, rightContentWidth, selectedConversationItems, selectedConversationStatus)
|
|
2453
2286
|
const wideDetailBodyScrollable = wideDetailBodyHeight > wideDetailBodyViewportHeight
|
|
2454
2287
|
|
|
2455
2288
|
const prListProps = {
|
|
@@ -2463,6 +2296,7 @@ export const App = () => {
|
|
|
2463
2296
|
loadedCount: loadedPullRequestCount,
|
|
2464
2297
|
hasMore: hasMorePullRequests,
|
|
2465
2298
|
isLoadingMore: isLoadingMorePullRequests,
|
|
2299
|
+
loadingIndicator,
|
|
2466
2300
|
onSelectPullRequest: selectPullRequestByUrl,
|
|
2467
2301
|
} as const
|
|
2468
2302
|
|
|
@@ -2491,8 +2325,8 @@ export const App = () => {
|
|
|
2491
2325
|
const commentThreadModalHeight = commentThreadLayout.height
|
|
2492
2326
|
const commentThreadModalLeft = commentThreadLayout.left
|
|
2493
2327
|
const commentThreadModalTop = commentThreadLayout.top
|
|
2494
|
-
const commentAnchorLabel = selectedDiffCommentAnchor
|
|
2495
|
-
? `${selectedDiffCommentAnchor.path}
|
|
2328
|
+
const commentAnchorLabel = selectedDiffCommentAnchor && selectedDiffCommentLabel
|
|
2329
|
+
? `${selectedDiffCommentAnchor.path} ${selectedDiffCommentLabel}`
|
|
2496
2330
|
: "No diff line selected"
|
|
2497
2331
|
const mergeLayout = sizedModal(46, 68, 12, 16)
|
|
2498
2332
|
const mergeModalWidth = mergeLayout.width
|
|
@@ -2520,28 +2354,27 @@ export const App = () => {
|
|
|
2520
2354
|
<box paddingLeft={1} paddingRight={1} flexDirection="column" backgroundColor={colors.background}>
|
|
2521
2355
|
<PlainLine text={headerLine} fg={colors.muted} bold />
|
|
2522
2356
|
</box>
|
|
2523
|
-
{isWideLayout && !detailFullView && !diffFullView
|
|
2357
|
+
{isWideLayout && !detailFullView && !diffFullView ? (
|
|
2524
2358
|
<Divider width={contentWidth} junctionAt={dividerJunctionAt} junctionChar="┬" />
|
|
2525
2359
|
) : (
|
|
2526
2360
|
<Divider width={contentWidth} />
|
|
2527
2361
|
)}
|
|
2528
|
-
{
|
|
2529
|
-
<LoadingPane content={detailPlaceholderContent} width={contentWidth} height={wideBodyHeight} />
|
|
2530
|
-
) : diffFullView ? (
|
|
2362
|
+
{diffFullView ? (
|
|
2531
2363
|
<PullRequestDiffPane
|
|
2532
2364
|
pullRequest={selectedPullRequest}
|
|
2533
|
-
diffState={
|
|
2365
|
+
diffState={displayedDiffState}
|
|
2534
2366
|
stackedFiles={stackedDiffFiles}
|
|
2535
2367
|
scrollTop={diffScrollTop}
|
|
2536
2368
|
view={effectiveDiffRenderView}
|
|
2369
|
+
whitespaceMode={diffWhitespaceMode}
|
|
2537
2370
|
wrapMode={diffWrapMode}
|
|
2538
2371
|
paneWidth={contentWidth}
|
|
2539
2372
|
height={wideBodyHeight}
|
|
2540
2373
|
loadingIndicator={loadingIndicator}
|
|
2541
2374
|
scrollRef={diffScrollRef}
|
|
2542
2375
|
setDiffRef={setDiffRenderableRef}
|
|
2543
|
-
commentMode={diffCommentMode}
|
|
2544
2376
|
selectedCommentAnchor={selectedDiffCommentAnchor}
|
|
2377
|
+
selectedCommentLabel={selectedDiffCommentLabel}
|
|
2545
2378
|
selectedCommentThread={selectedDiffCommentThread}
|
|
2546
2379
|
onSelectCommentLine={selectDiffCommentLine}
|
|
2547
2380
|
themeId={themeId}
|
|
@@ -2553,20 +2386,16 @@ export const App = () => {
|
|
|
2553
2386
|
</box>
|
|
2554
2387
|
) : isWideLayout && detailFullView ? (
|
|
2555
2388
|
<box flexGrow={1} flexDirection="column">
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
pullRequest={selectedPullRequest}
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
loadingIndicator={loadingIndicator}
|
|
2567
|
-
themeId={themeId}
|
|
2568
|
-
/>
|
|
2569
|
-
</scrollbox>
|
|
2389
|
+
{selectedPullRequest ? (
|
|
2390
|
+
<>
|
|
2391
|
+
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} showChecks />
|
|
2392
|
+
<scrollbox ref={detailScrollRef} focusable={false} flexGrow={1} verticalScrollbarOptions={{ visible: fullscreenDetailBodyScrollable }}>
|
|
2393
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={fullscreenContentWidth} bodyLines={fullscreenBodyLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2394
|
+
</scrollbox>
|
|
2395
|
+
</>
|
|
2396
|
+
) : (
|
|
2397
|
+
<DetailsPane pullRequest={null} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2398
|
+
)}
|
|
2570
2399
|
</box>
|
|
2571
2400
|
) : isWideLayout ? (
|
|
2572
2401
|
<box key="wide-main" flexGrow={1} flexDirection="row">
|
|
@@ -2588,7 +2417,7 @@ export const App = () => {
|
|
|
2588
2417
|
<>
|
|
2589
2418
|
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={rightContentWidth} paneWidth={rightPaneWidth} showChecks />
|
|
2590
2419
|
<scrollbox ref={detailPreviewScrollRef} flexGrow={1} verticalScrollbarOptions={{ visible: wideDetailBodyScrollable }}>
|
|
2591
|
-
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2420
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={rightContentWidth} bodyLines={wideDetailLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2592
2421
|
</scrollbox>
|
|
2593
2422
|
</>
|
|
2594
2423
|
) : (
|
|
@@ -2598,23 +2427,20 @@ export const App = () => {
|
|
|
2598
2427
|
</box>
|
|
2599
2428
|
) : detailFullView ? (
|
|
2600
2429
|
<box flexGrow={1} flexDirection="column">
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
pullRequest={selectedPullRequest}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
themeId={themeId}
|
|
2612
|
-
/>
|
|
2613
|
-
</scrollbox>
|
|
2430
|
+
{selectedPullRequest ? (
|
|
2431
|
+
<>
|
|
2432
|
+
<DetailHeader pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} />
|
|
2433
|
+
<scrollbox ref={detailScrollRef} focusable={false} flexGrow={1} verticalScrollbarOptions={{ visible: fullscreenDetailBodyScrollable }}>
|
|
2434
|
+
<DetailBody pullRequest={selectedPullRequest} contentWidth={fullscreenContentWidth} bodyLines={fullscreenBodyLines} bodyLineLimit={DETAIL_BODY_SCROLL_LIMIT} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2435
|
+
</scrollbox>
|
|
2436
|
+
</>
|
|
2437
|
+
) : (
|
|
2438
|
+
<DetailsPane pullRequest={null} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2439
|
+
)}
|
|
2614
2440
|
</box>
|
|
2615
2441
|
) : (
|
|
2616
2442
|
<box key="narrow-main" height={wideBodyHeight} flexDirection="column">
|
|
2617
|
-
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2443
|
+
<DetailsPane pullRequest={selectedPullRequest} viewerUsername={username} contentWidth={fullscreenContentWidth} paneWidth={contentWidth} conversationItems={selectedConversationItems} conversationStatus={selectedConversationStatus} placeholderContent={detailPlaceholderContent} loadingIndicator={loadingIndicator} themeId={themeId} />
|
|
2618
2444
|
<Divider width={contentWidth} />
|
|
2619
2445
|
<box flexGrow={1} flexDirection="column">
|
|
2620
2446
|
<scrollbox ref={prListScrollRef} focusable={false} flexGrow={1}>
|
|
@@ -2626,7 +2452,7 @@ export const App = () => {
|
|
|
2626
2452
|
</box>
|
|
2627
2453
|
)}
|
|
2628
2454
|
|
|
2629
|
-
{isWideLayout && !detailFullView && !diffFullView
|
|
2455
|
+
{isWideLayout && !detailFullView && !diffFullView ? (
|
|
2630
2456
|
<Divider width={contentWidth} junctionAt={dividerJunctionAt} junctionChar="┴" />
|
|
2631
2457
|
) : (
|
|
2632
2458
|
<Divider width={contentWidth} />
|
|
@@ -2640,7 +2466,7 @@ export const App = () => {
|
|
|
2640
2466
|
showFilterClear={filterMode || filterQuery.length > 0}
|
|
2641
2467
|
detailFullView={detailFullView}
|
|
2642
2468
|
diffFullView={diffFullView}
|
|
2643
|
-
|
|
2469
|
+
diffRangeActive={diffCommentRangeActive}
|
|
2644
2470
|
hasSelection={selectedPullRequest !== null}
|
|
2645
2471
|
canCloseSelection={selectedPullRequest?.state === "open"}
|
|
2646
2472
|
hasError={pullRequestStatus === "error"}
|
|
@@ -2730,6 +2556,8 @@ export const App = () => {
|
|
|
2730
2556
|
modalHeight={commandPaletteHeight}
|
|
2731
2557
|
offsetLeft={commandPaletteLeft}
|
|
2732
2558
|
offsetTop={commandPaletteTop}
|
|
2559
|
+
onSelectCommandIndex={selectCommandPaletteIndex}
|
|
2560
|
+
onRunCommand={runCommandPaletteCommand}
|
|
2733
2561
|
/>
|
|
2734
2562
|
) : null}
|
|
2735
2563
|
</box>
|