✨ feat(vscode): 新增「了解一块代码」:AI 找功能代码占一栏,对话里接着改
主栏标题栏的放大镜问要了解哪一块,占一个栏位建一栏(和收藏共用 30 个栏位,视图改名 slot1..30,按 slotKindN 分收藏 / 功能挂标题栏按钮),同时开对话页让 Claude 只读地在全部工作区文件夹里找,按 --json-schema 交回环节 → 文件 + 符号 + 一句话。 对话复用预览顶部的聊天(CodeQa.askTop:会话续接、进行中的一轮、记录存 workspaceState);结构不合格算这一轮没答成、留着能再画出来。 条目每次画、点时按眼下的代码找:有语言插件的解析成代码块按「类名.方法名」找,没有的问 VS Code 文档符号;找不到画成灰的。
This commit is contained in:
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()
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user