- Extract MatchesBookmark for the token + assembly identity that tied a bookmark
to a member; GetLineForBookmark and DocumentContainsBookmarkToken each spelled
it out (four copies), so a change to the match rule had to be made everywhere.
- Add a FoldingSection overload to FoldingsViewState.Restore and route the
bookmark folding restore through it, dropping a duplicated checksum + match loop.
- When two bookmarks resolve to the same gutter line, draw the enabled glyph so
the line never reads as disabled while an active bookmark sits on it.
- Drop an out-of-band reference in a test comment.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two costs on the gutter's hot path:
The margin rebuilt its document-line -> glyph map on every Render, resolving
each bookmark by scanning the document's reference segments. A scroll, a hover,
or the 600 ms post-navigation pulse repaints many times a second, so this
rescanned the references per bookmark per frame. Compute the map once and reuse
it, clearing the cache only when the document or the bookmark set changes (the
model's DebugInfo/References are set before the text, so the document-change
hook sees them current).
CanToggleBookmarkAtLine, called for every gutter line the pointer moves over,
built a full bookmark including a captured editor view state -- a foldings
snapshot, scroll offsets, and a tree-path walk -- only to test it for null. It
now does an anchor-only check; the view state is captured only when a bookmark
is actually created.
Assisted-by: Claude:claude-opus-4-8:Claude Code
After scrolling to a navigated bookmark, the deferred apply restored the
caret/scroll captured when the bookmark was created, which overwrote the
centering that had just been computed for the resolved line. A bookmark
re-anchors by token / IL offset, so a decompiler-setting change that reflows
the C# moves it to a different line than the one saved in its view state; the
stale offset then scrolled that line back off-screen, leaving only the
highlight playing where it could not be seen.
Restore just the captured foldings now -- and before centering, since
collapsing or expanding shifts where lines sit -- and let the centered,
re-resolved line be the final caret and scroll position.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Every save reloaded the JSON sidecar into fresh instances and then replaced the
whole observable collection with Clear()+Add(). That dropped and re-added every
bookmark on each toggle or edit: the bookmarks pane keeps its selection by
reference, so it was cleared on every change (leaving the toolbar acting on a
stale or null selection), the grid's scroll reset, and a checkbox/name edit made
from the grid re-entered the DataGrid mid-edit while its ItemsSource was torn
down and rebuilt.
Reconcile in place instead: keep the existing instance for any anchor still
present (refreshing only its editable Name/Enabled), remove anchors that are
gone, and append new ones. A name/enabled edit leaves the membership unchanged,
so it now touches the collection not at all; structural changes touch only the
affected rows. Instance identity is preserved, so the pane's selection and the
gutter's references survive.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Two fixes to the gutter's hover preview, the faint glyph shown on a hovered
bookmarkable line:
A removal click left the pointer hovering the line it had just cleared, so the
hover preview redrew a glyph there immediately and the click looked like a
no-op. The just-removed line's preview is now suppressed until the pointer
leaves that line; a jitter that stays on the same line keeps it hidden, and a
fresh hover after leaving shows it again.
The preview also never actually faded: the glyph is an SvgImage, which paints
through a custom Skia draw operation that ignores DrawingContext.PushOpacity,
so it always rendered fully opaque. The glyph is now rasterized once into a
bitmap, which DrawImage does composite at the pushed opacity.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Import read the chosen file through the tolerant sidecar-load path, which maps a
missing, empty, or unparseable file all to an empty list. Replace mode then
cleared the saved list and wrote the empty result back, so picking a corrupt or
non-bookmark JSON file for a Replace import silently destroyed the user's
bookmarks.
Import now reads through TryReadFrom, which distinguishes a valid (possibly
empty) file from a parse/read failure. A failure leaves the live and saved lists
untouched and returns false so the pane can report it; a valid empty file still
legitimately clears the list in Replace mode. The normal sidecar load keeps its
tolerant behavior.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Activating a bookmark only positioned the view when a decompiler tab was
already the active content: the pending bookmark was set on
DockWorkspace.ActiveDecompilerTab, which is null while a metadata table, the
Options page, or the About page is showing. Navigating from there selected and
decompiled the node but opened at the top with no line highlight.
The pending bookmark now travels to the decompiler model that ShowSelectedNode
routes the selection to, and is consumed in the document-apply step alongside
the view-state restore -- mirroring PendingViewState -- rather than reacting to
a property change. The earlier property-change path fired synchronously during
selection, scrolled, then a deferred document apply reset the caret to the top
with nothing left to re-apply. For a node that is already decompiled (re-shown
after an interlude, where no apply step runs) a ScrollToBookmark callback on the
model scrolls the live view directly, mirroring NavigateBookmarkInFile.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Bookmarks now capture and restore the requested decompiler view state, support rendered-line fallback anchors, expose hover and pane location affordances, route bookmark context-menu actions through the clicked offset, and update the JSON sidecar under a mutex so concurrent instances do not overwrite unrelated bookmark edits.
The plan file records the completed checklist and the remaining manual smoke-test gap.
Assisted-by: CodeAlta:gpt-5.5:CodeAlta
There was no way to mark and return to interesting spots in decompiled
code. This adds a flat (no folders, no labels) bookmark list: toggle on a
line from the context menu, the gutter, or Ctrl+B; see an icon in a new
left-margin gutter that honours a disabled state; and manage the list in a
dockable pane that auto-registers in the Window menu.
Bookmarks anchor by metadata token, never by a raw line number, so they
survive re-decompilation and decompiler-setting changes that reflow the C#
text: a definition line anchors to its token, while a line inside a method
body anchors to the method token plus an IL offset. Recovering an IL offset
needs the decompiler's sequence points, which the normal C# output did not
carry, so they are captured once at the WriteCode chokepoint and stored as a
per-document line/offset map. The map is also what places gutter icons and
scrolls navigation to the exact line.
The list persists to an ILSpy.Bookmarks.json sidecar next to ILSpy.xml; the
path logic is extracted into AppEnv/ConfigurationFiles so the dock layout
sidecar shares it. Navigating to a bookmark loads its assembly from disk if
it dropped out of the list (and only then offers to remove a bookmark whose
file is gone), then centres the line and plays a brief line flash plus a
gutter-icon pulse. Disabled bookmarks stay visible but are skipped by the
next/previous actions.
Assisted-by: Claude:claude-opus-4-8:Claude Code