diff --git a/CHANGELOG.md b/CHANGELOG.md index b752911..9f69d1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,12 @@ All notable changes to **Rich Markdown Diff** will be documented in this file. - **Quick Edit**: Fixed a bug where the Quick Edit overlay loaded incorrect source text when editing documents that contain frontmatter metadata. - **Obsidian Tags**: Expanded the tag parser to support Japanese, Chinese, and other multi-byte characters. - **Marp Support**: Fixed slide transition animations and restored accurate dark/light theme styling in the webview. -- **Stability & Rendering**: Resolved layout and event-handling bugs, including table scroll wrapping, code block placeholder collisions, and duplicate event listeners. +- **Stability & Rendering**: + - Fixed horizontal scroll clipping of line highlights on code blocks and tables. + - Resolved visual alignment and active highlight issues for embedded code blocks and task list checkboxes. + - Fixed potential memory leaks in clipboard comparisons and child process hangs in Git Blame. +- **Compare Commands**: Fixed a bug where comparing a file with itself opened a blank diff panel. + ## [1.3.1] - 2026-05-24 diff --git a/fixtures/expected/comprehensive.html b/fixtures/expected/comprehensive.html index b0e5e17..fd681be 100644 --- a/fixtures/expected/comprehensive.html +++ b/fixtures/expected/comprehensive.html @@ -25,14 +25,14 @@
function greet(name) {// Updated comment in v2
+function greet(name) {// Updated comment in v2
console.log(`Hello, ${name}! Welcome!`);
}
function farewell(name) {
console.log(`Goodbye, ${name}!`);
}
-
Tables
+Tables
Feature
@@ -53,8 +53,7 @@ Code Blocks
✅
-
-Links and Images
+
Links and Images
Visit GitHubVisit VS Code Marketplace
Blockquotes
diff --git a/src/extension.ts b/src/extension.ts
index f950005..4cfb93f 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -935,6 +935,14 @@ async function createAndBindDiffPanel(
getDiffPanelOptions(context),
);
+ context.subscriptions.push(panel);
+ panel.onDidDispose(() => {
+ const idx = context.subscriptions.indexOf(panel);
+ if (idx > -1) {
+ context.subscriptions.splice(idx, 1);
+ }
+ });
+
await bindDiffPanel(panel, context, resolveState);
return panel;
}
@@ -1170,10 +1178,12 @@ export function activate(context: vscode.ExtensionContext) {
// Helper to track active panel for shortcuts
attachPanelTracking(panel);
+ context.subscriptions.push(panel);
+
panel.webview.html = webviewContent;
// Handle Double Click
- panel.webview.onDidReceiveMessage((message) => {
+ const messageDisposable = panel.webview.onDidReceiveMessage((message) => {
if (message.command === "openSource") {
const side = message.side;
const line = message.line;
@@ -1198,6 +1208,14 @@ export function activate(context: vscode.ExtensionContext) {
}
}
});
+
+ panel.onDidDispose(() => {
+ messageDisposable.dispose();
+ const idx = context.subscriptions.indexOf(panel);
+ if (idx > -1) {
+ context.subscriptions.splice(idx, 1);
+ }
+ });
},
);
@@ -1373,6 +1391,7 @@ export function activate(context: vscode.ExtensionContext) {
vscode.window.showInformationMessage(
l10n.t("You are comparing the same file."),
);
+ return;
}
void showTwoFilesDiff(selectedForCompareUri, targetUri, context);
diff --git a/src/gitBlameResolver.ts b/src/gitBlameResolver.ts
index 6425189..f390212 100644
--- a/src/gitBlameResolver.ts
+++ b/src/gitBlameResolver.ts
@@ -67,6 +67,8 @@ export async function resolveBlameInfo(
// \t
const child = child_process.spawn("git", ["blame", "--porcelain", fileName], { cwd });
+ child.stderr?.resume();
+
const rl = readline.createInterface({
input: child.stdout,
crlfDelay: Infinity,
diff --git a/src/markdown/renderer.ts b/src/markdown/renderer.ts
index 0fef07a..220f581 100644
--- a/src/markdown/renderer.ts
+++ b/src/markdown/renderer.ts
@@ -99,7 +99,11 @@ function configureRules(md: MarkdownIt) {
return `\n${escapedContent}\n`;
}
- return defaultFence(tokens, idx, options, env, self);
+ // Wrap code blocks in a container div so that the ::after gutter marker
+ // (positioned at left: -16px) is not clipped by 's overflow-x: auto,
+ // which creates a scroll container that clips absolutely-positioned children.
+ const rendered = defaultFence(tokens, idx, options, env, self);
+ return `${rendered}`;
};
// Image Resolver Support
diff --git a/src/markdown/structuralDiff.ts b/src/markdown/structuralDiff.ts
index 58e7865..8cf8e09 100644
--- a/src/markdown/structuralDiff.ts
+++ b/src/markdown/structuralDiff.ts
@@ -960,6 +960,20 @@ export function replaceBalancedTags(
}
}
+ if (options.tokenizeCodeBlocks !== false && html.startsWith('', i)) {
+ const start = i;
+ const end = findClosing(html, i, "div");
+ if (end > -1) {
+ const content = html.substring(start, end);
+ const token = createToken(content, "CODEBLOCK", tokens);
+ result += token;
+ i = end;
+ continue;
+ }
+ }
+
+ // Fallback: also tokenize bare elements (e.g. from Marp or other renderers
+ // that do not use the code-block-wrapper div).
if (options.tokenizeCodeBlocks !== false && html.startsWith(" -1) {
@@ -1400,57 +1414,89 @@ export function refineBlockDiffs(
// NOTE: We cannot use a simple adjacency regex (deldel insins) because
// other diff elements (e.g. a deleted section) may sit between the del-pre and ins-pre blocks.
- // Instead, collect all del-wrapped and ins-wrapped blocks globally, pair them by index,
+ // Instead, collect all del-wrapped and ins-wrapped code blocks globally, pair them by index,
// re-diff each pair, and substitute back via placeholder tokens.
+ //
+ // Code blocks may appear as:
+ // A) ...
(standard renderer)
+ // B) ...
(bare, e.g. Marp)
{
- const delPreRegex =
- /]*)>\s*]*)>([\s\S]*?)<\/pre>\s*<\/del>/gi;
- const insPreRegex =
- /]*)>\s*]*)>([\s\S]*?)<\/pre>\s*<\/ins>/gi;
-
interface PreBlock {
full: string;
+ wrapperAttrs: string; // div attrs (empty string for bare )
preAttrs: string;
inner: string;
+ isWrapped: boolean; // true → code-block-wrapper present
}
- const delBlocks: PreBlock[] = [];
- const insBlocks: PreBlock[] = [];
+ const delWrappedBlocks: PreBlock[] = [];
+ const insWrappedBlocks: PreBlock[] = [];
+ const delBareBlocks: PreBlock[] = [];
+ const insBareBlocks: PreBlock[] = [];
+
+ // Pattern A: del/ins wrapping the code-block-wrapper div
+ const delWrappedRegex =
+ /]*)>\s*]*)>\s*]*)>([\s\S]*?)<\/pre>\s*<\/div>\s*<\/del>/gi;
+ const insWrappedRegex =
+ /]*)>\s*]*)>\s*]*)>([\s\S]*?)<\/pre>\s*<\/div>\s*<\/ins>/gi;
let m: RegExpExecArray | null;
+ while ((m = delWrappedRegex.exec(resultHtml)) !== null) {
+ delWrappedBlocks.push({ full: m[0], wrapperAttrs: m[2], preAttrs: m[3], inner: m[4], isWrapped: true });
+ }
+ while ((m = insWrappedRegex.exec(resultHtml)) !== null) {
+ insWrappedBlocks.push({ full: m[0], wrapperAttrs: m[2], preAttrs: m[3], inner: m[4], isWrapped: true });
+ }
+
+ // Pattern B: del/ins wrapping a bare (fallback for Marp etc.)
+ const delPreRegex =
+ /]*)>\s*]*)>([\s\S]*?)<\/pre>\s*<\/del>/gi;
+ const insPreRegex =
+ /]*)>\s*]*)>([\s\S]*?)<\/pre>\s*<\/ins>/gi;
+
while ((m = delPreRegex.exec(resultHtml)) !== null) {
- delBlocks.push({
- full: m[0],
- preAttrs: m[2],
- inner: m[3],
- });
+ delBareBlocks.push({ full: m[0], wrapperAttrs: "", preAttrs: m[2], inner: m[3], isWrapped: false });
}
while ((m = insPreRegex.exec(resultHtml)) !== null) {
- insBlocks.push({
- full: m[0],
- preAttrs: m[2],
- inner: m[3],
+ insBareBlocks.push({ full: m[0], wrapperAttrs: "", preAttrs: m[2], inner: m[3], isWrapped: false });
+ }
+
+ const diffedPairs: Array<{
+ delFull: string;
+ insFull: string;
+ diffed: string;
+ }> = [];
+
+ const pairWrappedCount = Math.min(delWrappedBlocks.length, insWrappedBlocks.length);
+ for (let i = 0; i < pairWrappedCount; i++) {
+ const innerDiff = diffCodeBlocks(
+ delWrappedBlocks[i].inner,
+ insWrappedBlocks[i].inner,
+ );
+ const diffedPre = `${innerDiff}`;
+ const diffed = `${diffedPre}`;
+ diffedPairs.push({
+ delFull: delWrappedBlocks[i].full,
+ insFull: insWrappedBlocks[i].full,
+ diffed,
+ });
+ }
+
+ const pairBareCount = Math.min(delBareBlocks.length, insBareBlocks.length);
+ for (let i = 0; i < pairBareCount; i++) {
+ const innerDiff = diffCodeBlocks(
+ delBareBlocks[i].inner,
+ insBareBlocks[i].inner,
+ );
+ const diffedPre = `${innerDiff}`;
+ diffedPairs.push({
+ delFull: delBareBlocks[i].full,
+ insFull: insBareBlocks[i].full,
+ diffed: diffedPre,
});
}
- const pairCount = Math.min(delBlocks.length, insBlocks.length);
+ const pairCount = diffedPairs.length;
if (pairCount > 0) {
- const diffedPairs: Array<{
- delFull: string;
- insFull: string;
- diffed: string;
- }> = [];
- for (let i = 0; i < pairCount; i++) {
- // Diff ONLY the inner content of the pre block
- const innerDiff = diffCodeBlocks(
- delBlocks[i].inner,
- insBlocks[i].inner,
- );
- diffedPairs.push({
- delFull: delBlocks[i].full,
- insFull: insBlocks[i].full,
- diffed: `${innerDiff}`,
- });
- }
const uniqueRunId = Math.random().toString(36).slice(2, 10);
for (let i = 0; i < diffedPairs.length; i++) {
@@ -1771,10 +1817,15 @@ export function normalizeMathBlockDiffs(html: string): string {
}
export function cleanupCheckboxArtifacts(html: string): string {
- return html.replace(
- /(]+class="task-list-item-checkbox"[^>]*>)(\s*)(?=(?:]*>\s*\[))/gi,
+ let result = html.replace(
+ /(]+class="task-list-item-checkbox"[^>]*>)(\s*)(?=(?:
]*>\s*\[))/gi,
'$1$2',
);
+ result = result.replace(
+ /(]+class="task-list-item-checkbox"[^>]*>)(\s*)(?=(?:
]*>\s*\[))/gi,
+ '$1$2',
+ );
+ return result;
}
export function stripDataLineAttributes(html: string): string {
@@ -2213,42 +2264,38 @@ export function verifyDiffIntegrity(
return true; // Nothing to check
}
- const diffWordsSet = new Set(getWords(diffText));
+ const diffWordsMap = new Map();
+ for (const word of getWords(diffText)) {
+ diffWordsMap.set(word, (diffWordsMap.get(word) || 0) + 1);
+ }
- // Sample a subset of words to check to avoid performance issues on huge documents
- // but ensure we check enough to detect truncation.
- const sampleSize = Math.min(newWords.length, 200);
- const step = Math.max(1, Math.floor(newWords.length / sampleSize));
+ const newWordsMap = new Map();
+ for (const word of newWords) {
+ newWordsMap.set(word, (newWordsMap.get(word) || 0) + 1);
+ }
let missingCount = 0;
- let checkedCount = 0;
- for (let i = 0; i < newWords.length; i += step) {
- const word = newWords[i];
- checkedCount++;
- if (!diffWordsSet.has(word)) {
- missingCount++;
+ let totalChecked = 0;
+ const missingWords: string[] = [];
+
+ for (const [word, newCount] of newWordsMap.entries()) {
+ const diffCount = diffWordsMap.get(word) || 0;
+ if (diffCount < newCount) {
+ missingCount += (newCount - diffCount);
+ if (missingWords.length < 10) {
+ missingWords.push(word);
+ }
}
+ totalChecked += newCount;
}
- // Allow a very small margin of error (e.g. 1.0%) for edge cases where htmldiff
- // might legitimately combine or slightly transform words (e.g. case changes, punctuation).
- const failureThreshold = 0.01; // 1.0% margin of error
- const missingRatio = checkedCount > 0 ? missingCount / checkedCount : 0;
+ const failureThreshold = 0.02; // 2.0% margin of error
+ const missingRatio = totalChecked > 0 ? missingCount / totalChecked : 0;
const isBroken = missingRatio > failureThreshold;
if (isBroken) {
- const missingWords = [];
- for (let i = 0; i < newWords.length; i += step) {
- const word = newWords[i];
- if (!diffWordsSet.has(word)) {
- missingWords.push(word);
- if (missingWords.length >= 10) {
- break;
- }
- }
- }
console.warn(
- `Integrity check failed: missing ${missingCount}/${checkedCount} words (${(missingRatio * 100).toFixed(1)}%). Missing: ${missingWords.join(", ")}`,
+ `Integrity check failed: missing ${missingCount}/${totalChecked} words (${(missingRatio * 100).toFixed(1)}%). Missing: ${missingWords.join(", ")}`,
);
return false;
}
diff --git a/src/markdown/webviewTemplate.ts b/src/markdown/webviewTemplate.ts
index ee5113c..c4b97f7 100644
--- a/src/markdown/webviewTemplate.ts
+++ b/src/markdown/webviewTemplate.ts
@@ -450,6 +450,8 @@ export function getWebviewContent(
dl,
blockquote,
pre,
+ .code-block-wrapper,
+ .table-block-wrapper,
.table-scroll,
hr,
.markdown-alert,
@@ -512,6 +514,17 @@ export function getWebviewContent(
padding: 0;
border-radius: 0;
}
+ /* Wrapper around code blocks to avoid the gutter marker being clipped
+ by pre's overflow-x: auto scroll container. The wrapper stays overflow:
+ visible so the ::after gutter bar can appear in the left gutter padding. */
+ .code-block-wrapper {
+ position: relative;
+ overflow: visible;
+ margin-bottom: var(--markdown-block-spacing);
+ }
+ .code-block-wrapper pre {
+ margin-bottom: 0;
+ }
pre {
background-color: var(--vscode-textCodeBlock-background, var(--markdown-raised-background));
padding: 8px 10px;
@@ -690,6 +703,11 @@ export function getWebviewContent(
p > img:only-child {
display: block;
}
+ .table-block-wrapper {
+ position: relative;
+ overflow: visible;
+ margin-bottom: var(--markdown-block-spacing);
+ }
.table-scroll {
width: 100%;
max-width: 100%;
@@ -834,6 +852,7 @@ export function getWebviewContent(
ins:has(.footnote-item), del:has(.footnote-item),
ins:has(li), del:has(li),
ins:has(pre), del:has(pre),
+ ins:has(.code-block-wrapper), del:has(.code-block-wrapper),
ins:has(table), del:has(table),
ins:has(h1), del:has(h1),
ins:has(h2), del:has(h2),
@@ -862,7 +881,7 @@ export function getWebviewContent(
/* Container Borders for Block Diffs (Alerts, Code, Mermaid) */
- :is(.markdown-alert, .mermaid, pre):is(:has(ins), :has(del), :parent(ins), :parent(del), .diffins, .diffdel) {
+ :is(.markdown-alert, .mermaid, pre, .code-block-wrapper):is(:has(ins), :has(del), :parent(ins), :parent(del), .diffins, .diffdel) {
position: relative;
}
@@ -955,8 +974,8 @@ export function getWebviewContent(
- ins:has(> h1, > h2, > h3, > h4, > h5, > h6, > p, > img, > table, > .table-scroll, > ul, > ol, > dl, > li, > blockquote, > div, > pre, > hr, > section, > details, > summary, > figure),
- del:has(> h1, > h2, > h3, > h4, > h5, > h6, > p, > img, > table, > .table-scroll, > ul, > ol, > dl, > li, > blockquote, > div, > pre, > hr, > section, > details, > summary, > figure) {
+ ins:has(> h1, > h2, > h3, > h4, > h5, > h6, > p, > img, > table, > .table-block-wrapper, > .table-scroll, > ul, > ol, > dl, > li, > blockquote, > div, > pre, > hr, > section, > details, > summary, > figure),
+ del:has(> h1, > h2, > h3, > h4, > h5, > h6, > p, > img, > table, > .table-block-wrapper, > .table-scroll, > ul, > ol, > dl, > li, > blockquote, > div, > pre, > hr, > section, > details, > summary, > figure) {
display: block;
width: fit-content;
max-width: 100%;
@@ -1344,14 +1363,14 @@ export function getWebviewContent(
}
/* Specific High Visibility for Complex Blocks (Code/Mermaid/Math) */
- pre.selected-change,
+ :is(pre, .code-block-wrapper).selected-change,
.mermaid.selected-change,
.katex-block.selected-change {
overflow: visible !important;
display: block;
}
- pre.selected-change.selected-ins,
+ :is(pre, .code-block-wrapper).selected-change.selected-ins,
.mermaid.selected-change.selected-ins,
.katex-block.selected-change.selected-ins {
background-color: rgba(34, 197, 94, 0.25) !important;
@@ -1359,7 +1378,7 @@ export function getWebviewContent(
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.8) !important;
}
- pre.selected-change.selected-del,
+ :is(pre, .code-block-wrapper).selected-change.selected-del,
.mermaid.selected-change.selected-del,
.katex-block.selected-change.selected-del {
background-color: rgba(248, 113, 113, 0.2) !important;
@@ -1367,7 +1386,7 @@ export function getWebviewContent(
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.8) !important;
}
- pre.selected-change.selected-mod,
+ :is(pre, .code-block-wrapper).selected-change.selected-mod,
.mermaid.selected-change.selected-mod,
.katex-block.selected-change.selected-mod {
background-color: rgba(59, 130, 246, 0.08) !important;
@@ -1375,6 +1394,10 @@ export function getWebviewContent(
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3) !important;
}
+ .code-block-wrapper.selected-change pre {
+ background-color: transparent !important;
+ }
+
/* Image Focus Style (same as Mermaid) */
.selected-change img {
border: 1px solid rgba(255, 165, 0, 0.9) !important;
@@ -2730,13 +2753,13 @@ export function getWebviewContent(
const getComplexContainer = (el) => {
// Case 1: The change is inside a complex visual block like Mermaid or KaTeX.
// Table and stable code-block diffs stay granular so the ruler and selection can stay local.
- const ancestor = el.closest('.mermaid') || el.closest('.katex-block') || el.closest('svg');
+ const ancestor = el.closest('.mermaid') || el.closest('.katex-block') || el.closest('svg') || el.closest('.code-block-wrapper');
if (ancestor) return ancestor;
// Case 2: The change wraps a complex visual block (e.g. a new code block or diagram added)
// We prefer the child container for highlighting as it's the visual element.
if (el.querySelector) {
- let child = el.querySelector('pre') || el.querySelector('.mermaid') || el.querySelector('.katex-block');
+ let child = el.querySelector('.code-block-wrapper') || el.querySelector('pre') || el.querySelector('.mermaid') || el.querySelector('.katex-block');
if (!child) {
// Only treat SVG as complex if it's NOT an alert icon
const svg = el.querySelector('svg');
diff --git a/src/markdownDiff.ts b/src/markdownDiff.ts
index f499f05..47b3f93 100644
--- a/src/markdownDiff.ts
+++ b/src/markdownDiff.ts
@@ -55,18 +55,18 @@ function wrapTablesForScrolling(html: string): string {
return html.replace(
tableRegex,
(match, tagType, tagAttrs, offset, fullString) => {
- // Check if this match is already wrapped in a div.table-scroll
+ // Check if this match is already wrapped in a div.table-scroll/table-block-wrapper
const preceding = fullString.slice(0, offset).trim();
const following = fullString.slice(offset + match.length).trim();
- const isPrecededByScrollDiv = /]*\bclass=["'][^"']*\btable-scroll\b[^"']*["'][^>]*>\s*$/i.test(preceding);
- const isFollowedByCloseDiv = /^\s*<\/div>/i.test(following);
+ const isPrecededByScrollDiv = /]*\bclass=["'][^"']*\b(table-scroll|table-block-wrapper)\b[^"']*["'][^>]*>\s*(?:]*\bclass=["'][^"']*\btable-scroll\b[^"']*["'][^>]*>\s*)?$/i.test(preceding);
+ const isFollowedByCloseDiv = /^\s*<\/div>\s*(?:<\/div>)?/i.test(following);
if (isPrecededByScrollDiv && isFollowedByCloseDiv) {
return match;
}
- return `${match}`;
+ return `${match}`;
}
);
}
diff --git a/src/test/runTest.ts b/src/test/runTest.ts
index 00723f8..c6c3369 100644
--- a/src/test/runTest.ts
+++ b/src/test/runTest.ts
@@ -66,12 +66,14 @@ async function main() {
"index",
);
const runtimeRoot = path.join(
- os.tmpdir(),
- `rich-markdown-diff-host-${Date.now()}`,
+ extensionDevelopmentPath,
+ ".vscode-test",
+ "user-data-dir",
);
const userDataDir = path.join(runtimeRoot, "user-data");
const extensionsDir = path.join(runtimeRoot, "extensions");
+ await fs.rm(runtimeRoot, { recursive: true, force: true });
await fs.mkdir(userDataDir, { recursive: true });
await fs.mkdir(extensionsDir, { recursive: true });
diff --git a/src/test/unit/edgeCases.test.ts b/src/test/unit/edgeCases.test.ts
index 7a99899..2b7c0d1 100644
--- a/src/test/unit/edgeCases.test.ts
+++ b/src/test/unit/edgeCases.test.ts
@@ -31,7 +31,12 @@ describe("MarkdownDiffProvider - Edge Cases", () => {
// It should be wrapped in a single del with diff-block class
assert.ok(diff.includes("diff-block"), "Should have diff-block class for code block deletion");
assert.ok(diff.includes("]*class="[^"]*diff-block[^"]*"[^>]*>\s*]*class="[^"]*diff-block[^"]*"[^>]*>\s*]*>\s*]*class="[^"]*diff-block[^"]*"[^>]*>\s* {
@@ -147,7 +152,7 @@ describe("MarkdownDiffProvider - Edge Cases", () => {
// 2. Table inside del should wrap the del container, not the table inside it
const delInput = `${table}`;
const delOutput = provider.getWebviewContent(delInput, "", "", "", "");
- assert.ok(delOutput.includes(`${table}`), "Should wrap outer del rather than nesting block-in-inline");
+ assert.ok(delOutput.includes(`${table}`), "Should wrap outer del rather than nesting block-in-inline");
});
it("BUG-03: should not double-wrap tables even if table-scroll div has attributes and trailing whitespace/newlines", () => {
diff --git a/src/test/unit/markdownDiff.test.ts b/src/test/unit/markdownDiff.test.ts
index abf63e3..3066a6c 100644
--- a/src/test/unit/markdownDiff.test.ts
+++ b/src/test/unit/markdownDiff.test.ts
@@ -852,7 +852,7 @@ describe("MarkdownDiffProvider", () => {
);
assert.ok(
webviewContent.includes(
- "const ancestor = el.closest('.mermaid') || el.closest('.katex-block') || el.closest('svg');",
+ "const ancestor = el.closest('.mermaid') || el.closest('.katex-block') || el.closest('svg') || el.closest('.code-block-wrapper');",
),
"Complex visual blocks like Mermaid and KaTeX should still be promoted to their rendered container while stable code blocks stay granular",
);
@@ -992,15 +992,15 @@ describe("MarkdownDiffProvider", () => {
);
assert.ok(
webviewContent.includes(
- "const ancestor = el.closest('.mermaid') || el.closest('.katex-block') || el.closest('svg');",
+ "const ancestor = el.closest('.mermaid') || el.closest('.katex-block') || el.closest('svg') || el.closest('.code-block-wrapper');",
),
- "Stable code blocks with inner line diffs should stay granular instead of being promoted through closest('pre')",
+ "Stable code blocks with inner line diffs should be promoted to .code-block-wrapper as a stable anchor",
);
assert.ok(
webviewContent.includes(
- "let child = el.querySelector('pre') || el.querySelector('.mermaid') || el.querySelector('.katex-block');",
+ "let child = el.querySelector('.code-block-wrapper') || el.querySelector('pre') || el.querySelector('.mermaid') || el.querySelector('.katex-block');",
),
- "Wrapped code-block insertions should resolve to the pre container for active highlighting",
+ "Wrapped code-block insertions should resolve to the .code-block-wrapper container for active highlighting",
);
assert.ok(
webviewContent.includes(".selected-change.selected-ins {") &&
@@ -1010,7 +1010,7 @@ describe("MarkdownDiffProvider", () => {
"Inserted active changes should use green selection styling instead of the old yellow overlay",
);
assert.ok(
- webviewContent.includes("pre.selected-change.selected-ins,") &&
+ webviewContent.includes(":is(pre, .code-block-wrapper).selected-change.selected-ins,") &&
webviewContent.includes("border: 1px solid #22c55e !important;") &&
webviewContent.includes(
"box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.8) !important;",
@@ -1052,10 +1052,10 @@ describe("MarkdownDiffProvider", () => {
"heading-prefix class should prevent number prefixes from wrapping",
);
assert.ok(
- /ins:has\(> h1, > h2, > h3, > h4, > h5, > h6, > p, > img, > table, > \.table-scroll, > ul, > ol, > dl, > li, > blockquote, > div, > pre, > hr, > section, > details, > summary, > figure\),[\s\S]*del:has\(> h1, > h2, > h3, > h4, > h5, > h6, > p, > img, > table, > \.table-scroll, > ul, > ol, > dl, > li, > blockquote, > div, > pre, > hr, > section, > details, > summary, > figure\) \{[\s\S]*display: block;[\s\S]*width: fit-content;/m.test(
+ /ins:has\(> h1, > h2, > h3, > h4, > h5, > h6, > p, > img, > table, > \.table-block-wrapper, > \.table-scroll, > ul, > ol, > dl, > li, > blockquote, > div, > pre, > hr, > section, > details, > summary, > figure\),[\s\S]*del:has\(> h1, > h2, > h3, > h4, > h5, > h6, > p, > img, > table, > \.table-block-wrapper, > \.table-scroll, > ul, > ol, > dl, > li, > blockquote, > div, > pre, > hr, > section, > details, > summary, > figure\) \{[\s\S]*display: block;[\s\S]*width: fit-content;/m.test(
webviewContent,
),
- "Modified headings should be set as block elements so the background color spans the full pane width",
+ "Modified headings should be set as block elements so the background color spans the full width",
);
});
@@ -1175,7 +1175,7 @@ describe("MarkdownDiffProvider", () => {
);
assert.ok(
- /\s*/m.test(webviewContent),
+ /\s*/m.test(webviewContent),
"Tables should be wrapped in a dedicated scroll container so wide tables scroll locally instead of stretching the pane",
);
assert.ok(
diff --git a/src/test/visual/__screenshots__/vrt.test.js-snapshots/comprehensive-v1-v2-split-light-chromium-linux.png b/src/test/visual/__screenshots__/vrt.test.js-snapshots/comprehensive-v1-v2-split-light-chromium-linux.png
index ad929a6..fd09c34 100644
Binary files a/src/test/visual/__screenshots__/vrt.test.js-snapshots/comprehensive-v1-v2-split-light-chromium-linux.png and b/src/test/visual/__screenshots__/vrt.test.js-snapshots/comprehensive-v1-v2-split-light-chromium-linux.png differ
diff --git a/src/test/visual/__screenshots__/vrt.test.js-snapshots/marp-v1-v2-split-dark-chromium-linux.png b/src/test/visual/__screenshots__/vrt.test.js-snapshots/marp-v1-v2-split-dark-chromium-linux.png
index 6c104ac..b5fe25c 100644
Binary files a/src/test/visual/__screenshots__/vrt.test.js-snapshots/marp-v1-v2-split-dark-chromium-linux.png and b/src/test/visual/__screenshots__/vrt.test.js-snapshots/marp-v1-v2-split-dark-chromium-linux.png differ