yaml-mode.el (18471B)
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 'comment-end) "") 226 (set (make-local-variable 'indent-line-function) 'yaml-indent-line) 227 (set (make-local-variable 'indent-tabs-mode) nil) 228 (set (make-local-variable 'fill-paragraph-function) 'yaml-fill-paragraph) 229 (set (make-local-variable 'page-delimiter) "^---\\([ \t].*\\)*\n") 230 231 (set (make-local-variable 'syntax-propertize-function) 232 'yaml-mode-syntax-propertize-function) 233 (setq font-lock-defaults '(yaml-font-lock-keywords))) 234 235 236 ;; Font-lock support 237 238 (defvar yaml-font-lock-keywords 239 `((yaml-font-lock-block-literals 0 font-lock-string-face) 240 (,yaml-constant-scalars-re . (1 font-lock-constant-face)) 241 (,yaml-tag-re . (0 font-lock-type-face)) 242 (,yaml-node-anchor-alias-re . (0 font-lock-function-name-face)) 243 (,yaml-hash-key-re . (1 font-lock-variable-name-face)) 244 (,yaml-document-delimiter-re . (0 font-lock-comment-face)) 245 (,yaml-directive-re . (1 font-lock-builtin-face)) 246 ("^[\t]+" 0 'yaml-tab-face t)) 247 "Additional expressions to highlight in YAML mode.") 248 249 (defun yaml-mode-syntax-propertize-function (beg end) 250 "Override buffer's syntax table for special syntactic constructs." 251 ;; Unhighlight foo#bar tokens between BEG and END. 252 (save-excursion 253 (goto-char beg) 254 (while (search-forward "#" end t) 255 (save-excursion 256 (forward-char -1) 257 ;; both ^# and [ \t]# are comments 258 (when (and (not (bolp)) 259 (not (memq (preceding-char) '(?\s ?\t)))) 260 (put-text-property (point) (1+ (point)) 261 'syntax-table (string-to-syntax "_")))))) 262 263 (save-excursion 264 (goto-char beg) 265 (while (and 266 (> end (point)) 267 (re-search-forward "['\"]" end t)) 268 (when (get-text-property (point) 'yaml-block-literal) 269 (put-text-property (1- (point)) (point) 270 'syntax-table (string-to-syntax "w"))) 271 (let* ((pt (point)) 272 (sps (save-excursion (syntax-ppss (1- pt))))) 273 (when (not (nth 8 sps)) 274 (cond 275 ((and (char-equal ?' (char-before (1- pt))) 276 (char-equal ?' (char-before pt))) 277 (put-text-property (- pt 2) pt 278 'syntax-table (string-to-syntax "w")) 279 ;; Workaround for https://debbugs.gnu.org/41195. 280 (let ((syntax-propertize--done syntax-propertize--done)) 281 ;; Carefully invalidate the last cached ppss. 282 (syntax-ppss-flush-cache (- pt 2)))) 283 ;; If quote is detected as a syntactic string start but appeared 284 ;; after a non-whitespace character, then mark it as syntactic word. 285 ((and (char-before (1- pt)) 286 (char-equal ?w (char-syntax (char-before (1- pt))))) 287 (put-text-property (1- pt) pt 288 'syntax-table (string-to-syntax "w"))) 289 (t 290 ;; We're right after a quote that opens a string literal. 291 ;; Skip over it (big speedup for long JSON strings). 292 (goto-char (1- pt)) 293 (condition-case nil 294 (forward-sexp) 295 (scan-error 296 (goto-char end)))))))))) 297 298 (defun yaml-font-lock-block-literals (bound) 299 "Find lines within block literals. 300 Find the next line of the first (if any) block literal after point and 301 prior to BOUND. Returns the beginning and end of the block literal 302 line in the match data, as consumed by `font-lock-keywords' matcher 303 functions. The function begins by searching backwards to determine 304 whether or not the current line is within a block literal. This could 305 be time-consuming in large buffers, so the number of lines searched is 306 artificially limited to the value of 307 `yaml-block-literal-search-lines'." 308 (if (eolp) (goto-char (1+ (point)))) 309 (unless (or (eobp) (>= (point) bound)) 310 (let ((begin (point)) 311 (end (min (1+ (line-end-position)) bound))) 312 (goto-char (line-beginning-position)) 313 (while (and (looking-at yaml-blank-line-re) 314 (not (bobp))) 315 (forward-line -1)) 316 (let ((nlines yaml-block-literal-search-lines) 317 (min-level (current-indentation))) 318 (forward-line -1) 319 (while (and (/= nlines 0) 320 (/= min-level 0) 321 (not (looking-at yaml-block-literal-re)) 322 (not (bobp))) 323 (setq nlines (1- nlines)) 324 (unless (looking-at yaml-blank-line-re) 325 (setq min-level (min min-level (current-indentation)))) 326 (forward-line -1)) 327 (when (looking-at-p " *- ") 328 (setq min-level (- min-level 2))) 329 (cond 330 ((and (< (current-indentation) min-level) 331 (looking-at yaml-block-literal-re)) 332 (goto-char end) 333 (put-text-property begin end 'yaml-block-literal t) 334 (set-match-data (list begin end)) 335 t) 336 ((progn 337 (goto-char begin) 338 (re-search-forward (concat yaml-block-literal-re 339 " *\\(.*\\)\n") 340 bound t)) 341 (let ((range (nthcdr 2 (match-data)))) 342 (put-text-property (car range) (cadr range) 'yaml-block-literal t) 343 (set-match-data range)) 344 t)))))) 345 346 347 ;; Indentation and electric keys 348 349 (defun yaml-compute-indentation () 350 "Calculate the maximum sensible indentation for the current line." 351 (save-excursion 352 (beginning-of-line) 353 (if (looking-at yaml-document-delimiter-re) 0 354 (forward-line -1) 355 (while (and (looking-at yaml-blank-line-re) 356 (> (point) (point-min))) 357 (forward-line -1)) 358 (+ (current-indentation) 359 (if (looking-at yaml-nested-map-re) yaml-indent-offset 0) 360 (if (looking-at yaml-nested-sequence-re) yaml-indent-offset 0) 361 (if (looking-at yaml-block-literal-re) yaml-indent-offset 0))))) 362 363 (defun yaml-indent-line () 364 "Indent the current line. 365 The first time this command is used, the line will be indented to the 366 maximum sensible indentation. Each immediately subsequent usage will 367 back-dent the line by `yaml-indent-offset' spaces. On reaching column 368 0, it will cycle back to the maximum sensible indentation." 369 (interactive "*") 370 (let ((ci (current-indentation)) 371 (need (yaml-compute-indentation))) 372 (save-excursion 373 (if (and (equal last-command this-command) (/= ci 0)) 374 (indent-line-to (* (/ (- ci 1) yaml-indent-offset) yaml-indent-offset)) 375 (indent-line-to need))) 376 (if (< (current-column) (current-indentation)) 377 (forward-to-indentation 0)))) 378 379 (defun yaml-electric-backspace (arg) 380 "Delete characters or back-dent the current line. 381 If invoked following only whitespace on a line, will back-dent to the 382 immediately previous multiple of `yaml-indent-offset' spaces." 383 (interactive "*p") 384 (if (or (/= (current-indentation) (current-column)) (bolp)) 385 (funcall yaml-backspace-function arg) 386 (let ((ci (current-column))) 387 (beginning-of-line) 388 (delete-horizontal-space) 389 (indent-to (* (/ (- ci (* arg yaml-indent-offset)) 390 yaml-indent-offset) 391 yaml-indent-offset))))) 392 393 (defun yaml-electric-bar-and-angle (arg) 394 "Insert the bound key and possibly begin a block literal. 395 Inserts the bound key. If inserting the bound key causes the current 396 line to match the initial line of a block literal, then inserts the 397 matching string from `yaml-block-literal-electric-alist', a newline, 398 and indents appropriately." 399 (interactive "*P") 400 (self-insert-command (prefix-numeric-value arg)) 401 (let ((extra-chars 402 (assoc last-command-event 403 yaml-block-literal-electric-alist))) 404 (cond 405 ((and extra-chars (not arg) (eolp) 406 (save-excursion 407 (beginning-of-line) 408 (looking-at yaml-block-literal-re))) 409 (insert (cdr extra-chars)) 410 (newline-and-indent))))) 411 412 (defun yaml-electric-dash-and-dot (arg) 413 "Insert the bound key and possibly de-dent line. 414 Inserts the bound key. If inserting the bound key causes the current 415 line to match a document delimiter, de-dent the line to the left 416 margin." 417 (interactive "*P") 418 (self-insert-command (prefix-numeric-value arg)) 419 (save-excursion 420 (beginning-of-line) 421 (when (and (not arg) (looking-at yaml-document-delimiter-re)) 422 (delete-horizontal-space)))) 423 424 (defun yaml-narrow-to-block-literal () 425 "Narrow the buffer to block literal if the point is in it, 426 otherwise do nothing." 427 (interactive) 428 (save-excursion 429 (goto-char (line-beginning-position)) 430 (while (and (looking-at-p yaml-blank-line-re) (not (bobp))) 431 (forward-line -1)) 432 (let ((nlines yaml-block-literal-search-lines) 433 (min-level (current-indentation)) 434 beg) 435 (forward-line -1) 436 (while (and (/= nlines 0) 437 (/= min-level 0) 438 (not (looking-at-p yaml-block-literal-re)) 439 (not (bobp))) 440 (setq nlines (1- nlines)) 441 (unless (looking-at-p yaml-blank-line-re) 442 (setq min-level (min min-level (current-indentation)))) 443 (forward-line -1)) 444 (when (and (< (current-indentation) min-level) 445 (looking-at-p yaml-block-literal-re)) 446 (setq min-level (current-indentation)) 447 (forward-line) 448 (setq beg (point)) 449 (while (and (not (eobp)) 450 (or (looking-at-p yaml-blank-line-re) 451 (> (current-indentation) min-level))) 452 (forward-line)) 453 (narrow-to-region beg (point)))))) 454 455 (defun yaml-fill-paragraph (&optional justify region) 456 "Fill paragraph. 457 Outside of comments, this behaves as `fill-paragraph' except that 458 filling does not cross boundaries of block literals. Inside comments, 459 this will do usual adaptive fill behaviors." 460 (interactive "*P") 461 (save-restriction 462 (yaml-narrow-to-block-literal) 463 (let ((fill-paragraph-function nil)) 464 (or (fill-comment-paragraph justify) 465 (fill-paragraph justify region))))) 466 467 (defun yaml-set-imenu-generic-expression () 468 (make-local-variable 'imenu-generic-expression) 469 (make-local-variable 'imenu-create-index-function) 470 (setq imenu-create-index-function 'imenu-default-create-index-function) 471 (setq imenu-generic-expression yaml-imenu-generic-expression)) 472 473 (add-hook 'yaml-mode-hook 'yaml-set-imenu-generic-expression) 474 475 476 (defun yaml-mode-version () 477 "Display version of `yaml-mode'." 478 (interactive) 479 (message "yaml-mode %s" yaml-mode-version) 480 yaml-mode-version) 481 482 ;;;###autoload 483 (add-to-list 'auto-mode-alist '("\\.\\(e?ya?\\|ra\\)ml\\'" . yaml-mode)) 484 485 ;;;###autoload 486 (add-to-list 'magic-mode-alist 487 '("^%YAML\\s-+[0-9]+\\.[0-9]+\\(\\s-+#\\|\\s-*$\\)" . yaml-mode)) 488 489 (provide 'yaml-mode) 490 491 ;;; yaml-mode.el ends here