Files
nvim-config/lua/lisp-school.lua
2026-02-15 01:05:43 -05:00

135 lines
23 KiB
Lua

-- [nfnl] fnl/lisp-school.fnl
local current_lesson = 0
local buf = nil
local function ll_str()
local ll = vim.g.maplocalleader
if (ll == " ") then
return "<Space>"
elseif (ll == nil) then
return "\\"
else
return ll
end
end
local function leader_str()
local l = vim.g.mapleader
if (l == " ") then
return "<Space>"
elseif (l == nil) then
return "\\"
else
return l
end
end
local function sep()
return ";; \226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128\226\148\128"
end
local function buf_valid_3f()
return (buf and vim.api.nvim_buf_is_valid(buf))
end
local function append_lines(lines)
if buf_valid_3f() then
do
local count = vim.api.nvim_buf_line_count(buf)
vim.api.nvim_buf_set_lines(buf, count, count, false, lines)
end
local new_count = vim.api.nvim_buf_line_count(buf)
local first_new = (new_count - #lines)
return vim.api.nvim_win_set_cursor(0, {(first_new + 1), 0})
else
return nil
end
end
local function lesson_1()
return {sep(), ";;", ";; \240\159\142\147 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 \226\128\148 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 \226\128\148 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 \226\128\148 move through forms and elements", ";; Lessons 4-6: Edit \226\128\148 slurp, barf, wrap, splice, swap", ";; Lesson 7: Insert \226\128\148 wrapping with insert mode", ";; Lesson 8: Flow \226\128\148 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 \226\128\148 forms inside forms.", "", "(defn greet [name]", " (str \"Hello, \" name \"!\"))", "", "(defn add [a b]", " (+ a b))", "", "(println (greet \"world\"))", "(println (add 2 3))", ""}
end
local function lesson_2()
return {sep(), ";;", ";; Lesson 2: Bracket Navigation & Top-Level Movement", ";; =================================================", ";;", ";; BRACKET JUMPS \226\128\148 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 \226\128\148 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.", ";;", ";; \226\156\143\239\184\143 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)))", ""}
end
local function lesson_3()
return {sep(), ";;", ";; Lesson 3: Element Motions & Text Objects", ";; =========================================", ";;", ";; ELEMENT MOTIONS \226\128\148 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 \226\128\148 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 \226\128\148 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)", ";;", ";; \226\156\143\239\184\143 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))", ""}
end
local function lesson_4()
return {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 \226\128\148 pull next sibling INTO the form", ";; The ) moves right, swallowing the next element.", ";;", ";; <) BARF FORWARD \226\128\148 push last child OUT of the form", ";; The ) moves left, spitting out the last element.", ";;", ";; <( SLURP BACKWARD \226\128\148 pull prev sibling INTO the form", ";; The ( moves left, swallowing the previous element.", ";;", ";; >( BARF BACKWARD \226\128\148 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.", ";;", ";; >) \226\134\146 the ) moves \226\134\146 (slurp from the right)", ";; <) \226\134\144 the ) moves \226\134\144 (barf from the right)", ";; <( \226\134\144 the ( moves \226\134\144 (slurp from the left)", ";; >( \226\134\146 the ( moves \226\134\146 (barf from the left)", ";;", ";; \226\156\143\239\184\143 Practice:", ";; 1. Put cursor inside (+ a) below. Press >) \226\128\148 watch b get slurped in.", ";; Result: (+ a b) c \226\134\146 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", ""}
end
local function lesson_5()
return {sep(), ";;", ";; Lesson 5: Moving Elements & Forms", ";; ===================================", ";;", ";; SWAP ELEMENTS \226\128\148 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.", ";;", ";; \226\156\143\239\184\143 Practice:", ";; 1. Put cursor on 'b' in (foo a b c) below. Press <e to swap it left.", ";; Result: (foo b a c) \226\134\146 (foo a b c) Wait, that's: b moves left.", ";; Actually: cursor on b, <e \226\134\146 (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)", ""}
end
local function lesson_6()
local ll = ll_str()
return {sep(), ";;", ";; Lesson 6: Wrapping & Splicing", ";; ===============================", ";;", ";; SIMPLE WRAP \226\128\148 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 \226\128\148 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 \226\128\148 remove surrounding form, keep contents:", ";; dsf delete surrounding form (splice)", ";;", ";; Wrap is like adding a function call around something.", ";; Splice is the opposite \226\128\148 removing a layer of nesting.", ";;", ";; In Java terms:", ";; Wrap: x \226\134\146 foo(x) (add a function call)", ";; Splice: foo(x) \226\134\146 x (remove a function call)", ";;", ";; \226\156\143\239\184\143 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)))", ""}
end
local function lesson_7()
local ll = ll_str()
return {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 \226\128\148 replace parent with child:", (";; " .. ll .. "o raise FORM (replace parent form with this form)"), (";; " .. ll .. "O raise ELEMENT (replace parent form with this element)"), ";;", ";; \226\156\143\239\184\143 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)", ""}
end
local function lesson_8()
local ll = ll_str()
local ld = leader_str()
local ref
local function _4_(key, desc)
local key_area = (" " .. key)
local gap = string.rep(" ", math.max(4, (20 - #key_area)))
local line = (key_area .. gap .. desc)
local pad = string.rep(" ", math.max(0, (66 - #line)))
return (";; \226\149\145" .. line .. pad .. "\226\149\145")
end
ref = _4_
return {sep(), ";;", ";; Lesson 8: Parinfer & Quick Reference", ";; ======================================", ";;", ";; PARINFER \226\128\148 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 \226\134\146 explicit structural commands (slurp, barf, wrap, etc.)", ";; parinfer \226\134\146 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.", ";;", ";; \226\156\143\239\184\143 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 \226\128\148 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(), ";;", ";; \226\149\148\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\151", ";; \226\149\145 QUICK REFERENCE CARD \226\149\145", ";; \226\149\160\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\163", ";; \226\149\145 \226\149\145", ";; \226\149\145 NAVIGATION \226\149\145", 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)"), ";; \226\149\145 \226\149\145", ";; \226\149\145 TEXT OBJECTS \226\149\145", ref("af / if", "around / in form"), ref("aF / iF", "around / in top-level form"), ref("ae / ie", "around / in element"), ";; \226\149\145 \226\149\145", ";; \226\149\145 SLURP & BARF \226\149\145", 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)"), ";; \226\149\145 \226\149\145", ";; \226\149\145 SWAP \226\149\145", ref(">e / <e", "swap element right / left"), ref(">f / <f", "swap form right / left"), ";; \226\149\145 \226\149\145", ";; \226\149\145 WRAP & SPLICE \226\149\145", ref("cseb / cse(", "wrap element in ()"), ref("cse[ / cse{", "wrap element in [] / {}"), ref("dsf", "splice (delete surrounding form)"), ";; \226\149\145 \226\149\145", ";; \226\149\145 INSERT \226\149\145", 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"), ";; \226\149\145 \226\149\145", ";; \226\149\145 PARINFER \226\149\145", ref((ld .. "tpi"), "toggle parinfer"), ";; \226\149\145 \226\149\145", ";; \226\149\154\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\144\226\149\157", ";;", ";; \240\159\142\137 Congratulations! You've completed LispSchool!", ";;", ";; Structural editing will feel awkward at first \226\128\148 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(), ""}
end
local lessons = {lesson_1, lesson_2, lesson_3, lesson_4, lesson_5, lesson_6, lesson_7, lesson_8}
local function create_buffer()
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)
return vim.api.nvim_buf_set_keymap(buf, "n", "]l", ":LispSchoolNext<CR>", {noremap = true, silent = true, desc = "Next LispSchool lesson"})
end
local function find_existing_buffer()
local found = nil
for _, b in ipairs(vim.api.nvim_list_bufs()) do
local and_5_ = vim.api.nvim_buf_is_valid(b)
if and_5_ then
local name = vim.api.nvim_buf_get_name(b)
and_5_ = string.find(name, "lisp%-school%.clj$")
end
if and_5_ then
found = b
else
end
end
return found
end
local function start()
do
local existing = find_existing_buffer()
if existing then
pcall(vim.api.nvim_buf_delete, existing, {force = true})
else
end
end
current_lesson = 0
buf = nil
create_buffer()
current_lesson = 1
do
local lesson_fn = lessons[current_lesson]
append_lines(lesson_fn())
end
return vim.api.nvim_win_set_cursor(0, {1, 0})
end
local function next_lesson()
if not buf_valid_3f() then
return vim.notify("Run :LispSchool first to start the tutorial.", vim.log.levels.WARN)
elseif (current_lesson >= #lessons) then
return vim.notify("You've completed all lessons! Run :LispSchool to restart.", vim.log.levels.INFO)
else
current_lesson = (current_lesson + 1)
local lesson_fn = lessons[current_lesson]
return append_lines(lesson_fn())
end
end
return {start = start, next = next_lesson}