✨ feat(vscode): 预览里代码的上色和卡片种类色跟着当前颜色主题走

This commit is contained in:
cruldra committed 2026-10-13 11:27:24 +11:00
1 parent 19302bbed0
commit dff38ee0c5
26 files changed
+1286 -75

No files matched your search

+4
View File
@@ -12,6 +12,7 @@ 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 }
@@ -138,6 +139,8 @@ export interface StickyScrollSettings {
* stickyScroll:VS Code 的 Sticky Scroll 设置(按这份文件取,语言自己的设置优先),见 StickyScrollSettings;ready 后发一次,设置改了再发
* occurrencesFound:id 那次 findOccurrences 的回答,语言扩展没答(或没装)就是空的
* filter:整个工作区共用的块筛选和「只看改动」开关;ready 后先于第一份文档发一次,之后任何一个预览改了都发给所有预览
* codeColors:当前颜色主题(叠上用户设置里改的)给代码的配色,见 CodeColors,页面设成词性、种类的颜色变量;
* ready 后先于第一份文档发一次,换了颜色主题、改了代码配色的设置再发给所有预览
* qa:这份文件的问答;ready 后发一次,问答、正在答的问题变了再发
* profiles:kind 那种对话输入框旁边的 profile 下拉:选项和选中的那项;ready 后两种各发一次,任何一个预览改了哪种就把哪种发给所有预览
* answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事
@@ -170,6 +173,7 @@ export type ExtensionToWebview
| { type: 'moveOccurrence', 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 }
@@ -3,6 +3,7 @@ import type { DiffBase, OpenedFrom } from '../diff/diffBase'
import type { ProfileChoice } from '../qa/askProfiles'
import type { RewriteRequest } from '../qa/codeQa'
import type { SymbolNode } from '../symbolLinks'
import type { CodeColors } from '../theme/codeColors'
import type { ExtensionToWebview, WebviewToExtension } from './messages'
import { describe, expect, it, vi } from 'vitest'
import { window, workspace } from 'vscode'
@@ -62,6 +63,7 @@ function preview(rewrite: (uri: unknown, request: RewriteRequest) => Promise<str
{},
{},
{ subscribe },
{ subscribe },
async () => [],
qa,
async () => [],
@@ -162,8 +164,27 @@ function profileSet(selected: string) {
}
}
const COLORS = { roles: { keyword: '#C586C0' }, kinds: { loop: '#C586C0' } } as unknown as CodeColors
/** 共用的代码配色:眼下是 colors,publish 模拟换了颜色主题、通知订阅的预览 */
function themeColors(colors: CodeColors) {
const listeners = new Set<(value: CodeColors) => void>()
let current = colors
return {
current: async () => current,
subscribe: (listener: (value: CodeColors) => void) => {
listeners.add(listener)
return { dispose: () => listeners.delete(listener) }
},
publish: (value: CodeColors) => {
current = value
listeners.forEach(listener => listener(value))
},
}
}
/** 对比基准只认从「未提交的更改」点开的:从那儿点开的对比上一次提交,别处打开的不比 */
function opening(profiles = { ask: profileSet(''), rewrite: profileSet('') }) {
function opening(profiles = { ask: profileSet(''), rewrite: profileSet('') }, colors = themeColors(COLORS)) {
const subscribe = () => ({ dispose: () => {} })
const diffBase = vi.fn((_uri: unknown, from: OpenedFrom): DiffBase | undefined => from === 'uncommitted' ? HEAD_BASE : undefined)
const qa = {
@@ -179,6 +200,7 @@ function opening(profiles = { ask: profileSet(''), rewrite: profileSet('') }) {
{},
{},
{ subscribe, current: { excluded: {}, onlyChanges: false } },
colors,
async () => [],
qa,
async () => [],
@@ -314,6 +336,28 @@ describe('预览对比什么看这个标签页是从哪儿打开的', () => {
})
})
describe('代码配色跟着当前颜色主题', () => {
it('ready 后先于第一份文档发;换了主题发给 ready 了的预览,还没 ready 的等 ready 时发眼下的', async () => {
const colors = themeColors(COLORS)
const { provider, document } = opening(undefined, colors)
const page = tab()
provider.resolveCustomTextEditor(document as never, page.panel)
const light = { roles: { keyword: '#AF00DB' }, kinds: { loop: '#AF00DB' } } as unknown as CodeColors
colors.publish(light)
expect(page.posted()).toEqual([])
await page.send({ type: 'ready' })
const types = page.posted().map(message => message.type)
expect(page.posted()).toContainEqual({ type: 'codeColors', ...light })
expect(types.indexOf('codeColors')).toBeGreaterThanOrEqual(0)
expect(types.indexOf('codeColors')).toBeLessThan(types.indexOf('document'))
page.clear()
colors.publish(COLORS)
expect(page.posted()).toEqual([{ type: 'codeColors', ...COLORS }])
})
})
describe('问 AI 和重写注释的 profile 下拉各管各的', () => {
it('ready 后两套各发一次;哪一套变了只发哪一套', async () => {
const profiles = { ask: profileSet('sonnet'), rewrite: profileSet('dsflash') }
@@ -12,6 +12,7 @@ import type { CodeReaderRegistry } from '../registry'
import type { LookupSources } from '../ruleLookups'
import type { TreeSitterRuntime } from '../runtime'
import type { ImportedName, LinkTarget, SymbolNode } from '../symbolLinks'
import type { ThemeColors } from '../theme/codeColors'
import type { BlockFilter } from './blockFilter'
import type { FoundLocation } from './definitions'
import type { ChatKind, DefinitionPeek, ExtensionToWebview, Occurrence, Reveal, SymbolReference, WebviewToExtension } from './messages'
@@ -82,6 +83,8 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
private readonly registry: CodeReaderRegistry,
private readonly runtime: TreeSitterRuntime,
private readonly filter: BlockFilter,
/** 代码配色:各预览共用当前颜色主题的那一份,换了主题、改了代码配色的设置时发给订阅的 */
readonly codeColors: Pick<ThemeColors, 'current' | 'subscribe'>,
/** 库规则标的引用去哪儿查定义(交给标它的规则) */
readonly resolveReference: (reference: RuleReference) => Promise<RuleLocation[]>,
/** 问 AI:各预览共用,同一份文件开在几处时问答、正在答的问题都一样 */
@@ -284,6 +287,11 @@ class Preview {
if (this.ready)
this.post({ type: 'filter', ...state })
}),
// 同上:没 ready 的等 ready 时在第一份文档之前发眼下的
owner.codeColors.subscribe((colors) => {
if (this.ready)
this.post({ type: 'codeColors', ...colors })
}),
// 项目的入口变了:重新发文档,卡片左边的入口图标跟着变
owner.entries.onDidChange((uris) => {
if (uris.some(uri => uri.toString() === document.uri.toString()))
@@ -605,6 +613,8 @@ class Preview {
this.postQa()
for (const kind of CHAT_KINDS)
void this.postProfiles(kind)
// 也先于文档发:第一份文档一画出来就是主题的颜色,不会先没颜色再闪一下
this.post({ type: 'codeColors', ...await this.owner.codeColors.current() })
await this.push()
if (this.pendingPathFocus)
this.focusPathAsk()