Skip to content

Cache the JSON tree per tab instead of drawing it on tab switch - #253

Open
leoshone wants to merge 3 commits into
NPP-JSONViewer:masterfrom
leoshone:fix/per-tab-tree-snapshot
Open

Cache the JSON tree per tab instead of drawing it on tab switch#253
leoshone wants to merge 3 commits into
NPP-JSONViewer:masterfrom
leoshone:fix/per-tab-tree-snapshot

Conversation

@leoshone

@leoshone leoshone commented Sep 3, 2026

Copy link
Copy Markdown

Problem

With Follow current tab disabled (the default), the plugin neither follows
the active tab nor clears the tree. The tree keeps showing whichever document
happened to be parsed last, with nothing indicating that it belongs to a
different tab. Reproduce:

  1. Open a.json, press Refresh JSON Tree.
  2. Switch to b.json — the tree still shows a.json.
  3. Switch back — no way to tell which document the tree belongs to.

Enabling the option instead parses on every tab switch, which is not always
wanted either (large files, and it re-parses even when the user only wanted a
look at the tree of one tab).

Change

The tree is now drawn only when the user asks for it (Refresh JSON Tree).
Switching tabs stores the tree of the tab being left and puts it back verbatim
when that tab comes back, so:

  • no re-parsing happens on tab switch, and
  • expansion state and selection survive.

The "Follow current tab" option is kept and behaves exactly as before when it
is enabled
— the document of the activated tab is parsed immediately. Only its
off path changes, from "do nothing" to "remember per tab". Users who rely on
the follow behaviour are unaffected, and no visible setting is removed.

Details:

  • Snapshots live in memory only. They are dropped when the buffer is closed,
    together with the association to the current buffer (Notepad++ does not
    guarantee whether NPPN_FILECLOSED or NPPN_BUFFERACTIVATED arrives first).
  • Auto format on open now formats the document without drawing the tree
    (FormatJsonDocument() is FormatJson() minus the redraw), so opening a file
    can no longer trigger a parse.
  • Formatting now redraws the tree while preserving its expansion state, which
    keeps it consistent with Refresh (see Keep the tree expansion state when refreshing the JSON tree #252).
  • New TreeState model (nodes + expansion + selection) next to the
    TreeExpansion helpers. TreeExpansion is the "same document, tree rebuilt"
    case; TreeState is the "whole tree carried across tabs" case.

Optional: draw the tree when a json file is opened

"Never parse on its own" is the right default, but it also means a user who
wants the tree right away has to press Refresh on every file. A new option,
Draw tree when a json file is opened (DRAW_ON_OPEN, off by default),
draws the tree once when a json document is opened.

It is deliberately built on top of the snapshot mechanism rather than next to
it: the check lives in RestoreTabState(), the single place reached when the
tree of a tab has never been drawn. So the document is parsed exactly once, and
from then on the tab behaves like any other — switching back replays the stored
snapshot and does not parse again. Opening a file, switching back to a tab and
showing the panel are all covered by that one code path.

Two details worth reviewing:

  • The tree is drawn for documents whose language is JSON, the same criterion
    the existing Follow current tab uses.
  • Drawing on open is initiated by the plugin, not by the user, so a parse
    failure must not stop the user with a dialog. DrawJsonTree() takes a
    bSilent flag for that and reports the failure as a node inside the tree
    instead. Manual Refresh is unchanged and still reports errors as before.

Note on the diff

This branch is built on top of #252 (TreeExpansion.h/.cpp), so its diff
currently includes that PR as well. The change specific to this PR is
+602/-28 over 17 files; the total against master is +1073/-31 over 20
files
. If #252 is merged first, this PR collapses to its own commits.

Verification

  • Unit tests: 167/167 (143 pre-existing, 14 from Keep the tree expansion state when refreshing the JSON tree #252, 9 TreeStateTest
    cases for snapshot capture/compare, 1 ProfileTest round-trip for the new
    setting).
  • Fork CI: all six jobs green (Debug/Release × Win32/x64/ARM64).
  • Manual end-to-end on a portable Notepad++ x64 build, driven through
    SendMessage (this host runs non-interactively, so no keyboard injection):
    • 21/21 assertions for the per-tab caching, including the decisive one —
      after rewriting the document behind the tab's back, switching away and back
      still shows the old snapshot, and only an explicit Refresh picks up the
      new text. That distinguishes "replay a snapshot" from "re-parse quietly".
    • 20/20 assertions for draw tree on open: a json file is drawn without
      pressing Refresh; a second file gets its own tree; switching back shows the
      right document; after the document is changed behind the plugin's back the
      stale snapshot is kept (no re-parse); a new non-json file is not parsed;
      Refresh still re-reads; with the option off nothing is drawn and manual
      Refresh still works; and a malformed json file reports the error inside
      the tree
      with no modal dialog, leaving Notepad++ responsive.

"Refresh JSON Tree" rebuilds every node, so the tree always came back
fully collapsed - even when the user only wanted to re-read a document
they were already looking at.

Capture which nodes are expanded and which one is selected before the
tree is thrown away, then re-apply that state onto the freshly built
tree, matching nodes by path. Paths that no longer exist (the document
changed in the meantime) are silently dropped, and nodes that are new
stay collapsed.

The state is keyed by node path, which is the list of keys from the
tree root down to a node. The pure path arithmetic lives in the new
TreeExpansion.h/.cpp so it can be unit tested without a window.

DrawJsonTree() gained a bPreserveExpansion parameter that defaults to
false, so every other caller (panel opening, formatting, compressing,
sorting) keeps behaving exactly as before; only the refresh button
opts in.
When "Follow current tab" is off (the default) the plugin never drew the
tree on its own, but it also never cleared it: the tree kept showing the
document of some earlier tab, with no indication that it belonged there.

With this change the tree is only ever drawn when the user asks for it
("Refresh JSON Tree"). Switching tabs stores the tree of the tab being
left and puts it back verbatim when the tab is activated again, so no
re-parsing happens and the expansion state and selection survive.

The "Follow current tab" option is kept and behaves exactly as before
when enabled: the document of the activated tab is parsed immediately.
Only its "off" path changes, from "do nothing" to "remember per tab".

Notes:
 - Snapshots live in memory only and are dropped when the buffer is
   closed, together with the association to the current buffer.
 - "Auto format on open" now formats the document without drawing the
   tree, so opening a file still cannot trigger a parse.
 - Formatting now redraws the tree while preserving its expansion state,
   which keeps it consistent with Refresh.
 - Built on top of the TreeExpansion helpers introduced for Refresh.
Adds an option, off by default, that draws the tree of a json document as soon as the file is opened. It complements the per-tab snapshot caching: the document is parsed exactly once, and switching back to the tab afterwards replays the stored snapshot instead of parsing again.

The check lives in RestoreTabState(), the single place reached when the tree of a tab has never been drawn, so opening a file, switching back to a tab and showing the panel are all covered by one code path.

Drawing on open is initiated by the plugin, not by the user, so parse errors are reported as a node inside the tree rather than through a modal dialog: DrawJsonTree() takes a bSilent flag for that.

The tree is drawn for documents whose language is JSON, the same criterion the existing "follow current tab" uses.
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