neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

clojure.vim (11557B)


      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 
     10 if exists("b:did_indent")
     11 finish
     12 endif
     13 let b:did_indent = 1
     14 
     15 let s:save_cpo = &cpo
     16 set cpo&vim
     17 
     18 let b:undo_indent = 'setlocal autoindent< smartindent< expandtab< softtabstop< shiftwidth< indentexpr< indentkeys<'
     19 
     20 setlocal noautoindent nosmartindent
     21 setlocal softtabstop=2 shiftwidth=2 expandtab
     22 setlocal indentkeys=!,o,O
     23 
     24 if 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 
    414 else
    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 
    422 endif
    423 
    424 let &cpo = s:save_cpo
    425 unlet! s:save_cpo
    426 
    427 " vim:sts=8:sw=8:ts=8:noet