✨ feat(vscode): 代码阅读的注释挂到它讲的代码块上成「注释」一行,重写注释改用技能一句话加结构化结果
- 普通注释不再单独成卡片:同一层挂到后面最近的块(隔空行也算),后面没有挂到前面那块,整层没代码挂到父块;只有文件开头隔着空行的注释、模块 docstring、//!、@file 单独成「文件开头的说明」 - 各语言(Python、TypeScript、Rust、Shell、Dockerfile)建好树后统一调 attachComments;块的编辑范围连着挂上的注释,差异、跳转、钉住卡片都按它 - 宿主统一给每张卡片(语言卡片、翻译插件的属性表、库规则卡片、一行译文的语句卡片)插「注释」一行,排在名称、说明后面 - TypeScript 的 JSDoc 也能当常量、变量的说明;sqlx、React 不再单独处理前面的注释 - 「重写注释」每块都有:提示只发 /ecc:writing-code-comments 路径:起-止,模型只读、按 --json-schema 交 body,没交结构化正文就失败不写;扩展换掉块前面的注释或在块上面新写一段
This commit is contained in:
1 parent
335cf9ca9f
commit
582ac4c987
49 files changed
+1730
-757
No files matched your search
@@ -17,8 +17,7 @@ function facts(source: string): Promise<Record<number, unknown>> {
|
||||
/** luffy 桌面端 fetch-runtime.sh 的代码块树(同原型 SHELL_BLOCKS,原型略去的 95-100、116-121 行也在)。 */
|
||||
const FETCH_TREE: Outline[] = [
|
||||
{ at: 1, kind: 'interpreter' },
|
||||
{ at: [2, 13], kind: 'comment' },
|
||||
{ at: 14, kind: 'options' },
|
||||
{ at: 14, kind: 'options', notes: [[2, 13]] },
|
||||
{ at: 16, kind: 'variable' },
|
||||
{ at: 17, kind: 'variable' },
|
||||
{ at: 18, kind: 'variable' },
|
||||
@@ -41,35 +40,28 @@ const FETCH_TREE: Outline[] = [
|
||||
{ at: 66, kind: 'command' },
|
||||
{ at: 67, kind: 'command' },
|
||||
{ at: [68, 71], kind: 'case' },
|
||||
{ at: 72, kind: 'comment' },
|
||||
{ at: 73, kind: 'command' },
|
||||
{ at: [75, 77], kind: 'comment' },
|
||||
{ at: 78, kind: 'case', kids: [
|
||||
{ at: 79, kind: 'case', kids: [{ at: 80, kind: 'variable' }, { at: [81, 82], kind: 'command' }, { at: 83, kind: 'comment' }, { at: 84, kind: 'command' }] },
|
||||
{ at: 86, kind: 'case', kids: [{ at: [87, 88], kind: 'comment' }, { at: 89, kind: 'variable' }, { at: 90, kind: 'command' }, { at: 91, kind: 'command' }] },
|
||||
{ at: 73, kind: 'command', notes: [72] },
|
||||
{ at: 78, kind: 'case', notes: [[75, 77]], kids: [
|
||||
{ at: 79, kind: 'case', kids: [{ at: 80, kind: 'variable' }, { at: [81, 82], kind: 'command' }, { at: 84, kind: 'command', notes: [83] }] },
|
||||
{ at: 86, kind: 'case', kids: [{ at: 89, kind: 'variable', notes: [[87, 88]] }, { at: 90, kind: 'command' }, { at: 91, kind: 'command' }] },
|
||||
] },
|
||||
{ at: [95, 97], kind: 'comment' },
|
||||
{ at: 98, kind: 'if', kids: [{ at: 99, kind: 'command' }] },
|
||||
{ at: [102, 104], kind: 'comment' },
|
||||
{ at: 105, kind: 'if', kids: [
|
||||
{ at: 98, kind: 'if', notes: [[95, 97]], kids: [{ at: 99, kind: 'command' }] },
|
||||
{ at: 105, kind: 'if', notes: [[102, 104]], kids: [
|
||||
{ at: 106, kind: 'variable' },
|
||||
{ at: [107, 108], kind: 'comment' },
|
||||
{ at: 109, kind: 'loop', kids: [{ at: 110, kind: 'if' }, { at: [111, 114], kind: 'case' }] },
|
||||
{ at: 116, kind: 'comment' },
|
||||
{ at: 117, kind: 'variable' },
|
||||
{ at: 109, kind: 'loop', notes: [[107, 108]], kids: [{ at: 110, kind: 'if' }, { at: [111, 114], kind: 'case' }] },
|
||||
{ at: 117, kind: 'variable', notes: [116] },
|
||||
{ at: 118, kind: 'variable' },
|
||||
{ at: 119, kind: 'command' },
|
||||
{ at: 120, kind: 'command' },
|
||||
{ at: 121, kind: 'command' },
|
||||
{ at: 122, kind: 'comment' },
|
||||
{ at: 123, kind: 'pipeline', kids: [{ at: 124, kind: 'command' }, { at: 125, kind: 'command' }] },
|
||||
{ at: 123, kind: 'pipeline', notes: [122], kids: [{ at: 124, kind: 'command' }, { at: 125, kind: 'command' }] },
|
||||
] },
|
||||
{ at: 129, kind: 'command' },
|
||||
{ at: 130, kind: 'command' },
|
||||
]
|
||||
|
||||
describe('shell 代码块树', () => {
|
||||
it('fetch-runtime.sh:解释器、选项、变量 / 常量、分情况(短的整条一块,长的每个情况一块子块)、函数(紧贴的注释算进头部)、命令、管道后面的循环主体是子块', async () => {
|
||||
it('fetch-runtime.sh:解释器、选项、变量 / 常量、分情况(短的整条一块,长的每个情况一块子块)、函数(紧贴的注释算进头部)、命令、管道后面的循环主体是子块;注释挂到后面那块上', async () => {
|
||||
expect(await tree(FETCH)).toEqual(FETCH_TREE)
|
||||
})
|
||||
|
||||
@@ -218,3 +210,43 @@ describe('shell 命令、名字', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('shell 注释挂到它讲的块上', () => {
|
||||
it('#! 后面隔着空行的是文件的说明;同一层后面最近的那块(隔着空行也算),和函数的说明并存;主体最后的挂到前面那块;文件最后的挂到最后一块', async () => {
|
||||
const source = [
|
||||
'#!/usr/bin/env bash',
|
||||
'# 文件的说明',
|
||||
'',
|
||||
'set -e',
|
||||
'',
|
||||
'# 讲 fetch:隔着空行',
|
||||
'',
|
||||
'# 下载一个文件',
|
||||
'fetch() {',
|
||||
' # 先检查',
|
||||
' if [ -z "$1" ]; then',
|
||||
' # 只有注释的分支',
|
||||
' :',
|
||||
' fi',
|
||||
' curl -fsSL "$1"',
|
||||
' # 函数体最后的注释',
|
||||
'}',
|
||||
'',
|
||||
'# 文件最后的注释',
|
||||
].join('\n')
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: 1, kind: 'interpreter' },
|
||||
{ at: 2, kind: 'comment' },
|
||||
{ at: 4, kind: 'options' },
|
||||
{ at: [8, 9], kind: 'function', notes: [6, 19], kids: [
|
||||
{ at: 11, kind: 'if', notes: [10], kids: [{ at: 13, kind: 'command', notes: [12] }] },
|
||||
{ at: 15, kind: 'command', notes: [16] },
|
||||
] },
|
||||
])
|
||||
expect((await facts(source))[8]).toMatchObject({ doc: '# 下载一个文件' })
|
||||
})
|
||||
|
||||
it('#! 后面紧挨着第一块代码的注释挂到它上面', async () => {
|
||||
expect(await tree('#!/bin/sh\n# 出错就停\nset -e\n')).toEqual([{ at: 1, kind: 'interpreter' }, { at: 3, kind: 'options', notes: [2] }])
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import type { BlockShape, CodeBlock, CommandFacts, CommandWord, DeclaratorFacts,
|
||||
import type { CommentRules, ListBlocks } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { bashNameReader } from './bashNames'
|
||||
import { children, compoundShape, leafBlock, leafShape, listBlocks, named } from './shared'
|
||||
import { attachComments, children, compoundShape, leafBlock, leafShape, listBlocks, named } from './shared'
|
||||
|
||||
/** 按习惯当常量用的名字:全大写(可带数字、下划线)。 */
|
||||
const CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/
|
||||
@@ -627,7 +627,7 @@ function bashSteps(root: Node): StepBlocks {
|
||||
const [only, ...more] = items.filter(item => item.type !== 'comment')
|
||||
if (only && more.length === 0 && andChain(only))
|
||||
return { blocks: chainOperands(only).flatMap(stepBlocks), chained: true }
|
||||
return { blocks: statements(items, -1, true), chained: false }
|
||||
return { blocks: attachComments(statements(items, -1, true), undefined, false), chained: false }
|
||||
}
|
||||
|
||||
/** bash / sh / zsh 脚本(VS Code 的 languageId 是 shellscript)的语言插件。@param wasmDir 语法包 .wasm 所在目录 */
|
||||
@@ -636,9 +636,10 @@ export function createBashAdapter(wasmDir: string): LanguageAdapter {
|
||||
id: 'bash',
|
||||
languageIds: ['shellscript'],
|
||||
grammarPath: () => path.join(wasmDir, 'tree-sitter-bash.wasm'),
|
||||
toBlocks: root => programBlocks(root),
|
||||
toBlocks: root => attachComments(programBlocks(root)),
|
||||
steps: root => bashSteps(root),
|
||||
tokenRole: bashTokenRole,
|
||||
lineComment: '#',
|
||||
command: bashCommand,
|
||||
symbols: root => bashSymbols(root),
|
||||
displayNames: bashNameReader,
|
||||
|
||||
@@ -30,8 +30,7 @@ const NEXT_TREE: Outline[] = [
|
||||
{ at: 12, kind: 'copy' },
|
||||
{ at: 13, kind: 'copy' },
|
||||
{ at: 14, kind: 'variable' },
|
||||
{ at: [15, 16], kind: 'comment' },
|
||||
{ at: 17, kind: 'variable' },
|
||||
{ at: 17, kind: 'variable', notes: [[15, 16]] },
|
||||
{ at: 18, kind: 'variable' },
|
||||
{ at: 19, kind: 'run' },
|
||||
] },
|
||||
@@ -42,8 +41,7 @@ const NEXT_TREE: Outline[] = [
|
||||
{ at: 28, kind: 'variable' },
|
||||
{ at: [30, 31], kind: 'run' },
|
||||
{ at: [33, 34], kind: 'copy' },
|
||||
{ at: 35, kind: 'comment' },
|
||||
{ at: 36, kind: 'copy' },
|
||||
{ at: 36, kind: 'copy', notes: [35] },
|
||||
{ at: 38, kind: 'setting' },
|
||||
{ at: 39, kind: 'setting' },
|
||||
{ at: 41, kind: 'start' },
|
||||
@@ -54,36 +52,25 @@ const NEXT_TREE: Outline[] = [
|
||||
const BACKEND_TREE: Outline[] = [
|
||||
{ at: [1, 4], kind: 'stage', kids: [
|
||||
{ at: [6, 7], kind: 'label' },
|
||||
{ at: [9, 12], kind: 'comment' },
|
||||
{ at: [13, 25], kind: 'run' },
|
||||
{ at: [27, 31], kind: 'comment' },
|
||||
{ at: [32, 33], kind: 'copy' },
|
||||
{ at: [13, 25], kind: 'run', notes: [[9, 12]] },
|
||||
{ at: [32, 33], kind: 'copy', notes: [[27, 31]] },
|
||||
{ at: [34, 35], kind: 'run' },
|
||||
{ at: 37, kind: 'copy' },
|
||||
{ at: [39, 41], kind: 'comment' },
|
||||
{ at: [42, 44], kind: 'run' },
|
||||
{ at: [42, 44], kind: 'run', notes: [[39, 41]] },
|
||||
{ at: 45, kind: 'setting' },
|
||||
{ at: 46, kind: 'variable' },
|
||||
{ at: 48, kind: 'setting' },
|
||||
{ at: [50, 53], kind: 'comment' },
|
||||
{ at: [54, 65], kind: 'copy' },
|
||||
{ at: 67, kind: 'comment' },
|
||||
{ at: [68, 79], kind: 'run' },
|
||||
{ at: 81, kind: 'comment' },
|
||||
{ at: 82, kind: 'copy' },
|
||||
{ at: [83, 84], kind: 'comment' },
|
||||
{ at: [85, 89], kind: 'copy' },
|
||||
{ at: 91, kind: 'comment' },
|
||||
{ at: 92, kind: 'run' },
|
||||
{ at: [94, 95], kind: 'comment' },
|
||||
{ at: 96, kind: 'run' },
|
||||
{ at: [54, 65], kind: 'copy', notes: [[50, 53]] },
|
||||
{ at: [68, 79], kind: 'run', notes: [67] },
|
||||
{ at: 82, kind: 'copy', notes: [81] },
|
||||
{ at: [85, 89], kind: 'copy', notes: [[83, 84]] },
|
||||
{ at: 92, kind: 'run', notes: [91] },
|
||||
{ at: 96, kind: 'run', notes: [[94, 95]] },
|
||||
{ at: 98, kind: 'variable' },
|
||||
{ at: 100, kind: 'comment' },
|
||||
{ at: 101, kind: 'variable' },
|
||||
{ at: 101, kind: 'variable', notes: [100] },
|
||||
{ at: 102, kind: 'variable' },
|
||||
{ at: 104, kind: 'setting' },
|
||||
{ at: 106, kind: 'comment' },
|
||||
{ at: [107, 108], kind: 'health' },
|
||||
{ at: [107, 108], kind: 'health', notes: [106] },
|
||||
{ at: 110, kind: 'start' },
|
||||
] },
|
||||
]
|
||||
@@ -238,3 +225,29 @@ describe('dockerfile 嵌着的 shell、命令、名字', () => {
|
||||
expect(roles).toEqual(expect.arrayContaining(['FROM:keyword', 'node:type', 'AS:keyword', 'deps:variable', 'COPY:keyword', 'a:string', '/b:string', 'PORT:constant', 'X:constant']))
|
||||
})
|
||||
})
|
||||
|
||||
describe('dockerfile 注释挂到它讲的块上', () => {
|
||||
it('解析指令后面隔着空行的是文件的说明;阶段里挂到后面那条指令,阶段最后的挂到前面那条;FROM 上面紧贴的照旧是阶段的说明', async () => {
|
||||
const source = [
|
||||
'# syntax=docker/dockerfile:1.7',
|
||||
'',
|
||||
'# 文件的说明',
|
||||
'',
|
||||
'# 基础镜像',
|
||||
'FROM node:22 AS base',
|
||||
'# 讲 WORKDIR',
|
||||
'',
|
||||
'WORKDIR /app',
|
||||
'RUN npm ci',
|
||||
'# 阶段最后的注释',
|
||||
].join('\n')
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: 1, kind: 'directive' },
|
||||
{ at: 3, kind: 'comment' },
|
||||
{ at: [5, 6], kind: 'stage', kids: [
|
||||
{ at: 9, kind: 'setting', notes: [7] },
|
||||
{ at: 10, kind: 'run', notes: [11] },
|
||||
] },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CodeBlock, CommandFacts, CommandForm, CommandWord, DeclaratorFacts, DirectiveFacts, DisplayName, DisplayNameReader, EmbeddedCode, InstructionFlag, LanguageAdapter, LocalSymbol, Node, ParserDirective, SourceRange, SourceSpan, StageFacts, TextBlock, TokenRole } from '../contract'
|
||||
import * as path from 'node:path'
|
||||
import { COMMENT_MARK, commentTag } from './displayNames'
|
||||
import { children, leafBlock, leafShape, named } from './shared'
|
||||
import { attachComments, children, leafBlock, leafShape, named } from './shared'
|
||||
|
||||
/** RUN、shell 写法的 CMD / ENTRYPOINT、HEALTHCHECK 的检查命令交给 shell 的语言插件读。 */
|
||||
const SHELL = 'shellscript'
|
||||
@@ -422,8 +422,9 @@ export function createDockerfileAdapter(wasmDir: string): LanguageAdapter {
|
||||
id: 'dockerfile',
|
||||
languageIds: ['dockerfile'],
|
||||
grammarPath: () => path.join(wasmDir, 'tree-sitter-dockerfile.wasm'),
|
||||
toBlocks: root => dockerBlocks(root),
|
||||
toBlocks: root => attachComments(dockerBlocks(root)),
|
||||
tokenRole: dockerTokenRole,
|
||||
lineComment: '#',
|
||||
command: dockerCommand,
|
||||
symbols: root => dockerSymbols(root),
|
||||
embedded: root => root.descendantsOfType('shell_command').flatMap((node) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { celeryRule } from '../rules/celery'
|
||||
import { dataclassRule } from '../rules/dataclass'
|
||||
import { enumRule } from '../rules/enum'
|
||||
import { sqlalchemyRule } from '../rules/sqlalchemy'
|
||||
import { flatSegments, inlineOf, outline, propertyLines, readFixture, runtime, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { commentText, flatSegments, inlineOf, outline, propertyLines, readFixture, runtime, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { pythonZhTranslator } from '../translators/pythonZh'
|
||||
import { typescriptZhTranslator } from '../translators/typescriptZh'
|
||||
import { createPythonAdapter } from './python'
|
||||
@@ -197,7 +197,7 @@ describe('异常卡片(Python)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('类属性:给一个名字赋值(带不带注解、有没有值),说明取上面的注释块、行尾注释、属性 docstring;连着赋值、解构赋值、方法照常是子块;构造参数去掉 self', async () => {
|
||||
it('类属性:给一个名字赋值(带不带注解、有没有值),说明取讲它的注释、行尾注释、属性 docstring;连着赋值、解构赋值、方法照常是子块,讲方法的注释挂在方法上;构造参数去掉 self', async () => {
|
||||
const list = await pyViews(PY_CASES)
|
||||
expect(lines(list, 'Base')).toEqual([
|
||||
'名称: Base',
|
||||
@@ -217,12 +217,11 @@ describe('异常卡片(Python)', () => {
|
||||
const base = named(list, 'Base')
|
||||
expect(base.children.map(child => source[child.startLine].trim())).toEqual([
|
||||
'# 讲方法的注释',
|
||||
'def describe(self) -> str:',
|
||||
'a = b = 2',
|
||||
'x, y = 1, 2',
|
||||
'def __init__(self, message: str, *, level: int = 1, **extra) -> None:',
|
||||
'# 收尾',
|
||||
])
|
||||
expect(base.children.map(commentText)).toEqual(['讲方法的注释', undefined, undefined, '收尾'])
|
||||
expect(lines(list, 'Loose').slice(-2)).toEqual(['构造参数: 1 个', ' *args | 未标注 | '])
|
||||
expect(lines(list, 'Payload')).toEqual(['名称: Payload', '访问级别: 公共', '装饰器: @dataclass', '继承: ValueError', '属性: 1 个', ' field | 整数 | 0'])
|
||||
expect(named(list, 'Local').attributes).toEqual({ kind: 'exception' })
|
||||
|
||||
@@ -22,8 +22,7 @@ const SAMPLE: Outline[] = [
|
||||
{ at: 6, kind: 'const' },
|
||||
{ at: 7, kind: 'variable' },
|
||||
{ at: [10, 11], kind: 'function', kids: [
|
||||
{ at: 12, kind: 'comment' },
|
||||
{ at: 13, kind: 'variable' },
|
||||
{ at: 13, kind: 'variable', notes: [12] },
|
||||
{ at: 14, kind: 'if', kids: [{ at: 15, kind: 'return' }] },
|
||||
{ at: 16, kind: 'else', kids: [{ at: 17, kind: 'variable' }] },
|
||||
{ at: 18, kind: 'else', kids: [{ at: 19, kind: 'statement' }] },
|
||||
@@ -51,7 +50,7 @@ describe('python 代码块树', () => {
|
||||
expect(await tree(readFixture('sample.py'))).toEqual(SAMPLE)
|
||||
})
|
||||
|
||||
it('装饰器算进头部;冒号后、主体前的注释归子块;循环的 else 是兄弟块;match 的 case 是子块 if', async () => {
|
||||
it('装饰器算进头部;冒号后、主体前的注释挂到主体第一条语句上;循环的 else 是兄弟块;match 的 case 是子块 if', async () => {
|
||||
const source = [
|
||||
'@cache',
|
||||
'def load(key):',
|
||||
@@ -68,8 +67,7 @@ describe('python 代码块树', () => {
|
||||
].join('\n')
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: [1, 2], kind: 'function', kids: [
|
||||
{ at: 3, kind: 'comment' },
|
||||
{ at: 4, kind: 'loop', kids: [{ at: 5, kind: 'variable' }] },
|
||||
{ at: 4, kind: 'loop', notes: [3], kids: [{ at: 5, kind: 'variable' }] },
|
||||
{ at: 6, kind: 'else', kids: [{ at: 7, kind: 'statement' }] },
|
||||
{ at: 8, kind: 'if', kids: [
|
||||
{ at: 9, kind: 'if', kids: [{ at: 10, kind: 'return' }] },
|
||||
@@ -79,7 +77,7 @@ describe('python 代码块树', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('docstring 只认主体开头;不是开头、也没挂到赋值上的字符串是注释块,f-string、字节串照旧是语句', async () => {
|
||||
it('docstring 只认主体开头;不是开头、也没挂到赋值上的字符串是注释,挂到后面那条语句上,f-string、字节串照旧是语句', async () => {
|
||||
const source = [
|
||||
'def f():',
|
||||
' x = 1',
|
||||
@@ -92,9 +90,7 @@ describe('python 代码块树', () => {
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: 1, kind: 'function', kids: [
|
||||
{ at: 2, kind: 'variable' },
|
||||
{ at: 3, kind: 'comment' },
|
||||
{ at: 4, kind: 'comment' },
|
||||
{ at: 5, kind: 'statement' },
|
||||
{ at: 5, kind: 'statement', notes: [3, 4] },
|
||||
{ at: 6, kind: 'statement' },
|
||||
{ at: 7, kind: 'statement' },
|
||||
] },
|
||||
@@ -116,21 +112,18 @@ describe('python 属性 docstring', () => {
|
||||
})
|
||||
}
|
||||
|
||||
it('attributeDocs.py:模块、类主体里紧跟在赋值后面的字符串并进那块,头部延到字符串结尾;隔着注释、不跟在赋值后面的是注释块', async () => {
|
||||
it('attributeDocs.py:模块、类主体里紧跟在赋值后面的字符串并进那块,头部延到字符串结尾;隔着注释、不跟在赋值后面的是注释,挂到讲的块上', async () => {
|
||||
expect(await tree(readFixture('attributeDocs.py'))).toEqual([
|
||||
{ at: 1, kind: 'comment' },
|
||||
{ at: [3, 4], kind: 'const' },
|
||||
{ at: [6, 8], kind: 'variable' },
|
||||
{ at: [11, 12], kind: 'class', kids: [
|
||||
{ at: [14, 15], kind: 'variable' },
|
||||
{ at: [17, 19], kind: 'field' },
|
||||
{ at: 21, kind: 'comment' },
|
||||
{ at: 22, kind: 'comment' },
|
||||
{ at: [17, 19], kind: 'field', notes: [21, 22] },
|
||||
] },
|
||||
{ at: 25, kind: 'function', kids: [
|
||||
{ at: 26, kind: 'variable' },
|
||||
{ at: [27, 30], kind: 'comment' },
|
||||
{ at: 31, kind: 'statement' },
|
||||
{ at: 31, kind: 'statement', notes: [[27, 30]] },
|
||||
{ at: 32, kind: 'statement' },
|
||||
{ at: 33, kind: 'return' },
|
||||
] },
|
||||
@@ -158,8 +151,8 @@ describe('python 属性 docstring', () => {
|
||||
' """不算"""',
|
||||
].join('\n')
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: 1, kind: 'class', kids: [{ at: [2, 3], kind: 'field' }, { at: 4, kind: 'comment' }] },
|
||||
{ at: 5, kind: 'if', kids: [{ at: 6, kind: 'variable' }, { at: 7, kind: 'comment' }] },
|
||||
{ at: 1, kind: 'class', kids: [{ at: [2, 3], kind: 'field', notes: [4] }] },
|
||||
{ at: 5, kind: 'if', kids: [{ at: 6, kind: 'variable', notes: [7] }] },
|
||||
])
|
||||
expect(await fieldDocs(source)).toEqual({ 2: '"A" "B"' })
|
||||
})
|
||||
@@ -180,12 +173,11 @@ describe('python 函数、类的 docstring', () => {
|
||||
'def g(): "单行"',
|
||||
].join('\n')
|
||||
|
||||
it('算进函数、类头部、不单独成块,它前面的注释也在头部里;后面的注释照旧成块', async () => {
|
||||
it('算进函数、类头部、不单独成块,它前面的注释也在头部里;后面的注释挂到后面那条语句上', async () => {
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: [1, 2], kind: 'class', kids: [
|
||||
{ at: [3, 8], kind: 'function', kids: [
|
||||
{ at: 9, kind: 'comment' },
|
||||
{ at: 10, kind: 'return' },
|
||||
{ at: 10, kind: 'return', notes: [9] },
|
||||
] },
|
||||
] },
|
||||
{ at: 11, kind: 'function' },
|
||||
@@ -513,3 +505,55 @@ describe('python 调用', () => {
|
||||
expect(await calls('@cache\nclass A(Base):\n pass')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('python 注释挂到它讲的块上', () => {
|
||||
it('同一层后面最近的那块(隔着空行也算);主体最后的挂到前面那块;主体里只有注释时挂到父块;文件最后的挂到最后一块', async () => {
|
||||
const source = [
|
||||
'"""模块说明。"""',
|
||||
'',
|
||||
'import os',
|
||||
'',
|
||||
'',
|
||||
'# 讲 load:隔着空行',
|
||||
'def load(path):',
|
||||
' """读出来。"""',
|
||||
' # 先检查',
|
||||
' if not path:',
|
||||
' return None',
|
||||
' return path',
|
||||
' # 函数体最后的注释',
|
||||
'',
|
||||
'',
|
||||
'class Empty:',
|
||||
' """只有说明。"""',
|
||||
' # 只有注释的主体',
|
||||
'',
|
||||
'',
|
||||
'# 文件最后的注释',
|
||||
].join('\n')
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: 1, kind: 'comment' },
|
||||
{ at: 3, kind: 'import' },
|
||||
{ at: [7, 8], kind: 'function', notes: [6], kids: [
|
||||
{ at: 10, kind: 'if', notes: [9], kids: [{ at: 11, kind: 'return' }] },
|
||||
{ at: 12, kind: 'return', notes: [13] },
|
||||
] },
|
||||
{ at: [16, 17], kind: 'class', notes: [18, 21] },
|
||||
])
|
||||
})
|
||||
|
||||
it('文件开头的注释和第一块代码隔着空行是文件的说明,紧挨着就挂到它上面;#! 和写编码的那行总是文件的', async () => {
|
||||
expect(await tree('# 文件的说明\n\nimport os\n')).toEqual([{ at: 1, kind: 'comment' }, { at: 3, kind: 'import' }])
|
||||
expect(await tree('# 讲导入\nimport os\n')).toEqual([{ at: 2, kind: 'import', notes: [1] }])
|
||||
expect(await tree('#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n# 讲导入\nimport os\n')).toEqual([
|
||||
{ at: [1, 2], kind: 'comment' },
|
||||
{ at: 4, kind: 'import', notes: [3] },
|
||||
])
|
||||
})
|
||||
|
||||
it('luffy 的 acquisition/repository.py:导入后面隔着两个空行的注释挂到类上,是类卡片的「注释」一行,类的编辑范围连着这几行', async () => {
|
||||
const source = readFixture('acquisitionRepository.py')
|
||||
const [klass] = (await tree(source)).filter(block => block.kind === 'class')
|
||||
expect(klass).toMatchObject({ at: [19, 21], notes: [[13, 16]] })
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CommentBlock, EntryFacts, FunctionFacts, ImportFacts, ImportItem, IterationClause, IterationFacts, IterationResult, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { CommentRules, ExceptionRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { pythonNameReader } from './pythonNames'
|
||||
import { children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren, withExceptions } from './shared'
|
||||
import { attachComments, children, compoundShape, leafBlock, leafShape, listBlocks, named, sameNode, syntaxChildren, withExceptions } from './shared'
|
||||
|
||||
/** 常量、类字段只认直接写在模块 / 类主体里的赋值;这两处的声明也才有访问级别。 */
|
||||
type Scope = 'module' | 'class' | 'other'
|
||||
@@ -12,8 +12,25 @@ const PARAMETER_SEPARATORS = new Set(['keyword_separator', 'positional_separator
|
||||
const PATTERNS = new Set(['pattern_list', 'tuple_pattern', 'list_pattern'])
|
||||
/** 字符串开头(string_start):普通字符串的前缀只有 r / u 或没有;f、t、b 开头的不是。 */
|
||||
const STRING_PREFIX = /^[ru]?["']/i
|
||||
// 只有 # 行注释;函数、类的说明是主体开头的 docstring,不是写在前面的注释
|
||||
const COMMENTS: CommentRules = { lineStyle: () => '#', isDoc: () => false, documents: new Set() }
|
||||
/** PEP 263 写编码的注释:`# -*- coding: utf-8 -*-`、`# vim: set fileencoding=utf-8 :`。 */
|
||||
const CODING = /^#.*?coding[:=]\s*[-\w.]+/
|
||||
|
||||
/** 文件头两行里写给解释器看的注释:第一行的 `#!`,头两行里写编码的。 */
|
||||
function isPragma(comment: Node): boolean {
|
||||
const row = comment.startPosition.row
|
||||
return (row === 0 && comment.text.startsWith('#!')) || (row <= 1 && CODING.test(comment.text))
|
||||
}
|
||||
|
||||
// 只有 # 行注释;函数、类的说明是主体开头的 docstring,不是写在前面的注释。写给解释器的那两行不和后面的注释连成一块
|
||||
const COMMENTS: CommentRules = { lineStyle: comment => isPragma(comment) ? '#!' : '#', isDoc: () => false, documents: new Set() }
|
||||
|
||||
/** 讲整个文件的注释:模块开头的 docstring(第一条语句,注释不算),写给解释器的那两行。 */
|
||||
function aboutModule(comment: CommentBlock): boolean {
|
||||
const { node } = comment
|
||||
if (node.type === 'comment')
|
||||
return isPragma(node)
|
||||
return node.parent?.type === 'module' && sameNode(named(node.parent).find(child => child.type !== 'comment'), node)
|
||||
}
|
||||
|
||||
function statements(items: Node[], scope: Scope, afterRow: number): CodeBlock[] {
|
||||
const docs = attributeDocs(items, scope)
|
||||
@@ -636,8 +653,9 @@ export function createPythonAdapter(wasmDir: string): LanguageAdapter {
|
||||
id: 'python',
|
||||
languageIds: ['python'],
|
||||
grammarPath: () => path.join(wasmDir, 'tree-sitter-python.wasm'),
|
||||
toBlocks: root => withExceptions(statements(named(root), 'module', -1), PYTHON_EXCEPTIONS),
|
||||
toBlocks: root => withExceptions(attachComments(statements(named(root), 'module', -1), aboutModule), PYTHON_EXCEPTIONS),
|
||||
tokenRole: pythonTokenRole,
|
||||
lineComment: '#',
|
||||
iteration: pythonIteration,
|
||||
call: pythonCall,
|
||||
displayNames: pythonNameReader,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { BlockView } from '../blockView'
|
||||
import type { Outline } from '../testSupport'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from '../blockView'
|
||||
import { blockMarks } from '../diff/viewDiff'
|
||||
import { rustImplRule } from '../rules/rustImpl'
|
||||
import { isCrateRoot } from '../rules/rustTree'
|
||||
import { propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { commentText, outline, propertyLines, readFixture, viewLines, WASM_DIR, withBlocks } from '../testSupport'
|
||||
import { rustZhTranslator } from '../translators/rustZh'
|
||||
import { createRustAdapter } from './rust'
|
||||
|
||||
@@ -144,15 +145,15 @@ describe('rust 语言卡片', () => {
|
||||
expect(mod?.kind).toBe('module')
|
||||
expect(list.some(block => block.kind === 'statement' && block.code.includes('const LIMIT'))).toBe(false)
|
||||
expect(list.filter(block => block.kind === 'statement').map(block => block.code)).toEqual(['mod other;'])
|
||||
expect(mod?.children.map(block => block.kind)).toEqual(['comment', 'import', 'const', 'variable', 'function', 'function', 'statement', 'statement'])
|
||||
expect(mod?.children.map(block => block.kind)).toEqual(['import', 'const', 'variable', 'function', 'function', 'statement', 'statement'])
|
||||
expect(viewLines(mod ? [mod] : [])).toEqual([
|
||||
'module 1',
|
||||
' 名称: platform',
|
||||
' 说明: 平台相关。',
|
||||
' 可见性: 公开',
|
||||
' comment 3: 读入。',
|
||||
' import 4',
|
||||
' import 3',
|
||||
' 来源: std::io',
|
||||
' 注释: 读入。',
|
||||
' 导入项: 1 个',
|
||||
' Read | —',
|
||||
' const 5',
|
||||
@@ -204,7 +205,7 @@ describe('rust 注释', () => {
|
||||
].join('\n'))
|
||||
})
|
||||
|
||||
it('隔着空行、隔着语句的注释各是一块,行尾的 // 不单独成块', async () => {
|
||||
it('文件开头隔着空行的注释是文件的说明,别的挂到后面那一项上(中间隔着空行也算),行尾的 // 不单独成块', async () => {
|
||||
const source = [
|
||||
'// 第一段',
|
||||
'// 缩进四格的代码',
|
||||
@@ -218,15 +219,14 @@ describe('rust 注释', () => {
|
||||
const list = await views(source)
|
||||
expect(list.map(block => [block.kind, block.startLine + 1, block.endLine + 1])).toEqual([
|
||||
['comment', 1, 2],
|
||||
['comment', 4, 4],
|
||||
['import', 5, 5],
|
||||
['comment', 6, 6],
|
||||
['const', 7, 7],
|
||||
['import', 4, 5],
|
||||
['const', 6, 7],
|
||||
])
|
||||
expect(markdown(list[0])).toBe('第一段\n 缩进四格的代码')
|
||||
expect(list.map(commentText)).toEqual([undefined, '第二段', '第三段'])
|
||||
})
|
||||
|
||||
it('/// 并进下一个函数当说明,//! 连着几行合成一块,// 和 /// 不合在一起', async () => {
|
||||
it('/// 并进下一个函数当说明,//! 连着几行合成一块、是文件的说明,// 和 /// 不合在一起', async () => {
|
||||
const source = [
|
||||
'//! 本模块管凭证。',
|
||||
'//!',
|
||||
@@ -242,11 +242,10 @@ describe('rust 注释', () => {
|
||||
const list = await views(source)
|
||||
expect(list.map(block => [block.kind, block.startLine + 1, block.endLine + 1])).toEqual([
|
||||
['comment', 1, 3],
|
||||
['comment', 5, 5],
|
||||
['function', 6, 9],
|
||||
['function', 5, 9],
|
||||
])
|
||||
expect(markdown(list[0])).toBe('本模块管凭证。\n\n第二段。')
|
||||
expect(viewLines([list[2]!])).toContain(' 说明: 读出这个账号的凭证。')
|
||||
expect(viewLines([list[1]!])).toEqual(expect.arrayContaining([' 说明: 读出这个账号的凭证。', ' 注释: 普通注释']))
|
||||
})
|
||||
|
||||
it('连着几行的 /// 是一段说明,块标签拆开放:@param 进参数表、@return 跟在返回后面、@throws 成异常表;结构体的 @param 对到字段', async () => {
|
||||
@@ -306,8 +305,8 @@ describe('rust 注释', () => {
|
||||
const list = await views(source)
|
||||
const mod = list.find(block => block.kind === 'module')
|
||||
const impl = list.find(block => block.kind === 'rust.impl')
|
||||
expect(markdown(mod?.children[0])).toBe('一\n二')
|
||||
expect(markdown(impl?.children.find(block => block.kind === 'comment'))).toBe('三\n\n四')
|
||||
expect(commentText(mod?.children[0])).toBe('一\n二')
|
||||
expect(commentText(impl?.children[0])).toBe('三\n\n四')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -321,10 +320,11 @@ describe('rust 编译属性', () => {
|
||||
'',
|
||||
].join('\n')
|
||||
const list = await views(source, '/repo/apps/desktop/src-tauri/src/main.rs')
|
||||
expect(list.map(block => block.kind)).toEqual(['comment', 'attribute', 'function'])
|
||||
expect(list.map(block => block.kind)).toEqual(['attribute', 'function'])
|
||||
expect(viewLines([card(list, 'attribute')])).toEqual([
|
||||
'attribute 2',
|
||||
'attribute 1',
|
||||
' 属性: windows_subsystem = "windows"',
|
||||
' 注释: Prevents additional console window on Windows in release, DO NOT REMOVE!!',
|
||||
' 生效条件: 非 调试构建',
|
||||
' 作用范围: 整个 crate',
|
||||
' 说明: 在 Windows 上编译为图形界面程序(windows 子系统),启动时不弹出控制台窗口',
|
||||
@@ -375,3 +375,46 @@ describe('rust 编译属性', () => {
|
||||
expect(['/p/src/credentials.rs', '/p/src/tests/helpers.rs', '/p/src/bin/tool/args.rs'].map(isCrateRoot)).toEqual([false, false, false])
|
||||
})
|
||||
})
|
||||
|
||||
describe('rust 注释挂到它讲的项上', () => {
|
||||
const tree = (source: string): Promise<Outline[]> => withBlocks(adapter, 'rust', source, outline)
|
||||
|
||||
it('同一层后面最近的那项(隔着空行也算),和 /// 说明并存;只有注释的 mod 挂到 mod 上;impl 最后的挂到前面那项;文件最后的挂到最后一项;//! 是文件的说明', async () => {
|
||||
const source = [
|
||||
'//! 凭证存储。',
|
||||
'',
|
||||
'use std::io;',
|
||||
'',
|
||||
'// 讲 load:隔着空行',
|
||||
'',
|
||||
'/// 读出凭证。',
|
||||
'pub fn load() -> Option<String> { None }',
|
||||
'',
|
||||
'mod empty {',
|
||||
' // 只有注释的模块',
|
||||
'}',
|
||||
'',
|
||||
'impl Store {',
|
||||
' // 讲 get',
|
||||
' fn get(&self) {}',
|
||||
' // impl 最后的注释',
|
||||
'}',
|
||||
'',
|
||||
'// 文件最后的注释',
|
||||
].join('\n')
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: 1, kind: 'comment' },
|
||||
{ at: 3, kind: 'import' },
|
||||
{ at: [7, 8], kind: 'function', notes: [5] },
|
||||
{ at: 10, kind: 'module', notes: [11] },
|
||||
{ at: 14, kind: 'impl', notes: [20], kids: [{ at: 16, kind: 'function', notes: [15, 17] }] },
|
||||
])
|
||||
})
|
||||
|
||||
it('紧挨着第一项的注释挂到它上面;mod 里的 //! 交给那个 mod', async () => {
|
||||
expect(await tree('// 讲导入\nuse std::io;\n\nmod inner {\n //! 模块自己的\n fn a() {}\n}\n')).toEqual([
|
||||
{ at: 2, kind: 'import', notes: [1] },
|
||||
{ at: 4, kind: 'module', notes: [5], kids: [{ at: 6, kind: 'function' }] },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract'
|
||||
import type { Access, AttributeFacts, AttributeItem, CfgPredicate, ClassFacts, CodeBlock, CommentBlock, EnumBlock, FunctionBlock, ImplBlock, ImportBlock, ImportItem, LanguageAdapter, Node, ParameterFacts, SourceSpan, TokenRole, TraitBlock } from '../contract'
|
||||
import type { CommentRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { leadingAttributes, visibilityFacts } from '../rules/rustTree'
|
||||
import { children, compoundShape, extendHeader, headerSpan, isCommentNode, leafBlock, leafShape, listBlocks, named } from './shared'
|
||||
import { attachComments, children, compoundShape, extendHeader, headerSpan, isCommentNode, leafBlock, leafShape, listBlocks, named } from './shared'
|
||||
|
||||
const STORAGE = new Set(['pub', 'async', 'const', 'static', 'mut', 'let'])
|
||||
|
||||
@@ -11,8 +11,9 @@ export function createRustAdapter(wasmDir: string): LanguageAdapter {
|
||||
id: 'rust',
|
||||
languageIds: ['rust'],
|
||||
grammarPath: () => path.join(wasmDir, 'tree-sitter-rust.wasm'),
|
||||
toBlocks: root => topBlocks(named(root), -1),
|
||||
toBlocks: root => attachComments(topBlocks(named(root), -1), aboutModule),
|
||||
tokenRole: rustTokenRole,
|
||||
lineComment: '//',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +35,11 @@ const COMMENTS: CommentRules = {
|
||||
documents: new Set(['function', 'class', 'trait', 'impl', 'module', 'enum', 'const', 'variable']),
|
||||
}
|
||||
|
||||
/** 讲所在模块或 crate 的内部文档注释(`//!`、`/*!`):写在文件开头是文件的说明,写在 `mod` 身体里交给那个模块。 */
|
||||
function aboutModule(comment: CommentBlock): boolean {
|
||||
return named(comment.node).some(child => child.type === 'inner_doc_comment_marker')
|
||||
}
|
||||
|
||||
/** 文档注释记号:外部的 `/`(块注释是 `*`)、内部的 `!`;普通注释没有。 */
|
||||
function docMarker(comment: Node): string {
|
||||
return named(comment).find(child => child.type === 'outer_doc_comment_marker' || child.type === 'inner_doc_comment_marker')?.text ?? ''
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BaseFacts, BlockShape, ClassBlock, ClassFacts, CodeBlock, DocumentedBlock, ExceptionBlock, FieldFacts, Node, ParameterFacts, SourceSpan, TextKind } from '../contract'
|
||||
import type { BaseFacts, BlockShape, ClassBlock, ClassFacts, CodeBlock, CommentBlock, DocumentedBlock, ExceptionBlock, FieldFacts, Node, ParameterFacts, SourceSpan, TextKind } from '../contract'
|
||||
|
||||
/** 语言插件共用的建树工具:取子节点、算头部、把一串语句切成块。 */
|
||||
|
||||
@@ -125,7 +125,7 @@ function documentable(block: CodeBlock, kinds: ReadonlySet<string>): block is Do
|
||||
}
|
||||
|
||||
/**
|
||||
* 一串语句 → 块。注释单独成块,相邻行同一写法的行注释合成一块(见 CommentRules.lineStyle);
|
||||
* 一串语句 → 块。注释先成注释块(建好整棵树后由 attachComments 挂到它讲的块上),相邻行同一写法的行注释合成一块(见 CommentRules.lineStyle);
|
||||
* 和前一条语句(或头部)同一行的注释是行尾注释,跟着那一行显示,不单独成块。
|
||||
* 文档注释(连着几行的算一段)和下一条语句之间只有空白、下一条语句是 CommentRules.documents 里的块时,它是这一块的说明,不单独成块;
|
||||
* 中间隔着的装饰器(不出块的语句)不算隔开。
|
||||
@@ -193,6 +193,88 @@ export function listBlocks(
|
||||
return out
|
||||
}
|
||||
|
||||
// ─── 注释挂到它讲的块上 ───────────────────────────────────────────────────────
|
||||
|
||||
export function isCommentBlock(block: CodeBlock): block is CommentBlock {
|
||||
return block.kind === 'comment'
|
||||
}
|
||||
|
||||
/** 文件开头算不上「第一块代码」的块:`#!` 写的解释器、Dockerfile 的解析指令。 */
|
||||
const PREAMBLE_KINDS = new Set<string>(['interpreter', 'directive'])
|
||||
|
||||
/** 块挂上注释,和它已有的(子块交上来的)合在一起照源码顺序排。 */
|
||||
function withComments(block: CodeBlock, comments: readonly CommentBlock[]): CodeBlock {
|
||||
if (comments.length === 0)
|
||||
return block
|
||||
const all = [...(block.comments ?? []), ...comments].sort((a, b) => a.header.startIndex - b.header.startIndex)
|
||||
return { ...block, comments: all }
|
||||
}
|
||||
|
||||
/**
|
||||
* 一层兄弟块里的注释块挂到它讲的块上:同一层里它后面最近的一块代码(中间隔空行也算),后面没有代码就挂到前面最近的一块;
|
||||
* 这一层一块代码都没有时交给父块(up)。aboutScope 认出的(讲所在模块的,Rust 的 `//!`)也交给父块。先挂好各块的子块。
|
||||
*/
|
||||
function attachIn(list: readonly CodeBlock[], aboutScope: (comment: CommentBlock) => boolean): { blocks: CodeBlock[], up: CommentBlock[] } {
|
||||
const up: CommentBlock[] = []
|
||||
const done: CodeBlock[] = []
|
||||
for (const block of list) {
|
||||
if (isCommentBlock(block)) {
|
||||
if (aboutScope(block))
|
||||
up.push(block)
|
||||
else
|
||||
done.push(block)
|
||||
continue
|
||||
}
|
||||
const inner = attachIn(block.children, aboutScope)
|
||||
const changed = inner.blocks.length !== block.children.length || inner.blocks.some((child, i) => child !== block.children[i])
|
||||
done.push(withComments(changed ? { ...block, children: inner.blocks } : block, inner.up))
|
||||
}
|
||||
if (!done.some(block => !isCommentBlock(block)))
|
||||
return { blocks: [], up: [...up, ...done.filter(isCommentBlock)] }
|
||||
const out: CodeBlock[] = []
|
||||
let pending: CommentBlock[] = []
|
||||
for (const block of done) {
|
||||
if (isCommentBlock(block)) {
|
||||
pending.push(block)
|
||||
continue
|
||||
}
|
||||
out.push(withComments(block, pending))
|
||||
pending = []
|
||||
}
|
||||
out[out.length - 1] = withComments(out[out.length - 1], pending)
|
||||
return { blocks: out, up }
|
||||
}
|
||||
|
||||
/**
|
||||
* 注释挂到它讲的块上(BlockShape.comments),不再单独成块;语言插件建好整棵树后调一次。见 attachIn。
|
||||
* 单独成块的只有文件开头的说明:第一块代码(`#!` 解释器、解析指令不算)之前的注释,和第一块代码之间隔着空行的;
|
||||
* 紧挨着第一块代码(中间没空行)的那几段照常挂到它上面。aboutScope 认出的讲整个文件的注释(Python 模块开头的 docstring、
|
||||
* Rust 的 `//!`、JSDoc 的 `@file`)写在文件开头时总是文件的说明,写在模块里时交给那个模块。
|
||||
* file 为假时(嵌在别的语言里的一段代码)没有文件开头的说明,一块代码都没有时注释照旧单独成块。
|
||||
*/
|
||||
export function attachComments(blocks: readonly CodeBlock[], aboutScope: (comment: CommentBlock) => boolean = () => false, file = true): CodeBlock[] {
|
||||
const first = blocks.findIndex(block => !isCommentBlock(block) && !PREAMBLE_KINDS.has(block.kind))
|
||||
if (first < 0)
|
||||
return [...blocks]
|
||||
let header = file ? first : 0
|
||||
for (let start = blocks[first].header.startRow; header > 0; header--) {
|
||||
const block = blocks[header - 1]
|
||||
if (!isCommentBlock(block) || aboutScope(block) || block.header.endRow + 1 < start)
|
||||
break
|
||||
start = block.header.startRow
|
||||
}
|
||||
const { blocks: rest, up } = attachIn(blocks.slice(header), aboutScope)
|
||||
return [...blocks.slice(0, header), ...withScopeComments(rest, up)]
|
||||
}
|
||||
|
||||
/** 文件这一层交上来的讲所在模块的注释(不在文件开头的)挂到第一块代码上。 */
|
||||
function withScopeComments(blocks: CodeBlock[], up: readonly CommentBlock[]): CodeBlock[] {
|
||||
if (up.length === 0)
|
||||
return blocks
|
||||
const at = blocks.findIndex(block => !isCommentBlock(block))
|
||||
return at < 0 ? [...up, ...blocks] : blocks.map((block, i) => i === at ? withComments(block, up) : block)
|
||||
}
|
||||
|
||||
/** 同一个节点:web-tree-sitter 每次取子节点都会新建对象,只能按位置和类型比。 */
|
||||
export function sameNode(a: Node | null | undefined, b: Node): boolean {
|
||||
return !!a && a.startIndex === b.startIndex && a.endIndex === b.endIndex && a.type === b.type
|
||||
@@ -216,15 +298,6 @@ function classBlocks(blocks: readonly CodeBlock[]): ClassBlock[] {
|
||||
return blocks.flatMap(block => [...(block.kind === 'class' ? [block] : []), ...classBlocks(block.children)])
|
||||
}
|
||||
|
||||
/** 紧挨在 child 前面的注释块(连着几块都算,中间没隔别的子块),照源码顺序:讲的是它下面的这个 child。 */
|
||||
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)
|
||||
}
|
||||
|
||||
/** 注释块里的各条注释(几条行注释连成的一块是好几个节点);Python 当注释用的字符串是那个字符串。 */
|
||||
function commentNodes(block: CodeBlock): Node[] {
|
||||
if (!isCommentNode(block.node))
|
||||
@@ -255,29 +328,24 @@ function ownDoc(block: CodeBlock): Node | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
/** 能重写的那段注释:注释块是整块;函数、类、枚举、异常、常量、变量是它的说明。没有说明就没有。 */
|
||||
export function commentSpan(block: CodeBlock): { startLine: number, endLine: number } | undefined {
|
||||
if (block.kind === 'comment')
|
||||
return { startLine: block.header.startRow, endLine: block.header.endRow }
|
||||
const doc = ownDoc(block)
|
||||
if (!doc)
|
||||
return undefined
|
||||
return { startLine: doc.startPosition.row, endLine: doc.endPosition.row }
|
||||
}
|
||||
|
||||
/** 类属性的说明,照源码顺序:紧挨在它上面的注释块、它这一行行尾的注释、Python 紧跟在后面的属性 docstring。 */
|
||||
function fieldDocs(children: readonly CodeBlock[], block: CodeBlock): Node[] {
|
||||
/** 类属性的说明,照源码顺序:讲它的注释(BlockShape.comments)、它这一行行尾的注释、Python 紧跟在后面的属性 docstring。 */
|
||||
function fieldDocs(block: CodeBlock): Node[] {
|
||||
const trailing = block.node.nextNamedSibling
|
||||
const doc = ownDoc(block)
|
||||
return [
|
||||
...leadingComments(children, block).flatMap(commentNodes),
|
||||
...(block.comments ?? []).flatMap(commentNodes),
|
||||
...(trailing && isCommentNode(trailing) && trailing.startPosition.row === block.node.endPosition.row ? [trailing] : []),
|
||||
...(doc ? [doc] : []),
|
||||
]
|
||||
].sort((a, b) => a.startIndex - b.startIndex)
|
||||
}
|
||||
|
||||
/** 块连同讲它的注释各段的范围。 */
|
||||
function spansOf(block: CodeBlock): SourceSpan[] {
|
||||
return [block.header, nodeSpan(block.node), ...(block.comments ?? []).map(comment => comment.header)]
|
||||
}
|
||||
|
||||
/**
|
||||
* 类块换成异常块:类属性连同紧挨在它前面的注释块并进属性表,构造函数(写了几个取最后一个,重载的实现写在最后)的参数进属性表,
|
||||
* 类块换成异常块:类属性连同讲它的注释并进属性表,构造函数(写了几个取最后一个,重载的实现写在最后)的参数进属性表,
|
||||
* 留下的子块(方法、构造函数本身、别的语句)不管写在哪都照源码顺序留作子块;头部是整个类(到它连同各子块的最后一行),
|
||||
* 「编辑」时整段一起改,webview 在它编辑时不画留下的子块。
|
||||
*/
|
||||
@@ -289,19 +357,19 @@ function exceptionBlock(block: ClassBlock, rules: ExceptionRules): ExceptionBloc
|
||||
for (const child of children) {
|
||||
const field = rules.field(child)
|
||||
if (field) {
|
||||
fields.push({ ...field, docs: fieldDocs(children, child) })
|
||||
fields.push({ ...field, docs: fieldDocs(child) })
|
||||
members.push(child)
|
||||
}
|
||||
parameters = rules.constructorParameters(child) ?? parameters
|
||||
}
|
||||
const absorbed = new Set(members.flatMap(member => [...leadingComments(children, member), member]))
|
||||
const ends = [block.header, nodeSpan(block.node), ...children.flatMap(child => [child.header, nodeSpan(child.node)])]
|
||||
const ends = [block.header, nodeSpan(block.node), ...children.flatMap(spansOf)]
|
||||
const last = ends.reduce((end, span) => span.endIndex > end.endIndex ? span : end)
|
||||
return {
|
||||
kind: 'exception',
|
||||
node: block.node,
|
||||
header: { ...block.header, endIndex: last.endIndex, endRow: last.endRow },
|
||||
children: children.filter(child => !absorbed.has(child)),
|
||||
children: children.filter(child => !members.includes(child)),
|
||||
...(block.comments && { comments: block.comments }),
|
||||
...(block.attributes && { attributes: block.attributes }),
|
||||
facts: {
|
||||
name: facts.name,
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('typeScript 代码块树', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('类:方法 → function、字段 → field、注释 → comment,装饰器算进成员头部', async () => {
|
||||
it('类:方法 → function、字段 → field,注释挂到它讲的成员上,装饰器算进成员头部', async () => {
|
||||
const source = [
|
||||
'export class Store extends Base {',
|
||||
' // 缓存',
|
||||
@@ -147,8 +147,7 @@ describe('typeScript 代码块树', () => {
|
||||
].join('\n')
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: 1, kind: 'class', kids: [
|
||||
{ at: 2, kind: 'comment' },
|
||||
{ at: 3, kind: 'field' },
|
||||
{ at: 3, kind: 'field', notes: [2] },
|
||||
{ at: [4, 5], kind: 'function', kids: [{ at: 6, kind: 'return' }] },
|
||||
{ at: 8, kind: 'function', kids: [{ at: 9, kind: 'statement' }] },
|
||||
] },
|
||||
@@ -208,7 +207,7 @@ describe('typeScript 代码块树', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('相邻行的行注释合成一块,空行隔开的分开;行尾注释不单独成块', async () => {
|
||||
it('相邻行的行注释合成一段,空行隔开的分开;文件开头隔着空行的是文件的说明,紧挨着代码的挂到代码上;行尾注释不单独成块', async () => {
|
||||
const source = [
|
||||
'// a',
|
||||
'// b',
|
||||
@@ -219,8 +218,7 @@ describe('typeScript 代码块树', () => {
|
||||
].join('\n')
|
||||
expect(await tree(source, 'javascript')).toEqual([
|
||||
{ at: [1, 2], kind: 'comment' },
|
||||
{ at: 4, kind: 'comment' },
|
||||
{ at: 5, kind: 'const' },
|
||||
{ at: 5, kind: 'const', notes: [4] },
|
||||
{ at: 6, kind: 'statement' },
|
||||
])
|
||||
})
|
||||
@@ -279,7 +277,7 @@ describe('typeScript 函数的 JSDoc', () => {
|
||||
'function i() {}',
|
||||
].join('\n')
|
||||
|
||||
it('紧贴在函数块前面的算进函数头部、不单独成块;前面不是函数块、没紧贴、不是 JSDoc 的照旧是注释块', async () => {
|
||||
it('紧贴在函数、常量块前面的算进它的头部、不单独成块;前面不是能带说明的块、没紧贴、不是 JSDoc 的是普通注释,挂到后面那块上', async () => {
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: [1, 2], kind: 'function' },
|
||||
{ at: [3, 5], kind: 'function' },
|
||||
@@ -287,22 +285,15 @@ describe('typeScript 函数的 JSDoc', () => {
|
||||
{ at: [8, 9], kind: 'function', kids: [{ at: [10, 11], kind: 'function' }] },
|
||||
{ at: 13, kind: 'class', kids: [
|
||||
{ at: [14, 16], kind: 'function' },
|
||||
{ at: 17, kind: 'comment' },
|
||||
{ at: 18, kind: 'field' },
|
||||
{ at: 18, kind: 'field', notes: [17] },
|
||||
] },
|
||||
{ at: 20, kind: 'comment' },
|
||||
{ at: 21, kind: 'const' },
|
||||
{ at: 22, kind: 'comment' },
|
||||
{ at: 23, kind: 'function' },
|
||||
{ at: 24, kind: 'comment' },
|
||||
{ at: 25, kind: 'function' },
|
||||
{ at: 26, kind: 'comment' },
|
||||
{ at: 27, kind: 'comment' },
|
||||
{ at: 28, kind: 'function' },
|
||||
{ at: [20, 21], kind: 'const' },
|
||||
{ at: 23, kind: 'function', notes: [22] },
|
||||
{ at: 25, kind: 'function', notes: [24] },
|
||||
{ at: 28, kind: 'function', notes: [26, 27] },
|
||||
{ at: 29, kind: 'statement' },
|
||||
{ at: 30, kind: 'function' },
|
||||
{ at: 31, kind: 'comment' },
|
||||
{ at: 32, kind: 'function' },
|
||||
{ at: 32, kind: 'function', notes: [31] },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -314,6 +305,7 @@ describe('typeScript 函数的 JSDoc', () => {
|
||||
expect(found[8]).toMatchObject({ name: 'c', doc: '/** 函数值的变量 */' })
|
||||
expect(found[10]).toMatchObject({ name: 'd', doc: '/** 嵌套 */' })
|
||||
expect(found[14]).toMatchObject({ name: 'm', doc: '/** 方法,隔着装饰器 */', decorators: ['@memo()'] })
|
||||
expect(found[20]).toMatchObject({ doc: '/** 常量 */', declarators: [{ name: 'y' }] })
|
||||
for (const line of [23, 25, 28, 30, 32])
|
||||
expect(found[line], `L${line}`).not.toHaveProperty('doc')
|
||||
})
|
||||
@@ -498,8 +490,7 @@ describe('typeScript 类', () => {
|
||||
expect(await tree(readFixture('classes.ts.txt'))).toEqual([
|
||||
{ at: 1, kind: 'import' },
|
||||
{ at: [3, 9], kind: 'class', kids: [
|
||||
{ at: 10, kind: 'comment' },
|
||||
{ at: 11, kind: 'field' },
|
||||
{ at: 11, kind: 'field', notes: [10] },
|
||||
{ at: 13, kind: 'function' },
|
||||
{ at: 15, kind: 'class', kids: [{ at: 16, kind: 'function' }] },
|
||||
] },
|
||||
@@ -632,3 +623,48 @@ describe('typeScript 调用', () => {
|
||||
expect(await calls('new Date')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('typeScript 注释挂到它讲的块上', () => {
|
||||
it('同一层后面最近的那块(隔着空行也算),和 JSDoc 说明并存;主体最后的挂到前面那块;主体里只有注释时挂到父块;文件最后的挂到最后一块;@file 是文件的说明', async () => {
|
||||
const source = [
|
||||
'/**',
|
||||
' * @file 读配置的工具。',
|
||||
' */',
|
||||
'import { readFile } from \'node:fs/promises\'',
|
||||
'',
|
||||
'// 讲 load:隔着空行',
|
||||
'',
|
||||
'/** 读出配置。 */',
|
||||
'export async function load(path: string) {',
|
||||
' // 先检查',
|
||||
' if (!path) {',
|
||||
' // 只有注释的分支',
|
||||
' }',
|
||||
' return readFile(path)',
|
||||
' // 函数体最后的注释',
|
||||
'}',
|
||||
'',
|
||||
'class Empty {',
|
||||
' // 只有注释的类',
|
||||
'}',
|
||||
'',
|
||||
'// 文件最后的注释',
|
||||
].join('\n')
|
||||
expect(await tree(source)).toEqual([
|
||||
{ at: [1, 3], kind: 'comment' },
|
||||
{ at: 4, kind: 'import' },
|
||||
{ at: [8, 9], kind: 'function', notes: [6], kids: [
|
||||
{ at: 11, kind: 'if', notes: [10, 12] },
|
||||
{ at: 14, kind: 'return', notes: [15] },
|
||||
] },
|
||||
{ at: 18, kind: 'class', notes: [19, 22] },
|
||||
])
|
||||
})
|
||||
|
||||
it('文件开头的注释和第一块代码隔着空行是文件的说明,紧挨着就挂到它上面', async () => {
|
||||
expect(await tree('// 文件的说明\n\n// 紧挨着第一块代码\nconst x = 1\n')).toEqual([
|
||||
{ at: 1, kind: 'comment' },
|
||||
{ at: 4, kind: 'const', notes: [3] },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { Access, BaseFacts, BlockShape, CallArgument, CallFacts, ClassFacts, CodeBlock, CommentBlock, DeclaratorFacts, EntryFacts, EnumMemberFacts, FunctionFacts, ImportFacts, ImportItem, LanguageAdapter, Node, ParameterFacts, TextKind, TokenRole } from '../contract'
|
||||
import type { CommentRules, ExceptionRules } from './shared'
|
||||
import * as path from 'node:path'
|
||||
import { children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
|
||||
import { attachComments, children, compoundShape, extendHeader, headerSpan, leafBlock, leafShape, listBlocks, named, nodeSpan, sameNode, syntaxChildren, withExceptions } from './shared'
|
||||
import { typescriptNameReader } from './typescriptNames'
|
||||
|
||||
const GRAMMARS: Record<string, string> = {
|
||||
@@ -22,7 +22,14 @@ const COMMENTS: CommentRules = {
|
||||
lineStyle: comment => comment.text.startsWith('//') ? '//' : undefined,
|
||||
// `/**/` 是空的块注释,不是 JSDoc
|
||||
isDoc: comment => comment.text.startsWith('/**') && !comment.text.startsWith('/**/'),
|
||||
documents: new Set(['function', 'class', 'enum']),
|
||||
documents: new Set(['function', 'class', 'enum', 'const', 'variable']),
|
||||
}
|
||||
|
||||
/** 讲整个文件的 JSDoc:写了 `@file`、`@fileoverview` 或 `@overview`。 */
|
||||
const FILE_TAG = /^\/\*\*[\s\S]*@(?:file|fileoverview|overview)\b/
|
||||
|
||||
function aboutFile(comment: CommentBlock): boolean {
|
||||
return FILE_TAG.test(comment.node.text)
|
||||
}
|
||||
|
||||
/** 带语句块主体的函数值(箭头函数 / 函数表达式)的主体。 */
|
||||
@@ -639,8 +646,9 @@ export function createTypeScriptAdapter(wasmDir: string): LanguageAdapter {
|
||||
throw new Error(`TypeScript 语言插件不支持 ${languageId}`)
|
||||
return path.join(wasmDir, file)
|
||||
},
|
||||
toBlocks: (root, _source, languageId) => withExceptions(blockReader(TYPED_LANGUAGES.has(languageId))(root), TYPESCRIPT_EXCEPTIONS),
|
||||
toBlocks: (root, _source, languageId) => withExceptions(attachComments(blockReader(TYPED_LANGUAGES.has(languageId))(root), aboutFile), TYPESCRIPT_EXCEPTIONS),
|
||||
tokenRole: typescriptTokenRole,
|
||||
lineComment: '//',
|
||||
call: typescriptCall,
|
||||
displayNames: typescriptNameReader,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user