Files
nvim-config/fnl/lisp-school.fnl
2026-02-15 01:24:02 -05:00

565 lines
22 KiB
Fennel

;; lisp-school.fnl - Interactive structural editing tutorial for vim-sexp
;; Compiled to lua/lisp-school.lua by nfnl
;;
;; Usage:
;; :LispSchool - Start (or restart) the tutorial
;; :LispSchoolNext - Advance to the next lesson
;; ]l - Buffer-local shortcut for :LispSchoolNext
;; ---------------------------------------------------------------------------
;; State
;; ---------------------------------------------------------------------------
(var current-lesson 0)
(var buf nil)
;; ---------------------------------------------------------------------------
;; Helpers
;; ---------------------------------------------------------------------------
(fn ll-str []
"Return a display string for <localleader>."
(let [ll vim.g.maplocalleader]
(if (= ll " ") "<Space>"
(= ll nil) "\\"
ll)))
(fn leader-str []
"Return a display string for <leader>."
(let [l vim.g.mapleader]
(if (= l " ") "<Space>"
(= l nil) "\\"
l)))
(fn sep []
"Section separator line."
";; ─────────────────────────────────────────────────────────────────────────")
(fn buf-valid? []
"Check if our tutorial buffer still exists and is valid."
(and buf (vim.api.nvim_buf_is_valid buf)))
(fn append-lines [lines]
"Append lines to the tutorial buffer and scroll to them."
(when (buf-valid?)
(let [count (vim.api.nvim_buf_line_count buf)]
(vim.api.nvim_buf_set_lines buf count count false lines))
;; Move cursor to the first new line
(let [new-count (vim.api.nvim_buf_line_count buf)
first-new (- new-count (length lines))]
(vim.api.nvim_win_set_cursor 0 [(+ first-new 1) 0]))))
;; ---------------------------------------------------------------------------
;; Lessons
;; ---------------------------------------------------------------------------
(fn lesson-1 []
"Welcome - Thinking in Forms"
[(sep)
";;"
";; 🎓 Welcome to LispSchool!"
";; ========================="
";;"
";; This interactive tutorial teaches structural editing for Lisp code."
";;"
";; If you've written Java or C, you think in statements and lines."
";; In Lisp, we think in FORMS — nested parenthesized expressions."
";;"
";; Vocabulary:"
";; sexp = \"s-expression\", the formal name for Lisp's nested"
";; parenthesized syntax. (+ 1 2) is a sexp. So is (defn foo [x] x)."
";; You'll see this term a lot — it just means a balanced expression."
";; form = same thing as sexp, but the everyday Clojure word for it"
";; element = any single thing: a symbol, number, keyword, string, or a form"
";; atom = a non-list element: 42, :name, \"hello\", true"
";;"
";; The Neovim plugin that powers this tutorial is called vim-sexp"
";; (with tpope's vim-sexp-mappings-for-regular-people for sane keybindings)."
";;"
";; In Java: int x = foo(1, bar(2, 3));"
";; In Clojure: (def x (foo 1 (bar 2 3)))"
";;"
";; Every operation is a list. The first element is the operator."
";; Structure IS meaning. That's why we edit structure, not text."
";;"
";; What you'll learn:"
";; Lessons 2-3: Navigate — move through forms and elements"
";; Lessons 4-6: Edit — slurp, barf, wrap, splice, swap"
";; Lesson 7: Insert — wrapping with insert mode"
";; Lesson 8: Flow — parinfer + quick reference"
";;"
";; Advance to the next lesson with ]l or :LispSchoolNext"
";; Use u (undo) to reset after practicing."
";; Restart anytime with :LispSchool"
";;"
(sep)
""
";; Here's your first form. Place your cursor on it and just look."
";; Notice how everything is nested lists — forms inside forms."
""
"(defn greet [name]"
" (str \"Hello, \" name \"!\"))"
""
"(defn add [a b]"
" (+ a b))"
""
"(println (greet \"world\"))"
"(println (add 2 3))"
""])
(fn lesson-2 []
"Bracket Navigation & Top-Level Movement"
[(sep)
";;"
";; Lesson 2: Bracket Navigation & Top-Level Movement"
";; ================================================="
";;"
";; BRACKET JUMPS — find the edges of forms:"
";; ( jump to nearest opening paren/bracket (backward)"
";; ) jump to nearest closing paren/bracket (forward)"
";;"
";; These work like % in normal Vim but are sexp-aware."
";; They jump to the nearest enclosing bracket, not just the matching one."
";;"
";; TOP-LEVEL JUMPS — move between defn/def/ns forms:"
";; [[ jump to previous top-level form"
";; ]] jump to next top-level form"
";;"
";; Think of [[ and ]] like jumping between function definitions in C."
";;"
";; ✏️ Practice: Place your cursor inside the (+ a b) below."
";; Press ( to jump to the opening paren."
";; Press ) to jump to the closing paren."
";; Press [[ and ]] to jump between the three defns."
";;"
(sep)
""
"(defn first-fn [x]"
" (+ x 1))"
""
"(defn second-fn [a b]"
" (* (+ a b)"
" (- a b)))"
""
"(defn third-fn [items]"
" (map inc (filter even? items)))"
""])
(fn lesson-3 []
"Element Motions & Text Objects"
[(sep)
";;"
";; Lesson 3: Element Motions & Text Objects"
";; ========================================="
";;"
";; ELEMENT MOTIONS — move by element instead of by word:"
";; W move to next element (replaces WORD motion in sexp buffers)"
";; B move to previous element"
";; E move to end of element"
";; gE move to end of previous element"
";;"
";; In Java, 'word' makes sense. In Lisp, 'element' is the unit."
";; W treats (foo bar) as ONE element, not two words."
";;"
";; OPERATORS — the verbs that act on text objects:"
";; d delete (cut)"
";; c change (delete + enter insert mode)"
";; y yank (copy)"
";; v visual select (highlight for further action)"
";;"
";; TEXT OBJECTS — structural units you combine with operators:"
";; af around form (the parens + everything inside)"
";; if in form (everything inside, not the parens)"
";; aF around top-level form"
";; iF in top-level form"
";; ae around element (one element, including surrounding whitespace)"
";; ie in element (one element, no surrounding whitespace)"
";;"
";; ✏️ Practice:"
";; 1. Place cursor on 'inc' below. Press W to hop element-by-element."
";; 2. Put cursor inside (filter even? items). Press daf to delete the form."
";; 3. Undo with u. Put cursor on 'even?' and press die to delete just it."
";; 4. Undo with u. Put cursor anywhere in second-fn. Press daF to delete"
";; the entire top-level form."
";; 5. Undo with u. Try vif to visually select the inside of a form."
";;"
(sep)
""
"(defn first-fn [x y]"
" (+ x y 100))"
""
"(defn second-fn [items]"
" (map inc (filter even? items)))"
""
"(defn third-fn [a b c]"
" (println a b c))"
""])
(fn lesson-4 []
"Slurp & Barf (Growing and Shrinking Forms)"
[(sep)
";;"
";; Lesson 4: Slurp & Barf"
";; ======================="
";;"
";; These are the bread and butter of structural editing."
";; They grow or shrink a form by moving its brackets."
";;"
";; >) SLURP FORWARD — pull next sibling INTO the form"
";; The ) moves right, swallowing the next element."
";;"
";; <) BARF FORWARD — push last child OUT of the form"
";; The ) moves left, spitting out the last element."
";;"
";; <( SLURP BACKWARD — pull prev sibling INTO the form"
";; The ( moves left, swallowing the previous element."
";;"
";; >( BARF BACKWARD — push first child OUT of the form"
";; The ( moves right, spitting out the first element."
";;"
";; Mnemonic: the ARROW shows which way the bracket moves."
";; the PAREN shows which end of the form."
";;"
";; >) → the ) moves → (slurp from the right)"
";; <) ← the ) moves ← (barf from the right)"
";; <( ← the ( moves ← (slurp from the left)"
";; >( → the ( moves → (barf from the left)"
";;"
";; ✏️ Practice:"
";; 1. Put cursor inside (+ a) below. Press >) — watch b get slurped in."
";; Result: (+ a b) c → you're growing the form."
";; 2. Undo with u. Now press <) to barf a out: (+) a b c"
";; 3. Undo. Try <( and >( on the same form."
";; 4. Try slurping (* 1) to capture 2 and then 3."
";;"
(sep)
""
";; Slurp/barf playground"
"(+ a) b c"
""
"(* 1) 2 3"
""
";; Real-world example: build a nested call with slurp"
";; Goal: turn this: (println) (str \"hi \") name"
";; Into this: (println (str \"hi \" name))"
";; Steps: 1. cursor in (str \"hi \"), press >) to slurp name"
";; 2. cursor in (println), press >) to slurp (str \"hi \" name)"
"(println) (str \"hi \") name"
""])
(fn lesson-5 []
"Moving Elements & Forms"
[(sep)
";;"
";; Lesson 5: Moving Elements & Forms"
";; ==================================="
";;"
";; SWAP ELEMENTS — reorder siblings within a form:"
";; >e swap element to the right"
";; <e swap element to the left"
";;"
";; Think of these like \"move line up/down\" but for structural units."
";; In Java, you'd cut-paste a line. In Lisp, you swap elements."
";;"
";; ✏️ Practice:"
";; 1. Put cursor on 'b' in (foo a b c) below. Press <e to swap it left."
";; Result: (foo b a c) → (foo a b c) Wait, that's: b moves left."
";; Actually: cursor on b, <e → (foo b a c). Try >e to move it right."
";; 2. Reorder the function arguments to be (process c a b)."
";;"
(sep)
""
";; Swap elements"
"(foo a b c)"
""
";; Reorder arguments"
"(process a b c)"
""])
(fn lesson-6 []
"Wrapping & Splicing"
(let [ll (ll-str)]
[(sep)
";;"
";; Lesson 6: Wrapping & Splicing"
";; ==============================="
";;"
";; SIMPLE WRAP — wrap + insert in one step:"
(.. ";; " ll "w wrap ELEMENT in () and insert at head")
(.. ";; " ll "W wrap ELEMENT in () and insert at tail")
";;"
";; WRAP ELEMENT — surround an element with brackets (normal mode):"
";; cseb wrap element in () (mnemonic: change surround element, bracket)"
";; cse( wrap element in () (same as cseb)"
";; cse) wrap element in () (same as cseb)"
";; cse[ wrap element in []"
";; cse] wrap element in []"
";; cse{ wrap element in {}"
";; cse} wrap element in {}"
";;"
";; SPLICE — remove surrounding form, keep contents:"
";; dsf delete surrounding form (splice)"
";;"
";; Wrap is like adding a function call around something."
";; Splice is the opposite — removing a layer of nesting."
";;"
";; In Java terms:"
";; Wrap: x → foo(x) (add a function call)"
";; Splice: foo(x) → x (remove a function call)"
";;"
";; ✏️ Practice:"
(.. ";; 1. Put cursor on 'x' below. Press " ll "w and type 'inc '")
";; then Escape. You just wrapped x into (inc x). Undo with u."
";; 2. Put cursor on 'x' again. Press cseb to wrap it: (x)"
";; Undo with u."
";; 3. Put cursor on 'items' and press cse[ to wrap it: [items]"
";; Undo with u."
";; 4. Put cursor inside (inc x) and press dsf to splice."
";; Result: the (inc x) becomes just inc x"
";;"
(sep)
""
";; Wrapping playground"
"(def result x)"
""
"(def items (range 10))"
""
";; Splicing: remove the inner (inc x) wrapper"
"(defn foo [x]"
" (println (inc x)))"
""]))
(fn lesson-7 []
"Inserting, Wrapping Forms & Raise"
(let [ll (ll-str)]
[(sep)
";;"
";; Lesson 7: Inserting, Wrapping Forms & Raise"
";; ============================================="
";;"
";; INSERT AT FORM EDGES (tpope mappings):"
";; <I insert at the HEAD of the enclosing form (after opening paren)"
";; >I insert at the TAIL of the enclosing form (before closing paren)"
";;"
";; These are incredibly useful. <I jumps you to right after ( and enters"
";; insert mode. >I jumps to right before ) and enters insert mode."
";;"
(.. ";; WRAP FORM + INSERT (vim-sexp " ll " mappings):")
(.. ";; " ll "i wrap enclosing FORM in () and insert at head")
(.. ";; " ll "I wrap enclosing FORM in () and insert at tail")
";;"
(.. ";; " ll "[ wrap FORM in [] and insert at head")
(.. ";; " ll "] wrap FORM in [] and insert at tail")
(.. ";; " ll "{ wrap FORM in {} and insert at head")
(.. ";; " ll "} wrap FORM in {} and insert at tail")
";;"
";; RAISE — replace parent with child:"
(.. ";; " ll "o raise FORM (replace parent form with this form)")
(.. ";; " ll "O raise ELEMENT (replace parent form with this element)")
";;"
";; ✏️ Practice:"
";; 1. Put cursor inside (+ a b) below. Press <I to insert at head."
";; Type 'my-add ' then press Escape. You've renamed the function!"
";; Undo with u."
";; 2. Put cursor inside (+ a b). Press >I to insert at tail."
";; Type ' c' then Escape. You added an argument!"
";; Undo with u."
(.. ";; 3. Put cursor inside (inc x). Press " ll "o to raise it,")
";; replacing the (+ (inc x) y) with just (inc x)."
";;"
(sep)
""
";; Insert at edges"
"(+ a b)"
""
";; Raise"
"(+ (inc x) y)"
""
";; Build a threading macro: start with (process data)"
";; and wrap it step by step"
"(process data)"
""]))
(fn lesson-8 []
"Parinfer & Quick Reference"
(let [ll (ll-str)
ld (leader-str)
;; Build a reference card row: pads key+desc to exactly 66 chars between ║ markers
ref (fn [key desc]
(let [key-area (.. " " key)
gap (string.rep " " (math.max 4 (- 20 (length key-area))))
line (.. key-area gap desc)
pad (string.rep " " (math.max 0 (- 66 (length line))))]
(.. ";; ║" line pad "║")))]
[(sep)
";;"
";; Lesson 8: Parinfer & Quick Reference"
";; ======================================"
";;"
";; PARINFER — let indentation drive structure"
";;"
";; Parinfer automatically manages closing parens based on indentation."
";; Instead of manually balancing parens, you indent/dedent lines and"
";; parinfer moves the closing parens for you."
";;"
(.. ";; Toggle: " ld "tpi or :ParinferToggle")
";;"
";; When to use what:"
";; vim-sexp → explicit structural commands (slurp, barf, wrap, etc.)"
";; parinfer → casual editing, quick indentation-based restructuring"
";;"
";; They work great together: parinfer keeps parens balanced while you"
";; edit normally, and vim-sexp gives you precise structural control."
";;"
";; ✏️ Practice: Restructure a let binding with parinfer"
";;"
(.. ";; 1. Make sure parinfer is on (" ld "tpi to toggle).")
";;"
";; 2. Put cursor on the (+ a b c) line. Press << to dedent it."
";; The closing )] jumps up to the c line — parinfer moved it"
";; because (+ a b c) is no longer indented inside the let."
";; (let [a 1"
";; b 2"
";; c 3])"
";; (+ a b c)"
";;"
";; 3. Now go to the b 2 line. Press fb to jump to b, then"
";; i to enter insert mode. Type ( and press Escape."
";; Parinfer adds a closing ) at the end of the line:"
";; (let [a 1"
";; (b 2)"
";; c 3])"
";; (+ a b c)"
";;"
";; 4. Press f2 to jump to 2, then i to enter insert mode."
";; Type ) and press Escape. This closes the paren before 2:"
";; (let [a 1"
";; (b) 2"
";; c 3])"
";; (+ a b c)"
";;"
(sep)
""
";; Parinfer playground"
"(let [a 1"
" b 2"
" c 3]"
" (+ a b c))"
""
(sep)
";;"
";; ╔══════════════════════════════════════════════════════════════════╗"
";; ║ QUICK REFERENCE CARD ║"
";; ╠══════════════════════════════════════════════════════════════════╣"
";; ║ ║"
";; ║ NAVIGATION ║"
(ref "( / )" "jump to opening / closing bracket")
(ref "[[ / ]]" "prev / next top-level form")
(ref "W B E gE" "move by element (next, prev, end, prev-end)")
";; ║ ║"
";; ║ TEXT OBJECTS ║"
(ref "af / if" "around / in form")
(ref "aF / iF" "around / in top-level form")
(ref "ae / ie" "around / in element")
";; ║ ║"
";; ║ SLURP & BARF ║"
(ref ">)" "slurp forward (pull next elem in)")
(ref "<)" "barf forward (push last elem out)")
(ref "<(" "slurp backward (pull prev elem in)")
(ref ">(" "barf backward (push first elem out)")
";; ║ ║"
";; ║ SWAP ║"
(ref ">e / <e" "swap element right / left")
(ref ">f / <f" "swap form right / left")
";; ║ ║"
";; ║ WRAP & SPLICE ║"
(ref "cseb / cse(" "wrap element in ()")
(ref "cse[ / cse{" "wrap element in [] / {}")
(ref "dsf" "splice (delete surrounding form)")
";; ║ ║"
";; ║ INSERT ║"
(ref "<I / >I" "insert at form head / tail")
(ref (.. ll "w / " ll "W") "wrap element + insert head / tail")
(ref (.. ll "i / " ll "I") "wrap form + insert head / tail")
(ref (.. ll "o / " ll "O") "raise form / element")
";; ║ ║"
";; ║ PARINFER ║"
(ref (.. ld "tpi") "toggle parinfer")
";; ║ ║"
";; ╚══════════════════════════════════════════════════════════════════╝"
";;"
";; 🎉 Congratulations! You've completed LispSchool!"
";;"
";; Structural editing will feel awkward at first — that's normal."
";; Start with just ( ) for navigation and >) <) for slurp/barf."
";; Add more commands to your muscle memory over time."
";;"
";; Run :LispSchool anytime to review."
";;"
(sep)
""]))
;; Ordered list of lesson functions
(local lessons [lesson-1 lesson-2 lesson-3 lesson-4
lesson-5 lesson-6 lesson-7 lesson-8])
;; ---------------------------------------------------------------------------
;; Buffer management
;; ---------------------------------------------------------------------------
(fn create-buffer []
"Create the lisp-school.clj scratch buffer and switch to it."
(set buf (vim.api.nvim_create_buf true true))
(vim.api.nvim_buf_set_name buf "lisp-school.clj")
(vim.api.nvim_buf_set_option buf :buftype "nofile")
(vim.api.nvim_buf_set_option buf :swapfile false)
(vim.api.nvim_buf_set_option buf :filetype "clojure")
(vim.api.nvim_set_current_buf buf)
;; Buffer-local keymap: ]l to advance
(vim.api.nvim_buf_set_keymap buf :n "]l" ":LispSchoolNext<CR>"
{:noremap true :silent true :desc "Next LispSchool lesson"}))
(fn find-existing-buffer []
"Find an existing lisp-school.clj buffer, or nil."
(var found nil)
(each [_ b (ipairs (vim.api.nvim_list_bufs))]
(when (and (vim.api.nvim_buf_is_valid b)
(let [name (vim.api.nvim_buf_get_name b)]
(string.find name "lisp%-school%.clj$")))
(set found b)))
found)
;; ---------------------------------------------------------------------------
;; Public API
;; ---------------------------------------------------------------------------
(fn start []
"Start or restart LispSchool."
;; Clean up existing buffer if any
(let [existing (find-existing-buffer)]
(when existing
(pcall vim.api.nvim_buf_delete existing {:force true})))
(set current-lesson 0)
(set buf nil)
(create-buffer)
;; Append lesson 1
(set current-lesson 1)
(let [lesson-fn (. lessons current-lesson)]
(append-lines (lesson-fn)))
;; Scroll to top
(vim.api.nvim_win_set_cursor 0 [1 0]))
(fn next-lesson []
"Advance to the next lesson."
(if (not (buf-valid?))
(vim.notify "Run :LispSchool first to start the tutorial." vim.log.levels.WARN)
(>= current-lesson (length lessons))
(vim.notify "You've completed all lessons! Run :LispSchool to restart." vim.log.levels.INFO)
(do
(set current-lesson (+ current-lesson 1))
(let [lesson-fn (. lessons current-lesson)]
(append-lines (lesson-fn))))))
{:start start
:next next-lesson}