52
|
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 |