Take a note on any tab, in markdown that knows the heap dump - #2951
Merged
Conversation
pyricau
force-pushed
the
add-ability-to-add-save-notes
branch
from
August 10, 2026 11:32
b204b11 to
d21eace
Compare
pyricau
force-pushed
the
add-ability-to-add-save-notes
branch
2 times, most recently
from
August 10, 2026 14:45
c474389 to
717f65b
Compare
An investigation is a series of questions about one heap dump, and the answers have so far lived in whatever the reader happened to have open beside the app. Every tab now takes a note: ✎ Add Note, at the end of the row that says where the tab is, turns into a plain markdown box with save and cancel, and saving draws the note under that row and above the panes. There because a note is about the whole of what the tab is showing, and only there once there is one — a tab nobody has written about is the button and nothing else, and the button goes away as soon as there is a note, which carries its own ✎ Edit. What makes it worth having in the app rather than in an editor is that the names in it are read against the heap dump. A class name that exists shows up as its simple name and opens the class in a new tab. Anything starting with 0x that resolves reads as "Holder instance (0x82182c00)" and opens the object. A shark:// link is followed exactly as one arriving from the OS is, so a link copied from a tab goes back to that tab. GitHub URLs shorten the way GitHub shortens them, and every other http link opens in the browser. Anything this dump has nothing for is left exactly as it was typed. A note is filed under what its tab is about rather than under the tab or the place: the object, the object list however it is filtered, the leaks however they are unfolded. Keyed on the place itself it would follow the search box, and keyed on the tab it would die with it. So the same object in two tabs, or in two windows, is one note, and the tab strip marks the tabs that have one from a single directory listing. The notes are markdown files in the app's own directory, one per dump and one per place, because a dump is opened from wherever it came from and writing beside it means littering some places and failing on the rest. Nothing is written before the file has been read, and the button that starts a note is disabled until then, so a slow or failed read can never be written over by an empty note. The draft being typed is the run's rather than the screen's, so leaving the tab half way through a sentence and coming back finds the sentence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A tab that has walked twenty objects down a chain is one where getting back to where the walk started is twenty clicks and a guess about which of them it was. Right clicking ← or → now lists everywhere that arrow leads, nearest first and named the way the tab strip names a place, so any one of them is one click. NavigationHistory.backEntries and forwardEntries are that list and goBack(steps) is the move, clamped rather than checked because the list a click came from and the history it lands in are the same value one recomposition apart. The nearest fifteen, since the menu is drawn where the pointer is and one taller than the window has entries nobody can reach; everything past them is still one click of the arrow at a time away. An arrow that leads nowhere keeps the disabled button it was, because an empty menu under the pointer reads as the window having lost the history rather than as there being none. Naming the entries is why every place of every history is named now rather than only the one each tab is on: a menu entry called "…" is a move nobody will make. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panes either side of the map are headed "What holds it" and "What it is", and the map between them was headed nothing at all, which left the two of them reading as a pair with a picture wedged between rather than as three questions about one object. It gets "What it holds", the name its fold button already used, and the fold control moves into that header like the other two. The object the tab is on is set as a title where it is named above the panes, which is the one place in the window where it is the subject rather than a mention of an object among others: everything under it, three panes or a list and the note, is about that object. At body size it read as one more label in a bar of them. ObjectIdentity takes the style for its first line rather than picking one, so the same three lines stay the same everywhere else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pyricau
force-pushed
the
add-ability-to-add-save-notes
branch
from
August 10, 2026 15:09
717f65b to
b33947c
Compare
The row above the panes was one undifferentiated strip: two arrows, the title, and the button that starts a note at the far end of it. A rule after the arrows makes it two things instead — how the tab got here, and what it is on — which is what says the note button belongs to the object named beside it rather than to the window. Which is also the answer to where that button goes. Before the title puts the action ahead of its subject and moves the title depending on whether a note exists; hugging the end of the title moves the button on every click, the title being the object the tab is on; under the title reads best but is a row of window spent on every tab. The end of the row keeps the reading order and one position, and costs nothing. It is drawn at the size of every other button in the bars now, rather than a step smaller: the one thing on screen that says notes exist was the smallest text in a row of them.
Where a note appears is where the button that starts one belongs: under the title it will be about, reading in the order it happens — this object, then write about it. At the far end of the row it kept one position, but it was as far from the title as the window is wide, and it read as the window's button rather than as this object's. What that costs is a line of every tab nobody has written about, so it is drawn small and with no padding of its own, which also lines its label up with the title above it. The tooltip on it is one sentence now. Markdown, class names, addresses and shark:// links are what the box can do, and that paragraph is in the way of deciding whether to open the box; it is the placeholder inside it instead, which is on screen exactly while it is worth reading. And the wording throughout says what was always true of the code: a note is the object's, not the tab's. Two tabs on one object are one note and show each other's writing as it is typed, which is now a test rather than only a consequence of Place.noteKey and one PlaceNotes per place.
Three of the note keys are lists rather than objects, one is a group of them, and the tab a window opens with is the whole heap dump — so "a note per object" reads as if the object list and the leaks had none, which is the opposite of what noteKey does. The code has always been per Place; this is the wording catching up, with "location" as the word the docs use since Place is ours rather than anybody else's. Also says how to throw a note away, which the app has always been able to do and nothing wrote down: open it, delete the text, save. An empty note is no note, so the file goes and the mark comes off the tab.
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.
An investigation is a series of questions about one heap dump, and the answers have so far lived in whatever the reader happened to have open beside the app. Every location now takes a note. Two smaller things the same window asked for came with it: the arrows list where they lead, and the window says what it is about.
✎ Add Note hangs under the title that says what the tab is on, and the note it starts is drawn under that row and above the three panes that read it — a note is about the whole of what the tab is showing, so under the title it is about and above everything that describes it. Clicking the button turns that space into a plain markdown box with Save and Cancel; saving draws the note where the box was. A location nobody has written about has nothing there at all, and the button goes away as soon as there is a note, which carries its own ✎ Edit — so a section that is on every tab costs nothing on the tabs it has nothing to say about.
A note belongs to the location, not to the tab: two tabs on one location are one note, and both show what the other is typing. Location rather than object, because three of the keys are lists and one is a group of objects —
Place.noteKeyis the definition.What makes it worth having in the app rather than in an editor is that the names written in it are read against the heap dump:
com.example.HolderHolder0x82182c00Holder instance (0x82182c00)shark://abcd2345/starredStarredhttps://github.com/square/leakcanary/pull/2941square/leakcanary#2941https://example.com/thingA class name that isn't in the dump, or an address nothing lives at, stays exactly as typed — the note is still readable when it's about a dump other than the one open, and nothing is ever rewritten on disk, only on screen. Reading the markdown is in memory; asking the heap dump what the names mean is a read, so it happens once per save, and not at all for a note that mentions nothing.
shark://links are followed through the sameDeepLinkPeers.followan OS link arrives through, so a link copied from a tab (⌘-click, or the right click menu) pasted into a note goes back to that tab, in that window, even across runs of the app.What a note is attached to
Place.noteKey()is the whole of it: a note is filed under what its tab is about, not under the tab or the place.A place carries the state of its screen as well as its subject — what the object list is filtered to, which leaks are unfolded, how many objects a pile of small ones stands for at this window width. Keyed on the place itself, a note would follow the search box: typing a letter into it would be moving to a different notepad. Keyed on the tab, it would die with the tab. So: one note per object, one for the object list however it is filtered, one for the leaks however they are unfolded, one for the starred objects — and the whole heap dump is an object like any other here, so the tab a window opens with is where the note about the dump as a whole goes.
That also means the same object open in two tabs, or in two windows, is one note rather than two saving over each other.
The strip marks a tab whose place has a note with a
✎, because a note nobody is reminded of is a note nobody comes back to. Which tabs those are is one directory listing rather than a file opened per tab.Where the notes live
Markdown files in the app's own directory: a directory per dump (
heap.hprof-1f3a9c0b/), a file per place inside it (heap-dump.md,object-0x82182c00.md,leaks.md). Not beside the heap dump — a dump is opened from a temporary file, a read-only mount, a directory pulled off a device, or a checkout of this repository, so writing beside it means littering some places and failing on the rest. A file per place rather than one document with sections, so a save touches only the note that was typed into and nothing has to be parsed back out of a document that also holds someone's own headings. The box shows the path while you write, and the file is plain markdown, because a note about a leak gets pasted into an issue or read months later.Not losing what someone typed
The notes are the one thing in the app nobody else has a copy of, so:
PlaceNotes: clicking another tab half way through a sentence and coming back finds the sentence. It is not filed on the way out, which is the same answer Cancel gives..partialfile and are renamed over, so a run killed halfway through leaves the previous note rather than half of the new one.List where an arrow leads on a right click
A tab that has walked twenty objects down a chain is one where getting back to where the walk started is twenty clicks and a guess about which of them it was. Right clicking ← or → now lists everywhere that arrow leads, nearest first and named the way the tab strip names a place, so any one of them is one click.
NavigationHistory.backEntries/forwardEntriesare that list andgoBack(steps)is the move, clamped rather than checked because the list a click came from and the history it lands in are the same value one recomposition apart. The nearest fifteen, since the menu is drawn where the pointer is and one taller than the window has entries nobody can reach. An arrow that leads nowhere keeps the disabled button it was: an empty menu under the pointer reads as the window having lost the history rather than as there being none.Say what the middle pane answers, and make the tab's subject a title
The panes either side of the map are headed "What holds it" and "What it is", and the map between them was headed nothing at all, which left the two of them reading as a pair with a picture wedged between rather than as three questions about one object. It gets "What it holds", the name its fold button already used, and the fold control moves into that header like the other two.
That row is now two things with a rule between them: how the tab got here — the arrows — and what it is on, which is the title with what the notes offer under it. One undifferentiated strip of controls is what made anything at the end of it read as belonging to the window rather than to the object named beside it; the grouping says which, so nothing has to be labelled to say so.
IntrinsicSize.Minon the row is what makes the rule as tall as the row, the title being one line on the whole heap dump and three on an object.✎ Add Notesits inside the second of those, under the title, of the four places it could be: before the title puts the action ahead of its subject and moves the title depending on whether a note exists; hugging the end of the title moves the button on every click, the title being the object the tab is on; the far end of the row keeps one position but is as far from the title as the window is wide, and reads as the window's button rather than as this object's. Under the title is where its own result appears — the note opens exactly there — and it reads in the order it happens. What that costs is a line of every tab nobody has written about, so it is drawn small and with no padding of its own, which also lines its label up with the title.The object the tab is on is set as a title where it is named above the panes — the one place in the window where it is the subject rather than a mention of an object among others, since everything under it is about that object.
ObjectIdentitytakes the style for its first line rather than picking one, so the same three lines are unchanged everywhere else.Tests
50 unit tests in
shark-explorer-coreover the markdown reading, the GitHub shortening, resolution against a real dump and where a note is filed — including that filtering the object list, unfolding a leak and resizing a pile of small objects all stay on one note. 7 more over the history a right click lists and moving several steps at once. 15 headless UI tests inshark-explorer-appcover each row of the table above, the tab a class link opens, that Cancel throws a draft away while Save puts it on disk, that changing a note starts from what was saved, that a note is only about the place its own tab is at, that two tabs on one place are one note, and that a place nobody has written about is the button and nothing else until saving takes the button away.🤖 Generated with Claude Code