comparison .elisp/yaml-mode.el @ 52:d6ea7c108453

Add yaml mode.
author Augie Fackler <durin42@gmail.com>
date Thu, 19 Feb 2009 09:19:18 -0600
parents
children
comparison
equal deleted inserted replaced
51:ac7a8f9a0924 52:d6ea7c108453
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