diff --git a/tempel-tempo-tests.el b/tempel-tempo-tests.el new file mode 100644 index 0000000..214be28 --- /dev/null +++ b/tempel-tempo-tests.el @@ -0,0 +1,262 @@ +;;; tempel-tempo-tests.el --- Test suite for tempo.el compatibility -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Free Software Foundation, Inc. + +;; Author: Elias Gabriel Pérez +;; Keywords: abbrev, languages, tools, text + +;; This program 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 3 of the License, or +;; (at your option) any later version. + +;; This program 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 this program. If not, see . + +;;; Code: + +(require 'tempel-tempo) +(eval-when-compile (require 'cl-lib)) + +(ert-deftest tempel-tempo-string-element-test () + "Test a template containing a string element." + (with-temp-buffer + (tempel-tempo-define-template "test" '("GNU Emacs Tempo test")) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (should (equal (buffer-string) "GNU Emacs Tempo test")))) + +(ert-deftest tempel-tempo-p-bare-element-test () + "Test a template containing a bare `p' element." + (with-temp-buffer + (tempel-tempo-define-template "test" '("abcde" p)) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (tempel-next) + (should (equal (point) 6)))) + +(ert-deftest tempel-tempo-r-bare-element-test () + "Test a template containing a bare `r' element." + (with-temp-buffer + (tempel-tempo-define-template "test" '("abcde" r "ghijk")) + (insert "F") + (set-mark (point)) + (goto-char (point-min)) + (tempel-tempo-insert-template 'tempel-tempo-template-test t) + (should (equal (buffer-string) "abcdeFghijk")))) + +(ert-deftest tempel-tempo-p-element-test () + "Testing template containing a `p' (prompt) element." + (with-temp-buffer + (tempel-tempo-define-template "test" '("hello " (p ">"))) + (let ((tempel-tempo-interactive t)) + (cl-letf (((symbol-function 'read-string) (lambda (&rest _) "world"))) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil)) + (should (equal (buffer-string) "hello world"))))) + +(ert-deftest tempel-tempo-P-element-test () + "Testing template containing a `P' (prompt) element." + (with-temp-buffer + (tempel-tempo-define-template "test" '("hello " (P ">"))) + ;; By default, `tempo-interactive' is nil, `P' should ignore this. + (cl-letf (((symbol-function 'read-string) (lambda (&rest _) "world"))) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil)) + (should (equal (buffer-string) "hello world")))) + +(ert-deftest tempel-tempo-r-element-test () + "Testing template containing an `r' (with prompt) element." + (with-temp-buffer + (tempel-tempo-define-template "test" '("abcde" (r ">") "ghijk")) + (let ((tempel-tempo-interactive t)) + (cl-letf (((symbol-function 'read-string) (lambda (&rest _) "F"))) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil)) + (should (equal (buffer-string) "abcdeFghijk"))))) + +(ert-deftest tempel-tempo-s-element-test () + "Testing template containing an `s' element." + (with-temp-buffer + (tempel-tempo-define-template "test" '("hello " (p ">" P1) " " (s P1))) + (let ((tempel-tempo-interactive t)) + (cl-letf (((symbol-function 'read-string) (lambda (&rest _) "world!"))) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil)) + (should (equal (buffer-string) "hello world! world!"))))) + +(ert-deftest tempel-tempo-&-element-test () + "Testing template containing an `&' element." + (tempel-tempo-define-template "test" '(& "test")) + (with-temp-buffer + (insert " ") + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (should (equal (buffer-string) " test"))) + (with-temp-buffer + (insert "hello") + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (should (equal (buffer-string) "hello\ntest")))) + +(ert-deftest tempel-tempo-%-element-test () + "Testing template containing an `%' element." + (tempel-tempo-define-template "test" '("test" %)) + (with-temp-buffer + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (should (equal (buffer-string) "test"))) + (with-temp-buffer + (insert "hello") + (goto-char (point-min)) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (should (equal (buffer-string) "test\nhello")))) + +(ert-deftest tempel-tempo-n-element-test () + "Testing template containing an `n' element." + (tempel-tempo-define-template "test" '("test" n "test")) + (with-temp-buffer + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (should (equal (buffer-string) "test\ntest")))) + +(ert-deftest tempel-tempo-n>-element-test () + "Testing template containing an `n>' element." + (tempel-tempo-define-template "test" '("(progn" n> "(list 1 2 3))")) + (with-temp-buffer + (emacs-lisp-mode) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + ;; Tempo should have inserted two spaces before (list 1 2 3) + (should (equal (buffer-string) "(progn\n (list 1 2 3))")))) + +(ert-deftest tempel-tempo->-element-test () + "Testing template containing a `>' element." + (with-temp-buffer + (emacs-lisp-mode) + (insert "(progn\n)") + (backward-char) + (tempel-tempo-define-template "test" '("(list 1 2 3)" >)) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + ;; Tempo should have inserted two spaces before (list 1 2 3) + (should (equal (buffer-string) "(progn\n (list 1 2 3))")))) + +(ert-deftest tempel-tempo-r>-bare-element-test () + "Testing template containing a bare `r>' element." + (with-temp-buffer + (tempel-tempo-define-template "test" '("(progn" n r> ")")) + (emacs-lisp-mode) + (insert "(list 1 2 3)") + (set-mark (point)) + (goto-char (point-min)) + (tempel-tempo-insert-template 'tempel-tempo-template-test t) + ;; Tempo should have inserted two spaces before (list 1 2 3) + (should (equal (buffer-string) "(progn\n (list 1 2 3))")))) + +(ert-deftest tempel-tempo-r>-element-test () + "Testing template containing an `r>' (with prompt) element." + (tempel-tempo-define-template "test" '("(progn" n (r> ":") ")")) + (with-temp-buffer + ;; Test on-region use + (emacs-lisp-mode) + (insert "(list 1 2 3)") + (set-mark (point)) + (goto-char (point-min)) + (tempel-tempo-insert-template 'tempel-tempo-template-test t) + (should (equal (buffer-string) "(progn\n (list 1 2 3))"))) + (with-temp-buffer + ;; Test interactive use + (emacs-lisp-mode) + (let ((tempel-tempo-interactive t)) + (cl-letf (((symbol-function 'read-string) (lambda (&rest _) " (list 1 2 3)"))) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil)) + (should (equal (buffer-string) "(progn\n (list 1 2 3))"))))) + +(ert-deftest tempel-tempo-o-element-test () + "Testing template containing an `o' element." + (with-temp-buffer + (tempel-tempo-define-template "test" '("test" o)) + (insert "hello") + (goto-char (point-min)) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (should (equal (buffer-string) "test\nhello")) + (should (equal (point) 5)))) + +(ert-deftest tempel-tempo-nil-element-test () + "Testing template with nil elements." + (with-temp-buffer + (tempel-tempo-define-template "test" '("Hello," nil " World!")) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (should (equal (buffer-string) "Hello, World!")))) + +(ert-deftest tempel-tempo-eval-element-test () + "Testing template with Emacs Lisp expressions." + (with-temp-buffer + (tempel-tempo-define-template "test" '((int-to-string (+ 1 1)) "=" (concat "1" "+1"))) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (should (equal (buffer-string) "2=1+1")))) + +(ert-deftest tempel-tempo-l-element-test () + "Testing template containing an `l' element." + (with-temp-buffer + (tempel-tempo-define-template "test" '("list: " (l "1, " "2, " (int-to-string (+ 1 2))))) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (should (equal (buffer-string) "list: 1, 2, 3")))) + +(ert-deftest tempel-tempo-tempo-user-elements-test () + "Testing a template with elements for `tempo-user-elements'." + (with-temp-buffer + (add-hook 'tempel-user-elements + (lambda (x) (int-to-string (* x x))) nil :local) + (tempel-tempo-define-template "test" '(1 " " 2 " " 3 " " 4)) + (tempel-tempo-insert-template 'tempel-tempo-template-test nil) + (should (equal (buffer-string) "1 4 9 16")))) + +(ert-deftest tempel-tempo-expand-tag-test () + "Testing expansion of a template with a tag." + (with-temp-buffer + (tempel-tempo-define-template "test" '("Hello, World!") "hello") + (insert "hello") + (tempel-expand t) + (should (equal (buffer-string) "Hello, World!")))) + +(ert-deftest tempel-tempo-define-tag-globally-test () + "Testing usage of a template tag defined from another buffer." + (tempel-tempo-define-template "test" '("Hello, World!") "hello") + + (with-temp-buffer + ;; Use a tag in buffer 1 + (insert "hello") + (tempel-expand t) + (should (equal (buffer-string) "Hello, World!")) + (erase-buffer) + + ;; Define a tag on buffer 2 + (with-temp-buffer + (tempel-tempo-define-template "test2" '("Now expanded.") "mytag")) + + ;; I should be able to use this template back in buffer 1 + (insert "mytag") + (tempel-expand t) + (should (equal (buffer-string) "Now expanded.")))) + +(ert-deftest tempel-tempo-overwrite-tag-test () + "Testing ability to reassign templates to tags." + (with-temp-buffer + ;; Define a tag and use it + (tempel-tempo-define-template "test-tag-1" '("abc") "footag") + (insert "footag") + (tempel-expand t) + (should (equal (buffer-string) "abc")) + (erase-buffer) + + ;; Define a new template with the same tag + (tempel-tempo-define-template "test-tag-2" '("xyz") "footag") + (insert "footag") + (tempel-expand t) + (should (equal (buffer-string) "xyz")))) + +(ert-deftest tempel-tempo-expand-partial-tag-test () + "Testing expansion of a template with a tag, with a partial match." + (with-temp-buffer + (tempel-tempo-define-template "test" '("Hello, World!") "hello") + (insert "hel") + (tempel-complete t) + (should (equal (buffer-string) "Hello, World!")))) + +(provide 'tempel-tempo-tests) +;;; tempel-tempo-tests.el ends here diff --git a/tempel-tempo.el b/tempel-tempo.el new file mode 100644 index 0000000..f2217d5 --- /dev/null +++ b/tempel-tempo.el @@ -0,0 +1,188 @@ +;;; tempel-tempo.el --- Tempo Compatibility layer -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Free Software Foundation, Inc. + +;; Author: Elias Gabriel Pérez +;; Keywords: abbrev, languages, tools, text + +;; This program 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 3 of the License, or +;; (at your option) any later version. + +;; This program 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 this program. If not, see . + +;;; Commentary: + +;; This package provides ports of some tempo functions and variables. + +;;; Code: + +(require 'tempel) + +;; DONE: + + +;;; User Options + +(defgroup tempel nil + "Flexible template insertion." + :prefix "tempo-" + :group 'tools + :group 'tempel) + +;; NOTE: `tempel-tempo-insert-region' and `tempel-tempo-interactive' +;; must have effect only in `tempo-complete-tag' and +;; `tempo-expand-if-complete'. + +(defcustom tempel-tempo-interactive nil + "Prompt user for strings in templates. +If this variable is non-nil, tempel prompts the user for text to insert +in the templates." + :type 'boolean) + +(defcustom tempel-tempo-insert-region nil + "Automatically insert current region when there is a `r' in the template. +If this variable is nil, `r' elements will be treated just like `p' +elements, unless the template function is given a prefix (or a non-nil +argument). If this variable is non-nil, the behavior is reversed." + :type 'boolean) + + +;;; Variables + +(defvar tempel-tempo-tags nil + "An association list with tags and corresponding templates.") + +(defvar-local tempel-tempo-local-tags nil + "A list of locally installed tag completion lists. +`tempel-tempo-tags' is always in the last position in this list.") + + +;;; Functions and Macros + +;; Not from tempo.el +(defmacro tempel-tempo--progn (&rest body) + "Used to provide tempo compatibility to some functions or commands. +This acts like `progn', with BODY." + `(cl-letf (((symbol-function #'tempel--region) + (lambda () + (when (and (use-region-p) + tempel-tempo-insert-region) + (when (< (mark) (point)) (exchange-point-and-mark)) + (cons (point-marker) (mark-marker))))) + ((symbol-function #'tempel--placeholder) + (lambda (&optional prompt name noinsert only-prompt) + (when tempel-tempo-interactive (setq only-prompt t)) + (setq prompt + (cond + ((and (stringp prompt) (or only-prompt noinsert)) + (read-string prompt)) + ((stringp prompt) (propertize prompt 'tempel--default t)) + ;; TEMPEL EXTENSION: Evaluate prompt + (t (eval prompt (cdar tempel--active))))) + (if noinsert + (progn (setf (alist-get name (cdar tempel--active)) prompt) nil) + (tempel--field name prompt)) + (deactivate-mark)))) + (let (tempel-done-on-region) ; This var must be disabled + ,@body))) + +;; Not from tempo.el +(defun tempel-tempo--make-templates () + "Convert Tempo tags to valid Tempel templates format." + (mapcar + (lambda (tags) `(,(car tags) ,@(symbol-value (cdr tags)))) + (append tempel-tempo-local-tags + tempel-tempo-tags))) + +(defun tempel-tempo-save-named (name data) + "Save NAME with DATA as content for later insertion. +The data can later be retrieved with `tempel-tempo-lookup-named'. + +This function returns nil, so it can be used in a template without +inserting anything." + (setf (alist-get name (cdar tempel--active)) data) + nil) + +(defalias 'tempel-tempo-build-collection 'tempel--templates + "Build a collection of all the tags and return it.") + +;; Currently only used in `tempel-tempo-tests.el' +(defun tempel-tempo-insert-template (template on-region) + "Insert a template. +TEMPLATE is the template to be inserted. If ON-REGION is non-nil the +`r' elements are replaced with the current region. In Transient Mark +mode, ON-REGION is ignored and assumed true if the region is active." + (let ((tempel-tempo-insert-region + (or on-region tempel-tempo-insert-region))) + (tempel-tempo--progn + (when (symbolp template) (setq template (symbol-value template))) + (tempel--insert template (tempel--region))))) + +(defun tempel-tempo-define-template (name elements &optional tag documentation taglist) + "Define a template." + (let ((tag-name (intern (concat "tempel-tempo-template-" name))) + (taglist (or taglist 'tempel-tempo-tags)) + ;; Add DOCUMENTATION :doc to ELEMENTS unless it already have + ;; one. + (elements + (if (and (not (memq :doc elements)) + documentation) + (append elements `(:doc ,documentation)) + elements))) + (set tag-name elements) + (fset tag-name (lambda (&optional arg) + (:documentation + (or documentation (concat "Insert a " name "."))) + (interactive "*P") + (tempel--insert + elements + (and (or arg tempel-tempo-insert-region) + (tempel--region))))) + (when tag (tempel-tempo-add-tag tag tag-name taglist)) + tag-name)) + +(defun tempel-tempo-use-tag-list (tag-list) + "Install TAG-LIST to be used for template completion in the current buffer. +TAG-LIST is a symbol whose variable value is a tag list created with +`tempo-add-tag'." + (setq-local tempel-tempo-local-tags (symbol-value tag-list))) + + +;;; Commands + +(defalias 'tempel-tempo-forward-mark 'tempel-next + "Jump to the next mark.") + +(defalias 'tempel-tempo-backward-mark 'tempel-previous + "Jump to the previous mark.") + +(defun tempel-tempo-add-tag (tag template &optional tag-list) + "Add a template tag. +Add the TAG, that should complete to TEMPLATE to the list in TAG-LIST, +or to `tempel-tempo-tags' if TAG-LIST is nil. If TAG was already +in the list, replace its template with TEMPLATE." + (interactive "sTag: \nCTemplate: ") + (setq tag (intern tag)) + (unless tag-list (setq tag-list 'tempel-tempo-tags)) + + (let ((place (assoc tag (symbol-value tag-list)))) + (if place + ;; Tag is already in the list, assign a new template to it. + (setcdr place template) + ;; Tag is not present in the list, add as a new template + (add-to-list tag-list `(,tag ,@template))))) + + + +(add-hook 'tempel-template-sources #'tempel-tempo--make-templates) + +(provide 'tempel-tempo) +;;; tempel-tempo.el ends here diff --git a/tempo.el b/tempo.el new file mode 100644 index 0000000..29d6e9c --- /dev/null +++ b/tempo.el @@ -0,0 +1,772 @@ +;;; tempo.el --- Flexible template insertion -*- lexical-binding: t; -*- + +;; Copyright (C) 1994-1995, 2001-2026 Free Software Foundation, Inc. + +;; Author: David Kågedal +;; Created: 16 Feb 1994 +;; Kågedal's last version number: 1.2.4 +;; Keywords: abbrev, extensions, languages, tools + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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. If not, see . + +;;; Commentary: + +;; This file provides a simple way to define powerful templates, or +;; macros, if you wish. It is mainly intended for, but not limited to, +;; other programmers to be used for creating shortcuts for editing +;; certain kind of documents. It was originally written to be used by +;; a HTML editing mode written by Nelson Minar , +;; and his html-helper-mode.el is probably the best example of how to +;; use this program. + +;; A template is defined as a list of items to be inserted in the +;; current buffer at point. Some of the items can be simple strings, +;; while other can control formatting or define special points of +;; interest in the inserted text. + +;; If a template defines a "point of interest" that point is inserted +;; in a buffer-local list of "points of interest" that the user can +;; jump between with the commands `tempo-backward-mark' and +;; `tempo-forward-mark'. If the template definer provides a prompt for +;; the point, and the variable `tempo-interactive' is non-nil, the +;; user will be prompted for a string to be inserted in the buffer, +;; using the minibuffer. + +;; The template can also define one point to be replaced with the +;; current region if the template command is called with a prefix (or +;; a non-nil argument). + +;; More flexible templates can be created by including Lisp symbols, +;; which will be evaluated as variables, or lists, which will be +;; evaluated as Lisp expressions. + +;; See the documentation for tempo-define-template for the different +;; items that can be used to define a tempo template. + +;; One of the more powerful features of tempo templates are automatic +;; completion. With every template can be assigned a special tag that +;; should be recognized by `tempo-complete-tag' and expanded to the +;; complete template. By default the tags are added to a global list +;; of template tags, and are matched against the last word before +;; point. But if you assign your tags to a specific list, you can also +;; specify another method for matching text in the buffer against the +;; tags. In the HTML mode, for instance, the tags are matched against +;; the text between the last `<' and point. + +;; When defining a template named `foo', a symbol named +;; `tempo-template-foo' will be created whose value as a variable will +;; be the template definition, and its function value will be an +;; interactive function that inserts the template at the point. + +;; The latest tempo.el distribution can be fetched from +;; ftp.lysator.liu.se in the directory /pub/emacs + +;; There is also a WWW page at +;; https://www.lysator.liu.se/~davidk/elisp/ which has some information + +;;; Known bugs: + +;; If the 'o is the first element in a template, strange things can +;; happen when the template is inserted at the beginning of a +;; line. This is due to strange behavior in open-line. But it should +;; be easily avoided. + +;; The 'o tag is also a problem when including the region. This will +;; be looked into. + +;; Clicking mouse-2 in the completion buffer gives strange results. + +;; There is a bug in some emacs versions that prevents completion from +;; working. If it doesn't work for you, send me a note indicating your +;; emacs version and your problems. + +;;; Contributors: + +;; These people have given me important feedback and new ideas for +;; tempo.el. Thanks. + +;; Nelson Minar +;; Richard Stallman +;; Lars Lindberg +;; Glen Whitney + +;;; Code: + +(require 'tempel) +(require 'cl-lib) + +;;; User options + +(defgroup tempo nil + "Flexible template insertion." + :prefix "tempo-" + :group 'tools) + +(defcustom tempo-interactive nil + "Prompt user for strings in templates. +If this variable is non-nil, `tempo-insert' prompts the +user for text to insert in the templates." + :type 'boolean) + +(defcustom tempo-insert-region nil + "Automatically insert current region when there is a `r' in the template. +If this variable is nil, `r' elements will be treated just like `p' +elements, unless the template function is given a prefix (or a non-nil +argument). If this variable is non-nil, the behavior is reversed. + +In Transient Mark mode, this option is unused." + :type 'boolean) + +(defcustom tempo-show-completion-buffer t + "If non-nil, show a buffer with possible completions, when only +a partial completion can be found." + :type 'boolean) + +(defcustom tempo-leave-completion-buffer nil + "If nil, a completion buffer generated by \\[tempo-complete-tag] +disappears at the next keypress; otherwise, it remains forever." + :type 'boolean) + +;;; Internal variables + +(defvar tempo-insert-string-functions nil + "List of functions to run when inserting a string. +Each function is called with a single arg, STRING and should return +another string. This could be used for making all strings upcase by +setting it to (upcase), for example.") +(make-obsolete-variable + 'tempo-insert-string-functions nil "31.1") + +(defvar tempo-tags nil + "An association list with tags and corresponding templates.") + +(defvar-local tempo-local-tags '((tempo-tags)) + "A list of locally installed tag completion lists. +It is an association list where the car of every element is a symbol +whose variable value is a template list. The cdr part, if non-nil, +is a function or a regexp that defines the string to match. See the +documentation for the function `tempo-complete-tag' for more info. + +`tempo-tags' is always in the last position in this list.") + +(defvar-local tempo-collection nil + "A collection of all the tags defined for the current buffer.") +(make-obsolete-variable + 'tempo-collection nil "31.1") + +(defvar-local tempo-dirty-collection t + "Indicates if the tag collection needs to be rebuilt.") +(make-obsolete-variable + 'tempo-dirty-collection nil "31.1") + +(defvar-local tempo-marks nil + "A list of marks to jump to with \\[tempo-forward-mark] and \\[tempo-backward-mark].") +(make-obsolete-variable + 'tempo-marks nil "31.1") + +(defvar-local tempo-match-finder "\\b\\([[:word:]]+\\)\\=" + "The regexp or function used to find the string to match against tags. + +If `tempo-match-finder' is a string, it should contain a regular +expression with at least one \\( \\) pair. When searching for tags, +`tempo-complete-tag' calls `re-search-backward' with this string, and +the string between the first \\( and \\) is used for matching against +each string in the tag list. If one is found, the whole text between +the first \\( and the point is replaced with the inserted template. + +You will probably want to include \\=\\= at the end of the regexp to +make sure that the string is matched only against text adjacent to the +point. + +If `tempo-match-finder' is a symbol, it should be a function that +returns a pair of the form (STRING . POS), where STRING is the string +used for matching and POS is the buffer position after which text +should be replaced with a template.") + +(define-obsolete-variable-alias 'tempo-user-elements 'tempo-user-element-functions "30.1") +(defvaralias 'tempo-user-element-functions 'tempel-user-elements + "Element handlers for user-defined elements. +This is an abnormal hook where the functions are called with one argument +\(an element in a template) and they should return something to be sent to +`tempo-insert' if they recognize the argument, and nil otherwise.") + +(defvar-local tempo-named-insertions nil + "Temporary storage for named insertions.") +(make-obsolete-variable + 'tempo-named-insertions nil "31.1") + +(defvar-local tempo-region-start (make-marker) + "Region start when inserting around the region.") +(make-obsolete-variable + 'tempo-region-start nil "31.1") + +;; Insertion by the template at the region start position should move +;; the marker to preserve the original region contents. +(set-marker-insertion-type tempo-region-start t) + +(defvar-local tempo-region-stop (make-marker) + "Region stop when inserting around the region.") +(make-obsolete-variable + 'tempo-region-stop nil "31.1") + +;;; Functions + +(defmacro tempel-tempo--progn (&rest body) + "Used to provide tempo compatibility to some functions or commands. +This acts like `progn', with BODY." + ;; NOTE: Probably `tempel--element' should be added here too, using + ;; `tempo-insert'. + `(cl-letf (((symbol-function #'tempel--region) + (lambda () + (when (and (use-region-p) + tempo-insert-region) + (when (< (mark) (point)) (exchange-point-and-mark)) + (cons (point-marker) (mark-marker))))) + ((symbol-function #'tempel--placeholder) + (lambda (&optional prompt name noinsert only-prompt) + (when tempo-interactive (setq only-prompt t)) + (setq prompt + (cond + ((and (stringp prompt) (or only-prompt noinsert)) + (read-string prompt)) + ((stringp prompt) (propertize prompt 'tempel--default t)) + ;; TEMPEL EXTENSION: Evaluate prompt + (t (eval prompt (cdar tempel--active))))) + (if noinsert + (progn (setf (alist-get name (cdar tempel--active)) prompt) nil) + (tempel--field name prompt)) + (deactivate-mark)))) + (let (tempel-done-on-region) ; This var must be disabled + ,@body + ;; Remove tempel faces + (seq-find + (lambda (ov) + (when (memq (overlay-get ov 'face) + '(tempel-form tempel-field tempel-default)) + (overlay-put ov 'face 'defa))) + (overlays-in (point-min) (point-max)))))) + +(defun tempo--make-tempel-templates () + "Convert Tempo tags to valid Tempel templates format." + (mapcar + (lambda (tags) `(,(car tags) ,@(symbol-value (cdr tags)))) + (apply #'append + (mapcar (lambda (tag-list) + ; If the format for + ; tempo-local-tags changes, + ; change this + (eval (car tag-list) t)) + tempo-local-tags)))) + +;; +;; tempo-define-template + +(defun tempo-define-template (name elements &optional tag documentation taglist) + "Define a template. +This function creates a template variable `tempo-template-NAME' and an +interactive function `tempo-template-NAME' that inserts the template +at the point. The created function is returned. + +NAME is a string that contains the name of the template, ELEMENTS is a +list of elements in the template, TAG is the tag used for completion, +DOCUMENTATION is the documentation string for the insertion command +created, and TAGLIST (a symbol) is the tag list that TAG (if provided) +should be added to. If TAGLIST is nil and TAG is non-nil, TAG is +added to `tempo-tags'. If TAG already corresponds to a template in +the tag list, modify the list so that TAG now corresponds to the newly +defined template. + +The elements in ELEMENTS can be of several types: + + - A string: It is sent to the hooks in `tempo-insert-string-functions', + and the result is inserted. + - The symbol `p': This position is saved in `tempo-marks'. + - The symbol `r': If `tempo-insert' is called with ON-REGION non-nil + the current region is placed here. Otherwise it works like `p'. + - (p PROMPT ): If `tempo-interactive' is non-nil, the + user is prompted in the minibuffer with PROMPT for a string to be + inserted. If the optional parameter NAME is non-nil, the text is + saved for later insertion with the `s' tag. If there already is + something saved under NAME that value is used instead and no + prompting is made. If NOINSERT is provided and non-nil, nothing is + inserted, but text is still saved when a NAME is provided. For + clarity, the symbol `noinsert' should be used as argument. + - (P PROMPT ): Works just like the previous tag, but + forces `tempo-interactive' to be true. + - (r PROMPT ): Like the previous tag, but if + `tempo-interactive' is nil and `tempo-insert' is called with + ON-REGION non-nil, the current region is placed here. This usually + happens when you call the template function with a prefix argument. + - (s NAME): Inserts text previously read with the (p ..) construct. + Finds the insertion saved under NAME and inserts it. Acts like `p' + if `tempo-interactive' is nil. + - `&': If there is only whitespace between the line start and point, + nothing happens. Otherwise a newline is inserted. + - `%': If there is only whitespace between point and end of line, + nothing happens. Otherwise a newline is inserted. + - `n': Inserts a newline. + - `>': The line is indented using `indent-according-to-mode'. Note + that you often should place this item after the text you want on + the line. + - `r>': Like `r', but it also indents the region. + - (r> PROMPT ): Like (r ...), but is also indents + the region. + - `n>': Inserts a newline and indents line. + - `o': Like `%' but leaves the point before the newline. + - nil: It is ignored. + - Anything else: Each function in `tempo-user-element-functions' is called + with it as argument until one of them returns non-nil, and the + result is inserted. If all of them return nil, it is evaluated and + the result is treated as an element to be inserted. One additional + tag is useful for these cases. If an expression returns a list (l + foo bar), the elements after `l' will be inserted according to the + usual rules. This makes it possible to return several elements + from one expression." + (let ((tag-name (intern (concat "tempo-template-" name))) + (taglist (or taglist 'tempo-tags)) + ;; Add DOCUMENTATION :doc to ELEMENTS unless it already have + ;; one. + (elements + (if (and (not (memq :doc elements)) + documentation) + (append elements `(:doc ,documentation)) + elements))) + (set tag-name elements) + (fset tag-name (lambda (&optional arg) + (:documentation + (or documentation (concat "Insert a " name "."))) + (interactive "*P") + (tempel--insert + elements + (and (or arg tempo-insert-region) + (tempel--region))))) + (when tag (tempo-add-tag tag tag-name taglist)) + tag-name)) + +;;; +;;; tempo-insert-template + +(defun tempo-insert-template (template on-region) + "Insert a template. +TEMPLATE is the template to be inserted. If ON-REGION is non-nil the +`r' elements are replaced with the current region. In Transient Mark +mode, ON-REGION is ignored and assumed true if the region is active." + (unwind-protect + (let ((tempo-insert-region + (or on-region tempo-insert-region))) + (tempel-tempo--progn + (when (symbolp template) (setq template (symbol-value template))) + (tempel--insert template (tempel--region)))) + (and transient-mark-mode + (deactivate-mark)))) + +;;; +;;; tempo-insert + +;;; TODO: Probably `tempo-insert' must be reworked to work like +;;; `tempel--element', they are almost compatible. +(defun tempo-insert (element on-region) + "Insert a template ELEMENT. +Insert one element from a template. If ON-REGION is non-nil the `r' +elements are replaced with the current region. + +See documentation for `tempo-define-template' for the kind of elements +possible." + (declare (obsolete tempel-insert "31.1")) + (pcase element + ((pred stringp) (tempo-process-and-insert-string element)) + (`(p . ,rest) (tempo-insert-prompt-compat rest)) + (`(P . ,rest) (let ((tempo-interactive t)) + (tempo-insert-prompt-compat rest))) + ;; (`(v ,name ,data) (tempo-save-named name nil data)) + (`(r . ,rest) (if on-region + (goto-char tempo-region-stop) + (tempo-insert-prompt-compat rest))) + (`(r> . ,rest) (if on-region + (progn + (goto-char tempo-region-stop) + (indent-region tempo-region-start + tempo-region-stop)) + (tempo-insert-prompt-compat rest))) + (`(s ,name) (tempo-insert-named name)) + (`(l . ,rest) (dolist (elt rest) (tempo-insert elt on-region))) + ('p (tempo-insert-mark (point-marker))) + ('r (if on-region + (goto-char tempo-region-stop) + (tempo-insert-mark (point-marker)))) + ('r> (if on-region + (progn + (goto-char tempo-region-stop) + (indent-region tempo-region-start tempo-region-stop)) + (tempo-insert-mark (point-marker)))) + ('> (indent-according-to-mode)) + ('& (if (not (or (= (current-column) 0) + (save-excursion + (re-search-backward + "^\\s-*\\=" nil t)))) + (insert "\n"))) + ('% (if (not (or (eolp) + (save-excursion + (re-search-forward + "\\=\\s-*$" nil t)))) + (insert "\n"))) + ('n (insert "\n")) + ('n> (insert "\n") (indent-according-to-mode)) + ;; Bug: If the 'o is the first element in a template, strange + ;; things can happen when the template is inserted at the + ;; beginning of a line. + ('o (if (not (or on-region + (eolp) + (save-excursion + (re-search-forward + "\\=\\s-*$" nil t)))) + (open-line 1))) + ('nil nil) + (_ (tempo-insert (or (tempo-is-user-element element) + (eval element t)) + on-region)))) + +;;; +;;; tempo-insert-prompt + +(defun tempo-insert-prompt-compat (prompt) + "Compatibility hack for `tempo-insert-prompt'. +PROMPT can be either a prompt string, or a list of arguments to +`tempo-insert-prompt', or nil." + (declare (obsolete nil "31.1")) + (if (consp prompt) ; not nil either + (apply #'tempo-insert-prompt prompt) + (tempo-insert-prompt prompt))) + +(defun tempo-insert-prompt (prompt &optional save-name no-insert) + "Prompt for a text string and insert it in the current buffer. +If the variable `tempo-interactive' is non-nil the user is prompted +for a string in the minibuffer, which is then inserted in the current +buffer. If `tempo-interactive' is nil, the current point is placed on +`tempo-mark'. + +PROMPT is the prompt string, SAVE-NAME is a name to save the inserted +text under. If the optional argument NO-INSERT is non-nil, no text is +inserted. This can be useful when there is a SAVE-NAME. + +If there already is a value for SAVE-NAME, it is used and the user is +never prompted." + (declare (obsolete nil "31.1")) + (let (insertion + (previous (and save-name + (tempo-lookup-named save-name)))) + (cond + ;; Insert previous value, unless no-insert is non-nil + ((and previous + (not no-insert)) + (tempo-insert-named save-name)) ; A double lookup here, but who + ; cares + ;; If no-insert is non-nil, don't insert the previous value. Just + ;; keep it + (previous + nil) + ;; No previous value. Prompt or insert mark + (tempo-interactive + (if (not (stringp prompt)) + (error "tempo: The prompt (%s) is not a string" prompt)) + (setq insertion (read-string prompt)) + (or no-insert + (insert insertion)) + (if save-name + (tempo-save-named save-name insertion))) + (t + (tempo-insert-mark (point-marker)))))) + +;;; +;;; tempo-is-user-element + +(defun tempo-is-user-element (element) + "Try all the user-defined element handlers in `tempo-user-element-functions'." + (declare (obsolete nil "31.1")) + (run-hook-with-args-until-success 'tempo-user-element-functions element)) + +;;; +;;; tempo-forget-insertions + +(defun tempo-forget-insertions () + "Forget all the saved named insertions." + (declare (obsolete nil "31.1")) + (setq tempo-named-insertions nil)) + +;;; +;;; tempo-save-named + +(defun tempo-save-named (name data) ; Had an optional prompt for 'v + "Save some data for later insertion. +The contents of DATA is saved under the name NAME. + +The data can later be retrieved with `tempo-lookup-named'. + +This function returns nil, so it can be used in a template without +inserting anything." + (setf (alist-get name (cdar tempel--active)) data) + nil) + +;;; +;;; tempo-lookup-named + +(defun tempo-lookup-named (name) + "Lookup some saved data under the name NAME. +Returns the data if NAME was found, and nil otherwise." + (declare (obsolete "Use NAME instead." "31.1")) + (alist-get name (cdar tempel--active))) + +;;; +;;; tempo-insert-named + +(defun tempo-insert-named (name) + "Insert the previous insertion saved under a named specified in NAME. +If there is no such name saved, a tempo mark is inserted. + +Note that if the data is a string, it will not be run through the string +processor." + (let* ((insertion (tempo-lookup-named name))) + (cond ((null insertion) + (tempo-insert-mark (point-marker))) + ((stringp insertion) + (insert insertion)) + (t + (tempo-insert insertion nil))))) + + +;;; +;;; tempo-process-and-insert-string + +(defun tempo-process-and-insert-string (string) + "Insert a string from a template. +Run a string through the preprocessors in `tempo-insert-string-functions' +and insert the results." + (declare (obsolete nil "31.1")) + (cond ((null tempo-insert-string-functions) + nil) + ((symbolp tempo-insert-string-functions) + (setq string + (funcall tempo-insert-string-functions string))) + ((listp tempo-insert-string-functions) + (dolist (fn tempo-insert-string-functions) + (setq string (funcall fn string)))) + (t + (error "Bogus value in tempo-insert-string-functions: %s" + tempo-insert-string-functions))) + (insert string)) + +;;; +;;; tempo-insert-mark + +(defun tempo-insert-mark (mark) + "Insert a mark `tempo-marks' while keeping it sorted." + (declare (obsolete nil "31.1")) + (cond ((null tempo-marks) (setq tempo-marks (list mark))) + ((< mark (car tempo-marks)) (setq tempo-marks (cons mark tempo-marks))) + (t (let ((lp tempo-marks)) + (while (and (cdr lp) + (<= (car (cdr lp)) mark)) + (setq lp (cdr lp))) + (if (not (= mark (car lp))) + (setcdr lp (cons mark (cdr lp)))))))) + +;;; +;;; tempo-forward-mark + +(define-obsolete-function-alias + 'tempo-forward-mark 'tempel-next + "Jump to the next mark.") + +;;; +;;; tempo-backward-mark + +(define-obsolete-function-alias + 'tempo-backward-mark 'tempel-previous + "Jump to the previous mark.") + +;;; +;;; tempo-add-tag + +(defun tempo-add-tag (tag template &optional tag-list) + "Add a template tag. +Add the TAG, that should complete to TEMPLATE to the list in TAG-LIST, +or to `tempo-tags' if TAG-LIST is nil. If TAG was already in the list, +replace its template with TEMPLATE." + + (interactive "sTag: \nCTemplate: ") + (setq tag (intern tag)) + (unless tag-list (setq tag-list 'tempo-tags)) + + (let ((place (assoc tag (symbol-value tag-list)))) + (if place + ;; Tag is already in the list, assign a new template to it. + (setcdr place template) + ;; Tag is not present in the list, add as a new template + (add-to-list tag-list `(,tag ,@template))))) + +;;; +;;; tempo-use-tag-list + +(defun tempo-use-tag-list (tag-list &optional completion-function) + "Install TAG-LIST to be used for template completion in the current buffer. +TAG-LIST is a symbol whose variable value is a tag list created with +`tempo-add-tag'. + +COMPLETION-FUNCTION is an obsolete option for specifying an optional +function or string that is used by \\[tempo-complete-tag] to find a +string to match the tag against. It has the same definition as the +variable `tempo-match-finder'. In this version, supplying a +COMPLETION-FUNCTION just sets `tempo-match-finder' locally." + (setf (alist-get tag-list tempo-local-tags) completion-function) + (if completion-function + (setq tempo-match-finder completion-function))) + +;;; +;;; tempo-invalidate-collection + +(defun tempo-invalidate-collection (&optional global) + "Mark the tag collection as obsolete. +Whenever it is needed again it will be rebuilt. If GLOBAL is non-nil, +mark the tag collection of all buffers as obsolete, not just the +current one." + (declare (obsolete nil "31.1")) + (if global + (dolist (buffer (buffer-list)) + (with-current-buffer buffer + (when (assq 'tempo-dirty-collection (buffer-local-variables)) + (setq tempo-dirty-collection t)))) + (setq tempo-dirty-collection t))) + +;;; +;;; tempo-build-collection + +(define-obsolete-function-alias + 'tempo-build-collection 'tempel--templates + "Build a collection of all the tags and return it.") + +;;; +;;; tempo-find-match-string + +(defun tempo-find-match-string (finder) + "Find a string to be matched against a tag list. +FINDER is a function or a string. Returns (STRING . POS), or nil +if no reasonable string is found." + (cond ((stringp finder) + (if (save-excursion (re-search-backward finder nil t)) + (cons (match-string 1) ; This seems to be a bug in Emacs (?) + (match-beginning 1)) + nil)) + (t + (funcall finder)))) + +;;; +;;; tempo-complete-tag + +(defun tempo-complete-tag (&optional silent) + "Look for a tag and expand it. +All the tags in the tag lists in `tempo-local-tags' +\(this includes `tempo-tags') are searched for a match for the text +before the point. The way the string to match for is determined can +be altered with the variable `tempo-match-finder'. If +`tempo-match-finder' returns nil, then the results are the same as +no match at all. + +If a single match is found, the corresponding template is expanded in +place of the matching string. + +If a partial completion or no match at all is found, and SILENT is +non-nil, the function will give a signal. + +If a partial completion is found and `tempo-show-completion-buffer' is +non-nil, a buffer containing possible completions is displayed." + (declare (obsolete tempel-complete "31.1")) + ;; This function may look like a hack, but this is how I want it to + ;; work. + (interactive "*") + (let* ((collection (tempel--templates)) + (match-info (tempo-find-match-string tempo-match-finder)) + (match-string (car match-info)) + (match-start (cdr match-info)) + (exact (assoc (and match-string (intern match-string)) collection)) + (compl (or (car exact) + (and match-info (try-completion match-string collection))))) + (tempel-tempo--progn + (when compl (delete-region match-start (point))) + (tempel--save) + (cond ((null match-info) (or silent (ding))) + ((null compl) (or silent (ding))) + ((eq compl t) + (tempo-insert-template (cdr exact) :region-if-possible)) + (t (if (setq exact (assoc (if (stringp compl) (intern compl) compl) + collection)) + (tempo-insert-template (cdr exact) :region-if-possible) + (insert compl) + (or silent (ding)) + (if tempo-show-completion-buffer + (tempo-display-completions match-string + collection)))))))) + + +;;; +;;; tempo-display-completions + +(defun tempo-display-completions (string tag-list) + "Show a buffer containing possible completions for STRING." + (if tempo-leave-completion-buffer + (with-output-to-temp-buffer "*Completions*" + (display-completion-list + (completion-hilit-commonality (all-completions string tag-list) + (length string)))) + (save-window-excursion + (with-output-to-temp-buffer "*Completions*" + (display-completion-list + (completion-hilit-commonality (all-completions string tag-list) + (length string)))) + (sit-for 32767)))) + +;;; +;;; tempo-expand-if-complete + +(defun tempo-expand-if-complete () + "Expand the tag before point if it is complete. +Returns non-nil if an expansion was made and nil otherwise. + +This could as an example be used in a command that is bound to the +space bar, and looks something like this: + +\(defun tempo-space () + (interactive \"*\") + (or (tempo-expand-if-complete) + (insert \" \")))" + (declare (obsolete tempel-expand "31.1")) + (interactive) + (let* ((collection (tempel--templates)) + (match-info (tempo-find-match-string tempo-match-finder)) + (match-string (car match-info)) + (exact (assoc (intern match-string) collection))) + (when exact + (tempel-tempo--progn + (tempel--save) + (tempel--exit collection (tempel--region) match-string 'finished))))) + +;; +;; That is it + +(add-hook 'tempel-template-sources #'tempo--make-tempel-templates) + +(provide 'tempo) + +;;; tempo.el ends here