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