+ {removed(-1)}
{items.map((item, index) => {
- const id = propertyItemId(parentId, index)
- const children = item.items ?? []
+ const mark = marks?.changed[index]
return (
-
- {item.cells.map((cell, i) => i === 0 && nested
- ?
0} folding={folding} />
- : (
-
-
-
- ))}
-
- {children.length > 0 && !folding.collapsed.has(id) && }
+ {itemRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)}
+ {removed(index)}
)
})}
@@ -98,34 +136,49 @@ function PropertyItems({ parentId, items, folding }: { parentId: string, items:
* 给了列名的子行(输入参数、数据表的字段)排成表格:第一行是表头(列名),下面每个子行一行;外框圆角,格子之间、行之间一条分隔线。
* 列宽按整列的内容定(见 tableTemplate),最后一列分剩下的,长了在格子里换行。一行译文下面摊开实参的表也用它;
* 行带 table(实参的值里跨几行写的调用)时,那张表排在最后一格里、文字下面,往右缩一点,一层层往下套。
+ * 标差异时(marks)有加行、删行的表每行第一格前多一个记号位(+ / −,没变的空着),加的行淡绿底,删的行放回原来的位置、淡红底划掉。
*/
-export function PropertyTable({ parentId, columns, items }: { parentId: string, columns: string[], items: PropertyItem[] }): ReactElement {
+export function PropertyTable({ parentId, columns, items, marks }: { parentId: string, columns: string[], items: PropertyItem[], marks?: ItemsDiff }): ReactElement {
+ // 有加行、删行的表每行第一格前留一个记号位
+ const signed = marks !== undefined && (marks.removed.length > 0 || Object.values(marks.changed).some(mark => mark.added))
+ /** 一行:删掉的(change 是 del)不带 id,套着的表不再标 */
+ const tableRow = (item: PropertyItem, id: string, change: 'add' | 'del' | undefined, mark: ItemDiff | undefined): ReactElement => {
+ const cells = tableCells(item, columns)
+ const text = (cell: Inline[], i: number): ReactElement => (
+ <>
+ {signed && i === 0 &&
{SIGNS[change ?? 'none']}}
+
+ >
+ )
+ return (
+
+ {cells.map((cell, i) => item.table && i === cells.length - 1
+ ? (
+
+ )
+ :
{text(cell, i)}
)}
+
+ )
+ }
+ const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
+ .map(({ value, index }) =>
{tableRow(value, `${parentId}/x${index}`, 'del', undefined)})
return (
{columns.map((name, i) => {name})}
+ {removed(-1)}
{items.map((item, index) => {
- const id = propertyItemId(parentId, index)
- const cells = tableCells(item, columns)
+ const mark = marks?.changed[index]
return (
-
- {cells.map((cell, i) => item.table && i === cells.length - 1
- ? (
-
- )
- : (
-
-
-
- ))}
-
+
+ {tableRow(item, propertyItemId(parentId, index), mark?.added && 'add', mark)}
+ {removed(index)}
+
)
})}
@@ -136,23 +189,30 @@ export function PropertyTable({ parentId, columns, items }: { parentId: string,
/**
* 函数、类、枚举、导入、常量、变量块和库规则的卡片的预览:属性名一列对齐,值按词性着色;输入参数、导入项、字典 / 对象的值下面展开子行,
* 给了列名的(输入参数、数据表的字段)排成表格,没给的排成列表。
+ * 标差异时(marks)新加的行淡绿底、左边 +,删掉的行放回原来的位置、淡红底划掉、左边 −,值改了的按旧值 → 新值或逐词标。
*/
-export function PropertyGrid({ blockId, rows, collapsed, onToggleFold }: {
+export function PropertyGrid({ blockId, rows, collapsed, onToggleFold, marks }: {
blockId: string
rows: PropertyRow[]
collapsed: ReadonlySet
onToggleFold: (id: string) => void
+ /** 标差异时这张属性表改了哪些行 */
+ marks?: RowsDiff
}): ReactElement {
+ const removed = (after: number): ReactElement[] => removedAt(marks?.removed, after)
+ .map(({ value, index }) => )
return (
+ {removed(-1)}
{rows.map((row, index) => {
const layout = subRowsLayout(row)
const id = propertyRowId(blockId, index)
const folded = collapsed.has(id)
const Chevron = folded ? ChevronRight : ChevronDown
+ const mark = marks?.changed[index]
return (
-
+
{layout
? (
- {!folded && layout === 'table' &&
}
- {!folded && layout === 'list' &&
}
+ {!folded && layout === 'table' &&
}
+ {!folded && layout === 'list' &&
}
+ {removed(index)}
)
})}
)
}
+
+/** 属性表里删掉的一行:整行淡红底、划掉,左边 −;它下面的子行照样摊开、划掉。 */
+function RemovedRow({ id, row }: { id: string, row: PropertyRow }): ReactElement {
+ const layout = subRowsLayout(row)
+ return (
+ <>
+
+ {row.label}
+
+
+ {layout && (
+
+ {layout === 'table'
+ ?
+ :
}
+
+ )}
+ >
+ )
+}
diff --git a/vscode/webview-ui/src/codeReader/codeReader.css b/vscode/webview-ui/src/codeReader/codeReader.css
index 238ff6a..a0d17e9 100644
--- a/vscode/webview-ui/src/codeReader/codeReader.css
+++ b/vscode/webview-ui/src/codeReader/codeReader.css
@@ -931,3 +931,69 @@ body {
.cr-new-file > svg {
color: var(--cr-diff-add);
}
+
+/*
+ * 卡片里的标记:改了的值旧值红底划掉、紧跟新值绿底(.cr-diff-part),说明里增删的字同样(.cr-diff-word, / );
+ * 新加的行、子行整行淡绿底、左边 +,删掉的整行淡红底、划掉、左边 −;表格的行记号写在第一格前(.cr-diff-sign)
+ */
+body {
+ --cr-diff-add-text: var(--vscode-diffEditor-insertedTextBackground, color-mix(in srgb, var(--cr-diff-add) 32%, transparent));
+ --cr-diff-del-text: var(--vscode-diffEditor-removedTextBackground, color-mix(in srgb, var(--cr-diff-del) 32%, transparent));
+}
+.cr-diff-part,
+.cr-diff-word {
+ border-radius: 2px;
+ -webkit-box-decoration-break: clone;
+ box-decoration-break: clone;
+}
+:is(.cr-diff-part, .cr-diff-word)[data-change="add"],
+ins.cr-diff-word {
+ background: var(--cr-diff-add-text);
+ text-decoration: none;
+}
+:is(.cr-diff-part, .cr-diff-word)[data-change="del"],
+del.cr-diff-word {
+ background: var(--cr-diff-del-text);
+ text-decoration-line: line-through;
+}
+.cr-diff-part[data-change="del"] * {
+ text-decoration-line: line-through;
+}
+:is(.cr-prop-row, .cr-prop-item, .cr-table-row)[data-diff="add"] {
+ background: color-mix(in srgb, var(--cr-diff-add) 14%, transparent);
+}
+:is(.cr-prop-row, .cr-prop-item, .cr-table-row)[data-diff="del"] {
+ background: color-mix(in srgb, var(--cr-diff-del) 14%, transparent);
+}
+:is(.cr-prop-row, .cr-prop-item, .cr-table-row)[data-diff="del"] :not(.cr-diff-sign),
+.cr-diff-gone,
+.cr-diff-gone * {
+ text-decoration-line: line-through;
+}
+:is(.cr-prop-row, .cr-prop-item)[data-diff] {
+ position: relative;
+}
+:is(.cr-prop-row, .cr-prop-item)[data-diff]::before {
+ position: absolute;
+ top: 0;
+ left: -9px;
+ width: 8px;
+ font-family: var(--vscode-editor-font-family, monospace);
+ line-height: 20px;
+ text-align: center;
+}
+:is(.cr-prop-row, .cr-prop-item)[data-diff="add"]::before {
+ content: "+";
+ color: var(--cr-diff-add);
+}
+:is(.cr-prop-row, .cr-prop-item)[data-diff="del"]::before {
+ content: "−";
+ color: var(--cr-diff-del);
+}
+.cr-diff-sign {
+ display: inline-block;
+ width: 1.4ch;
+ white-space: pre;
+}
+.cr-diff-sign[data-change="add"] { color: var(--cr-diff-add); }
+.cr-diff-sign[data-change="del"] { color: var(--cr-diff-del); }
diff --git a/vscode/webview-ui/src/codeReader/markdown.test.ts b/vscode/webview-ui/src/codeReader/markdown.test.ts
index a92ed79..8905042 100644
--- a/vscode/webview-ui/src/codeReader/markdown.test.ts
+++ b/vscode/webview-ui/src/codeReader/markdown.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { renderMarkdown } from './markdown'
+import { renderMarkdown, renderMarkdownDiff } from './markdown'
describe('renderMarkdown', () => {
it('一行文字是一个段落;段内换行是软换行,空行分段', () => {
@@ -45,3 +45,19 @@ describe('renderMarkdown', () => {
expect(renderMarkdown('[文档](https://example.com "")')).toBe('文档
\n')
})
})
+
+describe('renderMarkdownDiff', () => {
+ it('照常排版,新加的字包进 、删掉的包进 ', () => {
+ expect(renderMarkdownDiff([{ text: '放锁' }, { text: '前', change: 'del' }, { text: '都', change: 'add' }, { text: '必须比对 `token`' }]))
+ .toBe('放锁前都必须比对 token
\n')
+ })
+
+ it('跨行的一截逐行包,新加的列表项留住列表记号', () => {
+ expect(renderMarkdownDiff([{ text: '- a\n' }, { text: '- b\n- c', change: 'add' }]))
+ .toBe('\n')
+ })
+
+ it('行中间的一截不把前面的字当块记号', () => {
+ expect(renderMarkdownDiff([{ text: '共 ' }, { text: '# 3', change: 'add' }])).toBe('共 # 3
\n')
+ })
+})
diff --git a/vscode/webview-ui/src/codeReader/markdown.ts b/vscode/webview-ui/src/codeReader/markdown.ts
index b37b80c..0ee7fcb 100644
--- a/vscode/webview-ui/src/codeReader/markdown.ts
+++ b/vscode/webview-ui/src/codeReader/markdown.ts
@@ -40,3 +40,37 @@ export function renderMarkdown(text: string): string {
cache.delete(cache.keys().next().value!)
return html
}
+
+/** 渲染带增删的 Markdown 时,新加 / 删掉的字前后夹的记号(Unicode 私用区的字,正文里不会有),渲染完换成 / 。 */
+const MARKS = {
+ add: ['', '', '', ''],
+ del: ['', '', '', ''],
+} as const
+
+/** 一行开头的块记号(列表、标题、引用):新加的一行要把它留在记号外面,不然就不认成列表、标题了。 */
+const BLOCK_LEAD = /^\s*(?:(?:[\-*+>]|\d+[.)]|#{1,6})\s+)*/
+
+/**
+ * 带增删的 Markdown 说明 → HTML:照常排版,新加的字包进 、删掉的包进 (样式见 .cr-diff-word)。
+ * 一截跨了几行时逐行包,记号不跨段落、列表项。
+ */
+export function renderMarkdownDiff(parts: readonly { text: string, change?: 'add' | 'del' }[]): string {
+ let text = ''
+ for (const { text: piece, change } of parts) {
+ if (!change) {
+ text += piece
+ continue
+ }
+ const [open, close] = MARKS[change]
+ text += piece.split('\n').map((line, index) => {
+ const atLineStart = index > 0 || text === '' || text.endsWith('\n')
+ const lead = atLineStart ? BLOCK_LEAD.exec(line)?.[0] ?? '' : ''
+ const rest = line.slice(lead.length)
+ return rest ? `${lead}${open}${rest}${close}` : line
+ }).join('\n')
+ }
+ let html = md.render(text)
+ for (const [open, close, openTag, closeTag] of Object.values(MARKS))
+ html = html.replaceAll(open, openTag).replaceAll(close, closeTag)
+ return html
+}