@brimveyn/aimux 1.5.4 → 1.6.1
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 -2
- package/package.json +5 -3
- package/src/app-runtime/side-effects.ts +41 -6
- package/src/app.tsx +17 -5
- package/src/config.ts +29 -4
- package/src/diff-parser/clean-last-newline.ts +5 -0
- package/src/diff-parser/constants.ts +13 -0
- package/src/diff-parser/index.ts +12 -0
- package/src/diff-parser/parse-line-type.ts +29 -0
- package/src/diff-parser/parse-patch-files.ts +369 -0
- package/src/diff-parser/types.ts +75 -0
- package/src/git/git-diff.ts +24 -12
- package/src/git/git-poller.ts +11 -3
- package/src/git/git-status.ts +98 -17
- package/src/input/modes/bridge.ts +8 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +4 -1
- package/src/pty/terminal-snapshot.ts +5 -5
- package/src/state/git-tree.ts +220 -0
- package/src/state/reducers/git-mode-state.ts +276 -36
- package/src/state/reducers/git-panel-state.ts +35 -4
- package/src/state/reducers/modal-state.ts +43 -10
- package/src/state/store.ts +5 -1
- package/src/state/types.ts +46 -6
- package/src/state/workspace-save.ts +2 -0
- package/src/ui/components/create-session-modal.tsx +25 -8
- package/src/ui/components/diff-renderer/build-rows.ts +349 -0
- package/src/ui/components/diff-renderer/filetype.ts +29 -0
- package/src/ui/components/diff-renderer/fold-strip.tsx +86 -0
- package/src/ui/components/diff-renderer/highlight.ts +48 -0
- package/src/ui/components/diff-renderer/index.ts +1 -0
- package/src/ui/components/diff-renderer/pierre-diff.tsx +171 -0
- package/src/ui/components/diff-renderer/split-view.tsx +211 -0
- package/src/ui/components/diff-renderer/stacked-view.tsx +168 -0
- package/src/ui/components/git-commit-modal.tsx +16 -3
- package/src/ui/components/git-pane-widget.tsx +6 -1
- package/src/ui/components/git-panel.tsx +215 -86
- package/src/ui/components/git-view.tsx +103 -90
- package/src/ui/components/help-modal.tsx +45 -12
- package/src/ui/components/input-field.tsx +4 -3
- package/src/ui/components/list-item.tsx +11 -2
- package/src/ui/components/modal-filter-bar.tsx +3 -2
- package/src/ui/components/modal-keybinds-overlay.tsx +4 -3
- package/src/ui/components/modal-shell.tsx +5 -4
- package/src/ui/components/new-tab-modal.tsx +17 -4
- package/src/ui/components/pending-chord-overlay.tsx +5 -4
- package/src/ui/components/session-bar.tsx +13 -6
- package/src/ui/components/session-picker-modal.tsx +28 -10
- package/src/ui/components/sidebar.tsx +26 -11
- package/src/ui/components/snippet-editor-modal.tsx +18 -3
- package/src/ui/components/snippet-picker-modal.tsx +13 -4
- package/src/ui/components/split-layout.tsx +3 -2
- package/src/ui/components/status-bar.tsx +15 -10
- package/src/ui/components/surface.tsx +6 -6
- package/src/ui/components/tab-item.tsx +28 -17
- package/src/ui/components/terminal-pane.tsx +28 -16
- package/src/ui/components/theme-picker-modal.tsx +113 -17
- package/src/ui/components/update-available-modal.tsx +13 -2
- package/src/ui/filter-themes.ts +15 -0
- package/src/ui/keymap-context.ts +10 -2
- package/src/ui/root.tsx +29 -7
- package/src/ui/shiki.ts +49 -0
- package/src/ui/status-bar-model.ts +32 -4
- package/src/ui/theme-store.ts +36 -0
- package/src/ui/theme.ts +4 -17
- package/src/ui/themes.ts +23 -277
- package/src/ui/syntax.ts +0 -102
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ThemedToken } from 'shiki'
|
|
2
|
+
|
|
3
|
+
import { ensureShikiLang, ensureShikiTheme, getShikiHighlighter } from '../../shiki'
|
|
4
|
+
|
|
5
|
+
export interface HighlightSpan {
|
|
6
|
+
bold?: boolean
|
|
7
|
+
fg?: string
|
|
8
|
+
italic?: boolean
|
|
9
|
+
text: string
|
|
10
|
+
underline?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Shiki FontStyle: 1 = italic, 2 = bold, 4 = underline.
|
|
14
|
+
export function tokenToSpan(token: ThemedToken): HighlightSpan {
|
|
15
|
+
const fs = token.fontStyle ?? 0
|
|
16
|
+
return {
|
|
17
|
+
bold: (fs & 2) !== 0 || undefined,
|
|
18
|
+
fg: token.color,
|
|
19
|
+
italic: (fs & 1) !== 0 || undefined,
|
|
20
|
+
text: token.content,
|
|
21
|
+
underline: (fs & 4) !== 0 || undefined,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function tokenizeSide(
|
|
26
|
+
lines: string[],
|
|
27
|
+
lang: string,
|
|
28
|
+
theme: string
|
|
29
|
+
): Promise<ThemedToken[][]> {
|
|
30
|
+
if (lines.length === 0) return []
|
|
31
|
+
const highlighter = await getShikiHighlighter()
|
|
32
|
+
const [langOk, themeOk] = await Promise.all([
|
|
33
|
+
ensureShikiLang(highlighter, lang),
|
|
34
|
+
ensureShikiTheme(highlighter, theme),
|
|
35
|
+
])
|
|
36
|
+
if (!langOk || !themeOk) return []
|
|
37
|
+
try {
|
|
38
|
+
const result = highlighter.codeToTokens(lines.join(''), {
|
|
39
|
+
// eslint-disable-next-line typescript/no-explicit-any
|
|
40
|
+
lang: lang as any,
|
|
41
|
+
// eslint-disable-next-line typescript/no-explicit-any
|
|
42
|
+
theme: theme as any,
|
|
43
|
+
})
|
|
44
|
+
return result.tokens
|
|
45
|
+
} catch {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type DiffView, PierreDiff, type PierreDiffHandle } from './pierre-diff'
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
|
+
import type { ThemedToken } from 'shiki'
|
|
3
|
+
|
|
4
|
+
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
5
|
+
|
|
6
|
+
import type { FoldState } from '../../../state/types'
|
|
7
|
+
import type { ThemeId } from '../../themes'
|
|
8
|
+
|
|
9
|
+
import { parsePatchFiles } from '../../../diff-parser'
|
|
10
|
+
import { useAppStore } from '../../../state/app-store'
|
|
11
|
+
import { dispatchGlobal } from '../../../state/dispatch-ref'
|
|
12
|
+
import { useTheme } from '../../theme'
|
|
13
|
+
import { buildSplitRows, buildUnifiedRows, firstChangeRowOffset, gutterWidth } from './build-rows'
|
|
14
|
+
import { filetypeFromPath } from './filetype'
|
|
15
|
+
import { tokenizeSide } from './highlight'
|
|
16
|
+
import { SplitView, type SplitViewHandle } from './split-view'
|
|
17
|
+
import { StackedView, type StackedViewHandle } from './stacked-view'
|
|
18
|
+
|
|
19
|
+
export type DiffView = 'split' | 'stacked'
|
|
20
|
+
|
|
21
|
+
export interface PierreDiffHandle {
|
|
22
|
+
leftScroll: ScrollBoxRenderable | null
|
|
23
|
+
rightScroll: ScrollBoxRenderable | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface DiffHighlights {
|
|
27
|
+
add: ThemedToken[][]
|
|
28
|
+
del: ThemedToken[][]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FoldDispatch {
|
|
32
|
+
adjust: (foldId: string, side: 'top' | 'bottom', delta: number) => void
|
|
33
|
+
set: (foldId: string, top: number, bottom: number) => void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Props {
|
|
37
|
+
cacheKey: string
|
|
38
|
+
diff: string
|
|
39
|
+
path: string
|
|
40
|
+
themeId: ThemeId
|
|
41
|
+
view: DiffView
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const EMPTY_HIGHLIGHTS: DiffHighlights = { add: [], del: [] }
|
|
45
|
+
const EMPTY_FOLDS: Record<string, FoldState> = {}
|
|
46
|
+
|
|
47
|
+
export const PierreDiff = forwardRef<PierreDiffHandle, Props>(function PierreDiff(
|
|
48
|
+
{ cacheKey, diff, path, themeId, view },
|
|
49
|
+
ref
|
|
50
|
+
) {
|
|
51
|
+
const theme = useTheme()
|
|
52
|
+
const file = useMemo(() => {
|
|
53
|
+
const patches = parsePatchFiles(diff)
|
|
54
|
+
return patches[0]?.files[0]
|
|
55
|
+
}, [diff])
|
|
56
|
+
|
|
57
|
+
const filetype = useMemo(() => filetypeFromPath(path), [path])
|
|
58
|
+
|
|
59
|
+
const terminalCols = useAppStore((s) => s.layout.terminalCols)
|
|
60
|
+
const sidebarWidth = useAppStore((s) => s.sidebar.width)
|
|
61
|
+
const contentWidth = useMemo(() => {
|
|
62
|
+
if (!file) return 0
|
|
63
|
+
const gw = gutterWidth(file)
|
|
64
|
+
const usable = Math.max(0, terminalCols - sidebarWidth - 1)
|
|
65
|
+
const paneCols = view === 'split' ? Math.max(0, Math.floor(usable / 2) - 1) : usable
|
|
66
|
+
const prefix = view === 'split' ? gw + 4 : gw * 2 + 5
|
|
67
|
+
return Math.max(1, paneCols - prefix)
|
|
68
|
+
}, [file, view, terminalCols, sidebarWidth])
|
|
69
|
+
|
|
70
|
+
const folds = useAppStore((s) => s.gitMode.folds[cacheKey]) ?? EMPTY_FOLDS
|
|
71
|
+
const foldDispatch = useMemo<FoldDispatch>(
|
|
72
|
+
() => ({
|
|
73
|
+
adjust: (foldId, side, delta) =>
|
|
74
|
+
dispatchGlobal({ delta, foldId, key: cacheKey, side, type: 'git-mode-fold-adjust' }),
|
|
75
|
+
set: (foldId, top, bottom) =>
|
|
76
|
+
dispatchGlobal({ bottom, foldId, key: cacheKey, top, type: 'git-mode-fold-set' }),
|
|
77
|
+
}),
|
|
78
|
+
[cacheKey]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const [highlights, setHighlights] = useState<DiffHighlights>(EMPTY_HIGHLIGHTS)
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
setHighlights(EMPTY_HIGHLIGHTS)
|
|
85
|
+
if (!file || !filetype) return
|
|
86
|
+
let cancelled = false
|
|
87
|
+
void Promise.all([
|
|
88
|
+
tokenizeSide(file.additionLines, filetype, themeId),
|
|
89
|
+
tokenizeSide(file.deletionLines, filetype, themeId),
|
|
90
|
+
]).then(([add, del]) => {
|
|
91
|
+
if (cancelled) return
|
|
92
|
+
setHighlights({ add, del })
|
|
93
|
+
})
|
|
94
|
+
return () => {
|
|
95
|
+
cancelled = true
|
|
96
|
+
}
|
|
97
|
+
}, [file, filetype, themeId])
|
|
98
|
+
|
|
99
|
+
const splitRef = useRef<SplitViewHandle | null>(null)
|
|
100
|
+
const stackedRef = useRef<StackedViewHandle | null>(null)
|
|
101
|
+
|
|
102
|
+
useImperativeHandle(
|
|
103
|
+
ref,
|
|
104
|
+
() => ({
|
|
105
|
+
get leftScroll() {
|
|
106
|
+
if (view === 'split') return splitRef.current?.leftScroll ?? null
|
|
107
|
+
return stackedRef.current?.scroll ?? null
|
|
108
|
+
},
|
|
109
|
+
get rightScroll() {
|
|
110
|
+
if (view === 'split') return splitRef.current?.rightScroll ?? null
|
|
111
|
+
return stackedRef.current?.scroll ?? null
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
[view]
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!file) return
|
|
119
|
+
const rows =
|
|
120
|
+
view === 'split'
|
|
121
|
+
? buildSplitRows(file, EMPTY_FOLDS, contentWidth)
|
|
122
|
+
: buildUnifiedRows(file, EMPTY_FOLDS, contentWidth)
|
|
123
|
+
const offset = firstChangeRowOffset(rows)
|
|
124
|
+
if (offset < 0) return
|
|
125
|
+
const target = Math.max(0, offset - 2)
|
|
126
|
+
const apply = (): void => {
|
|
127
|
+
if (view === 'split') {
|
|
128
|
+
const left = splitRef.current?.leftScroll
|
|
129
|
+
const right = splitRef.current?.rightScroll
|
|
130
|
+
if (left) left.scrollTop = target
|
|
131
|
+
if (right && right !== left) right.scrollTop = target
|
|
132
|
+
} else {
|
|
133
|
+
const node = stackedRef.current?.scroll
|
|
134
|
+
if (node) node.scrollTop = target
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const raf = requestAnimationFrame(apply)
|
|
138
|
+
return () => cancelAnimationFrame(raf)
|
|
139
|
+
}, [cacheKey, file, view, contentWidth])
|
|
140
|
+
|
|
141
|
+
if (!file) {
|
|
142
|
+
return (
|
|
143
|
+
<box flexGrow={1} padding={1}>
|
|
144
|
+
<text fg={theme.colors['descriptionForeground']}>(could not parse diff)</text>
|
|
145
|
+
</box>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (view === 'stacked') {
|
|
150
|
+
return (
|
|
151
|
+
<StackedView
|
|
152
|
+
ref={stackedRef}
|
|
153
|
+
contentWidth={contentWidth}
|
|
154
|
+
file={file}
|
|
155
|
+
foldDispatch={foldDispatch}
|
|
156
|
+
folds={folds}
|
|
157
|
+
highlights={highlights}
|
|
158
|
+
/>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
return (
|
|
162
|
+
<SplitView
|
|
163
|
+
ref={splitRef}
|
|
164
|
+
contentWidth={contentWidth}
|
|
165
|
+
file={file}
|
|
166
|
+
foldDispatch={foldDispatch}
|
|
167
|
+
folds={folds}
|
|
168
|
+
highlights={highlights}
|
|
169
|
+
/>
|
|
170
|
+
)
|
|
171
|
+
})
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { ThemedToken } from 'shiki'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type MouseEvent as OtuiMouseEvent,
|
|
5
|
+
type ScrollBoxRenderable,
|
|
6
|
+
TextAttributes,
|
|
7
|
+
} from '@opentui/core'
|
|
8
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
|
9
|
+
|
|
10
|
+
import type { FileDiffMetadata } from '../../../diff-parser'
|
|
11
|
+
import type { FoldState } from '../../../state/types'
|
|
12
|
+
import type { DiffHighlights, FoldDispatch } from './pierre-diff'
|
|
13
|
+
|
|
14
|
+
import { getScrollViewportDelta } from '../../../app-runtime/terminal-mouse-adapter'
|
|
15
|
+
import { scrollGitDiff } from '../../git-view-controls'
|
|
16
|
+
import { useTheme } from '../../theme'
|
|
17
|
+
import { buildSplitRows, gutterWidth, type SplitCell, type SplitRowOrHeader } from './build-rows'
|
|
18
|
+
import { FoldStrip } from './fold-strip'
|
|
19
|
+
import { tokenToSpan } from './highlight'
|
|
20
|
+
|
|
21
|
+
export interface SplitViewHandle {
|
|
22
|
+
leftScroll: ScrollBoxRenderable | null
|
|
23
|
+
rightScroll: ScrollBoxRenderable | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
file: FileDiffMetadata
|
|
28
|
+
highlights: DiffHighlights
|
|
29
|
+
folds: Record<string, FoldState>
|
|
30
|
+
foldDispatch: FoldDispatch
|
|
31
|
+
contentWidth: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleScroll(e: OtuiMouseEvent): void {
|
|
35
|
+
const delta = getScrollViewportDelta(e)
|
|
36
|
+
if (delta === null) return
|
|
37
|
+
e.preventDefault()
|
|
38
|
+
e.stopPropagation()
|
|
39
|
+
scrollGitDiff(delta)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const SplitView = forwardRef<SplitViewHandle, Props>(function SplitView(
|
|
43
|
+
{ contentWidth, file, foldDispatch, folds, highlights },
|
|
44
|
+
ref
|
|
45
|
+
) {
|
|
46
|
+
const theme = useTheme()
|
|
47
|
+
const leftRef = useRef<ScrollBoxRenderable | null>(null)
|
|
48
|
+
const rightRef = useRef<ScrollBoxRenderable | null>(null)
|
|
49
|
+
|
|
50
|
+
useImperativeHandle(
|
|
51
|
+
ref,
|
|
52
|
+
() => ({
|
|
53
|
+
get leftScroll() {
|
|
54
|
+
return leftRef.current
|
|
55
|
+
},
|
|
56
|
+
get rightScroll() {
|
|
57
|
+
return rightRef.current
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
[]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const rows = buildSplitRows(file, folds, contentWidth)
|
|
64
|
+
const gw = gutterWidth(file)
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<box flexDirection="row" flexGrow={1} overflow="hidden" onMouseScroll={handleScroll}>
|
|
68
|
+
<scrollbox
|
|
69
|
+
ref={leftRef}
|
|
70
|
+
flexGrow={1}
|
|
71
|
+
scrollY
|
|
72
|
+
viewportCulling
|
|
73
|
+
contentOptions={{ flexDirection: 'column', gap: 0 }}
|
|
74
|
+
verticalScrollbarOptions={{ visible: false }}
|
|
75
|
+
onMouseScroll={handleScroll}
|
|
76
|
+
>
|
|
77
|
+
{rows.map((row, i) => (
|
|
78
|
+
<SideRow
|
|
79
|
+
key={i}
|
|
80
|
+
cell={row.type === 'row' ? row.left : null}
|
|
81
|
+
foldDispatch={foldDispatch}
|
|
82
|
+
gw={gw}
|
|
83
|
+
header={row.type === 'hunk-header' ? row : null}
|
|
84
|
+
rowHeight={row.type === 'row' ? row.height : 1}
|
|
85
|
+
tokens={highlights.del}
|
|
86
|
+
/>
|
|
87
|
+
))}
|
|
88
|
+
</scrollbox>
|
|
89
|
+
<box width={1} backgroundColor={theme.colors['editor.background']} />
|
|
90
|
+
<scrollbox
|
|
91
|
+
ref={rightRef}
|
|
92
|
+
flexGrow={1}
|
|
93
|
+
scrollY
|
|
94
|
+
viewportCulling
|
|
95
|
+
contentOptions={{ flexDirection: 'column', gap: 0 }}
|
|
96
|
+
onMouseScroll={handleScroll}
|
|
97
|
+
>
|
|
98
|
+
{rows.map((row, i) => (
|
|
99
|
+
<SideRow
|
|
100
|
+
key={i}
|
|
101
|
+
cell={row.type === 'row' ? row.right : null}
|
|
102
|
+
foldDispatch={foldDispatch}
|
|
103
|
+
gw={gw}
|
|
104
|
+
header={row.type === 'hunk-header' ? row : null}
|
|
105
|
+
rowHeight={row.type === 'row' ? row.height : 1}
|
|
106
|
+
tokens={highlights.add}
|
|
107
|
+
/>
|
|
108
|
+
))}
|
|
109
|
+
</scrollbox>
|
|
110
|
+
</box>
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
function SideRow({
|
|
115
|
+
cell,
|
|
116
|
+
foldDispatch,
|
|
117
|
+
gw,
|
|
118
|
+
header,
|
|
119
|
+
rowHeight,
|
|
120
|
+
tokens,
|
|
121
|
+
}: {
|
|
122
|
+
cell: SplitCell | null
|
|
123
|
+
foldDispatch: FoldDispatch
|
|
124
|
+
gw: number
|
|
125
|
+
header: Extract<SplitRowOrHeader, { type: 'hunk-header' }> | null
|
|
126
|
+
rowHeight: number
|
|
127
|
+
tokens: ThemedToken[][]
|
|
128
|
+
}) {
|
|
129
|
+
if (header) return <HunkHeaderRow row={header} />
|
|
130
|
+
if (!cell) return null
|
|
131
|
+
if (cell.type === 'fold') return <FoldStrip dispatch={foldDispatch} fold={cell.fold} />
|
|
132
|
+
return <HalfRow cell={cell} gw={gw} height={rowHeight} tokens={tokens} />
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function HunkHeaderRow({ row }: { row: Extract<SplitRowOrHeader, { type: 'hunk-header' }> }) {
|
|
136
|
+
const theme = useTheme()
|
|
137
|
+
return (
|
|
138
|
+
<box
|
|
139
|
+
flexDirection="row"
|
|
140
|
+
backgroundColor={theme.colors['sideBarSectionHeader.background']}
|
|
141
|
+
paddingLeft={1}
|
|
142
|
+
paddingRight={1}
|
|
143
|
+
>
|
|
144
|
+
<text fg={theme.colors['descriptionForeground']}>{row.spec}</text>
|
|
145
|
+
{row.context ? (
|
|
146
|
+
<text fg={theme.colors['editor.lineHighlightBackground']}> {row.context}</text>
|
|
147
|
+
) : null}
|
|
148
|
+
</box>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function HalfRow({
|
|
153
|
+
cell,
|
|
154
|
+
gw,
|
|
155
|
+
height,
|
|
156
|
+
tokens,
|
|
157
|
+
}: {
|
|
158
|
+
cell: Exclude<SplitCell, { type: 'fold' }>
|
|
159
|
+
gw: number
|
|
160
|
+
height: number
|
|
161
|
+
tokens: ThemedToken[][]
|
|
162
|
+
}) {
|
|
163
|
+
const theme = useTheme()
|
|
164
|
+
if (cell.type === 'filler') {
|
|
165
|
+
return <box backgroundColor={theme.colors['sideBarSectionHeader.background']} height={height} />
|
|
166
|
+
}
|
|
167
|
+
let bg: string | undefined
|
|
168
|
+
let sign = ' '
|
|
169
|
+
let signColor = theme.colors['descriptionForeground']
|
|
170
|
+
if (cell.type === 'addition') {
|
|
171
|
+
bg = theme.colors['diffEditor.insertedLineBackground']
|
|
172
|
+
sign = '+'
|
|
173
|
+
signColor = theme.colors['gitDecoration.addedResourceForeground']
|
|
174
|
+
} else if (cell.type === 'deletion') {
|
|
175
|
+
bg = theme.colors['diffEditor.removedLineBackground']
|
|
176
|
+
sign = '-'
|
|
177
|
+
signColor = theme.colors['editorError.foreground']
|
|
178
|
+
}
|
|
179
|
+
const num = String(cell.lineNumber).padStart(gw, ' ')
|
|
180
|
+
const lineTokens = tokens[cell.lineIdx]
|
|
181
|
+
return (
|
|
182
|
+
<box flexDirection="row" backgroundColor={bg} height={height}>
|
|
183
|
+
<text fg={theme.colors['descriptionForeground']}>{` ${num} `}</text>
|
|
184
|
+
<text fg={signColor}>{`${sign} `}</text>
|
|
185
|
+
<LineContent content={cell.content} tokens={lineTokens} />
|
|
186
|
+
</box>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function LineContent({ content, tokens }: { content: string; tokens: ThemedToken[] | undefined }) {
|
|
191
|
+
const theme = useTheme()
|
|
192
|
+
if (!tokens || tokens.length === 0) {
|
|
193
|
+
return <text fg={theme.colors['editor.foreground']}>{content}</text>
|
|
194
|
+
}
|
|
195
|
+
return (
|
|
196
|
+
<text>
|
|
197
|
+
{tokens.map((t, i) => {
|
|
198
|
+
const s = tokenToSpan(t)
|
|
199
|
+
let attributes = 0
|
|
200
|
+
if (s.bold) attributes |= TextAttributes.BOLD
|
|
201
|
+
if (s.italic) attributes |= TextAttributes.ITALIC
|
|
202
|
+
if (s.underline) attributes |= TextAttributes.UNDERLINE
|
|
203
|
+
return (
|
|
204
|
+
<span key={i} fg={s.fg ?? theme.colors['editor.foreground']} attributes={attributes}>
|
|
205
|
+
{s.text}
|
|
206
|
+
</span>
|
|
207
|
+
)
|
|
208
|
+
})}
|
|
209
|
+
</text>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { ThemedToken } from 'shiki'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type MouseEvent as OtuiMouseEvent,
|
|
5
|
+
type ScrollBoxRenderable,
|
|
6
|
+
TextAttributes,
|
|
7
|
+
} from '@opentui/core'
|
|
8
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
|
9
|
+
|
|
10
|
+
import type { FileDiffMetadata } from '../../../diff-parser'
|
|
11
|
+
import type { FoldState } from '../../../state/types'
|
|
12
|
+
import type { DiffHighlights, FoldDispatch } from './pierre-diff'
|
|
13
|
+
|
|
14
|
+
import { getScrollViewportDelta } from '../../../app-runtime/terminal-mouse-adapter'
|
|
15
|
+
import { scrollGitDiff } from '../../git-view-controls'
|
|
16
|
+
import { useTheme } from '../../theme'
|
|
17
|
+
import { buildUnifiedRows, gutterWidth, type UnifiedRowOrHeader } from './build-rows'
|
|
18
|
+
import { FoldStrip } from './fold-strip'
|
|
19
|
+
import { tokenToSpan } from './highlight'
|
|
20
|
+
|
|
21
|
+
export interface StackedViewHandle {
|
|
22
|
+
scroll: ScrollBoxRenderable | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
file: FileDiffMetadata
|
|
27
|
+
highlights: DiffHighlights
|
|
28
|
+
folds: Record<string, FoldState>
|
|
29
|
+
foldDispatch: FoldDispatch
|
|
30
|
+
contentWidth: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function handleScroll(e: OtuiMouseEvent): void {
|
|
34
|
+
const delta = getScrollViewportDelta(e)
|
|
35
|
+
if (delta === null) return
|
|
36
|
+
e.preventDefault()
|
|
37
|
+
e.stopPropagation()
|
|
38
|
+
scrollGitDiff(delta)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const StackedView = forwardRef<StackedViewHandle, Props>(function StackedView(
|
|
42
|
+
{ contentWidth, file, foldDispatch, folds, highlights },
|
|
43
|
+
ref
|
|
44
|
+
) {
|
|
45
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
46
|
+
|
|
47
|
+
useImperativeHandle(
|
|
48
|
+
ref,
|
|
49
|
+
() => ({
|
|
50
|
+
get scroll() {
|
|
51
|
+
return scrollRef.current
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
[]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const rows = buildUnifiedRows(file, folds, contentWidth)
|
|
58
|
+
const gw = gutterWidth(file)
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<scrollbox
|
|
62
|
+
ref={scrollRef}
|
|
63
|
+
flexGrow={1}
|
|
64
|
+
scrollY
|
|
65
|
+
viewportCulling
|
|
66
|
+
contentOptions={{ flexDirection: 'column', gap: 0 }}
|
|
67
|
+
onMouseScroll={handleScroll}
|
|
68
|
+
>
|
|
69
|
+
{rows.map((row, i) => (
|
|
70
|
+
<UnifiedRowRender
|
|
71
|
+
key={i}
|
|
72
|
+
foldDispatch={foldDispatch}
|
|
73
|
+
gw={gw}
|
|
74
|
+
highlights={highlights}
|
|
75
|
+
row={row}
|
|
76
|
+
/>
|
|
77
|
+
))}
|
|
78
|
+
</scrollbox>
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
function UnifiedRowRender({
|
|
83
|
+
foldDispatch,
|
|
84
|
+
gw,
|
|
85
|
+
highlights,
|
|
86
|
+
row,
|
|
87
|
+
}: {
|
|
88
|
+
foldDispatch: FoldDispatch
|
|
89
|
+
gw: number
|
|
90
|
+
highlights: DiffHighlights
|
|
91
|
+
row: UnifiedRowOrHeader
|
|
92
|
+
}) {
|
|
93
|
+
const theme = useTheme()
|
|
94
|
+
if (row.type === 'hunk-header') {
|
|
95
|
+
return (
|
|
96
|
+
<box
|
|
97
|
+
flexDirection="row"
|
|
98
|
+
backgroundColor={theme.colors['sideBarSectionHeader.background']}
|
|
99
|
+
paddingLeft={1}
|
|
100
|
+
paddingRight={1}
|
|
101
|
+
>
|
|
102
|
+
<text fg={theme.colors['descriptionForeground']}>{row.spec}</text>
|
|
103
|
+
{row.context ? (
|
|
104
|
+
<text fg={theme.colors['editor.lineHighlightBackground']}> {row.context}</text>
|
|
105
|
+
) : null}
|
|
106
|
+
</box>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
if (row.type === 'fold') {
|
|
110
|
+
return <FoldStrip dispatch={foldDispatch} fold={row.fold} />
|
|
111
|
+
}
|
|
112
|
+
const pad = (n: number | undefined): string =>
|
|
113
|
+
n === undefined ? ' '.repeat(gw) : String(n).padStart(gw, ' ')
|
|
114
|
+
if (row.type === 'context') {
|
|
115
|
+
const tokens = highlights.add[row.lineIdx]
|
|
116
|
+
return (
|
|
117
|
+
<box flexDirection="row" height={row.height}>
|
|
118
|
+
<text
|
|
119
|
+
fg={theme.colors['descriptionForeground']}
|
|
120
|
+
>{` ${pad(row.delLineNumber)} ${pad(row.addLineNumber)} `}</text>
|
|
121
|
+
<text fg={theme.colors['editor.foreground']}> </text>
|
|
122
|
+
<LineContent content={row.content} tokens={tokens} />
|
|
123
|
+
</box>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
const bg =
|
|
127
|
+
row.type === 'addition'
|
|
128
|
+
? theme.colors['diffEditor.insertedLineBackground']
|
|
129
|
+
: theme.colors['diffEditor.removedLineBackground']
|
|
130
|
+
const sign = row.type === 'addition' ? '+' : '-'
|
|
131
|
+
const signColor =
|
|
132
|
+
row.type === 'addition'
|
|
133
|
+
? theme.colors['gitDecoration.addedResourceForeground']
|
|
134
|
+
: theme.colors['editorError.foreground']
|
|
135
|
+
const delNum = row.type === 'deletion' ? row.lineNumber : undefined
|
|
136
|
+
const addNum = row.type === 'addition' ? row.lineNumber : undefined
|
|
137
|
+
const tokens = row.type === 'addition' ? highlights.add[row.lineIdx] : highlights.del[row.lineIdx]
|
|
138
|
+
return (
|
|
139
|
+
<box flexDirection="row" backgroundColor={bg} height={row.height}>
|
|
140
|
+
<text fg={theme.colors['descriptionForeground']}>{` ${pad(delNum)} ${pad(addNum)} `}</text>
|
|
141
|
+
<text fg={signColor}>{`${sign} `}</text>
|
|
142
|
+
<LineContent content={row.content} tokens={tokens} />
|
|
143
|
+
</box>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function LineContent({ content, tokens }: { content: string; tokens: ThemedToken[] | undefined }) {
|
|
148
|
+
const theme = useTheme()
|
|
149
|
+
if (!tokens || tokens.length === 0) {
|
|
150
|
+
return <text fg={theme.colors['editor.foreground']}>{content}</text>
|
|
151
|
+
}
|
|
152
|
+
return (
|
|
153
|
+
<text>
|
|
154
|
+
{tokens.map((t, i) => {
|
|
155
|
+
const s = tokenToSpan(t)
|
|
156
|
+
let attributes = 0
|
|
157
|
+
if (s.bold) attributes |= TextAttributes.BOLD
|
|
158
|
+
if (s.italic) attributes |= TextAttributes.ITALIC
|
|
159
|
+
if (s.underline) attributes |= TextAttributes.UNDERLINE
|
|
160
|
+
return (
|
|
161
|
+
<span key={i} fg={s.fg ?? theme.colors['editor.foreground']} attributes={attributes}>
|
|
162
|
+
{s.text}
|
|
163
|
+
</span>
|
|
164
|
+
)
|
|
165
|
+
})}
|
|
166
|
+
</text>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useTheme } from '../theme'
|
|
2
2
|
import { uiTokens } from '../ui-tokens'
|
|
3
3
|
import { InputField } from './input-field'
|
|
4
4
|
import { ModalShell } from './modal-shell'
|
|
@@ -11,13 +11,20 @@ interface GitCommitModalProps {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function GitCommitModal({ activeField, body, cursorPos, title }: GitCommitModalProps) {
|
|
14
|
+
const theme = useTheme()
|
|
14
15
|
const titleActive = activeField === 'title'
|
|
15
16
|
const bodyActive = activeField === 'body'
|
|
16
17
|
|
|
17
18
|
return (
|
|
18
19
|
<ModalShell title="Commit" keybindsModeId="modal.git-commit" width={uiTokens.modalWidth.xl}>
|
|
19
20
|
<box flexDirection="column">
|
|
20
|
-
<text
|
|
21
|
+
<text
|
|
22
|
+
fg={
|
|
23
|
+
titleActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
24
|
+
}
|
|
25
|
+
>
|
|
26
|
+
Title
|
|
27
|
+
</text>
|
|
21
28
|
<InputField
|
|
22
29
|
active={titleActive}
|
|
23
30
|
cursorPos={titleActive ? cursorPos : undefined}
|
|
@@ -26,7 +33,13 @@ export function GitCommitModal({ activeField, body, cursorPos, title }: GitCommi
|
|
|
26
33
|
</box>
|
|
27
34
|
|
|
28
35
|
<box flexDirection="column">
|
|
29
|
-
<text
|
|
36
|
+
<text
|
|
37
|
+
fg={
|
|
38
|
+
bodyActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
Body (optional)
|
|
42
|
+
</text>
|
|
30
43
|
<InputField
|
|
31
44
|
active={bodyActive}
|
|
32
45
|
cursorPos={bodyActive ? cursorPos : undefined}
|
|
@@ -12,6 +12,8 @@ interface GitPaneWidgetProps {
|
|
|
12
12
|
|
|
13
13
|
export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: GitPaneWidgetProps) {
|
|
14
14
|
const gitPanel = useAppStore((s) => s.gitPanel)
|
|
15
|
+
const gitMode = useAppStore((s) => s.gitMode)
|
|
16
|
+
const gitFileListMode = useAppStore((s) => s.gitPane.fileListMode)
|
|
15
17
|
const pathConfig = useAppStore((s) => s.gitPane.path)
|
|
16
18
|
const diffCountConfig = useAppStore((s) => s.gitPane.diffCount)
|
|
17
19
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
@@ -21,7 +23,7 @@ export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: Git
|
|
|
21
23
|
: undefined
|
|
22
24
|
const projectPath = currentSession?.projectPath
|
|
23
25
|
|
|
24
|
-
useGitPanelPolling({ enabled: pollingEnabled, projectPath })
|
|
26
|
+
useGitPanelPolling({ enabled: pollingEnabled, headOffset: 0, projectPath })
|
|
25
27
|
|
|
26
28
|
const lastGoodRef = useRef<GitPanelState | null>(null)
|
|
27
29
|
const prevProjectPathRef = useRef(projectPath)
|
|
@@ -37,10 +39,13 @@ export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: Git
|
|
|
37
39
|
|
|
38
40
|
return (
|
|
39
41
|
<GitPanel
|
|
42
|
+
collapsedFolders={gitMode.collapsedFolders}
|
|
40
43
|
diffCountConfig={diffCountConfig}
|
|
44
|
+
fileListMode={gitFileListMode}
|
|
41
45
|
gitPanel={display}
|
|
42
46
|
pathConfig={pathConfig}
|
|
43
47
|
projectPath={projectPath}
|
|
48
|
+
selectedEntryKey={gitMode.selectedEntryKey}
|
|
44
49
|
/>
|
|
45
50
|
)
|
|
46
51
|
})
|