diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index c830e18..0244161 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -77,6 +77,13 @@ - 写剪贴板用 `vscode.env.clipboard.writeText`,状态栏 `已复制:<内容>` 停 3 秒,连着复制只留最新一条。 - 命令(`copyFileName` / `copyFolderName` / `copyFileStem` / `copyRelativePath` / `copyAbsolutePath`)只挂在侧栏行上,命令面板里藏起来(`when: false`)。文件名、文件夹名复制的是同一样东西,分两个命令只为菜单上写对字,按 `viewItem =~ /^file( noted)?$/`、`/^folder( noted)?$/` 挑。 +### 在系统文件管理器中显示 + +- 右键菜单里复制那组后面单独一组(中间有分隔线),再往后是注释:在系统的文件管理器里显示这一行。字和 VS Code 自带资源管理器一样按系统叫:macOS「在 Finder 中显示」、Windows「在文件资源管理器中显示」、Linux「打开所在的文件夹」。文件、目录都有(有没有注释、在不在「更改」组都一样,审查时也有);工作区根节点、分组行没有。只在右键菜单里,行尾没有按钮。 +- 做法:交给 VS Code 自带的命令 `revealFileInOS`,传这一行的 `Uri`(`explorer/revealCommands.ts`):文件是打开所在文件夹并选中它,目录是在上一层里选中它。 +- 命令(`revealInFinder` / `revealInFileExplorer` / `openContainingFolder`)做的事一样,分三个只为菜单上按系统写对字:`when` 分别带 `isMac` / `isWindows` / `isLinux`,每个系统只出自己那一项;命令面板里藏起来(`when: false`)。 +- 排在复制和注释中间靠组名:VS Code 按组名的字母序排右键菜单的组,组名 `fileManager` 落在 `copy` 和 `notes` 之间。 + ### 文件注释 读代码时给目录或文件随手记一句(原型画板「文件注释 · 悬停出按钮 / 填写 / 已保存」)。 @@ -2118,6 +2125,7 @@ vscode/src/codeReader/ explorer/fileTree.ts 侧栏文件树(FileTreeDecorator:给行添信息的接口;showChanges:审查工单时只摆 PR 改过的文件;nodeOf / getParent:按 Uri 找树上那一行、找上一层) explorer/copyText.ts 复制菜单的命令和复制出什么(纯函数) explorer/copyCommands.ts 复制命令:写剪贴板、状态栏提示 + explorer/revealCommands.ts 在系统文件管理器中显示:按系统叫法的三个命令都交给 VS Code 的 revealFileInOS review/changedFiles.ts 改动状态 → M / A / D、颜色,PR 改过的文件 → 按目录分好的树(纯函数) review/reviewRules.ts 能审查的列(「审查」「进行中」)、弹框列哪些工单、PR 号、能不能审查、标题栏的字(纯函数) review/pickList.ts 选工单弹框的各项:先摆出来、文件数读到一个换一个、还转不转圈、每项的字(纯函数) @@ -2224,7 +2232,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit - 异常(`languages/exceptions.test.ts`,真实解析 + 中文直译 + 视图数据):`fixtures/appExceptions.py` 的 `AppException`、`NotFound` 逐字同「异常」的样例(列名随有没有类型、说明变),`GatewayTimeout` 的多段说明,方法是子块;`fixtures/commerceExceptions.py` 16 个类都是异常、只有说明的四行、写了 `__init__` 的构造参数;认哪些类(自带的异常类、`Warning`、不合后缀的自带类 `StopIteration` / `KeyboardInterrupt`、`pkg.errors.AppException`、同一文件里写在后面的父类一层层往下传、泛型和别的父类混写,关键字参数、`Mapping[...]`、`*bases` 不算,函数里的类也认);类属性(带不带注解、只写注解、说明取上面的注释块 / 行尾注释 / 属性 docstring,连着赋值、解构赋值、方法、讲方法的注释、最后面的注释留作子块),构造参数去掉 self、`*args` 开头的不去;`@dataclass` 的异常类照样是异常卡片;编辑范围是整个类、改卡片写回整个类,类名、属性名、参数名的定义范围和带下标的词指回源码原文;TypeScript `fixtures/exceptions.ts.txt`(`extends Error`、类字段含 static、参数属性、JSDoc、写在后面的父类、类表达式、`ns.X`,父类名不合习惯的照常是类)、JavaScript 的类字段;TypeScript 和 Python 同样的异常得到一样的行和列名;筛选的种类叫「异常」、没有 badge;属性的中文名和去掉行尾记号。webview:异常块默认折叠(`viewState.test.ts`),跳到类名闪整块、属性名 / 构造参数名只闪表里那一行(`reveal.test.ts`)。 - 筛选:块的属性(TS、TSX 有抽象、仅类型,JavaScript / JSX 没有这两个属性、属性表也没有这两行,Python 有访问级别、异步);语言插件自己报的属性叠上去、盖过推出来的,kind 不变;文件里出现过的属性按契约顺序排、语言的排后面,值按第一次出现排、带所有层的块数,名字取翻译插件的、没有的显示 id,中文直译的名字和属性表一致;下拉框只给两种以上值的属性(或一种值正被去掉的),按钮上的字;全选 / 取消全选只动这份文件里的值、别的文件的去掉状态和别的属性不动、取消全选藏掉有这个属性的块(整棵子树)、再全选全回来;同一属性内「或」、不同属性「且」、没有这个属性不受影响、藏整棵子树、「已隐藏」只数直接筛掉的;筛选记在 `workspaceState`、新实例读得回来、所有订阅的预览收到、形状不对的当没有;跳到被筛掉的块临时露出路径、目标的子块照常筛、文档刷新只留正在编辑的。 - 文件注释:键相对工作区文件夹、`/` 分隔,根和外面的路径没有键,手写的 `./`、结尾 `/` 规范掉;保存去空白、清空即删;文件改名、目录挪位置里面各层一起走、名字只是前缀相同的不动、跨工作区文件夹先取出再放进去;删目录连带里面的;读写:文件缺失 / 解析失败 / 不是对象返回空表、跳过不合法条目、建 `.spx`、键排序、删光留 `{}`;有注释 / 没注释的行的描述、悬停提示(Markdown 转义)、contextValue,和 `package.json` 里菜单的 when 子句对得上。 -- 侧栏行上的菜单(`explorer/rowMenus.test.ts`,读 `package.json`):行尾按钮先「复制」子菜单再注释,有没有注释换「添加 / 编辑注释」,工作区根都没有;「复制」子菜单带 `$(copy)`,注释图标 `$(note)` / `$(edit)`;子菜单和右键菜单里文件四项、目录三项(有没有注释一样)、菜单上的字;复制命令都登记了、和代码里的命令表一致、命令面板里藏起来。 +- 侧栏行上的菜单(`explorer/rowMenus.test.ts`,读 `package.json`):行尾按钮先「复制」子菜单再注释,有没有注释换「添加 / 编辑注释」,工作区根都没有;「复制」子菜单带 `$(copy)`,注释图标 `$(note)` / `$(edit)`;子菜单和右键菜单里文件四项、目录三项(有没有注释一样)、菜单上的字;复制命令都登记了、和代码里的命令表一致、命令面板里藏起来;「在系统文件管理器中显示」文件和目录(含未提交、有注释)都有、工作区根和分组行没有、审查时也有,三个系统各只出自己那一项、字和 VS Code 自带的一样,自成一组排在复制和注释中间,不挂行尾按钮、命令面板里藏起来。 - 侧栏定位:树上一行和要找的路径的关系(自己、上层目录、名字开头相同的兄弟目录不算、别的协议不算);`explorer.autoReveal` 的 `true` / `false` / `focusNoScroll` / 没读到;`autoRevealExclude` 命中路上任何一层都不跟(`explorer/treeReveal.test.ts`)。树的数据源(`explorer/fileTree.test.ts`,假的 vscode 和磁盘):多层目录 `getParent` 交回 `getChildren` 给过的同一个对象、根的上一层没有;`nodeOf` 找到文件、目录,顺着 `getParent` 走回根;工作区外、`files.exclude` 藏了的、盘上没有的找不到;只摆改动的文件时在就找到、上一层一直走到分组行,没摆的找不到,同一文件在两组里取最先摆的,分组没名字时根在最上面。 - 审查工单: - 改动的文件树(`review/changedFiles.test.ts`):原型那 6 个文件加一个改名的,目录层级照留、每层只有通往改过的文件的目录,文件带改动和相对仓库根的路径,目录没有改动;排法同普通的树;文件换成同名目录两项都留着;没有文件时根下面是空的。状态 `added` → A、`deleted` → D、别的都是 M,三种颜色是 git 装饰色、删掉的提示「PR 里删掉了」。 diff --git a/vscode/package.json b/vscode/package.json index f873c67..a0c1807 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -189,6 +189,21 @@ "title": "复制绝对路径", "category": "代码阅读" }, + { + "command": "superpowers.codeReader.revealInFinder", + "title": "在 Finder 中显示", + "category": "代码阅读" + }, + { + "command": "superpowers.codeReader.revealInFileExplorer", + "title": "在文件资源管理器中显示", + "category": "代码阅读" + }, + { + "command": "superpowers.codeReader.openContainingFolder", + "title": "打开所在的文件夹", + "category": "代码阅读" + }, { "command": "superpowers.codeReader.addNote", "title": "添加注释", @@ -365,6 +380,21 @@ "when": "view == superpowers.codeReader.files && viewItem =~ /^(file( unstaged)?|folder)( noted)?$/", "group": "copy@4" }, + { + "command": "superpowers.codeReader.revealInFinder", + "when": "view == superpowers.codeReader.files && isMac && viewItem =~ /^(file( unstaged)?|folder)( noted)?$/", + "group": "fileManager" + }, + { + "command": "superpowers.codeReader.revealInFileExplorer", + "when": "view == superpowers.codeReader.files && isWindows && viewItem =~ /^(file( unstaged)?|folder)( noted)?$/", + "group": "fileManager" + }, + { + "command": "superpowers.codeReader.openContainingFolder", + "when": "view == superpowers.codeReader.files && isLinux && viewItem =~ /^(file( unstaged)?|folder)( noted)?$/", + "group": "fileManager" + }, { "command": "superpowers.codeReader.addNote", "when": "view == superpowers.codeReader.files && superpowers.codeReader.filter != review && viewItem =~ /^(file( unstaged)?|folder)$/", @@ -445,6 +475,18 @@ "command": "superpowers.codeReader.copyAbsolutePath", "when": "false" }, + { + "command": "superpowers.codeReader.revealInFinder", + "when": "false" + }, + { + "command": "superpowers.codeReader.revealInFileExplorer", + "when": "false" + }, + { + "command": "superpowers.codeReader.openContainingFolder", + "when": "false" + }, { "command": "superpowers.codeReader.addNote", "when": "false" diff --git a/vscode/src/codeReader/explorer/revealCommands.ts b/vscode/src/codeReader/explorer/revealCommands.ts new file mode 100644 index 0000000..cd62984 --- /dev/null +++ b/vscode/src/codeReader/explorer/revealCommands.ts @@ -0,0 +1,24 @@ +import type { FileNode } from './fileTree' +import { commands, Disposable } from 'vscode' + +/** + * 在系统的文件管理器里显示这一行,菜单上的字跟 VS Code 自带资源管理器一样按系统叫: + * macOS「在 Finder 中显示」、Windows「在文件资源管理器中显示」、Linux「打开所在的文件夹」。 + * 做的事一样,分成三个命令只为菜单上写对字(package.json 按 `isMac` / `isWindows` / `isLinux` 挑)。 + */ +const REVEAL_COMMANDS = [ + 'superpowers.codeReader.revealInFinder', + 'superpowers.codeReader.revealInFileExplorer', + 'superpowers.codeReader.openContainingFolder', +] + +/** 交给 VS Code 自带的 `revealFileInOS`:文件是打开所在文件夹并选中它,文件夹是在上一层里选中它。 */ +async function revealInOS(node: FileNode | undefined): Promise { + if (!node) + return // 只挂在侧栏的行上,别处调用没有目标 + await commands.executeCommand('revealFileInOS', node.uri) +} + +export function registerRevealCommands(): Disposable { + return Disposable.from(...REVEAL_COMMANDS.map(id => commands.registerCommand(id, revealInOS))) +} diff --git a/vscode/src/codeReader/explorer/rowMenus.test.ts b/vscode/src/codeReader/explorer/rowMenus.test.ts index 4959f24..a5d3c73 100644 --- a/vscode/src/codeReader/explorer/rowMenus.test.ts +++ b/vscode/src/codeReader/explorer/rowMenus.test.ts @@ -73,7 +73,7 @@ describe('侧栏行尾按钮', () => { }) it('分组行(「暂存的更改」「更改」)什么按钮、菜单都没有', () => { - for (const group of ['inline', 'copy', 'notes']) + for (const group of ['inline', 'copy', 'fileManager', 'notes']) expect(itemsFor('view/item/context', group, GROUP_CONTEXT)).toEqual([]) expect(itemsFor(COPY_MENU, 'copy', GROUP_CONTEXT)).toEqual([]) }) @@ -136,6 +136,48 @@ describe('右键菜单的注释', () => { }) }) +describe('右键菜单的「在系统文件管理器中显示」', () => { + /** 菜单上的字跟着系统走,和 VS Code 自带资源管理器一样 */ + const REVEALS = [ + { platform: 'isMac', command: 'superpowers.codeReader.revealInFinder', title: '在 Finder 中显示' }, + { platform: 'isWindows', command: 'superpowers.codeReader.revealInFileExplorer', title: '在文件资源管理器中显示' }, + { platform: 'isLinux', command: 'superpowers.codeReader.openContainingFolder', title: '打开所在的文件夹' }, + ] + const REVEAL_IDS = REVEALS.map(item => item.command) + const whenOf = (command: string): string | undefined => menus['view/item/context'].find(item => item.command === command)?.when + + it('文件、文件夹(含未提交、有注释)都有,工作区根没有;审查时也不藏', () => { + for (const context of [file, folder, unstaged, noted(file), noted(folder), noted(unstaged)]) + expect(itemsFor('view/item/context', 'fileManager', context)).toEqual(REVEAL_IDS) + expect(itemsFor('view/item/context', 'fileManager', root)).toEqual([]) + expect(REVEAL_IDS.some(id => whenOf(id)?.includes('filter'))).toBe(false) + }) + + it('每个系统只出自己那一项,字和 VS Code 自带的一样', () => { + for (const { platform, command, title } of REVEALS) { + const when = whenOf(command) + expect(when).toContain(`&& ${platform} &&`) + for (const other of REVEALS.filter(item => item.platform !== platform)) + expect(when).not.toContain(other.platform) + expect(commands.find(item => item.command === command)?.title).toBe(title) + } + }) + + it('自成一组,排在复制后面、注释前面(VS Code 按组名的字母序排组,组和组之间画分隔线)', () => { + const groups = menus['view/item/context'] + .filter(item => item.when.includes('view == superpowers.codeReader.files') && item.group !== undefined && !item.group.startsWith('inline')) + .map(item => item.group!.split('@')[0]) + expect([...new Set(groups)].sort((a, b) => a.localeCompare(b))).toEqual(['copy', 'fileManager', 'notes']) + }) + + it('只在右键菜单里,不挂行尾按钮,命令面板里藏起来', () => { + for (const id of REVEAL_IDS) { + expect(menus['view/item/context'].filter(item => item.command === id)).toHaveLength(1) + expect(menus.commandPalette.find(item => item.command === id)?.when).toBe('false') + } + }) +}) + describe('筛选(审查工单、未提交的更改)', () => { const FILTER = 'superpowers.codeReader.filter' const PICK = 'superpowers.codeReader.pickReviewIssue' diff --git a/vscode/src/codeReader/index.ts b/vscode/src/codeReader/index.ts index 39941b2..4f31e90 100644 --- a/vscode/src/codeReader/index.ts +++ b/vscode/src/codeReader/index.ts @@ -17,6 +17,7 @@ import { WorkspaceEntries } from './entries/workspaceEntries' import { registerCopyCommands } from './explorer/copyCommands' import { FileTreeProvider } from './explorer/fileTree' import { registerPathAsk } from './explorer/pathAskPanel' +import { registerRevealCommands } from './explorer/revealCommands' import { RowDecorator } from './explorer/rowDecorator' import { TreeFilters } from './explorer/treeFilters' import { TreeLocator } from './explorer/treeLocator' @@ -71,7 +72,7 @@ async function openFile(uri: Uri, registry: CodeReaderRegistry): Promise { } /** - * 接入代码阅读:注册表、侧栏文件树(问路径、复制、文件注释、筛选:按审查工单只看 PR 改过的文件、只看未提交的更改)、预览编辑器(含问 AI)、编辑器右上角的「预览 / 编辑」切换。 + * 接入代码阅读:注册表、侧栏文件树(问路径、复制、在系统文件管理器里显示、文件注释、筛选:按审查工单只看 PR 改过的文件、只看未提交的更改)、预览编辑器(含问 AI)、编辑器右上角的「预览 / 编辑」切换。 * 内置的语言插件、翻译插件、库规则和命令规则与其他扩展一样通过返回的 API 注册。 */ export function registerCodeReader(context: ExtensionContext): CodeReaderApi { @@ -108,6 +109,7 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi { indexes, registerNoteCommands(notes), registerCopyCommands(), + registerRevealCommands(), registerPathAsk( async (uri) => { const languageId = await languageOf(uri)