✨ feat(vscode): 新增「了解一块代码」:AI 找功能代码占一栏,对话里接着改

主栏标题栏的放大镜问要了解哪一块,占一个栏位建一栏(和收藏共用 30 个栏位,视图改名 slot1..30,按 slotKindN 分收藏 / 功能挂标题栏按钮),同时开对话页让 Claude 只读地在全部工作区文件夹里找,按 --json-schema 交回环节 → 文件 + 符号 + 一句话。
对话复用预览顶部的聊天(CodeQa.askTop:会话续接、进行中的一轮、记录存 workspaceState);结构不合格算这一轮没答成、留着能再画出来。
条目每次画、点时按眼下的代码找:有语言插件的解析成代码块按「类名.方法名」找,没有的问 VS Code 文档符号;找不到画成灰的。
This commit is contained in:
cruldra committed 2026-10-12 15:48:54 +11:00
1 parent 16d27004d8
commit 880bf7fc57
38 files changed
+3386 -378

No files matched your search

@@ -1,40 +1,48 @@
import type { Favorite } from './favoriteSlots'
import { describe, expect, it } from 'vitest'
import { FAVORITE_SLOTS, favoriteCommand, favoriteContext, favoriteViewId, placeFavorite, removeFavorite, renameFavorite } from './favoriteSlots'
import { SLOTS } from '../slots/slots'
import { favoriteCommand, placeFavorite, removeFavorite, renameFavorite } from './favoriteSlots'
const favorite = (slot: number, name = `收藏${slot}`): Favorite => ({ uri: `file:///ws/dir${slot}`, name, slot })
/** 栏位 1…count 全占着 */
const filled = (count: number): Favorite[] => Array.from({ length: count }, (_, index) => favorite(index + 1))
const slotsOf = (favorites: readonly Favorite[]): number[] => favorites.map(item => item.slot)
describe('新收藏占哪个栏位', () => {
it('没有收藏时占第 1 个', () => {
expect(placeFavorite([], 'file:///ws/a')).toEqual({ slot: 1 })
expect(placeFavorite([], 'file:///ws/a', [])).toEqual({ slot: 1 })
})
it('占没被占的里面最小的:中间空出来的先用', () => {
expect(placeFavorite([favorite(1), favorite(2)], 'file:///ws/a')).toEqual({ slot: 3 })
expect(placeFavorite([favorite(1), favorite(3), favorite(4)], 'file:///ws/a')).toEqual({ slot: 2 })
expect(placeFavorite([favorite(5)], 'file:///ws/a')).toEqual({ slot: 1 })
expect(placeFavorite([favorite(1), favorite(2)], 'file:///ws/a', [1, 2])).toEqual({ slot: 3 })
expect(placeFavorite([favorite(1), favorite(3), favorite(4)], 'file:///ws/a', [1, 3, 4])).toEqual({ slot: 2 })
})
it('和「了解一块代码」共用栏位:功能占着的跳过,功能删掉空出来的收藏照样能用', () => {
const favorites = [favorite(1)]
expect(placeFavorite(favorites, 'file:///ws/a', [1, 2, 3])).toEqual({ slot: 4 })
expect(placeFavorite(favorites, 'file:///ws/a', [1, 3])).toEqual({ slot: 2 })
})
it('取消收藏后空出来的栏位给下一个收藏用,别的收藏不挪位置', () => {
const left = removeFavorite([favorite(1), favorite(2), favorite(3)], 2)
expect(left).toEqual([favorite(1), favorite(3)])
expect(placeFavorite(left, 'file:///ws/a')).toEqual({ slot: 2 })
expect(placeFavorite(left, 'file:///ws/a', slotsOf(left))).toEqual({ slot: 2 })
})
it(`${FAVORITE_SLOTS.length} 个栏位都占满了:满了`, () => {
expect(FAVORITE_SLOTS).toEqual(Array.from({ length: 30 }, (_, index) => index + 1))
expect(placeFavorite(filled(FAVORITE_SLOTS.length), 'file:///ws/a')).toBe('full')
expect(placeFavorite(filled(FAVORITE_SLOTS.length - 1), 'file:///ws/a')).toEqual({ slot: FAVORITE_SLOTS.length })
it('栏位都被收藏和功能占满了:满了', () => {
const favorites = filled(10)
expect(placeFavorite(favorites, 'file:///ws/a', SLOTS)).toBe('full')
expect(placeFavorite(favorites, 'file:///ws/a', SLOTS.slice(0, -1))).toEqual({ slot: SLOTS.length })
})
it('同一个目录收藏过:给出原来那个,不再占栏位;满了也先认出收藏过', () => {
const favorites = [favorite(1), favorite(2)]
expect(placeFavorite(favorites, 'file:///ws/dir2')).toEqual({ existing: favorite(2) })
expect(placeFavorite(filled(FAVORITE_SLOTS.length), 'file:///ws/dir7')).toEqual({ existing: favorite(7) })
expect(placeFavorite(favorites, 'file:///ws/dir2', [1, 2])).toEqual({ existing: favorite(2) })
expect(placeFavorite(filled(SLOTS.length), 'file:///ws/dir7', SLOTS)).toEqual({ existing: favorite(7) })
})
})
@@ -49,10 +57,8 @@ describe('重命名、取消收藏', () => {
})
})
describe('栏位对应的视图、上下文键、命令', () => {
it('第 n 个栏位各有自己的视图 id、显示不显示的上下文键、标题栏上重命名 / 取消收藏的命令', () => {
expect(favoriteViewId(3)).toBe('superpowers.codeReader.favorite3')
expect(favoriteContext(3)).toBe('superpowers.codeReader.hasFavorite3')
describe('收藏栏标题栏上的命令', () => {
it('每个栏位各有重命名 / 取消收藏的命令', () => {
expect(favoriteCommand('rename', 3)).toBe('superpowers.codeReader.renameFavorite3')
expect(favoriteCommand('remove', 30)).toBe('superpowers.codeReader.removeFavorite30')
})
@@ -1,40 +1,26 @@
/**
* 收藏栏的栏位:VS Code 的视图要在 package.json 里预先声明,所以声明了固定这么多个收藏栏(superpowers.codeReader.favorite1 … favorite30),
* 有收藏占着的才显示。这里是栏位号 1、2、3…
*/
export const FAVORITE_SLOTS: readonly number[] = Array.from({ length: 30 }, (_, index) => index + 1)
import { freeSlot } from '../slots/slots'
/** 一个收藏:目录(Uri 字符串)、栏标题上的名字、占着第几个栏位。 */
/** 一个收藏:目录(Uri 字符串)、栏标题上的名字、占着第几个栏位(和「了解一块代码」共用,见 slots.ts)。 */
export interface Favorite {
uri: string
name: string
slot: number
}
/** 第 slot 个收藏栏的视图 id */
export function favoriteViewId(slot: number): string {
return `superpowers.codeReader.favorite${slot}`
}
/** 上下文键:第 slot 个栏位有收藏占着,这一栏才显示(package.json 里视图的 when 按它) */
export function favoriteContext(slot: number): string {
return `superpowers.codeReader.hasFavorite${slot}`
}
/** 第 slot 个收藏栏标题栏上的按钮命令:rename 重命名、remove 取消收藏。标题栏按钮不知道点的是哪一栏,所以每个栏位各一条。 */
export function favoriteCommand(action: 'rename' | 'remove', slot: number): string {
return `superpowers.codeReader.${action}Favorite${slot}`
}
/**
* 收藏 uri 这个目录要占哪个栏位:收藏过的给出原来那个(不重复收藏),栏位都占满了是 full,别的占没被占的里面最小的。
* 一个收藏一直占着它的栏位、删掉才空出来,用户拖动排过的位置 VS Code 按栏位记着,所以不会错乱。
* 收藏 uri 这个目录要占哪个栏位:收藏过的给出原来那个(不重复收藏),栏位都被收藏和功能占满了是 full,别的占空着的里面最小的。
* taken 是收藏、功能眼下占着的全部栏位。
*/
export function placeFavorite(favorites: readonly Favorite[], uri: string): { existing: Favorite } | { slot: number } | 'full' {
export function placeFavorite(favorites: readonly Favorite[], uri: string, taken: Iterable<number>): { existing: Favorite } | { slot: number } | 'full' {
const existing = favorites.find(favorite => favorite.uri === uri)
if (existing)
return { existing }
const slot = FAVORITE_SLOTS.find(slot => !favorites.some(favorite => favorite.slot === slot))
const slot = freeSlot(taken)
return slot === undefined ? 'full' : { slot }
}
@@ -1,31 +1,18 @@
import type { Memento, TreeView } from 'vscode'
import type { FileNode, FileTreeDecorator, TreeNode } from '../explorer/fileTree'
import type { SlotPool } from '../slots/slotPool'
import type { Favorite } from './favoriteSlots'
import * as path from 'node:path'
import { commands, FileType, Uri, window, workspace } from 'vscode'
import { FileType, Uri, window, workspace } from 'vscode'
import { FileTreeProvider } from '../explorer/fileTree'
import { TreeLocator } from '../explorer/treeLocator'
import { FAVORITE_SLOTS, favoriteContext, favoriteViewId, placeFavorite, removeFavorite, renameFavorite } from './favoriteSlots'
import { askSlotName, focusSlot } from '../slots/slotPool'
import { SLOTS, slotTitle, slotViewId } from '../slots/slots'
import { placeFavorite, removeFavorite, renameFavorite } from './favoriteSlots'
/** workspaceState:收藏的目录(Favorite 的数组),按工作区记。 */
const FAVORITES_KEY = 'superpowers.codeReader.favorites'
/** 弹顶部的输入框问收藏栏叫什么(原值整段选中);Esc 或点别处是不改。 */
async function askName(title: string, value: string): Promise<string | undefined> {
const name = await window.showInputBox({
title,
value,
prompt: '收藏栏标题上显示的名字',
validateInput: text => text.trim() ? undefined : '名字不能为空',
})
return name?.trim()
}
/** 亮出第 slot 个收藏栏(VS Code 给每个视图自带一条 `<视图 id>.focus` 命令)。 */
async function focus(slot: number): Promise<void> {
await commands.executeCommand(`${favoriteViewId(slot)}.focus`)
}
/**
* 一个收藏栏:这个目录下的完整文件树(自己的文件树和视图),标题是起的名字、后面淡色写目录相对工作区的路径;
* 当前文件在这个目录下、这一栏露着时跟过去定位。目录被删了时栏里说一句目录不在了。收藏时建、取消收藏时销毁。
@@ -40,8 +27,8 @@ class FavoritePane {
constructor(favorite: Favorite, openCommand: string, decorator: FileTreeDecorator) {
this.uri = Uri.parse(favorite.uri)
const files = new FileTreeProvider(openCommand, decorator, { kind: 'directory', uri: this.uri })
this.view = window.createTreeView(favoriteViewId(favorite.slot), { treeDataProvider: files, showCollapseAll: true })
this.view.title = favorite.name
this.view = window.createTreeView(slotViewId(favorite.slot), { treeDataProvider: files, showCollapseAll: true })
this.view.title = slotTitle('favorite', favorite.name)
this.view.description = workspace.asRelativePath(this.uri)
this.disposables = [
files,
@@ -57,7 +44,7 @@ class FavoritePane {
}
rename(name: string): void {
this.view.title = name
this.view.title = slotTitle('favorite', name)
}
dispose(): void {
@@ -74,7 +61,7 @@ class FavoritePane {
}
/**
* 侧栏的收藏栏:收藏一个目录占一个栏位,栏里是这个目录下的完整文件树,规则同主栏的工作区文件树、不受审查影响(栏位怎么分见 placeFavorite)。
* 侧栏的收藏栏:收藏一个目录占一个栏位(和「了解一块代码」共用,见 SlotPool),栏里是这个目录下的完整文件树,规则同主栏的工作区文件树、不受审查影响(栏位怎么分见 placeFavorite)。
* 只给有收藏占着的栏位建视图,上下文键让这一栏显示出来;收藏按工作区记住,打开窗口时照记着的建回来。
*/
export class FavoriteViews {
@@ -84,14 +71,16 @@ export class FavoriteViews {
/**
* @param state workspaceState
* @param pool 和「了解一块代码」共用的栏位
* @param openCommand 点文件时执行的命令,参数是文件 Uri
* @param decorator 给行添信息(文件注释),和别的栏共用
*/
constructor(private readonly state: Memento, private readonly openCommand: string, private readonly decorator: FileTreeDecorator) {
constructor(private readonly state: Memento, private readonly pool: SlotPool, private readonly openCommand: string, private readonly decorator: FileTreeDecorator) {
this.favorites = state.get<Favorite[]>(FAVORITES_KEY) ?? []
for (const favorite of this.favorites)
for (const favorite of this.favorites) {
this.open(favorite)
void Promise.all(this.favorites.map(favorite => commands.executeCommand('setContext', favoriteContext(favorite.slot), true)))
void pool.occupy(favorite.slot, 'favorite')
}
}
/** 目录行上的「收藏目录」:起个名字(默认是目录名)占一个空栏位并亮出那一栏;收藏过的说一声、亮出原来那一栏,满了说一声。 */
@@ -99,26 +88,23 @@ export class FavoriteViews {
if (!node)
return // 只挂在侧栏的目录行上,别处调用没有目标
const uri = node.uri.toString()
const placement = placeFavorite(this.favorites, uri)
const placement = placeFavorite(this.favorites, uri, this.pool.taken())
if (placement === 'full') {
void window.showWarningMessage(`收藏栏满了(最多 ${FAVORITE_SLOTS.length} 个),先取消一个再收藏`)
void window.showWarningMessage(`栏位满了(收藏和「了解一块代码」共用,最多 ${SLOTS.length} 个),先取消一个收藏或删掉一个功能再收藏`)
return
}
if ('existing' in placement) {
void window.showInformationMessage(`这个目录已经收藏过了:「${placement.existing.name}」`)
await focus(placement.existing.slot)
await focusSlot(placement.existing.slot)
return
}
const name = await askName('收藏目录', path.posix.basename(node.uri.path))
const name = await askSlotName('收藏目录', path.posix.basename(node.uri.path))
if (name === undefined)
return
const favorite: Favorite = { uri, name, slot: placement.slot }
this.open(favorite)
await Promise.all([
this.save([...this.favorites, favorite]),
commands.executeCommand('setContext', favoriteContext(favorite.slot), true),
])
await focus(favorite.slot)
await Promise.all([this.save([...this.favorites, favorite]), this.pool.occupy(favorite.slot, 'favorite')])
await focusSlot(favorite.slot)
}
/** 收藏栏标题栏的「重命名」:改第 slot 栏的标题。 */
@@ -126,7 +112,7 @@ export class FavoriteViews {
const favorite = this.favorites.find(item => item.slot === slot)
if (!favorite)
return
const name = await askName('重命名收藏', favorite.name)
const name = await askSlotName('重命名收藏', favorite.name)
if (name === undefined)
return
this.panes.get(slot)?.rename(name)
@@ -135,12 +121,11 @@ export class FavoriteViews {
/** 收藏栏标题栏的「取消收藏」:藏起第 slot 栏、空出栏位,再销毁它的视图(免得露着的栏里闪一下没有数据)。 */
async remove(slot: number): Promise<void> {
if (!this.panes.has(slot))
return
const pane = this.panes.get(slot)
this.panes.delete(slot)
await Promise.all([
this.save(removeFavorite(this.favorites, slot)),
commands.executeCommand('setContext', favoriteContext(slot), false),
])
await Promise.all([this.save(removeFavorite(this.favorites, slot)), this.pool.release(slot)])
pane?.dispose()
}