✨ feat(vscode): 代码阅读把推导式换成按读的顺序重排的遍历小框
This commit is contained in:
1 parent
677fb7da1f
commit
6416cdf714
16 files changed
+641
-91
No files matched your search
@@ -1,7 +1,8 @@
|
||||
import type { ReactElement } from 'react'
|
||||
import type { PropertyItem, PropertyRow, Segment } from './types'
|
||||
import type { Inline, PropertyItem, PropertyRow } from './types'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { Fragment } from 'react'
|
||||
import { inlineText } from './segments'
|
||||
import { SegmentText } from './SegmentText'
|
||||
import { TextPreview } from './TextPreview'
|
||||
import { propertyItemId, propertyRowId } from './viewState'
|
||||
@@ -20,15 +21,11 @@ interface Folding {
|
||||
onToggleFold: (id: string) => void
|
||||
}
|
||||
|
||||
function segmentsText(segments: Segment[]): string {
|
||||
return segments.map(segment => segment.text).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
|
||||
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
|
||||
*/
|
||||
function ItemHead({ id, cell, foldable, folding }: { id: string, cell: Segment[], foldable: boolean, folding: Folding }): ReactElement {
|
||||
function ItemHead({ id, cell, foldable, folding }: { id: string, cell: Inline[], foldable: boolean, folding: Folding }): ReactElement {
|
||||
if (!foldable) {
|
||||
return (
|
||||
<div className="flex min-w-0 gap-1">
|
||||
@@ -45,7 +42,7 @@ function ItemHead({ id, cell, foldable, folding }: { id: string, cell: Segment[]
|
||||
<button
|
||||
type="button"
|
||||
className="group flex min-w-0 items-start gap-1 rounded-sm text-left"
|
||||
aria-label={`${folded ? '展开' : '折叠'}${segmentsText(cell)}`}
|
||||
aria-label={`${folded ? '展开' : '折叠'}${inlineText(cell)}`}
|
||||
aria-expanded={!folded}
|
||||
onClick={() => folding.onToggleFold(id)}
|
||||
>
|
||||
|
||||
@@ -1,27 +1,48 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import type { ReactElement } from 'react'
|
||||
import type { Segment } from './types'
|
||||
import type { GroupStyle, Inline, SegmentGroup } from './types'
|
||||
import { Repeat } from 'lucide-react'
|
||||
import { useContext } from 'react'
|
||||
import { segmentOccurrence } from './occurrences'
|
||||
import { HighlightContext } from './useOccurrences'
|
||||
|
||||
/**
|
||||
* 按词性给每一段上色,颜色在 codeReader.css 的 .cr-role-* 里按主题切换;带源码下标的名字记在 data-offset 上,
|
||||
* 跳到定义、高亮各处出现用;是眼下高亮着的名字的一处出现时 data-occurrence 标出底色。
|
||||
* 跳到定义、高亮各处出现用;是眼下高亮着的名字的一处出现时 data-occurrence 标出底色。框起来的一组词画成小框,框里的词同样处理。
|
||||
*/
|
||||
export function SegmentText({ segments }: { segments: Segment[] }): ReactElement {
|
||||
export function SegmentText({ segments }: { segments: Inline[] }): ReactElement {
|
||||
const highlight = useContext(HighlightContext)
|
||||
return (
|
||||
<>
|
||||
{segments.map((segment, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={`cr-role-${segment.role}`}
|
||||
data-offset={segment.offset}
|
||||
data-occurrence={highlight && segmentOccurrence(segment, highlight.occurrences)}
|
||||
>
|
||||
{segment.text}
|
||||
</span>
|
||||
))}
|
||||
{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)}
|
||||
>
|
||||
{piece.text}
|
||||
</span>
|
||||
)
|
||||
: <GroupBox key={index} group={piece} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** 框的样式 → 框头图标;颜色在 codeReader.css 的 .cr-group[data-style] 里。 */
|
||||
const GROUP_ICONS: Record<GroupStyle, LucideIcon> = { iteration: Repeat }
|
||||
|
||||
/** 框起来的一组词:嵌在前后文字里的小框,放不下时整个挪到下一行,比一行还长时在框里换行。 */
|
||||
function GroupBox({ group }: { group: SegmentGroup }): ReactElement {
|
||||
const Icon = GROUP_ICONS[group.style]
|
||||
return (
|
||||
<span className="cr-group" data-style={group.style}>
|
||||
<span className="cr-group-head">
|
||||
<Icon size={12} strokeWidth={2} aria-hidden />
|
||||
{group.label}
|
||||
</span>
|
||||
<SegmentText segments={group.segments} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -278,6 +278,38 @@ body.vscode-high-contrast-light {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/*
|
||||
* 框起来的一组词(写在表达式里的遍历):嵌在译文里的小框,框头是图标和 label,边框、底色、框头取框的样式色(遍历同「循环」块)。
|
||||
* 整个框放不下时挪到下一行,比一行还长时占满一行、在框里换行。上下边框算在行高里:框和前后文字顶部对齐、基线对齐,不把这一行撑高;
|
||||
* 框里再套的框行高再减 2px,同样不撑高外面那个框的行
|
||||
*/
|
||||
.cr-group[data-style="iteration"] { --group: var(--cr-kind-loop); }
|
||||
.cr-group {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
padding: 0 5px;
|
||||
line-height: 18px;
|
||||
vertical-align: top;
|
||||
border: 1px solid color-mix(in srgb, var(--group) 45%, transparent);
|
||||
border-radius: 4px;
|
||||
background: color-mix(in srgb, var(--group) 14%, transparent);
|
||||
}
|
||||
.cr-group .cr-group {
|
||||
line-height: 16px;
|
||||
}
|
||||
.cr-group-head {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
margin-right: 6px;
|
||||
vertical-align: top;
|
||||
font-family: var(--vscode-font-family, system-ui, sans-serif), "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--group);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 编辑:透明输入框垫在底下接收输入和选区,着色层盖在上面只负责显示,高度由着色层撑开 */
|
||||
.cr-editor {
|
||||
position: relative;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Click, Highlight } from './occurrences'
|
||||
import type { BlockView, Occurrence, PreviewText, PropertyItem, Segment, WebviewToExtension } from './types'
|
||||
import type { BlockView, Inline, Occurrence, PreviewText, PropertyItem, Segment, WebviewToExtension } from './types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from '../../../src/codeReader/blockView'
|
||||
import { createPythonAdapter } from '../../../src/codeReader/languages/python'
|
||||
@@ -205,12 +205,17 @@ describe('occurrences:高亮的状态', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/** 框拆开,框里的词接在前后的词中间:SegmentText 同样一段段画框里的词。 */
|
||||
function inlineSegments(pieces: Inline[]): Segment[] {
|
||||
return pieces.flatMap(piece => 'role' in piece ? [piece] : inlineSegments(piece.segments))
|
||||
}
|
||||
|
||||
function textSegments(text: PreviewText): Segment[] {
|
||||
return 'markdown' in text ? [] : text
|
||||
return 'markdown' in text ? [] : inlineSegments(text)
|
||||
}
|
||||
|
||||
function itemSegments(items: PropertyItem[] | undefined): Segment[] {
|
||||
return (items ?? []).flatMap(item => [...item.cells.flat(), ...itemSegments(item.items)])
|
||||
return (items ?? []).flatMap(item => [...inlineSegments(item.cells.flat()), ...itemSegments(item.items)])
|
||||
}
|
||||
|
||||
/** 预览里画出来的所有段,按页面上从上到下的顺序:每块的属性表或译文,再是它的子块。 */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LanguageAdapter, Translator } from '../../../src/codeReader/contract'
|
||||
import type { RevealPlan } from './reveal'
|
||||
import type { BlockKind, BlockView, PropertyItem, PropertyRow, Segment } from './types'
|
||||
import type { BlockKind, BlockView, Inline, PropertyItem, PropertyRow, Segment } from './types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toBlockViews } from '../../../src/codeReader/blockView'
|
||||
import { createPythonAdapter } from '../../../src/codeReader/languages/python'
|
||||
@@ -19,7 +19,7 @@ function word(text: string): Segment {
|
||||
return { text, role: 'keyword' }
|
||||
}
|
||||
|
||||
function textBlock(id: string, kind: BlockKind, startLine: number, translation: Segment[], children: BlockView[] = []): BlockView {
|
||||
function textBlock(id: string, kind: BlockKind, startLine: number, translation: Inline[], children: BlockView[] = []): BlockView {
|
||||
return { id, kind, startLine, endLine: startLine, code: '', codeTokens: [], attributes: { kind }, children, translation }
|
||||
}
|
||||
|
||||
@@ -56,6 +56,15 @@ describe('跳到定义:滚到哪块、展开哪些', () => {
|
||||
expect(revealPlan(BLOCKS, 4, 102)).toEqual({ id: '1.0.0', expand: ['1', '1.0'] })
|
||||
})
|
||||
|
||||
it('名字在框里(套了几层都算)也是显示着它', () => {
|
||||
const inner = { style: 'iteration' as const, label: '列表', segments: [word('遍历'), name('row', 30), word('取'), name('y', 40)] }
|
||||
const blocks: BlockView[] = [
|
||||
textBlock('0', 'return', 0, [word('返回'), { style: 'iteration', label: '列表', segments: [word('遍历'), name('grid', 20), word('取'), inner] }]),
|
||||
]
|
||||
expect(revealPlan(blocks, 5, 40)).toEqual({ id: '0', expand: [] })
|
||||
expect(revealPlan(blocks, 5, 20)).toEqual({ id: '0', expand: [] })
|
||||
})
|
||||
|
||||
it('注释块、函数「说明」的 Markdown 里的名字不带下标,不算显示着定义', () => {
|
||||
const blocks: BlockView[] = [
|
||||
{ id: '0', kind: 'comment', startLine: 0, endLine: 0, code: '', codeTokens: [], attributes: { kind: 'comment' }, children: [], translation: { markdown: '见 `get_db`' } },
|
||||
@@ -235,6 +244,11 @@ describe('跳到定义:TS / Python 源码里闪哪一处', () => {
|
||||
expect(await at('i in range')).toEqual({ plan: { id: '1.1', expand: ['1'] }, flashed: 'for i in range(3):' })
|
||||
})
|
||||
|
||||
it('python:推导式的变量只在框里显示:闪框所在的那块', async () => {
|
||||
const source = ['def ids(users):', ' return [u.id for u in users]'].join('\n')
|
||||
expect(await jump(py, 'python', pythonZhTranslator, source, 'u in users')).toEqual({ plan: { id: '0.0', expand: ['0'] }, flashed: 'return [u.id for u in users]' })
|
||||
})
|
||||
|
||||
it('python:翻译插件没标 defines 时闪整块,名字在子行里照样展开那一行', async () => {
|
||||
const at = (needle: string): ReturnType<typeof jump> => jump(py, 'python', withoutDefines(pythonZhTranslator), PY, needle)
|
||||
expect(await at('phone: str')).toEqual({ plan: { id: '1', expand: ['1/p3'] }, flashed: 'def mask_phone(phone: str) -> str:' })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BlockView, PreviewText, PropertyItem, SourceRange } from './types'
|
||||
import { showsOffset } from './segments'
|
||||
import { propertyItemId, propertyRowId } from './viewState'
|
||||
|
||||
/** 跳到定义时要滚到的块,就是这处定义的属性行 / 子行(只闪它),以及要展开的块、属性行和子行:它们折叠着,目标就不在页面上。 */
|
||||
@@ -30,9 +31,9 @@ function covers(range: SourceRange | undefined, offset: number): boolean {
|
||||
return range !== undefined && range.startIndex <= offset && offset < range.endIndex
|
||||
}
|
||||
|
||||
/** 文字里显示着 offset 处的名字;Markdown 说明里的名字不带下标。 */
|
||||
/** 文字里(连框里)显示着 offset 处的名字;Markdown 说明里的名字不带下标。 */
|
||||
function shows(text: PreviewText, offset: number): boolean {
|
||||
return !('markdown' in text) && text.some(segment => segment.offset === offset)
|
||||
return !('markdown' in text) && showsOffset(text, offset)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Segment } from './types'
|
||||
import type { Inline, Segment } from './types'
|
||||
|
||||
/** 取 segments 拼起来的文字里 [from, to) 这一段,每个字保留原来的词性。 */
|
||||
export function sliceSegments(segments: Segment[], from: number, to: number): Segment[] {
|
||||
@@ -37,3 +37,13 @@ export function recolor(tokens: Segment[], code: string, text: string): Segment[
|
||||
...sliceSegments(tokens, code.length - tail, code.length),
|
||||
]
|
||||
}
|
||||
|
||||
/** 着色的词拼成的文字(给折叠按钮的说明用);框写成框头加框里的文字。 */
|
||||
export function inlineText(pieces: Inline[]): string {
|
||||
return pieces.map(piece => 'role' in piece ? piece.text : `${piece.label} ${inlineText(piece.segments)}`).join('')
|
||||
}
|
||||
|
||||
/** 文字里有没有 offset 处的那个名字,框里的也算。 */
|
||||
export function showsOffset(pieces: Inline[], offset: number): boolean {
|
||||
return pieces.some(piece => 'role' in piece ? piece.offset === offset : showsOffset(piece.segments, offset))
|
||||
}
|
||||
@@ -17,11 +17,24 @@ export type TokenRole
|
||||
/** offset:照抄自源码里的名字、字符串时它在源码里的起始下标,⌘ / Ctrl 悬停、点击时拿它查定义;译词、提示词没有 */
|
||||
export interface Segment { text: string, role: TokenRole, offset?: number }
|
||||
|
||||
/** 框的样式,决定框头的图标和框的颜色:'iteration' 是写在表达式里的遍历,图标、颜色同「循环」块 */
|
||||
export type GroupStyle = 'iteration'
|
||||
|
||||
/** 框起来的一组词:画成嵌在前后文字里的一个小框,框头是图标和 label;框里的词照样带源码下标,还能再套框 */
|
||||
export interface SegmentGroup {
|
||||
style: GroupStyle
|
||||
label: string
|
||||
segments: Inline[]
|
||||
}
|
||||
|
||||
/** 译文里的一段:着色的词,或框起来的一组词 */
|
||||
export type Inline = Segment | SegmentGroup
|
||||
|
||||
/** 一段 Markdown 说明文字:注释、JSDoc、docstring 去掉注释符号、引号和共同缩进后的原文,按 Markdown 渲染成注释色的段落、列表、代码块 */
|
||||
export interface Markdown { markdown: string }
|
||||
|
||||
/** 预览里显示的一段文字:着色的词(一行译文、属性值),或整段 Markdown 说明(注释块、函数和类的说明) */
|
||||
export type PreviewText = Segment[] | Markdown
|
||||
export type PreviewText = Inline[] | Markdown
|
||||
|
||||
/** 源码里一段的下标,end 为开区间 */
|
||||
export interface SourceRange { startIndex: number, endIndex: number }
|
||||
@@ -34,7 +47,7 @@ export interface Defines { defines?: SourceRange }
|
||||
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值 */
|
||||
export interface PropertyItem extends Defines {
|
||||
cells: Segment[][]
|
||||
cells: Inline[][]
|
||||
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项),一层层可以一直往下套 */
|
||||
items?: PropertyItem[]
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user