dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

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