├── .npmignore ├── vim-indent-error.lisp ├── package.json ├── .lispwords ├── .lispwords.lua ├── lispwords.json ├── scmindent.js ├── history ├── lispindent.lisp ├── scmindent.rkt ├── README.adoc └── scmindent.lua /.npmignore: -------------------------------------------------------------------------------- 1 | .lispwords.lua 2 | docs/ 3 | history 4 | lispindent.lisp 5 | scmindent.lua 6 | scmindent.rkt 7 | -------------------------------------------------------------------------------- /vim-indent-error.lisp: -------------------------------------------------------------------------------- 1 | ;last modified: 2015-10-30 2 | ;(n)vim with v:version == 704 still gets this wrong 3 | 4 | (begin 5 | (display "alpha 6 | bravo 7 | charlie") 8 | "should line up under lparen before display, not under charlie") 9 | 10 | ; ex:lisp:tw=0 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Dorai Sitaram ds26gte.github.io", 3 | "name": "scmindent", 4 | "description": "filter for indenting Racket, Scheme, Common Lisp, LFE, ART*Enterprise, etc", 5 | "version": "1.0.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/ds26gte/scmindent.git" 9 | }, 10 | "bin": "./scmindent.js" 11 | } 12 | -------------------------------------------------------------------------------- /.lispwords: -------------------------------------------------------------------------------- 1 | ; Sample ~/.lispwords for use with scmindent.scm or lispindent.lisp 2 | ; Dorai Sitaram 3 | ; Last modified 2019-11-17 4 | 5 | ((block 6 | datum->syntax 7 | handler-bind 8 | loop) 0) 9 | 10 | ((case 11 | call-with-input-file 12 | defpackage do-all-symbols do-external-symbols dolist do-symbols dotimes 13 | ecase etypecase eval-when 14 | flet 15 | handler-case 16 | labels lambda let let* let-values 17 | macrolet 18 | prog1 19 | typecase 20 | unless unwind-protect 21 | when with-input-from-string with-open-file with-open-socket 22 | with-open-stream with-output-to-string) 1) 23 | 24 | ((assert 25 | defun destructuring-bind do do* 26 | if 27 | multiple-value-bind 28 | with-slots) 2) 29 | -------------------------------------------------------------------------------- /.lispwords.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | .lispwords.lua 3 | Dorai Sitaram 4 | Last modified: 2017-12-12 5 | 6 | Associates an indent number with a Lisp keyword. If keyword A's indent number 7 | is N, and a Lisp form introduced by A is split across lines, then if the Ith 8 | subform starts one such line, that line is indented by 3 columns past A if I <= 9 | N, and by 1 column past A otherwise. This file should be placed in $HOME. 10 | --]] 11 | 12 | return { 13 | ['call-with-input-file'] = 1, 14 | ['case'] = 1, 15 | ['do'] = 2, 16 | ['do*'] = 2, 17 | ['dolist'] = 1, 18 | ['flet'] = 1, 19 | ['fluid-let'] = 1, 20 | ['if'] = 2, 21 | ['lambda'] = 1, 22 | ['let'] = 1, 23 | ['let*'] = 1, 24 | ['letrec'] = 1, 25 | ['let-values'] = 1, 26 | ['multiple-value-bind'] = 2, 27 | ['unless'] = 1, 28 | ['when'] = 1, 29 | ['with-open-file'] = 1, 30 | } 31 | -------------------------------------------------------------------------------- /lispwords.json: -------------------------------------------------------------------------------- 1 | { 2 | "Last modified": "2017-12-12", 3 | "Description of this file" : 4 | "Config file used by scmindent.js. Associates an indent number with a Lisp keyword. If keyword A's indent number is N, and a Lisp form introduced by A is split across lines, then if the Ith subform starts one such line, that line is indented by 3 columns past A if I <= N, and by 1 column past A otherwise. This file should be placed in $HOME.", 5 | 6 | "block": 0, 7 | "call-with-input-file": 1, 8 | "call-with-output-file": 1, 9 | "case": 1, 10 | "defpackage": 1, 11 | "defun": 2, 12 | "destructuring-bind": 2, 13 | "do": 2, 14 | "do*": 2, 15 | "dolist": 1, 16 | "dotimes": 1, 17 | "ecase": 1, 18 | "etypecase": 1, 19 | "eval-when": 1, 20 | "flet": 1, 21 | "handler-bind": 0, 22 | "if": 2, 23 | "labels": 1, 24 | "lambda": 1, 25 | "let": 1, 26 | "let*": 1, 27 | "let-values": 1, 28 | "letrec": 1, 29 | "loop": 0, 30 | "multiple-value-bind": 2, 31 | "prog1": 1, 32 | "typecase": 1, 33 | "unless": 1, 34 | "unwind-protect": 1, 35 | "when": 1, 36 | "with-input-from-string": 1, 37 | "with-open-file": 1, 38 | "with-open-socket": 1, 39 | "with-open-stream": 1, 40 | "with-output-to-string": 1, 41 | "with-slots": 2 42 | } 43 | -------------------------------------------------------------------------------- /scmindent.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Dorai Sitaram 4 | // last modified 2020-11-14 5 | 6 | // this script takes lines of Lisp or Scheme code from its 7 | // stdin and produces an indented version thereof on its 8 | // stdout 9 | 10 | var fs = require('fs'); 11 | var events = require('events'); 12 | 13 | var tbd = new events.EventEmitter(); 14 | var lispKeywords = {}; 15 | 16 | function numLeadingSpaces(s) { 17 | if (s.match(/^\s*$/)) { 18 | return 0; 19 | } else { 20 | s = s.replace(/^(\s*).*/, '$1'); 21 | s = s.replace(/\t/g, ' '); 22 | return s.length; 23 | } 24 | } 25 | 26 | function stringTrimBlanks(s) { 27 | s = s.replace(/^\s*/, ''); 28 | s = s.replace(/\s*$/, ''); 29 | return s; 30 | } 31 | 32 | function literalTokenP(s) { 33 | return !!s.match(/^[0-9#"]/); 34 | } 35 | 36 | function getLispIndentNumber(s) { 37 | var n; 38 | s = s.toLowerCase(); 39 | if (n = lispKeywords[s]) { 40 | return n; 41 | } else if (s.match(/^def/)) { 42 | return 0; 43 | } else { 44 | return -1; 45 | } 46 | } 47 | 48 | function pastNextAtom(s, i) { 49 | var n = s.length; 50 | while (true) { 51 | if (i >= n) { 52 | return n; 53 | } 54 | var c = s[i]; 55 | if (c === '\\') { 56 | i++; i++; 57 | } else if (c.match(/[ \t\(\)\[\]"'`,;]/)) { 58 | return i; 59 | } else { 60 | i++; 61 | } 62 | } 63 | } 64 | 65 | function calcSubindent(s, i) { 66 | var n = s.length; 67 | var j = pastNextAtom(s, i); 68 | var deltaIndent = 0; 69 | var lispIndentNum = -1; 70 | if (j === i) { 71 | ; 72 | } else { 73 | var w = s.substring(i, j); 74 | if (i >= 2 && s[i-2].match(/['`]/)) { 75 | ; 76 | } else { 77 | var lispIndentNum = getLispIndentNumber(w); 78 | if (lispIndentNum == -2) { 79 | ; 80 | } else if (lispIndentNum == -1) { 81 | if (j < n) { 82 | deltaIndent = j - i + 1 83 | } else { 84 | deltaIndent = 1 85 | } 86 | } else { 87 | deltaIndent = 1 88 | } 89 | } 90 | } 91 | return { 92 | deltaIndent: deltaIndent, 93 | lispIndentNum: lispIndentNum, 94 | nextTokenIndex: j 95 | } 96 | } 97 | 98 | var defaultLeftI = -1; 99 | var leftI = 0; 100 | var parenStack = []; 101 | var insideStringP = false; 102 | 103 | function indentLine(currLine) { 104 | var leadingSpaces = numLeadingSpaces(currLine); 105 | var currLeftI; 106 | if (insideStringP) { 107 | currLeftI = leadingSpaces; 108 | } else if (parenStack.length === 0) { 109 | if (defaultLeftI === -1) { defaultLeftI = leadingSpaces; } 110 | leftI = defaultLeftI; 111 | currLeftI = leftI; 112 | } else { 113 | currLeftI = parenStack[0].spacesBefore; 114 | if (parenStack[0].numFinishedSubforms < parenStack[0].lispIndentNum) { 115 | currLeftI += 2; 116 | } 117 | } 118 | currLine = stringTrimBlanks(currLine); 119 | if (currLine !== "") { 120 | for (var i = 0; i < currLeftI; i++) { 121 | process.stdout.write(' '); 122 | } 123 | process.stdout.write(currLine); 124 | } 125 | process.stdout.write('\n'); 126 | 127 | var escapeP = false; 128 | var tokenIntersticeP = false; 129 | var i = 0; 130 | function incrFinishedSubforms() { 131 | if (!tokenIntersticeP) { 132 | if (parenStack.length > 0) { 133 | parenStack[0].numFinishedSubforms++; 134 | } 135 | } 136 | tokenIntersticeP = true; 137 | } 138 | while (i < currLine.length) { 139 | var c = currLine[i]; 140 | if (escapeP) { 141 | escapeP = false; i++; 142 | } else if (c === '\\') { 143 | tokenIntersticeP = false; escapeP = true; i++; 144 | } else if (insideStringP) { 145 | if (c === '"') { insideStringP = false; incrFinishedSubforms(); } i++; 146 | } else if (c === ';') { 147 | incrFinishedSubforms(); break; 148 | } else if (c === '"') { 149 | incrFinishedSubforms(); insideStringP = true; i++; 150 | } else if (c.match(/[ \t]/)) { 151 | incrFinishedSubforms(); i++; 152 | } else if (c.match(/[\(\[]/)) { 153 | incrFinishedSubforms(); 154 | var si = calcSubindent(currLine, i+1); 155 | parenStack.unshift({ spacesBefore: 1 + i + currLeftI + si.deltaIndent, 156 | lispIndentNum: si.lispIndentNum, 157 | numFinishedSubforms: -1 158 | }); 159 | tokenIntersticeP = true; 160 | var iNext = i+1; 161 | if (si.nextTokenIndex > iNext) { 162 | iNext = si.nextTokenIndex; 163 | tokenIntersticeP = false; 164 | } 165 | i = iNext; 166 | } else if (c.match(/[\)\]]/)) { 167 | tokenIntersticeP = false; 168 | if (parenStack.length > 0) { 169 | parenStack.shift(); 170 | } else { 171 | leftI = 0; 172 | } 173 | i++; 174 | } else { 175 | tokenIntersticeP = false; i++; 176 | } 177 | } 178 | incrFinishedSubforms(); 179 | } 180 | 181 | function indentLines() { 182 | process.stdin.setEncoding('utf8'); 183 | var prevLine; 184 | process.stdin.on('data', function(data) { 185 | var lines = data.split('\n'); 186 | if (lines.length) { 187 | if (prevLine) { 188 | lines[0] = prevLine + lines[0]; 189 | } 190 | for (var i = 0; i < lines.length-1; i++) { 191 | tbd.emit('indentLine', lines[i]); 192 | } 193 | prevLine = lines[lines.length-1]; 194 | } 195 | }); 196 | process.stdin.on('end', function() { 197 | if (prevLine) { 198 | tbd.emit('indentLine', prevLine); 199 | prevLine = ''; 200 | } 201 | }); 202 | } 203 | 204 | function customize() { 205 | fs.readFile(process.env.LISPWORDS || (process.env.HOME + '/lispwords.json'), 'utf8', 206 | function(err, data) { 207 | if (!err) { 208 | var lw = JSON.parse(data); 209 | for (var kw in lw) { 210 | lispKeywords[kw] = lw[kw]; 211 | } 212 | } else { 213 | // process.stderr.write('~/lispwords.json missing or ill-formed\n'); 214 | } 215 | tbd.emit('customizationDone'); 216 | }); 217 | } 218 | 219 | tbd.on('start', customize); 220 | tbd.on('customizationDone', indentLines); 221 | tbd.on('indentLine', indentLine); 222 | 223 | tbd.emit('start'); 224 | -------------------------------------------------------------------------------- /history: -------------------------------------------------------------------------------- 1 | Last change 2017-07-29 2 | 3 | 2017 Jul 29 4 | 5 | Added scmindent.lua, .lispwords.lua. 6 | 7 | 2016 Dec 28 8 | 9 | Fix string-trim-blanks to work on blank lines. From @netmany. 10 | 11 | 2014 Sep 16 12 | 13 | Added scmindent.js, lispwords.json. 14 | 15 | Fixed bug in num-leading-spaces (that fortunately didn't trigger 16 | any indentation error all these years!). 17 | 18 | Oct 30, 2010 19 | 20 | Forms starting with "(#" should not trigger error. From Lasse Kliemann. 21 | 22 | Sep 6, 2010 23 | 24 | Scheme version should take car of paren-stack only after checking if latter is 25 | non-null. From Eric Christopherson. 26 | 27 | Mar 28, 2009 28 | 29 | " is a token delimiter! 30 | 31 | Mar 7, 2009 32 | 33 | Scheme version is PLT only. Hopefully, R6RS-compliance later. 34 | 35 | Mar 4, 2009 36 | 37 | Allow keywords to have an indent number, i.e., the number of argument 38 | subforms that should be indented “wide”. E.g., if has number 2, so the 39 | then-clause is indented wide, but the else-clause backs up again, so you 40 | can distinguish them. 41 | 42 | ( ... 43 | ...) 44 | 45 | because it's obviously not a function call, and the literal items look 46 | better aligned flushleft. 47 | 48 | Mar 3, 2009 49 | 50 | vim's Lisp indenting still fails at times (a series of backslashed 51 | doublequotes inside doublequotes trips it). So I have to resurrect this 52 | old script. Added a CL version lispindent.lisp. 53 | 54 | v 1b2 55 | 56 | May 2, 2003 57 | 58 | indent/scheme.vim now obsolete, as Vim's built-in 59 | lispindent has incorporated it by not only 60 | recognizing 'lispwords' but also by correctly 61 | ignoring backslashed parens. 62 | 63 | v 1b1 64 | 65 | Dec 7, 2002 66 | 67 | Use s: prefix for names of functions in scheme.vim 68 | that will be used inside that file only. 69 | 70 | v 1b 71 | 72 | Nov 28, 2002 73 | 74 | When looking for flush-left left-paren in order 75 | to start calculating indents, make sure it has 76 | immediately above it either: beginning of file, 77 | a blank line, or a comment line. This allows 78 | a series of flush-left parens to indent properly 79 | at the first go, instead of user having to 80 | repeatedly invoke the indenter. 81 | 82 | Bugfix: String_trim_blanks wasn't trimming right space 83 | if left space also present. This caused some lines 84 | with (invisible) trailing spaces to indent 85 | improperly. 86 | 87 | Nov 17, 2002 88 | 89 | Added makefile for my own use. 90 | 91 | v 1a 92 | 93 | Nov 4, 2002 94 | 95 | search() when it returns 0 doesn't move cursor. 96 | Take this into account. 97 | 98 | Nov 3, 2002 99 | 100 | scheme.vim: Copy in vim distribution dispensed with the 101 | 7-line window in GetSchemeIndent(), possibly because 102 | the window wasn't large enough for some indentation 103 | patterns. However, the change always picked a window 104 | starting from file's first line, which slowed down 105 | indents to a crawl for code further down the 106 | file. Added code to pick a close enough line 107 | (but without a hard-wired number) that nevertheless 108 | gives accurate indents. 109 | 110 | Feb 11, 2002 111 | 112 | Use variable scheme_indent_window (default = 7) 113 | to limit how far indenter looks back to indent current 114 | line. Lower number => quicker response. If the 115 | terminus line is already correctly indented, this 116 | will not produce inaccurate indentation. 117 | 118 | Feb 4, 2002 119 | 120 | added note on how to define b:match_words 121 | for the Scheme filetype so % works correctly. 122 | 123 | Feb 3, 2002 124 | 125 | scheme.vim, a Scheme indent plugin, is the new form of 126 | scmindent.vim. scheme.vim does 127 | 128 | setlocal indentexpr=GetSchemeIndent() 129 | 130 | You must put scheme.vim in the indent subdir of 131 | some dir in your runtimepath, and ensure that you 132 | have 'filetype indent on'. 133 | 134 | The indenting procedure treats the keywords in the 135 | Vim option 'lispwords' as define-like. You can 136 | add your own keywords to 'lispwords', perhaps on 137 | a per-filetype basis. 138 | 139 | Jan 31, 2002 140 | 141 | Ensure multi-line strings aren't indented. 142 | 143 | Use traditional Scheme indentation. Ie, most headwords 144 | indent like cond, some special ones indent like define 145 | or let. The latter can be added to in ~/.schemewords 146 | (for scmindent.scm) or as the variable scheme_words 147 | (for scmindent.vim). This style is practically like 148 | emacs, except we don't allow for some headwords to have 149 | a distinguished set of immediately following arguments 150 | that are indented differently from the body. This 151 | feature doesn't seem to be used and would have required 152 | a user-definable table (and consequent 153 | decision-making from the user). 154 | 155 | scmindent.scm is strictly from stdin in stdout, 156 | for easy portability. 157 | 158 | Pare down the package for easy maintenance. 159 | 160 | Nov 8, 2000 161 | 162 | scmindent.scm script can take an arg file, which it 163 | indents into stdout. This is to allow an alternate 164 | nmap in Windows vi, where filters don't seem 165 | able to read stdin. 166 | 167 | scmtags.scm (like scmtags.pl) generates tags file for 168 | Scheme/Lisp/ARTIM code for use with vim. scmetags 169 | generates TAGS for use with emacs. 170 | 171 | Nov 2, 2000 172 | 173 | Separated lang-specific indentation into 174 | different programs: scmindent for Scheme/Lisp, 175 | shindent for shellscripts, brindent for 176 | C-, Perl-like langs 177 | 178 | Mar 11, 2000 179 | 180 | scmindent.vim: Added a vmap for indenting a region. 181 | Added support for autoindenting new lines. 182 | 183 | Mar 4, 2000 184 | 185 | scmindent.vim (Scheme indenter written directly in 186 | Vim's extension lang) 187 | 188 | Jan 28, 2000 189 | 190 | scmindent.scm will indent C- or Perl-like code too, if 191 | it realizes that that's what it's looking at. 192 | 193 | Nov 12, 1999 194 | 195 | scmtags.pl 196 | 197 | Oct 30, 1999 198 | 199 | Lparen followed by token of length 1 or 2 (eg, if, 200 | =) will cause following line to indent past the token 201 | (just like the preferred indentation for cond). 202 | 203 | scmindent.pl will indent C- and Perl-like code 204 | too, if it realizes that that's what it's looking at. 205 | 206 | Oct 24, 1999 207 | 208 | Perl, for portability. 209 | 210 | Oct 8, 1999 211 | 212 | MzScheme version 213 | -------------------------------------------------------------------------------- /lispindent.lisp: -------------------------------------------------------------------------------- 1 | ":";if test -z "$LISP"; then LISP=ecl; fi 2 | ":";if test "$LISP" = clasp; then exec clasp --script $0 3 | ":";elif test "$LISP" = clisp; then exec clisp -q $0 4 | ":";elif test "$LISP" = clozure; then exec ccl -b -Q -l $0 5 | ":";elif test "$LISP" = ecl; then exec ecl -shell $0 6 | ":";elif test "$LISP" = sbcl; then exec sbcl --script $0 7 | ":";fi 8 | 9 | ;Dorai Sitaram 10 | ;Oct 8, 1999 11 | ;last change 2022-07-05 12 | 13 | ;this script takes lines of Lisp or Scheme code from its 14 | ;stdin and produces an indented version thereof on its 15 | ;stdout 16 | 17 | (defvar *lisp-keywords* '()) 18 | 19 | (defun set-lisp-indent-number (sym num-of-subforms-to-be-indented-wide) 20 | (declare (symbol sym) (integer num-of-subforms-to-be-indented-wide)) 21 | (let* ((x (symbol-name sym)) 22 | (c (assoc x *lisp-keywords* :test #'string-equal))) 23 | (unless c 24 | (push (setq c (cons x nil)) *lisp-keywords*)) 25 | (setf (cdr c) num-of-subforms-to-be-indented-wide))) 26 | 27 | (defun retrieve-env (s) 28 | (declare (string s)) 29 | #+(or abcl clasp clisp ecl) (ext:getenv s) 30 | #+allegro (sys:getenv s) 31 | #+clozure (ccl:getenv s) 32 | #+cmucl (cdr (assoc (intern s :keyword) 33 | ext:*environment-list* :test #'string=)) 34 | #+mkcl (mkcl:getenv s) 35 | #+sbcl (sb-ext:posix-getenv s)) 36 | 37 | (defun read-home-lispwords () 38 | (with-open-file (i (or (retrieve-env "LISPWORDS") 39 | (merge-pathnames ".lispwords" (user-homedir-pathname))) 40 | :if-does-not-exist nil) 41 | (when i 42 | (loop 43 | (let* ((w (or (read i nil) (return))) 44 | (a w)) 45 | (unless (atom w) 46 | (setq a (car w))) 47 | (cond ((atom w) 48 | (set-lisp-indent-number a 0)) 49 | ((numberp a) 50 | (mapc (lambda (x) (set-lisp-indent-number x a)) (cdr w))) 51 | ((consp a) 52 | (let ((n (cadr w))) 53 | (mapc (lambda (x) (set-lisp-indent-number x n)) a))) 54 | (t (set-lisp-indent-number a (cadr w))))))))) 55 | 56 | (defun past-next-atom (s i n) 57 | (declare (string s) (integer i) (integer n)) 58 | (loop 59 | (when (>= i n) (return n)) 60 | (let ((c (char s i))) 61 | (cond ((char= c #\\) (incf i)) 62 | ((member c '(#\space #\tab #\( #\) #\[ #\] #\" #\' #\` #\, #\;)) 63 | (return i)))) 64 | (incf i))) 65 | 66 | (defun get-lisp-indent-number (s) 67 | (declare (symbol s) (symbol raw-symbol-p)) 68 | (labels ((get-lisp-indent-number 69 | (s &key (raw-symbol-p nil)) 70 | (or (cdr (assoc s *lisp-keywords* :test #'string-equal)) 71 | (cond ((eql (search "def" s :test #'char-equal) 0) 0) 72 | (raw-symbol-p -1) 73 | (t (let ((p (position #\: s :from-end t))) 74 | (if p 75 | (get-lisp-indent-number (subseq s (1+ p)) :raw-symbol-p t) 76 | -1))))))) 77 | (get-lisp-indent-number s))) 78 | 79 | (defun literal-token-p (s) 80 | (declare (string s)) 81 | (let ((colon-pos (position #\: s))) 82 | (if colon-pos (= colon-pos 0) 83 | (let ((s (read-from-string s))) 84 | (or (characterp s) (numberp s) (stringp s)))))) 85 | 86 | (defstruct lparen 87 | spaces-before 88 | lisp-indent-num 89 | num-finished-subforms) 90 | 91 | (defun calc-subindent (s i n) 92 | (declare (string s) (integer i) (integer n)) 93 | (let* ((j (past-next-atom s i n)) 94 | (lisp-indent-num 0) 95 | (delta-indent 96 | (if (= j i) 0 97 | (let ((w (subseq s i j))) 98 | (if (or (and (>= i 2) (member (char s (- i 2)) '(#\' #\`))) 99 | (literal-token-p w)) 0 100 | (progn (setq lisp-indent-num (get-lisp-indent-number w)) 101 | (case lisp-indent-num 102 | (-2 0) 103 | (-1 (if (< j n) (1+ (- j i)) 1)) 104 | (t 1)))))))) 105 | (values delta-indent lisp-indent-num j))) 106 | 107 | (defun num-leading-spaces (s) 108 | (declare (string s)) 109 | (let ((n (length s)) 110 | (i 0) (j 0)) 111 | (loop 112 | (when (>= i n) (return 0)) 113 | (case (char s i) 114 | (#\space (incf i) (incf j)) 115 | (#\tab (incf i) (incf j 8)) 116 | (t (return j)))))) 117 | 118 | (defun string-trim-blanks (s) 119 | (declare (string s)) 120 | (string-trim '(#\space #\tab #\newline #\return) s)) 121 | 122 | (defun indent-lines () 123 | (let ((default-left-i -1) (left-i 0) (paren-stack '()) (inside-stringp nil)) 124 | (loop 125 | (let* ((curr-line (or (read-line nil nil) (return))) 126 | (leading-spaces (num-leading-spaces curr-line)) 127 | (curr-left-i 128 | (cond (inside-stringp leading-spaces) 129 | ((null paren-stack) 130 | (when (= left-i 0) 131 | (when (= default-left-i -1) 132 | (setq default-left-i leading-spaces)) 133 | (setq left-i default-left-i)) 134 | left-i) 135 | (t (let* ((lp (car paren-stack)) 136 | (nas (lparen-lisp-indent-num lp)) 137 | (nfs (lparen-num-finished-subforms lp)) 138 | (extra-w 0)) 139 | (when (< nfs nas) ;(and (>= nas 0) (< nfs nas)) 140 | (incf (lparen-num-finished-subforms lp)) 141 | (setq extra-w 2)) 142 | (+ (lparen-spaces-before lp) 143 | extra-w)))))) 144 | (setq curr-line (string-trim-blanks curr-line)) 145 | (unless (string= curr-line "") 146 | (dotimes (k curr-left-i) (write-char #\space)) 147 | (princ curr-line)) 148 | (terpri) 149 | ; 150 | (let ((i 0) (n (length curr-line)) (escapep nil) 151 | (token-interstice-p nil)) 152 | (flet ((incr-finished-subforms () 153 | (unless token-interstice-p 154 | (when paren-stack 155 | (incf (lparen-num-finished-subforms 156 | (car paren-stack)))) 157 | (setq token-interstice-p t)))) 158 | ; 159 | (loop 160 | (when (>= i n) (return)) 161 | (let ((c (char curr-line i))) 162 | (cond (escapep (setq escapep nil)) 163 | ((char= c #\\) (setq token-interstice-p nil escapep t)) 164 | (inside-stringp (when (char= c #\") 165 | (setq inside-stringp nil) 166 | (incr-finished-subforms))) 167 | ((char= c #\;) (incr-finished-subforms) (return)) 168 | ((char= c #\") (incr-finished-subforms) (setq inside-stringp t)) 169 | ((member c '(#\space #\tab) :test #'char=) 170 | (incr-finished-subforms)) 171 | ((member c '(#\( #\[) :test #'char=) 172 | (incr-finished-subforms) 173 | (multiple-value-bind (delta-indent lisp-indent-num j) 174 | (calc-subindent curr-line (1+ i) n) 175 | (push (make-lparen :spaces-before (+ 1 i curr-left-i delta-indent) 176 | :lisp-indent-num lisp-indent-num 177 | :num-finished-subforms -1) 178 | paren-stack) 179 | (setq token-interstice-p t) 180 | (let ((inext (1+ i))) 181 | (when (> j inext) 182 | (setq inext j token-interstice-p nil)) 183 | (setq i (1- inext))))) 184 | ((member c '(#\) #\]) :test #'char=) 185 | (setq token-interstice-p nil) 186 | (cond (paren-stack (pop paren-stack)) 187 | (t (setq left-i 0)))) 188 | (t (setq token-interstice-p nil)))) 189 | (incf i)) 190 | (incr-finished-subforms))))))) 191 | 192 | (read-home-lispwords) 193 | 194 | (indent-lines) 195 | 196 | ;eof 197 | -------------------------------------------------------------------------------- /scmindent.rkt: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env racket 2 | 3 | #lang racket/base 4 | 5 | ;Dorai Sitaram 6 | ;Oct 8, 1999 7 | ;last change 2022-07-05 8 | 9 | ;This script takes lines of Scheme or Lisp code from its 10 | ;stdin and produces an indented version thereof on its 11 | ;stdout. 12 | 13 | (define *lisp-keywords* '()) 14 | 15 | (define (set-lisp-indent-number sym num-of-subforms-to-be-indented-wide) 16 | (let* ((x (symbol->string sym)) 17 | (c (assf (lambda (a) (string-ci=? x a)) *lisp-keywords*))) 18 | (unless c 19 | (set! c (cons x (box 0))) 20 | (set! *lisp-keywords* (cons c *lisp-keywords*))) 21 | (set-box! (cdr c) num-of-subforms-to-be-indented-wide))) 22 | 23 | (define (read-home-lispwords) 24 | (let ([init-file (or (getenv "LISPWORDS") 25 | (build-path (find-system-path 'home-dir) ".lispwords"))]) 26 | (when (file-exists? init-file) 27 | (call-with-input-file init-file 28 | (lambda (i) 29 | (let loop () 30 | (let ([w (read i)]) 31 | (unless (eof-object? w) 32 | (let ([a w]) 33 | (when (pair? w) (set! a (car w))) 34 | (cond [(not (pair? w)) (set-lisp-indent-number a 0)] 35 | [(number? a) 36 | (for-each (lambda (x) (set-lisp-indent-number x a)) (cdr w))] 37 | [(list? a) 38 | (let ([n (cadr w)]) 39 | (for-each (lambda (x) (set-lisp-indent-number x n)) a))] 40 | [else 41 | (set-lisp-indent-number a (cadr w))])) 42 | (loop))))))))) 43 | 44 | (define (past-next-atom s i n) 45 | (let loop ((i i)) 46 | (if (>= i n) n 47 | (let ((c (string-ref s i))) 48 | (cond ((char=? c #\\) (loop (+ i 2))) 49 | ((memv c '(#\space #\tab #\( #\) #\[ #\] #\" #\' #\` #\, #\;)) 50 | i) 51 | (else (loop (+ i 1)))))))) 52 | 53 | (define (get-lisp-indent-number s) 54 | (let ((c (assf (lambda (a) (string-ci=? s a)) *lisp-keywords*))) 55 | (cond (c (unbox (cdr c))) 56 | ((and (>= (string-length s) 3) 57 | (string-ci=? (substring s 0 3) "def")) 0) 58 | (else -1)))) 59 | 60 | (define (literal-token? s) 61 | (let ((x (let ((i (open-input-string s))) 62 | (begin0 (read i) (close-input-port i))))) 63 | (or (char? x) (number? x) (string? x)))) 64 | 65 | (define (calc-subindent s i n) 66 | (let* ((j (past-next-atom s i n)) 67 | (lisp-indent-num -1) 68 | (delta-indent 69 | (if (= j i) 0 70 | (let ((w (substring s i j))) 71 | (if (and (>= i 2) 72 | (memv (string-ref s (- i 2)) '(#\' #\`))) 73 | 0 74 | (begin 75 | (set! lisp-indent-num (get-lisp-indent-number w)) 76 | (case lisp-indent-num 77 | ((-2) 0) 78 | ((-1) (if (< j n) (+ (- j i) 1) 1)) 79 | (else 1)))))))) 80 | (values delta-indent lisp-indent-num j))) 81 | 82 | (define (num-leading-spaces s) 83 | (let ((n (string-length s))) 84 | (let loop ((i 0) (j 0)) 85 | (if (>= i n) 0 86 | (case (string-ref s i) 87 | ((#\space) (loop (+ i 1) (+ j 1))) 88 | ((#\tab) (loop (+ i 1) (+ j 8))) 89 | (else j)))))) 90 | 91 | (define (string-trim-blanks s) 92 | (let ((n (string-length s))) 93 | (let ((j (let loop ((j 0)) 94 | (if (or (>= j n) 95 | (not (char-whitespace? (string-ref s j)))) 96 | j 97 | (loop (+ j 1)))))) 98 | (if (>= j n) "" 99 | (let ((k (let loop ((k (- n 1))) 100 | (if (or (< k 0) 101 | (not (char-whitespace? (string-ref s k)))) 102 | (+ k 1) 103 | (loop (- k 1)))))) 104 | (substring s j k)))))) 105 | 106 | (define-struct lparen 107 | (spaces-before lisp-indent-num num-finished-subforms) 108 | #:mutable) 109 | 110 | (define (indent-lines) 111 | (let ((default-left-i -1) (left-i 0) (paren-stack '()) (inside-string? #f)) 112 | (let line-loop () 113 | (let ((curr-line (read-line))) 114 | (unless (eof-object? curr-line) 115 | (let* ((leading-spaces (num-leading-spaces curr-line)) 116 | (curr-left-i 117 | (cond (inside-string? leading-spaces) 118 | ((null? paren-stack) 119 | (when (= left-i 0) 120 | (when (= default-left-i -1) 121 | (set! default-left-i leading-spaces)) 122 | (set! left-i default-left-i)) 123 | left-i) 124 | (else (let* ((lp (car paren-stack)) 125 | (lin (lparen-lisp-indent-num lp)) 126 | (nfs (lparen-num-finished-subforms lp)) 127 | (extra-w 0)) 128 | (when (< nfs lin) 129 | (set! extra-w 2)) 130 | (+ (lparen-spaces-before lp) extra-w)))))) 131 | (set! curr-line (string-trim-blanks curr-line)) 132 | (unless (string=? curr-line "") 133 | (do ((i 0 (+ i 1))) 134 | ((= i curr-left-i)) 135 | (write-char #\space)) 136 | (display curr-line)) 137 | (newline) 138 | ; 139 | (let ((n (string-length curr-line)) 140 | (escape? #f) 141 | (token-interstice? #f)) 142 | (let ((incr-finished-subforms (lambda () 143 | (unless token-interstice? 144 | (cond ((and (pair? paren-stack) 145 | (car paren-stack)) => 146 | (lambda (lp) 147 | (let ((nfs (lparen-num-finished-subforms lp))) 148 | (set-lparen-num-finished-subforms! 149 | lp (+ nfs 1)))))) 150 | (set! token-interstice? #t))))) 151 | (let loop ((i 0)) 152 | (unless (>= i n) 153 | (let ((c (string-ref curr-line i))) 154 | (cond (escape? (set! escape? #f) (loop (+ i 1))) 155 | ((char=? c #\\) 156 | (set! token-interstice? #f) 157 | (set! escape? #t) (loop (+ i 1))) 158 | (inside-string? 159 | (when (char=? c #\") 160 | (set! inside-string? #f) 161 | (incr-finished-subforms)) 162 | (loop (+ i 1))) 163 | ((char=? c #\;) 164 | (incr-finished-subforms) 165 | 'break-loop) 166 | ((char=? c #\") 167 | (incr-finished-subforms) 168 | (set! inside-string? #t) 169 | (loop (+ i 1))) 170 | ((memv c '(#\space #\tab)) 171 | (incr-finished-subforms) 172 | (loop (+ i 1))) 173 | ((memv c '(#\( #\[)) 174 | (incr-finished-subforms) 175 | (let-values (((delta-indent lisp-indent-num j) 176 | (calc-subindent curr-line (+ i 1) n))) 177 | (set! paren-stack 178 | (cons (make-lparen (+ 1 i curr-left-i delta-indent) 179 | lisp-indent-num 180 | -1) 181 | paren-stack)) 182 | (set! token-interstice? #t) 183 | (let ((inext (+ i 1))) 184 | (when (> j inext) 185 | (set! inext j) 186 | (set! token-interstice? #f)) 187 | (loop inext)))) 188 | ((memv c '(#\) #\])) 189 | (set! token-interstice? #f) 190 | (cond ((pair? paren-stack) 191 | (set! paren-stack (cdr paren-stack))) 192 | (else (set! left-i 0))) 193 | (loop (+ i 1))) 194 | (else (set! token-interstice? #f) 195 | (loop (+ i 1))))))) 196 | (incr-finished-subforms)))) 197 | (line-loop)))))) 198 | 199 | (read-home-lispwords) 200 | 201 | (indent-lines) 202 | 203 | ;eof 204 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | // last modified 2022-07-05 2 | = Editing Lisp and Scheme files in vi 3 | 4 | [quote] 5 | But then, when was the last time you heard 6 | of a lisp programmer that used vi? (Paul Fox, vile developer) 7 | 8 | The ability to automatically indent code is indispensable when 9 | editing files containing s-expression-based code such as Racket, 10 | Common Lisp, Scheme, LFE, ART*Enterprise, and other Lisps. 11 | 12 | The text editor family vi provides the option `lisp`, which in 13 | conjunction with the options `autoindent` and `showmatch` 14 | provides a form of Lisp indenting, but except in the improved vi 15 | clones Vim and Neovim, this support is poor in at least two 16 | respects: 17 | 18 | 1. escaped parentheses and double-quotes are not treated 19 | correctly; and 20 | 21 | 2. all head-words are treated identically. 22 | 23 | Even the redoubtable Vim, which has improved its Lisp editing 24 | support over the years, and provides the `lispwords` option, 25 | continues to fail in 26 | https://github.com/ds26gte/scmindent/blob/master/vim-indent-error.lisp[strange 27 | ways]. 28 | 29 | Fortunately, both vi and Vim let you delegate the responsibility 30 | for indenting such code to an external filter program of your 31 | choosing. I provide here four such filtering scripts: 32 | `scmindent.rkt` written in Racket, `lispindent.lisp` in Common 33 | Lisp, `scmindent.lua` in Lua, and `scmindent.js` in JavaScript. 34 | 35 | == Installation 36 | 37 | The Racket 38 | and CL scripts are 39 | operationally identical and use the same type of customization 40 | via the file `~/.lispwords`. 41 | 42 | The Lua and JavaScript versions differ from the above in having their own format for the 43 | customization file. 44 | The Lua file is named https://github.com/ds26gte/scmindent/blob/master/.lispwords.lua[.lispwords.lua] 45 | and uses a Lua table. 46 | The JavaScript file is named 47 | https://github.com/ds26gte/scmindent/blob/master/lispwords.json[lispwords.json] and uses 48 | JSON. 49 | 50 | To use either of `scmindent.rkt`, `lispindent.lisp`, `scmindent.lua`, or 51 | `scmindent.js`, place them in your `PATH`. Alternatively, 52 | the JavaScript version is also available as a Node 53 | package: 54 | 55 | sudo npm install -g scmindent 56 | 57 | This directly installs the executable `scmindent` in your `PATH`. 58 | 59 | Henceforth, I will refer to just `scmindent.rkt` with the understanding that 60 | everything mentioned applies equally to `lispindent.lisp`, 61 | `scmindent.lua`, `scmindent.js`, and the NPM version `scmindent`. 62 | 63 | == Usage 64 | 65 | `scmindent.rkt` takes 66 | Lisp text from its standard input and produces an indented version 67 | thereof on its standard output. (Thus, it is possible to use 68 | `scmindent.rkt` as a command-line filter to "beautify" Lisp code, even if 69 | you don't use vi.) 70 | 71 | In Vim, set the `equalprg` option to the filter name, which causes the 72 | indenting command `=` to invoke the filter rather than the built-in 73 | indenter. 74 | 75 | You might want to make the `equalprg` setting local to a file 76 | based on its extension: 77 | 78 | autocmd bufread,bufnewfile *.lisp,*.scm setlocal equalprg=scmindent.rkt 79 | 80 | or its filetype: 81 | 82 | autocmd filetype lisp,scheme setlocal equalprg=scmindent.rkt 83 | 84 | In vi's other than Vim, use the `!` command to invoke the filter on part or all of 85 | your buffer: Type `!` to declare you'll be filtering; a movement command 86 | to scoop up the lines you'll be filtering; then the filter name 87 | (`scmindent.rkt`) followed by `Return`. 88 | 89 | == How subforms are indented 90 | 91 | Lisp indentation has a tacit, widely accepted convention that is not 92 | lightly to be messed with, so `scmindent.rkt` strives to provide the same 93 | style as Emacs, with the same type of customization. 94 | 95 | By default, the indentation procedure treats 96 | a form split over two or more lines as 97 | follows. (A form, if it is a list, is considered to have a head subform and zero or 98 | more argument subforms.) 99 | 100 | 1: If the head subform is followed by at 101 | least one other subform on the same line, then subsequent lines in the 102 | form are indented to line up directly under the first argument subform. 103 | 104 | (some-user-function-1 arg1 105 | arg2 106 | ...) 107 | 108 | 2: If the head subform is a list and is on a line by itself, then 109 | subsequent lines in the form are indented to 110 | line up directly under the head subform. 111 | 112 | ((some-user-function-2) 113 | arg1 114 | arg2 115 | ...) 116 | 117 | 3: If the head subform is a symbol and is on a line by itself, then 118 | subsequent lines in the form are indented one column past the beginning 119 | of the head symbol. 120 | 121 | (some-user-function-3 122 | arg1 123 | arg2 124 | ...) 125 | 126 | 4: If the head form can be deduced to be a literal, then subforms on 127 | subsequent lines line up directly under it, e.g., 128 | 129 | (1 2 3 130 | 4 5 6) 131 | 132 | '(alpha 133 | beta 134 | gamma) 135 | 136 | == Keywords 137 | 138 | However, some keyword symbols are treated differently. Each such 139 | keyword has a number N associated with it called its Lisp Indent 140 | Number, or LIN, 141 | which influences how its subforms are indented. This is almost exactly 142 | analogous to Emacs's `lisp-indent-function`, except I'm using numbers 143 | throughout. 144 | 145 | If 146 | the i'th argument subform starts 147 | on a subsequent line, and i ≤ N, then it is indented 3 columns past the 148 | keyword. All subsequent 149 | subforms are indented simply one column past the keyword. 150 | 151 | (defun some-user-function-4 (x y) ;defun is a 2-keyword 152 | body ...) 153 | 154 | (defun some-user-function-5 155 | (x y) 156 | body ...) 157 | 158 | (if test ;if is also a 2-keyword 159 | then-branch 160 | else-branch) 161 | 162 | `scmindent.rkt` pre-sets the LINs of many well-known 163 | Lisp keywords. In addition, any symbol that starts with `def` and whose 164 | LIN has not 165 | been explicitly set is assumed to 166 | have an LIN of 0. 167 | 168 | == Customization 169 | 170 | You can specify your own LINs for keywords via a customization 171 | file. The name of the file is the first available of the 172 | following: 173 | 174 | - the value of the environment variable `LISPWORDS`, if set 175 | - the file `.lispwords` in your home directory 176 | 177 | If `LISPWORDS` isn't set and `~/.lispwords` doesn't exist, no 178 | customization is done. If `LISPWORDS` is set but the thus named 179 | file doesn't exist, no customization is done. 180 | 181 | `~/.lispwords` can contain any number of 182 | 2-element lists: The first element of each list is a Lisp symbol 183 | and the second element is the LIN associated with 184 | it. E.g., 185 | 186 | (begin0 0) 187 | (when 1) 188 | (unless 1) 189 | (do 2) 190 | (define 2) 191 | 192 | This assigns a LIN of 0 to `begin0`; 1 to 193 | `when` and `unless`; and 2 to `do` and `defun`. 194 | 195 | If the LIN is 0, it may be omitted and the enclosing parentheses 196 | dispensed with. Thus, `begin0` is short for `(begin0 0)`. 197 | 198 | As a convenience, you can bunch symbols with the same LIN 199 | together in one of two ways, e.g., 200 | 201 | (begin0 0) 202 | ((when unless) 1) 203 | ((do define) 2) 204 | 205 | or 206 | 207 | (0 begin0) 208 | (1 when unless) 209 | (2 do defun) 210 | 211 | If using the JavaScript `scmindent`, see below for the 212 | corresponding `lispwords.json` format. 213 | 214 | (Note that in contrast 215 | to Vim's flat list of `lispwords`, `~/.lispwords` 216 | allows for different categories of lispwords. Vim's `lispwords` are 217 | all of LIN 0.) 218 | 219 | For example, a lot of users prefer the keyword `if` to have its then- 220 | and else-clauses indented the same amount of 3 columns. I.e., 221 | they want it to be a 3-keyword. A `.lispwords` entry that would 222 | secure this is: 223 | 224 | (if 3) 225 | 226 | To remove the keywordness of a symbol, you can assign it a LIN 227 | < 0. E.g. 228 | 229 | (if -1) 230 | 231 | would also cause all of ``if```'s subforms to be aligned. (This is because 232 | −1 causes subforms on subsequent lines to line up against the first 233 | argument subform on the first line, and that happens to be 3 columns 234 | past the beginning of a 2-column keyword like `if`. The only difference 235 | between −1 and 3 here is what happens when the `if` is on a line by 236 | itself, with the test on the line following. −1 indents subsequent 237 | lines one column past the beginning of the `if`, whereas 3 continues to 238 | indent them three columns past the beginning of the `if`. Further 239 | differences emerge between 3 and −1 when the `if` has more than three 240 | argument subforms, as allowed by Emacs Lisp, where 2 and −1 immediately 241 | prove to be better choices than 3. The author has made 2 the default 242 | because it is the only option that has the merit of indenting the then- 243 | and else-subforms by differing amounts.) 244 | 245 | == Customization (`.lispwords.lua`) 246 | 247 | `~/.lispwords.lua`, used by the Lua version, employs a different 248 | format than `~/.lispwords`. It ``return``s a 249 | Lua table, whose keys are strings corresponding to Lisp keywords, 250 | and whose values are their corresponding LINs. 251 | Keywords sharing the same LIN cannot be bunched. 252 | E.g., the example `.lispwords` above will be specified as follows 253 | in `.lispwords.lua`: 254 | 255 | return { 256 | ['begin0'] = 0, 257 | ['when'] = 1, 258 | ['unless'] = 1, 259 | ['do'] = 2, 260 | ['defun'] = 2, 261 | } 262 | 263 | == Customization (`lispwords.json`) 264 | 265 | `lispwords.json`, used by the JavaScript version, employs a different format 266 | than `.lispwords` in order to accommodate JSON. Keywords are 267 | specified as keys, the LINs as values, and 268 | keywords sharing the same LIN cannot be bunched. 269 | E.g., the example `.lispwords` of the previous section will 270 | be specified as follows in `lispwords.json`: 271 | 272 | { 273 | "begin0": 0, 274 | "when": 1, 275 | "unless": 1, 276 | "do": 2, 277 | "defun": 2 278 | } 279 | -------------------------------------------------------------------------------- /scmindent.lua: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env lua 2 | 3 | -- Dorai Sitaram 4 | -- Last modified 2020-11-14 5 | 6 | -- Find if this file is being run within Neovim Lua. 7 | 8 | local running_in_neovim = (vim and type(vim) == 'table' and 9 | vim.api and type(vim.api) == 'table' and 10 | vim.api.nvim_eval and type(vim.api.nvim_eval) == 'function') 11 | 12 | -- If running_in_neovim, this file defines a GetScmIndent() that can be used 13 | -- for 'indentexpr' to automatically indent Scheme/Lisp code in a Neovim 14 | -- buffer. If not, it is a script that takes lines of Lisp or Scheme code from 15 | -- its stdin and produces an indented version thereof on its stdout. 16 | 17 | function file_exists(f) 18 | local h = io.open(f) 19 | if h ~= nil then 20 | io.close(h); return true 21 | else 22 | return false 23 | end 24 | end 25 | 26 | local lwfile 27 | 28 | if running_in_neovim then 29 | lwfile = (os.getenv('NVIM_LISPWORDS') or os.getenv('LISPWORDS')) 30 | else 31 | lwfile = (os.getenv('LISPWORDS') or os.getenv('NVIM_LISPWORDS')) 32 | end 33 | 34 | lwfile = (lwfile or (os.getenv('HOME') .. '/.lispwords.lua')) 35 | 36 | local basic_lispwords = (file_exists(lwfile) and dofile(lwfile) or {}) 37 | 38 | function split_string(s, c) 39 | local r = {} 40 | if s then 41 | local start = 1 42 | local i 43 | while true do 44 | i = string.find(s, c, start, true) 45 | if not i then 46 | table.insert(r, string.sub(s, start, -1)) 47 | break 48 | end 49 | table.insert(r, string.sub(s, start, i-1)) 50 | start = i+1 51 | end 52 | end 53 | return r 54 | end 55 | 56 | local lispwords 57 | 58 | if not running_in_neovim then 59 | lispwords = basic_lispwords 60 | end 61 | 62 | function get_lw_option(buf) 63 | local function get_local_lw_option () 64 | return vim.api.nvim_buf_get_option(buf, 'lw') 65 | end 66 | local succeeded, lw = pcall(get_local_lw_option) 67 | if not succeeded then 68 | lw = vim.api.nvim_get_option('lw') 69 | end 70 | return lw 71 | end 72 | 73 | local prevailing_filetype 74 | 75 | function slurp_in_lw(buf) 76 | lispwords = {} 77 | for w,n in pairs(basic_lispwords) do 78 | lispwords[w] = n 79 | end 80 | local vimlw = split_string(get_lw_option(buf), ',') 81 | for _,w in ipairs(vimlw) do 82 | if not lispwords[w] then 83 | lispwords[w] = 0 84 | end 85 | end 86 | end 87 | 88 | function is_literal_token(s) 89 | return string.find(s, '^[0-9#]') 90 | end 91 | 92 | function get_lisp_indent_number(s) 93 | s = string.lower(s) 94 | local n = lispwords[s] 95 | if n then return n 96 | elseif string.find(s, '^def') then return 0 97 | else return -1 98 | end 99 | end 100 | 101 | function past_next_atom(s, i, n) 102 | -- gives the index in s that's just past the atom starting at s[i]. 103 | -- Result can't be more than n+1. 104 | -- If no atom found, result is i itself. 105 | while true do 106 | if i > n then return n+1 end 107 | local c = string.sub(s, i, i) 108 | if c == '\\' then 109 | i = i+2 110 | elseif string.find(c, '[][ \t()\'"`,;]') then 111 | return i 112 | else 113 | i = i+1 114 | end 115 | end 116 | end 117 | 118 | function calc_subindent(s, i, n) 119 | -- we're looking for a keyword directly after an lparen in s[i-1] 120 | local j = past_next_atom(s, i, n) 121 | -- j would be just after that keyword 122 | -- it's possible no keyword found, in which case j == i 123 | local delta_indent = 0 124 | local lisp_indent_num = -1 125 | if j == i then 126 | do end 127 | else 128 | local w = string.sub(s, i, j-1) -- the keyword itself 129 | local c2 130 | if i > 2 then 131 | local i2 = i-2; c2 = string.sub(s, i2, i2) 132 | -- c2 is the char before the lparen 133 | end 134 | if c2 == "'" or c2 == '`' or is_literal_token(w) then 135 | do end 136 | else 137 | lisp_indent_num = get_lisp_indent_number(w) 138 | if lisp_indent_num == -2 then 139 | do end 140 | elseif lisp_indent_num == -1 then 141 | if j < n then 142 | delta_indent = j - i + 1 143 | else 144 | delta_indent = 1 145 | end 146 | else 147 | delta_indent = 1 148 | end 149 | end 150 | end 151 | return delta_indent, lisp_indent_num, j 152 | end 153 | 154 | function num_leading_spaces(s) 155 | local n = #s 156 | local i = 1 157 | local j = 0 158 | while true do 159 | if i > n then return 0 end 160 | local c = string.sub(s, i, i) 161 | if c == ' ' then 162 | i = i+1; j = j+1 163 | elseif c == '\t' then 164 | i = i+1; j = j+8 165 | else 166 | return j 167 | end 168 | end 169 | end 170 | 171 | function do_indent(curr_buf, cnum, lnum) 172 | local default_left_i = -1 173 | local left_i = 0 174 | local paren_stack = {} 175 | local is_inside_string = false 176 | while true do 177 | local curr_line 178 | if running_in_neovim then 179 | if cnum > lnum then break end 180 | curr_line = vim.api.nvim_buf_get_lines(curr_buf, cnum, cnum+1, 1)[1] 181 | else 182 | curr_line = io.read() 183 | if not curr_line then break end 184 | end 185 | local leading_spaces = num_leading_spaces(curr_line) 186 | local curr_left_i 187 | -- 188 | if is_inside_string then 189 | curr_left_i = leading_spaces 190 | elseif #paren_stack == 0 then 191 | if left_i == 0 then 192 | if default_left_i == -1 then 193 | default_left_i = leading_spaces 194 | end 195 | left_i = default_left_i 196 | end 197 | curr_left_i = left_i 198 | else 199 | -- current line is inside a form. 200 | curr_left_i = paren_stack[1].spaces_before 201 | if paren_stack[1].num_finished_subforms < paren_stack[1].lisp_indent_num then 202 | --paren_stack[1].num_finished_subforms = paren_stack[1].num_finished_subforms + 1 203 | curr_left_i = curr_left_i + 2 204 | end 205 | end 206 | if running_in_neovim and cnum == lnum then 207 | return curr_left_i 208 | end 209 | curr_line = curr_line:gsub('^%s+', ''):gsub('%s+$', '') 210 | if not running_in_neovim then 211 | if curr_line ~= "" then 212 | io.write(string.rep(' ', curr_left_i), curr_line) 213 | end 214 | io.write('\n') 215 | end 216 | -- 217 | local n = #curr_line 218 | local is_escape = false 219 | local is_token_interstice = true 220 | local function incr_finished_subforms() 221 | if not is_token_interstice and not is_inside_string then 222 | if #paren_stack > 0 then 223 | paren_stack[1].num_finished_subforms = paren_stack[1].num_finished_subforms + 1 224 | end 225 | is_token_interstice = true 226 | end 227 | end 228 | local i = 1 229 | while i <= n do 230 | local c = curr_line:sub(i, i) 231 | if is_escape then 232 | is_escape = false; i = i+1 233 | elseif c == '\\' then 234 | is_token_interstice = false 235 | is_escape = true; i = i+1 236 | elseif is_inside_string then 237 | if c == '"' then 238 | is_inside_string = false 239 | incr_finished_subforms() 240 | end 241 | i = i+1 242 | elseif c == ';' then 243 | incr_finished_subforms() 244 | break 245 | elseif c == '"' then 246 | incr_finished_subforms() 247 | is_inside_string = true; 248 | is_token_interstice = false; 249 | i = i+1 250 | elseif c == ' ' or c == '\t' then 251 | incr_finished_subforms() 252 | i = i+1 253 | elseif c == '(' or c == '[' then 254 | incr_finished_subforms() 255 | local delta_indent, lisp_indent_num, j = calc_subindent(curr_line, i+1, n) 256 | table.insert(paren_stack, 1, { 257 | spaces_before = curr_left_i + i + delta_indent, 258 | lisp_indent_num = lisp_indent_num, 259 | num_finished_subforms = -1 260 | }) 261 | is_token_interstice = true 262 | i = i+1 263 | if j > i then 264 | i = j 265 | is_token_interstice = false 266 | end 267 | elseif string.find(c, '[])]') then 268 | if #paren_stack > 0 then 269 | table.remove(paren_stack, 1) 270 | is_token_interstice = false 271 | if #paren_stack == 0 then 272 | left_i = 0 273 | is_token_interstice = true 274 | else 275 | incr_finished_subforms() 276 | end 277 | end 278 | i = i+1 279 | else 280 | is_token_interstice = false; i = i+1 281 | end 282 | end 283 | incr_finished_subforms() 284 | if running_in_neovim then 285 | cnum = cnum+1 286 | end 287 | end 288 | end 289 | 290 | local scmindent = {} 291 | 292 | if running_in_neovim then 293 | scmindent.GetScmIndent = function(lnum1) 294 | local lnum = lnum1 - 1 -- convert to 0-based line number 295 | local curr_buf = vim.api.nvim_get_current_buf() 296 | local curr_filetype = vim.api.nvim_buf_get_option(buf, 'filetype') 297 | if curr_filetype ~= prevailing_filetype then 298 | prevailing_filetype = curr_filetype 299 | slurp_in_lw(curr_buf) 300 | end 301 | -- 302 | -- pnum is determined by going up until you cross two contiguous blank 303 | -- regions (if possible), then finding the first nonblank after that. 304 | -- 305 | local pnum = lnum - 1 306 | if pnum < 0 then pnum = 0 end 307 | local one_blank_seen = false 308 | local currently_blank = false 309 | while pnum > 0 do 310 | local pstr = vim.api.nvim_buf_get_lines(curr_buf, pnum, pnum+1, 1)[1] 311 | if pstr:match("^%s*$") then 312 | if currently_blank then 313 | do end 314 | elseif one_blank_seen then 315 | pnum = pnum + 1 316 | break 317 | else 318 | currently_blank = true 319 | one_blank_seen = true 320 | end 321 | else 322 | currently_blank = false 323 | end 324 | pnum = pnum - 1 325 | end 326 | -- 327 | return do_indent(curr_buf, pnum, lnum) 328 | end 329 | end 330 | 331 | if running_in_neovim then 332 | return scmindent 333 | else 334 | do_indent(false, 1, 1) 335 | end 336 | --------------------------------------------------------------------------------