commit 806f4eb1ecf13204140ca0a35c5ad62e10d46681
map588 <map588@pitt.edu>
2026-09-27 02:16:25 -0400
Add summary comments and named placeholders
Every template now starts with a one-line summary comment. The
converter reads it as the snippet description. <+name+> is a named
placeholder; the converter emits it as ${n:name}. The Vim mapping
pattern matches both forms. git-config-branch and cl/class use
named placeholders as examples.
README.md | 23 +++++++++++++++++------
c/main.c | 1 +
cl/class | 9 +++++----
cl/defmethod-specialized | 1 +
cl/defpackage | 1 +
cl/defstruct | 1 +
cl/defsystem | 1 +
cl/do | 2 +-
cl/dolist | 1 +
cl/dotimes | 1 +
cl/handler-case | 1 +
cl/restart-case | 1 +
cl/slot | 1 +
cl/tagbody-go | 1 +
cl/with-open-file | 1 +
cl/with-simple-restart | 1 +
convert_to_luasnip.py | 28 ++++++++++++++++++++++------
git-config-branch | 5 +++--
html/template.html | 1 +
make/conditionals/conditional-variable | 1 +
make/conditionals/ifeq-endif | 1 +
make/functions/foreach-simple | 2 +-
make/golf/if-pkgconfig-exists | 2 +-
make/mapping/map-fn | 1 +
make/pedantic-phony | 1 +
make/shell | 1 +
make/templates/config.mk | 1 +
make/templates/dist | 1 +
sed/append-after-match | 1 +
sed/d | 2 +-
sh/for-array | 2 +-
sh/str-fn | 1 +
test_convert_to_luasnip.py | 34 ++++++++++++++++++++++++++++++++++
33 files changed, 109 insertions(+), 23 deletions(-)
diff --git a/README.md b/README.md
index 4cf5530..47f4fa4 100644
--- a/README.md
+++ b/README.md
@@ -38,23 +38,33 @@ Two kinds of file live here:
most of `sh/`). They are notes to read or `:read` in whole. The snippet
converter skips them.
+# Conventions
+
+- The first line of a template is a one-line summary in the language's
+ comment syntax (`#`, `;;`, `//` or `<!-- -->`). Tools read it as the
+ description. A `#!` line may come before it.
+- `<++>` marks a spot to fill. `<+name+>` does the same and names the spot,
+ for example `<+remote+>`. The snippet converter turns the name into the
+ placeholder text. Use the plain form unless a name helps.
+
# Using with Vim
```vim
:read /path/to/tmpl/thing
```
-Add a shortcut on your leader key that jumps to the next `<++>`, deletes it,
-and leaves you in insert mode.
+Add a shortcut on your leader key that jumps to the next placeholder, deletes
+it, and leaves you in insert mode. The pattern matches both `<++>` and
+`<+name+>`.
Vimscript:
```vim
-map <leader><Space> /<++><CR>dt>a<BS>
+map <leader><Space> /<+.\{-}+><CR>dt>a<BS>
```
Lua:
```lua
-map("n", "<leader><Space>", "/<++><CR>dt>a<BS>", { desc = "Jump to next template placeholder" })
+map("n", "<leader><Space>", "/<+.\\{-}+><CR>dt>a<BS>", { desc = "Jump to next template placeholder" })
```
# Using with Neovim and LuaSnip
@@ -77,8 +87,9 @@ require("luasnip.loaders.from_vscode").lazy_load({
```
The snippet prefix is the path with dashes: `c/main.c` is `c-main`,
-`sh/trap/cleanup-temp` is `sh-trap-cleanup-temp`. Type the prefix, press Tab,
-and Tab again to move between the placeholders.
+`sh/trap/cleanup-temp` is `sh-trap-cleanup-temp`. The description is the
+template's summary comment. Type the prefix, press Tab, and Tab again to move
+between the placeholders.
Tests:
```sh
diff --git a/c/main.c b/c/main.c
index b4b8e1a..59415c9 100644
--- a/c/main.c
+++ b/c/main.c
@@ -1,3 +1,4 @@
+// main with argc and argv
#include <stdio.h>
int main(int argc, char **argv) {
diff --git a/cl/class b/cl/class
index 71c90f9..35dcca6 100644
--- a/cl/class
+++ b/cl/class
@@ -1,4 +1,5 @@
-(defclass <++> (<++>) ; name (inherits)
- ((<++> :initarg :<++>
- :accessor <++>))
- (:documentation "<++>"))
+;; defclass with one slot
+(defclass <+name+> (<+superclass+>)
+ ((<+slot+> :initarg :<+slot+>
+ :accessor <+slot+>))
+ (:documentation "<+doc+>"))
diff --git a/cl/defmethod-specialized b/cl/defmethod-specialized
index d5b2e8d..51e7863 100644
--- a/cl/defmethod-specialized
+++ b/cl/defmethod-specialized
@@ -1,3 +1,4 @@
+;; defmethod specialized on two arguments
(defmethod <++> ((<++> <++>) (<++> <++>))
"<++>"
<++>)
\ No newline at end of file
diff --git a/cl/defpackage b/cl/defpackage
index 7ff1983..6d3510d 100644
--- a/cl/defpackage
+++ b/cl/defpackage
@@ -1,3 +1,4 @@
+;; defpackage with every common option
(defpackage #:<++>
(:nicknames #:<++>)
(:shadow #:<++>)
diff --git a/cl/defstruct b/cl/defstruct
index 20342da..105d462 100644
--- a/cl/defstruct
+++ b/cl/defstruct
@@ -1,3 +1,4 @@
+;; defstruct with options and one slot
(defstruct (<++> :<++> <++>) ; name and options*
"<++>"
(<++> <++> :<++> <++>) ; name initform options*
diff --git a/cl/defsystem b/cl/defsystem
index ae20e81..48d13b8 100644
--- a/cl/defsystem
+++ b/cl/defsystem
@@ -1,3 +1,4 @@
+;; ASDF defsystem with its own package
(defpackage :<++>-system (:use :cl :asdf))
(in-package :<++>-system)
(defsystem <++>
diff --git a/cl/do b/cl/do
index bb033b2..3ca4103 100644
--- a/cl/do
+++ b/cl/do
@@ -1,4 +1,4 @@
-;; * or no *?
+;; do or do* loop with two variables
(do<++> ((<++> <++> <++>) ; var initform stepform
(<++> <++> <++>)) ; more vars
(<++> <++>) ; condition, return
diff --git a/cl/dolist b/cl/dolist
index bc66d2a..3da77ba 100644
--- a/cl/dolist
+++ b/cl/dolist
@@ -1,2 +1,3 @@
+;; dolist over a list
(dolist (<++> <++>) ; var listform
<++>) ; body
diff --git a/cl/dotimes b/cl/dotimes
index 6b37e14..9ac7e75 100644
--- a/cl/dotimes
+++ b/cl/dotimes
@@ -1,2 +1,3 @@
+;; dotimes counted loop
(dotimes (<++> <++> <++>) ; var count optional-return
<++>) ; optional-body
diff --git a/cl/handler-case b/cl/handler-case
index 5564929..d80660d 100644
--- a/cl/handler-case
+++ b/cl/handler-case
@@ -1,3 +1,4 @@
+;; handler-case with a :no-error clause
(handler-case (progn
<++>)
(<++> (condition)
diff --git a/cl/restart-case b/cl/restart-case
index fb2649a..be9229d 100644
--- a/cl/restart-case
+++ b/cl/restart-case
@@ -1,3 +1,4 @@
+;; restart-case with a report and an interactive function
(restart-case <++>
(<++> (<++>)
:report "<++>"
diff --git a/cl/slot b/cl/slot
index fbe218b..367ed57 100644
--- a/cl/slot
+++ b/cl/slot
@@ -1,3 +1,4 @@
+;; defclass slot with every option
(<++> :initarg :<++>
:initform <++>
;; :accessor <++>
diff --git a/cl/tagbody-go b/cl/tagbody-go
index 9355692..7bcea5a 100644
--- a/cl/tagbody-go
+++ b/cl/tagbody-go
@@ -1,3 +1,4 @@
+;; tagbody with go labels
(tagbody
place-1
(go place-1)
diff --git a/cl/with-open-file b/cl/with-open-file
index 6eac378..f0f0d01 100644
--- a/cl/with-open-file
+++ b/cl/with-open-file
@@ -1,3 +1,4 @@
+;; with-open-file with direction and if-exists options
(with-open-file (<++> "<++>"
:direction <++> ; :input :output :io
:if-exists <++> ; :supersede :append :overwrite
diff --git a/cl/with-simple-restart b/cl/with-simple-restart
index 7e84cc3..6014756 100644
--- a/cl/with-simple-restart
+++ b/cl/with-simple-restart
@@ -1,2 +1,3 @@
+;; with-simple-restart around a body
(with-simple-restart (<++> "<++>")
<++>)
\ No newline at end of file
diff --git a/convert_to_luasnip.py b/convert_to_luasnip.py
index f7e0e56..15f49bf 100644
--- a/convert_to_luasnip.py
+++ b/convert_to_luasnip.py
@@ -30,7 +30,11 @@ ROOT_FILE_MAP = {
"uad.json": "json",
}
-PLACEHOLDER_RE = re.compile(r"<\+\+>")
+# <++> is an unnamed placeholder. <+name+> is a named placeholder.
+PLACEHOLDER_RE = re.compile(r"<\+([^+>]*)\+>")
+
+# A summary comment on the first line of a template, in any supported comment syntax.
+HEAD_COMMENT_RE = re.compile(r"^\s*(?:#+|;+|//|<!--)\s*(.*?)\s*(?:-->)?\s*$")
def escape_snippet_text(text: str) -> str:
@@ -39,12 +43,18 @@ def escape_snippet_text(text: str) -> str:
def convert_placeholders(content: str) -> Tuple[str, int]:
- """Replace each <++> with a numbered tab stop. Escape all other text."""
+ """Replace each placeholder with a numbered tab stop. Escape all other text.
+
+ <++> becomes ${n}. <+name+> becomes ${n:name}.
+ """
parts = PLACEHOLDER_RE.split(content)
body = escape_snippet_text(parts[0])
- for index, part in enumerate(parts[1:], start=1):
- body += "${" + str(index) + "}" + escape_snippet_text(part)
- return body, len(parts) - 1
+ count = 0
+ for name, text in zip(parts[1::2], parts[2::2]):
+ count += 1
+ stop = f"${{{count}:{escape_snippet_text(name)}}}" if name else f"${{{count}}}"
+ body += stop + escape_snippet_text(text)
+ return body, count
def generate_snippet_name(filepath: Path, template_dir: Path) -> str:
@@ -54,7 +64,13 @@ def generate_snippet_name(filepath: Path, template_dir: Path) -> str:
def get_description(filepath: Path, template_dir: Path) -> str:
- """Describe the snippet by its path."""
+ """Use the summary comment on the first line. Fall back to the path."""
+ lines = filepath.read_text(encoding="utf-8").splitlines()
+ if lines and lines[0].startswith("#!"):
+ lines = lines[1:]
+ match = HEAD_COMMENT_RE.match(lines[0]) if lines else None
+ if match and match.group(1):
+ return match.group(1)
rel_path = filepath.relative_to(template_dir)
return f"{rel_path} template"
diff --git a/git-config-branch b/git-config-branch
index aaba2ca..73db71c 100644
--- a/git-config-branch
+++ b/git-config-branch
@@ -1,3 +1,4 @@
-[remote "<++>"]
- url = git@github.com:equwal/<++>
+# git remote section for a GitHub repository
+[remote "<+remote+>"]
+ url = git@github.com:equwal/<+repository+>
fetch = +refs/heads/*:refs/remotes/origin/*
diff --git a/html/template.html b/html/template.html
index 117a4e0..64e9a95 100644
--- a/html/template.html
+++ b/html/template.html
@@ -1,3 +1,4 @@
+<!-- HTML5 page skeleton -->
<!DOCTYPE html>
<html>
<head>
diff --git a/make/conditionals/conditional-variable b/make/conditionals/conditional-variable
index 61fc1bf..2b12b7f 100644
--- a/make/conditionals/conditional-variable
+++ b/make/conditionals/conditional-variable
@@ -1 +1,2 @@
+# Set a variable only when it is not set already
<++> ?= <++>
diff --git a/make/conditionals/ifeq-endif b/make/conditionals/ifeq-endif
index 16a9851..14ebbf6 100644
--- a/make/conditionals/ifeq-endif
+++ b/make/conditionals/ifeq-endif
@@ -1,3 +1,4 @@
+# ifeq/ifneq conditional block
if<++>eq (<++>,<++>) #space before the condition is important
<++>
endif
diff --git a/make/functions/foreach-simple b/make/functions/foreach-simple
index 099f9f5..daef786 100644
--- a/make/functions/foreach-simple
+++ b/make/functions/foreach-simple
@@ -1,2 +1,2 @@
-# see complex for more info
+# foreach over a list (see foreach-complex for more)
$(foreach <++>, <++>, <++>)
diff --git a/make/golf/if-pkgconfig-exists b/make/golf/if-pkgconfig-exists
index 09b6ddb..7aabfa8 100644
--- a/make/golf/if-pkgconfig-exists
+++ b/make/golf/if-pkgconfig-exists
@@ -1,4 +1,4 @@
-# example with ncursesw like mtm or dvtm
+# Use pkg-config for a library when it is installed (example: ncursesw)
CURSESLIB ?= <++>
LIBS ?= -l$(CURSESLIB) -lutil
diff --git a/make/mapping/map-fn b/make/mapping/map-fn
index 8994df9..706fbaa 100644
--- a/make/mapping/map-fn
+++ b/make/mapping/map-fn
@@ -1,3 +1,4 @@
+# Define a map function from a one-argument function
var := a
map = $(foreach $(var), $(2), $(call $(1),$(var)))
# <new-mapped-fn> = $(call map,<old-onearg-fn>,<new-mapped-fn> map MAKE
diff --git a/make/pedantic-phony b/make/pedantic-phony
index 45212a3..a779912 100644
--- a/make/pedantic-phony
+++ b/make/pedantic-phony
@@ -1 +1,2 @@
+# Declare the standard phony targets
.PHONY: install uninstall clean <++> # etc.
diff --git a/make/shell b/make/shell
index 3639c50..599bba4 100644
--- a/make/shell
+++ b/make/shell
@@ -1 +1,2 @@
+# Run a shell command and capture its output
$(shell <++>)
diff --git a/make/templates/config.mk b/make/templates/config.mk
index 424ac8e..2b7847e 100644
--- a/make/templates/config.mk
+++ b/make/templates/config.mk
@@ -1,3 +1,4 @@
+# config.mk with version, shell and install prefixes
VERSION=0.1
SHELL=/bin/sh
PROJECT=<++>
diff --git a/make/templates/dist b/make/templates/dist
index 1988968..156e158 100644
--- a/make/templates/dist
+++ b/make/templates/dist
@@ -1,3 +1,4 @@
+# Minimal makefile that builds a release tarball
include config.mk
# A super minimal makefile for deployment of a tarball.
diff --git a/sed/append-after-match b/sed/append-after-match
index 566870f..2ee3ccd 100644
--- a/sed/append-after-match
+++ b/sed/append-after-match
@@ -1 +1,2 @@
+# Append a line after the first line that matches
sed "/# Use a comment for example to mark where you want to append/a\\$1" (FILE)<++>
diff --git a/sed/d b/sed/d
index 482895c..bc50e96 100644
--- a/sed/d
+++ b/sed/d
@@ -1,4 +1,4 @@
-# Use this command to try an example (in this case, the D sed command).
+# Delete matching lines with D (keeps the newline) or d
# the line's contents -- but not the newline at the end -- will be deleted with
# same as sed 's/^pattern.*//'
sed '/^<++>/D'
diff --git a/sh/for-array b/sh/for-array
index ffabaef..a5d8ba4 100644
--- a/sh/for-array
+++ b/sh/for-array
@@ -1,4 +1,4 @@
-#
+# Iterate over a saved argument list in POSIX sh
# Yes, it really is this hard to do
# and it is broken on GNU non-POSIX non-+ support.
save () {
diff --git a/sh/str-fn b/sh/str-fn
index e5e15bf..badc4ff 100644
--- a/sh/str-fn
+++ b/sh/str-fn
@@ -1,3 +1,4 @@
+# Shell function that returns a string by reference
fn () {
# argument: the name of variable to call-by-reference, output string into.
<++>
diff --git a/test_convert_to_luasnip.py b/test_convert_to_luasnip.py
index 7490353..913ff7e 100644
--- a/test_convert_to_luasnip.py
+++ b/test_convert_to_luasnip.py
@@ -90,3 +90,37 @@ def test_write_creates_package_json(tmp_path):
package = json.loads((out / "package.json").read_text())
assert package["contributes"]["snippets"] == [{"language": "sh", "path": "./sh.json"}]
assert json.loads((out / "sh.json").read_text())["sh-x"]["prefix"] == "sh-x"
+
+
+def test_named_placeholder_becomes_named_tab_stop():
+ body, count = conv.convert_placeholders("[remote \"<+name+>\"] <++>")
+ assert body == '[remote "${1:name}"] ${2}'
+ assert count == 2
+
+
+@given(st.text(alphabet=st.characters(blacklist_characters="+>"), min_size=1, max_size=8))
+def test_named_placeholder_name_is_escaped(name):
+ body, _ = conv.convert_placeholders(f"<+{name}+>")
+ assert body == "${1:" + conv.escape_snippet_text(name) + "}"
+
+
+def test_description_comes_from_head_comment(tmp_path):
+ root = tmp_path / "tmpl"
+ (root / "sh").mkdir(parents=True)
+ f = root / "sh" / "thing"
+ f.write_text("#!/bin/sh\n# Print the thing\necho <++>\n")
+ assert conv.get_description(f, root) == "Print the thing"
+ g = root / "sh" / "lisp"
+ g.write_text(";; Lisp style\n(<++>)\n")
+ assert conv.get_description(g, root) == "Lisp style"
+ h = root / "sh" / "page"
+ h.write_text("<!-- Html style -->\n<++>\n")
+ assert conv.get_description(h, root) == "Html style"
+
+
+def test_description_falls_back_to_path(tmp_path):
+ root = tmp_path / "tmpl"
+ (root / "sh").mkdir(parents=True)
+ f = root / "sh" / "bare"
+ f.write_text("echo <++>\n")
+ assert conv.get_description(f, root) == "sh/bare template"