Skip to content
Merged
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
10 changes: 6 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

All notable changes to **Rich Markdown Diff** will be documented in this file.

## [NEXT RELEASE]
## [1.4.0] - 2026-07-19

### Added in NEXT RELEASE
### Added in 1.4.0

- **MDX & Custom Components**: Native rendering and high-fidelity diffs for MDX, Docusaurus, and Astro Starlight components. Supports interactive `<Tabs>`, inline `<Badge>` elements, timeline `<Steps>`, premium `<Card>` layouts, Docusaurus triple-colon admonitions (`:::note` etc.), and graceful visual fallbacks for custom JSX/TSX tags.
- **Interactive Wikilinks**: Click to open local target files directly from `[[Wiki-Style Links]]` in the diff view.

### Fixed in NEXT RELEASE
### Fixed in 1.4.0

- **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.
Expand All @@ -17,9 +18,10 @@ All notable changes to **Rich Markdown Diff** will be documented in this file.
- 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.
- Fixed duplicate containers in GitHub Alerts and incorrect numbering in Starlight `<Steps>`.
- Added outline and background highlights for active cells in frontmatter diffs.
- **Compare Commands**: Fixed a bug where comparing a file with itself opened a blank diff panel.


## [1.3.1] - 2026-05-24

### Improved in 1.3.1
Expand Down
21 changes: 13 additions & 8 deletions fixtures/comprehensive_v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ This document tests all supported markdown extensions.

## Text Formatting

This is **bold text** and this is _italic text_.
You can also use ~~strikethrough~~ and `inline code`.
This is **Bold text** and this is _Italic text_.
You can also use ~~Strikethrough~~ and `inline code`.
Use ==highlighted text== for emphasis.

## Lists

### Unordered List

- Item 1
- Item 2

- Item 3

### Ordered List

1. First item
2. Second item
2. Second item (older)
3. Third item

### Task List
Expand All @@ -32,6 +32,7 @@ Use ==highlighted text== for emphasis.
```javascript
function greet(name) {
console.log(`Hello, ${name}!`);
console.log(`Good night!`);
}
```

Expand All @@ -40,8 +41,9 @@ function greet(name) {
| Feature | Status |
| ----------------- | ------ |
| Text diff | ✅ |
| Code highlighting | |
| Code highlighting | |
| Emoji support | ✅ |
| Obsidian support | ✅ |

## Links and Images

Expand All @@ -58,7 +60,7 @@ function greet(name) {

## Math (KaTeX)

Inline math: $E = mc^2$
Inline math: $E = mc^2$ and $F = ma$

Block math:

Expand All @@ -85,7 +87,7 @@ graph TD;

# Obsidian

#mermaid
#mermaid #日本語

![[Page]]

Expand Down Expand Up @@ -160,13 +162,15 @@ SOFTWARE.
Let's test inline badges: <Badge text="Caution" variant="caution" /> and <Badge text="Deprecated" variant="danger" />.

And here is a Starlight steps component:

<Steps>
1. Download the tool
2. Configure settings
3. Start running
</Steps>
</Steps>

Here is a Starlight Card:

<Card title="Introduction" icon="document">
Welcome to the premium card view.
</Card>
Expand All @@ -177,6 +181,7 @@ This is standard admonition text in v1.
:::

And a custom unknown fallback element:

<CustomReactComponent user="alice" role="admin" />

## Summary
Expand Down
5 changes: 4 additions & 1 deletion fixtures/comprehensive_v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,14 @@ Let's test inline badges: <Badge text="Warning" variant="warning" /> and <Badge

And here is a Starlight steps component:
<Steps>

1. Download the tool (new version)
2. Configure settings securely
3. Start running the process
</Steps>
</Steps>

Here is a Starlight Card:

<Card title="Introduction" icon="note">
Welcome to the premium card view, now with updated content.
</Card>
Expand All @@ -211,6 +213,7 @@ Critical warning!
:::

And a custom unknown fallback element:

<CustomReactComponent user="alice" role="super-admin" theme="dark" />

## Summary
Expand Down
107 changes: 59 additions & 48 deletions fixtures/expected/comprehensive.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"publisher": "phine-apps",
"displayName": "Rich Markdown Diff",
"description": "Professional visual Markdown diff with Math, Mermaid, and Git integration for better document reviews.",
"version": "1.3.1",
"version": "1.4.0",
"icon": "images/icon.png",
"author": "phine-apps",
"license": "MIT",
Expand Down
98 changes: 98 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,28 @@ async function bindDiffPanel(
return;
}

if (message.page) {
const resolvedUri = await resolveWikilinkUri(message.page, uriToOpen);
if (resolvedUri) {
try {
const document = await vscode.workspace.openTextDocument(resolvedUri);
await vscode.window.showTextDocument(document, {
viewColumn: vscode.ViewColumn.One,
preserveFocus: false,
});
} catch (error) {
vscode.window.showErrorMessage(
l10n.t("Could not open file: {0}", String(error)),
);
}
} else {
vscode.window.showWarningMessage(
l10n.t("Wikilink target not found: {0}", message.page),
);
}
return;
}

try {
const document = await vscode.workspace.openTextDocument(uriToOpen);
const editor = await vscode.window.showTextDocument(document, {
Expand Down Expand Up @@ -1197,6 +1219,28 @@ export function activate(context: vscode.ExtensionContext) {
}

if (editor) {
if (message.page) {
resolveWikilinkUri(message.page, editor.document.uri).then((resolvedUri) => {
if (resolvedUri) {
vscode.workspace.openTextDocument(resolvedUri).then((doc) => {
vscode.window.showTextDocument(doc, {
viewColumn: vscode.ViewColumn.One,
preserveFocus: false,
});
}, (err) => {
vscode.window.showErrorMessage(
l10n.t("Could not open file: {0}", String(err)),
);
});
} else {
vscode.window.showWarningMessage(
l10n.t("Wikilink target not found: {0}", message.page),
);
}
});
return;
}

vscode.window
.showTextDocument(editor.document, vscode.ViewColumn.One)
.then((e) => {
Expand Down Expand Up @@ -1502,4 +1546,58 @@ class DiffEditorProvider implements vscode.CustomReadonlyEditorProvider {
}
}

async function resolveWikilinkUri(
page: string,
baseUri: vscode.Uri,
): Promise<vscode.Uri | undefined> {
const cleanBase = toFileBackedUri(baseUri);
// 1. Try relative to the base file directory
let targetUri = vscode.Uri.joinPath(cleanBase, "..", page);
if (!path.extname(page)) {
targetUri = targetUri.with({ path: targetUri.path + ".md" });
}

try {
await vscode.workspace.fs.stat(targetUri);
return targetUri;
} catch {
// Ignore error, file does not exist at relative path
}

// 2. Try relative to workspace folders
if (vscode.workspace.workspaceFolders) {
for (const folder of vscode.workspace.workspaceFolders) {
let rootTargetUri = vscode.Uri.joinPath(folder.uri, page);
if (!path.extname(page)) {
rootTargetUri = rootTargetUri.with({ path: rootTargetUri.path + ".md" });
}
try {
await vscode.workspace.fs.stat(rootTargetUri);
return rootTargetUri;
} catch {
// Ignore error, file does not exist at workspace root
}
}
}

// 3. Search globally in the workspace for shortest path matching
const basename = path.basename(page);
const ext = path.extname(page) ? "" : ".md";
const globPattern = `**/${basename}${ext}`;
try {
const files = await vscode.workspace.findFiles(
globPattern,
"**/node_modules/**",
5,
);
if (files.length > 0) {
return files[0];
}
} catch (err) {
console.error("findFiles failed:", err);
}

return undefined;
}

export function deactivate() { }
58 changes: 33 additions & 25 deletions src/markdown/structuralDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -845,7 +845,11 @@ export function restoreBlockAttributes(
let res;
const inDel = tagStack.includes("del");
const inIns = tagStack.includes("ins");
if (inDel) {

const isOnlyInOld = oldPools[key] && (!newPools[key] || newPools[key].length === 0);
const isOnlyInNew = newPools[key] && (!oldPools[key] || oldPools[key].length === 0);

if (inDel || isOnlyInOld) {
const idx = oldCounters[key] || 0;
const pool = oldPools[key] || [];
res = pool[idx] || pool[pool.length - 1] || "";
Expand All @@ -858,7 +862,7 @@ export function restoreBlockAttributes(
newCounters[key] = idx + 1;

// Also increment old counter if shared to keep them "aligned" where possible
if (!inIns) {
if (!inIns && !isOnlyInNew) {
oldCounters[key] = (oldCounters[key] || 0) + 1;
}
}
Expand Down Expand Up @@ -1220,32 +1224,36 @@ export function refineBlockDiffs(
resultHtml = resultHtml.replace(
alertRegex,
(match, delBlock, oldInner, insBlock, newInner) => {
const alertCount = (
newInner.match(/<div[^>]*class="markdown-alert/g) || []
).length;
if (alertCount > 1) {
return match;
}

const titleRegex = /<p class="markdown-alert-title">([\s\S]*?)<\/p>/;
const oldTitleMatch = oldInner.match(titleRegex);
const newTitleMatch = newInner.match(titleRegex);
const openTagRegex = /^<div class="markdown-alert[^>]*>/;
const oldOpenMatch = oldInner.match(openTagRegex);
const newOpenMatch = newInner.match(openTagRegex);

if (
oldTitleMatch &&
newTitleMatch &&
oldTitleMatch[0] === newTitleMatch[0]
oldOpenMatch &&
newOpenMatch &&
oldInner.endsWith("</div>") &&
newInner.endsWith("</div>")
) {
const titleHtml = oldTitleMatch[0];
const oldBody = oldInner.replace(titleHtml, "").trim();
const newBody = newInner.replace(titleHtml, "").trim();
const diffBody = execute(oldBody, newBody);
const openTagRegex = /^<div class="markdown-alert[^>]*>/;
const openTagMatch = newInner.match(openTagRegex);
const openTag = openTagMatch
? openTagMatch[0]
: '<div class="markdown-alert">';
return `${openTag}${titleHtml}\n${diffBody}</div>`;
const oldOpen = oldOpenMatch[0];
const newOpen = newOpenMatch[0];
const oldContent = oldInner.substring(oldOpen.length, oldInner.length - 6).trim();
const newContent = newInner.substring(newOpen.length, newInner.length - 6).trim();

const titleRegex = /<p class="markdown-alert-title">([\s\S]*?)<\/p>/;
const oldTitleMatch = oldContent.match(titleRegex);
const newTitleMatch = newContent.match(titleRegex);

if (
oldTitleMatch &&
newTitleMatch &&
oldTitleMatch[0] === newTitleMatch[0]
) {
const titleHtml = oldTitleMatch[0];
const oldBody = oldContent.replace(titleHtml, "").trim();
const newBody = newContent.replace(titleHtml, "").trim();
const diffBody = execute(oldBody, newBody);
return `${newOpen}${titleHtml}\n${diffBody}</div>`;
}
}

return replacer(match, delBlock, oldInner, insBlock, newInner);
Expand Down
Loading