99e032674ddd

Indent common lisp correctly
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Sun, 21 Feb 2016 16:37:29 +0000
parents 74729ab9307b
children 9062792cfda3
branches/tags (none)
files bin/lispindent vim/indent/lisp.vim vim/vimrc

Changes

--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/lispindent	Sun Feb 21 16:37:29 2016 +0000
@@ -0,0 +1,190 @@
+":";if test -z "$LISP"; then
+":";  if test "$USER" = evalwhen; then LISP=ecl
+":";  elif test "$(arch 2>/dev/null)" = ppc; then LISP=clozure
+":";  else LISP=sbcl
+":";  fi; fi
+":";if test "$LISP" = clisp; then exec clisp -q $0
+":";elif test "$LISP" = clozure; then exec ccl -b -Q -l $0
+":";elif test "$LISP" = ecl; then exec ecl -shell $0
+":";elif test "$LISP" = sbcl; then exec sbcl --script $0
+":";fi
+
+;Dorai Sitaram
+;Oct 8, 1999
+;last change 2014-09-16
+
+;this script takes lines of Lisp or Scheme code from its
+;stdin and produces an indented version thereof on its
+;stdout
+
+(defvar *lisp-keywords* '())
+
+(defun define-with-lisp-indent-number (n syms)
+  (dolist (sym syms)
+    (let* ((x (symbol-name sym))
+           (c (assoc x *lisp-keywords* :test #'string-equal)))
+      (unless c
+        (push (setq c (cons x nil)) *lisp-keywords*))
+      (setf (cdr c) n))))
+
+(define-with-lisp-indent-number 0
+  '(block
+    handler-bind
+    loop))
+
+(define-with-lisp-indent-number 1
+  '(case
+    defpackage do-all-symbols do-external-symbols dolist do-symbols dotimes
+    ecase etypecase eval-when
+    flet
+    handler-case
+    labels lambda let let* let-values
+    macrolet
+    prog1
+    typecase
+    unless unwind-protect
+    when with-input-from-string with-open-file with-open-socket
+    with-open-stream with-output-to-string))
+
+(define-with-lisp-indent-number 2
+  '(assert
+    defun destructuring-bind do do*
+    if
+    multiple-value-bind
+    with-slots))
+
+(with-open-file (i (merge-pathnames ".lispwords" (user-homedir-pathname))
+                   :if-does-not-exist nil)
+  (when i
+    (loop
+      (let ((w (or (read i nil) (return))))
+        (define-with-lisp-indent-number (car w) (cdr w))))))
+
+(defun past-next-token (s i n)
+  (let ((escapep nil))
+    (loop
+      (when (>= i n) (return i))
+      (let ((c (char s i)))
+        (cond (escapep (setq escapep nil))
+              ((char= c #\\) (setq escapep t))
+              ((char= c #\#)
+               (let ((j (+ i 1)))
+                 (if (>= j n) (return i)
+                   (let ((c (char s j)))
+                     (cond ((char= c #\\) (setq escapep t i j))
+                           (t (return i)))))))
+              ((member c '(#\space #\tab #\( #\) #\[ #\] #\" #\' #\` #\, #\;))
+               (return i))))
+      (incf i))))
+
+(defun lisp-indent-number (s &optional (possible-keyword-p t))
+  (or (cdr (assoc s *lisp-keywords* :test #'string-equal))
+      (if (zerop (or (search "def" s :test #'char-equal) -1))
+          0
+        (if possible-keyword-p
+            (let ((p (position #\: s :from-end t)))
+              (if p
+                  (lisp-indent-number (subseq s (1+ p)) nil)
+                -1))
+          -1))))
+
+(defun literal-token-p (s)
+  (let ((colon-pos (position #\: s)))
+    (if colon-pos
+        (if (= colon-pos 0) t nil)
+      (let ((s (read-from-string s)))
+        (or (characterp s) (numberp s) (stringp s))))))
+
+;(trace lisp-indent-number literal-token-p read-from-string past-next-token)
+
+(defstruct lparen
+  spaces-before
+  num-aligned-subforms
+  (num-finished-subforms 0))
+
+(defun calc-subindent (s i n)
+  (let* ((j (past-next-token s i n))
+         (num-aligned-subforms 0)
+         (left-indent
+          (if (= j i) 1
+            (let ((w (subseq s i j)))
+              (if (and (>= i 2) (member (char s (- i 2)) '(#\' #\`))) 1
+                (let ((nas (lisp-indent-number w)))
+                  (cond ((>= nas 0) (setq num-aligned-subforms nas)
+                         2)
+                        ((literal-token-p w) 1)
+                        ((= j n) 1)
+                        (t (+ (- j i) 2)))))))))
+    (values left-indent num-aligned-subforms (1- j))))
+
+(defun num-leading-spaces (s)
+  (let ((n (length s))
+        (i 0) (j 0))
+    (loop
+      (when (>= i n) (return 0))
+      (case (char s i)
+        (#\space (incf i) (incf j))
+        (#\tab (incf i) (incf j 8))
+        (t (return j))))))
+
+(defun string-trim-blanks (s)
+  (string-trim '(#\space #\tab #\newline #\return) s))
+
+(defun indent-lines ()
+  (let ((left-i 0) (paren-stack '()) (stringp nil))
+    (loop
+      (let* ((curr-line (or (read-line nil nil) (return)))
+             (leading-spaces (num-leading-spaces curr-line))
+             (curr-left-i
+              (cond (stringp leading-spaces)
+                    ((null paren-stack)
+                     (when (= left-i 0) (setq left-i leading-spaces))
+                     left-i)
+                    (t (let* ((lp (car paren-stack))
+                              (nas (lparen-num-aligned-subforms lp))
+                              (nfs (lparen-num-finished-subforms lp))
+                              (extra-w 0))
+                         (when (< nfs nas) ;(and (>= nas 0) (< nfs nas))
+                           (incf (lparen-num-finished-subforms lp))
+                           (setq extra-w 2))
+                         (+ (lparen-spaces-before lp)
+                            extra-w))))))
+        (setq curr-line (string-trim-blanks curr-line))
+        (dotimes (k curr-left-i) (write-char #\space))
+        (princ curr-line) (terpri)
+        ;
+        (let ((i 0) (n (length curr-line)) (escapep nil)
+              (inter-word-space-p nil))
+          (loop
+            (when (>= i n) (return))
+            (let ((c (char curr-line i)))
+              (cond (escapep (setq escapep nil))
+                    ((char= c #\\) (setq escapep t))
+                    (stringp (when (char= c #\") (setq stringp nil)))
+                    ((char= c #\;) (return))
+                    ((char= c #\") (setq stringp t))
+                    ((member c '(#\space #\tab) :test #'char=)
+                     (unless inter-word-space-p
+                       (setq inter-word-space-p t)
+                       (let ((lp (car paren-stack)))
+                         (when lp
+                           (incf (lparen-num-finished-subforms lp))))))
+                    ((member c '(#\( #\[) :test #'char=)
+                     (setq inter-word-space-p nil)
+                     (multiple-value-bind (left-indent num-aligned-subforms j)
+                         (calc-subindent curr-line (1+ i) n)
+                       (push
+                        (make-lparen :spaces-before (+ i curr-left-i left-indent)
+                                     :num-aligned-subforms num-aligned-subforms)
+                        paren-stack)
+                       (setq i j)))
+                    ((member c '(#\) #\]) :test #'char=)
+                     (setq inter-word-space-p nil)
+                     (cond (paren-stack (pop paren-stack))
+                           (t (setq left-i 0))))
+                    (t (setq inter-word-space-p nil)))
+              (incf i))))))))
+
+(indent-lines)
+
+;eof
--- a/vim/indent/lisp.vim	Sat Feb 20 13:18:08 2016 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,374 +0,0 @@
-" lisp.vim:
-"               Lisp indent plugin for Slimv
-" Version:      0.9.5
-" Last Change:  21 Feb 2012
-" Maintainer:   Tamas Kovacs <kovisoft at gmail dot com>
-" License:      This file is placed in the public domain.
-"               No warranty, express or implied.
-"               *** ***   Use At-Your-Own-Risk!   *** ***
-"
-" =====================================================================
-"
-"  Load Once:
-if exists("b:did_indent")
-   finish
-endif
-
-" Handle cases when lisp dialects explicitly use the lisp indent plugins
-if &ft == "clojure" && exists("g:slimv_disable_clojure")
-    finish
-endif
-
-if &ft == "scheme" && exists("g:slimv_disable_scheme")
-    finish
-endif 
-
-
-" Maximum number of lines searched backwards for indenting special forms
-if !exists( 'g:slimv_indent_maxlines' )
-    let g:slimv_indent_maxlines = 50
-endif
-
-" Special indentation for keyword lists
-if !exists( 'g:slimv_indent_keylists' )
-    let g:slimv_indent_keylists = 1
-endif
-
-let s:skip_sc = 'synIDattr(synID(line("."), col("."), 0), "name") =~ "[Ss]tring\\|[Cc]omment"'
-                                                          " Skip matches inside string or comment 
-let s:skip_q = 'getline(".")[col(".")-2] == "\\"'         " Skip escaped double quote characters in matches
-
-let s:indent = ''                                         " Most recent indentation info
-let s:spec_indent = 'flet\|labels\|macrolet\|symbol-macrolet'
-                                                          " List of symbols need special indenting
-let s:spec_param = 'defmacro'                             " List of symbols with special parameter list
-
-let s:binding_form = 'let\|let\*'                         " List of symbols with binding list
-
-" Get the filetype (Lisp dialect) used by Slimv
-function! SlimvGetFiletype()
-    if &ft != ''
-        " Return Vim filetype if defined
-        return &ft
-    endif
-
-    if match( tolower( g:slimv_lisp ), 'clojure' ) >= 0 || match( tolower( g:slimv_lisp ), 'clj' ) >= 0
-        " Must be Clojure
-        return 'clojure'
-    endif
-
-    " We have no clue, guess its lisp
-    return 'lisp'
-endfunction
-
-" Some multi-byte characters screw up the built-in lispindent()
-" This function is a wrapper that tries to fix it
-" TODO: implement custom indent procedure and omit lispindent()
-function! SlimvLispindent( lnum )
-    set lisp
-    let li = lispindent( a:lnum )
-    set nolisp
-    let backline = max([a:lnum-g:slimv_indent_maxlines, 1])
-    let oldpos = getpos( '.' )
-    call cursor( oldpos[1], 1 )
-    " Find containing form
-    let [lhead, chead] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
-    if lhead == 0
-        " No containing form, lispindent() is OK
-        call cursor( oldpos[1], oldpos[2] )
-        return li
-    endif
-    " Find outer form
-    let [lparent, cparent] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
-    call cursor( oldpos[1], oldpos[2] )
-    if lparent == 0 || lhead != lparent
-        " No outer form or starting above inner form, lispindent() is OK
-        return li
-    endif
-    " Count extra bytes before the function header
-    let header = strpart( getline( lparent ), 0 )
-    let total_extra = 0
-    let extra = 0
-    let c = 0
-    while a:lnum > 0 && c < chead-1
-        let bytes = byteidx( header, c+1 ) - byteidx( header, c )
-        if bytes > 1
-            let total_extra = total_extra + bytes - 1
-            if c >= cparent && extra < 10
-                " Extra bytes in the outer function header
-                let extra = extra + bytes - 1
-            endif
-        endif
-        let c = c + 1
-    endwhile
-    if total_extra == 0  
-        " No multi-byte character, lispindent() is OK
-        return li
-    endif
-    " In some cases ending spaces add up to lispindent() if there are multi-byte characters
-    let ending_sp = len( matchstr( getline( lparent ), ' *$' ) )
-    " Determine how wrong lispindent() is based on the number of extra bytes
-    " These values were determined empirically
-    if lparent == a:lnum - 1
-        " Function header is in the previous line
-        if extra == 0 && total_extra > 1
-            let ending_sp = ending_sp + 1
-        endif
-        return li + [0, 1, 0, -3, -3, -3, -5, -5, -7, -7, -8][extra] - ending_sp
-    else
-        " Function header is in an upper line
-        if extra == 0 || total_extra == extra
-            let ending_sp = 0
-        endif
-        return li + [0, 1, 0, -2, -2, -3, -3, -3, -3, -3, -3][extra] - ending_sp
-    endif
-endfunction
-
-" Return Lisp source code indentation at the given line
-function! SlimvIndent( lnum )
-    if &autoindent == 0 || a:lnum <= 1
-        " Start of the file
-        return 0
-    endif
-    let pnum = prevnonblank(a:lnum - 1)
-    if pnum == 0
-        " Hit the start of the file, use zero indent.
-        return 0
-    endif
-    let oldpos = getpos( '.' )
-    let linenum = a:lnum
-
-    " Handle multi-line string
-    let plen = len( getline( pnum ) )
-    if synIDattr( synID( pnum, plen, 0), 'name' ) =~ '[Ss]tring' && getline(pnum)[plen-1] != '"'
-        " Previous non-blank line ends with an unclosed string, so this is a multi-line string
-        let [l, c] = searchpairpos( '"', '', '"', 'bnW', s:skip_q )
-        if l == pnum && c > 0
-            " Indent to the opening double quote (if found)
-            return c
-        else
-            return SlimvLispindent( linenum )
-        endif
-    endif
-    if synIDattr( synID( pnum, 1, 0), 'name' ) =~ '[Ss]tring' && getline(pnum)[0] != '"'
-        " Previous non-blank line is the last line of a multi-line string
-        call cursor( pnum, 1 )
-        " First find the end of the multi-line string (omit \" characters)
-        let [lend, cend] = searchpos( '[^\\]"', 'nW' )
-        if lend > 0 && strpart(getline(lend), cend+1) =~ '(\|)\|\[\|\]\|{\|}'
-            " Structural change after the string, no special handling
-        else
-            " Find the start of the multi-line string (omit \" characters)
-            let [l, c] = searchpairpos( '"', '', '"', 'bnW', s:skip_q )
-            if l > 0 && strpart(getline(l), 0, c-1) =~ '^\s*$'
-                " Nothing else before the string: indent to the opening "
-                return c - 1
-            endif
-            if l > 0
-                " Pretend that we are really after the first line of the multi-line string
-                let pnum = l
-                let linenum = l + 1
-            endif
-        endif
-        call cursor( oldpos[1], oldpos[2] )
-    endif
-
-    " Handle special indentation style for flet, labels, etc.
-    " When searching for containing forms, don't go back
-    " more than g:slimv_indent_maxlines lines.
-    let backline = max([pnum-g:slimv_indent_maxlines, 1])
-    let indent_keylists = g:slimv_indent_keylists
-
-    " Check if the previous line actually ends with a multi-line subform
-    let parent = pnum
-    let [l, c] = searchpos( ')', 'bW' )
-    if l == pnum
-        let [l, c] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
-        if l > 0
-            " Make sure it is not a top level form and the containing form starts in the same line
-            let [l2, c2] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
-            if l2 == l
-                " Remember the first line of the multi-line form
-                let parent = l
-            endif
-        endif
-    endif
-
-    " Find beginning of the innermost containing form
-    call cursor( oldpos[1], 1 )
-    let [l, c] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
-    if l > 0
-        if SlimvGetFiletype() =~ '.*\(clojure\|scheme\|racket\).*'
-            " Is this a clojure form with [] binding list?
-            call cursor( oldpos[1], oldpos[2] )
-            let [lb, cb] = searchpairpos( '\[', '', '\]', 'bW', s:skip_sc, backline )
-            if lb >= l && (lb > l || cb > c)
-                return cb
-            endif
-        endif
-        " Is this a form with special indentation?
-        let line = strpart( getline(l), c-1 )
-        if match( line, '\c^(\s*\('.s:spec_indent.'\)\>' ) >= 0
-            " Search for the binding list and jump to its end
-            if search( '(' ) > 0
-                call searchpair( '(', '', ')', '', s:skip_sc )
-                if line('.') == pnum
-                    " We are indenting the first line after the end of the binding list
-                    return c + 1
-                endif
-            endif
-        elseif l == pnum
-            " If the containing form starts above this line then find the
-            " second outer containing form (possible start of the binding list)
-            let [l2, c2] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
-            if l2 > 0
-                let line2 = strpart( getline(l2), c2-1 )
-                if match( line2, '\c^(\s*\('.s:spec_param.'\)\>' ) >= 0
-                    if search( '(' ) > 0
-                        if line('.') == l && col('.') == c
-                            " This is the parameter list of a special form
-                            return c
-                        endif
-                    endif
-                endif
-                if SlimvGetFiletype() !~ '.*clojure.*'
-                    if l2 == l && match( line2, '\c^(\s*\('.s:binding_form.'\)\>' ) >= 0
-                        " Is this a lisp form with binding list?
-                        return c
-                    endif
-                    if match( line2, '\c^(\s*cond\>' ) >= 0 && match( line, '\c^(\s*t\>' ) >= 0
-                        " Is this the 't' case for a 'cond' form?
-                        return c
-                    endif
-                    if match( line2, '\c^(\s*defpackage\>' ) >= 0
-                        let indent_keylists = 0
-                    endif
-                endif
-                " Go one level higher and check if we reached a special form
-                let [l3, c3] = searchpairpos( '(', '', ')', 'bW', s:skip_sc, backline )
-                if l3 > 0
-                    " Is this a form with special indentation?
-                    let line3 = strpart( getline(l3), c3-1 )
-                    if match( line3, '\c^(\s*\('.s:spec_indent.'\)\>' ) >= 0
-                        " This is the first body-line of a binding
-                        return c + 1
-                    endif
-                    if match( line3, '\c^(\s*defsystem\>' ) >= 0
-                        let indent_keylists = 0
-                    endif
-                    " Finally go to the topmost level to check for some forms with special keyword indenting
-                    let [l4, c4] = searchpairpos( '(', '', ')', 'brW', s:skip_sc, backline )
-                    if l4 > 0
-                        let line4 = strpart( getline(l4), c4-1 )
-                        if match( line4, '\c^(\s*defsystem\>' ) >= 0
-                            let indent_keylists = 0
-                        endif
-                    endif
-                endif
-            endif
-        endif
-    endif
-
-    " Restore all cursor movements
-    call cursor( oldpos[1], oldpos[2] )
-
-    " Check if the current form started in the previous nonblank line
-    if l == parent
-        " Found opening paren in the previous line
-        let line = getline(l)
-        let form = strpart( line, c )
-        " Determine the length of the function part up to the 1st argument
-        let funclen = matchend( form, '\s*\S*\s*' ) + 1
-        " Contract strings, remove comments
-        let form = substitute( form, '".\{-}[^\\]"', '""', 'g' )
-        let form = substitute( form, ';.*$', '', 'g' )
-        " Contract subforms by replacing them with a single character
-        let f = ''
-        while form != f
-            let f = form
-            let form = substitute( form, '([^()]*)',     '0', 'g' )
-            let form = substitute( form, '([^()]*$',     '0', 'g' )
-            let form = substitute( form, '\[[^\[\]]*\]', '0', 'g' )
-            let form = substitute( form, '\[[^\[\]]*$',  '0', 'g' )
-            let form = substitute( form, '{[^{}]*}',     '0', 'g' )
-            let form = substitute( form, '{[^{}]*$',     '0', 'g' )
-        endwhile
-        " Find out the function name
-        let func = matchstr( form, '\<\k*\>' )
-        " If it's a keyword, keep the indentation straight
-        if indent_keylists && strpart(func, 0, 1) == ':'
-            if form =~ '^:\S*\s\+\S'
-                " This keyword has an associated value in the same line
-                return c
-            else
-                " The keyword stands alone in its line with no associated value
-                return c + 1
-            endif
-        endif
-        if SlimvGetFiletype() =~ '.*clojure.*'
-            " Fix clojure specific indentation issues not handled by the default lisp.vim
-            if match( func, 'defn$' ) >= 0
-                return c + 1
-            endif
-        else
-            if match( func, 'defgeneric$' ) >= 0 || match( func, 'defsystem$' ) >= 0 || match( func, 'aif$' ) >= 0
-                return c + 1
-            endif
-
-            if match( func, 'define-' ) >= 0
-                return c + 1
-            endif
-        endif
-        " Remove package specification
-        let func = substitute(func, '^.*:', '', '')
-        if func != '' && s:swank_connected
-            " Look how many arguments are on the same line
-            " If an argument is actually a multi-line subform, then replace it with a single character
-            let form = substitute( form, "([^()]*$", '0', 'g' )
-            let form = substitute( form, "[()\\[\\]{}#'`,]", '', 'g' )
-            let args_here = len( split( form ) ) - 1
-            " Get swank indent info
-            let s:indent = ''
-            silent execute 'python get_indent_info("' . func . '")'
-            if s:indent != '' && s:indent == args_here
-                " The next one is an &body argument, so indent by 2 spaces from the opening '('
-                return c + 1
-            endif
-            let llen = len( line )
-            if synIDattr( synID( l, llen, 0), 'name' ) =~ '[Ss]tring' && line[llen-1] != '"'
-                " Parent line ends with a multi-line string
-                " lispindent() fails to handle it correctly
-                if s:indent == '' && args_here > 0
-                    " No &body argument, ignore lispindent() and indent to the 1st argument
-                    return c + funclen - 1
-                endif
-            endif
-        endif
-    endif
-
-    " Use default Lisp indenting
-    let li = SlimvLispindent(linenum)
-    let line = strpart( getline(linenum-1), li-1 )
-    let gap = matchend( line, '^(\s\+\S' )
-    if gap >= 0
-        " Align to the gap between the opening paren and the first atom
-        return li + gap - 2
-    endif
-    return li
-endfunction 
-
-" Convert indent value to spaces or a mix of tabs and spaces
-" depending on the value of 'expandtab'
-function! s:MakeIndent( indent )
-    if &expandtab
-        return repeat( ' ', a:indent )
-    else
-        return repeat( "\<Tab>", a:indent / &tabstop ) . repeat( ' ', a:indent % &tabstop )
-    endif
-endfunction
-
-
-setlocal nolisp
-setlocal autoindent
-setlocal expandtab
-setlocal indentexpr=SlimvIndent(v:lnum)
--- a/vim/vimrc	Sat Feb 20 13:18:08 2016 +0000
+++ b/vim/vimrc	Sun Feb 21 16:37:29 2016 +0000
@@ -1022,9 +1022,12 @@
     " ))
 
     " Indent top-level form.
-    au FileType lisp nmap <buffer> <localleader>= mz:silent normal99[(<cr>v%='z
+    " au FileType lisp nmap <buffer> <localleader>= mz:silent normal99[(<cr>v%='z
+    au FileType lisp nmap <buffer> gi mz:silent normal99[(<cr>v%='z
     " ])
 
+    au FileType lisp setlocal equalprg=LISP=sbcl\ lispindent
+
     " s/it/happening/
     au FileType lisp silent! call OozeMapKeys()
 augroup END