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