clojure.vim 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. " Vim indent file
  2. " Language: Clojure
  3. " Maintainer: Alex Vear <alex@vear.uk>
  4. " Former Maintainers: Sung Pae <self@sungpae.com>
  5. " Meikel Brandmeyer <mb@kotka.de>
  6. " URL: https://github.com/clojure-vim/clojure.vim
  7. " License: Vim (see :h license)
  8. " Last Change: 2022-03-24
  9. if exists("b:did_indent")
  10. finish
  11. endif
  12. let b:did_indent = 1
  13. let s:save_cpo = &cpo
  14. set cpo&vim
  15. let b:undo_indent = 'setlocal autoindent< smartindent< expandtab< softtabstop< shiftwidth< indentexpr< indentkeys<'
  16. setlocal noautoindent nosmartindent
  17. setlocal softtabstop=2 shiftwidth=2 expandtab
  18. setlocal indentkeys=!,o,O
  19. if exists("*searchpairpos")
  20. if !exists('g:clojure_maxlines')
  21. let g:clojure_maxlines = 300
  22. endif
  23. if !exists('g:clojure_fuzzy_indent')
  24. let g:clojure_fuzzy_indent = 1
  25. endif
  26. if !exists('g:clojure_fuzzy_indent_patterns')
  27. let g:clojure_fuzzy_indent_patterns = ['^with', '^def', '^let']
  28. endif
  29. if !exists('g:clojure_fuzzy_indent_blacklist')
  30. let g:clojure_fuzzy_indent_blacklist = ['-fn$', '\v^with-%(meta|out-str|loading-context)$']
  31. endif
  32. if !exists('g:clojure_special_indent_words')
  33. let g:clojure_special_indent_words = 'deftype,defrecord,reify,proxy,extend-type,extend-protocol,letfn'
  34. endif
  35. if !exists('g:clojure_align_multiline_strings')
  36. let g:clojure_align_multiline_strings = 0
  37. endif
  38. if !exists('g:clojure_align_subforms')
  39. let g:clojure_align_subforms = 0
  40. endif
  41. function! s:syn_id_name()
  42. return synIDattr(synID(line("."), col("."), 0), "name")
  43. endfunction
  44. function! s:ignored_region()
  45. return s:syn_id_name() =~? '\vstring|regex|comment|character'
  46. endfunction
  47. function! s:current_char()
  48. return getline('.')[col('.')-1]
  49. endfunction
  50. function! s:current_word()
  51. return getline('.')[col('.')-1 : searchpos('\v>', 'n', line('.'))[1]-2]
  52. endfunction
  53. function! s:is_paren()
  54. return s:current_char() =~# '\v[\(\)\[\]\{\}]' && !s:ignored_region()
  55. endfunction
  56. " Returns 1 if string matches a pattern in 'patterns', which should be
  57. " a list of patterns.
  58. function! s:match_one(patterns, string)
  59. for pat in a:patterns
  60. if a:string =~# pat | return 1 | endif
  61. endfor
  62. endfunction
  63. function! s:match_pairs(open, close, stopat)
  64. " Stop only on vector and map [ resp. {. Ignore the ones in strings and
  65. " comments.
  66. if a:stopat == 0 && g:clojure_maxlines > 0
  67. let stopat = max([line(".") - g:clojure_maxlines, 0])
  68. else
  69. let stopat = a:stopat
  70. endif
  71. let pos = searchpairpos(a:open, '', a:close, 'bWn', "!s:is_paren()", stopat)
  72. return [pos[0], col(pos)]
  73. endfunction
  74. function! s:clojure_check_for_string_worker()
  75. " Check whether there is the last character of the previous line is
  76. " highlighted as a string. If so, we check whether it's a ". In this
  77. " case we have to check also the previous character. The " might be the
  78. " closing one. In case the we are still in the string, we search for the
  79. " opening ". If this is not found we take the indent of the line.
  80. let nb = prevnonblank(v:lnum - 1)
  81. if nb == 0
  82. return -1
  83. endif
  84. call cursor(nb, 0)
  85. call cursor(0, col("$") - 1)
  86. if s:syn_id_name() !~? "string"
  87. return -1
  88. endif
  89. " This will not work for a " in the first column...
  90. if s:current_char() == '"'
  91. call cursor(0, col("$") - 2)
  92. if s:syn_id_name() !~? "string"
  93. return -1
  94. endif
  95. if s:current_char() != '\'
  96. return -1
  97. endif
  98. call cursor(0, col("$") - 1)
  99. endif
  100. let p = searchpos('\(^\|[^\\]\)\zs"', 'bW')
  101. if p != [0, 0]
  102. return p[1] - 1
  103. endif
  104. return indent(".")
  105. endfunction
  106. function! s:check_for_string()
  107. let pos = getpos('.')
  108. try
  109. let val = s:clojure_check_for_string_worker()
  110. finally
  111. call setpos('.', pos)
  112. endtry
  113. return val
  114. endfunction
  115. function! s:strip_namespace_and_macro_chars(word)
  116. return substitute(a:word, "\\v%(.*/|[#'`~@^,]*)(.*)", '\1', '')
  117. endfunction
  118. function! s:clojure_is_method_special_case_worker(position)
  119. " Find the next enclosing form.
  120. call search('\S', 'Wb')
  121. " Special case: we are at a '(('.
  122. if s:current_char() == '('
  123. return 0
  124. endif
  125. call cursor(a:position)
  126. let next_paren = s:match_pairs('(', ')', 0)
  127. " Special case: we are now at toplevel.
  128. if next_paren == [0, 0]
  129. return 0
  130. endif
  131. call cursor(next_paren)
  132. call search('\S', 'W')
  133. let w = s:strip_namespace_and_macro_chars(s:current_word())
  134. if g:clojure_special_indent_words =~# '\V\<' . w . '\>'
  135. " `letfn` is a special-special-case.
  136. if w ==# 'letfn'
  137. " Earlier code left the cursor at:
  138. " (letfn [...] ...)
  139. " ^
  140. " Search and get coordinates of first `[`
  141. " (letfn [...] ...)
  142. " ^
  143. call search('\[', 'W')
  144. let pos = getcurpos()
  145. let letfn_bracket = [pos[1], pos[2]]
  146. " Move cursor to start of the form this function was
  147. " initially called on. Grab the coordinates of the
  148. " closest outer `[`.
  149. call cursor(a:position)
  150. let outer_bracket = s:match_pairs('\[', '\]', 0)
  151. " If the located square brackets are not the same,
  152. " don't use special-case formatting.
  153. if outer_bracket != letfn_bracket
  154. return 0
  155. endif
  156. endif
  157. return 1
  158. endif
  159. return 0
  160. endfunction
  161. function! s:is_method_special_case(position)
  162. let pos = getpos('.')
  163. try
  164. let val = s:clojure_is_method_special_case_worker(a:position)
  165. finally
  166. call setpos('.', pos)
  167. endtry
  168. return val
  169. endfunction
  170. " Check if form is a reader conditional, that is, it is prefixed by #?
  171. " or #?@
  172. function! s:is_reader_conditional_special_case(position)
  173. return getline(a:position[0])[a:position[1] - 3 : a:position[1] - 2] == "#?"
  174. \|| getline(a:position[0])[a:position[1] - 4 : a:position[1] - 2] == "#?@"
  175. endfunction
  176. " Returns 1 for opening brackets, -1 for _anything else_.
  177. function! s:bracket_type(char)
  178. return stridx('([{', a:char) > -1 ? 1 : -1
  179. endfunction
  180. " Returns: [opening-bracket-lnum, indent]
  181. function! s:clojure_indent_pos()
  182. " Get rid of special case.
  183. if line(".") == 1
  184. return [0, 0]
  185. endif
  186. " We have to apply some heuristics here to figure out, whether to use
  187. " normal lisp indenting or not.
  188. let i = s:check_for_string()
  189. if i > -1
  190. return [0, i + !!g:clojure_align_multiline_strings]
  191. endif
  192. call cursor(0, 1)
  193. " Find the next enclosing [ or {. We can limit the second search
  194. " to the line, where the [ was found. If no [ was there this is
  195. " zero and we search for an enclosing {.
  196. let paren = s:match_pairs('(', ')', 0)
  197. let bracket = s:match_pairs('\[', '\]', paren[0])
  198. let curly = s:match_pairs('{', '}', bracket[0])
  199. " In case the curly brace is on a line later then the [ or - in
  200. " case they are on the same line - in a higher column, we take the
  201. " curly indent.
  202. if curly[0] > bracket[0] || curly[1] > bracket[1]
  203. if curly[0] > paren[0] || curly[1] > paren[1]
  204. return curly
  205. endif
  206. endif
  207. " If the curly was not chosen, we take the bracket indent - if
  208. " there was one.
  209. if bracket[0] > paren[0] || bracket[1] > paren[1]
  210. return bracket
  211. endif
  212. " There are neither { nor [ nor (, ie. we are at the toplevel.
  213. if paren == [0, 0]
  214. return paren
  215. endif
  216. " Now we have to reimplement lispindent. This is surprisingly easy, as
  217. " soon as one has access to syntax items.
  218. "
  219. " - Check whether we are in a special position after a word in
  220. " g:clojure_special_indent_words. These are special cases.
  221. " - Get the next keyword after the (.
  222. " - If its first character is also a (, we have another sexp and align
  223. " one column to the right of the unmatched (.
  224. " - In case it is in lispwords, we indent the next line to the column of
  225. " the ( + sw.
  226. " - If not, we check whether it is last word in the line. In that case
  227. " we again use ( + sw for indent.
  228. " - In any other case we use the column of the end of the word + 2.
  229. call cursor(paren)
  230. if s:is_method_special_case(paren)
  231. return [paren[0], paren[1] + &shiftwidth - 1]
  232. endif
  233. if s:is_reader_conditional_special_case(paren)
  234. return paren
  235. endif
  236. " In case we are at the last character, we use the paren position.
  237. if col("$") - 1 == paren[1]
  238. return paren
  239. endif
  240. " In case after the paren is a whitespace, we search for the next word.
  241. call cursor(0, col('.') + 1)
  242. if s:current_char() == ' '
  243. call search('\v\S', 'W')
  244. endif
  245. " If we moved to another line, there is no word after the (. We
  246. " use the ( position for indent.
  247. if line(".") > paren[0]
  248. return paren
  249. endif
  250. " We still have to check, whether the keyword starts with a (, [ or {.
  251. " In that case we use the ( position for indent.
  252. let w = s:current_word()
  253. if s:bracket_type(w[0]) == 1
  254. return paren
  255. endif
  256. " If the keyword begins with #, check if it is an anonymous
  257. " function or set, in which case we indent by the shiftwidth
  258. " (minus one if g:clojure_align_subforms = 1), or if it is
  259. " ignored, in which case we use the ( position for indent.
  260. if w[0] == "#"
  261. " TODO: Handle #=() and other rare reader invocations?
  262. if w[1] == '(' || w[1] == '{'
  263. return [paren[0], paren[1] + (g:clojure_align_subforms ? 0 : &shiftwidth - 1)]
  264. elseif w[1] == '_'
  265. return paren
  266. endif
  267. endif
  268. " Test words without namespace qualifiers and leading reader macro
  269. " metacharacters.
  270. "
  271. " e.g. clojure.core/defn and #'defn should both indent like defn.
  272. let ww = s:strip_namespace_and_macro_chars(w)
  273. if &lispwords =~# '\V\<' . ww . '\>'
  274. return [paren[0], paren[1] + &shiftwidth - 1]
  275. endif
  276. if g:clojure_fuzzy_indent
  277. \ && !s:match_one(g:clojure_fuzzy_indent_blacklist, ww)
  278. \ && s:match_one(g:clojure_fuzzy_indent_patterns, ww)
  279. return [paren[0], paren[1] + &shiftwidth - 1]
  280. endif
  281. call search('\v\_s', 'cW')
  282. call search('\v\S', 'W')
  283. if paren[0] < line(".")
  284. return [paren[0], paren[1] + (g:clojure_align_subforms ? 0 : &shiftwidth - 1)]
  285. endif
  286. call search('\v\S', 'bW')
  287. return [line('.'), col('.') + 1]
  288. endfunction
  289. function! GetClojureIndent()
  290. let lnum = line('.')
  291. let orig_lnum = lnum
  292. let orig_col = col('.')
  293. let [opening_lnum, indent] = s:clojure_indent_pos()
  294. " Account for multibyte characters
  295. if opening_lnum > 0
  296. let indent -= indent - virtcol([opening_lnum, indent])
  297. endif
  298. " Return if there are no previous lines to inherit from
  299. if opening_lnum < 1 || opening_lnum >= lnum - 1
  300. call cursor(orig_lnum, orig_col)
  301. return indent
  302. endif
  303. let bracket_count = 0
  304. " Take the indent of the first previous non-white line that is
  305. " at the same sexp level. cf. src/misc1.c:get_lisp_indent()
  306. while 1
  307. let lnum = prevnonblank(lnum - 1)
  308. let col = 1
  309. if lnum <= opening_lnum
  310. break
  311. endif
  312. call cursor(lnum, col)
  313. " Handle bracket counting edge case
  314. if s:is_paren()
  315. let bracket_count += s:bracket_type(s:current_char())
  316. endif
  317. while 1
  318. if search('\v[(\[{}\])]', '', lnum) < 1
  319. break
  320. elseif !s:ignored_region()
  321. let bracket_count += s:bracket_type(s:current_char())
  322. endif
  323. endwhile
  324. if bracket_count == 0
  325. " Check if this is part of a multiline string
  326. call cursor(lnum, 1)
  327. if s:syn_id_name() !~? '\vstring|regex'
  328. call cursor(orig_lnum, orig_col)
  329. return indent(lnum)
  330. endif
  331. endif
  332. endwhile
  333. call cursor(orig_lnum, orig_col)
  334. return indent
  335. endfunction
  336. setlocal indentexpr=GetClojureIndent()
  337. else
  338. " In case we have searchpairpos not available we fall back to
  339. " normal lisp indenting.
  340. setlocal indentexpr=
  341. setlocal lisp
  342. let b:undo_indent .= '| setlocal lisp<'
  343. endif
  344. let &cpo = s:save_cpo
  345. unlet! s:save_cpo
  346. " vim:sts=8:sw=8:ts=8:noet