@kitlangton/ghui 0.1.18 → 0.1.20
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/README.md +14 -3
- package/bin/ghui.js +64 -1
- package/package.json +6 -2
- package/src/App.tsx +932 -510
- package/src/appCommands.ts +330 -0
- package/src/commands.ts +68 -0
- package/src/config.ts +10 -0
- package/src/domain.ts +43 -13
- package/src/errors.ts +10 -0
- package/src/index.tsx +23 -2
- package/src/mergeActions.ts +1 -6
- package/src/pullRequestCache.ts +19 -0
- package/src/pullRequestViews.ts +45 -0
- package/src/services/BrowserOpener.ts +22 -0
- package/src/services/Clipboard.ts +46 -0
- package/src/services/CommandRunner.ts +14 -6
- package/src/services/GitHubService.ts +327 -161
- package/src/services/MockGitHubService.ts +146 -0
- package/src/ui/CommandPalette.tsx +143 -0
- package/src/ui/DetailsPane.tsx +49 -63
- package/src/ui/FooterHints.tsx +91 -182
- package/src/ui/PullRequestDiffPane.tsx +105 -87
- package/src/ui/PullRequestList.tsx +102 -49
- package/src/ui/colors.ts +167 -1
- package/src/ui/diff.ts +69 -63
- package/src/ui/diffStats.tsx +25 -0
- package/src/ui/modals.tsx +270 -302
- package/src/ui/primitives.tsx +92 -2
- package/src/ui/pullRequests.ts +44 -12
- package/src/ui/singleLineInput.ts +25 -0
package/src/ui/FooterHints.tsx
CHANGED
|
@@ -1,190 +1,99 @@
|
|
|
1
|
+
import { Data } from "effect"
|
|
1
2
|
import { colors } from "./colors.js"
|
|
2
|
-
import {
|
|
3
|
+
import { HintRow, type HintItem } from "./primitives.js"
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
readonly max: number
|
|
5
|
+
export type RetryProgress = Data.TaggedEnum<{
|
|
6
|
+
Idle: {}
|
|
7
|
+
Retrying: { readonly attempt: number; readonly max: number }
|
|
8
|
+
}>
|
|
9
|
+
|
|
10
|
+
export const RetryProgress = Data.taggedEnum<RetryProgress>()
|
|
11
|
+
export const initialRetryProgress: RetryProgress = RetryProgress.Idle()
|
|
12
|
+
|
|
13
|
+
interface HintsContext {
|
|
14
|
+
readonly filterEditing: boolean
|
|
15
|
+
readonly showFilterClear: boolean
|
|
16
|
+
readonly detailFullView: boolean
|
|
17
|
+
readonly diffFullView: boolean
|
|
18
|
+
readonly diffCommentMode: boolean
|
|
19
|
+
readonly hasSelection: boolean
|
|
20
|
+
readonly canCloseSelection: boolean
|
|
21
|
+
readonly hasError: boolean
|
|
22
|
+
readonly isLoading: boolean
|
|
23
|
+
readonly loadingIndicator: string
|
|
24
|
+
readonly retryProgress: RetryProgress
|
|
7
25
|
}
|
|
8
26
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
canCloseSelection: boolean
|
|
29
|
-
hasError: boolean
|
|
30
|
-
isLoading: boolean
|
|
31
|
-
loadingIndicator: string
|
|
32
|
-
retryProgress: RetryProgress | null
|
|
33
|
-
}) => {
|
|
34
|
-
if (filterEditing) {
|
|
35
|
-
return (
|
|
36
|
-
<TextLine>
|
|
37
|
-
<span fg={colors.count}>search</span>
|
|
38
|
-
<span fg={colors.muted}> typing </span>
|
|
39
|
-
<span fg={colors.count}>↑↓</span>
|
|
40
|
-
<span fg={colors.muted}> move </span>
|
|
41
|
-
<span fg={colors.count}>enter</span>
|
|
42
|
-
<span fg={colors.muted}> apply </span>
|
|
43
|
-
<span fg={colors.count}>esc</span>
|
|
44
|
-
<span fg={colors.muted}> cancel </span>
|
|
45
|
-
<span fg={colors.count}>ctrl-u</span>
|
|
46
|
-
<span fg={colors.muted}> clear </span>
|
|
47
|
-
<span fg={colors.count}>ctrl-w</span>
|
|
48
|
-
<span fg={colors.muted}> word</span>
|
|
49
|
-
</TextLine>
|
|
50
|
-
)
|
|
51
|
-
}
|
|
27
|
+
const filterEditingHints: readonly HintItem[] = [
|
|
28
|
+
{ key: "search", label: "typing" },
|
|
29
|
+
{ key: "↑↓", label: "move" },
|
|
30
|
+
{ key: "enter", label: "apply" },
|
|
31
|
+
{ key: "esc", label: "cancel" },
|
|
32
|
+
{ key: "ctrl-u", label: "clear" },
|
|
33
|
+
{ key: "ctrl-w", label: "word" },
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const diffCommentModeHints: readonly HintItem[] = [
|
|
37
|
+
{ key: "↑↓", label: "line" },
|
|
38
|
+
{ key: "pgup/pgdn", label: "jump" },
|
|
39
|
+
{ key: "←→", label: "side" },
|
|
40
|
+
{ key: "enter", label: "open" },
|
|
41
|
+
{ key: "a", label: "comment" },
|
|
42
|
+
{ key: "c", label: "done" },
|
|
43
|
+
{ key: "[]", label: "files" },
|
|
44
|
+
{ key: "esc", label: "back" },
|
|
45
|
+
]
|
|
52
46
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<span fg={colors.count}>enter</span>
|
|
64
|
-
<span fg={colors.muted}> open </span>
|
|
65
|
-
<span fg={colors.count}>a</span>
|
|
66
|
-
<span fg={colors.muted}> comment </span>
|
|
67
|
-
<span fg={colors.count}>c</span>
|
|
68
|
-
<span fg={colors.muted}> done </span>
|
|
69
|
-
<span fg={colors.count}>[]</span>
|
|
70
|
-
<span fg={colors.muted}> files </span>
|
|
71
|
-
<span fg={colors.count}>esc</span>
|
|
72
|
-
<span fg={colors.muted}> back</span>
|
|
73
|
-
</TextLine>
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
return (
|
|
77
|
-
<TextLine>
|
|
78
|
-
<span fg={colors.count}>esc</span>
|
|
79
|
-
<span fg={colors.muted}> back </span>
|
|
80
|
-
<span fg={colors.count}>v</span>
|
|
81
|
-
<span fg={colors.muted}> view </span>
|
|
82
|
-
<span fg={colors.count}>w</span>
|
|
83
|
-
<span fg={colors.muted}> wrap </span>
|
|
84
|
-
<span fg={colors.count}>c</span>
|
|
85
|
-
<span fg={colors.muted}> comment </span>
|
|
86
|
-
<span fg={colors.count}>[]</span>
|
|
87
|
-
<span fg={colors.muted}> files </span>
|
|
88
|
-
<span fg={colors.count}>r</span>
|
|
89
|
-
<span fg={colors.muted}> reload </span>
|
|
90
|
-
<span fg={colors.count}>o</span>
|
|
91
|
-
<span fg={colors.muted}> open </span>
|
|
92
|
-
<span fg={colors.count}>q</span>
|
|
93
|
-
<span fg={colors.muted}> quit</span>
|
|
94
|
-
</TextLine>
|
|
95
|
-
)
|
|
96
|
-
}
|
|
47
|
+
const diffViewHints: readonly HintItem[] = [
|
|
48
|
+
{ key: "esc", label: "back" },
|
|
49
|
+
{ key: "v", label: "view" },
|
|
50
|
+
{ key: "w", label: "wrap" },
|
|
51
|
+
{ key: "c", label: "comment" },
|
|
52
|
+
{ key: "[]", label: "files" },
|
|
53
|
+
{ key: "r", label: "reload" },
|
|
54
|
+
{ key: "o", label: "open" },
|
|
55
|
+
{ key: "q", label: "quit" },
|
|
56
|
+
]
|
|
97
57
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
<span fg={colors.muted}> state </span>
|
|
113
|
-
<span fg={colors.count}>d</span>
|
|
114
|
-
<span fg={colors.muted}> diff </span>
|
|
115
|
-
<span fg={colors.count}>l</span>
|
|
116
|
-
<span fg={colors.muted}> labels </span>
|
|
117
|
-
<span fg={colors.count}>m</span>
|
|
118
|
-
<span fg={colors.muted}> merge </span>
|
|
119
|
-
{canCloseSelection ? (
|
|
120
|
-
<>
|
|
121
|
-
<span fg={colors.count}>x</span>
|
|
122
|
-
<span fg={colors.muted}> close </span>
|
|
123
|
-
</>
|
|
124
|
-
) : null}
|
|
125
|
-
</>
|
|
126
|
-
) : null}
|
|
127
|
-
<span fg={colors.count}>o</span>
|
|
128
|
-
<span fg={colors.muted}> open </span>
|
|
129
|
-
<span fg={colors.count}>y</span>
|
|
130
|
-
<span fg={colors.muted}> copy </span>
|
|
131
|
-
<span fg={colors.count}>q</span>
|
|
132
|
-
<span fg={colors.muted}> quit</span>
|
|
133
|
-
</TextLine>
|
|
134
|
-
)
|
|
135
|
-
}
|
|
58
|
+
const detailFullViewHints = (ctx: HintsContext): readonly HintItem[] => [
|
|
59
|
+
{ key: "esc", label: "back" },
|
|
60
|
+
{ key: "↑↓", label: "scroll" },
|
|
61
|
+
{ key: "r", label: ctx.hasError ? "retry" : "refresh" },
|
|
62
|
+
{ key: "t", label: "theme" },
|
|
63
|
+
{ key: "s", label: "state", when: ctx.hasSelection },
|
|
64
|
+
{ key: "d", label: "diff", when: ctx.hasSelection },
|
|
65
|
+
{ key: "l", label: "labels", when: ctx.hasSelection },
|
|
66
|
+
{ key: "m", label: "merge", when: ctx.hasSelection },
|
|
67
|
+
{ key: "x", label: "close", when: ctx.hasSelection && ctx.canCloseSelection },
|
|
68
|
+
{ key: "o", label: "open" },
|
|
69
|
+
{ key: "y", label: "copy" },
|
|
70
|
+
{ key: "q", label: "quit" },
|
|
71
|
+
]
|
|
136
72
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
<span fg={colors.muted}> {retryProgress.attempt}/{retryProgress.max} </span>
|
|
155
|
-
</>
|
|
156
|
-
) : isLoading ? (
|
|
157
|
-
<>
|
|
158
|
-
<span fg={colors.status.pending}>{loadingIndicator}</span>
|
|
159
|
-
<span fg={colors.muted}> loading </span>
|
|
160
|
-
</>
|
|
161
|
-
) : null}
|
|
162
|
-
<span fg={colors.count}>r</span>
|
|
163
|
-
<span fg={colors.muted}>{hasError ? " retry " : " refresh "}</span>
|
|
164
|
-
{hasSelection ? (
|
|
165
|
-
<>
|
|
166
|
-
<span fg={colors.count}>s</span>
|
|
167
|
-
<span fg={colors.muted}> state </span>
|
|
168
|
-
<span fg={colors.count}>d</span>
|
|
169
|
-
<span fg={colors.muted}> diff </span>
|
|
170
|
-
<span fg={colors.count}>l</span>
|
|
171
|
-
<span fg={colors.muted}> labels </span>
|
|
172
|
-
<span fg={colors.count}>m</span>
|
|
173
|
-
<span fg={colors.muted}> merge </span>
|
|
174
|
-
{canCloseSelection ? (
|
|
175
|
-
<>
|
|
176
|
-
<span fg={colors.count}>x</span>
|
|
177
|
-
<span fg={colors.muted}> close </span>
|
|
178
|
-
</>
|
|
179
|
-
) : null}
|
|
180
|
-
<span fg={colors.count}>o</span>
|
|
181
|
-
<span fg={colors.muted}> open </span>
|
|
182
|
-
<span fg={colors.count}>y</span>
|
|
183
|
-
<span fg={colors.muted}> copy </span>
|
|
184
|
-
</>
|
|
185
|
-
) : null}
|
|
186
|
-
<span fg={colors.count}>q</span>
|
|
187
|
-
<span fg={colors.muted}> quit</span>
|
|
188
|
-
</TextLine>
|
|
189
|
-
)
|
|
73
|
+
const defaultHints = (ctx: HintsContext): readonly HintItem[] => {
|
|
74
|
+
const retrying = ctx.retryProgress._tag === "Retrying"
|
|
75
|
+
return [
|
|
76
|
+
{ key: "/", label: "filter" },
|
|
77
|
+
{ key: "t", label: "theme" },
|
|
78
|
+
{ key: "esc", label: "clear", when: ctx.showFilterClear },
|
|
79
|
+
{ key: "retry", label: retrying ? `${(ctx.retryProgress as { attempt: number; max: number }).attempt}/${(ctx.retryProgress as { attempt: number; max: number }).max}` : "", when: retrying, keyFg: colors.status.pending },
|
|
80
|
+
{ key: ctx.loadingIndicator, label: "loading", when: !retrying && ctx.isLoading, keyFg: colors.status.pending },
|
|
81
|
+
{ key: "r", label: "retry", when: ctx.hasError },
|
|
82
|
+
{ key: "d", label: "diff", when: ctx.hasSelection },
|
|
83
|
+
{ key: "l", label: "labels", when: ctx.hasSelection },
|
|
84
|
+
{ key: "m", label: "merge", when: ctx.hasSelection },
|
|
85
|
+
{ key: "x", label: "close", when: ctx.hasSelection && ctx.canCloseSelection },
|
|
86
|
+
{ key: "o", label: "open", when: ctx.hasSelection },
|
|
87
|
+
{ key: "y", label: "copy", when: ctx.hasSelection },
|
|
88
|
+
{ key: "ctrl-p", label: "commands" },
|
|
89
|
+
]
|
|
190
90
|
}
|
|
91
|
+
|
|
92
|
+
const footerHints = (ctx: HintsContext): readonly HintItem[] => {
|
|
93
|
+
if (ctx.filterEditing) return filterEditingHints
|
|
94
|
+
if (ctx.diffFullView) return ctx.diffCommentMode ? diffCommentModeHints : diffViewHints
|
|
95
|
+
if (ctx.detailFullView) return detailFullViewHints(ctx)
|
|
96
|
+
return defaultHints(ctx)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const FooterHints = (props: HintsContext) => <HintRow items={footerHints(props)} />
|
|
@@ -1,34 +1,31 @@
|
|
|
1
|
-
import type { DiffRenderable, ScrollBoxRenderable } from "@opentui/core"
|
|
1
|
+
import type { DiffRenderable, MouseEvent, ScrollBoxRenderable } from "@opentui/core"
|
|
2
2
|
import { useMemo, type Ref } from "react"
|
|
3
|
-
import type { PullRequestItem, PullRequestReviewComment } from "../domain.js"
|
|
3
|
+
import type { DiffCommentSide, PullRequestItem, PullRequestReviewComment } from "../domain.js"
|
|
4
4
|
import { colors, type ThemeId } from "./colors.js"
|
|
5
|
-
import { createDiffSyntaxStyle, diffFileStats,
|
|
5
|
+
import { createDiffSyntaxStyle, diffFileStats, diffFileStatsText, diffStatText, stackedDiffFileAtLine, type DiffFileStats, type DiffView, type DiffWrapMode, type PullRequestDiffState, type StackedDiffCommentAnchor, type StackedDiffFilePatch } from "./diff.js"
|
|
6
6
|
import { LoadingPane, StatusCard } from "./DetailsPane.js"
|
|
7
|
-
import {
|
|
7
|
+
import { DiffStats } from "./diffStats.js"
|
|
8
|
+
import { Divider, fitCell, PaddedRow, PlainLine, TextLine } from "./primitives.js"
|
|
8
9
|
import { shortRepoName } from "./pullRequests.js"
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
pullRequest.additions > 0 ? { key: "additions", text: `+${pullRequest.additions}`, color: colors.status.passing } : null,
|
|
16
|
-
pullRequest.deletions > 0 ? { key: "deletions", text: `-${pullRequest.deletions}`, color: colors.status.failing } : null,
|
|
17
|
-
{ key: "files", text: files, color: colors.muted },
|
|
18
|
-
]
|
|
19
|
-
const parts = rawParts.filter((part): part is Part => part !== null)
|
|
20
|
-
|
|
11
|
+
const DiffPaneHeader = ({ pullRequest, paneWidth }: { pullRequest: PullRequestItem; paneWidth: number }) => {
|
|
12
|
+
const stats = diffStatText(pullRequest)
|
|
13
|
+
const headerWidth = Math.max(24, paneWidth - 2)
|
|
14
|
+
const leftHeader = `#${pullRequest.number} ${shortRepoName(pullRequest.repository)}`
|
|
15
|
+
const headerGap = Math.max(2, headerWidth - leftHeader.length - stats.length)
|
|
21
16
|
return (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<span
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
<PaddedRow>
|
|
18
|
+
<TextLine>
|
|
19
|
+
<span fg={colors.count}>#{pullRequest.number}</span>
|
|
20
|
+
<span fg={colors.muted}> {shortRepoName(pullRequest.repository)}</span>
|
|
21
|
+
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
22
|
+
<DiffStats pullRequest={pullRequest} />
|
|
23
|
+
</TextLine>
|
|
24
|
+
</PaddedRow>
|
|
27
25
|
)
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
const FileStats = ({
|
|
31
|
-
const stats = diffFileStats(file)
|
|
28
|
+
const FileStats = ({ stats }: { stats: DiffFileStats }) => {
|
|
32
29
|
return (
|
|
33
30
|
<>
|
|
34
31
|
{stats.additions > 0 ? <span fg={colors.status.passing}>{`+${stats.additions}`}</span> : null}
|
|
@@ -38,16 +35,32 @@ const FileStats = ({ file }: { file: StackedDiffFilePatch["file"] }) => {
|
|
|
38
35
|
)
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
const FileHeader = ({
|
|
38
|
+
const FileHeader = ({
|
|
39
|
+
file,
|
|
40
|
+
index,
|
|
41
|
+
count,
|
|
42
|
+
width,
|
|
43
|
+
suffix = "",
|
|
44
|
+
suffixColor = colors.muted,
|
|
45
|
+
}: {
|
|
46
|
+
file: StackedDiffFilePatch["file"]
|
|
47
|
+
index: number
|
|
48
|
+
count: number
|
|
49
|
+
width: number
|
|
50
|
+
suffix?: string
|
|
51
|
+
suffixColor?: string
|
|
52
|
+
}) => {
|
|
42
53
|
const counter = `${index + 1}/${count}`
|
|
43
|
-
const
|
|
44
|
-
const
|
|
54
|
+
const stats = diffFileStats(file)
|
|
55
|
+
const statsText = diffFileStatsText(stats)
|
|
56
|
+
const nameWidth = Math.max(1, width - counter.length - statsText.length - suffix.length - 5)
|
|
45
57
|
return (
|
|
46
58
|
<TextLine>
|
|
47
59
|
<span fg={colors.muted}>{counter} </span>
|
|
48
60
|
<span fg={colors.text}>{fitCell(file.name, nameWidth)}</span>
|
|
49
61
|
{statsText ? <span fg={colors.muted}> </span> : null}
|
|
50
|
-
<FileStats
|
|
62
|
+
<FileStats stats={stats} />
|
|
63
|
+
{suffix ? <span fg={suffixColor}>{suffix}</span> : null}
|
|
51
64
|
</TextLine>
|
|
52
65
|
)
|
|
53
66
|
}
|
|
@@ -56,7 +69,7 @@ export const PullRequestDiffPane = ({
|
|
|
56
69
|
pullRequest,
|
|
57
70
|
diffState,
|
|
58
71
|
stackedFiles,
|
|
59
|
-
|
|
72
|
+
scrollTop,
|
|
60
73
|
view,
|
|
61
74
|
wrapMode,
|
|
62
75
|
paneWidth,
|
|
@@ -67,83 +80,60 @@ export const PullRequestDiffPane = ({
|
|
|
67
80
|
commentMode,
|
|
68
81
|
selectedCommentAnchor,
|
|
69
82
|
selectedCommentThread,
|
|
70
|
-
|
|
83
|
+
onSelectCommentLine,
|
|
71
84
|
themeId,
|
|
72
85
|
}: {
|
|
73
86
|
pullRequest: PullRequestItem | null
|
|
74
87
|
diffState: PullRequestDiffState | undefined
|
|
75
88
|
stackedFiles: readonly StackedDiffFilePatch[]
|
|
76
|
-
|
|
77
|
-
view:
|
|
78
|
-
wrapMode:
|
|
89
|
+
scrollTop: number
|
|
90
|
+
view: DiffView
|
|
91
|
+
wrapMode: DiffWrapMode
|
|
79
92
|
paneWidth: number
|
|
80
93
|
height: number
|
|
81
94
|
loadingIndicator: string
|
|
82
95
|
scrollRef: Ref<ScrollBoxRenderable>
|
|
83
96
|
setDiffRef: (index: number, diff: DiffRenderable | null) => void
|
|
84
97
|
commentMode: boolean
|
|
85
|
-
selectedCommentAnchor:
|
|
98
|
+
selectedCommentAnchor: StackedDiffCommentAnchor | null
|
|
86
99
|
selectedCommentThread: readonly PullRequestReviewComment[]
|
|
87
|
-
|
|
100
|
+
onSelectCommentLine: (renderLine: number, side: DiffCommentSide | null) => void
|
|
88
101
|
themeId: ThemeId
|
|
89
102
|
}) => {
|
|
90
|
-
const readyFiles = diffState?.
|
|
91
|
-
const safeIndex = safeDiffFileIndex(readyFiles, fileIndex)
|
|
92
|
-
const file = readyFiles[safeIndex] ?? null
|
|
103
|
+
const readyFiles = diffState?._tag === "Ready" ? diffState.files : []
|
|
93
104
|
const syntaxStyle = useMemo(() => createDiffSyntaxStyle(), [themeId])
|
|
94
105
|
|
|
95
106
|
if (!pullRequest) {
|
|
96
107
|
return <LoadingPane content={{ title: "No pull request selected", hint: "Press esc to go back" }} width={paneWidth} height={height} />
|
|
97
108
|
}
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
const headerWidth = Math.max(24, paneWidth - 2)
|
|
101
|
-
const leftHeader = `#${pullRequest.number} ${shortRepoName(pullRequest.repository)}`
|
|
102
|
-
const headerGap = Math.max(2, headerWidth - leftHeader.length - stats.length)
|
|
103
|
-
|
|
104
|
-
if (!diffState || diffState.status === "loading") {
|
|
110
|
+
if (!diffState || diffState._tag === "Loading") {
|
|
105
111
|
return (
|
|
106
112
|
<box height={height} flexDirection="column">
|
|
107
|
-
<
|
|
108
|
-
<TextLine>
|
|
109
|
-
<span fg={colors.count}>#{pullRequest.number}</span>
|
|
110
|
-
<span fg={colors.muted}> {shortRepoName(pullRequest.repository)}</span>
|
|
111
|
-
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
112
|
-
<DiffStats pullRequest={pullRequest} />
|
|
113
|
-
</TextLine>
|
|
114
|
-
</box>
|
|
113
|
+
<DiffPaneHeader pullRequest={pullRequest} paneWidth={paneWidth} />
|
|
115
114
|
<Divider width={paneWidth} />
|
|
116
115
|
<LoadingPane content={{ title: `${loadingIndicator} Loading diff`, hint: "Fetching patch from GitHub" }} width={paneWidth} height={Math.max(1, height - 2)} />
|
|
117
116
|
</box>
|
|
118
117
|
)
|
|
119
118
|
}
|
|
120
119
|
|
|
121
|
-
if (diffState.
|
|
120
|
+
if (diffState._tag === "Error") {
|
|
122
121
|
return (
|
|
123
122
|
<box height={height} flexDirection="column">
|
|
124
|
-
<
|
|
123
|
+
<PaddedRow>
|
|
125
124
|
<PlainLine text={`#${pullRequest.number} ${shortRepoName(pullRequest.repository)} diff`} fg={colors.count} bold />
|
|
126
|
-
</
|
|
125
|
+
</PaddedRow>
|
|
127
126
|
<Divider width={paneWidth} />
|
|
128
127
|
<StatusCard content={{ title: "Could not load diff", hint: diffState.error }} width={paneWidth} />
|
|
129
128
|
</box>
|
|
130
129
|
)
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
if (readyFiles.length === 0 ||
|
|
132
|
+
if (readyFiles.length === 0 || stackedFiles.length === 0) {
|
|
134
133
|
return <LoadingPane content={{ title: "No diff", hint: "This PR has no patch contents" }} width={paneWidth} height={height} />
|
|
135
134
|
}
|
|
136
135
|
|
|
137
|
-
const fileCounter = `${safeIndex + 1}/${readyFiles.length}`
|
|
138
|
-
const currentFileStatsText = file ? diffFileStatText(file) : ""
|
|
139
136
|
const selectedSideLabel = selectedCommentAnchor?.side === "RIGHT" ? "right" : selectedCommentAnchor?.side === "LEFT" ? "left" : null
|
|
140
|
-
const commentLabel = commentMode && selectedCommentAnchor
|
|
141
|
-
? ` ${selectedSideLabel} ${selectedCommentAnchor.side === "RIGHT" ? "+" : "-"}${selectedCommentAnchor.line}`
|
|
142
|
-
: commentMode ? " c no lines" : commentCount > 0 ? ` ● ${commentCount}` : ""
|
|
143
|
-
const commentLabelColor = commentMode && selectedCommentAnchor?.side === "LEFT"
|
|
144
|
-
? colors.status.failing
|
|
145
|
-
: commentMode ? colors.status.passing : colors.status.passing
|
|
146
|
-
const fileNameWidth = Math.max(8, headerWidth - fileCounter.length - currentFileStatsText.length - commentLabel.length - 4)
|
|
147
137
|
const commentPeek = commentMode && selectedCommentAnchor && selectedCommentThread.length > 0
|
|
148
138
|
? selectedCommentThread[selectedCommentThread.length - 1]!
|
|
149
139
|
: null
|
|
@@ -152,34 +142,43 @@ export const PullRequestDiffPane = ({
|
|
|
152
142
|
const commentPeekMeta = commentPeek && selectedCommentAnchor
|
|
153
143
|
? `${selectedSideLabel ?? "line"} ${selectedCommentAnchor.side === "RIGHT" ? "+" : "-"}${selectedCommentAnchor.line} ${commentPeek.author} ${commentPeekCount} enter thread a comment`
|
|
154
144
|
: ""
|
|
145
|
+
const stickyScrollTop = Math.max(0, Math.floor(scrollTop))
|
|
146
|
+
const stickyFile = stackedDiffFileAtLine(stackedFiles, stickyScrollTop) ?? stackedFiles[0]
|
|
147
|
+
const stickyArrayIndex = stickyFile ? stackedFiles.indexOf(stickyFile) : -1
|
|
148
|
+
const incomingStickyFile = stickyArrayIndex >= 0 ? stackedFiles[stickyArrayIndex + 1] : undefined
|
|
149
|
+
const incomingHeaderDistance = incomingStickyFile ? incomingStickyFile.headerLine - stickyScrollTop : Number.POSITIVE_INFINITY
|
|
150
|
+
const incomingFile = incomingHeaderDistance === 1 ? incomingStickyFile : undefined
|
|
151
|
+
const stickyCommentLabelFor = (stackedFile: StackedDiffFilePatch | undefined) => {
|
|
152
|
+
if (!commentMode) return ""
|
|
153
|
+
if (!selectedCommentAnchor) return " c no lines"
|
|
154
|
+
if (selectedCommentAnchor.fileIndex !== stackedFile?.index) return ""
|
|
155
|
+
return ` ${selectedCommentAnchor.side === "RIGHT" ? "right" : "left"} ${selectedCommentAnchor.side === "RIGHT" ? "+" : "-"}${selectedCommentAnchor.line}`
|
|
156
|
+
}
|
|
157
|
+
const stickyCommentColor = selectedCommentAnchor?.side === "LEFT" ? colors.status.failing : colors.status.passing
|
|
158
|
+
const handleDiffMouseDown = function (this: ScrollBoxRenderable, event: MouseEvent) {
|
|
159
|
+
if (event.button !== 0) return
|
|
160
|
+
const localY = event.y - this.viewport.y
|
|
161
|
+
if (localY < 0 || localY >= this.viewport.height) return
|
|
162
|
+
const localX = event.x - this.viewport.x
|
|
163
|
+
const side = view === "split"
|
|
164
|
+
? localX < Math.floor(paneWidth / 2) ? "LEFT" : "RIGHT"
|
|
165
|
+
: null
|
|
166
|
+
onSelectCommentLine(Math.max(0, Math.floor(this.scrollTop + localY)), side)
|
|
167
|
+
event.preventDefault()
|
|
168
|
+
event.stopPropagation()
|
|
169
|
+
}
|
|
155
170
|
|
|
156
171
|
return (
|
|
157
172
|
<box height={height} flexDirection="column">
|
|
158
|
-
<
|
|
159
|
-
<TextLine>
|
|
160
|
-
<span fg={colors.count}>#{pullRequest.number}</span>
|
|
161
|
-
<span fg={colors.muted}> {shortRepoName(pullRequest.repository)}</span>
|
|
162
|
-
<span fg={colors.muted}>{" ".repeat(headerGap)}</span>
|
|
163
|
-
<DiffStats pullRequest={pullRequest} />
|
|
164
|
-
</TextLine>
|
|
165
|
-
</box>
|
|
166
|
-
<box height={1} paddingLeft={1} paddingRight={1}>
|
|
167
|
-
<TextLine>
|
|
168
|
-
<span fg={colors.text}>{fitCell(file.name, fileNameWidth)}</span>
|
|
169
|
-
<span fg={colors.muted}> {fileCounter}</span>
|
|
170
|
-
{currentFileStatsText ? <span fg={colors.muted}> </span> : null}
|
|
171
|
-
<FileStats file={file} />
|
|
172
|
-
{commentLabel ? <span fg={commentLabelColor}>{commentLabel}</span> : null}
|
|
173
|
-
</TextLine>
|
|
174
|
-
</box>
|
|
173
|
+
<DiffPaneHeader pullRequest={pullRequest} paneWidth={paneWidth} />
|
|
175
174
|
<Divider width={paneWidth} />
|
|
176
|
-
<scrollbox ref={scrollRef} focused={!commentMode} flexGrow={1} scrollY scrollX={false}>
|
|
175
|
+
<scrollbox ref={scrollRef} focused={!commentMode} flexGrow={1} scrollY scrollX={false} onMouseDown={handleDiffMouseDown}>
|
|
177
176
|
{stackedFiles.map((stackedFile) => (
|
|
178
177
|
<box key={`${pullRequest.url}-${stackedFile.index}-${view}-${wrapMode}`} flexDirection="column" flexShrink={0}>
|
|
179
178
|
{stackedFile.index > 0 ? <Divider width={paneWidth} /> : null}
|
|
180
|
-
<
|
|
179
|
+
<PaddedRow>
|
|
181
180
|
<FileHeader file={stackedFile.file} index={stackedFile.index} count={readyFiles.length} width={paneWidth} />
|
|
182
|
-
</
|
|
181
|
+
</PaddedRow>
|
|
183
182
|
<Divider width={paneWidth} />
|
|
184
183
|
<diff
|
|
185
184
|
ref={(diff: DiffRenderable | null) => setDiffRef(stackedFile.index, diff)}
|
|
@@ -207,15 +206,34 @@ export const PullRequestDiffPane = ({
|
|
|
207
206
|
</box>
|
|
208
207
|
))}
|
|
209
208
|
</scrollbox>
|
|
209
|
+
{stickyFile ? (
|
|
210
|
+
<box position="absolute" top={2} left={0} width={paneWidth} height={2} zIndex={10} flexDirection="column" backgroundColor={colors.background}>
|
|
211
|
+
{incomingFile ? (
|
|
212
|
+
<>
|
|
213
|
+
<Divider width={paneWidth} />
|
|
214
|
+
<PaddedRow backgroundColor={colors.background}>
|
|
215
|
+
<FileHeader file={incomingFile.file} index={incomingFile.index} count={readyFiles.length} width={paneWidth} suffix={stickyCommentLabelFor(incomingFile)} suffixColor={stickyCommentColor} />
|
|
216
|
+
</PaddedRow>
|
|
217
|
+
</>
|
|
218
|
+
) : (
|
|
219
|
+
<>
|
|
220
|
+
<PaddedRow backgroundColor={colors.background}>
|
|
221
|
+
<FileHeader file={stickyFile.file} index={stickyFile.index} count={readyFiles.length} width={paneWidth} suffix={stickyCommentLabelFor(stickyFile)} suffixColor={stickyCommentColor} />
|
|
222
|
+
</PaddedRow>
|
|
223
|
+
<Divider width={paneWidth} />
|
|
224
|
+
</>
|
|
225
|
+
)}
|
|
226
|
+
</box>
|
|
227
|
+
) : null}
|
|
210
228
|
{commentPeek ? (
|
|
211
229
|
<>
|
|
212
230
|
<Divider width={paneWidth} />
|
|
213
|
-
<
|
|
231
|
+
<PaddedRow>
|
|
214
232
|
<PlainLine text={fitCell(commentPeekMeta, Math.max(1, paneWidth - 2))} fg={colors.count} />
|
|
215
|
-
</
|
|
216
|
-
<
|
|
233
|
+
</PaddedRow>
|
|
234
|
+
<PaddedRow>
|
|
217
235
|
<PlainLine text={fitCell(commentPeekBody, Math.max(1, paneWidth - 2))} fg={colors.text} />
|
|
218
|
-
</
|
|
236
|
+
</PaddedRow>
|
|
219
237
|
</>
|
|
220
238
|
) : null}
|
|
221
239
|
</box>
|