From 800618ef605e7b7d6ed56984ad9d72bf210819f1 Mon Sep 17 00:00:00 2001 From: Adam Jeniski Date: Sun, 15 Feb 2026 01:15:04 -0500 Subject: [PATCH] update --- .gitignore | 1 + fnl/config/init.fnl | 9 --- fnl/plugins/init.fnl | 13 +++++ lua/lisp-school.lua | 134 ------------------------------------------- 4 files changed, 14 insertions(+), 143 deletions(-) delete mode 100644 lua/lisp-school.lua diff --git a/.gitignore b/.gitignore index e765139..5962235 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ undodir # Generated Lua files (compiled from Fennel by nfnl) lua/config/ lua/plugins/ +lua/lisp-school.lua # Clojure/LSP tooling .clj-kondo/ diff --git a/fnl/config/init.fnl b/fnl/config/init.fnl index 22c4550..f5bb8f8 100644 --- a/fnl/config/init.fnl +++ b/fnl/config/init.fnl @@ -139,13 +139,4 @@ (fn [] (vim.cmd (.. "edit " (vim.lsp.get_log_path)))) {:desc "Open LSP log file"}) -;; LispSchool - Interactive structural editing tutorial -(usercmd "LispSchool" - (fn [] (let [ls (require :lisp-school)] (ls.start))) - {:desc "Start LispSchool tutorial"}) - -(usercmd "LispSchoolNext" - (fn [] (let [ls (require :lisp-school)] (ls.next))) - {:desc "Next LispSchool lesson"}) - {} diff --git a/fnl/plugins/init.fnl b/fnl/plugins/init.fnl index 814f748..3bea7bc 100644 --- a/fnl/plugins/init.fnl +++ b/fnl/plugins/init.fnl @@ -70,6 +70,19 @@ (vim.keymap.set :n "gw" (fn [] (hop.hint_words)) {:desc "Hop to word"})))} + ;; LispSchool - Interactive structural editing tutorial + ;; Source: fnl/lisp-school.fnl (compiled by nfnl) + {:name "lisp-school" + :dir "." + :cmd ["LispSchool" "LispSchoolNext"] + :config (fn [] + (vim.api.nvim_create_user_command "LispSchool" + (fn [] (let [ls (require :lisp-school)] (ls.start))) + {:desc "Start LispSchool tutorial"}) + (vim.api.nvim_create_user_command "LispSchoolNext" + (fn [] (let [ls (require :lisp-school)] (ls.next))) + {:desc "Next LispSchool lesson"}))} + ;; which-key - Shows keybinding hints ;; lhs constant defined at top - which-key specs use index [1] for the key sequence {repo "folke/which-key.nvim" diff --git a/lua/lisp-school.lua b/lua/lisp-school.lua deleted file mode 100644 index 53b4898..0000000 --- a/lua/lisp-school.lua +++ /dev/null @@ -1,134 +0,0 @@ --- [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 "" - elseif (ll == nil) then - return "\\" - else - return ll - end -end -local function leader_str() - local l = vim.g.mapleader - if (l == " ") then - return "" - 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 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 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 \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 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 / 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"), ";; \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", {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}