565 lines
22 KiB
Fennel
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}
|