diff --git a/docs/superpowers/specs/code-reader/spec.md b/docs/superpowers/specs/code-reader/spec.md index 7aaf0eb..4fa4889 100644 --- a/docs/superpowers/specs/code-reader/spec.md +++ b/docs/superpowers/specs/code-reader/spec.md @@ -239,7 +239,7 @@ - 标了改动的块默认展开(函数、类本来默认收起子块),用户点过的照他的。 - 筛选栏第二行(高 30px,只有有差异数据时才有):比较图标、「对比:<基准>」、「·」、统计(各按改动色,没有的那种不写,都没有写「没有改动」),右边「只看改动」开关、上一处 / 下一处(↑ ↓,悬停提示、`aria-label`)。基准里没有这个文件时只写基准。 - 只看改动:每一层连着没变的块折成一条「… N 块没变 点开」(虚线框),被筛掉的块不算也不打断,删掉的块在哪儿就在哪儿断开;改过的块照常,它下面没变的子块同样折;点开一段就恢复这一段(筛选一变重新折)。开关按工作区记住(`BlockFilter`,`workspaceState` 的 `superpowers.codeReader.onlyChanges`),所有预览共用;跳到定义落在折起来的块上时临时露出来。 - - 上一处 / 下一处:在各处改动(新增的最外面那块、自己改了的修改块、删掉的块,照页面先后,被筛掉的不算)之间跳,展开折住它的块,滚到筛选栏下面(外层还钉着的卡片下面,见「钉住外层卡片」)、闪几下;跳完没滚过页面就接着刚跳到的那处走,自己滚过了按视口顶上那块的行找;到头绕回另一头。 + - 上一处 / 下一处:在各处改动(新增的最外面那块、自己改了的修改块、删掉的块,照页面先后,被筛掉的不算)之间跳,展开折住它的块,滚到筛选栏下面(外层还钉着的卡片下面,见「钉住卡片」)、闪几下;跳完没滚过页面就接着刚跳到的那处走,自己滚过了按视口顶上那块的行找;到头绕回另一头。 - 总览条:预览右边缘 14px 宽、从筛选栏下面贴到底,每处改动按它在新文件里的行画一段刻度(修改块只画它自己那几行,删掉的块一条 2px 细线),点刻度跳过去(同上一处 / 下一处)。 - 文档刚改、差异还没重算时:块按记号认,没动过的块照样带色条;卡片里的标记按下标认,只在页面上正是差异算的那一版时画。 - 颜色:取 VS Code 编辑器行号旁的改动色带 `--vscode-editorGutter-addedBackground` / `modifiedBackground` / `deletedBackground`(主题没给时用 Dark Modern 的默认值),行内增删的字用 `--vscode-diffEditor-insertedTextBackground` / `removedTextBackground`;整行底色是改动色的 14%,新增卡片底色 10%。 @@ -506,7 +506,7 @@ 2. 定义是属性表里某一行 / 子行讲的东西(参数、导入项、一条语句声明几个变量时的某一个、类的泛型参数、对象字面量一项的键——TypeScript 对象的属性名就是定义,嵌套几层都算)→ 只闪这一行 / 子行。范围是翻译插件从 facts 取的节点(`PropertyRow.defines`、`PropertyItem.defines`;字典 / 对象的项标的是键,展开项不标),定义处落在范围里就算(`*args` 的定义在 `args` 上、解构参数里的名字都对得上)。 3. 其余(定义在一行译文里,例如循环变量;翻译插件没填 `defines`)→ 显示着定义处那个名字的块,名字在属性行的子行里时展开那一行(在嵌套的子行里时连它上面各层一起展开);页面上找不到这个名字就取包含定义那一行的块。 - 同一文件原地跳、跳进另一个文件的预览,规则一样。 -- 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项、字典 / 对象的项)就展开那一行,在嵌套的子行里就连它上面折叠着的各层子行一起展开。目标已在视口里(筛选栏和钉住的外层卡片下面)不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(`--vscode-focusBorder`)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。 +- 滚过去:目标所在块的祖先块折叠着就展开,目标在折叠的属性行子行里(参数、导入项、字典 / 对象的项)就展开那一行,在嵌套的子行里就连它上面折叠着的各层子行一起展开。目标已在视口里(筛选栏和钉住的卡片下面)不滚,否则滚到视口正中(比视口高就顶部对齐)。然后目标的边框和底色往聚焦色(`--vscode-focusBorder`)闪三下,共约 1.5 秒后复原;同一处再跳一次从头闪;系统开了「减少动态效果」时改成静止高亮 1.5 秒。目标是属性行 / 子行但块正在编辑(页面上没有属性表)时闪整块。 - VS Code 里没有这种语言的定义提供者(shell):语言扩展答不上时,同一文件里的名字由语言插件认(`LanguageAdapter.symbols`,见「Shell 脚本」),跳到用到的那处之前最近的定义。 - 不按修饰键单击名字是高亮它的各处出现(见下一节);⌘ / Ctrl + 点击名字不会触发块的编辑切换、折叠,也不动高亮。 @@ -521,10 +521,10 @@ - 卡片只读:同页面上那张卡片(类、函数默认收起子块,能在浮窗里折叠展开,只记在浮窗里),块树不带编辑那部分(`BlockTreeState.edit`),没有编辑、问 AI、重写注释的按钮,不标差异、不筛选。浮窗里的名字下标是那个文件的,页面上管名字的几处都不认它们(浮窗根上标 `data-peek`):不按 ⌘ / Ctrl 变链接、不能跳,单击不高亮也不清页面上的高亮,不画页面上的高亮,不再弹卡片;注释里不带路径的引用不成链接(引用按注释所在的文件找,页面只替自己这份文件问),仓库路径链接照样能点。 - 按住 ⌘ / Ctrl 变链接、⌘ / Ctrl + 点击跳转、单击高亮各处出现都照旧;在浮窗外按下鼠标(单击名字、⌘ + 点击)先收起浮窗。 -### 钉住外层卡片(Sticky Scroll) +### 钉住卡片(Sticky Scroll) -- 同 VS Code 编辑器的 Sticky Scroll:滚进一张卡片的子块、卡片自己的开头已经滚到筛选栏后面时,筛选栏下面钉一行这张卡片的头;套着几层就摞几行,外层在上、里层在下。只认页面上画出来的:子块画出来了的卡片才钉,折叠着的、被筛掉的、正在编辑的、「只看改动」折起来的卡片下面没有子块,不钉;挂在子行上、展开着的子块也算它那张卡片的子块。和语言无关(只看块树),标差异的页面(未提交的更改、审查工单)一样。 -- 钉哪几张(`stickyScroll.ts` 的 `pinnedLines`,照 VS Code 的 `StickyScrollController.findScrollWidgetState`):每行一样高(24px),卡片那一行在筛选栏下沿往下 n × 行高,n 是它外面套着几张卡片;卡片上沿高过它那一行顶、子块的下沿还没过这一行顶时钉住。子块快滚完、下沿高过这一行的下沿时,这一行跟着被顶上去,从上一行下面滑走(层级低一级),整块跟着变矮;下一张同层卡片接上来时它已经滑走。 +- 同 VS Code 编辑器的 Sticky Scroll,一张卡片就是一层:卡片的开头(名称那一行)已经滚到筛选栏后面、卡片还有一部分看得见时,筛选栏下面钉一行这张卡片的头;套着几层就摞几行,外层在上、里层在下。卡片管到哪:卡片自己,连同它画出来的子块(挂在子行上、展开着的子块也算)。折叠着的卡片、没有子块的卡片照样钉(函数默认收着,属性表本身就高);折起来的子块、「只看改动」折起来的块没画出来,不算;被筛掉的不在页面上,正在编辑的卡片不钉;删掉的块那张不钉。和语言无关(只看块树),标差异的页面(未提交的更改、审查工单)一样。 +- 钉哪几张(`stickyScroll.ts` 的 `pinnedLines`,照 VS Code 的 `StickyScrollController.findScrollWidgetState`):每行一样高(24px),卡片那一行在筛选栏下沿往下 n × 行高,n 是它外面套着几张卡片;卡片上沿高过它那一行顶、下沿(连同子块)还没过这一行顶时钉住。卡片快滚完、下沿高过这一行的下沿时,这一行跟着被顶上去,从上一行下面滑走(层级低一级),整块跟着变矮;下一张同层卡片接上来时它已经滑走。 - 行数:`editor.stickyScroll.enabled` 关着不钉;最多钉 `editor.stickyScroll.maxLineCount` 行,且不超过筛选栏下面可视区域的四分之一(四舍五入,同 VS Code);套得比这深时留外层的,同 VS Code。设置由扩展端按这份文件读(语言自己的设置优先),`ready` 后发给页面,改了再发,页面不写死;没收到之前不钉。 - 一行的样子(`StickyLines.tsx`):和那张卡片左右对齐(越里层越往右),折叠箭头那格留空、概念图标、标题,位置同卡片上的;标题同卡片上的写法(属性表第一行(名称)的值或一行译文,中文名照换、定义处后面淡淡写原名;Markdown 只取第一行),一行放不下省略。底色 `--vscode-editorStickyScroll-background`、悬停 `--vscode-editorStickyScrollHover-background`、下沿阴影 `--vscode-editorStickyScroll-shadow`,同编辑器(主题没给时用编辑器底色、列表悬停色、控件阴影)。 - 点一行:把那张卡片滚到顶上,正好在外层还钉着的几行下面(外层各占一行,最多同上面的上限),闪几下(同跳到定义)。行里的名字照旧:⌘ / Ctrl + 点击跳到定义、悬停看定义卡片;单击名字高亮它的各处出现,同时滚过去;钉住的行里不画高亮(页面上找当前那一处只认卡片里的)。 @@ -2020,7 +2020,7 @@ type BlockView = BlockViewBase & ({ translation: PreviewText, table?: PreviewTab - `{ type: 'definitionProbed', id, found }`:回答第 `id` 次 `probeDefinition`。 - `{ type: 'definitionPeeked', id, peek? }`:回答第 `id` 次 `peekDefinition` / `peekLink`。`peek` 是 `{ path, line, block: BlockView, names: ShownName[] }`:定义所在的文件(从工作区文件夹算起)、定义处那一行(0 起)、那块卡片连同子块、卡片上显示成中文名的名字(下标是那个文件的);没有卡片时不带。见「悬停看定义卡片」。 - `{ type: 'hoverDelay', delay, hidingDelay }`:VS Code 的 `editor.hover.delay` / `editor.hover.hidingDelay`(毫秒,按这份文件读),`ready` 后发一次,设置改了再发。 - - `{ type: 'stickyScroll', enabled, maxLineCount }`:VS Code 的 `editor.stickyScroll.enabled` / `editor.stickyScroll.maxLineCount`(按这份文件读,语言自己的设置优先),`ready` 后发一次,设置改了再发;见「钉住外层卡片」。 + - `{ type: 'stickyScroll', enabled, maxLineCount }`:VS Code 的 `editor.stickyScroll.enabled` / `editor.stickyScroll.maxLineCount`(按这份文件读,语言自己的设置优先),`ready` 后发一次,设置改了再发;见「钉住卡片」。 - `{ type: 'occurrencesFound', id, occurrences: { start, end, kind: 'text' | 'read' | 'write' }[] }`:回答第 `id` 次 `findOccurrences`,`[start, end)` 是源码下标,`kind` 同 `DocumentHighlightKind`;语言扩展没答就是空数组。 - `{ type: 'moveOccurrence', step: 1 | -1 }`:两条命令发来,跳到下一处 / 上一处出现。 - `{ type: 'displayNames', version, names: ShownName[] }`:version 这份文档里显示成中文名的名字,查到一步发一步;页面只收自己眼下这个版本的。 @@ -2187,7 +2187,7 @@ vscode/src/codeReader/ editor/occurrences.ts 查名字的各处出现(vscode.executeDocumentHighlights) editor/occurrenceNavigation.ts 跳到下一处 / 上一处高亮:命令、上下文键 index.ts registerCodeReader(context): CodeReaderApi -vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;块右上角的图标按钮在 BlockTools.tsx,悬停提示在 src/components/ui/tooltip.tsx(和看板共用,页面根上挂一次),块的概念图标(图标名 → 图标组件只在 kindIcons.tsx;内置种类 → 图标名在 contract.ts 的 KIND_ICONS,宿主写进概念列表)在 kindIcons.tsx,属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,中文名换字在 displayNames.ts,Markdown 渲染在 markdown.ts / TextPreview.tsx(不带路径的引用的行内规则也在 markdown.ts),仓库链接和引用的点击、问坏没坏在 repoLink.ts / useRepoLinks.ts,悬停看定义卡片什么时候弹、收、放在哪在 definitionPeek.ts、接到页面上在 useDefinitionPeek.ts、浮窗和只读的卡片在 DefinitionPeekPopup.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,编辑时盖住哪些块、点「编辑」退出哪些块在 editing.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts,问 AI 的问答区在 AskThread.tsx、状态和规则在 qa.ts、贴图拖图的规则在 qaImages.ts、收发在 useQa.ts;代码差异:页面上怎么标(块 id、删掉的块挂哪、只看改动折哪几段、各处改动、上一处 / 下一处跳到哪)在 diffView.ts,筛选栏第二行和新文件横幅在 DiffBar.tsx,卡片里的增删在 DiffText.tsx(Markdown 的增删在 markdown.ts 的 renderMarkdownDiff),总览条在 OverviewRuler.tsx,跳到一处改动在 useChangeNav.ts;钉住外层卡片:钉哪几张、顶上去多少在 stickyScroll.ts,量页面、收设置在 useStickyScroll.ts,钉住的几行在 StickyLines.tsx) +vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;块右上角的图标按钮在 BlockTools.tsx,悬停提示在 src/components/ui/tooltip.tsx(和看板共用,页面根上挂一次),块的概念图标(图标名 → 图标组件只在 kindIcons.tsx;内置种类 → 图标名在 contract.ts 的 KIND_ICONS,宿主写进概念列表)在 kindIcons.tsx,属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,中文名换字在 displayNames.ts,Markdown 渲染在 markdown.ts / TextPreview.tsx(不带路径的引用的行内规则也在 markdown.ts),仓库链接和引用的点击、问坏没坏在 repoLink.ts / useRepoLinks.ts,悬停看定义卡片什么时候弹、收、放在哪在 definitionPeek.ts、接到页面上在 useDefinitionPeek.ts、浮窗和只读的卡片在 DefinitionPeekPopup.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,编辑时盖住哪些块、点「编辑」退出哪些块在 editing.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts,问 AI 的问答区在 AskThread.tsx、状态和规则在 qa.ts、贴图拖图的规则在 qaImages.ts、收发在 useQa.ts;代码差异:页面上怎么标(块 id、删掉的块挂哪、只看改动折哪几段、各处改动、上一处 / 下一处跳到哪)在 diffView.ts,筛选栏第二行和新文件横幅在 DiffBar.tsx,卡片里的增删在 DiffText.tsx(Markdown 的增删在 markdown.ts 的 renderMarkdownDiff),总览条在 OverviewRuler.tsx,跳到一处改动在 useChangeNav.ts;钉住卡片:钉哪几张、顶上去多少在 stickyScroll.ts,量页面、收设置在 useStickyScroll.ts,钉住的几行在 StickyLines.tsx) ``` ## 测试 @@ -2208,7 +2208,7 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit - 行替换:多行块替换后缩进正确。 - 跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 别的文件 / 挑选框,别的文件用哪种编辑器(`editor/openTarget.test.ts`:有代码阅读的用预览、带位置时滚到那一行闪定义处,没有的用默认的打开方式选中名字,打不开成文本的用默认的打开方式);webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。悬停看定义卡片:取哪块(`editor/definitionPeek.test.ts`:Python 源码走完整的语言插件 + 翻译插件,类名 → 类那块连同子块,参数 → 所在的函数,变量 → 变量那块,块外面 → 没有,挂在子行上的子块去掉 `slot`;卡片上的中文名只带画出来的、长度对得上的,别的文件只认定义本身),什么时候弹、收(`definitionPeek.test.ts`:停够延迟才问、没到时间移开不问、没有卡片不弹、迟到的回答丢掉、链接问 `peekLink` 点标题发 `openRepoLink`、没收到延迟不弹、移开过 `hidingDelay` 才收、移进浮窗不收、扫过别的名字不往后推、浮窗里不弹别的、换成新名字的卡片、Esc / 滚动后不再弹、换文档版本收起;指到符号的链接才弹;放在下方 / 上方 / 空间大的那边、钳在视口内),只读的卡片(`DefinitionPeekPopup.test.tsx`:类收起子块、没有编辑 / 问 AI / 重写按钮、中文名用卡片带的、引用不成链接;`markdown.test.ts` 的 `peek`)。闪哪一处(TS、Python 源码走完整的语言插件 + 翻译插件 + 视图数据):参数 → 它那个子行,导入项 → 它那一项,多变量声明里的一个 → 它的名称行,类的泛型参数 → 「类型参数」那一行,函数名 / 类名 / 单个变量名(含解构模式)→ 整块,循环变量 → 所在的块,翻译插件没填 `defines` → 整块(照样展开属性行);翻译插件给参数、导入项、声明的变量、泛型参数标的范围指回源码原文;块自己的名字只有函数、类和单变量声明有。 - 高亮各处出现:段的源码范围和出现重叠才亮、边界相接不算、没下标的不亮,讲别处名字的段(`refers`)按那个名字算、自己照抄的位置不算(`occurrences.test.ts` 的 `sourceOf`),点分模块名每一节各按自己的范围认(真实的 Python 译文和属性表:定义处按写入、用到处按读取);写入 → 强高亮色,读取、按文字 → 普通高亮色,同时碰上按写入;单击名字问、点空白处清、点按钮 / 链接 / 筛选栏不动、名字在按钮里照样问,拖选、双击第二下、带修饰键的点击不动,⌘ / Ctrl + 点击照旧发跳到定义、不问出现处;回答连同名字显示、空回答不亮、换名字时回答到前旧的留着、迟到的回答丢掉、答过「没有」再点照样再问、清掉后才到的回答丢掉、换文档版本或翻译清掉而同版本同翻译重发不清、没有文档时不问。键盘跳(`occurrences.test.ts`、`reveal.test.ts`、`editor/occurrenceNavigation.test.ts`):回答照源码先后排、点中的那处是当前那一处、「2/5」和「N 处」;往后 / 往前、到头接着跳,找不到的跳过、总数照旧、一处都找不到不动,点中的不在回答里从点中的地方数起;只有当前那一处描框;能不能跳(高亮、焦点、输入框)第一次必报、之后变了才报;页面上找第一处显示着它的地方、要展开的块 / 属性行 / 子行、跳过筛掉 / 编辑 / 盖住的块、Markdown 不算;`package.json` 的两条命令、默认键、`when`、命令面板,扩展和页面的代码里不写键、不拦键,上下文键有一个预览能跳就真、变了才设,命令发给能跳的预览、没有就发给活动的;按键那一刻的位置跳转时回一次、隔太久 / 点过鼠标 / 没按过键不回,跳完守住跳到的位置、过了 `KEY_SCROLL_MS` 或又按键、点鼠标不守。 -- 钉住外层卡片(`stickyScroll.test.ts`、`StickyLines.test.tsx`、`BlockTree.test.tsx`):上沿刚滚过它那一行才钉、里层按自己那一行算,外层在上里层在下,同层前一张滚完、后一张还没到只钉外层;子块快滚完时最里那行被顶上去、正好到行顶时整行顶到上一行下面,最后一张子块同时是外层的结尾时里层先走、再顶外层;比上限深时留外层的;最多行数按设置和可视区域的四分之一;滚到顶上时上面钉着多高;钉的没变不重画。一行里是概念图标和标题:属性表第一行的值(中文名照换、不带属性名、不带说明)、一行译文、Markdown 只取第一行、空属性表只有图标。画出来的子层记着它是哪张卡片的,收着时没有。 +- 钉住卡片(`stickyScroll.test.ts`、`StickyLines.test.tsx`、`BlockTree.test.tsx`):没有子块的高卡片(折叠着的函数)名称那行滚到后面、下面还看得见就钉,滚完时被顶上去;上沿刚滚过它那一行才钉、里层按自己那一行算,外层在上里层在下,同层前一张滚完、后一张还没到只钉外层;子块快滚完时最里那行被顶上去、正好到行顶时整行顶到上一行下面,最后一张子块同时是外层的结尾时里层先走、再顶外层;比上限深时留外层的;最多行数按设置和可视区域的四分之一;滚到顶上时上面钉着多高;钉的没变不重画。一行里是概念图标和标题:属性表第一行的值(中文名照换、不带属性名、不带说明)、一行译文、Markdown 只取第一行、空属性表只有图标。画出来的子层记着它是哪张卡片的,收着时没有。 - 中文名: - 语言插件认写法(`languages/pythonNames.test.ts`、`languages/typescriptNames.test.ts`,真实解析):Python 注解 `Annotated`(`typing.Annotated`、`t.Comment`,f-string、别的调用不算)、装饰器(函数、类)、行尾记号(多行参数表、`*args`、`self.x`、变量、字段、枚举成员,记号前面有别的字,记号的范围)、单独成行 / 那一行没有定义的不算、函数那一行给参数、docstring 的谷歌写法和 Sphinx 写法(带类型、星号、续行、别的段落不算)、第一小句和 12 个字的限制、明写的记号盖过说明文字、没取的记号照样去掉;TypeScript 装饰器(类、`@d export class`、字段、构造函数和方法的参数、方法隔着别的装饰器、名字最后一节)、行尾 `//` 记号(参数、变量、枚举成员、类字段、接口属性,块注释不算)、JSDoc `@param`(类型、短横、可选、属性不算、export 函数、方法、箭头函数变量、类字段)、JavaScript 默认值参数;预筛和各方言的文件。 - 记号不是说明(`nameResolution.test.ts`,走 `readBlockViews`):Python、TypeScript 枚举成员的行尾记号去掉、前面的字照留、只有记号的没有说明列。 diff --git a/vscode/src/codeReader/editor/messages.ts b/vscode/src/codeReader/editor/messages.ts index 0e812bc..704f7c5 100644 --- a/vscode/src/codeReader/editor/messages.ts +++ b/vscode/src/codeReader/editor/messages.ts @@ -98,7 +98,7 @@ export interface QaMessage { } /** - * VS Code 的 Sticky Scroll 设置(editor.stickyScroll.enabled / editor.stickyScroll.maxLineCount):预览滚进一张卡片的子块时把它钉在顶上, + * VS Code 的 Sticky Scroll 设置(editor.stickyScroll.enabled / editor.stickyScroll.maxLineCount):卡片的开头滚到预览顶上后面、卡片还有一部分看得见时把它钉在顶上, * 外层的在上、里层的在下,最多钉 maxLineCount 行。 */ export interface StickyScrollSettings { diff --git a/vscode/src/codeReader/editor/previewEditor.ts b/vscode/src/codeReader/editor/previewEditor.ts index e915b50..ff47db6 100644 --- a/vscode/src/codeReader/editor/previewEditor.ts +++ b/vscode/src/codeReader/editor/previewEditor.ts @@ -665,7 +665,7 @@ class Preview { this.post({ type: 'hoverDelay', delay: hover.get('delay', 300), hidingDelay: hover.get('hidingDelay', 300) }) } - /** VS Code 的 Sticky Scroll 设置(按这份文件取,语言自己的设置优先):页面按它把外层卡片钉在顶上。 */ + /** VS Code 的 Sticky Scroll 设置(按这份文件取,语言自己的设置优先):页面按它把滚出去一半的卡片钉在顶上。 */ private postStickyScroll(): void { const sticky = workspace.getConfiguration('editor.stickyScroll', this.document) this.post({ type: 'stickyScroll', enabled: sticky.get('enabled', true), maxLineCount: sticky.get('maxLineCount', 5) }) diff --git a/vscode/webview-ui/src/codeReader/App.tsx b/vscode/webview-ui/src/codeReader/App.tsx index 897ae46..8413216 100644 --- a/vscode/webview-ui/src/codeReader/App.tsx +++ b/vscode/webview-ui/src/codeReader/App.tsx @@ -64,7 +64,7 @@ export function App(): ReactElement | null { const [diff, setDiff] = useState(null) /** 收到消息时读眼下哪些块在编辑(消息回调只在挂载时注册一次) */ const editing = useRef(view.editing) - /** 块树所在的页面内容:钉住外层卡片时在这里面量 */ + /** 块树所在的页面内容:钉住卡片时在这里面量 */ const content = useRef(null) // 解析失败的文档没有块,不标差异;删掉的块按块筛选同样藏 const changes = useMemo(() => doc && doc.error === undefined diff --git a/vscode/webview-ui/src/codeReader/BlockTree.test.tsx b/vscode/webview-ui/src/codeReader/BlockTree.test.tsx index 982ccc0..3d791fb 100644 --- a/vscode/webview-ui/src/codeReader/BlockTree.test.tsx +++ b/vscode/webview-ui/src/codeReader/BlockTree.test.tsx @@ -100,7 +100,7 @@ describe('挂在卡片子行上的子块', () => { expect(render({ '0.0': false, '0/p1': true })).not.toContain('启动服务') }) - it('画出来的子层记着它是哪张卡片的(钉住外层卡片时按它认):子行下面那层、卡片后面那层都是;收着时没有', () => { + it('画出来的子层记着它是哪张卡片的(钉住卡片时按它算卡片管到哪):子行下面那层、卡片后面那层都是;收着时没有', () => { expect(render({})).not.toContain('data-owner') expect(render({ '0.0': false, '0': false }).match(/data-owner="0"/g)).toHaveLength(2) }) diff --git a/vscode/webview-ui/src/codeReader/BlockTree.tsx b/vscode/webview-ui/src/codeReader/BlockTree.tsx index 98f5ae5..f2e3233 100644 --- a/vscode/webview-ui/src/codeReader/BlockTree.tsx +++ b/vscode/webview-ui/src/codeReader/BlockTree.tsx @@ -129,7 +129,7 @@ function kindProps(block: BlockView | undefined): { 'data-kind'?: string, 'data- /** * 一层代码块:左边一条竖线标出这一层,每块的子块紧跟在它下面、再往右缩进一层。被筛掉、被盖住的块不画,不留空位,竖线跟着变短。 - * owner 是这一层所属的块(顶层没有,记在 data-owner 上,钉住外层卡片时按它认哪张卡片的子块画出来了),悬停它时竖线换成它的种类色。 + * owner 是这一层所属的块(顶层没有,记在 data-owner 上,钉住卡片时按它算卡片连同子块管到哪、套着几层),悬停它时竖线换成它的种类色。 * 标差异时每张卡片左边外侧一条改动色条,删掉的块收成一行放回原来的位置(见 RemovedCard);「只看改动」时连着没变的块折成一条(UnchangedStrip)。 * slotted:这一层是画在卡片属性表某一行下面的子块(BlockSlot),blocks 只有那几块。 */ diff --git a/vscode/webview-ui/src/codeReader/StickyLines.tsx b/vscode/webview-ui/src/codeReader/StickyLines.tsx index 0527022..fa95fe8 100644 --- a/vscode/webview-ui/src/codeReader/StickyLines.tsx +++ b/vscode/webview-ui/src/codeReader/StickyLines.tsx @@ -41,10 +41,10 @@ export function PinnedHead({ block }: { block: BlockView }): ReactElement { } /** - * 钉在顶上的外层卡片(同 VS Code 编辑器的 Sticky Scroll,设置也用它的):滚进一张卡片的子块、卡片自己的开头已经滚到筛选栏后面时, - * 筛选栏下面钉一行这张卡片的头(PinnedHead),外层的在上、里层的往下摞,每行和那张卡片左右对齐(越里层越往右); - * 这张卡片的子块快滚完时,最里那行被下面的内容顶上去、从上一行下面滑走。哪几张见 pinnedLines,只按页面上画出来的算: - * 折叠着的、被筛掉的、正在编辑的卡片下面没有子块,不钉。点一行把那张卡片滚到顶上(外层还钉着的几行下面)、闪几下。 + * 钉在顶上的卡片(同 VS Code 编辑器的 Sticky Scroll,设置也用它的,一张卡片就是一层):卡片的开头已经滚到筛选栏后面、卡片(连同子块) + * 还有一部分看得见时,筛选栏下面钉一行这张卡片的头(PinnedHead),外层的在上、里层的往下摞,每行和那张卡片左右对齐(越里层越往右); + * 这张卡片快滚完时,最里那行被下面的内容顶上去、从上一行下面滑走。哪几张见 pinnedLines,只按页面上画出来的算: + * 折叠着的卡片自己照样钉(它的子块没画出来,不算);被筛掉的不在页面上,正在编辑的不钉。点一行把那张卡片滚到顶上(外层还钉着的几行下面)、闪几下。 * 钉着的名字不画页面上的高亮(页面上找当前那一处只认卡片里的);跳到定义、悬停看卡片照旧。 * content 是块树所在的页面内容,blocks 是这份文档的块。 */ diff --git a/vscode/webview-ui/src/codeReader/codeReader.css b/vscode/webview-ui/src/codeReader/codeReader.css index 4d847d3..d2a6914 100644 --- a/vscode/webview-ui/src/codeReader/codeReader.css +++ b/vscode/webview-ui/src/codeReader/codeReader.css @@ -1248,7 +1248,7 @@ del.cr-diff-word { } /* - * 钉在顶上的外层卡片(StickyLines.tsx,同 VS Code 编辑器的 Sticky Scroll):从筛选栏下沿往下摞,底色、悬停色、下沿的阴影同编辑器的 + * 钉在顶上的卡片(StickyLines.tsx,同 VS Code 编辑器的 Sticky Scroll):从筛选栏下沿往下摞,底色、悬停色、下沿的阴影同编辑器的 * (--vscode-editorStickyScroll*,主题没给时用编辑器底色、控件阴影)。最里那行被顶上去时从上一行下面滑走(层级低一级),整块跟着变矮。 * 层级在页面内容之上,低于总览条(5)、筛选栏(10)和它的下拉菜单、定义卡片浮窗(40)、下拉列表和悬停提示(50) */ diff --git a/vscode/webview-ui/src/codeReader/stickyScroll.test.ts b/vscode/webview-ui/src/codeReader/stickyScroll.test.ts index cf7ed55..13d16b3 100644 --- a/vscode/webview-ui/src/codeReader/stickyScroll.test.ts +++ b/vscode/webview-ui/src/codeReader/stickyScroll.test.ts @@ -18,6 +18,12 @@ describe('pinnedLines:钉住哪几张卡片', () => { expect(ids(pinnedLines([scope('3', 0, -1, 500)], LINE, 5))).toEqual(['3']) }) + it('没有子块的高卡片(折叠着的函数,下沿就是卡片的):名称那行滚到后面、下面的行还看得见就钉,卡片滚完时被顶上去、再滚就不钉', () => { + expect(pinnedLines([scope('8', 0, -120, 180)], LINE, 5)).toEqual({ scopes: [scope('8', 0, -120, 180)], offset: 0 }) + expect(pinnedLines([scope('8', 0, -290, 10)], LINE, 5)).toMatchObject({ offset: -14 }) + expect(ids(pinnedLines([scope('8', 0, -310, -10), scope('9', 0, -2, 300)], LINE, 5))).toEqual(['9']) + }) + it('套着的几层:外层在上、里层在下,各在自己那一行;开头还没滚过它那一行的不钉', () => { const scopes = [scope('2', 0, -300, 800), scope('2.1', 1, -100, 400), scope('2.1.0', 2, 60, 300)] expect(pinnedLines(scopes, LINE, 5)).toEqual({ scopes: scopes.slice(0, 2), offset: 0 }) diff --git a/vscode/webview-ui/src/codeReader/stickyScroll.ts b/vscode/webview-ui/src/codeReader/stickyScroll.ts index 17b5201..87864d4 100644 --- a/vscode/webview-ui/src/codeReader/stickyScroll.ts +++ b/vscode/webview-ui/src/codeReader/stickyScroll.ts @@ -1,11 +1,11 @@ /** - * 钉在顶上的外层卡片(同 VS Code 编辑器的 Sticky Scroll):滚进一张卡片的子块、卡片自己的开头已经滚出去时,顶上钉一行它的图标和标题, - * 外层的在上、里层的往下摞,每行一样高。竖直坐标都以筛选栏下沿为 0、往下为正(像素)。 + * 钉在顶上的卡片(同 VS Code 编辑器的 Sticky Scroll,一张卡片就是一层):卡片的开头已经滚出去、卡片(连同子块)还有一部分看得见时, + * 顶上钉一行它的图标和标题,外层的在上、里层的往下摞,每行一样高。竖直坐标都以筛选栏下沿为 0、往下为正(像素)。 */ /** - * 页面上一张能钉住的卡片:子块画出来了的(折叠着的、被筛掉的、正在编辑的卡片下面没有)。 - * top 是卡片上沿,bottom 是它子块最后的下沿(卡片比子块还靠下时取卡片的);depth 是它外面套着几张卡片;left、right 是卡片左右边(视口里的 x) + * 页面上一张能钉住的卡片:画出来了、不在编辑的(折叠着的、没有子块的也算;被筛掉的、折起来的子块没画出来,不算)。 + * top 是卡片上沿,bottom 是卡片连同画出来的子块的下沿;depth 是它外面套着几张卡片;left、right 是卡片左右边(视口里的 x) */ export interface StickyScope { id: string @@ -26,7 +26,7 @@ export const NO_PINNED: PinnedLines = { scopes: [], offset: 0 } /** * 钉住哪几张,照 VS Code 的 `StickyScrollController.findScrollWidgetState`:按页面先后(外层在前)看每张,它那一行在 depth × lineHeight; - * 卡片上沿已经在这一行顶上面、子块的下沿还没过这一行顶时钉住。子块的下沿高过这一行的下沿时,这一行跟着往上推。 + * 卡片上沿已经在这一行顶上面、下沿(连同子块)还没过这一行顶时钉住。下沿高过这一行的下沿时,这一行跟着往上推。 * 钉满 limit 行就不再往里看:套得深时留外层的,同 VS Code。scopes 要按页面先后排。 */ export function pinnedLines(scopes: readonly StickyScope[], lineHeight: number, limit: number): PinnedLines { diff --git a/vscode/webview-ui/src/codeReader/types.ts b/vscode/webview-ui/src/codeReader/types.ts index 41d757b..385ddb2 100644 --- a/vscode/webview-ui/src/codeReader/types.ts +++ b/vscode/webview-ui/src/codeReader/types.ts @@ -409,7 +409,7 @@ export interface DocumentDiff { } /** - * VS Code 的 Sticky Scroll 设置(editor.stickyScroll.enabled / editor.stickyScroll.maxLineCount):预览滚进一张卡片的子块时把它钉在顶上, + * VS Code 的 Sticky Scroll 设置(editor.stickyScroll.enabled / editor.stickyScroll.maxLineCount):卡片的开头滚到预览顶上后面、卡片还有一部分看得见时把它钉在顶上, * 外层的在上、里层的在下,最多钉 maxLineCount 行。 */ export interface StickyScrollSettings { diff --git a/vscode/webview-ui/src/codeReader/useReveal.ts b/vscode/webview-ui/src/codeReader/useReveal.ts index 383e648..13613a6 100644 --- a/vscode/webview-ui/src/codeReader/useReveal.ts +++ b/vscode/webview-ui/src/codeReader/useReveal.ts @@ -25,7 +25,7 @@ export function flash(el: HTMLElement): void { el.setAttribute('data-flash', '') } -/** 把 el 滚进钉住的筛选栏和外层卡片下面的视口。已经整个看得见就不滚。beforeScroll 在真正滚动前拿到将要落到的位置。 */ +/** 把 el 滚进钉住的筛选栏和卡片下面的视口。已经整个看得见就不滚。beforeScroll 在真正滚动前拿到将要落到的位置。 */ export function showInViewport(el: HTMLElement, beforeScroll?: (position: ScrollPosition) => void): void { const rect = el.getBoundingClientRect() const pinned = pinnedHeight() + stickyHeight() diff --git a/vscode/webview-ui/src/codeReader/useScrollPosition.ts b/vscode/webview-ui/src/codeReader/useScrollPosition.ts index c6be5a9..7258797 100644 --- a/vscode/webview-ui/src/codeReader/useScrollPosition.ts +++ b/vscode/webview-ui/src/codeReader/useScrollPosition.ts @@ -12,7 +12,7 @@ export function pinnedHeight(): number { return document.querySelector('.cr-filter-bar')?.offsetHeight ?? 0 } -/** 筛选栏有多高,跟着它变(换行、多出第二行时):钉在它下面的东西(总览条、钉住的外层卡片)从这儿开始。 */ +/** 筛选栏有多高,跟着它变(换行、多出第二行时):钉在它下面的东西(总览条、钉住的卡片)从这儿开始。 */ export function usePinnedHeight(): number { const [height, setHeight] = useState(0) useLayoutEffect(() => { diff --git a/vscode/webview-ui/src/codeReader/useStickyScroll.ts b/vscode/webview-ui/src/codeReader/useStickyScroll.ts index 4371b86..3571020 100644 --- a/vscode/webview-ui/src/codeReader/useStickyScroll.ts +++ b/vscode/webview-ui/src/codeReader/useStickyScroll.ts @@ -22,8 +22,12 @@ function limitOf(maxLineCount: number): number { return lineLimit(window.innerHeight - pinnedHeight(), STICKY_LINE_HEIGHT, maxLineCount) } -/** root 里能钉住的卡片:画出了子层的那些(子层带 data-owner,挂在子行上的子块那层在卡片里面),按页面先后;竖直坐标以筛选栏下沿为 0 */ -function measureScopes(root: HTMLElement): StickyScope[] { +/** + * root 里能钉住的卡片,按页面先后,竖直坐标以筛选栏下沿为 0:画出来了、不在编辑的每一张(BlockCard 带 data-line;删掉的块那张没有), + * 折叠着的、没有子块的也算,卡片自己就是一层。下沿算到它画出来的子块的最后(它名下的子层带 data-owner,挂在子行上的子块那层在卡片里面)。 + * 只留可能钉住的:上沿在钉住的那几行(高 band)以内或更高、下沿还在筛选栏下沿以下 + */ +function measureScopes(root: HTMLElement, band: number): StickyScope[] { const bottoms = new Map() for (const level of root.querySelectorAll('.cr-level[data-owner]')) { const owner = level.dataset.owner ?? '' @@ -31,13 +35,13 @@ function measureScopes(root: HTMLElement): StickyScope[] { } const base = pinnedHeight() const scopes: StickyScope[] = [] - for (const card of root.querySelectorAll('.cr-block[data-id]')) { + for (const card of root.querySelectorAll('.cr-block[data-line]:not([data-editing])')) { const id = card.dataset.id ?? '' - const levelBottom = bottoms.get(id) - if (levelBottom === undefined) - continue const rect = card.getBoundingClientRect() - scopes.push({ id, depth: depthOf(card), top: rect.top - base, bottom: Math.max(rect.bottom, levelBottom) - base, left: rect.left, right: rect.right }) + const top = rect.top - base + const bottom = Math.max(rect.bottom, bottoms.get(id) ?? rect.bottom) - base + if (top < band && bottom >= 0) + scopes.push({ id, depth: depthOf(card), top, bottom, left: rect.left, right: rect.right }) } return scopes } @@ -65,7 +69,8 @@ export function useStickyLines(content: RefObject, maxLineCo const update = (): void => { frame = 0 const root = content.current - const next = root ? pinnedLines(measureScopes(root), STICKY_LINE_HEIGHT, limitOf(maxLineCount)) : NO_PINNED + const limit = limitOf(maxLineCount) + const next = root ? pinnedLines(measureScopes(root, limit * STICKY_LINE_HEIGHT), STICKY_LINE_HEIGHT, limit) : NO_PINNED setLines(prev => samePinned(prev, next) ? prev : next) } schedule.current = () => {