/ emacs.d / yaml-mode.el
yaml-mode.el
  1  ;;; yaml-mode.el --- Major mode for editing YAML files
  2  
  3  ;; Copyright (C) 2006  Yoshiki Kurihara
  4  
  5  ;; Author: Yoshiki Kurihara <kurihara@cpan.org>
  6  ;;         Marshall T. Vandegrift <llasram@gmail.com>
  7  ;; Keywords: data yaml
  8  ;; Version: 0.0.3
  9  
 10  ;; This file is not part of Emacs
 11  
 12  ;; This file 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 2, or (at your option)
 15  ;; any later version.
 16  
 17  ;; This file 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 GNU Emacs; see the file COPYING.  If not, write to
 24  ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 25  ;; Boston, MA 02111-1307, USA.
 26  
 27  ;;; Commentary:
 28  
 29  ;; This is a major mode for editing files in the YAML data
 30  ;; serialization format.  It was initially developed by Yoshiki
 31  ;; Kurihara and many features were added by Marshall Vandegrift.  As
 32  ;; YAML and Python share the fact that indentation determines
 33  ;; structure, this mode provides indentation and indentation command
 34  ;; behavior very similar to that of python-mode.
 35  
 36  ;;; Installation:
 37  
 38  ;; To install, just drop this file into a directory in your
 39  ;; `load-path' and (optionally) byte-compile it.  To automatically
 40  ;; handle files ending in '.yml', add something like:
 41  ;;
 42  ;;    (require 'yaml-mode)
 43  ;;    (add-to-list 'auto-mode-alist '("\\.yml$" . yaml-mode))
 44  ;;
 45  ;; to your .emacs file.
 46  ;;
 47  ;; Unlike python-mode, this mode follows the Emacs convention of not
 48  ;; binding the ENTER key to `newline-and-indent'.  To get this
 49  ;; behavior, add the key definition to `yaml-mode-hook':
 50  ;;
 51  ;;    (add-hook 'yaml-mode-hook
 52  ;;     '(lambda ()
 53  ;;        (define-key yaml-mode-map "\C-m" 'newline-and-indent)))
 54  
 55  ;;; Known Bugs:
 56  
 57  ;; YAML is easy to write but complex to parse, and this mode doesn't
 58  ;; even really try.  Indentation and highlighting will break on
 59  ;; abnormally complicated structures.
 60  
 61  ;;; Code:
 62  
 63  
 64  ;; User definable variables
 65  
 66  (defgroup yaml nil
 67    "Support for the YAML serialization format"
 68    :group 'languages
 69    :prefix "yaml-")
 70  
 71  (defcustom yaml-mode-hook nil
 72    "*Hook run by `yaml-mode'."
 73    :type 'hook
 74    :group 'yaml)
 75  
 76  (defcustom yaml-indent-offset 2
 77    "*Amount of offset per level of indentation."
 78    :type 'integer
 79    :group 'yaml)
 80  
 81  (defcustom yaml-backspace-function 'backward-delete-char-untabify
 82    "*Function called by `yaml-electric-backspace' when deleting backwards."
 83    :type 'function
 84    :group 'yaml)
 85  
 86  (defcustom yaml-block-literal-search-lines 100
 87    "*Maximum number of lines to search for start of block literals."
 88    :type 'integer
 89    :group 'yaml)
 90  
 91  (defcustom yaml-block-literal-electric-alist
 92    '((?| . "") (?> . "-"))
 93    "*Characters for which to provide electric behavior.
 94  The association list key should be a key code and the associated value
 95  should be a string containing additional characters to insert when
 96  that key is pressed to begin a block literal."
 97    :type 'alist
 98    :group 'yaml)
 99  
100  (defface yaml-tab-face
101     '((((class color)) (:background "red" :foreground "red" :bold t))
102       (t (:reverse-video t)))
103    "Face to use for highlighting tabs in YAML files."
104    :group 'faces
105    :group 'yaml)
106  
107  
108  ;; Constants
109  
110  (defconst yaml-mode-version "0.0.3" "Version of `yaml-mode.'")
111  
112  (defconst yaml-blank-line-re "^ *$"
113    "Regexp matching a line containing only (valid) whitespace.")
114  
115  (defconst yaml-comment-re "\\(#*.*\\)"
116    "Regexp matching a line containing a YAML comment or delimiter.")
117  
118  (defconst yaml-directive-re "^\\(?:--- \\)? *%\\(\\w+\\)"
119    "Regexp matching a line contatining a YAML directive.")
120  
121  (defconst yaml-document-delimiter-re "^ *\\(?:---\\|[.][.][.]\\)"
122    "Rexexp matching a YAML document delimiter line.")
123  
124  (defconst yaml-node-anchor-alias-re "[&*]\\w+"
125    "Regexp matching a YAML node anchor or alias.")
126  
127  (defconst yaml-tag-re "!!?[^ \n]+"
128    "Rexexp matching a YAML tag.")
129  
130  (defconst yaml-bare-scalar-re
131    "\\(?:[^-:,#!\n{\\[ ]\\|[^#!\n{\\[ ]\\S-\\)[^#\n]*?"
132    "Rexexp matching a YAML bare scalar.")
133  
134  (defconst yaml-hash-key-re
135    (concat "\\(?:^\\(?:--- \\)?\\|{\\|\\(?:[-,] +\\)+\\) *"
136            "\\(?:" yaml-tag-re " +\\)?"
137            "\\(" yaml-bare-scalar-re "\\) *:"
138            "\\(?: +\\|$\\)")
139    "Regexp matching a single YAML hash key.")
140  
141  (defconst yaml-scalar-context-re
142    (concat "\\(?:^\\(?:--- \\)?\\|{\\|\\(?:[-,] +\\)+\\) *"
143            "\\(?:" yaml-bare-scalar-re " *: \\)?")
144    "Regexp indicating the begininng of a scalar context.")
145  
146  (defconst yaml-nested-map-re
147    (concat ".*: *\\(?:&.*\\|{ *\\|" yaml-tag-re " *\\)?$")
148    "Regexp matching a line beginning a YAML nested structure.")
149  
150  (defconst yaml-block-literal-base-re " *[>|][-+0-9]* *\\(?:\n\\|\\'\\)"
151    "Regexp matching the substring start of a block literal.")
152  
153  (defconst yaml-block-literal-re
154    (concat yaml-scalar-context-re
155            "\\(?:" yaml-tag-re "\\)?"
156            yaml-block-literal-base-re)
157    "Regexp matching a line beginning a YAML block literal")
158  
159  (defconst yaml-nested-sequence-re
160    (concat "^\\(?: *- +\\)+"
161            "\\(?:" yaml-bare-scalar-re " *:\\(?: +.*\\)?\\)?$")
162    "Regexp matching a line containing one or more nested YAML sequences")
163  
164  (defconst yaml-constant-scalars-re
165    (concat "\\(?:^\\|\\(?::\\|-\\|,\\|{\\|\\[\\) +\\) *"
166            (regexp-opt
167             '("~" "null" "Null" "NULL"
168               ".nan" ".NaN" ".NAN"
169               ".inf" ".Inf" ".INF"
170               "-.inf" "-.Inf" "-.INF"
171               "y" "Y" "yes" "Yes" "YES" "n" "N" "no" "No" "NO"
172               "true" "True" "TRUE" "false" "False" "FALSE"
173               "on" "On" "ON" "off" "Off" "OFF") t)
174            " *$")
175    "Regexp matching certain scalar constants in scalar context")
176  
177  
178  ;; Mode setup
179  
180  (defvar yaml-mode-map ()
181    "Keymap used in `yaml-mode' buffers.")
182  (if yaml-mode-map
183      nil
184    (setq yaml-mode-map (make-sparse-keymap))
185    (define-key yaml-mode-map "|" 'yaml-electric-bar-and-angle)
186    (define-key yaml-mode-map ">" 'yaml-electric-bar-and-angle)
187    (define-key yaml-mode-map "-" 'yaml-electric-dash-and-dot)
188    (define-key yaml-mode-map "." 'yaml-electric-dash-and-dot)
189    (define-key yaml-mode-map [backspace] 'yaml-electric-backspace)
190    (define-key yaml-mode-map "\C-j" 'newline-and-indent))
191  
192  (defvar yaml-mode-syntax-table nil
193    "Syntax table in use in yaml-mode buffers.")
194  (if yaml-mode-syntax-table
195      nil
196    (setq yaml-mode-syntax-table (make-syntax-table))
197    (modify-syntax-entry ?\' "\"" yaml-mode-syntax-table)
198    (modify-syntax-entry ?\" "\"" yaml-mode-syntax-table)
199    (modify-syntax-entry ?# "<" yaml-mode-syntax-table)
200    (modify-syntax-entry ?\n ">" yaml-mode-syntax-table)
201    (modify-syntax-entry ?\\ "\\" yaml-mode-syntax-table)
202    (modify-syntax-entry ?- "." yaml-mode-syntax-table)
203    (modify-syntax-entry ?_ "_" yaml-mode-syntax-table)
204    (modify-syntax-entry ?\( "." yaml-mode-syntax-table)
205    (modify-syntax-entry ?\) "." yaml-mode-syntax-table)
206    (modify-syntax-entry ?\{ "(}" yaml-mode-syntax-table)
207    (modify-syntax-entry ?\} "){" yaml-mode-syntax-table)
208    (modify-syntax-entry ?\[ "(]" yaml-mode-syntax-table)
209    (modify-syntax-entry ?\] ")[" yaml-mode-syntax-table))
210  
211  (define-derived-mode yaml-mode fundamental-mode "YAML"
212    "Simple mode to edit YAML.
213  
214  \\{yaml-mode-map}"
215    (set (make-local-variable 'comment-start) "# ")
216    (set (make-local-variable 'comment-start-skip) "#+ *")
217    (set (make-local-variable 'indent-line-function) 'yaml-indent-line)
218    (set (make-local-variable 'font-lock-defaults)
219         '(yaml-font-lock-keywords
220           nil nil nil nil
221           (font-lock-syntactic-keywords . yaml-font-lock-syntactic-keywords))))
222  
223  
224  ;; Font-lock support
225  
226  (defvar yaml-font-lock-keywords
227     (list
228      (cons yaml-comment-re '(1 font-lock-comment-face))
229      (cons yaml-constant-scalars-re '(1 font-lock-constant-face))
230      (cons yaml-tag-re '(0 font-lock-type-face))
231      (cons yaml-node-anchor-alias-re '(0 font-lock-function-name-face t))
232      (cons yaml-hash-key-re '(1 font-lock-variable-name-face t))
233      (cons yaml-document-delimiter-re '(0 font-lock-comment-face))
234      (cons yaml-directive-re '(1 font-lock-builtin-face))
235      '(yaml-font-lock-block-literals 0 font-lock-string-face t)
236      '("^[\t]+" 0 'yaml-tab-face t))
237     "Additional expressions to highlight in YAML mode.")
238  
239  (defvar yaml-font-lock-syntactic-keywords
240    (list '(yaml-syntactic-block-literals 0 "." t))
241    "Additional syntax features to highlight in YAML mode.")
242  
243  
244  (defun yaml-font-lock-block-literals (bound)
245    "Find lines within block literals.
246  Find the next line of the first (if any) block literal after point and
247  prior to BOUND.  Returns the beginning and end of the block literal
248  line in the match data, as consumed by `font-lock-keywords' matcher
249  functions.  The function begins by searching backwards to determine
250  whether or not the current line is within a block literal.  This could
251  be time-consuming in large buffers, so the number of lines searched is
252  artificially limitted to the value of
253  `yaml-block-literal-search-lines'."
254    (if (eolp) (goto-char (1+ (point))))
255    (unless (or (eobp) (>= (point) bound))
256      (let ((begin (point))
257            (end (min (1+ (point-at-eol)) bound)))
258        (goto-char (point-at-bol))
259        (while (and (looking-at yaml-blank-line-re) (not (bobp)))
260          (forward-line -1))
261        (let ((nlines yaml-block-literal-search-lines) 
262              (min-level (current-indentation))) 
263        (forward-line -1) 
264        (while (and (/= nlines 0) 
265                    (/= min-level 0) 
266                    (not (looking-at yaml-block-literal-re)) 
267                    (not (bobp))) 
268          (set 'nlines (1- nlines)) 
269          (unless (looking-at yaml-blank-line-re) 
270            (set 'min-level (min min-level (current-indentation)))) 
271          (forward-line -1)) 
272        (cond
273         ((and (< (current-indentation) min-level)
274               (looking-at yaml-block-literal-re))
275            (goto-char end) (set-match-data (list begin end)) t)
276           ((progn 
277              (goto-char begin)
278              (re-search-forward (concat yaml-block-literal-re
279                                         " *\\(.*\\)\n")
280                                 bound t))
281            (set-match-data (nthcdr 2 (match-data))) t))))))
282  
283  (defun yaml-syntactic-block-literals (bound)
284    "Find quote characters within block literals.
285  Finds the first quote character within a block literal (if any) after
286  point and prior to BOUND.  Returns the position of the quote character
287  in the match data, as consumed by matcher functions in
288  `font-lock-syntactic-keywords'.  This allows the mode to treat ['\"]
289  characters in block literals as punctuation syntax instead of string
290  syntax, preventing unmatched quotes in block literals from painting
291  the entire buffer in `font-lock-string-face'."
292    (let ((found nil))
293      (while (and (not found)
294                  (/= (point) bound)
295                  (yaml-font-lock-block-literals bound))
296        (let ((begin (match-beginning 0)) (end (match-end 0)))
297          (goto-char begin)
298          (cond
299           ((re-search-forward "['\"]" end t) (setq found t))
300           ((goto-char end)))))
301      found))
302  
303  
304  ;; Indentation and electric keys
305  
306  (defun yaml-compute-indentation ()
307    "Calculate the maximum sensible indentation for the current line."
308    (save-excursion
309      (beginning-of-line)
310      (if (looking-at yaml-document-delimiter-re) 0
311        (forward-line -1)
312        (while (and (looking-at yaml-blank-line-re)
313                    (> (point) (point-min)))
314          (forward-line -1))
315        (+ (current-indentation)
316           (if (looking-at yaml-nested-map-re) yaml-indent-offset 0)
317           (if (looking-at yaml-nested-sequence-re) yaml-indent-offset 0)
318           (if (looking-at yaml-block-literal-re) yaml-indent-offset 0)))))
319  
320  (defun yaml-indent-line ()
321    "Indent the current line.
322  The first time this command is used, the line will be indented to the
323  maximum sensible indentation.  Each immediately subsequent usage will
324  back-dent the line by `yaml-indent-offset' spaces.  On reaching column
325  0, it will cycle back to the maximum sensible indentation."
326    (interactive "*")
327    (let ((ci (current-indentation))
328          (cc (current-column))
329          (need (yaml-compute-indentation)))
330      (save-excursion
331        (beginning-of-line)
332        (delete-horizontal-space)
333        (if (and (equal last-command this-command) (/= ci 0))
334            (indent-to (* (/ (- ci 1) yaml-indent-offset) yaml-indent-offset))
335          (indent-to need)))
336        (if (< (current-column) (current-indentation))
337            (forward-to-indentation 0))))
338  
339  (defun yaml-electric-backspace (arg)
340    "Delete characters or back-dent the current line.
341  If invoked following only whitespace on a line, will back-dent to the
342  immediately previous multiple of `yaml-indent-offset' spaces."
343    (interactive "*p")
344    (if (or (/= (current-indentation) (current-column)) (bolp))
345        (funcall yaml-backspace-function arg)
346      (let ((ci (current-column)))
347        (beginning-of-line)
348        (delete-horizontal-space)
349        (indent-to (* (/ (- ci (* arg yaml-indent-offset))
350                         yaml-indent-offset)
351                      yaml-indent-offset)))))
352    
353  (defun yaml-electric-bar-and-angle (arg)
354    "Insert the bound key and possibly begin a block literal.
355  Inserts the bound key.  If inserting the bound key causes the current
356  line to match the initial line of a block literal, then inserts the
357  matching string from `yaml-block-literal-electric-alist', a newline,
358  and indents appropriately."
359    (interactive "*P")
360    (self-insert-command (prefix-numeric-value arg))
361    (let ((extra-chars
362           (assoc last-command-char
363                  yaml-block-literal-electric-alist)))
364      (cond
365       ((and extra-chars (not arg) (eolp)
366             (save-excursion
367               (beginning-of-line)
368               (looking-at yaml-block-literal-re)))
369        (insert (cdr extra-chars))
370        (newline-and-indent)))))
371  
372  (defun yaml-electric-dash-and-dot (arg)
373    "Insert the bound key and possibly de-dent line.
374  Inserts the bound key.  If inserting the bound key causes the current
375  line to match a document delimiter, de-dent the line to the left
376  margin."
377    (interactive "*P")
378    (self-insert-command (prefix-numeric-value arg))
379    (save-excursion
380      (beginning-of-line)
381      (if (and (not arg) (looking-at yaml-document-delimiter-re))
382          (delete-horizontal-space))))
383  
384  (defun yaml-mode-version ()
385    "Diplay version of `yaml-mode'."
386    (interactive)
387    (message "yaml-mode %s" yaml-mode-version)
388    yaml-mode-version)
389  
390  (provide 'yaml-mode)
391  
392  ;;; yaml-mode.el ends here