jq-mode.el (11018B)
1 ;;; jq-mode.el --- Edit jq scripts. -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2015--2018 Bjarte Johansen 4 5 ;; Author: Bjarte Johansen <Bjarte dot Johansen at gmail dot com> 6 ;; Homepage: https://github.com/ljos/jq-mode 7 ;; Package-Requires: ((emacs "25.1")) 8 ;; Version: 0.4.1 9 10 ;; This file is not part of GNU Emacs. 11 12 ;; This program is free software; you can redistribute it and/or modify 13 ;; it under the terms of the GNU General Public License as published by 14 ;; the Free Software Foundation, either version 3 of the License, or 15 ;; (at your option) any later version. 16 17 ;; This program is distributed in the hope that it will be useful, 18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 ;; GNU General Public License for more details. 21 22 ;; You should have received a copy of the GNU General Public License 23 ;; along with jq-mode. If not, see <http://www.gnu.org/licenses/>. 24 25 ;;; Commentary: 26 27 ;; Mode for editing jq queries. 28 29 ;;; Usage: 30 31 ;; Add to your Emacs config: 32 33 ;; (add-to-list 'load-path "/path/to/jq-mode-dir") 34 ;; (autoload 'jq-mode "jq-mode.el" 35 ;; "Major mode for editing jq files" t) 36 ;; (add-to-list 'auto-mode-alist '("\\.jq\\'" . jq-mode)) 37 38 ;;; Code: 39 (defgroup jq nil 40 "Major mode for editing jq queries." 41 :group 'languages) 42 43 (defcustom jq-indent-offset 2 44 "*Indentation offset for `jq-mode'." 45 :group 'jq 46 :type 'integer) 47 48 (defconst jq--keywords 49 '("as" 50 "break" 51 "catch" 52 "def" 53 "elif" "else" "end" 54 "foreach" 55 "if" "import" "include" 56 "label" 57 "module" 58 "reduce" 59 "then" "try") 60 "The keywords used in jq.") 61 62 (defun jq-indent-line () 63 "Indent current line as a jq-script." 64 (interactive) 65 (let ((indent-column 0)) 66 (save-mark-and-excursion 67 (if (> 0 (forward-line -1)) 68 (setq indent-column (current-indentation)) 69 (end-of-line) 70 (or (search-backward ";" (line-beginning-position) t) 71 (back-to-indentation)) 72 (skip-chars-forward "[:space:]" (line-end-position)) 73 (cond ((looking-at-p "#.*$") 74 (setq indent-column (current-indentation))) 75 ((looking-at-p 76 (concat (regexp-opt (remove "end" jq--keywords)) "\\b")) 77 (setq indent-column (+ indent-column jq-indent-offset)))))) 78 (save-mark-and-excursion 79 (back-to-indentation) 80 (save-mark-and-excursion 81 (let ((extra (if (looking-at-p ")\\|}\\|\\]") 0 1))) 82 (ignore-errors 83 (up-list -1) 84 (when (looking-at-p "(\\|{\\|\\[") 85 (setq indent-column (+ extra (current-column))))))) 86 (when (looking-at-p "|") 87 (setq indent-column (+ indent-column jq-indent-offset))) 88 (end-of-line) 89 (delete-horizontal-space) 90 (indent-line-to indent-column))) 91 (when (let ((search-spaces-regexp t)) 92 (string-match-p "^ *$" 93 (buffer-substring-no-properties 94 (line-beginning-position) 95 (point)))) 96 (skip-chars-forward "[:space:]" (line-end-position)))) 97 98 (defconst jq--builtins 99 '("IN" "INDEX" 100 "JOIN" 101 "acos" "acosh" "add" "all" "any" "arrays" "ascii_downcase" "ascii_upcase" 102 "asin" "asinh" "atan" "atan2" "atanh" 103 "booleans" "bsearch" "builtins" 104 "capture" "cbrt" "ceil" "combinations" "contains" "copysign" "cos" "cosh" 105 "debug" "del" "delpaths" "drem" 106 "empty" "endswith" "env" "erf" "erfc" "error" "exp" "exp10" "exp2" 107 "explode" "expm1" 108 "fabs" "fdim" "finites" "first" "flatten" "floor" "fma" "fmax" "fmin" 109 "fmod" "format" "frexp" "from_entries" "fromdate" "fromdateiso8601" 110 "fromjson" "fromstream" 111 "gamma" "get_jq_origin" "get_prog_origin" "get_search_list" "getpath" 112 "gmtime" "group_by" "gsub" 113 "halt" "halt_error" "has" "hypot" 114 "implode" "in" "index" "indices" "infinite" "input" "input_filename" 115 "input_line_number" "inputs" "inside" "isempty" "isfinite" "isinfinite" 116 "isnan" "isnormal" "iterables" 117 "j0" "j1" "jn" "join" 118 "keys" "keys_unsorted" 119 "last" "ldexp" "leaf_paths" "length" "lgamma" "lgamma_r" "limit" 120 "localtime" "log" "log10" "log1p" "log2" "logb" "ltrimstr" 121 "map" "map_values" "match" "max" "max_by" "min" "min_by" "mktime" "modf" 122 "modulemeta" 123 "nan" "nearbyint" "nextafter" "nexttoward" "normals" "not" "now" "nth" 124 "nulls" "numbers" 125 "objects" 126 "path" "paths" "pow" "pow10" 127 "range" "recurse" "recurse_down" "remainder" "repeat" "reverse" "rindex" 128 "rint" "round" "rtrimstr" 129 "scalars" "scalars_or_empty" "scalb" "scalbln" "scan" "select" "setpath" 130 "significand" "sin" "sinh" "sort" "sort_by" "split" "splits" "sqrt" 131 "startswith" "stderr" "strflocaltime" "strftime" "strings" "strptime" "sub" 132 "tan" "tanh" "test" "tgamma" "to_entries" "todate" "todateiso8601" "tojson" 133 "tonumber" "tostream" "tostring" "transpose" "trunc" "truncate_stream" 134 "type" 135 "unique" "unique_by" "until" "utf8bytelength" 136 "values" 137 "walk" "while" "with_entries" 138 "y0" "y1" "yn") 139 "All builtin functions in jq.") 140 141 142 (defconst jq--escapings 143 '("text" "json" "html" "uri" "csv" "tsv" "sh" "base64") 144 "Jq escaping directives.") 145 146 (defconst jq-font-lock-keywords 147 `( ;; Variables 148 ("\\$\\w+" 0 font-lock-variable-name-face) 149 ;; Format strings and escaping 150 (,(concat "@" (regexp-opt jq--escapings) "\\b") . font-lock-type-face) 151 ;; Keywords 152 ,(concat "\\b" (regexp-opt jq--keywords) "\\b") 153 ;; Functions 154 ("\\bdef\\s-*\\([_[:alnum:]]+\\)\\s-*\(" (1 font-lock-function-name-face)))) 155 156 (defvar jq-mode-map 157 (let ((map (make-sparse-keymap))) 158 map) 159 "Keymap for `jq-mode'.") 160 161 (defvar jq-mode-syntax-table 162 (let ((syntax-table (make-syntax-table))) 163 ;; Strings 164 (modify-syntax-entry ?\" "\"\"" syntax-table) 165 166 ;; Comments 167 (modify-syntax-entry ?# "<" syntax-table) 168 (modify-syntax-entry ?\n ">" syntax-table) 169 syntax-table) 170 "Syntax table for `jq-mode.'") 171 172 (defun jq-completion-at-point () 173 (when-let ((bnds (bounds-of-thing-at-point 'symbol))) 174 (unless (eq ?$ (char-before (car bnds))) ; Ignore variables 175 (list (car bnds) (cdr bnds) jq--builtins)))) 176 177 (defvar company-keywords-alist) 178 (with-eval-after-load 'company-keywords 179 (add-to-list 'company-keywords-alist 180 `(jq-mode . ,(append jq--keywords 181 jq--builtins)))) 182 183 ;;;###autoload 184 (define-derived-mode jq-mode prog-mode "jq" 185 "Major mode for jq scripts. 186 \\{jq-mode-map}" 187 :group 'jq 188 (setq-local indent-line-function #'jq-indent-line) 189 (setq-local font-lock-defaults '(jq-font-lock-keywords)) 190 (setq-local comment-start "# ") 191 (add-hook 'completion-at-point-functions #'jq-completion-at-point nil t)) 192 193 ;;; jq-interactively 194 (defgroup jq-interactive nil 195 "Major mode for editing json with jq." 196 :group 'languages) 197 198 (defcustom jq-interactive-command "jq" 199 "Command to use for calling jq." 200 :group 'jq-interactive 201 :type 'string) 202 203 (defcustom jq-interactive-default-options "" 204 "Command line options to pass to jq." 205 :group 'jq-interactive 206 :type 'string) 207 208 (defcustom jq-interactive-default-prompt "jq: " 209 "Default prompt to use in minibuffer." 210 :group 'jq-interactive 211 :type 'string) 212 213 (defvar jq-interactive-history nil) 214 215 (defvar jq-interactive--last-minibuffer-contents "") 216 (defvar jq-interactive--positions nil) 217 (defvar jq-interactive--buffer nil) 218 (defvar jq-interactive--overlay nil) 219 220 (defun jq-interactive--run-command () 221 (with-temp-buffer 222 (let ((output (current-buffer))) 223 (with-current-buffer jq-interactive--buffer 224 (call-process-region 225 (point-min) 226 (point-max) 227 shell-file-name 228 nil 229 output 230 nil 231 shell-command-switch 232 (format "%s %s %s" 233 jq-interactive-command 234 jq-interactive-default-options 235 (shell-quote-argument 236 jq-interactive--last-minibuffer-contents)))) 237 (ignore-errors 238 (json-mode) 239 (font-lock-fontify-region (point-min) (point-max))) 240 (buffer-string)))) 241 242 (defun jq-interactive--feedback () 243 (save-mark-and-excursion 244 (let ((font-lock-defaults '(jq-font-lock-keywords))) 245 (font-lock-fontify-region (point) (point-max)))) 246 (with-current-buffer jq-interactive--buffer 247 (overlay-put jq-interactive--overlay 248 'after-string 249 (jq-interactive--run-command)))) 250 251 (defun jq-interactive--minibuffer-setup () 252 (setq-local font-lock-defaults '(jq-font-lock-keywords))) 253 254 (defun jq-interactive--quit () 255 (remove-hook 'after-change-functions #'jq-interactive--update) 256 (remove-hook 'minibuffer-setup-hook #'jq-interactive--minibuffer-setup) 257 (delete-overlay jq-interactive--overlay)) 258 259 (defun jq-interactive--update (_beg _end _len) 260 (unless (> (minibuffer-depth) 1) 261 (let ((contents (minibuffer-contents-no-properties))) 262 (unless (or (not (minibufferp)) 263 (and (string= "" contents) 264 (equal last-command 'previous-history-element)) 265 (string= contents jq-interactive--last-minibuffer-contents)) 266 (setq jq-interactive--last-minibuffer-contents contents) 267 (jq-interactive--feedback))))) 268 269 (defun jq-interactive-indent-line () 270 "Indents a jq expression in the jq-interactive mini-buffer." 271 (interactive) 272 (jq-indent-line) 273 (save-mark-and-excursion 274 (beginning-of-line) 275 (insert-char ?\s (length jq-interactive-default-prompt))) 276 (skip-chars-forward "[:space:]")) 277 278 (defvar jq-interactive-map 279 (let ((map (make-sparse-keymap))) 280 (set-keymap-parent map minibuffer-local-map) 281 (define-key map (kbd "<tab>") #'jq-interactive-indent-line) 282 (define-key map (kbd "C-j") #'electric-newline-and-maybe-indent) 283 map) 284 "Keymap for `jq-interactively'.") 285 286 ;;;###autoload 287 (defun jq-interactively (beg end) 288 "Runs jq interactively on a json buffer." 289 (interactive 290 (if (region-active-p) 291 (list (region-beginning) 292 (region-end)) 293 (list (point-min) 294 (point-max)))) 295 (unwind-protect 296 (progn 297 (setq jq-interactive--overlay (make-overlay beg end)) 298 (overlay-put jq-interactive--overlay 'invisible t) 299 (setq jq-interactive--positions (cons beg end)) 300 (setq jq-interactive--buffer (current-buffer)) 301 (setq jq-interactive--last-minibuffer-contents "") 302 (jq-interactive--feedback) 303 (add-hook 'after-change-functions #'jq-interactive--update) 304 (add-hook 'minibuffer-setup-hook #'jq-interactive--minibuffer-setup) 305 (save-mark-and-excursion 306 (deactivate-mark) 307 (read-from-minibuffer 308 jq-interactive-default-prompt 309 nil 310 jq-interactive-map 311 nil 312 jq-interactive-history)) 313 (goto-char beg) 314 (delete-region beg end) 315 (insert (plist-get (overlay-properties jq-interactive--overlay) 316 'after-string))) 317 (jq-interactive--quit))) 318 319 (provide 'jq-mode) 320 321 ;;; jq-mode.el ends here