Add workspace-wide wiki-link resolution, transclusion, and embeds#3
Merged
Conversation
Extends the markdown preview with Obsidian-style wikilinks resolved against a workspace-wide `.md` index, note transclusion, and image embeds: - `[[name]]`, `[[name#heading]]`, `[[name^block]]`, `[[name|alias]]` resolved workspace-wide via an index built on activate and maintained by per-root FileSystemWatchers. Fragment-before-pipe canonical form. - `![[note]]` transclusion (recursive md.render with a depth-2 cycle cap), `![[image.png]]` / `![[image.png|width]]` embeds with a broken-image fallback. - Four `markdownPreviewStyles.wikilinks.*` settings (enabled, extraIndexRoots, embedNotes, embedMaxBytes) defaulting to a zero-config working case. extraIndexRoots supports `~` expansion. - Bespoke parser/resolver (no runtime deps), unit-testable in plain Node via the `__setWikiStateForTest` seam. Click-behaviour fix: a resolved wikilink emits a path relative to the previewed document so clicking routes through VS Code's `openLink` channel (native in-preview navigation, no OS prompt) rather than a `vscode://file/...` URI. The previewed document's path is read from `env.currentDocument` (a vscode.Uri in 1.122 - `.fsPath`, not `.uri`) with `env.resourceProvider.resource` as the fallback that survives the incremental live-edit render path, where currentDocument is undefined. 108 unit tests; CLAUDE.md documents the hard-won VS Code preview internals.
The mps_embed_note degrade paths (cycle cap, fragment-miss, read error)
emitted href="${resolvedPath}" - a bare absolute disk path - which the
main renderer's own comment documents as broken: VS Code concatenates a
schemeless absolute href onto the preview directory and ENOENTs. Route
all three through the same href construction the wikilink renderer uses.
Extracts two helpers from the mps_wikilink renderer so every emit site
shares one implementation:
- docPathFromEnv(env): the currentDocument/resourceProvider chain.
- buildResolvedHref(resolvedPath, fragment, docPath): relative /
vscode://file / bare-fragment forms.
Also fixed in the same code:
- A resolvable target that simply wasn't transcluded (embedNotes off, or
over the size cap) now resolves in the degrade branch and keeps a
working href, instead of falling back to a dead bare-name link.
- Embed fallback display text now comes from the parsed target, so an
alias shows cleanly ("Nice Alias") rather than the literal "foo|Nice
Alias".
- A note that wiki-links to itself emits just the fragment, so the click
scrolls in place instead of reloading the document.
- Removed the dead meta.embedCycle branch in the wikilink renderer
(nothing ever set it; the cycle class is applied in mps_embed_note).
114 unit tests.
renderText (the Properties-table value renderer) emitted frontmatter [[wiki-links]] without resolving them or honouring alias/fragment syntax: `parent: [[meeting|Weekly sync]]` rendered as a dead href="meeting|Weekly sync" with the literal pipe as display text, while the same link in the document body resolved to a real path and showed "Weekly sync". renderText now parses + resolves via the same parseWikilinkTarget / resolveWikilinkTarget / buildResolvedHref path the body renderer uses. docPath is threaded through renderProperties -> renderValue -> renderText from the mps_frontmatter core rule's env, so resolved frontmatter links get a document-relative href where available and the vscode://file form otherwise. The "treated as a string" frontmatter rule is unaffected - that governs value-type PARSING (not splitting [[x]] into an array), not rendering. Also consolidated the scattered per-block extension.js requires in the test file into one top-level import. 116 unit tests.
The recursive md.render(sliced) used for note transclusion re-ran the full core-rule pipeline on the embedded slice. Two rules emit host-document-scoped output that must not run inside an embed: - mps_blank_lines stamped data-line / data-mps-line numbered from the SLICE's line 0 and injected blank-line placeholders, colliding with the host document's line numbers and misleading VS Code's active-line tracker / double-click-to-jump. Now skipped entirely when env.mpsEmbedDepth is set (embedded content carries no gutter). - mps_block_anchors emitted id="mps-block-<id>" for a ^block marker in the embedded note, producing a duplicate id (invalid HTML, ambiguous scroll target) when the host or a standalone open of the note also carries it. Now strips the marker for clean text but suppresses the id inside an embed. (mps_callouts still runs inside embeds - callouts are content, not source-mapping. mps_frontmatter is a no-op there because the slice has already had its frontmatter stripped.) 118 unit tests.
Three related fragment/slug fixes: - Heading slug: the lowercase/strip/hyphenate algorithm was copy-pasted three times (sliceToFragment x2, fragmentToAnchor) and diverged from VS Code's GithubSlugifier - it stripped Unicode letters (`Café` -> `caf`) and collapsed consecutive whitespace, so `[[note#Café]]` emitted an anchor that never matched the rendered heading id. Extracted one slugifyHeading helper that reproduces the GitHub slugifier's output (keeps Unicode letters/numbers/marks, whitespace -> '-' per character) without vendoring its ~2KB Unicode code-point table. - Block-id charset: parseWikilinkTarget accepted any chars after `^` ([^#^]+) while the slice strip and the mps-block anchor only handle [A-Za-z0-9_-]. `[[note^a b]]` half-parsed into a `#mps-block-a b` anchor that could never match. A shared BLOCK_ID_RE now gates the `^` fragment in the parser, so an invalid id is left in the name instead. - Pure fragments: `[[#heading]]` / `[[^block]]` parsed the marker as the name (fragment null), so the same-document link Obsidian supports resolved to nothing. The parser now allows an empty name; the renderer emits a bare same-document fragment anchor. (Per-segment href encoding - the `?`/`#`-in-filename fix - already landed with the embed/href helper extraction.) 125 unit tests.
rebuildWorkspaceIndex awaits findFiles per root, so a config change firing during the initial build (or a fast succession of changes) could interleave two rebuilds: the older one resumed after its await and kept adding to the now-shared index and pushing watchers, leaving duplicate live FileSystemWatchers (each file event fires twice) and orphaned watchers that the next rebuild's teardown missed. Adds a monotonic generation counter: each rebuild captures the generation at entry and, after every await, bails if a newer rebuild has started. Watchers accumulate in a local array and are committed to the shared _activeWatchers / context.subscriptions only once the rebuild wins the race; a superseded rebuild disposes anything it created instead of leaking it. This also bounds context.subscriptions growth - only the winner's watchers are registered. Adds an async test harness (testAsync/runAsyncTests) and a test that interleaves two rebuilds via a gated mock findFiles, asserting the superseded rebuild leaves exactly one live watcher and no orphan. 126 unit tests.
buildResolvedHref returned '' for a self-link with no fragment ([[current]] inside current.md), because fragmentToAnchor(null) is '' and the self-link branch returned the anchor unconditionally. The mps_wikilink renderer treats an empty href as the rejected-dangerous- scheme case and emits an inert <span>, so a note's bare link to itself went from a clickable link to a dead span - a regression from the pre-fix renderer, which emitted href="current.md". The self-link short-circuit now fires only when there's actually a fragment (scroll in place); a bare self-link falls through to the relative path (its own basename - a normal reload link). Adds the missing no-fragment self-link test that the original change's test blind-spotted. 127 unit tests.
Two hard-won facts from the review-fix pass: - Note transclusion's recursive md.render re-runs the full core-rule pipeline; source-mapping rules (mps_blank_lines, mps_block_anchors) must gate on env.mpsEmbedDepth or they inject host-colliding line numbers and duplicate block-anchor ids. Any new source-mapping rule needs the same gate. - Heading-anchor hrefs must match VS Code's GitHub slugifier (keeps Unicode letters, replaces whitespace per-character). The slugifyHeading helper reproduces it; don't simplify back to an ASCII-only class.
An unresolved pure-fragment link ([[#heading]] / [[^block]]) rendered a doubled, malformed href and the raw `#frag` as display text: `[[#Lists]]` -> href="#Lists#lists", display "#Lists". The no-resolve href branch used `(parsed.name || content)`, which falls back to the raw token content (already the fragment) and then appended fragmentToAnchor again; the display used basenameWithoutExt of the same raw content. The renderer now branches on the parsed shape: named link -> raw name + anchor; pure fragment -> the anchor alone (a same-document link); otherwise the raw content. Display for a pure fragment is the fragment's own text (heading text / block id), not the `#frag` string. Found by adding every wikilink format as a live example to example.md - the unresolved-pure-fragment path through the full renderer wasn't covered by any test. Adds tests for both heading and block pure fragments. 129 unit tests.
Replace the narrow two-row "Link format comparison" table (which mixed a plain markdown link in with wikilinks) with a tight list of every supported [[...]] / ![[...]] form - basic, heading/block fragment, alias, same-document fragment, folder-qualified, note transclusion, image embed - each with a live example that resolves against the fixtures. Folds the standalone "Image embeds" intro into the list (the block demos stay, since images must render at block size).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Obsidian-style wiki-link resolution, note transclusion, and image embeds to the markdown preview, plus the fixes from a full code-review pass.
Feature
.mdindex built on activate and maintained by per-rootFileSystemWatchers:[[name]],[[name#heading]],[[name^block]],[[name|alias]],[[#heading]](same-document), and folder-qualified[[folder/name]].![[note]]/![[note#heading]](recursive render, depth-2 cycle cap) and image embeds![[image.png]]/![[image.png|width]]with a broken-image fallback.markdownPreviewStyles.wikilinks.*settings (zero-config working defaults);extraIndexRootssupports~expansion.Click behaviour
A resolved wiki-link emits a path relative to the previewed document, so clicking routes through VS Code's
openLinkchannel (native in-preview navigation, no OS prompt) — identical to a plain[text](relative.md)link. The document path is read fromenv.currentDocument(avscode.Uriin 1.122), withenv.resourceProvider.resourceas the fallback that survives the incremental live-edit render path.Review fixes (in this branch)
mps_blank_lines,mps_block_anchors) are gated inside transclusion so embedded notes don't inject duplicate line numbers / block-anchor ids.Docs / tests
example.mdwiki-link section reworked into a tight list of every supported format, each with a live example.CLAUDE.mddocuments the hard-won VS Code preview internals (env shape, embed-depth gating, GitHub-slug parity).node test/test.js.Deferred to follow-up
embedMaxBytesboundary,vscode://fileWindows path separator, drive-lettersafeHrefrejection, andcontext.subscriptionsgrowth across rebuilds — all minor / Windows-only (this is a macOS-only sideload).