@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
package/src/state/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
1
3
|
export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal'
|
|
2
4
|
|
|
3
5
|
export type AssistantId = BuiltinAssistantId | (string & {})
|
|
@@ -144,13 +146,15 @@ export interface GitPaneState {
|
|
|
144
146
|
mode: GitPaneMode
|
|
145
147
|
position: GitPanePosition
|
|
146
148
|
ratio: number
|
|
149
|
+
diffModeRatio: number
|
|
150
|
+
fileListMode: GitFileListMode
|
|
147
151
|
path: GitPanePathConfig
|
|
148
152
|
diffCount: GitPaneDiffCountConfig
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
export type GitFileStatus = 'M' | 'A' | 'D' | 'R' | 'C' | 'U' | '?'
|
|
152
156
|
|
|
153
|
-
export type GitFileSection = 'staged' | 'unstaged' | 'untracked'
|
|
157
|
+
export type GitFileSection = 'staged' | 'unstaged' | 'untracked' | 'historical'
|
|
154
158
|
|
|
155
159
|
export interface GitFileEntry {
|
|
156
160
|
path: string
|
|
@@ -183,12 +187,25 @@ export interface DiffData {
|
|
|
183
187
|
errorMessage?: string
|
|
184
188
|
}
|
|
185
189
|
|
|
190
|
+
export type GitDiffView = 'split' | 'stacked'
|
|
191
|
+
export type GitFileListMode = 'tree' | 'flat'
|
|
192
|
+
|
|
193
|
+
export interface FoldState {
|
|
194
|
+
top: number
|
|
195
|
+
bottom: number
|
|
196
|
+
}
|
|
197
|
+
|
|
186
198
|
export interface GitModeState {
|
|
187
|
-
|
|
199
|
+
selectedEntryKey: string | null
|
|
200
|
+
collapsedFolders: Record<string, true>
|
|
188
201
|
diffs: Record<string, DiffData>
|
|
189
202
|
loading: Record<string, boolean>
|
|
190
203
|
pendingDeletePath: string | null
|
|
191
204
|
actionMessage: string | null
|
|
205
|
+
diffView: GitDiffView
|
|
206
|
+
folds: Record<string, Record<string, FoldState>>
|
|
207
|
+
/** Working-tree-vs-HEAD~N offset. 0 = working tree vs HEAD (default). */
|
|
208
|
+
headOffset: number
|
|
192
209
|
}
|
|
193
210
|
|
|
194
211
|
interface ModalBase {
|
|
@@ -226,11 +243,13 @@ export interface ModalSnippetPicker extends ModalBase {
|
|
|
226
243
|
|
|
227
244
|
export interface ModalThemePicker extends ModalBase {
|
|
228
245
|
type: 'theme-picker'
|
|
246
|
+
entryCount: number
|
|
229
247
|
}
|
|
230
248
|
|
|
231
249
|
export interface ModalHelp extends ModalBase {
|
|
232
250
|
type: 'help'
|
|
233
251
|
entryCount: number
|
|
252
|
+
scope: ModeId | null
|
|
234
253
|
}
|
|
235
254
|
|
|
236
255
|
export interface ModalSplitPicker extends ModalBase {
|
|
@@ -323,7 +342,7 @@ export interface AppState {
|
|
|
323
342
|
export type ModalAction =
|
|
324
343
|
| { type: 'move-modal-cursor'; delta?: number; to?: 'home' | 'end' }
|
|
325
344
|
| { type: 'open-new-tab-modal' }
|
|
326
|
-
| { type: 'open-help-modal' }
|
|
345
|
+
| { type: 'open-help-modal'; scope?: ModeId }
|
|
327
346
|
| { type: 'open-split-picker'; direction: import('./layout-tree').SplitDirection }
|
|
328
347
|
| { type: 'open-session-picker' }
|
|
329
348
|
| { type: 'open-session-name-modal'; sessionTargetId?: string; initialName?: string }
|
|
@@ -343,7 +362,9 @@ export type ModalAction =
|
|
|
343
362
|
| { type: 'open-snippet-editor'; snippetId?: string }
|
|
344
363
|
| { type: 'begin-snippet-filter' }
|
|
345
364
|
| { type: 'begin-help-filter' }
|
|
365
|
+
| { type: 'begin-theme-filter' }
|
|
346
366
|
| { type: 'set-help-entry-count'; count: number }
|
|
367
|
+
| { type: 'set-theme-entry-count'; count: number }
|
|
347
368
|
| { type: 'open-theme-picker' }
|
|
348
369
|
| { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
|
|
349
370
|
|
|
@@ -421,6 +442,7 @@ export type UIAction =
|
|
|
421
442
|
| { type: 'set-terminal-size'; cols: number; rows: number }
|
|
422
443
|
| { type: 'toggle-git-pane' }
|
|
423
444
|
| { type: 'resize-git-pane'; delta: number }
|
|
445
|
+
| { type: 'resize-git-diff-pane'; delta: number }
|
|
424
446
|
| { type: 'set-git-pane-mode'; mode: GitPaneMode }
|
|
425
447
|
| { type: 'set-git-pane-position'; position: GitPanePosition }
|
|
426
448
|
| { type: 'set-pending-chords'; chords: string[] | null }
|
|
@@ -443,12 +465,30 @@ export type GitPanelAction =
|
|
|
443
465
|
export type GitModeAction =
|
|
444
466
|
| { type: 'enter-git-mode' }
|
|
445
467
|
| { type: 'exit-git-mode' }
|
|
446
|
-
| { type: 'git-mode-
|
|
447
|
-
| { type: 'git-mode-
|
|
448
|
-
| { type: 'git-mode-
|
|
468
|
+
| { type: 'git-mode-move-selection'; delta: -1 | 1 }
|
|
469
|
+
| { type: 'git-mode-move-file-selection'; delta: -1 | 1 }
|
|
470
|
+
| { type: 'git-mode-select-entry-by-key'; key: string }
|
|
471
|
+
| { type: 'git-mode-toggle-folder'; key: string }
|
|
472
|
+
| { type: 'git-mode-toggle-selected-folder' }
|
|
473
|
+
| { type: 'git-mode-collapse-selection' }
|
|
474
|
+
| { type: 'git-mode-expand-selection' }
|
|
475
|
+
| { type: 'git-mode-toggle-file-list-mode' }
|
|
476
|
+
| { type: 'git-mode-set-diff'; key: string; diff: DiffData }
|
|
477
|
+
| { type: 'git-mode-set-loading'; key: string; loading: boolean }
|
|
449
478
|
| { type: 'git-mode-set-pending-delete'; path: string | null }
|
|
450
479
|
| { type: 'git-mode-clear-diff-cache'; path: string }
|
|
451
480
|
| { type: 'git-mode-set-message'; message: string | null }
|
|
481
|
+
| { type: 'git-mode-toggle-diff-view' }
|
|
482
|
+
| { type: 'git-mode-shift-head-offset'; delta: number }
|
|
483
|
+
| { type: 'git-mode-set-head-offset'; offset: number }
|
|
484
|
+
| {
|
|
485
|
+
type: 'git-mode-fold-adjust'
|
|
486
|
+
key: string
|
|
487
|
+
foldId: string
|
|
488
|
+
side: 'top' | 'bottom'
|
|
489
|
+
delta: number
|
|
490
|
+
}
|
|
491
|
+
| { type: 'git-mode-fold-set'; key: string; foldId: string; top: number; bottom: number }
|
|
452
492
|
| {
|
|
453
493
|
type: 'git-mode-optimistic-move'
|
|
454
494
|
path: string
|
|
@@ -25,6 +25,8 @@ export function saveCurrentWorkspace(state: AppState): void {
|
|
|
25
25
|
...loadConfig(),
|
|
26
26
|
customCommands: state.customCommands,
|
|
27
27
|
gitPane: {
|
|
28
|
+
diffModeRatio: state.gitPane.diffModeRatio,
|
|
29
|
+
fileListMode: state.gitPane.fileListMode,
|
|
28
30
|
mode: state.gitPane.mode,
|
|
29
31
|
position: state.gitPane.position,
|
|
30
32
|
ratio: state.gitPane.ratio,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DirectoryResult } from '../../state/types'
|
|
2
2
|
|
|
3
3
|
import { abbreviatePath } from '../path-format'
|
|
4
|
-
import {
|
|
4
|
+
import { getCurrentTheme, useTheme } from '../theme'
|
|
5
5
|
import { uiTokens } from '../ui-tokens'
|
|
6
6
|
import { InputField } from './input-field'
|
|
7
7
|
import { ListItem } from './list-item'
|
|
@@ -21,14 +21,14 @@ function getDirectoryResultIcon(result: DirectoryResult): string {
|
|
|
21
21
|
|
|
22
22
|
function getDirectoryResultColor(result: DirectoryResult): string {
|
|
23
23
|
if (result.type === 'worktree') {
|
|
24
|
-
return
|
|
24
|
+
return getCurrentTheme().colors['editorWarning.foreground']
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
if (result.type === 'workspace') {
|
|
28
|
-
return
|
|
28
|
+
return getCurrentTheme().colors['terminal.ansiMagenta']
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
return
|
|
31
|
+
return getCurrentTheme().colors['textLink.foreground']
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
interface CreateSessionModalProps {
|
|
@@ -48,6 +48,7 @@ export function CreateSessionModal({
|
|
|
48
48
|
selectedIndex,
|
|
49
49
|
sessionName,
|
|
50
50
|
}: CreateSessionModalProps) {
|
|
51
|
+
const theme = useTheme()
|
|
51
52
|
const dirActive = activeField === 'directory'
|
|
52
53
|
const nameActive = activeField === 'name'
|
|
53
54
|
|
|
@@ -58,7 +59,11 @@ export function CreateSessionModal({
|
|
|
58
59
|
width={uiTokens.modalWidth.xl}
|
|
59
60
|
>
|
|
60
61
|
<box flexDirection="column">
|
|
61
|
-
<text
|
|
62
|
+
<text
|
|
63
|
+
fg={dirActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
64
|
+
>
|
|
65
|
+
Search projects
|
|
66
|
+
</text>
|
|
62
67
|
<InputField
|
|
63
68
|
active={dirActive}
|
|
64
69
|
value={
|
|
@@ -68,7 +73,7 @@ export function CreateSessionModal({
|
|
|
68
73
|
</box>
|
|
69
74
|
|
|
70
75
|
{dirActive && results.length === 0 && directoryQuery.length > 0 ? (
|
|
71
|
-
<text fg={theme.
|
|
76
|
+
<text fg={theme.colors['descriptionForeground']}>No matches</text>
|
|
72
77
|
) : null}
|
|
73
78
|
|
|
74
79
|
{dirActive
|
|
@@ -82,7 +87,13 @@ export function CreateSessionModal({
|
|
|
82
87
|
<text fg={getDirectoryResultColor(result)}>{getDirectoryResultIcon(result)}</text>
|
|
83
88
|
}
|
|
84
89
|
title={
|
|
85
|
-
<text
|
|
90
|
+
<text
|
|
91
|
+
fg={
|
|
92
|
+
active
|
|
93
|
+
? theme.colors['editor.foreground']
|
|
94
|
+
: theme.colors['descriptionForeground']
|
|
95
|
+
}
|
|
96
|
+
>
|
|
86
97
|
{abbreviatePath(result.path)}
|
|
87
98
|
</text>
|
|
88
99
|
}
|
|
@@ -92,7 +103,13 @@ export function CreateSessionModal({
|
|
|
92
103
|
: null}
|
|
93
104
|
|
|
94
105
|
<box flexDirection="column">
|
|
95
|
-
<text
|
|
106
|
+
<text
|
|
107
|
+
fg={
|
|
108
|
+
nameActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
109
|
+
}
|
|
110
|
+
>
|
|
111
|
+
Session name
|
|
112
|
+
</text>
|
|
96
113
|
<InputField active={nameActive} value={sessionName} />
|
|
97
114
|
</box>
|
|
98
115
|
</ModalShell>
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import type { FileDiffMetadata, Hunk, HunkContent } from '../../../diff-parser'
|
|
2
|
+
import type { FoldState } from '../../../state/types'
|
|
3
|
+
|
|
4
|
+
export const KEEP_CONTEXT = 3
|
|
5
|
+
export const MIN_FOLD_SIZE = 3
|
|
6
|
+
export const FOLD_STEP = 5
|
|
7
|
+
|
|
8
|
+
export interface FoldInfo {
|
|
9
|
+
foldId: string
|
|
10
|
+
total: number
|
|
11
|
+
hidden: number
|
|
12
|
+
topExpanded: number
|
|
13
|
+
bottomExpanded: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type SplitCell =
|
|
17
|
+
| { content: string; lineIdx: number; lineNumber: number; type: 'context' }
|
|
18
|
+
| { content: string; lineIdx: number; lineNumber: number; type: 'addition' | 'deletion' }
|
|
19
|
+
| { fold: FoldInfo; type: 'fold' }
|
|
20
|
+
| { type: 'filler' }
|
|
21
|
+
|
|
22
|
+
export interface SplitRow {
|
|
23
|
+
type: 'row'
|
|
24
|
+
left: SplitCell
|
|
25
|
+
right: SplitCell
|
|
26
|
+
height: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type UnifiedRow =
|
|
30
|
+
| {
|
|
31
|
+
addLineNumber: number
|
|
32
|
+
content: string
|
|
33
|
+
lineIdx: number
|
|
34
|
+
delLineNumber: number
|
|
35
|
+
type: 'context'
|
|
36
|
+
height: number
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
content: string
|
|
40
|
+
lineIdx: number
|
|
41
|
+
lineNumber: number
|
|
42
|
+
type: 'addition' | 'deletion'
|
|
43
|
+
height: number
|
|
44
|
+
}
|
|
45
|
+
| { fold: FoldInfo; type: 'fold' }
|
|
46
|
+
|
|
47
|
+
export interface HunkHeader {
|
|
48
|
+
context?: string
|
|
49
|
+
spec: string
|
|
50
|
+
type: 'hunk-header'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type SplitRowOrHeader = HunkHeader | SplitRow
|
|
54
|
+
export type UnifiedRowOrHeader = HunkHeader | UnifiedRow
|
|
55
|
+
|
|
56
|
+
export type FoldMap = Record<string, FoldState>
|
|
57
|
+
|
|
58
|
+
function wrapCount(content: string, width: number): number {
|
|
59
|
+
if (width <= 0) return 1
|
|
60
|
+
return Math.max(1, Math.ceil(content.length / width))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cellWraps(content: string, width: number): number {
|
|
64
|
+
return wrapCount(content, width)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ContextSlice {
|
|
68
|
+
// which file-line indices (relative to content start) to show, and which to fold
|
|
69
|
+
leadingVisible: number
|
|
70
|
+
fold: { length: number; offset: number } | null
|
|
71
|
+
trailingVisible: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sliceContext(
|
|
75
|
+
totalLines: number,
|
|
76
|
+
hasChangeBefore: boolean,
|
|
77
|
+
hasChangeAfter: boolean,
|
|
78
|
+
fold: FoldState
|
|
79
|
+
): ContextSlice {
|
|
80
|
+
const preKeep = hasChangeBefore ? Math.min(KEEP_CONTEXT, totalLines) : 0
|
|
81
|
+
const postKeep = hasChangeAfter ? Math.min(KEEP_CONTEXT, totalLines - preKeep) : 0
|
|
82
|
+
const middle = totalLines - preKeep - postKeep
|
|
83
|
+
if (middle < MIN_FOLD_SIZE) {
|
|
84
|
+
return { fold: null, leadingVisible: totalLines, trailingVisible: 0 }
|
|
85
|
+
}
|
|
86
|
+
const top = Math.min(fold.top, middle)
|
|
87
|
+
const bottom = Math.min(fold.bottom, middle - top)
|
|
88
|
+
const hidden = middle - top - bottom
|
|
89
|
+
if (hidden <= 0) {
|
|
90
|
+
return { fold: null, leadingVisible: totalLines, trailingVisible: 0 }
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
fold: { length: hidden, offset: preKeep + top },
|
|
94
|
+
leadingVisible: preKeep + top,
|
|
95
|
+
trailingVisible: postKeep + bottom,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function makeFoldInfo(foldId: string, middle: number, fold: FoldState, hidden: number): FoldInfo {
|
|
100
|
+
const topExpanded = Math.min(fold.top, middle)
|
|
101
|
+
const bottomExpanded = Math.min(fold.bottom, middle - topExpanded)
|
|
102
|
+
return { bottomExpanded, foldId, hidden, topExpanded, total: middle }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function contextNeighbors(
|
|
106
|
+
contents: HunkContent[],
|
|
107
|
+
index: number
|
|
108
|
+
): { hasChangeBefore: boolean; hasChangeAfter: boolean } {
|
|
109
|
+
let hasChangeBefore = false
|
|
110
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
111
|
+
const c = contents[i]
|
|
112
|
+
if (c && c.type !== 'context') {
|
|
113
|
+
hasChangeBefore = true
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
let hasChangeAfter = false
|
|
118
|
+
for (let i = index + 1; i < contents.length; i++) {
|
|
119
|
+
const c = contents[i]
|
|
120
|
+
if (c && c.type !== 'context') {
|
|
121
|
+
hasChangeAfter = true
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { hasChangeAfter, hasChangeBefore }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function buildSplitRows(
|
|
129
|
+
file: FileDiffMetadata,
|
|
130
|
+
folds: FoldMap = {},
|
|
131
|
+
contentWidth = 0
|
|
132
|
+
): SplitRowOrHeader[] {
|
|
133
|
+
const rows: SplitRowOrHeader[] = []
|
|
134
|
+
const pairHeight = (l: SplitCell, r: SplitCell): number => {
|
|
135
|
+
const lh =
|
|
136
|
+
l.type === 'context' || l.type === 'addition' || l.type === 'deletion'
|
|
137
|
+
? cellWraps(l.content, contentWidth)
|
|
138
|
+
: 1
|
|
139
|
+
const rh =
|
|
140
|
+
r.type === 'context' || r.type === 'addition' || r.type === 'deletion'
|
|
141
|
+
? cellWraps(r.content, contentWidth)
|
|
142
|
+
: 1
|
|
143
|
+
return Math.max(lh, rh)
|
|
144
|
+
}
|
|
145
|
+
for (const [hIdx, hunk] of file.hunks.entries()) {
|
|
146
|
+
rows.push(makeHeader(hunk))
|
|
147
|
+
let delLine = hunk.deletionStart
|
|
148
|
+
let addLine = hunk.additionStart
|
|
149
|
+
for (const [cIdx, content] of hunk.hunkContent.entries()) {
|
|
150
|
+
if (content.type === 'context') {
|
|
151
|
+
const foldId = `${hIdx}:${cIdx}`
|
|
152
|
+
const { hasChangeAfter, hasChangeBefore } = contextNeighbors(hunk.hunkContent, cIdx)
|
|
153
|
+
const foldState = folds[foldId] ?? { bottom: 0, top: 0 }
|
|
154
|
+
const slice = sliceContext(content.lines, hasChangeBefore, hasChangeAfter, foldState)
|
|
155
|
+
|
|
156
|
+
const pushContext = (start: number, count: number): void => {
|
|
157
|
+
for (let i = 0; i < count; i++) {
|
|
158
|
+
const offset = start + i
|
|
159
|
+
const addIdx = content.additionLineIndex + offset
|
|
160
|
+
const delIdx = content.deletionLineIndex + offset
|
|
161
|
+
const text = stripNewline(file.additionLines[addIdx] ?? '')
|
|
162
|
+
const left: SplitCell = {
|
|
163
|
+
content: text,
|
|
164
|
+
lineIdx: delIdx,
|
|
165
|
+
lineNumber: delLine++,
|
|
166
|
+
type: 'context',
|
|
167
|
+
}
|
|
168
|
+
const right: SplitCell = {
|
|
169
|
+
content: text,
|
|
170
|
+
lineIdx: addIdx,
|
|
171
|
+
lineNumber: addLine++,
|
|
172
|
+
type: 'context',
|
|
173
|
+
}
|
|
174
|
+
rows.push({ height: pairHeight(left, right), left, right, type: 'row' })
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
pushContext(0, slice.leadingVisible)
|
|
179
|
+
if (slice.fold) {
|
|
180
|
+
const preKeep = hasChangeBefore ? Math.min(KEEP_CONTEXT, content.lines) : 0
|
|
181
|
+
const postKeep = hasChangeAfter ? Math.min(KEEP_CONTEXT, content.lines - preKeep) : 0
|
|
182
|
+
const middle = content.lines - preKeep - postKeep
|
|
183
|
+
const info = makeFoldInfo(foldId, middle, foldState, slice.fold.length)
|
|
184
|
+
rows.push({
|
|
185
|
+
height: 1,
|
|
186
|
+
left: { fold: info, type: 'fold' },
|
|
187
|
+
right: { fold: info, type: 'fold' },
|
|
188
|
+
type: 'row',
|
|
189
|
+
})
|
|
190
|
+
delLine += slice.fold.length
|
|
191
|
+
addLine += slice.fold.length
|
|
192
|
+
pushContext(slice.fold.offset + slice.fold.length, slice.trailingVisible)
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
const max = Math.max(content.additions, content.deletions)
|
|
196
|
+
for (let i = 0; i < max; i++) {
|
|
197
|
+
const delIdx = content.deletionLineIndex + i
|
|
198
|
+
const addIdx = content.additionLineIndex + i
|
|
199
|
+
const left: SplitCell =
|
|
200
|
+
i < content.deletions
|
|
201
|
+
? {
|
|
202
|
+
content: stripNewline(file.deletionLines[delIdx] ?? ''),
|
|
203
|
+
lineIdx: delIdx,
|
|
204
|
+
lineNumber: delLine++,
|
|
205
|
+
type: 'deletion',
|
|
206
|
+
}
|
|
207
|
+
: { type: 'filler' }
|
|
208
|
+
const right: SplitCell =
|
|
209
|
+
i < content.additions
|
|
210
|
+
? {
|
|
211
|
+
content: stripNewline(file.additionLines[addIdx] ?? ''),
|
|
212
|
+
lineIdx: addIdx,
|
|
213
|
+
lineNumber: addLine++,
|
|
214
|
+
type: 'addition',
|
|
215
|
+
}
|
|
216
|
+
: { type: 'filler' }
|
|
217
|
+
rows.push({ height: pairHeight(left, right), left, right, type: 'row' })
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return rows
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function buildUnifiedRows(
|
|
226
|
+
file: FileDiffMetadata,
|
|
227
|
+
folds: FoldMap = {},
|
|
228
|
+
contentWidth = 0
|
|
229
|
+
): UnifiedRowOrHeader[] {
|
|
230
|
+
const rows: UnifiedRowOrHeader[] = []
|
|
231
|
+
for (const [hIdx, hunk] of file.hunks.entries()) {
|
|
232
|
+
rows.push(makeHeader(hunk))
|
|
233
|
+
let delLine = hunk.deletionStart
|
|
234
|
+
let addLine = hunk.additionStart
|
|
235
|
+
for (const [cIdx, content] of hunk.hunkContent.entries()) {
|
|
236
|
+
if (content.type === 'context') {
|
|
237
|
+
const foldId = `${hIdx}:${cIdx}`
|
|
238
|
+
const { hasChangeAfter, hasChangeBefore } = contextNeighbors(hunk.hunkContent, cIdx)
|
|
239
|
+
const foldState = folds[foldId] ?? { bottom: 0, top: 0 }
|
|
240
|
+
const slice = sliceContext(content.lines, hasChangeBefore, hasChangeAfter, foldState)
|
|
241
|
+
|
|
242
|
+
const pushContext = (start: number, count: number): void => {
|
|
243
|
+
for (let i = 0; i < count; i++) {
|
|
244
|
+
const offset = start + i
|
|
245
|
+
const addIdx = content.additionLineIndex + offset
|
|
246
|
+
const text = stripNewline(file.additionLines[addIdx] ?? '')
|
|
247
|
+
rows.push({
|
|
248
|
+
addLineNumber: addLine++,
|
|
249
|
+
content: text,
|
|
250
|
+
delLineNumber: delLine++,
|
|
251
|
+
height: cellWraps(text, contentWidth),
|
|
252
|
+
lineIdx: addIdx,
|
|
253
|
+
type: 'context',
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
pushContext(0, slice.leadingVisible)
|
|
259
|
+
if (slice.fold) {
|
|
260
|
+
const preKeep = hasChangeBefore ? Math.min(KEEP_CONTEXT, content.lines) : 0
|
|
261
|
+
const postKeep = hasChangeAfter ? Math.min(KEEP_CONTEXT, content.lines - preKeep) : 0
|
|
262
|
+
const middle = content.lines - preKeep - postKeep
|
|
263
|
+
const info = makeFoldInfo(foldId, middle, foldState, slice.fold.length)
|
|
264
|
+
rows.push({ fold: info, type: 'fold' })
|
|
265
|
+
delLine += slice.fold.length
|
|
266
|
+
addLine += slice.fold.length
|
|
267
|
+
pushContext(slice.fold.offset + slice.fold.length, slice.trailingVisible)
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
for (let i = 0; i < content.deletions; i++) {
|
|
271
|
+
const delIdx = content.deletionLineIndex + i
|
|
272
|
+
const text = stripNewline(file.deletionLines[delIdx] ?? '')
|
|
273
|
+
rows.push({
|
|
274
|
+
content: text,
|
|
275
|
+
height: cellWraps(text, contentWidth),
|
|
276
|
+
lineIdx: delIdx,
|
|
277
|
+
lineNumber: delLine++,
|
|
278
|
+
type: 'deletion',
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
for (let i = 0; i < content.additions; i++) {
|
|
282
|
+
const addIdx = content.additionLineIndex + i
|
|
283
|
+
const text = stripNewline(file.additionLines[addIdx] ?? '')
|
|
284
|
+
rows.push({
|
|
285
|
+
content: text,
|
|
286
|
+
height: cellWraps(text, contentWidth),
|
|
287
|
+
lineIdx: addIdx,
|
|
288
|
+
lineNumber: addLine++,
|
|
289
|
+
type: 'addition',
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return rows
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function rowHeight(row: SplitRowOrHeader | UnifiedRowOrHeader): number {
|
|
299
|
+
if (row.type === 'hunk-header') return 1
|
|
300
|
+
if (row.type === 'row') return row.height
|
|
301
|
+
if (row.type === 'fold') return 1
|
|
302
|
+
return row.height
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isChangeRow(row: SplitRowOrHeader | UnifiedRowOrHeader): boolean {
|
|
306
|
+
if (row.type === 'row') {
|
|
307
|
+
return (
|
|
308
|
+
row.left.type === 'addition' ||
|
|
309
|
+
row.left.type === 'deletion' ||
|
|
310
|
+
row.right.type === 'addition' ||
|
|
311
|
+
row.right.type === 'deletion'
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
return row.type === 'addition' || row.type === 'deletion'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function firstChangeRowOffset(
|
|
318
|
+
rows: readonly (SplitRowOrHeader | UnifiedRowOrHeader)[]
|
|
319
|
+
): number {
|
|
320
|
+
let offset = 0
|
|
321
|
+
for (const row of rows) {
|
|
322
|
+
if (isChangeRow(row)) return offset
|
|
323
|
+
offset += rowHeight(row)
|
|
324
|
+
}
|
|
325
|
+
return -1
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function makeHeader(hunk: Hunk): HunkHeader {
|
|
329
|
+
const spec = `@@ -${hunk.deletionStart},${hunk.deletionCount} +${hunk.additionStart},${hunk.additionCount} @@`
|
|
330
|
+
return { context: hunk.hunkContext, spec, type: 'hunk-header' }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function stripNewline(s: string): string {
|
|
334
|
+
if (s.endsWith('\r\n')) return s.slice(0, -2)
|
|
335
|
+
if (s.endsWith('\n')) return s.slice(0, -1)
|
|
336
|
+
return s
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function gutterWidth(file: FileDiffMetadata): number {
|
|
340
|
+
let max = 1
|
|
341
|
+
for (const h of file.hunks) {
|
|
342
|
+
max = Math.max(
|
|
343
|
+
max,
|
|
344
|
+
h.deletionStart + h.deletionCount - 1,
|
|
345
|
+
h.additionStart + h.additionCount - 1
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
return Math.max(2, String(max).length)
|
|
349
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const EXT_TO_SHIKI: Record<string, string> = {
|
|
2
|
+
cjs: 'javascript',
|
|
3
|
+
css: 'css',
|
|
4
|
+
go: 'go',
|
|
5
|
+
html: 'html',
|
|
6
|
+
js: 'javascript',
|
|
7
|
+
json: 'json',
|
|
8
|
+
jsx: 'jsx',
|
|
9
|
+
markdown: 'markdown',
|
|
10
|
+
md: 'markdown',
|
|
11
|
+
mdx: 'mdx',
|
|
12
|
+
mjs: 'javascript',
|
|
13
|
+
py: 'python',
|
|
14
|
+
rs: 'rust',
|
|
15
|
+
sh: 'shellscript',
|
|
16
|
+
toml: 'toml',
|
|
17
|
+
ts: 'typescript',
|
|
18
|
+
tsx: 'tsx',
|
|
19
|
+
yaml: 'yaml',
|
|
20
|
+
yml: 'yaml',
|
|
21
|
+
zig: 'zig',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function filetypeFromPath(path: string): string | undefined {
|
|
25
|
+
const dot = path.lastIndexOf('.')
|
|
26
|
+
if (dot < 0) return undefined
|
|
27
|
+
const ext = path.slice(dot + 1).toLowerCase()
|
|
28
|
+
return EXT_TO_SHIKI[ext]
|
|
29
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useTheme } from '../../theme'
|
|
2
|
+
import { FOLD_STEP, type FoldInfo } from './build-rows'
|
|
3
|
+
import { type FoldDispatch } from './pierre-diff'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
fold: FoldInfo
|
|
7
|
+
dispatch: FoldDispatch
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function Button({ label, onPress }: { label: string; onPress: () => void }) {
|
|
11
|
+
const theme = useTheme()
|
|
12
|
+
return (
|
|
13
|
+
<box
|
|
14
|
+
paddingLeft={1}
|
|
15
|
+
paddingRight={1}
|
|
16
|
+
backgroundColor={theme.colors['sideBar.background']}
|
|
17
|
+
onMouseDown={onPress}
|
|
18
|
+
>
|
|
19
|
+
<text fg={theme.colors['textLink.foreground']}>{label}</text>
|
|
20
|
+
</box>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function Spacer() {
|
|
25
|
+
return <text> </text>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function FoldStrip({ dispatch, fold }: Props) {
|
|
29
|
+
const theme = useTheme()
|
|
30
|
+
const { bottomExpanded, foldId, hidden, topExpanded, total } = fold
|
|
31
|
+
const stepUp = Math.min(FOLD_STEP, hidden)
|
|
32
|
+
const stepDown = Math.min(FOLD_STEP, hidden)
|
|
33
|
+
const shrinkUp = Math.min(FOLD_STEP, topExpanded)
|
|
34
|
+
const shrinkDown = Math.min(FOLD_STEP, bottomExpanded)
|
|
35
|
+
|
|
36
|
+
const controls: React.ReactNode[] = []
|
|
37
|
+
if (hidden > 0) {
|
|
38
|
+
controls.push(
|
|
39
|
+
<Button
|
|
40
|
+
key="up"
|
|
41
|
+
label={`↑${stepUp}`}
|
|
42
|
+
onPress={() => dispatch.adjust(foldId, 'top', stepUp)}
|
|
43
|
+
/>,
|
|
44
|
+
<Spacer key="sp1" />,
|
|
45
|
+
<Button
|
|
46
|
+
key="down"
|
|
47
|
+
label={`↓${stepDown}`}
|
|
48
|
+
onPress={() => dispatch.adjust(foldId, 'bottom', stepDown)}
|
|
49
|
+
/>,
|
|
50
|
+
<Spacer key="sp2" />,
|
|
51
|
+
<Button key="all" label="⇅ all" onPress={() => dispatch.set(foldId, total, 0)} />
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
if (shrinkUp > 0) {
|
|
55
|
+
controls.push(
|
|
56
|
+
<Spacer key="sp3" />,
|
|
57
|
+
<Button
|
|
58
|
+
key="shrinkUp"
|
|
59
|
+
label={`−↑${shrinkUp}`}
|
|
60
|
+
onPress={() => dispatch.adjust(foldId, 'top', -shrinkUp)}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
if (shrinkDown > 0) {
|
|
65
|
+
controls.push(
|
|
66
|
+
<Spacer key="sp4" />,
|
|
67
|
+
<Button
|
|
68
|
+
key="shrinkDown"
|
|
69
|
+
label={`−↓${shrinkDown}`}
|
|
70
|
+
onPress={() => dispatch.adjust(foldId, 'bottom', -shrinkDown)}
|
|
71
|
+
/>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<box
|
|
77
|
+
flexDirection="row"
|
|
78
|
+
backgroundColor={theme.colors['sideBarSectionHeader.background']}
|
|
79
|
+
paddingLeft={1}
|
|
80
|
+
paddingRight={1}
|
|
81
|
+
>
|
|
82
|
+
<text fg={theme.colors['descriptionForeground']}>{`⋯ ${hidden} hidden `}</text>
|
|
83
|
+
{controls}
|
|
84
|
+
</box>
|
|
85
|
+
)
|
|
86
|
+
}
|