✨ feat(vscode): 代码阅读认出代码里起的中文名(Annotated[…, Comment("…")]、@Comment 装饰器、行尾 # @Comment、docstring / JSDoc 的参数说明),预览里定义和用到它的各处都写中文名,悬停看原名,定义处后面淡淡写原名
This commit is contained in:
1 parent
1b1299dc1f
commit
44c221ea2b
45 files changed
+1685
-128
No files matched your search
@@ -1,11 +1,13 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { BlockTreeState } from './BlockTree'
|
||||
import type { NameMap, VersionNames } from './displayNames'
|
||||
import type { DocumentMessage, Excluded, Reveal } from './types'
|
||||
import type { OccurrencePage } from './useOccurrences'
|
||||
import type { ViewState } from './viewState'
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { BlockLevel } from './BlockTree'
|
||||
import { acceptNames, DisplayNameContext, documentNames } from './displayNames'
|
||||
import { coveredByEditing, startEditing } from './editing'
|
||||
import { applyFilter, keptWhileEditing } from './filter'
|
||||
import { FilterBar } from './FilterBar'
|
||||
@@ -20,6 +22,7 @@ import { onMessage, postMessage, viewState } from './vscode'
|
||||
type BlockModes = Pick<ViewState, 'editing' | 'folds'>
|
||||
|
||||
const NONE: ReadonlySet<string> = new Set()
|
||||
const NO_NAMES: NameMap = new Map()
|
||||
|
||||
function savedBlockModes(): BlockModes {
|
||||
const { editing, folds } = viewState.get()
|
||||
@@ -38,6 +41,8 @@ export function App(): ReactElement | null {
|
||||
const [excluded, setExcluded] = useState<Excluded>({})
|
||||
/** 跳到定义时临时露出来的块(目标和它的各层祖先),筛选一变、文档一刷新就回到筛选状态 */
|
||||
const [keep, setKeep] = useState<ReadonlySet<string>>(NONE)
|
||||
/** 页面上这个文档版本的中文名:文档带来的,扩展端查到了再换 */
|
||||
const [names, setNames] = useState<VersionNames | undefined>()
|
||||
/** 收到消息时读眼下哪些块在编辑(消息回调只在挂载时注册一次) */
|
||||
const editing = useRef(view.editing)
|
||||
const collapsed = useMemo(() => collapsedIds(doc?.blocks ?? [], view.folds), [doc, view.folds])
|
||||
@@ -69,6 +74,7 @@ export function App(): ReactElement | null {
|
||||
switch (msg.type) {
|
||||
case 'document':
|
||||
setDoc(msg)
|
||||
setNames(documentNames(msg.version, msg.names))
|
||||
setKeep(prev => keptWhileEditing(prev, editing.current))
|
||||
if (msg.reveal)
|
||||
setReveal(msg.reveal)
|
||||
@@ -76,6 +82,9 @@ export function App(): ReactElement | null {
|
||||
case 'reveal':
|
||||
setReveal({ line: msg.line, definition: msg.definition })
|
||||
return
|
||||
case 'displayNames':
|
||||
setNames(prev => acceptNames(prev, msg.version, msg.names))
|
||||
return
|
||||
case 'filter':
|
||||
setExcluded(msg.excluded)
|
||||
setKeep(NONE)
|
||||
@@ -119,35 +128,37 @@ export function App(): ReactElement | null {
|
||||
|
||||
return (
|
||||
<HighlightContext value={highlight}>
|
||||
<FilterBar
|
||||
attributes={doc.attributes}
|
||||
excluded={excluded}
|
||||
view={filter}
|
||||
highlight={highlight}
|
||||
onChange={next => postMessage({ type: 'setFilter', excluded: next })}
|
||||
translators={doc.translators}
|
||||
translatorId={doc.translatorId}
|
||||
onSelectTranslator={id => postMessage({ type: 'selectTranslator', id })}
|
||||
/>
|
||||
<div className="mx-auto box-content max-w-[880px] px-6 py-5">
|
||||
{doc.error !== undefined && (
|
||||
<div className="mb-3 flex items-start gap-2 rounded-lg border border-[var(--vscode-inputValidation-errorBorder)] bg-[var(--vscode-inputValidation-errorBackground)] px-3 py-2">
|
||||
<TriangleAlert size={16} strokeWidth={1.8} className="mt-px shrink-0 text-[var(--vscode-errorForeground)]" />
|
||||
<div className="min-w-0">
|
||||
<div className="break-words">
|
||||
解析失败:
|
||||
{doc.error}
|
||||
<DisplayNameContext value={names?.names ?? NO_NAMES}>
|
||||
<FilterBar
|
||||
attributes={doc.attributes}
|
||||
excluded={excluded}
|
||||
view={filter}
|
||||
highlight={highlight}
|
||||
onChange={next => postMessage({ type: 'setFilter', excluded: next })}
|
||||
translators={doc.translators}
|
||||
translatorId={doc.translatorId}
|
||||
onSelectTranslator={id => postMessage({ type: 'selectTranslator', id })}
|
||||
/>
|
||||
<div className="mx-auto box-content max-w-[880px] px-6 py-5">
|
||||
{doc.error !== undefined && (
|
||||
<div className="mb-3 flex items-start gap-2 rounded-lg border border-[var(--vscode-inputValidation-errorBorder)] bg-[var(--vscode-inputValidation-errorBackground)] px-3 py-2">
|
||||
<TriangleAlert size={16} strokeWidth={1.8} className="mt-px shrink-0 text-[var(--vscode-errorForeground)]" />
|
||||
<div className="min-w-0">
|
||||
<div className="break-words">
|
||||
解析失败:
|
||||
{doc.error}
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-[var(--vscode-descriptionForeground)]">点编辑器右上角的「编辑」查看源码。</div>
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-[var(--vscode-descriptionForeground)]">点编辑器右上角的「编辑」查看源码。</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{doc.blocks.length === 0
|
||||
? doc.error === undefined && <div className="text-[var(--vscode-descriptionForeground)]">这个文件里没有代码。</div>
|
||||
: doc.blocks.some(block => !filter.hidden.has(block.id))
|
||||
? <BlockLevel blocks={doc.blocks} tree={tree} />
|
||||
: <div className="text-[var(--vscode-descriptionForeground)]">所有块都被筛选隐藏了。</div>}
|
||||
</div>
|
||||
)}
|
||||
{doc.blocks.length === 0
|
||||
? doc.error === undefined && <div className="text-[var(--vscode-descriptionForeground)]">这个文件里没有代码。</div>
|
||||
: doc.blocks.some(block => !filter.hidden.has(block.id))
|
||||
? <BlockLevel blocks={doc.blocks} tree={tree} />
|
||||
: <div className="text-[var(--vscode-descriptionForeground)]">所有块都被筛选隐藏了。</div>}
|
||||
</div>
|
||||
</DisplayNameContext>
|
||||
</HighlightContext>
|
||||
)
|
||||
}
|
||||
@@ -3,31 +3,40 @@ import type { ReactElement } from 'react'
|
||||
import type { GroupStyle, Inline, SegmentGroup } from './types'
|
||||
import { Repeat } from 'lucide-react'
|
||||
import { useContext } from 'react'
|
||||
import { DisplayNameContext, shownName } from './displayNames'
|
||||
import { isCurrent, segmentOccurrence } from './occurrences'
|
||||
import { HighlightContext } from './useOccurrences'
|
||||
|
||||
/**
|
||||
* 按词性给每一段上色,颜色在 codeReader.css 的 .cr-role-* 里按主题切换;带源码下标的名字记在 data-offset 上,
|
||||
* 跳到定义、高亮各处出现用;是眼下高亮着的名字的一处出现时 data-occurrence 标出底色,是当前那一处(点中的、用键盘跳到的)时
|
||||
* 再标 data-current。框起来的一组词画成小框,框里的词同样处理。
|
||||
* 再标 data-current。起了中文名的名字显示中文名(data-name),悬停看原名,定义处本身在后面淡淡写原名;下标照旧,跳到定义、高亮照常。
|
||||
* 框起来的一组词画成小框,框里的词同样处理。
|
||||
*/
|
||||
export function SegmentText({ segments }: { segments: Inline[] }): ReactElement {
|
||||
const highlight = useContext(HighlightContext)
|
||||
const names = useContext(DisplayNameContext)
|
||||
return (
|
||||
<>
|
||||
{segments.map((piece, index) => 'role' in piece
|
||||
? (
|
||||
<span
|
||||
key={index}
|
||||
className={`cr-role-${piece.role}`}
|
||||
data-offset={piece.offset}
|
||||
data-occurrence={highlight && segmentOccurrence(piece, highlight.occurrences)}
|
||||
data-current={highlight && isCurrent(piece, highlight) ? '' : undefined}
|
||||
>
|
||||
{piece.text}
|
||||
</span>
|
||||
)
|
||||
: <GroupBox key={index} group={piece} />)}
|
||||
{segments.map((piece, index) => {
|
||||
if (!('role' in piece))
|
||||
return <GroupBox key={index} group={piece} />
|
||||
const shown = shownName(piece, names)
|
||||
return (
|
||||
<span
|
||||
key={index}
|
||||
className={`cr-role-${piece.role}`}
|
||||
data-offset={piece.offset}
|
||||
data-occurrence={highlight && segmentOccurrence(piece, highlight.occurrences)}
|
||||
data-current={highlight && isCurrent(piece, highlight) ? '' : undefined}
|
||||
data-name={shown?.name}
|
||||
title={shown && `原名 ${piece.text}`}
|
||||
>
|
||||
{shown ? shown.name : piece.text}
|
||||
{shown?.definition && <span className="cr-original">{piece.text}</span>}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -283,6 +283,13 @@ body:is(.vscode-light, .vscode-high-contrast-light) [data-badge] { --kind: var(-
|
||||
border-top: 1px solid color-mix(in srgb, currentColor 30%, transparent);
|
||||
}
|
||||
|
||||
/* 起了中文名的定义处:中文名后面淡淡写原名,字小一号,悬停看完整的「原名 x」 */
|
||||
.cr-original {
|
||||
margin-left: 0.4em;
|
||||
font-size: 0.86em;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
/* 按住 ⌘ / Ctrl 停在查得到定义的名字上:和编辑器里的链接一样 */
|
||||
.cr-code [data-link] {
|
||||
color: var(--vscode-editorLink-activeForeground);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Inline, ShownName, WebviewToExtension } from './types'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { acceptNames, DisplayNameContext, documentNames, nameMap, shownName } from './displayNames'
|
||||
import { OccurrenceHighlight } from './occurrences'
|
||||
import { SegmentText } from './SegmentText'
|
||||
|
||||
// 高亮的上下文所在的模块连着和 VS Code 收发消息的那一套,渲染成 HTML 用不着
|
||||
vi.mock('./useOccurrences', async () => {
|
||||
const { createContext } = await import('react')
|
||||
return { HighlightContext: createContext(undefined) }
|
||||
})
|
||||
|
||||
const CURRENT_USER: ShownName = { offset: 40, length: 12, name: '当前用户', definition: true }
|
||||
const USE: ShownName = { offset: 90, length: 12, name: '当前用户' }
|
||||
const ADMIN: ShownName = { offset: 130, length: 5, name: '管理员' }
|
||||
|
||||
function render(segments: Inline[], names: ShownName[]): string {
|
||||
return renderToStaticMarkup(
|
||||
<DisplayNameContext value={nameMap(names)}>
|
||||
<SegmentText segments={segments} />
|
||||
</DisplayNameContext>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('页面上的中文名', () => {
|
||||
it('收到新文档换成它带来的;查到的是页面上这个版本的才换上,迟到的旧版本不理', () => {
|
||||
const current = documentNames(3, [USE])
|
||||
expect([...current.names.values()]).toEqual([USE])
|
||||
expect(acceptNames(current, 2, [ADMIN])).toBe(current)
|
||||
expect([...acceptNames(current, 3, [USE, ADMIN])!.names.values()]).toEqual([USE, ADMIN])
|
||||
expect(acceptNames(undefined, 3, [ADMIN])).toBeUndefined()
|
||||
})
|
||||
|
||||
it('只认下标和长度都对得上的名字:没下标的译词、长度不一样的(名字改长了)不换', () => {
|
||||
const names = nameMap([USE])
|
||||
expect(shownName({ text: 'current_user', role: 'variable', offset: 90 }, names)).toBe(USE)
|
||||
expect(shownName({ text: 'current_userx', role: 'variable', offset: 90 }, names)).toBeUndefined()
|
||||
expect(shownName({ text: 'current_user', role: 'variable' }, names)).toBeUndefined()
|
||||
expect(shownName({ text: 'current_user', role: 'variable', offset: 91 }, names)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('用到的地方显示中文名,悬停写「原名 x」,下标照旧(⌘ 点击跳到定义、单击高亮都认它)', () => {
|
||||
const html = render([
|
||||
{ text: '如果', role: 'keyword' },
|
||||
{ text: ' ', role: 'plain' },
|
||||
{ text: 'current_user', role: 'variable', offset: 90 },
|
||||
{ text: '.', role: 'plain' },
|
||||
{ text: 'role', role: 'variable', offset: 103 },
|
||||
{ text: ' 不等于 ', role: 'operator' },
|
||||
{ text: 'UserRole', role: 'variable', offset: 120 },
|
||||
{ text: '.', role: 'plain' },
|
||||
{ text: 'ADMIN', role: 'variable', offset: 130 },
|
||||
], [USE, ADMIN])
|
||||
expect(html.replace(/<[^>]*>/g, '')).toBe('如果 当前用户.role 不等于 UserRole.管理员')
|
||||
expect(html).toContain('<span class="cr-role-variable" data-offset="90" data-name="当前用户" title="原名 current_user">当前用户</span>')
|
||||
expect(html).toContain('<span class="cr-role-variable" data-offset="103">role</span>')
|
||||
expect(html).not.toContain('cr-original')
|
||||
})
|
||||
|
||||
it('定义处本身(名称格子、名称行):中文名后面跟着淡淡的原名;框里的名字同样换', () => {
|
||||
const html = render([
|
||||
{ text: 'current_user', role: 'variable', offset: 40 },
|
||||
{ style: 'iteration', label: '列表', segments: [{ text: 'ADMIN', role: 'variable', offset: 130 }] },
|
||||
], [CURRENT_USER, ADMIN])
|
||||
expect(html).toContain('data-offset="40" data-name="当前用户" title="原名 current_user">当前用户<span class="cr-original">current_user</span></span>')
|
||||
expect(html).toContain('data-offset="130" data-name="管理员" title="原名 ADMIN">管理员</span>')
|
||||
})
|
||||
|
||||
it('单击显示成中文名的名字:筛选栏写中文名,当前那一处按点中的下标认', () => {
|
||||
const sent: WebviewToExtension[] = []
|
||||
let shown: string | undefined
|
||||
const highlight = new OccurrenceHighlight(msg => sent.push(msg), next => shown = next && `${next.name} ${next.current}`)
|
||||
highlight.setDocument(1, 'zh')
|
||||
highlight.click({ kind: 'find', offset: 90, name: '当前用户' })
|
||||
const asked = sent.at(-1)
|
||||
expect(asked).toMatchObject({ type: 'findOccurrences', offset: 90 })
|
||||
highlight.answer(asked?.type === 'findOccurrences' ? asked.id : -1, [{ start: 40, end: 52, kind: 'write' }, { start: 90, end: 102, kind: 'read' }])
|
||||
expect(shown).toBe('当前用户 1')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { Segment, ShownName } from './types'
|
||||
import { createContext } from 'react'
|
||||
|
||||
/** 页面上这个版本的中文名:下标 → 显示成什么。 */
|
||||
export type NameMap = ReadonlyMap<number, ShownName>
|
||||
|
||||
/** 眼下页面上显示成中文名的名字,SegmentText 按它换字。 */
|
||||
export const DisplayNameContext = createContext<NameMap>(new Map())
|
||||
|
||||
/** 某个文档版本的中文名。 */
|
||||
export interface VersionNames {
|
||||
version: number
|
||||
names: NameMap
|
||||
}
|
||||
|
||||
export function nameMap(names: readonly ShownName[]): NameMap {
|
||||
return new Map(names.map(name => [name.offset, name]))
|
||||
}
|
||||
|
||||
/** 收到新文档:换成它带来的中文名(上一版查到的跟着改动挪过位置的,或这一版查过的)。 */
|
||||
export function documentNames(version: number, names: readonly ShownName[]): VersionNames {
|
||||
return { version, names: nameMap(names) }
|
||||
}
|
||||
|
||||
/** 收到查到的中文名:是页面上这个版本的才换上,迟到的旧版本的不理。 */
|
||||
export function acceptNames(current: VersionNames | undefined, version: number, names: readonly ShownName[]): VersionNames | undefined {
|
||||
return current?.version === version ? { version, names: nameMap(names) } : current
|
||||
}
|
||||
|
||||
/** 这段名字显示成什么中文名:照抄自源码、下标和长度都对得上才算。 */
|
||||
export function shownName(segment: Segment, names: NameMap): ShownName | undefined {
|
||||
const shown = segment.offset === undefined ? undefined : names.get(segment.offset)
|
||||
return shown?.length === segment.text.length ? shown : undefined
|
||||
}
|
||||
@@ -65,7 +65,7 @@ describe('occurrences:哪些段画高亮', () => {
|
||||
' return "".join(secrets.choice(ALPHABET) for _ in range(8))',
|
||||
].join('\n')
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
const views = await withBlocks(py, 'python', source, blocks => toBlockViews(blocks, source, py, pythonZhTranslator))
|
||||
const views = await withBlocks(py, 'python', source, blocks => toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator))
|
||||
const highlighted = (occurrences: Occurrence[]): string[] => allSegments(views).flatMap((segment) => {
|
||||
const style = segmentOccurrence(segment, occurrences)
|
||||
return style ? [`${segment.text}:${style}`] : []
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface Click {
|
||||
count: number
|
||||
/** 点完页面上有选中的文字:这是拖着选字,不是点 */
|
||||
selecting: boolean
|
||||
/** 点中的带下标的名字 */
|
||||
/** 点中的带下标的名字:源码下标,页面上显示的字(起了中文名的是中文名) */
|
||||
name?: { offset: number, text: string }
|
||||
/** 点在自己管点击的东西上:按钮、链接、输入框、下拉列表、筛选栏、属性名 */
|
||||
control: boolean
|
||||
@@ -175,8 +175,8 @@ export class OccurrenceHighlight {
|
||||
private translatorId: string | undefined
|
||||
/** 最近一次询问的 id;别的回答(点了别处、清掉之后才到的)都丢掉 */
|
||||
private request = 0
|
||||
/** 最近一次询问点中的那段名字;答过或清掉后没有 */
|
||||
private asked: Segment | undefined
|
||||
/** 最近一次询问点中的名字:源码下标和页面上显示的字;答过或清掉后没有 */
|
||||
private asked: { offset: number, name: string } | undefined
|
||||
/** 高亮着的名字没有当前那一处时,从这个源码下标(点中的名字)数起跳到哪一处 */
|
||||
private from = 0
|
||||
private current: Highlight | undefined
|
||||
@@ -213,9 +213,9 @@ export class OccurrenceHighlight {
|
||||
return
|
||||
this.asked = undefined
|
||||
const sorted = [...occurrences].sort((a, b) => a.start - b.start || a.end - b.end)
|
||||
const current = sorted.findIndex(occurrence => overlaps(asked, occurrence))
|
||||
this.from = asked.offset ?? 0
|
||||
this.show(sorted.length > 0 ? { name: asked.text, occurrences: sorted, ...(current >= 0 && { current }) } : undefined)
|
||||
const current = sorted.findIndex(occurrence => occurrence.start <= asked.offset && asked.offset < occurrence.end)
|
||||
this.from = asked.offset
|
||||
this.show(sorted.length > 0 ? { name: asked.name, occurrences: sorted, ...(current >= 0 && { current }) } : undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,7 +240,7 @@ export class OccurrenceHighlight {
|
||||
private find(offset: number, name: string): void {
|
||||
if (this.version === undefined)
|
||||
return
|
||||
this.asked = { text: name, role: 'plain', offset }
|
||||
this.asked = { offset, name }
|
||||
this.post({ type: 'findOccurrences', id: ++this.request, version: this.version, offset })
|
||||
}
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
|
||||
async function jump(adapter: LanguageAdapter, languageId: string, translator: Translator, source: string, needle: string): Promise<{ plan: RevealPlan | undefined, flashed: string }> {
|
||||
const offset = source.indexOf(needle)
|
||||
expect(offset, needle).toBeGreaterThanOrEqual(0)
|
||||
const views: BlockView[] = await withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, source, adapter, translator))
|
||||
const views: BlockView[] = await withBlocks(adapter, languageId, source, blocks => toBlockViews(blocks, { source, language: adapter, markers: [] }, translator))
|
||||
const plan = revealPlan(views, source.slice(0, offset).split('\n').length - 1, offset)
|
||||
return { plan, flashed: flashed(views, plan) }
|
||||
}
|
||||
@@ -323,7 +323,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
|
||||
expect(await at('T>')).toEqual({ plan: { id: '0', row: '0/p3', expand: [] }, flashed: '类型参数: T' })
|
||||
expect(await at('load')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'load(id: T) {' })
|
||||
expect(await at('id: T')).toEqual({ plan: { id: '0.0', row: '0.0/p3/0', expand: ['0', '0.0/p3'] }, flashed: 'id | T | ' })
|
||||
const views = await withBlocks(ts, 'typescript', source, blocks => toBlockViews(blocks, source, ts, typescriptZhTranslator))
|
||||
const views = await withBlocks(ts, 'typescript', source, blocks => toBlockViews(blocks, { source, language: ts, markers: [] }, typescriptZhTranslator))
|
||||
expect([...collapsedIds(views, {})].sort()).toEqual(['0', '0.0'])
|
||||
|
||||
const pySource = ['class NotFound(AppException):', ' """找不到。"""', ' def run(self):', ' pass'].join('\n')
|
||||
@@ -344,7 +344,7 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
|
||||
' return self.name',
|
||||
].join('\n')
|
||||
const views = await withBlocks(py, 'python', source, blocks =>
|
||||
toBlockViews(blocks, source, py, pythonZhTranslator, { rules: [sqlalchemyRule], report: () => {} }))
|
||||
toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator, { rules: [sqlalchemyRule], report: () => {} }))
|
||||
const at = (needle: string): { plan: RevealPlan | undefined, flashed: string } => {
|
||||
const offset = source.indexOf(needle)
|
||||
const plan = revealPlan(views, source.slice(0, offset).split('\n').length - 1, offset)
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('属性行下面的子行怎么排', () => {
|
||||
'D = {"k": 1}',
|
||||
].join('\n')
|
||||
const py = createPythonAdapter(WASM_DIR)
|
||||
const views = await withBlocks(py, 'python', source, blocks => toBlockViews(blocks, source, py, pythonZhTranslator))
|
||||
const views = await withBlocks(py, 'python', source, blocks => toBlockViews(blocks, { source, language: py, markers: [] }, pythonZhTranslator))
|
||||
const layouts = views.flatMap(view => 'properties' in view ? view.properties.flatMap((row: PropertyRow) => subRowsLayout(row) ? [[row.label, subRowsLayout(row)]] : []) : [])
|
||||
expect(layouts).toEqual([['导入项', 'list'], ['输入参数', 'table'], ['值', 'list']])
|
||||
})
|
||||
|
||||
@@ -154,6 +154,19 @@ export interface DocumentMessage {
|
||||
error?: string
|
||||
/** 只在新预览的第一份 document 里带:从文本编辑器切过来时是它顶部那一行,跳到定义时是定义处 */
|
||||
reveal?: Reveal
|
||||
/** 这个版本眼下已知的中文名;查完扩展端再发 displayNames 换掉 */
|
||||
names: ShownName[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示成中文名的一个名字:源码下标 [offset, offset + length) 的名字显示成 name,悬停看原名;
|
||||
* definition 是定义处本身,中文名后面淡淡写原名。页面只按下标认,不认识任何语言。
|
||||
*/
|
||||
export interface ShownName {
|
||||
offset: number
|
||||
length: number
|
||||
name: string
|
||||
definition?: true
|
||||
}
|
||||
|
||||
/** 问过的一次:问题、回答(Markdown)、什么时候问的(ISO 时间)、用哪个 profile 答的 */
|
||||
@@ -235,6 +248,7 @@ export interface AnswerEnd {
|
||||
* answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事
|
||||
* answerEnd:askId 那个问题答完了,见 AnswerEnd
|
||||
* moveOccurrence:跳到高亮着的名字的下一处(step 1)/ 上一处(step -1)出现
|
||||
* displayNames:version 这份文档里显示成中文名的名字,查到一步来一步;不是页面上这个版本的不理
|
||||
*/
|
||||
export type ExtensionToWebview
|
||||
= | DocumentMessage
|
||||
@@ -242,6 +256,7 @@ export type ExtensionToWebview
|
||||
| { type: 'definitionProbed', id: number, found: boolean }
|
||||
| { type: 'occurrencesFound', id: number, occurrences: Occurrence[] }
|
||||
| { type: 'moveOccurrence', step: 1 | -1 }
|
||||
| { type: 'displayNames', version: number, names: ShownName[] }
|
||||
| { type: 'filter', excluded: Excluded }
|
||||
| QaMessage
|
||||
| ({ type: 'askProfiles' } & ProfileChoice)
|
||||
|
||||
@@ -34,7 +34,8 @@ function clickOf(event: MouseEvent): Click {
|
||||
modified: event.metaKey || event.ctrlKey || event.altKey || event.shiftKey,
|
||||
count: event.detail,
|
||||
selecting: selection !== null && !selection.isCollapsed,
|
||||
name: name?.dataset.offset === undefined ? undefined : { offset: Number(name.dataset.offset), text: name.textContent ?? '' },
|
||||
// 起了中文名的名字写的是中文名(定义处后面还跟着原名),筛选栏上写中文名
|
||||
name: name?.dataset.offset === undefined ? undefined : { offset: Number(name.dataset.offset), text: name.dataset.name ?? name.textContent ?? '' },
|
||||
control: target?.closest(CONTROLS) != null,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user