Files
superwork/vscode/src/codeReader/features/features.ts
T

127 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 「了解一块代码」找过的功能:问的话、栏标题、占哪个栏位、AI 最近一次交回的结果,按工作区存着。
* 这里是记录怎么读、怎么改,栏里一行、栏上方一句话怎么写。不碰 VS Code。
*/
import type { CallNode, FeatureResult } from './featureResult'
import { escapeMarkdown } from '../explorer/rowDecoration'
import { freeSlot, SLOTS } from '../slots/slots'
import { itemLabel, parseFeatureResult } from './featureResult'
/**
* 一个功能:question 是用户问的那句话(对话页标签上的名字,开新会话时发给 AI),name 是栏标题(开始时就是问题),result 是 AI 最近一次交回的结果(还没交回过时没有)。
* `outdated` 是存着的结果是以前的格式、读不了,只好当没有结果,栏上方提示重新找一次;交回新结果后就没有了。
*/
export interface Feature {
id: string
question: string
name: string
slot: number
result?: FeatureResult
outdated?: true
}
/** 功能栏里找得到的节点行的 contextValue 带的标记(见 entryContext):行菜单只挂文件能用的那几样(问 AI、复制、在系统文件管理器中显示)。 */
export const FEATURE_MARKER = 'feature'
/** 找不到的节点行的 contextValue:什么菜单都不挂。 */
export const MISSING_CONTEXT = `${FEATURE_MARKER} missing`
/** 第 slot 个功能栏标题栏上的按钮命令:openChat 打开对话、rename 重命名、remove 删除。标题栏按钮不知道点的是哪一栏,所以每个栏位各一条。 */
export function featureCommand(action: 'openChat' | 'rename' | 'remove', slot: number): string {
const name = { openChat: 'openFeatureChat', rename: 'renameFeature', remove: 'removeFeature' }[action]
return `superpowers.codeReader.${name}${slot}`
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
/** 读 workspaceState 里存的功能;认不出的丢掉,同一个栏位存了两个时只认第一个,结果读不懂的当还没有结果。 */
export function parseFeatures(value: unknown): Feature[] {
if (!Array.isArray(value))
return []
const features: Feature[] = []
for (const raw of value) {
if (!isRecord(raw))
continue
const { id, question, name, slot, result } = raw
if (typeof id !== 'string' || typeof question !== 'string' || typeof name !== 'string' || typeof slot !== 'number' || !SLOTS.includes(slot))
continue
if (features.some(feature => feature.slot === slot))
continue
const parsed = result === undefined ? undefined : parseFeatureResult(result)
const outdated = raw.outdated === true || (parsed !== undefined && 'error' in parsed)
features.push({ id, question, name, slot, ...(parsed && 'result' in parsed && { result: parsed.result }), ...(outdated && { outdated }) })
}
return features
}
/** 新功能占哪个栏位:和收藏共用,taken 是两者眼下占着的全部栏位;满了没有。 */
export function placeFeature(taken: Iterable<number>): number | undefined {
return freeSlot(taken)
}
/** 第 slot 栏的功能改叫 name,栏位不动。 */
export function renameFeature(features: readonly Feature[], slot: number, name: string): Feature[] {
return features.map(feature => feature.slot === slot ? { ...feature, name } : feature)
}
/** id 那个功能换成这次交回的结果。 */
export function withResult(features: readonly Feature[], id: string, result: FeatureResult): Feature[] {
return features.map(({ outdated, ...feature }) => feature.id === id ? { ...feature, result } : { ...feature, ...(outdated && { outdated }) })
}
/** 删掉第 slot 栏的功能,空出这个栏位。 */
export function removeFeature(features: readonly Feature[], slot: number): Feature[] {
return features.filter(feature => feature.slot !== slot)
}
/**
* 栏里调用树上一个节点的那一行怎么画:名字(符号名,只有文件的写文件名)、后面淡色的说明(跨进程连过来的先写怎么连的,再写那一句话)、
* 悬停提示(文件路径和那一句话)。found 是按眼下的代码找没找到;找不到的悬停先说「找不到了」。
*/
export function nodeRow(node: CallNode, found: boolean): { label: string, description: string, tooltip: string } {
const lines = [`**${escapeMarkdown(node.path)}**`, escapeMarkdown(node.role)]
return {
label: itemLabel(node),
description: node.via === undefined ? node.role : `${node.via} · ${node.role}`,
tooltip: [...(found ? [] : ['找不到了']), ...lines].join(' \n'),
}
}
/** 一个功能眼下在不在找:running 还在找(activity 是正在做的事),failed 没找成(error 是原因),stopped 被叫停了;没在找时没有。 */
export type FeatureRun
= | { status: 'running', activity?: string }
| { status: 'failed', error: string }
| { status: 'stopped' }
/** 句子没有句末标点时补个句号,后面好接下一句。 */
function sentence(text: string): string {
return /[。!?.!?]$/.test(text) ? text : `${text}。`
}
/**
* 栏上方的一句话(TreeView.message):正在找、没找成(可以在对话里接着说重试)、没找到相关的代码、还没有结果、存着的结果是旧格式(`outdated`,见 [Feature]);
* 有结果、又没在找时不说话。有旧结果时再找没成,旧结果留着。
*/
export function featureMessage(result: FeatureResult | undefined, run: FeatureRun | undefined, outdated = false): string | undefined {
if (run?.status === 'running') {
const doing = result ? '正在按你说的改…' : '正在找…'
return run.activity ? `${doing}(${run.activity})` : doing
}
if (run?.status === 'failed') {
return result
? `这次没改成:${sentence(run.error)}下面还是上一次的结果,可以在对话里接着说,让它再改一次。`
: `没找成:${sentence(run.error)}可以在对话里接着说,让它再找一次。`
}
if (!result) {
return outdated
? '结果是旧格式,点标题栏的「打开对话」,说一声「重新找」,就按路径和调用树重新找一次。'
: '还没有结果,可以在对话里接着说,让它再找一次。'
}
if (result.paths.length === 0)
return '没找到和这个功能直接相关的代码,可以在对话里换个说法接着说。'
return undefined
}