refactor(tui): migrate diff viewer to V2 theme (#37999)
This commit is contained in:
parent
e93fa06d05
commit
a2c4c7a64b
3 changed files with 86 additions and 101 deletions
|
|
@ -1,31 +1,19 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import type { ColorInput, RGBA, ScrollBoxRenderable } from "@opentui/core"
|
||||
import type { ScrollBoxRenderable } from "@opentui/core"
|
||||
import { Locale } from "../../util/locale"
|
||||
import { tint } from "../../theme/color"
|
||||
import { createEffect, createMemo, For, Match, Switch } from "solid-js"
|
||||
import { buildFileTree, flattenFileTree, type FileTreeItem, type FileTreeRow } from "./diff-viewer-file-tree-utils"
|
||||
import { Panel } from "./diff-viewer-ui"
|
||||
import { useTheme } from "../../context/theme"
|
||||
|
||||
const FILE_TREE_STATUS_WIDTH = 2
|
||||
|
||||
export type DiffViewerFileTreeTheme = {
|
||||
readonly background: RGBA
|
||||
readonly backgroundPanel: ColorInput
|
||||
readonly backgroundElement: ColorInput
|
||||
readonly primary: ColorInput
|
||||
readonly secondary: ColorInput
|
||||
readonly selectedListItemText: ColorInput
|
||||
readonly text: RGBA
|
||||
readonly textMuted: RGBA
|
||||
readonly error: ColorInput
|
||||
}
|
||||
|
||||
export type DiffViewerFileTreeProps = {
|
||||
readonly width: number
|
||||
readonly files: readonly FileTreeItem[]
|
||||
readonly loading: boolean
|
||||
readonly error: unknown
|
||||
readonly theme: DiffViewerFileTreeTheme
|
||||
readonly focused?: boolean
|
||||
readonly highlightedNode?: number
|
||||
readonly selectedFileIndex?: number
|
||||
|
|
@ -35,6 +23,7 @@ export type DiffViewerFileTreeProps = {
|
|||
}
|
||||
|
||||
export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
||||
const { themeV2 } = useTheme()
|
||||
const tree = createMemo(() => buildFileTree(props.files))
|
||||
const rows = createMemo(() => flattenFileTree(tree(), props.expandedNodes))
|
||||
let scroll: ScrollBoxRenderable | undefined
|
||||
|
|
@ -49,7 +38,7 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
|||
requestAnimationFrame(scrollSelectedIntoView)
|
||||
})
|
||||
|
||||
const fadedColor = () => tint(props.theme.text, props.theme.background, 0.75)
|
||||
const fadedColor = () => tint(themeV2.text.default, themeV2.background.default, 0.75)
|
||||
|
||||
return (
|
||||
<Panel border="both" width={props.width}>
|
||||
|
|
@ -63,7 +52,7 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
|||
<text />
|
||||
</Match>
|
||||
<Match when={props.files.length === 0}>
|
||||
<text fg={props.theme.text}>No files</text>
|
||||
<text fg={themeV2.text.default}>No files</text>
|
||||
</Match>
|
||||
<Match when={props.files.length > 0}>
|
||||
<For each={rows()}>
|
||||
|
|
@ -82,22 +71,26 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
|||
<box
|
||||
flexDirection="row"
|
||||
width="100%"
|
||||
backgroundColor={highlighted() ? props.theme.primary : undefined}
|
||||
backgroundColor={highlighted() ? themeV2.background.action.primary.focused : undefined}
|
||||
onMouseUp={() => props.onRowClick?.(row)}
|
||||
>
|
||||
<text fg={highlighted() ? props.theme.background : fadedColor()} wrapMode="none" flexShrink={0}>
|
||||
<text
|
||||
fg={highlighted() ? themeV2.text.action.primary.focused : fadedColor()}
|
||||
wrapMode="none"
|
||||
flexShrink={0}
|
||||
>
|
||||
{prefix()}
|
||||
</text>
|
||||
<box flexGrow={1} minWidth={0}>
|
||||
<text
|
||||
fg={
|
||||
highlighted()
|
||||
? props.theme.background
|
||||
? themeV2.text.action.primary.focused
|
||||
: selected()
|
||||
? props.theme.primary
|
||||
? themeV2.text.formfield.selected
|
||||
: reviewed() || row.kind === "directory"
|
||||
? props.theme.textMuted
|
||||
: props.theme.text
|
||||
? themeV2.text.subdued
|
||||
: themeV2.text.default
|
||||
}
|
||||
wrapMode="none"
|
||||
>
|
||||
|
|
@ -105,7 +98,7 @@ export function DiffViewerFileTree(props: DiffViewerFileTreeProps) {
|
|||
</text>
|
||||
</box>
|
||||
<text
|
||||
fg={highlighted() ? props.theme.background : props.theme.textMuted}
|
||||
fg={highlighted() ? themeV2.text.action.primary.focused : themeV2.text.subdued}
|
||||
wrapMode="none"
|
||||
flexShrink={0}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
const config = useConfig()
|
||||
const dialog = useDialog()
|
||||
const themeState = useTheme()
|
||||
const theme = () => themeState.theme
|
||||
const themeV2 = themeState.themeV2
|
||||
const params = () => {
|
||||
const route = props.context.ui.router.current()
|
||||
return (route.type === "plugin" ? route.data : undefined) as
|
||||
|
|
@ -741,11 +741,11 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
<box position="absolute" zIndex={2500} left={0} top={0} width={dimensions().width} height={dimensions().height}>
|
||||
<PanelGroup axis="y" width="100%" height="100%">
|
||||
<Panel border="none" flexShrink={0} padding={0} paddingLeft={1}>
|
||||
<text fg={theme().text}>Diff </text>
|
||||
<text fg={theme().textMuted}>{diffSourceLabel(mode())}</text>
|
||||
<text fg={themeV2.text.default}>Diff </text>
|
||||
<text fg={themeV2.text.subdued}>{diffSourceLabel(mode())}</text>
|
||||
<box flexGrow={1} />
|
||||
<Show when={!diff.loading && !diff.error}>
|
||||
<text fg={theme().textMuted}>
|
||||
<text fg={themeV2.text.subdued}>
|
||||
{files().length} {files().length === 1 ? "file" : "files"}
|
||||
</text>
|
||||
</Show>
|
||||
|
|
@ -756,19 +756,21 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
<Match when={diff.loading}>
|
||||
<Separator axis="x" />
|
||||
<box flexGrow={1} paddingLeft={1}>
|
||||
<text fg={theme().textMuted}>Loading diff…</text>
|
||||
<text fg={themeV2.text.subdued}>Loading diff…</text>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={!diff.loading && diff.error}>
|
||||
<Separator axis="x" />
|
||||
<box flexGrow={1} paddingLeft={1}>
|
||||
<text fg={theme().error}>Could not load diff. Reopen the diff viewer to try again.</text>
|
||||
<text fg={themeV2.text.feedback.error.default}>
|
||||
Could not load diff. Reopen the diff viewer to try again.
|
||||
</text>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={!diff.loading && files().length === 0}>
|
||||
<Separator axis="x" />
|
||||
<box flexGrow={1} paddingLeft={1}>
|
||||
<text fg={theme().textMuted}>No changes to show</text>
|
||||
<text fg={themeV2.text.subdued}>No changes to show</text>
|
||||
</box>
|
||||
</Match>
|
||||
<Match when={!diff.loading}>
|
||||
|
|
@ -778,7 +780,6 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
files={files()}
|
||||
loading={diff.loading}
|
||||
error={diff.error}
|
||||
theme={theme()}
|
||||
focused={focus() === "files"}
|
||||
width={FILE_TREE_WIDTH}
|
||||
highlightedNode={highlightedFileNode()}
|
||||
|
|
@ -812,24 +813,26 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
border={patchLeftBorder()}
|
||||
borderColor={theme().border}
|
||||
borderColor={themeV2.border.default}
|
||||
>
|
||||
<text fg={reviewed() ? theme().textMuted : theme().text}>{entry.file.file}</text>
|
||||
<text fg={reviewed() ? themeV2.text.subdued : themeV2.text.default}>
|
||||
{entry.file.file}
|
||||
</text>
|
||||
<box flexGrow={1} />
|
||||
<text fg={reviewed() ? theme().textMuted : theme().diffAdded}>
|
||||
<text fg={reviewed() ? themeV2.text.subdued : themeV2.diff.text.added}>
|
||||
+{entry.file.additions}
|
||||
</text>
|
||||
<text fg={reviewed() ? theme().textMuted : theme().diffRemoved}>
|
||||
<text fg={reviewed() ? themeV2.text.subdued : themeV2.diff.text.removed}>
|
||||
-{entry.file.deletions}
|
||||
</text>
|
||||
</box>
|
||||
<Separator axis="x" start={showFileTree() ? "edge" : undefined} />
|
||||
<Show
|
||||
when={entry.file.patch}
|
||||
fallback={<text fg={theme().textMuted}>No patch available for this file.</text>}
|
||||
fallback={<text fg={themeV2.text.subdued}>No patch available for this file.</text>}
|
||||
>
|
||||
{(patch) => (
|
||||
<box border={patchLeftBorder()} borderColor={theme().border}>
|
||||
<box border={patchLeftBorder()} borderColor={themeV2.border.default}>
|
||||
<diff
|
||||
ref={(element: DiffRenderable) => diffNodeByFileIndex.set(entry.fileIndex, element)}
|
||||
diff={patch()}
|
||||
|
|
@ -839,17 +842,27 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
showLineNumbers={true}
|
||||
width="100%"
|
||||
wrapMode="char"
|
||||
fg={reviewed() ? theme().textMuted : theme().text}
|
||||
addedBg={reviewed() ? theme().backgroundElement : theme().diffAddedBg}
|
||||
removedBg={reviewed() ? theme().backgroundElement : theme().diffRemovedBg}
|
||||
addedSignColor={reviewed() ? theme().textMuted : theme().diffHighlightAdded}
|
||||
removedSignColor={reviewed() ? theme().textMuted : theme().diffHighlightRemoved}
|
||||
lineNumberFg={theme().diffLineNumber}
|
||||
fg={reviewed() ? themeV2.text.subdued : themeV2.text.default}
|
||||
addedBg={
|
||||
reviewed() ? themeV2.background.surface.overlay : themeV2.diff.background.added
|
||||
}
|
||||
removedBg={
|
||||
reviewed() ? themeV2.background.surface.overlay : themeV2.diff.background.removed
|
||||
}
|
||||
addedSignColor={reviewed() ? themeV2.text.subdued : themeV2.diff.highlight.added}
|
||||
removedSignColor={
|
||||
reviewed() ? themeV2.text.subdued : themeV2.diff.highlight.removed
|
||||
}
|
||||
lineNumberFg={themeV2.diff.lineNumber.text}
|
||||
addedLineNumberBg={
|
||||
reviewed() ? theme().backgroundElement : theme().diffAddedLineNumberBg
|
||||
reviewed()
|
||||
? themeV2.background.surface.overlay
|
||||
: themeV2.diff.lineNumber.background.added
|
||||
}
|
||||
removedLineNumberBg={
|
||||
reviewed() ? theme().backgroundElement : theme().diffRemovedLineNumberBg
|
||||
reviewed()
|
||||
? themeV2.background.surface.overlay
|
||||
: themeV2.diff.lineNumber.background.removed
|
||||
}
|
||||
/>
|
||||
</box>
|
||||
|
|
@ -860,7 +873,11 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
}}
|
||||
</For>
|
||||
<Show when={patchFillerHeight() > 0}>
|
||||
<box height={patchFillerHeight()} border={patchLeftBorder()} borderColor={theme().border} />
|
||||
<box
|
||||
height={patchFillerHeight()}
|
||||
border={patchLeftBorder()}
|
||||
borderColor={themeV2.border.default}
|
||||
/>
|
||||
</Show>
|
||||
</scrollbox>
|
||||
<Separator axis="x" start={showFileTree() ? "edge-in" : undefined} />
|
||||
|
|
@ -873,57 +890,57 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
<Panel flexShrink={0} gap={2} paddingLeft={1} border="none">
|
||||
<Show when={switchFocusShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={theme().text}>
|
||||
{shortcut()} <span style={{ fg: theme().textMuted }}>focus file tree</span>
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>focus file tree</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={nextFileShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={theme().text}>
|
||||
{shortcut()} <span style={{ fg: theme().textMuted }}>next file</span>
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>next file</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={nextHunkShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={theme().text}>
|
||||
{shortcut()} <span style={{ fg: theme().textMuted }}>next hunk</span>
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>next hunk</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={previousHunkShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={theme().text}>
|
||||
{shortcut()} <span style={{ fg: theme().textMuted }}>previous hunk</span>
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>previous hunk</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={previousFileShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={theme().text}>
|
||||
{shortcut()} <span style={{ fg: theme().textMuted }}>previous file</span>
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>previous file</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={switchSourceShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={theme().text}>
|
||||
{shortcut()} <span style={{ fg: theme().textMuted }}>switch source</span>
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>switch source</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={markReviewedShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={theme().text}>
|
||||
{shortcut()} <span style={{ fg: theme().textMuted }}>mark reviewed</span>
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>mark reviewed</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={helpShortcut()}>
|
||||
{(shortcut) => (
|
||||
<text fg={theme().text}>
|
||||
{shortcut()} <span style={{ fg: theme().textMuted }}>all</span>
|
||||
<text fg={themeV2.text.default}>
|
||||
{shortcut()} <span style={{ fg: themeV2.text.subdued }}>all</span>
|
||||
</text>
|
||||
)}
|
||||
</Show>
|
||||
|
|
@ -934,7 +951,7 @@ function DiffViewer(props: { context: Plugin.Context }) {
|
|||
}
|
||||
|
||||
function DiffViewerHelpDialog(props: { context: Plugin.Context }) {
|
||||
const { theme } = useTheme()
|
||||
const { themeV2 } = useTheme().contextual("elevated")
|
||||
const shortcut = (id: string) => () => props.context.keymap.shortcut(id)
|
||||
const rows = [
|
||||
{
|
||||
|
|
@ -1002,30 +1019,30 @@ function DiffViewerHelpDialog(props: { context: Plugin.Context }) {
|
|||
return (
|
||||
<box paddingLeft={2} paddingRight={2} paddingBottom={1} gap={1}>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD} fg={theme.text}>
|
||||
<text attributes={TextAttributes.BOLD} fg={themeV2.text.default}>
|
||||
Diff shortcuts
|
||||
</text>
|
||||
<text fg={theme.textMuted}>esc</text>
|
||||
<text fg={themeV2.text.subdued}>esc</text>
|
||||
</box>
|
||||
<box flexDirection="row">
|
||||
<text fg={theme.textMuted} width={5} wrapMode="none">
|
||||
<text fg={themeV2.text.subdued} width={5} wrapMode="none">
|
||||
Key
|
||||
</text>
|
||||
<text fg={theme.textMuted} width={22} wrapMode="none">
|
||||
<text fg={themeV2.text.subdued} width={22} wrapMode="none">
|
||||
Action
|
||||
</text>
|
||||
<text fg={theme.textMuted}>Description</text>
|
||||
<text fg={themeV2.text.subdued}>Description</text>
|
||||
</box>
|
||||
<For each={rows}>
|
||||
{(row) => (
|
||||
<box flexDirection="row">
|
||||
<text fg={theme.text} width={5} wrapMode="none">
|
||||
<text fg={themeV2.text.default} width={5} wrapMode="none">
|
||||
{row.shortcut() || "-"}
|
||||
</text>
|
||||
<text fg={theme.text} width={22} wrapMode="none">
|
||||
<text fg={themeV2.text.default} width={22} wrapMode="none">
|
||||
{row.action}
|
||||
</text>
|
||||
<text fg={theme.textMuted}>{row.description}</text>
|
||||
<text fg={themeV2.text.subdued}>{row.description}</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
/** @jsxImportSource @opentui/solid */
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { RGBA } from "@opentui/core"
|
||||
import { testRender } from "@opentui/solid"
|
||||
import type { JSX } from "solid-js"
|
||||
import { onMount, type ParentProps } from "solid-js"
|
||||
|
|
@ -14,18 +13,6 @@ import {
|
|||
buildFileTree,
|
||||
} from "../../../src/feature-plugins/system/diff-viewer-file-tree-utils"
|
||||
|
||||
const theme = {
|
||||
background: RGBA.fromHex("#000000"),
|
||||
backgroundPanel: RGBA.fromHex("#111111"),
|
||||
backgroundElement: RGBA.fromHex("#333333"),
|
||||
primary: RGBA.fromHex("#00ffff"),
|
||||
secondary: RGBA.fromHex("#0088ff"),
|
||||
selectedListItemText: RGBA.fromHex("#ffffff"),
|
||||
text: RGBA.fromHex("#ffffff"),
|
||||
textMuted: RGBA.fromHex("#888888"),
|
||||
error: RGBA.fromHex("#ff0000"),
|
||||
}
|
||||
|
||||
describe("DiffViewerFileTree", () => {
|
||||
test.skip("renders sorted hierarchical file rows", async () => {
|
||||
const lines = visibleLines(
|
||||
|
|
@ -41,7 +28,6 @@ describe("DiffViewerFileTree", () => {
|
|||
]}
|
||||
loading={false}
|
||||
error={undefined}
|
||||
theme={theme}
|
||||
focused={true}
|
||||
/>
|
||||
)),
|
||||
|
|
@ -59,13 +45,13 @@ describe("DiffViewerFileTree", () => {
|
|||
|
||||
test("keeps loading and error quiet while rendering an empty settled state", async () => {
|
||||
const loading = await renderFrame(() => (
|
||||
<DiffViewerFileTree width={32} files={[]} loading={true} error={undefined} theme={theme} />
|
||||
<DiffViewerFileTree width={32} files={[]} loading={true} error={undefined} />
|
||||
))
|
||||
const failed = await renderFrame(() => (
|
||||
<DiffViewerFileTree width={32} files={[]} loading={false} error={new Error("nope")} theme={theme} />
|
||||
<DiffViewerFileTree width={32} files={[]} loading={false} error={new Error("nope")} />
|
||||
))
|
||||
const empty = await renderFrame(() => (
|
||||
<DiffViewerFileTree width={32} files={[]} loading={false} error={undefined} theme={theme} />
|
||||
<DiffViewerFileTree width={32} files={[]} loading={false} error={undefined} />
|
||||
))
|
||||
|
||||
expect(loading).not.toContain("Loading diff...")
|
||||
|
|
@ -86,16 +72,13 @@ describe("DiffViewerFileTree", () => {
|
|||
files={files}
|
||||
loading={false}
|
||||
error={undefined}
|
||||
theme={theme}
|
||||
focused
|
||||
highlightedNode={src.id}
|
||||
/>
|
||||
)),
|
||||
)
|
||||
const unfocused = visibleLines(
|
||||
await renderFrame(() => (
|
||||
<DiffViewerFileTree width={32} files={files} loading={false} error={undefined} theme={theme} />
|
||||
)),
|
||||
await renderFrame(() => <DiffViewerFileTree width={32} files={files} loading={false} error={undefined} />),
|
||||
)
|
||||
|
||||
expect(focused).toContain("▾ src/config")
|
||||
|
|
@ -114,14 +97,7 @@ describe("DiffViewerFileTree", () => {
|
|||
expect(
|
||||
visibleLines(
|
||||
await renderFrame(() => (
|
||||
<DiffViewerFileTree
|
||||
width={32}
|
||||
files={files}
|
||||
loading={false}
|
||||
error={undefined}
|
||||
theme={theme}
|
||||
expandedNodes={collapsed}
|
||||
/>
|
||||
<DiffViewerFileTree width={32} files={files} loading={false} error={undefined} expandedNodes={collapsed} />
|
||||
)),
|
||||
),
|
||||
).toEqual(["▸ src/config"])
|
||||
|
|
@ -134,7 +110,6 @@ describe("DiffViewerFileTree", () => {
|
|||
width={32}
|
||||
loading={false}
|
||||
error={undefined}
|
||||
theme={theme}
|
||||
expandedNodes={allExpandedFileTreeDirectories(tree)}
|
||||
/>
|
||||
)),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue