clojure.vim 10 KB

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