diff --git a/.gitignore b/.gitignore index 592dfa3..e765139 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ undodir # Generated Lua files (compiled from Fennel by nfnl) lua/config/ lua/plugins/ + +# Clojure/LSP tooling +.clj-kondo/ +.lsp/ +.nrepl-port diff --git a/fnl/config/init.fnl b/fnl/config/init.fnl index 1dc7071..22c4550 100644 --- a/fnl/config/init.fnl +++ b/fnl/config/init.fnl @@ -35,6 +35,9 @@ (keymap :n "bp" ":bprevious" {:desc "Previous buffer"}) (keymap :n "bd" ":bdelete" {:desc "Delete buffer"}) +;; Copy to system clipboard +(keymap [:n :v] "y" "\"+y" {:desc "Yank to clipboard"}) + ;; Clear search highlight (keymap :n "" ":noh" {:desc "Clear search highlight"}) @@ -136,4 +139,13 @@ (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/config/parinfer.fnl b/fnl/config/parinfer.fnl new file mode 100644 index 0000000..ecd99b5 --- /dev/null +++ b/fnl/config/parinfer.fnl @@ -0,0 +1,45 @@ +;; config/parinfer.fnl - Parinfer + vim-sexp insert mode coordination +;; +;; Single source of truth for whether parinfer is on/off by default +;; and what that means for vim-sexp's auto-insert bracket mappings. +;; +;; When parinfer is ON: it manages closing parens via indentation, +;; so vim-sexp auto-insert is disabled. +;; When parinfer is OFF: vim-sexp auto-insert is re-enabled so you +;; get closing brackets when typing openers. + +;; ── Configuration ──────────────────────────────────────────────── +;; Change this single flag to flip the default for all sexp filetypes. + +(local default-enabled true) + +;; ── Implementation ─────────────────────────────────────────────── + +(local sexp-insert-keys ["(" ")" "[" "]" "{" "}" "\"" ""]) + +(fn enable-sexp-insert [] + "Restore vim-sexp insert-mode bracket mappings in the current buffer." + (vim.cmd "imap ( (sexp_insert_opening_round)") + (vim.cmd "imap ) (sexp_insert_closing_round)") + (vim.cmd "imap [ (sexp_insert_opening_square)") + (vim.cmd "imap ] (sexp_insert_closing_square)") + (vim.cmd "imap { (sexp_insert_opening_curly)") + (vim.cmd "imap } (sexp_insert_closing_curly)") + (vim.cmd "imap \" (sexp_insert_double_quote)") + (vim.cmd "imap (sexp_insert_backspace)")) + +(fn disable-sexp-insert [] + "Remove vim-sexp insert-mode bracket mappings from the current buffer." + (each [_ key (ipairs sexp-insert-keys)] + (pcall vim.api.nvim_buf_del_keymap 0 "i" key))) + +(fn toggle [] + "Toggle parinfer in the current buffer and sync vim-sexp insert mappings." + (vim.cmd "ParinferToggle") + (let [on (= (vim.api.nvim_buf_get_var 0 "parinfer_enabled") 1)] + (if on + (disable-sexp-insert) + (enable-sexp-insert)))) + +{:default-enabled default-enabled + :toggle toggle} diff --git a/fnl/lisp-school.fnl b/fnl/lisp-school.fnl new file mode 100644 index 0000000..c646948 --- /dev/null +++ b/fnl/lisp-school.fnl @@ -0,0 +1,564 @@ +;; 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} diff --git a/fnl/plugins/init.fnl b/fnl/plugins/init.fnl index 1c67b0d..4d925f8 100644 --- a/fnl/plugins/init.fnl +++ b/fnl/plugins/init.fnl @@ -25,199 +25,51 @@ (set vim.g.conjure#log#hud#enabled true))} ;; nvim-parinfer - Automatic parenthesis balancing - ;; Works alongside paredit: parinfer handles passive balancing, - ;; paredit provides explicit structural commands + ;; See fnl/config/parinfer.fnl for default-enabled flag and toggle logic {repo "gpanders/nvim-parinfer" :ft ["fennel" "clojure" "lisp" "scheme" "racket" "carp" "timl"] + :init (fn [] + (let [par (require :config.parinfer)] + (set vim.g.parinfer_enabled par.default-enabled))) :keys [{lhs "tpi" - rhs "ParinferToggle" + rhs (fn [] (let [par (require :config.parinfer)] (par.toggle))) :desc "Toggle Parinfer"}]} - ;; nvim-paredit - Structural editing for Lisp - ;; Configured with vim-sexp + vim-sexp-mappings-for-regular-people compatible bindings - ;; - ;; Default paredit bindings (from nvim-paredit): - ;; >)/<) slurp/barf forwards <(/>( slurp/barf backwards - ;; >e/f/p/

@ splice - ;; o/O raise form/element - ;; - ;; Additional bindings added below for vim-sexp compatibility - {repo "julienvincent/nvim-paredit" + ;; vim-sexp - Structural editing for S-expressions + ;; with tpope's vim-sexp-mappings-for-regular-people + {repo "guns/vim-sexp" :ft ["fennel" "clojure" "lisp" "scheme" "racket"] - :config (fn [] - (local paredit (require :nvim-paredit)) - (local api paredit.api) - (local wrap paredit.wrap) - (local cursor paredit.cursor) - (local keymap vim.keymap.set) - - ;; Use default keybindings - (paredit.setup {}) - - ;; ============================================================ - ;; vim-sexp-mappings-for-regular-people (tpope) style bindings - ;; ============================================================ - - ;; dsf - Splice (delete surrounding form) - alias for @ - (keymap :n "dsf" api.unwrap_form_under_cursor {:desc "Splice (delete surrounding form)"}) - - ;; cse* - Surround element (no insert mode, surround.vim style) - (keymap :n "cseb" #(api.wrap_element_under_cursor "(" ")") {:desc "Wrap element in ()"}) - (keymap :n "cse(" #(api.wrap_element_under_cursor "(" ")") {:desc "Wrap element in ()"}) - (keymap :n "cse)" #(api.wrap_element_under_cursor "(" ")") {:desc "Wrap element in ()"}) - (keymap :n "cse[" #(api.wrap_element_under_cursor "[" "]") {:desc "Wrap element in []"}) - (keymap :n "cse]" #(api.wrap_element_under_cursor "[" "]") {:desc "Wrap element in []"}) - (keymap :n "cse{" #(api.wrap_element_under_cursor "{" "}") {:desc "Wrap element in {}"}) - (keymap :n "cse}" #(api.wrap_element_under_cursor "{" "}") {:desc "Wrap element in {}"}) - - ;; I - Insert at form head/tail (tpope style) - (keymap :n "I" - (fn [] - (api.move_to_parent_form_end) - (vim.cmd "startinsert")) - {:desc "Insert at form tail"}) - - ;; W/B/E/gE - Element-wise motions (override WORD motions) - (each [_ mode (ipairs [:n :x :o])] - (keymap mode "W" api.move_to_next_element_head {:desc "Next element head"}) - (keymap mode "B" api.move_to_prev_element_head {:desc "Previous element head"}) - (keymap mode "E" api.move_to_next_element_tail {:desc "Next element tail"}) - (keymap mode "gE" api.move_to_prev_element_tail {:desc "Previous element tail"})) - - ;; ============================================================ - ;; vim-sexp (guns) style LocalLeader bindings - ;; ============================================================ - - ;; w/W - Wrap element + insert at head/tail - (keymap :n "w" - (fn [] - (cursor.place_cursor - (wrap.wrap_element_under_cursor "( " ")") - {:placement "inner_start" :mode "insert"})) - {:desc "Wrap element, insert head"}) - (keymap :n "W" - (fn [] - (cursor.place_cursor - (wrap.wrap_element_under_cursor "(" ")") - {:placement "inner_end" :mode "insert"})) - {:desc "Wrap element, insert tail"}) - - ;; i/I - Wrap form + insert at head/tail - (keymap :n "i" - (fn [] - (cursor.place_cursor - (wrap.wrap_enclosing_form_under_cursor "( " ")") - {:placement "inner_start" :mode "insert"})) - {:desc "Wrap form, insert head"}) - (keymap :n "I" - (fn [] - (cursor.place_cursor - (wrap.wrap_enclosing_form_under_cursor "(" ")") - {:placement "inner_end" :mode "insert"})) - {:desc "Wrap form, insert tail"}) - - ;; [ ] { } - Wrap form in brackets/braces + insert - (keymap :n "[" - (fn [] - (cursor.place_cursor - (wrap.wrap_enclosing_form_under_cursor "[ " "]") - {:placement "inner_start" :mode "insert"})) - {:desc "Wrap form in [], insert head"}) - (keymap :n "]" - (fn [] - (cursor.place_cursor - (wrap.wrap_enclosing_form_under_cursor "[" "]") - {:placement "inner_end" :mode "insert"})) - {:desc "Wrap form in [], insert tail"}) - (keymap :n "{" - (fn [] - (cursor.place_cursor - (wrap.wrap_enclosing_form_under_cursor "{ " "}") - {:placement "inner_start" :mode "insert"})) - {:desc "Wrap form in {}, insert head"}) - (keymap :n "}" - (fn [] - (cursor.place_cursor - (wrap.wrap_enclosing_form_under_cursor "{" "}") - {:placement "inner_end" :mode "insert"})) - {:desc "Wrap form in {}, insert tail"}) - - ;; e[ e] e{ e} - Wrap element in brackets/braces + insert - (keymap :n "e[" - (fn [] - (cursor.place_cursor - (wrap.wrap_element_under_cursor "[ " "]") - {:placement "inner_start" :mode "insert"})) - {:desc "Wrap element in [], insert head"}) - (keymap :n "e]" - (fn [] - (cursor.place_cursor - (wrap.wrap_element_under_cursor "[" "]") - {:placement "inner_end" :mode "insert"})) - {:desc "Wrap element in [], insert tail"}) - (keymap :n "e{" - (fn [] - (cursor.place_cursor - (wrap.wrap_element_under_cursor "{ " "}") - {:placement "inner_start" :mode "insert"})) - {:desc "Wrap element in {}, insert head"}) - (keymap :n "e}" - (fn [] - (cursor.place_cursor - (wrap.wrap_element_under_cursor "{" "}") - {:placement "inner_end" :mode "insert"})) - {:desc "Wrap element in {}, insert tail"}) - - ;; h/l - Insert at list head/tail (vim-sexp originals) - ;; Same as I but with localleader - (keymap :n "h" - (fn [] - (api.move_to_parent_form_start) - (vim.cmd "normal! l") - (vim.cmd "startinsert")) - {:desc "Insert at list head"}) - (keymap :n "l" - (fn [] - (api.move_to_parent_form_end) - (vim.cmd "startinsert")) - {:desc "Insert at list tail"}) - - ;; [[ / ]] - Move to prev/next top-level form - (each [_ mode (ipairs [:n :x :o])] - (keymap mode "]]" - (fn [] - ;; Go to current top-level, then find next - (api.move_to_parent_form_end) - (api.move_to_next_element_head)) - {:desc "Next top-level form"}) - (keymap mode "[[" - (fn [] - ;; Go to current top-level start, then find prev - (api.move_to_parent_form_start) - (api.move_to_prev_element_head)) - {:desc "Previous top-level form"})))} - - ;; Clojure-Typst - Syntax highlighting for #t"" Typst literals in Clojure - ;; Injects Typst highlighting into tagged literals, ~() switches back to Clojure - {:dir "/home/ajet/repos/clojure-typst/editors/nvim" - :ft ["clojure"]} + :dependencies ["tpope/vim-repeat" + "tpope/vim-sexp-mappings-for-regular-people"] + :init (fn [] + (set vim.g.sexp_filetypes "clojure,scheme,lisp,timl,fennel,racket") + ;; Sync sexp auto-insert with parinfer default + (let [par (require :config.parinfer)] + (set vim.g.sexp_enable_insert_mode_mappings + (if par.default-enabled 0 1))))} ;; Mason - Package manager for LSP servers, DAP servers, linters, formatters - ;; Run :MasonInstall clojure_lsp lua_ls to install servers {repo "williamboman/mason.nvim" - :cmd ["Mason" "MasonInstall" "MasonUpdate"] + :lazy false :build ":MasonUpdate" :opts {:ui {:border "rounded"}}} + ;; Auto-install LSP servers and tools via Mason + {repo "WhoIsSethDaniel/mason-tool-installer.nvim" + :lazy false + :dependencies ["williamboman/mason.nvim"] + :opts {:ensure_installed ["clojure-lsp" "clj-kondo"]}} + + ;; Hop - EasyMotion-like word jumping + {repo "smoka7/hop.nvim" + :version "*" + :event "VeryLazy" + :config (fn [] + (let [hop (require :hop)] + (hop.setup) + (vim.keymap.set :n "gw" (fn [] (hop.hint_words)) + {:desc "Hop to word"})))} + ;; 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/lazy-lock.json b/lazy-lock.json index 9f3f7f9..9f689c2 100644 --- a/lazy-lock.json +++ b/lazy-lock.json @@ -1,13 +1,17 @@ { "conjure": { "branch": "main", "commit": "403639610bcb9b6a5dfc494dc3179cc19773a471" }, + "hop.nvim": { "branch": "master", "commit": "08ddca799089ab96a6d1763db0b8adc5320bf050" }, "lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" }, + "mason-tool-installer.nvim": { "branch": "main", "commit": "443f1ef8b5e6bf47045cb2217b6f748a223cf7dc" }, "mason.nvim": { "branch": "main", "commit": "44d1e90e1f66e077268191e3ee9d2ac97cc18e65" }, "nfnl": { "branch": "main", "commit": "fecf731e02bc51d88372c4f992fe1ef0c19c02ae" }, - "nvim-paredit": { "branch": "master", "commit": "b6ba636874a3115d944e35746444724240568aca" }, "nvim-parinfer": { "branch": "master", "commit": "3968e669d9f02589aa311d33cb475b16b27c5fbb" }, - "nvim-treesitter": { "branch": "master", "commit": "42fc28ba918343ebfd5565147a42a26580579482" }, + "nvim-treesitter": { "branch": "main", "commit": "19c729dae6e0eeb79423df0cf37780aa9a7cc3b7" }, "plenary.nvim": { "branch": "master", "commit": "b9fd5226c2f76c951fc8ed5923d85e4de065e509" }, "telescope.nvim": { "branch": "master", "commit": "a0bbec21143c7bc5f8bb02e0005fa0b982edc026" }, "tokyonight.nvim": { "branch": "main", "commit": "5da1b76e64daf4c5d410f06bcb6b9cb640da7dfd" }, + "vim-repeat": { "branch": "master", "commit": "65846025c15494983dafe5e3b46c8f88ab2e9635" }, + "vim-sexp": { "branch": "master", "commit": "f5bd4cb2535969fe4741b24fa5f51690521dbaf0" }, + "vim-sexp-mappings-for-regular-people": { "branch": "master", "commit": "4debb74b0a3e530f1b18e5b7dff98a40b2ad26f1" }, "which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" } } diff --git a/lua/lisp-school.lua b/lua/lisp-school.lua new file mode 100644 index 0000000..53b4898 --- /dev/null +++ b/lua/lisp-school.lua @@ -0,0 +1,134 @@ +-- [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}