/** * 代码阅读预览与扩展之间的消息,与 webview 端 `webview-ui/src/codeReader/types.ts` 同构,两边手动保持一致。 */ import type { ClaudeImage } from '../../cc/spawnClaude' import type { FileAttribute } from '../attributes' import type { BlockSlot, CallTreeTarget, ChainSlot, CommentTarget, SourceColors } from '../blockView' import type { ContractMiss } from '../callTree/endpoint' import type { CallTree } from '../callTree/model' import type { BlockAttributes, BlockConcept, KindBadge, Markdown, PreviewTable, PreviewText, PropertyItem, PropertyRow, SourceRange, ThemeColor } from '../contract' import type { OpenedFrom } from '../diff/diffBase' import type { DocumentDiff } from '../diff/previewDiff' import type { ShownName } from '../nameResolution' import type { ProfileChoice } from '../qa/askProfiles' import type { AskRound } from '../qa/asks' import type { CodeColors } from '../theme/codeColors' import type { Excluded, FilterState } from './blockFilter' export type { Excluded, ShownName } export interface TranslatorOption { id: string, label: string } /** * 注释里一个不带路径的引用(`[Name]`、`[Class.member]`):symbol 是写的名字;block 是这段注释属于哪一块(块的 anchor), * 从那块代码的声明起一层层往外找(见 symbolLinks.ts)。不在哪块的注释里的没有 block,只从文件顶层找。 * file 是这段注释写在哪份文件里(uri,见 [DefinitionPeek] 的 `uri`):定义卡片里的注释带着卡片那份文件,按那份文件的块、导入找; * 页面上这份文档自己的注释没有 file。同一个名字写在不同块、不同文件的注释里,指到的可以不一样。 */ export interface SymbolReference { symbol: string block?: string file?: string } /** * 让预览滚到包含 line 的块。definition 只在跳到定义时有:定义处那个名字在源码里的下标(Segment.offset), * 预览展开把它折叠住的块和属性行,再让就是这处定义的那一块或属性表里那一行闪几下。 * endLine 只在跳到几行(line 到 endLine)时有:预览展开、闪包住这几行的那块。都没有是从文本编辑器切过来,只滚不闪。 */ export interface Reveal { line: number definition?: number endLine?: number } /** 文件里的几行:从 start 行到 end 行,1 起、两头都算,同链接里的 `#L起-L止`。 */ export interface LineRange { start: number end: number } /** * 点中的名字在这份文档里的一处出现:源码下标 [start, end)。kind 同 VS Code 的 DocumentHighlightKind: * write 是写入(定义、赋值),read 是读取,text 是语言扩展按文字认出来的。 */ export interface Occurrence { start: number end: number kind: 'text' | 'read' | 'write' } /** * 块在源码里占的地方,按相对的量记,前面的块变长变短、挪了位置它都不跟着变: * 离前一个兄弟块的结尾(第一个子块从父块的开头算,顶层第一块从文件开头算)隔几行(lineGap)、几个字符(gap), * 自己占几行(lines,首尾两行的行号差)、几个字符(length,第一行行首到最后一行行尾)。块的开头是它第一行的行首。 */ export interface BlockSpan { lineGap: number gap: number lines: number length: number } /** * 页面上一张卡片画的内容(发给页面的样子,见 pageCard):同 BlockView 的这几样,块里的下标(名字的 offset、refers、defines) * 都从块的开头算;不带行号、头部的最后一行(扩展端自己用),「重写注释」只带是换掉还是新写。 */ interface PageCardBase { id: string anchor: string kind: string defines?: SourceRange attributes: BlockAttributes concepts: BlockConcept[] commentTarget?: CommentTarget['kind'] badge?: KindBadge tint?: ThemeColor slot?: BlockSlot step?: ChainSlot callTree?: CallTreeTarget } export type PageCard = PageCardBase & ({ translation: PreviewText, items?: PropertyItem[], table?: PreviewTable, doc?: Markdown, rows?: PropertyRow[] } | { properties: PropertyRow[] }) /** * 发给页面的一块(见 pageBlocks):卡片的内容,在源码里占的地方(相对的,见 BlockSpan),子块。内容没变、只是挪了位置的块,发出去一模一样。 * hash 是这些(连子块)的内容指纹:页面按它认出没变的块,不用逐块比内容。 */ export type PageBlock = PageCard & { span: BlockSpan hash: string children: PageBlock[] } /** * 悬停名字、注释里的链接时浮窗里的定义卡片:定义所在的文件(path 是给人看的、从工作区文件夹算起的相对路径,同挑选框; * uri 是这份文件本身,卡片里注释中不带路径的引用带着它,按这份文件找,见 [SymbolReference])、定义处那一行(0 起), * 盖住定义处的那块(连同子块,和那个文件的预览画的是同一张卡片;它自己当那个文件的顶层第一块,span 从文件开头算), * 显示成中文名的名字(下标是那个文件里的)。 */ export interface DefinitionPeek { path: string uri: string line: number block: PageBlock names: ShownName[] /** 那块自己下面问过 AI 的各次,同那块下面的对话、按时间先后(子块上问的不算);没问过时没有 */ qa?: QaEntryView[] } export interface DocumentMessage { type: 'document' /** TextDocument.version:webview 靠它判断是不是新一份文档 */ version: number languageId: string blocks: PageBlock[] /** 整份源码和它的着色,只这一份:编辑框里一块的源码、着色按它的编辑范围取;解析失败、语言不支持时是空的(没有块) */ source: string colors: SourceColors /** 这份文件里出现过的筛选属性和值(带显示名、块数),按筛选栏的先后排,见 fileAttributes */ attributes: FileAttribute[] translators: TranslatorOption[] translatorId: string error?: string /** 新预览只在第一份 document 里带:从文本编辑器切过来时是它顶部那一行,跳到定义时是定义处 */ reveal?: Reveal /** 这个版本眼下已知的中文名:查过的,或上一版查到的跟着改动挪了位置的;查完再发 displayNames 换掉 */ names: ShownName[] } /** * 问过、重写过的一次(给预览看的):说的话、回答(Markdown;重写记录是交回的注释正文,没交回时是空的)、什么时候(ISO 时间)、用哪个 profile 答的。 */ export interface QaEntryView { id: string question: string answer: string createdAt: string /** 用哪个 profile 答的(profile 名);选「默认」时、以前存的没有 */ profile?: string /** 带的图片,预览里能直接用的地址(webview.asWebviewUri);没带图片时没有 */ images?: string[] /** 重写记录才有:没写进文件的原因 */ error?: string } /** 挂在一块上的问答或重写记录;记号对不上这份文件里任何一块的不显示。 */ export interface QaThreadView { anchor: string entries: QaEntryView[] } /** * 这份文件的问答和重写记录:available 为假时文件不在工作区文件夹里、没地方存,不给「问 AI」和「重写注释」; * current、rewriteCurrent 是当前这一轮:正在答的问题、排着队和正在重写的注释,和出错、被叫停没记下留着的(页面重新加载后靠它们接上)。 */ export interface QaMessage { type: 'qa' /** 能问 AI:问答有地方存(工作区外的库文件记在第一个工作区文件夹);没打开工作区文件夹时不能 */ available: boolean /** 能重写注释:只有工作区里的文件,工作区外的库文件不改 */ rewritable: boolean /** 块下面的问答,按块的记号 */ threads: QaThreadView[] /** 这一页顶部、按路径记的问答;不挂在块上 */ path: QaEntryView[] /** 顶部问的是这个文件、这个文件夹,还是「了解一块代码」找的一个功能 */ subject: 'file' | 'folder' | 'feature' current: AskRound[] /** 块下面的重写记录,按块的记号 */ rewrites: QaThreadView[] rewriteCurrent: AskRound[] } /** 预览里的两种对话:问 AI(ask,块下面和页面顶部)、重写注释(rewrite),各有各的记录和 profile 下拉 */ export type ChatKind = 'ask' | 'rewrite' /** * VS Code 的 Sticky Scroll 设置(editor.stickyScroll.enabled / editor.stickyScroll.maxLineCount):卡片的开头滚到预览顶上后面、卡片还有一部分看得见时把它钉在顶上, * 外层的在上、里层的在下,最多钉 maxLineCount 行。 */ export interface StickyScrollSettings { enabled: boolean maxLineCount: number } /** * reveal:已经开着的预览滚到某处(同 DocumentMessage.reveal) * definitionProbed:id 那次 probeDefinition 的回答,found 表示 VS Code 查得到定义 * definitionPeeked:id 那次 peekDefinition / peekLink 的回答。查不到定义、定义所在的语言没有代码阅读、没有块盖住定义处、 * 指着的名字就是定义本身时没有 peek,页面不弹浮窗 * hoverDelay:VS Code 的 editor.hover.delay / editor.hover.hidingDelay(毫秒),鼠标停多久弹定义卡片、移开后多久收;ready 后发一次,设置改了再发 * stickyScroll:VS Code 的 Sticky Scroll 设置(按这份文件取,语言自己的设置优先),见 StickyScrollSettings;ready 后发一次,设置改了再发 * occurrencesFound:id 那次 findOccurrences 的回答,语言扩展没答(或没装)就是空的 * filter:整个工作区共用的块筛选和「只看改动」开关;ready 后先于第一份文档发一次,之后任何一个预览改了都发给所有预览 * codeColors:当前颜色主题(叠上用户设置里改的)给代码的配色,见 CodeColors,页面设成词性、种类的颜色变量; * ready 后先于第一份文档发一次,换了颜色主题、改了代码配色的设置再发给所有预览 * qa:这份文件的问答;ready 后发一次,问答、正在答的问题变了再发 * profiles:kind 那种对话输入框旁边的 profile 下拉:选项和选中的那项;ready 后两种各发一次,任何一个预览改了哪种就把哪种发给所有预览 * answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事 * answerEnd:askId 那个问题答完了:done 带最终的回答和存下的那条的 id,failed 带给人看的原因,stopped 是被叫停 * rewriteProgress:askId 那一轮重写排上队了(queued)、开始了、正文长了(text 是到现在为止写出来的)、或换了在做的事;question 是记在这一轮上的话(点按钮时是那句技能命令) * rewriteEnd:askId 那一轮重写结束了:done 是模型答完、这一轮记下了,带交回的正文(text)、存下的那条的 id,没写进文件时带原因(error); * failed 带给人看的原因,stopped 是被叫停,这两种都没记、文件没改 * moveOccurrence:跳到高亮着的名字的下一处(step 1)/ 上一处(step -1)出现,由这两条命令发给活动的预览 * openFind:打开查找栏,由「查找」命令发给活动的预览 * moveFind:查找栏里跳到下一处(step 1)/ 上一处(step -1)命中,由「查找下一处 / 上一处」命令发给查找栏开着的预览 * displayNames:version 这份文档里显示成中文名的名字,查到一步发一步(先本文件定义的,再别的文件定义的);不是页面上这个版本的不理 * diff:这份文件相比对比基准改了什么(审查工作副本里的、用「未提交的更改」文件行上的「打开更改」打开的有改动的才有,见 [对比基准](vscode/src/codeReader/diff/diffBase.ts#DiffBase)):每发一份文档之后算好了补发, * 基准变了(提交后 HEAD 动了、换了审查的工单、文件有了改动、这个标签页换了从哪儿打开)再发;null 是不比了(退出了审查、改动提交了、从别处点开了) * openedFrom:这个标签页是从哪儿打开的,页面记进自己的状态,VS Code 重启后恢复这个标签页时随 ready 报回来;ready 后发一次,开着时又从侧栏点开、换了从哪儿打开的再发 * repoLinksChecked:id 那次 checkRepoLinks 的回答,missing 是仓库里没有的路径,unresolved 是找不到的不带路径的引用, * builtin 是语言自带、又问不到定义的不带路径的引用(Python 的 `ValueError`、`str`,页面当普通文字);两样都原样带回问的那几条 * repoLinkBroken:这个仓库链接打不开。没有 symbol 是文件或目录没有;有 symbol 是文件在、这个符号没有;没有 path 是不带路径的引用找不到, * block、file 同 openRepoLink * repoLinkBuiltin:点的不带路径的引用是语言自带、又问不到定义的名字,没打开,页面当普通文字;symbol、block、file 同 openRepoLink * repoLinkOpened:这个仓库链接打开了。路径在;带了 symbol 时这个符号也在;没有 path 是不带路径的引用找到了,block、file 同 openRepoLink * repoImagesChecked:id 那次 checkRepoImages 的回答。src 是预览里能直接用的图片地址;没有 src 是文件不在 * callTreeCounts:version 这份文档里各个调用树的次数(只包括大于 0 的),按调用树的键:能看调用树的块(BlockView.callTree)是块的记号, * 落在几棵调用树里;卡片属性表里的操作([看调用树的操作](vscode/src/codeReader/contract.ts#CallTreeAction))是它的键,按它的查法算次数。不是页面上这个版本的不理 * callTree:键是 key 的那个调用树(同 callTreeCounts)。trees 空着、又没有 contractMiss 是没有调用它的地方;接口卡片在契约文件里没对上时带 contractMiss。 * 不是页面上这个版本的不理 */ export type ExtensionToWebview = | DocumentMessage | ({ type: 'reveal' } & Reveal) | { type: 'definitionProbed', id: number, found: boolean } | { type: 'definitionPeeked', id: number, peek?: DefinitionPeek } | { type: 'hoverDelay', delay: number, hidingDelay: number } | ({ type: 'stickyScroll' } & StickyScrollSettings) | { type: 'occurrencesFound', id: number, occurrences: Occurrence[] } | { type: 'moveOccurrence', step: 1 | -1 } | { type: 'openFind' } | { type: 'moveFind', step: 1 | -1 } | { type: 'displayNames', version: number, names: ShownName[] } | ({ type: 'filter' } & FilterState) | ({ type: 'codeColors' } & CodeColors) | QaMessage | ({ type: 'profiles', kind: ChatKind } & ProfileChoice) | { type: 'answer', askId: string, text: string, activity?: string } | { type: 'answerEnd', askId: string, status: 'done' | 'failed' | 'stopped', text?: string, error?: string, entryId?: string } | { type: 'rewriteProgress', askId: string, question: string, text: string, activity?: string, queued?: true } | { type: 'rewriteEnd', askId: string, status: 'done' | 'failed' | 'stopped', text?: string, error?: string, entryId?: string } | { type: 'diff', diff: DocumentDiff | null } | { type: 'openedFrom', from: OpenedFrom } | { type: 'repoLinksChecked', id: number, missing: string[], unresolved: SymbolReference[], builtin: SymbolReference[] } | { type: 'repoLinkBroken', path?: string, symbol?: string, block?: string, file?: string } | ({ type: 'repoLinkBuiltin' } & SymbolReference) | { type: 'repoLinkOpened', path?: string, symbol?: string, block?: string, file?: string } | { type: 'repoImagesChecked', id: number, images: { path: string, src?: string }[] } | { type: 'focusPathAsk' } | { type: 'callTreeCounts', version: number, counts: Record } | { type: 'callTree', version: number, key: string, name: string, where: string, trees: CallTree[], contractMiss?: ContractMiss } /** * ready:页面准备好了,可以收消息;openedFrom 是页面记着的这个标签页是从哪儿打开的,新开的页面没记过就没有 * edit:把 startLine..endLine 这几行整行替换成 text(扩展端给每行补回原来的缩进) * visibleLine:预览视口顶部那块的起始行,切回文本编辑器时把这行放到顶部 * probeDefinition:按住 ⌘ / Ctrl 指到带下标的名字上,问源码 offset 处有没有定义;id 标出是哪一次问,回答带回去; * version 是这份下标所属的文档版本 * goToDefinition:⌘ / Ctrl + 点击,跳到 offset 处名字的定义 * peekDefinition:鼠标停在带下标的名字上,要 offset 处名字的定义卡片(几处定义取第一处);id、version 同 probeDefinition * peekLink:鼠标停在注释里指到符号的链接上(带 #符号 的仓库链接、不带路径的引用),要那个符号的定义卡片;path、symbol、block、file 同 openRepoLink * findOccurrences:单击带下标的名字,问它在这份文档里出现在哪几处;id、version 同 probeDefinition * setFilter:改块筛选(整份替换),扩展端记下后发 filter 给所有预览 * setOnlyChanges:打开 / 关掉「只看改动」,扩展端记下后发 filter 给所有预览 * ask:问 anchor 那块代码;askId 由 webview 起,回答的消息带回去;扩展端接着这块最近一次问答的会话问,用问 AI 眼下选的 profile; * images 是贴进输入框的图片(没贴是空的),随问题发给 Claude Code,答成了跟这条一起存 * rewriteComment:按注释规范重写 anchor 那块的注释(还没有就新写一段);askId 由 webview 起,过程用 rewriteProgress / rewriteEnd 带回去。 * 没有 request 是点了按钮:发技能命令,开新会话;有 request 是在输入框里说的修改意见,接着这块最近一次重写的会话说,images 是贴的图片 * stopRewrite:叫停 askId 那一轮重写,不写文件、不记 * forgetRound:页面上收起了 anchor 那块的问答(ask)或重写(rewrite),扩展端去掉这块留着的那一轮(出错、被叫停的) * deleteRewrite:删掉 anchor 那块下面的一条重写记录 * stopAsk:叫停 askId 那个问题,这次不存 * deleteQa:删掉 anchor 那块下面的一次问答(已经理解了) * selectProfile:kind 那种对话换 profile(id 是 profile 名,「默认」是 ''),扩展端记下后把这种的 profiles 发给所有预览 * occurrenceNavigation:眼下能不能用键盘在高亮着的名字的各处出现之间跳(有高亮、页面有焦点、焦点不在输入框里),变了才发; * 扩展端按它设 when 子句用的上下文键 * findNavigation:眼下查找栏的快捷键归不归这个页面(查找栏开着、页面有焦点),变了才发;扩展端按它设 when 子句用的上下文键 * openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,lines 是文件里的几行(和 symbol 二选一), * 都没有就只打开文件或在代码阅读侧栏的树上定位目录; * 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),按它写在的那份文件找 symbol:file 是那份文件(定义卡片里的注释才有,没有是这份文件), * block 是它写在哪块的注释里(见 SymbolReference),从那块的声明起一层层往外找那份文件定义的,再是导入的(见 symbolLinks.ts) * checkRepoLinks:预览里这批仓库路径问哪些文件或目录没有,这批不带路径的引用(references)问哪些找不到。id 标出是哪一次问 * checkRepoImages:预览里这批仓库图片,问哪些文件在,在的要预览里能直接用的地址。id 标出是哪一次问 * openCallTree:打开键是 key 的调用树(块的记号,或卡片上一个操作的键,同 callTreeCounts)。version 是它所在的文档版本,对不上就不查 * goToCallNode:点了调用树上的一个节点,打开 uri 那个文件,跳到 line 行 character 列(都从 0 起,见 CallTreeNode.jump) */ export type WebviewToExtension = | { type: 'ready', openedFrom?: OpenedFrom } | { type: 'setFilter', excluded: Excluded } | { type: 'setOnlyChanges', on: boolean } | { type: 'edit', startLine: number, endLine: number, text: string } | { type: 'rewriteComment', askId: string, anchor: string, request?: string, images: ClaudeImage[] } | { type: 'stopRewrite', askId: string } | { type: 'deleteRewrite', anchor: string, entryId: string } | { type: 'forgetRound', kind: ChatKind, anchor: string } | { type: 'selectTranslator', id: string } | { type: 'visibleLine', line: number } | { type: 'probeDefinition', id: number, version: number, offset: number } | { type: 'goToDefinition', version: number, offset: number } | { type: 'peekDefinition', id: number, version: number, offset: number } | { type: 'peekLink', id: number, path?: string, symbol: string, block?: string, file?: string } | { type: 'findOccurrences', id: number, version: number, offset: number } | { type: 'ask', askId: string, anchor: string, question: string, images: ClaudeImage[] } | { type: 'stopAsk', askId: string } | { type: 'deleteQa', anchor: string, entryId: string } | { type: 'selectProfile', kind: ChatKind, id: string } | { type: 'occurrenceNavigation', available: boolean } | { type: 'findNavigation', available: boolean } | { type: 'openRepoLink', path?: string, symbol?: string, block?: string, file?: string, lines?: LineRange } | { type: 'checkRepoLinks', id: number, paths: string[], references: SymbolReference[] } | { type: 'checkRepoImages', id: number, paths: string[] } | { type: 'openCallTree', version: number, key: string } | { type: 'goToCallNode', uri: string, line: number, character: number }