Markdown rendered-diff view#18
Merged
Merged
Conversation
First of four planned commits to integrate the markdown rendered-diff
feature explored in the spike on spike/markdown-diff-preview. This
commit is the ENGINE LIFT ONLY - the renderer is dead code in the
sense that no caller wires it in yet. Phase 2 will add the
ViewModel/View/dispatch, Phase 3 the toolbar toggle + settings +
CHANGELOG, Phase 4 the PR.
Changes:
- Add Markdig 0.37.0 PackageReference to DiffViewer.csproj. Pure
managed; no native libs; no impact on the single-file publish.
- Copy spikes/MarkdownDiffSpike/MarkdownDiffRenderer.cs verbatim to
DiffViewer/Rendering/MarkdownDiffRenderer.cs, with three small
changes per the spike's FINDINGS:
1. namespace changed from MarkdownDiffSpike to
DiffViewer.Rendering
2. Added .Freeze() to UrlChangedFg (the spike missed it; not a
runtime issue today but a production-hardening fix flagged in
the rubber-duck pass).
3. Class-level XML doc expanded to state the UI-thread-only
contract explicitly (FlowDocument is dispatcher-affine; we
inherit the contract).
- Add 12 black-box renderer tests in
DiffViewer.Tests/Rendering/MarkdownDiffRendererTests.cs covering the
spike's sample matrix:
- identical inputs produce no decorations
- happy path produces blocks + insert signal
- inline bold edit preserves bold on unchanged tokens
- inline code edit preserves monospace on unchanged tokens
- link text change renders as Hyperlink
- link URL-only change tints orange with both-URLs tooltip
- nested list inner-item edit diffs at item granularity
- table cell change falls back to plain-text (no WPF Table)
- heavy rewrite with no shared words falls back to two
full-block tints (similarity gate kicks in)
- small list-item replace stays coalesced via short-block
exemption
- reordered sections render as separate delete + insert
(caveat 3a, pinned as intentional)
All tests use [StaFact] from Xunit.StaFact because FlowDocument and
its descendants are dispatcher-affine.
Definition of done verified:
- dotnet build -c Release: clean, 0 new warnings
- dotnet test: 1459 passed (1447 baseline + 12 new), 0 failed
- dotnet publish DiffViewer\DiffViewer.csproj -c Release -o publish:
single-file exe (144 MB, similar to prior baseline) launches and
loads successfully
- .editorconfig honored, no new suppressions
- four load-bearing libraries (CommunityToolkit.Mvvm, AvalonEdit,
DiffPlex, LibGit2Sharp) versions unchanged
AI-Local-Session: 461e0a61-a892-44b3-897d-e4d2d37f1142
AI-Cloud-Session: 7ea21bb1-fbcf-4d33-a5f9-438a3a76b475
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Second of four planned commits. Phase 1 lifted the renderer; this commit makes it visible: extension-based dispatch in DiffPaneViewModel populates a sibling MarkdownDiffViewModel on .md/.markdown files, a new MarkdownDiffView FlowDocumentScrollViewer host shows the rendered diff in lieu of the source editors, and the visibility cascade is refactored to a single RaiseDisplaySurfaceChanged() helper per the rubber-duck recommendation. DiffPaneViewModel changes: - New ObservableProperties: IsMarkdownFile (extension flag), MarkdownDiff (sibling VM), RenderMarkdownRendered (user toggle, defaults true). - New derived properties: ShowMarkdownRenderedToggle (file is markdown AND rendered VM is built) and ShowMarkdownRendered (toggle gate AND user prefers rendered AND not behind a higher-priority surface). - ShowEditors now also gated on !ShowMarkdownRendered so the source editors and the rendered view stay mutually exclusive on the same canvas. - New RaiseDisplaySurfaceChanged() helper that fires OnPropertyChanged for every display-surface visibility property in one call. ALL existing partial On-Changed methods for display-affecting properties (OnImageDiffChanged, OnIsSvgFileChanged, OnIsSvgRenderableChanged, OnRenderSvgImageChanged) refactored to call it. New markdown partial methods do the same. - LoadAsync detects .md/.markdown extension after the SVG and image-dispatch gates fail. Sets IsMarkdownFile in the synchronous pre-Task.Run block; clears MarkdownDiff to drop any stale prior load. After the ContinueWith re-marshals to the UI thread (where FlowDocument construction must happen because it's dispatcher-affine), builds new MarkdownDiffViewModel(left, right). try/catch around the renderer so a Markdig throw falls back gracefully to source-only mode rather than failing the whole load. - Markdown state cleared in every early-return path that already clears ImageDiff: null entry, SVG dispatch entry, image dispatch entry, placeholder. - IsMarkdownPath helper: case-insensitive .md / .markdown extension check. - TryNavigateNextHunkInFile / TryNavigatePreviousHunkInFile / JumpToFirstHunk / JumpToLastHunk now early-return when ShowMarkdownRendered. The AvalonEdit editors and hunk-overview bar are hidden in rendered mode, so any caret move would scroll a surface the user can't see. Returning false from TryNavigate* makes the cross-file orchestrator advance to the next file (matching the "no more hunks in this file" behavior). New types: - ViewModels/MarkdownDiffViewModel.cs: sealed partial : ObservableObject wrapping a FlowDocument. Constructor takes (leftText, rightText), immediately calls MarkdownDiffRenderer.Render. Construct on the UI thread. - Views/MarkdownDiffView.xaml + .cs: thin FlowDocumentScrollViewer bound to Document. Pure XAML wiring, no code-behind logic. - DiffPaneView.xaml: MarkdownDiffView sibling to ImageDiffView with visibility binding to ShowMarkdownRendered. Tests added: - DiffViewer.Tests/ViewModels/MarkdownDiffViewModelTests.cs: 4 tests covering construction with non-trivial / identical / empty / null inputs. - DiffViewer.Tests/ViewModels/DiffPaneViewModelTests.cs: 12 new tests covering: extension detection (.md, .markdown, case-insensitive), non-markdown leaves state clear, rendered toggle flips view without re-reading blobs, navigating to non-markdown after markdown clears the state, null entry clears state, hunk nav early-returns in rendered mode, binary file clears state, toggle state independence from file-kind flag, the visibility cascade fires the right OnPropertyChangeds, and SVG dispatch doesn't leak markdown flags. Definition of done verified: - dotnet build -c Release: clean, 0 new warnings - dotnet test: 1475 passed (1459 from Phase 1 + 16 new), 0 failed - dotnet publish -c Release: single-file exe 144 MB, launches and responds - .editorconfig honored, no new suppressions - Markdig dep version unchanged AI-Local-Session: 461e0a61-a892-44b3-897d-e4d2d37f1142 AI-Cloud-Session: 7ea21bb1-fbcf-4d33-a5f9-438a3a76b475 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…+ CHANGELOG Third of four planned commits. Phase 1 lifted the renderer; Phase 2 wired the VM/View/dispatch with the rendered surface visible. Phase 3 makes the user's preference persistent and adds the user-facing toolbar control + release notes. Settings: - AppSettings.cs: bump CurrentSchemaVersion 8 -> 9. New PreferMarkdownRendered bool with default true. XML doc explains the parallel to RenderSvgImage (added in v6). - SettingsMigrations.cs: register MigrateV8ToV9 as a no-op. The new field has a safe default so pre-v9 files load with the rendered view enabled by default — matching how v5->v6 added RenderSvgImage. - SettingsJsonSerializer.cs: add preferMarkdownRendered to both Serialize and Deserialize. Serializer uses defaults.PreferMarkdownRendered for the missing-field fallback so a v9 file with the field stripped still round-trips. - SettingsServiceTests.cs: 2 new tests covering the v8->v9 migration (field defaults to true; other fields preserved) and the round-trip (save false, reload, observe false). DiffPaneViewModel wiring: - Seed RenderMarkdownRendered from settings.PreferMarkdownRendered in the constructor's settings-seed block (mirrors RenderSvgImage). - Push external settings changes back into the VM in OnSettingsChanged (mirrors RenderSvgImage's diff-and-push pattern). - Add PreferMarkdownRendered = RenderMarkdownRendered to PersistToolbarToSettings so VM toggles round-trip to disk. - 2 new tests: RenderMarkdownRendered_TogglePersistsToSettings and RenderMarkdownRendered_ExternalSettingsChange_PushesIntoViewModel (mirror the existing SVG persistence tests). Toolbar: - DiffToolbarView.xaml: new "Rendered" ToggleButton in the leftmost StackPanel, visible only when ShowMarkdownRenderedToggle (a .md/ .markdown file with a built rendered VM). IsChecked two-way to RenderMarkdownRendered. Sibling of the SVG Rendered toggle; same StackPanel pattern with the trailing thin Separator. CHANGELOG: - New "Markdown rendered-diff view" bullet at the top of [Unreleased] / ### Added. Describes the feature, the toggle, the default-on behavior, the inline-formatting preservation, the URL-change orange tint, the per-item nested list diff, the similarity-gated coalesce, the table fallback, the persistence, and the F7/F8 hunk-nav skip behavior. Definition of done verified: - dotnet build -c Release: clean, 0 new warnings - dotnet test: 1479 passed (1475 from Phase 2 + 4 new persistence/ migration), 0 failed - dotnet publish -c Release: single-file exe 144 MB, launches and responds - .editorconfig honored, no new suppressions - four load-bearing libraries' versions unchanged (Markdig version also unchanged from Phase 1) - CHANGELOG.md [Unreleased] entry present per AGENTS.md sec 12 Phase 4 (next): open PR for review. AI-Local-Session: 461e0a61-a892-44b3-897d-e4d2d37f1142 AI-Cloud-Session: 7ea21bb1-fbcf-4d33-a5f9-438a3a76b475 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolves one conflict in CHANGELOG.md: master shipped v1.7.0 (promoting [Unreleased] to [1.7.0] - 2026-06-05 with the diffviewer PATH-registrar entry), so the auto-merge placed my markdown rendered- diff bullet under [1.7.0]. The markdown feature has not shipped, so moved that bullet into a fresh [Unreleased] section at the top per Keep a Changelog conventions. The v1.7.0 entries (PATH registrar, running-version in Settings, dev-build versioning, fresh-install empty-state shell) stay under [1.7.0] - 2026-06-05 unchanged. No code conflicts. The PATH-registrar feature added new files only (no schema changes to AppSettings / SettingsMigrations / SettingsJsonSerializer) so the markdown integration's v8->v9 schema bump applies cleanly on top. Definition of done verified post-merge: - dotnet restore -r win-x64: clean - dotnet build -c Release: clean, 0 new warnings - dotnet test: 1520 passed (1479 from feature branch + 41 new from master's PATH-registrar work), 0 failed AI-Local-Session: 461e0a61-a892-44b3-897d-e4d2d37f1142 AI-Cloud-Session: 7ea21bb1-fbcf-4d33-a5f9-438a3a76b475 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new markdown rendered-diff “display surface” to DiffViewer. For .md / .markdown files, the diff pane can switch between the existing source-text diff (AvalonEdit) and a rendered FlowDocument view that embeds diff decorations into the rendered markdown, with a persisted user preference.
Changes:
- Introduces
MarkdownDiffRenderer(Markdig-based) to produce a diff-decoratedFlowDocument, plus a siblingMarkdownDiffViewModelandMarkdownDiffView. - Integrates the rendered surface into
DiffPaneViewModel(visibility cascade, load dispatch, hunk-nav gating) and adds a toolbar toggle. - Persists the preference via settings schema v9 (
preferMarkdownRendered) with serialization and migration coverage, plus new renderer and VM tests.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| DiffViewer/Views/MarkdownDiffView.xaml.cs | Adds new rendered markdown diff view code-behind (currently minimal). |
| DiffViewer/Views/MarkdownDiffView.xaml | Hosts a FlowDocumentScrollViewer bound to the rendered document. |
| DiffViewer/Views/DiffToolbarView.xaml | Adds the markdown “Rendered” toggle, visible only when applicable. |
| DiffViewer/Views/DiffPaneView.xaml | Adds the MarkdownDiffView surface to the diff pane. |
| DiffViewer/ViewModels/MarkdownDiffViewModel.cs | New VM that builds/holds the rendered FlowDocument. |
| DiffViewer/ViewModels/DiffPaneViewModel.cs | Integrates markdown rendered mode into load flow, visibility cascade, settings, and hunk navigation gating. |
| DiffViewer/Services/SettingsMigrations.cs | Adds v8→v9 migration step (no-op, defaultable). |
| DiffViewer/Services/SettingsJsonSerializer.cs | Serializes/deserializes preferMarkdownRendered. |
| DiffViewer/Rendering/MarkdownDiffRenderer.cs | New Markdig-based renderer that builds a diff-decorated FlowDocument. |
| DiffViewer/Models/AppSettings.cs | Bumps schema to v9 and adds PreferMarkdownRendered. |
| DiffViewer/DiffViewer.csproj | Adds Markdig dependency. |
| DiffViewer.Tests/ViewModels/MarkdownDiffViewModelTests.cs | New STA tests for VM construction and degenerate inputs. |
| DiffViewer.Tests/ViewModels/DiffPaneViewModelTests.cs | New tests for markdown dispatch, visibility gates, navigation gating, and settings propagation. |
| DiffViewer.Tests/Services/SettingsServiceTests.cs | Adds migration + round-trip tests for the new settings field. |
| DiffViewer.Tests/Rendering/MarkdownDiffRendererTests.cs | Adds black-box STA tests covering renderer behavior and caveats. |
| CHANGELOG.md | Documents the new markdown rendered-diff feature under [Unreleased]. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
copilot-pull-request-reviewer flagged that MarkdownDiffRenderer emits
Hyperlink elements with NavigateUri set, but WPF's
FlowDocumentScrollViewer (unlike NavigationWindow / Frame) doesn't
auto-handle the RequestNavigate routed event. Without a handler, link
clicks raise the event but do nothing - links look interactive
(cursor changes, underlined) and silently fail. Verified by checking
the WPF docs and the existing repo pattern in
BrowserNotifyUpdateService.OpenUrlInDefaultBrowser.
Fix: add a UserControl-level RequestNavigate handler in
MarkdownDiffView.xaml.cs ctor. Routes the click through
Process.Start(new ProcessStartInfo { FileName = uri, UseShellExecute = true })
- the OS default-browser shim - matching the existing
BrowserNotifyUpdateService and SettingsViewModel patterns.
Wrapped in try/catch (best-effort: a malformed URI or shell failure
swallows quietly rather than crashing the diff pane). e.Handled = true
stops further bubbling.
Particularly important for sample 04 (link text change) and sample 05
(URL-only change, where the orange tint + both-URLs tooltip is much
less useful if the user can't click through to the new URL).
No test per AGENTS.md sec 10 carve-out: view code-behind doing event-
handler delegation is exempt. The handler is a thin shim around
Process.Start that mirrors existing tested-by-usage patterns.
Definition of done verified:
- dotnet build -c Release: clean, 0 new warnings
- dotnet test: 1520 passed, 0 failed (unchanged from pre-fix)
AI-Local-Session: 461e0a61-a892-44b3-897d-e4d2d37f1142
AI-Cloud-Session: 7ea21bb1-fbcf-4d33-a5f9-438a3a76b475
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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 a markdown rendered-diff view: when viewing
.md/.markdownfiles, a "Rendered" toolbar toggle switches between the source-text diff (the existing Side-by-side / Inline editors) and a renderedFlowDocumentview that shows the diff inside the rendered markdown.This feature was designed via a feasibility spike on branch
spike/markdown-diff-preview(4 commits, lateste4373e9, not merged). The spike's full write-up lives atspikes/MarkdownDiffSpike/FINDINGS.mdon that branch. This PR is the production integration of the spike's verdict.What you see
.md/.markdownfiles. Defaults to rendered view (mirrors the SVGRenderedtoggle pattern). User preference persists across launches via the newpreferMarkdownRenderedsetting.Out of scope (deliberate)
Tablerendering of markdown tablesAGENTS.md§12Phasing
Each phase is one commit. Reviewers can read them in order:
a5612dePhase 1 — LiftMarkdownDiffRendererfrom the spike intoDiffViewer/Rendering/; add Markdig dep; 12 black-box renderer tests using[StaFact]. Engine is dead code at this point.8af44d1Phase 2 —MarkdownDiffViewModel+MarkdownDiffView+DiffPaneViewModeldispatch + visibility cascade refactor (RaiseDisplaySurfaceChanged()helper consolidates the per-handlerOnPropertyChangedfan-out) + hunk-nav gating. 16 new tests.ab49b86Phase 3 — ToolbarRenderedtoggle button +AppSettings.PreferMarkdownRendered+v8→v9migration + serializer round-trip + CHANGELOG[Unreleased]entry. 4 new tests.Definition of done
dotnet build -c Release: clean, zero new warningsdotnet test: 1479 passed (1447 baseline + 32 new), 0 faileddotnet publish DiffViewer\DiffViewer.csproj -c Release -o publish: single-file exe (144 MB) launches and responds.editorconfighonored; no new diagnostic suppressionsCHANGELOG.md[Unreleased]updated per AGENTS.md §12Design decisions captured during planning
I asked five clarifying questions before writing code; answers shaped the plan:
.mdfiles?The plan was then rubber-duck-reviewed before implementation. Three blocking findings were adopted: threading is made explicit (
Renderis UI-thread-only, dispatched in theContinueWithof the existing text load); tests use[StaFact]and are black-box on the producedFlowDocument; the visibility cascade gets a smallRaiseDisplaySurfaceChanged()helper instead of duplicating the sameOnPropertyChangedlist in every partial handler. Settings migration is handled in all four touchpoints (AppSettings,SettingsJsonSerializer.Serialize/Deserialize,SettingsMigrations+ round-trip tests).Session
461e0a61-a892-44b3-897d-e4d2d37f11427ea21bb1-fbcf-4d33-a5f9-438a3a76b475