✨ feat(vscode): 只有从「未提交的更改」点开的预览才对比上一次提交

This commit is contained in:
cruldra committed 2026-10-12 15:52:35 +11:00
1 parent 4ec2b9d5d6
commit 3c7513b7cd
18 files changed
+366 -143

No files matched your search

+7 -2
View File
@@ -72,7 +72,7 @@ export function App(): ReactElement | null {
const [keep, setKeep] = useState<ReadonlySet<string>>(NONE)
/** 页面上这个文档版本的中文名:文档带来的,扩展端查到了再换 */
const [names, setNames] = useState<VersionNames | undefined>()
/** 这份文件相比对比基准改了什么:审查工作副本里的、工作区仓库里有改动的文件扩展端算好了补来,不比时是 null */
/** 这份文件相比对比基准改了什么:审查工作副本里的、从「未提交的更改」点开的有改动的文件扩展端算好了补来,不比时是 null */
const [diff, setDiff] = useState<DocumentDiff | null>(null)
/** 收到消息时读眼下哪些块在编辑(消息回调只在挂载时注册一次) */
const editing = useRef(view.editing)
@@ -177,11 +177,16 @@ export function App(): ReactElement | null {
case 'diff':
setDiff(msg.diff)
return
case 'openedFrom':
viewState.update({ openedFrom: msg.from })
return
case 'focusPathAsk':
setPathFocus(n => n + 1)
}
})
postMessage({ type: 'ready' })
// 重启 VS Code 后恢复的标签页,扩展端不知道它是从哪儿打开的,按页面记着的
const { openedFrom } = viewState.get()
postMessage({ type: 'ready', ...(openedFrom && { openedFrom }) })
return off
}, [])
+8 -2
View File
@@ -448,6 +448,9 @@ export interface StickyScrollSettings {
maxLineCount: number
}
/** 这个标签页是从哪儿打开的:从侧栏「未提交的更改」一栏点开的是 `uncommitted`,别处打开的是 `other`;页面只记着,重启 VS Code 后恢复时报给扩展端。 */
export type OpenedFrom = 'uncommitted' | 'other'
/**
* reveal:已经开着的预览滚到某处(同 DocumentMessage.reveal)
* definitionProbed:id 那次 probeDefinition 的回答,found 表示查得到定义
@@ -464,7 +467,8 @@ export interface StickyScrollSettings {
* rewriteEnd:askId 那一轮重写结束了,见 RewriteEnd:done 是模型答完、记下了(没写进文件时带原因);failed、stopped 没记,文件没改
* moveOccurrence:跳到高亮着的名字的下一处(step 1)/ 上一处(step -1)出现
* displayNames:version 这份文档里显示成中文名的名字,查到一步来一步;不是页面上这个版本的不理
* diff:这份文件相比对比基准改了什么(审查工作副本里的、工作区仓库里有改动的文件才有);每来一份文档之后补来,基准变了再来;null 是不比了
* diff:这份文件相比对比基准改了什么(审查工作副本里的、从「未提交的更改」点开的有改动的文件才有);每来一份文档之后补来,基准变了、换了从哪儿打开再来;null 是不比了
* openedFrom:这个标签页是从哪儿打开的,记进页面状态;ready 后来一次,开着时又被点开再来
* repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用
* repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到
* repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了
@@ -530,6 +534,7 @@ export type ExtensionToWebview
| { type: 'rewriteProgress', askId: string, question: string, text: string, activity?: string, queued?: true }
| RewriteEnd
| { type: 'diff', diff: DocumentDiff | null }
| { type: 'openedFrom', from: OpenedFrom }
| { type: 'repoLinksChecked', id: number, missing: string[], unresolved: string[] }
| { type: 'repoLinkBroken', path?: string, symbol?: string }
| { type: 'repoLinkOpened', path?: string, symbol?: string }
@@ -539,6 +544,7 @@ export type ExtensionToWebview
| { type: 'callTree', version: number, anchor: string, name: string, where: string, trees: CallTree[], contractMiss?: ContractMiss }
/**
* ready:页面准备好了,可以收消息;openedFrom 是页面状态里记着的这个标签页是从哪儿打开的,新开的页面没记过就不带
* edit:把 startLine..endLine 这几行整行替换成 text(扩展端给每行补回原来第一行的缩进)
* visibleLine:视口顶部那块的起始行,切回文本编辑器时扩展端把这行放到顶部
* probeDefinition:按住 ⌘ / Ctrl 指到带下标的名字上,问 offset 处有没有定义;id 标出是哪一次问,回答带回来;
@@ -568,7 +574,7 @@ export type ExtensionToWebview
* goToCallNode:点了调用树上的一个节点,打开 uri 那个文件,跳到 line 行 character 列(见 CallTreeNode.jump)
*/
export type WebviewToExtension
= | { type: 'ready' }
= | { type: 'ready', openedFrom?: OpenedFrom }
| { type: 'setFilter', excluded: Excluded }
| { type: 'setOnlyChanges', on: boolean }
| { type: 'edit', startLine: number, endLine: number, text: string }
@@ -62,6 +62,14 @@ describe('webview 状态', () => {
expect(createViewStateStore(memoryStorage(null)).get()).toEqual({ editing: [], folds: {} })
})
it('记着这个标签页是从哪儿打开的,页面重新加载后取回;认不得的值当没记过', () => {
const storage = memoryStorage()
createViewStateStore(storage).update({ openedFrom: 'uncommitted' })
expect(createViewStateStore(storage).get()).toEqual({ editing: [], folds: {}, openedFrom: 'uncommitted' })
expect(createViewStateStore(memoryStorage({ openedFrom: 'other' })).get()).toEqual({ editing: [], folds: {}, openedFrom: 'other' })
expect(createViewStateStore(memoryStorage({ openedFrom: 'files' })).get()).toEqual({ editing: [], folds: {} })
})
it('属性行的折叠 id 是块 id 加行序号,不会和块 id 撞上', () => {
expect(propertyRowId('6.1.0', 3)).toBe('6.1.0/p3')
expect(propertyRowId('6', 1)).not.toBe('6.1')
+11 -3
View File
@@ -1,15 +1,18 @@
import type { ScrollAnchor } from './scrollPosition'
import type { BlockView } from './types'
import type { BlockView, OpenedFrom } from './types'
/**
* 标签页切走时 VS Code 会销毁 webview、切回来重新加载,靠它恢复:哪些块停在「编辑」、
* 用户把哪些块、属性行和子行折叠 / 展开了(块 id 是它在树里的位置,文档刷新后照旧对得上;属性行、子行 id 见 propertyRowId、propertyItemId),以及滚到了哪。
* 用户把哪些块、属性行和子行折叠 / 展开了(块 id 是它在树里的位置,文档刷新后照旧对得上;属性行、子行 id 见 [propertyRowId]、[propertyItemId]),以及滚到了哪。
* VS Code 重启后恢复标签页时也带着它,这个标签页是从哪儿打开的靠它报回扩展端。
*/
export interface ViewState {
editing: string[]
/** 用户自己点过折叠箭头的块、属性行和子行:id → 折叠着没有。没点过的按默认,见 collapsedIds */
folds: Record<string, boolean>
scroll?: ScrollAnchor
/** 这个标签页是从哪儿打开的,扩展端告诉的;新开的页面还没记 */
openedFrom?: OpenedFrom
}
export interface ViewStateStorage {
@@ -51,7 +54,12 @@ function readViewState(saved: unknown): ViewState {
const folds = isRecord(state.folds)
? Object.fromEntries(Object.entries(state.folds).filter((entry): entry is [string, boolean] => typeof entry[1] === 'boolean'))
: {}
return isScrollAnchor(state.scroll) ? { editing, folds, scroll: state.scroll } : { editing, folds }
return {
editing,
folds,
...(isScrollAnchor(state.scroll) && { scroll: state.scroll }),
...((state.openedFrom === 'uncommitted' || state.openedFrom === 'other') && { openedFrom: state.openedFrom }),
}
}
/**