✨ feat(vscode): 代码阅读标出项目写明的入口文件和入口函数
This commit is contained in:
1 parent
78d5748b81
commit
718bbb7f6c
19 files changed
+761
-83
No files matched your search
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { escapeMarkdown, rowDecoration } from './rowDecoration'
|
||||
|
||||
describe('树上的一行', () => {
|
||||
it('没有注释、不是入口的行保持原样', () => {
|
||||
expect(rowDecoration('file', 'src/a.ts', undefined, undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('有注释:名字后面灰字是注释,contextValue 加 noted,提示里粗体路径下一行注释全文', () => {
|
||||
expect(rowDecoration('file', 'campaign/order_bonus.py', '下单返利:订单完成后按活动规则发积分', undefined)).toEqual({
|
||||
description: '下单返利:订单完成后按活动规则发积分',
|
||||
tooltip: '**campaign\\/order\\_bonus\\.py** \n下单返利:订单完成后按活动规则发积分',
|
||||
contextValue: 'file noted',
|
||||
})
|
||||
expect(rowDecoration('folder', 'campaign', '营销活动', undefined)?.contextValue).toBe('folder noted')
|
||||
})
|
||||
|
||||
it('入口、没注释:灰字是「入口」,提示里写从哪个名字开始读,contextValue 不变', () => {
|
||||
expect(rowDecoration('file', 'src/agent_cli/main.py', undefined, ['app', 'dev'])).toEqual({
|
||||
description: '入口',
|
||||
tooltip: '**src\\/agent\\_cli\\/main\\.py** \n入口,从 `app`、`dev` 开始读',
|
||||
contextValue: 'file',
|
||||
})
|
||||
expect(rowDecoration('file', 'bin/cli.js', undefined, [])?.tooltip).toBe('**bin\\/cli\\.js** \n入口')
|
||||
})
|
||||
|
||||
it('入口、有注释:灰字是「注释 · 入口」,注释不被盖掉,提示里注释下面再写入口', () => {
|
||||
expect(rowDecoration('file', 'src/main.rs', '服务启动', ['main'])).toEqual({
|
||||
description: '服务启动 · 入口',
|
||||
tooltip: '**src\\/main\\.rs** \n服务启动 \n入口,从 `main` 开始读',
|
||||
contextValue: 'file noted',
|
||||
})
|
||||
})
|
||||
|
||||
it('注释里的 Markdown 符号原样显示', () => {
|
||||
expect(escapeMarkdown('**别加粗** <b>x</b> [链接](command:x) `代码` # 标题 $(icon)'))
|
||||
.toBe('\\*\\*别加粗\\*\\* \\<b\\>x\\<\\/b\\> \\[链接\\]\\(command\\:x\\) \\`代码\\` \\# 标题 \\$\\(icon\\)')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
/** 侧栏一行名字后面的灰字和悬停提示:文件注释、项目入口。 */
|
||||
export interface RowDecoration {
|
||||
/** 名字后面的灰字:注释,是入口时后面接「 · 入口」,没注释时就是「入口」;VS Code 放不下时自己截断 */
|
||||
description: string
|
||||
/** 悬停提示(Markdown):第一行粗体的相对路径,下面是注释全文,是入口时再一行写明 */
|
||||
tooltip: string
|
||||
/** 有注释时末尾加上 ` noted`:菜单的 when 子句靠它在「添加注释」和「编辑注释」之间换;是不是入口不影响 */
|
||||
contextValue: string
|
||||
}
|
||||
|
||||
/** 每个 ASCII 标点前加反斜杠(CommonMark 允许任何 ASCII 标点这样转义),注释里的 `*`、`_`、`<`、`[` 等原样显示。 */
|
||||
export function escapeMarkdown(text: string): string {
|
||||
return text.replace(/[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]/g, '\\$&')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param contextValue 这一行原本的 contextValue(`file` / `folder`)
|
||||
* @param key 相对工作区文件夹的路径
|
||||
* @param note 文件注释
|
||||
* @param entry 是项目写明的入口时,预览里要标的名字(package.json 的 `bin` 没有名字,是空的);不是入口时 undefined
|
||||
* @returns 没有注释、也不是入口时 undefined,这一行保持原样
|
||||
*/
|
||||
export function rowDecoration(contextValue: string, key: string, note: string | undefined, entry: readonly string[] | undefined): RowDecoration | undefined {
|
||||
if (note === undefined && entry === undefined)
|
||||
return undefined
|
||||
const lines = [`**${escapeMarkdown(key)}**`]
|
||||
if (note !== undefined)
|
||||
lines.push(escapeMarkdown(note))
|
||||
if (entry !== undefined)
|
||||
lines.push(entry.length > 0 ? `入口,从 ${entry.map(name => `\`${name}\``).join('、')} 开始读` : '入口')
|
||||
return {
|
||||
description: note === undefined ? '入口' : entry === undefined ? note : `${note} · 入口`,
|
||||
tooltip: lines.join(' \n'),
|
||||
contextValue: note === undefined ? contextValue : `${contextValue} noted`,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Event, TreeItem, Uri } from 'vscode'
|
||||
import type { FileNode, FileTreeDecorator } from './fileTree'
|
||||
import { Disposable, MarkdownString } from 'vscode'
|
||||
import { locate } from '../folderStore'
|
||||
import { rowDecoration } from './rowDecoration'
|
||||
|
||||
/** 各行的文件注释(见 notes/)。 */
|
||||
export interface RowNotes {
|
||||
readonly onDidChange: Event<readonly Uri[]>
|
||||
noteOf: (node: FileNode) => string | undefined
|
||||
}
|
||||
|
||||
/** 哪些文件是项目写明的入口(见 entries/)。 */
|
||||
export interface RowEntries {
|
||||
readonly onDidChange: Event<readonly Uri[]>
|
||||
namesOf: (uri: Uri) => readonly string[] | undefined
|
||||
}
|
||||
|
||||
/** 侧栏一行名字后面的灰字和悬停提示:文件注释,后面接项目入口(见 rowDecoration);工作区外面的行(审查的工作副本)不加。 */
|
||||
export class RowDecorator implements FileTreeDecorator {
|
||||
constructor(private readonly notes: RowNotes, private readonly entries: RowEntries) {}
|
||||
|
||||
/** 注释变了、入口变了的那些行 */
|
||||
readonly onDidChange: Event<readonly Uri[]> = (listener, thisArgs, disposables) => {
|
||||
const both = Disposable.from(this.notes.onDidChange(listener, thisArgs), this.entries.onDidChange(listener, thisArgs))
|
||||
disposables?.push(both)
|
||||
return both
|
||||
}
|
||||
|
||||
decorate(item: TreeItem, node: FileNode): void {
|
||||
const at = locate(node.uri)
|
||||
const entry = node.isDirectory ? undefined : this.entries.namesOf(node.uri)
|
||||
const decoration = at && rowDecoration(item.contextValue ?? '', at.key, this.notes.noteOf(node), entry)
|
||||
if (!decoration)
|
||||
return
|
||||
item.description = decoration.description
|
||||
item.tooltip = new MarkdownString(decoration.tooltip)
|
||||
item.contextValue = decoration.contextValue
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { noteDecoration } from '../notes/noteItem'
|
||||
import { UNSTAGED_MARKER } from '../uncommitted/uncommittedFiles'
|
||||
import { COPY_COMMANDS } from './copyText'
|
||||
import { entryContext, GROUP_CONTEXT } from './fileEntries'
|
||||
import { rowDecoration } from './rowDecoration'
|
||||
|
||||
interface MenuItem { command?: string, submenu?: string, when: string, group?: string }
|
||||
interface Manifest {
|
||||
@@ -28,7 +28,7 @@ const root = entryContext({ isDirectory: true, isRoot: true })
|
||||
const unstaged = entryContext({ isDirectory: false, isRoot: false }, UNSTAGED_MARKER)
|
||||
const STAGE = 'superpowers.codeReader.stage'
|
||||
const ASK = 'superpowers.codeReader.askPath'
|
||||
const noted = (context: string): string => noteDecoration(context, 'x', '注释')!.contextValue
|
||||
const noted = (context: string): string => rowDecoration(context, 'x', '注释', undefined)!.contextValue
|
||||
|
||||
const orderOf = (item: MenuItem): number => Number(item.group?.split('@')[1] ?? 0)
|
||||
|
||||
|
||||
Reference in new issue
Block a user