Skip to content

feat(editor): precise diagnostic→node mapping (v0.7) - #1

Merged
elvogel merged 61 commits into
mainfrom
feature/xdm-source-positions-consume
May 9, 2026
Merged

elvogel merged 61 commits into
mainfrom
feature/xdm-source-positions-consume

Conversation

@elvogel

@elvogel elvogel commented May 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Plan 13 Phase B — consumes PhoenixmlDb.Core 1.1.0's new SourceLine / SourceColumn fields and rewires the schema-validation pipeline for exact diagnostic→node mapping.

  • Bumps PhoenixmlDb.Core PackageReference to 1.1.0.
  • Types IDiagnostic.Location as DiagnosticLocation(NodeId, Line, Column) (was object?).
  • Replaces XmlSchemaValidator's serialize-and-reparse implementation with a push-based walk over XdmDocument, driving System.Xml.Schema.XmlSchemaValidator directly. The current XdmNode is in scope when each call returns; diagnostics attach the exact NodeId without any line/col arithmetic.
  • EditorMainWindow.problems.DiagnosticActivated uses EnsureVisibleAndSelect (new) — DFS-walks the provider tree, expands every ancestor on the path, rebuilds, then selects. Works from a fully-collapsed tree.
  • DiagnosticIndex (NodeId → severity) populated from the validation service. XmlTreeNode.IconKey returns diag.error / diag.warning. NodeListView row template renders ✕ red / ⚠ yellow glyphs in front of offending rows.

Why

v0.6's click-to-nav was best-effort: regex-extract a quoted element name from the validator message, find the first matching XdmElement. False positives on duplicates. v0.7's first attempt at precision (commit b9c05ca) used FindNodeNearLine against parse-time SourceLine / SourceColumn — but the validator round-trips through serialization, so its line/col was in the serialized output, not the source. Even on a freshly-loaded document, multi-line attribute formatting in the source vs. single-line in the serialized output meant lines didn't align. Glyphs landed on the wrong rows.

Push-based validation eliminates the round-trip and the line/col mapping problem in one move.

Smoke test (2026-05-09)

Loaded library.xml (two-book <library>, second book missing @isbn and with <year>not-a-year</year>):

  • Glyphs land on the second <book> row and the second <year> row. First book is clean.
  • Diagnostic line/col match the original file exactly (line 9 col 4, line 12 col 6).
  • Double-click in ProblemsView selects the exact node, even from a fully-collapsed tree.
  • Validation re-runs on edits; glyphs and ProblemsView update consistently.

Test plan

Commits

  • fa96d68 chore: bump PhoenixmlDb.Core to 1.1.0
  • cee4af5 feat(diagnostics): IDiagnostic.Location carries a NodeId reference
  • b9c05ca feat(validation): map diagnostics to XdmNode via parse-time positions (superseded by 122889e)
  • a8f5197 feat(editor): precise click-to-navigate using DiagnosticLocation
  • 5c30183 feat(editor): inline error overlay on offending tree rows
  • 11733fd fix(editor): expand ancestors when navigating from a diagnostic
  • 122889e fix(validation): push-based validator over XdmDocument walk
  • (this PR) docs(editor): v0.7 walkthrough + cross-link from v0.6

🤖 Generated with Claude Code

elvogel and others added 30 commits May 5, 2026 22:04
Avalonia desktop exe; pulls in Avalonia.Desktop only at the executable
seam so the library packages stay clear of Tmds.DBus.Protocol's
transitive vulnerability. References Phoenixml.Platform + Editor.Xml
as ProjectReferences for in-repo dev.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…views

v0.1 layout: tree on the left (~360 dip), text on the right; classic
XmlNotepad ergonomic. EditorState holds the active XmlDocument and
fires ActiveDocumentChanged so views update in lockstep. No File
menu yet (Task 4); window opens empty until Task 4 wires Open.

Adds InternalsVisibleTo to Editor.Xml so Editor can call the
internal SerializeToString helper.

MainWindow is sealed so EditorMainWindow uses composition: wraps
MainWindow and exposes it as .Window. App.axaml.cs assigns that
to desktop.MainWindow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Procedural commands wired directly to menu click handlers.
CommandRegistry-driven menu generation deferred to v0.2 once
we have a command palette to consume the registry too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Round of fixes after the first manual smoke test surfaced real
issues. Each:

- ConfigureAwait(false) in commands → 'Call from invalid thread'
  crash on Open. Stripped across all commands; CA2007 suppressed
  project-wide for the editor with a comment explaining the UI-app
  context where awaits MUST resume on the captured Avalonia
  SynchronizationContext.
- Theme tokens never applied → DynamicResource lookups returning
  null. App.OnFrameworkInitializationCompleted now applies
  FluentDarkTheme.Default before any window opens.
- Color vs IBrush mismatch → Background bindings can't auto-convert
  Color resources. Themes now store ImmutableSolidColorBrush
  instances under the same Phoenixml.Color.* keys.
- Theme.Apply over-narrowly cast to StyledElement → Application
  isn't one in Avalonia 11. Now accepts either Application or
  StyledElement.
- Status bar segment never updated when ActiveDocument changed →
  stayed '(no document)' forever. UpdateStatusBar helper invoked
  from OnActiveDocumentChanged.
- Two title bars stacked on Linux (OS + custom) →
  SystemDecorations='Full' was keeping OS chrome.
  SystemDecorations='BorderOnly' + ExtendClientAreaChromeHints='NoChrome'.
- Document tree label was full file:// URI causing horizontal
  overflow → XmlTreeNode.DocumentLabel extracts filename.
- Right pane was single-line utf-16-declared XML on Open →
  underlying XmlSerializer produces unindented output. v0.1
  sidesteps by displaying source file's raw text. Real v0.2 work
  item: proper XmlWriterSettings in PhoenixmlDb.Core's serializer.
- Splitter invisible → bumped to 6 dip, bound Background to
  Phoenixml.Color.Border, added SizeWestEast cursor.
- Tree expansion needed multiple precise clicks → IsExpanded
  property added to TreeNodeViewModel; root + first level
  auto-expanded; bound via TreeView.IsExpanded Style.

Walkthrough doc at docs/editor-v0.1-walkthrough.md captures
what works, fixes, and the v0.2 backlog.
Plan 7 wraps:

- Directory.Packages.props: PhoenixmlDb.Core 1.0.24 → 1.0.30, picking
  up the Utf8StringWriter + indent-default-true fix from the
  feature/serializer-utf8-indent branch in phoenixmldb-core.

- EditorMainWindow.OnActiveDocumentChanged: removed the source-file
  ReadAllText workaround. Text view now shows doc.SerializeToString()
  directly — the serializer output is finally readable indented UTF-8.

- docs/editor-v0.1-walkthrough.md: marked the serializer fix resolved;
  promoted the 'edit commands wired to undo' item to v0.2 #1.

- NuGet.config: temporary local feed pointing at the freshly-packed
  1.0.30 nupkg while NuGet's CDN finishes propagating. Remove this
  file once 'curl https://api.nuget.org/v3-flatcontainer/phoenixmldb.core/index.json'
  shows 1.0.30.

Validated: 166 tests pass, editor opens + Save round-trips clean
indented UTF-8 XML.
XmlDocument.ReplaceNode: swaps a node in the resolver dictionary under
the same NodeId. XdmNode types are immutable (init properties); edits
happen by constructing a new node and swapping via this method. Parent
Children lists (which reference by id) remain valid.

RenameElementCommand: first concrete IEditorCommand. Reconstructs the
XdmElement with a new LocalName, swaps via ReplaceNode. NodeId preserved;
attributes, children, namespace, prefix, parent untouched.
TryMergeWith collapses consecutive renames on the same element into one
undo step, preserving the ORIGINAL pre-rename snapshot as revert target.

Note: TryMergeWith parameter semantics — UndoStack calls stackTop.TryMergeWith(newCmd),
so this=already-applied (has _previous), parameter=new cmd. Fixed vs. plan sketch.

7 tests: Apply, Revert, KeepsChildren, KeepsId, Merge, NoMerge, Serialize.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Provider's Changed event was declared in Plan 5 but never raised (CS0067
pragma suppressed the warning). Now it subscribes to _document.Changed
in the constructor and re-fires on every edit, so TreeEditorView rebuilds
in lockstep with commands.

Editor's OnActiveDocumentChanged subscribes to the active doc's Changed
event so the text view re-serializes after each edit; tracks
_subscribedDocument to unsubscribe when the active document changes.
Naive full re-render; incremental refresh is a Plan 10+ optimization.

+1 test: ChangedEventFiresWhenRenameCommandExecutesViaUndoStack.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UndoCommand and RedoCommand call ActiveDocument.Undo.Undo/Redo.
EditMenu adds Undo + Redo menu items with InputGesture display.

KeyBindings on the window so shortcuts fire window-wide regardless
of focus. RelayCommand helper for ICommand-bound keybindings.

FileMenu.Build refactored to return a MenuItem (was Menu); the
top-level Menu is now constructed in EditorMainWindow with both
File and Edit items.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows the selected element's LocalName in an editable TextBox.
Enter commits a RenameElementCommand through the document's
UndoStack; Escape reverts the textbox; LostFocus auto-commits.

Layout: tree on top (star), 1-dip separator, pane on bottom (140 dip).
First concrete UI surface for editing. Plan 9 adds attribute + text +
insert/delete UI on the same pattern.
…ound

User feedback from the v0.2 rename demo:
- Undo didn't refresh PropertiesPane textbox (the pane didn't subscribe
  to Document.Changed)
- F2 inline rename preferred over side pane
- Element labels in tree had redundant angle brackets
- Attribute rename wasn't supported
- Right pane was an editable TextBox that didn't flow back to XDM

Changes:
- TreeEditorView gains F2 inline rename. Selected row toggles to edit
  mode showing a TextBox (Enter commits, Escape cancels, LostFocus
  auto-commits). Raises NodeRenameRequested(ITreeNode, string newLabel).
  TreeNodeViewModel now implements INotifyPropertyChanged.
- New RenameAttributeCommand mirrors RenameElementCommand for
  XdmAttribute. Same merge semantics.
- Editor wires NodeRenameRequested -> routes to RenameElementCommand
  or RenameAttributeCommand based on node kind.
- XmlTreeNode element labels drop the <> brackets -- show just
  'catalog', not '<catalog>'. Attributes keep '@id' as the
  disambiguator.
- TextEditorView set read-only in editor. Text->XDM sync (typing in
  the right pane and having it parse back) is Plan 10+ work; until
  then, the right pane displays the live serialized XML but doesn't
  accept typing.
- PropertiesPane removed entirely. F2 gesture replaces it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
User feedback: 'I'd like the UI flow to follow more of what we have
going on with XmlNotepad. We have a treeview on the left, but on the
right, we have the list of values contained in the elements/attributes.
It really does make for a nicer visual flow of what's going on.'

Replaces the TreeEditorView+TextEditorView+SplitView arrangement with
a single new NodeListView control that renders nodes as a flat list,
one row per visible (expanded) node, with two columns: name (with
depth indent + chevron + label) on the left, value on the right. All
rows share fixed column widths so values align vertically — the
canonical XmlNotepad layout.

ITreeNode gains a 'Value' property. XmlTreeNode populates it from the
underlying XdmNode: attribute values, text-only-element text, comment
text, PI value. Container elements (those with element children)
return empty string in Value. Node children are resolved through
XmlDocument.Resolve(NodeId) because XdmElement.Children is a list
of NodeId references, not XdmNode instances.

F2 inline rename works the same as before (now in the new control).
NodeRenameRequested event has the same shape; editor's existing
RenameElementCommand / RenameAttributeCommand wiring is unchanged.

TextEditorView and TreeEditorView remain in the foundation for other
consumers; the editor app just doesn't use them directly anymore.
SplitView also unused by the editor for now (future tabs/secondary
panes may pick it back up).

4 new Value tests added to XmlTreeNodeProviderTests (attribute value,
text-only element content, container element empty, document node
empty). All 183 tests pass (was 179).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
User feedback after first launch of the XmlNotepad-style view:
1. CLI file arg wasn't actually wired (App.axaml.cs read desktop.Args
   but the path was never passed in)
2. Auto-expand only went 2 levels — every visible row was a container
   with an empty value column, so the demo looked uninformative
3. No visible separator between name and value columns
4. Text-only elements (e.g. <name>Laptop Pro</name>) duplicated
   their text: once as the parent's Value, once as a child text-node
   row labeled "Laptop Pro" with the same value

This commit:

- App.axaml.cs: pass desktop.Args[0] to editor.OpenFileOnLaunch when
  it points at an existing file. Silent skip otherwise (no scary
  startup dialog if the path's wrong).
- NodeListView: auto-expand 3 levels via ExpandRecursively helper
  (was 2). For the catalog demo this surfaces product attributes +
  text-content elements on first open instead of bottoming out at
  containers.
- NodeListView XAML: insert a 1-dip Border column themed with
  Phoenixml.Color.Border between the name and value columns so the
  right side reads as a defined column, not a black void.
- XmlTreeNode.HasChildren: text-only elements (no attributes, no
  element children) now report false. Their text is in the Value
  column; no expand chevron, no child rows.
- XmlTreeNodeProvider.GetElementChildren: when no element children
  exist, also skip text-node children entirely. Mixed content (text
  interleaved with elements) keeps the text rows so document order
  is preserved.

Visual result (catalog fixture): leaf rows like <name>, <price>,
<rating>, <tag> now show as a single line with name on the left and
value on the right — no duplicate "Laptop Pro" rows underneath.
v0.2 walkthrough captures the XmlNotepad-style dual-pane state
after Plan 8 + the UX iterations: how to run (including CLI file
arg), what works, the 16 bugs caught and fixed during v0.2's
real-use feedback loop, architectural validations, and the v0.3
priority list (value editing first).
Mirror of RenameAttributeCommand but mutates Value instead of
LocalName. Same TryMergeWith semantics — consecutive sets on the
same attribute collapse to one undo step that reverts to the
pre-FIRST-edit value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the existing XdmText child's Value. Element itself is
unchanged (its Children list still references the same NodeId).

Throws on empty elements (would need to allocate a new NodeId
and update the parent's Children list — Plan 10) and on mixed
content / container elements (no single text child to update).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Click in a value cell → row enters value-editing mode (TextBox
shown, TextBlock hidden). Enter commits, Escape cancels, click-away
auto-commits. Raises NodeValueChangeRequested(ITreeNode, string)
event for the host to route to a vocabulary command.

Mirrors the F2 name-column edit pattern from Plan 8 with a
different gesture (click vs F2) so the two are independently
discoverable.
Routes by XDM node kind:
- XdmAttribute → SetAttributeValueCommand
- XdmElement with text-only content → SetTextContentCommand
- Anything else (mixed / container / empty) → silently ignored
  (no command issued; the row rebuilds with its prior value)
Document.Changed can fire on a worker thread — XmlDocument.SaveAsAsync
resumes off the UI thread after file I/O, then calls MarkClean which
fires Changed. The provider re-raised it directly to NodeListView.
Rebuild touches FindControl / ListBox.ItemsSource which require the
UI thread → 'Call from invalid thread' crash on Save As.

Marshal via Dispatcher.UIThread.Post when not already on the UI
thread. Pre-existing v0.2 bug surfaced when smoke-testing v0.3
value editing.
Counter-based allocator on XmlDocument; lazy-initializes to
max(parsed-id) + 1 on first call. AddNode adds a fresh node to
_byId — for swap, ReplaceNode is still the right primitive.
Foundation for Insert commands and lifting the empty-element
restriction in SetTextContentCommand.
Adds the foundation for Compact-on-Save: each command declares
the orphan NodeIds it needs to keep alive for undo/redo. Most
commands return empty (their _previous snapshots reference live
nodes); only deletion commands will populate this.

UndoStack exposes its undo and redo stacks as read-only
enumerations so the compactor can walk them.
Allocates a new NodeId, builds an empty XdmElement, inserts it
into the parent's Children list at the specified index (or
appends if null). Revert restores the parent without the inserted
child; the orphaned node stays in _byId. Idempotent across
redo: same _insertedId reused on re-Apply.
ReferencedNodeIds reports the inserted id so Compact preserves
it across Revert + redo state.
Allocates a new NodeId, builds an XdmAttribute with the given
local name and value, appends to the parent's Attributes list.
Same idempotency + ReferencedNodeIds semantics as
InsertElementCommand.
Removes a node from its parent's Children/Attributes list. The
node and its descendants stay in _byId so Revert is a single
ReplaceNode of the parent (no rebuild). ReferencedNodeIds
returns the entire deleted subtree so Compact-on-Save preserves
it across saves while this command remains on the undo stack.
Throws on document-root delete.
…mand

Now allocates a new XdmText node for elements with no children.
Container/mixed-content elements still reject (value column edits
on those would be the wrong UX). Removes the
ApplyOnEmptyElementThrows test from Plan 9 — behavior changed.

TryMergeWith only merges within the ReplacedExistingText path;
CreatedNewText edits get separate undo entries. Acceptable trade-off
since first-time-text-in-empty-element is rare.
Mark-and-sweep eviction of orphan NodeIds in _byId at Save time.
Mark phase: BFS from root + every command's ReferencedNodeIds
across undo + redo stacks. Sweep: remove unmarked entries.

Preserves undo across save without unbounded session growth.
Trade-off: orphans pinned by undo/redo entries stay until those
commands age out of the stacks.
Right-click on a row → Insert Child Element / Insert Attribute /
Delete. Delete key on the selected row also fires the delete
event. The host wires events to vocabulary commands; menu items
are always shown (host decides whether to act based on node kind).
NodeInsertChildElementRequested → InsertElementCommand on the
selected element, default name 'newElement'. Same for
InsertAttribute. Delete routes to DeleteNodeCommand; document
node and root-element-with-throw both silent-ignore.

Also drops the 'el.Children.Count == 0 ⇒ reject' guard from the
value-edit predicate now that Plan 10's SetTextContentCommand
handles the empty case.
Material.Icons.Avalonia for cross-platform Avalonia glyphs.
Toolbar sits between menu and content; buttons share the same
Action<EditorMainWindow> handlers as the keyboard shortcuts so
behavior stays consistent across menu, toolbar, and shortcut.
XmlNotepad-style address bar below the toolbar. Shows the active
document's file path; Enter opens the typed path; Escape reverts.
Reuses OpenFileOnLaunch's silent-skip semantics for invalid paths.
elvogel and others added 28 commits May 7, 2026 13:25
Wraps an XmlDocument with the title rendered on its tab header.
Title appends '*' when the document has unsaved changes; updates
fire PropertyChanged via Document.Changed so the tab strip
re-renders without manual refresh.
Refactors single-document state into a tabs list with one active.
ActiveDocument property is preserved as a forwarding accessor so
existing callers (Save, Undo, address bar, status bar) compile
unchanged. Setter routes to OpenDocument which deduplicates by
document reference.

CloseTab activates the right neighbor (or left if no right).

New test project Phoenixml.Editor.Tests added to solution with 8
passing tests covering OpenDocument, CloseTab, ActiveDocument
backwards-compat setter, and ActiveDocumentChanged event.
Avalonia TabStrip bound to EditorState.Tabs. Tab header shows
DocumentTab.Title + a close button. Selection drives ActiveTab;
close button calls CloseTab. Tab strip docks below the address bar
so toolbar buttons stay visible regardless of tab count.

TabStrip lives in Avalonia.Controls.Primitives (not Controls).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Explicit per-tab opening; the file-picker no longer silently
replaces the previously-open document. Two opens of the same
file path produce one tab (deduplication by XmlDocument
reference).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JSON-backed editor settings with a single field for v0.5:
RecentFiles. Store dedupes on record, caps at 10, persists in
the background via SettingsService.SaveAsync. Load tolerates
missing or corrupt files (starts fresh).
Path is injectable for tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lists EditorSettings.RecentFiles, most recent first. Click an
entry to open. Rebuilds on settings.Changed so newly opened
files surface immediately. Empty state is rendered as a
disabled '(no recent files)' item rather than an empty submenu.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EditorSettingsStore.RecordRecentFile is called from the file
open path (CLI arg, file picker) and from Save As (so a 'save
to new location' shows up in Recent Files immediately).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pivot in Plan 11: Avalonia's TabStrip + ItemsControl rendering
of multiple tabs proved unreliable in our DockPanel layout
(state was correct, count went 0→1→2 per diagnostics, but only
the active tab rendered). User redirected: a left explorer
column is more useful for folder-scoped XML editing anyway.

What's added:
- ExplorerNode: lazy-loading folder/file model. Filters to
  *.xml, *.xsd, *.xsl, *.xslt, *.sch and shows only folders
  containing XML content (recursive).
- EditorExplorer: TreeView-based left column with 'Open Folder…'
  button + auto-root from active document's folder. Double-click
  a file → routes through OpenFileOnLaunch (silent skip on bad
  paths, same as address bar).

What's kept:
- EditorState tabs collection. Tests still pass; future tab
  strip can re-render against this without rework.

What's removed:
- EditorTabStrip.cs (deleted; no slop)
- Diagnostic Console.WriteLine in EditorState.OpenDocument

Layout: menu/toolbar/addressBar top; explorer + 22-dip collapse
toggle docked left; nodeList fills the rest. Toggle hides the
explorer's TreeView while keeping the toggle button visible.
Two UX fixes from explorer first-use feedback:

1. Double-clicking a folder collapsed the tree because Avalonia's
   default TreeViewItem behavior toggles expansion on row
   double-click, and our handler only set e.Handled for files.
   Now handled for both — folders skip the open path; the
   chevron remains the (correct) expand gesture.

2. Address bar previously only accepted file paths. Now also
   accepts directories — typing a folder + Enter retargets the
   explorer's root (via the new internal EditorExplorer.SetRoot
   exposed through EditorMainWindow.Explorer).
Previously every active-document change triggered SetRoot to the
file's parent. Double-clicking a file from a deep folder in the
explorer collapsed the user's chosen root down to that file's
folder, losing the broader project view.

Now: re-root only when the new file's folder is NOT under the
current root. Same-tree opens stay put; cross-tree opens still
follow.
Resolves XmlSchemaSet for a document. Per-document override in
EditorSettings.PerDocumentSchema wins; otherwise reads xsi:
schemaLocation / noNamespaceSchemaLocation hints from the
document root. Relative paths resolve against the document's
source directory. Returns null on missing/corrupt XSDs (validation
is then skipped silently).

Adds PerDocumentSchema and ExplorerWidth to EditorSettings —
ExplorerWidth used by Plan 12's GridSplitter task.

Implementation note: XdmAttribute.Namespace is a NamespaceId
(not a string), so xsi: comparison uses NamespaceId.Xsi (the
pre-seeded intern for http://www.w3.org/2001/XMLSchema-instance).
TryLoad catches XmlException in addition to XmlSchemaException to
handle corrupt (non-XML) XSD files gracefully.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Background validation: subscribes to active document's
Document.Changed, debounces 500ms, runs XmlSchemaValidator.Validate
SYNCHRONOUSLY on the UI thread (XmlDocument._byId is not
thread-safe; the 500ms debounce keeps validation off the typing
hot path).

ConfigureAwait is omitted (matching existing editor command
conventions) so the post-delay continuation resumes on Avalonia's
SynchronizationContext — the same thread that called SetActiveDocument.
Failures don't crash the editor — surface as a single error diagnostic
on unexpected exceptions. Service is IDisposable; Dispose cancels the
debounce and unsubscribes from the active document.

Plan 13 may revisit if very large documents make synchronous
validation user-visible — likely fix is a thread-safe snapshot
of the relevant subset of _byId before handing off to Task.Run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ProblemsView shows diagnostics from EditorValidationService.
Bottom-docked, 120 dip default. SetActiveDocument plumbs the
service to the active doc; DiagnosticsChanged updates the view.

EditorMainWindow gains _validation (EditorValidationService backed
by SchemaResolver). CA1001/CA2000 suppressed in csproj — both
objects live for the application lifetime; disposal on process
exit is correct for a desktop app.

Collapse toggle deferred to a later polish round — keeps this
change minimal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the docked-left explorer + collapse-button block with a
Grid layout: Col 0 = explorer (initial width from settings), Col
1 = 4-dip GridSplitter, Col 2 = nodeList (*).

Drag persists the new column width via
EditorSettingsStore.UpdateExplorerWidth — sub-pixel changes are
ignored to avoid spurious saves on every drag tick.

Drops the explicit collapse button — the splitter itself is now
the resize affordance (drag to ~zero to hide). Plan 13 may add a
dedicated button back if user feedback wants it.
When the window closes with one or more dirty tabs, present a
modal Save All / Discard All / Cancel dialog. Cancel keeps the
window open. Save All loops through dirty docs and calls
Document.SaveAsync; never-saved docs (Source == null) are skipped
silently for v0.6 (Plan 13 should force Save As for them).
Discard All calls MarkClean — small lie, fine because the editor
exits immediately after.

Status bar gains an 'N modified' segment so dirty docs that
aren't currently visible (no tab strip in v0.6) are still
surfaced to the user.

XmlDocument gains a public new MarkClean() forwarding method so
the editor assembly (a separate project) can call it without
requiring InternalsVisibleTo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
App.axaml only included FluentTheme; Material.Icons.Avalonia ships
its own style dictionary that must be added to Application.Styles
or MaterialIcon controls render as empty content. Toolbar buttons
were silently invisible from Plan 10 onwards.
UpdateExplorerWidth and RecordRecentFile fire-and-forget SaveAsync
calls were racing with explicit awaited SaveAsync calls (used by
tests). Two writes to the same .tmp file would clobber each other
before File.Move, and the catch(IOException) swallowed the loser
silently — sometimes leaving the persisted file with stale content.

Added a SemaphoreSlim to serialize saves. Production effect: rapid
UpdateExplorerWidth calls during a splitter drag now queue rather
than race; same for rapid RecordRecentFile calls.
Adds SourceLine and SourceColumn fields on XdmNode (default 0).
No code changes yet — Tasks B2–B5 wire the fields into validation
+ diagnostic mapping.
Replaces the placeholder Location with a structured
DiagnosticLocation record (NodeId + Line + Column). Foundation
for precise click-to-navigate (Task B4) and inline overlay (B5).

Updated DiagnosticTests and ProblemsViewTests fixtures to use the
new DiagnosticLocation? type.
XmlSchemaValidator's event handler walks the in-memory tree to
find the closest node whose SourceLine ≤ diagnostic line
(tiebreak on column). Populates IDiagnostic.Location with the
matched NodeId.

Trade-off: validator runs against the serialized form, so its
line/col is in the *serialized* output. Our SourceLine/Column is
parse-time. Lookup is approximate but exact for simple violations
on docs that serialize back close to themselves.
Diagnostics with a non-null Location route directly to the
matched NodeId. Falls back to the regex heuristic from Plan 12
for diagnostics without a mapped node (e.g., document-level
errors).
DiagnosticIndex (NodeId → severity) populated from the validation
service's DiagnosticsChanged event. XmlTreeNode.IconKey returns
'diag.error' / 'diag.warning' when the node has a diagnostic;
NodeListView renders ✕ / ⚠ glyphs in red/yellow before the row
label.

IDiagnosticIndex interface added to Phoenixml.Platform.Abstractions
so the provider can reference it without a reverse dependency on
Phoenixml.Editor. XmlTreeNodeProvider.TriggerRefresh() triggers
a rebuild after each DiagnosticsChanged event.

Plan 14 may swap to real icon assets — for v0.7, Unicode glyphs
are visible and zero-asset.
SelectById only walks _flatRows (visible/expanded nodes); if the
offending node was inside a collapsed parent the click silently
no-op'd. Added NodeListView.EnsureVisibleAndSelect which DFS-walks
the provider tree to locate the target id, marks each ancestor
expanded along the path, rebuilds, then selects.

EditorMainWindow's click-to-navigate (both precise and regex
fallback paths) routes through the new method.
Previously XmlSchemaValidator serialized the document and re-parsed
it through XmlReader to validate. The validator's reported line/col
was in the SERIALIZED output, which doesn't match our parse-time
SourceLine/SourceColumn — diagnostic→node mapping landed on the
wrong nodes (e.g., glyph on the first <year>1978</year> instead of
the invalid <year>not-a-year</year>).

New approach: drive System.Xml.Schema.XmlSchemaValidator (the
runtime class) push-style by walking XdmDocument and calling
ValidateElement / ValidateAttribute / ValidateText /
ValidateEndElement explicitly. The current XdmNode is in scope
when each call returns; diagnostics attach the exact NodeId
without any line/col arithmetic.

Text-content diagnostics attach to the containing element (not
the text node) so the inline glyph renders on the element row.

Also adds XmlDocument.ResolveNamespace(NamespaceId) (internal)
to convert interned namespace IDs to URI strings for the validator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@elvogel
elvogel merged commit 882e724 into main May 9, 2026
1 of 2 checks passed
@elvogel
elvogel deleted the feature/xdm-source-positions-consume branch May 9, 2026 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant