🐛 fix(vscode): 改动 tab 默认统一视图并保留阅读位置
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
DEFAULT_PR_DIFF_MODE,
|
||||||
|
isPrDiffMode,
|
||||||
|
parsePrDiffMode,
|
||||||
|
type PrDiffMode,
|
||||||
|
} from './prDiffMode'
|
||||||
|
|
||||||
|
describe('DEFAULT_PR_DIFF_MODE', () => {
|
||||||
|
it('默认是 unified', () => {
|
||||||
|
expect(DEFAULT_PR_DIFF_MODE).toBe('unified')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isPrDiffMode', () => {
|
||||||
|
it('仅接受 unified / split', () => {
|
||||||
|
expect(isPrDiffMode('unified')).toBe(true)
|
||||||
|
expect(isPrDiffMode('split')).toBe(true)
|
||||||
|
expect(isPrDiffMode('Unified')).toBe(false)
|
||||||
|
expect(isPrDiffMode('SPLIT')).toBe(false)
|
||||||
|
expect(isPrDiffMode('')).toBe(false)
|
||||||
|
expect(isPrDiffMode(null)).toBe(false)
|
||||||
|
expect(isPrDiffMode(undefined)).toBe(false)
|
||||||
|
expect(isPrDiffMode(3)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('parsePrDiffMode', () => {
|
||||||
|
it('split → split', () => {
|
||||||
|
expect(parsePrDiffMode('split')).toBe('split')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('unified → unified', () => {
|
||||||
|
expect(parsePrDiffMode('unified')).toBe('unified')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('缺省 / 非法一律 unified', () => {
|
||||||
|
expect(parsePrDiffMode(undefined)).toBe('unified')
|
||||||
|
expect(parsePrDiffMode(null)).toBe('unified')
|
||||||
|
expect(parsePrDiffMode('')).toBe('unified')
|
||||||
|
expect(parsePrDiffMode('Split')).toBe('unified')
|
||||||
|
expect(parsePrDiffMode('UNIFIED')).toBe('unified')
|
||||||
|
expect(parsePrDiffMode(1)).toBe('unified')
|
||||||
|
expect(parsePrDiffMode({})).toBe('unified')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('返回类型可赋值给 PrDiffMode', () => {
|
||||||
|
const mode: PrDiffMode = parsePrDiffMode('split')
|
||||||
|
expect(mode).toBe('split')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* 「改动」tab 的 diff 模式(统一 / 分栏)解析。
|
||||||
|
* 纯逻辑:不碰 VS Code API,便于单测。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const DEFAULT_PR_DIFF_MODE = 'unified' as const
|
||||||
|
|
||||||
|
export type PrDiffMode = 'unified' | 'split'
|
||||||
|
|
||||||
|
export function isPrDiffMode(x: unknown): x is PrDiffMode {
|
||||||
|
return x === 'unified' || x === 'split'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* workspaceState 读出值 → 合法模式。
|
||||||
|
* 仅精确字符串 `'split'` 才是分栏;其它一律统一(默认路径)。
|
||||||
|
*/
|
||||||
|
export function parsePrDiffMode(stored: unknown): PrDiffMode {
|
||||||
|
return stored === 'split' ? 'split' : DEFAULT_PR_DIFF_MODE
|
||||||
|
}
|
||||||
@@ -588,6 +588,14 @@ export class KanbanWebviewPanel {
|
|||||||
void prFiles.handleSetPrReviewConfirmed(this, msg)
|
void prFiles.handleSetPrReviewConfirmed(this, msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (msg.type === 'pr-diff-mode/get') {
|
||||||
|
prFiles.handleGetPrDiffMode(this)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (msg.type === 'pr-diff-mode/set') {
|
||||||
|
void prFiles.handleSetPrDiffMode(this, msg.mode)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal: handler 模块访问
|
// internal: handler 模块访问
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { getPullRequest, getRawFile, listPullRequestFiles } from '../../gitea/ap
|
|||||||
import { readStateJsonComment } from '../../gitea/stateJson'
|
import { readStateJsonComment } from '../../gitea/stateJson'
|
||||||
import { readFileSummaries, readSummarySession, writeFileSummary, writeSummarySession } from '../../sessions/prFileSummaryStore'
|
import { readFileSummaries, readSummarySession, writeFileSummary, writeSummarySession } from '../../sessions/prFileSummaryStore'
|
||||||
import { readConfirmedSigs, writeConfirmedSigs } from '../../sessions/prReviewStore'
|
import { readConfirmedSigs, writeConfirmedSigs } from '../../sessions/prReviewStore'
|
||||||
|
import { parsePrDiffMode, type PrDiffMode } from '../../cc/prDiffMode'
|
||||||
import { makeNonce } from '../KanbanPanel'
|
import { makeNonce } from '../KanbanPanel'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -351,3 +352,22 @@ export async function handleGeneratePrFileSummary(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PR_DIFF_MODE_KEY = 'prDiffMode'
|
||||||
|
|
||||||
|
/** 读 workspace 级 diff 模式(缺省 unified),回推 webview。 */
|
||||||
|
export function handleGetPrDiffMode(panel: KanbanWebviewPanel): void {
|
||||||
|
const stored = panel.context.workspaceState.get<unknown>(PR_DIFF_MODE_KEY)
|
||||||
|
const mode = parsePrDiffMode(stored)
|
||||||
|
panel.postMessage({ type: 'pr-diff-mode/show', mode })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 写 workspace 级 diff 模式;非法值按 unified 落盘。 */
|
||||||
|
export async function handleSetPrDiffMode(
|
||||||
|
panel: KanbanWebviewPanel,
|
||||||
|
mode: unknown,
|
||||||
|
): Promise<void> {
|
||||||
|
const next: PrDiffMode = parsePrDiffMode(mode)
|
||||||
|
await panel.context.workspaceState.update(PR_DIFF_MODE_KEY, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ export type ExtensionToWebview
|
|||||||
| { type: 'pr-files/show', issueNumber: number, files: PrFile[], confirmed: string[], summaries: Record<string, string>, error?: string }
|
| { type: 'pr-files/show', issueNumber: number, files: PrFile[], confirmed: string[], summaries: Record<string, string>, error?: string }
|
||||||
| { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string }
|
| { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string }
|
||||||
| { type: 'pr-file-summary/show', issueNumber: number, path: string, summary?: string, error?: string }
|
| { type: 'pr-file-summary/show', issueNumber: number, path: string, summary?: string, error?: string }
|
||||||
|
| { type: 'pr-diff-mode/show', mode: 'unified' | 'split' }
|
||||||
// profile 值编辑器粘贴的图片:存盘/读盘的回执,requestId 关联并发请求。
|
// profile 值编辑器粘贴的图片:存盘/读盘的回执,requestId 关联并发请求。
|
||||||
| { type: 'profile-asset/saved', requestId: string, assetPath?: string, error?: string }
|
| { type: 'profile-asset/saved', requestId: string, assetPath?: string, error?: string }
|
||||||
| { type: 'profile-asset/loaded', requestId: string, dataUrl?: string, error?: string }
|
| { type: 'profile-asset/loaded', requestId: string, dataUrl?: string, error?: string }
|
||||||
@@ -194,6 +195,8 @@ export type WebviewToExtension
|
|||||||
| { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string }
|
| { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string }
|
||||||
| { type: 'pr-file-summary/generate', issueNumber: number, path: string, previousPath?: string }
|
| { type: 'pr-file-summary/generate', issueNumber: number, path: string, previousPath?: string }
|
||||||
| { type: 'pr-review/set', issueNumber: number, confirmed: Record<string, string> }
|
| { type: 'pr-review/set', issueNumber: number, confirmed: Record<string, string> }
|
||||||
|
| { type: 'pr-diff-mode/get' }
|
||||||
|
| { type: 'pr-diff-mode/set', mode: 'unified' | 'split' }
|
||||||
// profile 值编辑器:图片存盘(base64→磁盘)与读盘(磁盘→dataUrl)请求。
|
// profile 值编辑器:图片存盘(base64→磁盘)与读盘(磁盘→dataUrl)请求。
|
||||||
| { type: 'profile-asset/save', requestId: string, base64: string, mediaType: string }
|
| { type: 'profile-asset/save', requestId: string, base64: string, mediaType: string }
|
||||||
| { type: 'profile-asset/load', requestId: string, assetPath: string }
|
| { type: 'profile-asset/load', requestId: string, assetPath: string }
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* 底部 panel:左侧垂直 tab 切换两个 property grid。
|
* 底部 panel:左侧垂直 tab 切换四个内容区。
|
||||||
*
|
*
|
||||||
* - tab 1 「工单」:现有 IssueDetailPanel(基于 PropertyGrid)
|
* - tab 1 「工单」:现有 IssueDetailPanel(基于 PropertyGrid)
|
||||||
* - tab 2 「Profile」:工作区 KV 表 ProfileGrid
|
* - tab 2 「Profile」:工作区 KV 表 ProfileGrid
|
||||||
|
* - tab 3 「会话」:ManagedSessionsPanel
|
||||||
|
* - tab 4 「改动」:PrFilesPanel
|
||||||
*
|
*
|
||||||
* tab 状态用 useState,无持久化;激活态加左侧 2px 高亮条。
|
* 四个内容始终挂载,非当前用 hidden 隐藏,避免切 tab 卸载导致阅读态(选中文件、
|
||||||
|
* 树滚动/折叠、diff scroll)丢失。tab 状态用 useState,无持久化;激活态加左侧 2px 高亮条。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ClaudeProfile } from '../hooks/useIssues'
|
import type { ClaudeProfile } from '../hooks/useIssues'
|
||||||
@@ -99,9 +102,13 @@ export function BottomTabs(props: BottomTabsProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧内容 */}
|
{/* 右侧内容:始终挂载,非当前 hidden,保留阅读态 */}
|
||||||
<div className="min-w-0 flex-1 overflow-hidden">
|
<div className="relative min-w-0 flex-1 overflow-hidden">
|
||||||
{tab === 'issue' && (
|
<div
|
||||||
|
className={`h-full w-full ${tab === 'issue' ? '' : 'hidden'}`}
|
||||||
|
aria-hidden={tab !== 'issue'}
|
||||||
|
inert={tab !== 'issue' ? true : undefined}
|
||||||
|
>
|
||||||
<IssueDetailPanel
|
<IssueDetailPanel
|
||||||
issue={props.issue}
|
issue={props.issue}
|
||||||
allIssues={props.allIssues}
|
allIssues={props.allIssues}
|
||||||
@@ -130,15 +137,23 @@ export function BottomTabs(props: BottomTabsProps) {
|
|||||||
profiles={props.profiles}
|
profiles={props.profiles}
|
||||||
onOpenLogs={props.onOpenLogs}
|
onOpenLogs={props.onOpenLogs}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
{tab === 'profile' && (
|
<div
|
||||||
|
className={`h-full w-full ${tab === 'profile' ? '' : 'hidden'}`}
|
||||||
|
aria-hidden={tab !== 'profile'}
|
||||||
|
inert={tab !== 'profile' ? true : undefined}
|
||||||
|
>
|
||||||
<ProfileGrid
|
<ProfileGrid
|
||||||
data={props.profileData}
|
data={props.profileData}
|
||||||
onSave={props.onProfileSave}
|
onSave={props.onProfileSave}
|
||||||
onOpen={props.onProfileOpen}
|
onOpen={props.onProfileOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
{tab === 'sessions' && (
|
<div
|
||||||
|
className={`h-full w-full ${tab === 'sessions' ? '' : 'hidden'}`}
|
||||||
|
aria-hidden={tab !== 'sessions'}
|
||||||
|
inert={tab !== 'sessions' ? true : undefined}
|
||||||
|
>
|
||||||
<ManagedSessionsPanel
|
<ManagedSessionsPanel
|
||||||
data={props.managedSessions}
|
data={props.managedSessions}
|
||||||
profiles={props.profiles}
|
profiles={props.profiles}
|
||||||
@@ -152,10 +167,14 @@ export function BottomTabs(props: BottomTabsProps) {
|
|||||||
onListImportable={props.onListImportableSessions}
|
onListImportable={props.onListImportableSessions}
|
||||||
onImport={props.onImportNamedSession}
|
onImport={props.onImportNamedSession}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
{tab === 'changes' && (
|
<div
|
||||||
|
className={`h-full w-full ${tab === 'changes' ? '' : 'hidden'}`}
|
||||||
|
aria-hidden={tab !== 'changes'}
|
||||||
|
inert={tab !== 'changes' ? true : undefined}
|
||||||
|
>
|
||||||
<PrFilesPanel issue={props.issue} />
|
<PrFilesPanel issue={props.issue} />
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,11 +7,14 @@
|
|||||||
*
|
*
|
||||||
* 自带 usePrFiles hook,直接经 lib/vscode 收发消息,不依赖上层回调。diff 左右两侧用
|
* 自带 usePrFiles hook,直接经 lib/vscode 收发消息,不依赖上层回调。diff 左右两侧用
|
||||||
* merge_base → head 的原文(与 Gitea 网页 diff 口径一致),已查看态按文件变更指纹落盘。
|
* merge_base → head 的原文(与 Gitea 网页 diff 口径一致),已查看态按文件变更指纹落盘。
|
||||||
|
* 统一/分栏模式按 workspace 持久化,与当前工单无关。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Issue } from '../types'
|
import type { Issue } from '../types'
|
||||||
|
import { DiffModeEnum } from '@git-diff-view/react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { usePrFiles } from '../hooks/usePrFiles'
|
import { usePrFiles } from '../hooks/usePrFiles'
|
||||||
|
import { onMessage, postMessage } from '../lib/vscode'
|
||||||
import { FileDiffPane } from './prFiles/FileDiffPane'
|
import { FileDiffPane } from './prFiles/FileDiffPane'
|
||||||
import { FileTree } from './prFiles/FileTree'
|
import { FileTree } from './prFiles/FileTree'
|
||||||
import { buildDirTree, flattenTree, isIgnoredPath } from './prFiles/fileTree'
|
import { buildDirTree, flattenTree, isIgnoredPath } from './prFiles/fileTree'
|
||||||
@@ -28,6 +31,14 @@ function Centered({ children }: { children: React.ReactNode }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function modeToWire(mode: DiffModeEnum): 'unified' | 'split' {
|
||||||
|
return mode === DiffModeEnum.Split ? 'split' : 'unified'
|
||||||
|
}
|
||||||
|
|
||||||
|
function modeFromWire(mode: 'unified' | 'split'): DiffModeEnum {
|
||||||
|
return mode === 'split' ? DiffModeEnum.Split : DiffModeEnum.Unified
|
||||||
|
}
|
||||||
|
|
||||||
export function PrFilesPanel({ issue }: PrFilesPanelProps) {
|
export function PrFilesPanel({ issue }: PrFilesPanelProps) {
|
||||||
const issueNumber = issue?.number
|
const issueNumber = issue?.number
|
||||||
const {
|
const {
|
||||||
@@ -46,8 +57,19 @@ export function PrFilesPanel({ issue }: PrFilesPanelProps) {
|
|||||||
const [selectedPath, setSelectedPath] = useState<string | undefined>(undefined)
|
const [selectedPath, setSelectedPath] = useState<string | undefined>(undefined)
|
||||||
// 已折叠目录的完整路径集合;不在集合内即展开(默认全展开)。
|
// 已折叠目录的完整路径集合;不在集合内即展开(默认全展开)。
|
||||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set())
|
const [collapsed, setCollapsed] = useState<Set<string>>(new Set())
|
||||||
|
// workspace 级 diff 模式:默认统一;mount 时从扩展侧拉一次。
|
||||||
|
const [diffMode, setDiffMode] = useState<DiffModeEnum>(DiffModeEnum.Unified)
|
||||||
|
|
||||||
// 切工单时清掉选中态与折叠态(hook 已清缓存)。
|
useEffect(() => {
|
||||||
|
const cleanup = onMessage((msg) => {
|
||||||
|
if (msg.type === 'pr-diff-mode/show')
|
||||||
|
setDiffMode(modeFromWire(msg.mode))
|
||||||
|
})
|
||||||
|
postMessage({ type: 'pr-diff-mode/get' })
|
||||||
|
return cleanup
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// 切工单时清掉选中态与折叠态(hook 已清缓存);FileDiffPane 用 key 重置 scroll map。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedPath(undefined)
|
setSelectedPath(undefined)
|
||||||
setCollapsed(new Set())
|
setCollapsed(new Set())
|
||||||
@@ -78,6 +100,11 @@ export function PrFilesPanel({ issue }: PrFilesPanelProps) {
|
|||||||
|
|
||||||
const selectedFile = selectedPath ? fileByPath.get(selectedPath) : undefined
|
const selectedFile = selectedPath ? fileByPath.get(selectedPath) : undefined
|
||||||
|
|
||||||
|
const onModeChange = (next: DiffModeEnum) => {
|
||||||
|
setDiffMode(next)
|
||||||
|
postMessage({ type: 'pr-diff-mode/set', mode: modeToWire(next) })
|
||||||
|
}
|
||||||
|
|
||||||
// 所有 hook 必须在任何 early return 之前调用完,否则工单有无 PR 之间切换时
|
// 所有 hook 必须在任何 early return 之前调用完,否则工单有无 PR 之间切换时
|
||||||
// hook 数量变化会触发 React #300(表现为面板空白 / 渲染出错)。
|
// hook 数量变化会触发 React #300(表现为面板空白 / 渲染出错)。
|
||||||
if (!issue)
|
if (!issue)
|
||||||
@@ -147,9 +174,10 @@ export function PrFilesPanel({ issue }: PrFilesPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右栏:选中文件的内联 diff */}
|
{/* 右栏:选中文件的内联 diff;key=工单号 → 换工单清 scroll map */}
|
||||||
<div className="min-w-0 flex-1 overflow-hidden">
|
<div className="min-w-0 flex-1 overflow-hidden">
|
||||||
<FileDiffPane
|
<FileDiffPane
|
||||||
|
key={issueNumber ?? 'none'}
|
||||||
path={selectedPath}
|
path={selectedPath}
|
||||||
status={selectedFile?.status}
|
status={selectedFile?.status}
|
||||||
diff={selectedPath ? diffByPath[selectedPath] : undefined}
|
diff={selectedPath ? diffByPath[selectedPath] : undefined}
|
||||||
@@ -158,6 +186,8 @@ export function PrFilesPanel({ issue }: PrFilesPanelProps) {
|
|||||||
summary={selectedPath ? summariesByPath[selectedPath] : undefined}
|
summary={selectedPath ? summariesByPath[selectedPath] : undefined}
|
||||||
generating={!!selectedPath && generatingPath === selectedPath}
|
generating={!!selectedPath && generatingPath === selectedPath}
|
||||||
onGenerate={() => selectedPath && generateSummary(selectedPath, selectedFile?.previousFilename)}
|
onGenerate={() => selectedPath && generateSummary(selectedPath, selectedFile?.previousFilename)}
|
||||||
|
mode={diffMode}
|
||||||
|
onModeChange={onModeChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
* - 主题跟随 VS Code(body 的 vscode-light/dark class),MutationObserver 监听切换。
|
* - 主题跟随 VS Code(body 的 vscode-light/dark class),MutationObserver 监听切换。
|
||||||
* - 语法高亮用库自带(diffViewHighlight),色彩取库的 light/dark 主题——不会和 VS Code
|
* - 语法高亮用库自带(diffViewHighlight),色彩取库的 light/dark 主题——不会和 VS Code
|
||||||
* 主题 100% 一致,是本方案已知且可接受的小瑕疵。
|
* 主题 100% 一致,是本方案已知且可接受的小瑕疵。
|
||||||
|
* - 分栏/统一模式由父级持有(workspace 级);本组件只按 path 记 scrollTop,换文件可回位。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { FileDiff } from '../../hooks/usePrFiles'
|
import type { FileDiff } from '../../hooks/usePrFiles'
|
||||||
import { generateDiffFile } from '@git-diff-view/file'
|
import { generateDiffFile } from '@git-diff-view/file'
|
||||||
import { DiffModeEnum, DiffView } from '@git-diff-view/react'
|
import { DiffModeEnum, DiffView } from '@git-diff-view/react'
|
||||||
import { Check, Loader2, Sparkles } from 'lucide-react'
|
import { Check, Loader2, Sparkles } from 'lucide-react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { statusBadge, statusColorVar } from './fileTree'
|
import { statusBadge, statusColorVar } from './fileTree'
|
||||||
import '@git-diff-view/react/styles/diff-view.css'
|
import '@git-diff-view/react/styles/diff-view.css'
|
||||||
|
|
||||||
@@ -33,6 +34,10 @@ interface FileDiffPaneProps {
|
|||||||
generating: boolean
|
generating: boolean
|
||||||
/** 触发 deepseek 生成当前文件说明。 */
|
/** 触发 deepseek 生成当前文件说明。 */
|
||||||
onGenerate: () => void
|
onGenerate: () => void
|
||||||
|
/** 统一 / 分栏;由父级从 workspaceState 拉并持久化。 */
|
||||||
|
mode: DiffModeEnum
|
||||||
|
/** 用户切换模式。 */
|
||||||
|
onModeChange: (mode: DiffModeEnum) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 读当前 VS Code 主题明暗:body 带 vscode-light / vscode-high-contrast-light 即浅色,否则深色。 */
|
/** 读当前 VS Code 主题明暗:body 带 vscode-light / vscode-high-contrast-light 即浅色,否则深色。 */
|
||||||
@@ -53,9 +58,22 @@ function Centered({ children }: { children: React.ReactNode }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileDiffPane({ path, status, diff, confirmed, onToggleConfirm, summary, generating, onGenerate }: FileDiffPaneProps) {
|
export function FileDiffPane({
|
||||||
const [mode, setMode] = useState<DiffModeEnum>(DiffModeEnum.Split)
|
path,
|
||||||
|
status,
|
||||||
|
diff,
|
||||||
|
confirmed,
|
||||||
|
onToggleConfirm,
|
||||||
|
summary,
|
||||||
|
generating,
|
||||||
|
onGenerate,
|
||||||
|
mode,
|
||||||
|
onModeChange,
|
||||||
|
}: FileDiffPaneProps) {
|
||||||
const [theme, setTheme] = useState<'light' | 'dark'>(() => readThemeKind())
|
const [theme, setTheme] = useState<'light' | 'dark'>(() => readThemeKind())
|
||||||
|
const scrollRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
// 按文件路径记 scrollTop;随组件生命周期(key=issueNumber)重置。
|
||||||
|
const scrollByPath = useRef<Record<string, number>>({})
|
||||||
|
|
||||||
// 主题随 VS Code 实时切换。
|
// 主题随 VS Code 实时切换。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -88,6 +106,20 @@ export function FileDiffPane({ path, status, diff, confirmed, onToggleConfirm, s
|
|||||||
return file
|
return file
|
||||||
}, [path, diff, mode, theme])
|
}, [path, diff, mode, theme])
|
||||||
|
|
||||||
|
// path / 内容就绪后恢复该文件上次的滚动位置;DiffView 异步出高时再 rAF 补一次。
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const el = scrollRef.current
|
||||||
|
if (!el || !path)
|
||||||
|
return
|
||||||
|
const top = scrollByPath.current[path] ?? 0
|
||||||
|
el.scrollTop = top
|
||||||
|
const id = requestAnimationFrame(() => {
|
||||||
|
if (scrollRef.current)
|
||||||
|
scrollRef.current.scrollTop = top
|
||||||
|
})
|
||||||
|
return () => cancelAnimationFrame(id)
|
||||||
|
}, [path, diffFile])
|
||||||
|
|
||||||
if (!path)
|
if (!path)
|
||||||
return <Centered>选择左侧文件查看改动</Centered>
|
return <Centered>选择左侧文件查看改动</Centered>
|
||||||
|
|
||||||
@@ -141,7 +173,7 @@ export function FileDiffPane({ path, status, diff, confirmed, onToggleConfirm, s
|
|||||||
<button
|
<button
|
||||||
key={label}
|
key={label}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMode(m)}
|
onClick={() => onModeChange(m)}
|
||||||
className={`px-1.5 py-0.5 transition-colors ${
|
className={`px-1.5 py-0.5 transition-colors ${
|
||||||
mode === m
|
mode === m
|
||||||
? 'bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)]'
|
? 'bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)]'
|
||||||
@@ -160,8 +192,15 @@ export function FileDiffPane({ path, status, diff, confirmed, onToggleConfirm, s
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 内容区 */}
|
{/* 内容区:按 path 记 scrollTop,换文件再回来可恢复 */}
|
||||||
<div className="min-h-0 flex-1 overflow-auto bg-[var(--vscode-editor-background)] text-xs">
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
className="min-h-0 flex-1 overflow-auto bg-[var(--vscode-editor-background)] text-xs"
|
||||||
|
onScroll={(e) => {
|
||||||
|
if (path)
|
||||||
|
scrollByPath.current[path] = e.currentTarget.scrollTop
|
||||||
|
}}
|
||||||
|
>
|
||||||
{diff === undefined
|
{diff === undefined
|
||||||
? <Centered>加载中…</Centered>
|
? <Centered>加载中…</Centered>
|
||||||
: diff.error
|
: diff.error
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ export type ExtensionToWebview
|
|||||||
| { type: 'pr-files/show', issueNumber: number, files: PrFile[], confirmed: string[], summaries: Record<string, string>, error?: string }
|
| { type: 'pr-files/show', issueNumber: number, files: PrFile[], confirmed: string[], summaries: Record<string, string>, error?: string }
|
||||||
| { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string }
|
| { type: 'pr-file-diff/show', issueNumber: number, path: string, oldContent: string, newContent: string, oldLang?: string, newLang?: string, error?: string }
|
||||||
| { type: 'pr-file-summary/show', issueNumber: number, path: string, summary?: string, error?: string }
|
| { type: 'pr-file-summary/show', issueNumber: number, path: string, summary?: string, error?: string }
|
||||||
|
| { type: 'pr-diff-mode/show', mode: 'unified' | 'split' }
|
||||||
// profile 值编辑器粘贴的图片:存盘/读盘的回执,requestId 关联并发请求。
|
// profile 值编辑器粘贴的图片:存盘/读盘的回执,requestId 关联并发请求。
|
||||||
| { type: 'profile-asset/saved', requestId: string, assetPath?: string, error?: string }
|
| { type: 'profile-asset/saved', requestId: string, assetPath?: string, error?: string }
|
||||||
| { type: 'profile-asset/loaded', requestId: string, dataUrl?: string, error?: string }
|
| { type: 'profile-asset/loaded', requestId: string, dataUrl?: string, error?: string }
|
||||||
@@ -209,6 +210,8 @@ export type WebviewToExtension
|
|||||||
| { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string }
|
| { type: 'pr-file-diff/get', issueNumber: number, path: string, previousPath?: string }
|
||||||
| { type: 'pr-file-summary/generate', issueNumber: number, path: string, previousPath?: string }
|
| { type: 'pr-file-summary/generate', issueNumber: number, path: string, previousPath?: string }
|
||||||
| { type: 'pr-review/set', issueNumber: number, confirmed: Record<string, string> }
|
| { type: 'pr-review/set', issueNumber: number, confirmed: Record<string, string> }
|
||||||
|
| { type: 'pr-diff-mode/get' }
|
||||||
|
| { type: 'pr-diff-mode/set', mode: 'unified' | 'split' }
|
||||||
// profile 值编辑器:图片存盘(base64→磁盘)与读盘(磁盘→dataUrl)请求。
|
// profile 值编辑器:图片存盘(base64→磁盘)与读盘(磁盘→dataUrl)请求。
|
||||||
| { type: 'profile-asset/save', requestId: string, base64: string, mediaType: string }
|
| { type: 'profile-asset/save', requestId: string, base64: string, mediaType: string }
|
||||||
| { type: 'profile-asset/load', requestId: string, assetPath: string }
|
| { type: 'profile-asset/load', requestId: string, assetPath: string }
|
||||||
|
|||||||
Reference in New Issue
Block a user