Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/layout-engine/layout-bridge/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,17 @@ const hashRuns = (block: FlowBlock): string => {
const cellBlocks = cell.blocks ?? (cell.paragraph ? [cell.paragraph] : []);

for (const cellBlock of cellBlocks) {
if (cellBlock?.kind === 'table') {
// Intentional split with renderer.ts: cache.ts hashes nested tables only,
// while renderer.ts also hashes non-paragraph cell blocks. Keep both
// in sync when adjusting nested-block invalidation behavior.
// Nested tables inside table cells must contribute to the parent
// table cache key, otherwise edits can be missed until a later
// broader invalidation.
cellHashes.push(`nt:${hashRuns(cellBlock as FlowBlock)}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this works but there's no test for it. cache.test.ts already has lots of table tests — adding one for a table-inside-a-table (~30 lines, same pattern) would make sure this path doesn't break silently. worth adding?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added test

continue;
}

const paragraphBlock = cellBlock as ParagraphBlock;

// Safety: Check that runs array exists before iterating
Expand Down
72 changes: 72 additions & 0 deletions packages/layout-engine/layout-bridge/test/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,78 @@ describe('MeasureCache', () => {
expect(cache.get(table2, 800, 600)).toEqual({ totalHeight: 120 });
});

it('invalidates when nested table content changes inside a cell block', () => {
const nestedTable = (id: string, text: string): TableBlock => ({
kind: 'table',
id,
rows: [
{
id: `${id}-row-0`,
cells: [
{
id: `${id}-cell-0-0`,
blocks: [
{
kind: 'paragraph',
id: `${id}-para-0`,
runs: [{ text, fontFamily: 'Arial', fontSize: 12 }],
},
],
},
],
},
],
});

const parentTable1: TableBlock = {
kind: 'table',
id: 'parent-table',
rows: [
{
id: 'parent-row-0',
cells: [
{
id: 'parent-cell-0-0',
blocks: [
{
kind: 'paragraph',
id: 'parent-cell-0-0-para',
runs: [{ text: 'Host', fontFamily: 'Arial', fontSize: 12 }],
},
nestedTable('nested-table', 'Nested A'),
],
},
],
},
],
};

const parentTable2: TableBlock = {
...parentTable1,
rows: [
{
...parentTable1.rows[0],
cells: [
{
...parentTable1.rows[0].cells[0],
blocks: [
{
kind: 'paragraph',
id: 'parent-cell-0-0-para',
runs: [{ text: 'Host', fontFamily: 'Arial', fontSize: 12 }],
},
nestedTable('nested-table', 'Nested B'),
],
},
],
},
],
};

cache.set(parentTable1, 800, 600, { totalHeight: 130 });
expect(cache.get(parentTable2, 800, 600)).toBeUndefined();
});

it('handles legacy single paragraph cells', () => {
const table1 = tableBlock(
'table-1',
Expand Down
7 changes: 7 additions & 0 deletions packages/layout-engine/painters/dom/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7412,6 +7412,13 @@ const deriveBlockVersion = (block: FlowBlock): string => {
hash = hashString(hash, getRunStringProp(run, 'vertAlign'));
hash = hashNumber(hash, getRunNumberProp(run, 'baselineShift'));
}
} else if (cellBlock?.kind) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this catches all non-paragraph blocks in cells, while cache.ts only catches tables specifically. both approaches work, just different — worth a comment pointing to each other so whoever changes one remembers the other exists.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment added

// Keep this broader than layout-bridge cache.ts on purpose:
// renderer hashes any non-paragraph cell block, while cache.ts hashes
// nested tables only. If you tighten one side, review the other.
// Include nested non-paragraph blocks (notably nested tables inside
// table cells) so edits there invalidate this parent table version.
hash = hashString(hash, deriveBlockVersion(cellBlock as FlowBlock));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1290,9 +1290,13 @@ export class EditorInputManager {
if (!handledByDepth) {
try {
// SD-1584: clicking inside a block SDT selects the node (NodeSelection).
// Exception: clicks inside tables nested in this SDT should use text
// selection so caret placement/editing inside table cells works.
const sdtBlock = clickDepth === 1 ? this.#findStructuredContentBlockAtPos(doc, hit.pos) : null;
let nextSelection: Selection;
if (sdtBlock) {
const insideTableInSdt =
!!sdtBlock && this.#isInsideTableWithinStructuredContentBlock(doc, hit.pos, sdtBlock.pos);
if (sdtBlock && !insideTableInSdt) {
nextSelection = NodeSelection.create(doc, sdtBlock.pos);
} else {
nextSelection = TextSelection.create(doc, hit.pos);
Expand Down Expand Up @@ -1597,6 +1601,34 @@ export class EditorInputManager {
return null;
}

#isInsideTableWithinStructuredContentBlock(doc: ProseMirrorNode, pos: number, sdtPos: number): boolean {
if (!Number.isFinite(pos) || !Number.isFinite(sdtPos)) return false;

try {
const $pos = doc.resolve(pos);
let tableDepth = -1;
let blockDepth = -1;

for (let depth = $pos.depth; depth > 0; depth--) {
const nodeName = $pos.node(depth)?.type?.name;
if (tableDepth === -1 && nodeName === 'table') {
tableDepth = depth;
}
if (nodeName === 'structuredContentBlock') {
const candidatePos = $pos.before(depth);
if (candidatePos === sdtPos) {
blockDepth = depth;
break;
}
}
}

return tableDepth !== -1 && blockDepth !== -1 && tableDepth > blockDepth;
} catch {
return false;
}
}

#findStructuredContentBlockById(doc: ProseMirrorNode, id: string): StructuredContentSelection | null {
let found: StructuredContentSelection | null = null;
doc.descendants((node, pos) => {
Expand Down
Loading
Loading