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