Subject: Re: bug#21798: 25.0.50; [PATCH] Add support for retrieving paths to JSON elements

From: Simen Heggestøyl <[email-masked]>
Date: Sun, 01 Nov 2015 20:52:33 +0100

Hi Dmitry, thanks for the feedback!

On Sat, Oct 31, 2015 at 3:23 PM, Dmitry Gutov <[email-masked]> wrote:
> The video looks great, but the inline patch lacks indentation, and 
> doesn't apply (dunno if the former is the cause of the latter). 
> Please resend it as an attachment.

Ah, yes, it appear that the whitespace got lost in the email for some
reason. I'll try attaching it to this email.

To test it, I've been using the following interactive function:

  (defun json-mode-show-path ()
    "Show the path to the JSON value under point."
    (interactive)
    (let ((path (json-path-to-position (point))))
      (if path
          (let ((formatted-path
                 (json-mode--format-path (plist-get path :path))))
            (pulse-momentary-highlight-region
             (plist-get path :match-start)
             (plist-get path :match-end))
            (message formatted-path)
            (kill-new formatted-path))
        (message "Not a JSON value"))))

  (defun json-mode--format-path (path)
    "Return PATH formatted as a JSON data selector.
  PATH should be a list of keys, which can be either strings or
  integers."
    (mapconcat (lambda (key) (format "[%S]" key)) path ""))

It's also included it in json-mode.el:
http://folk.uio.no/simenheg/json-mode.el.

> Without trying it, my main concern would be any performance 
> regression to json-read (it's not particularly fast already). Have 
> you done any benchmarking?

I agree that would be bad. I'll try to produce some benchmarks when
I've got some free time on my hands!

-- Simen
>From f6ddd3b797d6b0d92a1ffa0f5db59543ac7cdc11 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= <[email-masked]>
Date: Sun, 25 Oct 2015 14:44:59 +0100
Subject: [PATCH] Add support for retrieving paths to JSON elements

Add support for retrieving the path to a JSON element. This can for
instance be useful to retrieve paths in deeply nested JSON
structures.

* lisp/json.el (json-path-to-position): New function for retrieving the
path to a JSON object at a given position.
(json--path-to-position, json--path): New variables, used internally by
`json-path-to-position'.
(json-read-object, json-read-array): Respect `json--path-to-position'.

* test/automated/json-tests.el (test-json-path-to-position-with-objects)
(test-json-path-to-position-with-arrays): New tests for
`json-path-to-position'.
---
 lisp/json.el                 | 67 ++++++++++++++++++++++++++++++++++++++++++--
 test/automated/json-tests.el | 14 +++++++++
 2 files changed, 79 insertions(+), 2 deletions(-)

diff --git a/lisp/json.el b/lisp/json.el
index b23d12a..2c82eee 100644
--- a/lisp/json.el
+++ b/lisp/json.el
@@ -196,6 +196,49 @@ 'json-end-of-file
 
 
 
+;;; Paths
+
+(defvar json--path-to-position nil
+  "When set to a position, `json-read' will return the path to
+the JSON element at the specified position, instead of returning
+the parsed JSON object.")
+
+(defvar json--path '()
+  "Used internally by `json-path-to-position' to keep track of
+the path during recursive calls to `json-read'.")
+
+(defun json-path-to-position (position &optional string)
+  "Return the path to the JSON element at POSITION.
+
+When STRING is provided, return the path to the position in the
+string, else to the position in the current buffer.
+
+The return value is a property list with the following
+properties:
+
+:path        -- A list of strings and numbers forming the path to
+                the JSON element at the given position.  Strings
+                denote object names, while numbers denote array
+                indexes.
+
+:match-start -- Position where the matched JSON element begins.
+
+:match-end   -- Position where the matched JSON element ends.
+
+This can for instance be useful to determine the path to a JSON
+element in a deeply nested structure."
+  (save-excursion
+    (unless string
+      (goto-char (point-min)))
+    (let* ((json--path '())
+           (json--path-to-position position)
+           (path (catch :json-path
+                   (if string
+                       (json-read-from-string string)
+                     (json-read)))))
+      (when (plist-get path :path)
+        path))))
+
 ;;; Keywords
 
 (defvar json-keywords '("true" "false" "null")
@@ -399,11 +442,21 @@ json-read-object
     (while (not (char-equal (json-peek) ?}))
       (json-skip-whitespace)
       (setq key (json-read-string))
+      (when json--path-to-position
+        (push key json--path))
       (json-skip-whitespace)
       (if (char-equal (json-peek) ?:)
           (json-advance)
         (signal 'json-object-format (list ":" (json-peek))))
-      (setq value (json-read))
+      (json-skip-whitespace)
+      (let ((start (point)))
+        (setq value (json-read))
+        (when json--path-to-position
+          (when (< start json--path-to-position (+ (point) 1))
+            (throw :json-path (list :path (nreverse json--path)
+                                    :match-start start
+                                    :match-end (point))))
+          (pop json--path)))
       (setq elements (json-add-to-object elements key value))
       (json-skip-whitespace)
       (unless (char-equal (json-peek) ?})
@@ -509,7 +562,17 @@ json-read-array
   ;; read values until "]"
   (let (elements)
     (while (not (char-equal (json-peek) ?\]))
-      (push (json-read) elements)
+      (json-skip-whitespace)
+      (when json--path-to-position
+        (push (length elements) json--path))
+      (let ((start (point)))
+        (push (json-read) elements)
+        (when json--path-to-position
+          (when (< start json--path-to-position (+ (point) 1))
+            (throw :json-path (list :path (nreverse json--path)
+                                    :match-start start
+                                    :match-end (point))))
+          (pop json--path)))
       (json-skip-whitespace)
       (unless (char-equal (json-peek) ?\])
         (if (char-equal (json-peek) ?,)
diff --git a/test/automated/json-tests.el b/test/automated/json-tests.el
index d1b7a2f..e0672dd 100644
--- a/test/automated/json-tests.el
+++ b/test/automated/json-tests.el
@@ -49,5 +49,19 @@
   (should (equal (json-read-from-string "\"\\nasd\\u0444\\u044b\\u0432fgh\\t\"")
                  "\nasdфывfgh\t")))
 
+(ert-deftest test-json-path-to-position-with-objects ()
+  (let* ((json-string "{\"foo\": {\"bar\": {\"baz\": \"value\"}}}")
+         (matched-path (json-path-to-position 32 json-string)))
+    (should (equal (plist-get matched-path :path) '("foo" "bar" "baz")))
+    (should (equal (plist-get matched-path :match-start) 25))
+    (should (equal (plist-get matched-path :match-end) 32))))
+
+(ert-deftest test-json-path-to-position-with-arrays ()
+  (let* ((json-string "{\"foo\": [\"bar\", [\"baz\"]]}")
+         (matched-path (json-path-to-position 20 json-string)))
+    (should (equal (plist-get matched-path :path) '("foo" 1 0)))
+    (should (equal (plist-get matched-path :match-start) 18))
+    (should (equal (plist-get matched-path :match-end) 23))))
+
 (provide 'json-tests)
 ;;; json-tests.el ends here
-- 
2.6.1