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