7

I need to look at larger, quite deeply nested JSON files in Emacs. Similar to the terminal tool json-view I would like to start with the JSON file completely folded and then open each level step-by-step.

I've tried Emacs' built-in Hideshow minor mode, but after I have totally folded an JSON tree (via hs-hide-all) I issue hs-show-block and Emacs shows the next level and all sub-levels, i.e. the whole JSON document, instead I would liketo see incrementally just the next level with all deeper sub-levels still folded.

It looks to me that hideshow is good at incrementally hiding levels, but is there a way in Emacs to view JSON files that the levels are step-by-step revealed?

halloleo
  • 1,215
  • 9
  • 23

2 Answers2

2

The json-navigator Emacs package displays any JSON document in a tree-like structure. It uses the hierarchy package.

Damien Cassou
  • 877
  • 4
  • 14
  • This is almost a link-only answer. Please add details here to avoid total loss of information if the link becomes invalid. – Tobias Apr 18 '18 at 13:48
  • Looks super promising. Will check it out. - But I do have to concur with @Tobias: the answer is a bit short. – halloleo Apr 19 '18 at 05:08
  • I edited the answer. I don't really know what I could add more though. – Damien Cassou Apr 19 '18 at 14:30
  • `json-navigator does exactly what I want! :-) Shame though that it seems to be *very* slow on big JSON files, because, I guess, it reads the whole hierarchy in one go. – halloleo Apr 23 '18 at 06:21
  • There is a new JSON library in Emacs master that is said to be much faster. If you need json-navigator to be faster and can accept working with Emacs master, I could try. Please open an issue on the project. – Damien Cassou Apr 25 '18 at 06:50
1

You should try hs-hide-level with numeric prefix argument. The argument specifies the sub-level of the blocks to hide relative to the block you are in.

If you want to have some kind of current level within the whole buffer and you want to hide the next or the previous sub-level you can try the following code.

Paste that code into your init-file and re-start emacs. Afterwards you can use C-c @ > for hiding the next level and C-c @ < for hiding the previous level. There are also menu items in the Hide/Show menu for that.

Note that this is a minimal implementation. An actual implementation could be much more refined.

Possible improvements:

  • optionally detect the current level by the currently hidden parts of the buffer
  • reduce hs-current-level appropriately when regions of the json buffer are killed
(require 'hideshow)

(defvar-local hs-current-level 1
  "Currently hidden level of hideshow.")

(defvar-local hs-prev-level-active nil
  "Deactivate `hs-hide-prev-level' when `hs-current-level' is 1.")

(defvar-local hs-next-level-active t
  "Deactivate `hs-hide-next-level' when there are no hs overlays anymore.")

(defun hs-overlays-p (&optional ignore-comments)
  "Return non-nil if there are still hs-overlays.
Ignore comments if IGNORE-COMMENTS is non-nil."
  (let (ol)
    (save-excursion
      (goto-char (point-min))
      (while (and (null (eobp))
                  (null (and (setq ol (hs-overlay-at (goto-char (next-overlay-change (point)))))
                             (or (null ignore-comments)
                                 (null (eq (overlay-get ol 'hs) 'comment)))))))
      (null (eobp)))))

(defun hs-hide-prev-level ()
  "Hide level `hs-current-level'-1."
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (setq hs-current-level (max (1- hs-current-level) 1))
    (hs-hide-level hs-current-level)))

(defun hs-hide-next-level ()
  "Hide level `hs-current-level'+1."
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (setq hs-current-level (1+ hs-current-level))
    (hs-hide-level hs-current-level)))

(define-key hs-minor-mode-map (kbd "C-c @ >") #'hs-hide-next-level)
(define-key hs-minor-mode-map (kbd "C-c @ <") #'hs-hide-prev-level)

(easy-menu-add-item hs-minor-mode-menu nil ["Hide previous level" hs-hide-prev-level (> hs-current-level 1)] "Toggle Hiding")
(easy-menu-add-item hs-minor-mode-menu nil ["Hide next level" hs-hide-next-level (hs-overlays-p t)] "Toggle Hiding")

Test-conditions:

  • emacs-version: GNU Emacs 25.3.1 (x86_64-unknown-cygwin, GTK+ Version 3.22.20) of 2017-09-11
  • .json-buffers are associated to java-mode as major-mode from cc-mode.el shipping with emacs 25.3.1
  • hs-minor-mode shipping with emacs 25.3.1

Testing the built-in functionality of hide/show:

Modified version of an example from http://json.org/example.html:

{"web-app": {
  "servlet": [
    {
      "servlet-name": "cofaxCDS",
      "servlet-class": "org.cofax.cds.CDSServlet",
      "init-param": {
        "additional sub-level": {
            "item 1": "1",
            "item 2": "2",
        },
        "configGlossary:installationAt": "Philadelphia, PA",
        "configGlossary:adminEmail": "ksm@pobox.com"}},
    {
      "servlet-name": "cofaxEmail",
      "servlet-class": "org.cofax.cds.EmailServlet",
      "init-param": {
      "mailHost": "mail1",
      "mailHostOverride": "mail2"}},
    {
      "servlet-name": "cofaxAdmin",
      "servlet-class": "org.cofax.cds.AdminServlet"},
    {
      "servlet-name": "fileServlet",
      "servlet-class": "org.cofax.cds.FileServlet"},
    {
      "servlet-name": "cofaxTools",
      "servlet-class": "org.cofax.cms.CofaxToolsServlet",
      "init-param": {
        "templatePath": "toolstemplates/",
        "log": 1}}],
  "servlet-mapping": {
    "cofaxCDS": "/",
    "cofaxEmail": "/cofaxutil/aemail/*"},
  "taglib": {
    "taglib-uri": "cofax.tld",
    "taglib-location": "/WEB-INF/tlds/cofax.tld"}}}

Hiding sub-levels:

Put point behind "web-app": and press C-2 C-c @ C-l for hs-hide-level with prefix-arg 2:

{"web-app": {
  "servlet": [
    {...},
    {...},
    {...},
    {...},
    {...}],
  "servlet-mapping": {...},
  "taglib": {...}}}

Revealing one more sub-level:

Keeping point position press C-3 C-c @ C-l:

{"web-app": {
  "servlet": [
    {
      "servlet-name": "cofaxCDS",
      "servlet-class": "org.cofax.cds.CDSServlet",
      "init-param": {...}},
    {
      "servlet-name": "cofaxEmail",
      "servlet-class": "org.cofax.cds.EmailServlet",
      "init-param": {...}},
    {
      "servlet-name": "cofaxAdmin",
      "servlet-class": "org.cofax.cds.AdminServlet"},
    {
      "servlet-name": "fileServlet",
      "servlet-class": "org.cofax.cds.FileServlet"},
    {
      "servlet-name": "cofaxTools",
      "servlet-class": "org.cofax.cms.CofaxToolsServlet",
      "init-param": {...}}],
  "servlet-mapping": {
    "cofaxCDS": "/",
    "cofaxEmail": "/cofaxutil/aemail/*"},
  "taglib": {
    "taglib-uri": "cofax.tld",
    "taglib-location": "/WEB-INF/tlds/cofax.tld"}}}

Revealing sub-levels in single blocks:

Put point behind the first "servlet-name": and press C-2 C-c @ C-l:

{"web-app": {
  "servlet": [
    {
      "servlet-name": "cofaxCDS",
      "servlet-class": "org.cofax.cds.CDSServlet",
      "init-param": {
        "additional sub-level": {...},
        "configGlossary:installationAt": "Philadelphia, PA",
        "configGlossary:adminEmail": "ksm@pobox.com"}},
    {
      "servlet-name": "cofaxEmail",
      "servlet-class": "org.cofax.cds.EmailServlet",
      "init-param": {...}},
    {
      "servlet-name": "cofaxAdmin",
      "servlet-class": "org.cofax.cds.AdminServlet"},
    {
      "servlet-name": "fileServlet",
      "servlet-class": "org.cofax.cds.FileServlet"},
    {
      "servlet-name": "cofaxTools",
      "servlet-class": "org.cofax.cms.CofaxToolsServlet",
      "init-param": {...}}],
  "servlet-mapping": {
    "cofaxCDS": "/",
    "cofaxEmail": "/cofaxutil/aemail/*"},
  "taglib": {
    "taglib-uri": "cofax.tld",
    "taglib-location": "/WEB-INF/tlds/cofax.tld"}}}

Testing the new hs-hide-prev-level and hs-hide-next-level:

At first application `hs-current-level` is 1. If you put point anywhere in the buffer and press C-c @ > you get independent of the currently hidden blocks:

{"web-app": {
  "servlet": [
    {...},
    {...},
    {...},
    {...},
    {...}],
  "servlet-mapping": {...},
  "taglib": {...}}}

Next application of C-c @ > gives:

{"web-app": {
  "servlet": [
    {
      "servlet-name": "cofaxCDS",
      "servlet-class": "org.cofax.cds.CDSServlet",
      "init-param": {...}},
    {
      "servlet-name": "cofaxEmail",
      "servlet-class": "org.cofax.cds.EmailServlet",
      "init-param": {...}},
    {
      "servlet-name": "cofaxAdmin",
      "servlet-class": "org.cofax.cds.AdminServlet"},
    {
      "servlet-name": "fileServlet",
      "servlet-class": "org.cofax.cds.FileServlet"},
    {
      "servlet-name": "cofaxTools",
      "servlet-class": "org.cofax.cms.CofaxToolsServlet",
      "init-param": {...}}],
  "servlet-mapping": {
    "cofaxCDS": "/",
    "cofaxEmail": "/cofaxutil/aemail/*"},
  "taglib": {
    "taglib-uri": "cofax.tld",
    "taglib-location": "/WEB-INF/tlds/cofax.tld"}}}

Afterwards pressing C-c @ < gives the previous hs-state again.

Tobias
  • 32,569
  • 1
  • 34
  • 75
  • Thanks for your effort, @Tobias! It is not exactly what I want (*incrementally* showing JSON nodes) but gives me certainly an insight into `hideshow`. – halloleo Apr 18 '18 at 07:41
  • @halloleo Sorry, I do not really understand what you want. You can _incrementally_ reveal and hide the levels of a single node by putting point into that node and giving a numeric prefix argument to `hs-hide-level`. You can _incrementally_ reveal and hide the levels of the whole JSON file by my code. – Tobias Apr 18 '18 at 10:59
  • Mmmh, for me it reveals always the next level *and* all sublevels. - Will try to add a detailed guide to the problem. – halloleo Apr 19 '18 at 05:07
  • @halloleo I added a detailed test in my answer together with the expected results. There you see what I mean. – Tobias Apr 19 '18 at 07:39
  • True, nifty use of universal arguments! :-) Now I see how you understand it: If you unfold a level you expand all branches on that level. I rather want to unfold individual branches of a level... `json-navigator` is more suitable for this – halloleo Apr 23 '18 at 06:19