guns / vim-clojure-static

Meikel Brandmeyer's excellent Clojure runtime files
Other
420 stars 50 forks source link

Indenting after lonely parens/brackets #47

Closed brandonbloom closed 10 years ago

brandonbloom commented 10 years ago

Sometimes, I want to write code that looks like this:

(foo bar [

  some
  long

  list
  of
  stuff

])

It's bad style for small cases, but much nicer for long lists, particularly with extra whitespace and whatnot.

Right now, if you press <cr> after a naked (, [, or {, you get something like this:

(foo bar [
          |

Where the | stands for the cursor. Worse, if you forcibly outdent, type some, then press <cr> again, you get this:

(foo bar [
  {:some "kinda" :thing "here"}
          |

Ideally, pressing return after naked braces would indent 2 spaces. Alternatively, the indenting should prefer to align with the previous line, not the most recent open-bracket.

guns commented 10 years ago

Hi Brandon,

I definitely understand your desire here. Lisp indenting for literal data sometimes feels too right-indented after writing some JavaScript literals and the like.

OTOH, the standard convention of "subforms are never indented behind the opening bracket" is the one rule that keeps Lisp readable without having to move to full K&R brace style:

(defn example []
  (foo {
    :alpha (bar {
      (baz {:alpha (quux '[
        one {:beta abc
             :gamma (zab [
               rab
               oof])} three])})})}))

;; At a glance: who is the grandparent of (zab)?

This contrived example is a terrible mixture of Lisp and C conventions that totally obscures the tree structure of the code. Rewriting this in either Lisp or C brace style would be better, but mixing them is sinful.

The indenting script offers two options that can help with deeply nested forms: lispwords and g:clojure_fuzzy_indent_patterns. For instance, if we add foo to lispwords, your examples could look like¹:

(foo bar
  [some
   long

   list
   of
   stuff
   █
   ])

(foo bar
  [{:some "kinda" :thing "here"}
   █
   ])

This looks acceptable to me, so I think what we might lack here is a quick way to add symbols to lispwords on the fly:¹

function! ToggleLispwords(word)
    " Strip leading namespace qualifiers and macro characters from symbol
    let word = substitute(a:word, "\\v%(.*/|[#'`~@^,]*)(.*)", '\1', '')

    if &lispwords =~# '\V\<' . word . '\>'
        execute 'setlocal lispwords-=' . word
        echo "Removed " . word . " from lispwords."
    else
        execute 'setlocal lispwords+=' . word
        echo "Added " . word . " to lispwords."
    endif
endfunction

nnoremap <Leader>l :<C-U>call ToggleLispwords(expand('<cword>'))<CR>

Now I can place my cursor on any symbol that I'd like to always two-space indent and use <Leader>l to add it to lispwords. Of course, if you find that you always want this symbol indented like this, you would add this to your vimrc to make it persist.

My personal philosophy on developer tools is that they should be maximally flexible (unlike, say, client software), but I also think they should teach users good habits. This is especially true when the habit to be learned is Lisp indenting.

Hope I've been persuasive, but if not, I am always open to discussion. :)

¹ Indenting compound forms in this way is really intended for "special" functions and macros, but eh, it's our code and we can do what we like.

EDIT: Fixed the vim snippet

brandonbloom commented 10 years ago

Thanks for the detailed response. Please allow me to clarify my position in light of your perspective.

Ideally, pressing return after naked braces would indent 2 spaces.

I've already got good habits, so I wouldn't press return there unless I wanted a long flat list like I described above. I understand your position regarding teaching good habits, so I rescind this first half of my request. However...

Alternatively, the indenting should prefer to align with the previous line, not the most recent open-bracket.

What I mean by this is that if I forcibly outdent, the indenter should respect that on the next line break. My last example is clearly wrong indenting. When faced with such a case, which would be an abomination anywhere outside of the long data-like forms I'm interested in here, it would be great of the indented didn't fight me.

Does that make sense?

guns commented 10 years ago

When faced with such a case, which would be an abomination anywhere outside of the long data-like forms I'm interested in here, it would be great of the indented didn't fight me.

While I happen to like the current behavior very much, I have definitely been on the other side of this very same fence in another language (it was the infuriatingly inflexible behavior of the Ruby indenter WRT indenting var = if…else…end, fixed in the past year).

So okay, I agree that this is a good idea. This would add flexibility without impacting my own workflow or encouraging bad habits. The builtin lisp indenter also behaves like this, so that's another argument in your favor.

Please note that using ={motion} will still re-indent forms to the conventional format, since there is little value to an indent system if it cannot tidy existing lines. The builtin lisp indenter also behaves this way.

For now, you can accomplish this same effect by temporarily disabling the indentexpr and switching on autoindent:

" Toggle between simple indenting and Clojure indenting
if &autoindent
    setlocal noautoindent indentexpr=GetClojureIndent()
else
    setlocal autoindent indentexpr=
end

I understand your position regarding teaching good habits, so I rescind this first half of my request.

I can just imagine a Vim -> Emacs convert opening this ticket on clojure-mode a year from now:

"Issue #1000: Indent lines after a lone brace like the Clojure filetype in Vim"

Then surely:

  1. The Emacs cabal posts this issue to Hacker News
  2. Lisp weenies worldwide snicker derisively
  3. I am never allowed to touch an indent script again

Hyperbole?

guns commented 10 years ago

Okay, I've made the change! This was a good opportunity to start an indent test suite, so I made a test case for this behavior. I hope to expand these tests, but I would really appreciate your feedback and testing.

brandonbloom commented 10 years ago

Works wonderfully. Thanks!

On Fri, Apr 4, 2014 at 4:13 PM, Sung Pae notifications@github.com wrote:

Okay, I've made the change! This was a good opportunity to start an indent test suite, so I made a test case for this behavior. I hope to expand these tests, but I would really appreciate your feedback and testing.

Reply to this email directly or view it on GitHubhttps://github.com/guns/vim-clojure-static/issues/47#issuecomment-39606557 .

guns commented 10 years ago

Thank you for not giving up too easily. This is a great addition.