From 8ea6f6da7739adb054871da80a073672f6c706d7 Mon Sep 17 00:00:00 2001 From: Binbin Ye Date: Tue, 13 Jan 2026 22:40:24 +0900 Subject: [PATCH] Add JSON path utility command to json-ts-mode * lisp/progmodes/json-ts-mode.el (json-ts--get-path-at-node) (json-ts--path-to-jq, json-ts--path-to-python): New functions. (json-ts-jq-path-at-point): New command for getting JSON path at point. * test/lisp/progmodes/json-ts-mode-tests.el: New file. Add tests for the utility command. * etc/NEWS: Announce new command 'json-ts-jq-path-at-point' (bug#80190). --- etc/NEWS | 6 ++ lisp/progmodes/json-ts-mode.el | 54 ++++++++++++++ test/lisp/progmodes/json-ts-mode-tests.el | 86 +++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 test/lisp/progmodes/json-ts-mode-tests.el diff --git a/etc/NEWS b/etc/NEWS index 32b5ff02cc1..25641b45a4b 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -1329,6 +1329,12 @@ available. Now method chaining is indented by 8 spaces rather than 4, and this option controls how much is indented for method chaining. +** JSON-ts mode + +*** New command 'json-ts-jq-path-at-point'. +This command copies the path of the JSON element at point to the +kill-ring, formatted for use with the 'jq' utility. + ** PHP-ts mode --- diff --git a/lisp/progmodes/json-ts-mode.el b/lisp/progmodes/json-ts-mode.el index 0f9f4f4f6a7..cd4cb468095 100644 --- a/lisp/progmodes/json-ts-mode.el +++ b/lisp/progmodes/json-ts-mode.el @@ -128,6 +128,60 @@ Return nil if there is no name or if NODE is not a defun node." t) "\"" "\"")))) +(defun json-ts--get-path-at-node (node) + "Get the path from the root of the JSON tree to NODE. +Return a list of keys (strings) and indices (numbers). +NODE is a tree-sitter node." + (let ((path nil) + (parent nil)) + (while (setq parent (treesit-node-parent node)) + (let ((type (treesit-node-type parent))) + (cond + ((equal type "array") + (push (treesit-node-index node t) path)) + ((equal type "pair") + (let ((key (treesit-node-child-by-field-name parent "key"))) + (push (treesit-node-text key t) path))))) + (setq node parent)) + path)) + +(defun json-ts--path-to-jq (path) + "Convert PATH list to a jq-style path string. +PATH is a list of keys (strings) and indices (numbers)." + (mapconcat + (lambda (x) + (cond + ((numberp x) (format "[%d]" x)) + ((stringp x) + (let ((key (string-trim x "\"" "\""))) + (if (string-match-p (rx bos (any alpha "_") (* (any alnum "_")) eos) key) + (format ".%s" key) + (format "[%S]" key)))) + (t ""))) + path + "")) + +(defun json-ts--path-to-python (path) + "Convert PATH list to a Python-style path string. +PATH is a list of keys (strings) and indices (numbers)." + (mapconcat + (lambda (x) + (cond + ((numberp x) (format "[%d]" x)) + ((stringp x) (format "[\"%s\"]" x)) + (t ""))) + path + "")) + +(defun json-ts-jq-path-at-point () + "Show the JSON path at point in jq format." + (interactive) + (if-let* ((node (treesit-node-at (point)))) + (let ((path (json-ts--path-to-jq (json-ts--get-path-at-node node)))) + (kill-new path) + (message "%s" path)) + (user-error "No JSON node at point"))) + ;;;###autoload (define-derived-mode json-ts-mode prog-mode "JSON" "Major mode for editing JSON, powered by tree-sitter." diff --git a/test/lisp/progmodes/json-ts-mode-tests.el b/test/lisp/progmodes/json-ts-mode-tests.el new file mode 100644 index 00000000000..4fe4582f2f1 --- /dev/null +++ b/test/lisp/progmodes/json-ts-mode-tests.el @@ -0,0 +1,86 @@ +;;; json-ts-mode-tests.el --- Tests for json-ts-mode.el -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; Tests for json-ts-mode. + +;;; Code: + +(require 'ert) +(require 'treesit) +(require 'json-ts-mode) + +(ert-deftest json-ts-mode-test-path-at-point () + "Test `json-ts--get-path-at-node' and `json-ts--path-to-jq'." + (skip-unless (treesit-language-available-p 'json)) + (with-temp-buffer + (json-ts-mode) + (insert "{\"a\": [1, {\"b\": 2}, 3]}") + + ;; Point at '1' (index 0 of array 'a') + (goto-char (point-min)) + (search-forward "1") + (backward-char) + (should (equal (json-ts--path-to-jq (json-ts--get-path-at-node (treesit-node-at (point)))) + ".a[0]")) + + ;; Point at '2' (key 'b' inside object at index 1) + (goto-char (point-min)) + (search-forward "2") + (backward-char) + (should (equal (json-ts--path-to-jq (json-ts--get-path-at-node (treesit-node-at (point)))) + ".a[1].b")) + + ;; Point at '3' (index 2 of array 'a') + (goto-char (point-min)) + (search-forward "3") + (backward-char) + (should (equal (json-ts--path-to-jq (json-ts--get-path-at-node (treesit-node-at (point)))) + ".a[2]")))) + +(ert-deftest json-ts-mode-test-path-at-point-complex-keys () + "Test path generation with complex keys." + (skip-unless (treesit-language-available-p 'json)) + (with-temp-buffer + (json-ts-mode) + (insert "{\"key.with.dot\": {\"key with space\": 1}}") + + (goto-char (point-min)) + (search-forward "1") + (backward-char) + (should (equal (json-ts--path-to-jq (json-ts--get-path-at-node (treesit-node-at (point)))) + "[\"key.with.dot\"][\"key with space\"]")))) + +(ert-deftest json-ts-mode-test-jq-path-keys () + "Test `json-ts--path-to-jq' with various key formats." + (should (equal (json-ts--path-to-jq '("v123")) ".v123")) + (should (equal (json-ts--path-to-jq '("-123")) "[\"-123\"]")) + (should (equal (json-ts--path-to-jq '("v_v")) ".v_v")) + (should (equal (json-ts--path-to-jq '("123")) "[\"123\"]")) + (should (equal (json-ts--path-to-jq '("_123")) "._123")) + (should (equal (json-ts--path-to-jq '("1v2")) "[\"1v2\"]"))) + +(ert-deftest json-ts-mode-test-path-to-python () + "Test `json-ts--path-to-python'." + (should (equal (json-ts--path-to-python '("a" 0 "b")) + "[\"a\"][0][\"b\"]"))) + +(provide 'json-ts-mode-tests) +;;; json-ts-mode-tests.el ends here