Terse and flexible in-file code generation.
Reminder: C-c '
to edit source blocks.
WARNING this minor mode is a security concern. It reads and evaluates arbitrary elisp code in potentially any file. You’ve been warned.
autoc-mode
allows you to embed operations into Emacs buffers in order to
perform arbitrary text generation.
In the most flexible case, you can define arbitrary elisp that you can later evaluate.
More usefully, a number of operations are predefined to let you work with source code.
TODO sandbox arbitrary code (https://github.com/joelmccracken/elisp-sandbox).
All autoc functionality is accessed via “autoc” markers in the source code comments. Markers are made up of a base string, and a single character:
(defvar autoc-marker-prefix "autoc"
"The base autoc marker prefix")
(defvar autoc-marker-block ":"
"Operate on block (requires block end marker)")
(defvar autoc-marker-end "#"
"Block end marker")
(defvar autoc-marker-command "!"
"Autoc command (single line)")
For example, the full marker to perform a block operation is autoc:
.
Markers are then given arguments, which are used to determine what to do. The
arguments are separated by spaces (first space is optional). The first
argument is a “operation” (function, in a special autoc namespace). The rest
of the arguments are passed to the operation as normal function arguments.
<marker-prefix><marker> <operation> <arg2> ...
We require a few external libraries.
#+NAME requires
(require 's)
(require 'thing-at-point)
Core functionality. We need to do a few things
- Find previous/next autoc marker
- Get a block contents
- Clear a block contents
- Extract marker type and arguments
First, we need to be able to find markers.
(defun autoc-word-at-point-is-marker-p (&optional suffix)
"Return t if the word at point is an autoc marker ending in suffix"
(s-starts-with-p (concat autoc-marker-prefix suffix)
(buffer-substring-no-properties (point)
(line-end-position))
'ignore-case))
We want to be able to find the next/previous marker in the buffer, so we can jump between them.
(defun autoc-previous-marker (&optional suffix)
"Return point at the start of the previous autoc marker. If
suffix is provided, the marker must have the given suffix"
(save-excursion
(when (search-backward autoc-marker-prefix nil t)
(if (autoc-word-at-point-is-marker-p suffix)
(point)
(progn
(forward-line -1)
(end-of-line)
(autoc-previous-marker suffix))))))
(defun autoc-next-marker (&optional suffix)
"Return point at the start of the next autoc marker"
(save-excursion
(when (search-forward autoc-marker-prefix nil t)
;; search-forward puts point at end of marker, so move left:
(left-char (length autoc-marker-prefix))
(if (autoc-word-at-point-is-marker-p suffix)
(point)
(progn
(forward-line 1)
(autoc-next-marker suffix))))))
We can retrieve the current marker’s arguments, which include everything after the marker until the end of the line. We read the whole thing in as an elisp list.
(defun autoc-get-marker-args (line)
"Get all the arguments for the marker in the given line
autoc:lines foo bar => '(lines foo bar)
autoc: funcall alice => '(funcall alice)
autoc! blah => '(blah)"
(string-match (concat autoc-marker-prefix ".\\(.*\\)") line)
(read (concat "(" (s-trim (match-string 1 line)) ")")))
Next, some block manipulation routines. We need to be able to deal with blocks without worrying about them.
Here we define where a block starts and ends.
(defun autoc-block-start ()
"Return point at the start of the current block
e.g. with point before 'block':
autoc:something
inside |block
autoc#
point returned:
autoc:something
|inside block
autoc:end"
(save-excursion
(end-of-line)
(let ((pos (autoc-previous-marker autoc-marker-block)))
(when pos
(goto-char pos)
(next-line)
(line-beginning-position)))))
(defun autoc-block-end (start)
"Return point at the end of the block starting at `start'
e.g. with point before 'block':
autoc:something
inside |block
autoc:end
point returned:
autoc:something
inside block
|autoc:end"
(save-excursion
(goto-char start)
(let ((pos (autoc-next-marker autoc-marker-end)))
(when pos
(goto-char pos)
(line-beginning-position)))))
We can check whether we are in a block, which will be useful later.
(defun autoc-in-block-p ()
"Return t if point is on any line in a block, including the start and end marker lines"
(interactive)
(let* ((start (autoc-block-start))
(end (autoc-block-end start))
(pos (line-number-at-pos)))
(and start
end
(>= pos (- (line-number-at-pos start) 1))
(<= pos (line-number-at-pos end)))))
(defmacro autoc-when-in-block (&rest body)
"Evaluate `body' if currently in a block, alerting the user otherwise"
`(if (autoc-in-block-p)
(progn
,@body)
(message "autoc: not in a block")))
Now that we have the start and end of the blocks defined, we can do operations on blocks.
(defun autoc-kill-block ()
"Delete all content in the current block"
(interactive)
(let* ((start (autoc-block-start))
(end (autoc-block-end start)))
(when (> (count-lines start end) 0)
(kill-region start end))))
(defun autoc-block-contents ()
"Get the contents of the current block"
(let* ((start (autoc-block-start))
(end (autoc-block-end start)))
(s-trim (buffer-substring-no-properties start end))))
(defun autoc-block-get-marker-line ()
"Get the full starting marker line for the current block"
(save-excursion
(goto-char (autoc-block-start))
(previous-line)
(s-trim (thing-at-point 'line t))))
(defun autoc-end-of-current-block ()
"Go to the end of the block under point if it exists"
(interactive)
(autoc-when-in-block
(goto-char (autoc-block-end (autoc-block-start)))))
Operations are functions that run in a buffer, and can do pretty much anything. Most of the time, they operate on a block of text, delimited by markers.
Who knows what the best way of implementing this is. All the rest of the code cares about is making and running operations. For now lets go with a simple plist.
(defvar autoc-operations-plist nil
"List of operations and their functions")
The magical lookup function, and a helper.
(defun autoc-get-operation-fn (symbol)
"Return the function implementing the operation"
(plist-get autoc-operations-plist symbol))
(defun autoc-has-operation-fn-p (symbol)
"Return t if the given operation exists"
(plist-member autoc-operations-plist symbol))
And adding new operations.
(defun autoc-add-operation (symbol function)
"Add a operation. If it already exists, it is replaced"
(setq autoc-operations-plist
(plist-put autoc-operations-plist symbol function)))
Now we can run operations!
(defun autoc-run-operation (operation args)
"Run the `operation' with `args' if possible"
;; TODO if len op is 1, lookup in aliases
(if (autoc-has-operation-fn-p operation)
(apply (autoc-get-operation-fn operation) args)
(message (format "Unknown operation: ~A" operation))))
Lets test it for fun:
(autoc-add-operation 'message (lambda (&rest args) (apply 'message args))) (autoc-run-operation 'message '("bla"))
Instead of using the full operation names, you can use aliases for the commonly used operations. This can be customised.
;; TODO defcustom
(defvar autoc-aliases-alist
'(("=" . block)
("\\" . funcall)
(">" . format-lines)))
Here we tie together the operations (functions in a special namespace) with
autoc
markers. We want to be able to take a marker line, get the operation,
and call it with the arguments. This is easy!
(defun autoc-run-line-operation (line)
"Run the operation for the given marker line"
(let* ((marker-args (autoc-get-marker-args line))
(operation (first marker-args))
(args (rest marker-args)))
(autoc-run-operation operation args)))
We also want to take the current block, find the operation, and run it.
(defun autoc-run-block-operation ()
"Run the autoc operation for the current block"
(interactive)
(autoc-when-in-block
(let ((marker-line (autoc-block-get-marker-line)))
(autoc-run-line-operation marker-line))))
Here we actually implement some useful operations.
A couple of useful bits of pretty syntax.
(defmacro def-autoc-op (name lambda-list &rest body)
"Define an operation with the given name and argument list"
`(autoc-add-operation ',name (lambda ,lambda-list ,@body)))
(defmacro autoc-replace-block (&rest body)
"Kill the current block and then execute body at the start of the block"
`(autoc-when-in-block
(autoc-kill-block)
(goto-char (autoc-block-start))
,@body))
These operations do not directly generate text. They are used for defining things to be used by generator operations below.
We need a buffer local variable to hold source data.
(make-variable-buffer-local
(defvar autoc-source-text-alist nil
"alist of source text blocks - key is block name"))
Define arbitrary functions in your source code, which can be called later with the funcall operation. All other sources can be implemented with this.
TODO: We probably need some safety / sandboxing…
//autoc:defun insert_text (arg1 &key blah) // implicit progn here // (autoc-insert (format nil "Hello ~a" arg1)) //autoc:end
(def-source defun (block-text lambda-list)
"Define an arbitrary function"
(lambda
))
Define an alias for an autoc operation.
//autoc:defalias ^ some-operation
This will allow you to use autoc:^
instead of autoc:some-operation
in other
blocks.
The entire text block between the markers is loaded into a buffer local variable.
//autoc:block block-var hello world //autoc:end -> block-var := "hello\nworld"
Implementation:
(def-autoc-op block (name)
(autoc-when-in-block
(set (make-local-variable name) (autoc-block-contents))))
Load the block into a buffer local variable as a list of lines, optionally doing some processing on them first. The processing is a function that is passed each line and returns the new line.
//autoc:lines lines-var string-upcase hello world //autoc:end -> lines-var := ("HELLO" "WORLD")
Implementation:
(def-autoc-op lines (name &optional fn)
(autoc-when-in-block
(let* ((content (autoc-block-contents))
(modifier (or fn #'identity))
(lines (map 'list modifier (s-split "\n" content))))
(set (make-local-variable name) lines))))
These are all functions that generate the text content of the current block. When the operation is run, the block contents are first cleared and then the function is run. The function operates directly in the Emacs buffer, so be careful :).
Possibly there should be something in between - e.g. the new block contents are returned by the function. I think not. This is more general.
Call a function previously defined with defun.
Take a list of lines and a format string and apply it to each line to generate the new content.
//autoc:format-lines lines-var "FOO($);" FOO(line1); FOO(line2); //autoc:end
Definition:
(def-autoc-op format-lines (lines fmt)
(autoc-replace-block
(dolist (l (symbol-value lines))
(insert (format fmt l))
(newline-and-indent))))
Note, this could also have been done like this:
(defun autoc--format-lines (lines fmt) ...) (autoc-add-operation 'format-lines #'autoc--format-lines)
This is a minor mode! See http://nullprogram.com/blog/2013/02/06/.
;;;###autoload
(define-minor-mode autoc-mode
"Automatic embedded code generation"
:lighter " autoc"
:keymap (make-sparse-keymap)
(progn
(autoc-load-sources)))
It is not global, and we’ll define the keymap separately later on.
(let ((map autoc-mode-map))
(define-key map (kbd "C-c e e") 'autoc-run-block-operation)
map)
TODO: Highlight the autoc markers