✨ feat(vscode): React 文件照源码一块一张卡片,组件卡片只留名称、说明、构造参数
- 组件体的语句不再并进组件卡片,照源码顺序是它的子块;状态、引用、数据源、派生值、副作用、事件处理函数、渲染输出各成一张卡片,回调和函数里的语句是这些卡片下面的子卡片 - 普通变量、提前返回的 if、注释照语言默认的卡片;表格格子里只写名字和位置,去掉「改它的」「来自」格子里单独打开源码的那条路 - 用词改成书面叫法(更新位置、使用位置、计算表达式、依赖项、触发时机、清理函数、绑定事件、样式取决于……),新增 toggle-right、anchor、download、sigma、layout-template 五个种类图标 - 夹具加上 luffy 的 QueuedBubble.tsx、PlanChecklist.tsx,规格的 React 一节照新样子改写
This commit is contained in:
1 parent
194544b4ba
commit
6ce4ef7ee2
20 files changed
+1327
-989
No files matched your search
@@ -11,11 +11,10 @@ import { BlockLevel } from './BlockTree'
|
||||
import { DiffBar, NewFileBanner } from './DiffBar'
|
||||
import { changeStops, diffTree, foldUnchanged, openedByDiff } from './diffView'
|
||||
import { acceptNames, DisplayNameContext, documentNames } from './displayNames'
|
||||
import { coveredByEditing, snippetRefs, startEditing } from './editing'
|
||||
import { coveredByEditing, startEditing } from './editing'
|
||||
import { applyFilter, excludedBy, keptWhileEditing } from './filter'
|
||||
import { FilterBar } from './FilterBar'
|
||||
import { OverviewRuler } from './OverviewRuler'
|
||||
import { SnippetEditContext } from './PropertyGrid'
|
||||
import { asksOf, historyOf, PATH_ASK_ANCHOR } from './qa'
|
||||
import { useChangeNav } from './useChangeNav'
|
||||
import { useDefinitionLinks } from './useDefinitionLinks'
|
||||
@@ -68,7 +67,6 @@ export function App(): ReactElement | null {
|
||||
const collapsed = useMemo(() => collapsedIds(doc?.blocks ?? [], view.folds, openedByDiff(changes)), [doc, view.folds, changes])
|
||||
const filter = useMemo(() => applyFilter(doc?.blocks ?? [], excluded, keep), [doc, excluded, keep])
|
||||
const covered = useMemo(() => coveredByEditing(doc?.blocks ?? [], new Set(view.editing)), [doc, view.editing])
|
||||
const snippetIds = useMemo(() => new Map(snippetRefs(doc?.blocks ?? []).map(ref => [ref.edit, ref.id])), [doc])
|
||||
/** 「只看改动」开着(整个工作区共用,扩展端记着);本地不改,改了发给扩展端、等它发回来 */
|
||||
const [onlyChanges, setOnlyChanges] = useState(false)
|
||||
/** 「只看改动」时点开过的几段没变的块(按第一块的 id),筛选一变就重新折 */
|
||||
@@ -178,7 +176,6 @@ export function App(): ReactElement | null {
|
||||
return (
|
||||
<HighlightContext value={highlight}>
|
||||
<DisplayNameContext value={names?.names ?? NO_NAMES}>
|
||||
<SnippetEditContext value={{ ids: snippetIds, editing: tree.editing, onToggle: tree.onToggleEdit, version: doc.version, focusId, onFocused }}>
|
||||
<FilterBar
|
||||
attributes={doc.attributes}
|
||||
excluded={excluded}
|
||||
@@ -239,7 +236,6 @@ export function App(): ReactElement | null {
|
||||
? <BlockLevel blocks={doc.blocks} tree={tree} />
|
||||
: <div className="text-[var(--vscode-descriptionForeground)]">所有块都被筛选隐藏了。</div>}
|
||||
</div>
|
||||
</SnippetEditContext>
|
||||
</DisplayNameContext>
|
||||
</HighlightContext>
|
||||
)
|
||||
|
||||
@@ -270,7 +270,7 @@ function BlockCard({ block, tree }: { block: BlockView, tree: BlockTreeState }):
|
||||
{editing
|
||||
? (
|
||||
<CodeEditor
|
||||
source={block}
|
||||
block={block}
|
||||
version={tree.version}
|
||||
autoFocus={tree.focusId === block.id}
|
||||
onAutoFocused={tree.onFocused}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ChangeEvent, KeyboardEvent, ReactElement } from 'react'
|
||||
import type { Segment } from './types'
|
||||
import type { BlockView } from './types'
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { recolor } from './segments'
|
||||
import { SegmentText } from './SegmentText'
|
||||
@@ -7,17 +7,9 @@ import { postMessage } from './vscode'
|
||||
|
||||
const EDIT_DEBOUNCE_MS = 200
|
||||
|
||||
/** 编辑框要的那一段源码:整块,或格子里单独打开的一段,字段一样。 */
|
||||
export interface EditableSource {
|
||||
startLine: number
|
||||
endLine: number
|
||||
code: string
|
||||
codeTokens: Segment[]
|
||||
}
|
||||
|
||||
interface CodeEditorProps {
|
||||
source: EditableSource
|
||||
/** 文档版本:每来一份新文档都要对一次输入框内容,哪怕这一段的代码没变 */
|
||||
block: BlockView
|
||||
/** 文档版本:每来一份新文档都要对一次输入框内容,哪怕这一块的代码没变 */
|
||||
version: number
|
||||
autoFocus: boolean
|
||||
onAutoFocused: () => void
|
||||
@@ -25,11 +17,11 @@ interface CodeEditorProps {
|
||||
}
|
||||
|
||||
/** 一块源码的编辑状态:着色层显示,透明输入框接收输入,改动防抖后整行替换回文件。 */
|
||||
export function CodeEditor({ source, version, autoFocus, onAutoFocused, onExit }: CodeEditorProps): ReactElement {
|
||||
const [text, setText] = useState(source.code)
|
||||
export function CodeEditor({ block, version, autoFocus, onAutoFocused, onExit }: CodeEditorProps): ReactElement {
|
||||
const [text, setText] = useState(block.code)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
const sourceRef = useRef(source)
|
||||
sourceRef.current = source
|
||||
const blockRef = useRef(block)
|
||||
blockRef.current = block
|
||||
const textRef = useRef(text)
|
||||
textRef.current = text
|
||||
/** 本地改过、还没在新文档里看到的文字;有它时新文档不能覆盖有焦点的输入框 */
|
||||
@@ -45,7 +37,7 @@ export function CodeEditor({ source, version, autoFocus, onAutoFocused, onExit }
|
||||
if (!unsent.current)
|
||||
return
|
||||
unsent.current = false
|
||||
const { startLine, endLine } = sourceRef.current
|
||||
const { startLine, endLine } = blockRef.current
|
||||
const value = textRef.current
|
||||
postMessage({
|
||||
type: 'edit',
|
||||
@@ -59,16 +51,16 @@ export function CodeEditor({ source, version, autoFocus, onAutoFocused, onExit }
|
||||
useEffect(() => {
|
||||
const el = textareaRef.current
|
||||
const focused = el !== null && document.activeElement === el
|
||||
if (focused && unconfirmed.current !== null && unconfirmed.current !== source.code)
|
||||
if (focused && unconfirmed.current !== null && unconfirmed.current !== block.code)
|
||||
return
|
||||
unconfirmed.current = null
|
||||
sentLineCount.current = null
|
||||
if (source.code === textRef.current)
|
||||
if (block.code === textRef.current)
|
||||
return
|
||||
if (focused)
|
||||
restoreSelection.current = [el.selectionStart, el.selectionEnd]
|
||||
setText(source.code)
|
||||
}, [version, source.code])
|
||||
setText(block.code)
|
||||
}, [version, block.code])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = textareaRef.current
|
||||
@@ -129,7 +121,7 @@ export function CodeEditor({ source, version, autoFocus, onAutoFocused, onExit }
|
||||
onBlur={flush}
|
||||
/>
|
||||
<pre className="cr-code" aria-hidden="true">
|
||||
<SegmentText segments={recolor(source.codeTokens, source.code, text)} />
|
||||
<SegmentText segments={recolor(block.codeTokens, block.code, text)} />
|
||||
{text === '' || text.endsWith('\n') ? ' ' : ''}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
||||
import type { BlockView, CellEdit, Inline, InlinePart, ItemDiff, ItemsDiff, KindBadge, PropertyItem, PropertyRow, Removed, RowsDiff } from './types'
|
||||
import { ChevronDown, ChevronRight, Eye, Pencil } from 'lucide-react'
|
||||
import { createContext, Fragment, useContext } from 'react'
|
||||
import { CodeEditor } from './CodeEditor'
|
||||
import type { BlockView, Inline, InlinePart, ItemDiff, ItemsDiff, KindBadge, PropertyItem, PropertyRow, Removed, RowsDiff } from './types'
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react'
|
||||
import { Fragment } from 'react'
|
||||
import { DiffInline, DiffText } from './DiffText'
|
||||
import { inlineText } from './segments'
|
||||
import { SegmentText } from './SegmentText'
|
||||
@@ -57,83 +56,6 @@ function CellText({ cell, parts }: { cell: Inline[], parts: InlinePart[] | undef
|
||||
return parts ? <DiffInline parts={parts} /> : <SegmentText segments={cell} />
|
||||
}
|
||||
|
||||
/** 格子里单独打开的那段源码:和块共用同一套铅笔、同一个编辑框。 */
|
||||
export interface SnippetEditState {
|
||||
ids: ReadonlyMap<CellEdit, string>
|
||||
editing: ReadonlySet<string>
|
||||
onToggle: (id: string) => void
|
||||
version: number
|
||||
focusId: string | null
|
||||
onFocused: () => void
|
||||
}
|
||||
|
||||
export const SnippetEditContext = createContext<SnippetEditState | null>(null)
|
||||
|
||||
function EditToggle({ editing, onToggle }: { editing: boolean, onToggle: () => void }): ReactElement {
|
||||
const label = editing ? '预览' : '编辑'
|
||||
const Icon = editing ? Eye : Pencil
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="cr-tool inline-flex h-5 w-6 items-center justify-center rounded align-middle"
|
||||
data-active={editing ? '' : undefined}
|
||||
data-tooltip={label}
|
||||
aria-label={label}
|
||||
onClick={onToggle}
|
||||
>
|
||||
<Icon size={14} strokeWidth={1.8} aria-hidden />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function SnippetSpan({ edit, segments }: { edit: CellEdit, segments: Inline[] }): ReactElement {
|
||||
const ctx = useContext(SnippetEditContext)
|
||||
const id = ctx?.ids.get(edit)
|
||||
if (!ctx || id === undefined)
|
||||
return <SegmentText segments={segments} />
|
||||
const editing = ctx.editing.has(id)
|
||||
return (
|
||||
<span className="inline-flex flex-wrap items-baseline gap-0.5">
|
||||
{editing
|
||||
? (
|
||||
<span className="block w-full">
|
||||
<CodeEditor
|
||||
source={edit.snippet}
|
||||
version={ctx.version}
|
||||
autoFocus={ctx.focusId === id}
|
||||
onAutoFocused={ctx.onFocused}
|
||||
onExit={() => ctx.onToggle(id)}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
: <SegmentText segments={segments} />}
|
||||
<EditToggle editing={editing} onToggle={() => ctx.onToggle(id)} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function CellView({ cell, index, edits, parts }: {
|
||||
cell: Inline[]
|
||||
index: number
|
||||
edits: CellEdit[] | undefined
|
||||
parts: InlinePart[] | undefined
|
||||
}): ReactElement {
|
||||
const mine = (edits ?? []).filter(edit => edit.cell === index).sort((a, b) => a.from - b.from)
|
||||
if (mine.length === 0)
|
||||
return <CellText cell={cell} parts={parts} />
|
||||
const chunks: ReactElement[] = []
|
||||
let at = 0
|
||||
mine.forEach((edit, n) => {
|
||||
if (edit.from > at)
|
||||
chunks.push(<SegmentText key={`t${n}`} segments={cell.slice(at, edit.from)} />)
|
||||
chunks.push(<SnippetSpan key={`s${n}`} edit={edit} segments={cell.slice(edit.from, edit.to)} />)
|
||||
at = edit.to
|
||||
})
|
||||
if (at < cell.length)
|
||||
chunks.push(<SegmentText key="tail" segments={cell.slice(at)} />)
|
||||
return <>{chunks}</>
|
||||
}
|
||||
|
||||
/**
|
||||
* 子行的第一列(字典的键):带下一层的子行前面是折叠箭头(图标、大小、颜色同属性行的),点箭头或这一列切换;
|
||||
* 同一层里不带下一层的子行在同样的位置留空,第一列才对得齐。
|
||||
@@ -193,9 +115,9 @@ function PropertyItems({ parentId, items, folding, marks }: { parentId: string,
|
||||
{item.cells.map((cell, i) => i === 0 && nested
|
||||
? <ItemHead key={i} id={id} cell={cell} foldable={children.length > 0 && change !== 'del'} folding={folds} />
|
||||
: (
|
||||
<div key={i} className="cr-code min-w-0">
|
||||
<CellView cell={cell} index={i} edits={item.edits} parts={mark?.cells?.[i]} />
|
||||
</div>
|
||||
<p key={i} className="cr-code min-w-0">
|
||||
<CellText cell={cell} parts={mark?.cells?.[i]} />
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
{children.length > 0 && !folds.collapsed.has(id) && (change === 'del'
|
||||
@@ -234,7 +156,7 @@ function PropertyItems({ parentId, items, folding, marks }: { parentId: string,
|
||||
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。一行译文下面摊开实参的表也用它;
|
||||
* 行带 table(实参的值里跨几行写的调用)时,那张表排在最后一格里、文字下面,往右缩一点,一层层往下套。
|
||||
* 标差异时(marks)有加行、删行的表每行第一格前多一个记号位(+ / −,没变的空着),加的行淡绿底,删的行放回原来的位置、淡红底划掉。
|
||||
* 讲某个子块的行(toggles,React 组件的事件处理表)第一格是按钮,点它在表下面展开那一块,展开时这一行淡淡垫上那一块的种类色。
|
||||
* 讲某个子块的行(toggles,Rust 实现块的方法表)第一格是按钮,点它在表下面展开那一块,展开时这一行淡淡垫上那一块的种类色。
|
||||
*/
|
||||
export function PropertyTable({ parentId, columns, items, marks, toggles }: {
|
||||
parentId: string
|
||||
@@ -255,10 +177,10 @@ export function PropertyTable({ parentId, columns, items, marks, toggles }: {
|
||||
{toggle && i === 0
|
||||
? (
|
||||
<button type="button" className="cr-item-toggle" aria-expanded={toggle.open} aria-label={`${toggle.open ? '收起' : '展开'}${inlineText(cell)}`} onClick={toggle.onToggle}>
|
||||
<CellView cell={cell} index={i} edits={item.edits} parts={mark?.cells?.[i]} />
|
||||
<CellText cell={cell} parts={mark?.cells?.[i]} />
|
||||
</button>
|
||||
)
|
||||
: <CellView cell={cell} index={i} edits={item.edits} parts={mark?.cells?.[i]} />}
|
||||
: <CellText cell={cell} parts={mark?.cells?.[i]} />}
|
||||
</>
|
||||
)
|
||||
const tint = toggle?.open && toggle.badge
|
||||
@@ -269,11 +191,11 @@ export function PropertyTable({ parentId, columns, items, marks, toggles }: {
|
||||
{cells.map((cell, i) => item.table && i === cells.length - 1
|
||||
? (
|
||||
<div key={i} className="cr-table-cell flex min-w-0 flex-col gap-[3px]" role="cell">
|
||||
<div className="cr-code">{text(cell, i)}</div>
|
||||
<p className="cr-code">{text(cell, i)}</p>
|
||||
<PropertyTable parentId={tableId(id)} columns={item.table.columns} items={item.table.items} marks={mark?.table} />
|
||||
</div>
|
||||
)
|
||||
: <div key={i} className="cr-table-cell cr-code min-w-0" role="cell">{text(cell, i)}</div>)}
|
||||
: <p key={i} className="cr-table-cell cr-code min-w-0" role="cell">{text(cell, i)}</p>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,14 +43,13 @@ export function SegmentText({ segments }: { segments: Inline[] }): ReactElement
|
||||
}
|
||||
|
||||
/**
|
||||
* 框的样式 → 框头图标:遍历同「循环」块、按条件同「条件」块、不渲染同「返回」块的图标;条件的一支、小标签、类名不带图标。
|
||||
* 框的样式 → 框头图标:遍历同「循环」块、按条件同「条件」块的图标;条件的一支、小标签、类名不带图标。
|
||||
* 颜色在 codeReader.css 的 .cr-group[data-style] 里,框自己给了颜色(color)时用它。
|
||||
*/
|
||||
const GROUP_ICONS: Record<GroupStyle, LucideIcon | undefined> = {
|
||||
iteration: KIND_ICON_COMPONENTS.repeat,
|
||||
condition: KIND_ICON_COMPONENTS.split,
|
||||
branch: undefined,
|
||||
skip: KIND_ICON_COMPONENTS['corner-down-left'],
|
||||
tag: undefined,
|
||||
classes: undefined,
|
||||
}
|
||||
|
||||
@@ -412,11 +412,10 @@ body:is(.vscode-light, .vscode-high-contrast-light) .cr-colored { color: var(--c
|
||||
}
|
||||
|
||||
/*
|
||||
* 渲染大纲一行前面的小框(React 组件卡片):按条件、条件的一支同「条件」块的颜色,不渲染同「返回」块;
|
||||
* 渲染大纲一行前面的小框(React 渲染输出卡片):按条件、条件的一支同「条件」块的颜色;
|
||||
* 小标签(名称后面的「具名导出」、属性名后面的「回调」)用规则给的颜色(暗色、亮色各一个,按主题挑)
|
||||
*/
|
||||
.cr-group:is([data-style="condition"], [data-style="branch"]) { --group: var(--cr-kind-branch); }
|
||||
.cr-group[data-style="skip"] { --group: var(--cr-kind-return); }
|
||||
.cr-group[data-colored] { --group: var(--group-dark); }
|
||||
body:is(.vscode-light, .vscode-high-contrast-light) .cr-group[data-colored] { --group: var(--group-light); }
|
||||
.cr-group[data-style="tag"] {
|
||||
@@ -495,7 +494,7 @@ body:is(.vscode-light, .vscode-high-contrast-light) .cr-group[data-colored] { --
|
||||
.cr-table-row {
|
||||
border-top: 1px solid var(--cr-table-border);
|
||||
}
|
||||
/* 讲某个子块的行(React 组件的事件处理表):第一格是按钮,悬停划下划线;展开时这一行淡淡垫上那一块的种类色 */
|
||||
/* 讲某个子块的行(Rust 实现块的方法表):第一格是按钮,悬停划下划线;展开时这一行淡淡垫上那一块的种类色 */
|
||||
.cr-table-row[data-open] {
|
||||
background: color-mix(in srgb, var(--kind) 10%, transparent);
|
||||
}
|
||||
|
||||
@@ -63,24 +63,4 @@ describe('同一行不会同时在两处能改', () => {
|
||||
const plain = revealPlan(BLOCKS, 15, 999)
|
||||
expect(plain && uncoveredPlan(plain, covered)).toEqual(plain)
|
||||
})
|
||||
|
||||
it('格子里单独打开的一段,和盖住它的块、跟它重叠的另一段不能同时编辑', () => {
|
||||
const snippet = (startLine: number, endLine: number) => ({ startLine, endLine, code: 'x', codeTokens: [] })
|
||||
const card = block('1', 'react.component', [0, 20])
|
||||
card.properties = [{
|
||||
label: '状态',
|
||||
value: [],
|
||||
items: [{
|
||||
cells: [[{ text: 'a', role: 'plain' }], [{ text: 'b', role: 'plain' }]],
|
||||
edits: [
|
||||
{ cell: 0, from: 0, to: 1, snippet: snippet(4, 6) },
|
||||
{ cell: 1, from: 0, to: 1, snippet: snippet(6, 7) },
|
||||
],
|
||||
}],
|
||||
}]
|
||||
const blocks = [card, block('2', 'function', [30, 32])]
|
||||
expect(startEditing(blocks, ['1~0', '2'], '1')).toEqual(['2', '1'])
|
||||
expect(startEditing(blocks, ['1', '2'], '1~0')).toEqual(['2', '1~0'])
|
||||
expect(startEditing(blocks, ['1~0', '2'], '1~1')).toEqual(['2', '1~1'])
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { BlockView, CellEdit, PropertyItem } from './types'
|
||||
import type { BlockView } from './types'
|
||||
|
||||
/**
|
||||
* 同一时刻能编辑的几段源码不重叠,同一行不会同时在两处能改:
|
||||
* ① 块的编辑范围是它的完整源码,子孙都在它的编辑框里:块编辑时它的子孙都不画;
|
||||
* ② 点「编辑」时,编辑范围和这块重叠的别的块(祖先、子孙,`} else {` 这样共用一行的兄弟块)退出编辑;
|
||||
* ③ 属性表格子里单独打开的那一段(见 CellEdit)行号落在组件块里面,和块同一条规矩:重叠就不能同时编辑。
|
||||
* 组件正在编辑时预览不画,里面这段的编辑器也不画。
|
||||
* ② 点「编辑」时,编辑范围和这块重叠的别的块(祖先、子孙,`} else {` 这样共用一行的兄弟块)退出编辑。
|
||||
*/
|
||||
|
||||
type Lines = Pick<BlockView, 'startLine' | 'endLine'>
|
||||
@@ -43,51 +41,12 @@ function* allBlocks(blocks: readonly BlockView[]): Generator<BlockView> {
|
||||
}
|
||||
|
||||
/** 点了 id 这块的「编辑」之后正在编辑的块:加上它,编辑范围和它重叠的别的块退出编辑。 */
|
||||
export interface SnippetRef {
|
||||
id: string
|
||||
edit: CellEdit
|
||||
startLine: number
|
||||
endLine: number
|
||||
}
|
||||
|
||||
/** 各块属性表里能单独打开的源码,按画出来的顺序编号。id 是「块 id~序号」。 */
|
||||
export function snippetRefs(blocks: readonly BlockView[]): SnippetRef[] {
|
||||
const refs: SnippetRef[] = []
|
||||
const take = (blockId: string, items: PropertyItem[] | undefined, n: number): number => {
|
||||
for (const item of items ?? []) {
|
||||
for (const edit of item.edits ?? []) {
|
||||
refs.push({ id: `${blockId}~${n}`, edit, startLine: edit.snippet.startLine, endLine: edit.snippet.endLine })
|
||||
n += 1
|
||||
}
|
||||
n = take(blockId, item.items, n)
|
||||
n = take(blockId, item.table?.items, n)
|
||||
}
|
||||
return n
|
||||
}
|
||||
for (const block of allBlocks(blocks)) {
|
||||
let n = 0
|
||||
if ('properties' in block) {
|
||||
for (const row of block.properties)
|
||||
n = take(block.id, row.items, n)
|
||||
}
|
||||
else if (block.table) {
|
||||
take(block.id, block.table.items, 0)
|
||||
}
|
||||
}
|
||||
return refs
|
||||
}
|
||||
|
||||
/** 点了 id 这段的「编辑」之后正在编辑的几段:加上它,行重叠的别的段(块,或格子里的一段源码)退出编辑。 */
|
||||
export function startEditing(blocks: readonly BlockView[], editing: readonly string[], id: string): string[] {
|
||||
const byId = new Map<string, Lines>()
|
||||
for (const block of allBlocks(blocks))
|
||||
byId.set(block.id, block)
|
||||
for (const ref of snippetRefs(blocks))
|
||||
byId.set(ref.id, ref)
|
||||
const byId = new Map([...allBlocks(blocks)].map(block => [block.id, block]))
|
||||
const target = byId.get(id)
|
||||
const others = editing.filter((other) => {
|
||||
const span = byId.get(other)
|
||||
return other !== id && !(target && span && overlaps(target, span))
|
||||
const block = byId.get(other)
|
||||
return other !== id && !(target && block && overlaps(target, block))
|
||||
})
|
||||
return [...others, id]
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { CSSProperties, ReactElement } from 'react'
|
||||
import type { BlockConcept, KindIconName } from './types'
|
||||
import {
|
||||
Activity,
|
||||
Anchor,
|
||||
Atom,
|
||||
Box,
|
||||
Braces,
|
||||
@@ -12,9 +13,11 @@ import {
|
||||
CornerDownLeft,
|
||||
CornerDownRight,
|
||||
Database,
|
||||
Download,
|
||||
Globe,
|
||||
Import,
|
||||
Layers,
|
||||
LayoutTemplate,
|
||||
ListOrdered,
|
||||
Lock,
|
||||
MessageSquareText,
|
||||
@@ -27,6 +30,7 @@ import {
|
||||
Shield,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
Sigma,
|
||||
Signpost,
|
||||
SlidersHorizontal,
|
||||
Split,
|
||||
@@ -34,6 +38,7 @@ import {
|
||||
SquareTerminal,
|
||||
Tag,
|
||||
Terminal,
|
||||
ToggleRight,
|
||||
TriangleAlert,
|
||||
Variable,
|
||||
Zap,
|
||||
@@ -76,6 +81,11 @@ export const KIND_ICON_COMPONENTS: Record<KindIconName, LucideIcon> = {
|
||||
'atom': Atom,
|
||||
'zap': Zap,
|
||||
'mouse-pointer-click': MousePointerClick,
|
||||
'toggle-right': ToggleRight,
|
||||
'anchor': Anchor,
|
||||
'download': Download,
|
||||
'sigma': Sigma,
|
||||
'layout-template': LayoutTemplate,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,10 +31,10 @@ export interface RuleReference { rule: string, key: string }
|
||||
|
||||
/**
|
||||
* 框的样式,决定框头的图标和框的颜色:iteration 写在表达式里的遍历(同「循环」块),condition 按条件(同「条件」块),
|
||||
* branch 条件的一支(同「条件」块的颜色、不带图标),skip 这个条件下不渲染(同「返回」块),tag 小标签(颜色是 SegmentGroup.color),
|
||||
* branch 条件的一支(同「条件」块的颜色、不带图标),tag 小标签(颜色是 SegmentGroup.color),
|
||||
* classes 收起来的一串类名(中性色,点开、悬停看 detail)
|
||||
*/
|
||||
export type GroupStyle = 'iteration' | 'condition' | 'branch' | 'skip' | 'tag' | 'classes'
|
||||
export type GroupStyle = 'iteration' | 'condition' | 'branch' | 'tag' | 'classes'
|
||||
|
||||
/**
|
||||
* 框起来的一组词:画成嵌在前后文字里的一个小框,框头是图标和 label(只有图标时 label 是空的);框里的词照样带源码下标,还能再套框。
|
||||
@@ -66,30 +66,11 @@ export interface SourceRange { startIndex: number, endIndex: number }
|
||||
*/
|
||||
export interface Defines { defines?: SourceRange }
|
||||
|
||||
/** 能单独打开编辑的一段源码:整行切好、着色好,和块的 code / codeTokens 同一套。网页不自己切 */
|
||||
export interface SourceSnippet {
|
||||
/** 起止行,0 起,含头含尾 */
|
||||
startLine: number
|
||||
endLine: number
|
||||
code: string
|
||||
codeTokens: Segment[]
|
||||
}
|
||||
|
||||
/** 属性表一个子行的某一格里,能单独打开的一段源码。cell 是第几格,from / to 是这一格文字里这一处(含头不含尾) */
|
||||
export interface CellEdit {
|
||||
cell: number
|
||||
from: number
|
||||
to: number
|
||||
snippet: SourceSnippet
|
||||
}
|
||||
|
||||
/** 属性表里展开的一个子行:几列对齐的格子,例如参数的名称 / 类型 / 默认值、字典一项的键 / 值 */
|
||||
export interface PropertyItem extends Defines {
|
||||
cells: Inline[][]
|
||||
/** 展开在这个子行下面的下一层子行(字典 / 对象的值又是字典 / 对象时,它的各项),一层层可以一直往下套 */
|
||||
items?: PropertyItem[]
|
||||
/** 这一行里能单独打开的几段源码。打开后只出现那一段。没有能打开的就不写 */
|
||||
edits?: CellEdit[]
|
||||
/** 排在这一行最后一格里、那格文字下面的表(实参的值里跨几行写的调用的实参表),表里的行又可以带 table;总是展开 */
|
||||
table?: PreviewTable
|
||||
}
|
||||
@@ -120,6 +101,7 @@ export type KindIconName
|
||||
| 'split' | 'corner-down-right' | 'repeat' | 'shield' | 'shield-alert' | 'shield-check' | 'corner-down-left' | 'code'
|
||||
| 'message-square-text' | 'terminal' | 'sliders-horizontal' | 'signpost' | 'square-terminal' | 'layers' | 'copy' | 'activity' | 'play'
|
||||
| 'database' | 'package' | 'server-cog' | 'calendar-clock' | 'globe' | 'network' | 'atom' | 'zap' | 'mouse-pointer-click'
|
||||
| 'toggle-right' | 'anchor' | 'download' | 'sigma' | 'layout-template'
|
||||
|
||||
/**
|
||||
* 一块上认出的一个概念。宿主放进预览数据:稳定 id、悬停名、图标名。
|
||||
@@ -167,7 +149,7 @@ interface BlockViewBase {
|
||||
|
||||
/**
|
||||
* 子块画在父卡片的哪儿:属性表第 row 行下面(折叠这一行时跟着收起);item 是讲它的那个子行,
|
||||
* 这时它默认收着(它的块 id 记成折叠着),点那个子行的第一格才展开、一次只展开一个(React 组件的事件处理表点名称)
|
||||
* 这时它默认收着(它的块 id 记成折叠着),点那个子行的第一格才展开、一次只展开一个(Rust 实现块的方法表点方法名)
|
||||
*/
|
||||
export interface BlockSlot {
|
||||
row: number
|
||||
|
||||
Reference in new issue
Block a user