├── README.md └── json-mode.el /README.md: -------------------------------------------------------------------------------- 1 | json-mode.el 2 | ==== 3 | 4 | Major mode for editing JSON files. 5 | 6 | Extends the builtin js-mode to add better syntax highlighting for JSON 7 | and some nice editing keybindings. 8 | 9 | Install 10 | ---- 11 | 12 | ``` 13 | M-x package-install json-mode 14 | ``` 15 | 16 | You need to have the [MELPA repository](https://melpa.org/) or [MELPA Stable repository](https://stable.melpa.org/) enabled in emacs for this to work. 17 | 18 | Default Keybindings 19 | ---- 20 | 21 | - `C-c C-f`: format the region/buffer with `json-pretty-print` () 22 | - `C-c C-p`: display a path to the object at point with `json-snatcher` () 23 | - `C-c P`: copy a path to the object at point to the kill ring with `json-snatcher` () 24 | - `C-c C-t`: Toggle between `true` and `false` at point 25 | - `C-c C-k`: Replace the sexp at point with `null` 26 | - `C-c C-i`: Increment the number at point 27 | - `C-c C-d`: Decrement the number at point 28 | 29 | Indent Width 30 | ---- 31 | 32 | Customize `js-indent-level`. 33 | 34 | JSON With Comments 35 | --- 36 | 37 | In addition to JSON files, this package provides `jsonc-mode` for editing JSON with commas and comments (sometimes referred to as huJSON or JWCC). 38 | 39 | License 40 | ---- 41 | 42 | This program 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. 43 | 44 | This program 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. 45 | 46 | You should have received a copy of the GNU General Public License along with this program. If not, see . 47 | -------------------------------------------------------------------------------- /json-mode.el: -------------------------------------------------------------------------------- 1 | ;;; json-mode.el --- Major mode for editing JSON files -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2011-2023 Josh Johnston, taku0 4 | 5 | ;; Author: Josh Johnston 6 | ;; taku0 7 | ;; URL: https://github.com/joshwnj/json-mode 8 | ;; Version: 1.9.2 9 | ;; Package-Requires: ((json-snatcher "1.0.0") (emacs "24.4")) 10 | 11 | ;; This program is free software; you can redistribute it and/or modify 12 | ;; it under the terms of the GNU General Public License as published by 13 | ;; the Free Software Foundation, either version 3 of the License, or 14 | ;; (at your option) any later version. 15 | 16 | ;; This program is distributed in the hope that it will be useful, 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | ;; GNU General Public License for more details. 20 | 21 | ;; You should have received a copy of the GNU General Public License 22 | ;; along with this program. If not, see . 23 | 24 | ;;; Commentary: 25 | 26 | ;; extend the builtin js-mode's syntax highlighting 27 | 28 | ;;; Code: 29 | 30 | (require 'js) 31 | (require 'rx) 32 | (require 'json-snatcher) 33 | 34 | (defgroup json '() 35 | "Major mode for editing JSON files." 36 | :group 'js) 37 | 38 | ;;;###autoload 39 | (defconst json-mode-standard-file-ext '(".json" ".jsonld") 40 | "List of JSON file extensions.") 41 | 42 | ;; This is to be sure the customization is loaded. Otherwise, 43 | ;; autoload discards any defun or defcustom. 44 | ;;;###autoload 45 | (defsubst json-mode--update-auto-mode (filenames) 46 | "Update the `json-mode' entry of `auto-mode-alist'. 47 | 48 | FILENAMES should be a list of file as string. 49 | Return the new `auto-mode-alist' entry" 50 | (let* ((new-regexp 51 | (rx-to-string 52 | `(seq (eval 53 | (cons 'or 54 | (append json-mode-standard-file-ext 55 | ',filenames))) eot))) 56 | (new-entry (cons new-regexp 'json-mode)) 57 | (old-entry (when (boundp 'json-mode--auto-mode-entry) 58 | json-mode--auto-mode-entry))) 59 | (setq auto-mode-alist (delete old-entry auto-mode-alist)) 60 | (add-to-list 'auto-mode-alist new-entry) 61 | new-entry)) 62 | 63 | ;;; make byte-compiler happy 64 | (defvar json-mode--auto-mode-entry) 65 | 66 | ;;;###autoload 67 | (defcustom json-mode-auto-mode-list '(".babelrc" 68 | ".bowerrc" 69 | "composer.lock") 70 | "List of filenames for the JSON entry of `auto-mode-alist'. 71 | 72 | Note however that custom `json-mode' entries in `auto-mode-alist' 73 | won’t be affected." 74 | :group 'json 75 | :type '(repeat string) 76 | :set (lambda (symbol value) 77 | "Update SYMBOL with a new regexp made from VALUE. 78 | 79 | This function calls `json-mode--update-auto-mode' to change the 80 | `json-mode--auto-mode-entry' entry in `auto-mode-alist'." 81 | (set-default symbol value) 82 | (setq json-mode--auto-mode-entry (json-mode--update-auto-mode value)))) 83 | 84 | ;; Autoload needed to initalize the the `auto-list-mode' entry. 85 | ;;;###autoload 86 | (defvar json-mode--auto-mode-entry (json-mode--update-auto-mode json-mode-auto-mode-list) 87 | "Regexp generated from the `json-mode-auto-mode-list'.") 88 | 89 | (defconst json-mode-quoted-string-re 90 | (rx (group (char ?\") 91 | (zero-or-more (or (seq ?\\ ?\\) 92 | (seq ?\\ ?\") 93 | (seq ?\\ (not (any ?\" ?\\))) 94 | (not (any ?\" ?\\)))) 95 | (char ?\")))) 96 | (defconst json-mode-quoted-key-re 97 | (rx (group (char ?\") 98 | (zero-or-more (or (seq ?\\ ?\\) 99 | (seq ?\\ ?\") 100 | (seq ?\\ (not (any ?\" ?\\))) 101 | (not (any ?\" ?\\)))) 102 | (char ?\")) 103 | (zero-or-more blank) 104 | ?\:)) 105 | (defconst json-mode-number-re (rx (group (optional ?-) 106 | (one-or-more digit) 107 | (optional ?\. (one-or-more digit))))) 108 | (defconst json-mode-keyword-re (rx (group (or "true" "false" "null")))) 109 | 110 | (defconst json-font-lock-keywords-1 111 | (list 112 | (list json-mode-keyword-re 1 font-lock-constant-face) 113 | (list json-mode-number-re 1 font-lock-constant-face)) 114 | "Level one font lock.") 115 | 116 | (defvar json-mode-syntax-table 117 | (let ((st (make-syntax-table))) 118 | ;; Objects 119 | (modify-syntax-entry ?\{ "(}" st) 120 | (modify-syntax-entry ?\} "){" st) 121 | ;; Arrays 122 | (modify-syntax-entry ?\[ "(]" st) 123 | (modify-syntax-entry ?\] ")[" st) 124 | ;; Strings 125 | (modify-syntax-entry ?\" "\"" st) 126 | ;; Comments 127 | (modify-syntax-entry ?\n ">" st) 128 | ;; Dot in floating point number literal. 129 | (modify-syntax-entry ?. "_" st) 130 | st)) 131 | 132 | (defvar json-mode--string-syntax-table 133 | (let ((st (copy-syntax-table json-mode-syntax-table))) 134 | (modify-syntax-entry ?. "." st) 135 | st) 136 | "Syntax table for strings.") 137 | 138 | (defvar jsonc-mode-syntax-table 139 | (let ((st (copy-syntax-table json-mode-syntax-table))) 140 | ;; Comments 141 | (modify-syntax-entry ?/ ". 124" st) 142 | (modify-syntax-entry ?\n ">" st) 143 | (modify-syntax-entry ?\^m ">" st) 144 | (modify-syntax-entry ?* ". 23bn" st) 145 | st)) 146 | 147 | (defvar jsonc-mode--string-syntax-table 148 | (let ((st (copy-syntax-table jsonc-mode-syntax-table))) 149 | (modify-syntax-entry ?. "." st) 150 | st) 151 | "Syntax table for strings and comments.") 152 | 153 | (defun json-mode--syntactic-face (state) 154 | "Return syntactic face function for the position represented by STATE. 155 | STATE is a `parse-partial-sexp' state, and the returned function is the 156 | json font lock syntactic face function." 157 | (cond 158 | ((nth 3 state) 159 | ;; This might be a string or a name 160 | (let ((startpos (nth 8 state))) 161 | (save-excursion 162 | (goto-char startpos) 163 | (if (looking-at-p json-mode-quoted-key-re) 164 | font-lock-keyword-face 165 | font-lock-string-face)))) 166 | ((nth 4 state) font-lock-comment-face))) 167 | 168 | (defun json-mode-forward-sexp (&optional arg) 169 | "Move point forward an atom or balanced bracket. 170 | 171 | See `forward-sexp for ARG." 172 | (interactive "p") 173 | (unless arg 174 | (setq arg 1)) 175 | (let ((forward-sexp-function nil) 176 | (sign (if (< arg 0) -1 1)) 177 | state) 178 | (while (not (zerop arg)) 179 | (setq state (syntax-ppss)) 180 | (if (nth 8 state) 181 | ;; Inside a string or comment. 182 | (progn 183 | (with-syntax-table 184 | (if (eq major-mode 'jsonc-mode) 185 | jsonc-mode--string-syntax-table 186 | json-mode--string-syntax-table) 187 | (forward-sexp sign))) 188 | (forward-sexp sign)) 189 | (setq arg (- arg sign))))) 190 | 191 | ;;;###autoload 192 | (define-derived-mode json-mode javascript-mode "JSON" 193 | "Major mode for editing JSON files." 194 | :syntax-table json-mode-syntax-table 195 | (setq font-lock-defaults 196 | '(json-font-lock-keywords-1 197 | nil nil nil nil 198 | (font-lock-syntactic-face-function . json-mode--syntactic-face))) 199 | (setq-local forward-sexp-function #'json-mode-forward-sexp)) 200 | 201 | ;;;###autoload 202 | (define-derived-mode jsonc-mode json-mode "JSONC" 203 | "Major mode for editing JSON files with comments." 204 | :syntax-table jsonc-mode-syntax-table) 205 | 206 | ;; Well formatted JSON files almost always begin with “{” or “[”. 207 | ;;;###autoload 208 | (add-to-list 'magic-fallback-mode-alist '("^[{[]$" . json-mode)) 209 | 210 | ;;;###autoload 211 | (defun json-mode-show-path () 212 | "Print the path to the node at point to the minibuffer." 213 | (interactive) 214 | (message (jsons-print-path))) 215 | 216 | (define-key json-mode-map (kbd "C-c C-p") 'json-mode-show-path) 217 | 218 | ;;;###autoload 219 | (defun json-mode-kill-path () 220 | "Save JSON path to object at point to kill ring." 221 | (interactive) 222 | (kill-new (jsons-print-path))) 223 | 224 | (define-key json-mode-map (kbd "C-c C-k") 'json-mode-kill-path) 225 | 226 | ;;;###autoload 227 | (defun json-mode-beautify (begin end) 228 | "Beautify/pretty-print from BEGIN to END. 229 | 230 | If the region is not active, beautify the entire buffer ." 231 | (interactive "r") 232 | (unless (use-region-p) 233 | (setq begin (point-min) 234 | end (point-max))) 235 | (json-pretty-print begin end)) 236 | 237 | (define-key json-mode-map (kbd "C-c C-f") 'json-mode-beautify) 238 | 239 | (defun json-toggle-boolean () 240 | "If point is on `true' or `false', toggle it." 241 | (interactive) 242 | (unless (nth 8 (syntax-ppss)) ; inside a keyword, string or comment 243 | (let* ((bounds (bounds-of-thing-at-point 'symbol)) 244 | (string (and bounds (buffer-substring-no-properties (car bounds) (cdr bounds)))) 245 | (pt (point))) 246 | (when (and bounds (member string '("true" "false"))) 247 | (delete-region (car bounds) (cdr bounds)) 248 | (cond 249 | ((string= "true" string) 250 | (insert "false") 251 | (goto-char (if (= pt (cdr bounds)) (1+ pt) pt))) 252 | (t 253 | (insert "true") 254 | (goto-char (if (= pt (cdr bounds)) (1- pt) pt)))))))) 255 | 256 | (define-key json-mode-map (kbd "C-c C-t") 'json-toggle-boolean) 257 | 258 | (defun json-nullify-sexp () 259 | "Replace the sexp at point with `null'." 260 | (interactive) 261 | (let ((syntax (syntax-ppss)) symbol) 262 | (cond 263 | ((nth 4 syntax) nil) ; inside a comment 264 | ((nth 3 syntax) ; inside a string 265 | (goto-char (nth 8 syntax)) 266 | (when (save-excursion (forward-sexp) (skip-chars-forward "[:space:]") (eq (char-after) ?:)) 267 | ;; sexp is an object key, so we nullify the entire object 268 | (goto-char (nth 1 syntax))) 269 | (kill-sexp) 270 | (insert "null")) 271 | ((setq symbol (bounds-of-thing-at-point 'symbol)) 272 | (cond 273 | ((looking-at-p "null")) 274 | ((save-excursion (skip-chars-backward "-0-9.") (looking-at json-mode-number-re)) 275 | (kill-region (match-beginning 0) (match-end 0)) 276 | (insert "null")) 277 | (t (kill-region (car symbol) (cdr symbol)) (insert "null")))) 278 | ((< 0 (nth 0 syntax)) 279 | (goto-char (nth 1 syntax)) 280 | (kill-sexp) 281 | (insert "null")) 282 | (t nil)))) 283 | 284 | (define-key json-mode-map (kbd "C-c C-k") 'json-nullify-sexp) 285 | 286 | (defun json-increment-number-at-point (&optional delta) 287 | "Add DELTA to the number at point; DELTA defaults to 1." 288 | (interactive "P") 289 | (when (save-excursion (skip-chars-backward "-0-9.") (looking-at json-mode-number-re)) 290 | (let ((num (+ (or delta 1) 291 | (string-to-number (buffer-substring-no-properties (match-beginning 0) (match-end 0))))) 292 | (pt (point))) 293 | (delete-region (match-beginning 0) (match-end 0)) 294 | (insert (number-to-string num)) 295 | (goto-char pt)))) 296 | 297 | (define-key json-mode-map (kbd "C-c C-i") 'json-increment-number-at-point) 298 | 299 | (defun json-decrement-number-at-point (&optional delta) 300 | "Subtract DELTA from the number at point; DELTA defaults to 1." 301 | (interactive "P") 302 | (json-increment-number-at-point (- (or delta 1)))) 303 | 304 | (define-key json-mode-map (kbd "C-c C-d") 'json-decrement-number-at-point) 305 | 306 | (provide 'json-mode) 307 | ;;; json-mode.el ends here 308 | --------------------------------------------------------------------------------