✨ 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
@@ -0,0 +1,476 @@
|
||||
/**
|
||||
* 「了解一块代码」的侧栏:主栏标题栏的放大镜问要了解哪一块,占一个栏位(和收藏共用)建一栏,同时让 AI 在整个工作区里找;
|
||||
* 对话放在这个功能的对话页上(预览顶部的聊天,记录、进行中的那一轮都归 CodeQa 管),接着说就按上次的会话改、整栏换成新结果。
|
||||
* 栏里是 AI 交回的各个环节和条目;条目每次画、每次点时按眼下的代码找位置,找不到的画成灰的。
|
||||
*/
|
||||
|
||||
import type { Disposable, FileDecorationProvider, Memento, TreeDataProvider, TreeView, WorkspaceFolder } from 'vscode'
|
||||
import type { LanguageAdapter, SourceRange } from '../contract'
|
||||
import type { TargetSpot } from '../editor/openTarget'
|
||||
import type { FileNode } from '../explorer/fileTree'
|
||||
import type { Answer } from '../qa/claudeAsk'
|
||||
import type { AskEvent, CodeQa, TopRequest } from '../qa/codeQa'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { SlotPool } from '../slots/slotPool'
|
||||
import type { FeatureGroup, FeatureItem, FeatureResult } from './featureResult'
|
||||
import type { Feature, FeatureRun } from './features'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { EventEmitter, FileDecoration, MarkdownString, Range, TabInputCustom, ThemeColor, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri, window, workspace } from 'vscode'
|
||||
import { documentSymbolNodes } from '../editor/documentSymbols'
|
||||
import { featureIdOf, featurePageUri, featureSubjectUri } from '../editor/pathPreview'
|
||||
import { findRepoSymbol } from '../editor/repoPath'
|
||||
import { entryContext, GROUP_CONTEXT } from '../explorer/fileEntries'
|
||||
import { PATH_ASK_ANCHOR } from '../qa/pathQa'
|
||||
import { askSlotName, focusSlot } from '../slots/slotPool'
|
||||
import { SLOTS, slotTitle, slotViewId } from '../slots/slots'
|
||||
import { locateInSource } from './featureLocate'
|
||||
import { featureAnswer, featureFollowUp, featurePrompt, featureSchema, parseFeatureResult } from './featureResult'
|
||||
import { FEATURE_MARKER, featureMessage, itemRow, MISSING_CONTEXT, parseFeatures, removeFeature, renameFeature, withResult } from './features'
|
||||
|
||||
/** workspaceState:「了解一块代码」找过的功能(Feature 的数组),按工作区记。 */
|
||||
const FEATURES_KEY = 'superpowers.codeReader.features'
|
||||
|
||||
/** 点栏里一条的命令:参数是这一行。 */
|
||||
export const OPEN_FEATURE_ITEM = 'superpowers.codeReader.openFeatureItem'
|
||||
|
||||
/** 找不到的条目行挂的 Uri 的协议:MissingRows 把它的字画成灰的,真文件在别处的颜色不受影响。 */
|
||||
const MISSING_SCHEME = 'code-reader-feature-missing'
|
||||
|
||||
/** 文件改了、增删了,合并成一次重找。 */
|
||||
const REFRESH_DELAY_MS = 300
|
||||
|
||||
/** 栏位满了时说的话 */
|
||||
const FULL_MESSAGE = `栏位满了(收藏和「了解一块代码」共用,最多 ${SLOTS.length} 个),先取消一个收藏或删掉一个功能再找`
|
||||
|
||||
/** 符号行的图标:按语言插件认出的块的种类;问 VS Code 文档符号找到的不知道种类,用方法的图标。 */
|
||||
const SYMBOL_ICONS: Record<string, string> = {
|
||||
class: 'symbol-class',
|
||||
exception: 'symbol-class',
|
||||
impl: 'symbol-class',
|
||||
trait: 'symbol-interface',
|
||||
enum: 'symbol-enum',
|
||||
module: 'symbol-module',
|
||||
const: 'symbol-constant',
|
||||
variable: 'symbol-variable',
|
||||
function: 'symbol-method',
|
||||
}
|
||||
|
||||
/** 一条按眼下的代码找到的地方:文件,有符号时再加符号名字的位置(spot)和块的种类(kind,语言插件认出来的才有)。 */
|
||||
export interface FoundItem {
|
||||
uri: Uri
|
||||
folder: WorkspaceFolder
|
||||
spot?: TargetSpot
|
||||
kind?: string
|
||||
}
|
||||
|
||||
/** 找条目用的语言插件:这种语言的插件(没有时 undefined)和解析器。 */
|
||||
export interface ItemLanguages {
|
||||
adapterFor: (languageId: string) => LanguageAdapter | undefined
|
||||
runtime: Pick<TreeSitterRuntime, 'parse'>
|
||||
}
|
||||
|
||||
/** 分组行(一个环节) */
|
||||
interface GroupRow {
|
||||
slot: number
|
||||
group: FeatureGroup
|
||||
}
|
||||
|
||||
/** 找得到的条目行:同侧栏文件行一样带着文件(FileNode),行菜单(问 AI、复制、在系统文件管理器中显示)照常用。 */
|
||||
type FoundRow = FileNode & { slot: number, item: FeatureItem, kind?: string }
|
||||
|
||||
/** 找不到的条目行 */
|
||||
interface MissingRow {
|
||||
slot: number
|
||||
item: FeatureItem
|
||||
missing: true
|
||||
}
|
||||
|
||||
export type FeatureRow = GroupRow | FoundRow | MissingRow
|
||||
|
||||
/** 条目在磁盘上是哪个文件:工作区文件夹按名字找;那个文件夹不在了时没有。 */
|
||||
function itemUri(item: FeatureItem): { uri: Uri, folder: WorkspaceFolder } | undefined {
|
||||
const folder = workspace.workspaceFolders?.find(candidate => candidate.name === item.folder)
|
||||
return folder && { uri: Uri.joinPath(folder.uri, ...item.path.split('/')), folder }
|
||||
}
|
||||
|
||||
/**
|
||||
* 按眼下的代码找一条在哪:只有文件的看文件还在不在;有符号的,这种语言有语言插件就解析成代码块按 `类名.方法名` 找(见 locateInSource),
|
||||
* 没有插件就问 VS Code 的文档符号。文件、符号没有了时没有。
|
||||
*/
|
||||
export async function locateItem(item: FeatureItem, languages: ItemLanguages): Promise<FoundItem | undefined> {
|
||||
const at = itemUri(item)
|
||||
if (!at || !await workspace.fs.stat(at.uri).then(() => true, () => false))
|
||||
return undefined
|
||||
if (item.symbol === undefined)
|
||||
return at
|
||||
const { symbol } = item
|
||||
try {
|
||||
const doc = await workspace.openTextDocument(at.uri)
|
||||
const adapter = languages.adapterFor(doc.languageId)
|
||||
let found: { range: SourceRange, kind?: string } | undefined
|
||||
if (adapter) {
|
||||
const block = await locateInSource(languages.runtime, adapter, doc.languageId, doc.getText(), symbol)
|
||||
found = block && { range: block.range, kind: block.kind }
|
||||
}
|
||||
else {
|
||||
const node = findRepoSymbol(await documentSymbolNodes(at.uri.toString()), symbol)
|
||||
found = node && { range: node.selection }
|
||||
}
|
||||
if (!found)
|
||||
return undefined
|
||||
const range = new Range(doc.positionAt(found.range.startIndex), doc.positionAt(found.range.endIndex))
|
||||
return { ...at, spot: { range, offset: found.range.startIndex }, ...(found.kind !== undefined && { kind: found.kind }) }
|
||||
}
|
||||
catch {
|
||||
// 打不开成文本(二进制文件)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 找不到的条目行画成灰字:只认 MISSING_SCHEME 的 Uri。 */
|
||||
class MissingRows implements FileDecorationProvider {
|
||||
provideFileDecoration(uri: Uri): FileDecoration | undefined {
|
||||
return uri.scheme === MISSING_SCHEME ? new FileDecoration(undefined, '找不到了', new ThemeColor('disabledForeground')) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** 一个功能栏里的树:环节 → 条目。条目每次画时按眼下的代码找位置。 */
|
||||
class FeatureTree implements TreeDataProvider<FeatureRow> {
|
||||
private readonly changed = new EventEmitter<void>()
|
||||
readonly onDidChangeTreeData = this.changed.event
|
||||
|
||||
constructor(private readonly slot: number, private result: FeatureResult | undefined, private readonly locate: (item: FeatureItem) => Promise<FoundItem | undefined>) {}
|
||||
|
||||
/** 换成这次的结果,整棵树重画。 */
|
||||
show(result: FeatureResult): void {
|
||||
this.result = result
|
||||
this.changed.fire()
|
||||
}
|
||||
|
||||
/** 按眼下的代码重找各条,整棵树重画。 */
|
||||
refresh(): void {
|
||||
this.changed.fire()
|
||||
}
|
||||
|
||||
/** 有一条是不是这个文件 */
|
||||
covers(uri: Uri): boolean {
|
||||
const key = uri.toString()
|
||||
return this.result?.groups.some(group => group.items.some(item => itemUri(item)?.uri.toString() === key)) ?? false
|
||||
}
|
||||
|
||||
async getChildren(row?: FeatureRow): Promise<FeatureRow[]> {
|
||||
if (!row)
|
||||
return this.result?.groups.map(group => ({ slot: this.slot, group })) ?? []
|
||||
if (!('group' in row))
|
||||
return []
|
||||
return Promise.all(row.group.items.map(async (item): Promise<FeatureRow> => {
|
||||
const found = await this.locate(item)
|
||||
if (!found)
|
||||
return { slot: this.slot, item, missing: true }
|
||||
return { slot: this.slot, item, uri: found.uri, folder: found.folder, isDirectory: false, isRoot: false, ...(found.kind !== undefined && { kind: found.kind }) }
|
||||
}))
|
||||
}
|
||||
|
||||
getTreeItem(row: FeatureRow): TreeItem {
|
||||
if ('group' in row) {
|
||||
const item = new TreeItem(row.group.name, TreeItemCollapsibleState.Expanded)
|
||||
item.contextValue = GROUP_CONTEXT
|
||||
return item
|
||||
}
|
||||
const found = !('missing' in row)
|
||||
const { label, description, tooltip } = itemRow(row.item, found)
|
||||
const item = new TreeItem(label, TreeItemCollapsibleState.None)
|
||||
item.description = description
|
||||
item.tooltip = new MarkdownString(tooltip)
|
||||
item.command = { command: OPEN_FEATURE_ITEM, title: '打开', arguments: [row] }
|
||||
if (!found) {
|
||||
item.contextValue = MISSING_CONTEXT
|
||||
item.resourceUri = Uri.from({ scheme: MISSING_SCHEME, path: `/${row.item.path}` })
|
||||
item.iconPath = new ThemeIcon('circle-slash', new ThemeColor('disabledForeground'))
|
||||
return item
|
||||
}
|
||||
item.contextValue = entryContext(row, FEATURE_MARKER)
|
||||
if (row.item.symbol === undefined) {
|
||||
// 只有文件的条目:文件图标、git 改动的颜色由 VS Code 按文件加
|
||||
item.resourceUri = row.uri
|
||||
item.iconPath = ThemeIcon.File
|
||||
}
|
||||
else {
|
||||
item.iconPath = new ThemeIcon(SYMBOL_ICONS[row.kind ?? 'function'] ?? 'symbol-method')
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.changed.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/** 一个功能栏:标题是放大镜加名字,栏上方一句话说正在找、没找成、没有结果(见 featureMessage)。占上栏位时建、删掉时销毁。 */
|
||||
class FeaturePane {
|
||||
private readonly tree: FeatureTree
|
||||
private readonly view: TreeView<FeatureRow>
|
||||
private result: FeatureResult | undefined
|
||||
private run: FeatureRun | undefined
|
||||
|
||||
constructor(feature: Feature, locate: (item: FeatureItem) => Promise<FoundItem | undefined>) {
|
||||
this.result = feature.result
|
||||
this.tree = new FeatureTree(feature.slot, feature.result, locate)
|
||||
this.view = window.createTreeView(slotViewId(feature.slot), { treeDataProvider: this.tree, showCollapseAll: true })
|
||||
this.view.title = slotTitle('feature', feature.name)
|
||||
this.view.message = featureMessage(this.result, this.run)
|
||||
}
|
||||
|
||||
rename(name: string): void {
|
||||
this.view.title = slotTitle('feature', name)
|
||||
}
|
||||
|
||||
/** AI 交回了新结果:整栏换成它。 */
|
||||
show(result: FeatureResult): void {
|
||||
this.result = result
|
||||
this.tree.show(result)
|
||||
this.view.message = featureMessage(this.result, this.run)
|
||||
}
|
||||
|
||||
/** 正在找、没找成、停下了、没在找(undefined)。 */
|
||||
setRun(run: FeatureRun | undefined): void {
|
||||
this.run = run
|
||||
this.view.message = featureMessage(this.result, this.run)
|
||||
}
|
||||
|
||||
covers(uri: Uri): boolean {
|
||||
return this.tree.covers(uri)
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.tree.refresh()
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.view.dispose()
|
||||
this.tree.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 「了解一块代码」:找过的功能各占一栏(和收藏共用栏位,见 SlotPool),记在 workspaceState 里,重开窗口栏还在。
|
||||
* 找、接着说都交给 CodeQa 的顶部聊天(askTop):会话、进行中的那一轮、对话记录都在那里,关掉对话页也不中断;
|
||||
* 这里拼发给 AI 的话、校验交回的结果、换栏里的树,跟着 CodeQa 报的进展改栏上方那句话。
|
||||
* 打开对话页、打开文件不在这里做(要用预览),只给出要打开什么(chatPage、target)。
|
||||
*/
|
||||
export class FeatureViews implements Disposable {
|
||||
private features: Feature[]
|
||||
/** 栏位号 → 那一栏 */
|
||||
private readonly panes = new Map<number, FeaturePane>()
|
||||
private readonly disposables: Disposable[]
|
||||
private timer: NodeJS.Timeout | undefined
|
||||
/** 等着重找的文件 */
|
||||
private readonly touched = new Map<string, Uri>()
|
||||
|
||||
/**
|
||||
* @param state workspaceState
|
||||
* @param pool 和收藏共用的栏位
|
||||
* @param qa 对话:顶部聊天的一轮怎么问、记录怎么存
|
||||
* @param locate 按眼下的代码找一条在哪(见 locateItem)
|
||||
*/
|
||||
constructor(
|
||||
private readonly state: Memento,
|
||||
private readonly pool: SlotPool,
|
||||
private readonly qa: CodeQa,
|
||||
private readonly locate: (item: FeatureItem) => Promise<FoundItem | undefined>,
|
||||
) {
|
||||
this.features = parseFeatures(state.get(FEATURES_KEY))
|
||||
for (const feature of this.features) {
|
||||
this.open(feature)
|
||||
void pool.occupy(feature.slot, 'feature')
|
||||
}
|
||||
const watcher = workspace.createFileSystemWatcher('**/*')
|
||||
this.disposables = [
|
||||
watcher,
|
||||
watcher.onDidChange(uri => this.touch(uri)),
|
||||
watcher.onDidCreate(uri => this.touch(uri)),
|
||||
watcher.onDidDelete(uri => this.touch(uri)),
|
||||
workspace.onDidChangeWorkspaceFolders(() => this.refreshAll()),
|
||||
window.registerFileDecorationProvider(new MissingRows()),
|
||||
qa.onDidAnswer(event => this.answered(event)),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 主栏标题栏的「了解一块代码」:问要了解哪一块,占最小的空栏位建一栏(栏名就是问的话)、亮出来,让 AI 开始找。
|
||||
* 返回这个功能的对话页(要打开它看找的过程);没问、栏位满了、没打开工作区文件夹时没有。
|
||||
*/
|
||||
async find(): Promise<Uri | undefined> {
|
||||
if (!workspace.workspaceFolders?.length) {
|
||||
void window.showWarningMessage('先打开工作区文件夹,再了解里面的代码')
|
||||
return undefined
|
||||
}
|
||||
if (this.pool.free() === undefined) {
|
||||
void window.showWarningMessage(FULL_MESSAGE)
|
||||
return undefined
|
||||
}
|
||||
const question = (await window.showInputBox({
|
||||
title: '了解一块代码',
|
||||
prompt: '你想要了解哪一块的代码',
|
||||
placeHolder: '例如:用户登录、会员自动续费',
|
||||
validateInput: text => text.trim() ? undefined : '说说想了解哪一块',
|
||||
}))?.trim()
|
||||
if (!question)
|
||||
return undefined
|
||||
const slot = this.pool.free()
|
||||
if (slot === undefined) {
|
||||
void window.showWarningMessage(FULL_MESSAGE)
|
||||
return undefined
|
||||
}
|
||||
const feature: Feature = { id: randomUUID(), question, name: question, slot }
|
||||
this.open(feature)
|
||||
await Promise.all([this.save([...this.features, feature]), this.pool.occupy(slot, 'feature')])
|
||||
await focusSlot(slot)
|
||||
await this.start(feature, { askId: randomUUID(), question, images: [] })
|
||||
return featurePageUri(feature.id, feature.question)
|
||||
}
|
||||
|
||||
/** 功能对话页里接着说(subject 见 featureSubjectUri):AI 接着上次的会话改,交回完整的新结果。说不了时返回给人看的原因。 */
|
||||
async ask(subject: Uri, request: TopRequest): Promise<string | undefined> {
|
||||
const feature = this.features.find(item => item.id === featureIdOf(subject))
|
||||
if (!feature)
|
||||
return '这个功能已经删掉了。'
|
||||
return this.start(feature, request, request.question)
|
||||
}
|
||||
|
||||
/** 第 slot 栏的对话页;这一栏不是功能时没有。 */
|
||||
chatPage(slot: number): Uri | undefined {
|
||||
const feature = this.features.find(item => item.slot === slot)
|
||||
return feature && featurePageUri(feature.id, feature.question)
|
||||
}
|
||||
|
||||
/** 点栏里一条:按眼下的代码找它在哪;找不到了说一声、这一栏重画(画成灰的)。 */
|
||||
async target(row: FeatureRow | undefined): Promise<FoundItem | undefined> {
|
||||
if (!row || 'group' in row)
|
||||
return undefined
|
||||
const found = await this.locate(row.item)
|
||||
if (!found) {
|
||||
void window.showInformationMessage(`找不到了:${row.item.path}${row.item.symbol === undefined ? '' : ` 里的 ${row.item.symbol}`}`)
|
||||
this.panes.get(row.slot)?.refresh()
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
/** 功能栏标题栏的「重命名」:改第 slot 栏的标题。 */
|
||||
async rename(slot: number): Promise<void> {
|
||||
const feature = this.features.find(item => item.slot === slot)
|
||||
if (!feature)
|
||||
return
|
||||
const name = await askSlotName('重命名', feature.name)
|
||||
if (name === undefined)
|
||||
return
|
||||
this.panes.get(slot)?.rename(name)
|
||||
await this.save(renameFeature(this.features, slot, name))
|
||||
}
|
||||
|
||||
/** 功能栏标题栏的「删除」:删栏(空出栏位)、删结果、删对话记录,正在找的叫停,开着的对话页关掉。 */
|
||||
async remove(slot: number): Promise<void> {
|
||||
const feature = this.features.find(item => item.slot === slot)
|
||||
if (!feature)
|
||||
return
|
||||
const pane = this.panes.get(slot)
|
||||
this.panes.delete(slot)
|
||||
const pages = window.tabGroups.all.flatMap(group => group.tabs).filter(tab => tab.input instanceof TabInputCustom && featureIdOf(tab.input.uri) === feature.id)
|
||||
await Promise.all([
|
||||
this.save(removeFeature(this.features, slot)),
|
||||
this.pool.release(slot),
|
||||
this.qa.deleteTop(featureSubjectUri(feature.id)),
|
||||
...(pages.length > 0 ? [window.tabGroups.close(pages)] : []),
|
||||
])
|
||||
pane?.dispose()
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
clearTimeout(this.timer)
|
||||
for (const disposable of this.disposables)
|
||||
disposable.dispose()
|
||||
for (const pane of this.panes.values())
|
||||
pane.dispose()
|
||||
}
|
||||
|
||||
/**
|
||||
* 让 AI 找一轮:只读、按 featureSchema 交结果,在第一个工作区文件夹里跑、别的加进来读。
|
||||
* 接着上次的会话说时发 request.question 再要完整的新结果;没有会话(第一次、会话找不到了)发完整的要求,said 是用户接着说的话。
|
||||
*/
|
||||
private async start(feature: Feature, request: TopRequest, said?: string): Promise<string | undefined> {
|
||||
const folders = workspace.workspaceFolders ?? []
|
||||
if (folders.length === 0)
|
||||
return '没打开工作区文件夹,没地方找。'
|
||||
const names = folders.map(folder => folder.name)
|
||||
const refused = await this.qa.askTop(featureSubjectUri(feature.id), request, {
|
||||
prompt: featurePrompt(feature.question, folders.map(folder => ({ name: folder.name, path: folder.uri.fsPath })), said),
|
||||
followUp: featureFollowUp(request.question),
|
||||
schema: featureSchema(names),
|
||||
addDirs: folders.slice(1).map(folder => folder.uri.fsPath),
|
||||
}, answer => this.settle(feature.id, answer, names))
|
||||
if (refused !== undefined)
|
||||
this.panes.get(feature.slot)?.setRun({ status: 'failed', error: refused })
|
||||
return refused
|
||||
}
|
||||
|
||||
/** AI 答完了:交回的结构合格就存下、整栏换成它,返回对话里显示的回答;不合格说清哪里不对。 */
|
||||
private async settle(id: string, answer: Answer, folders: readonly string[]): Promise<{ text: string } | { error: string }> {
|
||||
const parsed = parseFeatureResult(answer.output, folders)
|
||||
if ('error' in parsed)
|
||||
return parsed
|
||||
const feature = this.features.find(item => item.id === id)
|
||||
if (!feature)
|
||||
return { error: '这个功能已经删掉了。' }
|
||||
await this.save(withResult(this.features, id, parsed.result))
|
||||
this.panes.get(feature.slot)?.show(parsed.result)
|
||||
return { text: featureAnswer(parsed.result, folders.length > 1) }
|
||||
}
|
||||
|
||||
/** CodeQa 报的进展:功能对话页上的那一轮在找、找完、没找成、停下了,改那一栏上方的话。删掉了的功能那一轮停下后不再留着。 */
|
||||
private answered(event: AskEvent): void {
|
||||
const id = featureIdOf(Uri.parse(event.owner))
|
||||
if (id === undefined)
|
||||
return
|
||||
const feature = this.features.find(item => item.id === id)
|
||||
if (!feature) {
|
||||
if (event.kind === 'end')
|
||||
this.qa.forgetAsk(featureSubjectUri(id), PATH_ASK_ANCHOR)
|
||||
return
|
||||
}
|
||||
const pane = this.panes.get(feature.slot)
|
||||
if (event.kind === 'update') {
|
||||
pane?.setRun({ status: 'running', ...(event.ask.activity !== undefined && { activity: event.ask.activity }) })
|
||||
return
|
||||
}
|
||||
const { end } = event
|
||||
pane?.setRun(end.status === 'done' ? undefined : end.status === 'failed' ? { status: 'failed', error: end.error } : { status: 'stopped' })
|
||||
}
|
||||
|
||||
/** 文件改了、增删了:等一会儿,把有条目在这些文件里的栏重找一遍。 */
|
||||
private touch(uri: Uri): void {
|
||||
this.touched.set(uri.toString(), uri)
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(() => {
|
||||
const uris = [...this.touched.values()]
|
||||
this.touched.clear()
|
||||
for (const pane of this.panes.values()) {
|
||||
if (uris.some(touched => pane.covers(touched)))
|
||||
pane.refresh()
|
||||
}
|
||||
}, REFRESH_DELAY_MS)
|
||||
}
|
||||
|
||||
private refreshAll(): void {
|
||||
for (const pane of this.panes.values())
|
||||
pane.refresh()
|
||||
}
|
||||
|
||||
private open(feature: Feature): void {
|
||||
this.panes.set(feature.slot, new FeaturePane(feature, this.locate))
|
||||
}
|
||||
|
||||
private async save(features: Feature[]): Promise<void> {
|
||||
this.features = features
|
||||
await this.state.update(FEATURES_KEY, features.length > 0 ? features : undefined)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user