dotemacs

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

yaml-mode.el (18356B)


      1 ;;; yaml-mode.el --- Major mode for editing YAML files -*- lexical-binding: t -*-
      2 
      3 ;; Copyright (C) 2010-2014 Yoshiki Kurihara
      4 
      5 ;; Author: Yoshiki Kurihara <clouder@gmail.com>
      6 ;;         Marshall T. Vandegrift <llasram@gmail.com>
      7 ;; Maintainer: Vasilij Schneidermann <mail@vasilij.de>
      8 ;; URL: https://github.com/yoshiki/yaml-mode
      9 ;; Package-Requires: ((emacs "24.1"))
     10 ;; Keywords: data yaml
     11 ;; Version: 0.0.16
     12 
     13 ;; This file is not part of Emacs
     14 
     15 ;; This program is free software; you can redistribute it and/or modify
     16 ;; it under the terms of the GNU General Public License as published by
     17 ;; the Free Software Foundation, either version 3 of the License, or
     18 ;; (at your option) any later version.
     19 
     20 ;; This program is distributed in the hope that it will be useful,
     21 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     22 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     23 ;; GNU General Public License for more details.
     24 
     25 ;; You should have received a copy of the GNU General Public License
     26 ;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
     27 
     28 ;;; Commentary:
     29 
     30 ;; This is a major mode for editing files in the YAML data
     31 ;; serialization format.  It was initially developed by Yoshiki
     32 ;; Kurihara and many features were added by Marshall Vandegrift.  As
     33 ;; YAML and Python share the fact that indentation determines
     34 ;; structure, this mode provides indentation and indentation command
     35 ;; behavior very similar to that of python-mode.
     36 
     37 ;;; Installation:
     38 
     39 ;; To install, just drop this file into a directory in your
     40 ;; `load-path' and (optionally) byte-compile it.  To automatically
     41 ;; handle files ending in '.yml', add something like:
     42 ;;
     43 ;;    (require 'yaml-mode)
     44 ;;    (add-to-list 'auto-mode-alist '("\\.yml\\'" . yaml-mode))
     45 ;;
     46 ;; to your .emacs file.
     47 ;;
     48 ;; Unlike python-mode, this mode follows the Emacs convention of not
     49 ;; binding the ENTER key to `newline-and-indent'.  To get this
     50 ;; behavior, add the key definition to `yaml-mode-hook':
     51 ;;
     52 ;;    (add-hook 'yaml-mode-hook
     53 ;;     '(lambda ()
     54 ;;        (define-key yaml-mode-map "\C-m" 'newline-and-indent)))
     55 
     56 ;;; Known Bugs:
     57 
     58 ;; YAML is easy to write but complex to parse, and this mode doesn't
     59 ;; even really try.  Indentation and highlighting will break on
     60 ;; abnormally complicated structures.
     61 
     62 ;;; Code:
     63 
     64 
     65 ;; User definable variables
     66 
     67 ;;;###autoload
     68 (defgroup yaml nil
     69   "Support for the YAML serialization format"
     70   :group 'languages
     71   :prefix "yaml-")
     72 
     73 (defcustom yaml-mode-hook nil
     74   "*Hook run by `yaml-mode'."
     75   :type 'hook
     76   :group 'yaml)
     77 
     78 (defcustom yaml-indent-offset 2
     79   "*Amount of offset per level of indentation."
     80   :type 'integer
     81   :safe 'natnump
     82   :group 'yaml)
     83 
     84 (defcustom yaml-backspace-function 'backward-delete-char-untabify
     85   "*Function called by `yaml-electric-backspace' when deleting backwards.
     86 It will receive one argument, the numeric prefix value."
     87   :type 'function
     88   :group 'yaml)
     89 
     90 (defcustom yaml-block-literal-search-lines 100
     91   "*Maximum number of lines to search for start of block literals."
     92   :type 'integer
     93   :group 'yaml)
     94 
     95 (defcustom yaml-block-literal-electric-alist
     96   '((?| . "") (?> . "-"))
     97   "*Characters for which to provide electric behavior.
     98 The association list key should be a key code and the associated value
     99 should be a string containing additional characters to insert when
    100 that key is pressed to begin a block literal."
    101   :type 'alist
    102   :group 'yaml)
    103 
    104 (defface yaml-tab-face
    105   '((((class color)) (:background "red" :foreground "red" :bold t))
    106     (t (:reverse-video t)))
    107   "Face to use for highlighting tabs in YAML files."
    108   :group 'faces
    109   :group 'yaml)
    110 
    111 (defcustom yaml-imenu-generic-expression
    112   '((nil  "^\\(:?[a-zA-Z_-]+\\):"          1))
    113   "The imenu regex to parse an outline of the yaml file."
    114   :type 'string
    115   :group 'yaml)
    116 
    117 
    118 ;; Constants
    119 
    120 (defconst yaml-mode-version "0.0.15" "Version of `yaml-mode'.")
    121 
    122 (defconst yaml-blank-line-re "^ *$"
    123   "Regexp matching a line containing only (valid) whitespace.")
    124 
    125 (defconst yaml-directive-re "^\\(?:--- \\)? *%\\(\\w+\\)"
    126   "Regexp matching a line containing a YAML directive.")
    127 
    128 (defconst yaml-document-delimiter-re "^\\(?:---\\|[.][.][.]\\)"
    129   "Rexexp matching a YAML document delimiter line.")
    130 
    131 (defconst yaml-node-anchor-alias-re "[&*][a-zA-Z0-9_-]+"
    132   "Regexp matching a YAML node anchor or alias.")
    133 
    134 (defconst yaml-tag-re "!!?[^ \n]+"
    135   "Rexexp matching a YAML tag.")
    136 
    137 (defconst yaml-bare-scalar-re
    138   "\\(?:[^-:,#!\n{\\[ ]\\|[^#!\n{\\[ ]\\S-\\)[^#\n]*?"
    139   "Rexexp matching a YAML bare scalar.")
    140 
    141 (defconst yaml-hash-key-re
    142   (concat "\\(?:^\\(?:--- \\)?\\|{\\|\\(?:[-,] +\\)+\\) *"
    143           "\\(?:" yaml-tag-re " +\\)?"
    144           "\\(" yaml-bare-scalar-re "\\) *:"
    145           "\\(?: +\\|$\\)")
    146   "Regexp matching a single YAML hash key.")
    147 
    148 (defconst yaml-scalar-context-re
    149   (concat "\\(?:^\\(?:--- \\)?\\|{\\|\\(?: *[-,] +\\)+\\) *"
    150           "\\(?:" yaml-bare-scalar-re " *: \\)?")
    151   "Regexp indicating the beginning of a scalar context.")
    152 
    153 (defconst yaml-nested-map-re
    154   (concat "[^#\n]*: *\\(?:&.*\\|{ *\\|" yaml-tag-re " *\\)?$")
    155   "Regexp matching a line beginning a YAML nested structure.")
    156 
    157 (defconst yaml-block-literal-base-re " *[>|][-+0-9]* *\\(?:\n\\|\\'\\)"
    158   "Regexp matching the substring start of a block literal.")
    159 
    160 (defconst yaml-block-literal-re
    161   (concat yaml-scalar-context-re
    162           "\\(?:" yaml-tag-re "\\)?"
    163           yaml-block-literal-base-re)
    164   "Regexp matching a line beginning a YAML block literal.")
    165 
    166 (defconst yaml-nested-sequence-re
    167   (concat "^\\(?:\\(?: *- +\\)+\\|\\(:? *-$\\)\\)"
    168           "\\(?:" yaml-bare-scalar-re " *:\\(?: +.*\\)?\\)?$")
    169   "Regexp matching a line containing one or more nested YAML sequences.")
    170 
    171 (defconst yaml-constant-scalars-re
    172   (concat "\\(?:^\\|\\(?::\\|-\\|,\\|{\\|\\[\\) +\\) *"
    173           (regexp-opt
    174            '("~" "null" "Null" "NULL"
    175              ".nan" ".NaN" ".NAN"
    176              ".inf" ".Inf" ".INF"
    177              "-.inf" "-.Inf" "-.INF"
    178              "y" "Y" "yes" "Yes" "YES" "n" "N" "no" "No" "NO"
    179              "true" "True" "TRUE" "false" "False" "FALSE"
    180              "on" "On" "ON" "off" "Off" "OFF") t)
    181           "\\_>")
    182   "Regexp matching certain scalar constants in scalar context.")
    183 
    184 
    185 ;; Mode setup
    186 
    187 (defvar yaml-mode-map
    188   (let ((map (make-sparse-keymap)))
    189     (define-key map "|" 'yaml-electric-bar-and-angle)
    190     (define-key map ">" 'yaml-electric-bar-and-angle)
    191     (define-key map "-" 'yaml-electric-dash-and-dot)
    192     (define-key map "." 'yaml-electric-dash-and-dot)
    193     (define-key map (kbd "DEL") 'yaml-electric-backspace)
    194     map)
    195   "Keymap used in `yaml-mode' buffers.")
    196 
    197 (defvar yaml-mode-syntax-table
    198   (let ((syntax-table (make-syntax-table)))
    199     (modify-syntax-entry ?\' "\"" syntax-table)
    200     (modify-syntax-entry ?\" "\"" syntax-table)
    201     (modify-syntax-entry ?# "<" syntax-table)
    202     (modify-syntax-entry ?\n ">" syntax-table)
    203     (modify-syntax-entry ?\\ "\\" syntax-table)
    204     (modify-syntax-entry ?- "_" syntax-table)
    205     (modify-syntax-entry ?_ "_" syntax-table)
    206     (modify-syntax-entry ?& "." syntax-table)
    207     (modify-syntax-entry ?* "." syntax-table)
    208     (modify-syntax-entry ?\( "." syntax-table)
    209     (modify-syntax-entry ?\) "." syntax-table)
    210     (modify-syntax-entry ?\{ "(}" syntax-table)
    211     (modify-syntax-entry ?\} "){" syntax-table)
    212     (modify-syntax-entry ?\[ "(]" syntax-table)
    213     (modify-syntax-entry ?\] ")[" syntax-table)
    214     syntax-table)
    215   "Syntax table in use in `yaml-mode' buffers.")
    216 
    217 ;;;###autoload
    218 (define-derived-mode yaml-mode text-mode "YAML"
    219   "Simple mode to edit YAML.
    220 
    221 \\{yaml-mode-map}"
    222   :syntax-table yaml-mode-syntax-table
    223   (set (make-local-variable 'comment-start) "# ")
    224   (set (make-local-variable 'comment-start-skip) "#+ *")
    225   (set (make-local-variable 'indent-line-function) 'yaml-indent-line)
    226   (set (make-local-variable 'indent-tabs-mode) nil)
    227   (set (make-local-variable 'fill-paragraph-function) 'yaml-fill-paragraph)
    228 
    229   (set (make-local-variable 'syntax-propertize-function)
    230        'yaml-mode-syntax-propertize-function)
    231   (setq font-lock-defaults '(yaml-font-lock-keywords)))
    232 
    233 
    234 ;; Font-lock support
    235 
    236 (defvar yaml-font-lock-keywords
    237   `((yaml-font-lock-block-literals 0 font-lock-string-face)
    238     (,yaml-constant-scalars-re . (1 font-lock-constant-face))
    239     (,yaml-tag-re . (0 font-lock-type-face))
    240     (,yaml-node-anchor-alias-re . (0 font-lock-function-name-face))
    241     (,yaml-hash-key-re . (1 font-lock-variable-name-face))
    242     (,yaml-document-delimiter-re . (0 font-lock-comment-face))
    243     (,yaml-directive-re . (1 font-lock-builtin-face))
    244     ("^[\t]+" 0 'yaml-tab-face t))
    245   "Additional expressions to highlight in YAML mode.")
    246 
    247 (defun yaml-mode-syntax-propertize-function (beg end)
    248   "Override buffer's syntax table for special syntactic constructs."
    249   ;; Unhighlight foo#bar tokens between BEG and END.
    250   (save-excursion
    251     (goto-char beg)
    252     (while (search-forward "#" end t)
    253       (save-excursion
    254         (forward-char -1)
    255         ;; both ^# and [ \t]# are comments
    256         (when (and (not (bolp))
    257                    (not (memq (preceding-char) '(?\s ?\t))))
    258           (put-text-property (point) (1+ (point))
    259                              'syntax-table (string-to-syntax "_"))))))
    260 
    261   (save-excursion
    262     (goto-char beg)
    263     (while (and
    264             (> end (point))
    265             (re-search-forward "['\"]" end t))
    266       (when (get-text-property (point) 'yaml-block-literal)
    267         (put-text-property (1- (point)) (point)
    268                            'syntax-table (string-to-syntax "w")))
    269       (let* ((pt (point))
    270              (sps (save-excursion (syntax-ppss (1- pt)))))
    271         (when (not (nth 8 sps))
    272           (cond
    273            ((and (char-equal ?' (char-before (1- pt)))
    274                  (char-equal ?' (char-before pt)))
    275             (put-text-property (- pt 2) pt
    276                                'syntax-table (string-to-syntax "w"))
    277             ;; Workaround for https://debbugs.gnu.org/41195.
    278             (let ((syntax-propertize--done syntax-propertize--done))
    279               ;; Carefully invalidate the last cached ppss.
    280               (syntax-ppss-flush-cache (- pt 2))))
    281            ;; If quote is detected as a syntactic string start but appeared
    282            ;; after a non-whitespace character, then mark it as syntactic word.
    283            ((and (char-before (1- pt))
    284                  (char-equal ?w (char-syntax (char-before (1- pt)))))
    285             (put-text-property (1- pt) pt
    286                                'syntax-table (string-to-syntax "w")))
    287            (t
    288             ;; We're right after a quote that opens a string literal.
    289             ;; Skip over it (big speedup for long JSON strings).
    290             (goto-char (1- pt))
    291             (condition-case nil
    292                 (forward-sexp)
    293               (scan-error
    294                (goto-char end))))))))))
    295 
    296 (defun yaml-font-lock-block-literals (bound)
    297   "Find lines within block literals.
    298 Find the next line of the first (if any) block literal after point and
    299 prior to BOUND.  Returns the beginning and end of the block literal
    300 line in the match data, as consumed by `font-lock-keywords' matcher
    301 functions.  The function begins by searching backwards to determine
    302 whether or not the current line is within a block literal.  This could
    303 be time-consuming in large buffers, so the number of lines searched is
    304 artificially limited to the value of
    305 `yaml-block-literal-search-lines'."
    306   (if (eolp) (goto-char (1+ (point))))
    307   (unless (or (eobp) (>= (point) bound))
    308     (let ((begin (point))
    309           (end (min (1+ (line-end-position)) bound)))
    310       (goto-char (line-beginning-position))
    311       (while (and (looking-at yaml-blank-line-re)
    312                   (not (bobp)))
    313         (forward-line -1))
    314       (let ((nlines yaml-block-literal-search-lines)
    315             (min-level (current-indentation)))
    316         (forward-line -1)
    317         (while (and (/= nlines 0)
    318                     (/= min-level 0)
    319                     (not (looking-at yaml-block-literal-re))
    320                     (not (bobp)))
    321           (setq nlines (1- nlines))
    322           (unless (looking-at yaml-blank-line-re)
    323             (setq min-level (min min-level (current-indentation))))
    324           (forward-line -1))
    325         (when (looking-at-p " *- ")
    326           (setq min-level (- min-level 2)))
    327         (cond
    328          ((and (< (current-indentation) min-level)
    329                (looking-at yaml-block-literal-re))
    330           (goto-char end)
    331           (put-text-property begin end 'yaml-block-literal t)
    332           (set-match-data (list begin end))
    333           t)
    334          ((progn
    335             (goto-char begin)
    336             (re-search-forward (concat yaml-block-literal-re
    337                                        " *\\(.*\\)\n")
    338                                bound t))
    339           (let ((range (nthcdr 2 (match-data))))
    340             (put-text-property (car range) (cadr range) 'yaml-block-literal t)
    341             (set-match-data range))
    342           t))))))
    343 
    344 
    345 ;; Indentation and electric keys
    346 
    347 (defun yaml-compute-indentation ()
    348   "Calculate the maximum sensible indentation for the current line."
    349   (save-excursion
    350     (beginning-of-line)
    351     (if (looking-at yaml-document-delimiter-re) 0
    352       (forward-line -1)
    353       (while (and (looking-at yaml-blank-line-re)
    354                   (> (point) (point-min)))
    355         (forward-line -1))
    356       (+ (current-indentation)
    357          (if (looking-at yaml-nested-map-re) yaml-indent-offset 0)
    358          (if (looking-at yaml-nested-sequence-re) yaml-indent-offset 0)
    359          (if (looking-at yaml-block-literal-re) yaml-indent-offset 0)))))
    360 
    361 (defun yaml-indent-line ()
    362   "Indent the current line.
    363 The first time this command is used, the line will be indented to the
    364 maximum sensible indentation.  Each immediately subsequent usage will
    365 back-dent the line by `yaml-indent-offset' spaces.  On reaching column
    366 0, it will cycle back to the maximum sensible indentation."
    367   (interactive "*")
    368   (let ((ci (current-indentation))
    369         (need (yaml-compute-indentation)))
    370     (save-excursion
    371       (if (and (equal last-command this-command) (/= ci 0))
    372           (indent-line-to (* (/ (- ci 1) yaml-indent-offset) yaml-indent-offset))
    373         (indent-line-to need)))
    374     (if (< (current-column) (current-indentation))
    375         (forward-to-indentation 0))))
    376 
    377 (defun yaml-electric-backspace (arg)
    378   "Delete characters or back-dent the current line.
    379 If invoked following only whitespace on a line, will back-dent to the
    380 immediately previous multiple of `yaml-indent-offset' spaces."
    381   (interactive "*p")
    382   (if (or (/= (current-indentation) (current-column)) (bolp))
    383       (funcall yaml-backspace-function arg)
    384     (let ((ci (current-column)))
    385       (beginning-of-line)
    386       (delete-horizontal-space)
    387       (indent-to (* (/ (- ci (* arg yaml-indent-offset))
    388                        yaml-indent-offset)
    389                     yaml-indent-offset)))))
    390 
    391 (defun yaml-electric-bar-and-angle (arg)
    392   "Insert the bound key and possibly begin a block literal.
    393 Inserts the bound key.  If inserting the bound key causes the current
    394 line to match the initial line of a block literal, then inserts the
    395 matching string from `yaml-block-literal-electric-alist', a newline,
    396 and indents appropriately."
    397   (interactive "*P")
    398   (self-insert-command (prefix-numeric-value arg))
    399   (let ((extra-chars
    400          (assoc last-command-event
    401                 yaml-block-literal-electric-alist)))
    402     (cond
    403      ((and extra-chars (not arg) (eolp)
    404            (save-excursion
    405              (beginning-of-line)
    406              (looking-at yaml-block-literal-re)))
    407       (insert (cdr extra-chars))
    408       (newline-and-indent)))))
    409 
    410 (defun yaml-electric-dash-and-dot (arg)
    411   "Insert the bound key and possibly de-dent line.
    412 Inserts the bound key.  If inserting the bound key causes the current
    413 line to match a document delimiter, de-dent the line to the left
    414 margin."
    415   (interactive "*P")
    416   (self-insert-command (prefix-numeric-value arg))
    417   (save-excursion
    418     (beginning-of-line)
    419     (when (and (not arg) (looking-at yaml-document-delimiter-re))
    420       (delete-horizontal-space))))
    421 
    422 (defun yaml-narrow-to-block-literal ()
    423   "Narrow the buffer to block literal if the point is in it,
    424 otherwise do nothing."
    425   (interactive)
    426   (save-excursion
    427     (goto-char (line-beginning-position))
    428     (while (and (looking-at-p yaml-blank-line-re) (not (bobp)))
    429       (forward-line -1))
    430     (let ((nlines yaml-block-literal-search-lines)
    431           (min-level (current-indentation))
    432           beg)
    433       (forward-line -1)
    434       (while (and (/= nlines 0)
    435                   (/= min-level 0)
    436                   (not (looking-at-p yaml-block-literal-re))
    437                   (not (bobp)))
    438         (setq nlines (1- nlines))
    439         (unless (looking-at-p yaml-blank-line-re)
    440           (setq min-level (min min-level (current-indentation))))
    441         (forward-line -1))
    442       (when (and (< (current-indentation) min-level)
    443                  (looking-at-p yaml-block-literal-re))
    444         (setq min-level (current-indentation))
    445         (forward-line)
    446         (setq beg (point))
    447         (while (and (not (eobp))
    448                     (or (looking-at-p yaml-blank-line-re)
    449                         (> (current-indentation) min-level)))
    450           (forward-line))
    451         (narrow-to-region beg (point))))))
    452 
    453 (defun yaml-fill-paragraph (&optional justify region)
    454   "Fill paragraph.
    455 Outside of comments, this behaves as `fill-paragraph' except that
    456 filling does not cross boundaries of block literals.  Inside comments,
    457 this will do usual adaptive fill behaviors."
    458   (interactive "*P")
    459   (save-restriction
    460     (yaml-narrow-to-block-literal)
    461     (let ((fill-paragraph-function nil))
    462       (or (fill-comment-paragraph justify)
    463           (fill-paragraph justify region)))))
    464 
    465 (defun yaml-set-imenu-generic-expression ()
    466   (make-local-variable 'imenu-generic-expression)
    467   (make-local-variable 'imenu-create-index-function)
    468   (setq imenu-create-index-function 'imenu-default-create-index-function)
    469   (setq imenu-generic-expression yaml-imenu-generic-expression))
    470 
    471 (add-hook 'yaml-mode-hook 'yaml-set-imenu-generic-expression)
    472 
    473 
    474 (defun yaml-mode-version ()
    475   "Display version of `yaml-mode'."
    476   (interactive)
    477   (message "yaml-mode %s" yaml-mode-version)
    478   yaml-mode-version)
    479 
    480 ;;;###autoload
    481 (add-to-list 'auto-mode-alist '("\\.\\(e?ya?\\|ra\\)ml\\'" . yaml-mode))
    482 
    483 ;;;###autoload
    484 (add-to-list 'magic-mode-alist
    485              '("^%YAML\\s-+[0-9]+\\.[0-9]+\\(\\s-+#\\|\\s-*$\\)" . yaml-mode))
    486 
    487 (provide 'yaml-mode)
    488 
    489 ;;; yaml-mode.el ends here