;;; yasnippet.el --- Yet another snippet extension for Emacs. ;; Author: pluskid ;; Version: 0.1 ;; This file is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation; either version 2, or (at your option) ;; any later version. ;; This file is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs; see the file COPYING. If not, write to ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330, ;; Boston, MA 02111-1307, USA. ;;; Commentary: ;; Nothing. (require 'cl) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; User customizable variables ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defvar yas/key-syntax "w" "Syntax of a key. This is used to determine the current key being expanded.") (defvar yas/indent-line t "Each (except the 1st) line of the snippet template is indented to current column if this variable is non-`nil'.") (make-variable-buffer-local 'yas/indent-line) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Internal variables ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defvar yas/snippet-tables (make-hash-table) "A hash table of snippet tables corresponding to each major-mode.") (defun yas/snippet-new () "Create a new snippet." (cons nil nil)) (defun yas/snippet-field-groups (snippet) "Get field groups of SNIPPET." (car snippet)) (defun yas/snippet-field-groups-set (snippet groups) "Set field groups of SNIPPET." (setf (car snippet) groups)) (defun yas/snippet-exit-marker (snippet) "Get exit marker of SNIPPET." (cdr snippet)) (defun yas/snippet-add-field (snippet field) "Add FIELD to SNIPPET." (let ((group (find field (yas/snippet-field-groups snippet) :test '(lambda (field group) (= (yas/snippet-field-number field) (yas/snippet-field-group-number group)))))) (if group (yas/snippet-field-group-add group field) (push (yas/snippet-field-group-new field) (car snippet))))) (defun yas/snippet-field-group-new (field) "Create a new field group." (list field ; primary field (list field) ; fields nil ; next field group nil)) ; prev field group (defun yas/snippet-field-group-primary (group) "Get the primary field of this group." (car group)) (defun yas/snippet-field-group-fields (group) "Get all fields belonging to this group." (cadr group)) (defun yas/snippet-field-group-set-next (group next) "Set next field group of GROUP." (setf (nth 2 group) next)) (defun yas/snippet-field-group-next (group) "Get next field group." (nth 2 group)) (defun yas/snippet-field-group-set-prev (group prev) "Set previous field group of GROUP." (setf (nth 2 group) prev)) (defun yas/snippet-field-group-prev (group) "Get previous field group." (nth 3 group)) (defun yas/snippet-field-group-value (group) "Get the default value of the field group." (or (yas/snippet-field-value (yas/snippet-field-group-primary group)) "")) (defun yas/snippet-field-group-number (group) "Get the number of the field group." (yas/snippet-field-number (yas/snippet-field-group-primary group))) (defun yas/snippet-field-group-add (group field) "Add a field to the field group. If the value of the field is not nil, it is set the primary field of the group." (push field (nth 1 group)) (when (yas/snippet-field-value field) (setf (car group) field))) (defun yas/snippet-field-new (overlay number value) "Create a new snippet-field." (cons overlay (cons number value))) (defun yas/snippet-field-overlay (field) "Get the overlay of the field." (car field)) (defun yas/snippet-field-number (field) "Get the number of the field." (cadr field)) (defun yas/snippet-field-value (field) "Get the value of the field." (cddr field)) (defun yas/snippet-field-compare (field1 field2) "Compare two fields. The field with a number is sorted first. If they both have a number, compare through the number. If neither have, compare through the start point of the overlay." (let ((n1 (yas/snippet-field-number field1)) (n2 (yas/snippet-field-number field2))) (if n1 (if n2 (< n1 n2) t) (if n2 nil (< (overlay-start (yas/snippet-field-overlay field1)) (overlay-start (yas/snippet-field-overlay field2))))))) (defconst yas/escape-backslash (concat "YASESCAPE" "BACKSLASH" "PROTECTGUARD")) (defconst yas/escape-dollar (concat "YASESCAPE" "DOLLAR" "PROTECTGUARD")) (defconst yas/escape-backquote (concat "YASESCAPE" "BACKQUOTE" "PROTECTGUARD")) (defconst yas/field-regexp (concat "$\\(?1:[0-9]+\\)" "\\|" "${\\(?:\\(?1:[0-9]+\\):\\)?\\(?2:[^}]*\\)}")) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Internal functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defun yas/eval-string (string) "Evaluate STRING and convert the result to string." (condition-case err (format "%s" (eval (read string))) (error (format "(error in elisp evaluation: %s)" (error-message-string err))))) (defsubst yas/replace-all (from to) "Replace all occurance from FROM to TO." (goto-char (point-min)) (while (search-forward from nil t) (replace-match to t t))) (defun yas/snippet-table (mode) "Get the snippet table corresponding to MODE." (let ((table (gethash mode yas/snippet-tables))) (unless table (setq table (make-hash-table :test 'equal)) (puthash mode table yas/snippet-tables)) table)) (defsubst yas/current-snippet-table () "Get the snippet table for current major-mode." (yas/snippet-table major-mode)) (defsubst yas/template (key snippet-table) "Get template for KEY in SNIPPET-TABLE." (gethash key snippet-table)) (defun yas/current-key () "Get the key under current position." (let ((start (point)) (end (point))) (save-excursion (skip-syntax-backward yas/key-syntax) (setq start (point)) (list (buffer-substring-no-properties start end) start end)))) (defun yas/expand-snippet (start end template) "Expand snippet at current point. Text between START and END will be deleted before inserting template." (goto-char start) (let ((length (- end start)) (column (current-column))) (save-restriction (narrow-to-region start start) (insert template) ;; Step 1: do necessary indent (when yas/indent-line (let* ((indent (if indent-tabs-mode (concat (make-string (/ column tab-width) ?\t) (make-string (% column tab-width) ?\ )) (make-string column ?\ )))) (goto-char (point-min)) (while (and (zerop (forward-line)) (= (current-column) 0)) (insert indent)))) ;; Step 2: protect backslash and backquote (yas/replace-all "\\\\" yas/escape-backslash) (yas/replace-all "\\`" yas/escape-backquote) ;; Step 3: evaluate all backquotes (goto-char (point-min)) (while (re-search-forward "`\\([^`]*\\)`" nil t) (replace-match (yas/eval-string (match-string-no-properties 1)) t t)) ;; Step 4: protect all escapes, including backslash and backquot ;; which may be produced in Step 3 (yas/replace-all "\\\\" yas/escape-backslash) (yas/replace-all "\\`" yas/escape-backquote) (yas/replace-all "\\$" yas/escape-dollar) (let ((snippet (yas/snippet-new))) ;; Step 5: Create fields (goto-char (point-min)) (while (re-search-forward yas/field-regexp nil t) (yas/snippet-add-field snippet (yas/snippet-field-new (make-overlay (match-beginning 0) (match-end 0)) (if (match-string-no-properties 1) (string-to-number (match-string-no-properties 1)) nil) (match-string-no-properties 2)))) ;; Step 6: Sort and link each field group (yas/snippet-field-groups-set snippet (sort (yas/snippet-field-groups snippet) '(lambda (group1 group2) (yas/snippet-field-compare (yas/snippet-field-group-primary group1) (yas/snippet-field-group-primary group2))))) (let ((prev nil)) (dolist (group (yas/snippet-field-groups snippet)) (yas/snippet-field-group-set-prev group prev) (when prev (yas/snippet-field-group-set-next prev group)) (setq prev group))) ;; Step 7: Set up properties of overlays, including keymaps ;; Step 8: Replace fields with default values (dolist (group (yas/snippet-field-groups snippet)) (let ((value (yas/snippet-field-group-value group))) (dolist (field (yas/snippet-field-group-fields group)) (let* ((overlay (yas/snippet-field-overlay field)) (start (overlay-start overlay)) (end (overlay-end overlay)) (length (- end start))) (goto-char start) (insert value) (delete-char length)))))) ;; Step : restore all escape characters (yas/replace-all yas/escape-dollar "$") (yas/replace-all yas/escape-backquote "`") (yas/replace-all yas/escape-backslash "\\") (goto-char (point-max))) (delete-char length))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; User level functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defun yas/define (mode key template) "Define a snippet. Expanding KEY into TEMPLATE." (puthash key template (yas/snippet-table mode))) (defun yas/expand () "Expand a snippet. When a snippet is expanded, t is returned, otherwise, nil returned." (interactive) (multiple-value-bind (key start end) (yas/current-key) (let ((template (yas/template key (yas/current-snippet-table)))) (if template (progn (yas/expand-snippet start end template) t) nil)))) (provide 'yasnippet)