Skip to content

Latest commit

 

History

History
522 lines (390 loc) · 15.1 KB

autoc-core.org

File metadata and controls

522 lines (390 loc) · 15.1 KB

autoc.el

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.

Overview

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).

Markers

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

Requirements

We require a few external libraries.

#+NAME requires

(require 's)
(require 'thing-at-point)

Marker Handling

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

Searching

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

Argument Parsing

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)) ")")))

Block movements and operations

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

Operation API

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.

Interface

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"))

Operation Aliases

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

Markers with Operations

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

Operations Implementation

Here we actually implement some useful operations.

Helper Macros

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

Non-Generative Operations

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"))

defun [%]

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

defalias [!]

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.

block [<]

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

lines [=]

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

Generator Operations

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.

funcall [!]

Call a function previously defined with defun.

format-lines [>]

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)

Minor Mode

This is a minor mode! See http://nullprogram.com/blog/2013/02/06/.

Definition

;;;###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.

Keymap

(let ((map autoc-mode-map))
  (define-key map (kbd "C-c e e") 'autoc-run-block-operation)
  map)

Font Lock

TODO: Highlight the autoc markers