opencode-git-plugin 0.2.0 → 0.2.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 +5 -2
- package/dist/tui.js +1828 -0
- package/package.json +5 -4
- package/src/config/constants.ts +0 -16
- package/src/config/index.ts +0 -2
- package/src/config/options.ts +0 -8
- package/src/diff/body.tsx +0 -90
- package/src/diff/header.tsx +0 -77
- package/src/diff/index.ts +0 -2
- package/src/diff/keymap.ts +0 -113
- package/src/diff/open-diff.ts +0 -57
- package/src/diff/panel.tsx +0 -156
- package/src/diff/search.tsx +0 -38
- package/src/git/diff-parse.ts +0 -35
- package/src/git/index.ts +0 -3
- package/src/git/porcelain.ts +0 -10
- package/src/git/runner.ts +0 -45
- package/src/sidebar/file-list.tsx +0 -47
- package/src/sidebar/focus.tsx +0 -37
- package/src/sidebar/header.tsx +0 -55
- package/src/sidebar/index.ts +0 -1
- package/src/sidebar/keymap.ts +0 -143
- package/src/sidebar/list-navigation.tsx +0 -96
- package/src/sidebar/staging.tsx +0 -108
- package/src/sidebar/status.tsx +0 -29
- package/src/sidebar/view.tsx +0 -159
- package/src/sidebar/working-copy.tsx +0 -81
- package/src/state/diff-store.ts +0 -34
- package/src/state/index.ts +0 -1
- package/src/tui.tsx +0 -25
- package/src/types.ts +0 -63
- package/src/ui/format.ts +0 -67
- package/src/ui/index.ts +0 -2
- package/src/ui/syntax.ts +0 -61
- package/tui.ts +0 -1
package/package.json
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "opencode-git-plugin",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"description": "OpenCode v2 TUI sidebar plugin listing git working-copy changes with diff preview",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
|
-
"./tui": "./tui.
|
|
8
|
+
"./tui": "./dist/tui.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"
|
|
12
|
-
"src",
|
|
11
|
+
"dist",
|
|
13
12
|
"docs",
|
|
14
13
|
"README.md",
|
|
15
14
|
"LICENSE"
|
|
16
15
|
],
|
|
17
16
|
"scripts": {
|
|
17
|
+
"build": "bun run scripts/build.ts",
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
19
|
"test": "bun test",
|
|
20
20
|
"check": "bun run typecheck && bun test",
|
|
21
|
+
"prepack": "bun run build",
|
|
21
22
|
"prepublishOnly": "bun run check"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
package/src/config/constants.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_INTERVAL_MS = 4000
|
|
2
|
-
export const MIN_INTERVAL_MS = 1000
|
|
3
|
-
export const MAX_DIFF_LINES = 4000
|
|
4
|
-
export const GIT_TIMEOUT_MS = 120_000
|
|
5
|
-
export const GIT_POLL_MS = 50
|
|
6
|
-
|
|
7
|
-
export const DIFF_STORE_KEY = "git-panel.diff-view"
|
|
8
|
-
export const DIFF_PANEL_NAME = "git-panel.diff"
|
|
9
|
-
export const REFRESH_STORE_KEY = "git-panel.refresh"
|
|
10
|
-
export const SETTINGS_STORE_KEY = "git-panel.settings"
|
|
11
|
-
export const STAGED_STORE_KEY = "git-panel.staged"
|
|
12
|
-
|
|
13
|
-
export const LIST_NAV_MODE = "git-panel-list"
|
|
14
|
-
|
|
15
|
-
export const FILE_ROW_BADGE_WIDTH = 3
|
|
16
|
-
export const EVENT_REFRESH_DEBOUNCE_MS = 2000
|
package/src/config/index.ts
DELETED
package/src/config/options.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { Context } from "@opencode/plugin/tui/context"
|
|
2
|
-
import { DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS } from "./constants"
|
|
3
|
-
|
|
4
|
-
export function intervalMs(ctx: Context): number {
|
|
5
|
-
const raw = ctx.options.intervalMs
|
|
6
|
-
if (typeof raw !== "number" || !Number.isFinite(raw)) return DEFAULT_INTERVAL_MS
|
|
7
|
-
return Math.max(MIN_INTERVAL_MS, Math.floor(raw))
|
|
8
|
-
}
|
package/src/diff/body.tsx
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { For, Show } from "solid-js"
|
|
3
|
-
import type { generateSyntax } from "@opencode/theme/tui"
|
|
4
|
-
import { diffLinePrefix, diffLineStyle, syntaxFiletype } from "../ui"
|
|
5
|
-
import type { DiffRow, DiffViewState, ScrollHandle, Theme } from "../types"
|
|
6
|
-
|
|
7
|
-
function DiffLineRow(props: { theme: Theme; row: DiffRow; index: number }) {
|
|
8
|
-
const theme = props.theme
|
|
9
|
-
const row = props.row
|
|
10
|
-
const style = diffLineStyle(theme, row.kind)
|
|
11
|
-
const isCurrent = row.state === "current"
|
|
12
|
-
const gutter = row.line !== undefined ? String(row.line).padStart(4, " ") : " "
|
|
13
|
-
const fg = isCurrent
|
|
14
|
-
? theme.diff.background.context
|
|
15
|
-
: row.state === "hit"
|
|
16
|
-
? theme.text.feedback.warning.base
|
|
17
|
-
: style.fg
|
|
18
|
-
const bg = isCurrent ? theme.text.feedback.warning.base : style.bg
|
|
19
|
-
return (
|
|
20
|
-
<box flexDirection="row" width="100%" id={`git-panel-diff-line-${props.index}`}>
|
|
21
|
-
<text fg={isCurrent ? theme.text.feedback.warning.base : theme.text.muted} width={5}>
|
|
22
|
-
{isCurrent ? `❯${gutter.slice(1)}` : `${gutter} `}
|
|
23
|
-
</text>
|
|
24
|
-
<text fg={fg} bg={bg} wrapMode="word" flexGrow={1}>
|
|
25
|
-
{diffLinePrefix(row.kind)}
|
|
26
|
-
{row.text}
|
|
27
|
-
</text>
|
|
28
|
-
</box>
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function DiffBody(props: {
|
|
33
|
-
theme: Theme
|
|
34
|
-
state: DiffViewState
|
|
35
|
-
rows: DiffRow[]
|
|
36
|
-
focused: boolean
|
|
37
|
-
syntaxActive: boolean
|
|
38
|
-
syntaxStyle: ReturnType<typeof generateSyntax> | undefined
|
|
39
|
-
onScrollRef: (el: ScrollHandle) => void
|
|
40
|
-
}) {
|
|
41
|
-
const theme = props.theme
|
|
42
|
-
return (
|
|
43
|
-
<>
|
|
44
|
-
<Show when={props.state.loading}>
|
|
45
|
-
<text fg={theme.text.muted}>Loading diff…</text>
|
|
46
|
-
</Show>
|
|
47
|
-
<Show when={!props.state.loading && props.state.message}>
|
|
48
|
-
<text fg={theme.text.muted}>{props.state.message}</text>
|
|
49
|
-
</Show>
|
|
50
|
-
<Show when={!props.state.loading && !props.state.message}>
|
|
51
|
-
<scrollbox
|
|
52
|
-
width="100%"
|
|
53
|
-
flexGrow={1}
|
|
54
|
-
flexShrink={1}
|
|
55
|
-
minHeight={0}
|
|
56
|
-
scrollY
|
|
57
|
-
scrollX={false}
|
|
58
|
-
focused={props.focused}
|
|
59
|
-
ref={(el) => props.onScrollRef(el)}
|
|
60
|
-
contentOptions={{ flexDirection: "column", gap: 0 }}
|
|
61
|
-
wrapperOptions={{ paddingRight: 1 }}
|
|
62
|
-
>
|
|
63
|
-
<Show when={props.syntaxActive}>
|
|
64
|
-
<diff
|
|
65
|
-
diff={props.state.patch}
|
|
66
|
-
view="unified"
|
|
67
|
-
filetype={syntaxFiletype(props.state.file)}
|
|
68
|
-
syntaxStyle={props.syntaxStyle}
|
|
69
|
-
showLineNumbers
|
|
70
|
-
wrapMode="word"
|
|
71
|
-
fg={theme.text.base}
|
|
72
|
-
addedBg={theme.diff.background.added}
|
|
73
|
-
removedBg={theme.diff.background.removed}
|
|
74
|
-
contextBg={theme.diff.background.context}
|
|
75
|
-
addedSignColor={theme.diff.text.added}
|
|
76
|
-
removedSignColor={theme.diff.text.removed}
|
|
77
|
-
lineNumberFg={theme.text.muted}
|
|
78
|
-
/>
|
|
79
|
-
</Show>
|
|
80
|
-
<Show when={!props.syntaxActive}>
|
|
81
|
-
<For each={props.rows}>{(row, i) => <DiffLineRow theme={theme} row={row} index={i()} />}</For>
|
|
82
|
-
</Show>
|
|
83
|
-
<Show when={props.state.truncated}>
|
|
84
|
-
<text fg={theme.text.muted}>… truncated</text>
|
|
85
|
-
</Show>
|
|
86
|
-
</scrollbox>
|
|
87
|
-
</Show>
|
|
88
|
-
</>
|
|
89
|
-
)
|
|
90
|
-
}
|
package/src/diff/header.tsx
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { Show } from "solid-js"
|
|
3
|
-
import { displayWidth, maxTextWidth, truncatePath } from "../ui"
|
|
4
|
-
import type { Theme } from "../types"
|
|
5
|
-
|
|
6
|
-
export function DiffHeader(props: {
|
|
7
|
-
theme: Theme
|
|
8
|
-
width: number
|
|
9
|
-
file: string
|
|
10
|
-
additions: number
|
|
11
|
-
deletions: number
|
|
12
|
-
query: string
|
|
13
|
-
matchCursor: number
|
|
14
|
-
matchCount: number
|
|
15
|
-
syntax: boolean
|
|
16
|
-
onSearch: () => void
|
|
17
|
-
onToggleSyntax: () => void
|
|
18
|
-
onClose: () => void
|
|
19
|
-
}) {
|
|
20
|
-
const theme = props.theme
|
|
21
|
-
return (
|
|
22
|
-
<box flexDirection="column" gap={0} flexShrink={0}>
|
|
23
|
-
<box flexDirection="row" width={Math.max(1, props.width)} flexShrink={0}>
|
|
24
|
-
<text fg={theme.text.base} wrapMode="none" flexShrink={0}>
|
|
25
|
-
<b>
|
|
26
|
-
{truncatePath(
|
|
27
|
-
props.file,
|
|
28
|
-
maxTextWidth(
|
|
29
|
-
props.width,
|
|
30
|
-
displayWidth(`+${props.additions}`) + 1 + displayWidth(`-${props.deletions}`) + 1,
|
|
31
|
-
),
|
|
32
|
-
)}
|
|
33
|
-
</b>
|
|
34
|
-
</text>
|
|
35
|
-
<box flexGrow={1} />
|
|
36
|
-
<box flexDirection="row" gap={1} flexShrink={0}>
|
|
37
|
-
<text fg={theme.diff.text.added} wrapMode="none" flexShrink={0}>
|
|
38
|
-
+{props.additions}
|
|
39
|
-
</text>
|
|
40
|
-
<text fg={theme.diff.text.removed} wrapMode="none" flexShrink={0}>
|
|
41
|
-
-{props.deletions}
|
|
42
|
-
</text>
|
|
43
|
-
</box>
|
|
44
|
-
</box>
|
|
45
|
-
<Show when={props.query}>
|
|
46
|
-
<text fg={theme.text.feedback.warning.base} wrapMode="none">
|
|
47
|
-
{props.matchCount > 0 ? `${props.matchCursor + 1}/${props.matchCount}` : "0/0"} · {props.query}
|
|
48
|
-
</text>
|
|
49
|
-
</Show>
|
|
50
|
-
<box height={1} flexShrink={0} />
|
|
51
|
-
<box flexDirection="row" flexWrap="wrap" width={Math.max(1, props.width)} gap={1} flexShrink={0}>
|
|
52
|
-
<text fg={theme.text.muted} wrapMode="none" flexShrink={0} onMouseUp={() => props.onSearch()}>
|
|
53
|
-
[? : Search]
|
|
54
|
-
</text>
|
|
55
|
-
<Show when={props.query}>
|
|
56
|
-
<text fg={theme.text.muted} wrapMode="none" flexShrink={0}>
|
|
57
|
-
[↑↓ : Jump]
|
|
58
|
-
</text>
|
|
59
|
-
</Show>
|
|
60
|
-
<text
|
|
61
|
-
fg={props.syntax ? theme.text.feedback.warning.base : theme.text.muted}
|
|
62
|
-
wrapMode="none"
|
|
63
|
-
flexShrink={0}
|
|
64
|
-
onMouseUp={() => props.onToggleSyntax()}
|
|
65
|
-
>
|
|
66
|
-
{props.syntax ? "[syntax:on]" : "[syntax:off]"}
|
|
67
|
-
</text>
|
|
68
|
-
<text fg={theme.text.base} wrapMode="none" flexShrink={0} onMouseUp={() => props.onClose()}>
|
|
69
|
-
[ Close · ESC/⌫ ]
|
|
70
|
-
</text>
|
|
71
|
-
</box>
|
|
72
|
-
<text fg={theme.text.muted} wrapMode="none">
|
|
73
|
-
{"─".repeat(Math.max(1, props.width))}
|
|
74
|
-
</text>
|
|
75
|
-
</box>
|
|
76
|
-
)
|
|
77
|
-
}
|
package/src/diff/index.ts
DELETED
package/src/diff/keymap.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type { Context, PanelInput } from "@opencode/plugin/tui/context"
|
|
2
|
-
import { DIFF_PANEL_NAME } from "../config"
|
|
3
|
-
|
|
4
|
-
export function registerDiffKeymap(
|
|
5
|
-
ctx: Context,
|
|
6
|
-
deps: {
|
|
7
|
-
input: () => PanelInput
|
|
8
|
-
searching: () => boolean
|
|
9
|
-
hasMatches: () => boolean
|
|
10
|
-
syntaxActive: () => boolean
|
|
11
|
-
search(): void
|
|
12
|
-
stepMatch(dir: 1 | -1): void
|
|
13
|
-
clearSearch(): void
|
|
14
|
-
close(): void
|
|
15
|
-
toggleSyntax(): void
|
|
16
|
-
scrollBy(delta: number): void
|
|
17
|
-
},
|
|
18
|
-
): void {
|
|
19
|
-
ctx.keymap.layer(() => {
|
|
20
|
-
const searching = deps.searching()
|
|
21
|
-
const hasMatches = deps.hasMatches()
|
|
22
|
-
const inSyntax = deps.syntaxActive()
|
|
23
|
-
return {
|
|
24
|
-
mode: "global",
|
|
25
|
-
priority: 20,
|
|
26
|
-
enabled: () => deps.input().name === DIFF_PANEL_NAME,
|
|
27
|
-
bindings: [
|
|
28
|
-
"git-panel.diff-search",
|
|
29
|
-
"git-panel.diff-next-match",
|
|
30
|
-
"git-panel.diff-prev-match",
|
|
31
|
-
"git-panel.diff-clear-search",
|
|
32
|
-
"git-panel.close-diff",
|
|
33
|
-
"git-panel.close-diff-backspace",
|
|
34
|
-
"git-panel.toggle-syntax",
|
|
35
|
-
"git-panel.syntax-down",
|
|
36
|
-
"git-panel.syntax-up",
|
|
37
|
-
],
|
|
38
|
-
commands: [
|
|
39
|
-
{
|
|
40
|
-
id: "git-panel.diff-search",
|
|
41
|
-
title: "Git Panel: Search in Diff",
|
|
42
|
-
group: "Git Panel",
|
|
43
|
-
bind: "?",
|
|
44
|
-
run: () => deps.search(),
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
id: "git-panel.diff-next-match",
|
|
48
|
-
title: "Git Panel: Next Diff Match",
|
|
49
|
-
group: "Git Panel",
|
|
50
|
-
bind: "down",
|
|
51
|
-
enabled: searching && hasMatches,
|
|
52
|
-
run: () => deps.stepMatch(1),
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
id: "git-panel.diff-prev-match",
|
|
56
|
-
title: "Git Panel: Previous Diff Match",
|
|
57
|
-
group: "Git Panel",
|
|
58
|
-
bind: "up",
|
|
59
|
-
enabled: searching && hasMatches,
|
|
60
|
-
run: () => deps.stepMatch(-1),
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: "git-panel.diff-clear-search",
|
|
64
|
-
title: "Git Panel: Clear Diff Search",
|
|
65
|
-
group: "Git Panel",
|
|
66
|
-
bind: "escape",
|
|
67
|
-
enabled: searching,
|
|
68
|
-
run: () => deps.clearSearch(),
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: "git-panel.close-diff",
|
|
72
|
-
title: "Git Panel: Close Diff",
|
|
73
|
-
group: "Git Panel",
|
|
74
|
-
bind: "escape",
|
|
75
|
-
enabled: !searching,
|
|
76
|
-
run: () => deps.close(),
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: "git-panel.close-diff-backspace",
|
|
80
|
-
title: "Git Panel: Close Diff (Backspace)",
|
|
81
|
-
group: "Git Panel",
|
|
82
|
-
bind: "backspace",
|
|
83
|
-
enabled: !searching,
|
|
84
|
-
run: () => deps.close(),
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: "git-panel.toggle-syntax",
|
|
88
|
-
title: "Git Panel: Toggle Diff Syntax Highlighting",
|
|
89
|
-
group: "Git Panel",
|
|
90
|
-
bind: "s",
|
|
91
|
-
palette: true,
|
|
92
|
-
run: () => deps.toggleSyntax(),
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
id: "git-panel.syntax-down",
|
|
96
|
-
title: "Git Panel: Scroll Syntax Diff Down",
|
|
97
|
-
group: "Git Panel",
|
|
98
|
-
bind: "j",
|
|
99
|
-
enabled: inSyntax,
|
|
100
|
-
run: () => deps.scrollBy(3),
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: "git-panel.syntax-up",
|
|
104
|
-
title: "Git Panel: Scroll Syntax Diff Up",
|
|
105
|
-
group: "Git Panel",
|
|
106
|
-
bind: "k",
|
|
107
|
-
enabled: inSyntax,
|
|
108
|
-
run: () => deps.scrollBy(-3),
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
}
|
package/src/diff/open-diff.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { Context } from "@opencode/plugin/tui/context"
|
|
2
|
-
import { DIFF_PANEL_NAME, MAX_DIFF_LINES } from "../config"
|
|
3
|
-
import { parseDiff } from "../git"
|
|
4
|
-
import { useDiffStore } from "../state"
|
|
5
|
-
import { friendlyError } from "../ui"
|
|
6
|
-
import type { ChangeFile } from "../types"
|
|
7
|
-
|
|
8
|
-
export function createDiffOpener(ctx: Context, location: () => { directory: string }) {
|
|
9
|
-
const [diffView, setDiffState] = useDiffStore(ctx)
|
|
10
|
-
let activeRequestId = 0
|
|
11
|
-
|
|
12
|
-
return async (f: ChangeFile) => {
|
|
13
|
-
const requestId = ++activeRequestId
|
|
14
|
-
const isStale = () => requestId !== activeRequestId || diffView.file !== f.file || !diffView.panelOpen
|
|
15
|
-
|
|
16
|
-
setDiffState((draft) => {
|
|
17
|
-
draft.file = f.file
|
|
18
|
-
draft.additions = f.additions
|
|
19
|
-
draft.deletions = f.deletions
|
|
20
|
-
draft.lines = []
|
|
21
|
-
draft.patch = ""
|
|
22
|
-
draft.message = ""
|
|
23
|
-
draft.truncated = false
|
|
24
|
-
draft.loading = true
|
|
25
|
-
draft.query = ""
|
|
26
|
-
draft.matchCursor = 0
|
|
27
|
-
draft.panelOpen = true
|
|
28
|
-
})
|
|
29
|
-
ctx.ui.panel.open(DIFF_PANEL_NAME, { presentation: "panel" })
|
|
30
|
-
try {
|
|
31
|
-
const res = await ctx.client.vcs.diff({ location: location(), mode: "working", context: 3 })
|
|
32
|
-
if (isStale()) return
|
|
33
|
-
const entry = res.data.find((d: { file: string }) => d.file === f.file)
|
|
34
|
-
const parsed = entry ? parseDiff(entry.patch) : []
|
|
35
|
-
setDiffState((draft) => {
|
|
36
|
-
draft.loading = false
|
|
37
|
-
draft.patch = entry?.patch ?? ""
|
|
38
|
-
if (!entry) {
|
|
39
|
-
draft.message = "No diff available"
|
|
40
|
-
} else if (parsed.length === 0) {
|
|
41
|
-
draft.message = "No visible changes (empty or binary file)"
|
|
42
|
-
} else if (parsed.length > MAX_DIFF_LINES) {
|
|
43
|
-
draft.lines = parsed.slice(0, MAX_DIFF_LINES)
|
|
44
|
-
draft.truncated = true
|
|
45
|
-
} else {
|
|
46
|
-
draft.lines = parsed
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
} catch (e) {
|
|
50
|
-
if (isStale()) return
|
|
51
|
-
setDiffState((draft) => {
|
|
52
|
-
draft.loading = false
|
|
53
|
-
draft.message = friendlyError(e)
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
package/src/diff/panel.tsx
DELETED
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import { createEffect, createMemo, Show } from "solid-js"
|
|
3
|
-
import { generateSyntax } from "@opencode/theme/tui"
|
|
4
|
-
import type { Context, PanelInput } from "@opencode/plugin/tui/context"
|
|
5
|
-
import { DIFF_PANEL_NAME, SETTINGS_STORE_KEY } from "../config"
|
|
6
|
-
import { notifyDiffClosed, useDiffStore } from "../state"
|
|
7
|
-
import { friendlyError } from "../ui"
|
|
8
|
-
import type { ScrollHandle } from "../types"
|
|
9
|
-
import { DiffBody } from "./body"
|
|
10
|
-
import { DiffHeader } from "./header"
|
|
11
|
-
import { registerDiffKeymap } from "./keymap"
|
|
12
|
-
import { createDiffSearch } from "./search"
|
|
13
|
-
|
|
14
|
-
export function DiffPanel(props: { ctx: Context; input: PanelInput }) {
|
|
15
|
-
const ctx = props.ctx
|
|
16
|
-
const input = () => props.input
|
|
17
|
-
const theme = ctx.theme
|
|
18
|
-
const [diffState, setDiffState] = useDiffStore(ctx)
|
|
19
|
-
const [settings, setSettings] = ctx.storage.store(SETTINGS_STORE_KEY, {
|
|
20
|
-
initial: { syntax: false },
|
|
21
|
-
})
|
|
22
|
-
const toggleSyntax = () => {
|
|
23
|
-
void setSettings((draft) => {
|
|
24
|
-
draft.syntax = !draft.syntax
|
|
25
|
-
}).catch(() => {})
|
|
26
|
-
}
|
|
27
|
-
const isSyntaxViewActive = () =>
|
|
28
|
-
settings.syntax && !diffState.truncated && diffState.query === "" && diffState.patch !== ""
|
|
29
|
-
const syntaxHighlightStyle = createMemo(() => (settings.syntax ? generateSyntax(theme) : undefined))
|
|
30
|
-
const search = createDiffSearch(diffState)
|
|
31
|
-
|
|
32
|
-
createEffect(() => {
|
|
33
|
-
if (input().name !== DIFF_PANEL_NAME && diffState.panelOpen) {
|
|
34
|
-
setDiffState((draft) => {
|
|
35
|
-
draft.panelOpen = false
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
createEffect(() => {
|
|
41
|
-
if (input().name === DIFF_PANEL_NAME && !input().focused) {
|
|
42
|
-
try {
|
|
43
|
-
input().focus()
|
|
44
|
-
} catch {
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
let scrollEl: ScrollHandle | undefined
|
|
50
|
-
|
|
51
|
-
const safeScrollTo = (y: number) => {
|
|
52
|
-
const el = scrollEl
|
|
53
|
-
if (!el) return
|
|
54
|
-
try {
|
|
55
|
-
el.scrollTo({ x: 0, y: Math.max(0, y) })
|
|
56
|
-
} catch {
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
createEffect(() => {
|
|
61
|
-
const line = search.currentMatchIndex()
|
|
62
|
-
if (line < 0 || input().name !== DIFF_PANEL_NAME || !scrollEl) return
|
|
63
|
-
const target = Math.max(0, line - 10)
|
|
64
|
-
setTimeout(() => safeScrollTo(target), 50)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
const scrollSyntaxBy = (delta: number) => {
|
|
68
|
-
if (!scrollEl) return
|
|
69
|
-
safeScrollTo(scrollEl.scrollTop + delta)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const stepSearchMatch = (dir: 1 | -1) => {
|
|
73
|
-
const m = search.matchIndices()
|
|
74
|
-
if (m.length === 0) return
|
|
75
|
-
setDiffState((draft) => {
|
|
76
|
-
draft.matchCursor = (draft.matchCursor + dir + m.length) % m.length
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const clearSearch = () => {
|
|
81
|
-
setDiffState((draft) => {
|
|
82
|
-
draft.query = ""
|
|
83
|
-
draft.matchCursor = 0
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const closeDiff = () => {
|
|
88
|
-
setDiffState((draft) => {
|
|
89
|
-
draft.panelOpen = false
|
|
90
|
-
})
|
|
91
|
-
ctx.ui.panel.close()
|
|
92
|
-
notifyDiffClosed()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const promptSearchQuery = async () => {
|
|
96
|
-
try {
|
|
97
|
-
const q = await ctx.ui.dialog.prompt({
|
|
98
|
-
title: "Search in diff",
|
|
99
|
-
placeholder: "Type to highlight matches",
|
|
100
|
-
value: diffState.query,
|
|
101
|
-
})
|
|
102
|
-
if (q === undefined) return
|
|
103
|
-
setDiffState((draft) => {
|
|
104
|
-
draft.query = q
|
|
105
|
-
draft.matchCursor = 0
|
|
106
|
-
})
|
|
107
|
-
} catch (e) {
|
|
108
|
-
ctx.ui.toast.show({ message: `Search failed: ${friendlyError(e)}`, variant: "error" })
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
registerDiffKeymap(ctx, {
|
|
113
|
-
input,
|
|
114
|
-
searching: () => diffState.query.length > 0,
|
|
115
|
-
hasMatches: () => search.matchIndices().length > 0,
|
|
116
|
-
syntaxActive: isSyntaxViewActive,
|
|
117
|
-
search: () => void promptSearchQuery(),
|
|
118
|
-
stepMatch: stepSearchMatch,
|
|
119
|
-
clearSearch,
|
|
120
|
-
close: closeDiff,
|
|
121
|
-
toggleSyntax,
|
|
122
|
-
scrollBy: scrollSyntaxBy,
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<Show when={input().name === DIFF_PANEL_NAME}>
|
|
127
|
-
<box flexDirection="column" gap={1} width="100%" height="100%">
|
|
128
|
-
<DiffHeader
|
|
129
|
-
theme={theme}
|
|
130
|
-
width={input().width}
|
|
131
|
-
file={diffState.file}
|
|
132
|
-
additions={diffState.additions}
|
|
133
|
-
deletions={diffState.deletions}
|
|
134
|
-
query={diffState.query}
|
|
135
|
-
matchCursor={diffState.matchCursor}
|
|
136
|
-
matchCount={search.matchIndices().length}
|
|
137
|
-
syntax={settings.syntax}
|
|
138
|
-
onSearch={() => void promptSearchQuery()}
|
|
139
|
-
onToggleSyntax={toggleSyntax}
|
|
140
|
-
onClose={closeDiff}
|
|
141
|
-
/>
|
|
142
|
-
<DiffBody
|
|
143
|
-
theme={theme}
|
|
144
|
-
state={diffState}
|
|
145
|
-
rows={search.visibleRows()}
|
|
146
|
-
focused={input().focused}
|
|
147
|
-
syntaxActive={isSyntaxViewActive()}
|
|
148
|
-
syntaxStyle={syntaxHighlightStyle()}
|
|
149
|
-
onScrollRef={(el) => {
|
|
150
|
-
scrollEl = el
|
|
151
|
-
}}
|
|
152
|
-
/>
|
|
153
|
-
</box>
|
|
154
|
-
</Show>
|
|
155
|
-
)
|
|
156
|
-
}
|
package/src/diff/search.tsx
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { createMemo } from "solid-js"
|
|
2
|
-
import type { DiffRow, DiffViewState } from "../types"
|
|
3
|
-
|
|
4
|
-
export function createDiffSearch(state: DiffViewState) {
|
|
5
|
-
const matchIndices = createMemo(() => {
|
|
6
|
-
const q = state.query.toLowerCase()
|
|
7
|
-
if (!q) return []
|
|
8
|
-
const out: number[] = []
|
|
9
|
-
state.lines.forEach((line, i) => {
|
|
10
|
-
if (line.text.toLowerCase().includes(q)) out.push(i)
|
|
11
|
-
})
|
|
12
|
-
return out
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
const currentMatchIndex = createMemo(() => {
|
|
16
|
-
const m = matchIndices()
|
|
17
|
-
if (m.length === 0) return -1
|
|
18
|
-
return m[((state.matchCursor % m.length) + m.length) % m.length]
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
let previousRows: DiffRow[] = []
|
|
22
|
-
const visibleRows = createMemo(() => {
|
|
23
|
-
const q = state.query.toLowerCase()
|
|
24
|
-
const cur = currentMatchIndex()
|
|
25
|
-
const prev = previousRows
|
|
26
|
-
const next = state.lines.map((line, idx) => {
|
|
27
|
-
const hit = q !== "" && line.text.toLowerCase().includes(q)
|
|
28
|
-
const rowState = (idx === cur ? "current" : hit ? "hit" : "plain") as DiffRow["state"]
|
|
29
|
-
const p = prev[idx]
|
|
30
|
-
if (p && p.kind === line.kind && p.text === line.text && p.line === line.line && p.state === rowState) return p
|
|
31
|
-
return { kind: line.kind, text: line.text, line: line.line, state: rowState }
|
|
32
|
-
})
|
|
33
|
-
previousRows = next
|
|
34
|
-
return next
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
return { matchIndices, currentMatchIndex, visibleRows }
|
|
38
|
-
}
|
package/src/git/diff-parse.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { DiffLine } from "../types"
|
|
2
|
-
|
|
3
|
-
const DIFF_PREAMBLE_RE = /^(diff --git |index |new file mode|deleted file mode|similarity index|rename from|rename to|old mode|new mode)/
|
|
4
|
-
const HUNK_HEADER_RE = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@ ?(.*)$/
|
|
5
|
-
|
|
6
|
-
export function formatHunkHeader(raw: string): string {
|
|
7
|
-
const m = raw.match(HUNK_HEADER_RE)
|
|
8
|
-
if (!m) return raw
|
|
9
|
-
const [, newStart, trailingContext] = m
|
|
10
|
-
return trailingContext ? `⋯ ${trailingContext}` : `⋯ line ${newStart}`
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function parseDiff(patch: string): DiffLine[] {
|
|
14
|
-
const out: DiffLine[] = []
|
|
15
|
-
let newLine = 0
|
|
16
|
-
for (const line of patch.split("\n")) {
|
|
17
|
-
if (line === "") continue
|
|
18
|
-
if (line.startsWith("@@")) {
|
|
19
|
-
const m = line.match(HUNK_HEADER_RE)
|
|
20
|
-
if (m) newLine = parseInt(m[1], 10)
|
|
21
|
-
out.push({ kind: "hunk", text: formatHunkHeader(line) })
|
|
22
|
-
} else if (line.startsWith("+++") || line.startsWith("---")) {
|
|
23
|
-
} else if (DIFF_PREAMBLE_RE.test(line)) {
|
|
24
|
-
} else if (line.startsWith("Binary files")) {
|
|
25
|
-
out.push({ kind: "meta", text: line })
|
|
26
|
-
} else if (line.startsWith("+")) {
|
|
27
|
-
out.push({ kind: "add", text: line.slice(1), line: newLine++ })
|
|
28
|
-
} else if (line.startsWith("-")) {
|
|
29
|
-
out.push({ kind: "del", text: line.slice(1) })
|
|
30
|
-
} else {
|
|
31
|
-
out.push({ kind: "context", text: line.startsWith(" ") ? line.slice(1) : line, line: newLine++ })
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return out
|
|
35
|
-
}
|
package/src/git/index.ts
DELETED
package/src/git/porcelain.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export function parseStagedPorcelain(porcelain: string): Set<string> {
|
|
2
|
-
const staged = new Set<string>()
|
|
3
|
-
for (const record of porcelain.split("\0")) {
|
|
4
|
-
if (record.length < 4) continue
|
|
5
|
-
const indexStatus = record[0]
|
|
6
|
-
if (indexStatus === " " || indexStatus === "?") continue
|
|
7
|
-
staged.add(record.slice(3))
|
|
8
|
-
}
|
|
9
|
-
return staged
|
|
10
|
-
}
|