✨ feat(vscode): 代码阅读库规则卡片不再吞掉字段中间的方法,编辑时收起子块
This commit is contained in:
1 parent
c803fd457f
commit
2251e67ef8
15 files changed
+280
-54
No files matched your search
@@ -77,7 +77,8 @@
|
||||
- 预览:函数、类、导入、常量、变量块是属性表(见下节),注释块是 Markdown 说明(见「注释的 Markdown 排版」),其余块是一行译文;按词着色,等宽字体(中文部分用系统中文字体回退)。一行译文的块带说明(Python 字段的属性 docstring)时,说明按 Markdown 排在译文下面。
|
||||
- 编辑:这一块的源码(按语法树着色),可以直接改;蓝色边框。按钮文字「预览」;Esc 也回到预览。
|
||||
- 复合语句(函数、条件、循环……)块里只放它自己的头部那几行(主体交给子块);收尾括号、空行不成块。函数、类的说明(JSDoc / docstring)也算在函数、类块的头部里;Python 常量、变量、字段的属性 docstring 算在它那一块里(中间隔着的空行也是)。
|
||||
- 库规则认领的块(数据表卡片等,见「库规则」):种类标签的字和颜色由规则给,内容是规则给的属性表;它的编辑范围是原来的头部加上并进来的成员,「编辑」时一起改。
|
||||
- 库规则认领的块(数据表卡片等,见「库规则」):种类标签的字和颜色由规则给,内容是规则给的属性表;它的编辑范围是原块整条语句(类就是整个类),「编辑」时整段一起改。规则没并进卡片的子块(方法、别的成员)不管写在类主体哪儿(两个字段中间也算),都照源码顺序是卡片的子块,各自的「编辑」照旧只改自己那几行。
|
||||
- 同一行不会同时在两处能改(能同时编辑的几段互不重叠):一块在编辑时,编辑范围和它重叠的子孙块不画(卡片编辑时它下面的子块全收起来,一行写完的 `if x: return 1` 编辑时同一行的子块也不画),回到预览就照原样出来;点一块的「编辑」时,编辑范围和它重叠的别的块退出编辑。跳到定义的目标块正被盖住时闪盖住它的那块。
|
||||
- 折叠:函数、类块和库规则认领的块打开时默认收起子块(属性表照常显示),其余块默认展开;用户点过折叠箭头的块按他点的,记在视图状态里(见「预览数据与消息」)。
|
||||
- 顶部筛选栏:见「筛选」;翻译下拉框(多于一种翻译时才显示)也在栏里。
|
||||
- 解析失败:显示错误原因,并提示用「编辑」查看源码。
|
||||
@@ -426,7 +427,7 @@ export interface RuleBlock {
|
||||
rows: PropertyRow[]
|
||||
defines?: SourceRange // 块自己的名字(类名):跳到的定义是它时闪整块
|
||||
attributes?: BlockAttributes // 规则自己的筛选属性,叠在宿主从原块推出的上面;kind 填了不算
|
||||
absorbs: CodeBlock[] // 并进来的子块(原块 children 里的对象本身),不再单独成块
|
||||
absorbs: CodeBlock[] // 并进来的子块(原块 children 里的对象本身),不再单独成块;没并的照源码顺序留作子块,编辑范围是原块整条语句
|
||||
}
|
||||
|
||||
/** 规则看一块时宿主给的。 */
|
||||
@@ -505,7 +506,7 @@ export interface CodeReaderApi {
|
||||
- 启用:宿主收集这份源码里各层导入块的 `ImportFacts.sources`(去掉引号),有一个等于规则的某个库、或以「库.」「库/」开头(子模块),规则才对这份源码启用。规则不用自己读导入语句。
|
||||
- 认领:宿主从顶层往下每块问一遍启用的规则,按注册顺序,第一个认领的算(先注册的优先);被认领的块留下的子块照样问。
|
||||
- 出错:规则抛错、或替身不合约定(种类 id 没带规则 id 前缀、并进来的不是这块的子块、属性表里的引用标的不是自己 / 规则没给 `resolve` / 引用的段没带下标)时,宿主记一条警告日志(输出面板「Superpowers」),当它没认、接着问下一个规则;都没认这块照常显示。预览不会因为规则出错而失败。
|
||||
- 并进来的子块:不再单独成块。替身的编辑范围从原块头部开始,到最后一个并进来的子块(整条语句)结束,连成一段;夹在这段中间的子块(注释,或者写在字段中间的方法)也跟着并进来,保证每一行只在一块里能改;这段以后的子块(方法等)照常是替身的子块。
|
||||
- 并进来的子块:不再单独成块,由规则挑(`absorbs`),宿主不替规则多并。没并进来的子块(方法、规则不讲的注释)不管写在哪(字段中间也算),都照源码顺序是替身的子块,预览里照常看得见。替身的编辑范围是原块整条语句(从头部开始,到原块连同各子块的最后一行,类就是整个类),盖住留下的子块的行:替身编辑时预览不画这些子块,同一行不会同时在两处能改(见「预览(代码块树)」)。内置规则并成员时连同紧挨在它前面的注释块一起并(讲的是这个成员),讲方法的注释、最后面的注释照常留下。
|
||||
- 替身在预览里:种类标签的字、颜色取 `badge`(暗色、亮色主题各取一个),内容是 `rows`;筛选的 `kind` 是替身的种类 id,其余属性从原块推出(原来的类是公共的,数据表也是公共的),叠上规则给的;筛选栏里这个种类叫 `badge.label`;和类块一样默认收起子块。
|
||||
|
||||
### 规则自己查定义
|
||||
@@ -575,7 +576,7 @@ export interface CodeReaderApi {
|
||||
|
||||
说明:`comment=` 的字符串(comment 色);没写 `comment=` 时用字段的属性 docstring(`context.doc` 给的原文,comment 色;它随字段并进卡片)。关系的目标取第一个位置参数(字符串去掉引号),没写就用 `Mapped[...]` 里的类型;说明写「对方字段 X」(`back_populates` / `backref`)、「单个对象」(`uselist=False`)、「中间表 X」(`secondary`),用 ` · ` 隔开。
|
||||
|
||||
并进卡片的子块:`__tablename__`、`__table_args__`、每个字段和关系的赋值;`__mapper_args__`、普通属性、方法照常是子块。
|
||||
并进卡片的子块:`__tablename__`、`__table_args__`、每个字段和关系的赋值,连同紧挨在它们前面的注释块;`__mapper_args__`、普通属性、方法(写在两个字段中间也算)和讲它们的注释照常是子块。
|
||||
|
||||
样例(luffy `referral/models.py` 的 `ReferralRelation`,测试里逐字核对):
|
||||
|
||||
@@ -803,7 +804,7 @@ Python 内置类型(仅注解里):str 字符串、int 整数、float 浮
|
||||
interface BlockViewBase {
|
||||
id: string // 在树里的位置,例如 '6.1.0'
|
||||
kind: string // BlockKind,或库规则认领的块的种类 id(sqlalchemy.table)
|
||||
startLine: number // 编辑范围的起止行,0 起:一般是 header,库规则认领的块到最后一个并进来的子块为止
|
||||
startLine: number // 编辑范围的起止行,0 起:一般是 header,库规则认领的块是原块整条语句(盖住它留下的子块)
|
||||
endLine: number
|
||||
code: string // 编辑范围的源码,去掉共同缩进
|
||||
codeTokens: Segment[] // 源码着色(拼起来等于 code)
|
||||
@@ -866,13 +867,14 @@ vscode/src/codeReader/
|
||||
runtime.ts web-tree-sitter 初始化、语法包加载、解析
|
||||
blockView.ts CodeBlock → BlockView(源码着色、去缩进、调用翻译)
|
||||
attributes.ts 块的筛选属性(从 facts 推出通用属性 + 语言自己报的)、文件里出现过的属性和块数
|
||||
libraryRules.ts 库规则的宿主部分:按导入启用、按注册顺序认领、出错退回、并进来的范围、查规则标的引用
|
||||
libraryRules.ts 库规则的宿主部分:按导入启用、按注册顺序认领、出错退回、编辑范围和留下的子块、查规则标的引用
|
||||
workspaceIndex.ts 库规则的工作区索引(WorkspaceIndex)的宿主部分:建、缓存、跟着文件改动更新(不碰 VS Code)
|
||||
workspaceFiles.ts 工作区索引在 VS Code 里怎么找文件、读文件、盯改动(排除规则)
|
||||
rules/sqlalchemy.ts 内置 SQLAlchemy 库规则(只 import contract 和同目录的共用代码)
|
||||
rules/celery.ts 内置 Celery 库规则:两张卡片、频率换算、任务名索引
|
||||
rules/pythonTree.ts Python 库规则共用的读语法树的办法(名字最后一节、字符串内容、调用的参数、别名)
|
||||
rules/segments.ts 库规则共用的拼属性表文字的办法
|
||||
rules/children.ts 库规则共用的挑子块的办法(成员连同紧挨在它前面的注释块)
|
||||
languages/typescript.ts
|
||||
languages/python.ts (iteration:推导式的结构)
|
||||
translators/zhTemplate.ts 模板直译引擎(按语言的表驱动,词性、遍历结构从 SourceDocument.language 取;推导式排进框见 iterationGroup)
|
||||
@@ -892,7 +894,7 @@ vscode/src/codeReader/
|
||||
editor/definitionAction.ts 查到的定义 → 怎么跳(纯函数)
|
||||
editor/occurrences.ts 查名字的各处出现(vscode.executeDocumentHighlights)
|
||||
index.ts registerCodeReader(context): CodeReaderApi
|
||||
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts)
|
||||
vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vite 第二个入口;属性表在 PropertyGrid.tsx,子行排成表格还是列表在 subRows.ts,着色的词和框在 SegmentText.tsx,Markdown 渲染在 markdown.ts / TextPreview.tsx,筛选栏在 FilterBar.tsx,筛选规则在 filter.ts,编辑时盖住哪些块、点「编辑」退出哪些块在 editing.ts,高亮各处出现的规则在 occurrences.ts、接到页面上在 useOccurrences.ts)
|
||||
```
|
||||
|
||||
## 测试
|
||||
@@ -912,8 +914,9 @@ vscode/webview-ui/code-reader.html + src/codeReader/ 预览界面(React,Vit
|
||||
- 跳到定义:TS / Python 译文和属性表里的名字、点分模块名每一节、模块字符串带下标且指回源码原文,关键字、运算符、说明词不带(前面有中文注释也对得上);查到的定义 → 不动 / 原地 / 预览 / 普通编辑器 / 挑选框;webview 里按下标找目标块、要展开的祖先块和属性行,滚动位置。闪哪一处(TS、Python 源码走完整的语言插件 + 翻译插件 + 视图数据):参数 → 它那个子行,导入项 → 它那一项,多变量声明里的一个 → 它的名称行,类的泛型参数 → 「类型参数」那一行,函数名 / 类名 / 单个变量名(含解构模式)→ 整块,循环变量 → 所在的块,翻译插件没填 `defines` → 整块(照样展开属性行);翻译插件给参数、导入项、声明的变量、泛型参数标的范围指回源码原文;块自己的名字只有函数、类和单变量声明有。
|
||||
- 高亮各处出现:段的源码范围和出现重叠才亮、边界相接不算、没下标的不亮,点分模块名每一节各按自己的范围认(真实的 Python 译文和属性表:定义处按写入、用到处按读取);写入 → 强高亮色,读取、按文字 → 普通高亮色,同时碰上按写入;单击名字问、点空白处清、点按钮 / 链接 / 筛选栏不动、名字在按钮里照样问,拖选、双击第二下、带修饰键的点击不动,⌘ / Ctrl + 点击照旧发跳到定义、不问出现处;回答连同名字显示、空回答不亮、换名字时回答到前旧的留着、迟到的回答丢掉、答过「没有」再点照样再问、清掉后才到的回答丢掉、换文档版本或翻译清掉而同版本同翻译重发不清、没有文档时不问。
|
||||
- 注册表:注册 / 注销后 `languageIds` 与可选翻译正确;同一语言多个翻译时按 `globalState` 选中;库规则按语言过滤、保持注册顺序,注销后不再给。
|
||||
- 库规则(宿主):导入了库(`from sqlalchemy.orm import …`、`import sqlalchemy as sa`、`import os, sqlalchemy`、函数里的导入)才启用,`sqlalchemy_utils`、相对导入 `.sqlalchemy` 不算;先注册的规则优先;规则抛错、替身不合约定时记下来、接着问下一个、都没认照常显示,document 消息不带 error;编辑范围到最后一个并进来的子块、夹在中间的注释并进来、之后的子块留下,每一行最多在一块里能改;筛选的种类计数、名字取规则的标签;`rules/` 下的非测试文件只 import contract。
|
||||
- SQLAlchemy 规则:luffy `referral/models.py` 的节选(`fixtures/sqlalchemyModels.py`)里 `ReferralRelation` 的属性表逐字同上面的样例,其余模型覆盖 `Mapped[bool]`、`int | None` 可空、`default=None`、`server_default`、别名的 `PgUUID`、`_enum` 辅助函数、关系、唯一约束、删除时置空 / 级联删除;枚举类、函数照常显示;卡片的编辑范围是整个类、每行只在一块里;类名、字段名的定义范围和带下标的词都指回源码原文;webview 里跳到类名闪整块、跳到字段名只闪字段表里那一行并先展开「字段」那一行,卡片默认折叠;`import sqlalchemy as sa` 的老写法 `Column`、类型全名、只写注解的字段、不认得的类型照写法、docstring 当说明、方法留作子块。
|
||||
- 库规则(宿主):导入了库(`from sqlalchemy.orm import …`、`import sqlalchemy as sa`、`import os, sqlalchemy`、函数里的导入)才启用,`sqlalchemy_utils`、相对导入 `.sqlalchemy` 不算;先注册的规则优先;规则抛错、替身不合约定时记下来、接着问下一个、都没认照常显示,document 消息不带 error;编辑范围是整个类、没并进来的子块(夹在中间的注释、两个字段中间的方法)照源码顺序留下,卡片「编辑」写回整个类,能编辑的范围只在块和它的子孙之间重叠;筛选的种类计数、名字取规则的标签;`rules/` 下的非测试文件只 import contract。
|
||||
- 同一行不在两处能改(webview `editing.test.ts`):卡片编辑时盖住它留下的子块(字段中间的方法连同主体),跳到被盖住的块改闪卡片;只编辑头部的块不盖住子块,一行写完的块盖住同一行的子块;点「编辑」时重叠的块退出编辑;任意点法下正在编辑、画出来的块两两不重叠。
|
||||
- SQLAlchemy 规则:luffy `referral/models.py` 的节选(`fixtures/sqlalchemyModels.py`)里 `ReferralRelation` 的属性表逐字同上面的样例,其余模型覆盖 `Mapped[bool]`、`int | None` 可空、`default=None`、`server_default`、别名的 `PgUUID`、`_enum` 辅助函数、关系、唯一约束、删除时置空 / 级联删除;枚举类、函数照常显示;卡片的编辑范围是整个类、每行只在一块里;两个字段中间写着的方法照常是子块、卡片「编辑」写回整个类、字段前面的注释并进卡片、讲方法的注释留下;类名、字段名的定义范围和带下标的词都指回源码原文;webview 里跳到类名闪整块、跳到字段名只闪字段表里那一行并先展开「字段」那一行,卡片默认折叠;`import sqlalchemy as sa` 的老写法 `Column`、类型全名、只写注解的字段、不认得的类型照写法、docstring 当说明、方法留作子块。
|
||||
- 库规则的引用:引用标的不是自己、规则没给 `resolve`、段没带下标时当替身不合约定;查引用交给标它的规则,规则不在、没给 `resolve`、出错时记下来、当作没有定义;视图数据里的引用(下标 → 引用)只从规则认领的块的属性表里取,查定义时是引用的问规则、其余问语言扩展,只对记下它的那个文档版本有效(`editor/definitionSources.test.ts`)。
|
||||
- 工作区索引(内存里的工作区,`MemoryIndexHost`):第一次查才建、同一个 id 只建一份;查到的是名字的行列(列按 UTF-16)、照文件和行排好;预筛不过的不解析;改了只重看那一份、新建加进来、删目录连里面的去掉;新建 / 删掉问过在不在的文件(`__init__.py`)时问过它的文件重看;建索引期间的改动建好后补上;读不了、规则出错的记下来、别的照常;dispose 后不再盯着。
|
||||
- Celery 规则:`fixtures/celeryApp.py` 的应用卡片和定时任务卡片逐字同上面的样例(列名、「12 条,另有展开 1 处」、行尾注释、条目里单独成行的注释、跨行的 `10⏎ * 60`、`每 get_settings().… 秒`、展开一行);任务名是引用、下标指回源码原文;应用卡片的位置参数按参数顺序认、别的参数各一行、展开、属性 docstring、别名;没导入 celery、不是 `Celery(...)` 的不认;频率换算(秒数算式、`timedelta`、`crontab` 各种写法、`schedule`、换算不了的「每 X 秒」和照写法);说明取哪些注释;条目名和任务名不同、没写任务名、任务名不是字符串、值不是字典、选项一列有才有;`conf.update(beat_schedule=…)`、不带 `beat_schedule` 的 `conf.update` 和 `autodiscover_tasks` 照常。任务名索引:显式 `name=`、默认「模块路径.函数名」(包、`src` 下的命名空间包、没有包)、`@shared_task` / `@shared_task()` / `@app.task` 不带括号 / 老写法 `@task`、`async def`、函数里的只认显式名字、`name=` 不是字符串的不认、同名几处都给、文件改了和新建 `__init__.py` 后跟着变、没有任务装饰器的文件不解析。
|
||||
|
||||
@@ -2,14 +2,17 @@ import type { BlockAttributes, CodeBlock, KindBadge, LanguageAdapter, LibraryRul
|
||||
import type { TreeSitterRuntime } from './runtime'
|
||||
import { blockAttributes } from './attributes'
|
||||
import { commonIndent, splitLines } from './editText'
|
||||
import { absorbedRange, activeRules, claimBlock, importsIn } from './libraryRules'
|
||||
import { activeRules, claimBlock, claimedShape, importsIn } from './libraryRules'
|
||||
|
||||
interface BlockViewBase {
|
||||
/** 在树里的位置,例如 '6.1.0'。 */
|
||||
id: string
|
||||
/** BlockKind,或库规则认领的块的种类 id(RuleBlock.kind,带点,例如 sqlalchemy.table)。 */
|
||||
kind: string
|
||||
/** 编辑范围的起止行,0 起:一般是 header;库规则认领的块到最后一个并进来的子块为止(见 RuleBlock.absorbs)。 */
|
||||
/**
|
||||
* 编辑范围的起止行,0 起:一般是 header;库规则认领的块是原块整条语句(见 claimedShape),盖住它留下的子块的行。
|
||||
* 同一时刻能编辑的几段不重叠:块编辑时,webview 不画编辑范围和它重叠的子孙块,点「编辑」时和它重叠的别的块退出编辑。
|
||||
*/
|
||||
startLine: number
|
||||
endLine: number
|
||||
/** 编辑范围的整行源码,去掉共同缩进。 */
|
||||
@@ -155,11 +158,11 @@ export function toBlockViews(blocks: CodeBlock[], source: string, adapter: Langu
|
||||
const claim = active.length > 0 ? claimBlock(block, active, context, rules.report) : undefined
|
||||
if (claim) {
|
||||
const { kind, badge, rows, defines, attributes } = claim.block
|
||||
const { endRow, kept } = absorbedRange(block, claim.block.absorbs)
|
||||
const { span, kept } = claimedShape(block, claim.block.absorbs)
|
||||
return {
|
||||
id,
|
||||
kind,
|
||||
...sourceRows(root, block.header.startRow, endRow),
|
||||
...sourceRows(root, span.startRow, span.endRow),
|
||||
...(defines && { defines: { startIndex: defines.startIndex, endIndex: defines.endIndex } }),
|
||||
attributes: { ...blockAttributes(block), ...attributes, kind },
|
||||
badge,
|
||||
|
||||
@@ -332,8 +332,8 @@ export interface RuleBlock {
|
||||
attributes?: BlockAttributes
|
||||
/**
|
||||
* 并进这一块的子块(原块 children 里的对象本身):不再单独成块,内容由 rows 讲。
|
||||
* 这一块的编辑范围从原块头部开始、到最后一个并进来的子块结束,连成一段;夹在这段中间的别的子块(注释等)也跟着并进来,
|
||||
* 不然同一行能在两处改。这段以后的子块(方法等)照常是这一块的子块,也照常交给各规则看。
|
||||
* 没并进来的子块(方法、别的成员、规则不讲的注释)不管写在哪,都照源码顺序照常是这一块的子块,也照常交给各规则看。
|
||||
* 这一块的编辑范围是原块整条语句(类就是整个类),盖住留下的子块:它编辑时预览不画这些子块,同一行不会同时在两处能改。
|
||||
*/
|
||||
absorbs: CodeBlock[]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CodeBlock, LibraryRule, RuleBlock, RuleLocation, RuleWorkspace, Se
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { fileAttributes } from './attributes'
|
||||
import { toBlockViews } from './blockView'
|
||||
import { replaceLines } from './editText'
|
||||
import { createPythonAdapter } from './languages/python'
|
||||
import { activeRules, fromLibrary, importsIn, resolveReference } from './libraryRules'
|
||||
import { WASM_DIR, withBlocks } from './testSupport'
|
||||
@@ -90,7 +91,7 @@ describe('库规则:启用', () => {
|
||||
})
|
||||
|
||||
describe('库规则:认领', () => {
|
||||
it('认领的块换成替身:种类、标签、编辑范围到最后一个并进来的子块,夹在中间的注释也并进来,之后的子块留下', async () => {
|
||||
it('认领的块换成替身:种类、标签、编辑范围是整个类;规则没并进来的子块(夹在中间的注释、方法)照源码顺序留下', async () => {
|
||||
const rule: LibraryRule = {
|
||||
...cardRule('orm'),
|
||||
claim: (block) => {
|
||||
@@ -105,22 +106,46 @@ describe('库规则:认领', () => {
|
||||
kind: 'orm.card',
|
||||
badge: { label: '卡片' },
|
||||
startLine: 2,
|
||||
endLine: 5,
|
||||
code: 'class User(Base):\n __tablename__ = "users"\n # 基本信息\n name = mapped_column(String)',
|
||||
endLine: 8,
|
||||
code: 'class User(Base):\n __tablename__ = "users"\n # 基本信息\n name = mapped_column(String)\n\n def greet(self) -> str:\n return self.name',
|
||||
attributes: { kind: 'orm.card', access: 'public' },
|
||||
properties: [{ label: '名称' }],
|
||||
})
|
||||
expect(card.children.map(child => [child.id, child.kind, child.startLine])).toEqual([['1.0', 'function', 7]])
|
||||
expect(card.children.map(child => [child.id, child.kind, child.startLine])).toEqual([['1.0', 'comment', 4], ['1.1', 'function', 7]])
|
||||
})
|
||||
|
||||
it('每一行最多在一块里能编辑:并进来的行只在替身里', async () => {
|
||||
it('两个字段中间写着的方法:预览里照常是卡片的子块;卡片「编辑」写回整个类', async () => {
|
||||
const source = [
|
||||
'from sqlalchemy.orm import mapped_column',
|
||||
'',
|
||||
'class User(Base):',
|
||||
' id = mapped_column(Integer)',
|
||||
'',
|
||||
' def greet(self) -> str:',
|
||||
' return self.name',
|
||||
'',
|
||||
' name = mapped_column(String)',
|
||||
'',
|
||||
].join('\n')
|
||||
const fields: LibraryRule = { ...cardRule('orm'), claim: block => block.kind === 'class'
|
||||
? { kind: 'orm.card', badge: { label: '卡片', color: { dark: '#fff', light: '#000' } }, rows: [], absorbs: block.children.filter(child => child.kind === 'variable') }
|
||||
: undefined }
|
||||
const [, card] = await views(source, [fields])
|
||||
expect(card.children.map(child => [child.kind, child.startLine, child.endLine])).toEqual([['function', 5, 5]])
|
||||
expect([card.startLine, card.endLine]).toEqual([2, 8])
|
||||
expect(replaceLines(source, card.startLine, card.endLine, card.code)).toBe(source)
|
||||
const edited = replaceLines(source, card.startLine, card.endLine, card.code.replace('greet', 'hello').replace('String', 'Text'))
|
||||
expect(edited).toBe(source.replace('greet', 'hello').replace('String', 'Text'))
|
||||
})
|
||||
|
||||
it('能编辑的范围只在块和它的子孙之间重叠:卡片盖住的是它自己留下的子块,别的块互不重叠', async () => {
|
||||
const list = flatten(await views(MODEL, [cardRule('orm', {})]))
|
||||
const owners = new Map<number, string[]>()
|
||||
for (const view of list) {
|
||||
for (let line = view.startLine; line <= view.endLine; line++)
|
||||
owners.set(line, [...owners.get(line) ?? [], view.id])
|
||||
const related = (a: string, b: string): boolean => a.startsWith(`${b}.`) || b.startsWith(`${a}.`)
|
||||
for (const a of list) {
|
||||
for (const b of list.filter(other => other !== a && !related(a.id, other.id)))
|
||||
expect(a.startLine > b.endLine || b.startLine > a.endLine, `${a.id} / ${b.id}`).toBe(true)
|
||||
}
|
||||
expect([...owners.values()].filter(ids => ids.length > 1)).toEqual([])
|
||||
expect(list.filter(view => view.id.startsWith('1.')).every(view => view.startLine >= list[1].startLine && view.endLine <= list[1].endLine)).toBe(true)
|
||||
})
|
||||
|
||||
it('几个规则都认同一块时先注册的算', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CodeBlock, ImportFacts, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleBlock, RuleContext, RuleLocation, RuleReference, RuleWorkspace, Segment } from './contract'
|
||||
import type { CodeBlock, ImportFacts, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleBlock, RuleContext, RuleLocation, RuleReference, RuleWorkspace, Segment, SourceSpan } from './contract'
|
||||
|
||||
/** 宿主这边怎么用库规则:什么时候启用、问哪个规则、认领了之后哪些子块并进去。规则本身只经 contract 拿信息。 */
|
||||
/** 宿主这边怎么用库规则:什么时候启用、问哪个规则、认领了之后编辑哪一段、留下哪些子块。规则本身只经 contract 拿信息。 */
|
||||
|
||||
/** 这份源码里各处导入的 facts,照源码顺序(先父块、再子块)。 */
|
||||
export function importsIn(blocks: readonly CodeBlock[]): ImportFacts[] {
|
||||
@@ -82,13 +82,23 @@ export function claimBlock(block: CodeBlock, rules: readonly LibraryRule[], cont
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 源码里一个节点占的那段。 */
|
||||
function nodeSpan(node: Node): SourceSpan {
|
||||
return { startIndex: node.startIndex, endIndex: node.endIndex, startRow: node.startPosition.row, endRow: node.endPosition.row }
|
||||
}
|
||||
|
||||
/**
|
||||
* 替身并进了哪几行、留下哪些子块:编辑范围从原块头部开始,到最后一个并进来的子块(整条语句)结束;
|
||||
* 起始行落在这段里的子块都并进来(包括夹在中间、规则没点名的),之后的留下。
|
||||
* 认领的块的编辑范围和留下的子块:编辑范围是原块整条语句(从头部开始,到它连同各子块的最后一行,类就是整个类),「编辑」时整段一起改;
|
||||
* 规则没并进来的子块(方法、别的成员)不管写在哪,都照源码顺序留作它的子块。
|
||||
* 这些子块的行也在这段里:webview 在这块编辑时不画它们,同一行不会同时在两处能改。
|
||||
*/
|
||||
export function absorbedRange(block: CodeBlock, absorbs: readonly CodeBlock[]): { endRow: number, kept: CodeBlock[] } {
|
||||
const endRow = Math.max(block.header.endRow, ...absorbs.map(child => Math.max(child.header.endRow, child.node.endPosition.row)))
|
||||
return { endRow, kept: block.children.filter(child => child.header.startRow > endRow) }
|
||||
export function claimedShape(block: CodeBlock, absorbs: readonly CodeBlock[]): { span: SourceSpan, kept: CodeBlock[] } {
|
||||
const ends = [block.header, nodeSpan(block.node), ...block.children.flatMap(child => [child.header, nodeSpan(child.node)])]
|
||||
const last = ends.reduce((end, span) => span.endIndex > end.endIndex ? span : end)
|
||||
return {
|
||||
span: { ...block.header, endIndex: last.endIndex, endRow: last.endRow },
|
||||
kept: block.children.filter(child => !absorbs.includes(child)),
|
||||
}
|
||||
}
|
||||
|
||||
/** 文字里(连框里)带引用的段。 */
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { CodeBlock } from '../contract'
|
||||
|
||||
/** 库规则共用的挑子块的办法。 */
|
||||
|
||||
/** 紧挨在 child 前面的注释块(连着几块都算,中间没隔别的子块),照源码顺序:讲的是它下面的这个 child。 */
|
||||
export function leadingComments(children: readonly CodeBlock[], child: CodeBlock): CodeBlock[] {
|
||||
const at = children.indexOf(child)
|
||||
let from = at
|
||||
while (from > 0 && children[from - 1].kind === 'comment')
|
||||
from--
|
||||
return children.slice(from, at)
|
||||
}
|
||||
|
||||
/**
|
||||
* 要并进卡片的子块:成员连同各自前面紧挨着的注释块,照源码顺序。成员并进来了,讲它的注释跟着并,不在卡片下面单独留一块;
|
||||
* 讲别的子块(方法)的注释、最后一个子块后面的注释照常留下。
|
||||
*/
|
||||
export function withLeadingComments(children: readonly CodeBlock[], members: readonly CodeBlock[]): CodeBlock[] {
|
||||
return children.filter(child => members.includes(child))
|
||||
.flatMap(member => [...leadingComments(children, member), member])
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { BlockView } from '../blockView'
|
||||
import type { Inline, PropertyRow, Segment } from '../contract'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from '../blockView'
|
||||
import { replaceLines } from '../editText'
|
||||
import { createPythonAdapter } from '../languages/python'
|
||||
import { flatSegments, propertyLines, readFixture, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||
@@ -122,7 +123,7 @@ describe('sqlAlchemy 库规则', () => {
|
||||
expect(view.children, view.id).toEqual([])
|
||||
})
|
||||
|
||||
it('并进来的行只在卡片里能编辑,卡片的编辑范围是整个类', async () => {
|
||||
it('卡片的编辑范围是整个类,每行只在一块里能编辑', async () => {
|
||||
const lines = SNAPSHOT.split('\n')
|
||||
const table = await model(SNAPSHOT, 'ReferralRelation')
|
||||
expect(lines[table.startLine]).toBe('class ReferralRelation(IntPK, TimestampMixin, Base):')
|
||||
@@ -137,6 +138,32 @@ describe('sqlAlchemy 库规则', () => {
|
||||
expect(owners.get(lines.indexOf(' __tablename__ = "referral_relations"'))).toEqual([table.id])
|
||||
})
|
||||
|
||||
it('两个字段中间写着的方法照常是子块,卡片编辑范围盖住它、写回整个类;字段前面的注释并进卡片', async () => {
|
||||
const source = [
|
||||
'from sqlalchemy.orm import Mapped, mapped_column',
|
||||
'',
|
||||
'',
|
||||
'class Order(Base):',
|
||||
' __tablename__ = "orders"',
|
||||
'',
|
||||
' # 金额',
|
||||
' amount: Mapped[int] = mapped_column(Integer)',
|
||||
'',
|
||||
' # 算税',
|
||||
' def tax(self) -> int:',
|
||||
' return self.amount // 10',
|
||||
'',
|
||||
' note: Mapped[str] = mapped_column(Text)',
|
||||
'',
|
||||
].join('\n')
|
||||
const [, order] = await views(source)
|
||||
expect(propertyLines(properties(order)).slice(-3)).toEqual(['字段: 2 个', ' amount | 整数 | | ', ' note | 长文本 | | '])
|
||||
expect(order.children.map(child => [child.kind, child.startLine])).toEqual([['comment', 9], ['function', 10]])
|
||||
expect([order.startLine, order.endLine]).toEqual([3, 13])
|
||||
expect(replaceLines(source, order.startLine, order.endLine, order.code)).toBe(source)
|
||||
expect(replaceLines(source, order.startLine, order.endLine, order.code.replace('// 10', '// 8'))).toBe(source.replace('// 10', '// 8'))
|
||||
})
|
||||
|
||||
it('跳到定义、高亮出现处:卡片自己是类名的定义,字段行是字段名的定义,带下标的词都指着源码里的这个名字', async () => {
|
||||
const table = await model(SNAPSHOT, 'ReferralRelation')
|
||||
expect(SNAPSHOT.slice(table.defines?.startIndex, table.defines?.endIndex)).toBe('ReferralRelation')
|
||||
@@ -162,7 +189,7 @@ describe('sqlAlchemy 库规则', () => {
|
||||
expect(table.attributes).toEqual({ kind: 'sqlalchemy.table', access: 'public' })
|
||||
})
|
||||
|
||||
it('import sqlalchemy as sa:老写法 Column、类型全名、只写注解的字段、不认得的类型照常翻、docstring 当说明;方法照常是子块,夹在中间的注释并进来', async () => {
|
||||
it('import sqlalchemy as sa:老写法 Column、类型全名、只写注解的字段、不认得的类型照常翻、docstring 当说明;方法照常是子块,字段前面的注释并进来', async () => {
|
||||
const source = [
|
||||
'import sqlalchemy as sa',
|
||||
'from sqlalchemy.orm import DeclarativeBase, relationship',
|
||||
@@ -205,11 +232,11 @@ describe('sqlAlchemy 库规则', () => {
|
||||
'关系: 1 个',
|
||||
' orders | Order | 对方字段 user · 单个对象',
|
||||
])
|
||||
expect([user.startLine, user.endLine]).toEqual([8, 18])
|
||||
expect([user.startLine, user.endLine]).toEqual([8, 21])
|
||||
expect(user.children.map(child => [child.kind, child.startLine])).toEqual([['function', 20]])
|
||||
})
|
||||
|
||||
it('字段的属性 docstring:没写 comment= 时当说明,写了以 comment= 为准;它并进卡片,编辑范围到最后一个字段的说明为止', async () => {
|
||||
it('字段的属性 docstring:没写 comment= 时当说明,写了以 comment= 为准;它并进卡片', async () => {
|
||||
const source = [
|
||||
'from sqlalchemy.orm import Mapped, mapped_column',
|
||||
'',
|
||||
@@ -234,7 +261,7 @@ describe('sqlAlchemy 库规则', () => {
|
||||
' summary | 长文本 | | 案例描述',
|
||||
' status | 字符串(16) | 可空 | 案例状态',
|
||||
])
|
||||
expect([table.startLine, table.endLine]).toEqual([3, 11])
|
||||
expect([table.startLine, table.endLine]).toEqual([3, 14])
|
||||
expect(table.children.map(child => [child.kind, child.startLine])).toEqual([['function', 13]])
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { ClassBlock, CodeBlock, Inline, LibraryRule, Node, PropertyItem, PropertyRow, RuleBlock, RuleContext, ThemeColor, TokenRole } from '../contract'
|
||||
import type { Call } from './pythonTree'
|
||||
import { withLeadingComments } from './children'
|
||||
import { aliasesOf, callOf, canonicalName, lastName, stringValue, syntaxChildren } from './pythonTree'
|
||||
import { hint, joined, name, plain, range, word } from './segments'
|
||||
|
||||
/**
|
||||
* SQLAlchemy 库规则:声明式模型类(有 `__tablename__`,或主体里用 `mapped_column(...)` / `Column(...)` 定义字段)整块换成「数据表」卡片:
|
||||
* 名称 / 表名 / 说明 / 继承 / 装饰器,字段、关系、索引、唯一约束各一张表,都是有才显示。
|
||||
* `__tablename__`、`__table_args__`、字段和关系的赋值并进卡片,方法等别的成员照常是子块。
|
||||
* `__tablename__`、`__table_args__`、字段和关系的赋值(连同紧挨在它们前面的注释)并进卡片,方法等别的成员不管写在哪都照常是子块。
|
||||
* 规则只经 contract 拿信息,直接读的是 Python 语法树(tree-sitter-python 的节点类型);用词都在词表里,按翻译插件的显示语言挑。
|
||||
*/
|
||||
|
||||
@@ -449,7 +450,7 @@ function tableBlock(block: ClassBlock, model: Model, env: Env): RuleBlock {
|
||||
badge: { label: words.kind, color: TABLE_COLOR },
|
||||
rows,
|
||||
...(facts.name && { defines: range(facts.name) }),
|
||||
absorbs: members.filter((member): member is Member => member !== undefined).map(member => member.block),
|
||||
absorbs: withLeadingComments(block.children, members.filter((member): member is Member => member !== undefined).map(member => member.block)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { ViewState } from './viewState'
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { BlockLevel } from './BlockTree'
|
||||
import { coveredByEditing, startEditing } from './editing'
|
||||
import { applyFilter, keptWhileEditing } from './filter'
|
||||
import { FilterBar } from './FilterBar'
|
||||
import { useDefinitionLinks } from './useDefinitionLinks'
|
||||
@@ -18,10 +19,6 @@ type BlockModes = Pick<ViewState, 'editing' | 'folds'>
|
||||
|
||||
const NONE: ReadonlySet<string> = new Set()
|
||||
|
||||
function toggled(ids: string[], id: string): string[] {
|
||||
return ids.includes(id) ? ids.filter(x => x !== id) : [...ids, id]
|
||||
}
|
||||
|
||||
function savedBlockModes(): BlockModes {
|
||||
const { editing, folds } = viewState.get()
|
||||
return { editing, folds }
|
||||
@@ -41,6 +38,7 @@ export function App(): ReactElement | null {
|
||||
const editing = useRef(view.editing)
|
||||
const collapsed = useMemo(() => collapsedIds(doc?.blocks ?? [], view.folds), [doc, view.folds])
|
||||
const filter = useMemo(() => applyFilter(doc?.blocks ?? [], excluded, keep), [doc, excluded, keep])
|
||||
const covered = useMemo(() => coveredByEditing(doc?.blocks ?? [], new Set(view.editing)), [doc, view.editing])
|
||||
/** 记下这几块、属性行、子行折叠还是展开,从此不再按默认 */
|
||||
const setFolded = useCallback((ids: string[], folded: boolean) => setView(prev => ({
|
||||
...prev,
|
||||
@@ -50,7 +48,7 @@ export function App(): ReactElement | null {
|
||||
const show = useCallback((ids: string[]) => setKeep(prev => new Set([...prev, ...ids])), [])
|
||||
const revealed = useCallback(() => setReveal(null), [])
|
||||
useScrollPosition(doc)
|
||||
useReveal(doc, reveal, collapsed, filter.hidden, show, expand, revealed)
|
||||
useReveal(doc, reveal, collapsed, filter.hidden, covered, show, expand, revealed)
|
||||
useDefinitionLinks(doc?.version)
|
||||
const highlight = useOccurrences(doc?.version, doc?.translatorId)
|
||||
|
||||
@@ -88,15 +86,19 @@ export function App(): ReactElement | null {
|
||||
collapsed,
|
||||
hidden: filter.hidden,
|
||||
temporary: filter.temporary,
|
||||
covered,
|
||||
kindName: kind => kindNames.get(kind) ?? kind,
|
||||
focusId,
|
||||
onFocused,
|
||||
onToggleEdit: (id) => {
|
||||
setFocusId(view.editing.includes(id) ? null : id)
|
||||
setView(prev => ({ ...prev, editing: toggled(prev.editing, id) }))
|
||||
setView(prev => ({
|
||||
...prev,
|
||||
editing: prev.editing.includes(id) ? prev.editing.filter(other => other !== id) : startEditing(doc.blocks, prev.editing, id),
|
||||
}))
|
||||
},
|
||||
onToggleFold: id => setFolded([id], !collapsed.has(id)),
|
||||
}, [doc, view, collapsed, filter, kindNames, focusId, onFocused, setFolded])
|
||||
}, [doc, view, collapsed, filter, covered, kindNames, focusId, onFocused, setFolded])
|
||||
|
||||
if (!doc || !tree)
|
||||
return null
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface BlockTreeState {
|
||||
hidden: ReadonlySet<string>
|
||||
/** 本来被筛掉、跳到定义时临时显示的块 */
|
||||
temporary: ReadonlySet<string>
|
||||
/** 被正在编辑的祖先盖住、不画的块(它们的行在祖先的编辑范围里),见 coveredByEditing */
|
||||
covered: ReadonlyMap<string, string>
|
||||
/** 种类标签上写什么:翻译插件给种类起的名字(库规则的种类是规则给的标签),没给就是 id */
|
||||
kindName: (kind: string) => string
|
||||
/** 刚点了「编辑」、要把光标放进去的块 */
|
||||
@@ -25,9 +27,14 @@ export interface BlockTreeState {
|
||||
onToggleFold: (id: string) => void
|
||||
}
|
||||
|
||||
/** 块下面有没有画出来的子块:子块都被筛掉时不留空的一层、不给折叠箭头。 */
|
||||
/** 块下面有没有画出来的子块:子块都被筛掉、被盖住时不留空的一层、不给折叠箭头。 */
|
||||
function hasShownChildren(block: BlockView, tree: BlockTreeState): boolean {
|
||||
return block.children.some(child => !tree.hidden.has(child.id))
|
||||
return block.children.some(child => shown(child, tree))
|
||||
}
|
||||
|
||||
/** 块画不画:被筛掉的、被正在编辑的祖先盖住的都不画。 */
|
||||
function shown(block: BlockView, tree: BlockTreeState): boolean {
|
||||
return !tree.hidden.has(block.id) && !tree.covered.has(block.id)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +51,7 @@ function kindProps(block: BlockView | undefined): { 'data-kind'?: string, 'data-
|
||||
}
|
||||
|
||||
/**
|
||||
* 一层代码块:左边一条竖线标出这一层,每块的子块紧跟在它下面、再往右缩进一层。被筛掉的块不画,不留空位,竖线跟着变短。
|
||||
* 一层代码块:左边一条竖线标出这一层,每块的子块紧跟在它下面、再往右缩进一层。被筛掉、被盖住的块不画,不留空位,竖线跟着变短。
|
||||
* owner 是这一层所属的块(顶层没有),悬停它时竖线换成它的种类色。
|
||||
*/
|
||||
export function BlockLevel({ blocks, owner, tree }: { blocks: BlockView[], owner?: BlockView, tree: BlockTreeState }): ReactElement {
|
||||
@@ -54,7 +61,7 @@ export function BlockLevel({ blocks, owner, tree }: { blocks: BlockView[], owner
|
||||
<div className="cr-guide w-px" />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
{blocks.filter(block => !tree.hidden.has(block.id)).map(block => (
|
||||
{blocks.filter(block => shown(block, tree)).map(block => (
|
||||
<Fragment key={block.id}>
|
||||
<BlockCard block={block} tree={tree} />
|
||||
{hasShownChildren(block, tree) && !tree.collapsed.has(block.id) && (
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { BlockView } from './types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { coveredByEditing, startEditing } from './editing'
|
||||
import { revealPlan, uncoveredPlan } from './reveal'
|
||||
|
||||
function block(id: string, kind: string, [startLine, endLine]: [number, number], children: BlockView[] = []): BlockView {
|
||||
return { id, kind, startLine, endLine, code: '', codeTokens: [], attributes: { kind }, children, properties: [] }
|
||||
}
|
||||
|
||||
/**
|
||||
* 库规则的卡片(1)编辑范围是整个类(2–12 行),两个字段中间写着的方法(1.0)和它的主体(1.0.0)、类末尾的方法(1.1)都在这段里;
|
||||
* 普通的类(2)只编辑头部,方法(2.0)不在头部里;一行写完的 if(3)和它的返回(3.0)在同一行。
|
||||
*/
|
||||
const BLOCKS = [
|
||||
block('0', 'import', [0, 0]),
|
||||
block('1', 'sqlalchemy.table', [2, 12], [
|
||||
block('1.0', 'function', [5, 5], [block('1.0.0', 'return', [6, 6])]),
|
||||
block('1.1', 'function', [11, 12]),
|
||||
]),
|
||||
block('2', 'class', [14, 14], [block('2.0', 'function', [15, 16])]),
|
||||
block('3', 'if', [18, 18], [block('3.0', 'return', [18, 18])]),
|
||||
]
|
||||
|
||||
describe('同一行不会同时在两处能改', () => {
|
||||
it('卡片编辑时,它留下的子块(字段中间的方法连同主体)不画,跳到它们时改闪卡片', () => {
|
||||
expect(coveredByEditing(BLOCKS, new Set(['1']))).toEqual(new Map([['1.0', '1'], ['1.0.0', '1'], ['1.1', '1']]))
|
||||
})
|
||||
|
||||
it('只编辑头部的块不盖住子块;一行写完的块编辑时盖住同一行的子块', () => {
|
||||
expect(coveredByEditing(BLOCKS, new Set(['2', '2.0']))).toEqual(new Map())
|
||||
expect(coveredByEditing(BLOCKS, new Set(['3']))).toEqual(new Map([['3.0', '3']]))
|
||||
})
|
||||
|
||||
it('没在编辑的卡片不盖住子块:子块照常画、照常能单独编辑', () => {
|
||||
expect(coveredByEditing(BLOCKS, new Set(['1.0']))).toEqual(new Map())
|
||||
})
|
||||
|
||||
it('点「编辑」:编辑范围和它重叠的别的块退出编辑,不重叠的留着', () => {
|
||||
expect(startEditing(BLOCKS, ['1.0', '1.1', '2'], '1')).toEqual(['2', '1'])
|
||||
expect(startEditing(BLOCKS, ['1', '2'], '1.0')).toEqual(['2', '1.0'])
|
||||
expect(startEditing(BLOCKS, ['2'], '2.0')).toEqual(['2', '2.0'])
|
||||
})
|
||||
|
||||
it('任何时候正在编辑、画出来的几块,编辑范围两两不重叠', () => {
|
||||
const all = (list: BlockView[]): BlockView[] => list.flatMap(item => [item, ...all(item.children)])
|
||||
let editing: string[] = []
|
||||
for (const id of ['1.0', '1.0.0', '2', '2.0', '1', '3.0', '3', '1.1'])
|
||||
editing = startEditing(BLOCKS, editing, id)
|
||||
const covered = coveredByEditing(BLOCKS, new Set(editing))
|
||||
const shown = all(BLOCKS).filter(item => editing.includes(item.id) && !covered.has(item.id))
|
||||
for (const a of shown) {
|
||||
for (const b of shown.filter(other => other !== a))
|
||||
expect(a.startLine > b.endLine || b.startLine > a.endLine, `${a.id} / ${b.id}`).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('跳到定义的目标被盖住:闪盖住它的那块,展开那块的祖先', () => {
|
||||
const covered = coveredByEditing(BLOCKS, new Set(['1']))
|
||||
expect(uncoveredPlan({ id: '1.0.0', expand: ['1', '1.0'] }, covered)).toEqual({ id: '1', expand: [] })
|
||||
const plain = revealPlan(BLOCKS, 15, 999)
|
||||
expect(plain && uncoveredPlan(plain, covered)).toEqual(plain)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { BlockView } from './types'
|
||||
|
||||
/**
|
||||
* 同一时刻能编辑的几段源码不重叠,同一行不会同时在两处能改:
|
||||
* ① 块编辑时,编辑范围和它重叠的子孙块不画(库规则的卡片编辑整个类,留下的方法也在这段里;
|
||||
* 一行写完的 `if x: return 1`,子块和头部在同一行),它们下面的子孙也跟着不画;
|
||||
* ② 点「编辑」时,编辑范围和这块重叠的别的块退出编辑。
|
||||
*/
|
||||
|
||||
type Lines = Pick<BlockView, 'startLine' | 'endLine'>
|
||||
|
||||
function overlaps(a: Lines, b: Lines): boolean {
|
||||
return a.startLine <= b.endLine && b.startLine <= a.endLine
|
||||
}
|
||||
|
||||
/** 正在编辑的块盖住、不画的块 → 盖住它的那块(页面上画着的、正在编辑的祖先):跳到定义落在被盖住的块上时改闪它。 */
|
||||
export function coveredByEditing(blocks: readonly BlockView[], editing: ReadonlySet<string>): Map<string, string> {
|
||||
const covered = new Map<string, string>()
|
||||
const visit = (list: readonly BlockView[], editors: readonly BlockView[], owner: string | undefined): void => {
|
||||
for (const block of list) {
|
||||
const by = owner ?? editors.find(editor => overlaps(editor, block))?.id
|
||||
if (by !== undefined) {
|
||||
covered.set(block.id, by)
|
||||
visit(block.children, editors, by)
|
||||
}
|
||||
else {
|
||||
visit(block.children, editing.has(block.id) ? [...editors, block] : editors, undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
visit(blocks, [], undefined)
|
||||
return covered
|
||||
}
|
||||
|
||||
function* allBlocks(blocks: readonly BlockView[]): Generator<BlockView> {
|
||||
for (const block of blocks) {
|
||||
yield block
|
||||
yield* allBlocks(block.children)
|
||||
}
|
||||
}
|
||||
|
||||
/** 点了 id 这块的「编辑」之后正在编辑的块:加上它,编辑范围和它重叠的别的块退出编辑。 */
|
||||
export function startEditing(blocks: readonly BlockView[], editing: readonly string[], id: string): string[] {
|
||||
const byId = new Map([...allBlocks(blocks)].map(block => [block.id, block]))
|
||||
const target = byId.get(id)
|
||||
const others = editing.filter((other) => {
|
||||
const block = byId.get(other)
|
||||
return other !== id && !(target && block && overlaps(target, block))
|
||||
})
|
||||
return [...others, id]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BlockView, PreviewText, PropertyItem, SourceRange } from './types'
|
||||
import { blockPath } from './filter'
|
||||
import { showsOffset } from './segments'
|
||||
import { propertyItemId, propertyRowId } from './viewState'
|
||||
|
||||
@@ -109,6 +110,15 @@ export function revealPlan(blocks: BlockView[], line: number, definition: number
|
||||
return target && { id: target.block.id, expand: target.ancestors }
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标块被正在编辑的祖先盖住、不在页面上(库规则的卡片正在编辑,目标是它下面留下的方法):改成闪盖住它的那块,
|
||||
* 展开那块的祖先,不再找目标里的属性行。covered 见 coveredByEditing。
|
||||
*/
|
||||
export function uncoveredPlan(plan: RevealPlan, covered: ReadonlyMap<string, string>): RevealPlan {
|
||||
const owner = covered.get(plan.id)
|
||||
return owner === undefined ? plan : { id: owner, expand: blockPath(owner).slice(0, -1) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 让一块露出来时视口顶部要在的位置,和 VS Code 跳到定义一样:整块已经在视口里就不滚;否则放到视口正中,比视口还高就顶部对齐。
|
||||
* viewTop / viewHeight 是视口里露出块的那部分(钉住的筛选栏下面)。
|
||||
|
||||
@@ -80,7 +80,7 @@ interface BlockViewBase {
|
||||
id: string
|
||||
/** BlockKind,或库规则认领的块的种类 id(带点,例如 sqlalchemy.table);webview 不认识后者,只按 badge 画 */
|
||||
kind: string
|
||||
/** 编辑范围的起止行,0 起:一般是 header,库规则认领的块到最后一个并进来的子块为止 */
|
||||
/** 编辑范围的起止行,0 起:一般是 header,库规则认领的块是原块整条语句(盖住它留下的子块,见 coveredByEditing) */
|
||||
startLine: number
|
||||
endLine: number
|
||||
/** 编辑范围的源码,去掉共同缩进 */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DocumentMessage, Reveal } from './types'
|
||||
import { useLayoutEffect } from 'react'
|
||||
import { blockPath } from './filter'
|
||||
import { revealPlan, scrollTopToShow } from './reveal'
|
||||
import { revealPlan, scrollTopToShow, uncoveredPlan } from './reveal'
|
||||
import { blockForLine } from './scrollPosition'
|
||||
import { measureBlocks, pinnedHeight } from './useScrollPosition'
|
||||
|
||||
@@ -28,12 +28,14 @@ function flash(el: HTMLElement): void {
|
||||
* ① 从文本编辑器切过来(没有 definition):包含那一行的块顶到视口顶部(筛选栏下面),折叠着的、被筛掉的不展开;
|
||||
* ② 跳到定义:目标或它的祖先块被筛掉了,先临时露出这条路径(show);再展开把目标折叠住的块、属性行和子行(expand)——
|
||||
* 每一步之后重新渲染、再进来一次;最后把目标(整块,或就是这处定义的属性行 / 子行)滚进视口、闪几下。
|
||||
* 目标被正在编辑的祖先盖住时闪那个祖先(uncoveredPlan)。
|
||||
*/
|
||||
export function useReveal(
|
||||
doc: DocumentMessage | null,
|
||||
reveal: Reveal | null,
|
||||
collapsed: ReadonlySet<string>,
|
||||
filtered: ReadonlySet<string>,
|
||||
covered: ReadonlyMap<string, string>,
|
||||
show: (ids: string[]) => void,
|
||||
expand: (ids: string[]) => void,
|
||||
done: () => void,
|
||||
@@ -48,7 +50,8 @@ export function useReveal(
|
||||
done()
|
||||
return
|
||||
}
|
||||
const plan = revealPlan(doc.blocks, reveal.line, reveal.definition)
|
||||
const found = revealPlan(doc.blocks, reveal.line, reveal.definition)
|
||||
const plan = found && uncoveredPlan(found, covered)
|
||||
const path = plan ? blockPath(plan.id) : []
|
||||
if (path.some(id => filtered.has(id))) {
|
||||
show(path)
|
||||
@@ -71,5 +74,5 @@ export function useReveal(
|
||||
flash(el)
|
||||
}
|
||||
done()
|
||||
}, [doc, reveal, collapsed, filtered, show, expand, done])
|
||||
}, [doc, reveal, collapsed, filtered, covered, show, expand, done])
|
||||
}
|
||||
Reference in new issue
Block a user