;; 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 ." (let [ll vim.g.maplocalleader] (if (= ll " ") "" (= ll nil) "\\" ll))) (fn leader-str [] "Return a display string for ." (let [l vim.g.mapleader] (if (= l " ") "" (= 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 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 TAIL of the enclosing form (before closing paren)" ";;" ";; These are incredibly useful. 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 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 / f / 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" {: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}