xref: /vim-8.2.3635/runtime/indent/clojure.vim (revision 113cb513)
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