✨ feat(vscode): 代码阅读库规则卡片不再吞掉字段中间的方法,编辑时收起子块
This commit is contained in:
1 parent
c803fd457f
commit
2251e67ef8
15 files changed
+280
-54
No files matched your search
@@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user